This commit is contained in:
Ls
2026-04-23 10:07:44 +08:00
parent 7dba9711a9
commit 11486220aa
16 changed files with 2908 additions and 2836 deletions

View File

@@ -1,11 +1,12 @@
export class BaseConfig { export class BaseConfig {
protected static servesUrl: string = "http://192.168.0.142:5298"; // protected static servesUrl: string = "http://192.168.0.142:5298";
protected static imgUrl: string = "http://192.168.0.142:5298"; // protected static imgUrl: string = "http://192.168.0.142:5298";
protected static mediaUrl: string = "http://192.168.0.142:5298/"; // protected static mediaUrl: string = "http://192.168.0.142:5298/";
// protected static servesUrl: string = "http://vp.xypays.cn";
// protected static imgUrl: string = "http://vp.cloud.xypays.cn"; protected static servesUrl: string = "https://swimming.api.xypays.cn";
// protected static mediaUrl: string = "http://byc1.xypays.cn/"; protected static imgUrl: string = "https://swimming.api.xypays.cn";
protected static mediaUrl: string = "https://swimming.api.xypays.cn/";
protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo"; protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
protected static payuploadUrl: string = "http://192.168.0.142:5298"; protected static payuploadUrl: string = "https://swimming.api.xypays.cn";
} }

View File

