最新状态

This commit is contained in:
Ls
2026-03-26 08:34:32 +08:00
parent 22f77eb290
commit deaf39e943
43 changed files with 18037 additions and 86 deletions

View File

@@ -0,0 +1,809 @@
<template>
<view class="set-course-container">
<!-- 项目信息 -->
<view class="card-section">
<view class="section-title">项目信息</view>
<!-- 项目名称输入 -->
<view class="form-item">
<view class="form-label">项目名称</view>
<input class="form-input" v-model="tempProjectName" placeholder="请输入项目名称自由泳50米" type="text" />
</view>
<!-- 模块选择 -->
<view class="form-item">
<view class="form-label">模块类型</view>
<view class="select-wrapper" @click="showModulePicker = true">
<text class="select-value"
:class="{ placeholder: !selectedModule }">{{ selectedModule || '请选择模块类型' }}</text>
<u-icon name="arrow-down" size="16" color="#999"></u-icon>
</view>
</view>
<!-- 段数输入仅在选择分段模块时显示 -->
<view v-if="selectedModule === '分段'" class="form-item">
<view class="form-label">段数</view>
<input class="form-input" v-model="segmentCount" placeholder="请输入段数" type="number" />
</view>
<!-- 秒表模式 -->
<view class="form-item">
<view class="form-label">秒表模式</view>
<view class="radio-group">
<view class="radio-item" :class="{ active: stopwatchMode === 'interval' }"
@click="stopwatchMode = 'interval'">
<view class="radio-circle">
<view v-if="stopwatchMode === 'interval'" class="radio-dot"></view>
</view>
<text class="radio-label">间隔出发</text>
</view>
<view class="radio-item" :class="{ active: stopwatchMode === 'together' }"
@click="stopwatchMode = 'together'">
<view class="radio-circle">
<view v-if="stopwatchMode === 'together'" class="radio-dot"></view>
</view>
<text class="radio-label">一起出发</text>
</view>
</view>
</view>
<!-- 间隔时间输入 -->
<view v-if=" stopwatchMode === 'interval'" class="form-item">
<view class="form-label">间隔时间</view>
<input class="form-input" v-model="intervalTime" placeholder="请输入间隔时间" type="number" />
</view>
</view>
<!-- 计时器区域 -->
<view class="card-section timer-section" @click="showTimePicker = true">
<view class="timer-display">
<view class="timer-text">{{ hours<10?('0'+hours):hours }}:{{ minutes<10?('0'+minutes):minutes }}</view>
<text class="timer-milliseconds">:{{ seconds<10?('0'+seconds):seconds }}</text>
</view>
<view class="timer-hint">点击设置时间</view>
</view>
<!-- 学生列表 -->
<view class="card-section">
<view class="section-header">
<text class="section-title">学生列表</text>
<view class="header-actions">
<view class="select-all" @click="toggleSelectAll">
<view class="checkbox" :class="{ checked: isAllSelected }">
<u-icon v-if="isAllSelected" name="checkmark" size="14" color="#fff"></u-icon>
</view>
<text class="select-text">全选</text>
</view>
<view v-if="tempSelectedStudents.length > 0" class="delete-selected"
@click="deleteSelectedStudents">
<u-icon name="trash" size="18" color="#ff4444"></u-icon>
<text class="delete-text">删除({{ tempSelectedStudents.length }})</text>
</view>
</view>
</view>
<view class="students-list">
<up-dragsort :key="dragSortKey" :initialList="tempStudents" direction="vertical"
@drag-end="handleDragEnd">
<template #default="{ item, index }">
<view class="student-item" :class="{ selected: item.selected }">
<view class="student-info" @click.stop="toggleStudent(item)">
<text class="student-index">{{ index + 1 }}</text>
<text class="student-name">{{ item.name }}</text>
</view>
<view class="lane-input-wrapper" @click.stop="openLanePicker(item)">
<view class="lane-select" :class="{ placeholder: !item.lane }">
{{ item.lane || '选择泳道' }}
</view>
<u-icon name="arrow-down" size="14" color="#999"></u-icon>
</view>
<view v-if="selectedModule === '包干'" class="laps-input-wrapper">
<input class="laps-input" v-model="item.laps" placeholder="圈数" type="number"
@click.stop />
</view>
<view class="student-actions">
<view class="action-icon delete" @click.stop="deleteStudent(item.id)">
<u-icon name="trash" size="18" color="#ff4444"></u-icon>
</view>
</view>
</view>
</template>
</up-dragsort>
</view>
<!-- 添加学生 -->
<view class="add-student-wrapper">
<input class="add-student-input" v-model="newStudentName" placeholder="输入学生姓名" type="text"
@confirm="addStudent" />
<button class="add-student-btn" @click="addStudent">
<u-icon name="plus" size="20" color="#fff"></u-icon>
</button>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-section">
<button class="submit-btn" @click="recordTime"
:disabled="tempSelectedStudents.length === 0 || !tempProjectName.trim() || !selectedModule">新增项目</button>
</view>
<!-- 模块选择器 -->
<u-picker :show="showModulePicker" :columns="moduleColumns" @confirm="onModuleConfirm"
@cancel="showModulePicker = false"></u-picker>
<!-- 时间选择器 -->
<u-picker :show="showTimePicker" :columns="timeColumns" @confirm="onTimeConfirm"
@cancel="showTimePicker = false"></u-picker>
<!-- 泳道选择器 -->
<u-picker :show="showLanePicker" :columns="laneOptions" @confirm="onLaneConfirm"
@cancel="closeLanePicker"></u-picker>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onUnmounted, onMounted, nextTick } from 'vue'
import { Service } from '@/Service/Service'
// 定义学生类型
interface Student {
id : string
name : string
selected : boolean
recordTime : string
lane : string
laps : string
}
// 定义成绩记录类型
interface Record {
projectName : string
moduleType : string
stopwatchMode ?: string
intervalTime ?: string
segmentCount ?: string
date : string
students : {
id : string
name : string
time : string
laps ?: string
}[]
}
// 用于强制更新拖拽组件的 key
const dragSortKey = ref(0)
// 模块选择相关
const showModulePicker = ref(false)
const selectedModule = ref('')
const moduleColumns = ref([
[{ text: '计时', value: '计时' },
{ text: '包干', value: '包干' },
{ text: '分段', value: '分段' }]
])
let hours = ref('0')
let minutes = ref('0')
let seconds = ref('0')
// 秒表模式
const stopwatchMode = ref<'interval' | 'together'>('interval')
// 间隔时间(秒)
const intervalTime = ref('')
// 段数
const segmentCount = ref('')
// 项目名称
const projectName = ref('')
// 临时项目名称
const tempProjectName = ref('')
// 新学生姓名
const newStudentName = ref('')
// 学生列表
const students = ref<Student[]>([
{ id: '1', name: '张三', selected: false, recordTime: '', lane: '', laps: '' },
{ id: '2', name: '李四', selected: false, recordTime: '', lane: '', laps: '' },
{ id: '3', name: '王五', selected: false, recordTime: '', lane: '', laps: '' },
{ id: '4', name: '赵六', selected: false, recordTime: '', lane: '', laps: '' },
])
// 临时学生列表
const tempStudents = ref<Student[]>([])
// 成绩记录
const records = ref<Record[]>([])
// 计时器状态
const isRunning = ref(false)
const currentTime = ref(0)
let timerInterval : number | null = null
let startTime : number = 0
// 模块选择确认
const onModuleConfirm = (e : any) => {
selectedModule.value = e.value[0].text
showModulePicker.value = false
}
// 泳道选择相关
const showLanePicker = ref(false)
const currentEditStudent = ref<Student | null>(null)
const laneOptions = ref([
[{ text: '泳道1', value: '泳道1' },
{ text: '泳道2', value: '泳道2' }]
])
// 时间选择相关
const showTimePicker = ref(false)
const timeColumns = ref([
// 小时0-99
Array.from({ length: 100 }, (_, i) => ({ text: `${i.toString().padStart(2, '0')}`, value: i })),
// 分钟0-59
Array.from({ length: 60 }, (_, i) => ({ text: `${i.toString().padStart(2, '0')}`, value: i })),
// 秒0-59
Array.from({ length: 60 }, (_, i) => ({ text: `${i.toString().padStart(2, '0')}`, value: i }))
])
// 计算属性:选中的学生
const selectedStudents = computed(() => {
return students.value.filter((s) => s.selected)
})
// 计算属性:临时选中的学生
const tempSelectedStudents = computed(() => {
return tempStudents.value.filter((s) => s.selected)
})
// 计算属性:是否全选
const isAllSelected = computed(() => {
return tempStudents.value.length > 0 && tempStudents.value.every((s) => s.selected)
})
// 打开泳道选择器
const openLanePicker = (student : Student) => {
currentEditStudent.value = student
showLanePicker.value = true
}
// 关闭泳道选择器
const closeLanePicker = () => {
showLanePicker.value = false
currentEditStudent.value = null
}
// 泳道选择确认
const onLaneConfirm = (e : any) => {
if (currentEditStudent.value) {
currentEditStudent.value.lane = e.value[0].text
}
closeLanePicker()
}
// 时间选择确认
const onTimeConfirm = (e : any) => {
console.log(e.value);
hours.value = e.value[0].value
minutes.value = e.value[1].value
seconds.value = e.value[2].value
currentTime.value = Number(hours.value) * 3600 + Number(minutes.value) * 60 + Number(seconds.value)
showTimePicker.value = false
}
// 全选/取消全选
const toggleSelectAll = () => {
const newValue = !isAllSelected.value
tempStudents.value.forEach((s) => {
s.selected = newValue
})
}
// 切换学生选择状态
const toggleStudent = (student : Student) => {
student.selected = !student.selected
}
// 添加学生
const addStudent = () => {
if (!newStudentName.value.trim()) {
Service.Msg('请输入学生姓名')
return
}
const newStudent : Student = {
id: Date.now().toString(),
name: newStudentName.value.trim(),
selected: false,
recordTime: '',
lane: '',
laps: '',
}
tempStudents.value.push(newStudent)
newStudentName.value = ''
dragSortKey.value++
Service.Msg('添加成功', 'success')
}
// 删除学生
const deleteStudent = (id : string) => {
uni.showModal({
title: '确认删除',
content: '确定要删除这个学生吗?',
success: (res) => {
if (res.confirm) {
tempStudents.value = tempStudents.value.filter((s) => s.id !== id)
dragSortKey.value++
}
},
})
}
// 删除选中的学生
const deleteSelectedStudents = () => {
if (tempSelectedStudents.value.length === 0) {
Service.Msg('请先选择要删除的学生')
return
}
uni.showModal({
title: '确认删除',
content: `确定要删除选中的 ${tempSelectedStudents.value.length} 个学生吗?`,
success: (res) => {
if (res.confirm) {
const selectedIds = tempSelectedStudents.value.map((s) => s.id)
tempStudents.value = tempStudents.value.filter((s) => !selectedIds.includes(s.id))
dragSortKey.value++
Service.Msg('删除成功', 'success')
}
},
})
}
// 创建项目
const recordTime = () => {
if (!tempProjectName.value.trim()) {
Service.Msg('请输入项目名称')
return
}
if (!selectedModule.value) {
Service.Msg('请选择模块类型')
return
}
// 如果选择了计时模块且是间隔出发模式,需要验证间隔时间
if (selectedModule.value === '计时' && stopwatchMode.value === 'interval') {
if (!intervalTime.value.trim()) {
Service.Msg('请输入间隔时间')
return
}
const intervalNum = parseFloat(intervalTime.value)
if (isNaN(intervalNum) || intervalNum <= 0) {
Service.Msg('请输入有效的间隔时间')
return
}
}
if (tempSelectedStudents.value.length === 0) {
Service.Msg('请选择学生')
return
}
Service.Msg('项目创建成功', 'success')
}
// 拖拽结束处理
const handleDragEnd = (list : Student[]) => {
tempStudents.value = list
dragSortKey.value++
}
// 页面加载
onMounted(() => {
// 深拷贝学生列表到临时列表
tempStudents.value = JSON.parse(JSON.stringify(students.value))
})
</script>
<style lang="scss" scoped>
.set-course-container {
min-height: 100vh;
background-color: #f5f5f5;
padding: 20rpx;
padding-bottom: 140rpx;
}
/* 卡片区域 */
.card-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
}
/* 表单项 */
.form-item {
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 26rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.header-actions {
display: flex;
align-items: center;
gap: 20rpx;
.select-all {
display: flex;
align-items: center;
gap: 10rpx;
.checkbox {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
&.checked {
background-color: #1890ff;
border-color: #1890ff;
}
}
.select-text {
font-size: 26rpx;
color: #666;
}
}
.delete-selected {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
background-color: #fff0f0;
border-radius: 30rpx;
.delete-text {
font-size: 24rpx;
color: #ff4444;
}
}
}
}
/* 表单输入 */
.form-input {
width: 100%;
height: 88rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
}
/* 选择器包装器 */
.select-wrapper {
width: 100%;
height: 88rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
padding: 0 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
.select-value {
font-size: 28rpx;
color: #333;
&.placeholder {
color: #999;
}
}
}
/* 单选按钮组 */
.radio-group {
display: flex;
gap: 40rpx;
.radio-item {
display: flex;
align-items: center;
gap: 12rpx;
cursor: pointer;
.radio-circle {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s;
.radio-dot {
width: 18rpx;
height: 18rpx;
background-color: #1890ff;
border-radius: 50%;
}
}
.radio-label {
font-size: 28rpx;
color: #333;
}
&.active {
.radio-circle {
border-color: #1890ff;
}
}
}
}
/* 计时器区域 */
.timer-section {
text-align: center;
cursor: pointer;
transition: all 0.3s;
&:active {
opacity: 0.8;
}
.timer-display {
display: flex;
align-items: baseline;
justify-content: center;
.timer-text {
font-size: 72rpx;
font-weight: bold;
color: #333;
}
.timer-milliseconds {
font-size: 36rpx;
color: #666;
font-family: 'DIN Alternate', monospace;
}
}
.timer-hint {
font-size: 24rpx;
color: #999;
margin-top: 16rpx;
}
}
/* 学生列表 */
.students-list {
margin-bottom: 20rpx;
.student-item {
display: flex;
align-items: center;
padding: 24rpx 20rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
&.selected {
margin-top: 10rpx;
background-color: #e6f7ff;
padding-left: 30rpx;
padding-right: 30rpx;
border-radius: 12rpx;
}
.drag-handle {
margin-right: 12rpx;
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
&:active {
cursor: grabbing;
}
}
.student-checkbox {
margin-right: 20rpx;
.checkbox {
width: 36rpx;
height: 36rpx;
border: 2rpx solid #ddd;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
&.checked {
background-color: #1890ff;
border-color: #1890ff;
}
}
}
.student-info {
flex: 1;
display: flex;
align-items: center;
gap: 20rpx;
cursor: pointer;
.student-index {
font-size: 24rpx;
color: #999;
width: 40rpx;
}
.student-name {
font-size: 30rpx;
color: #333;
}
}
.lane-input-wrapper {
margin-right: 20rpx;
display: flex;
align-items: center;
gap: 8rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 16rpx;
height: 60rpx;
transition: all 0.2s ease;
&:active {
background-color: #e8e8e8;
}
.lane-select {
font-size: 26rpx;
color: #333;
text-align: center;
&.placeholder {
color: #999;
}
}
}
.laps-input-wrapper {
margin-right: 20rpx;
display: flex;
align-items: center;
.laps-input {
width: 100rpx;
height: 60rpx;
background-color: #f5f5f5;
border-radius: 8rpx;
padding: 0 16rpx;
font-size: 26rpx;
color: #333;
text-align: center;
}
}
.student-actions {
.action-icon {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
background-color: #fff0f0;
}
}
}
}
/* 添加学生 */
.add-student-wrapper {
display: flex;
gap: 20rpx;
.add-student-input {
flex: 1;
height: 80rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
}
.add-student-btn {
width: 80rpx;
height: 80rpx;
background-color: #1890ff;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
border: none;
}
}
/* 底部按钮区域 */
.bottom-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
background-color: #fff;
border-top: 1rpx solid #f0f0f0;
.submit-btn {
width: 100%;
height: 88rpx;
background-color: #1890ff;
color: #fff;
font-size: 32rpx;
font-weight: 600;
border-radius: 12rpx;
border: none;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
&:disabled {
background-color: #ccc;
box-shadow: none;
}
}
}
</style>