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