@@ -1,75 +1,65 @@
<template> <template>
<view class="curve-container"> <view class="curve-container">
<!-- 页面标题区域 --> <!-- 项目选择卡片 -->
<view class="header-section"> <view class="filter-card ">
<view class="header-title"> <view class="card-title">
<text class="title">曲线走势图</text> <u-icon name="grid" size="18" color="#1890ff"></u-icon>
<text class="subtitle">学员成绩趋势变化分析</text> <text>选择项目</text>
</view>
<view class="picker-row" @click="showProject = true">
<text class="picker-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
<!-- 日期选择卡片 -->
<view class="filter-card " v-if="selectProcet">
<view class="card-title">
<u-icon name="calendar" size="18" color="#13c2c2"></u-icon>
<text>日期范围</text>
</view>
<view class="date-filter" @click="openCalendar">
<view class="date-item">
<text class="date-label">开始日期</text>
<text class="date-value" :class="{ placeholder: !begin }">{{ begin || '请选择' }}</text>
</view>
<view class="date-divider"></view>
<view class="date-item">
<text class="date-label">结束日期</text>
<text class="date-value" :class="{ placeholder: !end }">{{ end || '请选择' }}</text>
</view>
<u-icon name="arrow-right" size="16" color="#bbb" class="date-arrow"></u-icon>
</view> </view>
</view> </view>
<!-- 项目选择区域 --> <!-- 学生选择卡片 -->
<view class="select-section"> <view class="filter-card " v-if="selectProcet && begin && projectStudents.length > 0">
<view class="select-label">
<view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
<view class="picker-wrapper" @click="showProject=true">
<text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view>
</view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
:columns="projectOptions"></up-picker>
<!-- 日期选择 -->
<view class="date-filter" style="margin: 20rpx 20rpx 0;" v-if="selectProcet">
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">开始日期</text>
<text class="value">{{ begin || '请选择' }}</text>
</view>
</view>
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">结束日期</text>
<text class="value">{{ end || '请选择' }}</text>
</view>
</view>
</view>
<!-- ==================== 学生选择区域 ==================== -->
<!-- 仅在选择项目和时间后显示 -->
<view class="student-select-section" v-if="selectProcet && begin && projectStudents.length > 0">
<view class="section-header"> <view class="section-header">
<text class="section-title">选择学员最多3人</text> <view class="card-title">
<text class="select-count">已选{{ selectedStudentIds.length }}/3</text> <u-icon name="account" size="18" color="#52c41a"></u-icon>
<text>选择学员</text>
</view>
<text class="select-count">已选 {{ selectedStudentIds.length }}/3</text>
</view> </view>
<!-- 学生列表 -->
<view class="student-list"> <view class="student-list">
<view v-for="student in projectStudents" :key="student.studentId" <view v-for="student in projectStudents" :key="student.studentId"
:class="['student-item', { 'selected': selectedStudentIds.includes(student.studentId) }]" :class="['student-item', { selected: selectedStudentIds.includes(student.studentId) }]"
@click="toggleStudent(student.studentId)"> @click="toggleStudent(student.studentId)">
<!-- 学生头像使用首字母作为头像 -->
<view class="student-avatar"> <view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text> <text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view> </view>
<!-- 学生姓名 -->
<text class="student-name">{{ student.name }}</text> <text class="student-name">{{ student.name }}</text>
<!-- 选择状态图标 -->
<view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)"> <view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)">
<u-icon name="checkmark" size="16" color="#fff"></u-icon> <u-icon name="checkmark" size="14" color="#fff"></u-icon>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<!-- 图表区域 -->
<!-- ==================== 图表展示区域 ==================== -->
<!-- 仅在选择了至少一名学生后显示 -->
<view class="chart-section" v-if="selectedStudentIds.length > 0"> <view class="chart-section" v-if="selectedStudentIds.length > 0">
<!-- 数据统计卡片 --> <!-- 统计概览 -->
<view class="stats-card"> <view class="stats-card">
<view class="stat-item"> <view class="stat-item">
<text class="stat-label">选中项目</text> <text class="stat-label">选中项目</text>
@@ -78,112 +68,77 @@
<view class="stat-divider"></view> <view class="stat-divider"></view>
<view class="stat-item"> <view class="stat-item">
<text class="stat-label">对比学员</text> <text class="stat-label">对比学员</text>
<text class="stat-value">{{ selectedStudentIds.length }}</text> <text class="stat-value">{{ selectedStudentIds.length }} </text>
</view> </view>
</view> </view>
<!-- 折线图卡片 --> <!-- 折线图 -->
<view class="chart-card"> <view class="chart-card">
<view class="chart-header"> <view class="chart-header">
<text class="chart-title">成绩趋势对比</text> <text class="chart-title">成绩趋势对比</text>
<text class="chart-desc">历史训练成绩变化曲线</text> <text class="chart-desc">历史训练成绩变化曲线</text>
</view> </view>
<!-- 折线图容器 -->
<scroll-view class="chart-scroll" scroll-x :show-scrollbar="false"> <scroll-view class="chart-scroll" scroll-x :show-scrollbar="false">
<view class="chart-box" :style="{ width: chartWidth + 'rpx' }"> <view class="chart-box" :style="{ width: chartWidth + 'rpx' }">
<qiun-data-charts type="line" :opts="lineOpts" :chartData="lineChartData" :ontouch="true" /> <qiun-data-charts type="line" :opts="lineOpts" :chartData="lineChartData" :ontouch="true" />
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
</view> </view>
<!-- 日历弹窗 --> <!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate='1776240407000' @confirm="calendarConfirm" <up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar>
@close="calendarClose">
</up-calendar>
</view> </view>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app" import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed, watch } from 'vue' import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService' import { PlanService } from '@/Service/swimming/PlanService'
// ==================== 常量定义 ====================
// 最大选择学生数量限制
const MAX_SELECTED_STUDENTS = 3 const MAX_SELECTED_STUDENTS = 3
// 图表颜色配置最多支持3种颜色
const chartColors = ['#52c41a', '#1890ff', '#faad14'] const chartColors = ['#52c41a', '#1890ff', '#faad14']
// ==================== 响应式数据 - 项目相关 ====================
// 项目选择
let showProject = ref(false) let showProject = ref(false)
let selectProcet = ref('') let selectProcet = ref('')
let selectId = ref('') let selectId = ref('')
// 日期选择
const begin = ref<string>('') const begin = ref<string>('')
const end = ref<string>('') const end = ref<string>('')
const showCalendar = ref(false) const showCalendar = ref(false)
const projectOptions = ref<Array<any>>([[]]) const projectOptions = ref<Array<any>>([[]])
// ==================== TypeScript 接口定义 ====================
/**
* 学生接口
* 定义学生的基本信息
*/
interface Student { interface Student {
studentId : string studentId: string
name : string name: string
} }
/**
* 学生训练记录接口
* 定义学生某次训练的详细记录
*/
interface StudentTrainingRecord { interface StudentTrainingRecord {
studentId : string studentId: string
studentName : string studentName: string
time : number time: number
recordDate : string recordDate: string
recordFullDate : string recordFullDate: string
round : number round: number
} }
// ==================== 响应式数据 - 学生相关 ====================
// 当前项目的学生列表
const projectStudents = ref<Student[]>([]) const projectStudents = ref<Student[]>([])
// 已选中的学生 ID 列表
const selectedStudentIds = ref<string[]>([]) const selectedStudentIds = ref<string[]>([])
// 已选中的学生详细信息列表((计算属性)
const selectedStudents = computed(() => { const selectedStudents = computed(() => {
return projectStudents.value.filter(student => return projectStudents.value.filter(student =>
selectedStudentIds.value.includes(student.studentId) selectedStudentIds.value.includes(student.studentId)
) )
}) })
// ==================== 响应式数据 - 图表相关 ====================
// 计算图表宽度(根据数据点数量动态调整)
const chartWidth = computed(() => { const chartWidth = computed(() => {
const categoryCount = lineChartData.value.categories.length const categoryCount = lineChartData.value.categories.length
if (categoryCount <= 6) return 700 if (categoryCount <= 6) return 700
// 每个数据点分配 100rpx最小 700rpx
return Math.max(700, categoryCount * 100) return Math.max(700, categoryCount * 100)
}) })
// 折线图配置选项
const lineOpts = ref({ const lineOpts = ref({
color: chartColors, color: chartColors,
padding: [15, 10, 0, 15], padding: [15, 10, 0, 15],
@@ -201,7 +156,7 @@
yAxis: { yAxis: {
data: [{ data: [{
min: 0, min: 0,
format: (val : number) => val.toFixed(1) + 's' format: (val: number) => val.toFixed(1) + 's'
}] }]
}, },
extra: { extra: {
@@ -215,25 +170,16 @@
} }
}) })
// 折线图数据
const lineChartData = ref({ const lineChartData = ref({
categories: [] as string[], categories: [] as string[],
series: [] as any[] series: [] as any[]
}) })
onLoad(() => { onLoad(() => {
getProjectData() getProjectData()
}) })
onShow(() => { onShow(() => {})
})
const getProjectData = () => { const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => { PlanService.GetPlanListNoPage('计时项目').then(res => {
@@ -245,10 +191,15 @@
}) })
} }
const selectProcetFunc = (e : any) => { const selectProcetFunc = (e: any) => {
selectProcet.value = e.value[0].name selectProcet.value = e.value[0].name
selectId.value = e.value[0].planId selectId.value = e.value[0].planId
selectedStudentIds.value = [] selectedStudentIds.value = []
begin.value = ''
end.value = ''
projectStudents.value = []
lineChartData.value.categories = []
lineChartData.value.series = []
loadProjectStudents(selectId.value) loadProjectStudents(selectId.value)
} }
@@ -256,7 +207,7 @@
showCalendar.value = true showCalendar.value = true
} }
const calendarConfirm = (e : any) => { const calendarConfirm = (e: any) => {
begin.value = e[0] begin.value = e[0]
end.value = e[e.length - 1] end.value = e[e.length - 1]
showCalendar.value = false showCalendar.value = false
@@ -266,7 +217,7 @@
showCalendar.value = false showCalendar.value = false
} }
const loadProjectStudents = (projectId : string) => { const loadProjectStudents = (projectId: string) => {
PlanService.GetPlanInfo(projectId).then(res => { PlanService.GetPlanInfo(projectId).then(res => {
if (res.code == 0) { if (res.code == 0) {
projectStudents.value = res.data.plan.users projectStudents.value = res.data.plan.users
@@ -276,7 +227,7 @@
}) })
} }
const toggleStudent = (studentId : string) => { const toggleStudent = (studentId: string) => {
const index = selectedStudentIds.value.indexOf(studentId) const index = selectedStudentIds.value.indexOf(studentId)
if (index !== -1) { if (index !== -1) {
selectedStudentIds.value.splice(index, 1) selectedStudentIds.value.splice(index, 1)
@@ -287,9 +238,7 @@
} }
selectedStudentIds.value.push(studentId) selectedStudentIds.value.push(studentId)
} }
let studentIdList = selectedStudentIds.value.map((item) => { let studentIdList = selectedStudentIds.value.map((item) => item)
return item
})
let data = { let data = {
planId: selectId.value, planId: selectId.value,
studentId: JSON.stringify(studentIdList), studentId: JSON.stringify(studentIdList),
@@ -297,11 +246,10 @@
eTime: end.value eTime: end.value
} }
interface DataItem { interface DataItem {
name : string name: string
data : any[] data: any[]
} }
const xData = ref<DataItem[]>( const xData = ref<DataItem[]>(
Array.from({ length: studentIdList.length }, () => ({ Array.from({ length: studentIdList.length }, () => ({
name: '', name: '',
@@ -310,343 +258,294 @@
) )
PlanService.GetQuxianLog(data).then(res => { PlanService.GetQuxianLog(data).then(res => {
if (res.code == 0) { if (res.code == 0) {
lineChartData.value.categories = []
res.data.list.map((item : any) => { res.data.list.map((item: any) => {
lineChartData.value.categories.push(item.dayTime) lineChartData.value.categories.push(item.dayTime)
item.data.map((content : any, index : any) => { item.data.map((content: any, index: any) => {
xData.value[index].name = content.studentName xData.value[index].name = content.studentName
xData.value[index].data.push(content.time) xData.value[index].data.push(content.time)
}) })
}) })
console.log(xData.value);
lineChartData.value.series = xData.value lineChartData.value.series = xData.value
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }
}) })
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
page { page {
background-color: #f5f5f5; background-color: #e8ecf3;
} }
.curve-container { .curve-container {
min-height: 100vh; min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx; padding-bottom: 40rpx;
} }
.header-section { /* 通用卡片 */
background-color: #fff; .filter-card {
padding: 32rpx 28rpx 24rpx; background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
// border-left: 8rpx solid #999;
.header-title { }
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.subtitle { .card-title {
font-size: 24rpx; display: flex;
align-items: center;
gap: 10rpx;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
/* 项目选择 */
.picker-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22rpx 24rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 1rpx solid #e4e8ee;
transition: all 0.2s;
&:active {
background: #eef1f6;
}
.picker-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
&.placeholder {
color: #999; color: #999;
font-weight: 400;
} }
} }
} }
/* 日期选择 */
.date-filter {
display: flex;
align-items: center;
background: #f0fafa;
border-radius: 12rpx;
padding: 22rpx 24rpx;
border: 1rpx solid #d6f0f0;
transition: all 0.2s;
&:active {
background: #e6f5f5;
}
}
.date-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
}
.date-label {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
.date-value {
font-size: 30rpx;
color: #333;
font-weight: 600;
&.placeholder {
color: #999;
font-weight: 400;
}
}
.date-divider {
width: 2rpx;
height: 50rpx;
background: #b2dfdb;
margin: 0 20rpx;
}
.date-arrow {
margin-left: 12rpx;
}
/* 学生选择区域 */
.section-header { .section-header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 20rpx; margin-bottom: 20rpx;
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.select-count { .select-count {
font-size: 24rpx; font-size: 24rpx;
color: #52c41a; color: #52c41a;
font-weight: 600;
background: #f0f9eb;
padding: 6rpx 16rpx;
border-radius: 20rpx;
} }
} }
.date-filter { .student-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
}
.student-item {
display: flex; display: flex;
gap: 24rpx; flex-direction: column;
align-items: center;
.date-picker { padding: 20rpx 10rpx;
flex: 1; background: #f5f7fa;
display: flex; border-radius: 12rpx;
align-items: center; border: 2rpx solid transparent;
gap: 16rpx; transition: all 0.2s;
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx;
.date-text {
.label {
display: block;
font-size: 26rpx;
color: #999;
}
.value {
display: block;
font-size: 30rpx;
color: #333;
margin-top: 4rpx;
font-weight: 600;
}
}
}
}
.select-section {
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative; position: relative;
overflow: hidden;
.select-label { &:active {
margin-bottom: 16rpx; transform: scale(0.96);
.label-text {
font-size: 26rpx;
font-weight: 600;
color: #666;
}
} }
.picker-wrapper { &.selected {
background: #f0f9eb;
border-color: #52c41a;
}
.student-avatar {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-radius: 50%;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: center;
padding: 20rpx 24rpx; margin-bottom: 12rpx;
background-color: #f8f8f8; box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
transition: all 0.3s ease;
&:active { .avatar-text {
background-color: #f0f0f0; font-size: 24rpx;
border-color: #faad14; font-weight: 700;
transform: scale(0.98); color: #fff;
} }
}
.picker-text { .student-name {
font-size: 28rpx; font-size: 24rpx;
color: #333; color: #333;
} text-align: center;
font-weight: 500;
}
.check-icon {
position: absolute;
top: 6rpx;
right: 6rpx;
width: 28rpx;
height: 28rpx;
background: #52c41a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
} }
} }
.student-select-section { /* 图表区域 */
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.section-header {
margin-bottom: 24rpx;
}
.student-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
.student-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 10rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
border: 2rpx solid transparent;
transition: all 0.3s ease;
position: relative;
&:active {
transform: scale(0.95);
}
&.selected {
background-color: #f0f9eb;
border-color: #52c41a;
}
.student-avatar {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
.avatar-text {
font-size: 24rpx;
font-weight: 700;
color: #fff;
}
}
.student-name {
font-size: 24rpx;
color: #333;
text-align: center;
}
.check-icon {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 28rpx;
height: 28rpx;
background-color: #52c41a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}
.chart-section { .chart-section {
margin: 0 20rpx; margin-top: 4rpx;
}
.stats-card { .stats-card {
background-color: #fff; background: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
padding: 24rpx; padding: 24rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; justify-content: space-around;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
// border-left: 8rpx solid #faad14;
.stat-item { .stat-item {
text-align: center; text-align: center;
.stat-label { .stat-label {
font-size: 24rpx; font-size: 24rpx;
color: #999; color: #666;
display: block; display: block;
margin-bottom: 8rpx; margin-bottom: 8rpx;
} font-weight: 500;
.stat-value {
font-size: 32rpx;
font-weight: 700;
color: #52c41a;
}
} }
.stat-divider { .stat-value {
width: 1rpx; font-size: 32rpx;
height: 50rpx; font-weight: 700;
background-color: #eee; color: #faad14;
} }
} }
.chart-card { .stat-divider {
background-color: #fff; width: 1rpx;
border-radius: 16rpx; height: 50rpx;
padding: 24rpx; background-color: #f0f0f0;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.chart-header {
margin-bottom: 20rpx;
padding-left: 12rpx;
border-left: 6rpx solid #52c41a;
.chart-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 6rpx;
}
.chart-desc {
font-size: 24rpx;
color: #999;
}
}
.chart-scroll {
width: 100%;
height: 500rpx;
background-color: #fafafa;
border-radius: 12rpx;
overflow: hidden;
white-space: nowrap;
.chart-box {
height: 100%;
display: inline-block;
}
}
} }
}
.legend-section { .chart-card {
background-color: #fff; background: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
padding: 24rpx; padding: 24rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
// border-left: 8rpx solid #52c41a;
.legend-title { .chart-header {
font-size: 28rpx; margin-bottom: 20rpx;
padding-left: 12rpx;
border-left: 6rpx solid #52c41a;
.chart-title {
font-size: 30rpx;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 20rpx; display: block;
margin-bottom: 6rpx;
} }
.legend-list { .chart-desc {
display: flex; font-size: 24rpx;
flex-direction: column; color: #999;
gap: 16rpx; }
}
.legend-item { .chart-scroll {
display: flex; width: 100%;
align-items: center; height: 500rpx;
gap: 12rpx; background-color: #fafafa;
border-radius: 12rpx;
overflow: hidden;
white-space: nowrap;
border: 1rpx solid #f0f0f0;
.legend-color { .chart-box {
width: 24rpx; height: 100%;
height: 24rpx; display: inline-block;
border-radius: 50%;
flex-shrink: 0;
}
.legend-name {
font-size: 26rpx;
color: #333;
font-weight: 600;
}
.legend-desc {
font-size: 24rpx;
color: #999;
}
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +1,21 @@
<template> <template>
<view class="grades-container"> <view class="grades-container">
<!-- 页面标题区域 --> <!-- 项目选择卡片 -->
<view class="header-section"> <view class="filter-card ">
<view class="header-title"> <view class="card-title">
<text class="title">成绩排名</text> <u-icon name="grid" size="18" color="#1890ff"></u-icon>
<text class="subtitle">学生最佳成绩排名</text> <text>选择项目</text>
</view>
<view class="picker-row" @click="showProject = true">
<text class="picker-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view> </view>
</view> </view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
<!-- 项目选择区域 --> <!-- 排名列表区域 -->
<view class="select-section">
<view class="select-label">
<view class="label-text" style="margin-bottom: 20rpx;">选择项目</view>
<view class="picker-wrapper" @click="showProject=true">
<text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view>
</view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc"
:columns="projectOptions"></up-picker>
<!-- ==================== 排名列表区域 ==================== -->
<!-- 仅在选择了项目且有数据时显示 -->
<view class="ranking-section" v-if="selectProcet && gradeList.length > 0"> <view class="ranking-section" v-if="selectProcet && gradeList.length > 0">
<!-- 排名统计卡片 --> <!-- 统计卡片 -->
<view class="stats-card"> <view class="stats-card">
<view class="stat-item"> <view class="stat-item">
<text class="stat-label">对比项目</text> <text class="stat-label">对比项目</text>
@@ -34,7 +24,7 @@
<view class="stat-divider"></view> <view class="stat-divider"></view>
<view class="stat-item"> <view class="stat-item">
<text class="stat-label">参与人数</text> <text class="stat-label">参与人数</text>
<text class="stat-value">{{ gradeList.length }}</text> <text class="stat-value">{{ gradeList.length }} </text>
</view> </view>
</view> </view>
@@ -42,24 +32,27 @@
<view class="ranking-list"> <view class="ranking-list">
<view v-for="(item, index) in gradeList" :key="item.id" <view v-for="(item, index) in gradeList" :key="item.id"
:class="['ranking-item', { 'top-three': index < 3 }]" @click="handleRankingClick(item)"> :class="['ranking-item', { 'top-three': index < 3 }]" @click="handleRankingClick(item)">
<!-- 排名徽章 -->
<view class="rank-badge" :class="`rank-${index + 1}`"> <view class="rank-badge" :class="`rank-${index + 1}`">
<text class="rank-number">{{ index + 1 }}</text> <text class="rank-number">{{ index + 1 }}</text>
</view> </view>
<!-- 学生信息 -->
<view class="student-info"> <view class="student-info">
<text class="student-name">{{ item.studentName }}</text> <text class="student-name">{{ item.studentName }}</text>
<text class="student-speed">最快用时{{ item.quicklyTime }}s</text> <text class="student-speed">最快用时{{ item.quicklyTime }}s</text>
</view> </view>
<!-- 成绩详情 -->
<view class="speed-detail"> <view class="speed-detail">
<text class="detail-text">日期{{ Service.formatDate(item.addTime,2)}}</text> <text class="detail-text">{{ Service.formatDate(item.addTime,2) }}</text>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<!-- 空状态 -->
<view class="empty-card" v-if="selectProcet && gradeList.length === 0">
<u-icon name="file-text" size="64" color="#d9d9d9"></u-icon>
<text class="empty-text">暂无排名数据</text>
</view>
</view> </view>
</template> </template>
@@ -69,8 +62,6 @@
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService' import { PlanService } from '@/Service/swimming/PlanService'
// ==================== 响应式数据 - 项目相关 ====================
// 项目选择 // 项目选择
let showProject = ref(false) let showProject = ref(false)
let selectProcet = ref('') let selectProcet = ref('')
@@ -78,49 +69,26 @@
const projectOptions = ref<Array<any>>([[]]) const projectOptions = ref<Array<any>>([[]])
// ==================== TypeScript 接口定义 ====================
/**
* 学生成绩接口
* 定义学生的成绩和排名信息
*/
interface StudentGrade { interface StudentGrade {
id : string // 学生 ID id: string
name : string // 学生姓名 name: string
bestSpeed : number // 最快速度m/s bestSpeed: number
bestTime : number // 最快用时(秒) bestTime: number
recordDate : string // 记录日期 recordDate: string
studentId : string // 学生 ID用于数据关联 studentId: string
} }
// ==================== 响应式数据 - 排名相关 ====================
// 排名列表数据
const gradeList = ref<StudentGrade[]>([]) const gradeList = ref<StudentGrade[]>([])
// 最高速度(计算属性)
const maxSpeed = computed(() => { const maxSpeed = computed(() => {
if (gradeList.value.length === 0) return 0 if (gradeList.value.length === 0) return 0
return Math.max(...gradeList.value.map(item => item.bestSpeed)).toFixed(2) return Math.max(...gradeList.value.map(item => item.bestSpeed)).toFixed(2)
}) })
// ==================== 生命周期钩子 ====================
/**
* 页面加载时触发
* 在页面初始化时执行,只执行一次
*/
onLoad(() => { onLoad(() => {
// 初始化数据
getProjectData() getProjectData()
}) })
// ==================== 业务逻辑方法 - 项目相关 ====================
const getProjectData = () => { const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => { PlanService.GetPlanListNoPage('计时项目').then(res => {
if (res.code == 0) { if (res.code == 0) {
@@ -131,314 +99,244 @@
}) })
} }
const selectProcetFunc = (e : any) => { const selectProcetFunc = (e: any) => {
selectProcet.value = e.value[0].name selectProcet.value = e.value[0].name
selectId.value = e.value[0].planId selectId.value = e.value[0].planId
loadProjectGrades(selectId.value) loadProjectGrades(selectId.value)
} }
/** const loadProjectGrades = (projectId: string) => {
* 加载项目排名数据
* 根据项目 ID 获取该项目的学生成绩排名列表
* @param projectId 项目 ID
*/
const loadProjectGrades = (projectId : string) => {
PlanService.GetRankData(projectId).then(res => { PlanService.GetRankData(projectId).then(res => {
if (res.code == 0) { if (res.code == 0) {
gradeList.value=res.data.list gradeList.value = res.data.list
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }
}) })
} }
// ==================== 业务逻辑方法 - 排名相关 ==================== const handleRankingClick = (item: StudentGrade) => {
/**
* 处理排名项点击事件
* 点击某排名项查看详情
* @param item 排名项数据
*/
const handleRankingClick = (item : StudentGrade) => {
console.log('点击了排名项:', item) console.log('点击了排名项:', item)
// TODO: 跳转到学生详情页面
// Service.GoPage('/pages/student/detail', { studentId: item.studentId })
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
// 页面背景色设置
page { page {
background-color: #f5f5f5; background-color: #e8ecf3;
} }
// 页面容器
.grades-container { .grades-container {
min-height: 100vh; min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx; padding-bottom: 40rpx;
} }
/* ==================== 页面标题区域 ==================== */ /* 通用卡片 */
.header-section { .filter-card {
background-color: #fff; background: #ffffff;
padding: 32rpx 28rpx 24rpx; border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.header-title { .card-title {
.title { display: flex;
font-size: 36rpx; align-items: center;
font-weight: 700; gap: 10rpx;
color: #333; font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.picker-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22rpx 24rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 1rpx solid #e4e8ee;
transition: all 0.2s;
&:active {
background: #eef1f6;
}
.picker-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
&.placeholder {
color: #999;
font-weight: 400;
}
}
}
/* 排名区域 */
.ranking-section {
margin-top: 4rpx;
}
.stats-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
display: flex;
align-items: center;
justify-content: space-around;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.stat-item {
text-align: center;
.stat-label {
font-size: 24rpx;
color: #666;
display: block; display: block;
margin-bottom: 8rpx; margin-bottom: 8rpx;
font-weight: 500;
} }
.subtitle { .stat-value {
font-size: 24rpx; font-size: 32rpx;
color: #999; font-weight: 700;
color: #faad14;
} }
} }
.stat-divider {
width: 1rpx;
height: 50rpx;
background-color: #f0f0f0;
}
} }
/* ==================== 项目选择区域 ==================== */ .ranking-list {
.select-section { .ranking-item {
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
.select-label {
margin-bottom: 16rpx;
.label-text {
font-size: 26rpx;
font-weight: 600;
color: #666;
}
}
.picker-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
transition: all 0.3s ease;
&:active {
background-color: #f0f0f0;
border-color: #faad14;
transform: scale(0.98);
}
.picker-text {
font-size: 28rpx;
color: #333;
}
}
}
/* ==================== 排名列表区域 ==================== */
.ranking-section {
margin: 0 20rpx;
// 统计卡片
.stats-card {
background-color: #fff; background-color: #fff;
border-radius: 20rpx; border-radius: 16rpx;
padding: 24rpx; padding: 24rpx;
margin-bottom: 20rpx; margin-bottom: 16rpx;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-around; gap: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
transition: all 0.2s;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
&::before { &:active {
content: ''; transform: scale(0.99);
position: absolute; box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
left: 0;
top: 0;
bottom: 0;
width: 6rpx;
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
border-radius: 20rpx 0 0 20rpx;
} }
.stat-item { &.top-three {
text-align: center; border-left-color: #ffd700;
.stat-label {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 8rpx;
}
.stat-value {
font-size: 32rpx;
font-weight: 700;
color: #1890ff;
}
} }
.stat-divider { .rank-badge {
width: 1rpx; width: 60rpx;
height: 50rpx; height: 60rpx;
background-color: #eee; border-radius: 50%;
}
}
// 排名列表容器
.ranking-list {
// 排名项
.ranking-item {
background-color: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20rpx; justify-content: center;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); flex-shrink: 0;
transition: all 0.3s ease; background-color: #f0f0f0;
position: relative; box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
overflow: hidden;
&::before { .rank-number {
content: ''; font-size: 24rpx;
position: absolute; font-weight: 700;
left: 0; color: #999;
top: 0;
bottom: 0;
width: 6rpx;
// background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
opacity: 0;
transition: opacity 0.3s ease;
border-radius: 20rpx 0 0 20rpx;
} }
&:active { &.rank-1 {
transform: scale(0.98); background: linear-gradient(135deg, #ffd700 0%, #ffec3d 100%);
box-shadow: 0 4rpx 16rpx rgba(235, 47, 150, 0.15); box-shadow: 0 2rpx 8rpx rgba(255, 215, 0, 0.4);
&::before {
opacity: 1;
}
}
// 排名徽章
.rank-badge {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
background-color: #f0f0f0;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
.rank-number { .rank-number {
font-size: 24rpx; color: #fff;
font-weight: 700; font-size: 28rpx;
color: #999;
}
// 前三名特殊颜色
&.rank-1 {
background: linear-gradient(135deg, #ffd700 0%, #ffec3d 100%);
box-shadow: 0 2rpx 8.6rpx rgba(255, 215, 0, 0.4);
.rank-number {
color: #fff;
font-size: 28rpx;
}
}
&.rank-2 {
background: linear-gradient(135deg, #c0c0c0 0%, #d9d9d9 100%);
box-shadow: 0 2rpx 8rpx rgba(192, 192, 192, 0.4);
.rank-number {
color: #fff;
}
}
&.rank-3 {
background: linear-gradient(135deg, #cd7f32 0%, #e6963d 100%);
box-shadow: 0 2rpx 8rpx rgba(205, 127, 50, 0.4);
.rank-number {
color: #fff;
}
} }
} }
// 学生头像 &.rank-2 {
.student-avatar { background: linear-gradient(135deg, #c0c0c0 0%, #d9d9d9 100%);
width: 70rpx; box-shadow: 0 2rpx 8rpx rgba(192, 192, 192, 0.4);
height: 70rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
box-shadow: 0 2rpx 8rpx rgba(235, 47, 150, 0.3);
.avatar-text { .rank-number {
font-size: 28rpx;
font-weight: 700;
color: #fff; color: #fff;
} }
} }
// 学生信息 &.rank-3 {
.student-info { background: linear-gradient(135deg, #cd7f32 0%, #e6963d 100%);
flex: 1; box-shadow: 0 2rpx 8rpx rgba(205, 127, 50, 0.4);
min-width: 0;
.student-name { .rank-number {
font-size: 32rpx; color: #fff;
font-weight: 600;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.student-speed {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
} }
} }
}
// 成绩详情 .student-info {
.speed-detail { flex: 1;
display: flex; min-width: 0;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
.detail-text { .student-name {
font-size: 24rpx; font-size: 32rpx;
color: #999; font-weight: 600;
background-color: #f5f5f5; color: #333;
padding: 6rpx 12rpx; display: block;
border-radius: 6rpx; margin-bottom: 8rpx;
} }
.student-speed {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
}
.speed-detail {
display: flex;
align-items: center;
gap: 16rpx;
flex-shrink: 0;
.detail-text {
font-size: 24rpx;
color: #999;
background-color: #f5f7fa;
padding: 6rpx 12rpx;
border-radius: 6rpx;
} }
} }
} }
} }
/* 空状态 */
.empty-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
margin-top: 4rpx;
.empty-text {
margin-top: 24rpx;
font-size: 28rpx;
color: #999;
}
}
</style> </style>

View File

@@ -1,54 +1,70 @@
<template> <template>
<view class="paragraph-container"> <view class="paragraph-container">
<view class="header-section"> <!-- 项目选择卡片 -->
<view class="header-title"> <view class="filter-card ">
<text class="title">分段数据</text> <view class="card-title">
<text class="subtitle">学生分段训练成绩分析</text> <u-icon name="grid" size="18" color="#1890ff"></u-icon>
<text>选择项目</text>
</view>
<view class="picker-row" @click="showProject = true">
<text class="picker-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
<!-- 日期选择卡片 -->
<view class="filter-card " v-if="selectProcet">
<view class="card-title">
<u-icon name="calendar" size="18" color="#13c2c2"></u-icon>
<text>日期范围</text>
</view>
<view class="date-filter" @click="openCalendar">
<view class="date-item">
<text class="date-label">开始日期</text>
<text class="date-value" :class="{ placeholder: !begin }">{{ begin || '请选择' }}</text>
</view>
<view class="date-divider"></view>
<view class="date-item">
<text class="date-label">结束日期</text>
<text class="date-value" :class="{ placeholder: !end }">{{ end || '请选择' }}</text>
</view>
<u-icon name="arrow-right" size="16" color="#bbb" class="date-arrow"></u-icon>
</view> </view>
</view> </view>
<!-- 项目选择区域 --> <!-- 学生选择卡片 -->
<view class="select-section"> <view class="filter-card " v-if="selectProcet && begin">
<view class="select-label"> <view class="card-title">
<view class="label-text" style="margin-bottom: 20rpx;">选择项目</view> <u-icon name="account" size="18" color="#52c41a"></u-icon>
<view class="picker-wrapper" @click="showProject=true"> <text>选择学生</text>
<text class="picker-text">{{ selectProcet || '请选择项目' }}</text>
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view>
</view> </view>
</view> <view class="picker-row" @click="showStudentPicker = true">
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" <text class="picker-text" :class="{ placeholder: selectedStudentIndexes.length === 0 }">{{ selectedStudentDisplay }}</text>
:columns="projectOptions"></up-picker> <u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
<!-- 日期选择 -->
<view class="date-filter" style="margin: 20rpx 20rpx 0;" v-if="selectProcet">
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">开始日期</text>
<text class="value">{{ begin || '请选择' }}</text>
</view>
</view>
<view class="date-picker" @click="openCalendar()">
<up-icon name="calendar" color="#3B82F6" size="25"></up-icon>
<view class="date-text">
<text class="label">结束日期</text>
<text class="value">{{ end || '请选择' }}</text>
</view>
</view> </view>
</view> </view>
<!-- 学生选择区域 --> <!-- 数据表格 -->
<view class="select-section" v-if="selectProcet && begin "> <view v-if="tableData.length > 0" class="data-card">
<view class="select-label"> <view class="card-title">
<text class="label-text">选择学生</text> <u-icon name="list" size="18" color="#faad14"></u-icon>
<text>分段数据</text>
</view> </view>
<view class="picker-wrapper" @click="showStudentPicker = true"> <view class="table-wrap">
<text class="picker-text">{{ selectedStudentDisplay }}</text> <next-table :show-header="true" :columns="columns" :stripe="true" :fit="false" :show-summary="false" :data="tableData" :showPaging="true" :pageIndex="page" @pageChange="pageChange" @cellClick="cellClick" :pageTotal="pageTotal"></next-table>
<u-icon name="arrow-down" size="18" color="#999"></u-icon>
</view> </view>
</view> </view>
<!-- 空状态 -->
<view class="empty-card" v-if="begin && selectedStudentIndexes.length > 0 && tableData.length === 0">
<u-icon name="file-text" size="64" color="#d9d9d9"></u-icon>
<text class="empty-text">该日期暂无分段数据</text>
</view>
<!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar>
<!-- 学生多选选择器 --> <!-- 学生多选选择器 -->
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view> <view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
<view class="student-picker-modal" v-if="showStudentPicker"> <view class="student-picker-modal" v-if="showStudentPicker">
@@ -60,45 +76,19 @@
</view> </view>
</view> </view>
<scroll-view class="student-list" scroll-y> <scroll-view class="student-list" scroll-y>
<view v-for="(student, index) in studentList" :key="student.studentId" class="student-item" <view v-for="(student, index) in studentList" :key="student.studentId" class="student-item" :class="{ selected: selectedStudentIndexes.includes(index) }" @click="toggleStudentSelection(index)">
:class="{ 'selected': selectedStudentIndexes.includes(index) }"
@click="toggleStudentSelection(index)">
<view class="item-checkbox"> <view class="item-checkbox">
<view class="checkbox-inner" :class="{ 'checked': selectedStudentIndexes.includes(index) }"> <view class="checkbox-inner" :class="{ checked: selectedStudentIndexes.includes(index) }">
<u-icon v-if="selectedStudentIndexes.includes(index)" name="checkmark" size="14" <u-icon v-if="selectedStudentIndexes.includes(index)" name="checkmark" size="12" color="#fff"></u-icon>
color="#fff"></u-icon>
</view> </view>
</view> </view>
<view class="item-avatar"> <view class="item-avatar">
<view class="avatar-circle male"> <text class="avatar-text">{{ student.name.charAt(0) }}</text>
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
</view>
<view class="item-info">
<text class="item-name">{{ student.name }}</text>
</view> </view>
<text class="item-name">{{ student.name }}</text>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
<view v-if="tableData.length>0" class="table-section">
<next-table :show-header="true" :columns="columns" :stripe="true" :fit="false" :show-summary='false'
:data="tableData" :showPaging='true' :pageIndex="page" @pageChange="pageChange" @cellClick="cellClick"
:pageTotal="pageTotal"></next-table>
</view>
<view class="empty-container" v-if="begin && selectedStudentIndexes.length > 0 && tableData.length === 0">
<view class="empty-icon">
<u-icon name="file-text" size="80" color="#ccc"></u-icon>
</view>
<text class="empty-text">该日期暂无分段数据</text>
</view>
<!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate='1776240407000' @confirm="calendarConfirm"
@close="calendarClose">
</up-calendar>
<!-- 分段详情弹窗 --> <!-- 分段详情弹窗 -->
<up-popup :show="showSegmentPopup" @close="closeSegmentPopup" @open="openSegmentPopup" mode="center" round="16" bgColor="#fff"> <up-popup :show="showSegmentPopup" @close="closeSegmentPopup" @open="openSegmentPopup" mode="center" round="16" bgColor="#fff">
@@ -152,7 +142,7 @@
segment170 : string segment170 : string
} }
let row=ref('') let row = ref('')
// 项目选择 // 项目选择
let showProject = ref(false) let showProject = ref(false)
@@ -206,11 +196,7 @@
const tableData = ref<TableDataItem[]>([]) const tableData = ref<TableDataItem[]>([])
onLoad(() => {
onLoad(() => {
getProjectData() getProjectData()
}) })
@@ -343,442 +329,322 @@
} }
const cellClick = (rows: any) => { const cellClick = (rows: any) => {
row.value=rows.subsection row.value = rows.subsection
openSegmentPopup() openSegmentPopup()
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
page { page {
background-color: #f5f5f5; background-color: #e8ecf3;
} }
.paragraph-container { .paragraph-container {
min-height: 100vh; min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx; padding-bottom: 40rpx;
} }
/* ==================== 页面标题区域) ==================== */ /* 通用卡片 */
.header-section { .filter-card {
background-color: #fff; background: #ffffff;
padding: 32rpx 28rpx 24rpx; border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx; margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.header-title { }
.title {
font-size: 36rpx;
font-weight: 700;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.subtitle { .card-title {
font-size: 24rpx; display: flex;
align-items: center;
gap: 10rpx;
font-size: 30rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
/* 选择行 */
.picker-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 22rpx 24rpx;
background: #f5f7fa;
border-radius: 12rpx;
border: 1rpx solid #e4e8ee;
transition: all 0.2s;
&:active {
background: #eef1f6;
}
.picker-text {
font-size: 28rpx;
color: #333;
font-weight: 500;
&.placeholder {
color: #999; color: #999;
font-weight: 400;
} }
} }
} }
/* 日期选择 */
.date-filter { .date-filter {
display: flex; display: flex;
gap: 24rpx; align-items: center;
background: #f0fafa;
border-radius: 12rpx;
padding: 22rpx 24rpx;
border: 1rpx solid #d6f0f0;
transition: all 0.2s;
.date-picker { &:active {
flex: 1; background: #e6f5f5;
display: flex;
align-items: center;
gap: 16rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 20rpx;
.date-text {
.label {
display: block;
font-size: 26rpx;
color: #999;
}
.value {
display: block;
font-size: 30rpx;
color: #333;
margin-top: 4rpx;
font-weight: 600;
}
}
} }
} }
/* ==================== 选择区域通用样式 ==================== */ .date-item {
.select-section { flex: 1;
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 20rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
.select-label {
margin-bottom: 16rpx;
.label-text {
font-size: 26rpx;
font-weight: 600;
color: #666;
}
}
.picker-wrapper {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
transition: all 0.3s ease;
&:active {
background-color: #f0f0f0;
border-color: #faad14;
transform: scale(0.98);
}
.picker-text {
font-size: 28rpx;
color: #333;
}
}
}
.student-picker-modal {
position: fixed;
left: 0;
right: 0;
bottom: 0;
height: 70vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx 32rpx 0 0;
z-index: 999;
animation: slideUp 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 -8rpx 40rpx rgba(0, 0, 0, 0.12);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden; align-items: center;
gap: 6rpx;
}
.picker-header { .date-label {
display: flex; font-size: 24rpx;
justify-content: space-between; color: #666;
align-items: center; font-weight: 500;
padding: 36rpx 32rpx 28rpx; }
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-bottom: 1rpx solid rgba(0, 0, 0, 0.06);
flex-shrink: 0;
.header-title { .date-value {
font-size: 36rpx; font-size: 30rpx;
font-weight: 700; color: #333;
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%); font-weight: 600;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.header-actions { &.placeholder {
display: flex; color: #999;
align-items: center; font-weight: 400;
gap: 16rpx;
.action-btn {
font-size: 26rpx;
color: #666;
padding: 14rpx 28rpx;
border-radius: 24rpx;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
font-weight: 600;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
&.primary {
color: #fff;
background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%);
box-shadow: 0 4rpx 16rpx rgba(250, 173, 20, 0.3);
}
&:active {
transform: scale(0.92);
}
}
}
}
.student-list {
flex: 1;
overflow-y: auto;
padding: 0 20rpx 40rpx;
.student-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 24rpx;
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
border-radius: 20rpx;
margin-top: 16rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset;
position: relative;
overflow: hidden;
&:active {
transform: scale(0.98);
}
.item-checkbox {
flex-shrink: 0;
position: relative;
z-index: 1;
.checkbox-inner {
width: 40rpx;
height: 40rpx;
border-radius: 10rpx;
border: 2rpx solid #d9d9d9;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&.checked {
border-color: #faad14;
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
box-shadow: 0 2rpx 8rpx rgba(250, 173, 20, 0.3);
}
}
}
.item-avatar {
flex-shrink: 0;
position: relative;
z-index: 1;
.avatar-circle {
width: 72rpx;
height: 72rpx;
border-radius: 18rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
&.male {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
}
&.female {
background: linear-gradient(135deg, #fa8c16 0%, #ffa940 50%, #d46b08 100%);
}
&::before {
content: '';
position: absolute;
top: 8rpx;
right: 8rpx;
width: 12rpx;
height: 12rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.avatar-text {
font-size: 34rpx;
color: #fff;
font-weight: 700;
letter-spacing: 2rpx;
}
}
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 10rpx;
position: relative;
z-index: 1;
.item-name {
font-size: 30rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
.item-meta {
display: flex;
align-items: center;
gap: 12rpx;
.gender-badge {
font-size: 22rpx;
padding: 6rpx 14rpx;
border-radius: 10rpx;
font-weight: 500;
&.male {
color: #1890ff;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(64, 169, 255, 0.05) 100%);
}
&.female {
color: #fa8c16;
background: linear-gradient(135deg, rgba(250, 140, 22, 0.1) 0%, rgba(255, 169, 64, 0.05) 100%);
}
}
.age-text {
font-size: 22rpx;
color: #999;
}
}
}
}
} }
} }
.date-divider {
width: 2rpx;
height: 50rpx;
background: #b2dfdb;
margin: 0 20rpx;
}
.date-arrow {
margin-left: 12rpx;
}
/* 数据表格卡片 */
.data-card {
background: #ffffff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.table-wrap {
border-radius: 12rpx;
overflow: hidden;
border: 1rpx solid #f0f0f0;
}
/* 空状态 */
.empty-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 40rpx;
background: #ffffff;
border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.empty-text {
margin-top: 24rpx;
font-size: 28rpx;
color: #999;
}
}
/* 弹窗遮罩 */
.modal-overlay { .modal-overlay {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
background: linear-gradient(180deg, rgba(0, 0, 0, 0.6) 0%, rgba(0, 0, 0, 0.4) 100%); background: rgba(0, 0, 0, 0.55);
backdrop-filter: blur(10rpx);
z-index: 998; z-index: 998;
animation: fadeIn 0.3s ease;
} }
/* ==================== 表格区域 ==================== */ /* 学生选择弹窗 */
.table-section { .student-picker-modal {
margin: 0 20rpx; position: fixed;
background-color: #fff; left: 0;
border-radius: 20rpx; right: 0;
overflow: hidden; bottom: 0;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05); height: 60vh;
background: #ffffff;
.table-wrapper-scroll { border-radius: 24rpx 24rpx 0 0;
width: 100%; z-index: 999;
overflow-x: auto;
}
.table-header {
// background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
.header-row {
display: flex;
.header-cell {
min-width: 100px;
padding: 24rpx 16rpx;
font-size: 28rpx;
font-weight: 700;
// color: #fff;
text-align: center;
border-right: 1rpx solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;
&:last-child {
border-right: none;
}
}
}
}
.table-body {
.body-row {
display: flex;
transition: all 0.3s ease;
&.even {
background-color: #fafafa;
}
&.odd {
background-color: #fff;
}
&:active {
background-color: #e6f7ff;
}
.body-cell {
min-width: 100px;
padding: 24rpx 16rpx;
font-size: 26rpx;
color: #333;
text-align: center;
border-right: 1rpx solid #f0f0f0;
flex-shrink: 0;
&:last-child {
border-right: none;
}
}
}
}
}
/* ==================== 空状态容器 ==================== */
.empty-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
animation: slideUp 0.25s ease;
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.15);
}
.picker-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
background: #fafafa;
border-radius: 24rpx 24rpx 0 0;
.header-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.header-actions {
display: flex;
align-items: center;
gap: 16rpx;
}
.action-btn {
font-size: 26rpx;
color: #666;
padding: 10rpx 24rpx;
border-radius: 28rpx;
background: #eeeeee;
font-weight: 500;
&.primary {
color: #fff;
background: #faad14;
}
&:active {
opacity: 0.8;
}
}
}
.student-list {
flex: 1;
overflow-y: auto;
padding: 16rpx 24rpx 40rpx;
}
.student-item {
display: flex;
align-items: center;
gap: 20rpx;
padding: 22rpx 20rpx;
border-radius: 12rpx;
margin-bottom: 12rpx;
background: #f8f9fa;
transition: background 0.2s;
&:active {
background: #eeeeee;
}
&.selected {
background: #fff3e0;
border: 1rpx solid #ffcc80;
}
}
.item-checkbox {
flex-shrink: 0;
.checkbox-inner {
width: 36rpx;
height: 36rpx;
border-radius: 8rpx;
border: 2rpx solid #bfbfbf;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&.checked {
border-color: #faad14;
background: #faad14;
}
}
}
.item-avatar {
flex-shrink: 0;
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: #1890ff;
display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin: 0 20rpx;
background-color: #fff;
border-radius: 20rpx;
padding: 80rpx 40rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.empty-icon { .avatar-text {
margin-bottom: 24rpx;
}
.empty-text {
font-size: 28rpx; font-size: 28rpx;
color: #999; color: #fff;
font-weight: 600;
} }
} }
@keyframes fadeIn { .item-name {
from { flex: 1;
opacity: 0; font-size: 30rpx;
} color: #333;
font-weight: 500;
to {
opacity: 1;
}
} }
@keyframes slideUp { /* 表格深度样式 */
from { ::v-deep .sl-table {
transform: translateY(100%); width: 100%;
}
to {
transform: translateY(0);
}
} }
::v-deep .sl-table__header {
background: #faad14 !important;
}
::v-deep .sl-table__header__cell {
color: #fff !important;
font-weight: 600;
font-size: 26rpx;
}
::v-deep .sl-table__body__cell {
font-size: 26rpx;
color: #333;
padding: 22rpx 14rpx;
}
::v-deep .sl-table__body__row:nth-child(even) {
background-color: #fafafa !important;
}
::v-deep .sl-table__body__row:nth-child(odd) {
background-color: #fff !important;
}
/* 分段详情弹窗 */
.segment-popup-content { .segment-popup-content {
width: 620rpx; width: 620rpx;
max-height: 70vh; max-height: 70vh;
@@ -853,4 +719,13 @@
} }
} }
} }
@keyframes slideUp {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -6,8 +6,8 @@
<view class="empty-state-container"> <view class="empty-state-container">
<!-- 标题区域 --> <!-- 标题区域 -->
<view class="title-section"> <view class="title-section">
<text class="main-title">暂无项目</text> <!-- <text class="main-title">暂无项目</text>
<text class="sub-title">您还没有创建任何训练项目开始您的第一次游泳训练记录吧</text> <text class="sub-title">您还没有创建任何训练项目开始您的第一次游泳训练记录吧</text> -->
</view> </view>
<!-- 创建项目模块列表 --> <!-- 创建项目模块列表 -->
@@ -150,6 +150,7 @@
}) })
onShow(()=>{ onShow(()=>{
showTimingModal.value=false
uni.showTabBar() uni.showTabBar()
}) })
@@ -247,6 +248,7 @@
showTimingModal.value = false showTimingModal.value = false
Service.GoPage('/pages/userFunc/hunyang?id=' + project.planId) Service.GoPage('/pages/userFunc/hunyang?id=' + project.planId)
} }
} }
</script> </script>

