提交数据

This commit is contained in:
Ls
2026-04-06 09:31:25 +08:00
parent a4bcd5ae1f
commit f682c0316b
8 changed files with 2352 additions and 1684 deletions

View File

@@ -77,7 +77,8 @@
{ {
"path": "project", "path": "project",
"style": { "style": {
"navigationBarTitleText": "包干" "navigationBarTitleText": "包干",
"navigationStyle": "custom"
} }
} }
] ]

View File

@@ -14,7 +14,8 @@
<text class="section-title">选择项目</text> <text class="section-title">选择项目</text>
</view> </view>
<!-- 项目选择器 --> <!-- 项目选择器 -->
<picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex" @change="handleProjectChange"> <picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex"
@change="handleProjectChange">
<view class="picker-wrapper"> <view class="picker-wrapper">
<text class="picker-text">{{ selectedProjectName }}</text> <text class="picker-text">{{ selectedProjectName }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon> <u-icon name="arrow-down" size="18" color="#999"></u-icon>
@@ -31,7 +32,9 @@
</view> </view>
<!-- 学生列表 --> <!-- 学生列表 -->
<view class="student-list"> <view class="student-list">
<view v-for="student in projectStudents" :key="student.id" :class="['student-item', { 'selected': selectedStudentIds.includes(student.id) }]" @click="toggleStudent(student.id)"> <view v-for="student in projectStudents" :key="student.id"
:class="['student-item', { 'selected': selectedStudentIds.includes(student.id) }]"
@click="toggleStudent(student.id)">
<!-- 学生头像使用首字母作为头像 --> <!-- 学生头像使用首字母作为头像 -->
<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>
@@ -75,9 +78,11 @@
<text class="chart-desc">历史训练成绩变化曲线</text> <text class="chart-desc">历史训练成绩变化曲线</text>
</view> </view>
<!-- 折线图容器 --> <!-- 折线图容器 -->
<view class="chart-box"> <scroll-view class="chart-scroll" scroll-x :show-scrollbar="false">
<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>
</view> </view>
<!-- 图例说明 --> <!-- 图例说明 -->
@@ -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({ const lineOpts = ref({
color: chartColors, color: chartColors,
@@ -187,8 +200,9 @@
}, },
xAxis: { xAxis: {
disableGrid: true, disableGrid: true,
itemCount: 6, rotateLabel: true,
rotateLabel: true scrollShow: true,
scrollAlign: 'left'
}, },
yAxis: { yAxis: {
data: [{ data: [{
@@ -430,7 +444,7 @@
.subtitle { .subtitle {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: #999;
}
} }
} }
@@ -622,12 +636,18 @@
} }
} }
.chart-box { .chart-scroll {
width: 100%; width: 100%;
height: 500rpx; height: 500rpx;
background-color: #fafafa; background-color: #fafafa;
border-radius: 12rpx; border-radius: 12rpx;
overflow: hidden; overflow: hidden;
white-space: nowrap;
.chart-box {
height: 100%;
display: inline-block;
}
} }
} }

View File

