完整
This commit is contained in:
@@ -3,8 +3,8 @@ import { Service } from '@/Service/Service';
|
||||
class studentService {
|
||||
private static GetStudentListPath : string = '/api/Student/GetStudentList';
|
||||
/*****学生列表不分页接口*****/
|
||||
static GetStudentList() {
|
||||
var result = Service.Request(this.GetStudentListPath, "GET", { });
|
||||
static GetStudentList(planId:string) {
|
||||
var result = Service.Request(this.GetStudentListPath, "GET", { planId });
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
// /*****根据学生id查详情接口*****/
|
||||
// static GetStudentInfo(studentId:string) {
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
{
|
||||
"path": "swiming",
|
||||
"style": {
|
||||
"navigationBarTitleText": "游泳项目"
|
||||
"navigationBarTitleText": "计时项目"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -107,6 +107,18 @@
|
||||
"navigationBarTitleText": "测试",
|
||||
"pageOrientation": "auto"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "userPermission",
|
||||
"style": {
|
||||
"navigationBarTitleText": "权限管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "contactManage",
|
||||
"style": {
|
||||
"navigationBarTitleText": "联系方式管理"
|
||||
}
|
||||
}
|
||||
]
|
||||
}, {
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
</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>
|
||||
</template>
|
||||
|
||||
@@ -133,17 +133,17 @@
|
||||
const projectOptions = ref<Array<any>>([[]])
|
||||
|
||||
interface Student {
|
||||
studentId: string
|
||||
name: string
|
||||
studentId : string
|
||||
name : string
|
||||
}
|
||||
|
||||
interface StudentTrainingRecord {
|
||||
studentId: string
|
||||
studentName: string
|
||||
time: number
|
||||
recordDate: string
|
||||
recordFullDate: string
|
||||
round: number
|
||||
studentId : string
|
||||
studentName : string
|
||||
time : number
|
||||
recordDate : string
|
||||
recordFullDate : string
|
||||
round : number
|
||||
}
|
||||
|
||||
const projectStudents = ref<Student[]>([])
|
||||
@@ -178,7 +178,7 @@
|
||||
yAxis: {
|
||||
data: [{
|
||||
min: 0,
|
||||
format: (val: number) => val.toFixed(1) + 's'
|
||||
format: (val : number) => val.toFixed(1) + 's'
|
||||
}]
|
||||
},
|
||||
extra: {
|
||||
@@ -197,11 +197,13 @@
|
||||
series: [] as any[]
|
||||
})
|
||||
|
||||
let calendar=ref(null)
|
||||
|
||||
onLoad(() => {
|
||||
getProjectData()
|
||||
})
|
||||
|
||||
onShow(() => {})
|
||||
onShow(() => { })
|
||||
|
||||
const getProjectData = () => {
|
||||
PlanService.GetPlanListNoPage('计时项目').then(res => {
|
||||
@@ -213,7 +215,7 @@
|
||||
})
|
||||
}
|
||||
|
||||
const selectProcetFunc = (project: any) => {
|
||||
const selectProcetFunc = (project : any) => {
|
||||
selectProcet.value = project.name
|
||||
selectId.value = project.planId
|
||||
showProject.value = false
|
||||
@@ -228,11 +230,23 @@
|
||||
|
||||
const openCalendar = () => {
|
||||
showCalendar.value = true
|
||||
calendar.value.open()
|
||||
}
|
||||
|
||||
const calendarConfirm = (e: any) => {
|
||||
begin.value = e[0]
|
||||
end.value = e[e.length - 1]
|
||||
const calendarConfirm = (e : any) => {
|
||||
const newDate = e.range
|
||||
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
|
||||
}
|
||||
|
||||
@@ -240,8 +254,8 @@
|
||||
showCalendar.value = false
|
||||
}
|
||||
|
||||
const loadProjectStudents = (projectId: string) => {
|
||||
studentService.GetStudentList().then(res => {
|
||||
const loadProjectStudents = (projectId : string) => {
|
||||
studentService.GetStudentList('').then(res => {
|
||||
if (res.code == 0) {
|
||||
projectStudents.value = res.data
|
||||
} else {
|
||||
@@ -251,7 +265,7 @@
|
||||
|
||||
}
|
||||
|
||||
const toggleStudent = (studentId: string) => {
|
||||
const toggleStudent = (studentId : string) => {
|
||||
const index = selectedStudentIds.value.indexOf(studentId)
|
||||
if (index !== -1) {
|
||||
selectedStudentIds.value.splice(index, 1)
|
||||
@@ -262,36 +276,49 @@
|
||||
}
|
||||
selectedStudentIds.value.push(studentId)
|
||||
}
|
||||
let studentIdList = selectedStudentIds.value.map((item) => item)
|
||||
let data = {
|
||||
const studentIdList = selectedStudentIds.value.map((item) => item)
|
||||
const reqData = {
|
||||
planId: selectId.value,
|
||||
studentId: JSON.stringify(studentIdList),
|
||||
sTime: begin.value,
|
||||
eTime: end.value
|
||||
}
|
||||
interface DataItem {
|
||||
name: string
|
||||
data: any[]
|
||||
}
|
||||
|
||||
const xData = ref<DataItem[]>(
|
||||
Array.from({ length: studentIdList.length }, () => ({
|
||||
name: '',
|
||||
data: []
|
||||
}))
|
||||
)
|
||||
PlanService.GetQuxianLog(data).then(res => {
|
||||
PlanService.GetQuxianLog(reqData).then(res => {
|
||||
if (res.code == 0) {
|
||||
lineChartData.value.categories = []
|
||||
res.data.list.map((item: any) => {
|
||||
lineChartData.value.categories.push(item.dayTime)
|
||||
item.data.map((content: any, index: any) => {
|
||||
const list = res.data.list || []
|
||||
const days = list.map((item : any) => item.dayTime)
|
||||
|
||||
xData.value[index].name = content.studentName
|
||||
xData.value[index].data.push(content.time)
|
||||
// 按已选学生顺序初始化 series,缺省日期填充 0
|
||||
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 {
|
||||
Service.Msg(res.msg)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -190,6 +193,8 @@
|
||||
let selectProcet = ref('')
|
||||
let selectId = ref('')
|
||||
|
||||
let calendar=ref(null)
|
||||
|
||||
// 日期选择
|
||||
const begin = ref<string>('')
|
||||
const end = ref<string>('')
|
||||
@@ -264,7 +269,7 @@
|
||||
}
|
||||
|
||||
const getStudentList = () => {
|
||||
studentService.GetStudentList().then(res => {
|
||||
studentService.GetStudentList('').then(res => {
|
||||
if (res.code == 0) {
|
||||
studentList.value = res.data
|
||||
} else {
|
||||
@@ -305,11 +310,17 @@
|
||||
|
||||
const openCalendar = () => {
|
||||
showCalendar.value = true
|
||||
calendar.value.open()
|
||||
}
|
||||
|
||||
const calendarConfirm = (e : any) => {
|
||||
begin.value = e[0]
|
||||
end.value = e[e.length - 1]
|
||||
console.log(e);
|
||||
begin.value = e.range.before
|
||||
if( e.range.after){
|
||||
end.value = e.range.after
|
||||
}else{
|
||||
end.value = e.range.before
|
||||
}
|
||||
showCalendar.value = false
|
||||
getRecord()
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
|
||||
<view class="student-info">
|
||||
<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 class="speed-detail">
|
||||
@@ -105,7 +105,7 @@
|
||||
studentId: string
|
||||
}
|
||||
|
||||
const gradeList = ref<StudentGrade[]>([])
|
||||
const gradeList = ref<Array<any>>([])
|
||||
|
||||
const maxSpeed = computed(() => {
|
||||
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) => {
|
||||
console.log('点击了排名项:', item)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@
|
||||
</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="modal-header">
|
||||
<text class="modal-title">选择项目</text>
|
||||
@@ -48,7 +49,8 @@
|
||||
</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 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>
|
||||
@@ -87,10 +89,8 @@
|
||||
</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>
|
||||
@@ -216,9 +216,9 @@
|
||||
|
||||
|
||||
let row = ref('')
|
||||
let rowData=ref({
|
||||
studentName:'',
|
||||
planName:''
|
||||
let rowData = ref({
|
||||
studentName: '',
|
||||
planName: ''
|
||||
})
|
||||
|
||||
|
||||
@@ -269,6 +269,8 @@
|
||||
})
|
||||
})
|
||||
|
||||
let calendar=ref(null)
|
||||
|
||||
onLoad(() => {
|
||||
getProjectData()
|
||||
getRecordList()
|
||||
@@ -282,7 +284,7 @@
|
||||
|
||||
|
||||
const getProjectData = () => {
|
||||
PlanService.GetPlanListNoPage('计时项目').then(res => {
|
||||
PlanService.GetPlanListNoPage('分段项目').then(res => {
|
||||
if (res.code == 0) {
|
||||
projectOptions.value[0] = res.data.list
|
||||
} else {
|
||||
@@ -292,7 +294,7 @@
|
||||
}
|
||||
|
||||
const getStudentList = () => {
|
||||
studentService.GetStudentList().then(res => {
|
||||
studentService.GetStudentList('').then(res => {
|
||||
if (res.code == 0) {
|
||||
studentList.value = res.data
|
||||
} else {
|
||||
@@ -303,7 +305,8 @@
|
||||
|
||||
|
||||
const cellClick = (rows : any) => {
|
||||
rowData.value=rows
|
||||
rowData.value = rows
|
||||
console.log(rowData.value);
|
||||
row.value = rows.subsection
|
||||
showRecordPopup.value = true
|
||||
}
|
||||
@@ -334,11 +337,16 @@
|
||||
|
||||
const openCalendar = () => {
|
||||
showCalendar.value = true
|
||||
calendar.value.open()
|
||||
}
|
||||
|
||||
const calendarConfirm = (e : any) => {
|
||||
begin.value = e[0]
|
||||
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
|
||||
getRecord()
|
||||
}
|
||||
@@ -950,80 +958,80 @@
|
||||
}
|
||||
|
||||
/* 分段详情弹窗 */
|
||||
.segment-popup-content {
|
||||
width: 90vw;
|
||||
max-height: 70vh;
|
||||
padding: 32rpx;
|
||||
.segment-popup-content {
|
||||
width: 90vw;
|
||||
max-height: 70vh;
|
||||
padding: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.popup-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
|
||||
.popup-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-table {
|
||||
background-color: #fafafa;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.popup-header {
|
||||
.popup-table-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
|
||||
padding: 24rpx 0;
|
||||
|
||||
.popup-title {
|
||||
font-size: 34rpx;
|
||||
.popup-th {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-table {
|
||||
background-color: #fafafa;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.popup-scroll {
|
||||
max-height: 400rpx;
|
||||
|
||||
.popup-table-header {
|
||||
.popup-table-row {
|
||||
display: flex;
|
||||
background: linear-gradient(135deg, #faad14 0%, #ffc53d 100%);
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
background-color: #fff;
|
||||
|
||||
.popup-th {
|
||||
&:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.popup-td {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-scroll {
|
||||
max-height: 400rpx;
|
||||
.popup-empty {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.popup-table-row {
|
||||
display: flex;
|
||||
padding: 24rpx 0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
background-color: #fff;
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: #fafafa;
|
||||
}
|
||||
|
||||
.popup-td {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.popup-empty {
|
||||
padding: 60rpx 0;
|
||||
text-align: center;
|
||||
|
||||
.popup-empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
.popup-empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
|
||||
@@ -90,9 +90,9 @@
|
||||
</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>
|
||||
@@ -132,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">{{ 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>
|
||||
</view>
|
||||
<scroll-view class="record-popup-body" scroll-y>
|
||||
@@ -196,6 +196,8 @@
|
||||
const end = ref<string>('')
|
||||
const showCalendar = ref(false)
|
||||
|
||||
let calendar=ref(null)
|
||||
|
||||
// 分页相关
|
||||
let page = ref(1)
|
||||
let pageTotal = ref(10)
|
||||
@@ -265,7 +267,7 @@
|
||||
}
|
||||
|
||||
const getStudentList=()=>{
|
||||
studentService.GetStudentList().then(res=>{
|
||||
studentService.GetStudentList('').then(res=>{
|
||||
if (res.code == 0) {
|
||||
studentList.value =res.data
|
||||
} else {
|
||||
@@ -276,6 +278,7 @@
|
||||
|
||||
|
||||
const cellClick=(e:any)=>{
|
||||
console.log(e);
|
||||
rowData.value=e
|
||||
showRecordPopup.value=true
|
||||
}
|
||||
@@ -306,11 +309,18 @@
|
||||
|
||||
const openCalendar = () => {
|
||||
showCalendar.value = true
|
||||
calendar.value.open()
|
||||
}
|
||||
|
||||
const calendarConfirm = (e: any) => {
|
||||
begin.value = e[0]
|
||||
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
|
||||
getRecord()
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
</view>
|
||||
<view class="card-info">
|
||||
<text class="card-title">计时项目</text>
|
||||
<text class="card-desc">记录单次训练的计时数据</text>
|
||||
</view>
|
||||
<view class="card-arrow">
|
||||
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
|
||||
@@ -37,7 +36,6 @@
|
||||
</view>
|
||||
<view class="card-info">
|
||||
<text class="card-title">包干项目</text>
|
||||
<text class="card-desc">记录固定距离的训练成绩</text>
|
||||
</view>
|
||||
<view class="card-arrow">
|
||||
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
|
||||
@@ -53,7 +51,6 @@
|
||||
</view>
|
||||
<view class="card-info">
|
||||
<text class="card-title">分段项目</text>
|
||||
<text class="card-desc">记录分段训练的详细数据</text>
|
||||
</view>
|
||||
<view class="card-arrow">
|
||||
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
|
||||
@@ -69,7 +66,6 @@
|
||||
</view>
|
||||
<view class="card-info">
|
||||
<text class="card-title">混氧项目</text>
|
||||
<text class="card-desc">记录混氧训练的计时数据</text>
|
||||
</view>
|
||||
<view class="card-arrow">
|
||||
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
|
||||
@@ -101,7 +97,8 @@
|
||||
<!-- 项目列表 -->
|
||||
<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">
|
||||
<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"
|
||||
@click="handleProjectClick(project)">
|
||||
<view class="item-icon">
|
||||
@@ -149,18 +146,18 @@
|
||||
const showTimingModal = ref(false)
|
||||
|
||||
|
||||
let isUse=ref(false)
|
||||
let isUse = ref(false)
|
||||
|
||||
onLoad(() => {
|
||||
if (!Service.GetUserToken()) {
|
||||
login()
|
||||
}else{
|
||||
} else {
|
||||
isUseFunc()
|
||||
}
|
||||
})
|
||||
|
||||
onShow(()=>{
|
||||
showTimingModal.value=false
|
||||
onShow(() => {
|
||||
showTimingModal.value = false
|
||||
uni.showTabBar()
|
||||
})
|
||||
|
||||
@@ -192,11 +189,11 @@
|
||||
}
|
||||
|
||||
|
||||
const isUseFunc=()=>{
|
||||
userService.IsCheakUserVerify().then(res=>{
|
||||
if(res.code==0){
|
||||
isUse.value=res.data.isCheak
|
||||
}else{
|
||||
const isUseFunc = () => {
|
||||
userService.IsCheakUserVerify().then(res => {
|
||||
if (res.code == 0) {
|
||||
isUse.value = res.data.isCheak
|
||||
} else {
|
||||
Service.Msg(res.msg)
|
||||
}
|
||||
})
|
||||
@@ -229,8 +226,8 @@
|
||||
})
|
||||
}
|
||||
|
||||
const goBaogan=()=>{
|
||||
if(!isUse.value){
|
||||
const goBaogan = () => {
|
||||
if (!isUse.value) {
|
||||
Service.Msg('用户暂无权限,请等待审核!')
|
||||
return
|
||||
}
|
||||
@@ -240,7 +237,7 @@
|
||||
|
||||
// 显示计时项目列表弹窗
|
||||
const showTimingProjectModal = (index : any) => {
|
||||
if(!isUse.value){
|
||||
if (!isUse.value) {
|
||||
Service.Msg('用户暂无权限,请等待审核!')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
<view class="avatar-circle" v-if="!userInfo.headImg">
|
||||
<u-icon name="account" size="52" color="#fff"></u-icon>
|
||||
</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 class="user-info">
|
||||
<text class="user-nickname">{{ userInfo.name }}</text>
|
||||
@@ -104,11 +105,55 @@
|
||||
</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">
|
||||
<text class="contact-label">客服联系人:张教练</text>
|
||||
<text class="contact-phone">138-0000-0000</text>
|
||||
<view v-if="userInfo.role!=1" class="contact-footer" @click="callContact">
|
||||
<text class="contact-label">客服联系人:{{ kefuName }}</text>
|
||||
<text class="contact-phone">{{ kefuPhone }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -119,10 +164,12 @@
|
||||
import { Service } from '@/Service/Service'
|
||||
import { userService } from '@/Service/swimming/userService'
|
||||
|
||||
let planCount=ref(0)
|
||||
let studentCount=ref(0)
|
||||
let planCount = ref(0)
|
||||
let studentCount = ref(0)
|
||||
let userInfo = ref<any>({})
|
||||
let isUse=ref(false)
|
||||
let isUse = ref(false)
|
||||
let kefuName = ref('')
|
||||
let kefuPhone = ref('')
|
||||
|
||||
onLoad(() => {
|
||||
|
||||
@@ -133,30 +180,33 @@
|
||||
isUseFunc()
|
||||
})
|
||||
|
||||
const isUseFunc=()=>{
|
||||
userService.IsCheakUserVerify().then(res=>{
|
||||
if(res.code==0){
|
||||
isUse.value=res.data.isCheak
|
||||
}else{
|
||||
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) => {
|
||||
userService.GetUserInfo().then((content : any) => {
|
||||
if (content.code == 0) {
|
||||
studentCount.value=content.data.studentCount
|
||||
planCount.value=content.data.planCount
|
||||
userInfo.value=content.data.userInfo
|
||||
studentCount.value = content.data.studentCount
|
||||
planCount.value = content.data.planCount
|
||||
userInfo.value = content.data.userInfo
|
||||
kefuName.value = content.data.kefuName
|
||||
kefuPhone.value = content.data.kefuPhone
|
||||
|
||||
} else {
|
||||
Service.Msg(content.msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const goPage=(path:any)=>{
|
||||
if(!isUse.value){
|
||||
const goPage = (path : any) => {
|
||||
if (!isUse.value) {
|
||||
Service.Msg('用户暂无权限,请等待审核!')
|
||||
return
|
||||
}
|
||||
@@ -165,7 +215,7 @@
|
||||
|
||||
const callContact = () => {
|
||||
uni.makePhoneCall({
|
||||
phoneNumber: '13800000000',
|
||||
phoneNumber: kefuPhone.value,
|
||||
fail: () => {
|
||||
Service.Msg('拨打电话失败')
|
||||
}
|
||||
@@ -181,7 +231,7 @@
|
||||
.user-container {
|
||||
padding: 20rpx;
|
||||
padding-top: 30rpx;
|
||||
padding-bottom: 140rpx;
|
||||
|
||||
}
|
||||
|
||||
/* 用户信息卡片 */
|
||||
@@ -467,6 +517,16 @@
|
||||
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 {
|
||||
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
|
||||
}
|
||||
@@ -523,6 +583,13 @@
|
||||
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 {
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
@@ -571,7 +638,6 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
padding: 16rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -359,7 +359,7 @@
|
||||
}
|
||||
})
|
||||
|
||||
console.log(projectData);
|
||||
|
||||
|
||||
const data = {
|
||||
planId: planId.value,
|
||||
@@ -375,13 +375,24 @@
|
||||
group: ''
|
||||
}
|
||||
|
||||
console.log('保存的数据:', data)
|
||||
|
||||
PlanService.AddPlan(data).then(res => {
|
||||
if (res.code == 0) {
|
||||
Service.Msg(planId.value?'修改成功!':'添加成功!')
|
||||
setTimeout(() => {
|
||||
Service.GoPageDelse('/pages/userFunc/hunyang?id=' + res.data.planId)
|
||||
}, 1000)
|
||||
|
||||
if(planId.value){
|
||||
setTimeout(() => {
|
||||
Service.GoPageBack()
|
||||
}, 1000)
|
||||
|
||||
uni.$emit('change')
|
||||
}else{
|
||||
setTimeout(() => {
|
||||
Service.GoPageDelse('/pages/userFunc/hunyang?id=' + res.data.planId)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
187
src/pages/userFunc/contactManage.vue
Normal file
187
src/pages/userFunc/contactManage.vue
Normal 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>
|
||||
@@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<up-navbar v-if="!isLandscape" title="混氧项目" placeholder='true' :autoBack="true">
|
||||
<up-navbar v-if="!isLandscape" title="混氧项目" placeholder='true' :autoBack="true">
|
||||
</up-navbar>
|
||||
<view class="hunyang-container">
|
||||
<!-- 项目信息头部 -->
|
||||
<view class="card-section header-section">
|
||||
<view class="header-content">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
@@ -22,47 +23,42 @@
|
||||
<!-- 分组头部 -->
|
||||
<view class="group-header">
|
||||
<view class="group-title-area">
|
||||
<text class="group-name">{{ groupState.groupName }}</text>
|
||||
<view class="group-name">{{ groupState.groupName }}</view>
|
||||
</view>
|
||||
<view class="plan-name">{{ groupState.currentPlan?.name }}</view>
|
||||
<text class="group-plan-index">
|
||||
计划总时长: {{ groupState.allTime }}
|
||||
{{ 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="countdown-box" :class="{ resting: groupState.isResting }">
|
||||
<text class="countdown-number">
|
||||
<view class="countdown-box" :style="{ 'margin': !isLandscape? '23rpx 0':'4rpx 0' }"
|
||||
: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) }}
|
||||
</text>
|
||||
</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 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">
|
||||
M:{{ formatCountdown(groupState.currentPlan?.target) }}
|
||||
</text>
|
||||
@@ -70,12 +66,18 @@
|
||||
X:{{ groupState.currentPlan?.rest }}
|
||||
</text>
|
||||
<text class="detail-badge">
|
||||
Q:{{ groupState.currentPlan?.circle }}
|
||||
Q: {{ groupState.completedLaps }}/{{ groupState.currentPlan?.circle }}
|
||||
</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 class="group-controls">
|
||||
<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>
|
||||
@@ -97,6 +99,8 @@
|
||||
<view class="plan-completed" v-else>
|
||||
<u-icon name="checkmark-circle-fill" size="48" color="#4caf50"></u-icon>
|
||||
<text class="completed-label">分组完成</text>
|
||||
<text class="completed-label" style="color: var(--nav-mian); font-size: 24rpx;"
|
||||
@click="resetGroup(groupIndex)">重置</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -194,12 +198,17 @@
|
||||
// ==================== 生命周期 ====================
|
||||
onLoad((data : any) => {
|
||||
planId.value = data.id
|
||||
getPlanInfo()
|
||||
})
|
||||
onShow(() => {
|
||||
checkOrientation()
|
||||
uni.onWindowResize(checkOrientation)
|
||||
})
|
||||
|
||||
onShow(() => {
|
||||
getPlanInfo()
|
||||
uni.$on('change',()=>{
|
||||
getPlanInfo()
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -542,6 +551,7 @@
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.group-title-area {
|
||||
@@ -554,6 +564,10 @@
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
width: 78rpx;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.group-plan-index {
|
||||
@@ -580,10 +594,11 @@
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
font-size: 28rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* 圈数指示器 */
|
||||
@@ -631,14 +646,11 @@
|
||||
}
|
||||
|
||||
.countdown-box {
|
||||
margin: 20rpx 0;
|
||||
|
||||
.countdown-number {
|
||||
font-size: 72rpx;
|
||||
font-weight: bold;
|
||||
font-family: 'Helvetica Neue', monospace;
|
||||
color: #000;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
&.resting .countdown-number {
|
||||
@@ -649,7 +661,7 @@
|
||||
|
||||
.plan-details {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
@@ -869,19 +881,27 @@
|
||||
}
|
||||
|
||||
.group-name {
|
||||
font-size: 22rpx;
|
||||
font-size: 18rpx;
|
||||
letter-spacing: 1rpx;
|
||||
width: 78rpx;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.group-plan-index {
|
||||
font-size: 22rpx;
|
||||
font-size: 18rpx;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 8rpx;
|
||||
font-size: 18rpx;
|
||||
letter-spacing: 1rpx;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.laps-indicator {
|
||||
@@ -898,8 +918,8 @@
|
||||
margin: 10rpx 0;
|
||||
|
||||
.countdown-number {
|
||||
font-size: 60rpx;
|
||||
letter-spacing: 4rpx;
|
||||
font-size: 72rpx;
|
||||
transform: scale(1.2, 1.3);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template>
|
||||
<up-navbar v-if="!isLandscape" title="包干项目" placeholder='true' :autoBack="true">
|
||||
<up-navbar v-if="!isLandscape" title="包干项目" placeholder='true' :autoBack="true">
|
||||
</up-navbar>
|
||||
<view class="project-container">
|
||||
<!-- 顶部一排:项目选择 + 全局设置 + 添加分组 -->
|
||||
<view class="header-row">
|
||||
<view class="header-row">
|
||||
<view class="header-item project-select-half" @click="showProjectSelect = true">
|
||||
<text class="header-text">{{ selectedProject?.name || '请选择项目' }}</text>
|
||||
<view class="header-icon-wrap gray">
|
||||
@@ -52,7 +52,7 @@
|
||||
</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 class="timer-circle" :class="{ resting: timer.isResting }" @click="recordTimer(timer)">
|
||||
<view class="timer-header">
|
||||
@@ -60,6 +60,14 @@
|
||||
<text class="name-text">{{ timer.studentName }}</text>
|
||||
</view>
|
||||
<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)">
|
||||
<u-icon name="trash" size="14" color="#fff"></u-icon>
|
||||
</view>
|
||||
@@ -73,25 +81,33 @@
|
||||
已完成
|
||||
</view>
|
||||
<view v-else-if="timer.isResting" class="timer-rest-countdown">
|
||||
<text class="rest-label">休息</text>
|
||||
<text
|
||||
class="rest-time">{{ formatRestCountdown(timer.restCountdown || 0) }}</text>
|
||||
|
||||
<text :class=" isLandscape? 'rest-time-x':'rest-time-y'"
|
||||
>{{ formatRestCountdown(timer.restCountdown || 0) }}</text>
|
||||
</view>
|
||||
<view v-else class="">
|
||||
<view class="timer-label">
|
||||
M{{formatTimeObject(timer.targetTime)}}″X{{ timer.restTime }}
|
||||
<view v-else class="circle-inner" >
|
||||
<view class="timer-label-box ">
|
||||
<view class="timer-label">
|
||||
M{{formatTimeObject(timer.targetTime)}}″X{{ timer.restTime }}
|
||||
</view>
|
||||
<view class="timer-info">
|
||||
<text
|
||||
class="timer-duration">{{ timer.records.length }}/{{ timer.totalLapCount || 4 }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view :class=" isLandscape? 'timer-time-x':'timer-time-y'">
|
||||
{{ formatTime(timer) }}
|
||||
</view>
|
||||
<view class="timer-time">{{ formatTime(timer) }}</view>
|
||||
</view>
|
||||
<view class="timer-info">
|
||||
<!-- <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 class="timer-controls">
|
||||
<view v-if="!isLandscape" class="timer-controls">
|
||||
<view @click.stop="openTimerDetail(timer)">
|
||||
<u-icon name="edit-pen" size="24" color="#22a6f2"></u-icon>
|
||||
</view>
|
||||
@@ -147,7 +163,7 @@
|
||||
<view class="detail-name">{{ selectedTimer?.studentName || '计时器' }}</view>
|
||||
<view class="stopwatch-display">
|
||||
<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 class="current-settings-info">
|
||||
<view class="setting-info-item">
|
||||
@@ -438,6 +454,10 @@
|
||||
lastRecordTimestamp : number // 上次记录时的真实时间戳(毫秒)
|
||||
currentLapTime : number // 当前圈已用时间(秒),用于记录
|
||||
hasRecordedThisLap : boolean // 当前圈是否已手动记录
|
||||
// 以下为运行时字段,不持久化,由全局驱动引擎维护
|
||||
_startAt ?: number // 当前轮次开始时间戳(毫秒)
|
||||
_currentLapStartAt ?: number // 当前圈开始时间戳(毫秒)
|
||||
_restStartAt ?: number // 休息倒计时开始时间戳(毫秒)
|
||||
}
|
||||
|
||||
/** 分组 */
|
||||
@@ -749,74 +769,135 @@
|
||||
getData()
|
||||
})
|
||||
|
||||
// ===================== 学生计时器核心逻辑 =====================
|
||||
// ===================== 全局统一驱动引擎 =====================
|
||||
|
||||
/**
|
||||
* 存储每个学生的主计时器 setInterval ID
|
||||
* Key = studentId,Value = intervalId
|
||||
* 方案 B:单一定时器驱动引擎
|
||||
* 不再给每个学生创建独立的 setInterval,而是用一个全局 interval 每 50ms 统一 tick,
|
||||
* 遍历所有 running/resting 的学生并计算时间差。彻底消除人多时大量 interval 导致的性能问题。
|
||||
*/
|
||||
const studentIntervals = new Map<string, number>()
|
||||
let engineIntervalId : number | null = null
|
||||
|
||||
/**
|
||||
* 存储每个学生的休息倒计时 setInterval ID
|
||||
* Key = studentId,Value = intervalId
|
||||
*/
|
||||
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
|
||||
const startEngine = () => {
|
||||
if (engineIntervalId) return
|
||||
engineIntervalId = setInterval(() => {
|
||||
tick()
|
||||
}, 50) as unknown as number
|
||||
elapsedTimeIntervals.set(timer.studentId, id)
|
||||
}
|
||||
|
||||
/** 停止真实时间计时器 */
|
||||
const stopElapsedTimeTimer = (studentId : string) => {
|
||||
const id = elapsedTimeIntervals.get(studentId)
|
||||
if (id) {
|
||||
clearInterval(id)
|
||||
elapsedTimeIntervals.delete(studentId)
|
||||
const stopEngine = () => {
|
||||
if (engineIntervalId) {
|
||||
clearInterval(engineIntervalId)
|
||||
engineIntervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
const tick = () => {
|
||||
const now = Date.now()
|
||||
|
||||
// 1. 更新全局计时器(间隔出发模式用)
|
||||
if (globalTimerRunning.value) {
|
||||
globalTimerTime.value = globalBaseTime + (now - globalStartAt) / 1000
|
||||
}
|
||||
|
||||
// 2. 间隔出发检查(全局计时器驱动模式下)
|
||||
if (globalTimerRunning.value && setting.value.mode === 'interval') {
|
||||
const intervalSec = toNumber(setting.value.interval, 5)
|
||||
groups.value.forEach((group) => {
|
||||
group.timers.forEach((t, studentIndex) => {
|
||||
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
|
||||
}
|
||||
|
||||
// 计算周期和显示状态
|
||||
const cycleTime = timer.targetTime + timer.restTime
|
||||
if (timer.currentLapTime <= timer.targetTime) {
|
||||
// 游泳阶段:显示目标倒计时
|
||||
timer.countdownTime = Math.max(0, timer.targetTime - timer.currentLapTime)
|
||||
timer.isResting = false
|
||||
timer.restCountdown = 0
|
||||
} else if (timer.currentLapTime < cycleTime) {
|
||||
// 休息阶段:显示休息倒计时
|
||||
timer.isResting = true
|
||||
timer.restCountdown = Math.max(0, cycleTime - timer.currentLapTime)
|
||||
timer.countdownTime = 0
|
||||
}
|
||||
|
||||
// 检查是否达到周期(目标时间 + 休息时间)
|
||||
if (timer.currentLapTime >= cycleTime) {
|
||||
// 周期结束,自动记录0分(如果本圈还没被记录过)
|
||||
if (!timer.hasRecordedThisLap && timer.records.length < timer.totalLapCount) {
|
||||
timer.records.push({ time: 0 })
|
||||
}
|
||||
// 清零,准备下一轮
|
||||
timer.currentLapTime = 0
|
||||
timer.hasRecordedThisLap = false
|
||||
timer.countdownTime = timer.targetTime
|
||||
timer.isResting = false
|
||||
timer.restCountdown = 0
|
||||
timer._currentLapStartAt = now
|
||||
// 检查是否完成所有圈数
|
||||
if (timer.records.length >= timer.totalLapCount) {
|
||||
completeTimer(timer)
|
||||
}
|
||||
}
|
||||
} else if (timer.status === 'resting') {
|
||||
// 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. 先清理旧的定时器(防止重复启动多个interval导致时间加速)
|
||||
* 2. 如果有保存的暂停休息时间,优先恢复休息倒计时
|
||||
* 3. 设置状态为 running,记录 hasStarted = true
|
||||
* 4. 通过 Date.now() 与 currentTime 反推出"开始时刻",然后用 setInterval 每50ms更新 currentTime
|
||||
*
|
||||
* 注意:允许从 resting 状态恢复计时,所以不拦截 resting
|
||||
* 4. 通过 Date.now() 与 currentTime 反推"开始时刻",存入 _startAt / _currentLapStartAt
|
||||
* 5. 启动全局引擎
|
||||
*/
|
||||
const startStudentTimer = (timer : TimerItem) => {
|
||||
if (timer.status === 'running' || timer.status === 'completed') return
|
||||
@@ -825,60 +906,20 @@
|
||||
startRestFromPause(timer)
|
||||
return
|
||||
}
|
||||
clearStudentInterval(timer.studentId)
|
||||
clearStudentRestInterval(timer.studentId)
|
||||
timer.status = 'running'
|
||||
timer.hasStarted = true
|
||||
// 如果倒计时已归零,重置为目标时间
|
||||
if (timer.countdownTime <= 0) {
|
||||
timer.countdownTime = timer.targetTime
|
||||
}
|
||||
// 启动真实时间计时器(不受休息影响)
|
||||
startElapsedTimeTimer(timer)
|
||||
// 初始化 elapsedTime 起始时间戳(仅首次)
|
||||
if (timer.startTimestamp === 0) {
|
||||
timer.startTimestamp = Date.now()
|
||||
}
|
||||
// 通过当前累计时间反推开始时刻,保证暂停后继续计时的连续性
|
||||
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
|
||||
if (timer.currentLapTime <= timer.targetTime) {
|
||||
// 游泳阶段:显示目标倒计时
|
||||
timer.countdownTime = Math.max(0, timer.targetTime - timer.currentLapTime)
|
||||
timer.isResting = false
|
||||
timer.restCountdown = 0
|
||||
} else if (timer.currentLapTime < cycleTime) {
|
||||
// 休息阶段:显示休息倒计时
|
||||
timer.isResting = true
|
||||
timer.restCountdown = Math.max(0, cycleTime - timer.currentLapTime)
|
||||
timer.countdownTime = 0
|
||||
}
|
||||
|
||||
// 检查是否达到周期(目标时间 + 休息时间)
|
||||
if (timer.currentLapTime >= cycleTime && timer.status === 'running') {
|
||||
// 周期结束,自动记录0分(如果本圈还没被记录过)
|
||||
if (!timer.hasRecordedThisLap && timer.records.length < timer.totalLapCount) {
|
||||
timer.records.push({ time: 0 })
|
||||
}
|
||||
// 清零,准备下一轮
|
||||
timer.currentLapTime = 0
|
||||
timer.hasRecordedThisLap = false
|
||||
timer.countdownTime = timer.targetTime
|
||||
timer.isResting = false
|
||||
timer.restCountdown = 0
|
||||
currentLapStartAt = Date.now()
|
||||
// 检查是否完成所有圈数
|
||||
if (timer.records.length >= timer.totalLapCount) {
|
||||
completeTimer(timer)
|
||||
}
|
||||
}
|
||||
}, 50) as unknown as number
|
||||
studentIntervals.set(timer.studentId, id)
|
||||
timer._startAt = Date.now() - timer.currentTime * 1000
|
||||
timer._currentLapStartAt = Date.now() - timer.currentLapTime * 1000
|
||||
startEngine()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -895,16 +936,12 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂停学生计时器
|
||||
* 暂停学生计时器(方案B:不再需要 clearInterval,只改状态)
|
||||
* 逻辑:
|
||||
* 1. 清除主计时器和休息倒计时
|
||||
* 2. 如果当前在 running 或 resting,状态改为 paused
|
||||
* 3. 重置 resting 相关标志
|
||||
* 1. 如果当前在 running 或 resting,状态改为 paused
|
||||
* 2. 重置 resting 相关标志
|
||||
*/
|
||||
const pauseStudentTimer = (timer : TimerItem) => {
|
||||
clearStudentInterval(timer.studentId)
|
||||
clearStudentRestInterval(timer.studentId)
|
||||
stopElapsedTimeTimer(timer.studentId)
|
||||
if (timer.status === 'running' || timer.status === 'resting') {
|
||||
// 如果正在休息,保存剩余休息时间
|
||||
if (timer.status === 'resting' && timer.restCountdown > 0) {
|
||||
@@ -917,20 +954,16 @@
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动休息倒计时
|
||||
* 启动休息倒计时(方案B:不再创建独立 setInterval,由全局 tick 驱动)
|
||||
* 触发时机:学生完成一圈后(自动完成,不记录成绩)
|
||||
*
|
||||
* 逻辑:
|
||||
* 1. 先停止主计时器(休息时间不计入 currentTime)
|
||||
* 2. 不停止 elapsedTime 计时器(休息期间继续计时)
|
||||
* 3. 设置状态为 resting,开始倒计时
|
||||
* 4. 每50ms更新 restCountdown
|
||||
* 5. 倒计时结束后:如果是最后一圈则标记为完成,否则开始下一轮
|
||||
* 1. 设置状态为 resting
|
||||
* 2. 记录 _restStartAt,由全局 tick 计算 restCountdown
|
||||
* 3. 如果没有休息时间,直接处理后续逻辑
|
||||
*/
|
||||
const startRest = (timer : TimerItem) => {
|
||||
const restTime = timer.restTime || 0
|
||||
clearStudentInterval(timer.studentId)
|
||||
clearStudentRestInterval(timer.studentId)
|
||||
timer.status = 'resting'
|
||||
timer.isResting = true
|
||||
timer.restCountdown = restTime
|
||||
@@ -947,31 +980,14 @@
|
||||
}
|
||||
return
|
||||
}
|
||||
const restStart = Date.now()
|
||||
const id = setInterval(() => {
|
||||
const remaining = Math.max(0, restTime - (Date.now() - restStart) / 1000)
|
||||
timer.restCountdown = remaining
|
||||
if (remaining <= 0) {
|
||||
clearStudentRestInterval(timer.studentId)
|
||||
// 休息结束,判断是否为最后一圈
|
||||
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)
|
||||
timer._restStartAt = Date.now()
|
||||
startEngine()
|
||||
const isLastLap = timer.lapCount >= timer.totalLapCount
|
||||
// Service.Msg(isLastLap ? '最后一圈完成,进入休息' : `完成一圈,休息${restTime}秒`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从暂停状态恢复休息倒计时
|
||||
* 从暂停状态恢复休息倒计时(方案B:不再创建独立 setInterval,由全局 tick 驱动)
|
||||
* 触发时机:学生在休息中暂停后再次点击开始
|
||||
*/
|
||||
const startRestFromPause = (timer : TimerItem) => {
|
||||
@@ -988,41 +1004,20 @@
|
||||
}
|
||||
return
|
||||
}
|
||||
clearStudentInterval(timer.studentId)
|
||||
clearStudentRestInterval(timer.studentId)
|
||||
timer.status = 'resting'
|
||||
timer.isResting = true
|
||||
timer.restCountdown = restTime
|
||||
const restStart = Date.now()
|
||||
const id = setInterval(() => {
|
||||
const remaining = Math.max(0, restTime - (Date.now() - restStart) / 1000)
|
||||
timer.restCountdown = remaining
|
||||
if (remaining <= 0) {
|
||||
clearStudentRestInterval(timer.studentId)
|
||||
// 休息结束,判断是否为最后一圈
|
||||
timer.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)
|
||||
timer._restStartAt = Date.now()
|
||||
startEngine()
|
||||
const isLastLap = timer.lapCount >= timer.totalLapCount
|
||||
Service.Msg(isLastLap ? '最后一圈休息中...' : `继续休息,剩余${restTime.toFixed(1)}秒`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 将学生计时器标记为已完成
|
||||
* 将学生计时器标记为已完成(方案B:不再需要 clearInterval)
|
||||
* 触发时机:lapCount 达到 totalLapCount
|
||||
*/
|
||||
const completeTimer = (timer : TimerItem) => {
|
||||
clearStudentInterval(timer.studentId)
|
||||
clearStudentRestInterval(timer.studentId)
|
||||
stopElapsedTimeTimer(timer.studentId)
|
||||
timer.status = 'completed'
|
||||
timer.isResting = false
|
||||
timer.restCountdown = 0
|
||||
@@ -1087,6 +1082,9 @@
|
||||
timer.lastRecordTimestamp = 0
|
||||
timer.currentLapTime = 0
|
||||
timer.hasRecordedThisLap = false
|
||||
timer._startAt = undefined
|
||||
timer._currentLapStartAt = undefined
|
||||
timer._restStartAt = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1109,7 +1107,6 @@
|
||||
|
||||
const globalTimerTime = ref(0) // 全局累计时间(秒)
|
||||
const globalTimerRunning = ref(false) // 全局计时器是否运行中
|
||||
let globalTimerInterval : number | null = null // 全局定时器ID
|
||||
let globalBaseTime = 0 // 暂停前已积累的时间
|
||||
let globalStartAt = 0 // 本次启动的时刻戳
|
||||
|
||||
@@ -1119,19 +1116,15 @@
|
||||
globalTimerTime.value = globalBaseTime + (Date.now() - globalStartAt) / 1000
|
||||
}
|
||||
globalTimerRunning.value = false
|
||||
if (globalTimerInterval) {
|
||||
clearInterval(globalTimerInterval)
|
||||
globalTimerInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动全局计时器
|
||||
* 启动全局计时器(方案B:全局计时逻辑已合并到统一 tick 中)
|
||||
*
|
||||
* 逻辑:
|
||||
* 1. 先停止旧全局计时器
|
||||
* 2. 恢复所有已暂停的学生计时器(间隔模式下)
|
||||
* 3. 启动全局时间累计(每50ms更新 globalTimerTime)
|
||||
* 3. 启动全局时间累计(由统一 tick 每50ms更新 globalTimerTime)
|
||||
* 4. "一起出发"模式下所有学生立即出发(startAt = 0)
|
||||
* 5. "间隔出发"模式下每组内学生按索引 * 间隔秒数依次出发
|
||||
* 6. 一旦 globalTimerTime >= 学生出发时间,且学生未出发过,则启动该学生计时器
|
||||
@@ -1141,7 +1134,6 @@
|
||||
globalTimerRunning.value = true
|
||||
globalStartAt = Date.now()
|
||||
globalBaseTime = globalTimerTime.value
|
||||
const intervalSec = toNumber(setting.value.interval, 5)
|
||||
|
||||
// 间隔模式下:立即恢复所有已暂停的学生
|
||||
if (setting.value.mode === 'interval') {
|
||||
@@ -1159,20 +1151,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
globalTimerInterval = setInterval(() => {
|
||||
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
|
||||
startEngine()
|
||||
}
|
||||
|
||||
// ===================== 分组级操作 =====================
|
||||
@@ -1793,14 +1772,28 @@
|
||||
})
|
||||
})
|
||||
|
||||
PlanService.AddPlanLog(selectedProject.value.planId, '包干项目', '', '', JSON.stringify(data)).then(res => {
|
||||
if (res.code == 0) {
|
||||
Service.Msg('提交成功!')
|
||||
// resetAllTimers()
|
||||
} else {
|
||||
Service.Msg(res.msg)
|
||||
uni.showModal({
|
||||
title: '提示', // 对话框标题
|
||||
content: '请确定提交数据!', // 显示的内容
|
||||
showCancel: true, // 是否显示取消按钮
|
||||
cancelText: '取消', // 取消按钮的文字
|
||||
confirmText: '确定', // 确认按钮的文字
|
||||
success: function (res) {
|
||||
if (res.confirm) {
|
||||
PlanService.AddPlanLog(selectedProject.value.planId, '包干项目', '', '', JSON.stringify(data)).then(res => {
|
||||
if (res.code == 0) {
|
||||
Service.Msg('提交成功!')
|
||||
// resetAllTimers()
|
||||
} else {
|
||||
Service.Msg(res.msg)
|
||||
}
|
||||
})
|
||||
} else if (res.cancel) {
|
||||
console.log('用户点击取消');
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
// ===================== 格式化函数 =====================
|
||||
@@ -1809,22 +1802,22 @@
|
||||
|
||||
const formatTimeObject = (timer : any) : string => {
|
||||
|
||||
const time = Math.max(0, timer)
|
||||
const time = Math.max(0, timer)
|
||||
const mins = Math.floor(time / 60)
|
||||
const secs = Math.floor(time % 60)
|
||||
const millis = Math.floor((time % 1) * 100)
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
|
||||
return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** 学生卡片主计时显示(目标时间倒计时):分:秒:毫秒 */
|
||||
|
||||
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 secs = Math.floor(time % 60)
|
||||
const millis = Math.floor((time % 1) * 100)
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
|
||||
return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** 计时器显示(真实经过时间):分:秒:毫秒 */
|
||||
@@ -1833,7 +1826,7 @@
|
||||
const mins = Math.floor(time / 60)
|
||||
const secs = Math.floor(time % 60)
|
||||
const millis = Math.floor((time % 1) * 100)
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
|
||||
return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** 目标时间显示:分:秒:毫秒 */
|
||||
@@ -1841,21 +1834,21 @@
|
||||
const mins = Math.floor(seconds / 60)
|
||||
const secs = Math.floor(seconds % 60)
|
||||
const millis = Math.floor((seconds % 1) * 100)
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}:${millis.toString().padStart(2, '0')}`
|
||||
return `${mins.toString().padStart(2, '0')}"${secs.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/** 休息倒计时显示:秒.毫秒 */
|
||||
/** 休息倒计时显示:秒 */
|
||||
const formatRestCountdown = (seconds : number) : string => {
|
||||
const secs = Math.floor(seconds)
|
||||
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 mins = 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(() => {
|
||||
stopGlobalTimer()
|
||||
groups.value.forEach(g => stopGroupDeparture(g))
|
||||
studentIntervals.forEach(id => clearInterval(id))
|
||||
studentRestIntervals.forEach(id => clearInterval(id))
|
||||
elapsedTimeIntervals.forEach(id => clearInterval(id))
|
||||
studentIntervals.clear()
|
||||
studentRestIntervals.clear()
|
||||
elapsedTimeIntervals.clear()
|
||||
stopEngine()
|
||||
uni.offWindowResize(updateOrientation)
|
||||
})
|
||||
</script>
|
||||
@@ -2117,8 +2105,8 @@
|
||||
.card-section {
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
padding: 20px 14px;
|
||||
margin-bottom: 16px;
|
||||
padding: 14rpx 20rpx;
|
||||
margin-bottom: 10rpx;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
@@ -2198,7 +2186,7 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 4rpx;
|
||||
|
||||
.section-title {
|
||||
margin-bottom: 0;
|
||||
@@ -2308,9 +2296,17 @@
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.timers-grid {
|
||||
.timers-grid-y {
|
||||
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;
|
||||
justify-content: start;
|
||||
}
|
||||
@@ -2344,7 +2340,7 @@
|
||||
border-radius: 10px;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
padding: 12px 10px;
|
||||
padding: 10rpx 6rpx 14rpx;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -2381,6 +2377,9 @@
|
||||
.delete-btn-wrapper {
|
||||
z-index: 10;
|
||||
margin-left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
|
||||
.delete-btn {
|
||||
width: 24px;
|
||||
@@ -2407,6 +2406,10 @@
|
||||
|
||||
.circle-content {
|
||||
width: 100%;
|
||||
height: 110px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 0;
|
||||
border-radius: 6px;
|
||||
border: 2px solid #faad14;
|
||||
@@ -2422,12 +2425,13 @@
|
||||
}
|
||||
|
||||
.circle-inner {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
justify-content: space-around;
|
||||
gap: 2rpx;
|
||||
// padding: 4px 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2447,12 +2451,22 @@
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.rest-time {
|
||||
font-size: 24px;
|
||||
.rest-time-x {
|
||||
font-size: 48rpx;
|
||||
transform: scale(1.7,1.9);
|
||||
font-weight: bold;
|
||||
color: #ff7875;
|
||||
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 {
|
||||
@@ -2466,17 +2480,34 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.timer-label-box {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.timer-label {
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.timer-time {
|
||||
font-size: 24px;
|
||||
.timer-time-y {
|
||||
font-size: 56rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
transform: scale(1.5, 1.9);
|
||||
}
|
||||
|
||||
|
||||
.timer-time-x {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
line-height: 1.2;
|
||||
transform: scale(1.2, 1.3);
|
||||
}
|
||||
|
||||
.timer-duration {
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-list"
|
||||
<scroll-view class="record-list" scroll-y
|
||||
v-if="currentStudent && currentStudent.segments && currentStudent.segments.length > 0">
|
||||
<view class="record-header">
|
||||
<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">{{ formatTimeDiff(index, segment.time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<view class="empty-record" v-else>
|
||||
<u-icon name="info-circle" size="48" color="#ccc"></u-icon>
|
||||
@@ -402,7 +402,7 @@
|
||||
students.value.map((item : any) => {
|
||||
let record = item.segments.map((content : any, index : any) => {
|
||||
return {
|
||||
circle: (index + 1) * segmentDistance.value,
|
||||
circle: (index + 1),
|
||||
time: content.time
|
||||
}
|
||||
})
|
||||
@@ -671,7 +671,7 @@
|
||||
.record-popup {
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx 20rpx 0 0;
|
||||
max-height: 80vh;
|
||||
height: 70vh;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
const getStudentList = async () => {
|
||||
loading.value = true
|
||||
|
||||
studentService.GetStudentList().then(res => {
|
||||
studentService.GetStudentList(planId.value ).then(res => {
|
||||
if (res.code == 0) {
|
||||
allStudents.value = res.data
|
||||
loading.value = false
|
||||
|
||||
@@ -316,7 +316,7 @@
|
||||
const groupOffset = (group.groupNumber - 1) * interval
|
||||
if (elapsed >= groupOffset && !notifiedGroupNumbers.value.has(group.groupNumber)) {
|
||||
notifiedGroupNumbers.value.add(group.groupNumber)
|
||||
Service.Msg(`第${group.groupNumber}组出发了`)
|
||||
// Service.Msg(`第${group.groupNumber}组出发了`)
|
||||
speak()
|
||||
}
|
||||
})
|
||||
@@ -672,8 +672,7 @@
|
||||
.athlete-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 14rpx 24rpx;
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 1rpx 6rpx rgba(0, 0, 0, 0.03);
|
||||
@@ -883,8 +882,8 @@
|
||||
/* 可拖动悬浮球 */
|
||||
.float-ball {
|
||||
position: fixed;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
width: 170rpx;
|
||||
height: 170rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
display: flex;
|
||||
@@ -916,7 +915,7 @@
|
||||
|
||||
.ball-text {
|
||||
color: #fff;
|
||||
font-size: 28rpx;
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 2rpx;
|
||||
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.15);
|
||||
|
||||
288
src/pages/userFunc/userPermission.vue
Normal file
288
src/pages/userFunc/userPermission.vue
Normal 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>
|
||||
Reference in New Issue
Block a user