4-23
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
export class BaseConfig {
|
export class BaseConfig {
|
||||||
protected static servesUrl: string = "http://192.168.0.142:5298";
|
// protected static servesUrl: string = "http://192.168.0.142:5298";
|
||||||
protected static imgUrl: string = "http://192.168.0.142:5298";
|
// protected static imgUrl: string = "http://192.168.0.142:5298";
|
||||||
protected static mediaUrl: string = "http://192.168.0.142:5298/";
|
// protected static mediaUrl: string = "http://192.168.0.142:5298/";
|
||||||
|
|
||||||
// protected static servesUrl: string = "http://vp.xypays.cn";
|
|
||||||
// protected static imgUrl: string = "http://vp.cloud.xypays.cn";
|
protected static servesUrl: string = "https://swimming.api.xypays.cn";
|
||||||
// protected static mediaUrl: string = "http://byc1.xypays.cn/";
|
protected static imgUrl: string = "https://swimming.api.xypays.cn";
|
||||||
|
protected static mediaUrl: string = "https://swimming.api.xypays.cn/";
|
||||||
protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
|
protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
|
||||||
protected static payuploadUrl: string = "http://192.168.0.142:5298";
|
protected static payuploadUrl: string = "https://swimming.api.xypays.cn";
|
||||||
}
|
}
|
||||||
@@ -1,75 +1,65 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="curve-container">
|
<view class="curve-container">
|
||||||
<!-- 页面标题区域 -->
|
<!-- 项目选择卡片 -->
|
||||||
<view class="header-section">
|
<view class="filter-card ">
|
||||||
<view class="header-title">
|
<view class="card-title">
|
||||||
<text class="title">曲线走势图</text>
|
<u-icon name="grid" size="18" color="#1890ff"></u-icon>
|
||||||
<text class="subtitle">学员成绩趋势变化分析</text>
|
<text>选择项目</text>
|
||||||
|
</view>
|
||||||
|
<view class="picker-row" @click="showProject = true">
|
||||||
|
<text class="picker-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
|
||||||
|
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
|
||||||
|
|
||||||
|
<!-- 日期选择卡片 -->
|
||||||
|
<view class="filter-card " v-if="selectProcet">
|
||||||
|
<view class="card-title">
|
||||||
|
<u-icon name="calendar" size="18" color="#13c2c2"></u-icon>
|
||||||
|
<text>日期范围</text>
|
||||||
|
</view>
|
||||||
|
<view class="date-filter" @click="openCalendar">
|
||||||
|
<view class="date-item">
|
||||||
|
<text class="date-label">开始日期</text>
|
||||||
|
<text class="date-value" :class="{ placeholder: !begin }">{{ begin || '请选择' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="date-divider"></view>
|
||||||
|
<view class="date-item">
|
||||||
|
<text class="date-label">结束日期</text>
|
||||||
|
<text class="date-value" :class="{ placeholder: !end }">{{ end || '请选择' }}</text>
|
||||||
|
</view>
|
||||||
|
<u-icon name="arrow-right" size="16" color="#bbb" class="date-arrow"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 项目选择区域 -->
|
<!-- 学生选择卡片 -->
|
||||||
<view class="select-section">
|
<view class="filter-card " v-if="selectProcet && begin && projectStudents.length > 0">
|
||||||
<view class="select-label">
|
|
||||||
<view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
|
|
||||||
<view class="picker-wrapper" @click="showProject=true">
|
|
||||||
<text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
|
|
||||||
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
|
|
||||||
:columns="projectOptions"></up-picker>
|
|
||||||
|
|
||||||
<!-- 日期选择 -->
|
|
||||||
<view class="date-filter" style="margin: 20rpx 20rpx 0;" v-if="selectProcet">
|
|
||||||
<view class="date-picker" @click="openCalendar()">
|
|
||||||
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
|
|
||||||
<view class="date-text">
|
|
||||||
<text class="label">开始日期</text>
|
|
||||||
<text class="value">{{ begin || '请选择' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="date-picker" @click="openCalendar()">
|
|
||||||
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
|
|
||||||
<view class="date-text">
|
|
||||||
<text class="label">结束日期</text>
|
|
||||||
<text class="value">{{ end || '请选择' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ==================== 学生选择区域 ==================== -->
|
|
||||||
<!-- 仅在选择项目和时间后显示 -->
|
|
||||||
<view class="student-select-section" v-if="selectProcet && begin && projectStudents.length > 0">
|
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<text class="section-title">选择学员(最多3人)</text>
|
<view class="card-title">
|
||||||
<text class="select-count">已选:{{ selectedStudentIds.length }}/3</text>
|
<u-icon name="account" size="18" color="#52c41a"></u-icon>
|
||||||
|
<text>选择学员</text>
|
||||||
|
</view>
|
||||||
|
<text class="select-count">已选 {{ selectedStudentIds.length }}/3</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 学生列表 -->
|
|
||||||
<view class="student-list">
|
<view class="student-list">
|
||||||
<view v-for="student in projectStudents" :key="student.studentId"
|
<view v-for="student in projectStudents" :key="student.studentId"
|
||||||
:class="['student-item', { 'selected': selectedStudentIds.includes(student.studentId) }]"
|
:class="['student-item', { selected: selectedStudentIds.includes(student.studentId) }]"
|
||||||
@click="toggleStudent(student.studentId)">
|
@click="toggleStudent(student.studentId)">
|
||||||
<!-- 学生头像(使用首字母作为头像) -->
|
|
||||||
<view class="student-avatar">
|
<view class="student-avatar">
|
||||||
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
|
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 学生姓名 -->
|
|
||||||
<text class="student-name">{{ student.name }}</text>
|
<text class="student-name">{{ student.name }}</text>
|
||||||
<!-- 选择状态图标 -->
|
|
||||||
<view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)">
|
<view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)">
|
||||||
<u-icon name="checkmark" size="16" color="#fff"></u-icon>
|
<u-icon name="checkmark" size="14" color="#fff"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 图表区域 -->
|
||||||
<!-- ==================== 图表展示区域 ==================== -->
|
|
||||||
<!-- 仅在选择了至少一名学生后显示 -->
|
|
||||||
<view class="chart-section" v-if="selectedStudentIds.length > 0">
|
<view class="chart-section" v-if="selectedStudentIds.length > 0">
|
||||||
<!-- 数据统计卡片 -->
|
<!-- 统计概览 -->
|
||||||
<view class="stats-card">
|
<view class="stats-card">
|
||||||
<view class="stat-item">
|
<view class="stat-item">
|
||||||
<text class="stat-label">选中项目</text>
|
<text class="stat-label">选中项目</text>
|
||||||
@@ -78,112 +68,77 @@
|
|||||||
<view class="stat-divider"></view>
|
<view class="stat-divider"></view>
|
||||||
<view class="stat-item">
|
<view class="stat-item">
|
||||||
<text class="stat-label">对比学员</text>
|
<text class="stat-label">对比学员</text>
|
||||||
<text class="stat-value">{{ selectedStudentIds.length }}人</text>
|
<text class="stat-value">{{ selectedStudentIds.length }} 人</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 折线图卡片 -->
|
<!-- 折线图 -->
|
||||||
<view class="chart-card">
|
<view class="chart-card">
|
||||||
<view class="chart-header">
|
<view class="chart-header">
|
||||||
<text class="chart-title">成绩趋势对比</text>
|
<text class="chart-title">成绩趋势对比</text>
|
||||||
<text class="chart-desc">历史训练成绩变化曲线</text>
|
<text class="chart-desc">历史训练成绩变化曲线</text>
|
||||||
</view>
|
</view>
|
||||||
<!-- 折线图容器 -->
|
|
||||||
<scroll-view class="chart-scroll" scroll-x :show-scrollbar="false">
|
<scroll-view class="chart-scroll" scroll-x :show-scrollbar="false">
|
||||||
<view class="chart-box" :style="{ width: chartWidth + 'rpx' }">
|
<view class="chart-box" :style="{ width: chartWidth + 'rpx' }">
|
||||||
<qiun-data-charts type="line" :opts="lineOpts" :chartData="lineChartData" :ontouch="true" />
|
<qiun-data-charts type="line" :opts="lineOpts" :chartData="lineChartData" :ontouch="true" />
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 日历弹窗 -->
|
<!-- 日历弹窗 -->
|
||||||
<up-calendar :show="showCalendar" mode="date" minDate='1776240407000' @confirm="calendarConfirm"
|
<up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar>
|
||||||
@close="calendarClose">
|
|
||||||
</up-calendar>
|
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onShow, onLoad } from "@dcloudio/uni-app"
|
import { onShow, onLoad } from "@dcloudio/uni-app"
|
||||||
import { Service } from '@/Service/Service'
|
import { Service } from '@/Service/Service'
|
||||||
import { ref, computed, watch } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { PlanService } from '@/Service/swimming/PlanService'
|
import { PlanService } from '@/Service/swimming/PlanService'
|
||||||
|
|
||||||
// ==================== 常量定义 ====================
|
|
||||||
|
|
||||||
// 最大选择学生数量限制
|
|
||||||
const MAX_SELECTED_STUDENTS = 3
|
const MAX_SELECTED_STUDENTS = 3
|
||||||
|
|
||||||
// 图表颜色配置(最多支持3种颜色)
|
|
||||||
const chartColors = ['#52c41a', '#1890ff', '#faad14']
|
const chartColors = ['#52c41a', '#1890ff', '#faad14']
|
||||||
|
|
||||||
// ==================== 响应式数据 - 项目相关 ====================
|
|
||||||
|
|
||||||
// 项目选择
|
|
||||||
let showProject = ref(false)
|
let showProject = ref(false)
|
||||||
let selectProcet = ref('')
|
let selectProcet = ref('')
|
||||||
let selectId = ref('')
|
let selectId = ref('')
|
||||||
|
|
||||||
// 日期选择
|
|
||||||
const begin = ref<string>('')
|
const begin = ref<string>('')
|
||||||
const end = ref<string>('')
|
const end = ref<string>('')
|
||||||
const showCalendar = ref(false)
|
const showCalendar = ref(false)
|
||||||
|
|
||||||
const projectOptions = ref<Array<any>>([[]])
|
const projectOptions = ref<Array<any>>([[]])
|
||||||
|
|
||||||
// ==================== TypeScript 接口定义 ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 学生接口
|
|
||||||
* 定义学生的基本信息
|
|
||||||
*/
|
|
||||||
interface Student {
|
interface Student {
|
||||||
studentId : string
|
studentId: string
|
||||||
name : string
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 学生训练记录接口
|
|
||||||
* 定义学生某次训练的详细记录
|
|
||||||
*/
|
|
||||||
interface StudentTrainingRecord {
|
interface StudentTrainingRecord {
|
||||||
studentId : string
|
studentId: string
|
||||||
studentName : string
|
studentName: string
|
||||||
time : number
|
time: number
|
||||||
recordDate : string
|
recordDate: string
|
||||||
recordFullDate : string
|
recordFullDate: string
|
||||||
round : number
|
round: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 响应式数据 - 学生相关 ====================
|
|
||||||
|
|
||||||
// 当前项目的学生列表
|
|
||||||
const projectStudents = ref<Student[]>([])
|
const projectStudents = ref<Student[]>([])
|
||||||
|
|
||||||
// 已选中的学生 ID 列表
|
|
||||||
const selectedStudentIds = ref<string[]>([])
|
const selectedStudentIds = ref<string[]>([])
|
||||||
|
|
||||||
// 已选中的学生详细信息列表((计算属性)
|
|
||||||
const selectedStudents = computed(() => {
|
const selectedStudents = computed(() => {
|
||||||
return projectStudents.value.filter(student =>
|
return projectStudents.value.filter(student =>
|
||||||
selectedStudentIds.value.includes(student.studentId)
|
selectedStudentIds.value.includes(student.studentId)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
// ==================== 响应式数据 - 图表相关 ====================
|
|
||||||
|
|
||||||
// 计算图表宽度(根据数据点数量动态调整)
|
|
||||||
const chartWidth = computed(() => {
|
const chartWidth = computed(() => {
|
||||||
const categoryCount = lineChartData.value.categories.length
|
const categoryCount = lineChartData.value.categories.length
|
||||||
if (categoryCount <= 6) return 700
|
if (categoryCount <= 6) return 700
|
||||||
// 每个数据点分配 100rpx,最小 700rpx
|
|
||||||
return Math.max(700, categoryCount * 100)
|
return Math.max(700, categoryCount * 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 折线图配置选项
|
|
||||||
const lineOpts = ref({
|
const lineOpts = ref({
|
||||||
color: chartColors,
|
color: chartColors,
|
||||||
padding: [15, 10, 0, 15],
|
padding: [15, 10, 0, 15],
|
||||||
@@ -201,7 +156,7 @@
|
|||||||
yAxis: {
|
yAxis: {
|
||||||
data: [{
|
data: [{
|
||||||
min: 0,
|
min: 0,
|
||||||
format: (val : number) => val.toFixed(1) + 's'
|
format: (val: number) => val.toFixed(1) + 's'
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
extra: {
|
extra: {
|
||||||
@@ -215,25 +170,16 @@
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 折线图数据
|
|
||||||
const lineChartData = ref({
|
const lineChartData = ref({
|
||||||
categories: [] as string[],
|
categories: [] as string[],
|
||||||
series: [] as any[]
|
series: [] as any[]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
getProjectData()
|
getProjectData()
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {})
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const getProjectData = () => {
|
const getProjectData = () => {
|
||||||
PlanService.GetPlanListNoPage('计时项目').then(res => {
|
PlanService.GetPlanListNoPage('计时项目').then(res => {
|
||||||
@@ -245,10 +191,15 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectProcetFunc = (e : any) => {
|
const selectProcetFunc = (e: any) => {
|
||||||
selectProcet.value = e.value[0].name
|
selectProcet.value = e.value[0].name
|
||||||
selectId.value = e.value[0].planId
|
selectId.value = e.value[0].planId
|
||||||
selectedStudentIds.value = []
|
selectedStudentIds.value = []
|
||||||
|
begin.value = ''
|
||||||
|
end.value = ''
|
||||||
|
projectStudents.value = []
|
||||||
|
lineChartData.value.categories = []
|
||||||
|
lineChartData.value.series = []
|
||||||
loadProjectStudents(selectId.value)
|
loadProjectStudents(selectId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +207,7 @@
|
|||||||
showCalendar.value = true
|
showCalendar.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const calendarConfirm = (e : any) => {
|
const calendarConfirm = (e: any) => {
|
||||||
begin.value = e[0]
|
begin.value = e[0]
|
||||||
end.value = e[e.length - 1]
|
end.value = e[e.length - 1]
|
||||||
showCalendar.value = false
|
showCalendar.value = false
|
||||||
@@ -266,7 +217,7 @@
|
|||||||
showCalendar.value = false
|
showCalendar.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadProjectStudents = (projectId : string) => {
|
const loadProjectStudents = (projectId: string) => {
|
||||||
PlanService.GetPlanInfo(projectId).then(res => {
|
PlanService.GetPlanInfo(projectId).then(res => {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
projectStudents.value = res.data.plan.users
|
projectStudents.value = res.data.plan.users
|
||||||
@@ -276,7 +227,7 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleStudent = (studentId : string) => {
|
const toggleStudent = (studentId: string) => {
|
||||||
const index = selectedStudentIds.value.indexOf(studentId)
|
const index = selectedStudentIds.value.indexOf(studentId)
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
selectedStudentIds.value.splice(index, 1)
|
selectedStudentIds.value.splice(index, 1)
|
||||||
@@ -287,9 +238,7 @@
|
|||||||
}
|
}
|
||||||
selectedStudentIds.value.push(studentId)
|
selectedStudentIds.value.push(studentId)
|
||||||
}
|
}
|
||||||
let studentIdList = selectedStudentIds.value.map((item) => {
|
let studentIdList = selectedStudentIds.value.map((item) => item)
|
||||||
return item
|
|
||||||
})
|
|
||||||
let data = {
|
let data = {
|
||||||
planId: selectId.value,
|
planId: selectId.value,
|
||||||
studentId: JSON.stringify(studentIdList),
|
studentId: JSON.stringify(studentIdList),
|
||||||
@@ -297,11 +246,10 @@
|
|||||||
eTime: end.value
|
eTime: end.value
|
||||||
}
|
}
|
||||||
interface DataItem {
|
interface DataItem {
|
||||||
name : string
|
name: string
|
||||||
data : any[]
|
data: any[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const xData = ref<DataItem[]>(
|
const xData = ref<DataItem[]>(
|
||||||
Array.from({ length: studentIdList.length }, () => ({
|
Array.from({ length: studentIdList.length }, () => ({
|
||||||
name: '',
|
name: '',
|
||||||
@@ -310,344 +258,295 @@
|
|||||||
)
|
)
|
||||||
PlanService.GetQuxianLog(data).then(res => {
|
PlanService.GetQuxianLog(data).then(res => {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
|
lineChartData.value.categories = []
|
||||||
res.data.list.map((item : any) => {
|
res.data.list.map((item: any) => {
|
||||||
lineChartData.value.categories.push(item.dayTime)
|
lineChartData.value.categories.push(item.dayTime)
|
||||||
item.data.map((content : any, index : any) => {
|
item.data.map((content: any, index: any) => {
|
||||||
xData.value[index].name = content.studentName
|
xData.value[index].name = content.studentName
|
||||||
xData.value[index].data.push(content.time)
|
xData.value[index].data.push(content.time)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
console.log(xData.value);
|
|
||||||
lineChartData.value.series = xData.value
|
lineChartData.value.series = xData.value
|
||||||
} else {
|
} else {
|
||||||
Service.Msg(res.msg)
|
Service.Msg(res.msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
page {
|
page {
|
||||||
background-color: #f5f5f5;
|
background-color: #e8ecf3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.curve-container {
|
.curve-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
padding: 20rpx;
|
||||||
padding-bottom: 40rpx;
|
padding-bottom: 40rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section {
|
/* 通用卡片 */
|
||||||
background-color: #fff;
|
.filter-card {
|
||||||
padding: 32rpx 28rpx 24rpx;
|
background: #ffffff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
// border-left: 8rpx solid #999;
|
||||||
|
|
||||||
.header-title {
|
}
|
||||||
.title {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #333;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
.card-title {
|
||||||
font-size: 24rpx;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10rpx;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 项目选择 */
|
||||||
|
.picker-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1rpx solid #e4e8ee;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #eef1f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
color: #999;
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 日期选择 */
|
||||||
|
.date-filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #f0fafa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
border: 1rpx solid #d6f0f0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #e6f5f5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-value {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-divider {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
background: #b2dfdb;
|
||||||
|
margin: 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-arrow {
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 学生选择区域 */
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
.section-title {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-count {
|
.select-count {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #52c41a;
|
color: #52c41a;
|
||||||
|
font-weight: 600;
|
||||||
|
background: #f0f9eb;
|
||||||
|
padding: 6rpx 16rpx;
|
||||||
|
border-radius: 20rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-filter {
|
.student-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24rpx;
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
.date-picker {
|
padding: 20rpx 10rpx;
|
||||||
flex: 1;
|
background: #f5f7fa;
|
||||||
display: flex;
|
border-radius: 12rpx;
|
||||||
align-items: center;
|
border: 2rpx solid transparent;
|
||||||
gap: 16rpx;
|
transition: all 0.2s;
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
padding: 20rpx;
|
|
||||||
|
|
||||||
.date-text {
|
|
||||||
.label {
|
|
||||||
display: block;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
display: block;
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #333;
|
|
||||||
margin-top: 4rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-section {
|
|
||||||
background-color: #fff;
|
|
||||||
margin: 0 20rpx 20rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 28rpx;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.select-label {
|
&:active {
|
||||||
margin-bottom: 16rpx;
|
transform: scale(0.96);
|
||||||
|
|
||||||
.label-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.picker-wrapper {
|
&.selected {
|
||||||
|
background: #f0f9eb;
|
||||||
|
border-color: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-avatar {
|
||||||
|
width: 60rpx;
|
||||||
|
height: 60rpx;
|
||||||
|
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||||
|
border-radius: 50%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: center;
|
||||||
padding: 20rpx 24rpx;
|
margin-bottom: 12rpx;
|
||||||
background-color: #f8f8f8;
|
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
|
||||||
border-radius: 12rpx;
|
|
||||||
border: 1rpx solid #e8e8e8;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:active {
|
.avatar-text {
|
||||||
background-color: #f0f0f0;
|
font-size: 24rpx;
|
||||||
border-color: #faad14;
|
font-weight: 700;
|
||||||
transform: scale(0.98);
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.picker-text {
|
.student-name {
|
||||||
font-size: 28rpx;
|
font-size: 24rpx;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
text-align: center;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.check-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 6rpx;
|
||||||
|
right: 6rpx;
|
||||||
|
width: 28rpx;
|
||||||
|
height: 28rpx;
|
||||||
|
background: #52c41a;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.student-select-section {
|
/* 图表区域 */
|
||||||
background-color: #fff;
|
|
||||||
margin: 0 20rpx 20rpx;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
padding: 28rpx;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
.section-header {
|
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.student-list {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 16rpx;
|
|
||||||
|
|
||||||
.student-item {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20rpx 10rpx;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
border: 2rpx solid transparent;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.95);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.selected {
|
|
||||||
background-color: #f0f9eb;
|
|
||||||
border-color: #52c41a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.student-avatar {
|
|
||||||
width: 60rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin-bottom: 12rpx;
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
|
|
||||||
|
|
||||||
.avatar-text {
|
|
||||||
font-size: 24rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.student-name {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.check-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 8rpx;
|
|
||||||
right: 8rpx;
|
|
||||||
width: 28rpx;
|
|
||||||
height: 28rpx;
|
|
||||||
background-color: #52c41a;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.chart-section {
|
.chart-section {
|
||||||
margin: 0 20rpx;
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.stats-card {
|
.stats-card {
|
||||||
background-color: #fff;
|
background: #ffffff;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
// border-left: 8rpx solid #faad14;
|
||||||
|
|
||||||
.stat-item {
|
.stat-item {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.stat-label {
|
.stat-label {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #999;
|
color: #666;
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 8rpx;
|
||||||
}
|
font-weight: 500;
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #52c41a;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-divider {
|
.stat-value {
|
||||||
width: 1rpx;
|
font-size: 32rpx;
|
||||||
height: 50rpx;
|
font-weight: 700;
|
||||||
background-color: #eee;
|
color: #faad14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.chart-card {
|
.stat-divider {
|
||||||
background-color: #fff;
|
width: 1rpx;
|
||||||
border-radius: 16rpx;
|
height: 50rpx;
|
||||||
padding: 24rpx;
|
background-color: #f0f0f0;
|
||||||
margin-bottom: 20rpx;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
.chart-header {
|
|
||||||
margin-bottom: 20rpx;
|
|
||||||
padding-left: 12rpx;
|
|
||||||
border-left: 6rpx solid #52c41a;
|
|
||||||
|
|
||||||
.chart-title {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-desc {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-scroll {
|
|
||||||
width: 100%;
|
|
||||||
height: 500rpx;
|
|
||||||
background-color: #fafafa;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
|
|
||||||
.chart-box {
|
|
||||||
height: 100%;
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.legend-section {
|
.chart-card {
|
||||||
background-color: #fff;
|
background: #ffffff;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
// border-left: 8rpx solid #52c41a;
|
||||||
|
|
||||||
.legend-title {
|
.chart-header {
|
||||||
font-size: 28rpx;
|
margin-bottom: 20rpx;
|
||||||
|
padding-left: 12rpx;
|
||||||
|
border-left: 6rpx solid #52c41a;
|
||||||
|
|
||||||
|
.chart-title {
|
||||||
|
font-size: 30rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 20rpx;
|
display: block;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.legend-list {
|
.chart-desc {
|
||||||
display: flex;
|
font-size: 24rpx;
|
||||||
flex-direction: column;
|
color: #999;
|
||||||
gap: 16rpx;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.legend-item {
|
.chart-scroll {
|
||||||
display: flex;
|
width: 100%;
|
||||||
align-items: center;
|
height: 500rpx;
|
||||||
gap: 12rpx;
|
background-color: #fafafa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
.legend-color {
|
.chart-box {
|
||||||
width: 24rpx;
|
height: 100%;
|
||||||
height: 24rpx;
|
display: inline-block;
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-name {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.legend-desc {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="grades-container">
|
<view class="grades-container">
|
||||||
<!-- 页面标题区域 -->
|
<!-- 项目选择卡片 -->
|
||||||
<view class="header-section">
|
<view class="filter-card ">
|
||||||
<view class="header-title">
|
<view class="card-title">
|
||||||
<text class="title">成绩排名</text>
|
<u-icon name="grid" size="18" color="#1890ff"></u-icon>
|
||||||
<text class="subtitle">学生最佳成绩排名</text>
|
<text>选择项目</text>
|
||||||
|
</view>
|
||||||
|
<view class="picker-row" @click="showProject = true">
|
||||||
|
<text class="picker-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
|
||||||
|
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
|
||||||
|
|
||||||
<!-- 项目选择区域 -->
|
<!-- 排名列表区域 -->
|
||||||
<view class="select-section">
|
|
||||||
<view class="select-label">
|
|
||||||
<view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
|
|
||||||
<view class="picker-wrapper" @click="showProject=true">
|
|
||||||
<text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
|
|
||||||
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
|
|
||||||
:columns="projectOptions"></up-picker>
|
|
||||||
|
|
||||||
|
|
||||||
<!-- ==================== 排名列表区域 ==================== -->
|
|
||||||
<!-- 仅在选择了项目且有数据时显示 -->
|
|
||||||
<view class="ranking-section" v-if="selectProcet && gradeList.length > 0">
|
<view class="ranking-section" v-if="selectProcet && gradeList.length > 0">
|
||||||
<!-- 排名统计卡片 -->
|
<!-- 统计卡片 -->
|
||||||
<view class="stats-card">
|
<view class="stats-card">
|
||||||
<view class="stat-item">
|
<view class="stat-item">
|
||||||
<text class="stat-label">对比项目</text>
|
<text class="stat-label">对比项目</text>
|
||||||
@@ -34,7 +24,7 @@
|
|||||||
<view class="stat-divider"></view>
|
<view class="stat-divider"></view>
|
||||||
<view class="stat-item">
|
<view class="stat-item">
|
||||||
<text class="stat-label">参与人数</text>
|
<text class="stat-label">参与人数</text>
|
||||||
<text class="stat-value">{{ gradeList.length }}人</text>
|
<text class="stat-value">{{ gradeList.length }} 人</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -42,24 +32,27 @@
|
|||||||
<view class="ranking-list">
|
<view class="ranking-list">
|
||||||
<view v-for="(item, index) in gradeList" :key="item.id"
|
<view v-for="(item, index) in gradeList" :key="item.id"
|
||||||
:class="['ranking-item', { 'top-three': index < 3 }]" @click="handleRankingClick(item)">
|
:class="['ranking-item', { 'top-three': index < 3 }]" @click="handleRankingClick(item)">
|
||||||
<!-- 排名徽章 -->
|
|
||||||
<view class="rank-badge" :class="`rank-${index + 1}`">
|
<view class="rank-badge" :class="`rank-${index + 1}`">
|
||||||
<text class="rank-number">{{ index + 1 }}</text>
|
<text class="rank-number">{{ index + 1 }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 学生信息 -->
|
|
||||||
<view class="student-info">
|
<view class="student-info">
|
||||||
<text class="student-name">{{ item.studentName }}</text>
|
<text class="student-name">{{ item.studentName }}</text>
|
||||||
<text class="student-speed">最快用时:{{ item.quicklyTime }}s</text>
|
<text class="student-speed">最快用时:{{ item.quicklyTime }}s</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 成绩详情 -->
|
|
||||||
<view class="speed-detail">
|
<view class="speed-detail">
|
||||||
<text class="detail-text">日期:{{ Service.formatDate(item.addTime,2)}}</text>
|
<text class="detail-text">{{ Service.formatDate(item.addTime,2) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view class="empty-card" v-if="selectProcet && gradeList.length === 0">
|
||||||
|
<u-icon name="file-text" size="64" color="#d9d9d9"></u-icon>
|
||||||
|
<text class="empty-text">暂无排名数据</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -69,8 +62,6 @@
|
|||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { PlanService } from '@/Service/swimming/PlanService'
|
import { PlanService } from '@/Service/swimming/PlanService'
|
||||||
|
|
||||||
// ==================== 响应式数据 - 项目相关 ====================
|
|
||||||
|
|
||||||
// 项目选择
|
// 项目选择
|
||||||
let showProject = ref(false)
|
let showProject = ref(false)
|
||||||
let selectProcet = ref('')
|
let selectProcet = ref('')
|
||||||
@@ -78,49 +69,26 @@
|
|||||||
|
|
||||||
const projectOptions = ref<Array<any>>([[]])
|
const projectOptions = ref<Array<any>>([[]])
|
||||||
|
|
||||||
// ==================== TypeScript 接口定义 ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 学生成绩接口
|
|
||||||
* 定义学生的成绩和排名信息
|
|
||||||
*/
|
|
||||||
interface StudentGrade {
|
interface StudentGrade {
|
||||||
id : string // 学生 ID
|
id: string
|
||||||
name : string // 学生姓名
|
name: string
|
||||||
bestSpeed : number // 最快速度(m/s)
|
bestSpeed: number
|
||||||
bestTime : number // 最快用时(秒)
|
bestTime: number
|
||||||
recordDate : string // 记录日期
|
recordDate: string
|
||||||
studentId : string // 学生 ID(用于数据关联)
|
studentId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 响应式数据 - 排名相关 ====================
|
|
||||||
|
|
||||||
// 排名列表数据
|
|
||||||
const gradeList = ref<StudentGrade[]>([])
|
const gradeList = ref<StudentGrade[]>([])
|
||||||
|
|
||||||
// 最高速度(计算属性)
|
|
||||||
const maxSpeed = computed(() => {
|
const maxSpeed = computed(() => {
|
||||||
if (gradeList.value.length === 0) return 0
|
if (gradeList.value.length === 0) return 0
|
||||||
return Math.max(...gradeList.value.map(item => item.bestSpeed)).toFixed(2)
|
return Math.max(...gradeList.value.map(item => item.bestSpeed)).toFixed(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ==================== 生命周期钩子 ====================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 页面加载时触发
|
|
||||||
* 在页面初始化时执行,只执行一次
|
|
||||||
*/
|
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
// 初始化数据
|
|
||||||
getProjectData()
|
getProjectData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ==================== 业务逻辑方法 - 项目相关 ====================
|
|
||||||
|
|
||||||
const getProjectData = () => {
|
const getProjectData = () => {
|
||||||
PlanService.GetPlanListNoPage('计时项目').then(res => {
|
PlanService.GetPlanListNoPage('计时项目').then(res => {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
@@ -131,314 +99,244 @@
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectProcetFunc = (e : any) => {
|
const selectProcetFunc = (e: any) => {
|
||||||
selectProcet.value = e.value[0].name
|
selectProcet.value = e.value[0].name
|
||||||
selectId.value = e.value[0].planId
|
selectId.value = e.value[0].planId
|
||||||
loadProjectGrades(selectId.value)
|
loadProjectGrades(selectId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const loadProjectGrades = (projectId: string) => {
|
||||||
* 加载项目排名数据
|
|
||||||
* 根据项目 ID 获取该项目的学生成绩排名列表
|
|
||||||
* @param projectId 项目 ID
|
|
||||||
*/
|
|
||||||
const loadProjectGrades = (projectId : string) => {
|
|
||||||
PlanService.GetRankData(projectId).then(res => {
|
PlanService.GetRankData(projectId).then(res => {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
gradeList.value=res.data.list
|
gradeList.value = res.data.list
|
||||||
} else {
|
} else {
|
||||||
Service.Msg(res.msg)
|
Service.Msg(res.msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 业务逻辑方法 - 排名相关 ====================
|
const handleRankingClick = (item: StudentGrade) => {
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理排名项点击事件
|
|
||||||
* 点击某排名项查看详情
|
|
||||||
* @param item 排名项数据
|
|
||||||
*/
|
|
||||||
const handleRankingClick = (item : StudentGrade) => {
|
|
||||||
console.log('点击了排名项:', item)
|
console.log('点击了排名项:', item)
|
||||||
// TODO: 跳转到学生详情页面
|
|
||||||
// Service.GoPage('/pages/student/detail', { studentId: item.studentId })
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
// 页面背景色设置
|
|
||||||
page {
|
page {
|
||||||
background-color: #f5f5f5;
|
background-color: #e8ecf3;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 页面容器
|
|
||||||
.grades-container {
|
.grades-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
padding: 20rpx;
|
||||||
padding-bottom: 40rpx;
|
padding-bottom: 40rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 页面标题区域 ==================== */
|
/* 通用卡片 */
|
||||||
.header-section {
|
.filter-card {
|
||||||
background-color: #fff;
|
background: #ffffff;
|
||||||
padding: 32rpx 28rpx 24rpx;
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
.header-title {
|
.card-title {
|
||||||
.title {
|
display: flex;
|
||||||
font-size: 36rpx;
|
align-items: center;
|
||||||
font-weight: 700;
|
gap: 10rpx;
|
||||||
color: #333;
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1rpx solid #e4e8ee;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #eef1f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 排名区域 */
|
||||||
|
.ranking-section {
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-around;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: 8rpx;
|
margin-bottom: 8rpx;
|
||||||
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.subtitle {
|
.stat-value {
|
||||||
font-size: 24rpx;
|
font-size: 32rpx;
|
||||||
color: #999;
|
font-weight: 700;
|
||||||
|
color: #faad14;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 1rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 项目选择区域 ==================== */
|
.ranking-list {
|
||||||
.select-section {
|
.ranking-item {
|
||||||
background-color: #fff;
|
|
||||||
margin: 0 20rpx 20rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 28rpx;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.select-label {
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
|
|
||||||
.label-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 20rpx 24rpx;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
border: 1rpx solid #e8e8e8;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border-color: #faad14;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* ==================== 排名列表区域 ==================== */
|
|
||||||
.ranking-section {
|
|
||||||
margin: 0 20rpx;
|
|
||||||
|
|
||||||
// 统计卡片
|
|
||||||
.stats-card {
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 20rpx;
|
border-radius: 16rpx;
|
||||||
padding: 24rpx;
|
padding: 24rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 16rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-around;
|
gap: 20rpx;
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.2s;
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
&::before {
|
&:active {
|
||||||
content: '';
|
transform: scale(0.99);
|
||||||
position: absolute;
|
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 6rpx;
|
|
||||||
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
|
|
||||||
border-radius: 20rpx 0 0 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-item {
|
&.top-three {
|
||||||
text-align: center;
|
border-left-color: #ffd700;
|
||||||
|
|
||||||
.stat-label {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #999;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-value {
|
|
||||||
font-size: 32rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1890ff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-divider {
|
.rank-badge {
|
||||||
width: 1rpx;
|
width: 60rpx;
|
||||||
height: 50rpx;
|
height: 60rpx;
|
||||||
background-color: #eee;
|
border-radius: 50%;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排名列表容器
|
|
||||||
.ranking-list {
|
|
||||||
|
|
||||||
// 排名项
|
|
||||||
.ranking-item {
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 24rpx;
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 20rpx;
|
justify-content: center;
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
flex-shrink: 0;
|
||||||
transition: all 0.3s ease;
|
background-color: #f0f0f0;
|
||||||
position: relative;
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&::before {
|
.rank-number {
|
||||||
content: '';
|
font-size: 24rpx;
|
||||||
position: absolute;
|
font-weight: 700;
|
||||||
left: 0;
|
color: #999;
|
||||||
top: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 6rpx;
|
|
||||||
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
border-radius: 20rpx 0 0 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&.rank-1 {
|
||||||
transform: scale(0.98);
|
background: linear-gradient(135deg, #ffd700 0%, #ffec3d 100%);
|
||||||
box-shadow: 0 4rpx 16rpx rgba(235, 47, 150, 0.15);
|
box-shadow: 0 2rpx 8rpx rgba(255, 215, 0, 0.4);
|
||||||
|
|
||||||
&::before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 排名徽章
|
|
||||||
.rank-badge {
|
|
||||||
width: 60rpx;
|
|
||||||
height: 60rpx;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
.rank-number {
|
.rank-number {
|
||||||
font-size: 24rpx;
|
color: #fff;
|
||||||
font-weight: 700;
|
font-size: 28rpx;
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 前三名特殊颜色
|
|
||||||
&.rank-1 {
|
|
||||||
background: linear-gradient(135deg, #ffd700 0%, #ffec3d 100%);
|
|
||||||
box-shadow: 0 2rpx 8.6rpx rgba(255, 215, 0, 0.4);
|
|
||||||
|
|
||||||
.rank-number {
|
|
||||||
color: #fff;
|
|
||||||
font-size: 28rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rank-2 {
|
|
||||||
background: linear-gradient(135deg, #c0c0c0 0%, #d9d9d9 100%);
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(192, 192, 192, 0.4);
|
|
||||||
|
|
||||||
.rank-number {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.rank-3 {
|
|
||||||
background: linear-gradient(135deg, #cd7f32 0%, #e6963d 100%);
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(205, 127, 50, 0.4);
|
|
||||||
|
|
||||||
.rank-number {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 学生头像
|
&.rank-2 {
|
||||||
.student-avatar {
|
background: linear-gradient(135deg, #c0c0c0 0%, #d9d9d9 100%);
|
||||||
width: 70rpx;
|
box-shadow: 0 2rpx 8rpx rgba(192, 192, 192, 0.4);
|
||||||
height: 70rpx;
|
|
||||||
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(235, 47, 150, 0.3);
|
|
||||||
|
|
||||||
.avatar-text {
|
.rank-number {
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 学生信息
|
&.rank-3 {
|
||||||
.student-info {
|
background: linear-gradient(135deg, #cd7f32 0%, #e6963d 100%);
|
||||||
flex: 1;
|
box-shadow: 0 2rpx 8rpx rgba(205, 127, 50, 0.4);
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.student-name {
|
.rank-number {
|
||||||
font-size: 32rpx;
|
color: #fff;
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.student-speed {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #1890ff;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 成绩详情
|
.student-info {
|
||||||
.speed-detail {
|
flex: 1;
|
||||||
display: flex;
|
min-width: 0;
|
||||||
align-items: center;
|
|
||||||
gap: 16rpx;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.detail-text {
|
.student-name {
|
||||||
font-size: 24rpx;
|
font-size: 32rpx;
|
||||||
color: #999;
|
font-weight: 600;
|
||||||
background-color: #f5f5f5;
|
color: #333;
|
||||||
padding: 6rpx 12rpx;
|
display: block;
|
||||||
border-radius: 6rpx;
|
margin-bottom: 8rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.student-speed {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.speed-detail {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.detail-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 120rpx 40rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
margin-top: 4rpx;
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,54 +1,70 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="paragraph-container">
|
<view class="paragraph-container">
|
||||||
<view class="header-section">
|
<!-- 项目选择卡片 -->
|
||||||
<view class="header-title">
|
<view class="filter-card ">
|
||||||
<text class="title">分段数据</text>
|
<view class="card-title">
|
||||||
<text class="subtitle">学生分段训练成绩分析</text>
|
<u-icon name="grid" size="18" color="#1890ff"></u-icon>
|
||||||
|
<text>选择项目</text>
|
||||||
|
</view>
|
||||||
|
<view class="picker-row" @click="showProject = true">
|
||||||
|
<text class="picker-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
|
||||||
|
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
|
||||||
|
|
||||||
|
<!-- 日期选择卡片 -->
|
||||||
|
<view class="filter-card " v-if="selectProcet">
|
||||||
|
<view class="card-title">
|
||||||
|
<u-icon name="calendar" size="18" color="#13c2c2"></u-icon>
|
||||||
|
<text>日期范围</text>
|
||||||
|
</view>
|
||||||
|
<view class="date-filter" @click="openCalendar">
|
||||||
|
<view class="date-item">
|
||||||
|
<text class="date-label">开始日期</text>
|
||||||
|
<text class="date-value" :class="{ placeholder: !begin }">{{ begin || '请选择' }}</text>
|
||||||
|
</view>
|
||||||
|
<view class="date-divider"></view>
|
||||||
|
<view class="date-item">
|
||||||
|
<text class="date-label">结束日期</text>
|
||||||
|
<text class="date-value" :class="{ placeholder: !end }">{{ end || '请选择' }}</text>
|
||||||
|
</view>
|
||||||
|
<u-icon name="arrow-right" size="16" color="#bbb" class="date-arrow"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 项目选择区域 -->
|
<!-- 学生选择卡片 -->
|
||||||
<view class="select-section">
|
<view class="filter-card " v-if="selectProcet && begin">
|
||||||
<view class="select-label">
|
<view class="card-title">
|
||||||
<view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
|
<u-icon name="account" size="18" color="#52c41a"></u-icon>
|
||||||
<view class="picker-wrapper" @click="showProject=true">
|
<text>选择学生</text>
|
||||||
<text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
|
|
||||||
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
<view class="picker-row" @click="showStudentPicker = true">
|
||||||
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
|
<text class="picker-text" :class="{ placeholder: selectedStudentIndexes.length === 0 }">{{ selectedStudentDisplay }}</text>
|
||||||
:columns="projectOptions"></up-picker>
|
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
|
||||||
|
|
||||||
<!-- 日期选择 -->
|
|
||||||
<view class="date-filter" style="margin: 20rpx 20rpx 0;" v-if="selectProcet">
|
|
||||||
<view class="date-picker" @click="openCalendar()">
|
|
||||||
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
|
|
||||||
<view class="date-text">
|
|
||||||
<text class="label">开始日期</text>
|
|
||||||
<text class="value">{{ begin || '请选择' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="date-picker" @click="openCalendar()">
|
|
||||||
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
|
|
||||||
<view class="date-text">
|
|
||||||
<text class="label">结束日期</text>
|
|
||||||
<text class="value">{{ end || '请选择' }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 学生选择区域 -->
|
<!-- 数据表格 -->
|
||||||
<view class="select-section" v-if="selectProcet && begin ">
|
<view v-if="tableData.length > 0" class="data-card">
|
||||||
<view class="select-label">
|
<view class="card-title">
|
||||||
<text class="label-text">选择学生</text>
|
<u-icon name="list" size="18" color="#faad14"></u-icon>
|
||||||
|
<text>分段数据</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="picker-wrapper" @click="showStudentPicker = true">
|
<view class="table-wrap">
|
||||||
<text class="picker-text">{{ selectedStudentDisplay }}</text>
|
<next-table :show-header="true" :columns="columns" :stripe="true" :fit="false" :show-summary="false" :data="tableData" :showPaging="true" :pageIndex="page" @pageChange="pageChange" @cellClick="cellClick" :pageTotal="pageTotal"></next-table>
|
||||||
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<view class="empty-card" v-if="begin && selectedStudentIndexes.length > 0 && tableData.length === 0">
|
||||||
|
<u-icon name="file-text" size="64" color="#d9d9d9"></u-icon>
|
||||||
|
<text class="empty-text">该日期暂无分段数据</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 日历弹窗 -->
|
||||||
|
<up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar>
|
||||||
|
|
||||||
<!-- 学生多选选择器 -->
|
<!-- 学生多选选择器 -->
|
||||||
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
|
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
|
||||||
<view class="student-picker-modal" v-if="showStudentPicker">
|
<view class="student-picker-modal" v-if="showStudentPicker">
|
||||||
@@ -60,45 +76,19 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<scroll-view class="student-list" scroll-y>
|
<scroll-view class="student-list" scroll-y>
|
||||||
<view v-for="(student, index) in studentList" :key="student.studentId" class="student-item"
|
<view v-for="(student, index) in studentList" :key="student.studentId" class="student-item" :class="{ selected: selectedStudentIndexes.includes(index) }" @click="toggleStudentSelection(index)">
|
||||||
:class="{ 'selected': selectedStudentIndexes.includes(index) }"
|
|
||||||
@click="toggleStudentSelection(index)">
|
|
||||||
<view class="item-checkbox">
|
<view class="item-checkbox">
|
||||||
<view class="checkbox-inner" :class="{ 'checked': selectedStudentIndexes.includes(index) }">
|
<view class="checkbox-inner" :class="{ checked: selectedStudentIndexes.includes(index) }">
|
||||||
<u-icon v-if="selectedStudentIndexes.includes(index)" name="checkmark" size="14"
|
<u-icon v-if="selectedStudentIndexes.includes(index)" name="checkmark" size="12" color="#fff"></u-icon>
|
||||||
color="#fff"></u-icon>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="item-avatar">
|
<view class="item-avatar">
|
||||||
<view class="avatar-circle male">
|
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
|
||||||
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="item-info">
|
|
||||||
<text class="item-name">{{ student.name }}</text>
|
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
<text class="item-name">{{ student.name }}</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="tableData.length>0" class="table-section">
|
|
||||||
<next-table :show-header="true" :columns="columns" :stripe="true" :fit="false" :show-summary='false'
|
|
||||||
:data="tableData" :showPaging='true' :pageIndex="page" @pageChange="pageChange" @cellClick="cellClick"
|
|
||||||
:pageTotal="pageTotal"></next-table>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
|
|
||||||
<view class="empty-container" v-if="begin && selectedStudentIndexes.length > 0 && tableData.length === 0">
|
|
||||||
<view class="empty-icon">
|
|
||||||
<u-icon name="file-text" size="80" color="#ccc"></u-icon>
|
|
||||||
</view>
|
|
||||||
<text class="empty-text">该日期暂无分段数据</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 日历弹窗 -->
|
|
||||||
<up-calendar :show="showCalendar" mode="date" minDate='1776240407000' @confirm="calendarConfirm"
|
|
||||||
@close="calendarClose">
|
|
||||||
</up-calendar>
|
|
||||||
|
|
||||||
<!-- 分段详情弹窗 -->
|
<!-- 分段详情弹窗 -->
|
||||||
<up-popup :show="showSegmentPopup" @close="closeSegmentPopup" @open="openSegmentPopup" mode="center" round="16" bgColor="#fff">
|
<up-popup :show="showSegmentPopup" @close="closeSegmentPopup" @open="openSegmentPopup" mode="center" round="16" bgColor="#fff">
|
||||||
@@ -151,9 +141,9 @@
|
|||||||
segment120 : string
|
segment120 : string
|
||||||
segment170 : string
|
segment170 : string
|
||||||
}
|
}
|
||||||
|
|
||||||
let row=ref('')
|
let row = ref('')
|
||||||
|
|
||||||
// 项目选择
|
// 项目选择
|
||||||
let showProject = ref(false)
|
let showProject = ref(false)
|
||||||
let selectProcet = ref('')
|
let selectProcet = ref('')
|
||||||
@@ -206,11 +196,7 @@
|
|||||||
|
|
||||||
const tableData = ref<TableDataItem[]>([])
|
const tableData = ref<TableDataItem[]>([])
|
||||||
|
|
||||||
|
onLoad(() => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
onLoad(() => {
|
|
||||||
getProjectData()
|
getProjectData()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -323,7 +309,7 @@
|
|||||||
page.value = e
|
page.value = e
|
||||||
getRecordList()
|
getRecordList()
|
||||||
}
|
}
|
||||||
|
|
||||||
const showSegmentPopup = ref(false)
|
const showSegmentPopup = ref(false)
|
||||||
|
|
||||||
interface SegmentItem {
|
interface SegmentItem {
|
||||||
@@ -343,442 +329,322 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cellClick = (rows: any) => {
|
const cellClick = (rows: any) => {
|
||||||
row.value=rows.subsection
|
row.value = rows.subsection
|
||||||
openSegmentPopup()
|
openSegmentPopup()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
page {
|
page {
|
||||||
background-color: #f5f5f5;
|
background-color: #e8ecf3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.paragraph-container {
|
.paragraph-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
padding: 20rpx;
|
||||||
padding-bottom: 40rpx;
|
padding-bottom: 40rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 页面标题区域) ==================== */
|
/* 通用卡片 */
|
||||||
.header-section {
|
.filter-card {
|
||||||
background-color: #fff;
|
background: #ffffff;
|
||||||
padding: 32rpx 28rpx 24rpx;
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
margin-bottom: 20rpx;
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
.header-title {
|
.card-title {
|
||||||
.title {
|
display: flex;
|
||||||
font-size: 36rpx;
|
align-items: center;
|
||||||
font-weight: 700;
|
gap: 10rpx;
|
||||||
color: #333;
|
font-size: 30rpx;
|
||||||
display: block;
|
font-weight: 600;
|
||||||
margin-bottom: 8rpx;
|
color: #333;
|
||||||
}
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.subtitle {
|
/* 选择行 */
|
||||||
font-size: 24rpx;
|
.picker-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
background: #f5f7fa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
border: 1rpx solid #e4e8ee;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #eef1f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-text {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.placeholder {
|
||||||
color: #999;
|
color: #999;
|
||||||
|
font-weight: 400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 日期选择 */
|
||||||
.date-filter {
|
.date-filter {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 24rpx;
|
align-items: center;
|
||||||
|
background: #f0fafa;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 22rpx 24rpx;
|
||||||
|
border: 1rpx solid #d6f0f0;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
.date-picker {
|
&:active {
|
||||||
flex: 1;
|
background: #e6f5f5;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16rpx;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
padding: 20rpx;
|
|
||||||
|
|
||||||
.date-text {
|
|
||||||
.label {
|
|
||||||
display: block;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.value {
|
|
||||||
display: block;
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #333;
|
|
||||||
margin-top: 4rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 选择区域通用样式 ==================== */
|
.date-item {
|
||||||
.select-section {
|
flex: 1;
|
||||||
background-color: #fff;
|
|
||||||
margin: 0 20rpx 20rpx;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 28rpx;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
.select-label {
|
|
||||||
margin-bottom: 16rpx;
|
|
||||||
|
|
||||||
.label-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 20rpx 24rpx;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
border: 1rpx solid #e8e8e8;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
border-color: #faad14;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.picker-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.student-picker-modal {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
height: 70vh;
|
|
||||||
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
|
|
||||||
border-radius: 32rpx 32rpx 0 0;
|
|
||||||
z-index: 999;
|
|
||||||
animation: slideUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
box-shadow: 0 -8rpx 40rpx rgba(0, 0, 0, 0.12);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.picker-header {
|
.date-label {
|
||||||
display: flex;
|
font-size: 24rpx;
|
||||||
justify-content: space-between;
|
color: #666;
|
||||||
align-items: center;
|
font-weight: 500;
|
||||||
padding: 36rpx 32rpx 28rpx;
|
}
|
||||||
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
|
|
||||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.header-title {
|
.date-value {
|
||||||
font-size: 36rpx;
|
font-size: 30rpx;
|
||||||
font-weight: 700;
|
color: #333;
|
||||||
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
|
font-weight: 600;
|
||||||
-webkit-background-clip: text;
|
|
||||||
-webkit-text-fill-color: transparent;
|
|
||||||
background-clip: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-actions {
|
&.placeholder {
|
||||||
display: flex;
|
color: #999;
|
||||||
align-items: center;
|
font-weight: 400;
|
||||||
gap: 16rpx;
|
|
||||||
|
|
||||||
.action-btn {
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #666;
|
|
||||||
padding: 14rpx 28rpx;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
|
|
||||||
font-weight: 600;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
|
|
||||||
|
|
||||||
&.primary {
|
|
||||||
color: #fff;
|
|
||||||
background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%);
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(250, 173, 20, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.92);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.student-list {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 0 20rpx 40rpx;
|
|
||||||
|
|
||||||
.student-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 20rpx;
|
|
||||||
padding: 24rpx;
|
|
||||||
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
|
|
||||||
border-radius: 20rpx;
|
|
||||||
margin-top: 16rpx;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-checkbox {
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.checkbox-inner {
|
|
||||||
width: 40rpx;
|
|
||||||
height: 40rpx;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
border: 2rpx solid #d9d9d9;
|
|
||||||
background-color: #fff;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
|
|
||||||
&.checked {
|
|
||||||
border-color: #faad14;
|
|
||||||
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
|
|
||||||
box-shadow: 0 2rpx 8rpx rgba(250, 173, 20, 0.3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-avatar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.avatar-circle {
|
|
||||||
width: 72rpx;
|
|
||||||
height: 72rpx;
|
|
||||||
border-radius: 18rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.male {
|
|
||||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.female {
|
|
||||||
background: linear-gradient(135deg, #fa8c16 0%, #ffa940 50%, #d46b08 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 8rpx;
|
|
||||||
right: 8rpx;
|
|
||||||
width: 12rpx;
|
|
||||||
height: 12rpx;
|
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-text {
|
|
||||||
font-size: 34rpx;
|
|
||||||
color: #fff;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 2rpx;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-info {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10rpx;
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
|
|
||||||
.item-name {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #333;
|
|
||||||
letter-spacing: 1rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.item-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12rpx;
|
|
||||||
|
|
||||||
.gender-badge {
|
|
||||||
font-size: 22rpx;
|
|
||||||
padding: 6rpx 14rpx;
|
|
||||||
border-radius: 10rpx;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
&.male {
|
|
||||||
color: #1890ff;
|
|
||||||
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(64, 169, 255, 0.05) 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.female {
|
|
||||||
color: #fa8c16;
|
|
||||||
background: linear-gradient(135deg, rgba(250, 140, 22, 0.1) 0%, rgba(255, 169, 64, 0.05) 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.age-text {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.date-divider {
|
||||||
|
width: 2rpx;
|
||||||
|
height: 50rpx;
|
||||||
|
background: #b2dfdb;
|
||||||
|
margin: 0 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date-arrow {
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 数据表格卡片 */
|
||||||
|
.data-card {
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 空状态 */
|
||||||
|
.empty-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 100rpx 40rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
margin-top: 24rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 弹窗遮罩 */
|
||||||
.modal-overlay {
|
.modal-overlay {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%);
|
background: rgba(0, 0, 0, 0.55);
|
||||||
backdrop-filter: blur(10rpx);
|
|
||||||
z-index: 998;
|
z-index: 998;
|
||||||
animation: fadeIn 0.3s ease;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==================== 表格区域 ==================== */
|
/* 学生选择弹窗 */
|
||||||
.table-section {
|
.student-picker-modal {
|
||||||
margin: 0 20rpx;
|
position: fixed;
|
||||||
background-color: #fff;
|
left: 0;
|
||||||
border-radius: 20rpx;
|
right: 0;
|
||||||
overflow: hidden;
|
bottom: 0;
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
height: 60vh;
|
||||||
|
background: #ffffff;
|
||||||
.table-wrapper-scroll {
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
width: 100%;
|
z-index: 999;
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-header {
|
|
||||||
// background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
|
|
||||||
|
|
||||||
.header-row {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
.header-cell {
|
|
||||||
min-width: 100px;
|
|
||||||
padding: 24rpx 16rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
// color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
border-right: 1rpx solid rgba(255, 255, 255, 0.1);
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-body {
|
|
||||||
.body-row {
|
|
||||||
display: flex;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&.even {
|
|
||||||
background-color: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.odd {
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
background-color: #e6f7ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.body-cell {
|
|
||||||
min-width: 100px;
|
|
||||||
padding: 24rpx 16rpx;
|
|
||||||
font-size: 26rpx;
|
|
||||||
color: #333;
|
|
||||||
text-align: center;
|
|
||||||
border-right: 1rpx solid #f0f0f0;
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-right: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ==================== 空状态容器 ==================== */
|
|
||||||
.empty-container {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
animation: slideUp 0.25s ease;
|
||||||
|
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 28rpx 24rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
background: #fafafa;
|
||||||
|
border-radius: 24rpx 24rpx 0 0;
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
padding: 10rpx 24rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
background: #eeeeee;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
&.primary {
|
||||||
|
color: #fff;
|
||||||
|
background: #faad14;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 16rpx 24rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
padding: 22rpx 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
background: #f8f9fa;
|
||||||
|
transition: background 0.2s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #eeeeee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background: #fff3e0;
|
||||||
|
border: 1rpx solid #ffcc80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-checkbox {
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.checkbox-inner {
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: 2rpx solid #bfbfbf;
|
||||||
|
background: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s;
|
||||||
|
|
||||||
|
&.checked {
|
||||||
|
border-color: #faad14;
|
||||||
|
background: #faad14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 64rpx;
|
||||||
|
height: 64rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #1890ff;
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin: 0 20rpx;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 20rpx;
|
|
||||||
padding: 80rpx 40rpx;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
||||||
|
|
||||||
.empty-icon {
|
.avatar-text {
|
||||||
margin-bottom: 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-text {
|
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #999;
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes fadeIn {
|
.item-name {
|
||||||
from {
|
flex: 1;
|
||||||
opacity: 0;
|
font-size: 30rpx;
|
||||||
}
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideUp {
|
/* 表格深度样式 */
|
||||||
from {
|
::v-deep .sl-table {
|
||||||
transform: translateY(100%);
|
width: 100%;
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .sl-table__header {
|
||||||
|
background: #faad14 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .sl-table__header__cell {
|
||||||
|
color: #fff !important;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .sl-table__body__cell {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #333;
|
||||||
|
padding: 22rpx 14rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .sl-table__body__row:nth-child(even) {
|
||||||
|
background-color: #fafafa !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .sl-table__body__row:nth-child(odd) {
|
||||||
|
background-color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分段详情弹窗 */
|
||||||
.segment-popup-content {
|
.segment-popup-content {
|
||||||
width: 620rpx;
|
width: 620rpx;
|
||||||
max-height: 70vh;
|
max-height: 70vh;
|
||||||
@@ -853,4 +719,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,8 +6,8 @@
|
|||||||
<view class="empty-state-container">
|
<view class="empty-state-container">
|
||||||
<!-- 标题区域 -->
|
<!-- 标题区域 -->
|
||||||
<view class="title-section">
|
<view class="title-section">
|
||||||
<text class="main-title">暂无项目</text>
|
<!-- <text class="main-title">暂无项目</text>
|
||||||
<text class="sub-title">您还没有创建任何训练项目,开始您的第一次游泳训练记录吧</text>
|
<text class="sub-title">您还没有创建任何训练项目,开始您的第一次游泳训练记录吧</text> -->
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 创建项目模块列表 -->
|
<!-- 创建项目模块列表 -->
|
||||||
@@ -150,6 +150,7 @@
|
|||||||
})
|
})
|
||||||
|
|
||||||
onShow(()=>{
|
onShow(()=>{
|
||||||
|
showTimingModal.value=false
|
||||||
uni.showTabBar()
|
uni.showTabBar()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -247,6 +248,7 @@
|
|||||||
showTimingModal.value = false
|
showTimingModal.value = false
|
||||||
Service.GoPage('/pages/userFunc/hunyang?id=' + project.planId)
|
Service.GoPage('/pages/userFunc/hunyang?id=' + project.planId)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -23,16 +23,15 @@
|
|||||||
<view class="stats-section">
|
<view class="stats-section">
|
||||||
<view class="stats-card">
|
<view class="stats-card">
|
||||||
<view class="stat-item">
|
<view class="stat-item">
|
||||||
|
<view @click="Service.GoPage('/pages/userFunc/projectList')" class="stat-info">
|
||||||
<view class="stat-info">
|
<text class="stat-value">{{ planCount }}</text>
|
||||||
<text class="stat-value">{{ userInfo.projectCount || 0 }}</text>
|
|
||||||
<text class="stat-label">我的项目</text>
|
<text class="stat-label">我的项目</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="stat-divider"></view>
|
<view class="stat-divider"></view>
|
||||||
<view class="stat-item">
|
<view class="stat-item">
|
||||||
<view class="stat-info">
|
<view @click="Service.GoPage('/pages/userFunc/student')" class="stat-info">
|
||||||
<text class="stat-value">{{ userInfo.studentCount || 0 }}</text>
|
<text class="stat-value">{{ studentCount }}</text>
|
||||||
<text class="stat-label">学员数</text>
|
<text class="stat-label">学员数</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -114,21 +113,24 @@
|
|||||||
import { Service } from '@/Service/Service'
|
import { Service } from '@/Service/Service'
|
||||||
import { userService } from '@/Service/swimming/userService'
|
import { userService } from '@/Service/swimming/userService'
|
||||||
|
|
||||||
|
let planCount=ref(0)
|
||||||
|
let studentCount=ref(0)
|
||||||
let userInfo = ref<any>({})
|
let userInfo = ref<any>({})
|
||||||
|
|
||||||
|
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
loadUserInfo()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
|
loadUserInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
const loadUserInfo = () => {
|
const loadUserInfo = () => {
|
||||||
userService.GetUserInfo().then((content) => {
|
userService.GetUserInfo().then((content:any) => {
|
||||||
if (content.code == 0) {
|
if (content.code == 0) {
|
||||||
|
studentCount.value=content.data.studentCount
|
||||||
|
planCount.value=content.data.planCount
|
||||||
userInfo.value=content.data.userInfo
|
userInfo.value=content.data.userInfo
|
||||||
} else {
|
} else {
|
||||||
Service.Msg(content.msg)
|
Service.Msg(content.msg)
|
||||||
@@ -314,13 +316,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
cursor: pointer;
|
|
||||||
transition: all 0.3s ease;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon-bg {
|
.stat-icon-bg {
|
||||||
|
|||||||
@@ -10,29 +10,44 @@
|
|||||||
<input class="form-input" v-model="projectName" placeholder="请输入项目名称"
|
<input class="form-input" v-model="projectName" placeholder="请输入项目名称"
|
||||||
placeholder-class="input-placeholder" />
|
placeholder-class="input-placeholder" />
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 计划总时长 -->
|
|
||||||
<view class="total-time-section">
|
|
||||||
<view class="total-time-label">计划总时长</view>
|
|
||||||
<view class="total-time-value">{{ formatTotalTime(totalDuration) }}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 计划列表 -->
|
<!-- 分组列表 -->
|
||||||
<view class="form-card">
|
<view class="form-card" v-for="(group, groupIndex) in groups" :key="group.id">
|
||||||
<view class="form-title">计划列表</view>
|
<view class="group-header">
|
||||||
|
<view class="group-title-wrapper">
|
||||||
|
<text class="form-title">{{ group.name || `分组 ${groupIndex + 1}` }}</text>
|
||||||
|
|
||||||
|
<view class="delete-group-btn" @click="deleteGroup(groupIndex)">
|
||||||
|
<u-icon name="trash" size="16" color="#fff"></u-icon>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 分组名称输入 -->
|
||||||
|
<view class="form-group" style="margin-bottom: 20rpx;">
|
||||||
|
<text class="form-label">分组名称</text>
|
||||||
|
<input class="form-input" v-model="group.name" placeholder="请输入分组名称"
|
||||||
|
placeholder-class="input-placeholder" />
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 计划项列表 -->
|
<!-- 计划项列表 -->
|
||||||
<view v-for="(item, index) in planList" :key="index" class="plan-item">
|
<view v-for="(item, planIndex) in group.plans" :key="planIndex" class="plan-item">
|
||||||
<view class="plan-item-header">
|
<view class="plan-item-header">
|
||||||
<text class="plan-item-title">计划 {{ index + 1 }}</text>
|
<text class="plan-item-title">计划 {{ planIndex + 1 }}</text>
|
||||||
<view class="delete-plan-btn" @click="deletePlanItem(index)">
|
<view class="delete-plan-btn" @click="deletePlanItem(groupIndex, planIndex)">
|
||||||
<u-icon name="trash" size="16" color="#fff"></u-icon>
|
<u-icon name="trash" size="16" color="#fff"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="plan-item-content">
|
<view class="plan-item-content">
|
||||||
<!-- 目标时间 - 分秒选择 -->
|
<!-- 计划名称 -->
|
||||||
|
<view class="input-group">
|
||||||
|
<text class="input-label">计划名称</text>
|
||||||
|
<input class="plan-name-input" v-model="item.name" placeholder="请输入计划名称" />
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 目标时间 - 秒输入 -->
|
||||||
<view class="input-group">
|
<view class="input-group">
|
||||||
<text class="input-label">目标时间</text>
|
<text class="input-label">目标时间</text>
|
||||||
<view class="time-selector">
|
<view class="time-selector">
|
||||||
@@ -43,6 +58,14 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 单项时长显示 -->
|
||||||
|
<!-- <view class="input-group" v-if="item.allTime > 0">
|
||||||
|
<text class="input-label">单项时长</text>
|
||||||
|
<view class="all-time-display">
|
||||||
|
<text class="all-time-value">{{ item.allTime }} 秒</text>
|
||||||
|
</view>
|
||||||
|
</view> -->
|
||||||
|
|
||||||
<!-- 休息时长 - 秒选择 -->
|
<!-- 休息时长 - 秒选择 -->
|
||||||
<view class="input-group">
|
<view class="input-group">
|
||||||
@@ -65,13 +88,21 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 添加计划按钮 -->
|
<!-- 添加计划按钮 -->
|
||||||
<view class="add-plan-btn" @click="addPlanItem">
|
<view class="add-plan-btn" @click="addPlanItem(groupIndex)">
|
||||||
<u-icon name="plus-circle" size="20" color="#1890ff"></u-icon>
|
<u-icon name="plus-circle" size="20" color="#1890ff"></u-icon>
|
||||||
<text class="add-plan-text">添加计划</text>
|
<text class="add-plan-text">添加计划</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
|
||||||
|
|
||||||
|
<!-- 添加分组按钮 -->
|
||||||
|
<view class="add-group-btn" @click="addGroup">
|
||||||
|
<u-icon name="plus-circle" size="24" color="#fff"></u-icon>
|
||||||
|
<text class="add-group-text">添加分组</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="" style="width: 100%; height: 80rpx;" >
|
||||||
|
|
||||||
|
</view>
|
||||||
<!-- 底部按钮区域 -->
|
<!-- 底部按钮区域 -->
|
||||||
<view class="bottom-actions">
|
<view class="bottom-actions">
|
||||||
<view class="action-buttons">
|
<view class="action-buttons">
|
||||||
@@ -85,50 +116,79 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { Service } from '@/Service/Service'
|
import { Service } from '@/Service/Service'
|
||||||
import { studentService } from '@/Service/swimming/studentService'
|
|
||||||
import { PlanService } from '@/Service/swimming/PlanService'
|
import { PlanService } from '@/Service/swimming/PlanService'
|
||||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
|
|
||||||
|
|
||||||
// 计划项接口
|
// 计划项接口
|
||||||
interface PlanItem {
|
interface PlanItem {
|
||||||
targetMinutes : string
|
name: string
|
||||||
targetSeconds : string
|
targetSeconds: string
|
||||||
restTime : string
|
restTime: string
|
||||||
lapCount : string
|
lapCount: string
|
||||||
|
allTime?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分组接口
|
||||||
|
interface Group {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
plans: PlanItem[]
|
||||||
|
allTime?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// 项目名称
|
// 项目名称
|
||||||
const projectName = ref('')
|
const projectName = ref('')
|
||||||
|
|
||||||
// 计划列表
|
// 分组列表
|
||||||
const planList = ref<PlanItem[]>([])
|
const groups = ref<Group[]>([])
|
||||||
let planId = ref('')
|
let planId = ref('')
|
||||||
|
|
||||||
onLoad((data : any) => {
|
onLoad((data: any) => {
|
||||||
planId.value = data.id
|
planId.value = data.id
|
||||||
if(planId.value ){
|
if (planId.value) {
|
||||||
getPlanInfo()
|
getPlanInfo()
|
||||||
|
} else {
|
||||||
|
// 新建时默认添加一个分组
|
||||||
|
addGroup()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 获取计划详情
|
// 获取计划详情
|
||||||
const getPlanInfo = () => {
|
const getPlanInfo = () => {
|
||||||
PlanService.GetPlanInfo(planId.value).then(res => {
|
PlanService.GetPlanInfo(planId.value).then(res => {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
projectName.value=res.data.plan.name
|
projectName.value = res.data.plan.name
|
||||||
JSON.parse(res.data.plan.project).map((item:any)=>{
|
const projectData = JSON.parse(res.data.plan.project)
|
||||||
planList.value.push({
|
// 兼容旧数据:如果 project 是数组,放入一个默认分组
|
||||||
targetMinutes:'',
|
if (Array.isArray(projectData)) {
|
||||||
targetSeconds : item.target,
|
groups.value = projectData.map((g: any, index: number) => ({
|
||||||
restTime : item.rest,
|
id: generateId(),
|
||||||
lapCount : item.circle
|
name: g.groupName || `分组 ${index + 1}`,
|
||||||
})
|
allTime: g.allTime || 0,
|
||||||
})
|
plans: (g.group || []).map((item: any) => ({
|
||||||
|
name: String(item.name || ''),
|
||||||
|
targetSeconds: String(item.target || ''),
|
||||||
|
restTime: String(item.rest || ''),
|
||||||
|
lapCount: String(item.circle || ''),
|
||||||
|
allTime: item.allTime || 0
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
} else if (projectData.groups && Array.isArray(projectData.groups)) {
|
||||||
|
// 新数据格式
|
||||||
|
groups.value = projectData.groups.map((g: any) => ({
|
||||||
|
id: g.id || generateId(),
|
||||||
|
name: g.name || '',
|
||||||
|
allTime: g.allTime || 0,
|
||||||
|
plans: (g.plans || []).map((item: any) => ({
|
||||||
|
name: String(item.name || ''),
|
||||||
|
targetSeconds: String(item.target || ''),
|
||||||
|
restTime: String(item.rest || ''),
|
||||||
|
lapCount: String(item.circle || ''),
|
||||||
|
allTime: item.allTime || 0
|
||||||
|
}))
|
||||||
|
}))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// 显示错误信息
|
|
||||||
Service.Msg(res.msg)
|
Service.Msg(res.msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -137,55 +197,75 @@
|
|||||||
// 计算计划总时长(秒)
|
// 计算计划总时长(秒)
|
||||||
const totalDuration = computed(() => {
|
const totalDuration = computed(() => {
|
||||||
let total = 0
|
let total = 0
|
||||||
planList.value.forEach(item => {
|
groups.value.forEach(group => {
|
||||||
const targetMinutes = parseInt(item.targetMinutes) || 0
|
group.plans.forEach(item => {
|
||||||
const targetSeconds = parseInt(item.targetSeconds) || 0
|
const targetSeconds = parseInt(item.targetSeconds) || 0
|
||||||
const targetTime = targetMinutes * 60 + targetSeconds
|
const restTime = parseInt(item.restTime) || 0
|
||||||
const restTime = parseInt(item.restTime) || 0
|
const lapCount = parseInt(item.lapCount) || 0
|
||||||
const lapCount = parseInt(item.lapCount) || 0
|
// 计算公式:(目标时间 + 休息时间) * 圈数
|
||||||
|
total += (targetSeconds + restTime) * lapCount
|
||||||
// 计算公式:(目标时间 + 休息时间) * 圈数
|
})
|
||||||
total += (targetTime + restTime) * lapCount
|
|
||||||
})
|
})
|
||||||
return total
|
return total
|
||||||
})
|
})
|
||||||
|
|
||||||
// 格式化总时长显示(时:分:秒)
|
// 格式化总时长显示(时:分:秒)
|
||||||
const formatTotalTime = (seconds : number) : string => {
|
const formatTotalTime = (seconds: number): string => {
|
||||||
if (seconds <= 0) return '00:00:00'
|
if (seconds <= 0) return '00:00:00'
|
||||||
|
|
||||||
const hours = Math.floor(seconds / 3600)
|
const hours = Math.floor(seconds / 3600)
|
||||||
const minutes = Math.floor((seconds % 3600) / 60)
|
const minutes = Math.floor((seconds % 3600) / 60)
|
||||||
const secs = Math.floor(seconds % 60)
|
const secs = Math.floor(seconds % 60)
|
||||||
|
|
||||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成唯一ID
|
// 生成唯一ID
|
||||||
const generateId = () : string => {
|
const generateId = (): string => {
|
||||||
return Date.now().toString() + Math.random().toString(36).substr(2, 9)
|
return Date.now().toString() + Math.random().toString(36).substr(2, 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加计划项
|
// 添加分组
|
||||||
const addPlanItem = () => {
|
const addGroup = () => {
|
||||||
const newItem : PlanItem = {
|
const newGroup: Group = {
|
||||||
id: generateId(),
|
id: generateId(),
|
||||||
targetMinutes: '',
|
name: '',
|
||||||
|
plans: []
|
||||||
|
}
|
||||||
|
groups.value.push(newGroup)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除分组
|
||||||
|
const deleteGroup = (groupIndex: number) => {
|
||||||
|
uni.showModal({
|
||||||
|
title: '确认删除',
|
||||||
|
content: '确定要删除该分组吗?分组内的所有计划也将被删除。',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
groups.value.splice(groupIndex, 1)
|
||||||
|
Service.Msg('删除成功')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加计划项
|
||||||
|
const addPlanItem = (groupIndex: number) => {
|
||||||
|
const newItem: PlanItem = {
|
||||||
|
name: '',
|
||||||
targetSeconds: '',
|
targetSeconds: '',
|
||||||
restTime: '',
|
restTime: '',
|
||||||
lapCount: ''
|
lapCount: ''
|
||||||
}
|
}
|
||||||
planList.value.push(newItem)
|
groups.value[groupIndex].plans.push(newItem)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除计划项
|
// 删除计划项
|
||||||
const deletePlanItem = (index : number) => {
|
const deletePlanItem = (groupIndex: number, planIndex: number) => {
|
||||||
uni.showModal({
|
uni.showModal({
|
||||||
title: '确认删除',
|
title: '确认删除',
|
||||||
content: '确定要删除该计划吗?',
|
content: '确定要删除该计划吗?',
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.confirm) {
|
if (res.confirm) {
|
||||||
planList.value.splice(index, 1)
|
groups.value[groupIndex].plans.splice(planIndex, 1)
|
||||||
Service.Msg('删除成功')
|
Service.Msg('删除成功')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -193,70 +273,106 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证表单
|
// 验证表单
|
||||||
const validateForm = () : boolean => {
|
const validateForm = (): boolean => {
|
||||||
if (!projectName.value.trim()) {
|
if (!projectName.value.trim()) {
|
||||||
Service.Msg('请输入项目名称')
|
Service.Msg('请输入项目名称')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (planList.value.length === 0) {
|
if (groups.value.length === 0) {
|
||||||
Service.Msg('请至少添加一个计划')
|
Service.Msg('请至少添加一个分组')
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < planList.value.length; i++) {
|
for (let g = 0; g < groups.value.length; g++) {
|
||||||
const item = planList.value[i]
|
const group = groups.value[g]
|
||||||
|
if (!group.name.trim()) {
|
||||||
if (!item.targetSeconds) {
|
Service.Msg(`分组 ${g + 1} 请输入分组名称`)
|
||||||
Service.Msg(`计划 ${i + 1} 请输入目标时间`)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const seconds = parseInt(item.targetSeconds) || 0
|
if (group.plans.length === 0) {
|
||||||
if (seconds === 0) {
|
Service.Msg(`分组 "${group.name}" 请至少添加一个计划`)
|
||||||
Service.Msg(`计划 ${i + 1} 目标时间不能为0`)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!item.restTime || parseInt(item.restTime) < 0) {
|
for (let i = 0; i < group.plans.length; i++) {
|
||||||
Service.Msg(`计划 ${i + 1} 请输入有效的休息时长`)
|
const item = group.plans[i]
|
||||||
return false
|
if (!item.name.trim()) {
|
||||||
}
|
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入计划名称`)
|
||||||
|
return false
|
||||||
if (!item.lapCount || parseInt(item.lapCount) <= 0) {
|
}
|
||||||
Service.Msg(`计划 ${i + 1} 请输入有效的圈数`)
|
if (!item.targetSeconds) {
|
||||||
return false
|
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入目标时间`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const seconds = parseInt(item.targetSeconds) || 0
|
||||||
|
if (seconds === 0) {
|
||||||
|
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 目标时间不能为0`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!item.restTime || parseInt(item.restTime) < 0) {
|
||||||
|
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入有效的休息时长`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if (!item.lapCount || parseInt(item.lapCount) <= 0) {
|
||||||
|
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入有效的圈数`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 计算单项时长
|
||||||
|
const calculatePlanAllTime = (item: PlanItem): number => {
|
||||||
|
const target = parseInt(item.targetSeconds) || 0
|
||||||
|
const rest = parseInt(item.restTime) || 0
|
||||||
|
const circle = parseInt(item.lapCount) || 0
|
||||||
|
return (target + rest) * circle
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算分组总时长
|
||||||
|
const calculateGroupAllTime = (group: Group): number => {
|
||||||
|
return group.plans.reduce((sum, item) => sum + calculatePlanAllTime(item), 0)
|
||||||
|
}
|
||||||
|
|
||||||
// 保存
|
// 保存
|
||||||
const handleSave = () => {
|
const handleSave = () => {
|
||||||
if (!validateForm()) {
|
if (!validateForm()) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let plan = planList.value.map(item => ({
|
const projectData = groups.value.map(group => {
|
||||||
target: (parseInt(item.targetMinutes) || 0) * 60 + (parseInt(item.targetSeconds) || 0),
|
const groupAllTime = calculateGroupAllTime(group)
|
||||||
rest: parseInt(item.restTime),
|
return {
|
||||||
circle: parseInt(item.lapCount)
|
allTime: groupAllTime,
|
||||||
}))
|
groupName: group.name,
|
||||||
// 整理数据
|
group: group.plans.map(item => ({
|
||||||
|
allTime: calculatePlanAllTime(item),
|
||||||
|
name: item.name,
|
||||||
|
target: parseInt(item.targetSeconds) || 0,
|
||||||
|
rest: parseInt(item.restTime) || 0,
|
||||||
|
circle: parseInt(item.lapCount) || 0
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(projectData);
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
planId: planId.value,
|
planId: planId.value,
|
||||||
name: projectName.value,
|
name: projectName.value,
|
||||||
planType: '混氧项目',
|
planType: '混氧项目',
|
||||||
departType: '',
|
departType: '',
|
||||||
interval: '',
|
interval: '',
|
||||||
groupInt: '',
|
groupInt: String(groups.value.length),
|
||||||
subsectionDistance: '',
|
subsectionDistance: '',
|
||||||
subsectionInt: '',
|
subsectionInt: '',
|
||||||
users: '',
|
users: '',
|
||||||
project: JSON.stringify(plan),
|
project: JSON.stringify(projectData),
|
||||||
group: '',
|
group: ''
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('保存的数据:', data)
|
console.log('保存的数据:', data)
|
||||||
@@ -317,6 +433,38 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 分组头部 */
|
||||||
|
.group-header {
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
|
||||||
|
.group-title-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.form-title {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 删除分组按钮 */
|
||||||
|
.delete-group-btn {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
background: linear-gradient(135deg, #ff4d4f 0%, #d9363e 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 表单输入 */
|
/* 表单输入 */
|
||||||
.form-group {
|
.form-group {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@@ -496,6 +644,45 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 计划名称输入 */
|
||||||
|
.plan-name-input {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
padding: 0 16rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分组总时长 */
|
||||||
|
.group-time-info {
|
||||||
|
margin-right: 16rpx;
|
||||||
|
padding: 6rpx 12rpx;
|
||||||
|
background: #e6f7ff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group-time-text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #1890ff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 单项时长显示 */
|
||||||
|
.all-time-display {
|
||||||
|
padding: 12rpx 16rpx;
|
||||||
|
background: #f6ffed;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
border: 1rpx solid #b7eb8f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.all-time-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #52c41a;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
/* 添加计划按钮 */
|
/* 添加计划按钮 */
|
||||||
.add-plan-btn {
|
.add-plan-btn {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -521,6 +708,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 添加分组按钮 */
|
||||||
|
.add-group-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12rpx;
|
||||||
|
padding: 28rpx 32rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
|
||||||
|
border-radius: 16rpx;
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-group-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* 底部操作按钮 */
|
/* 底部操作按钮 */
|
||||||
.bottom-actions {
|
.bottom-actions {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,32 +9,39 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 全局计时器 -->
|
<!-- 全局计时器 -->
|
||||||
<view v-if="selectedProject && setting.mode=='interval' " class="card-section global-timer-section">
|
<view v-if="selectedProject " class="card-section global-timer-section">
|
||||||
<view class="global-timer-header">
|
<!-- <view class="global-timer-header">
|
||||||
<view class="global-timer-label">全局计时</view>
|
<view class="global-timer-label">全局计时</view>
|
||||||
<view class="global-setting-btn" @click="showSetting = true">
|
<view class="global-setting-btn" @click="showSetting = true">
|
||||||
<u-icon name="setting" size="18" color="#1890ff"></u-icon>
|
<u-icon name="setting" size="18" color="#1890ff"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
|
</view> -->
|
||||||
|
|
||||||
|
<view class="global-timer-header" style="justify-content: normal;" >
|
||||||
|
<view class="project-select-text">{{ setting.mode=='interval'?'间隔出发':'一起出发' }}{{ setting.mode=='interval'?'·'+ setting.interval+'s':'' }}</view>
|
||||||
|
<view class="global-setting-btn" @click="showSetting = true">
|
||||||
|
<u-icon name="setting" size="18" color="#1890ff"></u-icon>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="global-timer-display">
|
<!-- <view class="global-timer-display">
|
||||||
<view class="global-timer-value">{{ formatGlobalTimer(globalTimerTime) }}</view>
|
<view class="global-timer-value">{{ formatGlobalTimer(globalTimerTime) }}</view>
|
||||||
</view>
|
</view> -->
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 分组计时器列表 -->
|
<!-- 分组计时器列表 -->
|
||||||
<view v-for="group in groups" :key="group.id" class="card-section">
|
<view v-for="group in groups" :key="group.id" class="card-section">
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<view class="group-title-wrapper">
|
<view class="group-title-wrapper">
|
||||||
<text class="section-title">{{ group.name }}</text>
|
<text class="section-title">{{ group.name }} · 总时长 {{ group.allTime}} </text>
|
||||||
<view class="delete-group-btn" @click="deleteGroup(group)">
|
<view class="delete-group-btn" @click="deleteGroup(group)">
|
||||||
<u-icon name="trash" size="16" color="#fff"></u-icon>
|
<u-icon name="trash" size="16" color="#fff"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="group-controls">
|
<view class="group-controls">
|
||||||
<!-- @click="openGroupSetting(group)" -->
|
<!-- @click="openGroupSetting(group)" -->
|
||||||
<!-- <view class="group-control-btn setting-btn">
|
<view @click="addTimer(group)" class="group-control-btn setting-btn">
|
||||||
<u-icon name="setting" size="18" color="#fff"></u-icon>
|
<u-icon name="plus" size="16" blod='true' color="#fff"></u-icon>
|
||||||
</view> -->
|
</view>
|
||||||
<view v-if="!isGroupAllRunning(group) && !isGroupAllCompleted(group)"
|
<view v-if="!isGroupAllRunning(group) && !isGroupAllCompleted(group)"
|
||||||
class="group-control-btn start-btn" @click="startGroup(group)">
|
class="group-control-btn start-btn" @click="startGroup(group)">
|
||||||
<u-icon name="play-circle" size="18" color="#fff"></u-icon>
|
<u-icon name="play-circle" size="18" color="#fff"></u-icon>
|
||||||
@@ -62,18 +69,16 @@
|
|||||||
<view class="circle-content-wrapper" style="margin-top: 20rpx;">
|
<view class="circle-content-wrapper" style="margin-top: 20rpx;">
|
||||||
<view class="circle-content" :class="{ resting: timer.isResting }">
|
<view class="circle-content" :class="{ resting: timer.isResting }">
|
||||||
<view class="circle-inner">
|
<view class="circle-inner">
|
||||||
<view class="timer-target" @click.stop="openTimerDetail(timer)">目标:
|
<view v-if="timer.status === 'completed'" class="timer-status-completed">
|
||||||
{{ formatTargetTime(timer.targetTime) }}
|
已完成
|
||||||
</view>
|
</view>
|
||||||
<view v-if="timer.isResting" class="timer-rest-countdown">
|
<view v-else-if="timer.isResting" class="timer-rest-countdown">
|
||||||
<text class="rest-label">休息中</text>
|
<text class="rest-label">休息</text>
|
||||||
<text
|
<text class="rest-time">{{ formatRestCountdown(timer.restCountdown || 0) }}</text>
|
||||||
class="rest-time">{{ formatRestCountdown(timer.restCountdown || 0) }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view v-else class="timer-time">{{ formatTime(timer) }}</view>
|
<view v-else class="timer-time">{{ formatTime(timer) }}</view>
|
||||||
<view class="timer-info" @click.stop="openTimerDetail(timer)">
|
<view class="timer-info">
|
||||||
<text
|
<text class="timer-duration">{{ timer.lapCount }}/{{ timer.totalLapCount || 4 }}圈</text>
|
||||||
class="timer-duration">{{ timer.lapCount }}/{{ timer.totalLapCount || 4 }}圈</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -92,11 +97,6 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="add-student-btn" @click="addTimer(group)">
|
|
||||||
<u-icon name="plus-circle" size="20" color="#1890ff"></u-icon>
|
|
||||||
<text class="add-student-text">点击添加学生到本组</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 添加分组按钮 -->
|
<!-- 添加分组按钮 -->
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部弹出框 - 秒表详情 -->
|
<!-- 底部弹出框 - 秒表详情 -->
|
||||||
<u-popup v-model:show="showTimerDetail" mode="bottom" :round="20" :closeable="true" closeOnClickOverlay>
|
<u-popup v-model:show="showTimerDetail" :safeAreaInsetTop='true' mode="bottom" :round="20" :closeable="true" closeOnClickOverlay>
|
||||||
<view class="timer-detail-popup">
|
<view class="timer-detail-popup">
|
||||||
<view class="mode-tabs">
|
<view class="mode-tabs">
|
||||||
<view class="mode-tab" :class="{ active: !isEditMode }" @click="isEditMode = false">
|
<view class="mode-tab" :class="{ active: !isEditMode }" @click="isEditMode = false">
|
||||||
@@ -145,8 +145,8 @@
|
|||||||
<view v-if="!isEditMode">
|
<view v-if="!isEditMode">
|
||||||
<view class="detail-name">{{ selectedTimer?.studentName || '计时器' }}</view>
|
<view class="detail-name">{{ selectedTimer?.studentName || '计时器' }}</view>
|
||||||
<view class="stopwatch-display">
|
<view class="stopwatch-display">
|
||||||
<text class="stopwatch-time">{{ formatStopwatchTime(selectedTimer?.currentTime || 0) }}</text>
|
<text class="stopwatch-time">{{ formatStopwatchTime(selectedTimer?.elapsedTime || 0) }}</text>
|
||||||
<text class="stopwatch-millis">{{ formatMillis(selectedTimer?.currentTime || 0) }}</text>
|
<text class="stopwatch-millis">{{ formatMillis(selectedTimer?.elapsedTime || 0) }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="current-settings-info">
|
<view class="current-settings-info">
|
||||||
<view class="setting-info-item">
|
<view class="setting-info-item">
|
||||||
@@ -183,9 +183,9 @@
|
|||||||
<text class="btn-label">{{ selectedTimer?.status === 'running' ? '暂停' : '开始' }}</text>
|
<text class="btn-label">{{ selectedTimer?.status === 'running' ? '暂停' : '开始' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-btn detail-btn-record" @click="recordLap"
|
<view class="detail-btn detail-btn-record" @click="recordLap"
|
||||||
:class="{ disabled: selectedTimer?.status !== 'running' }">
|
:class="{ disabled: selectedTimer?.status !== 'running' && selectedTimer?.status !== 'resting' }">
|
||||||
<u-icon name="edit-pen" size="24"
|
<u-icon name="edit-pen" size="24"
|
||||||
:color="selectedTimer?.status === 'running' ? '#fff' : '#bfbfbf'"></u-icon>
|
:color="selectedTimer?.status === 'running' || selectedTimer?.status === 'resting' ? '#fff' : '#bfbfbf'"></u-icon>
|
||||||
<text class="btn-label">记录</text>
|
<text class="btn-label">记录</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
</u-popup>
|
</u-popup>
|
||||||
|
|
||||||
<!-- 全局设置弹窗 -->
|
<!-- 全局设置弹窗 -->
|
||||||
<u-popup v-model:show="showSetting" mode="center" :round="16" :closeable="true" closeOnClickOverlay>
|
<u-popup v-model:show="showSetting" mode="center" :round="16" :safeAreaInsetBottom='false' :closeable="true" >
|
||||||
<view class="setting-modal">
|
<view class="setting-modal">
|
||||||
<view class="setting-title">出发设置</view>
|
<view class="setting-title">出发设置</view>
|
||||||
<view class="setting-item" :class="{ active: setting.mode === 'together' }"
|
<view class="setting-item" :class="{ active: setting.mode === 'together' }"
|
||||||
@@ -390,6 +390,8 @@
|
|||||||
id : string // 唯一标识
|
id : string // 唯一标识
|
||||||
studentId : string // 学生ID
|
studentId : string // 学生ID
|
||||||
currentTime : number // 当前累计游泳时间(秒),休息期间不累计
|
currentTime : number // 当前累计游泳时间(秒),休息期间不累计
|
||||||
|
countdownTime : number // 当前圈目标时间倒计时(秒)
|
||||||
|
elapsedTime : number // 真实经过时间(秒),用于记录成绩,休息不暂停
|
||||||
status : 'idle' | 'running' | 'paused' | 'completed' | 'resting'
|
status : 'idle' | 'running' | 'paused' | 'completed' | 'resting'
|
||||||
studentName : string // 学生姓名
|
studentName : string // 学生姓名
|
||||||
quicklyTime : number // 学生最快时间(秒)
|
quicklyTime : number // 学生最快时间(秒)
|
||||||
@@ -403,6 +405,8 @@
|
|||||||
totalLapCount : number // 总圈数
|
totalLapCount : number // 总圈数
|
||||||
restCountdown : number // 休息剩余时间(秒)
|
restCountdown : number // 休息剩余时间(秒)
|
||||||
isResting : boolean // 是否正在休息
|
isResting : boolean // 是否正在休息
|
||||||
|
startTimestamp : number // 项目开始的真实时间戳(毫秒)
|
||||||
|
lastRecordTimestamp : number // 上次记录时的真实时间戳(毫秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 分组 */
|
/** 分组 */
|
||||||
@@ -414,6 +418,7 @@
|
|||||||
targetTime : number // 分组默认pb(用于弹窗回显)
|
targetTime : number // 分组默认pb(用于弹窗回显)
|
||||||
restTime : number // 分组默认休息(用于弹窗回显)
|
restTime : number // 分组默认休息(用于弹窗回显)
|
||||||
lapCount : number // 分组默认圈数(用于弹窗回显)
|
lapCount : number // 分组默认圈数(用于弹窗回显)
|
||||||
|
allTime : number // 计划总时长(秒)
|
||||||
students : string[] // 组内学生ID列表
|
students : string[] // 组内学生ID列表
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -440,6 +445,7 @@
|
|||||||
groupName ?: string
|
groupName ?: string
|
||||||
gruopName ?: string // 接口历史拼写兼容
|
gruopName ?: string // 接口历史拼写兼容
|
||||||
group ?: PlanStudentRaw[] | string | null
|
group ?: PlanStudentRaw[] | string | null
|
||||||
|
allTime ?: number | string // 计划总时长(秒)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 接口返回的原始计划结构 */
|
/** 接口返回的原始计划结构 */
|
||||||
@@ -513,6 +519,8 @@
|
|||||||
id: createTimerId(groupId, student.studentId),
|
id: createTimerId(groupId, student.studentId),
|
||||||
studentId: student.studentId,
|
studentId: student.studentId,
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
|
countdownTime: 0,
|
||||||
|
elapsedTime: 0,
|
||||||
status: 'idle',
|
status: 'idle',
|
||||||
studentName: student.name,
|
studentName: student.name,
|
||||||
quicklyTime,
|
quicklyTime,
|
||||||
@@ -525,7 +533,9 @@
|
|||||||
restTime,
|
restTime,
|
||||||
totalLapCount,
|
totalLapCount,
|
||||||
restCountdown: 0,
|
restCountdown: 0,
|
||||||
isResting: false
|
isResting: false,
|
||||||
|
startTimestamp: 0,
|
||||||
|
lastRecordTimestamp: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,6 +547,7 @@
|
|||||||
const buildGroup = (raw : PlanGroupRaw, index : number) : Group => {
|
const buildGroup = (raw : PlanGroupRaw, index : number) : Group => {
|
||||||
const id = raw.groupId || createGroupId()
|
const id = raw.groupId || createGroupId()
|
||||||
const timers = parsePlanList(raw.group).map(s => buildTimer(s, id))
|
const timers = parsePlanList(raw.group).map(s => buildTimer(s, id))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
name: raw.groupName || raw.gruopName || `第${index + 1}组`,
|
name: raw.groupName || raw.gruopName || `第${index + 1}组`,
|
||||||
@@ -545,6 +556,7 @@
|
|||||||
targetTime: timers[0]?.pb || 5,
|
targetTime: timers[0]?.pb || 5,
|
||||||
restTime: timers[0]?.restTime || 10,
|
restTime: timers[0]?.restTime || 10,
|
||||||
lapCount: timers[0]?.totalLapCount || 4,
|
lapCount: timers[0]?.totalLapCount || 4,
|
||||||
|
allTime: toNumber(raw.allTime, 0),
|
||||||
students: timers.map(t => t.studentId)
|
students: timers.map(t => t.studentId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -575,11 +587,6 @@
|
|||||||
*/
|
*/
|
||||||
const applyPlan = (raw : PlanRaw) => {
|
const applyPlan = (raw : PlanRaw) => {
|
||||||
planSnapshot.value = { ...raw }
|
planSnapshot.value = { ...raw }
|
||||||
// ungroupedStudents.value = parsePlanList<PlanStudentRaw>(raw.users).map(s => ({
|
|
||||||
// studentId: s.studentId,
|
|
||||||
// name: s.name,
|
|
||||||
// quicklyTime: toNumber(s.quicklyTime, 0)
|
|
||||||
// }))
|
|
||||||
groups.value = parsePlanList<PlanGroupRaw>(raw.group).map((g, i) => buildGroup(g, i))
|
groups.value = parsePlanList<PlanGroupRaw>(raw.group).map((g, i) => buildGroup(g, i))
|
||||||
setting.value.mode = raw.departType === '间隔出发' ? 'interval' : 'together'
|
setting.value.mode = raw.departType === '间隔出发' ? 'interval' : 'together'
|
||||||
setting.value.interval = String(toNumber(raw.interval, 5))
|
setting.value.interval = String(toNumber(raw.interval, 5))
|
||||||
@@ -727,6 +734,33 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储每个学生的真实时间计时器 setInterval ID
|
||||||
|
* 用于 elapsedTime,不受休息影响,只在 pause/complete/reset 时停止
|
||||||
|
*/
|
||||||
|
const elapsedTimeIntervals = new Map<string, number>()
|
||||||
|
|
||||||
|
/** 启动真实时间计时器(不受休息影响) */
|
||||||
|
const startElapsedTimeTimer = (timer : TimerItem) => {
|
||||||
|
if (elapsedTimeIntervals.has(timer.studentId)) return
|
||||||
|
if (timer.startTimestamp === 0) {
|
||||||
|
timer.startTimestamp = Date.now()
|
||||||
|
}
|
||||||
|
const id = setInterval(() => {
|
||||||
|
timer.elapsedTime = (Date.now() - timer.startTimestamp) / 1000
|
||||||
|
}, 50) as unknown as number
|
||||||
|
elapsedTimeIntervals.set(timer.studentId, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 停止真实时间计时器 */
|
||||||
|
const stopElapsedTimeTimer = (studentId : string) => {
|
||||||
|
const id = elapsedTimeIntervals.get(studentId)
|
||||||
|
if (id) {
|
||||||
|
clearInterval(id)
|
||||||
|
elapsedTimeIntervals.delete(studentId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动学生主计时器
|
* 启动学生主计时器
|
||||||
* 逻辑:
|
* 逻辑:
|
||||||
@@ -745,14 +779,45 @@
|
|||||||
timer.isResting = false
|
timer.isResting = false
|
||||||
timer.restCountdown = 0
|
timer.restCountdown = 0
|
||||||
timer.hasStarted = true
|
timer.hasStarted = true
|
||||||
|
// 如果倒计时已归零,重置为目标时间
|
||||||
|
if (timer.countdownTime <= 0) {
|
||||||
|
timer.countdownTime = timer.targetTime
|
||||||
|
}
|
||||||
|
// 启动真实时间计时器(不受休息影响)
|
||||||
|
startElapsedTimeTimer(timer)
|
||||||
// 通过当前累计时间反推开始时刻,保证暂停后继续计时的连续性
|
// 通过当前累计时间反推开始时刻,保证暂停后继续计时的连续性
|
||||||
const startAt = Date.now() - timer.currentTime * 1000
|
const startAt = Date.now() - timer.currentTime * 1000
|
||||||
|
const countdownStartAt = Date.now()
|
||||||
|
const initialCountdown = timer.countdownTime
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
|
// 更新累计游泳时间
|
||||||
timer.currentTime = (Date.now() - startAt) / 1000
|
timer.currentTime = (Date.now() - startAt) / 1000
|
||||||
|
// 更新目标时间倒计时
|
||||||
|
const elapsed = (Date.now() - countdownStartAt) / 1000
|
||||||
|
timer.countdownTime = Math.max(0, initialCountdown - elapsed)
|
||||||
|
// 倒计时结束,自动完成一圈
|
||||||
|
if (timer.countdownTime <= 0 && timer.status === 'running') {
|
||||||
|
clearStudentInterval(timer.studentId)
|
||||||
|
autoCompleteLap(timer)
|
||||||
|
}
|
||||||
}, 50) as unknown as number
|
}, 50) as unknown as number
|
||||||
studentIntervals.set(timer.studentId, id)
|
studentIntervals.set(timer.studentId, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动完成一圈(倒计时结束时触发,不记录成绩)
|
||||||
|
* 注意:圈数跑完后不停止计时器,等待用户手动记录完成后再暂停
|
||||||
|
*/
|
||||||
|
const autoCompleteLap = (timer : TimerItem) => {
|
||||||
|
timer.lapCount += 1
|
||||||
|
if (timer.lapCount >= timer.totalLapCount) {
|
||||||
|
// 圈数已跑完,但不暂停计时器,继续等待用户记录
|
||||||
|
Service.Msg(`${timer.studentName} 所有圈数已完成,请记录成绩`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
startRest(timer)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 暂停学生计时器
|
* 暂停学生计时器
|
||||||
* 逻辑:
|
* 逻辑:
|
||||||
@@ -763,6 +828,7 @@
|
|||||||
const pauseStudentTimer = (timer : TimerItem) => {
|
const pauseStudentTimer = (timer : TimerItem) => {
|
||||||
clearStudentInterval(timer.studentId)
|
clearStudentInterval(timer.studentId)
|
||||||
clearStudentRestInterval(timer.studentId)
|
clearStudentRestInterval(timer.studentId)
|
||||||
|
stopElapsedTimeTimer(timer.studentId)
|
||||||
if (timer.status === 'running' || timer.status === 'resting') {
|
if (timer.status === 'running' || timer.status === 'resting') {
|
||||||
timer.status = 'paused'
|
timer.status = 'paused'
|
||||||
timer.isResting = false
|
timer.isResting = false
|
||||||
@@ -772,37 +838,41 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 启动休息倒计时
|
* 启动休息倒计时
|
||||||
* 触发时机:学生完成一圈记录后(且未完成总圈数)
|
* 触发时机:学生完成一圈后(自动完成,不记录成绩)
|
||||||
*
|
*
|
||||||
* 逻辑:
|
* 逻辑:
|
||||||
* 1. 先停止主计时器(休息时间不计入 currentTime)
|
* 1. 先停止主计时器(休息时间不计入 currentTime)
|
||||||
* 2. 设置状态为 resting,开始倒计时
|
* 2. 不停止 elapsedTime 计时器(休息期间继续计时)
|
||||||
* 3. 每50ms更新 restCountdown
|
* 3. 设置状态为 resting,开始倒计时
|
||||||
* 4. 倒计时结束后自动调用 startStudentTimer 恢复计时
|
* 4. 每50ms更新 restCountdown
|
||||||
|
* 5. 倒计时结束后重置 countdownTime 并自动调用 startStudentTimer 开始下一轮
|
||||||
*/
|
*/
|
||||||
const startRest = (timer : TimerItem) => {
|
const startRest = (timer : TimerItem) => {
|
||||||
const restTime = timer.restTime || 0
|
const restTime = timer.restTime || 0
|
||||||
if (restTime <= 0) {
|
|
||||||
Service.Msg('已记录')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clearStudentInterval(timer.studentId)
|
clearStudentInterval(timer.studentId)
|
||||||
clearStudentRestInterval(timer.studentId)
|
clearStudentRestInterval(timer.studentId)
|
||||||
timer.status = 'resting'
|
timer.status = 'resting'
|
||||||
timer.isResting = true
|
timer.isResting = true
|
||||||
timer.restCountdown = restTime
|
timer.restCountdown = restTime
|
||||||
|
// 如果没有休息时间,直接开始下一轮
|
||||||
|
if (restTime <= 0) {
|
||||||
|
timer.countdownTime = timer.targetTime
|
||||||
|
startStudentTimer(timer)
|
||||||
|
return
|
||||||
|
}
|
||||||
const restStart = Date.now()
|
const restStart = Date.now()
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
const remaining = Math.max(0, restTime - (Date.now() - restStart) / 1000)
|
const remaining = Math.max(0, restTime - (Date.now() - restStart) / 1000)
|
||||||
timer.restCountdown = remaining
|
timer.restCountdown = remaining
|
||||||
if (remaining <= 0) {
|
if (remaining <= 0) {
|
||||||
clearStudentRestInterval(timer.studentId)
|
clearStudentRestInterval(timer.studentId)
|
||||||
// 休息结束,自动开始下一圈计时
|
// 休息结束,重置倒计时并开始下一轮
|
||||||
|
timer.countdownTime = timer.targetTime
|
||||||
startStudentTimer(timer)
|
startStudentTimer(timer)
|
||||||
}
|
}
|
||||||
}, 50) as unknown as number
|
}, 50) as unknown as number
|
||||||
studentRestIntervals.set(timer.studentId, id)
|
studentRestIntervals.set(timer.studentId, id)
|
||||||
Service.Msg(`已记录,休息${restTime}秒`)
|
Service.Msg(`完成一圈,休息${restTime}秒`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -812,39 +882,45 @@
|
|||||||
const completeTimer = (timer : TimerItem) => {
|
const completeTimer = (timer : TimerItem) => {
|
||||||
clearStudentInterval(timer.studentId)
|
clearStudentInterval(timer.studentId)
|
||||||
clearStudentRestInterval(timer.studentId)
|
clearStudentRestInterval(timer.studentId)
|
||||||
|
stopElapsedTimeTimer(timer.studentId)
|
||||||
timer.status = 'completed'
|
timer.status = 'completed'
|
||||||
timer.isResting = false
|
timer.isResting = false
|
||||||
timer.restCountdown = 0
|
timer.restCountdown = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 记录学生一圈
|
* 记录学生成绩(手动点击,不增加圈数)
|
||||||
* 触发时机:点击学生圆形卡片、或详情弹窗点击"记录"
|
* 触发时机:点击学生圆形卡片、或详情弹窗点击"记录"
|
||||||
*
|
*
|
||||||
* 逻辑:
|
* 逻辑:
|
||||||
* 1. 校验:休息中和未运行时不允许记录
|
* 1. 校验:未运行/未休息时不能记录
|
||||||
* 2. 将当前 currentTime 推入 records
|
* 2. 如果记录数已达上限,不能记录
|
||||||
* 3. lapCount + 1
|
* 3. 计算净游泳时间 = elapsedTime - restTime * 已记录数 - sum(所有已记录时间)
|
||||||
* 4. 如果达到总圈数,标记完成并提示
|
* 记录1 = elapsedTime
|
||||||
* 5. 否则进入休息倒计时
|
* 记录2 = elapsedTime - restTime - 记录1
|
||||||
|
* 记录3 = elapsedTime - restTime*2 - 记录1 - 记录2
|
||||||
|
* 以此类推
|
||||||
|
* 4. 当前圈继续运行,不进入下一轮
|
||||||
*/
|
*/
|
||||||
const recordLapForTimer = (timer : TimerItem) => {
|
const recordLapForTimer = (timer : TimerItem) => {
|
||||||
if (timer.status === 'resting') {
|
if (timer.status !== 'running' && timer.status !== 'resting') {
|
||||||
Service.Msg('休息中,请等待倒计时结束')
|
Service.Msg('请在运行或休息中记录')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (timer.status !== 'running') {
|
if (timer.records.length >= timer.totalLapCount) {
|
||||||
Service.Msg('请在运行中记录')
|
Service.Msg('已达到最大记录数')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
timer.records.push({ time: timer.currentTime })
|
const sumRecords = timer.records.reduce((sum, r) => sum + r.time, 0)
|
||||||
timer.lapCount += 1
|
const usedTime = Math.max(0, timer.elapsedTime - (timer.restTime * timer.records.length) - sumRecords)
|
||||||
if (timer.lapCount >= timer.totalLapCount) {
|
timer.records.push({ time: usedTime })
|
||||||
completeTimer(timer)
|
// 判断记录数是否等于圈数,等于则暂停计时器
|
||||||
Service.Msg(`${timer.studentName} 已完成`)
|
if (timer.records.length >= timer.totalLapCount) {
|
||||||
|
pauseStudentTimer(timer)
|
||||||
|
Service.Msg(`${timer.studentName} 已完成所有记录`)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
startRest(timer)
|
Service.Msg(`${timer.studentName} 已记录`)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -854,12 +930,16 @@
|
|||||||
const resetTimer = (timer : TimerItem) => {
|
const resetTimer = (timer : TimerItem) => {
|
||||||
pauseStudentTimer(timer)
|
pauseStudentTimer(timer)
|
||||||
timer.currentTime = 0
|
timer.currentTime = 0
|
||||||
|
timer.countdownTime = 0
|
||||||
|
timer.elapsedTime = 0
|
||||||
timer.status = 'idle'
|
timer.status = 'idle'
|
||||||
timer.records = []
|
timer.records = []
|
||||||
timer.lapCount = 0
|
timer.lapCount = 0
|
||||||
timer.hasStarted = false
|
timer.hasStarted = false
|
||||||
timer.isResting = false
|
timer.isResting = false
|
||||||
timer.restCountdown = 0
|
timer.restCountdown = 0
|
||||||
|
timer.startTimestamp = 0
|
||||||
|
timer.lastRecordTimestamp = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -909,7 +989,7 @@
|
|||||||
* 5. 一旦 globalTimerTime >= 分组出发时间,且分组未出发过,则启动该分组内所有未开始的学生计时器
|
* 5. 一旦 globalTimerTime >= 分组出发时间,且分组未出发过,则启动该分组内所有未开始的学生计时器
|
||||||
*/
|
*/
|
||||||
const startGlobalTimer = () => {
|
const startGlobalTimer = () => {
|
||||||
stopGlobalTimer()
|
// stopGlobalTimer()
|
||||||
globalTimerRunning.value = true
|
globalTimerRunning.value = true
|
||||||
globalStartAt = Date.now()
|
globalStartAt = Date.now()
|
||||||
globalBaseTime = globalTimerTime.value
|
globalBaseTime = globalTimerTime.value
|
||||||
@@ -960,10 +1040,12 @@
|
|||||||
* 将分组内所有正在运行或休息的学生计时器暂停
|
* 将分组内所有正在运行或休息的学生计时器暂停
|
||||||
*/
|
*/
|
||||||
const pauseGroup = (group : Group) => {
|
const pauseGroup = (group : Group) => {
|
||||||
|
group.hasStarted = false
|
||||||
group.timers.forEach(t => {
|
group.timers.forEach(t => {
|
||||||
if (t.status === 'running' || t.status === 'resting') {
|
if (t.status === 'running' || t.status === 'resting') {
|
||||||
pauseStudentTimer(t)
|
pauseStudentTimer(t)
|
||||||
}
|
}
|
||||||
|
t.hasStarted = false
|
||||||
})
|
})
|
||||||
Service.Msg(`${group.name} 已暂停`)
|
Service.Msg(`${group.name} 已暂停`)
|
||||||
}
|
}
|
||||||
@@ -994,10 +1076,12 @@
|
|||||||
*/
|
*/
|
||||||
const pauseAllTimers = () => {
|
const pauseAllTimers = () => {
|
||||||
stopGlobalTimer()
|
stopGlobalTimer()
|
||||||
|
groups.value.forEach(g => g.hasStarted = false)
|
||||||
getAllTimers.value.forEach(t => {
|
getAllTimers.value.forEach(t => {
|
||||||
if (t.status === 'running' || t.status === 'resting') {
|
if (t.status === 'running' || t.status === 'resting') {
|
||||||
pauseStudentTimer(t)
|
pauseStudentTimer(t)
|
||||||
}
|
}
|
||||||
|
t.hasStarted = false
|
||||||
})
|
})
|
||||||
Service.Msg('全部已暂停')
|
Service.Msg('全部已暂停')
|
||||||
}
|
}
|
||||||
@@ -1268,6 +1352,8 @@
|
|||||||
* 分组内所有学生恢复为未分组状态,并停止他们的计时器
|
* 分组内所有学生恢复为未分组状态,并停止他们的计时器
|
||||||
*/
|
*/
|
||||||
const deleteGroup = (group : Group) => {
|
const deleteGroup = (group : Group) => {
|
||||||
|
console.log(group);
|
||||||
|
|
||||||
let index = selectedProject.value.group.findIndex((item : any) => {
|
let index = selectedProject.value.group.findIndex((item : any) => {
|
||||||
return item.groupId === group.id
|
return item.groupId === group.id
|
||||||
})
|
})
|
||||||
@@ -1429,8 +1515,6 @@
|
|||||||
*/
|
*/
|
||||||
const submitData = () => {
|
const submitData = () => {
|
||||||
pauseAllTimers()
|
pauseAllTimers()
|
||||||
|
|
||||||
|
|
||||||
console.log(getAllTimers.value);
|
console.log(getAllTimers.value);
|
||||||
const hasData = getAllTimers.value.some(t => t.currentTime > 0)
|
const hasData = getAllTimers.value.some(t => t.currentTime > 0)
|
||||||
if (!hasData) { Service.Msg('暂无数据可提交'); return }
|
if (!hasData) { Service.Msg('暂无数据可提交'); return }
|
||||||
@@ -1462,6 +1546,9 @@
|
|||||||
PlanService.AddPlanLog(selectedProject.value.planId,'包干项目','','',JSON.stringify(data)).then(res=>{
|
PlanService.AddPlanLog(selectedProject.value.planId,'包干项目','','',JSON.stringify(data)).then(res=>{
|
||||||
if(res.code==0){
|
if(res.code==0){
|
||||||
Service.Msg('提交成功!')
|
Service.Msg('提交成功!')
|
||||||
|
setTimeout(()=>{
|
||||||
|
Service.GoPageBack()
|
||||||
|
},1000)
|
||||||
}else{
|
}else{
|
||||||
Service.Msg(res.msg)
|
Service.Msg(res.msg)
|
||||||
}
|
}
|
||||||
@@ -1470,11 +1557,21 @@
|
|||||||
|
|
||||||
// ===================== 格式化函数 =====================
|
// ===================== 格式化函数 =====================
|
||||||
|
|
||||||
/** 学生卡片主计时显示:分:秒:毫秒 */
|
/** 学生卡片主计时显示(目标时间倒计时):分:秒:毫秒 */
|
||||||
const formatTime = (timer : TimerItem) : string => {
|
const formatTime = (timer : TimerItem) : string => {
|
||||||
const mins = Math.floor(timer.currentTime / 60)
|
const time = Math.max(0, timer.countdownTime || 0)
|
||||||
const secs = Math.floor(timer.currentTime % 60)
|
const mins = Math.floor(time / 60)
|
||||||
const millis = Math.floor((timer.currentTime % 1) * 100)
|
const secs = Math.floor(time % 60)
|
||||||
|
const millis = Math.floor((time % 1) * 100)
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 计时器显示(真实经过时间):分:秒:毫秒 */
|
||||||
|
const formatElapsedTime = (seconds : number) : string => {
|
||||||
|
const time = seconds || 0
|
||||||
|
const mins = Math.floor(time / 60)
|
||||||
|
const secs = Math.floor(time % 60)
|
||||||
|
const millis = Math.floor((time % 1) * 100)
|
||||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
|
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1524,14 +1621,10 @@
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 详情弹窗中每条记录的单圈时间
|
* 详情弹窗中每条记录的单圈时间
|
||||||
* 逻辑:第1条 = 记录时刻;第N条 = 记录时刻 - 上一条记录时刻
|
* records 中存储的已经是净游泳时间(已扣除休息时间),直接显示即可
|
||||||
*/
|
*/
|
||||||
const formatAdjustedRecordTime = (seconds : number, index : number) : string => {
|
const formatAdjustedRecordTime = (seconds : number, index : number) : string => {
|
||||||
let adjusted = seconds
|
return formatFullTime(seconds)
|
||||||
if (index > 0 && selectedTimer.value) {
|
|
||||||
adjusted = seconds - selectedTimer.value.records[index - 1].time
|
|
||||||
}
|
|
||||||
return formatFullTime(adjusted)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1542,8 +1635,10 @@
|
|||||||
stopGlobalTimer()
|
stopGlobalTimer()
|
||||||
studentIntervals.forEach(id => clearInterval(id))
|
studentIntervals.forEach(id => clearInterval(id))
|
||||||
studentRestIntervals.forEach(id => clearInterval(id))
|
studentRestIntervals.forEach(id => clearInterval(id))
|
||||||
|
elapsedTimeIntervals.forEach(id => clearInterval(id))
|
||||||
studentIntervals.clear()
|
studentIntervals.clear()
|
||||||
studentRestIntervals.clear()
|
studentRestIntervals.clear()
|
||||||
|
elapsedTimeIntervals.clear()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -1813,7 +1908,7 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: 20rpx;
|
// margin-bottom: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.global-timer-label {
|
.global-timer-label {
|
||||||
@@ -2085,7 +2180,7 @@
|
|||||||
right: 0;
|
right: 0;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 20rpx 30rpx;
|
padding: 20rpx 30rpx;
|
||||||
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
// padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||||
border-top: 1rpx solid #f0f0f0;
|
border-top: 1rpx solid #f0f0f0;
|
||||||
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
|
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
@@ -2147,7 +2242,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timer-detail-popup {
|
.timer-detail-popup {
|
||||||
padding: 50rpx 40rpx;
|
padding: 0rpx 40rpx 50rpx;
|
||||||
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
|
padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
|
||||||
min-height: 450rpx;
|
min-height: 450rpx;
|
||||||
max-height: 75vh;
|
max-height: 75vh;
|
||||||
|
|||||||
@@ -33,10 +33,15 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="project-stats">
|
<view class="project-stats">
|
||||||
<view class="stat-badge">
|
<view v-if="project.planType=='混氧项目'" class="stat-badge">
|
||||||
|
<u-icon name="account" size="14" color="#1890ff"></u-icon>
|
||||||
|
<text class="badge-text">{{ JSON.parse(project.project).length }}个计划</text>
|
||||||
|
</view>
|
||||||
|
<view v-else class="stat-badge">
|
||||||
<u-icon name="account" size="14" color="#1890ff"></u-icon>
|
<u-icon name="account" size="14" color="#1890ff"></u-icon>
|
||||||
<text class="badge-text">{{ project.users.length }}位学员</text>
|
<text class="badge-text">{{ project.users.length }}位学员</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -90,7 +95,14 @@
|
|||||||
|
|
||||||
// 查看项目详情
|
// 查看项目详情
|
||||||
const viewProjectDetail = (project : any) => {
|
const viewProjectDetail = (project : any) => {
|
||||||
Service.Msg(`查看「${project.name}」详情`)
|
|
||||||
|
if(project.planType=='分段项目'){
|
||||||
|
Service.GoPage('/pages/userFunc/segmentation?id=' + project.planId)
|
||||||
|
}else if(project.planType=='计时项目'){
|
||||||
|
Service.GoPage('/pages/userFunc/swiming?id=' + project.planId)
|
||||||
|
}else{
|
||||||
|
Service.GoPage('/pages/userFunc/hunyang?id=' + project.planId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除项目
|
// 删除项目
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
<view class="config-header">
|
<view class="config-header">
|
||||||
<text class="config-title">分段设置</text>
|
<text class="config-title">分段设置</text>
|
||||||
|
|
||||||
<u-icon @click="Service.GoPage('/pages/userFunc/setCourse?id='+planId+'&type=2')" name="setting" size="24"
|
<u-icon @click="Service.GoPage('/pages/userFunc/setCourse?id='+planId+'&type=2')" name="setting"
|
||||||
color="#1890ff"></u-icon>
|
size="24" color="#1890ff"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
|
||||||
<view class="config-info">
|
<view class="config-info">
|
||||||
<text class="info-text">总距离: {{ totalDistance }}米 ({{ segmentCount }}段 × {{ segmentDistance }}米)</text>
|
<view class="info-text">总距离: {{ totalDistance }}米 ({{ segmentCount }}段 × {{ segmentDistance }}米) {{ startMode==0?'一起出发':'间隔出发' }} {{ startMode==0?'':' · '+intervalTime+'s' }} </view>
|
||||||
|
<view class="info-text" style="margin-top: 14rpx;" >启动模式: {{ startMode==0?'一起出发':'间隔出发' }} {{ startMode==0?'':' · '+intervalTime+'s' }} </view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 学生列表 -->
|
<!-- 学生列表 -->
|
||||||
<view class="students-section">
|
<view class="students-section" style="margin-top: 20rpx;" >
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<text class="section-title">学生列表</text>
|
<text class="section-title">学生列表</text>
|
||||||
<text class="student-count">{{ students.length }}位学生</text>
|
<text class="student-count">{{ students.length }}位学生</text>
|
||||||
@@ -171,10 +172,10 @@
|
|||||||
let planId = ref('')
|
let planId = ref('')
|
||||||
onLoad((data : any) => {
|
onLoad((data : any) => {
|
||||||
planId.value = data.id
|
planId.value = data.id
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
onShow(()=>{
|
onShow(() => {
|
||||||
getPlanInfo()
|
getPlanInfo()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -183,17 +184,17 @@
|
|||||||
PlanService.GetPlanInfo(planId.value).then(res => {
|
PlanService.GetPlanInfo(planId.value).then(res => {
|
||||||
if (res.code == 0) {
|
if (res.code == 0) {
|
||||||
// planName.value = res.data.plan.name
|
// planName.value = res.data.plan.name
|
||||||
segmentDistance.value=res.data.plan.subsectionDistance/res.data.plan.subsectionInt
|
segmentDistance.value = res.data.plan.subsectionDistance / res.data.plan.subsectionInt
|
||||||
segmentCount.value=res.data.plan.subsectionInt
|
segmentCount.value = res.data.plan.subsectionInt
|
||||||
// 将计划数据转换为选手数据
|
// 将计划数据转换为选手数据
|
||||||
// athletes.value = res.data.plan.users.
|
// athletes.value = res.data.plan.users.
|
||||||
students.value=res.data.plan.users.map((item:any,index:any)=>{
|
students.value = res.data.plan.users.map((item : any, index : any) => {
|
||||||
return {
|
return {
|
||||||
id: item.studentId,
|
id: item.studentId,
|
||||||
number: index+1,
|
number: index + 1,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
segments: [],
|
segments: [],
|
||||||
hasStarted: res.data.plan.departType == '间隔出发'?false:true,
|
hasStarted: res.data.plan.departType == '间隔出发' ? false : true,
|
||||||
startTime: 0
|
startTime: 0
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -230,10 +231,12 @@
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// 格式化时间差
|
// 格式化时间差(详情弹窗用)
|
||||||
|
// 第一段显示累计时间,后续段显示分段用时
|
||||||
const formatTimeDiff = (index : number, currentTime : number) : string => {
|
const formatTimeDiff = (index : number, currentTime : number) : string => {
|
||||||
if (index === 0) {
|
if (index === 0) {
|
||||||
return '00:00:00'
|
// 第一段显示累计时间
|
||||||
|
return formatTime(currentTime)
|
||||||
}
|
}
|
||||||
if (!currentStudent.value || !currentStudent.value.segments[index - 1]) {
|
if (!currentStudent.value || !currentStudent.value.segments[index - 1]) {
|
||||||
return '00:00:00'
|
return '00:00:00'
|
||||||
@@ -243,12 +246,19 @@
|
|||||||
return formatTime(diff)
|
return formatTime(diff)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取学生最后一次记录的累计时间
|
// 获取学生最后一次记录的分段用时
|
||||||
|
// 第一段返回累计时间,后续段返回当前段用时(当前累计 - 上一次累计)
|
||||||
const getLastSegmentTime = (student : any) : number => {
|
const getLastSegmentTime = (student : any) : number => {
|
||||||
if (!student.segments || student.segments.length === 0) {
|
if (!student.segments || student.segments.length === 0) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
return student.segments[student.segments.length - 1].time
|
const lastIndex = student.segments.length - 1
|
||||||
|
if (lastIndex === 0) {
|
||||||
|
// 第一段,返回累计时间
|
||||||
|
return student.segments[0].time
|
||||||
|
}
|
||||||
|
// 后续段,返回分段用时
|
||||||
|
return student.segments[lastIndex].time - student.segments[lastIndex - 1].time
|
||||||
}
|
}
|
||||||
|
|
||||||
// 简化时间格式化(分:秒)
|
// 简化时间格式化(分:秒)
|
||||||
@@ -374,8 +384,8 @@
|
|||||||
currentTime.value = 0
|
currentTime.value = 0
|
||||||
students.value.forEach(student => {
|
students.value.forEach(student => {
|
||||||
student.segments = []
|
student.segments = []
|
||||||
student.hasStarted = startMode.value == 1?false:true,
|
student.hasStarted = startMode.value == 1 ? false : true,
|
||||||
student.startTime = 0
|
student.startTime = 0
|
||||||
})
|
})
|
||||||
Service.Msg('已全部重置')
|
Service.Msg('已全部重置')
|
||||||
}
|
}
|
||||||
@@ -384,15 +394,17 @@
|
|||||||
const saveData = () => {
|
const saveData = () => {
|
||||||
// 检查是否有记录的数据
|
// 检查是否有记录的数据
|
||||||
const hasData = students.value.some(s => s.segments.length > 0)
|
const hasData = students.value.some(s => s.segments.length > 0)
|
||||||
|
let isSave = students.value.every(s => s.segments.length >= segmentCount.value)
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
Service.Msg('暂无数据可保存')
|
Service.Msg('暂无数据可保存!')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isSave) {
|
||||||
|
Service.Msg('存在学生未完成!')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(students.value);
|
|
||||||
|
|
||||||
// 这里可以添加保存到后端或本地存储的逻辑
|
|
||||||
Service.Msg('保存成功', 'success')
|
|
||||||
|
|
||||||
let data = [
|
let data = [
|
||||||
{
|
{
|
||||||
@@ -405,22 +417,27 @@
|
|||||||
students.value.map((item : any) => {
|
students.value.map((item : any) => {
|
||||||
let record = item.segments.map((content : any, index : any) => {
|
let record = item.segments.map((content : any, index : any) => {
|
||||||
return {
|
return {
|
||||||
circle: (index + 1)*segmentDistance.value,
|
circle: (index + 1) * segmentDistance.value,
|
||||||
time: content.time
|
time: content.time
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
data.push({
|
data.push({
|
||||||
studentId: item.id,
|
studentId: item.id,
|
||||||
studentName: item.name,
|
studentName: item.name,
|
||||||
data: record
|
data: record
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
PlanService.AddPlanLog(planId.value,'分段项目','',JSON.stringify(data),'').then(res=>{
|
console.log(data);
|
||||||
if(res.code==0){
|
|
||||||
|
PlanService.AddPlanLog(planId.value, '分段项目', '', JSON.stringify(data), '').then(res => {
|
||||||
|
if (res.code == 0) {
|
||||||
Service.Msg('提交成功!')
|
Service.Msg('提交成功!')
|
||||||
}else{
|
setTimeout(()=>{
|
||||||
|
Service.GoPageBack()
|
||||||
|
},1000)
|
||||||
|
} else {
|
||||||
Service.Msg(res.msg)
|
Service.Msg(res.msg)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -495,15 +512,14 @@
|
|||||||
|
|
||||||
/* 总计时器区域 */
|
/* 总计时器区域 */
|
||||||
.total-time-section {
|
.total-time-section {
|
||||||
margin-top: 20rpx;
|
|
||||||
margin-bottom: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.timer-bar {
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 20rpx;
|
border-radius: 20rpx;
|
||||||
padding: 30rpx;
|
padding: 30rpx;
|
||||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timer-bar {
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<view class="form-title">项目信息</view>
|
<view class="form-title">项目信息</view>
|
||||||
<view class="form-group">
|
<view class="form-group">
|
||||||
<text class="form-label">项目名称</text>
|
<text class="form-label">项目名称</text>
|
||||||
<input class="form-input" v-model="courseData.projectName" placeholder="请输入项目名称"
|
<input class="form-input" :disabled="planId" v-model="courseData.projectName" placeholder="请输入项目名称"
|
||||||
placeholder-class="input-placeholder" />
|
placeholder-class="input-placeholder" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -115,11 +115,9 @@
|
|||||||
@click="toggleStudentSelect(student.studentId)">
|
@click="toggleStudentSelect(student.studentId)">
|
||||||
<view class="student-checkbox"
|
<view class="student-checkbox"
|
||||||
:class="{ checked: selectedStudentIds.includes(student.studentId) }">
|
:class="{ checked: selectedStudentIds.includes(student.studentId) }">
|
||||||
<u-icon v-if="selectedStudentIds.includes(student.studentId)" name="checkmark" size="14"
|
<text v-if="selectedStudentIds.includes(student.studentId)" class="checkbox-index">
|
||||||
color="#fff"></u-icon>
|
{{ selectedStudentIds.indexOf(student.studentId) + 1 }}
|
||||||
</view>
|
</text>
|
||||||
<view class="student-avatar">
|
|
||||||
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
|
|
||||||
</view>
|
</view>
|
||||||
<view class="student-info">
|
<view class="student-info">
|
||||||
<view class="student-name">{{ student.name }}</view>
|
<view class="student-name">{{ student.name }}</view>
|
||||||
@@ -127,21 +125,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 已选学生预览 -->
|
|
||||||
<view v-if="selectedStudents.length > 0" class="selected-preview">
|
|
||||||
<view class="preview-header">
|
|
||||||
<text class="preview-title">已选学生</text>
|
|
||||||
</view>
|
|
||||||
<view class="preview-list">
|
|
||||||
<view v-for="(student, index) in selectedStudents" :key="student.studentId" class="preview-item">
|
|
||||||
<text class="preview-index">{{ index + 1 }}</text>
|
|
||||||
<text class="preview-name">{{ student.name }}</text>
|
|
||||||
<view class="preview-remove" @click.stop="removeSelectedStudent(student.studentId)">
|
|
||||||
<u-icon name="close" size="14" color="#ff4d4f"></u-icon>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="" style="width: 100%; height: 80rpx;">
|
<view class="" style="width: 100%; height: 80rpx;">
|
||||||
@@ -326,10 +310,20 @@
|
|||||||
|
|
||||||
PlanService.AddPlan(data).then(res=>{
|
PlanService.AddPlan(data).then(res=>{
|
||||||
if(res.code==0){
|
if(res.code==0){
|
||||||
Service.Msg('添加成功!')
|
Service.Msg( planId.value?'修改成功!': '添加成功!')
|
||||||
setTimeout(()=>{
|
if(planId.value){
|
||||||
Service.GoPageBack()
|
Service.GoPageBack()
|
||||||
},1000)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(type.value=='1'){
|
||||||
|
Service.GoPageDelse('/pages/userFunc/swiming?id=' + res.data.planId)
|
||||||
|
}else{
|
||||||
|
Service.GoPageDelse('/pages/userFunc/segmentation?id=' + res.data.planId)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}else{
|
}else{
|
||||||
Service.Msg(res.msg)
|
Service.Msg(res.msg)
|
||||||
}
|
}
|
||||||
@@ -697,14 +691,15 @@
|
|||||||
/* 学生列表 */
|
/* 学生列表 */
|
||||||
.student-list {
|
.student-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-wrap: wrap;
|
||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
|
|
||||||
.student-item {
|
.student-item {
|
||||||
|
width: calc((100% - 24rpx) / 3);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16rpx;
|
gap: 8rpx;
|
||||||
padding: 20rpx;
|
padding: 16rpx 8rpx;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
border: 2rpx solid transparent;
|
border: 2rpx solid transparent;
|
||||||
@@ -720,8 +715,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.student-checkbox {
|
.student-checkbox {
|
||||||
width: 40rpx;
|
width: 36rpx;
|
||||||
height: 40rpx;
|
height: 36rpx;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
border: 2rpx solid #d9d9d9;
|
border: 2rpx solid #d9d9d9;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -734,6 +729,13 @@
|
|||||||
border-color: #1890ff;
|
border-color: #1890ff;
|
||||||
background-color: #1890ff;
|
background-color: #1890ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.checkbox-index {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.student-avatar {
|
.student-avatar {
|
||||||
@@ -745,7 +747,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.avatar-text {
|
.avatar-text {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
@@ -754,14 +756,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.student-info {
|
.student-info {
|
||||||
flex: 1;
|
width: 100%;
|
||||||
min-width: 0;
|
|
||||||
|
|
||||||
.student-name {
|
.student-name {
|
||||||
font-size: 28rpx;
|
font-size: 26rpx;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin-bottom: 6rpx;
|
|
||||||
display: block;
|
display: block;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -771,6 +770,7 @@
|
|||||||
.student-meta {
|
.student-meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
gap: 12rpx;
|
gap: 12rpx;
|
||||||
|
|
||||||
.gender-badge {
|
.gender-badge {
|
||||||
|
|||||||
@@ -264,19 +264,19 @@
|
|||||||
|
|
||||||
// 构造接口请求数据
|
// 构造接口请求数据
|
||||||
const requestData = {
|
const requestData = {
|
||||||
studentId:formData.value.id,
|
studentId:formData.value.id?formData.value.id:'' ,
|
||||||
name: formData.value.name,
|
name: formData.value.name,
|
||||||
sex: formData.value.gender,
|
sex: formData.value.gender,
|
||||||
birthday: formData.value.birthDate,
|
birthday: formData.value.birthDate,
|
||||||
school: formData.value.school ,
|
school: formData.value.school?formData.value.school:'' ,
|
||||||
address: formData.value.address
|
address: formData.value.address ?formData.value.address:''
|
||||||
}
|
}
|
||||||
console.log(requestData);
|
console.log(requestData);
|
||||||
|
|
||||||
// 调用添加学员接口
|
// 调用添加学员接口
|
||||||
studentService.Add(requestData).then((content) => {
|
studentService.Add(requestData).then((content) => {
|
||||||
if (content.code == 0) {
|
if (content.code == 0) {
|
||||||
Service.Msg('添加成功')
|
Service.Msg(formData.value.id?'修改成功!':'添加成功!')
|
||||||
getData()
|
getData()
|
||||||
} else {
|
} else {
|
||||||
Service.Msg(content.msg)
|
Service.Msg(content.msg)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
<!-- 选手列表 -->
|
<!-- 选手列表 -->
|
||||||
<view class="athletes-section">
|
<view class="athletes-section">
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<text class="section-title">间隔出发·{{ groupedAthletes.length }}组</text>
|
<text class="section-title">{{ stopwatchMode=='interval'?'间隔触发':'一起出发' }}·共{{ groupedAthletes.length }}组</text>
|
||||||
<text class="athlete-count">{{ athletes.length }}位选手</text>
|
<text class="athlete-count">{{ athletes.length }}位选手</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
<text
|
<text
|
||||||
class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text>
|
class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text>
|
||||||
<text class="best-time">最快:
|
<text class="best-time">最快:
|
||||||
{{ athlete.bestTime !== null ? formatTime(athlete.quicklyTime) : '无' }}</text>
|
{{ athlete.bestTime !== null ? athlete.quicklyTime: '无' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<view class="" style="display: flex;align-items: center; gap: 10rpx;">
|
<view class="" style="display: flex;align-items: center; gap: 10rpx;">
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 记录提示弹窗 -->
|
<!-- 记录提示弹窗 -->
|
||||||
<u-popup :show="showRecord" mode="center" :round="20" :closeable="true" @close="showRecord=false" closeOnClickOverlay>
|
<u-popup :show="showRecord" mode="center" :round="20" :closeable="true" :safeAreaInsetBottom='false' @close="showRecord=false" closeOnClickOverlay>
|
||||||
<view class="record-notice-modal">
|
<view class="record-notice-modal">
|
||||||
<view class="notice-header">
|
<view class="notice-header">
|
||||||
<text class="notice-title">恭喜以下学生打破记录</text>
|
<text class="notice-title">恭喜以下学生打破记录</text>
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
<view v-for="(item,index) in record" class="table-row">
|
<view v-for="(item,index) in record" class="table-row">
|
||||||
<text class="row-cell index-cell">{{ index+1 }}</text>
|
<text class="row-cell index-cell">{{ index+1 }}</text>
|
||||||
<text class="row-cell name-cell">{{ item.name }}</text>
|
<text class="row-cell name-cell">{{ item.name }}</text>
|
||||||
<text class="row-cell time-cell">{{ formatTime(item.quicklyTime) }}</text>
|
<text class="row-cell time-cell">{{ item.quicklyTime }}</text>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
|
|
||||||
<!-- 底部控制按钮 -->
|
<!-- 底部控制按钮 -->
|
||||||
<view class="control-buttons">
|
<view class="control-buttons">
|
||||||
<view class="button-group">
|
<view class="button-group" :class="{ 'record-group': isRunning }">
|
||||||
<button v-if="!isRunning" class="start-btn" @click="startTimer">
|
<button v-if="!isRunning" class="start-btn" @click="startTimer">
|
||||||
<u-icon name="play-right" size="24" color="#fff"></u-icon>
|
<u-icon name="play-right" size="24" color="#fff"></u-icon>
|
||||||
<text class="btn-text">开始</text>
|
<text class="btn-text">开始</text>
|
||||||
@@ -100,14 +100,13 @@
|
|||||||
<text class="btn-text">记录</text>
|
<text class="btn-text">记录</text>
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
<view class="button-group">
|
<view class="button-group" :class="{ 'pause-group': isRunning }">
|
||||||
<button v-if="!isRunning" class="reset-all-btn" @click="resetAll">
|
<button v-if="!isRunning" class="reset-all-btn" @click="resetAll">
|
||||||
<u-icon name="reload" size="24" color="#fff"></u-icon>
|
<u-icon name="reload" size="24" color="#fff"></u-icon>
|
||||||
<text class="btn-text">重置</text>
|
<text class="btn-text">重置</text>
|
||||||
</button>
|
</button>
|
||||||
<button v-if="isRunning" class="reset-all-btn" @click="stopTimer">
|
<button v-if="isRunning" class="reset-all-btn" @click="stopTimer">
|
||||||
<u-icon name="pause" size="24" color="#fff"></u-icon>
|
<text class="btn-text" style="font-size: 28rpx;">暂停</text>
|
||||||
<text class="btn-text">暂停</text>
|
|
||||||
</button>
|
</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
@@ -265,7 +264,7 @@
|
|||||||
athlete.time = Math.max(0, elapsed - offset)
|
athlete.time = Math.max(0, elapsed - offset)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, 10)
|
}, 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停止计时
|
// 停止计时
|
||||||
@@ -394,12 +393,18 @@
|
|||||||
|
|
||||||
// 提交数据
|
// 提交数据
|
||||||
const submitData = () => {
|
const submitData = () => {
|
||||||
stopTimer()
|
|
||||||
const hasData = athletes.value.some(a => a.time > 0)
|
const hasData = athletes.value.some(a => a.time > 0)
|
||||||
|
const allFinished = athletes.value.every(a => a.finished)
|
||||||
if (!hasData) {
|
if (!hasData) {
|
||||||
Service.Msg('暂无数据可提交')
|
Service.Msg('暂无数据可提交')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// if(!allFinished){
|
||||||
|
// Service.Msg('还用学生未完成!')
|
||||||
|
// return
|
||||||
|
// }
|
||||||
|
stopTimer()
|
||||||
let data = [{
|
let data = [{
|
||||||
planName: "",
|
planName: "",
|
||||||
studentId: "",
|
studentId: "",
|
||||||
@@ -419,6 +424,15 @@
|
|||||||
PlanService.AddPlanLog(planId.value,'计时项目',JSON.stringify(data),'','','').then(res=>{
|
PlanService.AddPlanLog(planId.value,'计时项目',JSON.stringify(data),'','','').then(res=>{
|
||||||
if(res.code==0){
|
if(res.code==0){
|
||||||
Service.Msg('提交成功!')
|
Service.Msg('提交成功!')
|
||||||
|
stopTimer()
|
||||||
|
currentTime.value = 0
|
||||||
|
currentAthleteIndex.value = 0
|
||||||
|
|
||||||
|
athletes.value.forEach(athlete => {
|
||||||
|
athlete.time = 0
|
||||||
|
athlete.finished = false
|
||||||
|
athlete.startOffset = undefined
|
||||||
|
})
|
||||||
|
|
||||||
if(res.data.record.length>0){
|
if(res.data.record.length>0){
|
||||||
record.value=res.data.record
|
record.value=res.data.record
|
||||||
@@ -558,7 +572,7 @@
|
|||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
border-radius: 16rpx;
|
border-radius: 16rpx;
|
||||||
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03);
|
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03);
|
||||||
transition: all 0.3s;
|
transition: background-color 0.3s, border-color 0.3s;
|
||||||
border: 2rpx solid transparent;
|
border: 2rpx solid transparent;
|
||||||
|
|
||||||
&.finished {
|
&.finished {
|
||||||
@@ -716,6 +730,14 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.record-group {
|
||||||
|
flex: 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pause-group {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.back-btn,
|
.back-btn,
|
||||||
.start-btn,
|
.start-btn,
|
||||||
.pause-btn,
|
.pause-btn,
|
||||||
|
|||||||
Reference in New Issue
Block a user