This commit is contained in:
Ls
2026-05-26 08:41:00 +08:00
parent 11486220aa
commit d1eedfdc5b
18 changed files with 7861 additions and 1475 deletions

View File

@@ -1,8 +1,7 @@
export class BaseConfig { export class BaseConfig {
// protected static servesUrl: string = "http://192.168.0.142:5298"; // protected static servesUrl: string = "http://192.168.0.142:5300";
// protected static imgUrl: string = "http://192.168.0.142:5298"; // protected static imgUrl: string = "http://192.168.0.142:5300";
// protected static mediaUrl: string = "http://192.168.0.142:5298/"; // protected static mediaUrl: string = "http://192.168.0.142:5300/";
protected static servesUrl: string = "https://swimming.api.xypays.cn"; protected static servesUrl: string = "https://swimming.api.xypays.cn";
protected static imgUrl: string = "https://swimming.api.xypays.cn"; protected static imgUrl: string = "https://swimming.api.xypays.cn";

View File

@@ -76,8 +76,9 @@
{ {
"path": "project", "path": "project",
"style": { "style": {
"navigationBarTitleText": "包干" "navigationBarTitleText": "包干",
"pageOrientation": "auto",
"navigationStyle": "custom"
} }
}, },
{ {
@@ -89,7 +90,9 @@
{ {
"path": "hunyang", "path": "hunyang",
"style": { "style": {
"navigationBarTitleText": "混氧" "navigationBarTitleText": "混氧",
"pageOrientation": "auto",
"navigationStyle": "custom"
} }
}, },
{ {
@@ -97,12 +100,18 @@
"style": { "style": {
"navigationBarTitleText": "创建项目" "navigationBarTitleText": "创建项目"
} }
},
{
"path": "cs",
"style": {
"navigationBarTitleText": "测试",
"pageOrientation": "auto"
}
} }
] ]
}, { }, {
"root": "pages/dataAnalyze", "root": "pages/dataAnalyze",
"pages": [ "pages": [{
{
"path": "baoganAnalyze", "path": "baoganAnalyze",
"style": { "style": {
"navigationBarTitleText": "包干数据" "navigationBarTitleText": "包干数据"

View File

@@ -11,7 +11,31 @@
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon> <u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view> </view>
</view> </view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker> <!-- 项目选择弹窗 -->
<up-popup :show="showProject" mode="center" round="32" :safeAreaInsetBottom="false"
@close="showProject = false">
<view class="project-popup-content">
<view class="modal-header">
<text class="modal-title">选择项目</text>
<view class="modal-close" @click="showProject = false">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<scroll-view scroll-y class="popup-project-list">
<view class="project-list-inner">
<view v-for="(project, index) in projectOptions[0]" :key="project.planId" class="project-item"
@click="selectProcetFunc(project)">
<view class="item-info">
<text class="item-name">{{ project.name }}</text>
</view>
</view>
<view v-if="!projectOptions[0] || projectOptions[0].length === 0" class="empty-project">
<text class="empty-text">暂无项目</text>
</view>
</view>
</scroll-view>
</view>
</up-popup>
<!-- 日期选择卡片 --> <!-- 日期选择卡片 -->
<view class="filter-card " v-if="selectProcet"> <view class="filter-card " v-if="selectProcet">
@@ -46,9 +70,6 @@
<view v-for="student in projectStudents" :key="student.studentId" <view v-for="student in projectStudents" :key="student.studentId"
:class="['student-item', { selected: selectedStudentIds.includes(student.studentId) }]" :class="['student-item', { selected: selectedStudentIds.includes(student.studentId) }]"
@click="toggleStudent(student.studentId)"> @click="toggleStudent(student.studentId)">
<view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
<text class="student-name">{{ student.name }}</text> <text class="student-name">{{ student.name }}</text>
<view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)"> <view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)">
<u-icon name="checkmark" size="14" color="#fff"></u-icon> <u-icon name="checkmark" size="14" color="#fff"></u-icon>
@@ -58,7 +79,7 @@
</view> </view>
<!-- 图表区域 --> <!-- 图表区域 -->
<view class="chart-section" v-if="selectedStudentIds.length > 0"> <view class="chart-section" v-if="selectedStudentIds.length > 0 && !showCalendar">
<!-- 统计概览 --> <!-- 统计概览 -->
<view class="stats-card"> <view class="stats-card">
<view class="stat-item"> <view class="stat-item">
@@ -87,7 +108,7 @@
</view> </view>
<!-- 日历弹窗 --> <!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar> <up-calendar :show="showCalendar" mode="date" minDate="1776240407000" zIndex="1000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar>
</view> </view>
</template> </template>
@@ -96,6 +117,7 @@
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService' import { PlanService } from '@/Service/swimming/PlanService'
import { studentService } from '@/Service/swimming/studentService'
const MAX_SELECTED_STUDENTS = 3 const MAX_SELECTED_STUDENTS = 3
const chartColors = ['#52c41a', '#1890ff', '#faad14'] const chartColors = ['#52c41a', '#1890ff', '#faad14']
@@ -191,9 +213,10 @@
}) })
} }
const selectProcetFunc = (e: any) => { const selectProcetFunc = (project: any) => {
selectProcet.value = e.value[0].name selectProcet.value = project.name
selectId.value = e.value[0].planId selectId.value = project.planId
showProject.value = false
selectedStudentIds.value = [] selectedStudentIds.value = []
begin.value = '' begin.value = ''
end.value = '' end.value = ''
@@ -218,13 +241,14 @@
} }
const loadProjectStudents = (projectId: string) => { const loadProjectStudents = (projectId: string) => {
PlanService.GetPlanInfo(projectId).then(res => { studentService.GetStudentList().then(res => {
if (res.code == 0) { if (res.code == 0) {
projectStudents.value = res.data.plan.users projectStudents.value = res.data
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }
}) })
} }
const toggleStudent = (studentId: string) => { const toggleStudent = (studentId: string) => {
@@ -262,6 +286,7 @@
res.data.list.map((item: any) => { res.data.list.map((item: any) => {
lineChartData.value.categories.push(item.dayTime) lineChartData.value.categories.push(item.dayTime)
item.data.map((content: any, index: any) => { item.data.map((content: any, index: any) => {
xData.value[index].name = content.studentName xData.value[index].name = content.studentName
xData.value[index].data.push(content.time) xData.value[index].data.push(content.time)
}) })
@@ -469,6 +494,8 @@
/* 图表区域 */ /* 图表区域 */
.chart-section { .chart-section {
margin-top: 4rpx; margin-top: 4rpx;
position: relative;
z-index: 1;
} }
.stats-card { .stats-card {
@@ -549,4 +576,117 @@
} }
} }
} }
/* 项目选择弹窗 */
.project-popup-content {
width: 680rpx;
max-height: 70vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.25), 0 0 0 1rpx rgba(0, 0, 0, 0.04);
}
.modal-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);
.modal-title {
font-size: 36rpx;
font-weight: 700;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.modal-close {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
&:active {
background: linear-gradient(135deg, #e8e8e8 0%, #e0e0e0 100%);
transform: scale(0.92);
}
}
}
.popup-project-list {
max-height: 55vh;
padding: 24rpx 28rpx;
}
.project-list-inner {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.project-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
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;
border-radius: 16rpx;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4rpx;
height: 100%;
background: linear-gradient(180deg, #1890ff 0%, #40a9ff 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active::after {
opacity: 1;
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
}
}
.empty-project {
display: flex;
align-items: center;
justify-content: center;
padding: 60rpx 0;
.empty-text {
font-size: 28rpx;
color: #999;
}
}
</style> </style>

View File

@@ -1,5 +1,5 @@
<template> <template>
<view class="baogan-container"> <view class="timing-container">
<!-- 顶部筛选卡片 --> <!-- 顶部筛选卡片 -->
<view class="filter-card"> <view class="filter-card">
<view class="card-title"> <view class="card-title">
@@ -7,15 +7,16 @@
<text>筛选条件</text> <text>筛选条件</text>
</view> </view>
<view class="filter-item" @click="showProject = true"> <view class="filter-item" @click="showProjectPopup = true">
<text class="filter-label">项目</text> <text class="filter-label">项目</text>
<view class="filter-value"> <view class="filter-value">
<text class="value-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text> <text class="value-text"
:class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
<u-icon name="arrow-right" size="14" color="#bbb"></u-icon> <u-icon name="arrow-right" size="14" color="#bbb"></u-icon>
</view> </view>
</view> </view>
<view v-if="selectProcet" class="filter-item" @click="openCalendar"> <view class="filter-item" @click="openCalendar">
<text class="filter-label">日期</text> <text class="filter-label">日期</text>
<view class="filter-value"> <view class="filter-value">
<text class="value-text" :class="{ placeholder: !begin }"> <text class="value-text" :class="{ placeholder: !begin }">
@@ -25,7 +26,7 @@
</view> </view>
</view> </view>
<view v-if="selectProcet && begin" class="filter-item" @click="showStudentPicker = true"> <view class="filter-item" @click="showStudentPicker = true">
<text class="filter-label">学生</text> <text class="filter-label">学生</text>
<view class="filter-value"> <view class="filter-value">
<text class="value-text" :class="{ placeholder: selectedStudentIndexes.length === 0 }"> <text class="value-text" :class="{ placeholder: selectedStudentIndexes.length === 0 }">
@@ -36,40 +37,106 @@
</view> </view>
</view> </view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker> <!-- 项目选择弹窗 -->
<up-popup :show="showProjectPopup" mode="center" round="32" :safeAreaInsetBottom="false"
@close="showProjectPopup = false">
<view class="project-popup-content">
<view class="modal-header">
<text class="modal-title">选择项目</text>
<view class="modal-close" @click="showProjectPopup = false">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<scroll-view scroll-y class="popup-project-list">
<view class="project-list-inner">
<view v-for="(project, index) in projectOptions[0]" :key="project.planId" class="project-item"
@click="selectProject(project)">
<view class="item-info">
<text class="item-name">{{ project.name }}</text>
</view>
</view>
<view v-if="!projectOptions[0] || projectOptions[0].length === 0" class="empty-project">
<text class="empty-text">暂无项目</text>
</view>
</view>
</scroll-view>
</view>
</up-popup>
<!-- 数据表格 --> <!-- 数据表格 -->
<view v-if="tableData.length > 0" class="data-card"> <view class="data-card">
<view class="card-title"> <view class="card-title">
<u-icon name="list" size="18" color="#1890ff"></u-icon> <u-icon name="list" size="18" color="#1890ff"></u-icon>
<text>训练记录</text> <text>训练记录</text>
</view> </view>
<view class="table-wrap"> <view v-for="(item,index) in tableData" :key="index">
<next-table :show-header="true" :columns="columns" :stripe="true" @cellClick="cellClick" :fit="false" :show-summary="false" :data="tableData" :show-paging="true" :pageIndex="page" @pageChange="pageChange" :pageTotal="pageTotal"></next-table> <view class="day-title" @click="toggleDayCollapse(index)">
<text>{{ item.dayTime }} · {{ item.planName }} </text>
<u-icon name="arrow-down" size="16" color="#666"
:class="{ 'arrow-rotate': collapsedDays[index] }"></u-icon>
</view>
<view class="table-wrap" v-if="collapsedDays[index]">
<next-table :show-header="true" @cellClick="cellClick" :columns="columns" :stripe="true"
:fit="false" :show-summary="false" :data="item.data"></next-table>
</view>
</view> </view>
</view> </view>
<!-- 空状态 --> <!-- 空状态 -->
<view v-else-if="selectProcet && begin && selectedStudentIndexes.length > 0" class="empty-card"> <view v-if=" tableData.length == 0" class="empty-card">
<u-icon name="info-circle" size="64" color="#d9d9d9"></u-icon> <u-icon name="info-circle" size="64" color="#d9d9d9"></u-icon>
<text class="empty-text">暂无训练记录</text> <text class="empty-text"> {{ isLoading?'暂无训练记录':'加载中' }} </text>
</view> </view>
<!-- 提示状态 -->
<view v-else-if="selectProcet" class="empty-card">
<u-icon name="search" 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> <up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm"
@close="calendarClose"></up-calendar>
<!-- 圈数达标时间展示弹窗 --> <!-- 学生选择弹窗 -->
<up-popup :show="showLapsModal" @close="closeLapsModal" mode="center" round="16" bgColor="#fff"> <view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
<view class="student-picker-modal" v-if="showStudentPicker">
<view class="picker-header">
<view class="header-left">
<text class="header-title">选择学生</text>
<text class="student-count">已选({{ selectedStudentIndexes.length }}/{{ studentList.length }})</text>
</view>
<view class="header-actions">
<view class="select-all-btn" @click="toggleSelectAll">
<view class="checkbox-icon" :class="{ checked: allSelected }">
<u-icon v-if="allSelected" name="checkmark" size="14" color="#fff"></u-icon>
</view>
<text class="select-all-text">{{ allSelected ? '取消全选' : '全选' }}</text>
</view>
<text class="action-btn primary" @click="confirmStudentSelection">确定</text>
</view>
</view>
<scroll-view class="student-list" scroll-y>
<view class="student-grid">
<view v-for="(student, index) in studentList" :key="student.studentId" class="student-item"
:class="{ checked: selectedStudentIndexes.includes(index) }"
@click="toggleStudentSelection(index)">
<view class="student-checkbox" :class="{ checked: selectedStudentIndexes.includes(index) }">
<text v-if="selectedStudentIndexes.includes(index)" class="checkbox-index">
{{ selectedStudentIndexes.indexOf(index) + 1 }}
</text>
</view>
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 记录详情弹窗 -->
<up-popup :show="showRecordPopup" @close="closeRecordPopup" mode="center" round="16" bgColor="#fff">
<view class="laps-popup-content"> <view class="laps-popup-content">
<view class="laps-popup-header"> <view class="laps-popup-header">
<text class="laps-popup-title">圈数达标详情</text> <text
<u-icon name="close" size="22" color="#999" @click="closeLapsModal"></u-icon> class="laps-popup-title">{{ row.studentName + ' · '+row.planName + ' · ' +row.subsection }}</text>
<u-icon name="close" size="22" color="#999" @click="closeRecordPopup"></u-icon>
</view> </view>
<scroll-view class="laps-popup-body" scroll-y> <scroll-view class="laps-popup-body" scroll-y>
<view class="laps-table"> <view class="laps-table">
@@ -77,12 +144,8 @@
<text class="laps-th">圈数</text> <text class="laps-th">圈数</text>
<text class="laps-th">达标时间</text> <text class="laps-th">达标时间</text>
</view> </view>
<view <view v-for="(item, index) in rowData.split('|')" :key="index" class="laps-table-row"
v-for="(item, index) in rowData.split('|')" :class="{ 'laps-row-odd': index % 2 === 1 }">
:key="index"
class="laps-table-row"
:class="{ 'laps-row-odd': index % 2 === 1 }"
>
<text class="laps-td"> {{ index+1 }} </text> <text class="laps-td"> {{ index+1 }} </text>
<text class="laps-td">{{ item }}</text> <text class="laps-td">{{ item }}</text>
</view> </view>
@@ -93,31 +156,6 @@
</scroll-view> </scroll-view>
</view> </view>
</up-popup> </up-popup>
<!-- 学生选择弹窗 -->
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
<view class="student-picker-modal" v-if="showStudentPicker">
<view class="picker-header">
<text class="header-title">选择学生</text>
<view class="header-actions">
<text class="action-btn" @click="selectAllStudents">全选</text>
<text class="action-btn primary" @click="confirmStudentSelection">确定</text>
</view>
</view>
<scroll-view class="student-list" scroll-y>
<view v-for="(student, index) in studentList" :key="student.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="12" color="#fff"></u-icon>
</view>
</view>
<view class="item-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
<text class="item-name">{{ student.name }}</text>
</view>
</scroll-view>
</view>
</view> </view>
</template> </template>
@@ -126,36 +164,29 @@
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService' import { PlanService } from '@/Service/swimming/PlanService'
import { studentService } from '@/Service/swimming/studentService'
interface Project {
id: string
name: string
}
interface Student { interface Student {
id: string studentId : string
name: string name : string
gender: string gender : string
age: number age : number
} }
interface TableDataItem { interface TableDataItem {
date: string date : string
name: string name : string
projectName: string projectName : string
plan: string plan : string
completion: number completion : number
detail: string detail : string
} }
interface StudentRecord { // 数据加载
student: Student let isLoading = ref(false)
date: string
segments: TableDataItem[]
}
// 项目选择 // 项目选择
let showProject = ref(false) let showProjectPopup = ref(false)
let selectProcet = ref('') let selectProcet = ref('')
let selectId = ref('') let selectId = ref('')
@@ -169,40 +200,30 @@
let pageTotal = ref(10) let pageTotal = ref(10)
const columns = ref([ const columns = ref([
{ label: '训练日期', name: 'addTime' },
{ label: '姓名', name: 'studentName' }, { label: '姓名', name: 'studentName' },
{ label: '项目名称', name: 'name' }, { label: '训练数据', name: 'subsection' },
{ label: '项目类型', name: 'planType' },
{ label: '完成比例', name: 'completion' }, { label: '完成比例', name: 'completion' },
{ label: '详细数据', name: 'detail' } { label: '详细数据', name: 'detail' }
]) ])
let rowData=ref('')
const tableData = ref<TableDataItem[]>([]) let row = ref<any>({})
let rowData = ref('')
const tableData = ref<any[]>([])
const projectOptions = ref<Array<any>>([[]]) const projectOptions = ref<Array<any>>([[]])
const studentList = ref<Student[]>([]) const studentList = ref<Student[]>([])
const selectedStudentIndexes = ref<number[]>([]) const selectedStudentIndexes = ref<number[]>([])
const selectedStudentIds = ref<string[]>([]) const selectedStudentIds = ref<string[]>([])
const showStudentPicker = ref(false) const showStudentPicker = ref(false)
// 圈数达标时间弹窗 // 日期折叠状态
const showLapsModal = ref(false) const collapsedDays = ref<boolean[]>([])
interface LapsItem {
lap: number
time: string
}
const lapsData = ref<LapsItem[]>([])
// 打开圈数达标时间弹窗 // 切换日期折叠状态
const openLapsModal = (data: LapsItem[]) => { const toggleDayCollapse = (index : number) => {
lapsData.value = data collapsedDays.value[index] = !collapsedDays.value[index]
showLapsModal.value = true
}
// 关闭圈数达标时间弹窗
const closeLapsModal = () => {
showLapsModal.value = false
} }
const selectedStudentDisplay = computed(() => { const selectedStudentDisplay = computed(() => {
@@ -219,12 +240,18 @@
return `${begin.value}${end.value}` return `${begin.value}${end.value}`
}) })
onLoad(() => { onLoad(() => {
loadData()
getProjectData() getProjectData()
getRecordList()
getStudentList()
}) })
const loadData = () => {} onShow(() => { })
const getProjectData = () => { const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => { PlanService.GetPlanListNoPage('计时项目').then(res => {
@@ -236,21 +263,34 @@
}) })
} }
const cellClick = (rows: any) => { const getStudentList = () => {
rowData.value = rows.detail studentService.GetStudentList().then(res => {
console.log(rowData.value); if (res.code == 0) {
showLapsModal.value=true studentList.value = res.data
} else {
Service.Msg(res.msg)
}
})
} }
const selectProcetFunc = (e: any) => {
selectProcet.value = e.value[0].name const cellClick = (rows : any) => {
selectId.value = e.value[0].planId
row.value = rows
rowData.value = rows.detail
showRecordPopup.value = true
}
const selectProject = (project : any) => {
selectProcet.value = project.name
selectId.value = project.planId
showProjectPopup.value = false
begin.value = '' begin.value = ''
end.value = '' end.value = ''
selectedStudentIndexes.value = [] selectedStudentIndexes.value = []
studentList.value = [] selectedStudentIds.value = []
tableData.value = [] tableData.value = []
getProjectDetail() getRecord()
} }
const getProjectDetail = () => { const getProjectDetail = () => {
@@ -267,21 +307,30 @@
showCalendar.value = true showCalendar.value = true
} }
const calendarConfirm = (e: any) => { const calendarConfirm = (e : any) => {
begin.value = e[0] begin.value = e[0]
end.value = e[e.length - 1] end.value = e[e.length - 1]
showCalendar.value = false showCalendar.value = false
getRecord()
} }
const calendarClose = () => { const calendarClose = () => {
showCalendar.value = false showCalendar.value = false
} }
const selectAllStudents = () => { const allSelected = computed(() => {
selectedStudentIndexes.value = studentList.value.map((_, index) => index) return studentList.value.length > 0 && studentList.value.every((_, index) => selectedStudentIndexes.value.includes(index))
})
const toggleSelectAll = () => {
if (allSelected.value) {
selectedStudentIndexes.value = []
} else {
selectedStudentIndexes.value = studentList.value.map((_, index) => index)
}
} }
const toggleStudentSelection = (index: number) => { const toggleStudentSelection = (index : number) => {
const idx = selectedStudentIndexes.value.indexOf(index) const idx = selectedStudentIndexes.value.indexOf(index)
if (idx > -1) { if (idx > -1) {
selectedStudentIndexes.value.splice(idx, 1) selectedStudentIndexes.value.splice(idx, 1)
@@ -302,6 +351,7 @@
} }
const getRecordList = () => { const getRecordList = () => {
Service.Msg('数据加载中...')
let studentIdList = selectedStudentIndexes.value.map(index => { let studentIdList = selectedStudentIndexes.value.map(index => {
return studentList.value[index].studentId return studentList.value[index].studentId
}) })
@@ -310,21 +360,24 @@
planId: selectId.value, planId: selectId.value,
studentId: JSON.stringify(studentIdList), studentId: JSON.stringify(studentIdList),
sTime: begin.value, sTime: begin.value,
eTime: end.value, eTime: end.value
page: page.value
} }
PlanService.GetBaoganLog(data).then(res => { PlanService.GetBaoganLog(data).then(res => {
isLoading.value = true
Service.LoadClose()
if (res.code == 0) { if (res.code == 0) {
pageTotal.value = res.data.pageTotal pageTotal.value = res.data.pageTotal
tableData.value = res.data.list tableData.value = res.data.list
// 初始化折叠状态,默认全部展开
collapsedDays.value = new Array(res.data.list.length).fill(false)
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }
}) })
} }
const pageChange = (e: any) => { const pageChange = (e : any) => {
page.value = e page.value = e
getRecordList() getRecordList()
} }
@@ -332,16 +385,44 @@
const closeStudentPicker = () => { const closeStudentPicker = () => {
showStudentPicker.value = false showStudentPicker.value = false
} }
// 记录详情弹窗
const showRecordPopup = ref(false)
interface RecordItem {
name : string
time : string
}
const recordList = ref<RecordItem[]>([])
// 打开记录详情弹窗
const openRecordPopup = (data : RecordItem[]) => {
recordList.value = data
showRecordPopup.value = true
}
// 关闭记录详情弹窗
const closeRecordPopup = () => {
showRecordPopup.value = false
}
// 简化时间格式化(分:秒.毫秒)
const formatSimpleTime = (seconds : number) : string => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
const millis = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${millis.toString().padStart(2, '0')}`
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
page { page {
background-color: #eef1f6; background-color: #e8ecf3;
} }
.baogan-container { .timing-container {
min-height: 100vh; min-height: 100vh;
padding: 20rpx; padding: 24rpx;
} }
/* 通用卡片标题 */ /* 通用卡片标题 */
@@ -362,8 +443,8 @@
background: #ffffff; background: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
padding: 24rpx; padding: 24rpx;
margin-bottom: 20rpx; margin-bottom: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
} }
.filter-item { .filter-item {
@@ -418,13 +499,35 @@
background: #ffffff; background: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
padding: 24rpx; padding: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
}
.day-title {
display: flex;
align-items: center;
justify-content: space-between;
margin: 0 20rpx;
font-weight: bold;
font-size: 28rpx;
color: #333;
padding: 16rpx 0 8rpx;
cursor: pointer;
&:active {
opacity: 0.7;
}
.arrow-rotate {
transform: rotate(180deg);
transition: transform 0.3s ease;
}
} }
.table-wrap { .table-wrap {
border-radius: 12rpx; border-radius: 12rpx;
overflow: hidden; overflow: hidden;
border: 1rpx solid #f0f0f0; border: 1rpx solid #f0f0f0;
margin-bottom: 20rpx;
} }
/* 空状态 */ /* 空状态 */
@@ -433,10 +536,10 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 100rpx 40rpx; padding: 120rpx 40rpx;
background: #ffffff; background: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06); box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
.empty-text { .empty-text {
margin-top: 24rpx; margin-top: 24rpx;
@@ -445,6 +548,17 @@
} }
} }
/* 弹窗遮罩 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.55);
z-index: 998;
}
/* 学生选择弹窗 */ /* 学生选择弹窗 */
.student-picker-modal { .student-picker-modal {
position: fixed; position: fixed;
@@ -470,6 +584,18 @@
background: #fafafa; background: #fafafa;
border-radius: 24rpx 24rpx 0 0; border-radius: 24rpx 24rpx 0 0;
.header-left {
display: flex;
align-items: center;
gap: 8rpx;
.student-count {
font-size: 26rpx;
color: #999;
font-weight: 500;
}
}
.header-title { .header-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
@@ -482,6 +608,36 @@
gap: 16rpx; gap: 16rpx;
} }
.select-all-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
}
.checkbox-icon {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
&.checked {
color: #1890ff;
}
}
.select-all-text {
font-size: 24rpx;
color: #666;
}
}
.action-btn { .action-btn {
font-size: 26rpx; font-size: 26rpx;
color: #666; color: #666;
@@ -507,69 +663,69 @@
padding: 16rpx 24rpx 40rpx; padding: 16rpx 24rpx 40rpx;
} }
.student-grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.student-item { .student-item {
width: calc((100% - 24rpx) / 3);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20rpx; gap: 8rpx;
padding: 22rpx 20rpx; padding: 16rpx 8rpx;
border-radius: 12rpx; background-color: #f5f5f5;
margin-bottom: 12rpx; border-radius: 16rpx;
background: #f8f9fa; border: 2rpx solid transparent;
transition: background 0.2s; transition: all 0.25s ease;
&:active { &:active {
background: #eeeeee; transform: scale(0.98);
} }
&.selected { &.checked {
background: #fff3e0; background-color: #e6f7ff;
border: 1rpx solid #ffcc80; border-color: #1890ff;
} }
} }
.item-checkbox { .student-checkbox {
flex-shrink: 0; width: 36rpx;
height: 36rpx;
.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%; border-radius: 50%;
background: #1890ff; border: 2rpx solid #d9d9d9;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
.avatar-text { &.checked {
font-size: 28rpx; border-color: #1890ff;
background-color: #1890ff;
}
.checkbox-index {
font-size: 22rpx;
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
line-height: 1;
} }
} }
.item-name { .student-info {
flex: 1; width: 100%;
font-size: 30rpx;
color: #333; .student-name {
font-weight: 500; font-size: 26rpx;
font-weight: 600;
color: #333;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
/* 表格深度样式 */ /* 表格深度样式 */
@@ -601,10 +757,250 @@
background-color: #fff !important; background-color: #fff !important;
} }
/* 项目选择弹窗 */
.project-popup-content {
width: 680rpx;
max-height: 70vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.25), 0 0 0 1rpx rgba(0, 0, 0, 0.04);
}
.modal-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);
.modal-title {
font-size: 36rpx;
font-weight: 700;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.modal-close {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
&:active {
background: linear-gradient(135deg, #e8e8e8 0%, #e0e0e0 100%);
transform: scale(0.92);
}
}
}
.popup-project-list {
max-height: 55vh;
padding: 24rpx 28rpx;
}
.project-list-inner {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.project-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
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;
border-radius: 16rpx;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4rpx;
height: 100%;
background: linear-gradient(180deg, #1890ff 0%, #40a9ff 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active::after {
opacity: 1;
}
.item-icon {
flex-shrink: 0;
.icon-bg {
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #36cfc9 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.25);
position: relative;
&::before {
content: '';
position: absolute;
top: 6rpx;
right: 6rpx;
width: 10rpx;
height: 10rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.icon-text {
font-size: 28rpx;
color: #fff;
font-weight: 700;
letter-spacing: 2rpx;
}
}
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
}
}
.empty-project {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 20rpx;
gap: 16rpx;
.empty-text {
font-size: 28rpx;
color: #999;
font-weight: 500;
}
&::before {
content: '';
width: 120rpx;
height: 120rpx;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(64, 169, 255, 0.03) 100%);
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
}
}
/* 分段详情弹窗 */
.segment-popup-content {
width: 620rpx;
max-height: 70vh;
padding: 32rpx;
display: flex;
flex-direction: column;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.popup-title {
font-size: 34rpx;
font-weight: 700;
color: #333;
}
}
.popup-table {
background-color: #fafafa;
border-radius: 16rpx;
overflow: hidden;
display: flex;
flex-direction: column;
.popup-table-header {
display: flex;
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
padding: 24rpx 0;
.popup-th {
flex: 1;
text-align: center;
font-size: 28rpx;
font-weight: 700;
color: #fff;
}
}
.popup-scroll {
max-height: 400rpx;
.popup-table-row {
display: flex;
padding: 24rpx 0;
border-bottom: 1rpx solid #eee;
background-color: #fff;
&:nth-child(even) {
background-color: #fafafa;
}
.popup-td {
flex: 1;
text-align: center;
font-size: 28rpx;
color: #333;
}
}
.popup-empty {
padding: 60rpx 0;
text-align: center;
.popup-empty-text {
font-size: 28rpx;
color: #999;
}
}
}
}
}
@keyframes slideUp { @keyframes slideUp {
from { from {
transform: translateY(100%); transform: translateY(100%);
} }
to { to {
transform: translateY(0); transform: translateY(0);
} }
@@ -612,7 +1008,7 @@
/* 圈数达标弹窗 */ /* 圈数达标弹窗 */
.laps-popup-content { .laps-popup-content {
width: 600rpx; width: 700rpx;
max-height: 70vh; max-height: 70vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -629,7 +1025,11 @@
.laps-popup-title { .laps-popup-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
color: #333; color: #333;
} }
} }

View File

@@ -11,7 +11,7 @@
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon> <u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view> </view>
</view> </view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
<!-- 排名列表区域 --> <!-- 排名列表区域 -->
<view class="ranking-section" v-if="selectProcet && gradeList.length > 0"> <view class="ranking-section" v-if="selectProcet && gradeList.length > 0">
@@ -53,6 +53,33 @@
<u-icon name="file-text" size="64" color="#d9d9d9"></u-icon> <u-icon name="file-text" size="64" color="#d9d9d9"></u-icon>
<text class="empty-text">暂无排名数据</text> <text class="empty-text">暂无排名数据</text>
</view> </view>
<!-- 项目选择弹窗 -->
<up-popup :show="showProject" mode="center" round="32" :safeAreaInsetBottom="false"
@close="showProject = false">
<view class="project-popup-content">
<view class="modal-header">
<text class="modal-title">选择项目</text>
<view class="modal-close" @click="showProject = false">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<scroll-view scroll-y class="popup-project-list">
<view class="project-list-inner">
<view v-for="(project, index) in projectOptions[0]" :key="project.planId" class="project-item"
@click="selectProcetFunc(project)">
<view class="item-info">
<text class="item-name">{{ project.name }}</text>
</view>
</view>
<view v-if="!projectOptions[0] || projectOptions[0].length === 0" class="empty-project">
<text class="empty-text">暂无项目</text>
</view>
</view>
</scroll-view>
</view>
</up-popup>
</view> </view>
</template> </template>
@@ -100,8 +127,9 @@
} }
const selectProcetFunc = (e: any) => { const selectProcetFunc = (e: any) => {
selectProcet.value = e.value[0].name
selectId.value = e.value[0].planId selectProcet.value = e.name
selectId.value = e.planId
loadProjectGrades(selectId.value) loadProjectGrades(selectId.value)
} }
@@ -109,6 +137,7 @@
PlanService.GetRankData(projectId).then(res => { PlanService.GetRankData(projectId).then(res => {
if (res.code == 0) { if (res.code == 0) {
gradeList.value = res.data.list gradeList.value = res.data.list
showProject.value=false
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }
@@ -339,4 +368,141 @@
color: #999; color: #999;
} }
} }
/* 项目选择弹窗 */
.project-popup-content {
width: 680rpx;
max-height: 70vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.25), 0 0 0 1rpx rgba(0, 0, 0, 0.04);
}
.modal-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);
.modal-title {
font-size: 36rpx;
font-weight: 700;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.modal-close {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
&:active {
background: linear-gradient(135deg, #e8e8e8 0%, #e0e0e0 100%);
transform: scale(0.92);
}
}
}
.popup-project-list {
max-height: 55vh;
padding: 24rpx 28rpx;
}
.project-list-inner {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.project-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
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;
border-radius: 16rpx;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4rpx;
height: 100%;
background: linear-gradient(180deg, #1890ff 0%, #40a9ff 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active::after {
opacity: 1;
}
.item-icon {
flex-shrink: 0;
.icon-bg {
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #36cfc9 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.25);
position: relative;
&::before {
content: '';
position: absolute;
top: 6rpx;
right: 6rpx;
width: 10rpx;
height: 10rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.icon-text {
font-size: 28rpx;
color: #fff;
font-weight: 700;
letter-spacing: 2rpx;
}
}
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
}
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
<text>筛选条件</text> <text>筛选条件</text>
</view> </view>
<view class="filter-item" @click="showProject = true"> <view class="filter-item" @click="showProjectPopup = true">
<text class="filter-label">项目</text> <text class="filter-label">项目</text>
<view class="filter-value"> <view class="filter-value">
<text class="value-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text> <text class="value-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
@@ -36,7 +36,30 @@
</view> </view>
</view> </view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker> <!-- 项目选择弹窗 -->
<up-popup :show="showProjectPopup" mode="center" round="32" :safeAreaInsetBottom="false" @close="showProjectPopup = false">
<view class="project-popup-content">
<view class="modal-header">
<text class="modal-title">选择项目</text>
<view class="modal-close" @click="showProjectPopup = false">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<scroll-view scroll-y class="popup-project-list">
<view class="project-list-inner">
<view v-for="(project, index) in projectOptions[0]" :key="project.planId" class="project-item" @click="selectProject(project)">
<view class="item-info">
<text class="item-name">{{ project.name }}</text>
</view>
</view>
<view v-if="!projectOptions[0] || projectOptions[0].length === 0" class="empty-project">
<text class="empty-text">暂无项目</text>
</view>
</view>
</scroll-view>
</view>
</up-popup>
<!-- 数据表格 --> <!-- 数据表格 -->
<view class="data-card"> <view class="data-card">
@@ -46,7 +69,7 @@
</view> </view>
<view v-for="(item,index) in tableData" :key="index"> <view v-for="(item,index) in tableData" :key="index">
<view class="day-title" @click="toggleDayCollapse(index)"> <view class="day-title" @click="toggleDayCollapse(index)">
<text>{{ item.dayTime }}</text> <text>{{ item.dayTime }} · {{ item.planName }} </text>
<u-icon <u-icon
name="arrow-down" name="arrow-down"
size="16" size="16"
@@ -54,7 +77,7 @@
:class="{ 'arrow-rotate': collapsedDays[index] }" :class="{ 'arrow-rotate': collapsedDays[index] }"
></u-icon> ></u-icon>
</view> </view>
<view class="table-wrap" v-if="collapsedDays[index]"> <view class="table-wrap" v-if="collapsedDays[index]">
<next-table :show-header="true" @cellClick="cellClick" :columns="columns" :stripe="true" :fit="false" :show-summary="false" :data="item.data" ></next-table> <next-table :show-header="true" @cellClick="cellClick" :columns="columns" :stripe="true" :fit="false" :show-summary="false" :data="item.data" ></next-table>
</view> </view>
</view> </view>
@@ -66,11 +89,7 @@
<text class="empty-text"> {{ isLoading?'暂无训练记录':'加载中' }} </text> <text class="empty-text"> {{ isLoading?'暂无训练记录':'加载中' }} </text>
</view> </view>
<!-- 提示状态 -->
<!-- <view v-else-if="selectProcet" class="empty-card">
<u-icon name="search" 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> <up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar>
@@ -79,23 +98,32 @@
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view> <view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
<view class="student-picker-modal" v-if="showStudentPicker"> <view class="student-picker-modal" v-if="showStudentPicker">
<view class="picker-header"> <view class="picker-header">
<text class="header-title">选择学生</text> <view class="header-left">
<text class="header-title">选择学生</text>
<text class="student-count">已选({{ selectedStudentIndexes.length }}/{{ studentList.length }})</text>
</view>
<view class="header-actions"> <view class="header-actions">
<text class="action-btn" @click="selectAllStudents">全选</text> <view class="select-all-btn" @click="toggleSelectAll">
<view class="checkbox-icon" :class="{ checked: allSelected }">
<u-icon v-if="allSelected" name="checkmark" size="14" color="#fff"></u-icon>
</view>
<text class="select-all-text">{{ allSelected ? '取消全选' : '全选' }}</text>
</view>
<text class="action-btn primary" @click="confirmStudentSelection">确定</text> <text class="action-btn primary" @click="confirmStudentSelection">确定</text>
</view> </view>
</view> </view>
<scroll-view class="student-list" scroll-y> <scroll-view class="student-list" scroll-y>
<view v-for="(student, index) in studentList" :key="student.studentId" class="student-item" :class="{ selected: selectedStudentIndexes.includes(index) }" @click="toggleStudentSelection(index)"> <view class="student-grid">
<view class="item-checkbox"> <view v-for="(student, index) in studentList" :key="student.studentId" class="student-item" :class="{ checked: selectedStudentIndexes.includes(index) }" @click="toggleStudentSelection(index)">
<view class="checkbox-inner" :class="{ checked: selectedStudentIndexes.includes(index) }"> <view class="student-checkbox" :class="{ checked: selectedStudentIndexes.includes(index) }">
<u-icon v-if="selectedStudentIndexes.includes(index)" name="checkmark" size="12" color="#fff"></u-icon> <text v-if="selectedStudentIndexes.includes(index)" class="checkbox-index">
{{ selectedStudentIndexes.indexOf(index) + 1 }}
</text>
</view>
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
</view> </view>
</view> </view>
<view class="item-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
<text class="item-name">{{ student.name }}</text>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
@@ -104,7 +132,7 @@
<up-popup :show="showRecordPopup" @close="closeRecordPopup" mode="center" round="16" bgColor="#fff"> <up-popup :show="showRecordPopup" @close="closeRecordPopup" mode="center" round="16" bgColor="#fff">
<view class="record-popup-content"> <view class="record-popup-content">
<view class="record-popup-header"> <view class="record-popup-header">
<text class="record-popup-title">记录详情</text> <text class="record-popup-title">{{ rowData.studentName +'·' + rowData.planName}}</text>
<u-icon name="close" size="22" color="#999" @click="closeRecordPopup"></u-icon> <u-icon name="close" size="22" color="#999" @click="closeRecordPopup"></u-icon>
</view> </view>
<scroll-view class="record-popup-body" scroll-y> <scroll-view class="record-popup-body" scroll-y>
@@ -114,7 +142,7 @@
<text class="record-th">时间</text> <text class="record-th">时间</text>
</view> </view>
<view <view
v-for="(item, index) in rowData.split('|')" v-for="(item, index) in rowData.time.split('|')"
:key="index" :key="index"
class="record-table-row" class="record-table-row"
:class="{ 'record-row-odd': index % 2 === 1 }" :class="{ 'record-row-odd': index % 2 === 1 }"
@@ -122,7 +150,7 @@
<text class="record-td">记录{{ index+1 }}</text> <text class="record-td">记录{{ index+1 }}</text>
<text class="record-td">{{ item }}</text> <text class="record-td">{{ item }}</text>
</view> </view>
<view v-if="rowData.split('|').length === 0" class="record-empty"> <view v-if="rowData.time.split('|').length === 0" class="record-empty">
<text class="record-empty-text">暂无数据</text> <text class="record-empty-text">暂无数据</text>
</view> </view>
</view> </view>
@@ -159,7 +187,7 @@
let isLoading=ref(false) let isLoading=ref(false)
// 项目选择 // 项目选择
let showProject = ref(false) let showProjectPopup = ref(false)
let selectProcet = ref('') let selectProcet = ref('')
let selectId = ref('') let selectId = ref('')
@@ -174,12 +202,14 @@
const columns = ref([ const columns = ref([
{ label: '姓名', name: 'studentName' }, { label: '姓名', name: 'studentName' },
{ label: '项目名称', name: 'planName' },
{ label: '最快速度', name: 'quicklyTime' }, { label: '最快速度', name: 'quicklyTime' },
{ label: '详细数据', name: 'time', width: '200' } { label: '详细数据', name: 'time', width: '200' }
]) ])
let rowData=ref('') let rowData=ref<any>({
studentName:'',
time:''
})
const tableData = ref<any[]>([]) const tableData = ref<any[]>([])
const projectOptions = ref<Array<any>>([[]]) const projectOptions = ref<Array<any>>([[]])
@@ -218,6 +248,12 @@
onShow(() => {}) onShow(() => {})
const cs=(e)=>{
console.log(e);
}
const getProjectData = () => { const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => { PlanService.GetPlanListNoPage('计时项目').then(res => {
if (res.code == 0) { if (res.code == 0) {
@@ -240,17 +276,18 @@
const cellClick=(e:any)=>{ const cellClick=(e:any)=>{
rowData.value=e.time rowData.value=e
console.log(rowData.value);
showRecordPopup.value=true showRecordPopup.value=true
} }
const selectProcetFunc = (e: any) => { const selectProject = (project: any) => {
selectProcet.value = e.value[0].name selectProcet.value = project.name
selectId.value = e.value[0].planId selectId.value = project.planId
showProjectPopup.value = false
begin.value = '' begin.value = ''
end.value = '' end.value = ''
selectedStudentIndexes.value = [] selectedStudentIndexes.value = []
selectedStudentIds.value = []
// studentList.value = [] // studentList.value = []
tableData.value = [] tableData.value = []
// getProjectDetail() // getProjectDetail()
@@ -282,8 +319,16 @@
showCalendar.value = false showCalendar.value = false
} }
const selectAllStudents = () => { const allSelected = computed(() => {
selectedStudentIndexes.value = studentList.value.map((_, index) => index) return studentList.value.length > 0 && studentList.value.every((_, index) => selectedStudentIndexes.value.includes(index))
})
const toggleSelectAll = () => {
if (allSelected.value) {
selectedStudentIndexes.value = []
} else {
selectedStudentIndexes.value = studentList.value.map((_, index) => index)
}
} }
const toggleStudentSelection = (index: number) => { const toggleStudentSelection = (index: number) => {
@@ -533,6 +578,18 @@
background: #fafafa; background: #fafafa;
border-radius: 24rpx 24rpx 0 0; border-radius: 24rpx 24rpx 0 0;
.header-left {
display: flex;
align-items: center;
gap: 8rpx;
.student-count {
font-size: 26rpx;
color: #999;
font-weight: 500;
}
}
.header-title { .header-title {
font-size: 32rpx; font-size: 32rpx;
font-weight: 600; font-weight: 600;
@@ -545,6 +602,36 @@
gap: 16rpx; gap: 16rpx;
} }
.select-all-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
}
.checkbox-icon {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
&.checked {
color: #1890ff;
}
}
.select-all-text {
font-size: 24rpx;
color: #666;
}
}
.action-btn { .action-btn {
font-size: 26rpx; font-size: 26rpx;
color: #666; color: #666;
@@ -570,69 +657,69 @@
padding: 16rpx 24rpx 40rpx; padding: 16rpx 24rpx 40rpx;
} }
.student-grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.student-item { .student-item {
width: calc((100% - 24rpx) / 3);
display: flex; display: flex;
align-items: center; align-items: center;
gap: 20rpx; gap: 8rpx;
padding: 22rpx 20rpx; padding: 16rpx 8rpx;
border-radius: 12rpx; background-color: #f5f5f5;
margin-bottom: 12rpx; border-radius: 16rpx;
background: #f8f9fa; border: 2rpx solid transparent;
transition: background 0.2s; transition: all 0.25s ease;
&:active { &:active {
background: #eeeeee; transform: scale(0.98);
} }
&.selected { &.checked {
background: #fff3e0; background-color: #e6f7ff;
border: 1rpx solid #ffcc80; border-color: #1890ff;
} }
} }
.item-checkbox { .student-checkbox {
flex-shrink: 0; width: 36rpx;
height: 36rpx;
.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%; border-radius: 50%;
background: #1890ff; border: 2rpx solid #d9d9d9;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
.avatar-text { &.checked {
font-size: 28rpx; border-color: #1890ff;
background-color: #1890ff;
}
.checkbox-index {
font-size: 22rpx;
color: #fff; color: #fff;
font-weight: 600; font-weight: 600;
line-height: 1;
} }
} }
.item-name { .student-info {
flex: 1; width: 100%;
font-size: 30rpx;
color: #333; .student-name {
font-weight: 500; font-size: 26rpx;
font-weight: 600;
color: #333;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
} }
/* 表格深度样式 */ /* 表格深度样式 */
@@ -664,6 +751,168 @@
background-color: #fff !important; background-color: #fff !important;
} }
/* 项目选择弹窗 */
.project-popup-content {
width: 680rpx;
max-height: 70vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.25), 0 0 0 1rpx rgba(0, 0, 0, 0.04);
}
.modal-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);
.modal-title {
font-size: 36rpx;
font-weight: 700;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.modal-close {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
background: linear-gradient(135deg, #f5f5f5 0%, #f0f0f0 100%);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
&:active {
background: linear-gradient(135deg, #e8e8e8 0%, #e0e0e0 100%);
transform: scale(0.92);
}
}
}
.popup-project-list {
max-height: 55vh;
padding: 24rpx 28rpx;
}
.project-list-inner {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.project-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx 24rpx;
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
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;
border-radius: 16rpx;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4rpx;
height: 100%;
background: linear-gradient(180deg, #1890ff 0%, #40a9ff 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:active::after {
opacity: 1;
}
.item-icon {
flex-shrink: 0;
.icon-bg {
width: 56rpx;
height: 56rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #36cfc9 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.25);
position: relative;
&::before {
content: '';
position: absolute;
top: 6rpx;
right: 6rpx;
width: 10rpx;
height: 10rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
.icon-text {
font-size: 28rpx;
color: #fff;
font-weight: 700;
letter-spacing: 2rpx;
}
}
}
.item-info {
flex: 1;
min-width: 0;
display: flex;
align-items: center;
.item-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
letter-spacing: 1rpx;
}
}
}
.empty-project {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 100rpx 20rpx;
gap: 16rpx;
.empty-text {
font-size: 28rpx;
color: #999;
font-weight: 500;
}
&::before {
content: '';
width: 120rpx;
height: 120rpx;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.08) 0%, rgba(64, 169, 255, 0.03) 100%);
border-radius: 28rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8rpx;
}
}
@keyframes slideUp { @keyframes slideUp {
from { from {
transform: translateY(100%); transform: translateY(100%);

View File

@@ -29,7 +29,7 @@
</view> </view>
<!-- 包干项目模块 --> <!-- 包干项目模块 -->
<view @click="Service.GoPage('/pages/userFunc/project')" class="create-card package-card"> <view @click=" goBaogan()" class="create-card package-card">
<view class="card-icon"> <view class="card-icon">
<view class="icon-circle package-icon"> <view class="icon-circle package-icon">
<u-icon name="grid" size="40" color="#fff"></u-icon> <u-icon name="grid" size="40" color="#fff"></u-icon>
@@ -74,8 +74,12 @@
<view class="card-arrow"> <view class="card-arrow">
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon> <u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
</view> </view>
</view> </view>
</view> </view>
<!-- <view @click="Service.GoPage('/pages/userFunc/cs')" class="">
cs
</view> -->
</view> </view>
<!-- 计时项目列表弹窗 --> <!-- 计时项目列表弹窗 -->
@@ -98,11 +102,11 @@
<view class="project-list"> <view class="project-list">
<text class="list-title" v-if="projects.length > 0">项目列表</text> <text class="list-title" v-if="projects.length > 0">项目列表</text>
<scroll-view v-if="projects.length > 0" scroll-y="true" @scrolltolower='getList()' class="list-container"> <scroll-view v-if="projects.length > 0" scroll-y="true" @scrolltolower='getList()' class="list-container">
<view v-for="project in projects" :key="project.planId" class="project-item" <view v-for="(project,index) in projects" :key="project.planId" class="project-item"
@click="handleProjectClick(project)"> @click="handleProjectClick(project)">
<view class="item-icon"> <view class="item-icon">
<view class="icon-bg"> <view class="icon-bg">
<text class="icon-text">{{ project.name.charAt(0) }}</text> <text class="icon-text">{{ index+1 }}</text>
</view> </view>
</view> </view>
<view class="item-info"> <view class="item-info">
@@ -128,6 +132,7 @@
import { onLoad, onShow } from '@dcloudio/uni-app' import { onLoad, onShow } from '@dcloudio/uni-app'
import { loginService } from '@/Service/swimming/loginService' import { loginService } from '@/Service/swimming/loginService'
import { PlanService } from '@/Service/swimming/PlanService' import { PlanService } from '@/Service/swimming/PlanService'
import { userService } from '@/Service/swimming/userService'
@@ -143,9 +148,14 @@
// 弹窗状态 // 弹窗状态
const showTimingModal = ref(false) const showTimingModal = ref(false)
let isUse=ref(false)
onLoad(() => { onLoad(() => {
if (!Service.GetUserToken()) { if (!Service.GetUserToken()) {
login() login()
}else{
isUseFunc()
} }
}) })
@@ -167,6 +177,7 @@
).then(content => { ).then(content => {
if (content.code == 0) { if (content.code == 0) {
Service.SetUserToken(content.data.token) Service.SetUserToken(content.data.token)
isUseFunc()
} else { } else {
Service.Msg(content.msg) Service.Msg(content.msg)
} }
@@ -180,6 +191,18 @@
}); });
} }
const isUseFunc=()=>{
userService.IsCheakUserVerify().then(res=>{
if(res.code==0){
isUse.value=res.data.isCheak
}else{
Service.Msg(res.msg)
}
})
}
// 获取项目列表数据 // 获取项目列表数据
const getData = () => { const getData = () => {
projects.value = [] projects.value = []
@@ -206,11 +229,23 @@
}) })
} }
const goBaogan=()=>{
if(!isUse.value){
Service.Msg('用户暂无权限,请等待审核!')
return
}
Service.GoPage('/pages/userFunc/project')
}
// 显示计时项目列表弹窗 // 显示计时项目列表弹窗
const showTimingProjectModal = (index : any) => { const showTimingProjectModal = (index : any) => {
uni.hideTabBar() if(!isUse.value){
Service.Msg('用户暂无权限,请等待审核!')
return
}
uni.hideTabBar()
showTimingModal.value = true showTimingModal.value = true
currentIndex.value = index currentIndex.value = index
getData() getData()
@@ -531,6 +566,7 @@
backdrop-filter: blur(10rpx); backdrop-filter: blur(10rpx);
z-index: 998; z-index: 998;
animation: fadeIn 0.3s ease; animation: fadeIn 0.3s ease;
height: 100vh;
} }
.modal-header { .modal-header {
@@ -578,7 +614,7 @@
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
width: 90%; width: 90%;
max-width: 680rpx; max-width: 680rpx;
max-height: 80vh; height: 80vh;
background: linear-gradient(180deg, #fafbfc 0%, #fff 100%); background: linear-gradient(180deg, #fafbfc 0%, #fff 100%);
border-radius: 32rpx; border-radius: 32rpx;
z-index: 999; z-index: 999;
@@ -659,7 +695,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16rpx; gap: 16rpx;
max-height: 240rpx; max-height: 800rpx;
} }
.project-item { .project-item {

View File

@@ -23,14 +23,14 @@
<view class="stats-section"> <view class="stats-section">
<view class="stats-card"> <view class="stats-card">
<view class="stat-item"> <view class="stat-item">
<view @click="Service.GoPage('/pages/userFunc/projectList')" class="stat-info"> <view @click="goPage('/pages/userFunc/projectList')" class="stat-info">
<text class="stat-value">{{ planCount }}</text> <text class="stat-value">{{ planCount }}</text>
<text class="stat-label">我的项目</text> <text class="stat-label">我的项目</text>
</view> </view>
</view> </view>
<view class="stat-divider"></view> <view class="stat-divider"></view>
<view class="stat-item"> <view class="stat-item">
<view @click="Service.GoPage('/pages/userFunc/student')" class="stat-info"> <view @click="goPage('/pages/userFunc/student')" class="stat-info">
<text class="stat-value">{{ studentCount }}</text> <text class="stat-value">{{ studentCount }}</text>
<text class="stat-label">学员数</text> <text class="stat-label">学员数</text>
</view> </view>
@@ -42,7 +42,7 @@
<view class="menu-section"> <view class="menu-section">
<view class="menu-card"> <view class="menu-card">
<!-- 项目管理菜单项 --> <!-- 项目管理菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/projectList')"> <view class="menu-item" @click="goPage('/pages/userFunc/projectList')">
<view class="menu-icon-bg project-icon-bg"> <view class="menu-icon-bg project-icon-bg">
<u-icon name="list" size="28" color="#fff"></u-icon> <u-icon name="list" size="28" color="#fff"></u-icon>
</view> </view>
@@ -64,7 +64,7 @@
<!-- 学员管理菜单项 --> <!-- 学员管理菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/student')"> <view class="menu-item" @click="goPage('/pages/userFunc/student')">
<view class="menu-icon-bg academy-icon-bg"> <view class="menu-icon-bg academy-icon-bg">
<u-icon name="grid" size="28" color="#fff"></u-icon> <u-icon name="grid" size="28" color="#fff"></u-icon>
</view> </view>
@@ -85,7 +85,7 @@
<view class="menu-divider"></view> <view class="menu-divider"></view>
<!-- 数据分析菜单项 --> <!-- 数据分析菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/analyze')"> <view class="menu-item" @click="goPage('/pages/userFunc/analyze')">
<view class="menu-icon-bg analysis-icon-bg"> <view class="menu-icon-bg analysis-icon-bg">
<u-icon name="order" size="28" color="#fff"></u-icon> <u-icon name="order" size="28" color="#fff"></u-icon>
</view> </view>
@@ -104,6 +104,12 @@
</view> </view>
</view> </view>
</view> </view>
<!-- 底部联系信息 -->
<view class="contact-footer" @click="callContact">
<text class="contact-label">客服联系人张教练</text>
<text class="contact-phone">138-0000-0000</text>
</view>
</view> </view>
</template> </template>
@@ -116,7 +122,7 @@
let planCount=ref(0) let planCount=ref(0)
let studentCount=ref(0) let studentCount=ref(0)
let userInfo = ref<any>({}) let userInfo = ref<any>({})
let isUse=ref(false)
onLoad(() => { onLoad(() => {
@@ -124,8 +130,19 @@
onShow(() => { onShow(() => {
loadUserInfo() loadUserInfo()
isUseFunc()
}) })
const isUseFunc=()=>{
userService.IsCheakUserVerify().then(res=>{
if(res.code==0){
isUse.value=res.data.isCheak
}else{
Service.Msg(res.msg)
}
})
}
const loadUserInfo = () => { const loadUserInfo = () => {
userService.GetUserInfo().then((content:any) => { userService.GetUserInfo().then((content:any) => {
if (content.code == 0) { if (content.code == 0) {
@@ -138,26 +155,21 @@
}) })
} }
const goPage=(path:any)=>{
/** if(!isUse.value){
* 跳转到训练计划页面 Service.Msg('用户暂无权限,请等待审核!')
* 点击"训练计划"菜单项时触发 return
* 功能说明: }
* - 跳转到训练计划列表页面 Service.GoPage(path)
* - 用户可以查看、添加、编辑训练计划
*/
const goToTrainingPlans = () => {
Service.GoPage('/pages/userFunc/plan')
} }
// 跳转到设置 const callContact = () => {
const goToSettings = () => { uni.makePhoneCall({
Service.Msg('系统设置功能开发中') phoneNumber: '13800000000',
} fail: () => {
Service.Msg('拨打电话失败')
// 跳转到关于 }
const goToAbout = () => { })
Service.Msg('关于我们功能开发中')
} }
</script> </script>
@@ -169,7 +181,7 @@
.user-container { .user-container {
padding: 20rpx; padding: 20rpx;
padding-top: 30rpx; padding-top: 30rpx;
padding-bottom: 40rpx; padding-bottom: 140rpx;
} }
/* 用户信息卡片 */ /* 用户信息卡片 */
@@ -551,4 +563,30 @@
margin: 0 32rpx; margin: 0 32rpx;
} }
} }
/* 底部固定联系信息 */
.contact-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
border-top: 1rpx solid #eee;
padding: 16rpx 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 6rpx;
z-index: 10;
.contact-label {
font-size: 24rpx;
color: #999;
}
.contact-phone {
font-size: 26rpx;
color: #1890ff;
}
}
</style> </style>

View File

@@ -378,10 +378,12 @@
console.log('保存的数据:', data) console.log('保存的数据:', data)
PlanService.AddPlan(data).then(res => { PlanService.AddPlan(data).then(res => {
if (res.code == 0) { if (res.code == 0) {
Service.Msg('添加成功!') Service.Msg(planId.value?'修改成功!':'添加成功!')
setTimeout(() => { setTimeout(() => {
Service.GoPageBack() Service.GoPageDelse('/pages/userFunc/hunyang?id=' + res.data.planId)
}, 1000) }, 1000)
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }

3872
src/pages/userFunc/cs.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,51 +1,33 @@
<template> <template>
<up-navbar v-if="!isLandscape" title="混氧项目" placeholder='true' :autoBack="true">
</up-navbar>
<view class="hunyang-container"> <view class="hunyang-container">
<!-- 项目信息头部 --> <!-- 项目信息头部 -->
<view class="card-section header-section" style="position: relative;"> <view class="card-section header-section">
<view class="header-content"> <view class="header-content">
<view class="project-name">{{ name }}</view> <view class="project-name">{{ name }}</view>
<view class="total-duration"> <view class="header-action" @click="Service.GoPage('/pages/userFunc/addHunyang?id='+planId)">
<text class="duration-label">计划总时长:</text> <up-icon name="setting" size="22"></up-icon>
<text class="duration-value">{{ formatTotalDuration(totalDuration) }}</text>
</view> </view>
<view class="total-progress">
<text class="progress-text">
总进度: {{ totalCompletedGroupPlans }}/{{ totalGroupPlans }} 计划
</text>
</view>
</view>
<view style="position: absolute; top: 20rpx; right: 30rpx;">
<up-icon @click="Service.GoPage('/pages/userFunc/addHunyang?id='+planId)" name="setting" size="22"></up-icon>
</view> </view>
</view> </view>
<!-- 分组训练区域 --> <!-- 分组训练区域 -->
<view class="card-section groups-section" v-if="groupStates.length > 0"> <view class="card-section groups-section" v-if="groupStates.length > 0">
<view class="section-title">训练分组 (同时进行)</view>
<view class="groups-list"> <view class="groups-list">
<view <view v-for="(groupState, groupIndex) in groupStates" :key="groupIndex" class="group-card" :class="{
v-for="(groupState, groupIndex) in groupStates"
:key="groupIndex"
class="group-card"
:class="{
resting: groupState.isResting && !groupState.isCompleted, resting: groupState.isResting && !groupState.isCompleted,
completed: groupState.isCompleted completed: groupState.isCompleted
}" }">
>
<!-- 分组头部 --> <!-- 分组头部 -->
<view class="group-header"> <view class="group-header">
<view class="group-title-area"> <view class="group-title-area">
<text class="group-name">{{ groupState.groupName }}</text> <text class="group-name">{{ groupState.groupName }}</text>
<text class="group-plan-index">
计划 {{ groupState.currentPlanIndex }}/{{ groupState.plans.length }}
</text>
</view>
<view class="status-tag" :class="{ completed: groupState.isCompleted }">
<text v-if="groupState.isCompleted">已完成</text>
<text v-else-if="groupState.isResting">休息中</text>
<text v-else>训练中</text>
</view> </view>
<text class="group-plan-index">
计划总时长: {{ groupState.allTime }}
<!-- {{ groupState.currentPlanIndex }}/{{ groupState.plans.length }} -->
</text>
</view> </view>
<!-- 当前计划内容 --> <!-- 当前计划内容 -->
@@ -54,14 +36,10 @@
<view class="plan-name">{{ groupState.currentPlan?.name }}</view> <view class="plan-name">{{ groupState.currentPlan?.name }}</view>
<!-- 圈数指示器 --> <!-- 圈数指示器 -->
<view class="laps-indicator"> <!-- <view class="laps-indicator">
<view <view v-for="lap in groupState.currentPlan?.circle" :key="lap" class="lap-dot"
v-for="lap in groupState.currentPlan?.circle" :class="{ completed: lap <= groupState.completedLaps }"></view>
:key="lap" </view> -->
class="lap-dot"
:class="{ completed: lap <= groupState.completedLaps }"
></view>
</view>
<!-- 倒计时 --> <!-- 倒计时 -->
<view class="countdown-box" :class="{ resting: groupState.isResting }"> <view class="countdown-box" :class="{ resting: groupState.isResting }">
@@ -71,30 +49,46 @@
</view> </view>
<!-- 阶段提示 --> <!-- 阶段提示 -->
<view class="phase-hint" :class="{ resting: groupState.isResting }"> <!-- <view class="phase-hint" :class="{ resting: groupState.isResting }">
<text v-if="groupState.isResting" class="phase-text"> <text v-if="groupState.isResting && groupState.isPlanCompleted" class="phase-text">
计划完成 - 休息中
</text>
<text v-else-if="groupState.isResting" class="phase-text">
休息中 - {{ groupState.completedLaps + 1 }} 圈准备 休息中 - {{ groupState.completedLaps + 1 }} 圈准备
</text> </text>
<text v-else class="phase-text"> <text v-else class="phase-text">
训练中 - {{ groupState.completedLaps + 1 }} 训练中 - {{ groupState.completedLaps + 1 }}
</text> </text>
</view> </view> -->
<!-- 计划详情 --> <!-- 计划详情 -->
<view class="plan-details"> <view class="plan-details">
<view class="detail-box"> <text class="detail-badge">
<text class="detail-label">目标时间</text> M:{{ formatCountdown(groupState.currentPlan?.target) }}
<text class="detail-value">{{ groupState.currentPlan?.target }}</text> </text>
<text class="detail-badge">
X:{{ groupState.currentPlan?.rest }}
</text>
<text class="detail-badge">
Q:{{ groupState.currentPlan?.circle }}
</text>
</view>
<!-- 分组控制按钮 -->
<view class="group-controls">
<view v-if="!groupState.isRunning && !groupState.isCompleted"
class="group-control-btn group-start-btn" @click="startGroup(groupState)">
<u-icon name="play-circle" size="24" color="#fff"></u-icon>
<text>开始</text>
</view> </view>
<view class="detail-box"> <view v-else-if="groupState.isRunning" class="group-control-btn group-pause-btn"
<text class="detail-label">休息时间</text> @click="pauseGroup(groupState)">
<text class="detail-value">{{ groupState.currentPlan?.rest }}</text> <u-icon name="pause-circle" size="24" color="#fff"></u-icon>
<text>暂停</text>
</view> </view>
<view class="detail-box"> <view class="group-control-btn group-reset-btn" @click="resetGroup(groupIndex)">
<text class="detail-label">圈数</text> <u-icon name="reload" size="24" color="#fff"></u-icon>
<text class="detail-value"> <text>重置</text>
{{ groupState.completedLaps }}/{{ groupState.currentPlan?.circle }}
</text>
</view> </view>
</view> </view>
</view> </view>
@@ -108,14 +102,7 @@
</view> </view>
</view> </view>
<!-- 全部完成 -->
<!-- <view class="card-section all-completed" v-if="isAllCompleted">
<view class="completed-center">
<u-icon name="checkmark-circle-fill" size="80" color="#4caf50"></u-icon>
<text class="completed-title">训练完成</text>
<text class="completed-desc">所有分组计划已执行完毕</text>
</view>
</view> -->
<!-- 底部按钮 --> <!-- 底部按钮 -->
<view class="bottom-controls"> <view class="bottom-controls">
@@ -145,27 +132,30 @@
// ==================== 类型 ==================== // ==================== 类型 ====================
interface PlanItem { interface PlanItem {
name: string name : string
target: number target : number
rest: number rest : number
circle: number circle : number
} }
interface GroupData { interface GroupData {
groupName: string groupName : string
group: PlanItem[] allTime : string
group : PlanItem[]
} }
interface GroupState { interface GroupState {
groupName: string groupName : string
plans: PlanItem[] plans : PlanItem[]
currentPlanIndex: number currentPlanIndex : number
currentPlan: PlanItem | null currentPlan : PlanItem | null
countdown: number countdown : number
restCountdown: number restCountdown : number
completedLaps: number completedLaps : number
isResting: boolean isResting : boolean
isCompleted: boolean isCompleted : boolean
isPlanCompleted : boolean // 当前计划是否已完成(最后一圈休息后切换计划)
isRunning : boolean // 当前分组是否正在运行
} }
// ==================== 数据 ==================== // ==================== 数据 ====================
@@ -174,6 +164,8 @@
const groupStates = ref<GroupState[]>([]) const groupStates = ref<GroupState[]>([])
const isRunning = ref(false) const isRunning = ref(false)
const timer = ref<number | null>(null) const timer = ref<number | null>(null)
const planTotalDuration = ref(0) // 接口返回的计划总时长
const isLandscape = ref(false) // 是否为横屏
// ==================== 计算属性 ==================== // ==================== 计算属性 ====================
const totalGroupPlans = computed(() => { const totalGroupPlans = computed(() => {
@@ -200,8 +192,10 @@
}) })
// ==================== 生命周期 ==================== // ==================== 生命周期 ====================
onLoad((data: any) => { onLoad((data : any) => {
planId.value = data.id planId.value = data.id
checkOrientation()
uni.onWindowResize(checkOrientation)
}) })
onShow(() => { onShow(() => {
@@ -210,14 +204,20 @@
onUnmounted(() => { onUnmounted(() => {
stopTimer() stopTimer()
uni.offWindowResize(checkOrientation)
}) })
// ==================== 方法 ==================== // ==================== 方法 ====================
const checkOrientation = () => {
const info = uni.getSystemInfoSync()
isLandscape.value = info.windowWidth > info.windowHeight
}
const getPlanInfo = () => { const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => { PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code === 0) { if (res.code === 0) {
name.value = res.data.plan.name name.value = res.data.plan.name
const data: GroupData[] = JSON.parse(res.data.plan.project) const data : GroupData[] = JSON.parse(res.data.plan.project)
initGroups(data) initGroups(data)
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
@@ -227,9 +227,10 @@
}) })
} }
const initGroups = (data: GroupData[]) => { const initGroups = (data : GroupData[]) => {
groupStates.value = data.map(g => ({ groupStates.value = data.map(g => ({
groupName: g.groupName, groupName: g.groupName,
allTime: g.allTime,
plans: g.group, plans: g.group,
currentPlanIndex: 0, currentPlanIndex: 0,
currentPlan: g.group[0] || null, currentPlan: g.group[0] || null,
@@ -237,10 +238,134 @@
restCountdown: 0, restCountdown: 0,
completedLaps: 0, completedLaps: 0,
isResting: false, isResting: false,
isCompleted: g.group.length === 0 isCompleted: g.group.length === 0,
isPlanCompleted: false,
isRunning: false
})) }))
} }
// ==================== 分组级控制 ====================
const startGroup = (gs : GroupState) => {
if (gs.isCompleted) {
Service.Msg('该分组已完成')
return
}
// 初始化未开始的分组
if (!gs.isCompleted && gs.countdown === 0 && !gs.isResting) {
gs.countdown = gs.currentPlan?.target || 0
}
gs.isRunning = true
// 如果全局计时器未运行,启动它
if (!timer.value) {
startGlobalTimer()
}
}
const pauseGroup = (gs : GroupState) => {
gs.isRunning = false
// 检查是否还有运行的分组,如果没有则停止全局计时器
const hasRunning = groupStates.value.some(g => g.isRunning)
if (!hasRunning) {
stopTimer()
}
}
const resetGroup = (groupIndex : number) => {
const gs = groupStates.value[groupIndex]
if (!gs) return
// 停止当前分组运行
gs.isRunning = false
// 重置分组状态
gs.currentPlanIndex = 0
gs.currentPlan = gs.plans[0] || null
gs.countdown = 0
gs.restCountdown = 0
gs.completedLaps = 0
gs.isResting = false
gs.isCompleted = gs.plans.length === 0
gs.isPlanCompleted = false
// 检查是否还有运行的分组,如果没有则停止全局计时器
const hasRunning = groupStates.value.some(g => g.isRunning)
if (!hasRunning) {
stopTimer()
}
}
// ==================== 全局计时器 ====================
const startGlobalTimer = () => {
isRunning.value = true
timer.value = setInterval(() => {
let hasRunningGroup = false
let allCompleted = true
groupStates.value.forEach(gs => {
if (gs.isCompleted) return
allCompleted = false
if (!gs.isRunning) return
hasRunningGroup = true
if (gs.isResting) {
// 休息倒计时
gs.restCountdown -= 0.01
if (gs.restCountdown <= 0) {
gs.isResting = false
gs.restCountdown = 0
if (gs.isPlanCompleted) {
// 当前计划所有圈已完成,切换到下一个计划
gs.isPlanCompleted = false
gs.currentPlanIndex++
if (gs.currentPlanIndex >= gs.plans.length) {
// 分组全部完成
gs.isCompleted = true
gs.currentPlan = null
gs.isRunning = false
} else {
// 开始下一个计划
gs.currentPlan = gs.plans[gs.currentPlanIndex]
gs.countdown = gs.currentPlan.target
gs.completedLaps = 0
}
} else {
// 继续当前计划的下一圈
gs.countdown = gs.currentPlan?.target || 0
}
}
} else {
// 训练倒计时
gs.countdown -= 0.01
if (gs.countdown <= 0) {
gs.completedLaps++
if (gs.completedLaps >= (gs.currentPlan?.circle || 0)) {
// 当前计划最后一圈完成,进入休息(休息后切换计划)
gs.isPlanCompleted = true
gs.isResting = true
gs.restCountdown = gs.currentPlan?.rest || 0
} else {
// 进入休息
gs.isResting = true
gs.restCountdown = gs.currentPlan?.rest || 0
}
}
}
})
if (allCompleted || !hasRunningGroup) {
stopTimer()
}
}, 10)
}
const startTraining = () => { const startTraining = () => {
if (groupStates.value.length === 0) { if (groupStates.value.length === 0) {
Service.Msg('暂无训练计划') Service.Msg('暂无训练计划')
@@ -251,66 +376,18 @@
return return
} }
isRunning.value = true // 启动所有未完成的组
// 初始化未开始的分组
groupStates.value.forEach(gs => { groupStates.value.forEach(gs => {
if (!gs.isCompleted && gs.countdown === 0 && !gs.isResting) { if (!gs.isCompleted) {
gs.countdown = gs.currentPlan?.target || 0 startGroup(gs)
} }
}) })
// 启动计时器
timer.value = setInterval(() => {
let allDone = true
groupStates.value.forEach(gs => {
if (gs.isCompleted) return
allDone = false
if (gs.isResting) {
// 休息倒计时
gs.restCountdown -= 0.01
if (gs.restCountdown <= 0) {
gs.isResting = false
gs.restCountdown = 0
gs.countdown = gs.currentPlan?.target || 0
}
} else {
// 训练倒计时
gs.countdown -= 0.01
if (gs.countdown <= 0) {
gs.completedLaps++
if (gs.completedLaps >= (gs.currentPlan?.circle || 0)) {
// 当前计划完成,切换到下一个
gs.currentPlanIndex++
if (gs.currentPlanIndex >= gs.plans.length) {
// 分组全部完成
gs.isCompleted = true
gs.currentPlan = null
} else {
// 开始下一个计划
gs.currentPlan = gs.plans[gs.currentPlanIndex]
gs.countdown = gs.currentPlan.target
gs.completedLaps = 0
}
} else {
// 进入休息
gs.isResting = true
gs.restCountdown = gs.currentPlan?.rest || 0
}
}
}
})
if (allDone) {
stopTimer()
}
}, 10)
} }
const pauseTraining = () => { const pauseTraining = () => {
groupStates.value.forEach(gs => {
gs.isRunning = false
})
stopTimer() stopTimer()
} }
@@ -326,19 +403,20 @@
stopTimer() stopTimer()
const data = groupStates.value.map(gs => ({ const data = groupStates.value.map(gs => ({
groupName: gs.groupName, groupName: gs.groupName,
allTime: gs.allTime,
group: gs.plans group: gs.plans
})) }))
initGroups(data) initGroups(data)
} }
const formatCountdown = (seconds: number): string => { const formatCountdown = (seconds : number) : string => {
const s = Math.max(0, Math.floor(seconds)) const s = Math.max(0, Math.floor(seconds))
const m = Math.floor(s / 60) const m = Math.floor(s / 60)
const sec = s % 60 const sec = s % 60
return `${m.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}` return `${m.toString().padStart(2, '0')}${sec.toString().padStart(2, '0')}`
} }
const formatTotalDuration = (seconds: number): string => { const formatTotalDuration = (seconds : number) : string => {
if (seconds === 0) return '0分0秒' if (seconds === 0) return '0分0秒'
const m = Math.floor(seconds / 60) const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60) const s = Math.floor(seconds % 60)
@@ -363,6 +441,8 @@
} }
.header-section { .header-section {
position: relative;
.header-content { .header-content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -371,11 +451,18 @@
} }
.project-name { .project-name {
font-size: 36rpx; font-size: 28rpx;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
} }
.header-action {
position: absolute;
top: 24rpx;
right: 24rpx;
padding: 4rpx;
}
.total-duration { .total-duration {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -464,7 +551,7 @@
} }
.group-name { .group-name {
font-size: 30rpx; font-size: 22rpx;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
} }
@@ -493,10 +580,10 @@
} }
.plan-name { .plan-name {
font-size: 34rpx; font-size: 28rpx;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 16rpx; margin-bottom: 8rpx;
} }
/* 圈数指示器 */ /* 圈数指示器 */
@@ -550,40 +637,82 @@
font-size: 72rpx; font-size: 72rpx;
font-weight: bold; font-weight: bold;
font-family: 'Helvetica Neue', monospace; font-family: 'Helvetica Neue', monospace;
color: #4a90e2; color: #000;
letter-spacing: 2rpx; letter-spacing: 2rpx;
} }
&.resting .countdown-number { &.resting .countdown-number {
color: #ff9800; color: red;
animation: flash 0.5s ease-in-out infinite; animation: flash 0.5s ease-in-out infinite;
} }
} }
.plan-details { .plan-details {
display: flex; display: flex;
justify-content: space-around; justify-content: center;
margin-top: 20rpx;
padding-top: 16rpx;
border-top: 1rpx dashed #e0e0e0;
}
.detail-box {
display: flex;
flex-direction: column;
align-items: center; align-items: center;
gap: 6rpx; gap: 16rpx;
} }
.detail-label { .detail-badge {
font-size: 22rpx; display: inline-flex;
color: #999; align-items: center;
gap: 4rpx;
font-size: 24rpx;
color: #666;
background: #f5f7fa;
padding: 8rpx 4rpx;
border-radius: 24rpx;
} }
.detail-value { .detail-badge-tag {
font-size: 28rpx; width: 32rpx;
font-weight: 600; height: 32rpx;
color: #333; background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
border-radius: 50%;
font-size: 20rpx;
color: #fff;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
/* 分组控制按钮 */
.group-controls {
display: flex;
justify-content: space-between;
gap: 20rpx;
padding-top: 16rpx;
}
.group-control-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 24rpx;
border-radius: 24rpx;
font-size: 24rpx;
color: #fff;
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
}
}
.group-start-btn {
background: linear-gradient(135deg, #52c41a 0%, #389e0d 100%);
}
.group-pause-btn {
background: linear-gradient(135deg, #faad14 0%, #d48806 100%);
}
.group-reset-btn {
background: linear-gradient(135deg, #ff4d4f 0%, #d9363e 100%);
} }
.plan-completed { .plan-completed {
@@ -675,7 +804,168 @@
} }
@keyframes flash { @keyframes flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; } 0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
/* 横屏适配 */
@media (orientation: landscape) {
.hunyang-container {
padding: 16rpx;
padding-bottom: 140rpx;
}
.card-section {
padding: 16rpx 20rpx;
margin-bottom: 12rpx;
}
.header-section {
.header-content {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.header-action {
position: static;
}
.project-name {
font-size: 34rpx;
letter-spacing: 2rpx;
}
}
.section-title {
font-size: 26rpx;
margin-bottom: 12rpx;
letter-spacing: 1rpx;
}
.groups-list {
flex-direction: row;
flex-wrap: wrap;
gap: 16rpx;
}
.group-card {
padding: 16rpx 20rpx;
flex: 1;
min-width: 40%;
max-width: 50%;
box-sizing: border-box;
}
.group-header {
margin-bottom: 10rpx;
}
.group-name {
font-size: 22rpx;
letter-spacing: 1rpx;
}
.group-plan-index {
font-size: 22rpx;
letter-spacing: 1rpx;
}
.plan-name {
font-size: 28rpx;
margin-bottom: 8rpx;
letter-spacing: 1rpx;
}
.laps-indicator {
margin-bottom: 10rpx;
gap: 10rpx;
.lap-dot {
width: 14rpx;
height: 14rpx;
}
}
.countdown-box {
margin: 10rpx 0;
.countdown-number {
font-size: 60rpx;
letter-spacing: 4rpx;
}
}
.phase-hint {
margin-bottom: 10rpx;
padding: 6rpx 16rpx;
.phase-text {
font-size: 22rpx;
letter-spacing: 1rpx;
}
}
.plan-details {
gap: 12rpx;
}
.detail-badge {
font-size: 24rpx;
padding: 6rpx 14rpx;
letter-spacing: 1rpx;
}
.group-controls {
padding-top: 10rpx;
gap: 16rpx;
justify-content: center;
}
.group-control-btn {
font-size: 24rpx;
padding: 10rpx 28rpx;
letter-spacing: 2rpx;
gap: 6rpx;
}
.plan-completed {
padding: 20rpx;
gap: 10rpx;
.completed-label {
font-size: 26rpx;
letter-spacing: 2rpx;
}
}
.bottom-controls {
padding: 10rpx 32rpx;
.controls-inner {
max-width: 100%;
gap: 24rpx;
justify-content: center;
}
}
.control-btn {
padding: 7rpx 32rpx;
flex: none;
min-width: 140rpx;
gap: 8rpx;
}
.btn-text {
font-size: 18rpx;
letter-spacing: 2rpx;
}
} }
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,35 @@
<template> <template>
<view class="segmentation-container"> <view class="segmentation-container">
<!-- 配置区域 -->
<view class="config-section">
<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>
</view>
<view class="config-info">
<view class="info-text">总距离: {{ totalDistance }} ({{ segmentCount }} × {{ segmentDistance }}) {{ startMode==0?'一起出发':'间隔出发' }} {{ startMode==0?'':' · '+intervalTime+'s' }} </view>
<view class="info-text" style="margin-top: 14rpx;" >启动模式: {{ startMode==0?'一起出发':'间隔出发' }} {{ startMode==0?'':' · '+intervalTime+'s' }} </view>
</view>
</view>
<!-- 总计时器区域 --> <!-- 总计时器区域 -->
<view class="total-time-section"> <view class="total-time-section" style="position: relative;">
<view class="timer-bar"> <view class="timer-bar">
<view class="timer-time">{{ formatTime(currentTime) }}</view> <view class="timer-time">{{ formatTime(currentTime) }}</view>
</view> </view>
<view class="config-info">
<view class="info-text">总距离: {{ totalDistance }} ({{ segmentCount }} × {{ segmentDistance }})
{{ startMode==0?'一起出发':'间隔出发' }} {{ startMode==0?'':' · '+intervalTime+'s' }} </view>
</view>
<view class="" style="position: absolute; top: 20rpx; right: 20rpx; ">
<u-icon @click="Service.GoPage('/pages/userFunc/setCourse?id='+planId+'&type=2')" name="setting"
size="24" color="#1890ff"></u-icon>
</view>
</view> </view>
<!-- 学生列表 --> <!-- 学生列表 -->
<view class="students-section" style="margin-top: 20rpx;" > <view class="students-section" style="margin-top: 20rpx;">
<view class="section-header"> <!-- <view class="section-header">
<text class="section-title">学生列表</text> <text class="section-title">学生列表</text>
<text class="student-count">{{ students.length }}位学生</text> <text class="student-count">{{ students.length }}位学生</text>
</view> </view> -->
<view class="students-list"> <view class="students-list">
<view v-for="(student, index) in students" :key="student.id" class="student-item" <view v-for="(student, index) in students" :key="student.id" class="student-item"
:class="{ 'not-started': !student.hasStarted }"> :class="{ 'not-started': !student.hasStarted }">
<view class="student-header"> <view class="student-header">
<view class="student-number" :class="{ 'started': student.hasStarted }">{{ student.number }} <view class="student-number" @click="showStudentRecords(student)"
:class="{ 'started': student.hasStarted }">{{ student.number }}
</view> </view>
<view class="student-info"> <view class="student-info">
<text class="student-name">{{ student.name }}</text> <text class="student-name">{{ student.name }}</text>
@@ -51,17 +46,14 @@
<view class="student-buttons"> <view class="student-buttons">
<button class="record-btn" :disabled="!student.hasStarted" <button class="record-btn" :disabled="!student.hasStarted"
@click="recordStudentSegment(student)"> @click="recordStudentSegment(student)">
<text class="btn-text">记录</text> <text class="btn-text">记录</text>
</button> </button>
<button class="view-btn" @click="showStudentRecords(student)"> <view @click="resetStudent(student)" class="">
<u-icon name="reload" :bold="true" size="24" color="#1890ff"></u-icon>
<text class="btn-text">详情</text> </view>
</button> <!-- <button class="reset-btn" @click="resetStudent(student)">
<button class="reset-btn" @click="resetStudent(student)">
<text class="btn-text">重置</text> <text class="btn-text">重置</text>
</button> </button> -->
</view> </view>
</view> </view>
</view> </view>
@@ -206,12 +198,12 @@
}) })
} }
// 格式化时间显示 // 格式化时间显示(分:秒.毫秒)
const formatTime = (seconds : number) : string => { const formatTime = (seconds : number) : string => {
const hours = Math.floor(seconds / 3600) const mins = Math.floor(seconds / 60)
const mins = Math.floor((seconds % 3600) / 60)
const secs = Math.floor(seconds % 60) const secs = Math.floor(seconds % 60)
return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` const millis = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${millis.toString().padStart(2, '0')}`
} }
// 格式化持续时间 // 格式化持续时间
@@ -261,11 +253,12 @@
return student.segments[lastIndex].time - student.segments[lastIndex - 1].time return student.segments[lastIndex].time - student.segments[lastIndex - 1].time
} }
// 简化时间格式化(分:秒) // 简化时间格式化(分:秒.毫秒
const formatSimpleTime = (seconds : number) : string => { const formatSimpleTime = (seconds : number) : string => {
const mins = Math.floor(seconds / 60) const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60) const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}` const millis = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${millis.toString().padStart(2, '0')}`
} }
// 开始计时 // 开始计时
@@ -278,7 +271,7 @@
timerInterval = setInterval(() => { timerInterval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000 const elapsed = (Date.now() - startTime) / 1000
currentTime.value = elapsed currentTime.value = elapsed
}, 10) }, 1)
// 间隔出发模式 // 间隔出发模式
if (startMode.value === 1) { if (startMode.value === 1) {
@@ -333,7 +326,6 @@
// 记录学生分段 // 记录学生分段
const recordStudentSegment = (student : any) => { const recordStudentSegment = (student : any) => {
console.log(22222);
if (!isRunning.value) { if (!isRunning.value) {
Service.Msg('请先开始计时') Service.Msg('请先开始计时')
return return
@@ -357,7 +349,7 @@
} }
student.segments.push({ student.segments.push({
time: recordTime time: Number(recordTime).toFixed(3)
}) })
Service.Msg(`${student.name}${student.segments.length}段已记录`) Service.Msg(`${student.name}${student.segments.length}段已记录`)
@@ -399,13 +391,6 @@
Service.Msg('暂无数据可保存!') Service.Msg('暂无数据可保存!')
return return
} }
if (!isSave) {
Service.Msg('存在学生未完成!')
return
}
let data = [ let data = [
{ {
studentId: "", studentId: "",
@@ -429,18 +414,29 @@
}) })
}) })
console.log(data);
PlanService.AddPlanLog(planId.value, '分段项目', '', JSON.stringify(data), '').then(res => { uni.showModal({
if (res.code == 0) { title: '提示', // 对话框标题
Service.Msg('提交成功!') content: '请确定提交数据!', // 显示的内容
setTimeout(()=>{ showCancel: true, // 是否显示取消按钮
Service.GoPageBack() cancelText: '取消', // 取消按钮的文字
},1000) confirmText: '确定', // 确认按钮的文字
} else { success: function (res) {
Service.Msg(res.msg) if (res.confirm) {
PlanService.AddPlanLog(planId.value, '分段项目', '', JSON.stringify(data), '').then(res => {
if (res.code == 0) {
Service.Msg('提交成功!')
} else {
Service.Msg(res.msg)
}
})
} else if (res.cancel) {
console.log('用户点击取消');
}
} }
}) });
} }
// 显示学生记录弹窗 // 显示学生记录弹窗
@@ -474,41 +470,7 @@
padding-bottom: 180rpx; padding-bottom: 180rpx;
} }
/* 配置区域 */
.config-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.config-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.config-info {
background-color: #e6f7ff;
border-radius: 8rpx;
padding: 16rpx 20rpx;
.info-text {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
}
}
/* 总计时器区域 */ /* 总计时器区域 */
.total-time-section { .total-time-section {
@@ -516,6 +478,20 @@
border-radius: 20rpx; border-radius: 20rpx;
padding: 30rpx; padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05); box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.config-info {
margin-top: 10rpx;
background-color: #e6f7ff;
border-radius: 8rpx;
padding: 16rpx 20rpx;
text-align: center;
.info-text {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
}
} }
.timer-bar { .timer-bar {
@@ -599,6 +575,7 @@
gap: 12rpx; gap: 12rpx;
.student-name { .student-name {
width: 120rpx;
font-size: 30rpx; font-size: 30rpx;
font-weight: 500; font-weight: 500;
color: #333; color: #333;
@@ -617,7 +594,7 @@
} }
.total-time-text { .total-time-text {
font-size: 24rpx; font-size: 30rpx;
color: #999; color: #999;
font-family: 'DIN Alternate', monospace; font-family: 'DIN Alternate', monospace;
font-weight: 500; font-weight: 500;
@@ -652,7 +629,7 @@
.view-btn, .view-btn,
.reset-btn { .reset-btn {
height: 56rpx; height: 56rpx;
padding: 0 20rpx; padding: 0 35rpx;
border-radius: 8rpx; border-radius: 8rpx;
border: none; border: none;
font-size: 24rpx; font-size: 24rpx;

View File

@@ -121,6 +121,9 @@
</view> </view>
<view class="student-info"> <view class="student-info">
<view class="student-name">{{ student.name }}</view> <view class="student-name">{{ student.name }}</view>
<view v-if="planId && type==1 " class="student-time" >
pb:{{ student.quicklyTime}}
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -766,6 +769,14 @@
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.student-time{
font-size: 24rpx;
color: #888;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.student-meta { .student-meta {
display: flex; display: flex;

View File

@@ -19,17 +19,19 @@
</view> </view>
<view v-else class="students-list"> <view v-else class="students-list">
<view v-for="(student, index) in students" :key="student.id" class="student-card"> <view v-for="(student, index) in students" :key="student.id" class="student-card" >
<view class="student-info"> <view class="student-info">
<view class="student-avatar"> <view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text> <text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view> </view>
<view class="student-details"> <view class="student-details">
<view class="detail-row"> <view class="detail-row" style="justify-content: space-between;">
<text class="student-name">{{ student.name }}</text> <view style="display: flex; align-items: center; gap: 8rpx;">
<view class="student-gender" :class="student.sex === '男' ? 'male' : 'female'"> <text class="student-name">{{ student.name }}</text>
<u-icon :name="student.sex === '男' ? 'man' : 'woman'" size="14"></u-icon> <view class="student-gender" :class="student.sex === '男' ? 'male' : 'female'">
{{ student.sex }} <u-icon :name="student.sex === '男' ? 'man' : 'woman'" size="14"></u-icon>
{{ student.sex }}
</view>
</view> </view>
</view> </view>
<view class="detail-row"> <view class="detail-row">
@@ -48,18 +50,22 @@
<text class="detail-label">住址</text> <text class="detail-label">住址</text>
<text class="detail-value address-text">{{ student.address }}</text> <text class="detail-value address-text">{{ student.address }}</text>
</view> </view>
<view class="detail-row">
<text class="detail-label">状态</text>
<up-switch v-model="student.active" size="16" @change="confirmDelete(student)" ></up-switch>
</view>
</view> </view>
</view> </view>
<view class="student-actions"> <view class="student-actions">
<view class="action-btn edit-btn" @click="openEditModal(student)"> <view class="action-btn edit-btn" @click="openEditModal(student)">
<u-icon name="edit-pen" size="18" color="#1890ff"></u-icon> <u-icon name="edit-pen" size="18" color="#1890ff"></u-icon>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<view class="" style="width: 100%; height: 100rpx;" > <view class="" style="width: 100%; height: 100rpx;">
</view> </view>
<!-- 底部添加按钮 --> <!-- 底部添加按钮 -->
@@ -87,15 +93,19 @@
<view class="form-group"> <view class="form-group">
<text class="form-label">性别</text> <text class="form-label">性别</text>
<view class="gender-selector"> <view class="gender-selector">
<view class="gender-option" :class="{ active: formData.gender === '男' }" @click="formData.gender = '男'"> <view class="gender-option" :class="{ active: formData.gender === '男' }"
@click="formData.gender = '男'">
<view class="gender-icon-box"> <view class="gender-icon-box">
<u-icon name="man" size="20" :color="formData.gender === '男' ? '#fff' : '#1890ff'"></u-icon> <u-icon name="man" size="20"
:color="formData.gender === '男' ? '#fff' : '#1890ff'"></u-icon>
</view> </view>
<text></text> <text></text>
</view> </view>
<view class="gender-option" :class="{ active: formData.gender === '女' }" @click="formData.gender = '女'"> <view class="gender-option" :class="{ active: formData.gender === '女' }"
@click="formData.gender = '女'">
<view class="gender-icon-box"> <view class="gender-icon-box">
<u-icon name="woman" size="20" :color="formData.gender === '女' ? '#fff' : '#fa8c16'"></u-icon> <u-icon name="woman" size="20"
:color="formData.gender === '女' ? '#fff' : '#fa8c16'"></u-icon>
</view> </view>
<text></text> <text></text>
</view> </view>
@@ -196,12 +206,12 @@
} }
// 打开编辑弹窗 // 打开编辑弹窗
const openEditModal = (student :any) => { const openEditModal = (student : any) => {
editingStudent.value = student editingStudent.value = student
formData.value = { formData.value = {
id: student.studentId, id: student.studentId,
name: student.name, name: student.name,
gender: student.sex?student.sex: '男', gender: student.sex ? student.sex : '男',
birthDate: student.birthday, birthDate: student.birthday,
school: student.school, school: student.school,
address: student.address address: student.address
@@ -241,7 +251,11 @@
status.value = 'loading' status.value = 'loading'
studentService.GetStudentListPage(page.value.toString()).then(res => { studentService.GetStudentListPage(page.value.toString()).then(res => {
if (res.code == 0) { if (res.code == 0) {
students.value = [...students.value, ...res.data] const list = res.data.map((item : any) => ({
...item,
active:item.status==1?true:false
}))
students.value = [...students.value, ...list]
status.value = res.data.length == 10 ? 'loadmore' : 'nomore' status.value = res.data.length == 10 ? 'loadmore' : 'nomore'
page.value++ page.value++
} else { } else {
@@ -264,19 +278,19 @@
// 构造接口请求数据 // 构造接口请求数据
const requestData = { const requestData = {
studentId:formData.value.id?formData.value.id:'' , studentId: formData.value.id ? formData.value.id : '',
name: formData.value.name, name: formData.value.name,
sex: formData.value.gender, sex: formData.value.gender,
birthday: formData.value.birthDate, birthday: formData.value.birthDate,
school: formData.value.school?formData.value.school:'' , school: formData.value.school ? formData.value.school : '',
address: formData.value.address ?formData.value.address:'' address: formData.value.address ? formData.value.address : ''
} }
console.log(requestData); console.log(requestData);
// 调用添加学员接口 // 调用添加学员接口
studentService.Add(requestData).then((content) => { studentService.Add(requestData).then((content) => {
if (content.code == 0) { if (content.code == 0) {
Service.Msg(formData.value.id?'修改成功!':'添加成功!') Service.Msg(formData.value.id ? '修改成功!' : '添加成功!')
getData() getData()
} else { } else {
Service.Msg(content.msg) Service.Msg(content.msg)
@@ -287,17 +301,9 @@
} }
// 确认删除 // 确认删除
const confirmDelete = (student : Student) => { const confirmDelete = (student : any) => {
uni.showModal({
title: '确认删除', deleteStudent(student.studentId)
content: `确定要删除学员「${student.name}」吗?`,
confirmColor: '#ff4d4f',
success: (res) => {
if (res.confirm) {
deleteStudent(student.id)
}
}
})
} }
// 删除学员 // 删除学员
@@ -305,7 +311,7 @@
studentService.Delete(id).then(res => { studentService.Delete(id).then(res => {
if (res.code == 0) { if (res.code == 0) {
students.value = students.value.filter(s => s.id !== id) students.value = students.value.filter(s => s.id !== id)
Service.Msg('删除成功', 'success') Service.Msg('修改成功!')
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }
@@ -332,6 +338,7 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
.title { .title {
font-size: 36rpx; font-size: 36rpx;
font-weight: 700; font-weight: 700;
@@ -501,10 +508,10 @@
} }
&.delete-btn { &.delete-btn {
background-color: #fff1f0; background-color: #f3f3f3;
&:active { &:active {
background-color: #ffccc7; background-color: #f3f3f3;
transform: scale(0.92); transform: scale(0.92);
} }
} }

View File

@@ -11,17 +11,13 @@
</view> </view>
<!-- 计划信息 --> <!-- 计划信息 -->
<view class="plan-info" v-if="planId"> <view class="plan-info" v-if="planId">
<text class="plan-title">项目名称: {{ planName }}</text> <text class="plan-title">{{ planName }} · {{ stopwatchMode=='interval'?'间隔触发':'一起出发' }} ·
{{ groupedAthletes.length }}{{ athletes.length }}位选手</text>
</view> </view>
</view> </view>
<!-- 选手列表 --> <!-- 选手列表 -->
<view class="athletes-section"> <view class="athletes-section">
<view class="section-header">
<text class="section-title">{{ stopwatchMode=='interval'?'间隔触发':'一起出发' }}·{{ groupedAthletes.length }}</text>
<text class="athlete-count">{{ athletes.length }}位选手</text>
</view>
<view class="athletes-list"> <view class="athletes-list">
<view v-for="group in groupedAthletes" :key="group.groupNumber" class="group-wrapper"> <view v-for="group in groupedAthletes" :key="group.groupNumber" class="group-wrapper">
<view class="group-header"> <view class="group-header">
@@ -42,9 +38,9 @@
</view> </view>
<view class="time-wrapper"> <view class="time-wrapper">
<text <text
class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text> class="athlete-time">{{ !athlete.time? ' 000000 ':formatTime(athlete.time) }}</text>
<text class="best-time">最快: <text class="best-time">PB:
{{ athlete.bestTime !== null ? athlete.quicklyTime: '无' }}</text> {{ athlete.studentQuicklyTime !== null ? athlete.studentQuicklyTime: '无' }}</text>
</view> </view>
<view class="" style="display: flex;align-items: center; gap: 10rpx;"> <view class="" style="display: flex;align-items: center; gap: 10rpx;">
@@ -60,12 +56,13 @@
<view class="" style="width: 100%; height: 100rpx;"></view> <view class="" style="width: 100%; height: 100rpx;"></view>
<!-- 右下角悬浮提交按钮 --> <!-- 右下角悬浮提交按钮 -->
<view class="fab-submit" @click="submitData"> <!-- <view class="fab-submit" @click="submitData">
<text class="fab-text">提交</text> <text class="fab-text">提交</text>
</view> </view> -->
<!-- 记录提示弹窗 --> <!-- 记录提示弹窗 -->
<u-popup :show="showRecord" mode="center" :round="20" :closeable="true" :safeAreaInsetBottom='false' @close="showRecord=false" closeOnClickOverlay> <u-popup :show="showRecord" mode="center" :round="20" :closeable="true" :safeAreaInsetBottom='false'
@close="showRecord=false" closeOnClickOverlay>
<view class="record-notice-modal"> <view class="record-notice-modal">
<view class="notice-header"> <view class="notice-header">
<text class="notice-title">恭喜以下学生打破记录</text> <text class="notice-title">恭喜以下学生打破记录</text>
@@ -80,36 +77,39 @@
<view v-for="(item,index) in record" class="table-row"> <view v-for="(item,index) in record" class="table-row">
<text class="row-cell index-cell">{{ index+1 }}</text> <text class="row-cell index-cell">{{ index+1 }}</text>
<text class="row-cell name-cell">{{ item.name }}</text> <text class="row-cell name-cell">{{ item.name }}</text>
<text class="row-cell time-cell">{{ item.quicklyTime }}</text> <text class="row-cell time-cell">{{ item.studentQuicklyTime }}</text>
</view> </view>
</scroll-view> </scroll-view>
</view> </view>
</view> </view>
</u-popup> </u-popup>
<!-- 可拖动悬浮球 -->
<view class="float-ball" @click="ballClick(isRunning)" :style="ballStyle" @touchstart="touchStart" @touchmove="touchMove"
@touchend="touchEnd">
<view class="ball-shine"></view>
<!-- @click="startTimer" @click="recordNextAthlete" -->
<view v-if="!isRunning" class="ball-text">开始</view>
<view v-if="isRunning" class="ball-text">记录</view>
</view>
<!-- 底部控制按钮 --> <!-- 底部控制按钮 -->
<view class="control-buttons"> <view class="control-buttons">
<view class="button-group" :class="{ 'record-group': isRunning }"> <button class="ctrl-btn btn-submit" @click="submitData">
<button v-if="!isRunning" class="start-btn" @click="startTimer"> <u-icon name="checkmark-circle" size="24" color="#fff"></u-icon>
<u-icon name="play-right" size="24" color="#fff"></u-icon> <text class="btn-text">提交</text>
<text class="btn-text">开始</text> </button>
</button> <button class="ctrl-btn btn-data" @click="Service.GoPage('/pages/dataAnalyze/timingAnalze')">
<u-icon name="list-dot" size="24" color="#fff"></u-icon>
<button v-if="isRunning" class="record-btn" @click="recordNextAthlete"> <text class="btn-text">数据</text>
<u-icon name="checkmark-circle" size="24" color="#fff"></u-icon> </button>
<text class="btn-text">记录</text> <button v-if="!isRunning" class="ctrl-btn btn-reset" @click="resetAll">
</button> <u-icon name="reload" size="24" color="#fff"></u-icon>
</view> <text class="btn-text">重置</text>
<view class="button-group" :class="{ 'pause-group': isRunning }"> </button>
<button v-if="!isRunning" class="reset-all-btn" @click="resetAll"> <button v-if="isRunning" class="ctrl-btn btn-pause" @click="stopTimer">
<u-icon name="reload" size="24" color="#fff"></u-icon> <text class="btn-text">暂停</text>
<text class="btn-text">重置</text> </button>
</button>
<button v-if="isRunning" class="reset-all-btn" @click="stopTimer">
<text class="btn-text" style="font-size: 28rpx;">暂停</text>
</button>
</view>
</view> </view>
@@ -154,7 +154,8 @@
const groupSize = ref('0') const groupSize = ref('0')
// 创建播放器
const video = uni.createInnerAudioContext()
// 计时器状态 // 计时器状态
const isRunning = ref(false) const isRunning = ref(false)
@@ -169,11 +170,46 @@
const stopwatchMode = ref<'interval' | 'together'>('interval') const stopwatchMode = ref<'interval' | 'together'>('interval')
// 间隔时间(秒) // 间隔时间(秒)
const intervalTime = ref('0') const intervalTime = ref('0')
let record=ref<Array<any>>([]) let record = ref<Array<any>>([])
let showRecord=ref(false) let showRecord = ref(false)
// 已提示过出发的组号集合(间隔模式用)
const notifiedGroupNumbers = ref<Set<number>>(new Set())
// 可拖动悬浮球
const FLOAT_BALL_KEY = 'swiming_float_ball_position'
const ballPosition = ref({ x: 20, y: 300 })
const startPosition = ref({ x: 0, y: 0 })
const touchStartPoint = ref({ x: 0, y: 0 })
const ballSize = ref({ w: 50, h: 50 })
const ballStyle = computed(() => {
return `left:${ballPosition.value.x}px;top:${ballPosition.value.y}px;`
})
onLoad((data : any) => { onLoad((data : any) => {
planId.value = data.id planId.value = data.id
const sys = uni.getSystemInfoSync()
ballSize.value.w = sys.windowWidth * 50 / 375
ballSize.value.h = ballSize.value.w
ballPosition.value = {
x: 20,
y: sys.windowHeight - ballSize.value.h - 200
}
const saved = uni.getStorageSync(FLOAT_BALL_KEY)
if (saved) {
try {
const pos = JSON.parse(saved)
if (typeof pos.x === 'number' && typeof pos.y === 'number') {
ballPosition.value = {
x: Math.max(0, Math.min(pos.x, sys.windowWidth - ballSize.value.w)),
y: Math.max(0, Math.min(pos.y, sys.windowHeight - ballSize.value.h))
}
}
} catch (e) { }
}
}) })
onShow(() => { onShow(() => {
@@ -196,6 +232,21 @@
}) })
} }
// 悬浮球事件
const ballClick=( data:any)=>{
if(data){
recordNextAthlete()
}else{
startTimer()
}
}
// 播报
const speak=()=>{
video.src='/static/icon/video.mp3'
video.play()
}
// 计算分组 // 计算分组
const groupedAthletes = computed<Group[]>(() => { const groupedAthletes = computed<Group[]>(() => {
const size = parseInt(groupSize.value) || 2 const size = parseInt(groupSize.value) || 2
@@ -243,7 +294,7 @@
const mins = Math.floor(seconds / 60) const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60) const secs = Math.floor(seconds % 60)
const ms = Math.floor((seconds % 1) * 100) const ms = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(2, '0')}` return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${ms.toString().padStart(2, '0')}`
} }
// 开始计时 // 开始计时
@@ -251,12 +302,26 @@
if (isRunning.value) return if (isRunning.value) return
isRunning.value = true isRunning.value = true
notifiedGroupNumbers.value.clear()
startTime = Date.now() - currentTime.value * 1000 startTime = Date.now() - currentTime.value * 1000
timerInterval = setInterval(() => { timerInterval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000 const elapsed = (Date.now() - startTime) / 1000
currentTime.value = elapsed currentTime.value = elapsed
// 间隔模式下,检查是否有新组到达出发时间
if (stopwatchMode.value === 'interval') {
const interval = parseFloat(intervalTime.value) || 10
groupedAthletes.value.forEach(group => {
const groupOffset = (group.groupNumber - 1) * interval
if (elapsed >= groupOffset && !notifiedGroupNumbers.value.has(group.groupNumber)) {
notifiedGroupNumbers.value.add(group.groupNumber)
Service.Msg(`${group.groupNumber}组出发了`)
speak()
}
})
}
// 更新未完成选手的时间(考虑偏移量) // 更新未完成选手的时间(考虑偏移量)
athletes.value.forEach(athlete => { athletes.value.forEach(athlete => {
if (!athlete.finished) { if (!athlete.finished) {
@@ -264,7 +329,7 @@
athlete.time = Math.max(0, elapsed - offset) athlete.time = Math.max(0, elapsed - offset)
} }
}) })
}, 16) }, 30)
} }
// 停止计时 // 停止计时
@@ -375,6 +440,7 @@
stopTimer() stopTimer()
currentTime.value = 0 currentTime.value = 0
currentAthleteIndex.value = 0 currentAthleteIndex.value = 0
notifiedGroupNumbers.value.clear()
athletes.value.forEach(athlete => { athletes.value.forEach(athlete => {
athlete.time = 0 athlete.time = 0
@@ -388,7 +454,7 @@
const mins = Math.floor(seconds / 60) const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60) const secs = Math.floor(seconds % 60)
const ms = Math.floor((seconds % 1) * 1000) const ms = Math.floor((seconds % 1) * 1000)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}` return `${mins.toString().padStart(2, '0')}${secs.toString().padStart(2, '0')}${ms.toString().padStart(3, '0')}`
} }
// 提交数据 // 提交数据
@@ -400,10 +466,6 @@
Service.Msg('暂无数据可提交') Service.Msg('暂无数据可提交')
return return
} }
// if(!allFinished){
// Service.Msg('还用学生未完成!')
// return
// }
stopTimer() stopTimer()
let data = [{ let data = [{
planName: "", planName: "",
@@ -417,35 +479,74 @@
planName: planName.value, planName: planName.value,
studentId: item.studentId, studentId: item.studentId,
studentName: item.name, studentName: item.name,
time: formatMinuteTime(item.time) time: item.time
}) })
}) })
uni.showModal({
title: '提示', // 对话框标题
content: '请确定提交数据!', // 显示的内容
showCancel: true, // 是否显示取消按钮
cancelText: '取消', // 取消按钮的文字
confirmText: '确定', // 确认按钮的文字
success: function (res) {
if (res.confirm) {
PlanService.AddPlanLog(planId.value, '计时项目', JSON.stringify(data), '', '', '').then(res => {
if (res.code == 0) {
Service.Msg('提交成功!')
stopTimer()
currentTime.value = 0
currentAthleteIndex.value = 0
PlanService.AddPlanLog(planId.value,'计时项目',JSON.stringify(data),'','','').then(res=>{ athletes.value.forEach(athlete => {
if(res.code==0){ athlete.time = 0
Service.Msg('提交成功!') athlete.finished = false
stopTimer() athlete.startOffset = undefined
currentTime.value = 0 })
currentAthleteIndex.value = 0
athletes.value.forEach(athlete => { if (res.data.record.length > 0) {
athlete.time = 0 record.value = res.data.record
athlete.finished = false showRecord.value = true
athlete.startOffset = undefined }
}) } else {
Service.Msg(res.msg)
if(res.data.record.length>0){ }
record.value=res.data.record })
showRecord.value=true } else if (res.cancel) {
console.log('用户点击取消');
} }
}else{
Service.Msg(res.msg)
} }
}) });
} }
// 悬浮球拖动事件
const touchStart = (e : any) => {
const touch = e.touches[0]
touchStartPoint.value = { x: touch.clientX, y: touch.clientY }
startPosition.value = { ...ballPosition.value }
}
const touchMove = (e : any) => {
const touch = e.touches[0]
const deltaX = touch.clientX - touchStartPoint.value.x
const deltaY = touch.clientY - touchStartPoint.value.y
ballPosition.value = {
x: startPosition.value.x + deltaX,
y: startPosition.value.y + deltaY
}
}
const touchEnd = () => {
const sys = uni.getSystemInfoSync()
ballPosition.value = {
x: Math.max(0, Math.min(ballPosition.value.x, sys.windowWidth - ballSize.value.w)),
y: Math.max(0, Math.min(ballPosition.value.y, sys.windowHeight - ballSize.value.h))
}
uni.setStorageSync(FLOAT_BALL_KEY, JSON.stringify(ballPosition.value))
}
// 页面卸载时清理计时器 // 页面卸载时清理计时器
onUnmounted(() => { onUnmounted(() => {
if (timerInterval) { if (timerInterval) {
@@ -479,6 +580,10 @@
font-size: 80rpx; font-size: 80rpx;
font-weight: bold; font-weight: bold;
color: #ff0000; color: #ff0000;
font-family: 'DIN Alternate', monospace;
min-width: 400rpx;
display: inline-block;
text-align: center;
} }
.plan-info { .plan-info {
@@ -534,7 +639,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 24rpx 28rpx; padding: 10rpx 28rpx;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%); background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
border-left: 6rpx solid #1890ff; border-left: 6rpx solid #1890ff;
@@ -712,143 +817,111 @@
/* 底部控制按钮 */ /* 底部控制按钮 */
.control-buttons { .control-buttons {
display: flex; display: flex;
gap: 24rpx; gap: 20rpx;
justify-content: center; justify-content: space-between;
position: fixed; position: fixed;
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #ffffff 100%); background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #ffffff 100%);
padding: 32rpx 40rpx; padding: 24rpx 32rpx;
// padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid rgba(0, 0, 0, 0.06); border-top: 1rpx solid rgba(0, 0, 0, 0.06);
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.08); box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.08);
.button-group { .ctrl-btn {
flex: 1; flex: 1;
display: flex; height: 92rpx;
justify-content: center; border-radius: 24rpx;
}
.record-group {
flex: 4;
}
.pause-group {
flex: 1;
}
.back-btn,
.start-btn,
.pause-btn,
.reset-all-btn,
.record-btn,
.submit-btn {
flex: 1;
height: 100rpx;
border-radius: 20rpx;
border: none; border: none;
font-size: 32rpx; font-size: 30rpx;
font-weight: 600; font-weight: 600;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 12rpx; gap: 10rpx;
position: relative; transition: all 0.25s ease;
overflow: hidden; box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, transparent 100%);
border-radius: 20rpx 20rpx 0 0;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2rpx;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%);
}
:deep(.u-icon) {
position: relative;
z-index: 1;
}
}
.start-btn {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4);
&:active { &:active {
transform: scale(0.96); transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
} }
} }
.pause-btn { .btn-submit {
background: linear-gradient(135deg, #fa541c 0%, #ff7a45 100%); background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
color: #ffffff; color: #fff;
box-shadow: 0 6rpx 20rpx rgba(250, 84, 28, 0.4); box-shadow: 0 6rpx 18rpx rgba(250, 173, 20, 0.35);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(250, 84, 28, 0.3);
}
} }
.reset-all-btn { .btn-data {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%); background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
color: #ffffff; color: #fff;
box-shadow: 0 6rpx 20rpx rgba(24, 144, 255, 0.4); box-shadow: 0 6rpx 18rpx rgba(24, 144, 255, 0.35);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3);
}
} }
.back-btn { .btn-reset {
background: linear-gradient(135deg, #722ed1 0%, #9254de 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(114, 46, 209, 0.4);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(114, 46, 209, 0.3);
}
}
.record-btn {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff; color: #fff;
box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4); box-shadow: 0 6rpx 18rpx rgba(82, 196, 26, 0.35);
}
&:active { .btn-pause {
transform: scale(0.96); background: linear-gradient(135deg, #fa541c 0%, #ff7a45 100%);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3); color: #fff;
} box-shadow: 0 6rpx 18rpx rgba(250, 84, 28, 0.35);
} }
.btn-text { .btn-text {
font-size: 32rpx; font-size: 30rpx;
color: #ffffff; color: #ffffff;
font-weight: 600; font-weight: 600;
letter-spacing: 2rpx; letter-spacing: 2rpx;
}
}
/* 可拖动悬浮球 */
.float-ball {
position: fixed;
width: 120rpx;
height: 120rpx;
border-radius: 50%;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow:
0 8rpx 24rpx rgba(82, 196, 26, 0.45),
0 16rpx 40rpx rgba(0, 0, 0, 0.15),
inset 0 4rpx 10rpx rgba(255, 255, 255, 0.35);
z-index: 999;
touch-action: none;
transition: transform 0.2s ease;
&:active {
transform: scale(0.92);
}
.ball-shine {
position: absolute;
top: 20rpx;
left: 24rpx;
width: 36rpx;
height: 18rpx;
background: rgba(255, 255, 255, 0.4);
border-radius: 50%;
filter: blur(2rpx);
pointer-events: none;
}
.ball-text {
color: #fff;
font-size: 28rpx;
font-weight: 700;
letter-spacing: 2rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.15);
position: relative; position: relative;
z-index: 1; z-index: 1;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
} }
} }

BIN
src/static/icon/video.mp3 Normal file

Binary file not shown.