@@ -8,78 +8,81 @@
</view> </view>
</view> </view>
<!-- 选择区域 --> <!-- 项目选择区域 -->
<view class="selector-section"> <view class="select-section">
<view class="selector-item" @click="showProjectPicker = true"> <view class="select-label">
<text class="selector-label">项目名称</text> <text class="label-text">选择项目</text>
<view class="selector-value">
<text class="value-text">{{ selectedProject }}</text>
<u-icon name="arrow-down" size="24" color="#999"></u-icon>
</view> </view>
<picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex"
@change="handleProjectChange">
<view class="picker-wrapper">
<text class="picker-text">{{ selectedProjectName }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view> </view>
<view class="selector-item" @click="showStudentPicker = true"> </picker>
<text class="selector-label">选择学生</text>
<view class="selector-value">
<text class="value-text">{{ selectedStudent }}</text>
<u-icon name="arrow-down" size="24" color="#999"></u-icon>
</view> </view>
</view>
</view>
<!-- 项目选择器 -->
<u-picker
:show="showProjectPicker"
:columns="[projectList]"
@confirm="handleProjectConfirm"
@cancel="showProjectPicker = false"
keyName="label"></u-picker>
<!-- 学生选择器 -->
<u-picker
:show="showStudentPicker"
:columns="[studentList]"
@confirm="handleStudentConfirm"
@cancel="showStudentPicker = false"
keyName="label"
multiple
:defaultIndex="defaultStudentIndex"></u-picker>
<!-- 日历组件 --> <!-- 日历组件 -->
<view class="calendar-wrapper"> <view class="calendar-wrapper" v-if="selectedProjectId">
<uni-calendar <uni-calendar :insert="true" :range='true' :lunar="false" :show-month="true" :selected="selectedDates"
:insert="true" @monthSwitch="handleMonthSwitch" @change="handleDateChange">
:lunar="false"
:show-month="true"
:selected="selectedDates"
@monthSwitch="handleMonthSwitch"
@change="handleDateChange">
</uni-calendar> </uni-calendar>
</view> </view>
<!-- 选中日期数据统计 --> <!-- 学生选择区域 -->
<view class="date-summary" v-if="selectedDate"> <view class="select-section" v-if="selectedProjectId && selectedDate">
<view class="summary-item"> <view class="select-label">
<text class="summary-label">中日期</text> <text class="label-text">择学生</text>
<text class="summary-value">{{ selectedDate }}</text>
</view> </view>
<view class="summary-divider"></view> <view class="picker-wrapper" @click="showStudentPicker = true">
<view class="summary-item"> <text class="picker-text">{{ selectedStudentDisplay }}</text>
<text class="summary-label">训练人数</text> <u-icon name="arrow-down" size="18" color="#999"></u-icon>
<text class="summary-value">{{ tableData.length }}</text>
</view>
<view class="summary-divider"></view>
<view class="summary-item">
<text class="summary-label">完成率平均</text>
<text class="summary-value">{{ averageCompletion }}%</text>
</view> </view>
</view> </view>
<!-- 学生多选选择器 -->
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
<view class="student-picker-modal" v-if="showStudentPicker">
<view class="picker-header">
<text class="header-title">选择学生</text>
<view class="header-actions">
<text class="action-btn" @click="selectAllStudents">全选</text>
<text class="action-btn primary" @click="confirmStudentSelection">确定</text>
</view>
</view>
<scroll-view class="student-list" scroll-y>
<view v-for="(student, index) in studentList" :key="student.id" class="student-item"
:class="{ 'selected': selectedStudentIndexes.includes(index) }"
@click="toggleStudentSelection(index)">
<view class="item-checkbox">
<view class="checkbox-inner" :class="{ 'checked': selectedStudentIndexes.includes(index) }">
<u-icon v-if="selectedStudentIndexes.includes(index)" name="checkmark" size="14"
color="#fff"></u-icon>
</view>
</view>
<view class="item-avatar">
<view class="avatar-circle"
:class="{ 'male': student.gender === '男', 'female': student.gender === '女' }">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
</view>
<view class="item-info">
<text class="item-name">{{ student.name }}</text>
<view class="item-meta">
<text class="gender-badge"
:class="{ 'male': student.gender === '男', 'female': student.gender === '女' }">
{{ student.gender }}
</text>
<text class="age-text">{{ student.age }}</text>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 数据表格 --> <!-- 数据表格 -->
<view class="table-section" v-if="selectedDate"> <view class="table-section" v-if="selectedDate && selectedStudentIndexes.length > 0">
<sl-table <sl-table :columns="columns" :tableData="tableData" @cell-click="handleCellClick">
:columns="columns"
:tableData="tableData"
@cell-click="handleCellClick">
<template #empty> <template #empty>
<view class="empty-container"> <view class="empty-container">
<view class="empty-icon"> <view class="empty-icon">
@@ -100,15 +103,43 @@
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed } from 'vue' 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 currentYear = ref(new Date().getFullYear())
const currentMonth = ref(new Date().getMonth() + 1) const currentMonth = ref(new Date().getMonth() + 1)
// 选中的日期
const selectedDate = ref('') const selectedDate = ref('')
// 表格列定义
const columns = ref([ const columns = ref([
{
label: '日期',
prop: 'date',
width: '100px'
},
{ {
label: '姓名', label: '姓名',
prop: 'name', prop: 'name',
@@ -136,211 +167,218 @@
} }
]) ])
// 表格数据
interface TableDataItem {
name: string
projectName: string
plan: string
completion: number
detail: string
}
const tableData = ref<TableDataItem[]>([]) const tableData = ref<TableDataItem[]>([])
// 项目列表 const projectList = ref<Project[]>([
const projectList = ref([ { id: '1', name: '100米自由泳' },
{ label: '全部项目', value: 'all' }, { id: '2', name: '200米自由泳' },
{ label: '100米自由泳', value: '100m-free' }, { id: '3', name: '400米自由泳' },
{ label: '200米自由泳', value: '200m-free' }, { id: '4', name: '100米蛙泳' }
{ label: '400米自由泳', value: '400m-free' },
{ label: '100米蛙泳', value: '100m-breast' }
]) ])
// 学生列表 const projectOptions = ref(projectList.value)
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 selectedProjectIndex = ref(-1)
const selectedProject = ref('全部项目') const selectedProjectId = ref('')
const selectedStudent = ref('全部学生')
const selectedProjectValue = ref('all')
const selectedStudentValues = ref<string[]>([])
// 选择器显示状态 const selectedProjectName = computed(() => {
const showProjectPicker = ref(false) if (selectedProjectIndex.value === -1) {
const showStudentPicker = ref(false) return '请选择项目'
const defaultStudentIndex = ref<number[]>([0]) }
return projectList.value[selectedProjectIndex.value]?.name || '请选择项目'
// 日历打点数据
// 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 mockData: Record<string, TableDataItem[]> = {
'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([ const selectedDates = ref([
{ date: '2026-03-28', info: '训练' }, { date: '2026-03-28', info: '训练' },
{ date: '2026-03-29', info: '训练' }, { date: '2026-03-29', info: '训练' },
{ date: '2026-03-30', info: '训练' } { date: '2026-03-30', info: '训练' }
]) ])
const studentList = ref<Student[]>([])
const selectedStudentIndexes = ref<number[]>([])
const selectedStudentIds = ref<string[]>([])
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<string, Record<string, TableDataItem[]>> = {
'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<string, Student[]> = {
'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(() => { onLoad(() => {
loadData() loadData()
}) })
onShow(() => { onShow(() => {
// 页面显示时刷新数据
}) })
// 加载数据
const loadData = () => { const loadData = () => {
// TODO: 调用API获取数据
} }
// 月份单元格点击处理
const handleCellClick = (event : any) => { const handleCellClick = (event : any) => {
console.log('单元格点击事件:', event) console.log('单元格点击事件:', event)
} }
// 月份切换处理 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) => { const handleMonthSwitch = (e : any) => {
currentYear.value = e.year currentYear.value = e.year
currentMonth.value = e.month currentMonth.value = e.month
loadData() loadData()
// 切换月份后清空选中日期
selectedDate.value = '' selectedDate.value = ''
selectedStudentIndexes.value = []
selectedStudentIds.value = []
tableData.value = [] tableData.value = []
} }
// 日期选择处理
const handleDateChange = (e : any) => { const handleDateChange = (e : any) => {
console.log(e);
const date = e.fulldate const date = e.fulldate
selectedDate.value = date selectedDate.value = date
filterAndLoadData() selectedStudentIndexes.value = []
selectedStudentIds.value = []
tableData.value = []
} }
// 项目选择确认 const loadProjectStudents = (projectId : string) => {
const handleProjectConfirm = (e: any) => { const students = mockStudents[projectId] || []
const item = e.value[0] studentList.value = students
selectedProject.value = item.label console.log(`项目 ${projectId} 的学生列表加载完成,共 ${students.length} 位学生`)
selectedProjectValue.value = item.value
showProjectPicker.value = false
filterAndLoadData()
} }
// 学生选择确认 const selectAllStudents = () => {
const handleStudentConfirm = (e: any) => { selectedStudentIndexes.value = studentList.value.map((_, index) => index)
const items = e.value }
selectedStudentValues.value = items.map((item: any) => item.value)
if (selectedStudentValues.value.length === 0) { const handleStudentChange = (e : any) => {
selectedStudent.value = '未选择' console.log('学生选择变化:', e)
} else if (selectedStudentValues.value.includes('all')) { }
selectedStudent.value = '全部学生'
const toggleStudentSelection = (index : number) => {
const idx = selectedStudentIndexes.value.indexOf(index)
if (idx > -1) {
selectedStudentIndexes.value.splice(idx, 1)
} else { } else {
const selectedNames = items.map((item: any) => item.label) selectedStudentIndexes.value.push(index)
selectedStudent.value = selectedNames.length > 2 }
? `${selectedNames.slice(0, 2).join(', ')}${selectedNames.length}`
: selectedNames.join(', ')
} }
defaultStudentIndex.value = e.indexs const confirmStudentSelection = () => {
selectedStudentIds.value = selectedStudentIndexes.value.map(index => studentList.value[index].id)
showStudentPicker.value = false showStudentPicker.value = false
filterAndLoadData() filterAndLoadData()
} }
// 过滤并加载数据 const closeStudentPicker = () => {
showStudentPicker.value = false
}
const filterAndLoadData = () => { const filterAndLoadData = () => {
if (!selectedDate.value) { if (!selectedDate.value || selectedStudentIndexes.value.length === 0) {
tableData.value = [] tableData.value = []
return return
} }
const dateData = mockData[selectedDate.value] const projectData = mockData[selectedProjectId.value]
if (!projectData) {
tableData.value = []
return
}
const dateData = projectData[selectedDate.value]
if (!dateData) { if (!dateData) {
tableData.value = [] tableData.value = []
return return
} }
let filteredData = [...dateData] const selectedStudentNames = selectedStudentIndexes.value.map(index => studentList.value[index].name)
tableData.value = dateData.filter(item => selectedStudentNames.includes(item.name))
// 按项目过滤
if (selectedProjectValue.value !== 'all') {
const projectMap: Record<string, string> = {
'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<string, string> = {
'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
} }
</script> </script>
@@ -354,7 +392,6 @@
padding-bottom: 40rpx; padding-bottom: 40rpx;
} }
/* 页面标题区域 */
.header-section { .header-section {
background-color: #fff; background-color: #fff;
padding: 32rpx 28rpx 24rpx; padding: 32rpx 28rpx 24rpx;
@@ -376,59 +413,272 @@
} }
} }
/* 选择器区域 */ .select-section {
.selector-section {
background-color: #fff; background-color: #fff;
margin: 0 20rpx 20rpx; margin: 0 20rpx 20rpx;
border-radius: 16rpx; border-radius: 20rpx;
padding: 24rpx; padding: 28rpx;
display: flex;
gap: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
.selector-item { .select-label {
flex: 1; margin-bottom: 16rpx;
.label-text {
font-size: 26rpx;
font-weight: 600;
color: #666;
}
}
.picker-wrapper {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
background-color: #f8f8f8; background-color: #f8f8f8;
border-radius: 12rpx; border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
transition: all 0.3s ease;
.selector-label { &:active {
font-size: 28rpx; background-color: #f0f0f0;
color: #666; border-color: #faad14;
margin-right: 16rpx; transform: scale(0.98);
} }
.selector-value { .picker-text {
flex: 1;
display: flex;
align-items: center;
justify-content: flex-end;
.value-text {
font-size: 28rpx; font-size: 28rpx;
color: #333; color: #333;
font-weight: 500;
margin-right: 8rpx;
}
} }
} }
} }
/* 日历组件包装 */
.calendar-wrapper { .calendar-wrapper {
background-color: #fff; background-color: #fff;
margin-bottom: 20rpx; margin: 0 20rpx 20rpx;
padding: 20rpx 0; 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;
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;
}
}
}
}
}
}
.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 { .date-summary {
background-color: #fff; background-color: #fff;
margin: 0 20rpx 20rpx; margin: 0 20rpx 20rpx;
border-radius: 16rpx; border-radius: 20rpx;
padding: 24rpx; padding: 24rpx;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -459,16 +709,14 @@
} }
} }
/* 表格区域 */
.table-section { .table-section {
margin: 0 20rpx; margin: 0 20rpx;
background-color: #fff; background-color: #fff;
border-radius: 16rpx; border-radius: 20rpx;
overflow: hidden; overflow: hidden;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
} }
/* 空状态容器 */
.empty-container { .empty-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -486,11 +734,10 @@
} }
} }
/* 提示状态 */
.hint-state { .hint-state {
margin: 0 20rpx; margin: 0 20rpx;
background-color: #fff; background-color: #fff;
border-radius: 16rpx; border-radius: 20rpx;
padding: 80rpx 40rpx; padding: 80rpx 40rpx;
text-align: center; text-align: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
@@ -505,7 +752,6 @@
} }
} }
/* sl-table 样式覆盖 */
::v-deep .sl-table { ::v-deep .sl-table {
width: 100%; width: 100%;
} }
@@ -533,4 +779,24 @@
::v-deep .sl-table__body__row:nth-child(odd) { ::v-deep .sl-table__body__row:nth-child(odd) {
background-color: #fff !important; background-color: #fff !important;
} }
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@
<!-- 创建项目模块列表 --> <!-- 创建项目模块列表 -->
<view class="create-modules"> <view class="create-modules">
<!-- 计时项目模块 --> <!-- 计时项目模块 -->
<view @click="showTimingProjectModal" class="create-card timing-card"> <view @click="showTimingProjectModal(1)" class="create-card timing-card">
<view class="card-icon"> <view class="card-icon">
<view class="icon-circle timing-icon"> <view class="icon-circle timing-icon">
<u-icon name="clock" size="40" color="#fff"></u-icon> <u-icon name="clock" size="40" color="#fff"></u-icon>
@@ -29,7 +29,7 @@
</view> </view>
<!-- 包干项目模块 --> <!-- 包干项目模块 -->
<view @click="createPackageProject" class="create-card package-card"> <view @click="showTimingProjectModal(2)" class="create-card package-card">
<view class="card-icon"> <view class="card-icon">
<view class="icon-circle package-icon"> <view class="icon-circle package-icon">
<u-icon name="grid" size="40" color="#fff"></u-icon> <u-icon name="grid" size="40" color="#fff"></u-icon>
@@ -74,7 +74,7 @@
<view class="modal-content"> <view class="modal-content">
<!-- 新增按钮 --> <!-- 新增按钮 -->
<view class="add-btn" @click="createTimingProject"> <view v-if="currentIndex!==2" class="add-btn" @click="createTimingProject">
<u-icon name="plus" size="18" color="#1890ff"></u-icon> <u-icon name="plus" size="18" color="#1890ff"></u-icon>
<text class="add-btn-text">新增项目</text> <text class="add-btn-text">新增项目</text>
</view> </view>
@@ -84,7 +84,7 @@
<text class="list-title" v-if="projects.length > 0">项目列表</text> <text class="list-title" v-if="projects.length > 0">项目列表</text>
<view v-if="projects.length > 0" class="list-container"> <view v-if="projects.length > 0" class="list-container">
<view v-for="project in projects" :key="project.id" class="project-item" <view v-for="project in projects" :key="project.id" class="project-item"
@click="Service.GoPage('/pages/userFunc/swiming')"> @click=" goPageFunc() ">
<view class="item-icon"> <view class="item-icon">
<view class="icon-bg"> <view class="icon-bg">
<text class="icon-text">{{ project.name.charAt(0) }}</text> <text class="icon-text">{{ project.name.charAt(0) }}</text>
@@ -129,6 +129,8 @@
}[] }[]
} }
let currentIndex=ref(0)
// 模拟项目数据 // 模拟项目数据
const projects = ref<Project[]>([ const projects = ref<Project[]>([
{ {
@@ -176,9 +178,20 @@
const showDetailModal = ref(false) const showDetailModal = ref(false)
const currentProject = ref<Project | null>(null) const currentProject = ref<Project | null>(null)
// 生命周期
onMounted(() => {
// TODO: 实际应从接口获取项目列表
})
onUnmounted(() => {
// 清理
})
// 显示计时项目列表弹窗 // 显示计时项目列表弹窗
const showTimingProjectModal = () => { const showTimingProjectModal = (index:any) => {
showTimingModal.value = true showTimingModal.value = true
currentIndex.value=index
} }
// 关闭计时项目列表弹窗 // 关闭计时项目列表弹窗
@@ -214,14 +227,16 @@
currentProject.value = null currentProject.value = null
} }
// 生命周期
onMounted(() => {
// TODO: 实际应从接口获取项目列表
})
onUnmounted(() => { const goPageFunc=()=>{
// 清理 if(currentIndex.value==1){
}) Service.GoPage('/pages/userFunc/swiming')
}else{
Service.GoPage('/pages/userFunc/project')
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

File diff suppressed because it is too large Load Diff

View File

@@ -32,6 +32,10 @@
{{ student.gender }} {{ student.gender }}
</view> </view>
</view> </view>
<view class="detail-row">
<text class="detail-label">出生日期</text>
<text class="detail-value">{{ student.birthDate }}</text>
</view>
<view class="detail-row"> <view class="detail-row">
<text class="detail-label">年龄</text> <text class="detail-label">年龄</text>
<text class="detail-value">{{ student.age }}</text> <text class="detail-value">{{ student.age }}</text>
@@ -84,18 +88,40 @@
<text class="form-label">性别</text> <text class="form-label">性别</text>
<view class="gender-selector"> <view class="gender-selector">
<view class="gender-option" :class="{ active: formData.gender === '男' }" @click="formData.gender = '男'"> <view class="gender-option" :class="{ active: formData.gender === '男' }" @click="formData.gender = '男'">
<u-icon name="man" size="18" :color="formData.gender === '男' ? '#1890ff' : '#999'"></u-icon> <view class="gender-icon-box">
<u-icon name="man" size="20" :color="formData.gender === '男' ? '#fff' : '#1890ff'"></u-icon>
</view>
<text></text> <text></text>
</view> </view>
<view class="gender-option" :class="{ active: formData.gender === '女' }" @click="formData.gender = '女'"> <view class="gender-option" :class="{ active: formData.gender === '女' }" @click="formData.gender = '女'">
<u-icon name="woman" size="18" :color="formData.gender === '女' ? '#fa8c16' : '#999'"></u-icon> <view class="gender-icon-box">
<u-icon name="woman" size="20" :color="formData.gender === '女' ? '#fff' : '#fa8c16'"></u-icon>
</view>
<text></text> <text></text>
</view> </view>
</view> </view>
</view> </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="formData.age" placeholder="请输入年龄" type="number" /> <view class="date-picker-wrapper">
<picker mode="date" :value="formData.birthDate" @change="onBirthDateChange">
<view class="form-input date-picker" :class="{ placeholder: !formData.birthDate }">
<u-icon name="calendar" size="18" color="#1890ff"></u-icon>
<text>{{ formData.birthDate || '请选择出生日期' }}</text>
<u-icon name="arrow-down" size="14" color="#999"></u-icon>
</view>
</picker>
</view>
<view v-if="formData.birthDate" class="age-display">
<view class="age-icon">
<u-icon name="account" size="24" color="#fff"></u-icon>
</view>
<view class="age-info">
<text class="age-label">年龄</text>
<text class="age-value">{{ calculateAge(formData.birthDate) }}</text>
<text class="age-unit"></text>
</view>
</view>
</view> </view>
<view class="form-group"> <view class="form-group">
<text class="form-label">学校选填</text> <text class="form-label">学校选填</text>
@@ -128,16 +154,17 @@
name : string name : string
gender : string gender : string
age : string age : string
birthDate : string
school : string school : string
address : string address : string
} }
// 学员列表 // 学员列表
const students = ref<Student[]>([ const students = ref<Student[]>([
{ id: '001', name: '张三', gender: '男', age: '12', school: '第一小学', address: '北京市朝阳区' }, { id: '001', name: '张三', gender: '男', age: '12', birthDate: '2012-05-15', school: '第一小学', address: '北京市朝阳区' },
{ id: '002', name: '李四', gender: '女', age: '13', school: '', address: '' }, { id: '002', name: '李四', gender: '女', age: '13', birthDate: '2011-08-20', school: '', address: '' },
{ id: '003', name: '王五', gender: '男', age: '11', school: '第二小学', address: '' }, { id: '003', name: '王五', gender: '男', age: '11', birthDate: '2013-03-10', school: '第二小学', address: '' },
{ id: '004', name: '赵六', gender: '女', age: '12', school: '', address: '上海市浦东新区' } { id: '004', name: '赵六', gender: '女', age: '12', birthDate: '2012-11-25', school: '', address: '上海市浦东新区' }
]) ])
// 弹窗状态 // 弹窗状态
@@ -150,6 +177,7 @@
name: '', name: '',
gender: '男', gender: '男',
age: '', age: '',
birthDate: '',
school: '', school: '',
address: '' 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) => { const openEditModal = (student : Student) => {
editingStudent.value = student editingStudent.value = student
@@ -173,6 +220,7 @@
name: student.name, name: student.name,
gender: student.gender, gender: student.gender,
age: student.age, age: student.age,
birthDate: student.birthDate,
school: student.school, school: student.school,
address: student.address address: student.address
} }
@@ -189,6 +237,7 @@
name: '', name: '',
gender: '男', gender: '男',
age: '', age: '',
birthDate: '',
school: '', school: '',
address: '' address: ''
} }
@@ -201,8 +250,8 @@
return return
} }
if (!formData.value.age.trim()) { if (!formData.value.birthDate) {
Service.Msg('请输入年龄') Service.Msg('请选择出生日期')
return return
} }
@@ -212,7 +261,8 @@
id: Date.now().toString().slice(-6), id: Date.now().toString().slice(-6),
name: formData.value.name.trim(), name: formData.value.name.trim(),
gender: formData.value.gender, gender: formData.value.gender,
age: formData.value.age.trim(), age: formData.value.age,
birthDate: formData.value.birthDate,
school: formData.value.school.trim(), school: formData.value.school.trim(),
address: formData.value.address.trim() address: formData.value.address.trim()
} }
@@ -226,7 +276,8 @@
...students.value[index], ...students.value[index],
name: formData.value.name.trim(), name: formData.value.name.trim(),
gender: formData.value.gender, gender: formData.value.gender,
age: formData.value.age.trim(), age: formData.value.age,
birthDate: formData.value.birthDate,
school: formData.value.school.trim(), school: formData.value.school.trim(),
address: formData.value.address.trim() address: formData.value.address.trim()
} }
@@ -554,6 +605,101 @@
background-color: #fff; background-color: #fff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.15); 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 { .gender-selector {
@@ -562,17 +708,20 @@
.gender-option { .gender-option {
flex: 1; flex: 1;
height: 88rpx; height: 100rpx;
background-color: #f5f5f5; background-color: #f5f5f5;
border-radius: 16rpx; border-radius: 20rpx;
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8rpx; gap: 8rpx;
font-size: 28rpx; font-size: 26rpx;
color: #666; color: #666;
transition: all 0.25s ease; transition: all 0.3s ease;
border: 2rpx solid transparent; border: 2rpx solid transparent;
position: relative;
overflow: hidden;
&:active { &:active {
transform: scale(0.96); transform: scale(0.96);
@@ -580,9 +729,25 @@
&.active { &.active {
font-weight: 600; font-weight: 600;
background-color: #e6f7ff; background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-color: #1890ff; 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;
} }
} }
} }

