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 {
// protected static servesUrl: string = "http://192.168.0.142:5298";
// protected static imgUrl: string = "http://192.168.0.142:5298";
// protected static mediaUrl: string = "http://192.168.0.142:5298/";
// protected static servesUrl: string = "http://192.168.0.142:5300";
// protected static imgUrl: string = "http://192.168.0.142:5300";
// protected static mediaUrl: string = "http://192.168.0.142:5300/";
protected static servesUrl: string = "https://swimming.api.xypays.cn";
protected static imgUrl: string = "https://swimming.api.xypays.cn";

View File

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

View File

@@ -11,7 +11,31 @@
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
<!-- 项目选择弹窗 -->
<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">
@@ -46,9 +70,6 @@
<view v-for="student in projectStudents" :key="student.studentId"
:class="['student-item', { selected: selectedStudentIds.includes(student.studentId) }]"
@click="toggleStudent(student.studentId)">
<view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
<text class="student-name">{{ student.name }}</text>
<view class="check-icon" v-if="selectedStudentIds.includes(student.studentId)">
<u-icon name="checkmark" size="14" color="#fff"></u-icon>
@@ -58,7 +79,7 @@
</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="stat-item">
@@ -87,7 +108,7 @@
</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>
</template>
@@ -96,6 +117,7 @@
import { Service } from '@/Service/Service'
import { ref, computed } from 'vue'
import { PlanService } from '@/Service/swimming/PlanService'
import { studentService } from '@/Service/swimming/studentService'
const MAX_SELECTED_STUDENTS = 3
const chartColors = ['#52c41a', '#1890ff', '#faad14']
@@ -191,9 +213,10 @@
})
}
const selectProcetFunc = (e: any) => {
selectProcet.value = e.value[0].name
selectId.value = e.value[0].planId
const selectProcetFunc = (project: any) => {
selectProcet.value = project.name
selectId.value = project.planId
showProject.value = false
selectedStudentIds.value = []
begin.value = ''
end.value = ''
@@ -218,13 +241,14 @@
}
const loadProjectStudents = (projectId: string) => {
PlanService.GetPlanInfo(projectId).then(res => {
studentService.GetStudentList().then(res => {
if (res.code == 0) {
projectStudents.value = res.data.plan.users
projectStudents.value = res.data
} else {
Service.Msg(res.msg)
}
})
}
const toggleStudent = (studentId: string) => {
@@ -262,6 +286,7 @@
res.data.list.map((item: any) => {
lineChartData.value.categories.push(item.dayTime)
item.data.map((content: any, index: any) => {
xData.value[index].name = content.studentName
xData.value[index].data.push(content.time)
})
@@ -469,6 +494,8 @@
/* 图表区域 */
.chart-section {
margin-top: 4rpx;
position: relative;
z-index: 1;
}
.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>

File diff suppressed because it is too large Load Diff

View File

@@ -11,7 +11,7 @@
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
<up-picker v-model:show="showProject" keyName="name" valueName="planId" @confirm="selectProcetFunc" :columns="projectOptions"></up-picker>
<!-- 排名列表区域 -->
<view class="ranking-section" v-if="selectProcet && gradeList.length > 0">
@@ -53,6 +53,33 @@
<u-icon name="file-text" size="64" color="#d9d9d9"></u-icon>
<text class="empty-text">暂无排名数据</text>
</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>
</template>
@@ -100,8 +127,9 @@
}
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)
}
@@ -109,6 +137,7 @@
PlanService.GetRankData(projectId).then(res => {
if (res.code == 0) {
gradeList.value = res.data.list
showProject.value=false
} else {
Service.Msg(res.msg)
}
@@ -339,4 +368,141 @@
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>

File diff suppressed because it is too large Load Diff

View File

@@ -7,7 +7,7 @@
<text>筛选条件</text>
</view>
<view class="filter-item" @click="showProject = true">
<view class="filter-item" @click="showProjectPopup = true">
<text class="filter-label">项目</text>
<view class="filter-value">
<text class="value-text" :class="{ placeholder: !selectProcet }">{{ selectProcet || '请选择项目' }}</text>
@@ -36,7 +36,30 @@
</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">
@@ -46,7 +69,7 @@
</view>
<view v-for="(item,index) in tableData" :key="index">
<view class="day-title" @click="toggleDayCollapse(index)">
<text>{{ item.dayTime }}</text>
<text>{{ item.dayTime }} · {{ item.planName }} </text>
<u-icon
name="arrow-down"
size="16"
@@ -54,7 +77,7 @@
:class="{ 'arrow-rotate': collapsedDays[index] }"
></u-icon>
</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>
</view>
</view>
@@ -66,11 +89,7 @@
<text class="empty-text"> {{ isLoading?'暂无训练记录':'加载中' }} </text>
</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>
@@ -79,23 +98,32 @@
<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-left">
<text class="header-title">选择学生</text>
<text class="student-count">已选({{ selectedStudentIndexes.length }}/{{ studentList.length }})</text>
</view>
<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>
</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 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 class="item-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
<text class="item-name">{{ student.name }}</text>
</view>
</scroll-view>
</view>
@@ -104,7 +132,7 @@
<up-popup :show="showRecordPopup" @close="closeRecordPopup" mode="center" round="16" bgColor="#fff">
<view class="record-popup-content">
<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>
</view>
<scroll-view class="record-popup-body" scroll-y>
@@ -114,7 +142,7 @@
<text class="record-th">时间</text>
</view>
<view
v-for="(item, index) in rowData.split('|')"
v-for="(item, index) in rowData.time.split('|')"
:key="index"
class="record-table-row"
:class="{ 'record-row-odd': index % 2 === 1 }"
@@ -122,7 +150,7 @@
<text class="record-td">记录{{ index+1 }}</text>
<text class="record-td">{{ item }}</text>
</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>
</view>
</view>
@@ -159,7 +187,7 @@
let isLoading=ref(false)
// 项目选择
let showProject = ref(false)
let showProjectPopup = ref(false)
let selectProcet = ref('')
let selectId = ref('')
@@ -174,12 +202,14 @@
const columns = ref([
{ label: '姓名', name: 'studentName' },
{ label: '项目名称', name: 'planName' },
{ label: '最快速度', name: 'quicklyTime' },
{ label: '详细数据', name: 'time', width: '200' }
])
let rowData=ref('')
let rowData=ref<any>({
studentName:'',
time:''
})
const tableData = ref<any[]>([])
const projectOptions = ref<Array<any>>([[]])
@@ -217,6 +247,12 @@
})
onShow(() => {})
const cs=(e)=>{
console.log(e);
}
const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => {
@@ -240,17 +276,18 @@
const cellClick=(e:any)=>{
rowData.value=e.time
console.log(rowData.value);
rowData.value=e
showRecordPopup.value=true
}
const selectProcetFunc = (e: any) => {
selectProcet.value = e.value[0].name
selectId.value = e.value[0].planId
const selectProject = (project: any) => {
selectProcet.value = project.name
selectId.value = project.planId
showProjectPopup.value = false
begin.value = ''
end.value = ''
selectedStudentIndexes.value = []
selectedStudentIds.value = []
// studentList.value = []
tableData.value = []
// getProjectDetail()
@@ -282,8 +319,16 @@
showCalendar.value = false
}
const selectAllStudents = () => {
selectedStudentIndexes.value = studentList.value.map((_, index) => index)
const allSelected = computed(() => {
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) => {
@@ -533,6 +578,18 @@
background: #fafafa;
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 {
font-size: 32rpx;
font-weight: 600;
@@ -545,6 +602,36 @@
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 {
font-size: 26rpx;
color: #666;
@@ -570,69 +657,69 @@
padding: 16rpx 24rpx 40rpx;
}
.student-grid {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.student-item {
width: calc((100% - 24rpx) / 3);
display: flex;
align-items: center;
gap: 20rpx;
padding: 22rpx 20rpx;
border-radius: 12rpx;
margin-bottom: 12rpx;
background: #f8f9fa;
transition: background 0.2s;
gap: 8rpx;
padding: 16rpx 8rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
border: 2rpx solid transparent;
transition: all 0.25s ease;
&:active {
background: #eeeeee;
transform: scale(0.98);
}
&.selected {
background: #fff3e0;
border: 1rpx solid #ffcc80;
&.checked {
background-color: #e6f7ff;
border-color: #1890ff;
}
}
.item-checkbox {
flex-shrink: 0;
.checkbox-inner {
width: 36rpx;
height: 36rpx;
border-radius: 8rpx;
border: 2rpx solid #bfbfbf;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
&.checked {
border-color: #faad14;
background: #faad14;
}
}
}
.item-avatar {
flex-shrink: 0;
width: 64rpx;
height: 64rpx;
.student-checkbox {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: #1890ff;
border: 2rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
.avatar-text {
font-size: 28rpx;
&.checked {
border-color: #1890ff;
background-color: #1890ff;
}
.checkbox-index {
font-size: 22rpx;
color: #fff;
font-weight: 600;
line-height: 1;
}
}
.item-name {
flex: 1;
font-size: 30rpx;
color: #333;
font-weight: 500;
.student-info {
width: 100%;
.student-name {
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;
}
/* 项目选择弹窗 */
.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 {
from {
transform: translateY(100%);

View File

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

View File

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

View File

@@ -378,10 +378,12 @@
console.log('保存的数据:', data)
PlanService.AddPlan(data).then(res => {
if (res.code == 0) {
Service.Msg('添加成功!')
Service.Msg(planId.value?'修改成功!':'添加成功!')
setTimeout(() => {
Service.GoPageBack()
Service.GoPageDelse('/pages/userFunc/hunyang?id=' + res.data.planId)
}, 1000)
} else {
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,100 +1,94 @@
<template>
<up-navbar v-if="!isLandscape" title="混氧项目" placeholder='true' :autoBack="true">
</up-navbar>
<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="project-name">{{ name }}</view>
<view class="total-duration">
<text class="duration-label">计划总时长:</text>
<text class="duration-value">{{ formatTotalDuration(totalDuration) }}</text>
<view class="header-action" @click="Service.GoPage('/pages/userFunc/addHunyang?id='+planId)">
<up-icon name="setting" size="22"></up-icon>
</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 class="card-section groups-section" v-if="groupStates.length > 0">
<view class="section-title">训练分组 (同时进行)</view>
<view class="groups-list">
<view
v-for="(groupState, groupIndex) in groupStates"
:key="groupIndex"
class="group-card"
:class="{
<view v-for="(groupState, groupIndex) in groupStates" :key="groupIndex" class="group-card" :class="{
resting: groupState.isResting && !groupState.isCompleted,
completed: groupState.isCompleted
}"
>
}">
<!-- 分组头部 -->
<view class="group-header">
<view class="group-title-area">
<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>
<text class="group-plan-index">
计划总时长: {{ groupState.allTime }}
<!-- {{ groupState.currentPlanIndex }}/{{ groupState.plans.length }} -->
</text>
</view>
<!-- 当前计划内容 -->
<view class="plan-content" v-if="!groupState.isCompleted">
<!-- 计划名称 -->
<view class="plan-name">{{ groupState.currentPlan?.name }}</view>
<!-- 圈数指示器 -->
<view class="laps-indicator">
<view
v-for="lap in groupState.currentPlan?.circle"
:key="lap"
class="lap-dot"
:class="{ completed: lap <= groupState.completedLaps }"
></view>
</view>
<!-- <view class="laps-indicator">
<view v-for="lap in groupState.currentPlan?.circle" :key="lap" class="lap-dot"
:class="{ completed: lap <= groupState.completedLaps }"></view>
</view> -->
<!-- 倒计时 -->
<view class="countdown-box" :class="{ resting: groupState.isResting }">
<text class="countdown-number">
{{ formatCountdown(groupState.isResting ? groupState.restCountdown : groupState.countdown) }}
</text>
</view>
<!-- 阶段提示 -->
<view class="phase-hint" :class="{ resting: groupState.isResting }">
<text v-if="groupState.isResting" class="phase-text">
<!-- <view class="phase-hint" :class="{ resting: groupState.isResting }">
<text v-if="groupState.isResting && groupState.isPlanCompleted" class="phase-text">
计划完成 - 休息中
</text>
<text v-else-if="groupState.isResting" class="phase-text">
休息中 - {{ groupState.completedLaps + 1 }} 圈准备
</text>
<text v-else class="phase-text">
训练中 - {{ groupState.completedLaps + 1 }}
</text>
</view>
</view> -->
<!-- 计划详情 -->
<view class="plan-details">
<view class="detail-box">
<text class="detail-label">目标时间</text>
<text class="detail-value">{{ groupState.currentPlan?.target }}</text>
<text class="detail-badge">
M:{{ formatCountdown(groupState.currentPlan?.target) }}
</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 class="detail-box">
<text class="detail-label">休息时间</text>
<text class="detail-value">{{ groupState.currentPlan?.rest }}</text>
<view v-else-if="groupState.isRunning" class="group-control-btn group-pause-btn"
@click="pauseGroup(groupState)">
<u-icon name="pause-circle" size="24" color="#fff"></u-icon>
<text>暂停</text>
</view>
<view class="detail-box">
<text class="detail-label">圈数</text>
<text class="detail-value">
{{ groupState.completedLaps }}/{{ groupState.currentPlan?.circle }}
</text>
<view class="group-control-btn group-reset-btn" @click="resetGroup(groupIndex)">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text>重置</text>
</view>
</view>
</view>
@@ -108,14 +102,7 @@
</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">
@@ -145,27 +132,30 @@
// ==================== 类型 ====================
interface PlanItem {
name: string
target: number
rest: number
circle: number
name : string
target : number
rest : number
circle : number
}
interface GroupData {
groupName: string
group: PlanItem[]
groupName : string
allTime : string
group : PlanItem[]
}
interface GroupState {
groupName: string
plans: PlanItem[]
currentPlanIndex: number
currentPlan: PlanItem | null
countdown: number
restCountdown: number
completedLaps: number
isResting: boolean
isCompleted: boolean
groupName : string
plans : PlanItem[]
currentPlanIndex : number
currentPlan : PlanItem | null
countdown : number
restCountdown : number
completedLaps : number
isResting : boolean
isCompleted : boolean
isPlanCompleted : boolean // 当前计划是否已完成(最后一圈休息后切换计划)
isRunning : boolean // 当前分组是否正在运行
}
// ==================== 数据 ====================
@@ -174,6 +164,8 @@
const groupStates = ref<GroupState[]>([])
const isRunning = ref(false)
const timer = ref<number | null>(null)
const planTotalDuration = ref(0) // 接口返回的计划总时长
const isLandscape = ref(false) // 是否为横屏
// ==================== 计算属性 ====================
const totalGroupPlans = computed(() => {
@@ -200,8 +192,10 @@
})
// ==================== 生命周期 ====================
onLoad((data: any) => {
onLoad((data : any) => {
planId.value = data.id
checkOrientation()
uni.onWindowResize(checkOrientation)
})
onShow(() => {
@@ -210,14 +204,20 @@
onUnmounted(() => {
stopTimer()
uni.offWindowResize(checkOrientation)
})
// ==================== 方法 ====================
const checkOrientation = () => {
const info = uni.getSystemInfoSync()
isLandscape.value = info.windowWidth > info.windowHeight
}
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code === 0) {
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)
} else {
Service.Msg(res.msg)
@@ -227,9 +227,10 @@
})
}
const initGroups = (data: GroupData[]) => {
const initGroups = (data : GroupData[]) => {
groupStates.value = data.map(g => ({
groupName: g.groupName,
allTime: g.allTime,
plans: g.group,
currentPlanIndex: 0,
currentPlan: g.group[0] || null,
@@ -237,10 +238,134 @@
restCountdown: 0,
completedLaps: 0,
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 = () => {
if (groupStates.value.length === 0) {
Service.Msg('暂无训练计划')
@@ -251,66 +376,18 @@
return
}
isRunning.value = true
// 初始化未开始的分组
// 启动所有未完成的组
groupStates.value.forEach(gs => {
if (!gs.isCompleted && gs.countdown === 0 && !gs.isResting) {
gs.countdown = gs.currentPlan?.target || 0
if (!gs.isCompleted) {
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 = () => {
groupStates.value.forEach(gs => {
gs.isRunning = false
})
stopTimer()
}
@@ -326,19 +403,20 @@
stopTimer()
const data = groupStates.value.map(gs => ({
groupName: gs.groupName,
allTime: gs.allTime,
group: gs.plans
}))
initGroups(data)
}
const formatCountdown = (seconds: number): string => {
const formatCountdown = (seconds : number) : string => {
const s = Math.max(0, Math.floor(seconds))
const m = Math.floor(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秒'
const m = Math.floor(seconds / 60)
const s = Math.floor(seconds % 60)
@@ -363,6 +441,8 @@
}
.header-section {
position: relative;
.header-content {
display: flex;
flex-direction: column;
@@ -371,11 +451,18 @@
}
.project-name {
font-size: 36rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.header-action {
position: absolute;
top: 24rpx;
right: 24rpx;
padding: 4rpx;
}
.total-duration {
display: flex;
align-items: center;
@@ -464,7 +551,7 @@
}
.group-name {
font-size: 30rpx;
font-size: 22rpx;
font-weight: 600;
color: #333;
}
@@ -493,10 +580,10 @@
}
.plan-name {
font-size: 34rpx;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
margin-bottom: 8rpx;
}
/* 圈数指示器 */
@@ -550,40 +637,82 @@
font-size: 72rpx;
font-weight: bold;
font-family: 'Helvetica Neue', monospace;
color: #4a90e2;
color: #000;
letter-spacing: 2rpx;
}
&.resting .countdown-number {
color: #ff9800;
color: red;
animation: flash 0.5s ease-in-out infinite;
}
}
.plan-details {
display: flex;
justify-content: space-around;
margin-top: 20rpx;
padding-top: 16rpx;
border-top: 1rpx dashed #e0e0e0;
}
.detail-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 6rpx;
gap: 16rpx;
}
.detail-label {
font-size: 22rpx;
color: #999;
.detail-badge {
display: inline-flex;
align-items: center;
gap: 4rpx;
font-size: 24rpx;
color: #666;
background: #f5f7fa;
padding: 8rpx 4rpx;
border-radius: 24rpx;
}
.detail-value {
font-size: 28rpx;
font-weight: 600;
color: #333;
.detail-badge-tag {
width: 32rpx;
height: 32rpx;
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 {
@@ -675,7 +804,168 @@
}
@keyframes flash {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
</style>
/* 横屏适配 */
@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>

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +1,35 @@
<template>
<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-time">{{ formatTime(currentTime) }}</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 class="students-section" style="margin-top: 20rpx;" >
<view class="section-header">
<view class="students-section" style="margin-top: 20rpx;">
<!-- <view class="section-header">
<text class="section-title">学生列表</text>
<text class="student-count">{{ students.length }}位学生</text>
</view>
</view> -->
<view class="students-list">
<view v-for="(student, index) in students" :key="student.id" class="student-item"
:class="{ 'not-started': !student.hasStarted }">
<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 class="student-info">
<text class="student-name">{{ student.name }}</text>
@@ -51,17 +46,14 @@
<view class="student-buttons">
<button class="record-btn" :disabled="!student.hasStarted"
@click="recordStudentSegment(student)">
<text class="btn-text">记录</text>
</button>
<button class="view-btn" @click="showStudentRecords(student)">
<text class="btn-text">详情</text>
</button>
<button class="reset-btn" @click="resetStudent(student)">
<view @click="resetStudent(student)" class="">
<u-icon name="reload" :bold="true" size="24" color="#1890ff"></u-icon>
</view>
<!-- <button class="reset-btn" @click="resetStudent(student)">
<text class="btn-text">重置</text>
</button>
</button> -->
</view>
</view>
</view>
@@ -206,12 +198,12 @@
})
}
// 格式化时间显示
// 格式化时间显示(分:秒.毫秒)
const formatTime = (seconds : number) : string => {
const hours = Math.floor(seconds / 3600)
const mins = Math.floor((seconds % 3600) / 60)
const mins = 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
}
// 简化时间格式化(分:秒)
// 简化时间格式化(分:秒.毫秒
const formatSimpleTime = (seconds : number) : string => {
const mins = 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(() => {
const elapsed = (Date.now() - startTime) / 1000
currentTime.value = elapsed
}, 10)
}, 1)
// 间隔出发模式
if (startMode.value === 1) {
@@ -333,7 +326,6 @@
// 记录学生分段
const recordStudentSegment = (student : any) => {
console.log(22222);
if (!isRunning.value) {
Service.Msg('请先开始计时')
return
@@ -355,9 +347,9 @@
if (startMode.value === 1) {
recordTime = currentTime.value - student.startTime
}
student.segments.push({
time: recordTime
time: Number(recordTime).toFixed(3)
})
Service.Msg(`${student.name}${student.segments.length}段已记录`)
@@ -399,13 +391,6 @@
Service.Msg('暂无数据可保存!')
return
}
if (!isSave) {
Service.Msg('存在学生未完成!')
return
}
let data = [
{
studentId: "",
@@ -429,18 +414,29 @@
})
})
console.log(data);
PlanService.AddPlanLog(planId.value, '分段项目', '', JSON.stringify(data), '').then(res => {
if (res.code == 0) {
Service.Msg('提交成功!')
setTimeout(()=>{
Service.GoPageBack()
},1000)
} else {
Service.Msg(res.msg)
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('提交成功!')
} else {
Service.Msg(res.msg)
}
})
} else if (res.cancel) {
console.log('用户点击取消');
}
}
})
});
}
// 显示学生记录弹窗
@@ -474,41 +470,7 @@
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 {
@@ -516,6 +478,20 @@
border-radius: 20rpx;
padding: 30rpx;
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 {
@@ -599,6 +575,7 @@
gap: 12rpx;
.student-name {
width: 120rpx;
font-size: 30rpx;
font-weight: 500;
color: #333;
@@ -617,7 +594,7 @@
}
.total-time-text {
font-size: 24rpx;
font-size: 30rpx;
color: #999;
font-family: 'DIN Alternate', monospace;
font-weight: 500;
@@ -652,7 +629,7 @@
.view-btn,
.reset-btn {
height: 56rpx;
padding: 0 20rpx;
padding: 0 35rpx;
border-radius: 8rpx;
border: none;
font-size: 24rpx;

View File

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

View File

@@ -19,17 +19,19 @@
</view>
<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-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
<view class="student-details">
<view class="detail-row">
<text class="student-name">{{ student.name }}</text>
<view class="student-gender" :class="student.sex === '男' ? 'male' : 'female'">
<u-icon :name="student.sex === '男' ? 'man' : 'woman'" size="14"></u-icon>
{{ student.sex }}
<view class="detail-row" style="justify-content: space-between;">
<view style="display: flex; align-items: center; gap: 8rpx;">
<text class="student-name">{{ student.name }}</text>
<view class="student-gender" :class="student.sex === '男' ? 'male' : 'female'">
<u-icon :name="student.sex === '男' ? 'man' : 'woman'" size="14"></u-icon>
{{ student.sex }}
</view>
</view>
</view>
<view class="detail-row">
@@ -48,19 +50,23 @@
<text class="detail-label">住址</text>
<text class="detail-value address-text">{{ student.address }}</text>
</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 class="student-actions">
<view class="action-btn edit-btn" @click="openEditModal(student)">
<u-icon name="edit-pen" size="18" color="#1890ff"></u-icon>
</view>
</view>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 100rpx;" >
<view class="" style="width: 100%; height: 100rpx;">
</view>
<!-- 底部添加按钮 -->
<view class="bottom-add-btn">
@@ -87,15 +93,19 @@
<view class="form-group">
<text class="form-label">性别</text>
<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">
<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>
<text></text>
</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">
<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>
<text></text>
</view>
@@ -148,13 +158,13 @@
import { studentService } from '@/Service/swimming/studentService'
import { ref } from "vue"
// 分页相关
let page = ref(1)
let status = ref('loadmore')
// 学员列表
const students = ref<Array<any>>([])
@@ -196,12 +206,12 @@
}
// 打开编辑弹窗
const openEditModal = (student :any) => {
const openEditModal = (student : any) => {
editingStudent.value = student
formData.value = {
id: student.studentId,
name: student.name,
gender: student.sex?student.sex: '男',
gender: student.sex ? student.sex : '男',
birthDate: student.birthday,
school: student.school,
address: student.address
@@ -241,7 +251,11 @@
status.value = 'loading'
studentService.GetStudentListPage(page.value.toString()).then(res => {
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'
page.value++
} else {
@@ -264,19 +278,19 @@
// 构造接口请求数据
const requestData = {
studentId:formData.value.id?formData.value.id:'' ,
studentId: formData.value.id ? formData.value.id : '',
name: formData.value.name,
sex: formData.value.gender,
birthday: formData.value.birthDate,
school: formData.value.school?formData.value.school:'' ,
address: formData.value.address ?formData.value.address:''
school: formData.value.school ? formData.value.school : '',
address: formData.value.address ? formData.value.address : ''
}
console.log(requestData);
// 调用添加学员接口
studentService.Add(requestData).then((content) => {
if (content.code == 0) {
Service.Msg(formData.value.id?'修改成功!':'添加成功!')
Service.Msg(formData.value.id ? '修改成功!' : '添加成功!')
getData()
} else {
Service.Msg(content.msg)
@@ -287,17 +301,9 @@
}
// 确认删除
const confirmDelete = (student : Student) => {
uni.showModal({
title: '确认删除',
content: `确定要删除学员「${student.name}」吗?`,
confirmColor: '#ff4d4f',
success: (res) => {
if (res.confirm) {
deleteStudent(student.id)
}
}
})
const confirmDelete = (student : any) => {
deleteStudent(student.studentId)
}
// 删除学员
@@ -305,7 +311,7 @@
studentService.Delete(id).then(res => {
if (res.code == 0) {
students.value = students.value.filter(s => s.id !== id)
Service.Msg('删除成功', 'success')
Service.Msg('修改成功!')
} else {
Service.Msg(res.msg)
}
@@ -329,9 +335,10 @@
margin-bottom: 24rpx;
.header-title {
display: flex;
display: flex;
align-items: center;
justify-content: space-between;
.title {
font-size: 36rpx;
font-weight: 700;
@@ -501,10 +508,10 @@
}
&.delete-btn {
background-color: #fff1f0;
background-color: #f3f3f3;
&:active {
background-color: #ffccc7;
background-color: #f3f3f3;
transform: scale(0.92);
}
}

View File

@@ -11,17 +11,13 @@
</view>
<!-- 计划信息 -->
<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 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 v-for="group in groupedAthletes" :key="group.groupNumber" class="group-wrapper">
<view class="group-header">
@@ -42,9 +38,9 @@
</view>
<view class="time-wrapper">
<text
class="athlete-time">{{ !athlete.time? ' 00:00:00 ':formatTime(athlete.time) }}</text>
<text class="best-time">最快:
{{ athlete.bestTime !== null ? athlete.quicklyTime: '无' }}</text>
class="athlete-time">{{ !athlete.time? ' 000000 ':formatTime(athlete.time) }}</text>
<text class="best-time">PB:
{{ athlete.studentQuicklyTime !== null ? athlete.studentQuicklyTime: '无' }}</text>
</view>
<view class="" style="display: flex;align-items: center; gap: 10rpx;">
@@ -60,12 +56,13 @@
<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>
</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="notice-header">
<text class="notice-title">恭喜以下学生打破记录</text>
@@ -80,39 +77,42 @@
<view v-for="(item,index) in record" class="table-row">
<text class="row-cell index-cell">{{ index+1 }}</text>
<text class="row-cell name-cell">{{ item.name }}</text>
<text class="row-cell time-cell">{{ item.quicklyTime }}</text>
<text class="row-cell time-cell">{{ item.studentQuicklyTime }}</text>
</view>
</scroll-view>
</view>
</view>
</u-popup>
<!-- 底部控制按钮 -->
<view class="control-buttons">
<view class="button-group" :class="{ 'record-group': isRunning }">
<button v-if="!isRunning" class="start-btn" @click="startTimer">
<u-icon name="play-right" size="24" color="#fff"></u-icon>
<text class="btn-text">开始</text>
</button>
<button v-if="isRunning" class="record-btn" @click="recordNextAthlete">
<u-icon name="checkmark-circle" size="24" color="#fff"></u-icon>
<text class="btn-text">记录</text>
</button>
</view>
<view class="button-group" :class="{ 'pause-group': isRunning }">
<button v-if="!isRunning" class="reset-all-btn" @click="resetAll">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text class="btn-text">重置</text>
</button>
<button v-if="isRunning" class="reset-all-btn" @click="stopTimer">
<text class="btn-text" style="font-size: 28rpx;">暂停</text>
</button>
</view>
<!-- 可拖动悬浮球 -->
<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">
<button class="ctrl-btn btn-submit" @click="submitData">
<u-icon name="checkmark-circle" size="24" color="#fff"></u-icon>
<text class="btn-text">提交</text>
</button>
<button class="ctrl-btn btn-data" @click="Service.GoPage('/pages/dataAnalyze/timingAnalze')">
<u-icon name="list-dot" size="24" color="#fff"></u-icon>
<text class="btn-text">数据</text>
</button>
<button v-if="!isRunning" class="ctrl-btn btn-reset" @click="resetAll">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text class="btn-text">重置</text>
</button>
<button v-if="isRunning" class="ctrl-btn btn-pause" @click="stopTimer">
<text class="btn-text">暂停</text>
</button>
</view>
</view>
</template>
@@ -152,9 +152,10 @@
// 分组设置
const groupSize = ref('0')
// 创建播放器
const video = uni.createInnerAudioContext()
// 计时器状态
const isRunning = ref(false)
@@ -169,11 +170,46 @@
const stopwatchMode = ref<'interval' | 'together'>('interval')
// 间隔时间(秒)
const intervalTime = ref('0')
let record=ref<Array<any>>([])
let showRecord=ref(false)
let record = ref<Array<any>>([])
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) => {
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(() => {
@@ -195,7 +231,22 @@
}
})
}
// 悬浮球事件
const ballClick=( data:any)=>{
if(data){
recordNextAthlete()
}else{
startTimer()
}
}
// 播报
const speak=()=>{
video.src='/static/icon/video.mp3'
video.play()
}
// 计算分组
const groupedAthletes = computed<Group[]>(() => {
const size = parseInt(groupSize.value) || 2
@@ -243,7 +294,7 @@
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
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
isRunning.value = true
notifiedGroupNumbers.value.clear()
startTime = Date.now() - currentTime.value * 1000
timerInterval = setInterval(() => {
const elapsed = (Date.now() - startTime) / 1000
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 => {
if (!athlete.finished) {
@@ -264,7 +329,7 @@
athlete.time = Math.max(0, elapsed - offset)
}
})
}, 16)
}, 30)
}
// 停止计时
@@ -375,6 +440,7 @@
stopTimer()
currentTime.value = 0
currentAthleteIndex.value = 0
notifiedGroupNumbers.value.clear()
athletes.value.forEach(athlete => {
athlete.time = 0
@@ -388,22 +454,18 @@
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
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')}`
}
// 提交数据
const submitData = () => {
const hasData = athletes.value.some(a => a.time > 0)
const allFinished = athletes.value.every(a => a.finished)
if (!hasData) {
Service.Msg('暂无数据可提交')
return
}
// if(!allFinished){
// Service.Msg('还用学生未完成!')
// return
// }
stopTimer()
let data = [{
planName: "",
@@ -417,35 +479,74 @@
planName: planName.value,
studentId: item.studentId,
studentName: item.name,
time: formatMinuteTime(item.time)
time: item.time
})
})
PlanService.AddPlanLog(planId.value,'计时项目',JSON.stringify(data),'','','').then(res=>{
if(res.code==0){
Service.Msg('提交成功!')
stopTimer()
currentTime.value = 0
currentAthleteIndex.value = 0
athletes.value.forEach(athlete => {
athlete.time = 0
athlete.finished = false
athlete.startOffset = undefined
})
if(res.data.record.length>0){
record.value=res.data.record
showRecord.value=true
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
athletes.value.forEach(athlete => {
athlete.time = 0
athlete.finished = false
athlete.startOffset = undefined
})
if (res.data.record.length > 0) {
record.value = res.data.record
showRecord.value = true
}
} else {
Service.Msg(res.msg)
}
})
} 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(() => {
if (timerInterval) {
@@ -479,6 +580,10 @@
font-size: 80rpx;
font-weight: bold;
color: #ff0000;
font-family: 'DIN Alternate', monospace;
min-width: 400rpx;
display: inline-block;
text-align: center;
}
.plan-info {
@@ -534,7 +639,7 @@
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 28rpx;
padding: 10rpx 28rpx;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
border-left: 6rpx solid #1890ff;
@@ -712,143 +817,111 @@
/* 底部控制按钮 */
.control-buttons {
display: flex;
gap: 24rpx;
justify-content: center;
gap: 20rpx;
justify-content: space-between;
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, #ffffff 100%);
padding: 32rpx 40rpx;
// padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
padding: 24rpx 32rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid rgba(0, 0, 0, 0.06);
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.08);
.button-group {
.ctrl-btn {
flex: 1;
display: flex;
justify-content: center;
}
.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;
height: 92rpx;
border-radius: 24rpx;
border: none;
font-size: 32rpx;
font-size: 30rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
position: relative;
overflow: hidden;
transition: all 0.3s ease;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.15);
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 50%;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.3) 0%, transparent 100%);
border-radius: 20rpx 20rpx 0 0;
}
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2rpx;
background: linear-gradient(90deg, transparent 0%, rgba(255, 255, 255, 0.5) 50%, transparent 100%);
}
:deep(.u-icon) {
position: relative;
z-index: 1;
}
}
.start-btn {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4);
gap: 10rpx;
transition: all 0.25s ease;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.12);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
}
.pause-btn {
background: linear-gradient(135deg, #fa541c 0%, #ff7a45 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(250, 84, 28, 0.4);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(250, 84, 28, 0.3);
}
.btn-submit {
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(250, 173, 20, 0.35);
}
.reset-all-btn {
.btn-data {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(24, 144, 255, 0.4);
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(24, 144, 255, 0.3);
}
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(24, 144, 255, 0.35);
}
.back-btn {
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 {
.btn-reset {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
color: #ffffff;
box-shadow: 0 6rpx 20rpx rgba(82, 196, 26, 0.4);
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(82, 196, 26, 0.35);
}
&:active {
transform: scale(0.96);
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
}
.btn-pause {
background: linear-gradient(135deg, #fa541c 0%, #ff7a45 100%);
color: #fff;
box-shadow: 0 6rpx 18rpx rgba(250, 84, 28, 0.35);
}
.btn-text {
font-size: 32rpx;
font-size: 30rpx;
color: #ffffff;
font-weight: 600;
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;
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.