Compare commits

...

2 Commits

Author SHA1 Message Date
Ls
fb9a622a56 修改 2026-07-10 09:06:31 +08:00
Ls
d020483461 完整 2026-07-10 08:41:45 +08:00
18 changed files with 1204 additions and 497 deletions

View File

@@ -3,8 +3,8 @@ import { Service } from '@/Service/Service';
class studentService { class studentService {
private static GetStudentListPath : string = '/api/Student/GetStudentList'; private static GetStudentListPath : string = '/api/Student/GetStudentList';
/*****学生列表不分页接口*****/ /*****学生列表不分页接口*****/
static GetStudentList() { static GetStudentList(planId:string) {
var result = Service.Request(this.GetStudentListPath, "GET", { }); var result = Service.Request(this.GetStudentListPath, "GET", { planId });
return result; return result;
} }

View File

@@ -24,6 +24,38 @@ class userService {
} }
private static GetUserListPath : string = '/api/User/GetUserList';
/*****查询用户列表接口*****/
static GetUserList(serch:string,page:number) {
var result = Service.Request(this.GetUserListPath, "GET", {serch,page});
return result;
}
private static UpdateUserRolePath : string = '/api/User/UpdateUserRole';
/*****更改用户权限接口*****/
static UpdateUserRole(userId:string) {
var result = Service.Request(this.UpdateUserRolePath, "POST", {userId});
return result;
}
private static GetKefuInfoPath : string = '/api/User/GetKefuInfo';
/*****获取客服信息接口*****/
static GetKefuInfo() {
var result = Service.Request(this.GetKefuInfoPath, "GET", {});
return result;
}
private static UpdateKefuPath : string = '/api/User/UpdateKefu';
/*****修改客服信息接口*****/
static UpdateKefu(phone:string,name:string) {
var result = Service.Request(this.UpdateKefuPath, "POST", {phone,name});
return result;
}
// private static GetStudentInfoPath : string = '/api/Student/GetStudentInfo'; // private static GetStudentInfoPath : string = '/api/Student/GetStudentInfo';
// /*****根据学生id查详情接口*****/ // /*****根据学生id查详情接口*****/
// static GetStudentInfo(studentId:string) { // static GetStudentInfo(studentId:string) {

View File

@@ -34,7 +34,7 @@
{ {
"path": "swiming", "path": "swiming",
"style": { "style": {
"navigationBarTitleText": "游泳项目" "navigationBarTitleText": "计时项目"
} }
}, },
{ {
@@ -107,6 +107,18 @@
"navigationBarTitleText": "测试", "navigationBarTitleText": "测试",
"pageOrientation": "auto" "pageOrientation": "auto"
} }
},
{
"path": "userPermission",
"style": {
"navigationBarTitleText": "权限管理"
}
},
{
"path": "contactManage",
"style": {
"navigationBarTitleText": "联系方式管理"
}
} }
] ]
}, { }, {

View File

@@ -108,7 +108,7 @@
</view> </view>
<!-- 日历弹窗 --> <!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate="1776240407000" zIndex="1000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar> <uni-calendar ref="calendar" :insert="false" range='true' :lunar="true" @confirm="calendarConfirm" />
</view> </view>
</template> </template>
@@ -133,17 +133,17 @@
const projectOptions = ref<Array<any>>([[]]) const projectOptions = ref<Array<any>>([[]])
interface Student { interface Student {
studentId: string studentId : string
name: string name : string
} }
interface StudentTrainingRecord { interface StudentTrainingRecord {
studentId: string studentId : string
studentName: string studentName : string
time: number time : number
recordDate: string recordDate : string
recordFullDate: string recordFullDate : string
round: number round : number
} }
const projectStudents = ref<Student[]>([]) const projectStudents = ref<Student[]>([])
@@ -178,7 +178,7 @@
yAxis: { yAxis: {
data: [{ data: [{
min: 0, min: 0,
format: (val: number) => val.toFixed(1) + 's' format: (val : number) => val.toFixed(1) + 's'
}] }]
}, },
extra: { extra: {
@@ -197,11 +197,13 @@
series: [] as any[] series: [] as any[]
}) })
let calendar=ref(null)
onLoad(() => { onLoad(() => {
getProjectData() getProjectData()
}) })
onShow(() => {}) onShow(() => { })
const getProjectData = () => { const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => { PlanService.GetPlanListNoPage('计时项目').then(res => {
@@ -213,7 +215,7 @@
}) })
} }
const selectProcetFunc = (project: any) => { const selectProcetFunc = (project : any) => {
selectProcet.value = project.name selectProcet.value = project.name
selectId.value = project.planId selectId.value = project.planId
showProject.value = false showProject.value = false
@@ -228,11 +230,23 @@
const openCalendar = () => { const openCalendar = () => {
showCalendar.value = true showCalendar.value = true
calendar.value.open()
} }
const calendarConfirm = (e: any) => { const calendarConfirm = (e : any) => {
begin.value = e[0] const newDate = e.range
end.value = e[e.length - 1] if (e.range.before !== begin.value || e.range.after !== end.value ) {
selectedStudentIds.value = []
lineChartData.value.categories = []
lineChartData.value.series = []
}
begin.value = newDate.before
if( e.range.after){
end.value = e.range.after
}else{
end.value = e.range.before
}
showCalendar.value = false showCalendar.value = false
} }
@@ -240,8 +254,8 @@
showCalendar.value = false showCalendar.value = false
} }
const loadProjectStudents = (projectId: string) => { const loadProjectStudents = (projectId : string) => {
studentService.GetStudentList().then(res => { studentService.GetStudentList('').then(res => {
if (res.code == 0) { if (res.code == 0) {
projectStudents.value = res.data projectStudents.value = res.data
} else { } else {
@@ -251,7 +265,7 @@
} }
const toggleStudent = (studentId: string) => { const toggleStudent = (studentId : string) => {
const index = selectedStudentIds.value.indexOf(studentId) const index = selectedStudentIds.value.indexOf(studentId)
if (index !== -1) { if (index !== -1) {
selectedStudentIds.value.splice(index, 1) selectedStudentIds.value.splice(index, 1)
@@ -262,36 +276,49 @@
} }
selectedStudentIds.value.push(studentId) selectedStudentIds.value.push(studentId)
} }
let studentIdList = selectedStudentIds.value.map((item) => item) const studentIdList = selectedStudentIds.value.map((item) => item)
let data = { const reqData = {
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
} }
interface DataItem {
name: string
data: any[]
}
const xData = ref<DataItem[]>( PlanService.GetQuxianLog(reqData).then(res => {
Array.from({ length: studentIdList.length }, () => ({
name: '',
data: []
}))
)
PlanService.GetQuxianLog(data).then(res => {
if (res.code == 0) { if (res.code == 0) {
lineChartData.value.categories = [] const list = res.data.list || []
res.data.list.map((item: any) => { const days = list.map((item : any) => item.dayTime)
lineChartData.value.categories.push(item.dayTime)
item.data.map((content: any, index: any) => {
xData.value[index].name = content.studentName // 按已选学生顺序初始化 series缺省日期填充 0
xData.value[index].data.push(content.time) const series = studentIdList.map(id => {
const student = projectStudents.value.find(s => s.studentId === id)
return {
name: student?.name || '',
data: new Array(days.length).fill(0)
}
})
// 建立 studentId -> series 索引映射
const idToIndex : Record<string, number> = {}
studentIdList.forEach((id, idx) => {
idToIndex[id] = idx
})
// 按日期填充数据
list.forEach((dayItem : any, dayIndex : number) => {
; (dayItem.data || []).forEach((record : any) => {
const sIdx = idToIndex[record.studentId]
if (sIdx !== undefined) {
series[sIdx].data[dayIndex] = record.time ?? 0
if (!series[sIdx].name && record.studentName) {
series[sIdx].name = record.studentName
}
}
}) })
}) })
lineChartData.value.series = xData.value
lineChartData.value.categories = days
lineChartData.value.series = series
} else { } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }

View File

@@ -91,8 +91,11 @@
<!-- 日历弹窗 --> <!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm"
@close="calendarClose"></up-calendar> <uni-calendar ref="calendar" :insert="false" range='true' :lunar="true" @confirm="calendarConfirm" />
<!-- <up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm"
@close="calendarClose"></up-calendar> -->
<!-- 学生选择弹窗 --> <!-- 学生选择弹窗 -->
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view> <view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
@@ -190,6 +193,8 @@
let selectProcet = ref('') let selectProcet = ref('')
let selectId = ref('') let selectId = ref('')
let calendar=ref(null)
// 日期选择 // 日期选择
const begin = ref<string>('') const begin = ref<string>('')
const end = ref<string>('') const end = ref<string>('')
@@ -264,7 +269,7 @@
} }
const getStudentList = () => { const getStudentList = () => {
studentService.GetStudentList().then(res => { studentService.GetStudentList('').then(res => {
if (res.code == 0) { if (res.code == 0) {
studentList.value = res.data studentList.value = res.data
} else { } else {
@@ -305,11 +310,17 @@
const openCalendar = () => { const openCalendar = () => {
showCalendar.value = true showCalendar.value = true
calendar.value.open()
} }
const calendarConfirm = (e : any) => { const calendarConfirm = (e : any) => {
begin.value = e[0] console.log(e);
end.value = e[e.length - 1] begin.value = e.range.before
if( e.range.after){
end.value = e.range.after
}else{
end.value = e.range.before
}
showCalendar.value = false showCalendar.value = false
getRecord() getRecord()
} }

View File

@@ -38,7 +38,7 @@
<view class="student-info"> <view class="student-info">
<text class="student-name">{{ item.studentName }}</text> <text class="student-name">{{ item.studentName }}</text>
<text class="student-speed">最快用时{{ item.quicklyTime }}s</text> <text class="student-speed">最快用时{{ formatTime(item.quicklyTime ) }}</text>
</view> </view>
<view class="speed-detail"> <view class="speed-detail">
@@ -105,7 +105,7 @@
studentId: string studentId: string
} }
const gradeList = ref<StudentGrade[]>([]) const gradeList = ref<Array<any>>([])
const maxSpeed = computed(() => { const maxSpeed = computed(() => {
if (gradeList.value.length === 0) return 0 if (gradeList.value.length === 0) return 0
@@ -144,6 +144,14 @@
}) })
} }
// 格式化时间显示
const formatTime = (seconds : number) : string => {
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')}`
}
const handleRankingClick = (item: StudentGrade) => { const handleRankingClick = (item: StudentGrade) => {
console.log('点击了排名项:', item) console.log('点击了排名项:', item)
} }

View File

@@ -38,7 +38,8 @@
</view> </view>
<!-- 项目选择弹窗 --> <!-- 项目选择弹窗 -->
<up-popup :show="showProjectPopup" mode="center" round="32" :safeAreaInsetBottom="false" @close="showProjectPopup = false"> <up-popup :show="showProjectPopup" mode="center" round="32" :safeAreaInsetBottom="false"
@close="showProjectPopup = false">
<view class="project-popup-content"> <view class="project-popup-content">
<view class="modal-header"> <view class="modal-header">
<text class="modal-title">选择项目</text> <text class="modal-title">选择项目</text>
@@ -48,7 +49,8 @@
</view> </view>
<scroll-view scroll-y class="popup-project-list"> <scroll-view scroll-y class="popup-project-list">
<view class="project-list-inner"> <view class="project-list-inner">
<view v-for="(project, index) in projectOptions[0]" :key="project.planId" class="project-item" @click="selectProject(project)"> <view v-for="(project, index) in projectOptions[0]" :key="project.planId" class="project-item"
@click="selectProject(project)">
<view class="item-info"> <view class="item-info">
<text class="item-name">{{ project.name }}</text> <text class="item-name">{{ project.name }}</text>
</view> </view>
@@ -87,10 +89,8 @@
</view> </view>
<uni-calendar ref="calendar" :insert="false" range='true' :lunar="true" @confirm="calendarConfirm" />
<!-- 日历弹窗 --> <!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm"
@close="calendarClose"></up-calendar>
<!-- 学生选择弹窗 --> <!-- 学生选择弹窗 -->
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view> <view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
@@ -216,9 +216,9 @@
let row = ref('') let row = ref('')
let rowData=ref({ let rowData = ref({
studentName:'', studentName: '',
planName:'' planName: ''
}) })
@@ -269,6 +269,8 @@
}) })
}) })
let calendar=ref(null)
onLoad(() => { onLoad(() => {
getProjectData() getProjectData()
getRecordList() getRecordList()
@@ -282,7 +284,7 @@
const getProjectData = () => { const getProjectData = () => {
PlanService.GetPlanListNoPage('计时项目').then(res => { PlanService.GetPlanListNoPage('分段项目').then(res => {
if (res.code == 0) { if (res.code == 0) {
projectOptions.value[0] = res.data.list projectOptions.value[0] = res.data.list
} else { } else {
@@ -292,7 +294,7 @@
} }
const getStudentList = () => { const getStudentList = () => {
studentService.GetStudentList().then(res => { studentService.GetStudentList('').then(res => {
if (res.code == 0) { if (res.code == 0) {
studentList.value = res.data studentList.value = res.data
} else { } else {
@@ -303,7 +305,8 @@
const cellClick = (rows : any) => { const cellClick = (rows : any) => {
rowData.value=rows rowData.value = rows
console.log(rowData.value);
row.value = rows.subsection row.value = rows.subsection
showRecordPopup.value = true showRecordPopup.value = true
} }
@@ -334,11 +337,16 @@
const openCalendar = () => { const openCalendar = () => {
showCalendar.value = true showCalendar.value = true
calendar.value.open()
} }
const calendarConfirm = (e : any) => { const calendarConfirm = (e : any) => {
begin.value = e[0] begin.value = e.range.before
end.value = e[e.length - 1] if( e.range.after){
end.value = e.range.after
}else{
end.value = e.range.before
}
showCalendar.value = false showCalendar.value = false
getRecord() getRecord()
} }

View File

@@ -90,9 +90,9 @@
</view> </view>
<uni-calendar ref="calendar" :insert="false" range='true' :lunar="true" @confirm="calendarConfirm" />
<!-- 日历弹窗 --> <!-- 日历弹窗 -->
<up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar> <!-- <up-calendar :show="showCalendar" mode="date" minDate="1776240407000" @confirm="calendarConfirm" @close="calendarClose"></up-calendar> -->
<!-- 学生选择弹窗 --> <!-- 学生选择弹窗 -->
<view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view> <view class="modal-overlay" v-if="showStudentPicker" @click="closeStudentPicker"></view>
@@ -132,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">{{ rowData.studentName +'·' + rowData.planName}}</text> <text class="record-popup-title">{{ rowData.studentName +'·' + rowData.planName+' PB: '+rowData.quicklyTime}}</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>
@@ -196,6 +196,8 @@
const end = ref<string>('') const end = ref<string>('')
const showCalendar = ref(false) const showCalendar = ref(false)
let calendar=ref(null)
// 分页相关 // 分页相关
let page = ref(1) let page = ref(1)
let pageTotal = ref(10) let pageTotal = ref(10)
@@ -265,7 +267,7 @@
} }
const getStudentList=()=>{ const getStudentList=()=>{
studentService.GetStudentList().then(res=>{ studentService.GetStudentList('').then(res=>{
if (res.code == 0) { if (res.code == 0) {
studentList.value =res.data studentList.value =res.data
} else { } else {
@@ -276,6 +278,7 @@
const cellClick=(e:any)=>{ const cellClick=(e:any)=>{
console.log(e);
rowData.value=e rowData.value=e
showRecordPopup.value=true showRecordPopup.value=true
} }
@@ -306,11 +309,18 @@
const openCalendar = () => { const openCalendar = () => {
showCalendar.value = true showCalendar.value = true
calendar.value.open()
} }
const calendarConfirm = (e: any) => { const calendarConfirm = (e: any) => {
begin.value = e[0] begin.value = e.range.before
end.value = e[e.length - 1] if( e.range.after){
end.value = e.range.after
}else{
end.value = e.range.before
}
showCalendar.value = false showCalendar.value = false
getRecord() getRecord()
} }

View File

@@ -21,7 +21,6 @@
</view> </view>
<view class="card-info"> <view class="card-info">
<text class="card-title">计时项目</text> <text class="card-title">计时项目</text>
<text class="card-desc">记录单次训练的计时数据</text>
</view> </view>
<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>
@@ -37,7 +36,6 @@
</view> </view>
<view class="card-info"> <view class="card-info">
<text class="card-title">包干项目</text> <text class="card-title">包干项目</text>
<text class="card-desc">记录固定距离的训练成绩</text>
</view> </view>
<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>
@@ -53,7 +51,6 @@
</view> </view>
<view class="card-info"> <view class="card-info">
<text class="card-title">分段项目</text> <text class="card-title">分段项目</text>
<text class="card-desc">记录分段训练的详细数据</text>
</view> </view>
<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>
@@ -69,7 +66,6 @@
</view> </view>
<view class="card-info"> <view class="card-info">
<text class="card-title">混氧项目</text> <text class="card-title">混氧项目</text>
<text class="card-desc">记录混氧训练的计时数据</text>
</view> </view>
<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>
@@ -101,7 +97,8 @@
<!-- 项目列表 --> <!-- 项目列表 -->
<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,index) 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">
@@ -149,18 +146,18 @@
const showTimingModal = ref(false) const showTimingModal = ref(false)
let isUse=ref(false) let isUse = ref(false)
onLoad(() => { onLoad(() => {
if (!Service.GetUserToken()) { if (!Service.GetUserToken()) {
login() login()
}else{ } else {
isUseFunc() isUseFunc()
} }
}) })
onShow(()=>{ onShow(() => {
showTimingModal.value=false showTimingModal.value = false
uni.showTabBar() uni.showTabBar()
}) })
@@ -192,11 +189,11 @@
} }
const isUseFunc=()=>{ const isUseFunc = () => {
userService.IsCheakUserVerify().then(res=>{ userService.IsCheakUserVerify().then(res => {
if(res.code==0){ if (res.code == 0) {
isUse.value=res.data.isCheak isUse.value = res.data.isCheak
}else{ } else {
Service.Msg(res.msg) Service.Msg(res.msg)
} }
}) })
@@ -229,8 +226,8 @@
}) })
} }
const goBaogan=()=>{ const goBaogan = () => {
if(!isUse.value){ if (!isUse.value) {
Service.Msg('用户暂无权限,请等待审核!') Service.Msg('用户暂无权限,请等待审核!')
return return
} }
@@ -240,7 +237,7 @@
// 显示计时项目列表弹窗 // 显示计时项目列表弹窗
const showTimingProjectModal = (index : any) => { const showTimingProjectModal = (index : any) => {
if(!isUse.value){ if (!isUse.value) {
Service.Msg('用户暂无权限,请等待审核!') Service.Msg('用户暂无权限,请等待审核!')
return return
} }

View File

@@ -6,7 +6,8 @@
<view class="avatar-circle" v-if="!userInfo.headImg"> <view class="avatar-circle" v-if="!userInfo.headImg">
<u-icon name="account" size="52" color="#fff"></u-icon> <u-icon name="account" size="52" color="#fff"></u-icon>
</view> </view>
<image v-else class="avatar-image" :src="Service.GetMateUrlByImg(userInfo.headImg)" mode="aspectFill"></image> <image v-else class="avatar-image" :src="Service.GetMateUrlByImg(userInfo.headImg)" mode="aspectFill">
</image>
</view> </view>
<view class="user-info"> <view class="user-info">
<text class="user-nickname">{{ userInfo.name }}</text> <text class="user-nickname">{{ userInfo.name }}</text>
@@ -104,11 +105,55 @@
</view> </view>
</view> </view>
</view> </view>
<!-- 管理员功能区域 -->
<view class="menu-section" v-if="userInfo.role==1">
<view class="menu-card">
<!-- 权限管理菜单项 -->
<view class="menu-item" @click="goPage('/pages/userFunc/userPermission')">
<view class="menu-icon-bg admin-icon-bg">
<u-icon name="lock" size="28" color="#fff"></u-icon>
</view>
<view class="menu-content">
<view class="menu-header">
<text class="menu-title">权限管理</text>
<view class="menu-tag admin-tag">
<text class="tag-text">管理员</text>
</view>
</view>
<text class="menu-desc">管理用户权限和角色</text>
</view>
<view class="menu-arrow">
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
<view class="menu-divider"></view>
<!-- 联系方式管理菜单项 -->
<view class="menu-item" @click="goPage('/pages/userFunc/contactManage')">
<view class="menu-icon-bg contact-icon-bg">
<u-icon name="phone" size="28" color="#fff"></u-icon>
</view>
<view class="menu-content">
<view class="menu-header">
<text class="menu-title">联系方式管理</text>
<view class="menu-tag contact-tag">
<text class="tag-text">管理员</text>
</view>
</view>
<text class="menu-desc">管理客服联系方式</text>
</view>
<view class="menu-arrow">
<u-icon name="arrow-right" size="16" color="#bbb"></u-icon>
</view>
</view>
</view>
</view>
<!-- 底部联系信息 --> <!-- 底部联系信息 -->
<view class="contact-footer" @click="callContact"> <view v-if="userInfo.role!=1" class="contact-footer" @click="callContact">
<text class="contact-label">客服联系人张教练</text> <text class="contact-label">客服联系人{{ kefuName }}</text>
<text class="contact-phone">138-0000-0000</text> <text class="contact-phone">{{ kefuPhone }}</text>
</view> </view>
</view> </view>
</template> </template>
@@ -119,10 +164,12 @@
import { Service } from '@/Service/Service' import { Service } from '@/Service/Service'
import { userService } from '@/Service/swimming/userService' import { userService } from '@/Service/swimming/userService'
let planCount=ref(0) let planCount = ref(0)
let studentCount=ref(0) let studentCount = ref(0)
let userInfo = ref<any>({}) let userInfo = ref<any>({})
let isUse=ref(false) let isUse = ref(false)
let kefuName = ref('')
let kefuPhone = ref('')
onLoad(() => { onLoad(() => {
@@ -133,30 +180,33 @@
isUseFunc() isUseFunc()
}) })
const isUseFunc=()=>{ const isUseFunc = () => {
userService.IsCheakUserVerify().then(res=>{ userService.IsCheakUserVerify().then(res => {
if(res.code==0){ if (res.code == 0) {
isUse.value=res.data.isCheak isUse.value = res.data.isCheak
}else{ } else {
Service.Msg(res.msg) 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) {
studentCount.value=content.data.studentCount studentCount.value = content.data.studentCount
planCount.value=content.data.planCount planCount.value = content.data.planCount
userInfo.value=content.data.userInfo userInfo.value = content.data.userInfo
kefuName.value = content.data.kefuName
kefuPhone.value = content.data.kefuPhone
} else { } else {
Service.Msg(content.msg) Service.Msg(content.msg)
} }
}) })
} }
const goPage=(path:any)=>{ const goPage = (path : any) => {
if(!isUse.value){ if (!isUse.value) {
Service.Msg('用户暂无权限,请等待审核!') Service.Msg('用户暂无权限,请等待审核!')
return return
} }
@@ -165,7 +215,7 @@
const callContact = () => { const callContact = () => {
uni.makePhoneCall({ uni.makePhoneCall({
phoneNumber: '13800000000', phoneNumber: kefuPhone.value,
fail: () => { fail: () => {
Service.Msg('拨打电话失败') Service.Msg('拨打电话失败')
} }
@@ -181,7 +231,7 @@
.user-container { .user-container {
padding: 20rpx; padding: 20rpx;
padding-top: 30rpx; padding-top: 30rpx;
padding-bottom: 140rpx;
} }
/* 用户信息卡片 */ /* 用户信息卡片 */
@@ -467,6 +517,16 @@
background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%); background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%);
} }
/* 权限管理图标背景色 - 红色渐变 */
&.admin-icon-bg {
background: linear-gradient(135deg, #f5222d 0%, #ff4d4f 50%, #cf1322 100%);
}
/* 联系方式管理图标背景色 - 青色渐变 */
&.contact-icon-bg {
background: linear-gradient(135deg, #13c2c2 0%, #36cfc9 50%, #08979c 100%);
}
&.settings-icon-bg { &.settings-icon-bg {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%); background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
} }
@@ -523,6 +583,13 @@
color: #faad14; color: #faad14;
} }
/* 管理员标签样式 - 红色 */
&.admin-tag,
&.contact-tag {
background: linear-gradient(135deg, rgba(245, 34, 45, 0.12) 0%, rgba(245, 34, 45, 0.08) 100%);
color: #f5222d;
}
.tag-text { .tag-text {
font-size: 20rpx; font-size: 20rpx;
font-weight: 500; font-weight: 500;
@@ -571,7 +638,6 @@
left: 0; left: 0;
right: 0; right: 0;
background-color: #fff; background-color: #fff;
border-top: 1rpx solid #eee;
padding: 16rpx 0; padding: 16rpx 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -359,7 +359,7 @@
} }
}) })
console.log(projectData);
const data = { const data = {
planId: planId.value, planId: planId.value,
@@ -375,13 +375,24 @@
group: '' group: ''
} }
console.log('保存的数据:', data)
PlanService.AddPlan(data).then(res => { PlanService.AddPlan(data).then(res => {
if (res.code == 0) { if (res.code == 0) {
Service.Msg(planId.value?'修改成功!':'添加成功!') Service.Msg(planId.value?'修改成功!':'添加成功!')
if(planId.value){
setTimeout(() => {
Service.GoPageBack()
}, 1000)
uni.$emit('change')
}else{
setTimeout(() => { setTimeout(() => {
Service.GoPageDelse('/pages/userFunc/hunyang?id=' + res.data.planId) Service.GoPageDelse('/pages/userFunc/hunyang?id=' + res.data.planId)
}, 1000) }, 1000)
}
} else { } else {

View File

@@ -0,0 +1,187 @@
<template>
<view class="contact-container">
<!-- 编辑表单 -->
<view class="edit-card">
<view class="card-header">
<u-icon name="edit-pen" size="20" color="#faad14"></u-icon>
<text class="card-title">修改联系方式</text>
</view>
<view class="form-content">
<view class="form-group">
<text class="form-label">联系人姓名</text>
<input class="form-input" v-model="kefuInfo.name" placeholder="请输入联系人姓名" type="text" />
</view>
<view class="form-group">
<text class="form-label">联系电话</text>
<input class="form-input" v-model="kefuInfo.phone" placeholder="请输入联系电话" type="number"
maxlength="11" />
</view>
<button class="save-btn" @click="saveContact">保存修改</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Service } from '@/Service/Service'
import { userService } from '@/Service/swimming/userService'
import { onLoad } from '@dcloudio/uni-app'
let kefuInfo = ref<any>({})
onLoad(() => {
getData()
})
const getData = () => {
userService.GetKefuInfo().then(res => {
if (res.code == 0) {
kefuInfo.value = res.data.kefuInfo
} else {
Service.Msg(res.msg)
}
})
}
const saveContact = () => {
if (!kefuInfo.value.name.trim()) {
Service.Msg('请输入联系人姓名')
return
}
if (!kefuInfo.value.phone.trim()) {
Service.Msg('请输入联系电话')
return
}
const phoneReg = /^1[3-9]\d{9}$|^400-\d{3}-\d{4}$|^\d{3,4}-\d{7,8}$/
if (!phoneReg.test(kefuInfo.value.phone)) {
Service.Msg('请输入有效的联系电话')
return
}
userService.UpdateKefu(kefuInfo.value.phone, kefuInfo.value.name).then(res => {
if (res.code == 0) {
Service.Msg('修改成功!')
getData()
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.contact-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
.contact-card,
.edit-card {
background-color: #fff;
border-radius: 24rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
}
.card-header {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 20rpx;
.card-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
}
}
.contact-info {
background-color: #f8f9fa;
border-radius: 16rpx;
padding: 24rpx;
.info-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
&:last-child {
margin-bottom: 0;
}
.info-label {
font-size: 28rpx;
color: #666;
}
.info-value {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
}
}
.form-content {
.form-group {
margin-bottom: 24rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 12rpx;
}
.form-input {
width: 100%;
height: 88rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 0 24rpx;
font-size: 28rpx;
color: #333;
transition: all 0.2s ease;
&:focus {
background-color: #fff;
box-shadow: 0 0 0 4rpx rgba(24, 144, 255, 0.15);
}
}
}
.save-btn {
width: 100%;
height: 96rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 20rpx;
border: none;
font-size: 32rpx;
color: #fff;
font-weight: 700;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.4);
transition: all 0.3s ease;
&:active {
transform: scale(0.97);
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
}
}
</style>

View File

@@ -6,7 +6,8 @@
<view class="card-section header-section"> <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="header-action" @click="Service.GoPage('/pages/userFunc/addHunyang?id='+planId)"> <view class="header-action" :style="{ 'margin-right': isLandscape?'70rpx':'0' } "
@click="Service.GoPage('/pages/userFunc/addHunyang?id='+planId)">
<up-icon name="setting" size="22"></up-icon> <up-icon name="setting" size="22"></up-icon>
</view> </view>
</view> </view>
@@ -22,47 +23,42 @@
<!-- 分组头部 --> <!-- 分组头部 -->
<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> <view class="group-name">{{ groupState.groupName }}</view>
</view> </view>
<view class="plan-name">{{ groupState.currentPlan?.name }}</view>
<text class="group-plan-index"> <text class="group-plan-index">
计划总时长: {{ groupState.allTime }} {{ groupState.allTime }}
<!-- {{ groupState.currentPlanIndex }}/{{ groupState.plans.length }} --> <!-- {{ groupState.currentPlanIndex }}/{{ groupState.plans.length }} -->
</text> </text>
</view> </view>
<!-- 当前计划内容 --> <!-- 当前计划内容 -->
<view class="plan-content" v-if="!groupState.isCompleted"> <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="countdown-box" :class="{ resting: groupState.isResting }"> <view class="countdown-box" :style="{ 'margin': !isLandscape? '23rpx 0':'4rpx 0' }"
<text class="countdown-number"> :class="{ resting: groupState.isResting }">
<view class="countdown-number"
:style="{transform: !isLandscape? 'scale(2, 2)':'scale(1.3, 1.4 )'}">
{{ formatCountdown(groupState.isResting ? groupState.restCountdown : groupState.countdown) }} {{ formatCountdown(groupState.isResting ? groupState.restCountdown : groupState.countdown) }}
</text> </view>
</view> </view>
<!-- 阶段提示 -->
<!-- <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 class="plan-details"> <view class="plan-details">
<view v-if="!isLandscape" 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 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>
<text class="detail-badge"> <text class="detail-badge">
M:{{ formatCountdown(groupState.currentPlan?.target) }} M:{{ formatCountdown(groupState.currentPlan?.target) }}
</text> </text>
@@ -70,12 +66,18 @@
X:{{ groupState.currentPlan?.rest }} X:{{ groupState.currentPlan?.rest }}
</text> </text>
<text class="detail-badge"> <text class="detail-badge">
Q:{{ groupState.currentPlan?.circle }} Q: {{ groupState.completedLaps }}/{{ groupState.currentPlan?.circle }}
</text> </text>
<view v-if="!isLandscape" class="group-controls">
<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> </view>
<!-- 分组控制按钮 --> <!-- 分组控制按钮 -->
<view class="group-controls"> <view v-if="isLandscape" class="group-controls">
<view v-if="!groupState.isRunning && !groupState.isCompleted" <view v-if="!groupState.isRunning && !groupState.isCompleted"
class="group-control-btn group-start-btn" @click="startGroup(groupState)"> class="group-control-btn group-start-btn" @click="startGroup(groupState)">
<u-icon name="play-circle" size="24" color="#fff"></u-icon> <u-icon name="play-circle" size="24" color="#fff"></u-icon>
@@ -97,6 +99,8 @@
<view class="plan-completed" v-else> <view class="plan-completed" v-else>
<u-icon name="checkmark-circle-fill" size="48" color="#4caf50"></u-icon> <u-icon name="checkmark-circle-fill" size="48" color="#4caf50"></u-icon>
<text class="completed-label">分组完成</text> <text class="completed-label">分组完成</text>
<text class="completed-label" style="color: var(--nav-mian); font-size: 24rpx;"
@click="resetGroup(groupIndex)">重置</text>
</view> </view>
</view> </view>
</view> </view>
@@ -194,13 +198,18 @@
// ==================== 生命周期 ==================== // ==================== 生命周期 ====================
onLoad((data : any) => { onLoad((data : any) => {
planId.value = data.id planId.value = data.id
getPlanInfo()
})
onShow(() => {
checkOrientation() checkOrientation()
uni.onWindowResize(checkOrientation) uni.onWindowResize(checkOrientation)
}) })
onShow(() => { onShow(() => {
uni.$on('change',()=>{
getPlanInfo() getPlanInfo()
}) })
})
onUnmounted(() => { onUnmounted(() => {
stopTimer() stopTimer()
@@ -542,6 +551,7 @@
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 16rpx; margin-bottom: 16rpx;
gap: 4rpx;
} }
.group-title-area { .group-title-area {
@@ -554,6 +564,10 @@
font-size: 22rpx; font-size: 22rpx;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
width: 78rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
.group-plan-index { .group-plan-index {
@@ -580,10 +594,11 @@
} }
.plan-name { .plan-name {
font-size: 28rpx; font-size: 22rpx;
font-weight: 600; font-weight: 600;
color: #333; color: #333;
margin-bottom: 8rpx;
} }
/* 圈数指示器 */ /* 圈数指示器 */
@@ -631,14 +646,11 @@
} }
.countdown-box { .countdown-box {
margin: 20rpx 0;
.countdown-number { .countdown-number {
font-size: 72rpx; font-size: 72rpx;
font-weight: bold; font-weight: bold;
font-family: 'Helvetica Neue', monospace;
color: #000; color: #000;
letter-spacing: 2rpx;
} }
&.resting .countdown-number { &.resting .countdown-number {
@@ -649,7 +661,7 @@
.plan-details { .plan-details {
display: flex; display: flex;
justify-content: center; justify-content: space-between;
align-items: center; align-items: center;
gap: 16rpx; gap: 16rpx;
} }
@@ -869,19 +881,27 @@
} }
.group-name { .group-name {
font-size: 22rpx; font-size: 18rpx;
letter-spacing: 1rpx; letter-spacing: 1rpx;
width: 78rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
} }
.group-plan-index { .group-plan-index {
font-size: 22rpx; font-size: 18rpx;
letter-spacing: 1rpx; letter-spacing: 1rpx;
} }
.plan-name { .plan-name {
font-size: 28rpx; font-size: 18rpx;
margin-bottom: 8rpx;
letter-spacing: 1rpx; letter-spacing: 1rpx;
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: center;
} }
.laps-indicator { .laps-indicator {
@@ -898,8 +918,8 @@
margin: 10rpx 0; margin: 10rpx 0;
.countdown-number { .countdown-number {
font-size: 60rpx; font-size: 72rpx;
letter-spacing: 4rpx; transform: scale(1.2, 1.3);
} }
} }

View File

@@ -52,7 +52,7 @@
</view> </view>
</view> </view>
<view class="timers-grid"> <view :class=" isLandscape?'timers-grid-y':'timers-grid-x' ">
<view v-for="(timer, index) in group.timers" :key="timer.id" class="timer-card-wrapper"> <view v-for="(timer, index) in group.timers" :key="timer.id" class="timer-card-wrapper">
<view class="timer-circle" :class="{ resting: timer.isResting }" @click="recordTimer(timer)"> <view class="timer-circle" :class="{ resting: timer.isResting }" @click="recordTimer(timer)">
<view class="timer-header"> <view class="timer-header">
@@ -60,6 +60,14 @@
<text class="name-text">{{ timer.studentName }}</text> <text class="name-text">{{ timer.studentName }}</text>
</view> </view>
<view class="delete-btn-wrapper"> <view class="delete-btn-wrapper">
<view v-if="isLandscape" @click.stop="openTimerDetail(timer)">
<u-icon name="edit-pen" size="24" color="#22a6f2"></u-icon>
</view>
<view v-if="isLandscape" @click.stop="toggleTimer(timer)" class="">
<u-icon
:name="timer.status === 'running' || timer.status === 'resting' ? 'pause-circle' : 'play-circle'"
size="24" :color="timer.status === 'resting' ? '#faad14' : '#22a6f2'"></u-icon>
</view>
<view class="delete-btn" @click.stop="deleteTimer(timer, group, index)"> <view class="delete-btn" @click.stop="deleteTimer(timer, group, index)">
<u-icon name="trash" size="14" color="#fff"></u-icon> <u-icon name="trash" size="14" color="#fff"></u-icon>
</view> </view>
@@ -73,25 +81,33 @@
已完成 已完成
</view> </view>
<view v-else-if="timer.isResting" class="timer-rest-countdown"> <view v-else-if="timer.isResting" class="timer-rest-countdown">
<text class="rest-label">休息</text>
<text <text :class=" isLandscape? 'rest-time-x':'rest-time-y'"
class="rest-time">{{ formatRestCountdown(timer.restCountdown || 0) }}</text> >{{ formatRestCountdown(timer.restCountdown || 0) }}</text>
</view> </view>
<view v-else class=""> <view v-else class="circle-inner" >
<view class="timer-label-box ">
<view class="timer-label"> <view class="timer-label">
M{{formatTimeObject(timer.targetTime)}}X{{ timer.restTime }} M{{formatTimeObject(timer.targetTime)}}X{{ timer.restTime }}
</view> </view>
<view class="timer-time">{{ formatTime(timer) }}</view>
</view>
<view class="timer-info"> <view class="timer-info">
<text <text
class="timer-duration">{{ timer.records.length }}/{{ timer.totalLapCount || 4 }}</text> class="timer-duration">{{ timer.records.length }}/{{ timer.totalLapCount || 4 }}</text>
</view> </view>
</view> </view>
<view :class=" isLandscape? 'timer-time-x':'timer-time-y'">
{{ formatTime(timer) }}
</view>
</view>
<!-- <view v-if="!isLandscape && !timer.isResting " class="timer-info">
<text
class="timer-duration">{{ timer.records.length }}/{{ timer.totalLapCount || 4 }}</text>
</view> -->
</view>
</view> </view>
</view> </view>
<view class="timer-controls"> <view v-if="!isLandscape" class="timer-controls">
<view @click.stop="openTimerDetail(timer)"> <view @click.stop="openTimerDetail(timer)">
<u-icon name="edit-pen" size="24" color="#22a6f2"></u-icon> <u-icon name="edit-pen" size="24" color="#22a6f2"></u-icon>
</view> </view>
@@ -147,7 +163,7 @@
<view class="detail-name">{{ selectedTimer?.studentName || '计时器' }}</view> <view class="detail-name">{{ selectedTimer?.studentName || '计时器' }}</view>
<view class="stopwatch-display"> <view class="stopwatch-display">
<text class="stopwatch-time">{{ formatStopwatchTime(selectedTimer?.elapsedTime || 0) }}</text> <text class="stopwatch-time">{{ formatStopwatchTime(selectedTimer?.elapsedTime || 0) }}</text>
<text class="stopwatch-millis">{{ formatMillis(selectedTimer?.elapsedTime || 0) }}</text> <!-- <text class="stopwatch-millis">{{ formatMillis(selectedTimer?.elapsedTime || 0) }}</text> -->
</view> </view>
<view class="current-settings-info"> <view class="current-settings-info">
<view class="setting-info-item"> <view class="setting-info-item">
@@ -438,6 +454,10 @@
lastRecordTimestamp : number // 上次记录时的真实时间戳(毫秒) lastRecordTimestamp : number // 上次记录时的真实时间戳(毫秒)
currentLapTime : number // 当前圈已用时间(秒),用于记录 currentLapTime : number // 当前圈已用时间(秒),用于记录
hasRecordedThisLap : boolean // 当前圈是否已手动记录 hasRecordedThisLap : boolean // 当前圈是否已手动记录
// 以下为运行时字段,不持久化,由全局驱动引擎维护
_startAt ?: number // 当前轮次开始时间戳(毫秒)
_currentLapStartAt ?: number // 当前圈开始时间戳(毫秒)
_restStartAt ?: number // 休息倒计时开始时间戳(毫秒)
} }
/** 分组 */ /** 分组 */
@@ -749,101 +769,69 @@
getData() getData()
}) })
// ===================== 学生计时器核心逻辑 ===================== // ===================== 全局统一驱动引擎 =====================
/** /**
* 存储每个学生的主计时器 setInterval ID * 方案 B单一定时器驱动引擎
* Key = studentIdValue = intervalId * 不再给每个学生创建独立的 setInterval而是用一个全局 interval 每 50ms 统一 tick
* 遍历所有 running/resting 的学生并计算时间差。彻底消除人多时大量 interval 导致的性能问题。
*/ */
const studentIntervals = new Map<string, number>() let engineIntervalId : number | null = null
/** const startEngine = () => {
* 存储每个学生的休息倒计时 setInterval ID if (engineIntervalId) return
* Key = studentIdValue = intervalId engineIntervalId = setInterval(() => {
*/ tick()
const studentRestIntervals = new Map<string, number>()
/** 清除指定学生的主计时器定时器 */
const clearStudentInterval = (studentId : string) => {
const id = studentIntervals.get(studentId)
if (id) {
clearInterval(id)
studentIntervals.delete(studentId)
}
}
/** 清除指定学生的休息倒计时定时器 */
const clearStudentRestInterval = (studentId : string) => {
const id = studentRestIntervals.get(studentId)
if (id) {
clearInterval(id)
studentRestIntervals.delete(studentId)
}
}
/**
* 存储每个学生的真实时间计时器 setInterval ID
* 用于 elapsedTime不受休息影响只在 pause/complete/reset 时停止
*/
const elapsedTimeIntervals = new Map<string, number>()
/** 启动真实时间计时器(不受休息影响) */
const startElapsedTimeTimer = (timer : TimerItem) => {
if (elapsedTimeIntervals.has(timer.studentId)) return
if (timer.startTimestamp === 0) {
timer.startTimestamp = Date.now()
}
const id = setInterval(() => {
timer.elapsedTime = (Date.now() - timer.startTimestamp) / 1000
}, 50) as unknown as number }, 50) as unknown as number
elapsedTimeIntervals.set(timer.studentId, id)
} }
/** 停止真实时间计时器 */ const stopEngine = () => {
const stopElapsedTimeTimer = (studentId : string) => { if (engineIntervalId) {
const id = elapsedTimeIntervals.get(studentId) clearInterval(engineIntervalId)
if (id) { engineIntervalId = null
clearInterval(id)
elapsedTimeIntervals.delete(studentId)
} }
} }
/** const tick = () => {
* 启动学生主计时器 const now = Date.now()
* 逻辑:
* 1. 如果当前已经在运行中或已完成,则直接返回 // 1. 更新全局计时器(间隔出发模式用)
* 2. 先清理旧的定时器防止重复启动多个interval导致时间加速 if (globalTimerRunning.value) {
* 3. 设置状态为 running记录 hasStarted = true globalTimerTime.value = globalBaseTime + (now - globalStartAt) / 1000
* 4. 通过 Date.now() 与 currentTime 反推出"开始时刻",然后用 setInterval 每50ms更新 currentTime
*
* 注意:允许从 resting 状态恢复计时,所以不拦截 resting
*/
const startStudentTimer = (timer : TimerItem) => {
if (timer.status === 'running' || timer.status === 'completed') return
// 如果有保存的暂停休息时间,优先恢复休息倒计时
if (timer.pausedRestCountdown > 0) {
startRestFromPause(timer)
return
} }
clearStudentInterval(timer.studentId)
clearStudentRestInterval(timer.studentId) // 2. 间隔出发检查(全局计时器驱动模式下)
timer.status = 'running' if (globalTimerRunning.value && setting.value.mode === 'interval') {
timer.hasStarted = true const intervalSec = toNumber(setting.value.interval, 5)
// 如果倒计时已归零,重置为目标时间 groups.value.forEach((group) => {
if (timer.countdownTime <= 0) { group.timers.forEach((t, studentIndex) => {
timer.countdownTime = timer.targetTime const startAt = studentIndex * intervalSec
if (globalTimerTime.value >= startAt && !t.hasStarted && t.status !== 'completed' && t.status !== 'running' && t.status !== 'resting') {
startStudentTimer(t)
if (studentIndex > 0) {
Service.Msg(`${t.studentName} 已出发`)
}
}
})
})
}
// 3. 统一更新所有 running / resting 的学生计时器
getAllTimers.value.forEach((timer) => {
if (timer.status === 'running') {
// elapsedTime真实时间休息不暂停
if (timer.startTimestamp > 0) {
timer.elapsedTime = (now - timer.startTimestamp) / 1000
}
// currentTime累计游泳时间
if (timer._startAt) {
timer.currentTime = (now - timer._startAt) / 1000
}
// currentLapTime当前圈已用时间
if (timer._currentLapStartAt) {
timer.currentLapTime = (now - timer._currentLapStartAt) / 1000
} }
// 启动真实时间计时器(不受休息影响)
startElapsedTimeTimer(timer)
// 通过当前累计时间反推开始时刻,保证暂停后继续计时的连续性
const startAt = Date.now() - timer.currentTime * 1000
// 当前圈开始时刻(用于计算 currentLapTime
let currentLapStartAt = Date.now() - timer.currentLapTime * 1000
const id = setInterval(() => {
// 更新累计游泳时间
timer.currentTime = (Date.now() - startAt) / 1000
// 更新当前圈已用时间
timer.currentLapTime = (Date.now() - currentLapStartAt) / 1000
// 计算周期和显示状态 // 计算周期和显示状态
const cycleTime = timer.targetTime + timer.restTime const cycleTime = timer.targetTime + timer.restTime
@@ -860,7 +848,7 @@
} }
// 检查是否达到周期(目标时间 + 休息时间) // 检查是否达到周期(目标时间 + 休息时间)
if (timer.currentLapTime >= cycleTime && timer.status === 'running') { if (timer.currentLapTime >= cycleTime) {
// 周期结束自动记录0分如果本圈还没被记录过 // 周期结束自动记录0分如果本圈还没被记录过
if (!timer.hasRecordedThisLap && timer.records.length < timer.totalLapCount) { if (!timer.hasRecordedThisLap && timer.records.length < timer.totalLapCount) {
timer.records.push({ time: 0 }) timer.records.push({ time: 0 })
@@ -871,14 +859,67 @@
timer.countdownTime = timer.targetTime timer.countdownTime = timer.targetTime
timer.isResting = false timer.isResting = false
timer.restCountdown = 0 timer.restCountdown = 0
currentLapStartAt = Date.now() timer._currentLapStartAt = now
// 检查是否完成所有圈数 // 检查是否完成所有圈数
if (timer.records.length >= timer.totalLapCount) { if (timer.records.length >= timer.totalLapCount) {
completeTimer(timer) completeTimer(timer)
} }
} }
}, 50) as unknown as number } else if (timer.status === 'resting') {
studentIntervals.set(timer.studentId, id) // elapsedTime休息期间也继续计时
if (timer.startTimestamp > 0) {
timer.elapsedTime = (now - timer.startTimestamp) / 1000
}
// 休息倒计时
if (timer._restStartAt) {
const remaining = Math.max(0, timer.restTime - (now - timer._restStartAt) / 1000)
timer.restCountdown = remaining
if (remaining <= 0) {
timer.pausedRestCountdown = 0
if (timer.lapCount >= timer.totalLapCount) {
completeTimer(timer)
Service.Msg(`${timer.studentName} 休息结束,已完成所有圈数`)
} else {
timer.countdownTime = timer.targetTime
startStudentTimer(timer)
}
}
}
}
})
}
/**
* 启动学生主计时器方案B不再创建独立 setInterval由全局 tick 驱动)
* 逻辑:
* 1. 如果当前已经在运行中或已完成,则直接返回
* 2. 如果有保存的暂停休息时间,优先恢复休息倒计时
* 3. 设置状态为 running记录 hasStarted = true
* 4. 通过 Date.now() 与 currentTime 反推"开始时刻",存入 _startAt / _currentLapStartAt
* 5. 启动全局引擎
*/
const startStudentTimer = (timer : TimerItem) => {
if (timer.status === 'running' || timer.status === 'completed') return
// 如果有保存的暂停休息时间,优先恢复休息倒计时
if (timer.pausedRestCountdown > 0) {
startRestFromPause(timer)
return
}
timer.status = 'running'
timer.hasStarted = true
// 如果倒计时已归零,重置为目标时间
if (timer.countdownTime <= 0) {
timer.countdownTime = timer.targetTime
}
// 初始化 elapsedTime 起始时间戳(仅首次)
if (timer.startTimestamp === 0) {
timer.startTimestamp = Date.now()
}
// 通过当前累计时间反推开始时刻,保证暂停后继续计时的连续性
timer._startAt = Date.now() - timer.currentTime * 1000
timer._currentLapStartAt = Date.now() - timer.currentLapTime * 1000
startEngine()
} }
/** /**
@@ -895,16 +936,12 @@
} }
/** /**
* 暂停学生计时器 * 暂停学生计时器方案B不再需要 clearInterval只改状态
* 逻辑: * 逻辑:
* 1. 清除主计时器和休息倒计时 * 1. 如果当前在 running 或 resting状态改为 paused
* 2. 如果当前在 running 或 resting状态改为 paused * 2. 重置 resting 相关标志
* 3. 重置 resting 相关标志
*/ */
const pauseStudentTimer = (timer : TimerItem) => { const pauseStudentTimer = (timer : TimerItem) => {
clearStudentInterval(timer.studentId)
clearStudentRestInterval(timer.studentId)
stopElapsedTimeTimer(timer.studentId)
if (timer.status === 'running' || timer.status === 'resting') { if (timer.status === 'running' || timer.status === 'resting') {
// 如果正在休息,保存剩余休息时间 // 如果正在休息,保存剩余休息时间
if (timer.status === 'resting' && timer.restCountdown > 0) { if (timer.status === 'resting' && timer.restCountdown > 0) {
@@ -917,20 +954,16 @@
} }
/** /**
* 启动休息倒计时 * 启动休息倒计时方案B不再创建独立 setInterval由全局 tick 驱动)
* 触发时机:学生完成一圈后(自动完成,不记录成绩) * 触发时机:学生完成一圈后(自动完成,不记录成绩)
* *
* 逻辑: * 逻辑:
* 1. 先停止主计时器(休息时间不计入 currentTime * 1. 设置状态为 resting
* 2. 不停止 elapsedTime 计时器(休息期间继续计时) * 2. 记录 _restStartAt由全局 tick 计算 restCountdown
* 3. 设置状态为 resting开始倒计时 * 3. 如果没有休息时间,直接处理后续逻辑
* 4. 每50ms更新 restCountdown
* 5. 倒计时结束后:如果是最后一圈则标记为完成,否则开始下一轮
*/ */
const startRest = (timer : TimerItem) => { const startRest = (timer : TimerItem) => {
const restTime = timer.restTime || 0 const restTime = timer.restTime || 0
clearStudentInterval(timer.studentId)
clearStudentRestInterval(timer.studentId)
timer.status = 'resting' timer.status = 'resting'
timer.isResting = true timer.isResting = true
timer.restCountdown = restTime timer.restCountdown = restTime
@@ -947,31 +980,14 @@
} }
return return
} }
const restStart = Date.now() timer._restStartAt = Date.now()
const id = setInterval(() => { startEngine()
const remaining = Math.max(0, restTime - (Date.now() - restStart) / 1000)
timer.restCountdown = remaining
if (remaining <= 0) {
clearStudentRestInterval(timer.studentId)
// 休息结束,判断是否为最后一圈
if (timer.lapCount >= timer.totalLapCount) {
// 最后一圈休息结束,标记为完成
// completeTimer(timer)
Service.Msg(`${timer.studentName} 休息结束,已完成所有圈数`)
} else {
// 非最后一圈,开始下一轮
timer.countdownTime = timer.targetTime
startStudentTimer(timer)
}
}
}, 50) as unknown as number
studentRestIntervals.set(timer.studentId, id)
const isLastLap = timer.lapCount >= timer.totalLapCount const isLastLap = timer.lapCount >= timer.totalLapCount
// Service.Msg(isLastLap ? '最后一圈完成,进入休息' : `完成一圈,休息${restTime}秒`) // Service.Msg(isLastLap ? '最后一圈完成,进入休息' : `完成一圈,休息${restTime}秒`)
} }
/** /**
* 从暂停状态恢复休息倒计时 * 从暂停状态恢复休息倒计时方案B不再创建独立 setInterval由全局 tick 驱动)
* 触发时机:学生在休息中暂停后再次点击开始 * 触发时机:学生在休息中暂停后再次点击开始
*/ */
const startRestFromPause = (timer : TimerItem) => { const startRestFromPause = (timer : TimerItem) => {
@@ -988,41 +1004,20 @@
} }
return return
} }
clearStudentInterval(timer.studentId)
clearStudentRestInterval(timer.studentId)
timer.status = 'resting' timer.status = 'resting'
timer.isResting = true timer.isResting = true
timer.restCountdown = restTime timer.restCountdown = restTime
const restStart = Date.now() timer._restStartAt = Date.now()
const id = setInterval(() => { startEngine()
const remaining = Math.max(0, restTime - (Date.now() - restStart) / 1000)
timer.restCountdown = remaining
if (remaining <= 0) {
clearStudentRestInterval(timer.studentId)
// 休息结束,判断是否为最后一圈
timer.pausedRestCountdown = 0
if (timer.lapCount >= timer.totalLapCount) {
completeTimer(timer)
Service.Msg(`${timer.studentName} 休息结束,已完成所有圈数`)
} else {
timer.countdownTime = timer.targetTime
startStudentTimer(timer)
}
}
}, 50) as unknown as number
studentRestIntervals.set(timer.studentId, id)
const isLastLap = timer.lapCount >= timer.totalLapCount const isLastLap = timer.lapCount >= timer.totalLapCount
Service.Msg(isLastLap ? '最后一圈休息中...' : `继续休息,剩余${restTime.toFixed(1)}`) Service.Msg(isLastLap ? '最后一圈休息中...' : `继续休息,剩余${restTime.toFixed(1)}`)
} }
/** /**
* 将学生计时器标记为已完成 * 将学生计时器标记为已完成方案B不再需要 clearInterval
* 触发时机lapCount 达到 totalLapCount * 触发时机lapCount 达到 totalLapCount
*/ */
const completeTimer = (timer : TimerItem) => { const completeTimer = (timer : TimerItem) => {
clearStudentInterval(timer.studentId)
clearStudentRestInterval(timer.studentId)
stopElapsedTimeTimer(timer.studentId)
timer.status = 'completed' timer.status = 'completed'
timer.isResting = false timer.isResting = false
timer.restCountdown = 0 timer.restCountdown = 0
@@ -1087,6 +1082,9 @@
timer.lastRecordTimestamp = 0 timer.lastRecordTimestamp = 0
timer.currentLapTime = 0 timer.currentLapTime = 0
timer.hasRecordedThisLap = false timer.hasRecordedThisLap = false
timer._startAt = undefined
timer._currentLapStartAt = undefined
timer._restStartAt = undefined
} }
/** /**
@@ -1109,7 +1107,6 @@
const globalTimerTime = ref(0) // 全局累计时间(秒) const globalTimerTime = ref(0) // 全局累计时间(秒)
const globalTimerRunning = ref(false) // 全局计时器是否运行中 const globalTimerRunning = ref(false) // 全局计时器是否运行中
let globalTimerInterval : number | null = null // 全局定时器ID
let globalBaseTime = 0 // 暂停前已积累的时间 let globalBaseTime = 0 // 暂停前已积累的时间
let globalStartAt = 0 // 本次启动的时刻戳 let globalStartAt = 0 // 本次启动的时刻戳
@@ -1119,19 +1116,15 @@
globalTimerTime.value = globalBaseTime + (Date.now() - globalStartAt) / 1000 globalTimerTime.value = globalBaseTime + (Date.now() - globalStartAt) / 1000
} }
globalTimerRunning.value = false globalTimerRunning.value = false
if (globalTimerInterval) {
clearInterval(globalTimerInterval)
globalTimerInterval = null
}
} }
/** /**
* 启动全局计时器 * 启动全局计时器方案B全局计时逻辑已合并到统一 tick 中)
* *
* 逻辑: * 逻辑:
* 1. 先停止旧全局计时器 * 1. 先停止旧全局计时器
* 2. 恢复所有已暂停的学生计时器(间隔模式下) * 2. 恢复所有已暂停的学生计时器(间隔模式下)
* 3. 启动全局时间累计每50ms更新 globalTimerTime * 3. 启动全局时间累计(由统一 tick 每50ms更新 globalTimerTime
* 4. "一起出发"模式下所有学生立即出发startAt = 0 * 4. "一起出发"模式下所有学生立即出发startAt = 0
* 5. "间隔出发"模式下每组内学生按索引 * 间隔秒数依次出发 * 5. "间隔出发"模式下每组内学生按索引 * 间隔秒数依次出发
* 6. 一旦 globalTimerTime >= 学生出发时间,且学生未出发过,则启动该学生计时器 * 6. 一旦 globalTimerTime >= 学生出发时间,且学生未出发过,则启动该学生计时器
@@ -1141,7 +1134,6 @@
globalTimerRunning.value = true globalTimerRunning.value = true
globalStartAt = Date.now() globalStartAt = Date.now()
globalBaseTime = globalTimerTime.value globalBaseTime = globalTimerTime.value
const intervalSec = toNumber(setting.value.interval, 5)
// 间隔模式下:立即恢复所有已暂停的学生 // 间隔模式下:立即恢复所有已暂停的学生
if (setting.value.mode === 'interval') { if (setting.value.mode === 'interval') {
@@ -1159,20 +1151,7 @@
} }
} }
globalTimerInterval = setInterval(() => { startEngine()
globalTimerTime.value = globalBaseTime + (Date.now() - globalStartAt) / 1000
groups.value.forEach((group) => {
group.timers.forEach((t, studentIndex) => {
const startAt = setting.value.mode === 'together' ? 0 : studentIndex * intervalSec
if (globalTimerTime.value >= startAt && !t.hasStarted && t.status !== 'completed' && t.status !== 'running' && t.status !== 'resting') {
startStudentTimer(t)
if (setting.value.mode !== 'together' && studentIndex > 0) {
Service.Msg(`${t.studentName} 已出发`)
}
}
})
})
}, 50) as unknown as number
} }
// ===================== 分组级操作 ===================== // ===================== 分组级操作 =====================
@@ -1793,6 +1772,14 @@
}) })
}) })
uni.showModal({
title: '提示', // 对话框标题
content: '请确定提交数据!', // 显示的内容
showCancel: true, // 是否显示取消按钮
cancelText: '取消', // 取消按钮的文字
confirmText: '确定', // 确认按钮的文字
success: function (res) {
if (res.confirm) {
PlanService.AddPlanLog(selectedProject.value.planId, '包干项目', '', '', JSON.stringify(data)).then(res => { PlanService.AddPlanLog(selectedProject.value.planId, '包干项目', '', '', JSON.stringify(data)).then(res => {
if (res.code == 0) { if (res.code == 0) {
Service.Msg('提交成功!') Service.Msg('提交成功!')
@@ -1801,6 +1788,12 @@
Service.Msg(res.msg) Service.Msg(res.msg)
} }
}) })
} else if (res.cancel) {
console.log('用户点击取消');
}
}
});
} }
// ===================== 格式化函数 ===================== // ===================== 格式化函数 =====================
@@ -1813,18 +1806,18 @@
const mins = Math.floor(time / 60) const mins = Math.floor(time / 60)
const secs = Math.floor(time % 60) const secs = Math.floor(time % 60)
const millis = Math.floor((time % 1) * 100) const millis = Math.floor((time % 1) * 100)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}` return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
} }
/** 学生卡片主计时显示(目标时间倒计时):分:秒:毫秒 */ /** 学生卡片主计时显示(目标时间倒计时):分:秒:毫秒 */
const formatTime = (timer : any) : string => { const formatTime = (timer : any) : string => {
const time = Math.max(0, timer.countdownTime ||0) const time = Math.max(0, timer.countdownTime || 0)
const mins = Math.floor(time / 60) const mins = Math.floor(time / 60)
const secs = Math.floor(time % 60) const secs = Math.floor(time % 60)
const millis = Math.floor((time % 1) * 100) const millis = Math.floor((time % 1) * 100)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}` return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
} }
/** 计时器显示(真实经过时间):分:秒:毫秒 */ /** 计时器显示(真实经过时间):分:秒:毫秒 */
@@ -1833,7 +1826,7 @@
const mins = Math.floor(time / 60) const mins = Math.floor(time / 60)
const secs = Math.floor(time % 60) const secs = Math.floor(time % 60)
const millis = Math.floor((time % 1) * 100) const millis = Math.floor((time % 1) * 100)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}` return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
} }
/** 目标时间显示:分:秒:毫秒 */ /** 目标时间显示:分:秒:毫秒 */
@@ -1841,21 +1834,21 @@
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 millis = Math.floor((seconds % 1) * 100) const millis = Math.floor((seconds % 1) * 100)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}` return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
} }
/** 休息倒计时显示:秒.毫秒 */ /** 休息倒计时显示:秒 */
const formatRestCountdown = (seconds : number) : string => { const formatRestCountdown = (seconds : number) : string => {
const secs = Math.floor(seconds) const secs = Math.floor(seconds)
const millis = Math.floor((seconds % 1) * 100) const millis = Math.floor((seconds % 1) * 100)
return `${secs.toString().padStart(2, '0')}.${millis.toString().padStart(2, '0')}` return `${secs.toString().padStart(2, '0')}`
} }
/** 详情弹窗秒表大字体部分:分:秒 */ /** 详情弹窗秒表大字体部分:分:秒 */
const formatStopwatchTime = (seconds : number) : string => { const formatStopwatchTime = (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')}` return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
} }
/** 详情弹窗毫秒小字体部分 */ /** 详情弹窗毫秒小字体部分 */
@@ -1898,12 +1891,7 @@
onUnmounted(() => { onUnmounted(() => {
stopGlobalTimer() stopGlobalTimer()
groups.value.forEach(g => stopGroupDeparture(g)) groups.value.forEach(g => stopGroupDeparture(g))
studentIntervals.forEach(id => clearInterval(id)) stopEngine()
studentRestIntervals.forEach(id => clearInterval(id))
elapsedTimeIntervals.forEach(id => clearInterval(id))
studentIntervals.clear()
studentRestIntervals.clear()
elapsedTimeIntervals.clear()
uni.offWindowResize(updateOrientation) uni.offWindowResize(updateOrientation)
}) })
</script> </script>
@@ -2117,8 +2105,8 @@
.card-section { .card-section {
background-color: #fff; background-color: #fff;
border-radius: 16px; border-radius: 16px;
padding: 20px 14px; padding: 14rpx 20rpx;
margin-bottom: 16px; margin-bottom: 10rpx;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
} }
@@ -2198,7 +2186,7 @@
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 16px; margin-bottom: 4rpx;
.section-title { .section-title {
margin-bottom: 0; margin-bottom: 0;
@@ -2308,9 +2296,17 @@
font-variant-numeric: tabular-nums; font-variant-numeric: tabular-nums;
} }
.timers-grid { .timers-grid-y {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, 162px); grid-template-columns: repeat(4, 1fr);
gap: 6px;
justify-content: start;
}
.timers-grid-x {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 6px; gap: 6px;
justify-content: start; justify-content: start;
} }
@@ -2344,7 +2340,7 @@
border-radius: 10px; border-radius: 10px;
background-color: #fff; background-color: #fff;
display: flex; display: flex;
padding: 12px 10px; padding: 10rpx 6rpx 14rpx;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -2381,6 +2377,9 @@
.delete-btn-wrapper { .delete-btn-wrapper {
z-index: 10; z-index: 10;
margin-left: 8px; margin-left: 8px;
display: flex;
align-items: center;
gap: 10rpx;
.delete-btn { .delete-btn {
width: 24px; width: 24px;
@@ -2407,6 +2406,10 @@
.circle-content { .circle-content {
width: 100%; width: 100%;
height: 110px;
display: flex;
align-items: center;
justify-content: center;
padding: 6px 0; padding: 6px 0;
border-radius: 6px; border-radius: 6px;
border: 2px solid #faad14; border: 2px solid #faad14;
@@ -2422,12 +2425,13 @@
} }
.circle-inner { .circle-inner {
height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: space-around;
gap: 4px; gap: 2rpx;
padding: 4px 8px; // padding: 4px 8px;
} }
} }
@@ -2447,12 +2451,22 @@
background: rgba(255, 255, 255, 0.8); background: rgba(255, 255, 255, 0.8);
} }
.rest-time { .rest-time-x {
font-size: 24px; font-size: 48rpx;
transform: scale(1.7,1.9);
font-weight: bold; font-weight: bold;
color: #ff7875; color: #ff7875;
animation: countdownFlash 0.5s ease-in-out infinite; animation: countdownFlash 0.5s ease-in-out infinite;
} }
.rest-time-y {
font-size: 48rpx;
transform: scale(2.8,3.2);
font-weight: bold;
color: #ff7875;
animation: countdownFlash 0.5s ease-in-out infinite;
}
} }
.timer-target { .timer-target {
@@ -2466,17 +2480,34 @@
white-space: nowrap; white-space: nowrap;
} }
.timer-label-box {
display: flex;
justify-content: space-between;
align-items: center;
}
.timer-label { .timer-label {
font-size: 12px; font-size: 12px;
color: #333; color: #333;
text-align: center; text-align: center;
} }
.timer-time { .timer-time-y {
font-size: 24px; font-size: 56rpx;
font-weight: bold;
color: #333;
transform: scale(1.5, 1.9);
}
.timer-time-x {
font-size: 40rpx;
font-weight: bold; font-weight: bold;
color: #333; color: #333;
line-height: 1.2; line-height: 1.2;
transform: scale(1.2, 1.3);
} }
.timer-duration { .timer-duration {

View File

@@ -98,7 +98,7 @@
</view> </view>
</view> </view>
<view class="record-list" <scroll-view class="record-list" scroll-y
v-if="currentStudent && currentStudent.segments && currentStudent.segments.length > 0"> v-if="currentStudent && currentStudent.segments && currentStudent.segments.length > 0">
<view class="record-header"> <view class="record-header">
<text class="record-cell header-cell">分段</text> <text class="record-cell header-cell">分段</text>
@@ -112,7 +112,7 @@
<text class="record-cell time-cell">{{ formatTime(segment.time) }}</text> <text class="record-cell time-cell">{{ formatTime(segment.time) }}</text>
<text class="record-cell time-cell">{{ formatTimeDiff(index, segment.time) }}</text> <text class="record-cell time-cell">{{ formatTimeDiff(index, segment.time) }}</text>
</view> </view>
</view> </scroll-view>
<view class="empty-record" v-else> <view class="empty-record" v-else>
<u-icon name="info-circle" size="48" color="#ccc"></u-icon> <u-icon name="info-circle" size="48" color="#ccc"></u-icon>
@@ -402,7 +402,7 @@
students.value.map((item : any) => { students.value.map((item : any) => {
let record = item.segments.map((content : any, index : any) => { let record = item.segments.map((content : any, index : any) => {
return { return {
circle: (index + 1) * segmentDistance.value, circle: (index + 1)*segmentDistance.value,
time: content.time time: content.time
} }
}) })
@@ -671,7 +671,7 @@
.record-popup { .record-popup {
background-color: #fff; background-color: #fff;
border-radius: 20rpx 20rpx 0 0; border-radius: 20rpx 20rpx 0 0;
max-height: 80vh; height: 70vh;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -220,7 +220,7 @@
const getStudentList = async () => { const getStudentList = async () => {
loading.value = true loading.value = true
studentService.GetStudentList().then(res => { studentService.GetStudentList(planId.value ).then(res => {
if (res.code == 0) { if (res.code == 0) {
allStudents.value = res.data allStudents.value = res.data
loading.value = false loading.value = false

View File

@@ -316,7 +316,7 @@
const groupOffset = (group.groupNumber - 1) * interval const groupOffset = (group.groupNumber - 1) * interval
if (elapsed >= groupOffset && !notifiedGroupNumbers.value.has(group.groupNumber)) { if (elapsed >= groupOffset && !notifiedGroupNumbers.value.has(group.groupNumber)) {
notifiedGroupNumbers.value.add(group.groupNumber) notifiedGroupNumbers.value.add(group.groupNumber)
Service.Msg(`${group.groupNumber}组出发了`) // Service.Msg(`第${group.groupNumber}组出发了`)
speak() speak()
} }
}) })
@@ -672,8 +672,7 @@
.athlete-item { .athlete-item {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 24rpx; padding: 14rpx 24rpx;
margin-bottom: 12rpx;
background-color: #ffffff; background-color: #ffffff;
border-radius: 16rpx; border-radius: 16rpx;
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03); box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03);
@@ -883,8 +882,8 @@
/* 可拖动悬浮球 */ /* 可拖动悬浮球 */
.float-ball { .float-ball {
position: fixed; position: fixed;
width: 120rpx; width: 170rpx;
height: 120rpx; height: 170rpx;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%); background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
display: flex; display: flex;
@@ -916,7 +915,7 @@
.ball-text { .ball-text {
color: #fff; color: #fff;
font-size: 28rpx; font-size: 40rpx;
font-weight: 700; font-weight: 700;
letter-spacing: 2rpx; letter-spacing: 2rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.15); text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.15);

View File

@@ -0,0 +1,288 @@
<template>
<view class="permission-container">
<!-- 搜索栏 -->
<view class="search-section">
<view class="search-box">
<u-icon name="search" size="20" color="#bbb"></u-icon>
<input class="search-input" v-model="keyword" placeholder="搜索用户姓名或手机号" confirm-type="search"
@input="onSearch" />
<view v-if="keyword" class="search-clear" @click="clearSearch">
<u-icon name="close-circle" size="18" color="#ccc"></u-icon>
</view>
</view>
</view>
<!-- 用户列表 -->
<view class="users-section">
<view v-if="users.length === 0" class="empty-state">
<view class="empty-icon">
<u-icon name="account" size="64" color="#ddd"></u-icon>
</view>
<text class="empty-text">暂无用户</text>
</view>
<view v-else class="users-list">
<view v-for="user in users" :key="user.userId" class="user-card">
<view class="user-avatar">
<image v-if="user.headImg" class="avatar-img" :src="Service.GetMateUrlByImg(user.headImg)"
mode="aspectFill"></image>
<view v-else class="avatar-default">
<text class="avatar-text">{{ user.name ? user.name.charAt(0) : '?' }}</text>
</view>
</view>
<view class="user-info">
<text class="user-name">{{ user.name }}</text>
<text class="user-phone">{{ user.phone }}</text>
</view>
<view class="user-action">
<!-- 手写开关器status == 1 为开 -->
<view
class="custom-switch"
:class="{ 'switch-on': user.status == 1 }"
@click="toggleStatus(user)"
>
<view class="switch-thumb"></view>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Service } from '@/Service/Service'
import { userService } from '@/Service/swimming/userService'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
const keyword = ref('')
const users = ref<Array<any>>([])
let page = ref(1)
let loadStatus = ref('nomore')
onLoad(() => {
getData()
})
onReachBottom(() => {
getList()
})
const getData = () => {
users.value = []
page.value = 1
loadStatus.value = 'loadmore'
getList()
}
const getList = () => {
if (loadStatus.value == 'loading' || loadStatus.value == 'nomore') {
return
}
loadStatus.value = 'loading'
userService.GetUserList(keyword.value, page.value).then((res: any) => {
if (res.code == 0) {
users.value = [...users.value, ...res.data.list]
loadStatus.value = res.data.list.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
const onSearch = () => {
getData()
}
const clearSearch = () => {
keyword.value = ''
getData()
}
const toggleStatus = (user: any) => {
userService.UpdateUserRole(user.userId).then(res=>{
if (res.code == 0) {
const newStatus = user.status == 1 ? 0 : 1
user.status = newStatus
} else {
Service.Msg(res.msg)
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.permission-container {
min-height: 100vh;
padding: 20rpx;
padding-bottom: 40rpx;
}
/* 搜索栏 */
.search-section {
margin-bottom: 20rpx;
.search-box {
display: flex;
align-items: center;
gap: 12rpx;
background-color: #fff;
border-radius: 16rpx;
padding: 16rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.search-input {
flex: 1;
font-size: 28rpx;
color: #333;
height: 48rpx;
}
.search-clear {
display: flex;
align-items: center;
justify-content: center;
width: 44rpx;
height: 44rpx;
}
}
}
/* 用户列表 */
.users-section {
.empty-state {
background-color: #fff;
border-radius: 24rpx;
padding: 80rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.empty-icon {
margin-bottom: 24rpx;
opacity: 0.6;
}
.empty-text {
font-size: 30rpx;
font-weight: 600;
color: #666;
}
}
}
.users-list {
.user-card {
background-color: #fff;
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
display: flex;
align-items: center;
gap: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
}
.user-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
overflow: hidden;
flex-shrink: 0;
.avatar-img {
width: 100%;
height: 100%;
}
.avatar-default {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.avatar-text {
color: #fff;
font-size: 30rpx;
font-weight: 700;
}
}
}
.user-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
min-width: 0;
.user-name {
font-size: 32rpx;
font-weight: 700;
color: #333;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-phone {
font-size: 24rpx;
color: #999;
}
}
.user-action {
flex-shrink: 0;
}
}
}
/* 手写开关器 */
.custom-switch {
width: 96rpx;
height: 52rpx;
border-radius: 26rpx;
background-color: #d9d9d9;
position: relative;
transition: background-color 0.3s ease;
cursor: pointer;
.switch-thumb {
width: 44rpx;
height: 44rpx;
border-radius: 50%;
background-color: #fff;
position: absolute;
top: 4rpx;
left: 4rpx;
transition: transform 0.3s ease;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.15);
}
&.switch-on {
background-color: #52c41a;
.switch-thumb {
transform: translateX(44rpx);
}
}
&:active {
opacity: 0.8;
}
}
</style>