View File

@@ -6,13 +6,6 @@
<text class="time-label">当前总用时</text> <text class="time-label">当前总用时</text>
<view class="" style="position: relative;"> <view class="" style="position: relative;">
<view class="total-time">{{ formatTime(currentTime) }}</view> <view class="total-time">{{ formatTime(currentTime) }}</view>
<view class="" style="position: absolute; right: 0; bottom: 4rpx;">
<view v-if="stopwatchMode!=='together'" @click="recordNextAthlete"
style=" display: flex; align-items: center; justify-content: center; background-color: #fff; border: 1rpx solid #e2e2e2; width: 70rpx; height: 70rpx; padding: 10rpx; border-radius: 50%;">
<img :src="Service.GetIconImg('/static/index/Flag.png')" style="width: 35rpx; height: 35rpx;"
alt="" />
</view>
</view>
</view> </view>
</view> </view>
@@ -25,7 +18,7 @@
<view class="athletes-list"> <view class="athletes-list">
<view v-for="(athlete, index) in athletes" :key="athlete.id" @longpress="deleStu(athlete.id)" <view v-for="(athlete, index) in athletes" :key="athlete.id" @longpress="deleStu(athlete.id)"
class="athlete-item" :class="{ finished: athlete.finished, fastest: athlete.isFastest }"> class="athlete-item" :class="{ finished: athlete.finished }">
<view class="athlete-number">{{ index+1 }}</view> <view class="athlete-number">{{ index+1 }}</view>
<view class="athlete-info"> <view class="athlete-info">
<text class="athlete-name">{{ athlete.name }}</text> <text class="athlete-name">{{ athlete.name }}</text>
@@ -33,7 +26,7 @@
</view> </view>
<view class="time-wrapper"> <view class="time-wrapper">
<text class="athlete-time">{{ formatTime(athlete.time) }}</text> <text class="athlete-time">{{ formatTime(athlete.time) }}</text>
<text v-if="athlete.bestTime" class="best-time">最快: {{ formatTime(athlete.bestTime) }}</text> <text class="best-time">最快: {{ athlete.bestTime !== null ? formatTime(athlete.bestTime) : '无' }}</text>
</view> </view>
<view class="" style="display: flex;align-items: center; gap: 10rpx;"> <view class="" style="display: flex;align-items: center; gap: 10rpx;">
<view @click="handleAthleteFirstButton(athlete, index)"> <view @click="handleAthleteFirstButton(athlete, index)">
@@ -49,18 +42,27 @@
<!-- 底部控制按钮 --> <!-- 底部控制按钮 -->
<view class="control-buttons"> <view class="control-buttons">
<button class="start-btn" @click="startTimer" :disabled="isRunning"> <view class="button-group">
<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>
</button> </button>
<button class="pause-btn" @click="stopTimer" :disabled="!isRunning">
<u-icon name="pause" size="24" color="#fff"></u-icon> <button v-if="isRunning" class="record-btn" @click="recordNextAthlete">
<text class="btn-text">暂停</text> <u-icon name="checkmark-circle" size="24" color="#fff"></u-icon>
<text class="btn-text">记录</text>
</button> </button>
<button class="reset-all-btn" @click="resetAll"> </view>
<view class="button-group">
<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">
<u-icon name="pause" size="24" color="#fff"></u-icon>
<text class="btn-text">暂停</text>
</button>
</view>
</view> </view>
<!-- 更多选项弹出框 --> <!-- 更多选项弹出框 -->
@@ -76,6 +78,11 @@
<text class="modal-item-text">添加学生</text> <text class="modal-item-text">添加学生</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon> <u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view> </view>
<view class="modal-item" @click="showGroupModal = true; showMoreModal = false">
<u-icon name="grid" size="24" color="#fa8c16"></u-icon>
<text class="modal-item-text">修改组别</text>
<u-icon name="arrow-right" size="16" color="#ccc"></u-icon>
</view>
<view class="modal-cancel" @click="closeMoreModal"> <view class="modal-cancel" @click="closeMoreModal">
取消 取消
</view> </view>
@@ -164,13 +171,39 @@
</view> </view>
</view> </view>
<!-- 修改组别弹出框 -->
<view v-if="showGroupModal" class="modal-overlay" @click="closeGroupModal"></view>
<view v-if="showGroupModal" class="group-modal">
<view class="modal-header">
<text class="modal-title">修改组别</text>
<view class="modal-close" @click="closeGroupModal">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<view class="modal-content">
<view class="form-group">
<text class="form-label">组别个数</text>
<input class="form-input" v-model="groupCount" placeholder="请输入组别个数" type="number" />
</view>
<view class="form-group">
<text class="form-label">一组多少人</text>
<input class="form-input" v-model="groupSize" placeholder="请输入一组多少人" type="number" />
</view>
</view>
<view class="modal-footer">
<button class="modal-cancel-btn" @click="closeGroupModal">取消</button>
<button class="modal-confirm-btn" @click="confirmGroupSettings">确定</button>
</view>
</view>
<!-- 悬浮保存按钮 --> <!-- 悬浮保存按钮 -->
<view class="float-save-btn" style="color: #fff; font-size: 24rpx;" @click="saveData"> <view class="float-save-btn" style="color: #fff; font-size: 24rpx;" @click="saveData">
保存 保存
</view> </view>
<!-- 日期选择器 --> <!-- 日期选择器 -->
<u-datetime-picker v-model="newStudentBirthday" :show="showDatePicker" mode="date" @confirm="onDateConfirm" @cancel="showDatePicker = false" /> <u-datetime-picker v-model="newStudentBirthday" :show="showDatePicker" mode="date" @confirm="onDateConfirm"
@cancel="showDatePicker = false" />
</view> </view>
</template> </template>
@@ -189,15 +222,14 @@
time : number time : number
bestTime : number | null bestTime : number | null
finished : boolean finished : boolean
isFastest : boolean
} }
// 选手列表 // 选手列表
const athletes = ref<Athlete[]>([ const athletes = ref<Athlete[]>([
{ id: '1', number: '01', 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, isFastest: 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, isFastest: 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, isFastest: 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 showMoreModal = ref(false)
const showStopwatchModal = ref(false) const showStopwatchModal = ref(false)
const showAddStudentModal = ref(false) const showAddStudentModal = ref(false)
const showGroupModal = ref(false)
// 组别设置
const groupCount = ref('')
const groupSize = ref('')
// 新学生信息 // 新学生信息
const newStudentName = ref('') const newStudentName = ref('')
@@ -293,6 +330,17 @@
// 记录下一个选手 // 记录下一个选手
const recordNextAthlete = () => { 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) { if (currentAthleteIndex.value >= athletes.value.length) {
Service.Msg('所有选手已完成') Service.Msg('所有选手已完成')
return return
@@ -302,14 +350,6 @@
athlete.finished = true athlete.finished = true
athlete.time = currentTime.value athlete.time = currentTime.value
// 更新最快记录
if (athlete.bestTime === null || athlete.time < athlete.bestTime) {
athlete.bestTime = athlete.time
}
// 更新最快选手
updateFastestAthlete()
currentAthleteIndex.value++ 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) => { const resetAthleteTime = (athlete : Athlete) => {
athlete.time = 0 athlete.time = 0
athlete.finished = false athlete.finished = false
athlete.isFastest = false
// 重新排序索引 // 重新排序索引
reorderAthletes() reorderAthletes()
updateFastestAthlete()
} }
// 处理选手第一个按钮点击 // 处理选手第一个按钮点击
@@ -358,14 +379,6 @@
athlete.finished = true athlete.finished = true
athlete.time = currentTime.value 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) const allFinished = athletes.value.every(a => a.finished)
if (allFinished) { if (allFinished) {
@@ -382,6 +395,24 @@
currentAthleteIndex.value = athletes.value.length - unfinished.length 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 = () => { const resetAll = () => {
stopTimer() stopTimer()
@@ -391,7 +422,6 @@
athletes.value.forEach(athlete => { athletes.value.forEach(athlete => {
athlete.time = 0 athlete.time = 0
athlete.finished = false athlete.finished = false
athlete.isFastest = false
}) })
} }
@@ -476,6 +506,37 @@
newStudentAge.value = '' 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 = () => { const addNewStudent = () => {
if (!newStudentName.value.trim()) { if (!newStudentName.value.trim()) {
@@ -497,8 +558,7 @@
birthday: newStudentBirthday.value.trim(), birthday: newStudentBirthday.value.trim(),
time: 0, time: 0,
bestTime: null, bestTime: null,
finished: false, finished: false
isFastest: false
} }
athletes.value.push(newAthlete) athletes.value.push(newAthlete)
@@ -514,7 +574,7 @@
stopwatchMode: stopwatchMode.value, stopwatchMode: stopwatchMode.value,
intervalTime: intervalTime.value intervalTime: intervalTime.value
} }
Service.SetStorageCache('swimTimingData', data) console.log(data);
Service.Msg('保存成功', 'success') Service.Msg('保存成功', 'success')
} }
@@ -549,7 +609,7 @@
.total-time { .total-time {
font-size: 80rpx; font-size: 80rpx;
font-weight: bold; font-weight: bold;
color: #1890ff; color: #ff0000;
} }
} }
@@ -652,8 +712,8 @@
.athlete-time { .athlete-time {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: bold;
color: #333333; color: #ff0000;
font-family: 'DIN Alternate', monospace; font-family: 'DIN Alternate', monospace;
} }
@@ -680,60 +740,122 @@
/* 底部控制按钮 */ /* 底部控制按钮 */
.control-buttons { .control-buttons {
display: flex; display: flex;
gap: 20rpx; gap: 24rpx;
justify-content: center; justify-content: center;
padding-bottom: env(safe-area-inset-bottom);
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background-color: #fff; background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #ffffff 100%);
padding: 20rpx; padding: 32rpx 40rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0; 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, .start-btn,
.pause-btn, .pause-btn,
.reset-all-btn { .reset-all-btn,
.record-btn {
flex: 1; flex: 1;
height: 88rpx; height: 100rpx;
border-radius: 12rpx; border-radius: 20rpx;
border: none; border: none;
font-size: 30rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 10rpx; gap: 12rpx;
max-width: 220rpx; 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 { .start-btn {
background-color: #52c41a; background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff; color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4);
&:disabled { &:active {
background-color: #ccc; transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
} }
} }
.pause-btn { .pause-btn {
background-color: #fa541c; background: linear-gradient(135deg, #fa541c 0%, #ff7a45 100%);
color: #ffffff; color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(250, 84, 28, 0.4);
&:disabled { &:active {
background-color: #ccc; transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(250, 84, 28, 0.3);
} }
} }
.reset-all-btn { .reset-all-btn {
background-color: #1890ff; background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
color: #ffffff; 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 { .btn-text {
font-size: 30rpx; font-size: 32rpx;
color: #ffffff; 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, .stopwatch-modal,
.add-student-modal { .add-student-modal,
.group-modal {
position: fixed; position: fixed;
top: 50%; top: 50%;
left: 50%; left: 50%;
@@ -991,15 +1114,36 @@
.float-save-btn { .float-save-btn {
position: fixed; position: fixed;
right: 40rpx; right: 40rpx;
bottom: 160rpx; bottom: 180rpx;
width: 100rpx; width: 110rpx;
height: 100rpx; height: 110rpx;
background: linear-gradient(135deg, #1890ff, #40a9ff); background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
border-radius: 50%; border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: 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; 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);
}
} }
</style> </style>