From f682c0316b2071dd661e14b0ef91f29fd385fe5f Mon Sep 17 00:00:00 2001 From: Ls <2391972606@qq.com> Date: Mon, 6 Apr 2026 09:31:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8F=90=E4=BA=A4=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages.json | 3 +- src/pages/dataAnalyze/Curve.vue | 468 ++++---- src/pages/dataAnalyze/baoganAnalyze.vue | 758 +++++++++---- src/pages/dataAnalyze/paragraphAnalyze.vue | 1176 ++++++++------------ src/pages/index/index.vue | 49 +- src/pages/userFunc/project.vue | 1043 +++++++++++------ src/pages/userFunc/student.vue | 201 +++- src/pages/userFunc/swiming.vue | 338 ++++-- 8 files changed, 2352 insertions(+), 1684 deletions(-) diff --git a/src/pages.json b/src/pages.json index a2cf92b..264bb20 100644 --- a/src/pages.json +++ b/src/pages.json @@ -77,7 +77,8 @@ { "path": "project", "style": { - "navigationBarTitleText": "包干" + "navigationBarTitleText": "包干", + "navigationStyle": "custom" } } ] diff --git a/src/pages/dataAnalyze/Curve.vue b/src/pages/dataAnalyze/Curve.vue index 812b324..3180c34 100644 --- a/src/pages/dataAnalyze/Curve.vue +++ b/src/pages/dataAnalyze/Curve.vue @@ -14,7 +14,8 @@ 选择项目 - + {{ selectedProjectName }} @@ -31,7 +32,9 @@ - + {{ student.name.charAt(0) }} @@ -75,9 +78,11 @@ 历史训练成绩变化曲线 - - - + + + + + @@ -142,8 +147,8 @@ * 定义学生的基本信息 */ interface Student { - id: string - name: string + id : string + name : string } /** @@ -151,12 +156,12 @@ * 定义学生某次训练的详细记录 */ interface StudentTrainingRecord { - studentId: string - studentName: string - time: number - recordDate: string - recordFullDate: string - round: number + studentId : string + studentName : string + time : number + recordDate : string + recordFullDate : string + round : number } // ==================== 响应式数据 - 学生相关 ==================== @@ -176,6 +181,14 @@ // ==================== 响应式数据 - 图表相关 ==================== + // 计算图表宽度(根据数据点数量动态调整) + const chartWidth = computed(() => { + const categoryCount = lineChartData.value.categories.length + if (categoryCount <= 6) return 700 + // 每个数据点分配 100rpx,最小 700rpx + return Math.max(700, categoryCount * 100) + }) + // 折线图配置选项 const lineOpts = ref({ color: chartColors, @@ -187,13 +200,14 @@ }, xAxis: { disableGrid: true, - itemCount: 6, - rotateLabel: true + rotateLabel: true, + scrollShow: true, + scrollAlign: 'left' }, yAxis: { data: [{ min: 0, - format: (val: number) => val.toFixed(1) + 's' + format: (val : number) => val.toFixed(1) + 's' }] }, extra: { @@ -226,7 +240,7 @@ // ==================== 模拟数据 ==================== // 模拟的学生数据(按项目分组) - const mockProjectStudents: Record = { + const mockProjectStudents : Record = { '1': [ { id: 's1', name: '张小明' }, { id: 's2', name: '李小红' }, @@ -252,7 +266,7 @@ } // 模拟的训练记录数据(按项目分组) - const mockTrainingRecords: Record = { + const mockTrainingRecords : Record = { '1': [ { studentId: 's1', studentName: '张小明', time: 28.5, recordDate: '03-12', recordFullDate: '2026-03-12', round: 1 }, { studentId: 's1', studentName: '张小明', time: 27.2, recordDate: '03-14', recordFullDate: '2026-03-14', round: 1 }, @@ -319,7 +333,7 @@ // TODO: 调用后端 API 获取项目列表 } - const handleProjectChange = (e: any) => { + const handleProjectChange = (e : any) => { const index = e.detail.value selectedProjectIndex.value = index const selectedProject = projectList.value[index] @@ -330,7 +344,7 @@ selectedStudentIds.value = [] } - const loadProjectStudents = (projectId: string) => { + const loadProjectStudents = (projectId : string) => { if (mockProjectStudents[projectId]) { projectStudents.value = mockProjectStudents[projectId] } else { @@ -338,7 +352,7 @@ } } - const toggleStudent = (studentId: string) => { + const toggleStudent = (studentId : string) => { const index = selectedStudentIds.value.indexOf(studentId) if (index !== -1) { selectedStudentIds.value.splice(index, 1) @@ -351,7 +365,7 @@ } } - const getStudentBestTime = (studentId: string): number => { + const getStudentBestTime = (studentId : string) : number => { const records = mockTrainingRecords[selectedProjectId.value] || [] const studentRecords = records.filter(r => r.studentId === studentId) if (studentRecords.length === 0) return 0 @@ -378,7 +392,7 @@ return `${parts[1]}-${parts[2]}` }) - const seriesData: any[] = [] + const seriesData : any[] = [] selectedStudentIds.value.forEach((studentId, index) => { const studentRecords = projectRecords.filter(r => r.studentId === studentId) @@ -430,249 +444,255 @@ .subtitle { font-size: 24rpx; color: #999; + } } - } - - .section-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 20rpx; - - .section-title { - font-size: 30rpx; - font-weight: 600; - color: #333; - } - - .select-count { - font-size: 24rpx; - color: #52c41a; - } - } - - .project-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); - - .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: #52c41a; - } - - .picker-text { - font-size: 28rpx; - color: #333; - } - } - } - - .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; + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 20rpx; + + .section-title { + font-size: 30rpx; + font-weight: 600; + color: #333; + } + + .select-count { + font-size: 24rpx; + color: #52c41a; + } } - .student-list { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 16rpx; + .project-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); - .student-item { + .picker-wrapper { display: flex; - flex-direction: column; align-items: center; - padding: 20rpx 10rpx; + justify-content: space-between; + padding: 20rpx 24rpx; background-color: #f8f8f8; border-radius: 12rpx; - border: 2rpx solid transparent; + border: 1rpx solid #e8e8e8; transition: all 0.3s ease; - position: relative; &:active { - transform: scale(0.95); - } - - &.selected { - background-color: #f0f9eb; + background-color: #f0f0f0; 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; + .picker-text { + font-size: 28rpx; 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 { - margin: 0 20rpx; - - .stats-card { + .student-select-section { background-color: #fff; + margin: 0 20rpx 20rpx; border-radius: 16rpx; - padding: 24rpx; - margin-bottom: 20rpx; - display: flex; - align-items: center; - justify-content: space-around; + padding: 28rpx; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); - .stat-item { - text-align: center; - - .stat-label { - font-size: 24rpx; - color: #999; - display: block; - margin-bottom: 8rpx; - } - - .stat-value { - font-size: 32rpx; - font-weight: 700; - color: #52c41a; - } + .section-header { + margin-bottom: 24rpx; } - .stat-divider { - width: 1rpx; - height: 50rpx; - background-color: #eee; - } - } - - .chart-card { - background-color: #fff; - border-radius: 16rpx; - padding: 24rpx; - 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-box { - width: 100%; - height: 500rpx; - background-color: #fafafa; - border-radius: 12rpx; - overflow: hidden; - } - } - - .legend-section { - background-color: #fff; - border-radius: 16rpx; - padding: 24rpx; - box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); - - .legend-title { - font-size: 28rpx; - font-weight: 600; - color: #333; - margin-bottom: 20rpx; - } - - .legend-list { - display: flex; - flex-direction: column; + .student-list { + display: grid; + grid-template-columns: repeat(4, 1fr); gap: 16rpx; - .legend-item { + .student-item { display: flex; + flex-direction: column; align-items: center; - gap: 12rpx; + padding: 20rpx 10rpx; + background-color: #f8f8f8; + border-radius: 12rpx; + border: 2rpx solid transparent; + transition: all 0.3s ease; + position: relative; - .legend-color { - width: 24rpx; - height: 24rpx; + &: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%; - flex-shrink: 0; + 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; + } } - .legend-name { - font-size: 26rpx; + .student-name { + font-size: 24rpx; color: #333; - font-weight: 600; + text-align: center; } - .legend-desc { + .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 { + margin: 0 20rpx; + + .stats-card { + background-color: #fff; + border-radius: 16rpx; + padding: 24rpx; + margin-bottom: 20rpx; + display: flex; + align-items: center; + justify-content: space-around; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); + + .stat-item { + text-align: center; + + .stat-label { + font-size: 24rpx; + color: #999; + display: block; + margin-bottom: 8rpx; + } + + .stat-value { + font-size: 32rpx; + font-weight: 700; + color: #52c41a; + } + } + + .stat-divider { + width: 1rpx; + height: 50rpx; + background-color: #eee; + } + } + + .chart-card { + background-color: #fff; + border-radius: 16rpx; + padding: 24rpx; + 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 { + background-color: #fff; + border-radius: 16rpx; + padding: 24rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); + + .legend-title { + font-size: 28rpx; + font-weight: 600; + color: #333; + margin-bottom: 20rpx; + } + + .legend-list { + display: flex; + flex-direction: column; + gap: 16rpx; + + .legend-item { + display: flex; + align-items: center; + gap: 12rpx; + + .legend-color { + width: 24rpx; + height: 24rpx; + border-radius: 50%; + flex-shrink: 0; + } + + .legend-name { + font-size: 26rpx; + color: #333; + font-weight: 600; + } + + .legend-desc { + font-size: 24rpx; + color: #999; + } + } + } } } - } - + \ No newline at end of file diff --git a/src/pages/dataAnalyze/baoganAnalyze.vue b/src/pages/dataAnalyze/baoganAnalyze.vue index 527aa20..c3e7f63 100644 --- a/src/pages/dataAnalyze/baoganAnalyze.vue +++ b/src/pages/dataAnalyze/baoganAnalyze.vue @@ -8,78 +8,81 @@ - - - - 项目名称 - - {{ selectedProject }} - - + + + + 选择项目 - - 选择学生 - - {{ selectedStudent }} - + + + {{ selectedProjectName }} + - + - - - - - - - - + + - - - - 选中日期 - {{ selectedDate }} + + + + 选择学生 - - - 训练人数 - {{ tableData.length }}人 - - - - 完成率平均 - {{ averageCompletion }}% + + {{ selectedStudentDisplay }} + + + + + + 选择学生 + + 全选 + 确定 + + + + + + + + + + + + {{ student.name.charAt(0) }} + + + + {{ student.name }} + + + {{ student.gender }} + + {{ student.age }}岁 + + + + + + - - + + @@ -100,15 +103,43 @@ import { Service } from '@/Service/Service' import { ref, computed } from 'vue' - // 当前年月 + interface Project { + id : string + name : string + } + + interface Student { + id : string + name : string + gender : string + age : number + } + + interface TableDataItem { + date : string + name : string + projectName : string + plan : string + completion : number + detail : string + } + + interface StudentRecord { + student : Student + date : string + segments : TableDataItem[] + } + const currentYear = ref(new Date().getFullYear()) const currentMonth = ref(new Date().getMonth() + 1) - - // 选中的日期 const selectedDate = ref('') - // 表格列定义 const columns = ref([ + { + label: '日期', + prop: 'date', + width: '100px' + }, { label: '姓名', prop: 'name', @@ -136,211 +167,218 @@ } ]) - // 表格数据 - interface TableDataItem { - name: string - projectName: string - plan: string - completion: number - detail: string - } - const tableData = ref([]) - // 项目列表 - const projectList = ref([ - { label: '全部项目', value: 'all' }, - { label: '100米自由泳', value: '100m-free' }, - { label: '200米自由泳', value: '200m-free' }, - { label: '400米自由泳', value: '400m-free' }, - { label: '100米蛙泳', value: '100m-breast' } + const projectList = ref([ + { id: '1', name: '100米自由泳' }, + { id: '2', name: '200米自由泳' }, + { id: '3', name: '400米自由泳' }, + { id: '4', name: '100米蛙泳' } ]) - // 学生列表 - const studentList = ref([ - { label: '全部学生', value: 'all' }, - { label: '张小明', value: 'zhang-xiaoming' }, - { label: '李小红', value: 'li-xiaohong' }, - { label: '王小明', value: 'wang-xiaoming' }, - { label: '赵小芳', value: 'zhao-xiaofang' }, - { label: '陈小刚', value: 'chen-xiaogang' } - ]) + const projectOptions = ref(projectList.value) - // 选中的项目和学生 - const selectedProject = ref('全部项目') - const selectedStudent = ref('全部学生') - const selectedProjectValue = ref('all') - const selectedStudentValues = ref([]) + const selectedProjectIndex = ref(-1) + const selectedProjectId = ref('') - // 选择器显示状态 - const showProjectPicker = ref(false) - const showStudentPicker = ref(false) - const defaultStudentIndex = ref([0]) - - // 日历打点数据 - // const selectedDates = ref([ - // { date: '2026-03-15', info: '训练' }, - // { date: '2026-03-18', info: '训练' }, - // { date: '2026-03-20', info: '训练' }, - // { date: '2026-03-22', info: '训练' }, - // { date: '2026-03-25', info: '训练' } - // ]) - - // 平均完成率 - const averageCompletion = computed(() => { - if (tableData.value.length === 0) return 0 - const total = tableData.value.reduce((sum, item) => sum + item.completion, 0) - return Math.round(total / tableData.value.length) + const selectedProjectName = computed(() => { + if (selectedProjectIndex.value === -1) { + return '请选择项目' + } + return projectList.value[selectedProjectIndex.value]?.name || '请选择项目' }) - // 完整模拟数据 - const mockData: Record = { - '2026-03-28': [ - { name: '张小明', projectName: '100米自由泳', plan: '2000米', completion: 95, detail: '1900/2000' }, - { name: '张小明', projectName: '200米自由泳', plan: '1500米', completion: 88, detail: '1320/1500' }, - { name: '李小红', projectName: '100米自由泳', plan: '2000米', completion: 92, detail: '1840/2000' }, - { name: '李小红', projectName: '400米自由泳', plan: '1200米', completion: 85, detail: '1020/1200' }, - { name: '王小明', projectName: '100米自由泳', plan: '2000米', completion: 90, detail: '1800/2000' }, - { name: '赵小芳', projectName: '100米蛙泳', plan: '1800米', completion: 88, detail: '1584/1800' }, - { name: '陈小刚', projectName: '200米自由泳', plan: '1500米', completion: 95, detail: '1425/1500' } - ], - '2026-03-29': [ - { name: '张小明', projectName: '100米自由泳', plan: '2000米', completion: 90, detail: '1800/2000' }, - { name: '张小明', projectName: '100米蛙泳', plan: '1800米', completion: 85, detail: '1530/1800' }, - { name: '李小红', projectName: '100米自由泳', plan: '2000米', completion: 95, detail: '1900/2000' }, - { name: '李小红', projectName: '200米自由泳', plan: '1500米', completion: 92, detail: '1380/1500' }, - { name: '王小明', projectName: '200米自由泳', plan: '1500米', completion: 98, detail: '1470/1500' }, - { name: '赵小芳', projectName: '100米自由泳', plan: '2000米', completion: 82, detail: '1640/2000' }, - { name: '陈小刚', projectName: '100米自由泳', plan: '2000米', completion: 88, detail: '1760/2000' } - ], - '2026-03-30': [ - { name: '张小明', projectName: '100米自由泳', plan: '2000米', completion: 92, detail: '1840/2000' }, - { name: '张小明', projectName: '400米自由泳', plan: '1200米', completion: 90, detail: '1080/1200' }, - { name: '李小红', projectName: '100米自由泳', plan: '2000米', completion: 88, detail: '1760/2000' }, - { name: '李小红', projectName: '100米蛙泳', plan: '1800米', completion: 95, detail: '1710/1800' }, - { name: '王小明', projectName: '100米自由泳', plan: '2000米', completion: 95, detail: '1900/2000' }, - { name: '赵小芳', projectName: '200米自由泳', plan: '1500米', completion: 85, detail: '1275/1500' }, - { name: '陈小刚', projectName: '400米自由泳', plan: '1200米', completion: 92, detail: '1104/1200' } - ] - } - - // 日历打点数据 const selectedDates = ref([ { date: '2026-03-28', info: '训练' }, { date: '2026-03-29', info: '训练' }, { date: '2026-03-30', info: '训练' } ]) + const studentList = ref([]) + + const selectedStudentIndexes = ref([]) + const selectedStudentIds = ref([]) + const showStudentPicker = ref(false) + + const selectedStudentDisplay = computed(() => { + if (selectedStudentIndexes.value.length === 0) { + return '请选择学生' + } else { + const names = selectedStudentIndexes.value.map(index => studentList.value[index].name) + return names.length > 2 ? `${names.slice(0, 2).join(', ')}等${names.length}人` : names.join(', ') + } + }) + + const averageCompletion = computed(() => { + if (tableData.value.length === 0) return 0 + const total = tableData.value.reduce((sum, item) => sum + item.completion, 0) + return Math.round(total / tableData.value.length) + }) + + const mockData : Record> = { + '1': { + '2026-03-28': [ + { name: '张小明', projectName: '100米自由泳', plan: '2000米', completion: 95, detail: '1900/2000' }, + { name: '李小红', projectName: '100米自由泳', plan: '2000米', completion: 92, detail: '1840/2000' }, + { name: '王小明', projectName: '100米自由泳', plan: '2000米', completion: 90, detail: '1800/2000' } + ], + '2026-03-29': [ + { name: '张小明', projectName: '100米自由泳', plan: '2000米', completion: 90, detail: '1800/2000' }, + { name: '李小红', projectName: '100米自由泳', plan: '2000米', completion: 95, detail: '1900/2000' }, + { name: '王小明', projectName: '100米自由泳', plan: '2000米', completion: 88, detail: '1760/2000' } + ] + }, + '2': { + '2026-03-28': [ + { name: '张小明', projectName: '200米自由泳', plan: '1500米', completion: 88, detail: '1320/1500' }, + { name: '李小红', projectName: '200米自由泳', plan: '1500米', completion: 92, detail: '1380/1500' }, + { name: '王小明', projectName: '200米自由泳', plan: '1500米', completion: 85, detail: '1275/1500' } + ], + '2026-03-29': [ + { name: '张小明', projectName: '200米自由泳', plan: '1500米', completion: 90, detail: '1350/1500' }, + { name: '李小红', projectName: '200米自由泳', plan: '1500米', completion: 88, detail: '1320/1500' } + ] + }, + '3': { + '2026-03-28': [ + { name: '张小明', projectName: '400米自由泳', plan: '1200米', completion: 85, detail: '1020/1200' }, + { name: '李小红', projectName: '400米自由泳', plan: '1200米', completion: 90, detail: '1080/1200' } + ] + }, + '4': { + '2026-03-28': [ + { name: '赵小芳', projectName: '100米蛙泳', plan: '1800米', completion: 88, detail: '1584/1800' }, + { name: '陈小' + '刚', projectName: '100米蛙泳', plan: '1800米', completion: 95, detail: '1710/1800' } + ] + } + } + + const mockStudents : Record = { + '1': [ + { id: 's1', name: '张小明', gender: '男', age: 12 }, + { id: 's2', name: '李小红', gender: '女', age: 11 }, + { id: 's3', name: '王小明', gender: '男', age: 13 } + + ], + '2': [ + { id: 's1', name: '张小明', gender: '男', age: 12 }, + { id: 's2', name: '李小红', gender: '女', age: 11 }, + { id: 's3', name: '王小明', gender: '男', age: 13 } + + ], + '3': [ + { id: 's1', name: '张小明', gender: '男', age: 12 }, + { id: 's2', name: '李小红', gender: '女', age: 11 } + ], + '4': [ + { id: 's4', name: '赵小芳', gender: '女', age: 10 }, + { id: 's5', name: '陈小' + '刚', gender: '男', age: 14 } + ] + } + onLoad(() => { loadData() }) onShow(() => { - // 页面显示时刷新数据 + }) - // 加载数据 const loadData = () => { - // TODO: 调用API获取数据 + } - // 月份单元格点击处理 - const handleCellClick = (event: any) => { + const handleCellClick = (event : any) => { console.log('单元格点击事件:', event) } - // 月份切换处理 - const handleMonthSwitch = (e: any) => { + const handleProjectChange = (e : any) => { + const index = e.detail.value + selectedProjectIndex.value = index + const selectedProject = projectList.value[index] + + if (selectedProject) { + selectedProjectId.value = selectedProject.id + selectedDate.value = '' + selectedStudentIndexes.value = [] + selectedStudentIds.value = [] + tableData.value = [] + loadProjectStudents(selectedProject.id) + } + } + + const handleMonthSwitch = (e : any) => { currentYear.value = e.year currentMonth.value = e.month loadData() - // 切换月份后清空选中日期 selectedDate.value = '' + selectedStudentIndexes.value = [] + selectedStudentIds.value = [] tableData.value = [] } - // 日期选择处理 - const handleDateChange = (e: any) => { + const handleDateChange = (e : any) => { + console.log(e); const date = e.fulldate selectedDate.value = date - filterAndLoadData() + selectedStudentIndexes.value = [] + selectedStudentIds.value = [] + tableData.value = [] } - // 项目选择确认 - const handleProjectConfirm = (e: any) => { - const item = e.value[0] - selectedProject.value = item.label - selectedProjectValue.value = item.value - showProjectPicker.value = false - filterAndLoadData() + const loadProjectStudents = (projectId : string) => { + const students = mockStudents[projectId] || [] + studentList.value = students + console.log(`项目 ${projectId} 的学生列表加载完成,共 ${students.length} 位学生`) } - // 学生选择确认 - const handleStudentConfirm = (e: any) => { - const items = e.value - selectedStudentValues.value = items.map((item: any) => item.value) + const selectAllStudents = () => { + selectedStudentIndexes.value = studentList.value.map((_, index) => index) + } - if (selectedStudentValues.value.length === 0) { - selectedStudent.value = '未选择' - } else if (selectedStudentValues.value.includes('all')) { - selectedStudent.value = '全部学生' + const handleStudentChange = (e : any) => { + console.log('学生选择变化:', e) + } + + const toggleStudentSelection = (index : number) => { + const idx = selectedStudentIndexes.value.indexOf(index) + if (idx > -1) { + selectedStudentIndexes.value.splice(idx, 1) } else { - const selectedNames = items.map((item: any) => item.label) - selectedStudent.value = selectedNames.length > 2 - ? `${selectedNames.slice(0, 2).join(', ')}等${selectedNames.length}人` - : selectedNames.join(', ') + selectedStudentIndexes.value.push(index) } + } - defaultStudentIndex.value = e.indexs + const confirmStudentSelection = () => { + selectedStudentIds.value = selectedStudentIndexes.value.map(index => studentList.value[index].id) showStudentPicker.value = false filterAndLoadData() } - // 过滤并加载数据 + const closeStudentPicker = () => { + showStudentPicker.value = false + } + const filterAndLoadData = () => { - if (!selectedDate.value) { + if (!selectedDate.value || selectedStudentIndexes.value.length === 0) { tableData.value = [] return } - const dateData = mockData[selectedDate.value] + const projectData = mockData[selectedProjectId.value] + if (!projectData) { + tableData.value = [] + return + } + + const dateData = projectData[selectedDate.value] if (!dateData) { tableData.value = [] return } - let filteredData = [...dateData] - - // 按项目过滤 - if (selectedProjectValue.value !== 'all') { - const projectMap: Record = { - '100m-free': '100米自由泳', - '200m-free': '200米自由泳', - '400m-free': '400米自由泳', - '100m-breast': '100米蛙泳' - } - const targetProject = projectMap[selectedProjectValue.value] - if (targetProject) { - filteredData = filteredData.filter(item => item.projectName === targetProject) - } - } - - // 按学生过滤 - if (selectedStudentValue.value !== 'all') { - const studentMap: Record = { - 'zhang-xiaoming': '张小明', - 'li-xiaohong': '李小红', - 'wang-xiaoming': '王小明', - 'zhao-xiaofang': '赵小芳', - 'chen-xiaogang': '陈小刚' - } - const targetStudent = studentMap[selectedStudentValue.value] - if (targetStudent) { - filteredData = filteredData.filter(item => item.name === targetStudent) - } - } - - tableData.value = filteredData + const selectedStudentNames = selectedStudentIndexes.value.map(index => studentList.value[index].name) + tableData.value = dateData.filter(item => selectedStudentNames.includes(item.name)) } @@ -354,7 +392,6 @@ padding-bottom: 40rpx; } - /* 页面标题区域 */ .header-section { background-color: #fff; padding: 32rpx 28rpx 24rpx; @@ -376,59 +413,272 @@ } } - /* 选择器区域 */ - .selector-section { + .select-section { background-color: #fff; margin: 0 20rpx 20rpx; - border-radius: 16rpx; - padding: 24rpx; - display: flex; - gap: 20rpx; + border-radius: 20rpx; + padding: 28rpx; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; - .selector-item { - flex: 1; + .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; - .selector-label { - font-size: 28rpx; - color: #666; - margin-right: 16rpx; + &:active { + background-color: #f0f0f0; + border-color: #faad14; + transform: scale(0.98); } - .selector-value { - flex: 1; + .picker-text { + font-size: 28rpx; + color: #333; + } + } + } + + .calendar-wrapper { + background-color: #fff; + margin: 0 20rpx 20rpx; + padding: 20rpx 0; + border-radius: 20rpx; + box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); + } + + .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; + flex-direction: column; + overflow: hidden; + + .picker-header { + display: flex; + justify-content: space-between; + align-items: center; + 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 { + font-size: 36rpx; + font-weight: 700; + background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + .header-actions { display: flex; align-items: center; - justify-content: flex-end; + gap: 16rpx; - .value-text { - font-size: 28rpx; - color: #333; - font-weight: 500; - margin-right: 8rpx; + .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; + } + } } } } } - /* 日历组件包装 */ - .calendar-wrapper { - background-color: #fff; - margin-bottom: 20rpx; - padding: 20rpx 0; + .modal-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%); + backdrop-filter: blur(10rpx); + z-index: 998; + animation: fadeIn 0.3s ease; } - /* 日期数据统计 */ .date-summary { background-color: #fff; margin: 0 20rpx 20rpx; - border-radius: 16rpx; + border-radius: 20rpx; padding: 24rpx; display: flex; align-items: center; @@ -459,16 +709,14 @@ } } - /* 表格区域 */ .table-section { margin: 0 20rpx; background-color: #fff; - border-radius: 16rpx; + border-radius: 20rpx; overflow: hidden; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); } - /* 空状态容器 */ .empty-container { display: flex; flex-direction: column; @@ -486,11 +734,10 @@ } } - /* 提示状态 */ .hint-state { margin: 0 20rpx; background-color: #fff; - border-radius: 16rpx; + border-radius: 20rpx; padding: 80rpx 40rpx; text-align: center; box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); @@ -505,7 +752,6 @@ } } - /* sl-table 样式覆盖 */ ::v-deep .sl-table { width: 100%; } @@ -533,4 +779,24 @@ ::v-deep .sl-table__body__row:nth-child(odd) { background-color: #fff !important; } - + + @keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } + + @keyframes slideUp { + from { + transform: translateY(100%); + } + + to { + transform: translateY(0); + } + } + \ No newline at end of file diff --git a/src/pages/dataAnalyze/paragraphAnalyze.vue b/src/pages/dataAnalyze/paragraphAnalyze.vue index 891705e..e75ba52 100644 --- a/src/pages/dataAnalyze/paragraphAnalyze.vue +++ b/src/pages/dataAnalyze/paragraphAnalyze.vue @@ -1,19 +1,16 @@ @@ -132,72 +121,32 @@ import { Service } from '@/Service/Service' import { ref, computed } from 'vue' - // ==================== TypeScript 接口定义 ==================== - - /** - * 项目接口 - * 定义游泳项目的基本信息 - */ interface Project { - id: string // 项目 ID - name: string // 项目名称 + id: string + name: string } - /** - * 学生接口 - * 定义学生的基本信息和成绩 - */ interface Student { - id: string // 学生 ID - name: string // 学生姓名 - gender: string // 性别:男/女 - age: number // 年龄 - projectName: string // 项目名称 - totalTime: number // 总成绩(秒) - projectId: string // 所属项目 ID + id: string + name: string + gender: string + age: number } - /** - * 分段数据接口 - * 定义分段训练的详细信息 - */ - interface SegmentData { - id: string // 分段 ID - distance: number // 分段距离(米) - time: number // 分段成绩(秒) - timeDiff: number // 时间差(与平均值的差值,秒) + interface TableDataItem { + date: string + name: string + projectName: string + segment20: string + segment70: string + segment120: string + segment170: string } - /** - * 完整学生记录接口 - * 包含学生信息和分段数据 - */ - interface StudentRecord { - student: Student // 学生基本信息 - segments: SegmentData[] // 分段数据列表 - date: string // 记录日期 - } - - // ==================== 响应式数据 - 年月相关 ==================== - - /** - * 当前年份 - * 用于月份切换时记录当前年份 - */ const currentYear = ref(new Date().getFullYear()) - - /** - * 当前月份(1-12) - * 用于月份切换时记录当前月份 - */ const currentMonth = ref(new Date().getMonth() + 1) + const selectedDate = ref('') - // ==================== 响应式数据 - 项目相关 ==================== - - /** - * 项目列表数据 - * 从后端 API 获取或使用模拟数据 - */ const projectList = ref([ { id: '1', name: '100米自由泳' }, { id: '2', name: '200米自由泳' }, @@ -205,28 +154,10 @@ { id: '4', name: '100米蝶泳' } ]) - /** - * 项目选择器选项 - * 用于 picker 组件显示 - */ const projectOptions = ref(projectList.value) - - /** - * 当前选中的项目索引 - * 用于 picker 组件的 value 属性 - */ const selectedProjectIndex = ref(-1) - - /** - * 当前选中的项目 ID - * 用于后续数据查询 - */ const selectedProjectId = ref('') - /** - * 计算属性:当前选中的项目名称 - * 根据选中的索引返回项目名称,默认显示提示文本 - */ const selectedProjectName = computed(() => { if (selectedProjectIndex.value === -1) { return '请选择项目' @@ -234,20 +165,6 @@ return projectList.value[selectedProjectIndex.value]?.name || '请选择项目' }) - // ==================== 响应式数据 - 日期相关 ==================== - - /** - * 当前选中的日期 - * 格式:YYYY-MM-DD - */ - const selectedDate = ref('') - - /** - * 日历打点数据 - * 用于在日历上标记有训练记录的日期 - * date: 日期字符串,'YYYY-MM-DD' - * info: 显示在日期上的标记信息 - */ const selectedDates = ref([ { date: '2026-03-12', info: '分段' }, { date: '2026-03-14', info: '分段' }, @@ -256,495 +173,186 @@ { date: '2026-03-24', info: '分段' } ]) - // ==================== 响应式数据 - 学生相关 ==================== - - /** - * 学生列表数据 - * 根据选择的项目动态更新 - */ const studentList = ref([]) + const selectedStudentIndexes = ref([]) + const selectedStudentIds = ref([]) + const showStudentSelect = ref(false) - /** - * 学生选择器选项 - * 用于 picker 组件显示 - */ - const studentOptions = ref([]) - - /** - * 当前选中的学生索引 - * 用于 picker 组件的 value 属性 - */ - const selectedStudentIndex = ref(-1) - - /** - * 当前选中的学生 ID - * 用于后续数据查询 - */ - const selectedStudentId = ref('') - - /** - * 计算属性:当前选中的学生名称 - * 根据选中的索引返回学生名称,默认显示提示文本 - */ - const selectedStudentName = computed(() => { - if (selectedStudentIndex.value === -1) { + const selectedStudentDisplay = computed(() => { + if (selectedStudentIndexes.value.length === 0) { return '请选择学生' + } else { + const names = selectedStudentIndexes.value.map(index => studentList.value[index].name) + return names.length > 2 ? `${names.slice(0, 2).join(', ')}等${names.length}人` : names.join(', ') } - return studentList.value[selectedStudentIndex.value]?.name || '请选择学生' }) - // ==================== 响应式数据 - 分段相关 ==================== + const tableData = ref([]) - /** - * 当前选中的学生详细信息 - * 包含学生基本信息和分段数据 - */ - const currentStudent = ref(null) - - /** - * 分段数据列表(用于表格展示) - * 按段序号排序后的数据 - */ - const segmentList = ref([]) - - /** - * 分段表格列定义 - * 列顺序:分段距离、时间差、分段成绩 - */ - const segmentColumns = ref([ - { - label: '分段距离', - prop: 'distance', - width: '140px' + const mockData: Record> = { + '1': { + '2026-03-12': [ + { name: '王小明', projectName: '100米'自由泳', segment20: 0.037, segment70: 0.128, segment120: 0.219, segment170: 0.311 }, + { name: '张小红', projectName: '100米'自由泳', segment20: 0.042, segment70: 0.145, segment120: 0.248, segment170: 0.352 }, + { name: '李小龙', projectName: '100米'自由泳', segment20: 0.032, segment70: 0.112, segment120: 0.192, segment170: 0.272 } + ], + '2026-03-14': [ + { name: '王小明', projectName: '100米'自由泳', segment20: 0.035, segment70: 0.122, segment120: 0.209, segment170: 0.296 }, + { name: '张小红', projectName: '100米'自由泳', segment20: 0.040, segment70: 0.138, segment120: 0.236, segment170: 0.334 } + ] }, - { - label: '时间差', - prop: 'timeDiff', - width: '140px' + '2': { + '2026-03-16': [ + { name: '赵小芳', projectName: '200米'自由泳', segment20: 0.038, segment70: 0.133, segment120: 0.228, segment170: 0.323 } + ] }, - { - label: '分段成绩', - prop: 'time', - width: '140px' + '3': { + '2026-03-19': [ + { name: '陈小刚', projectName: '50米蛙泳', segment20: 0.044, segment70: 0.154, segment120: 0.264, segment170: 0.374 } + ] + }, + '4': { + '2026-03-24': [ + { name: '周小丽', projectName: '100米'蝶泳', segment20: 0.041, segment70: 0.143, segment120: 0.245, segment170: 0.347 } + ] } - ]) + } - /** - * 分段表格数据 - * 将分段数据转换为表格所需格式 - */ - const segmentTableData = computed(() => { - return segmentList.value.map(segment => ({ - distance: segment.distance, // 分段距离 - timeDiff: segment.timeDiff, // 时间差 - time: segment.time // 分段成绩 - })) - }) - - // ==================== 计算属性 - 页面状态判断 ==================== - - /** - * 计算属性:数据是否准备就绪 - * 只有项目、日期、学生都选择后才返回 true - */ - const isDataReady = computed(() => { - return selectedProjectId.value !== '' && - selectedDate.value !== '' && - selectedStudentId.value !== '' && - currentStudent.value !== null - }) - - /** - * 计算属性:提示文本 - * 根据当前选择状态返回不同的提示信息 - */ - const hintText = computed(() => { - if (!selectedProjectId.value) { - return '请先选择项目' - } else if (!selectedDate.value) { - return '请在日历选择日期' - } else if (!selectedStudentId.value) { - return '请选择学生' - } - return '暂无数据' - }) - - // ==================== 模拟数据 ==================== - - /** - * 模拟的学生记录数据 - * 按项目 ID 分组,每组包含该项目的学生记录 - * 每个学生记录包含多个日期的分段数据 - */ - const mockStudentRecords: Record = { - '1': [ // 100米自由泳项目的学生记录 - { - student: { - id: 's1', - name: '王小明', - gender: '男', - age: 12, - projectName: '100米自由泳', - totalTime: 68.5, - projectId: '1' - }, - date: '2026-03-12', - segments: [ - { id: 'seg1', distance: 25, time: 17.2, timeDiff: 0.8 }, - { id: 'seg2', distance: 25, time: 17.5, timeDiff: 1.1 }, - { id: 'seg3', distance: 25, time: 16.8, timeDiff: 0.4 }, - { id: 'seg4', distance: 25, time: 17.0, timeDiff: 0.6 } - ] - }, - { - student: { - id: 's2', - name: '张小红', - gender: '女', - age: 11, - projectName: '100米自由泳', - totalTime: 72.3, - projectId: '1' - }, - date: '2026-03-12', - segments: [ - { id: 'seg5', distance: 25, time: 18.5, timeDiff: 1.2 }, - { id: 'seg6', distance: 25, time: 18.0, timeDiff: 0.7 }, - { id: 'seg7', distance: 25, time: 17.8, timeDiff: 0.5 }, - { id: 'seg8', distance: 25, time: 18.0, timeDiff: 0.7 } - ] - }, - { - student: { - id: 's3', - name: '李小龙', - gender: '男', - age: 13, - projectName: '100米自由泳', - totalTime: 65.2, - projectId: '1' - }, - date: '2026-03-14', - segments: [ - { id: 'seg9', distance: 25, time: 16.5, timeDiff: 0.1 }, - { id: 'seg10', distance: 25, time: 16.3, timeDiff: -0.1 }, - { id: 'seg11', distance: 25, time: 16.4, timeDiff: 0 }, - { id: 'seg12', distance: 25, time: 16.0, timeDiff: -0.4 } - ] - } + const mockStudents: Record = { + '1': [ + { id: 's1', name: '王小明', gender: '男', age: 12 }, + { id: 's2', name: '张小红', gender: '女', age: 11 }, + { id: 's3', name: '李小龙', gender: '男', age: 13 } ], - '2': [ // 200米自由泳项目的学生记录 - { - student: { - id: 's4', - name: '赵小芳', - gender: '女', - age: 10, - projectName: '200米自由泳', - totalTime: 145.2, - projectId: '2' - }, - date: '2026-03-16', - segments: [ - { id: 'seg13', distance: 50, time: 36.5, timeDiff: 2.1 }, - { id: 'seg14', distance: 50, time: 35.8, timeDiff: 1.4 }, - { id: 'seg15', distance: 50, time: 36.2, timeDiff: 1.8 }, - { id: 'seg16', distance: 50, time: 36.7, timeDiff: 2.3 } - ] - } + '2': [ + { id: 's4', name: '赵小芳', gender: '女', age: 10 } ], - '3': [ // 50米蛙泳项目的学生记录 - { - student: { - id: 's5', - name: '陈小刚', - gender: '男', - age: 14, - projectName: '50米蛙泳', - totalTime: 42.8, - projectId: '3' - }, - date: '2026-03-19', - segments: [ - { id: 'seg17', distance: 25, time: 21.5, timeDiff: 1.2 }, - { id: 'seg18', distance: 25, time: 21.3, timeDiff: 1.0 } - ] - } + '3': [ + { id: 's5', name: '陈小刚', gender: '男', age: 14 } ], - '4': [ // 100米蝶泳项目的学生记录 - { - student: { - id: 's6', - name: '周小丽', - gender: '女', - age: 12, - projectName: '100米蝶泳', - totalTime: 78.6, - projectId: '4' - }, - date: '2026-03-24', - segments: [ - { id: 'seg19', distance: 25, time: 19.8, timeDiff: 1.5 }, - { id: 'seg20', distance: 25, time: 19.5, timeDiff: 1.2 }, - { id: 'seg21', distance: 25, time: 19.6, timeDiff: 1.3 }, - { id: 'seg22', distance: 25, time: 19.7, timeDiff: 1.4 } - ] - } + '4': [ + { id: 's6', name: '周小丽', gender: '女', age: 12 } ] } - // ==================== 生命周期钩子 ==================== - - /** - * 页面加载时触发 - * 在页面初始化时执行,只执行一次 - */ onLoad(() => { - // 初始化数据,加载项目列表 - loadProjectData() + loadData() }) - /** - * 页面显示时触发 - * 每次页面从后台切换到前台时执行 - */ onShow(() => { - // TODO: 如果需要在页面显示时刷新数据,可以在这里添加逻辑 - // 例如:检查是否有数据更新,重新加载当前选中的数据 + }) - // ==================== 业务逻辑方法 - 项目相关 ==================== + const loadData = () => { - /** - * 加载项目数据 - * 从后端 API 获取项目列表 - * TODO: 实际项目中应该调用后端 API - */ - const loadProjectData = () => { - // 示例代码(调用后端 API): - // Service.Request('/api/projects', 'GET').then(res => { - // projectList.value = res.data - // projectOptions.value = res.data - // }) - - // 当前使用模拟数据,已在初始化时赋值 - console.log('项目数据加载完成') } - /** - * 处理项目选择变化事件 - * 当用户选择项目时触发 - * @param e picker 选择事件对象,包含 detail.value 属性表示选中的索引 - */ const handleProjectChange = (e: any) => { - // 获取选中的项目索引 const index = e.detail.value - - // 更新选中项目的索引 selectedProjectIndex.value = index - - // 获取选中的项目 const selectedProject = projectList.value[index] if (selectedProject) { - // 更新选中项目的 ID selectedProjectId.value = selectedProject.id - - // 清空之前的日期和学生选择 selectedDate.value = '' - selectedStudentId.value = '' - selectedStudentIndex.value = -1 - currentStudent.value = null - segmentList.value = [] - - // 加载该项目下的学生列表 + selectedStudentIndexes.value = [] + selectedStudentIds.value = [] + tableData.value = [] loadProjectStudents(selectedProject.id) } - - // TODO: 实际项目中可能需要调用 API 获取该项目的学生列表 } - // ==================== 业务逻辑方法 - 日历相关 ==================== - - /** - * 处理日历月份切换事件 - * 当用户切换日历的月份时触发 - * @param e 切换事件对象,包含 year 和 month 属性 - */ const handleMonthSwitch = (e: any) => { - // 更新当前年份 currentYear.value = e.year - - // 更新当前月份 currentMonth.value = e.month - - // 重新加载数据 - // 获取切换后月份的训练记录和日历标记 - loadMonthData() - - // 清空选中的日期 - // 因为切换了月份,之前选择的日期可能不再显示 + loadData() selectedDate.value = '' - - // 清空学生选择和数据 - selectedStudentId.value = '' - selectedStudentIndex.value = -1 - currentStudent.value = null - segmentList.value = [] - - // TODO: 实际项目中应该调用 API 获取该月份的日历标记数据 + selectedStudentIndexes.value = [] + selectedStudentIds.value = [] + tableData.value = [] } - /** - * 处理日历日期选择事件 - * 当用户点击日历上的某个日期时触发 - * @param e 选择事件对象,fulldate 属性包含完整的日期字符串 - */ const handleDateChange = (e: any) => { - // 获取选择的完整日期,格式为 'YYYY-MM-DD' const date = e.fulldate - - // 更新选中的日期 selectedDate.value = date - - // 清空学生选择 - selectedStudentId.value = '' - selectedStudentIndex.value = -1 - currentStudent.value = null - segmentList.value = [] - - // TODO: 实际项目中可能需要调用 API 获取该日期的数据 - // 并根据日期加载该日期的学生列表 + selectedStudentIndexes.value = [] + selectedStudentIds.value = [] + tableData.value = [] } - /** - * 加载月份数据 - * 根据当前年月加载日历标记数据 - */ - const loadMonthData = () => { - // TODO: 实际项目中应该调用 API 获取该月份的日历标记 - // Service.Request('/api/segment/dates', 'GET', { - // year: currentYear.value, - // month: currentMonth.value, - // projectId: selectedProjectId.value - // }).then(res => { - // selectedDates.value = res.data - // }) - - console.log(`加载 ${currentYear.value}年${currentMonth.value}月 的日历标记数据`) - } - - // ==================== 业务逻辑方法 - 学生相关 ==================== - - /** - * 加载项目学生列表 - * 根据项目 ID 获取该项目的学生列表 - * @param projectId 项目 ID - */ const loadProjectStudents = (projectId: string) => { - // 从模拟数据中提取该项目的学生列表(去重) - const records = mockStudentRecords[projectId] || [] - const uniqueStudents = new Map() - records.forEach(record => { - if (!uniqueStudents.has(record.student.id)) { - uniqueStudents.set(record.student.id, record.student) - } - }) - studentList.value = Array.from(uniqueStudents.values()) - studentOptions.value = studentList.value - - console.log(`项目 ${projectId} 的学生列表加载完成,共 ${studentList.value.length} 位学生`) + const students = mockStudents[projectId] || [] + studentList.value = students + console.log(`项目 ${projectId} 的学生列表加载完成,共 ${students.length} 位学生`) } - /** - * 处理学生选择变化事件 - * 当用户选择学生时触发 - * @param e picker 选择事件对象,包含 detail.value 属性表示选中的索引 - */ - const handleStudentChange = (e: any) => { - // 获取选中的学生索引 - const index = e.detail.value - - // 更新选中学生的索引 - selectedStudentIndex.value = index - - // 获取选中的学生 - const selectedStudent = studentList.value[index] - - if (selectedStudent) { - // 更新选中学生的 ID - selectedStudentId.value = selectedStudent.id - - // 加载学生数据(包括分段数据) - loadStudentData(selectedProjectId.value, selectedStudentId.value, selectedDate.value) - } - - // TODO: 实际项目中可能需要调用 API 获取学生的详细数据和分段记录 + const selectAllStudents = () => { + selectedStudentIndexes.value = studentList.value.map((_, index) => index) } - /** - * 加载学生数据 - * 根据项目 ID、学生 ID 和日期获取学生的详细信息和分段数据 - * @param projectId 项目 ID - * @param studentId 学生 ID - * @param date 记录日期 - */ - const loadStudentData = (projectId: string, studentId: string, date: string) => { - // 从模拟数据中查找匹配的记录 - const records = mockStudentRecords[projectId] || [] - const record = records.find(r => r.student.id === studentId && r.date === date) - - if (record) { - // 更新学生信息 - currentStudent.value = record.student - - // 更新分段列表(已按原顺序,通过计算属性转换为表格数据) - segmentList.value = record.segments - - console.log(`学生数据加载完成:${record.student.name},共 ${record.segments.length} 个分段`) + const toggleStudentSelect = (index: number) => { + const idx = selectedStudentIndexes.value.indexOf(index) + if (idx > -1) { + selectedStudentIndexes.value.splice(idx, 1) } else { - // 未找到匹配的记录 - currentStudent.value = null - segmentList.value = [] - - Service.Msg('未找到该日期的记录数据') + selectedStudentIndexes.value.push(index) } - - // TODO: 实际项目中应该调用后端 API - // Service.Request(`/api/segments?projectId=${projectId}&studentId=${studentId}&date=${date}`, 'GET') - // .then(res => { - // currentStudent.value = res.data.student - // segmentList.value = res.data.segments - // }) } - // ==================== 业务逻辑方法 - 分段相关 ==================== + const confirmStudentSelect = () => { + selectedStudentIds.value = selectedStudentIndexes.value.map(index => studentList.value[index].id) + showStudentSelect.value = false + filterAndLoadData() + } - /** - * 处理分段表格单元格点击事件 - * 点击某分段单元格查看详情 - * @param event 点击事件对象,包含行索引、列索引、单元格数据等信息 - */ - const handleSegmentCellClick = (event: any) => { - console.log('分段表格单元格点击事件:', event) - // TODO: 可以跳转到分段详情页面或弹出详细信息 - // Service.GoPage('/pages/dataAnalyze/segmentDetail', { segmentId: event.rowData.id }) + const closeStudentSelect = () => { + showStudentSelect.value = false + } + + const filterAndLoadData = () => { + if (!selectedDate.value || selectedStudentIndexes.value.length === 0) { + tableData.value = [] + return + } + + const projectData = mockData[selectedProjectId.value] + if (!projectData) { + tableData.value = [] + return + } + + const dateData = projectData[selectedDate.value] + if (!dateData) { + tableData.value = [] + return + } + + const selectedStudentNames = selectedStudentIndexes.value.map(index => studentList.value[index].name) + const filteredData = dateData.filter(item => selectedStudentNames.includes(item.name)) + + tableData.value = filteredData.map(item => ({ + date: selectedDate.value, + name: item.name, + projectName: item.projectName, + segment20: item.segment20.toFixed(3), + segment70: item.segment70.toFixed(3), + segment120: item.segment120.toFixed(3), + segment170: item.segment170.toFixed(3) + })) } + \ No newline at end of file diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index 427c482..c3acdbb 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -13,7 +13,7 @@ - + @@ -29,7 +29,7 @@ - + @@ -74,7 +74,7 @@ - + 新增项目 @@ -84,7 +84,7 @@ 项目列表 + @click=" goPageFunc() "> {{ project.name.charAt(0) }} @@ -128,7 +128,9 @@ age : string }[] } - + + let currentIndex=ref(0) + // 模拟项目数据 const projects = ref([ { @@ -175,10 +177,21 @@ const showTimingModal = ref(false) const showDetailModal = ref(false) const currentProject = ref(null) - + + + // 生命周期 + onMounted(() => { + // TODO: 实际应从接口获取项目列表 + }) + + onUnmounted(() => { + // 清理 + }) + // 显示计时项目列表弹窗 - const showTimingProjectModal = () => { + const showTimingProjectModal = (index:any) => { showTimingModal.value = true + currentIndex.value=index } // 关闭计时项目列表弹窗 @@ -200,7 +213,7 @@ const createSegmentProject = () => { Service.GoPage('/pages/userFunc/setCourse') } - + // 显示项目详情 const showProjectDetail = (project : Project) => { currentProject.value = project @@ -213,15 +226,17 @@ showDetailModal.value = false currentProject.value = null } - - // 生命周期 - onMounted(() => { - // TODO: 实际应从接口获取项目列表 - }) - - onUnmounted(() => { - // 清理 - }) + + + const goPageFunc=()=>{ + if(currentIndex.value==1){ + Service.GoPage('/pages/userFunc/swiming') + }else{ + Service.GoPage('/pages/userFunc/project') + } + + } + \ No newline at end of file diff --git a/src/pages/userFunc/student.vue b/src/pages/userFunc/student.vue index 47e5c59..4681250 100644 --- a/src/pages/userFunc/student.vue +++ b/src/pages/userFunc/student.vue @@ -32,6 +32,10 @@ {{ student.gender }} + + 出生日期: + {{ student.birthDate }} + 年龄: {{ student.age }}岁 @@ -84,18 +88,40 @@ 性别 - + + + - + + + - 年龄 - + 出生日期 + + + + + {{ formData.birthDate || '请选择出生日期' }} + + + + + + + + + + 年龄 + {{ calculateAge(formData.birthDate) }} + + + 学校(选填) @@ -128,16 +154,17 @@ name : string gender : string age : string + birthDate : string school : string address : string } // 学员列表 const students = ref([ - { id: '001', name: '张三', gender: '男', age: '12', school: '第一小学', address: '北京市朝阳区' }, - { id: '002', name: '李四', gender: '女', age: '13', school: '', address: '' }, - { id: '003', name: '王五', gender: '男', age: '11', school: '第二小学', address: '' }, - { id: '004', name: '赵六', gender: '女', age: '12', school: '', address: '上海市浦东新区' } + { id: '001', name: '张三', gender: '男', age: '12', birthDate: '2012-05-15', school: '第一小学', address: '北京市朝阳区' }, + { id: '002', name: '李四', gender: '女', age: '13', birthDate: '2011-08-20', school: '', address: '' }, + { id: '003', name: '王五', gender: '男', age: '11', birthDate: '2013-03-10', school: '第二小学', address: '' }, + { id: '004', name: '赵六', gender: '女', age: '12', birthDate: '2012-11-25', school: '', address: '上海市浦东新区' } ]) // 弹窗状态 @@ -150,6 +177,7 @@ name: '', gender: '男', age: '', + birthDate: '', school: '', address: '' }) @@ -165,6 +193,25 @@ }) + // 计算年龄 + const calculateAge = (birthDate : string) : number => { + if (!birthDate) return 0 + const birth = new Date(birthDate) + const today = new Date() + let age = today.getFullYear() - birth.getFullYear() + const monthDiff = today.getMonth() - birth.getMonth() + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) { + age-- + } + return age + } + + // 出生日期变化时自动计算年龄 + const onBirthDateChange = (e : any) => { + formData.value.birthDate = e.detail.value + formData.value.age = calculateAge(e.detail.value).toString() + } + // 打开编辑弹窗 const openEditModal = (student : Student) => { editingStudent.value = student @@ -173,6 +220,7 @@ name: student.name, gender: student.gender, age: student.age, + birthDate: student.birthDate, school: student.school, address: student.address } @@ -189,6 +237,7 @@ name: '', gender: '男', age: '', + birthDate: '', school: '', address: '' } @@ -201,8 +250,8 @@ return } - if (!formData.value.age.trim()) { - Service.Msg('请输入年龄') + if (!formData.value.birthDate) { + Service.Msg('请选择出生日期') return } @@ -212,7 +261,8 @@ id: Date.now().toString().slice(-6), name: formData.value.name.trim(), gender: formData.value.gender, - age: formData.value.age.trim(), + age: formData.value.age, + birthDate: formData.value.birthDate, school: formData.value.school.trim(), address: formData.value.address.trim() } @@ -226,7 +276,8 @@ ...students.value[index], name: formData.value.name.trim(), gender: formData.value.gender, - age: formData.value.age.trim(), + age: formData.value.age, + birthDate: formData.value.birthDate, school: formData.value.school.trim(), address: formData.value.address.trim() } @@ -554,6 +605,101 @@ background-color: #fff; box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.15); } + + &.date-picker { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12rpx; + line-height: 88rpx; + + &.placeholder { + color: #999; + } + } + } + + .date-picker-wrapper { + position: relative; + + picker { + width: 100%; + } + } + + .age-display { + margin-top: 24rpx; + padding: 28rpx 32rpx; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + border-radius: 20rpx; + display: flex; + align-items: center; + gap: 20rpx; + box-shadow: 0 8rpx 24rpx rgba(102, 126, 234, 0.3); + position: relative; + overflow: hidden; + + &::before { + content: ''; + position: absolute; + top: -50%; + right: -20%; + width: 100rpx; + height: 100rpx; + background: rgba(255, 255, 255, 0.1); + border-radius: 50%; + } + + &::after { + content: ''; + position: absolute; + bottom: -30%; + left: 10%; + width: 60rpx; + height: 60rpx; + background: rgba(255, 255, 255, 0.08); + border-radius: 50%; + } + + .age-icon { + width: 72rpx; + height: 72rpx; + background: rgba(255, 255, 255, 0.2); + border-radius: 18rpx; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + backdrop-filter: blur(10rpx); + border: 2rpx solid rgba(255, 255, 255, 0.3); + } + + .age-info { + flex: 1; + display: flex; + align-items: baseline; + gap: 8rpx; + + .age-label { + font-size: 24rpx; + color: rgba(255, 255, 255, 0.8); + font-weight: 500; + } + + .age-value { + font-size: 56rpx; + font-weight: 800; + color: #fff; + line-height: 1; + text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.2); + } + + .age-unit { + font-size: 28rpx; + color: rgba(255, 255, 255, 0.9); + font-weight: 600; + } + } } .gender-selector { @@ -562,17 +708,20 @@ .gender-option { flex: 1; - height: 88rpx; + height: 100rpx; background-color: #f5f5f5; - border-radius: 16rpx; + border-radius: 20rpx; display: flex; + flex-direction: column; align-items: center; justify-content: center; gap: 8rpx; - font-size: 28rpx; + font-size: 26rpx; color: #666; - transition: all 0.25s ease; + transition: all 0.3s ease; border: 2rpx solid transparent; + position: relative; + overflow: hidden; &:active { transform: scale(0.96); @@ -580,9 +729,25 @@ &.active { font-weight: 600; - background-color: #e6f7ff; + background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%); border-color: #1890ff; - color: #1890ff; + color: #fff; + box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.3); + + .gender-icon-box { + background-color: rgba(255, 255, 255, 0.2); + } + } + + .gender-icon-box { + width: 56rpx; + height: 56rpx; + background-color: #e6f7ff; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.3s ease; } } } diff --git a/src/pages/userFunc/swiming.vue b/src/pages/userFunc/swiming.vue index 1e306c8..67764d5 100644 --- a/src/pages/userFunc/swiming.vue +++ b/src/pages/userFunc/swiming.vue @@ -6,13 +6,6 @@ 当前总用时 {{ formatTime(currentTime) }} - - - - - @@ -25,7 +18,7 @@ + class="athlete-item" :class="{ finished: athlete.finished }"> {{ index+1 }} {{ athlete.name }} @@ -33,7 +26,7 @@ {{ formatTime(athlete.time) }} - 最快: {{ formatTime(athlete.bestTime) }} + 最快: {{ athlete.bestTime !== null ? formatTime(athlete.bestTime) : '无' }} @@ -49,18 +42,27 @@ - - - + + + + + + + + + @@ -76,12 +78,17 @@ 添加学生 + + + 修改组别 + + 取消 - + @@ -164,13 +171,39 @@ + + + + + 修改组别 + + + + + + + 组别个数 + + + + 一组多少人 + + + + + + + + + 保存 - + @@ -189,15 +222,14 @@ time : number bestTime : number | null finished : boolean - isFastest : boolean } // 选手列表 const athletes = ref([ - { id: '1', number: '01', name: '张三', gender: '男', age: '18', birthday: '2006-01-01', time: 0, bestTime: null, finished: false, isFastest: false }, - { id: '2', number: '02', name: '李四', gender: '女', age: '17', birthday: '2007-01-01', time: 0, bestTime: null, finished: false, isFastest: false }, - { id: '3', number: '03', name: '王五', gender: '男', age: '19', birthday: '2005-01-01', time: 0, bestTime: null, finished: false, isFastest: false }, - { id: '4', number: '04', name: '赵六', gender: '女', age: '18', birthday: '2006-01-01', time: 0, bestTime: null, finished: false, isFastest: false } + { id: '1', number: '01', name: '张三', gender: '男', age: '18', birthday: '2006-01-01', time: 0, bestTime: null, finished: false }, + { id: '2', number: '02', name: '李四', gender: '女', age: '17', birthday: '2007-01-01', time: 0, bestTime: null, finished: false }, + { id: '3', number: '03', name: '王五', gender: '男', age: '19', birthday: '2005-01-01', time: 0, bestTime: null, finished: false }, + { id: '4', number: '04', name: '赵六', gender: '女', age: '18', birthday: '2006-01-01', time: 0, bestTime: null, finished: false } ]) // 计时器状态 @@ -218,6 +250,11 @@ const showMoreModal = ref(false) const showStopwatchModal = ref(false) const showAddStudentModal = ref(false) + const showGroupModal = ref(false) + + // 组别设置 + const groupCount = ref('') + const groupSize = ref('') // 新学生信息 const newStudentName = ref('') @@ -293,6 +330,17 @@ // 记录下一个选手 const recordNextAthlete = () => { + // 查找下一个未完成的选手 + while (currentAthleteIndex.value < athletes.value.length) { + const athlete = athletes.value[currentAthleteIndex.value] + if (athlete.finished) { + // 已手动计时的学生,跳过 + currentAthleteIndex.value++ + } else { + break + } + } + if (currentAthleteIndex.value >= athletes.value.length) { Service.Msg('所有选手已完成') return @@ -302,14 +350,6 @@ athlete.finished = true athlete.time = currentTime.value - // 更新最快记录 - if (athlete.bestTime === null || athlete.time < athlete.bestTime) { - athlete.bestTime = athlete.time - } - - // 更新最快选手 - updateFastestAthlete() - currentAthleteIndex.value++ // 如果所有选手都完成了,停止计时 @@ -319,32 +359,13 @@ } } - // 更新最快选手 - const updateFastestAthlete = () => { - // 先清除所有最快标记 - athletes.value.forEach(a => a.isFastest = false) - - // 找出已完成的选手 - const finished = athletes.value.filter(a => a.finished && a.time > 0) - if (finished.length === 0) return - - // 找出时间最短的 - const fastest = finished.reduce((prev, curr) => { - return prev.time < curr.time ? prev : curr - }) - - fastest.isFastest = true - } - // 重置选手时间 const resetAthleteTime = (athlete : Athlete) => { athlete.time = 0 athlete.finished = false - athlete.isFastest = false // 重新排序索引 reorderAthletes() - updateFastestAthlete() } // 处理选手第一个按钮点击 @@ -358,14 +379,6 @@ athlete.finished = true athlete.time = currentTime.value - // 更新最快记录 - if (athlete.bestTime === null || athlete.time < athlete.bestTime) { - athlete.bestTime = athlete.time - } - - // 更新最快选手 - updateFastestAthlete() - // 检查是否所有选手都完成了 const allFinished = athletes.value.every(a => a.finished) if (allFinished) { @@ -382,6 +395,24 @@ currentAthleteIndex.value = athletes.value.length - unfinished.length } + // 记录所有未完成选手 + const recordAll = () => { + let recordedCount = 0 + athletes.value.forEach(athlete => { + if (!athlete.finished) { + athlete.finished = true + athlete.time = currentTime.value + recordedCount++ + } + }) + + if (recordedCount > 0) { + Service.Msg(`已记录${recordedCount}位选手`, 'success') + } else { + Service.Msg('没有需要记录的选手') + } + } + // 重置所有 const resetAll = () => { stopTimer() @@ -391,7 +422,6 @@ athletes.value.forEach(athlete => { athlete.time = 0 athlete.finished = false - athlete.isFastest = false }) } @@ -476,6 +506,37 @@ newStudentAge.value = '' } + // 关闭组别弹出框 + const closeGroupModal = () => { + showGroupModal.value = false + groupCount.value = '' + groupSize.value = '' + } + + // 确认组别设置 + const confirmGroupSettings = () => { + if (!groupCount.value.trim()) { + Service.Msg('请输入组别个数') + return + } + if (!groupSize.value.trim()) { + Service.Msg('请输入一组多少人') + return + } + const count = parseInt(groupCount.value) + const size = parseInt(groupSize.value) + if (isNaN(count) || count <= 0) { + Service.Msg('请输入有效的组别个数') + return + } + if (isNaN(size) || size <= 0) { + Service.Msg('请输入有效的一组人数') + return + } + Service.Msg(`已设置${count}组,每组${size}人`, 'success') + closeGroupModal() + } + // 添加新学生 const addNewStudent = () => { if (!newStudentName.value.trim()) { @@ -497,8 +558,7 @@ birthday: newStudentBirthday.value.trim(), time: 0, bestTime: null, - finished: false, - isFastest: false + finished: false } athletes.value.push(newAthlete) @@ -514,7 +574,7 @@ stopwatchMode: stopwatchMode.value, intervalTime: intervalTime.value } - Service.SetStorageCache('swimTimingData', data) + console.log(data); Service.Msg('保存成功', 'success') } @@ -549,7 +609,7 @@ .total-time { font-size: 80rpx; font-weight: bold; - color: #1890ff; + color: #ff0000; } } @@ -652,8 +712,8 @@ .athlete-time { font-size: 32rpx; - font-weight: 600; - color: #333333; + font-weight: bold; + color: #ff0000; font-family: 'DIN Alternate', monospace; } @@ -680,60 +740,122 @@ /* 底部控制按钮 */ .control-buttons { display: flex; - gap: 20rpx; + gap: 24rpx; justify-content: center; - padding-bottom: env(safe-area-inset-bottom); position: fixed; bottom: 0; left: 0; right: 0; - background-color: #fff; - padding: 20rpx; - padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); - border-top: 1rpx solid #f0f0f0; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #ffffff 100%); + padding: 32rpx 40rpx; + padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); + border-top: 1rpx solid rgba(0, 0, 0, 0.06); + box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.08); + + .button-group { + flex: 1; + display: flex; + justify-content: center; + } .start-btn, .pause-btn, - .reset-all-btn { + .reset-all-btn, + .record-btn { flex: 1; - height: 88rpx; - border-radius: 12rpx; + height: 100rpx; + border-radius: 20rpx; border: none; - font-size: 30rpx; + font-size: 32rpx; font-weight: 600; display: flex; align-items: center; justify-content: center; - gap: 10rpx; - max-width: 220rpx; + gap: 12rpx; + position: relative; + overflow: hidden; + transition: all 0.3s ease; + box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15); + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, transparent 100%); + border-radius: 20rpx 20rpx 0 0; + } + + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2rpx; + background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%); + } + + :deep(.u-icon) { + position: relative; + z-index: 1; + } } .start-btn { - background-color: #52c41a; + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); color: #ffffff; + box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4); - &:disabled { - background-color: #ccc; + &:active { + transform: scale(0.96); + box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3); } } .pause-btn { - background-color: #fa541c; + background: linear-gradient(135deg, #fa541c 0%, #ff7a45 100%); color: #ffffff; + box-shadow: 0 6rpx 20rpx rgba(250, 84, 28, 0.4); - &:disabled { - background-color: #ccc; + &:active { + transform: scale(0.96); + box-shadow: 0 2rpx 8rpx rgba(250, 84, 28, 0.3); } } .reset-all-btn { - background-color: #1890ff; + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); color: #ffffff; + box-shadow: 0 6rpx 20rpx rgba(24, 144, 255, 0.4); + + &:active { + transform: scale(0.96); + box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3); + } + } + + .record-btn { + background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); + color: #ffffff; + box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4); + + &:active { + transform: scale(0.96); + box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3); + } } .btn-text { - font-size: 30rpx; + font-size: 32rpx; color: #ffffff; + font-weight: 600; + letter-spacing: 2rpx; + position: relative; + z-index: 1; + text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1); } } @@ -785,9 +907,10 @@ } } - /* 秒表模式和添加学生弹出框 */ + /* 秒表模式、添加学生和组别弹出框 */ .stopwatch-modal, - .add-student-modal { + .add-student-modal, + .group-modal { position: fixed; top: 50%; left: 50%; @@ -991,15 +1114,36 @@ .float-save-btn { position: fixed; right: 40rpx; - bottom: 160rpx; - width: 100rpx; - height: 100rpx; - background: linear-gradient(135deg, #1890ff, #40a9ff); + bottom: 180rpx; + width: 110rpx; + height: 110rpx; + background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; - box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.4); + box-shadow: 0 6rpx 20rpx rgba(24, 144, 255, 0.4); z-index: 100; + font-size: 28rpx; + font-weight: 600; + color: #ffffff; + letter-spacing: 2rpx; + transition: all 0.3s ease; + + &::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 50%; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, transparent 100%); + border-radius: 50% 50% 0 0; + } + + &:active { + transform: scale(0.96); + box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3); + } } \ No newline at end of file