View File

@@ -23,16 +23,15 @@
<view class="stats-section"> <view class="stats-section">
<view class="stats-card"> <view class="stats-card">
<view class="stat-item"> <view class="stat-item">
<view @click="Service.GoPage('/pages/userFunc/projectList')" class="stat-info">
<view class="stat-info"> <text class="stat-value">{{ planCount }}</text>
<text class="stat-value">{{ userInfo.projectCount || 0 }}</text>
<text class="stat-label">我的项目</text> <text class="stat-label">我的项目</text>
</view> </view>
</view> </view>
<view class="stat-divider"></view> <view class="stat-divider"></view>
<view class="stat-item"> <view class="stat-item">
<view class="stat-info"> <view @click="Service.GoPage('/pages/userFunc/student')" class="stat-info">
<text class="stat-value">{{ userInfo.studentCount || 0 }}</text> <text class="stat-value">{{ studentCount }}</text>
<text class="stat-label">学员数</text> <text class="stat-label">学员数</text>
</view> </view>
</view> </view>
@@ -114,21 +113,24 @@
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { userService } from '@/Service/swimming/userService' import { userService } from '@/Service/swimming/userService'
let planCount=ref(0)
let studentCount=ref(0)
let userInfo = ref<any>({}) let userInfo = ref<any>({})
onLoad(() => { onLoad(() => {
loadUserInfo()
}) })
onShow(() => { onShow(() => {
loadUserInfo()
}) })
const loadUserInfo = () => { const loadUserInfo = () => {
userService.GetUserInfo().then((content) => { userService.GetUserInfo().then((content:any) => {
if (content.code == 0) { if (content.code == 0) {
studentCount.value=content.data.studentCount
planCount.value=content.data.planCount
userInfo.value=content.data.userInfo userInfo.value=content.data.userInfo
} else { } else {
Service.Msg(content.msg) Service.Msg(content.msg)
@@ -315,12 +317,6 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 16rpx; gap: 16rpx;
cursor: pointer;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
} }
.stat-icon-bg { .stat-icon-bg {

View File

@@ -10,29 +10,44 @@
<input class="form-input" v-model="projectName" placeholder="请输入项目名称" <input class="form-input" v-model="projectName" placeholder="请输入项目名称"
placeholder-class="input-placeholder" /> placeholder-class="input-placeholder" />
</view> </view>
<!-- 计划总时长 -->
<view class="total-time-section">
<view class="total-time-label">计划总时长</view>
<view class="total-time-value">{{ formatTotalTime(totalDuration) }}</view>
</view>
</view> </view>
<!-- 计划列表 --> <!-- 分组列表 -->
<view class="form-card"> <view class="form-card" v-for="(group, groupIndex) in groups" :key="group.id">
<view class="form-title">计划列表</view> <view class="group-header">
<view class="group-title-wrapper">
<text class="form-title">{{ group.name || `分组 ${groupIndex + 1}` }}</text>
<view class="delete-group-btn" @click="deleteGroup(groupIndex)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
</view>
<!-- 分组名称输入 -->
<view class="form-group" style="margin-bottom: 20rpx;">
<text class="form-label">分组名称</text>
<input class="form-input" v-model="group.name" placeholder="请输入分组名称"
placeholder-class="input-placeholder" />
</view>
<!-- 计划项列表 --> <!-- 计划项列表 -->
<view v-for="(item, index) in planList" :key="index" class="plan-item"> <view v-for="(item, planIndex) in group.plans" :key="planIndex" class="plan-item">
<view class="plan-item-header"> <view class="plan-item-header">
<text class="plan-item-title">计划 {{ index + 1 }}</text> <text class="plan-item-title">计划 {{ planIndex + 1 }}</text>
<view class="delete-plan-btn" @click="deletePlanItem(index)"> <view class="delete-plan-btn" @click="deletePlanItem(groupIndex, planIndex)">
<u-icon name="trash" size="16" color="#fff"></u-icon> <u-icon name="trash" size="16" color="#fff"></u-icon>
</view> </view>
</view> </view>
<view class="plan-item-content"> <view class="plan-item-content">
<!-- 目标时间 - 分秒选择 --> <!-- 计划名称 -->
<view class="input-group">
<text class="input-label">计划名称</text>
<input class="plan-name-input" v-model="item.name" placeholder="请输入计划名称" />
</view>
<!-- 目标时间 - 秒输入 -->
<view class="input-group"> <view class="input-group">
<text class="input-label">目标时间</text> <text class="input-label">目标时间</text>
<view class="time-selector"> <view class="time-selector">
@@ -44,6 +59,14 @@
</view> </view>
</view> </view>
<!-- 单项时长显示 -->
<!-- <view class="input-group" v-if="item.allTime > 0">
<text class="input-label">单项时长</text>
<view class="all-time-display">
<text class="all-time-value">{{ item.allTime }} </text>
</view>
</view> -->
<!-- 休息时长 - 秒选择 --> <!-- 休息时长 - 秒选择 -->
<view class="input-group"> <view class="input-group">
<text class="input-label">休息时长</text> <text class="input-label">休息时长</text>
@@ -65,13 +88,21 @@
</view> </view>
<!-- 添加计划按钮 --> <!-- 添加计划按钮 -->
<view class="add-plan-btn" @click="addPlanItem"> <view class="add-plan-btn" @click="addPlanItem(groupIndex)">
<u-icon name="plus-circle" size="20" color="#1890ff"></u-icon> <u-icon name="plus-circle" size="20" color="#1890ff"></u-icon>
<text class="add-plan-text">添加计划</text> <text class="add-plan-text">添加计划</text>
</view> </view>
</view> </view>
</view>
<!-- 添加分组按钮 -->
<view class="add-group-btn" @click="addGroup">
<u-icon name="plus-circle" size="24" color="#fff"></u-icon>
<text class="add-group-text">添加分组</text>
</view>
</view>
<view class="" style="width: 100%; height: 80rpx;" >
</view>
<!-- 底部按钮区域 --> <!-- 底部按钮区域 -->
<view class="bottom-actions"> <view class="bottom-actions">
<view class="action-buttons"> <view class="action-buttons">
@@ -85,50 +116,79 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { studentService } from '@/Service/swimming/studentService'
import { PlanService } from '@/Service/swimming/PlanService' import { PlanService } from '@/Service/swimming/PlanService'
import { onLoad, onShow } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
// 计划项接口 // 计划项接口
interface PlanItem { interface PlanItem {
targetMinutes : string name: string
targetSeconds : string targetSeconds: string
restTime : string restTime: string
lapCount : string lapCount: string
allTime?: number
}
// 分组接口
interface Group {
id: string
name: string
plans: PlanItem[]
allTime?: number
} }
// 项目名称 // 项目名称
const projectName = ref('') const projectName = ref('')
// 计划列表 // 分组列表
const planList = ref<PlanItem[]>([]) const groups = ref<Group[]>([])
let planId = ref('') let planId = ref('')
onLoad((data : any) => { onLoad((data: any) => {
planId.value = data.id planId.value = data.id
if(planId.value ){ if (planId.value) {
getPlanInfo() getPlanInfo()
} else {
// 新建时默认添加一个分组
addGroup()
} }
}) })
// 获取计划详情 // 获取计划详情
const getPlanInfo = () => { const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => { PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) { if (res.code == 0) {
projectName.value=res.data.plan.name projectName.value = res.data.plan.name
JSON.parse(res.data.plan.project).map((item:any)=>{ const projectData = JSON.parse(res.data.plan.project)
planList.value.push({ // 兼容旧数据:如果 project 是数组,放入一个默认分组
targetMinutes:'', if (Array.isArray(projectData)) {
targetSeconds : item.target, groups.value = projectData.map((g: any, index: number) => ({
restTime : item.rest, id: generateId(),
lapCount : item.circle name: g.groupName || `分组 ${index + 1}`,
}) allTime: g.allTime || 0,
}) plans: (g.group || []).map((item: any) => ({
name: String(item.name || ''),
targetSeconds: String(item.target || ''),
restTime: String(item.rest || ''),
lapCount: String(item.circle || ''),
allTime: item.allTime || 0
}))
}))
} else if (projectData.groups && Array.isArray(projectData.groups)) {
// 新数据格式
groups.value = projectData.groups.map((g: any) => ({
id: g.id || generateId(),
name: g.name || '',
allTime: g.allTime || 0,
plans: (g.plans || []).map((item: any) => ({
name: String(item.name || ''),
targetSeconds: String(item.target || ''),
restTime: String(item.rest || ''),
lapCount: String(item.circle || ''),
allTime: item.allTime || 0
}))
}))
}
} else { } else {
// 显示错误信息
Service.Msg(res.msg) Service.Msg(res.msg)
} }
}) })
@@ -137,55 +197,75 @@
// 计算计划总时长(秒) // 计算计划总时长(秒)
const totalDuration = computed(() => { const totalDuration = computed(() => {
let total = 0 let total = 0
planList.value.forEach(item => { groups.value.forEach(group => {
const targetMinutes = parseInt(item.targetMinutes) || 0 group.plans.forEach(item => {
const targetSeconds = parseInt(item.targetSeconds) || 0 const targetSeconds = parseInt(item.targetSeconds) || 0
const targetTime = targetMinutes * 60 + targetSeconds const restTime = parseInt(item.restTime) || 0
const restTime = parseInt(item.restTime) || 0 const lapCount = parseInt(item.lapCount) || 0
const lapCount = parseInt(item.lapCount) || 0 // 计算公式:(目标时间 + 休息时间) * 圈数
total += (targetSeconds + restTime) * lapCount
// 计算公式:(目标时间 + 休息时间) * 圈数 })
total += (targetTime + restTime) * lapCount
}) })
return total return total
}) })
// 格式化总时长显示(时:分:秒) // 格式化总时长显示(时:分:秒)
const formatTotalTime = (seconds : number) : string => { const formatTotalTime = (seconds: number): string => {
if (seconds <= 0) return '00:00:00' if (seconds <= 0) return '00:00:00'
const hours = Math.floor(seconds / 3600) const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60) const minutes = Math.floor((seconds % 3600) / 60)
const secs = Math.floor(seconds % 60) const secs = Math.floor(seconds % 60)
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
} }
// 生成唯一ID // 生成唯一ID
const generateId = () : string => { const generateId = (): string => {
return Date.now().toString() + Math.random().toString(36).substr(2, 9) return Date.now().toString() + Math.random().toString(36).substr(2, 9)
} }
// 添加计划项 // 添加分组
const addPlanItem = () => { const addGroup = () => {
const newItem : PlanItem = { const newGroup: Group = {
id: generateId(), id: generateId(),
targetMinutes: '', name: '',
plans: []
}
groups.value.push(newGroup)
}
// 删除分组
const deleteGroup = (groupIndex: number) => {
uni.showModal({
title: '确认删除',
content: '确定要删除该分组吗?分组内的所有计划也将被删除。',
success: (res) => {
if (res.confirm) {
groups.value.splice(groupIndex, 1)
Service.Msg('删除成功')
}
}
})
}
// 添加计划项
const addPlanItem = (groupIndex: number) => {
const newItem: PlanItem = {
name: '',
targetSeconds: '', targetSeconds: '',
restTime: '', restTime: '',
lapCount: '' lapCount: ''
} }
planList.value.push(newItem) groups.value[groupIndex].plans.push(newItem)
} }
// 删除计划项 // 删除计划项
const deletePlanItem = (index : number) => { const deletePlanItem = (groupIndex: number, planIndex: number) => {
uni.showModal({ uni.showModal({
title: '确认删除', title: '确认删除',
content: '确定要删除该计划吗?', content: '确定要删除该计划吗?',
success: (res) => { success: (res) => {
if (res.confirm) { if (res.confirm) {
planList.value.splice(index, 1) groups.value[groupIndex].plans.splice(planIndex, 1)
Service.Msg('删除成功') Service.Msg('删除成功')
} }
} }
@@ -193,70 +273,106 @@
} }
// 验证表单 // 验证表单
const validateForm = () : boolean => { const validateForm = (): boolean => {
if (!projectName.value.trim()) { if (!projectName.value.trim()) {
Service.Msg('请输入项目名称') Service.Msg('请输入项目名称')
return false return false
} }
if (planList.value.length === 0) { if (groups.value.length === 0) {
Service.Msg('请至少添加一个计划') Service.Msg('请至少添加一个分组')
return false return false
} }
for (let i = 0; i < planList.value.length; i++) { for (let g = 0; g < groups.value.length; g++) {
const item = planList.value[i] const group = groups.value[g]
if (!group.name.trim()) {
if (!item.targetSeconds) { Service.Msg(`分组 ${g + 1} 请输入分组名称`)
Service.Msg(`计划 ${i + 1} 请输入目标时间`)
return false return false
} }
const seconds = parseInt(item.targetSeconds) || 0 if (group.plans.length === 0) {
if (seconds === 0) { Service.Msg(`分组 "${group.name}" 请至少添加一个计划`)
Service.Msg(`计划 ${i + 1} 目标时间不能为0`)
return false return false
} }
if (!item.restTime || parseInt(item.restTime) < 0) { for (let i = 0; i < group.plans.length; i++) {
Service.Msg(`计划 ${i + 1} 请输入有效的休息时长`) const item = group.plans[i]
return false if (!item.name.trim()) {
} Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入计划名称`)
return false
if (!item.lapCount || parseInt(item.lapCount) <= 0) { }
Service.Msg(`计划 ${i + 1} 请输入有效的圈数`) if (!item.targetSeconds) {
return false Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入目标时间`)
return false
}
const seconds = parseInt(item.targetSeconds) || 0
if (seconds === 0) {
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 目标时间不能为0`)
return false
}
if (!item.restTime || parseInt(item.restTime) < 0) {
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入有效的休息时长`)
return false
}
if (!item.lapCount || parseInt(item.lapCount) <= 0) {
Service.Msg(`分组 "${group.name}" 计划 ${i + 1} 请输入有效的圈数`)
return false
}
} }
} }
return true return true
} }
// 计算单项时长
const calculatePlanAllTime = (item: PlanItem): number => {
const target = parseInt(item.targetSeconds) || 0
const rest = parseInt(item.restTime) || 0
const circle = parseInt(item.lapCount) || 0
return (target + rest) * circle
}
// 计算分组总时长
const calculateGroupAllTime = (group: Group): number => {
return group.plans.reduce((sum, item) => sum + calculatePlanAllTime(item), 0)
}
// 保存 // 保存
const handleSave = () => { const handleSave = () => {
if (!validateForm()) { if (!validateForm()) {
return return
} }
let plan = planList.value.map(item => ({ const projectData = groups.value.map(group => {
target: (parseInt(item.targetMinutes) || 0) * 60 + (parseInt(item.targetSeconds) || 0), const groupAllTime = calculateGroupAllTime(group)
rest: parseInt(item.restTime), return {
circle: parseInt(item.lapCount) allTime: groupAllTime,
})) groupName: group.name,
// 整理数据 group: group.plans.map(item => ({
allTime: calculatePlanAllTime(item),
name: item.name,
target: parseInt(item.targetSeconds) || 0,
rest: parseInt(item.restTime) || 0,
circle: parseInt(item.lapCount) || 0
}))
}
})
console.log(projectData);
const data = { const data = {
planId: planId.value, planId: planId.value,
name: projectName.value, name: projectName.value,
planType: '混氧项目', planType: '混氧项目',
departType: '', departType: '',
interval: '', interval: '',
groupInt: '', groupInt: String(groups.value.length),
subsectionDistance: '', subsectionDistance: '',
subsectionInt: '', subsectionInt: '',
users: '', users: '',
project: JSON.stringify(plan), project: JSON.stringify(projectData),
group: '', group: ''
} }
console.log('保存的数据:', data) console.log('保存的数据:', data)
@@ -317,6 +433,38 @@
} }
} }
/* 分组头部 */
.group-header {
margin-bottom: 20rpx;
.group-title-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
.form-title {
margin-bottom: 0;
}
}
}
/* 删除分组按钮 */
.delete-group-btn {
width: 44rpx;
height: 44rpx;
background: linear-gradient(135deg, #ff4d4f 0%, #d9363e 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
}
}
/* 表单输入 */ /* 表单输入 */
.form-group { .form-group {
margin-bottom: 0; margin-bottom: 0;
@@ -496,6 +644,45 @@
} }
} }
/* 计划名称输入 */
.plan-name-input {
flex: 1;
font-size: 28rpx;
color: #333;
background-color: #fff;
border-radius: 12rpx;
padding: 0 16rpx;
height: 72rpx;
}
/* 分组总时长 */
.group-time-info {
margin-right: 16rpx;
padding: 6rpx 12rpx;
background: #e6f7ff;
border-radius: 8rpx;
}
.group-time-text {
font-size: 24rpx;
color: #1890ff;
font-weight: 500;
}
/* 单项时长显示 */
.all-time-display {
padding: 12rpx 16rpx;
background: #f6ffed;
border-radius: 8rpx;
border: 1rpx solid #b7eb8f;
}
.all-time-value {
font-size: 28rpx;
color: #52c41a;
font-weight: 600;
}
/* 添加计划按钮 */ /* 添加计划按钮 */
.add-plan-btn { .add-plan-btn {
display: flex; display: flex;
@@ -521,6 +708,31 @@
} }
} }
/* 添加分组按钮 */
.add-group-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 28rpx 32rpx;
margin-top: 20rpx;
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(82, 196, 26, 0.3);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.2);
}
.add-group-text {
font-size: 30rpx;
color: #fff;
font-weight: 600;
}
}
/* 底部操作按钮 */ /* 底部操作按钮 */
.bottom-actions { .bottom-actions {
position: fixed; position: fixed;

File diff suppressed because it is too large Load Diff

View File

@@ -9,32 +9,39 @@
</view> </view>
<!-- 全局计时器 --> <!-- 全局计时器 -->
<view v-if="selectedProject && setting.mode=='interval' " class="card-section global-timer-section"> <view v-if="selectedProject " class="card-section global-timer-section">
<view class="global-timer-header"> <!-- <view class="global-timer-header">
<view class="global-timer-label">全局计时</view> <view class="global-timer-label">全局计时</view>
<view class="global-setting-btn" @click="showSetting = true"> <view class="global-setting-btn" @click="showSetting = true">
<u-icon name="setting" size="18" color="#1890ff"></u-icon> <u-icon name="setting" size="18" color="#1890ff"></u-icon>
</view> </view>
</view> -->
<view class="global-timer-header" style="justify-content: normal;" >
<view class="project-select-text">{{ setting.mode=='interval'?'间隔出发':'一起出发' }}{{ setting.mode=='interval'?'·'+ setting.interval+'s':'' }}</view>
<view class="global-setting-btn" @click="showSetting = true">
<u-icon name="setting" size="18" color="#1890ff"></u-icon>
</view>
</view> </view>
<view class="global-timer-display"> <!-- <view class="global-timer-display">
<view class="global-timer-value">{{ formatGlobalTimer(globalTimerTime) }}</view> <view class="global-timer-value">{{ formatGlobalTimer(globalTimerTime) }}</view>
</view> </view> -->
</view> </view>
<!-- 分组计时器列表 --> <!-- 分组计时器列表 -->
<view v-for="group in groups" :key="group.id" class="card-section"> <view v-for="group in groups" :key="group.id" class="card-section">
<view class="section-header"> <view class="section-header">
<view class="group-title-wrapper"> <view class="group-title-wrapper">
<text class="section-title">{{ group.name }}</text> <text class="section-title">{{ group.name }} · 总时长 {{ group.allTime}} </text>
<view class="delete-group-btn" @click="deleteGroup(group)"> <view class="delete-group-btn" @click="deleteGroup(group)">
<u-icon name="trash" size="16" color="#fff"></u-icon> <u-icon name="trash" size="16" color="#fff"></u-icon>
</view> </view>
</view> </view>
<view class="group-controls"> <view class="group-controls">
<!-- @click="openGroupSetting(group)" --> <!-- @click="openGroupSetting(group)" -->
<!-- <view class="group-control-btn setting-btn"> <view @click="addTimer(group)" class="group-control-btn setting-btn">
<u-icon name="setting" size="18" color="#fff"></u-icon> <u-icon name="plus" size="16" blod='true' color="#fff"></u-icon>
</view> --> </view>
<view v-if="!isGroupAllRunning(group) && !isGroupAllCompleted(group)" <view v-if="!isGroupAllRunning(group) && !isGroupAllCompleted(group)"
class="group-control-btn start-btn" @click="startGroup(group)"> class="group-control-btn start-btn" @click="startGroup(group)">
<u-icon name="play-circle" size="18" color="#fff"></u-icon> <u-icon name="play-circle" size="18" color="#fff"></u-icon>
@@ -62,18 +69,16 @@
<view class="circle-content-wrapper" style="margin-top: 20rpx;"> <view class="circle-content-wrapper" style="margin-top: 20rpx;">
<view class="circle-content" :class="{ resting: timer.isResting }"> <view class="circle-content" :class="{ resting: timer.isResting }">
<view class="circle-inner"> <view class="circle-inner">
<view class="timer-target" @click.stop="openTimerDetail(timer)">目标: <view v-if="timer.status === 'completed'" class="timer-status-completed">
{{ formatTargetTime(timer.targetTime) }} 已完成
</view> </view>
<view v-if="timer.isResting" class="timer-rest-countdown"> <view v-else-if="timer.isResting" class="timer-rest-countdown">
<text class="rest-label">休息</text> <text class="rest-label">休息</text>
<text <text class="rest-time">{{ formatRestCountdown(timer.restCountdown || 0) }}</text>
class="rest-time">{{ formatRestCountdown(timer.restCountdown || 0) }}</text>
</view> </view>
<view v-else class="timer-time">{{ formatTime(timer) }}</view> <view v-else class="timer-time">{{ formatTime(timer) }}</view>
<view class="timer-info" @click.stop="openTimerDetail(timer)"> <view class="timer-info">
<text <text class="timer-duration">{{ timer.lapCount }}/{{ timer.totalLapCount || 4 }}</text>
class="timer-duration">{{ timer.lapCount }}/{{ timer.totalLapCount || 4 }}</text>
</view> </view>
</view> </view>
</view> </view>
@@ -92,11 +97,6 @@
</view> </view>
</view> </view>
</view> </view>
<view class="add-student-btn" @click="addTimer(group)">
<u-icon name="plus-circle" size="20" color="#1890ff"></u-icon>
<text class="add-student-text">点击添加学生到本组</text>
</view>
</view> </view>
<!-- 添加分组按钮 --> <!-- 添加分组按钮 -->
@@ -131,7 +131,7 @@
</view> </view>
<!-- 底部弹出框 - 秒表详情 --> <!-- 底部弹出框 - 秒表详情 -->
<u-popup v-model:show="showTimerDetail" mode="bottom" :round="20" :closeable="true" closeOnClickOverlay> <u-popup v-model:show="showTimerDetail" :safeAreaInsetTop='true' mode="bottom" :round="20" :closeable="true" closeOnClickOverlay>
<view class="timer-detail-popup"> <view class="timer-detail-popup">
<view class="mode-tabs"> <view class="mode-tabs">
<view class="mode-tab" :class="{ active: !isEditMode }" @click="isEditMode = false"> <view class="mode-tab" :class="{ active: !isEditMode }" @click="isEditMode = false">
@@ -145,8 +145,8 @@
<view v-if="!isEditMode"> <view v-if="!isEditMode">
<view class="detail-name">{{ selectedTimer?.studentName || '计时器' }}</view> <view class="detail-name">{{ selectedTimer?.studentName || '计时器' }}</view>
<view class="stopwatch-display"> <view class="stopwatch-display">
<text class="stopwatch-time">{{ formatStopwatchTime(selectedTimer?.currentTime || 0) }}</text> <text class="stopwatch-time">{{ formatStopwatchTime(selectedTimer?.elapsedTime || 0) }}</text>
<text class="stopwatch-millis">{{ formatMillis(selectedTimer?.currentTime || 0) }}</text> <text class="stopwatch-millis">{{ formatMillis(selectedTimer?.elapsedTime || 0) }}</text>
</view> </view>
<view class="current-settings-info"> <view class="current-settings-info">
<view class="setting-info-item"> <view class="setting-info-item">
@@ -183,9 +183,9 @@
<text class="btn-label">{{ selectedTimer?.status === 'running' ? '暂停' : '开始' }}</text> <text class="btn-label">{{ selectedTimer?.status === 'running' ? '暂停' : '开始' }}</text>
</view> </view>
<view class="detail-btn detail-btn-record" @click="recordLap" <view class="detail-btn detail-btn-record" @click="recordLap"
:class="{ disabled: selectedTimer?.status !== 'running' }"> :class="{ disabled: selectedTimer?.status !== 'running' && selectedTimer?.status !== 'resting' }">
<u-icon name="edit-pen" size="24" <u-icon name="edit-pen" size="24"
:color="selectedTimer?.status === 'running' ? '#fff' : '#bfbfbf'"></u-icon> :color="selectedTimer?.status === 'running' || selectedTimer?.status === 'resting' ? '#fff' : '#bfbfbf'"></u-icon>
<text class="btn-label">记录</text> <text class="btn-label">记录</text>
</view> </view>
</view> </view>
@@ -282,7 +282,7 @@
</u-popup> </u-popup>
<!-- 全局设置弹窗 --> <!-- 全局设置弹窗 -->
<u-popup v-model:show="showSetting" mode="center" :round="16" :closeable="true" closeOnClickOverlay> <u-popup v-model:show="showSetting" mode="center" :round="16" :safeAreaInsetBottom='false' :closeable="true" >
<view class="setting-modal"> <view class="setting-modal">
<view class="setting-title">出发设置</view> <view class="setting-title">出发设置</view>
<view class="setting-item" :class="{ active: setting.mode === 'together' }" <view class="setting-item" :class="{ active: setting.mode === 'together' }"
@@ -390,6 +390,8 @@
id : string // 唯一标识 id : string // 唯一标识
studentId : string // 学生ID studentId : string // 学生ID
currentTime : number // 当前累计游泳时间(秒),休息期间不累计 currentTime : number // 当前累计游泳时间(秒),休息期间不累计
countdownTime : number // 当前圈目标时间倒计时(秒)
elapsedTime : number // 真实经过时间(秒),用于记录成绩,休息不暂停
status : 'idle' | 'running' | 'paused' | 'completed' | 'resting' status : 'idle' | 'running' | 'paused' | 'completed' | 'resting'
studentName : string // 学生姓名 studentName : string // 学生姓名
quicklyTime : number // 学生最快时间(秒) quicklyTime : number // 学生最快时间(秒)
@@ -403,6 +405,8 @@
totalLapCount : number // 总圈数 totalLapCount : number // 总圈数
restCountdown : number // 休息剩余时间(秒) restCountdown : number // 休息剩余时间(秒)
isResting : boolean // 是否正在休息 isResting : boolean // 是否正在休息
startTimestamp : number // 项目开始的真实时间戳(毫秒)
lastRecordTimestamp : number // 上次记录时的真实时间戳(毫秒)
} }
/** 分组 */ /** 分组 */
@@ -414,6 +418,7 @@
targetTime : number // 分组默认pb用于弹窗回显 targetTime : number // 分组默认pb用于弹窗回显
restTime : number // 分组默认休息(用于弹窗回显) restTime : number // 分组默认休息(用于弹窗回显)
lapCount : number // 分组默认圈数(用于弹窗回显) lapCount : number // 分组默认圈数(用于弹窗回显)
allTime : number // 计划总时长(秒)
students : string[] // 组内学生ID列表 students : string[] // 组内学生ID列表
} }
@@ -440,6 +445,7 @@
groupName ?: string groupName ?: string
gruopName ?: string // 接口历史拼写兼容 gruopName ?: string // 接口历史拼写兼容
group ?: PlanStudentRaw[] | string | null group ?: PlanStudentRaw[] | string | null
allTime ?: number | string // 计划总时长(秒)
} }
/** 接口返回的原始计划结构 */ /** 接口返回的原始计划结构 */
@@ -513,6 +519,8 @@
id: createTimerId(groupId, student.studentId), id: createTimerId(groupId, student.studentId),
studentId: student.studentId, studentId: student.studentId,
currentTime: 0, currentTime: 0,
countdownTime: 0,
elapsedTime: 0,
status: 'idle', status: 'idle',
studentName: student.name, studentName: student.name,
quicklyTime, quicklyTime,
@@ -525,7 +533,9 @@
restTime, restTime,
totalLapCount, totalLapCount,
restCountdown: 0, restCountdown: 0,
isResting: false isResting: false,
startTimestamp: 0,
lastRecordTimestamp: 0
} }
} }
@@ -537,6 +547,7 @@
const buildGroup = (raw : PlanGroupRaw, index : number) : Group => { const buildGroup = (raw : PlanGroupRaw, index : number) : Group => {
const id = raw.groupId || createGroupId() const id = raw.groupId || createGroupId()
const timers = parsePlanList(raw.group).map(s => buildTimer(s, id)) const timers = parsePlanList(raw.group).map(s => buildTimer(s, id))
return { return {
id, id,
name: raw.groupName || raw.gruopName || `${index + 1}`, name: raw.groupName || raw.gruopName || `${index + 1}`,
@@ -545,6 +556,7 @@
targetTime: timers[0]?.pb || 5, targetTime: timers[0]?.pb || 5,
restTime: timers[0]?.restTime || 10, restTime: timers[0]?.restTime || 10,
lapCount: timers[0]?.totalLapCount || 4, lapCount: timers[0]?.totalLapCount || 4,
allTime: toNumber(raw.allTime, 0),
students: timers.map(t => t.studentId) students: timers.map(t => t.studentId)
} }
} }
@@ -575,11 +587,6 @@
*/ */
const applyPlan = (raw : PlanRaw) => { const applyPlan = (raw : PlanRaw) => {
planSnapshot.value = { ...raw } planSnapshot.value = { ...raw }
// ungroupedStudents.value = parsePlanList<PlanStudentRaw>(raw.users).map(s => ({
// studentId: s.studentId,
// name: s.name,
// quicklyTime: toNumber(s.quicklyTime, 0)
// }))
groups.value = parsePlanList<PlanGroupRaw>(raw.group).map((g, i) => buildGroup(g, i)) groups.value = parsePlanList<PlanGroupRaw>(raw.group).map((g, i) => buildGroup(g, i))
setting.value.mode = raw.departType === '间隔出发' ? 'interval' : 'together' setting.value.mode = raw.departType === '间隔出发' ? 'interval' : 'together'
setting.value.interval = String(toNumber(raw.interval, 5)) setting.value.interval = String(toNumber(raw.interval, 5))
@@ -727,6 +734,33 @@
} }
} }
/**
* 存储每个学生的真实时间计时器 setInterval ID
* 用于 elapsedTime不受休息影响只在 pause/complete/reset 时停止
*/
const elapsedTimeIntervals = new Map<string, number>()
/** 启动真实时间计时器(不受休息影响) */
const startElapsedTimeTimer = (timer : TimerItem) => {
if (elapsedTimeIntervals.has(timer.studentId)) return
if (timer.startTimestamp === 0) {
timer.startTimestamp = Date.now()
}
const id = setInterval(() => {
timer.elapsedTime = (Date.now() - timer.startTimestamp) / 1000
}, 50) as unknown as number
elapsedTimeIntervals.set(timer.studentId, id)
}
/** 停止真实时间计时器 */
const stopElapsedTimeTimer = (studentId : string) => {
const id = elapsedTimeIntervals.get(studentId)
if (id) {
clearInterval(id)
elapsedTimeIntervals.delete(studentId)
}
}
/** /**
* 启动学生主计时器 * 启动学生主计时器
* 逻辑: * 逻辑:
@@ -745,14 +779,45 @@
timer.isResting = false timer.isResting = false
timer.restCountdown = 0 timer.restCountdown = 0
timer.hasStarted = true timer.hasStarted = true
// 如果倒计时已归零,重置为目标时间
if (timer.countdownTime <= 0) {
timer.countdownTime = timer.targetTime
}
// 启动真实时间计时器(不受休息影响)
startElapsedTimeTimer(timer)
// 通过当前累计时间反推开始时刻,保证暂停后继续计时的连续性 // 通过当前累计时间反推开始时刻,保证暂停后继续计时的连续性
const startAt = Date.now() - timer.currentTime * 1000 const startAt = Date.now() - timer.currentTime * 1000
const countdownStartAt = Date.now()
const initialCountdown = timer.countdownTime
const id = setInterval(() => { const id = setInterval(() => {
// 更新累计游泳时间
timer.currentTime = (Date.now() - startAt) / 1000 timer.currentTime = (Date.now() - startAt) / 1000
// 更新目标时间倒计时
const elapsed = (Date.now() - countdownStartAt) / 1000
timer.countdownTime = Math.max(0, initialCountdown - elapsed)
// 倒计时结束,自动完成一圈
if (timer.countdownTime <= 0 && timer.status === 'running') {
clearStudentInterval(timer.studentId)
autoCompleteLap(timer)
}
}, 50) as unknown as number }, 50) as unknown as number
studentIntervals.set(timer.studentId, id) studentIntervals.set(timer.studentId, id)
} }
/**
* 自动完成一圈(倒计时结束时触发,不记录成绩)
* 注意:圈数跑完后不停止计时器,等待用户手动记录完成后再暂停
*/
const autoCompleteLap = (timer : TimerItem) => {
timer.lapCount += 1
if (timer.lapCount >= timer.totalLapCount) {
// 圈数已跑完,但不暂停计时器,继续等待用户记录
Service.Msg(`${timer.studentName} 所有圈数已完成,请记录成绩`)
return
}
startRest(timer)
}
/** /**
* 暂停学生计时器 * 暂停学生计时器
* 逻辑: * 逻辑:
@@ -763,6 +828,7 @@
const pauseStudentTimer = (timer : TimerItem) => { const pauseStudentTimer = (timer : TimerItem) => {
clearStudentInterval(timer.studentId) clearStudentInterval(timer.studentId)
clearStudentRestInterval(timer.studentId) clearStudentRestInterval(timer.studentId)
stopElapsedTimeTimer(timer.studentId)
if (timer.status === 'running' || timer.status === 'resting') { if (timer.status === 'running' || timer.status === 'resting') {
timer.status = 'paused' timer.status = 'paused'
timer.isResting = false timer.isResting = false
@@ -772,37 +838,41 @@
/** /**
* 启动休息倒计时 * 启动休息倒计时
* 触发时机:学生完成一圈记录后(且未完成总圈数 * 触发时机:学生完成一圈后(自动完成,不记录成绩
* *
* 逻辑: * 逻辑:
* 1. 先停止主计时器(休息时间不计入 currentTime * 1. 先停止主计时器(休息时间不计入 currentTime
* 2. 设置状态为 resting开始倒计时 * 2. 不停止 elapsedTime 计时器(休息期间继续计时)
* 3. 每50ms更新 restCountdown * 3. 设置状态为 resting开始倒计时
* 4. 倒计时结束后自动调用 startStudentTimer 恢复计时 * 4. 每50ms更新 restCountdown
* 5. 倒计时结束后重置 countdownTime 并自动调用 startStudentTimer 开始下一轮
*/ */
const startRest = (timer : TimerItem) => { const startRest = (timer : TimerItem) => {
const restTime = timer.restTime || 0 const restTime = timer.restTime || 0
if (restTime <= 0) {
Service.Msg('已记录')
return
}
clearStudentInterval(timer.studentId) clearStudentInterval(timer.studentId)
clearStudentRestInterval(timer.studentId) clearStudentRestInterval(timer.studentId)
timer.status = 'resting' timer.status = 'resting'
timer.isResting = true timer.isResting = true
timer.restCountdown = restTime timer.restCountdown = restTime
// 如果没有休息时间,直接开始下一轮
if (restTime <= 0) {
timer.countdownTime = timer.targetTime
startStudentTimer(timer)
return
}
const restStart = Date.now() const restStart = Date.now()
const id = setInterval(() => { const id = setInterval(() => {
const remaining = Math.max(0, restTime - (Date.now() - restStart) / 1000) const remaining = Math.max(0, restTime - (Date.now() - restStart) / 1000)
timer.restCountdown = remaining timer.restCountdown = remaining
if (remaining <= 0) { if (remaining <= 0) {
clearStudentRestInterval(timer.studentId) clearStudentRestInterval(timer.studentId)
// 休息结束,自动开始下一圈计时 // 休息结束,重置倒计时并开始下一
timer.countdownTime = timer.targetTime
startStudentTimer(timer) startStudentTimer(timer)
} }
}, 50) as unknown as number }, 50) as unknown as number
studentRestIntervals.set(timer.studentId, id) studentRestIntervals.set(timer.studentId, id)
Service.Msg(`已记录,休息${restTime}`) Service.Msg(`完成一圈,休息${restTime}`)
} }
/** /**
@@ -812,39 +882,45 @@
const completeTimer = (timer : TimerItem) => { const completeTimer = (timer : TimerItem) => {
clearStudentInterval(timer.studentId) clearStudentInterval(timer.studentId)
clearStudentRestInterval(timer.studentId) clearStudentRestInterval(timer.studentId)
stopElapsedTimeTimer(timer.studentId)
timer.status = 'completed' timer.status = 'completed'
timer.isResting = false timer.isResting = false
timer.restCountdown = 0 timer.restCountdown = 0
} }
/** /**
* 记录学生一圈 * 记录学生成绩(手动点击,不增加圈数)
* 触发时机:点击学生圆形卡片、或详情弹窗点击"记录" * 触发时机:点击学生圆形卡片、或详情弹窗点击"记录"
* *
* 逻辑: * 逻辑:
* 1. 校验:休息中和未运行时不允许记录 * 1. 校验:未运行/未休息时不能记录
* 2. 将当前 currentTime 推入 records * 2. 如果记录数已达上限,不能记录
* 3. lapCount + 1 * 3. 计算净游泳时间 = elapsedTime - restTime * 已记录数 - sum(所有已记录时间)
* 4. 如果达到总圈数,标记完成并提示 * 记录1 = elapsedTime
* 5. 否则进入休息倒计时 * 记录2 = elapsedTime - restTime - 记录1
* 记录3 = elapsedTime - restTime*2 - 记录1 - 记录2
* 以此类推
* 4. 当前圈继续运行,不进入下一轮
*/ */
const recordLapForTimer = (timer : TimerItem) => { const recordLapForTimer = (timer : TimerItem) => {
if (timer.status === 'resting') { if (timer.status !== 'running' && timer.status !== 'resting') {
Service.Msg('休息中,请等待倒计时结束') Service.Msg('请在运行或休息中记录')
return return
} }
if (timer.status !== 'running') { if (timer.records.length >= timer.totalLapCount) {
Service.Msg('请在运行中记录') Service.Msg('已达到最大记录')
return return
} }
timer.records.push({ time: timer.currentTime }) const sumRecords = timer.records.reduce((sum, r) => sum + r.time, 0)
timer.lapCount += 1 const usedTime = Math.max(0, timer.elapsedTime - (timer.restTime * timer.records.length) - sumRecords)
if (timer.lapCount >= timer.totalLapCount) { timer.records.push({ time: usedTime })
completeTimer(timer) // 判断记录数是否等于圈数,等于则暂停计时器
Service.Msg(`${timer.studentName} 已完成`) if (timer.records.length >= timer.totalLapCount) {
pauseStudentTimer(timer)
Service.Msg(`${timer.studentName} 已完成所有记录`)
return return
} }
startRest(timer) Service.Msg(`${timer.studentName} 已记录`)
} }
/** /**
@@ -854,12 +930,16 @@
const resetTimer = (timer : TimerItem) => { const resetTimer = (timer : TimerItem) => {
pauseStudentTimer(timer) pauseStudentTimer(timer)
timer.currentTime = 0 timer.currentTime = 0
timer.countdownTime = 0
timer.elapsedTime = 0
timer.status = 'idle' timer.status = 'idle'
timer.records = [] timer.records = []
timer.lapCount = 0 timer.lapCount = 0
timer.hasStarted = false timer.hasStarted = false
timer.isResting = false timer.isResting = false
timer.restCountdown = 0 timer.restCountdown = 0
timer.startTimestamp = 0
timer.lastRecordTimestamp = 0
} }
/** /**
@@ -909,7 +989,7 @@
* 5. 一旦 globalTimerTime >= 分组出发时间,且分组未出发过,则启动该分组内所有未开始的学生计时器 * 5. 一旦 globalTimerTime >= 分组出发时间,且分组未出发过,则启动该分组内所有未开始的学生计时器
*/ */
const startGlobalTimer = () => { const startGlobalTimer = () => {
stopGlobalTimer() // stopGlobalTimer()
globalTimerRunning.value = true globalTimerRunning.value = true
globalStartAt = Date.now() globalStartAt = Date.now()
globalBaseTime = globalTimerTime.value globalBaseTime = globalTimerTime.value
@@ -960,10 +1040,12 @@
* 将分组内所有正在运行或休息的学生计时器暂停 * 将分组内所有正在运行或休息的学生计时器暂停
*/ */
const pauseGroup = (group : Group) => { const pauseGroup = (group : Group) => {
group.hasStarted = false
group.timers.forEach(t => { group.timers.forEach(t => {
if (t.status === 'running' || t.status === 'resting') { if (t.status === 'running' || t.status === 'resting') {
pauseStudentTimer(t) pauseStudentTimer(t)
} }
t.hasStarted = false
}) })
Service.Msg(`${group.name} 已暂停`) Service.Msg(`${group.name} 已暂停`)
} }
@@ -994,10 +1076,12 @@
*/ */
const pauseAllTimers = () => { const pauseAllTimers = () => {
stopGlobalTimer() stopGlobalTimer()
groups.value.forEach(g => g.hasStarted = false)
getAllTimers.value.forEach(t => { getAllTimers.value.forEach(t => {
if (t.status === 'running' || t.status === 'resting') { if (t.status === 'running' || t.status === 'resting') {
pauseStudentTimer(t) pauseStudentTimer(t)
} }
t.hasStarted = false
}) })
Service.Msg('全部已暂停') Service.Msg('全部已暂停')
} }
@@ -1268,6 +1352,8 @@
* 分组内所有学生恢复为未分组状态,并停止他们的计时器 * 分组内所有学生恢复为未分组状态,并停止他们的计时器
*/ */
const deleteGroup = (group : Group) => { const deleteGroup = (group : Group) => {
console.log(group);
let index = selectedProject.value.group.findIndex((item : any) => { let index = selectedProject.value.group.findIndex((item : any) => {
return item.groupId === group.id return item.groupId === group.id
}) })
@@ -1429,8 +1515,6 @@
*/ */
const submitData = () => { const submitData = () => {
pauseAllTimers() pauseAllTimers()
console.log(getAllTimers.value); console.log(getAllTimers.value);
const hasData = getAllTimers.value.some(t => t.currentTime > 0) const hasData = getAllTimers.value.some(t => t.currentTime > 0)
if (!hasData) { Service.Msg('暂无数据可提交'); return } if (!hasData) { Service.Msg('暂无数据可提交'); return }
@@ -1462,6 +1546,9 @@
PlanService.AddPlanLog(selectedProject.value.planId,'包干项目','','',JSON.stringify(data)).then(res=>{ PlanService.AddPlanLog(selectedProject.value.planId,'包干项目','','',JSON.stringify(data)).then(res=>{
if(res.code==0){ if(res.code==0){
Service.Msg('提交成功!') Service.Msg('提交成功!')
setTimeout(()=>{
Service.GoPageBack()
},1000)
}else{ }else{
Service.Msg(res.msg) Service.Msg(res.msg)
} }
@@ -1470,11 +1557,21 @@
// ===================== 格式化函数 ===================== // ===================== 格式化函数 =====================
/** 学生卡片主计时显示:分:秒:毫秒 */ /** 学生卡片主计时显示(目标时间倒计时):分:秒:毫秒 */
const formatTime = (timer : TimerItem) : string => { const formatTime = (timer : TimerItem) : string => {
const mins = Math.floor(timer.currentTime / 60) const time = Math.max(0, timer.countdownTime || 0)
const secs = Math.floor(timer.currentTime % 60) const mins = Math.floor(time / 60)
const millis = Math.floor((timer.currentTime % 1) * 100) const secs = Math.floor(time % 60)
const millis = Math.floor((time % 1) * 100)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
}
/** 计时器显示(真实经过时间):分:秒:毫秒 */
const formatElapsedTime = (seconds : number) : string => {
const time = seconds || 0
const mins = Math.floor(time / 60)
const secs = Math.floor(time % 60)
const millis = Math.floor((time % 1) * 100)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}` return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
} }
@@ -1524,14 +1621,10 @@
/** /**
* 详情弹窗中每条记录的单圈时间 * 详情弹窗中每条记录的单圈时间
* 逻辑第1条 = 记录时刻第N条 = 记录时刻 - 上一条记录时刻 * records 中存储的已经是净游泳时间(已扣除休息时间),直接显示即可
*/ */
const formatAdjustedRecordTime = (seconds : number, index : number) : string => { const formatAdjustedRecordTime = (seconds : number, index : number) : string => {
let adjusted = seconds return formatFullTime(seconds)
if (index > 0 && selectedTimer.value) {
adjusted = seconds - selectedTimer.value.records[index - 1].time
}
return formatFullTime(adjusted)
} }
/** /**
@@ -1542,8 +1635,10 @@
stopGlobalTimer() stopGlobalTimer()
studentIntervals.forEach(id => clearInterval(id)) studentIntervals.forEach(id => clearInterval(id))
studentRestIntervals.forEach(id => clearInterval(id)) studentRestIntervals.forEach(id => clearInterval(id))
elapsedTimeIntervals.forEach(id => clearInterval(id))
studentIntervals.clear() studentIntervals.clear()
studentRestIntervals.clear() studentRestIntervals.clear()
elapsedTimeIntervals.clear()
}) })
</script> </script>
@@ -1813,7 +1908,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: relative; position: relative;
margin-bottom: 20rpx; // margin-bottom: 20rpx;
} }
.global-timer-label { .global-timer-label {
@@ -2085,7 +2180,7 @@
right: 0; right: 0;
background-color: #fff; background-color: #fff;
padding: 20rpx 30rpx; padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); // padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0; border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08); box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 100; z-index: 100;
@@ -2147,7 +2242,7 @@
} }
.timer-detail-popup { .timer-detail-popup {
padding: 50rpx 40rpx; padding: 0rpx 40rpx 50rpx;
padding-bottom: calc(50rpx + env(safe-area-inset-bottom)); padding-bottom: calc(50rpx + env(safe-area-inset-bottom));
min-height: 450rpx; min-height: 450rpx;
max-height: 75vh; max-height: 75vh;

View File

@@ -33,10 +33,15 @@
</view> </view>
</view> </view>
<view class="project-stats"> <view class="project-stats">
<view class="stat-badge"> <view v-if="project.planType=='混氧项目'" class="stat-badge">
<u-icon name="account" size="14" color="#1890ff"></u-icon>
<text class="badge-text">{{ JSON.parse(project.project).length }}个计划</text>
</view>
<view v-else class="stat-badge">
<u-icon name="account" size="14" color="#1890ff"></u-icon> <u-icon name="account" size="14" color="#1890ff"></u-icon>
<text class="badge-text">{{ project.users.length }}位学员</text> <text class="badge-text">{{ project.users.length }}位学员</text>
</view> </view>
</view> </view>
</view> </view>
@@ -90,7 +95,14 @@
// 查看项目详情 // 查看项目详情
const viewProjectDetail = (project : any) => { const viewProjectDetail = (project : any) => {
Service.Msg(`查看「${project.name}」详情`)
if(project.planType=='分段项目'){
Service.GoPage('/pages/userFunc/segmentation?id=' + project.planId)
}else if(project.planType=='计时项目'){
Service.GoPage('/pages/userFunc/swiming?id=' + project.planId)
}else{
Service.GoPage('/pages/userFunc/hunyang?id=' + project.planId)
}
} }
// 删除项目 // 删除项目

View File

@@ -5,13 +5,14 @@
<view class="config-header"> <view class="config-header">
<text class="config-title">分段设置</text> <text class="config-title">分段设置</text>
<u-icon @click="Service.GoPage('/pages/userFunc/setCourse?id='+planId+'&type=2')" name="setting" size="24" <u-icon @click="Service.GoPage('/pages/userFunc/setCourse?id='+planId+'&type=2')" name="setting"
color="#1890ff"></u-icon> size="24" color="#1890ff"></u-icon>
</view> </view>
<view class="config-info"> <view class="config-info">
<text class="info-text">总距离: {{ totalDistance }} ({{ segmentCount }} × {{ segmentDistance }})</text> <view class="info-text">总距离: {{ totalDistance }} ({{ segmentCount }} × {{ segmentDistance }}) {{ startMode==0?'一起出发':'间隔出发' }} {{ startMode==0?'':' · '+intervalTime+'s' }} </view>
<view class="info-text" style="margin-top: 14rpx;" >启动模式: {{ startMode==0?'一起出发':'间隔出发' }} {{ startMode==0?'':' · '+intervalTime+'s' }} </view>
</view> </view>
</view> </view>
@@ -23,7 +24,7 @@
</view> </view>
<!-- 学生列表 --> <!-- 学生列表 -->
<view class="students-section"> <view class="students-section" style="margin-top: 20rpx;" >
<view class="section-header"> <view class="section-header">
<text class="section-title">学生列表</text> <text class="section-title">学生列表</text>
<text class="student-count">{{ students.length }}位学生</text> <text class="student-count">{{ students.length }}位学生</text>
@@ -174,7 +175,7 @@
}) })
onShow(()=>{ onShow(() => {
getPlanInfo() getPlanInfo()
}) })
@@ -183,17 +184,17 @@
PlanService.GetPlanInfo(planId.value).then(res => { PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) { if (res.code == 0) {
// planName.value = res.data.plan.name // planName.value = res.data.plan.name
segmentDistance.value=res.data.plan.subsectionDistance/res.data.plan.subsectionInt segmentDistance.value = res.data.plan.subsectionDistance / res.data.plan.subsectionInt
segmentCount.value=res.data.plan.subsectionInt segmentCount.value = res.data.plan.subsectionInt
// 将计划数据转换为选手数据 // 将计划数据转换为选手数据
// athletes.value = res.data.plan.users. // athletes.value = res.data.plan.users.
students.value=res.data.plan.users.map((item:any,index:any)=>{ students.value = res.data.plan.users.map((item : any, index : any) => {
return { return {
id: item.studentId, id: item.studentId,
number: index+1, number: index + 1,
name: item.name, name: item.name,
segments: [], segments: [],
hasStarted: res.data.plan.departType == '间隔出发'?false:true, hasStarted: res.data.plan.departType == '间隔出发' ? false : true,
startTime: 0 startTime: 0
} }
}) })
@@ -230,10 +231,12 @@
return result return result
} }
// 格式化时间差 // 格式化时间差(详情弹窗用)
// 第一段显示累计时间,后续段显示分段用时
const formatTimeDiff = (index : number, currentTime : number) : string => { const formatTimeDiff = (index : number, currentTime : number) : string => {
if (index === 0) { if (index === 0) {
return '00:00:00' // 第一段显示累计时间
return formatTime(currentTime)
} }
if (!currentStudent.value || !currentStudent.value.segments[index - 1]) { if (!currentStudent.value || !currentStudent.value.segments[index - 1]) {
return '00:00:00' return '00:00:00'
@@ -243,12 +246,19 @@
return formatTime(diff) return formatTime(diff)
} }
// 获取学生最后一次记录的累计时间 // 获取学生最后一次记录的分段用时
// 第一段返回累计时间,后续段返回当前段用时(当前累计 - 上一次累计)
const getLastSegmentTime = (student : any) : number => { const getLastSegmentTime = (student : any) : number => {
if (!student.segments || student.segments.length === 0) { if (!student.segments || student.segments.length === 0) {
return 0 return 0
} }
return student.segments[student.segments.length - 1].time const lastIndex = student.segments.length - 1
if (lastIndex === 0) {
// 第一段,返回累计时间
return student.segments[0].time
}
// 后续段,返回分段用时
return student.segments[lastIndex].time - student.segments[lastIndex - 1].time
} }
// 简化时间格式化(分:秒) // 简化时间格式化(分:秒)
@@ -374,8 +384,8 @@
currentTime.value = 0 currentTime.value = 0
students.value.forEach(student => { students.value.forEach(student => {
student.segments = [] student.segments = []
student.hasStarted = startMode.value == 1?false:true, student.hasStarted = startMode.value == 1 ? false : true,
student.startTime = 0 student.startTime = 0
}) })
Service.Msg('已全部重置') Service.Msg('已全部重置')
} }
@@ -384,15 +394,17 @@
const saveData = () => { const saveData = () => {
// 检查是否有记录的数据 // 检查是否有记录的数据
const hasData = students.value.some(s => s.segments.length > 0) const hasData = students.value.some(s => s.segments.length > 0)
let isSave = students.value.every(s => s.segments.length >= segmentCount.value)
if (!hasData) { if (!hasData) {
Service.Msg('暂无数据可保存') Service.Msg('暂无数据可保存!')
return return
} }
console.log(students.value); if (!isSave) {
Service.Msg('存在学生未完成!')
return
}
// 这里可以添加保存到后端或本地存储的逻辑
Service.Msg('保存成功', 'success')
let data = [ let data = [
{ {
@@ -405,7 +417,7 @@
students.value.map((item : any) => { students.value.map((item : any) => {
let record = item.segments.map((content : any, index : any) => { let record = item.segments.map((content : any, index : any) => {
return { return {
circle: (index + 1)*segmentDistance.value, circle: (index + 1) * segmentDistance.value,
time: content.time time: content.time
} }
}) })
@@ -417,10 +429,15 @@
}) })
}) })
PlanService.AddPlanLog(planId.value,'分段项目','',JSON.stringify(data),'').then(res=>{ console.log(data);
if(res.code==0){
PlanService.AddPlanLog(planId.value, '分段项目', '', JSON.stringify(data), '').then(res => {
if (res.code == 0) {
Service.Msg('提交成功!') Service.Msg('提交成功!')
}else{ setTimeout(()=>{
Service.GoPageBack()
},1000)
} else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }
}) })
@@ -495,15 +512,14 @@
/* 总计时器区域 */ /* 总计时器区域 */
.total-time-section { .total-time-section {
margin-top: 20rpx;
margin-bottom: 30rpx;
}
.timer-bar {
background-color: #fff; background-color: #fff;
border-radius: 20rpx; border-radius: 20rpx;
padding: 30rpx; padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.timer-bar {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;

View File

@@ -7,7 +7,7 @@
<view class="form-title">项目信息</view> <view class="form-title">项目信息</view>
<view class="form-group"> <view class="form-group">
<text class="form-label">项目名称</text> <text class="form-label">项目名称</text>
<input class="form-input" v-model="courseData.projectName" placeholder="请输入项目名称" <input class="form-input" :disabled="planId" v-model="courseData.projectName" placeholder="请输入项目名称"
placeholder-class="input-placeholder" /> placeholder-class="input-placeholder" />
</view> </view>
</view> </view>
@@ -115,11 +115,9 @@
@click="toggleStudentSelect(student.studentId)"> @click="toggleStudentSelect(student.studentId)">
<view class="student-checkbox" <view class="student-checkbox"
:class="{ checked: selectedStudentIds.includes(student.studentId) }"> :class="{ checked: selectedStudentIds.includes(student.studentId) }">
<u-icon v-if="selectedStudentIds.includes(student.studentId)" name="checkmark" size="14" <text v-if="selectedStudentIds.includes(student.studentId)" class="checkbox-index">
color="#fff"></u-icon> {{ selectedStudentIds.indexOf(student.studentId) + 1 }}
</view> </text>
<view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view> </view>
<view class="student-info"> <view class="student-info">
<view class="student-name">{{ student.name }}</view> <view class="student-name">{{ student.name }}</view>
@@ -127,21 +125,7 @@
</view> </view>
</view> </view>
<!-- 已选学生预览 -->
<view v-if="selectedStudents.length > 0" class="selected-preview">
<view class="preview-header">
<text class="preview-title">已选学生</text>
</view>
<view class="preview-list">
<view v-for="(student, index) in selectedStudents" :key="student.studentId" class="preview-item">
<text class="preview-index">{{ index + 1 }}</text>
<text class="preview-name">{{ student.name }}</text>
<view class="preview-remove" @click.stop="removeSelectedStudent(student.studentId)">
<u-icon name="close" size="14" color="#ff4d4f"></u-icon>
</view>
</view>
</view>
</view>
</view> </view>
</view> </view>
<view class="" style="width: 100%; height: 80rpx;"> <view class="" style="width: 100%; height: 80rpx;">
@@ -326,10 +310,20 @@
PlanService.AddPlan(data).then(res=>{ PlanService.AddPlan(data).then(res=>{
if(res.code==0){ if(res.code==0){
Service.Msg('添加成功!') Service.Msg( planId.value?'修改成功!': '添加成功!')
setTimeout(()=>{ if(planId.value){
Service.GoPageBack() Service.GoPageBack()
},1000) return
}
if(type.value=='1'){
Service.GoPageDelse('/pages/userFunc/swiming?id=' + res.data.planId)
}else{
Service.GoPageDelse('/pages/userFunc/segmentation?id=' + res.data.planId)
}
}else{ }else{
Service.Msg(res.msg) Service.Msg(res.msg)
} }
@@ -697,14 +691,15 @@
/* 学生列表 */ /* 学生列表 */
.student-list { .student-list {
display: flex; display: flex;
flex-direction: column; flex-wrap: wrap;
gap: 12rpx; gap: 12rpx;
.student-item { .student-item {
width: calc((100% - 24rpx) / 3);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16rpx; gap: 8rpx;
padding: 20rpx; padding: 16rpx 8rpx;
background-color: #f5f5f5; background-color: #f5f5f5;
border-radius: 16rpx; border-radius: 16rpx;
border: 2rpx solid transparent; border: 2rpx solid transparent;
@@ -720,8 +715,8 @@
} }
.student-checkbox { .student-checkbox {
width: 40rpx; width: 36rpx;
height: 40rpx; height: 36rpx;
border-radius: 50%; border-radius: 50%;
border: 2rpx solid #d9d9d9; border: 2rpx solid #d9d9d9;
display: flex; display: flex;
@@ -734,6 +729,13 @@
border-color: #1890ff; border-color: #1890ff;
background-color: #1890ff; background-color: #1890ff;
} }
.checkbox-index {
font-size: 22rpx;
color: #fff;
font-weight: 600;
line-height: 1;
}
} }
.student-avatar { .student-avatar {
@@ -754,14 +756,11 @@
} }
.student-info { .student-info {
flex: 1; width: 100%;
min-width: 0;
.student-name { .student-name {
font-size: 28rpx; font-size: 26rpx;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 6rpx;
display: block; display: block;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -771,6 +770,7 @@
.student-meta { .student-meta {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center;
gap: 12rpx; gap: 12rpx;
.gender-badge { .gender-badge {

View File

@@ -264,19 +264,19 @@
// 构造接口请求数据 // 构造接口请求数据
const requestData = { const requestData = {
studentId:formData.value.id, studentId:formData.value.id?formData.value.id:'' ,
name: formData.value.name, name: formData.value.name,
sex: formData.value.gender, sex: formData.value.gender,
birthday: formData.value.birthDate, birthday: formData.value.birthDate,
school: formData.value.school , school: formData.value.school?formData.value.school:'' ,
address: formData.value.address address: formData.value.address ?formData.value.address:''
} }
console.log(requestData); console.log(requestData);
// 调用添加学员接口 // 调用添加学员接口
studentService.Add(requestData).then((content) => { studentService.Add(requestData).then((content) => {
if (content.code == 0) { if (content.code == 0) {
Service.Msg('添加成功') Service.Msg(formData.value.id?'修改成功!':'添加成功!')
getData() getData()
} else { } else {
Service.Msg(content.msg) Service.Msg(content.msg)

View File

@@ -18,7 +18,7 @@
<!-- 选手列表 --> <!-- 选手列表 -->
<view class="athletes-section"> <view class="athletes-section">
<view class="section-header"> <view class="section-header">
<text class="section-title">间隔出发·{{ groupedAthletes.length }}</text> <text class="section-title">{{ stopwatchMode=='interval'?'间隔触发':'一起出发' }}·{{ groupedAthletes.length }}</text>
<text class="athlete-count">{{ athletes.length }}位选手</text> <text class="athlete-count">{{ athletes.length }}位选手</text>
</view> </view>
@@ -44,7 +44,7 @@
<text <text
class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text> class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text>
<text class="best-time">最快: <text class="best-time">最快:
{{ athlete.bestTime !== null ? formatTime(athlete.quicklyTime) : '无' }}</text> {{ athlete.bestTime !== null ? athlete.quicklyTime: '无' }}</text>
</view> </view>
<view class="" style="display: flex;align-items: center; gap: 10rpx;"> <view class="" style="display: flex;align-items: center; gap: 10rpx;">
@@ -65,7 +65,7 @@
</view> </view>
<!-- 记录提示弹窗 --> <!-- 记录提示弹窗 -->
<u-popup :show="showRecord" mode="center" :round="20" :closeable="true" @close="showRecord=false" closeOnClickOverlay> <u-popup :show="showRecord" mode="center" :round="20" :closeable="true" :safeAreaInsetBottom='false' @close="showRecord=false" closeOnClickOverlay>
<view class="record-notice-modal"> <view class="record-notice-modal">
<view class="notice-header"> <view class="notice-header">
<text class="notice-title">恭喜以下学生打破记录</text> <text class="notice-title">恭喜以下学生打破记录</text>
@@ -80,7 +80,7 @@
<view v-for="(item,index) in record" class="table-row"> <view v-for="(item,index) in record" class="table-row">
<text class="row-cell index-cell">{{ index+1 }}</text> <text class="row-cell index-cell">{{ index+1 }}</text>
<text class="row-cell name-cell">{{ item.name }}</text> <text class="row-cell name-cell">{{ item.name }}</text>
<text class="row-cell time-cell">{{ formatTime(item.quicklyTime) }}</text> <text class="row-cell time-cell">{{ item.quicklyTime }}</text>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@@ -89,7 +89,7 @@
<!-- 底部控制按钮 --> <!-- 底部控制按钮 -->
<view class="control-buttons"> <view class="control-buttons">
<view class="button-group"> <view class="button-group" :class="{ 'record-group': isRunning }">
<button v-if="!isRunning" class="start-btn" @click="startTimer"> <button v-if="!isRunning" class="start-btn" @click="startTimer">
<u-icon name="play-right" size="24" color="#fff"></u-icon> <u-icon name="play-right" size="24" color="#fff"></u-icon>
<text class="btn-text">开始</text> <text class="btn-text">开始</text>
@@ -100,14 +100,13 @@
<text class="btn-text">记录</text> <text class="btn-text">记录</text>
</button> </button>
</view> </view>
<view class="button-group"> <view class="button-group" :class="{ 'pause-group': isRunning }">
<button v-if="!isRunning" class="reset-all-btn" @click="resetAll"> <button v-if="!isRunning" class="reset-all-btn" @click="resetAll">
<u-icon name="reload" size="24" color="#fff"></u-icon> <u-icon name="reload" size="24" color="#fff"></u-icon>
<text class="btn-text">重置</text> <text class="btn-text">重置</text>
</button> </button>
<button v-if="isRunning" class="reset-all-btn" @click="stopTimer"> <button v-if="isRunning" class="reset-all-btn" @click="stopTimer">
<u-icon name="pause" size="24" color="#fff"></u-icon> <text class="btn-text" style="font-size: 28rpx;">暂停</text>
<text class="btn-text">暂停</text>
</button> </button>
</view> </view>
@@ -265,7 +264,7 @@
athlete.time = Math.max(0, elapsed - offset) athlete.time = Math.max(0, elapsed - offset)
} }
}) })
}, 10) }, 16)
} }
// 停止计时 // 停止计时
@@ -394,12 +393,18 @@
// 提交数据 // 提交数据
const submitData = () => { const submitData = () => {
stopTimer()
const hasData = athletes.value.some(a => a.time > 0) const hasData = athletes.value.some(a => a.time > 0)
const allFinished = athletes.value.every(a => a.finished)
if (!hasData) { if (!hasData) {
Service.Msg('暂无数据可提交') Service.Msg('暂无数据可提交')
return return
} }
// if(!allFinished){
// Service.Msg('还用学生未完成!')
// return
// }
stopTimer()
let data = [{ let data = [{
planName: "", planName: "",
studentId: "", studentId: "",
@@ -419,6 +424,15 @@
PlanService.AddPlanLog(planId.value,'计时项目',JSON.stringify(data),'','','').then(res=>{ PlanService.AddPlanLog(planId.value,'计时项目',JSON.stringify(data),'','','').then(res=>{
if(res.code==0){ if(res.code==0){
Service.Msg('提交成功!') Service.Msg('提交成功!')
stopTimer()
currentTime.value = 0
currentAthleteIndex.value = 0
athletes.value.forEach(athlete => {
athlete.time = 0
athlete.finished = false
athlete.startOffset = undefined
})
if(res.data.record.length>0){ if(res.data.record.length>0){
record.value=res.data.record record.value=res.data.record
@@ -558,7 +572,7 @@
background-color: #ffffff; background-color: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03); box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03);
transition: all 0.3s; transition: background-color 0.3s, border-color 0.3s;
border: 2rpx solid transparent; border: 2rpx solid transparent;
&.finished { &.finished {
@@ -716,6 +730,14 @@
justify-content: center; justify-content: center;
} }
.record-group {
flex: 4;
}
.pause-group {
flex: 1;
}
.back-btn, .back-btn,
.start-btn, .start-btn,
.pause-btn, .pause-btn,