保存数据

This commit is contained in:
Ls
2026-04-11 17:47:28 +08:00
parent f682c0316b
commit c9aeba0949
35 changed files with 5658 additions and 3544 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { NvpMerchService } from '@/Service/Nvp/NvpMerchService'
onLaunch(() => {
console.log("App Launch");
});

View File

@@ -1,9 +1,11 @@
export class BaseConfig {
// protected static servesUrl: string = "http://192.168.0.190:8806";//线下
protected static servesUrl: string = "http://192.168.0.142:5298";
protected static imgUrl: string = "http://192.168.0.142:5298";
protected static mediaUrl: string = "http://192.168.0.142:5298/";
protected static servesUrl: string = "http://vp.xypays.cn";
protected static imgUrl: string = "http://vp.cloud.xypays.cn";
protected static mediaUrl: string = "http://byc1.xypays.cn/";
// protected static servesUrl: string = "http://vp.xypays.cn";
// protected static imgUrl: string = "http://vp.cloud.xypays.cn";
// protected static mediaUrl: string = "http://byc1.xypays.cn/";
protected static uploadUrl: string = "/TencentCos/GetUpLoadInfo";
protected static payuploadUrl: string = "http://pay.xypays.cn";
protected static payuploadUrl: string = "http://192.168.0.142:5298";
}

View File

@@ -1,36 +0,0 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpAddressService {
private static GetAddressListPath: string = '/Address/GetAddressList';
/*****收货地址*****/
static GetAddressList(page: number) {
var result = Service.Request(this.GetAddressListPath, "GET", { page });
return result;
}
private static DelUserAddressPath: string = '/Address/DelUserAddress';
/*****删除地址*****/
static DelUserAddress(uaId: string) {
var result = Service.Request(this.DelUserAddressPath, "POST", { uaId });
return result;
}
private static AddUserAddressPath: string = '/Address/AddUserAddress';
/*****添加地址*****/
static AddUserAddress(uaId: string, name: string, phone: string, province: string, city: string, county: string, address: string, isDefault: number) {
var result = Service.Request(this.AddUserAddressPath, "POST", { uaId, name, phone, province, city, county, address, isDefault });
return result;
}
private static UpdateUserAddressPath: string = '/Address/UpdateUserAddress';
/*****修改默认地址*****/
static UpdateUserAddress(uaId: string) {
var result = Service.Request(this.UpdateUserAddressPath, "POST", { uaId });
return result;
}
}
export {
Service,
NvpAddressService
}

View File

@@ -1,43 +0,0 @@
import { Service } from '@/Service/Service';
/*****代理端接口*****/
class NvpAgentService {
private static LoginPath: string = '/Agent/Login';
/*****登录接口*****/
static Login(name: string, pwd: string) {
var result = Service.Request(this.LoginPath, "POST", { name, pwd });
return result;
}
private static GetAgentAccInfoPath: string = '/Agent/GetAgentAccInfo';
/*****账户接口*****/
static GetAgentAccInfo() {
var result = Service.Request(this.GetAgentAccInfoPath, "GET", "");
return result;
}
private static GetAgentAccLogPath: string = '/Agent/GetAgentAccLog';
/*****账户记录*****/
static GetAgentAccLog(code: string, page: number) {
var result = Service.Request(this.GetAgentAccLogPath, "GET", { code, page });
return result;
}
private static GetAgentMerchPath: string = '/Agent/GetAgentMerch';
/*****获取代理开通商家*****/
static GetAgentMerch(type: number, page: number) {
var result = Service.Request(this.GetAgentMerchPath, "GET", { type, page });
return result;
}
private static GetAgentHomePath: string = '/Agent/GetAgentHome';
/*****代理主页*****/
static GetAgentHome() {
var result = Service.Request(this.GetAgentHomePath, "GET", { });
return result;
}
}
export {
Service,
NvpAgentService
}

View File

@@ -1,156 +0,0 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpApplyService {
// private static WithDrawPath: string = '/With/WithDraw';
// /*****佣金提现*****/
// static WithDraw(money: number, name: string, account: string) {
// var result = Service.Request(this.WithDrawPath, "POST", { money, name, account });
// return result;
// }
private static GetSiteMccCodeListPath: string = '/Apply/GetSiteMccCodeList';
/*****获取mcc列表*****/
static GetSiteMccCodeList(mercType:string, mchType: string) {
var result = Service.Request(this.GetSiteMccCodeListPath, "GET", {mercType, mchType });
return result;
}
private static GetBankTypeListPath: string = '/Apply/GetBankTypeList';
/*****获取银行列表*****/
static GetBankTypeList(name:string) {
var result = Service.Request(this.GetBankTypeListPath, "GET", {name});
return result;
}
private static GetAreaListPath: string = '/Apply/GetAreaList';
/*****获取银行地区列表*****/
static GetAreaList(areaCode:string) {
var result = Service.Request(this.GetAreaListPath, "GET", { areaCode});
return result;
}
private static GetBankCodeListPath: string = '/Apply/GetBankCodeList';
/*****获取银行代码列表*****/
static GetBankCodeList(bankType:string,cityCode:string,name:string) {
var result = Service.Request(this.GetBankCodeListPath, "GET", {bankType,cityCode,name});
return result;
}
private static SendApplyMerchPath: string = '/Apply/SendApplyMerch';
/*****进价提交*****/
static SendApplyMerch(para:any) {
var result = Service.Request(this.SendApplyMerchPath, "POST", para);
return result;
}
private static GetAgentMerchLogPath: string = '/Apply/GetAgentMerchLog';
/*****获取待审核*****/
static GetAgentMerchLog() {
var result = Service.Request(this.GetAgentMerchLogPath, "GET", {});
return result;
}
private static AuditApplyPath: string = '/Apply/AuditApply';
/*****确认资料*****/
static AuditApply(outId:string) {
var result = Service.Request(this.AuditApplyPath, "POST", {outId});
return result;
}
private static SetPayFeePath: string = '/Apply/SetPayFee';
/*****确认资料*****/
static SetPayFee(outId:string) {
var result = Service.Request(this.SetPayFeePath, "POST", {outId});
return result;
}
private static GetAssortListPath: string = '/Apply/GetAssortList';
/*****获取v派分类*****/
static GetAssortList(code:string) {
var result = Service.Request(this.GetAssortListPath, "GET", {code,parent:'0'});
return result;
}
private static GetAgentMerchPath: string = '/Agent/GetAgentMerch';
/*****获取已开通商家*****/
static GetAgentMerch(type:number,page:number) {
var result = Service.Request(this.GetAgentMerchPath, "GET", {type,page});
return result;
}
private static GetAppMerchInfoPath: string = '/Apply/GetAppMerchInfo';
/*****获取银盛已填写信息*****/
static GetAppMerchInfo(outId:string) {
var result = Service.Request(this.GetAppMerchInfoPath, "GET", {outId});
return result;
}
private static UpdateMerchPath: string = '/Apply/UpdateMerch';
/*****修改银盛信息*****/
static UpdateMerch(obj:any) {
var result = Service.Request(this.UpdateMerchPath, "POST", obj);
return result;
}
private static UploadImgPath: string = '/Apply/UploadImg';
/*****修改银盛图片*****/
static UploadImg(outId:string,picType:string,img:string) {
var result = Service.Request(this.UploadImgPath, "POST", {outId,picType,img});
return result;
}
private static GetAgentApplyPath: string = '/Apply/GetAgentApply';
/*****获取添加的商家*****/
static GetAgentApply(serch:string,page:number) {
var result = Service.Request(this.GetAgentApplyPath, "GET", {serch,page});
return result;
}
private static GetCategoryPath: string = '/Apply/GetCategory';
/*****获取商家类型*****/
static GetCategory() {
var result = Service.Request(this.GetCategoryPath, "GET", {});
return result;
}
private static AddMerchInfoPath: string = '/Apply/AddMerchInfo';
/*****添加商户*****/
static AddMerchInfo(obj:any) {
var result = Service.Request(this.AddMerchInfoPath, "POST", obj);
return result;
}
private static BandAppIdPath: string = '/Agent/BandAppId';
/*****绑定appid*****/
static BandAppId(merchId:string) {
var result = Service.Request(this.BandAppIdPath, "POST", {merchId});
return result;
}
}
export {
Service,
NvpApplyService
}

View File

@@ -1,50 +0,0 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpBankService {
private static GetPageBankListPath: string = '/Bank/GetPageBankList';
/*****用户银行卡列表*****/
static GetPageBankList(page: number) {
var result = Service.Request(this.GetPageBankListPath, "GET", { page });
return result;
}
private static GetBankListPath: string = '/Bank/GetBankList';
/*****用户银行卡列表*****/
static GetBankList() {
var result = Service.Request(this.GetBankListPath, "GET", "");
return result;
}
private static GetUnitBankListPath: string = '/Bank/GetUnitBankList';
/*****银行卡列表*****/
static GetUnitBankList() {
var result = Service.Request(this.GetUnitBankListPath, "GET", "");
return result;
}
private static AddUserBankPath: string = '/Bank/AddUserBank';
/*****添加银行卡*****/
static AddUserBank(account: string, bank: string, name: string) {
var result = Service.Request(this.AddUserBankPath, "POST", { account, bank, name });
return result;
}
private static UpdateBankPath: string = '/Bank/UpdateBank';
/*****修改银行卡*****/
static UpdateBank(ubId: string, bank: string, name: string, account: string) {
var result = Service.Request(this.UpdateBankPath, "POST", { ubId, bank, name, account });
return result;
}
private static DelUserBankPath: string = '/Bank/DelUserBank';
/*****删除银行卡*****/
static DelUserBank(ubId: string) {
var result = Service.Request(this.DelUserBankPath, "POST", { ubId });
return result;
}
}
export {
Service,
NvpBankService
}

View File

@@ -1,36 +0,0 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class NvpLoginService {
private static GetWxLoginOpenIdPath: string = '/Login/GetWxLoginOpenId';
/*****小程序根据code获取openId*****/
static GetWxLoginOpenId(code: string, type: number) {
var result = Service.Request(this.GetWxLoginOpenIdPath, "GET", { code, type });
return result;
}
private static AppletLoginPath: string = '/Login/AppletLogin';
/*****普通登陆接口(小程序)*****/
static AppletLogin(openId: string, channel: string) {
var result = Service.Request(this.AppletLoginPath, "POST", { openId, channel });
return result;
}
private static RegistPath: string = '/Login/Regist';
/*****注册接口*****/
static Regist(remNo: string, openId: string, uniopenId: string, channel: string, type: number) {
var result = Service.Request(this.RegistPath, "POST", { remNo, openId, uniopenId, channel, type });
return result;
}
private static GetNumberPhonePath: string = '/Login/GetNumberPhone';
/*****获取手机号*****/
static GetNumberPhone(code: string) {
var result = Service.Request(this.GetNumberPhonePath, "GET", { code });
return result;
}
}
export {
Service,
NvpLoginService
}

View File

@@ -1,32 +0,0 @@
import { Service } from '@/Service/Service';
/*****设备接口*****/
class NvpMachineService {
private static GetMerchListPath: string = '/Machine/GetMerchList';
/*****商户设备*****/
static GetMerchList(merchId: string) {
var result = Service.Request(this.GetMerchListPath, "GET", { merchId });
return result;
}
private static DelMerchMachinePath: string = '/Machine/DelMerchMachine';
/*****删除设备*****/
static DelMerchMachine(merchId: string, machineId: string) {
var result = Service.Request(this.DelMerchMachinePath, "POST", { merchId, machineId });
return result;
}
private static AddMachinePath: string = '/Machine/AddMachine';
/*****添加设备*****/
static AddMachine(merchId: string, payCode: string) {
var result = Service.Request(this.AddMachinePath, "POST", { merchId, payCode });
return result;
}
}
export {
Service,
NvpMachineService
}

View File

@@ -1,106 +0,0 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpMerchService {
private static GetMerchInfoPath: string = '/Merch/GetMerchInfo';
/*****获取商家信息*****/
static GetMerchInfo(merchId: string,lon:number,lat:number) {
var result = Service.Request(this.GetMerchInfoPath, "GET", { merchId,lon,lat});
return result;
}
private static GetMerchOrderPath: string = '/Merch/GetMerchOrder';
/*****获取商家营业数据*****/
static GetMerchOrder(merchId: string) {
var result = Service.Request(this.GetMerchOrderPath, "GET", { merchId });
return result;
}
private static UpdateMerchInfoPath: string = '/Merch/UpdateMerchInfo';
/*****修改商家信息*****/
static UpdateMerchInfo(merchId: string, logo: string ,name:string, phone:string,province:string,city:string,county:string,address:string,lon:string,lat:string,sTime:string,eTime:string,showImg:string) {
var result = Service.Request(this.UpdateMerchInfoPath, "POST", { merchId, logo,name ,phone,province,city,county,address,lon,lat,sTime,eTime,showImg});
return result;
}
private static GetMerchAccPath: string = '/Merch/GetMerchAcc';
/*****获取商家佣金数据*****/
static GetMerchAcc(merchId: string) {
var result = Service.Request(this.GetMerchAccPath, "GET", { merchId });
return result;
}
private static GetMerchAccLogPath: string = '/Merch/GetMerchAccLog';
/*****获取商家佣金记录*****/
static GetMerchAccLog(merchId: string,op:string,page:number) {
var result = Service.Request(this.GetMerchAccLogPath, "GET", { merchId,op,page });
return result;
}
private static MerchWithDrawPath: string = '/Merch/MerchWithDraw';
/*****商家佣金提现*****/
static MerchWithDraw(merchId: string, money: number ,name:string, account:string) {
var result = Service.Request(this.MerchWithDrawPath, "POST", { merchId, money,name,account});
return result;
}
private static GetMerchWithLogPath: string = '/Merch/GetMerchWithLog';
/*****商家佣金提现记录*****/
static GetMerchWithLog(merchId: string) {
var result = Service.Request(this.GetMerchWithLogPath, "GET", { merchId });
return result;
}
private static GetMerchTicketListPath: string = '/Merch/GetMerchTicketList';
/*****商家优惠券列表*****/
static GetMerchTicketList(merchId: string) {
var result = Service.Request(this.GetMerchTicketListPath, "GET", { merchId });
return result;
}
private static AddMerchTicketPath: string = '/Merch/AddMerchTicket';
/*****商家优惠券修改添加*****/
static AddMerchTicket(ticketId: string, merchId: string ,code:string, atkAcc:number,redAcc:number,count:number,useTime:Number,state:Number,addTime:string,endTime:string) {
var result = Service.Request(this.AddMerchTicketPath, "POST", { ticketId, merchId,code,atkAcc,redAcc,count,useTime,state,addTime,endTime});
return result;
}
private static DelMerchTicketPath: string = '/Merch/DelMerchTicket';
/*****商家删除优惠券*****/
static DelMerchTicket(ticketId: string) {
var result = Service.Request(this.DelMerchTicketPath, "POST", { ticketId});
return result;
}
private static GetTicketInfoPath: string = '/Merch/GetTicketInfo';
/*****获取优惠券详情*****/
static GetTicketInfo(ticketId: string) {
var result = Service.Request(this.GetTicketInfoPath, "GET", { ticketId });
return result;
}
private static GetAppVersionPath: string = '/Login/GetAppVersion';
/*****更新*****/
static GetAppVersion() {
var result = Service.Request(this.GetAppVersionPath, "GET", {type:2});
return result;
}
}
export {
Service,
NvpMerchService
}

View File

@@ -1,43 +0,0 @@
import { Service } from '@/Service/Service';
/*****公共接口*****/
class NvpPubService {
private static GetIndexPath: string = '/Pub/GetIndex';
/*****主页信息*****/
static GetIndex() {
var result = Service.Request(this.GetIndexPath, "GET", "");
return result;
}
private static GetIndexDataPath: string = '/Pub/GetIndexData';
/*****获取首页数据*****/
static GetIndexData(lon: number, lat: number, city: string, county: string, sort: number, page: number) {
var result = Service.Request(this.GetIndexDataPath, "GET", { lon, lat, city, county, sort, page });
return result;
}
private static GetMenuDataPath: string = '/Pub/GetMenuData';
/*****获取分类*****/
static GetMenuData(type: string, parent: string) {
var result = Service.Request(this.GetMenuDataPath, "GET", { type, parent });
return result;
}
private static GetMerchDataPath: string = '/Pub/GetMerchData';
/*****获取店铺*****/
static GetMerchData(code: string, serch: string, assId: string, lon: number, lat: number, city: string, county: string, sort: number, page: number, limit: number) {
var result = Service.Request(this.GetMerchDataPath, "GET", { code, serch, assId, lon, lat, city, county, sort, page, limit });
return result;
}
private static GetRandomMerchPath: string = '/Pub/GetRandomMerch';
/*****随机获取商家*****/
static GetRandomMerch(count: number, lon: number, lat: number, city: string, county: string) {
var result = Service.Request(this.GetRandomMerchPath, "GET", { count, lon, lat, city, county });
return result;
}
}
export {
Service,
NvpPubService
}

View File

@@ -1,22 +0,0 @@
import { Service } from '@/Service/Service';
/*****腾讯云存储*****/
class NvpTencentCosService {
private static GetAuthorizationPath: string = '/TencentCos/GetAuthorization';
/*****获取云存储配置*****/
static GetAuthorization() {
var result = Service.Request(this.GetAuthorizationPath, "GET", "");
return result;
}
private static GetUpLoadInfoPath: string = '/TencentCos/GetUpLoadInfo';
/*****获取上传地址*****/
static GetUpLoadInfo(code: string, fileName: string, desire: string) {
var result = Service.Request(this.GetUpLoadInfoPath, "GET", { code, fileName, desire });
return result;
}
}
export {
Service,
NvpTencentCosService
}

View File

@@ -1,43 +0,0 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpUserService {
private static GetUserInfoPath: string = '/User/GetUserInfo';
/*****用户基础信息*****/
static GetUserInfo() {
var result = Service.Request(this.GetUserInfoPath, "GET", "");
return result;
}
private static UpdateUserInfoPath: string = '/User/UpdateUserInfo';
/*****修改用户信息*****/
static UpdateUserInfo(par: string, smsCode: string, type: number) {
var result = Service.Request(this.UpdateUserInfoPath, "POST", { par, smsCode, type });
return result;
}
private static GetUserAccLogPath: string = '/User/GetUserAccLog';
/*****获取账户记录*****/
static GetUserAccLog(code: string, accType: string, page: number) {
var result = Service.Request(this.GetUserAccLogPath, "GET", { code, accType, page });
return result;
}
private static GetUserCommissionInfoPath: string = '/User/GetUserCommissionInfo';
/*****用户佣金账户*****/
static GetUserCommissionInfo() {
var result = Service.Request(this.GetUserCommissionInfoPath, "GET", "");
return result;
}
private static GetUserCommissionLogPath: string = '/User/GetUserCommissionLog';
/*****用户佣金记录*****/
static GetUserCommissionLog(code: string, accType: string, page: number) {
var result = Service.Request(this.GetUserCommissionLogPath, "GET", { code, accType, page });
return result;
}
}
export {
Service,
NvpUserService
}

View File

@@ -1,22 +0,0 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class NvpWithService {
private static WithDrawPath: string = '/With/WithDraw';
/*****佣金提现*****/
static WithDraw(money: number, name: string, account: string) {
var result = Service.Request(this.WithDrawPath, "POST", { money, name, account });
return result;
}
private static GetUserWithLogPath: string = '/With/GetUserWithLog';
/*****用户提现记录*****/
static GetUserWithLog(page: number) {
var result = Service.Request(this.GetUserWithLogPath, "GET", { page });
return result;
}
}
export {
Service,
NvpWithService
}

View File

@@ -336,7 +336,7 @@ export class Service extends BaseConfig {
static uploadH5(path, dic, callback) {
console.log(this.payuploadUrl,'xxx')
uni.uploadFile({
url: this.payuploadUrl+'/Upload/UploadFile',
url: this.payuploadUrl+'/api/Upload/UploadImg',
method: "POST",
header: {
'Authorization': 'Bearer ' + Service.GetUserToken(),

View File

@@ -0,0 +1,49 @@
import { Service } from '@/Service/Service';
/*****项目接口*****/
class PlanService {
private static GetPlanListPath : string = '/api/Plan/GetPlanList';
/*****计划列表接口*****/
static GetPlanList( planType:string, page:string) {
var result = Service.Request(this.GetPlanListPath, "GET", { planType, page});
return result;
}
private static GetPlanInfoPath : string = '/api/Plan/GetPlanInfo';
/*****计划详情接口*****/
static GetPlanInfo(planId:string) {
var result = Service.Request(this.GetPlanInfoPath, "GET", {planId});
return result;
}
private static AddPlanPath : string = '/api/Plan/AddPlan';
/*****添加修改计划接口*****/
static AddPlan(data:any) {
var result = Service.Request(this.AddPlanPath, "POST", data);
return result;
}
private static DeletePlanPath : string = '/api/Plan/DeletePlan';
/*****删除计划接口*****/
static DeletePlan(planId:string) {
var result = Service.Request(this.DeletePlanPath, "POST", { planId});
return result;
}
private static GetNoSelectStudentPath : string = '/api/Plan/GetNoSelectStudent';
/*****查询项目分组未选择的学生接口*****/
static GetNoSelectStudent(planId:string) {
var result = Service.Request(this.GetNoSelectStudentPath, "GET", { planId});
return result;
}
}
export {
Service,
PlanService
}

View File

@@ -0,0 +1,14 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class loginService {
private static LoginPath : string = '/api/Login/Login';
/*****登录接口*****/
static Login(code : string) {
var result = Service.Request(this.LoginPath, "POST", { code });
return result;
}
}
export {
Service,
loginService
}

View File

@@ -0,0 +1,47 @@
import { Service } from '@/Service/Service';
/*****登录接口*****/
class studentService {
private static GetStudentListPath : string = '/api/Student/GetStudentList';
/*****学生列表不分页接口*****/
static GetStudentList() {
var result = Service.Request(this.GetStudentListPath, "GET", { });
return result;
}
private static GetStudentInfoPath : string = '/api/Student/GetStudentInfo';
/*****学生详情接口*****/
static GetStudentInfo(studentId : string) {
var result = Service.Request(this.GetStudentInfoPath, "GET", { studentId });
return result;
}
private static AddPath : string = '/api/Student/Add';
/*****添加学生接口*****/
static Add(data:any) {
var result = Service.Request(this.AddPath, "POST", data);
return result;
}
private static DeletePath : string = '/api/Student/Delete';
/*****删除学生接口*****/
static Delete(studentId:string) {
var result = Service.Request(this.DeletePath, "POST", {studentId });
return result;
}
private static GetStudentListPagePath : string = '/api/Student/GetStudentListPage';
/*****学生列表分页接口*****/
static GetStudentListPage(page : string) {
var result = Service.Request(this.GetStudentListPagePath, "GET", { page });
return result;
}
}
export {
Service,
studentService
}

View File

@@ -0,0 +1,14 @@
import { Service } from '@/Service/Service';
/*****上传接口*****/
class loginService {
private static UploadImgPath : string = '/api/Upload/UploadImg';
/*****上传接口*****/
static UploadImg(file : string,path:string) {
var result = Service.Request(this.UploadImgPath, "POST", { file, path});
return result;
}
}
export {
Service,
loginService
}

View File

@@ -0,0 +1,37 @@
import { Service } from '@/Service/Service';
/*****用户接口*****/
class userService {
private static GetUserInfoPath : string = '/api/User/GetUserInfo';
/*****获取用户信息接口*****/
static GetUserInfo() {
var result = Service.Request(this.GetUserInfoPath, "GET", {});
return result;
}
private static IsCheakUserVerifyPath : string = '/api/User/IsCheakUserVerify';
/*****是否可以使用接口*****/
static IsCheakUserVerify() {
var result = Service.Request(this.IsCheakUserVerifyPath, "GET", {});
return result;
}
private static UpdateUserInfoPath : string = '/api/User/UpdateUserInfo';
/*****修改用户信息接口*****/
static UpdateUserInfo(name:string,phone:string,headImg:string) {
var result = Service.Request(this.UpdateUserInfoPath, "POST", {name,phone,headImg});
return result;
}
// private static GetStudentInfoPath : string = '/api/Student/GetStudentInfo';
// /*****根据学生id查详情接口*****/
// static GetStudentInfo(studentId:string) {
// var result = Service.Request(this.GetStudentInfoPath, "GET", {studentId});
// return result;
// }
}
export {
Service,
userService
}

View File

@@ -34,8 +34,7 @@
{
"path": "swiming",
"style": {
"navigationBarTitleText": "游泳项目",
"navigationStyle": "custom"
"navigationBarTitleText": "游泳项目"
}
},
{
@@ -77,8 +76,26 @@
{
"path": "project",
"style": {
"navigationBarTitleText": "包干",
"navigationStyle": "custom"
"navigationBarTitleText": "包干"
}
},
{
"path": "plan",
"style": {
"navigationBarTitleText": "训练计划"
}
},
{
"path": "hunyang",
"style": {
"navigationBarTitleText": "混氧"
}
},
{
"path": "addHunyang",
"style": {
"navigationBarTitleText": "创建项目"
}
}
]

View File

@@ -364,7 +364,7 @@
selectedStudentIds.value.push(studentId)
}
}
const getStudentBestTime = (studentId : string) : number => {
const records = mockTrainingRecords[selectedProjectId.value] || []
const studentRecords = records.filter(r => r.studentId === studentId)
@@ -444,132 +444,132 @@
.subtitle {
font-size: 24rpx;
color: #999;
}
}
}
.section-header {
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.section-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.select-count {
font-size: 24rpx;
color: #52c41a;
}
}
.project-select-section {
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.picker-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
justify-content: space-between;
padding: 20rpx 24rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
transition: all 0.3s ease;
.section-title {
font-size: 30rpx;
font-weight: 600;
&:active {
background-color: #f0f0f0;
border-color: #52c41a;
}
.picker-text {
font-size: 28rpx;
color: #333;
}
}
}
.select-count {
font-size: 24rpx;
color: #52c41a;
}
.student-select-section {
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.section-header {
margin-bottom: 24rpx;
}
.project-select-section {
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.student-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
.picker-wrapper {
.student-item {
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 20rpx 24rpx;
padding: 20rpx 10rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
border: 1rpx solid #e8e8e8;
border: 2rpx solid transparent;
transition: all 0.3s ease;
position: relative;
&:active {
background-color: #f0f0f0;
transform: scale(0.95);
}
&.selected {
background-color: #f0f9eb;
border-color: #52c41a;
}
.picker-text {
font-size: 28rpx;
color: #333;
}
}
}
.student-select-section {
background-color: #fff;
margin: 0 20rpx 20rpx;
border-radius: 16rpx;
padding: 28rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
.section-header {
margin-bottom: 24rpx;
}
.student-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
.student-item {
.student-avatar {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
padding: 20rpx 10rpx;
background-color: #f8f8f8;
border-radius: 12rpx;
border: 2rpx solid transparent;
transition: all 0.3s ease;
position: relative;
justify-content: center;
margin-bottom: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
&:active {
transform: scale(0.95);
}
&.selected {
background-color: #f0f9eb;
border-color: #52c41a;
}
.student-avatar {
width: 60rpx;
height: 60rpx;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
box-shadow: 0 2rpx 8rpx rgba(82, 196, 26, 0.3);
.avatar-text {
font-size: 24rpx;
font-weight: 700;
color: #fff;
}
}
.student-name {
.avatar-text {
font-size: 24rpx;
color: #333;
text-align: center;
font-weight: 700;
color: #fff;
}
}
.check-icon {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 28rpx;
height: 28rpx;
background-color: #52c41a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.student-name {
font-size: 24rpx;
color: #333;
text-align: center;
}
.check-icon {
position: absolute;
top: 8rpx;
right: 8rpx;
width: 28rpx;
height: 28rpx;
background-color: #52c41a;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
}
}
}

View File

@@ -2,14 +2,14 @@
<view class="paragraph-container">
<view class="header-section">
<view class="header-title">
<text class="title">">分段数据</text>
<text class="title">分段数据</text>
<text class="subtitle">学生分段训练成绩分析</text>
</view>
</view>
<view class="select-section">
<view class="select-label">
<text class="label-text">">选择项目</text>
<text class="label-text">选择项目</text>
</view>
<picker mode="selector" :range="projectOptions" range-key="name" :value="selectedProjectIndex"
@change="handleProjectChange">
@@ -192,18 +192,18 @@
const mockData: Record<string, Record<string, any[]>> = {
'1': {
'2026-03-12': [
{ name: '王小明', projectName: '100米'自由泳', segment20: 0.037, segment70: 0.128, segment120: 0.219, segment170: 0.311 },
{ name: '张小红', projectName: '100'自由泳', segment20: 0.042, segment70: 0.145, segment120: 0.248, segment170: 0.352 },
{ name: '李小龙', projectName: '100米'自由泳', segment20: 0.032, segment70: 0.112, segment120: 0.192, segment170: 0.272 }
{ name: '王小明', projectName: '100米自由泳', segment20: 0.037, segment70: 0.128, segment120: 0.219, segment170: 0.311 },
{ name: '张小红', projectName: '100米自由泳', segment20: 0.042, segment70: 0.145, segment120: 0.248, segment170: 0.352 },
{ name: '李小龙', projectName: '100米自由泳', segment20: 0.032, segment70: 0.112, segment120: 0.192, segment170: 0.272 }
],
'2026-03-14': [
{ name: '王小明', projectName: '100'自由泳', segment20: 0.035, segment70: 0.122, segment120: 0.209, segment170: 0.296 },
{ name: '张小红', projectName: '100米'自由泳', segment20: 0.040, segment70: 0.138, segment120: 0.236, segment170: 0.334 }
{ name: '王小明', projectName: '100米自由泳', segment20: 0.035, segment70: 0.122, segment120: 0.209, segment170: 0.296 },
{ name: '张小红', projectName: '100米自由泳', segment20: 0.040, segment70: 0.138, segment120: 0.236, segment170: 0.334 }
]
},
'2': {
'2026-03-16': [
{ name: '赵小芳', projectName: '200'自由泳', segment20: 0.038, segment70: 0.133, segment120: 0.228, segment170: 0.323 }
{ name: '赵小芳', projectName: '200米自由泳', segment20: 0.038, segment70: 0.133, segment120: 0.228, segment170: 0.323 }
]
},
'3': {
@@ -213,7 +213,7 @@
},
'4': {
'2026-03-24': [
{ name: '周小丽', projectName: '100米'蝶泳', segment20: 0.041, segment70: 0.143, segment120: 0.245, segment170: 0.347 }
{ name: '周小丽', projectName: '100米蝶泳', segment20: 0.041, segment70: 0.143, segment120: 0.245, segment170: 0.347 }
]
}
}
@@ -503,7 +503,7 @@
border-radius: 20rpx;
margin-top: 16rpx;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06), 0 0 0 1rpx rgba(0, 0kt, 0, 0.04) inset;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.06);
position: relative;
overflow: hidden;
@@ -613,7 +613,7 @@
}
&.female {
color color: #fa8c16;
color: #fa8c16;
background: linear-gradient(135deg, rgba(250, 140, 22, 0.1) 0%, rgba(255, 169, 64, 0.05) 100%);
}
}
@@ -654,7 +654,7 @@
}
.table-header {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
// background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
.header-row {
display: flex;
@@ -664,7 +664,7 @@
padding: 24rpx 16rpx;
font-size: 28rpx;
font-weight: 700;
color: #fff;
// color: #fff;
text-align: center;
border-right: 1rpx solid rgba(255, 255, 255, 0.1);
flex-shrink: 0;

View File

@@ -29,7 +29,7 @@
</view>
<!-- 包干项目模块 -->
<view @click="showTimingProjectModal(2)" class="create-card package-card">
<view @click="Service.GoPage('/pages/userFunc/project')" class="create-card package-card">
<view class="card-icon">
<view class="icon-circle package-icon">
<u-icon name="grid" size="40" color="#fff"></u-icon>
@@ -45,7 +45,7 @@
</view>
<!-- 分段项目模块 -->
<view @click="createSegmentProject" class="create-card segment-card">
<view @click="showTimingProjectModal(2)" class="create-card segment-card">
<view class="card-icon">
<view class="icon-circle segment-icon">
<u-icon name="list" size="40" color="#fff"></u-icon>
@@ -59,6 +59,22 @@
<u-icon name="arrow-right" size="20" color="#1890ff"></u-icon>
</view>
</view>
<!-- 混氧项目模块 -->
<view @click="showTimingProjectModal(3)" class="create-card hunyang-card">
<view class="card-icon">
<view class="icon-circle hunyang-icon">
<u-icon name="grid" size="40" color="#fff"></u-icon>
</view>
</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>
</view>
</view>
</view>
</view>
@@ -66,15 +82,14 @@
<view v-if="showTimingModal" class="modal-overlay" @click="closeTimingProjectModal"></view>
<view v-if="showTimingModal" class="project-list-modal">
<view class="modal-header">
<text class="modal-title">计时项目</text>
<text class="modal-title"> {{ currentIndex==1?"计时项目":(currentIndex==2?"分段项目":"混氧项目") }} </text>
<view class="modal-close" @click="closeTimingProjectModal">
<u-icon name="close" size="20" color="#999"></u-icon>
</view>
</view>
<view class="modal-content">
<!-- 新增按钮 -->
<view v-if="currentIndex!==2" class="add-btn" @click="createTimingProject">
<view class="add-btn" @click="createTimingProject">
<u-icon name="plus" size="18" color="#1890ff"></u-icon>
<text class="add-btn-text">新增项目</text>
</view>
@@ -83,8 +98,8 @@
<view class="project-list">
<text class="list-title" v-if="projects.length > 0">项目列表</text>
<view v-if="projects.length > 0" class="list-container">
<view v-for="project in projects" :key="project.id" class="project-item"
@click=" goPageFunc() ">
<view v-for="project in projects" :key="project.planId" class="project-item"
@click="handleProjectClick(project)">
<view class="item-icon">
<view class="icon-bg">
<text class="icon-text">{{ project.name.charAt(0) }}</text>
@@ -92,7 +107,7 @@
</view>
<view class="item-info">
<text class="item-name">{{ project.name }}</text>
<text class="item-count">{{ project.studentCount }}</text>
<text v-if="currentIndex!==3" class="item-count">{{ project.users.length }}</text>
</view>
</view>
</view>
@@ -103,140 +118,131 @@
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Service } from '@/Service/Service'
import { onLoad } from '@dcloudio/uni-app'
import { loginService } from '@/Service/swimming/loginService'
import { PlanService } from '@/Service/swimming/PlanService'
// 项目类型
interface Project {
id : string
name : string
startType : string // together | interval
intervalSeconds : string
laneType : string // single | multi
laneCount : string
lanePersonCount : string
studentCount : number
students : {
id : string
name : string
gender : string
age : string
}[]
}
let currentIndex = ref(0)
let currentIndex=ref(0)
// 分页相关
let page = ref(1)
let status = ref('loadmore')
// 模拟项目数据
const projects = ref<Project[]>([
{
id: '001',
name: '自由泳500米',
startType: 'together',
intervalSeconds: '',
laneType: 'multi',
laneCount: '4',
lanePersonCount: '2',
studentCount: 8,
students: [
{ id: 's1', name: '张三', gender: '男', age: '12' },
{ id: 's2', name: '李四', gender: '女', age: '13' },
{ id: 's3', name: '王五', gender: '男', age: '11' },
{ id: 's4', name: '赵六', gender: '女', age: '12' },
{ id: 's5', name: '钱七', gender: '男', age: '14' },
{ id: 's6', name: '孙八', gender: '女', age: '10' },
{ id: 's7', name: '周九', gender: '男', age: '13' },
{ id: 's8', name: '吴十', gender: '女', age: '12' }
]
},
{
id: '002',
name: '蛙泳200米',
startType: 'interval',
intervalSeconds: '30',
laneType: 'single',
laneCount: '',
lanePersonCount: '',
studentCount: 5,
students: [
{ id: 's1', name: '陈十一', gender: '男', age: '14' },
{ id: 's2', name: '林十二', gender: '女', age: '12' },
{ id: 's3', name: '黄十三', gender: '男', age: '11' },
{ id: 's4', name: '周十四', gender: '女', age: '13' },
{ id: 's5', name: '吴十五', gender: '男', age: '10' }
]
}
])
const projects = ref<Array<any>>([])
// 弹窗状态
const showTimingModal = ref(false)
const showDetailModal = ref(false)
const currentProject = ref<Project | null>(null)
// 生命周期
onMounted(() => {
// TODO: 实际应从接口获取项目列表
onLoad(() => {
if (!Service.GetUserToken()) {
login()
}
})
const login = () => {
uni.getProvider({
service: 'oauth',
success: function (res : any) {
uni.login({
onlyAuthorize: true,
provider: res.provider,
success: function (loginRes) {
loginService.Login(
loginRes.code,
).then(content => {
if (content.code == 0) {
Service.SetUserToken(content.data.token)
} else {
Service.Msg(content.msg)
}
})
},
fail: function (error) {
}
});
}
});
}
onUnmounted(() => {
// 清理
})
// 获取项目列表数据
const getData = () => {
projects.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
// 获取项目列表
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
PlanService.GetPlanList(currentIndex.value==1?'计时项目':(currentIndex.value==2?'分段项目':'混氧项目'), page.value.toString()).then(res => {
if (res.code == 0) {
projects.value = [...projects.value, ...res.data]
status.value = res.data.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
// 显示计时项目列表弹窗
const showTimingProjectModal = (index:any) => {
const showTimingProjectModal = (index : any) => {
uni.hideTabBar()
showTimingModal.value = true
currentIndex.value=index
currentIndex.value = index
getData()
}
// 关闭计时项目列表弹窗
const closeTimingProjectModal = () => {
showTimingModal.value = false
uni.showTabBar()
}
// 创建计时项目
const createTimingProject = () => {
Service.GoPage('/pages/userFunc/setCourse')
}
// 创建包干项目
const createPackageProject = () => {
Service.GoPage('/pages/userFunc/setCourse')
}
// 创建分段项目
const createSegmentProject = () => {
Service.GoPage('/pages/userFunc/setCourse')
}
// 显示项目详情
const showProjectDetail = (project : Project) => {
currentProject.value = project
showTimingModal.value = false
showDetailModal.value = true
}
// 关闭弹窗
const closeDetailModal = () => {
showDetailModal.value = false
currentProject.value = null
}
const goPageFunc=()=>{
if(currentIndex.value==1){
Service.GoPage('/pages/userFunc/swiming')
}else{
Service.GoPage('/pages/userFunc/project')
if (currentIndex.value == 3) {
Service.GoPage('/pages/userFunc/addHunyang')
} else {
Service.GoPage('/pages/userFunc/setCourse?type=' + currentIndex.value)
}
}
// 处理项目点击事件
const handleProjectClick = (project:any) => {
if (currentIndex.value === 1) {
showTimingModal.value = false
Service.GoPage('/pages/userFunc/swiming?id='+project.planId)
}
else if (currentIndex.value === 2) {
showTimingModal.value = false
Service.GoPage('/pages/userFunc/segmentation?id='+project.planId)
}
else {
showTimingModal.value = false
Service.GoPage('/pages/userFunc/hunyang?id='+project.planId)
}
}
</script>
<style lang="scss" scoped>
@@ -447,6 +453,11 @@
&.segment-icon {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 50%, #389e0d 100%);
}
/* 混氧图标 - 紫色渐变 */
&.hunyang-icon {
background: linear-gradient(135deg, #722ed1 0%, #9254de 50%, #531dab 100%);
}
}
&::after {
@@ -483,7 +494,7 @@
line-height: 1.4;
}
.card-arrow {
flex-shrink: 0;
@@ -550,7 +561,7 @@
}
}
/* 计时项目列表弹窗 */
.project-list-modal {
@@ -934,4 +945,75 @@
transform: translate(-50%, -50%);
}
}
/* 包干项目选项样式 */
.package-options {
margin-bottom: 36rpx;
}
.form-group {
margin-bottom: 32rpx;
}
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
}
.radio-group {
display: flex;
gap: 20rpx;
}
.radio-item {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
padding: 24rpx 20rpx;
background: #f5f5f5;
border-radius: 16rpx;
border: 2rpx solid transparent;
transition: all 0.3s ease;
}
.radio-item.active {
background: linear-gradient(135deg, #e6f7ff 0%, #bae7ff 100%);
border-color: #1890ff;
}
.radio-item:active {
transform: scale(0.98);
}
.radio-circle {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 3rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.radio-item.active .radio-circle {
border-color: #1890ff;
}
.radio-dot {
width: 18rpx;
height: 18rpx;
border-radius: 50%;
background: #1890ff;
}
.radio-label {
font-size: 28rpx;
font-weight: 500;
color: #333;
}
</style>

View File

@@ -3,15 +3,15 @@
<!-- 用户信息卡片 -->
<view class="user-card">
<view class="user-avatar">
<view class="avatar-circle">
<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>
</view>
<view class="user-info">
<text class="user-nickname">游泳爱好者</text>
<text class="user-nickname">{{ userInfo.name }}</text>
<view class="user-phone-wrapper">
<text class="user-phone">138****8888</text>
<text class="user-phone">{{ userInfo.phone || '暂无手机号' }}</text>
</view>
</view>
<view @click="Service.GoPage('/pages/userFunc/set')" class="user-edit">
@@ -22,25 +22,17 @@
<!-- 统计数据区域 -->
<view class="stats-section">
<view class="stats-card">
<view class="stat-item" >
<view class="stat-item">
<view class="stat-info">
<text class="stat-value">12</text>
<text class="stat-value">{{ userInfo.projectCount || 0 }}</text>
<text class="stat-label">我的项目</text>
</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item" >
<view class="stat-item">
<view class="stat-info">
<text class="stat-value">8</text>
<text class="stat-label">记录数</text>
</view>
</view>
<view class="stat-divider"></view>
<view class="stat-item" >
<view class="stat-info">
<text class="stat-value">8</text>
<text class="stat-value">{{ userInfo.studentCount || 0 }}</text>
<text class="stat-label">学员数</text>
</view>
</view>
@@ -50,6 +42,7 @@
<!-- 功能菜单区域 -->
<view class="menu-section">
<view class="menu-card">
<!-- 项目管理菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/projectList')">
<view class="menu-icon-bg project-icon-bg">
<u-icon name="list" size="28" color="#fff"></u-icon>
@@ -70,6 +63,8 @@
<view class="menu-divider"></view>
<!-- 学员管理菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/student')">
<view class="menu-icon-bg academy-icon-bg">
<u-icon name="grid" size="28" color="#fff"></u-icon>
@@ -90,6 +85,7 @@
<view class="menu-divider"></view>
<!-- 数据分析菜单项 -->
<view class="menu-item" @click="Service.GoPage('/pages/userFunc/analyze')">
<view class="menu-icon-bg analysis-icon-bg">
<u-icon name="order" size="28" color="#fff"></u-icon>
@@ -114,19 +110,43 @@
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app"
import { reactive, ref } from "vue"
import { Service } from '@/Service/Service'
import { userService } from '@/Service/swimming/userService'
let userInfo = ref<any>({})
onLoad(() => {
loadUserInfo()
})
onShow(() => {
})
const loadUserInfo = () => {
userService.GetUserInfo().then((content) => {
if (content.code == 0) {
userInfo.value=content.data.userInfo
} else {
Service.Msg(content.msg)
}
})
}
/**
* 跳转到训练计划页面
* 点击"训练计划"菜单项时触发
* 功能说明:
* - 跳转到训练计划列表页面
* - 用户可以查看、添加、编辑训练计划
*/
const goToTrainingPlans = () => {
Service.GoPage('/pages/userFunc/plan')
}
// 跳转到设置
const goToSettings = () => {
@@ -203,6 +223,15 @@
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.avatar-image {
width: 130rpx;
height: 130rpx;
border-radius: 50%;
border: 5rpx solid rgba(255, 255, 255, 0.4);
backdrop-filter: blur(20rpx);
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.1);
}
.avatar-badge {
position: absolute;
bottom: 0;
@@ -410,14 +439,22 @@
border-radius: 50%;
}
/* 项目管理图标背景色 - 蓝色渐变 */
&.project-icon-bg {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
}
/* 训练计划图标背景色 - 紫色渐变 */
&.plan-icon-bg {
background: linear-gradient(135deg, #722ed1 0%, #9254de 50%, #531dab 100%);
}
/* 学员管理图标背景色 - 绿色渐变 */
&.academy-icon-bg {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 50%, #389e0d 100%);
}
/* 数据分析图标背景色 - 橙色渐变 */
&.analysis-icon-bg {
background: linear-gradient(135deg, #faad14 0%, #ffc53d 50%, #d48806 100%);
}
@@ -454,16 +491,25 @@
font-size: 20rpx;
font-weight: 500;
/* 项目管理标签样式 - 蓝色 */
&.project-tag {
background: linear-gradient(135deg, rgba(24, 144, 255, 0.12) 0%, rgba(24, 144, 255, 0.08) 100%);
color: #1890ff;
}
/* 训练计划标签样式 - 紫色 */
&.plan-tag {
background: linear-gradient(135deg, rgba(114, 46, 209, 0.12) 0%, rgba(114, 46, 209, 0.08) 100%);
color: #722ed1;
}
/* 学员管理标签样式 - 绿色 */
&.academy-tag {
background: linear-gradient(135deg, rgba(82, 196, 26, 0.12) 0%, rgba(82, 196, 26, 0.08) 100%);
color: #52c41a;
}
/* 数据分析标签样式 - 橙色 */
&.analysis-tag {
background: linear-gradient(135deg, rgba(250, 173, 20, 0.12) 0%, rgba(250, 173, 20, 0.08) 100%);
color: #faad14;

View File

@@ -0,0 +1,577 @@
<template>
<view class="add-hunyang-container">
<!-- 表单区域 -->
<view class="form-section">
<!-- 项目名称 -->
<view class="form-card">
<view class="form-title">项目信息</view>
<view class="form-group">
<text class="form-label">项目名称</text>
<input class="form-input" v-model="projectName" placeholder="请输入项目名称"
placeholder-class="input-placeholder" />
</view>
<!-- 计划总时长 -->
<view class="total-time-section">
<view class="total-time-label">计划总时长</view>
<view class="total-time-value">{{ formatTotalTime(totalDuration) }}</view>
</view>
</view>
<!-- 计划列表 -->
<view class="form-card">
<view class="form-title">计划列表</view>
<!-- 计划项列表 -->
<view v-for="(item, index) in planList" :key="index" class="plan-item">
<view class="plan-item-header">
<text class="plan-item-title">计划 {{ index + 1 }}</text>
<view class="delete-plan-btn" @click="deletePlanItem(index)">
<u-icon name="trash" size="16" color="#fff"></u-icon>
</view>
</view>
<view class="plan-item-content">
<!-- 目标时间 - 分秒选择 -->
<view class="input-group">
<text class="input-label">目标时间</text>
<view class="time-selector">
<view class="time-input-wrapper">
<input class="time-input" v-model="item.targetSeconds" type="number"
placeholder="" />
<text class="time-unit"></text>
</view>
</view>
</view>
<!-- 休息时长 - 秒选择 -->
<view class="input-group">
<text class="input-label">休息时长</text>
<view class="rest-selector">
<input class="rest-input" v-model="item.restTime" type="number" placeholder="请输入休息时长" />
<text class="time-unit"></text>
</view>
</view>
<!-- 圈数 -->
<view class="input-group">
<text class="input-label">圈数</text>
<view class="lap-selector">
<input class="lap-input" v-model="item.lapCount" type="number" placeholder="请输入圈数" />
<text class="time-unit"></text>
</view>
</view>
</view>
</view>
<!-- 添加计划按钮 -->
<view class="add-plan-btn" @click="addPlanItem">
<u-icon name="plus-circle" size="20" color="#1890ff"></u-icon>
<text class="add-plan-text">添加计划</text>
</view>
</view>
</view>
<!-- 底部按钮区域 -->
<view class="bottom-actions">
<view class="action-buttons">
<button class="cancel-btn" @click="handleCancel">取消</button>
<button class="confirm-btn" @click="handleSave">保存</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { Service } from '@/Service/Service'
import { studentService } from '@/Service/swimming/studentService'
import { PlanService } from '@/Service/swimming/PlanService'
import { onLoad, onShow } from '@dcloudio/uni-app'
// 计划项接口
interface PlanItem {
targetMinutes : string
targetSeconds : string
restTime : string
lapCount : string
}
// 项目名称
const projectName = ref('')
// 计划列表
const planList = ref<PlanItem[]>([])
let planId = ref('')
onLoad((data : any) => {
planId.value = data.id
if(planId.value ){
getPlanInfo()
}
})
// 获取计划详情
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) {
projectName.value=res.data.plan.name
JSON.parse(res.data.plan.project).map((item:any)=>{
planList.value.push({
targetMinutes:'',
targetSeconds : item.target,
restTime : item.rest,
lapCount : item.circle
})
})
} else {
// 显示错误信息
Service.Msg(res.msg)
}
})
}
// 计算计划总时长(秒)
const totalDuration = computed(() => {
let total = 0
planList.value.forEach(item => {
const targetMinutes = parseInt(item.targetMinutes) || 0
const targetSeconds = parseInt(item.targetSeconds) || 0
const targetTime = targetMinutes * 60 + targetSeconds
const restTime = parseInt(item.restTime) || 0
const lapCount = parseInt(item.lapCount) || 0
// 计算公式:(目标时间 + 休息时间) * 圈数
total += (targetTime + restTime) * lapCount
})
return total
})
// 格式化总时长显示(时:分:秒)
const formatTotalTime = (seconds : number) : string => {
if (seconds <= 0) return '00:00:00'
const hours = Math.floor(seconds / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
const secs = Math.floor(seconds % 60)
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
// 生成唯一ID
const generateId = () : string => {
return Date.now().toString() + Math.random().toString(36).substr(2, 9)
}
// 添加计划项
const addPlanItem = () => {
const newItem : PlanItem = {
id: generateId(),
targetMinutes: '',
targetSeconds: '',
restTime: '',
lapCount: ''
}
planList.value.push(newItem)
}
// 删除计划项
const deletePlanItem = (index : number) => {
uni.showModal({
title: '确认删除',
content: '确定要删除该计划吗?',
success: (res) => {
if (res.confirm) {
planList.value.splice(index, 1)
Service.Msg('删除成功')
}
}
})
}
// 验证表单
const validateForm = () : boolean => {
if (!projectName.value.trim()) {
Service.Msg('请输入项目名称')
return false
}
if (planList.value.length === 0) {
Service.Msg('请至少添加一个计划')
return false
}
for (let i = 0; i < planList.value.length; i++) {
const item = planList.value[i]
if (!item.targetSeconds) {
Service.Msg(`计划 ${i + 1} 请输入目标时间`)
return false
}
const seconds = parseInt(item.targetSeconds) || 0
if (seconds === 0) {
Service.Msg(`计划 ${i + 1} 目标时间不能为0`)
return false
}
if (!item.restTime || parseInt(item.restTime) < 0) {
Service.Msg(`计划 ${i + 1} 请输入有效的休息时长`)
return false
}
if (!item.lapCount || parseInt(item.lapCount) <= 0) {
Service.Msg(`计划 ${i + 1} 请输入有效的圈数`)
return false
}
}
return true
}
// 保存
const handleSave = () => {
if (!validateForm()) {
return
}
let plan = planList.value.map(item => ({
target: (parseInt(item.targetMinutes) || 0) * 60 + (parseInt(item.targetSeconds) || 0),
rest: parseInt(item.restTime),
circle: parseInt(item.lapCount)
}))
// 整理数据
const data = {
planId: planId.value,
name: projectName.value,
planType: '混氧项目',
departType: '',
interval: '',
groupInt: '',
subsectionDistance: '',
subsectionInt: '',
users: '',
project: JSON.stringify(plan),
group: '',
}
console.log('保存的数据:', data)
PlanService.AddPlan(data).then(res => {
if (res.code == 0) {
Service.Msg('添加成功!')
setTimeout(() => {
Service.GoPageBack()
}, 1000)
} else {
Service.Msg(res.msg)
}
})
}
// 取消
const handleCancel = () => {
uni.showModal({
title: '确认取消',
content: '确定要取消吗?已输入的内容将不会保存。',
success: (res) => {
if (res.confirm) {
Service.GoPageBack()
}
}
})
}
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.add-hunyang-container {
min-height: 100vh;
padding-bottom: 140rpx;
}
/* 表单区域 */
.form-section {
padding: 20rpx;
}
.form-card {
background-color: #fff;
border-radius: 24rpx;
padding: 32rpx 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.form-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-bottom: 28rpx;
display: block;
}
}
/* 表单输入 */
.form-group {
margin-bottom: 0;
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 14rpx;
}
.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);
}
}
}
/* 计划总时长 */
.total-time-section {
margin-top: 28rpx;
padding-top: 24rpx;
border-top: 2rpx solid #f0f0f0;
display: flex;
justify-content: space-between;
align-items: center;
.total-time-label {
font-size: 28rpx;
font-weight: 600;
color: #666;
}
.total-time-value {
font-size: 36rpx;
font-weight: bold;
color: #1890ff;
font-family: 'DIN Alternate', 'Helvetica Neue', monospace;
letter-spacing: 2rpx;
background: linear-gradient(135deg, #e6f7ff 0%, #f0f9ff 100%);
padding: 12rpx 24rpx;
border-radius: 12rpx;
border: 2rpx solid #91d5ff;
}
}
/* 计划项 */
.plan-item {
background-color: #f9f9f9;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 20rpx;
border: 2rpx solid #f0f0f0;
.plan-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20rpx;
.plan-item-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.delete-plan-btn {
width: 44rpx;
height: 44rpx;
background: linear-gradient(135deg, #ff4d4f 0%, #d9363e 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(255, 77, 79, 0.3);
transition: all 0.2s ease;
&:active {
transform: scale(0.95);
}
}
}
.plan-item-content {
display: flex;
flex-direction: column;
gap: 20rpx;
}
}
/* 输入组 */
.input-group {
display: flex;
align-items: center;
justify-content: space-between;
.input-label {
font-size: 26rpx;
font-weight: 500;
color: #666;
min-width: 120rpx;
}
}
/* 时间选择器 */
.time-selector {
display: flex;
align-items: center;
gap: 8rpx;
flex: 1;
.time-input-wrapper {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 16rpx;
height: 72rpx;
flex: 1;
.time-input {
flex: 1;
font-size: 28rpx;
color: #333;
text-align: right;
}
.time-unit {
font-size: 24rpx;
color: #999;
margin-left: 8rpx;
}
}
.time-separator {
font-size: 32rpx;
font-weight: bold;
color: #333;
padding: 0 4rpx;
}
}
/* 休息选择器 */
.rest-selector,
.lap-selector {
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 16rpx;
height: 72rpx;
flex: 1;
.rest-input,
.lap-input {
flex: 1;
font-size: 28rpx;
color: #333;
text-align: right;
}
.time-unit {
font-size: 24rpx;
color: #999;
margin-left: 12rpx;
}
}
/* 添加计划按钮 */
.add-plan-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
padding: 24rpx 32rpx;
margin-top: 20rpx;
background: linear-gradient(135deg, #f0f9ff 0%, #e6f7ff 100%);
border-radius: 16rpx;
border: 2rpx dashed #91d5ff;
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
background: linear-gradient(135deg, #e6f7ff 0%, #d6f4ff 100%);
}
.add-plan-text {
font-size: 28rpx;
color: #1890ff;
font-weight: 500;
}
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 99;
.action-buttons {
display: flex;
gap: 16rpx;
}
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
border: none;
font-size: 30rpx;
font-weight: 600;
transition: all 0.25s ease;
display: flex;
align-items: center;
justify-content: center;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
&:active {
background-color: #e8e8e8;
transform: scale(0.96);
}
}
.confirm-btn {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.35);
&:active {
transform: scale(0.96);
box-shadow: 0 3rpx 8rpx rgba(24, 144, 255, 0.25);
}
}
</style>

View File

@@ -0,0 +1,693 @@
<template>
<view class="hunyang-container">
<!--
混氧训练页面主容器
包含头部信息倒计时装置底部控制按钮
-->
<!-- 项目信息头部 -->
<view class="card-section header-section" style="position: relative;">
<view class="header-content">
<!-- 训练项目名称 -->
<view class="project-name">{{ name }}</view>
<!-- 总训练时长显示 -->
<view class="total-duration">
<text class="duration-label">计划总时长:</text>
<!-- 格式化显示总时长 -->
<text class="duration-value">{{ formatTotalDuration(totalDuration) }}</text>
</view>
</view>
<view class="" style="position: absolute; top: 20rpx; right: 30rpx; ">
<up-icon @click="Service.GoPage('/pages/userFunc/addHunyang?id='+planId)" name="setting"
size="22"></up-icon>
</view>
</view>
<!-- 多个倒计时装置区域 -->
<view class="card-section timer-section">
<!-- 区域标题 -->
<view class="section-title">倒计时装置</view>
<view class="multi-timers-wrapper">
<!--
使用v-for循环渲染每个训练计划
:active类用于高亮当前正在执行的计划
-->
<view v-for="(plan, index) in plans" :key="plan.id" class="timer-item"
:class="{ active: currentPlanIndex === index }">
<view class="timer-horizontal">
<!--
训练状态显示区域
根据不同状态显示休息中/已完成/计划X
-->
<view class="timer-status" :class="{ resting: timerStates[index].isResting }">
<text v-if="timerStates[index].isResting" class="status-label">休息中</text>
<text v-else-if="timerStates[index].isCompleted" class="status-label">已完成</text>
<text v-else class="status-label">计划{{ index + 1 }}</text>
</view>
<!--
倒计时显示区域
根据状态显示训练时间或休息时间
-->
<view class="timer-display" :class="{ resting: timerStates[index].isResting }">
<text v-if="timerStates[index].isResting"
class="display-time">{{ formatRestCountdown(timerStates[index].restCountdown) }}</text>
<text v-else class="display-time">{{ formatCountdown(timerStates[index].countdown) }}</text>
</view>
<!--
完成圈数显示
显示已完成的圈数/总圈数
-->
<view class="timer-laps">
<text class="laps-text">{{ timerStates[index].completedLaps }}/{{ plan.circle }}</text>
</view>
</view>
<!-- 计划详细信息 -->
<view class="plan-info-bar">
<view class="info-item">
<text class="info-label">目标:</text>
<text class="info-value">{{ plan.target }}</text>
</view>
<view class="info-item">
<text class="info-label">休息:</text>
<text class="info-value">{{ plan.rest }}</text>
</view>
</view>
</view>
<!-- 空状态提示 -->
<view v-if="plans.length === 0" class="empty-timer">
<text class="empty-text">暂无训练计划请添加</text>
</view>
</view>
</view>
<!-- 底部控制按钮区域 -->
<view class="bottom-controls">
<view class="controls-inner">
<!--
开始按钮 - 仅在未运行时显示
点击开始所有训练计划
-->
<view v-if="!isRunning" class="control-btn start-btn" @click="startAll">
<u-icon name="play-circle" size="32" color="#fff"></u-icon>
<text class="btn-text">开始</text>
</view>
<!--
暂停按钮 - 仅在运行时显示
点击暂停所有训练
-->
<view v-else class="control-btn pause-btn" @click="pauseAll">
<u-icon name="pause-circle" size="32" color="#fff"></u-icon>
<text class="btn-text">暂停</text>
</view>
<!--
重置按钮
点击重置所有训练状态
-->
<view class="control-btn reset-btn" @click="resetAll">
<u-icon name="reload" size="32" color="#fff"></u-icon>
<text class="btn-text">重置</text>
</view>
</view>
</view>
<!-- 保存悬浮按钮 -->
<view class="save-float-btn" @click="saveData">
<text class="btn-text">保存</text>
</view>
</view>
</template>
<script setup lang="ts">
// 导入必要的Vue组合式API和项目服务
import { ref, computed, onUnmounted } from 'vue'
import { Service } from '@/Service/Service'
import { PlanService } from '@/Service/swimming/PlanService'
import { onLoad, onShow } from '@dcloudio/uni-app'
// 定义训练计划的数据结构接口
interface PlanItem {
id : string // 计划唯一标识
targetTime : number // 目标训练时间(秒)
restTime : number // 休息时间(秒)
lapCount : number // 训练圈数
}
// 定义计时器状态的数据结构接口
interface TimerState {
isRunning : boolean // 是否正在运行
isResting : boolean // 是否处于休息状态
isCompleted : boolean // 是否已完成
countdown : number // 倒计时(秒)
restCountdown : number // 休息倒计时(秒)
completedLaps : number // 已完成圈数
}
// 训练计划列表使用ref实现响应式
const plans = ref<Array<any>>([
])
// 每个计划的计时器状态与plans一一对应
const timerStates = ref<TimerState[]>([])
// 当前正在执行的训练计划索引
const currentPlanIndex = ref(0)
// 训练是否正在运行的状态
const isRunning = ref(false)
// 主计时器间隔ID用于控制全局计时
const mainInterval = ref<number | null>(null)
// 训练计划ID从页面参数获取
let planId = ref('')
let name = ref('')
// 页面加载时触发,获取训练计划信息
onLoad((data : any) => {
planId.value = data.id
})
onShow(() => {
getPlanInfo()
})
// 获取计划详情
// 通过PlanService获取指定ID的训练计划并初始化计时器状态
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) {
// 解析计划数据并初始化计时器状态
name.value = res.data.plan.name
plans.value = JSON.parse(res.data.plan.project)
initTimerStates()
} else {
// 显示错误信息
Service.Msg(res.msg)
}
})
}
// 初始化计时器状态
// 为每个计划创建对应的计时器状态对象,重置所有状态
const initTimerStates = () => {
timerStates.value = plans.value.map(() => ({
isRunning: false,
isResting: false,
isCompleted: false,
countdown: 0,
restCountdown: 0,
completedLaps: 0
}))
}
// 开始所有训练计划
// 检查是否有计划,如果没有则提示用户
// 设置运行状态并启动第一个计划
const startAll = () => {
if (plans.value.length === 0) {
Service.Msg('请先添加训练计划')
return
}
isRunning.value = true
// 启动指定索引的计划
const startPlan = (index : number) => {
// 如果索引超出范围,停止所有训练并显示完成消息
if (index >= plans.value.length) {
stopAll()
Service.Msg('所有计划训练完成!', 'success')
return
}
// 设置当前计划索引并获取对应的状态
currentPlanIndex.value = index
const plan = plans.value[index]
const state = timerStates.value[index]
// 设置计划为运行状态,并初始化倒计时
state.isRunning = true
if (state.countdown === 0 && !state.isResting) {
state.countdown = plan.target
}
}
// 启动当前计划
startPlan(currentPlanIndex.value)
// 设置主计时器每10毫秒更新一次
mainInterval.value = setInterval(() => {
const index = currentPlanIndex.value
// 如果索引超出范围,停止所有训练
if (index >= plans.value.length) {
stopAll()
return
}
const plan = plans.value[index]
const state = timerStates.value[index]
// 如果处于休息状态,更新休息倒计时
if (state.isResting) {
state.restCountdown -= 0.01
if (state.restCountdown <= 0) {
// 休息结束,重置休息状态并开始训练
state.isResting = false
state.restCountdown = 0
state.countdown = plan.target
}
} else {
// 训练状态,更新训练倒计时
state.countdown -= 0.01
if (state.countdown <= 0) {
// 训练完成一圈,增加完成圈数
state.completedLaps += 1
// 如果达到总圈数,标记为已完成并启动下一个计划
if (state.completedLaps >= plan.circle) {
state.isRunning = false
state.isCompleted = true
currentPlanIndex.value += 1
startPlan(currentPlanIndex.value)
} else {
// 否则进入休息状态
state.isResting = true
state.restCountdown = plan.rest
}
}
}
}, 10)
}
// 暂停所有训练
// 调用stopAll方法暂停训练
const pauseAll = () => {
stopAll()
}
// 停止所有训练
// 重置运行状态,清除主计时器
const stopAll = () => {
isRunning.value = false
// 重置所有计划的运行状态
timerStates.value.forEach(state => {
state.isRunning = false
})
// 清除主计时器
if (mainInterval.value) {
clearInterval(mainInterval.value)
mainInterval.value = null
}
}
// 重置所有训练状态
// 停止训练,重置当前计划索引,重置所有计时器状态
const resetAll = () => {
stopAll()
currentPlanIndex.value = 0
// 重置所有计时器状态
timerStates.value.forEach(state => {
state.isResting = false
state.isCompleted = false
state.countdown = 0
state.restCountdown = 0
state.completedLaps = 0
})
}
// 格式化倒计时显示
// 将秒数格式化为"分:秒"格式
const formatCountdown = (seconds : number) : string => {
const totalSeconds = Math.max(0, Math.floor(seconds))
const minutes = Math.floor(totalSeconds / 60)
const secs = totalSeconds % 60
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
// 格式化休息倒计时显示
// 将秒数格式化为"分:秒"格式
const formatRestCountdown = (seconds : number) : string => {
const totalSeconds = Math.max(0, Math.floor(seconds))
const minutes = Math.floor(totalSeconds / 60)
const secs = totalSeconds % 60
return `${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
// 格式化总时长显示
// 将秒数格式化为"分:秒"格式
const formatTotalDuration = (seconds : number) : string => {
if (seconds === 0) return '0分0秒'
const minutes = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${minutes}${secs}`
}
// 计算总训练时长
// 根据所有计划的训练时间和休息时间计算总时长
const totalDuration = computed(() => {
return plans.value.reduce((total, plan) => {
const planTime = plan.target * plan.circle
const restTime = plan.rest * (plan.circle - 1)
return total + planTime + restTime
}, 0)
})
// 保存数据
const saveData = () => {
if (plans.value.length === 0) {
Service.Msg('没有可保存的训练计划')
return
}
// 构建要保存的数据
const saveData = {
name: name.value,
project: JSON.stringify(plans.value),
totalDuration: totalDuration.value
}
}
// 页面卸载时触发
// 确保停止所有训练,避免内存泄漏
onUnmounted(() => {
stopAll()
})
// 格式化总时长显示
// 将秒数格式化为"小时:分:秒"或"分:秒"或"秒"格式
// 页面卸载时触发
// 确保停止所有训练,避免内存泄漏
onUnmounted(() => {
stopAll()
})
</script>
<style lang="scss" scoped>
.hunyang-container {
min-height: 100vh;
background-color: #f8f9fa;
padding: 20rpx;
padding-bottom: 240rpx;
}
.card-section {
background-color: #ffffff;
border-radius: 12rpx;
padding: 24rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
}
.header-section {
background: #ffffff;
color: #333333;
}
.header-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 12rpx;
}
.project-name {
font-size: 36rpx;
font-weight: 600;
letter-spacing: 2rpx;
}
.total-duration {
display: flex;
align-items: center;
gap: 8rpx;
background: #f0f0f0;
padding: 12rpx 20rpx;
border-radius: 20rpx;
}
.duration-label {
font-size: 24rpx;
color: #666666;
}
.duration-value {
font-size: 28rpx;
font-weight: 500;
}
.section-title {
font-size: 28rpx;
font-weight: 500;
color: #333333;
margin-bottom: 20rpx;
position: relative;
padding-left: 16rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 4rpx;
height: 20rpx;
background: #4a90e2;
border-radius: 2rpx;
}
}
.empty-timer {
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 20rpx;
.empty-text {
font-size: 24rpx;
color: #999999;
}
}
.timer-section {
background: #ffffff;
}
.multi-timers-wrapper {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.timer-item {
background: #ffffff;
border-radius: 12rpx;
padding: 20rpx;
border: 1rpx solid #e0e0e0;
transition: all 0.2s ease;
&.active {
border-color: #4a90e2;
box-shadow: 0 0 0 2rpx rgba(74, 144, 226, 0.1);
}
&:active {
transform: scale(0.99);
}
}
.timer-horizontal {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
margin-bottom: 12rpx;
gap: 12rpx;
}
.timer-status {
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx 16rpx;
border-radius: 8rpx;
background: #e6f0ff;
border: 1rpx solid #d0e3ff;
min-width: 100rpx;
&.resting {
background: #fff8e6;
border-color: #ffe0b2;
}
.status-label {
font-size: 22rpx;
font-weight: 500;
color: #4a90e2;
}
&.resting .status-label {
color: #ff9800;
}
}
.timer-display {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.display-time {
font-size: 56rpx;
font-weight: bold;
font-family: 'Helvetica Neue', monospace;
color: #4a90e2;
letter-spacing: 1rpx;
}
&.resting .display-time {
color: #ff9800;
animation: countdownFlash 0.5s ease-in-out infinite;
}
}
.timer-laps {
display: flex;
align-items: center;
justify-content: center;
padding: 8rpx 16rpx;
background: #f5f5f5;
border-radius: 8rpx;
min-width: 100rpx;
.laps-text {
font-size: 22rpx;
color: #666666;
font-weight: 400;
}
}
.bottom-controls {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #ffffff;
padding: 16rpx 24rpx 24rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
z-index: 100;
border-top: 1rpx solid #e0e0e0;
}
.controls-inner {
display: flex;
justify-content: center;
align-items: center;
gap: 20rpx;
max-width: 750rpx;
margin: 0 auto;
}
.control-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 8rpx;
padding: 20rpx 16rpx;
border-radius: 8rpx;
transition: all 0.2s ease;
flex: 1;
}
.control-btn:active {
transform: scale(0.98);
}
.btn-text {
font-size: 22rpx;
font-weight: 500;
color: #ffffff;
}
.start-btn {
background-color: #4caf50;
}
.pause-btn {
background-color: #ff9800;
}
.reset-btn {
background-color: #2196f3;
}
.plan-info-bar {
display: flex;
justify-content: center;
gap: 30rpx;
padding-top: 10rpx;
border-top: 1rpx dashed #e0e0e0;
.info-item {
display: flex;
align-items: center;
gap: 6rpx;
}
.info-label {
font-size: 20rpx;
color: #666666;
}
.info-value {
font-size: 20rpx;
color: #333333;
font-weight: 500;
}
}
@keyframes countdownFlash {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.7;
}
}
/* 保存悬浮按钮样式 */
.save-float-btn {
position: fixed;
bottom: 240rpx;
right: 30rpx;
background-color: #2196f3;
padding: 40rpx;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(33, 150, 243, 0.3);
z-index: 101;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.save-float-btn:active {
transform: scale(0.95);
}
.save-float-btn .btn-text {
font-size: 20rpx;
}
</style>

394
src/pages/userFunc/plan.vue Normal file
View File

@@ -0,0 +1,394 @@
<template>
<view class="plan-container">
<!-- 训练计划列表 -->
<view class="plan-list-section">
<view class="section-header">
<text class="section-title">我的训练计划</text>
<text class="plan-count">{{ trainingPlans.length }}个计划</text>
</view>
<!-- 计划列表 -->
<view class="plan-list">
<view v-for="(plan, index) in trainingPlans" :key="plan.id" class="plan-card">
<!-- 计划信息 -->
<view class="plan-info">
<view class="plan-icon">
<view class="icon-circle">
<u-icon name="calendar" size="24" color="#fff"></u-icon>
</view>
</view>
<view class="plan-detail">
<text class="plan-name">{{ plan.name }}</text>
<view class="plan-meta">
<view class="meta-tag">
<text class="tag-text">{{ plan.laps }}</text>
</view>
<view class="meta-tag">
<text class="tag-text">混氧</text>
</view>
</view>
</view>
</view>
<!-- 删除按钮 -->
<view class="delete-btn">
<u-icon name="trash" size="20" color="#ff4d4f"></u-icon>
</view>
</view>
<!-- 空状态 -->
<view v-if="trainingPlans.length === 0" class="empty-state">
<view class="empty-icon">
<u-icon name="calendar" size="80" color="#d9d9d9"></u-icon>
</view>
<text class="empty-text">暂无训练计划</text>
<text class="empty-desc">点击下方按钮添加第一个训练计划</text>
</view>
</view>
</view>
<!-- 底部添加计划按钮 -->
<view class="bottom-section">
<view class="add-plan-btn" @click="Service.GoPage('/pages/userFunc/addplan')">
<u-icon name="plus" size="24" color="#fff"></u-icon>
<text class="add-btn-text">添加训练计划</text>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Service } from '@/Service/Service'
interface TrainingPlan {
id : string
name : string
laps : string
}
const trainingPlans = ref<TrainingPlan[]>([
{
id: '1',
name: '自由泳500米基础训练',
laps: '10'
},
{
id: '2',
name: '蛙泳200米进阶训练',
laps: '4'
},
{
id: '3',
name: '混合泳1000米挑战',
laps: '20'
},
{
id: '4',
name: '蝶泳400米专项训练',
laps: '8'
}
])
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.plan-container {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 180rpx;
}
/* 计划列表区域 */
.plan-list-section {
padding: 20rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
padding: 0 8rpx;
}
.section-title {
font-size: 34rpx;
font-weight: 700;
color: #333;
position: relative;
padding-left: 16rpx;
/* 标题左侧装饰条 */
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 6rpx;
height: 32rpx;
background: linear-gradient(180deg, #1890ff 0%, #096dd9 100%);
border-radius: 3rpx;
}
}
.plan-count {
font-size: 26rpx;
color: #1890ff;
font-weight: 600;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(24, 144, 255, 0.05) 100%);
padding: 8rpx 20rpx;
border-radius: 20rpx;
}
/* 计划列表 */
.plan-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
/* 单个计划卡片 - 美化版 */
.plan-card {
background: linear-gradient(135deg, #fff 0%, #fafbfc 100%);
border-radius: 28rpx;
padding: 28rpx 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08), 0 0 0 1rpx rgba(0, 0, 0, 0.04) inset;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
/* 卡片背景装饰 */
&::before {
content: '';
position: absolute;
top: 0;
right: 0;
width: 120rpx;
height: 120rpx;
background: radial-gradient(circle, rgba(24, 144, 255, 0.08) 0%, transparent 70%);
pointer-events: none;
}
&:active {
transform: scale(0.98) translateY(2rpx);
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
}
}
/* 左侧序号 */
.plan-index {
flex-shrink: 0;
width: 56rpx;
height: 56rpx;
border-radius: 50%;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
display: flex;
align-items: center;
justify-content: center;
margin-right: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.3);
}
.index-text {
font-size: 26rpx;
font-weight: 700;
color: #fff;
}
/* 计划信息区域 */
.plan-info {
display: flex;
align-items: center;
gap: 20rpx;
flex: 1;
min-width: 0;
}
.plan-icon {
flex-shrink: 0;
}
.icon-circle {
width: 80rpx;
height: 80rpx;
border-radius: 20rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #096dd9 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(24, 144, 255, 0.25);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 8rpx;
right: 8rpx;
width: 16rpx;
height: 16rpx;
background: rgba(255, 255, 255, 0.3);
border-radius: 50%;
}
}
.plan-detail {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 12rpx;
}
.plan-name {
font-size: 32rpx;
font-weight: 700;
color: #333;
line-height: 1.3;
background: linear-gradient(135deg, #333 0%, #666 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.plan-meta {
display: flex;
align-items: center;
gap: 12rpx;
}
.meta-tag {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
background: linear-gradient(135deg, rgba(24, 144, 255, 0.1) 0%, rgba(24, 144, 255, 0.05) 100%);
border-radius: 16rpx;
border: 1rpx solid rgba(24, 144, 255, 0.2);
}
.tag-text {
font-size: 24rpx;
color: #1890ff;
font-weight: 600;
}
/* 删除按钮 - 美化版 */
.delete-btn {
width: 72rpx;
height: 72rpx;
border-radius: 50%;
background: linear-gradient(135deg, rgba(255, 77, 79, 0.1) 0%, rgba(255, 77, 79, 0.05) 100%);
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
border: 2rpx solid rgba(255, 77, 79, 0.2);
&:active {
background: linear-gradient(135deg, #ff4d4f 0%, #d9363e 100%);
transform: scale(0.9);
box-shadow: 0 4rpx 12rpx rgba(255, 77, 79, 0.4);
border-color: transparent;
u-icon {
color: #fff;
}
}
}
/* 空状态 */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140rpx 40rpx;
gap: 24rpx;
}
.empty-icon {
margin-bottom: 20rpx;
opacity: 0.6;
}
.empty-text {
font-size: 32rpx;
color: #8c8c8c;
font-weight: 600;
}
.empty-desc {
font-size: 26rpx;
color: #bfbfbf;
}
/* 底部添加计划按钮 */
.bottom-section {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, transparent 0%, rgba(245, 245, 245, 0.95) 20%, #f5f5f5 100%);
padding: 24rpx 20rpx 40rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
z-index: 100;
}
.add-plan-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
width: 100%;
height: 100rpx;
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 50%, #36cfc9 100%);
background-size: 200% 200%;
border-radius: 24rpx;
box-shadow: 0 12rpx 32rpx rgba(24, 144, 255, 0.35), 0 0 0 1rpx rgba(255, 255, 255, 0.5) inset;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
/* 光泽效果 */
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.4), transparent);
animation: shimmer 3s infinite;
}
&:active {
transform: scale(0.96);
box-shadow: 0 6rpx 20rpx rgba(24, 144, 255, 0.25);
}
}
.add-btn-text {
font-size: 34rpx;
color: #fff;
font-weight: 700;
letter-spacing: 2rpx;
position: relative;
z-index: 1;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -19,7 +19,7 @@
</view>
<view v-else class="projects-list">
<view v-for="(project, index) in projects" :key="project.id" class="project-card"
<view v-for="(project, index) in projects" :key="project.planId" class="project-card"
@click="viewProjectDetail(project)">
<view class="project-delete" @click.stop="deleteProject(project, index)">
<u-icon name="trash" size="18" color="#ff4d4f"></u-icon>
@@ -35,7 +35,7 @@
<view class="project-stats">
<view class="stat-badge">
<u-icon name="account" size="14" color="#1890ff"></u-icon>
<text class="badge-text">{{ project.studentCount }}位学员</text>
<text class="badge-text">{{ project.users.length }}位学员</text>
</view>
</view>
</view>
@@ -46,7 +46,7 @@
</view>
<view class="project-time">
<u-icon name="clock" size="14" color="#999"></u-icon>
<text class="time-text">{{ project.createTime }}</text>
<text class="time-text">{{ Service.formatDate(project.addTime,1) }}</text>
</view>
</view>
@@ -57,69 +57,25 @@
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app"
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { PlanService } from '@/Service/swimming/PlanService'
import { ref } from "vue"
// 定义项目类型
interface Project {
id : string
name : string
mode : '计时' | '包干' | '分段'
createTime : string
studentCount : number
recordCount : number
}
// 分页相关
let page = ref(1)
let status = ref('loadmore')
// 项目列表数据
const projects = ref<Project[]>([
{
id: '001',
name: '自由泳50米',
mode: '计时',
createTime: '2024-01-15 14:30',
studentCount: 8,
recordCount: 24
},
{
id: '002',
name: '蛙泳100米',
mode: '包干',
createTime: '2024-01-14 10:15',
studentCount: 6,
recordCount: 18
},
{
id: '003',
name: '仰泳200米',
mode: '分段',
createTime: '2024-01-13 16:45',
studentCount: 5,
recordCount: 20
},
{
id: '004',
name: '蝶泳50米',
mode: '计时',
createTime: '2024-01-12 09:20',
studentCount: 4,
recordCount: 12
},
{
id: '005',
name: '混合泳100米',
mode: '包干',
createTime: '2024-01-11 15:00',
studentCount: 7,
recordCount: 21
}
])
const projects = ref<Array<any>>([])
onLoad(() => {
getData()
})
onShow(() => {
onReachBottom(() => {
getList()
})
// 获取模式对应的样式类
@@ -133,15 +89,46 @@
}
// 查看项目详情
const viewProjectDetail = (project : Project) => {
const viewProjectDetail = (project : any) => {
Service.Msg(`查看「${project.name}」详情`)
}
// 删除项目
const deleteProject = (project : Project, index : number) => {
const deleteProject = (project : any, index : number) => {
Service.Confirm(`确定要删除「${project.name}」吗?`, () => {
projects.value.splice(index, 1)
Service.Msg('删除成功')
PlanService.DeletePlan(project.planId).then(res => {
if (res.code == 0) {
getData()
Service.Msg('删除成功')
} else {
Service.Msg(res.msg)
}
})
})
}
// 获取项目列表数据
const getData = () => {
projects.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
// 获取项目列表
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
PlanService.GetPlanList( '', page.value.toString()).then(res => {
if (res.code == 0) {
projects.value = [...projects.value, ...res.data]
status.value = res.data.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}

View File

@@ -1,20 +1,24 @@
<template>
<view class="segmentation-container">
<!-- 配置区域 -->
<view class="config-section">
<view class="config-header">
<text class="config-title">分段设置</text>
<u-icon @click="Service.GoPage('/pages/userFunc/setCourse?id='+planId+'&type=2')" name="setting" size="24"
color="#1890ff"></u-icon>
</view>
<view class="config-info">
<text class="info-text">总距离: {{ totalDistance }} ({{ segmentCount }} × {{ segmentDistance }})</text>
</view>
</view>
<!-- 总计时器区域 -->
<view class="total-time-section">
<view class="timer-circle">
<view class="circle-content">
<view class="timer-sound">
<text class="sound-text">计时器</text>
</view>
<view class="timer-time">{{ formatTime(currentTime) }}</view>
<view class="timer-duration">{{ formatDuration(currentTime) }}</view>
</view>
<view class="timer-controls">
<u-icon name="reload" size="32" color="#22a6f2" @click="resetAll"></u-icon>
<u-icon :name="isRunning ? 'pause-circle' : 'play-circle'" size="32" color="#22a6f2"
@click="toggleTimer"></u-icon>
</view>
<view class="timer-bar">
<view class="timer-time">{{ formatTime(currentTime) }}</view>
</view>
</view>
@@ -26,96 +30,180 @@
</view>
<view class="students-list">
<view v-for="(student, index) in students" :key="student.id" class="student-item">
<view v-for="(student, index) in students" :key="student.id" class="student-item"
:class="{ 'not-started': !student.hasStarted }">
<view class="student-header">
<view class="student-number">{{ student.number }}</view>
<view class="student-number" :class="{ 'started': student.hasStarted }">{{ student.number }}
</view>
<view class="student-info">
<text class="student-name">{{ student.name }}</text>
<text class="student-lane">{{ student.lane }}</text>
<!-- <view class="start-status" v-if="startMode === 1 && !student.hasStarted">
<text class="status-text">等待</text>
</view> -->
<text class="total-time-text"
v-if="student.segments.length > 0">{{ formatSimpleTime(getLastSegmentTime(student)) }}</text>
<view class="record-badge"
:class="{ 'completed': student.segments.length >= Number(segmentCount) }">
<text class="badge-text">{{ student.segments.length }}/{{ segmentCount }}</text>
</view>
</view>
<view class="student-buttons">
<button class="record-btn" @click="recordStudentSegment(student)">
<u-icon name="checkmark" size="18" color="#fff"></u-icon>
<button class="record-btn" :disabled="!student.hasStarted"
@click="recordStudentSegment(student)">
<text class="btn-text">记录</text>
</button>
<button class="view-btn" @click="showStudentRecords(student)">
<text class="btn-text">详情</text>
</button>
<button class="reset-btn" @click="resetStudent(student)">
<u-icon name="reload" size="18" color="#fff"></u-icon>
<text class="btn-text">重置</text>
</button>
</view>
</view>
<!-- 分段时间块 -->
<view class="segments-list">
<view v-for="(segment, segIndex) in student.segments" :key="segIndex" class="segment-item">
<text class="segment-label">{{ segIndex + 1 }}</text>
<text class="segment-time">{{ segment.time ? formatTime(segment.time) : '--:--.--' }}</text>
</view>
<view v-if="student.segments.length<4" class="segment-item empty">
<text class="segment-label">{{ student.segments.length + 1 }}</text>
<text class="segment-time">--:--.--</text>
</view>
</view>
</view>
</view>
</view>
<!-- 保存按钮 -->
<view class="save-btn-wrapper">
<button class="save-btn" @click="saveData">
<text class="btn-text">保存</text>
</button>
<!-- 底部操作栏 -->
<view class="bottom-actions">
<view class="action-btn reset-action" @click="resetAll">
<u-icon name="reload" size="24" color="#fff"></u-icon>
<text class="action-text">重置</text>
</view>
<view class="action-btn timer-action" :class="{ 'running': isRunning }" @click="toggleTimer">
<u-icon :name="isRunning ? 'pause-circle' : 'play-circle'" size="28" color="#fff"></u-icon>
<text class="action-text">{{ isRunning ? '暂停' : '开始' }}</text>
</view>
<view class="action-btn save-action" @click="saveData">
<u-icon name="checkmark" size="24" color="#fff"></u-icon>
<text class="action-text">保存</text>
</view>
</view>
<!-- 记录弹窗 -->
<u-popup :show="showRecordPopup" mode="bottom" :round="20" @close="closeRecordPopup">
<view class="record-popup">
<view class="popup-header">
<text class="popup-title">{{ currentStudent?.name }} - 分段记录</text>
<view class="close-btn" @click="closeRecordPopup">
<u-icon name="close" size="24" color="#999"></u-icon>
</view>
</view>
<view class="popup-summary">
<view class="summary-item">
<text class="summary-label">总距离</text>
<text class="summary-value">{{ totalDistance }}</text>
</view>
<view class="summary-item">
<text class="summary-label">已记录</text>
<text
class="summary-value">{{ currentStudent?.segments?.length || 0 }}/{{ segmentCount }}</text>
</view>
</view>
<view class="record-list"
v-if="currentStudent && currentStudent.segments && currentStudent.segments.length > 0">
<view class="record-header">
<text class="record-cell header-cell">分段</text>
<text class="record-cell header-cell">距离</text>
<text class="record-cell header-cell">累计时间</text>
<text class="record-cell header-cell">分段用时</text>
</view>
<view v-for="(segment, index) in currentStudent.segments" :key="index" class="record-item">
<text class="record-cell">{{ index + 1 }}</text>
<text class="record-cell">{{ segmentDistance }}</text>
<text class="record-cell time-cell">{{ formatTime(segment.time) }}</text>
<text class="record-cell time-cell">{{ formatTimeDiff(index, segment.time) }}</text>
</view>
</view>
<view class="empty-record" v-else>
<u-icon name="info-circle" size="48" color="#ccc"></u-icon>
<text class="empty-text">暂无记录数据</text>
</view>
<view class="popup-footer">
<button class="popup-btn close-popup" @click="closeRecordPopup">关闭</button>
</view>
</view>
</u-popup>
</view>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue'
import { ref, onUnmounted, computed } from 'vue'
import { Service } from '@/Service/Service'
import { onLoad, onShow } from '@dcloudio/uni-app'
import { PlanService } from '@/Service/swimming/PlanService'
// 分段配置
const segmentDistance = ref(50)
const segmentCount = ref(4)
// 出发模式: 0-一起出发, 1-间隔出发
const startMode = ref(0)
// 间隔时间(秒)
const intervalTime = ref(10)
// 最大分段数
const maxSegments = 4
// 计算总距离
const totalDistance = computed(() => {
return Number(segmentDistance.value) * Number(segmentCount.value)
})
// 学生列表
const students = ref<Array<any>>([
{
id: '1',
number: '01',
name: '张三',
lane: '第一泳道',
segments: []
},
{
id: '2',
number: '02',
name: '李四',
lane: '第二泳道',
segments: []
},
{
id: '3',
number: '03',
name: '王五',
lane: '第三泳道',
segments: []
},
{
id: '4',
number: '04',
name: '赵六',
lane: '第四泳道',
segments: []
}
])
const students = ref<Array<any>>([])
// 弹窗状态
const showRecordPopup = ref(false)
const currentStudent = ref<any>(null)
// 计时器状态
const isRunning = ref(false)
const currentTime = ref(0)
let timerInterval : number | null = null
let startTime : number = 0
let intervalStartTimer : number | null = null
let planId = ref('')
onLoad((data : any) => {
planId.value = data.id
})
onShow(()=>{
getPlanInfo()
})
// 获取计划详情
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) {
// planName.value = res.data.plan.name
segmentDistance.value=res.data.plan.subsectionDistance/res.data.plan.subsectionInt
segmentCount.value=res.data.plan.subsectionInt
// 将计划数据转换为选手数据
// athletes.value = res.data.plan.users.
students.value=res.data.plan.users.map((item:any,index:any)=>{
return {
id: item.studentId,
number: index+1,
name: item.name,
segments: [],
hasStarted: res.data.plan.departType == '间隔出发'?false:true,
startTime: 0
}
})
startMode.value = res.data.plan.departType == '间隔出发' ? 1 : 0
intervalTime.value = res.data.plan.interval
} else {
Service.Msg(res.msg)
}
})
}
// 格式化时间显示
const formatTime = (seconds : number) : string => {
@@ -142,6 +230,34 @@
return result
}
// 格式化时间差
const formatTimeDiff = (index : number, currentTime : number) : string => {
if (index === 0) {
return '00:00:00'
}
if (!currentStudent.value || !currentStudent.value.segments[index - 1]) {
return '00:00:00'
}
const prevTime = currentStudent.value.segments[index - 1].time
const diff = currentTime - prevTime
return formatTime(diff)
}
// 获取学生最后一次记录的累计时间
const getLastSegmentTime = (student : any) : number => {
if (!student.segments || student.segments.length === 0) {
return 0
}
return student.segments[student.segments.length - 1].time
}
// 简化时间格式化(分:秒)
const formatSimpleTime = (seconds : number) : string => {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
// 开始计时
const startTimer = () => {
if (isRunning.value) return
@@ -153,6 +269,32 @@
const elapsed = (Date.now() - startTime) / 1000
currentTime.value = elapsed
}, 10)
// 间隔出发模式
if (startMode.value === 1) {
// 第一个学生立即出发
if (students.value.length > 0) {
students.value[0].hasStarted = true
students.value[0].startTime = 0
}
// 设置间隔出发定时器
const interval = intervalTime.value * 1000
let startedCount = 1
intervalStartTimer = setInterval(() => {
if (startedCount < students.value.length) {
const student = students.value[startedCount]
student.hasStarted = true
student.startTime = (Date.now() - startTime) / 1000
startedCount++
Service.Msg(`${student.name} 已出发`)
} else {
clearInterval(intervalStartTimer as number)
intervalStartTimer = null
}
}, interval)
}
}
// 停止计时
@@ -164,6 +306,10 @@
clearInterval(timerInterval)
timerInterval = null
}
if (intervalStartTimer) {
clearInterval(intervalStartTimer)
intervalStartTimer = null
}
}
// 切换计时器状态
@@ -176,27 +322,42 @@
}
// 记录学生分段
const recordStudentSegment = (student : Student) => {
const recordStudentSegment = (student : any) => {
console.log(22222);
if (!isRunning.value) {
Service.Msg('请先开始计时')
return
}
if (student.segments.length >= maxSegments) {
Service.Msg('已达到最大分段数')
if (!student.hasStarted) {
Service.Msg('该学生尚未出发')
return
}
const maxSeg = Number(segmentCount.value)
if (student.segments.length >= maxSeg) {
Service.Msg(`已达到最大分段数(${maxSeg}段)`)
return
}
let recordTime = currentTime.value
// 间隔出发模式下,使用学生自己的开始时间
if (startMode.value === 1) {
recordTime = currentTime.value - student.startTime
}
student.segments.push({
time: currentTime.value
time: recordTime
})
Service.Msg(`${student.name}${student.segments.length}段已记录`)
}
// 重置学生
const resetStudent = (student : Student) => {
const resetStudent = (student : any) => {
student.segments = []
student.hasStarted = false
student.startTime = 0
Service.Msg(`${student.name} 已重置`)
}
@@ -206,6 +367,8 @@
currentTime.value = 0
students.value.forEach(student => {
student.segments = []
student.hasStarted = startMode.value == 1?false:true,
student.startTime = 0
})
Service.Msg('已全部重置')
}
@@ -228,11 +391,26 @@
}, 1000)
}
// 显示学生记录弹窗
const showStudentRecords = (student : any) => {
currentStudent.value = student
showRecordPopup.value = true
}
// 关闭记录弹窗
const closeRecordPopup = () => {
showRecordPopup.value = false
currentStudent.value = null
}
// 页面卸载时清理计时器
onUnmounted(() => {
if (timerInterval) {
clearInterval(timerInterval)
}
if (intervalStartTimer) {
clearInterval(intervalStartTimer)
}
})
</script>
@@ -241,83 +419,65 @@
min-height: 100vh;
background-color: #f6f6f6;
padding: 20rpx 20rpx;
padding-bottom: 200rpx;
padding-bottom: 180rpx;
}
/* 配置区域 */
.config-section {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
.config-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
.config-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
.config-info {
background-color: #e6f7ff;
border-radius: 8rpx;
padding: 16rpx 20rpx;
.info-text {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
}
}
/* 总计时器区域 */
.total-time-section {
margin-top: 20rpx;
margin-bottom: 40rpx;
display: flex;
justify-content: center;
margin-bottom: 30rpx;
}
.timer-circle {
width: 350rpx;
height: 360rpx;
border-radius: 20rpx;
.timer-bar {
background-color: #fff;
border-radius: 20rpx;
padding: 30rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
display: flex;
padding: 20rpx;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
box-shadow: 0 4rpx 16rpx rgba(250, 140, 22, 0.3);
.circle-content {
// background-color: #faad14;
padding: 30rpx;
border-radius: 50%;
border: 16rpx solid #faad14;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16rpx;
}
.timer-sound {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
background-color: rgba(255, 255, 255, 0.2);
border-radius: 20rpx;
.sound-text {
font-size: 24rpx;
font-weight: 500;
}
}
.timer-time {
font-size: 48rpx;
font-size: 80rpx;
font-weight: bold;
}
.timer-duration {
font-size: 24rpx;
opacity: 0.9;
background-color: #f6f6f6;
padding: 4rpx 12rpx;
border-radius: 4rpx;
}
.timer-controls {
display: flex;
justify-content: space-between;
align-items: center;
width: 280rpx;
u-icon {
cursor: pointer;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
}
color: #ff4d4f;
font-family: 'DIN Alternate', monospace;
}
}
@@ -354,10 +514,13 @@
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
&.not-started {
opacity: 0.6;
}
.student-header {
display: flex;
align-items: center;
margin-bottom: 24rpx;
.student-number {
width: 56rpx;
@@ -371,22 +534,62 @@
align-items: center;
justify-content: center;
margin-right: 20rpx;
&.started {
background-color: #52c41a;
color: #fff;
}
}
.student-info {
flex: 1;
display: flex;
align-items: center;
gap: 12rpx;
.student-name {
font-size: 30rpx;
font-weight: 500;
color: #333;
margin-bottom: 4rpx;
display: block;
}
.student-lane {
.start-status {
background-color: #faad14;
border-radius: 8rpx;
padding: 4rpx 12rpx;
.status-text {
font-size: 22rpx;
color: #fff;
font-weight: 500;
}
}
.total-time-text {
font-size: 24rpx;
color: #999;
font-family: 'DIN Alternate', monospace;
font-weight: 500;
}
.record-badge {
background-color: #f0f0f0;
border-radius: 12rpx;
padding: 4rpx 12rpx;
.badge-text {
font-size: 22rpx;
color: #999;
font-weight: 500;
}
&.completed {
background-color: #52c41a;
.badge-text {
color: #fff;
}
}
}
}
@@ -395,6 +598,7 @@
gap: 12rpx;
.record-btn,
.view-btn,
.reset-btn {
height: 56rpx;
padding: 0 20rpx;
@@ -415,6 +619,15 @@
.record-btn {
background-color: #52c41a;
&:disabled {
background-color: #d9d9d9;
opacity: 0.6;
}
}
.view-btn {
background-color: #1890ff;
}
.reset-btn {
@@ -422,91 +635,201 @@
}
}
}
}
}
}
/* 分段时间列表 */
.segments-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
/* 记录弹窗 */
.record-popup {
background-color: #fff;
border-radius: 20rpx 20rpx 0 0;
max-height: 80vh;
overflow: hidden;
display: flex;
flex-direction: column;
.segment-item {
flex: 1;
min-width: 140rpx;
background-color: #f5f5f5;
border-radius: 12rpx;
padding: 20rpx 16rpx;
text-align: center;
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
&.empty {
opacity: 0.5;
}
.popup-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.segment-label {
font-size: 22rpx;
color: #666;
display: block;
margin-bottom: 8rpx;
}
.close-btn {
padding: 10rpx;
cursor: pointer;
}
}
.segment-time {
font-size: 26rpx;
font-weight: 600;
color: #333;
font-family: 'DIN Alternate', monospace;
}
.popup-summary {
display: flex;
gap: 20rpx;
padding: 20rpx 30rpx;
background-color: #f6f6f6;
.summary-item {
flex: 1;
background-color: #fff;
border-radius: 12rpx;
padding: 20rpx;
text-align: center;
.summary-label {
font-size: 24rpx;
color: #999;
display: block;
margin-bottom: 8rpx;
}
.summary-value {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
}
}
.record-list {
flex: 1;
overflow-y: auto;
padding: 0 30rpx 20rpx;
.record-header {
display: flex;
background-color: #f5f5f5;
border-radius: 8rpx;
margin-bottom: 16rpx;
margin-top: 20rpx;
.record-cell {
flex: 1;
text-align: center;
font-size: 26rpx;
padding: 16rpx 8rpx;
color: #666;
}
.header-cell {
font-weight: 600;
color: #333;
}
}
.record-item {
display: flex;
background-color: #fff;
border-radius: 8rpx;
margin-bottom: 12rpx;
border: 1rpx solid #f0f0f0;
.record-cell {
flex: 1;
text-align: center;
font-size: 26rpx;
padding: 20rpx 8rpx;
color: #333;
&.time-cell {
font-family: 'DIN Alternate', monospace;
font-weight: 600;
color: #1890ff;
}
}
}
}
}
/* 保存按钮 */
.save-btn-wrapper {
position: fixed;
bottom: 40rpx;
right: 30rpx;
z-index: 100;
.save-btn {
width: 140rpx;
height: 140rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 50%;
border: none;
.empty-record {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8rpx;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.5),
0 4rpx 12rpx rgba(24, 144, 255, 0.3);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
padding: 80rpx 0;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.2) 0%, transparent 50%);
pointer-events: none;
.empty-text {
font-size: 28rpx;
color: #999;
margin-top: 20rpx;
}
}
.popup-footer {
padding: 20rpx 30rpx 40rpx;
border-top: 1rpx solid #f0f0f0;
.popup-btn {
height: 80rpx;
border-radius: 12rpx;
border: none;
font-size: 30rpx;
font-weight: 500;
display: flex;
align-items: center;
justify-content: center;
}
&:active {
transform: scale(0.95);
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.4),
0 2rpx 6rpx rgba(24, 144, 255, 0.2);
}
.btn-text {
font-size: 26rpx;
color: #fff;
font-weight: 600;
letter-spacing: 1rpx;
.close-popup {
background-color: #f5f5f5;
color: #666;
}
}
}
/* 底部操作栏 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx 40rpx;
display: flex;
gap: 20rpx;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 100;
.action-btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
cursor: pointer;
transition: all 0.2s ease;
.action-text {
font-size: 28rpx;
color: #fff;
font-weight: 500;
}
&:active {
transform: scale(0.98);
}
}
.reset-action {
background-color: #faad14;
}
.timer-action {
background-color: #52c41a;
flex: 1.5;
&.running {
background-color: #ff4d4f;
}
}
.save-action {
background-color: #1890ff;
}
}
</style>

View File

@@ -8,7 +8,7 @@
<view class="avatar-section">
<view class="avatar-wrapper">
<view class="avatar-circle">
<u-icon name="account" size="64" color="#fff"></u-icon>
<img :src="Service.GetMateUrlByImg(userInfo.headImg)" alt="" style="width: 100%; height: 100%;" />
</view>
<view class="avatar-edit" @click="changeAvatar">
<u-icon name="camera" size="20" color="#fff"></u-icon>
@@ -67,24 +67,32 @@
import { onShow, onLoad } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { ref } from 'vue'
// 用户信息
interface UserInfo {
name: string
phone: string
}
const userInfo = ref<UserInfo>({
name: '游泳爱好者',
phone: '13888888888'
import { userService } from '@/Service/swimming/userService'
const userInfo = ref<any>({
headImg:'',
name: '',
phone: ''
})
onLoad(() => {
// 可以在这里从本地存储加载用户信息
loadUserInfo()
})
onShow(() => {
})
const loadUserInfo = () => {
userService.GetUserInfo().then((content) => {
if (content.code == 0) {
userInfo.value=content.data.userInfo
} else {
Service.Msg(content.msg)
}
})
}
// 更换头像
const changeAvatar = () => {
@@ -93,12 +101,15 @@
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
Service.Msg('头像更换成功', 'success')
let path = res.tempFiles[0].path
Service.uploadH5(path, 'Avatar', data => {
userInfo.value.headImg = data
})
}
})
}
// 保存用户信息
// 保存用户信息
const saveUserInfo = () => {
if (!userInfo.value.name.trim()) {
Service.Msg('请输入用户姓名')
@@ -117,13 +128,17 @@
return
}
// 模拟保存成功
Service.Msg('保存成功', 'success')
// 保存成功后可以返回上一页
setTimeout(() => {
Service.GoPageBack()
}, 1000)
// 调用UpdateUserInfo接口
userService.UpdateUserInfo(userInfo.value.name, userInfo.value.phone, userInfo.value.headImg).then((content) => {
if (content.code == 0) {
Service.Msg('保存成功', 'success')
setTimeout(() => {
Service.GoPageBack()
}, 1000)
} else {
Service.Msg(content.msg)
}
})
}
</script>
@@ -198,6 +213,7 @@
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(24, 144, 255, 0.3);
overflow: hidden;
}
.avatar-edit {

View File

@@ -41,38 +41,14 @@
</view>
</view>
<!-- 泳道设置 -->
<view class="form-card">
<view class="form-title">泳道设置</view>
<view class="radio-group">
<view class="radio-item" :class="{ active: courseData.laneType === 'single' }"
@click="selectSingleLane">
<view class="radio-icon">
<view v-if="courseData.laneType === 'single'" class="radio-inner"></view>
</view>
<text>一个泳道</text>
</view>
<view class="radio-item" :class="{ active: courseData.laneType === 'multi' }"
@click="selectMultiLane">
<view class="radio-icon">
<view v-if="courseData.laneType === 'multi'" class="radio-inner"></view>
</view>
<text>多个泳道</text>
</view>
</view>
<view v-if="courseData.laneType === 'multi'" class="multi-lane-options">
<view class="lane-input-wrapper">
<text class="lane-label">泳道个数</text>
<view class="lane-input">
<input class="number-input" v-model="courseData.laneCount" type="number"
placeholder="请输入泳道个数" />
<text class="unit-text"></text>
</view>
</view>
<!-- 组别设置 -->
<view v-if="type==1" class="form-card">
<view class="form-title">组别设置</view>
<view class="group-options">
<view class="person-input-wrapper">
<text class="person-label">一个泳道人数</text>
<text class="person-label">每组人数</text>
<view class="person-input">
<input class="number-input" v-model="courseData.lanePersonCount" type="number"
<input class="number-input" v-model="courseData.groupPersonCount" type="number"
placeholder="请输入人数" />
<text class="unit-text"></text>
</view>
@@ -80,11 +56,34 @@
</view>
</view>
<!-- 分段设置 -->
<view v-if="type==2" class="form-card">
<view class="form-title">分段设置</view>
<view class="segment-options">
<view class="segment-input-wrapper">
<text class="segment-label">分段距离</text>
<view class="segment-input">
<input class="number-input" v-model="courseData.segmentDistance" type="number"
placeholder="请输入距离" />
<text class="unit-text"></text>
</view>
</view>
<view class="segment-count-wrapper">
<text class="segment-count-label">分段数</text>
<view class="segment-count-input">
<input class="number-input" v-model="courseData.segmentCount" type="number"
placeholder="请输入段数" />
<text class="unit-text"></text>
</view>
</view>
</view>
</view>
<!-- 学生列表 -->
<view class="form-card">
<view class="student-header">
<view class="header-left">
<text class="form-title">选择学生</text>
<text class="form-title" style="margin-bottom: 0;">选择学生</text>
<text class="student-count">已选({{ selectedStudentIds.length }}/{{ allStudents.length }})</text>
</view>
<view class="header-actions">
@@ -111,11 +110,12 @@
</view>
<view v-else class="student-list">
<view v-for="student in allStudents" :key="student.id" class="student-item"
:class="{ checked: selectedStudentIds.includes(student.id) }"
@click="toggleStudentSelect(student.id)">
<view class="student-checkbox" :class="{ checked: selectedStudentIds.includes(student.id) }">
<u-icon v-if="selectedStudentIds.includes(student.id)" name="checkmark" size="14"
<view v-for="student in allStudents" :key="student.studentId" class="student-item"
:class="{ checked: selectedStudentIds.includes(student.studentId) }"
@click="toggleStudentSelect(student.studentId)">
<view class="student-checkbox"
:class="{ checked: selectedStudentIds.includes(student.studentId) }">
<u-icon v-if="selectedStudentIds.includes(student.studentId)" name="checkmark" size="14"
color="#fff"></u-icon>
</view>
<view class="student-avatar">
@@ -123,12 +123,6 @@
</view>
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
<view class="student-meta">
<text class="gender-badge" :class="student.gender === '男' ? 'male' : 'female'">
{{ student.gender }}
</text>
<text class="age-text">{{ student.age }}</text>
</view>
</view>
</view>
</view>
@@ -139,10 +133,10 @@
<text class="preview-title">已选学生</text>
</view>
<view class="preview-list">
<view v-for="(student, index) in selectedStudents" :key="student.id" class="preview-item">
<view v-for="(student, index) in selectedStudents" :key="student.studentId" class="preview-item">
<text class="preview-index">{{ index + 1 }}</text>
<text class="preview-name">{{ student.name }}</text>
<view class="preview-remove" @click.stop="removeSelectedStudent(student.id)">
<view class="preview-remove" @click.stop="removeSelectedStudent(student.studentId)">
<u-icon name="close" size="14" color="#ff4d4f"></u-icon>
</view>
</view>
@@ -150,7 +144,9 @@
</view>
</view>
</view>
<view class="" style="width: 100%; height: 80rpx;">
</view>
<!-- 底部按钮区域 -->
<view class="bottom-actions">
<view class="action-buttons">
@@ -164,29 +160,24 @@
import { ref, computed } from 'vue'
import { Service } from '@/Service/Service'
import { onLoad } from '@dcloudio/uni-app'
import { PlanService } from '@/Service/swimming/PlanService'
import { studentService } from '@/Service/swimming/studentService'
// 学生类型
interface Student {
id : string
name : string
gender : string
age : string
school ?: string
address ?: string
}
// 课程数据
const courseData = ref({
projectName: '',
startType: 'together', // together | interval
intervalSeconds: '',
laneType: 'single', // single | multi
laneCount: '',
lanePersonCount: ''
groupPersonCount: '',
segmentDistance: '',
segmentCount: ''
})
// 所有学生列表
const allStudents = ref<Student[]>([])
const allStudents = ref<Array<any>>([])
// 已选学生ID列表
const selectedStudentIds = ref<string[]>([])
@@ -194,60 +185,62 @@
// 加载状态
const loading = ref(false)
onLoad(() => {
let type = ref('')
let planId=ref('')
onLoad((data : any) => {
type.value = data.type
if(data.id){
planId.value=data.id
getPlanInfo()
}
getStudentList()
})
// 获取计划详情
const getPlanInfo = () => {
PlanService.GetPlanInfo(planId.value).then(res => {
if (res.code == 0) {
selectedStudentIds.value=res.data.plan.users.map((item:any)=>{
return item.studentId
})
courseData.value.groupPersonCount=res.data.plan.groupInt
courseData.value.segmentDistance=res.data.plan.subsectionDistance
courseData.value.segmentCount=res.data.plan.subsectionInt
courseData.value.projectName=res.data.plan.name
courseData.value.startType = res.data.plan.departType == '间隔出发' ? 'interval' : 'together'
courseData.value.intervalSeconds = res.data.plan.interval
} else {
Service.Msg(res.msg)
}
})
}
// 是否全选
const allSelected = computed(() => {
return allStudents.value.length > 0 && allStudents.value.every(s => selectedStudentIds.value.includes(s.id))
return allStudents.value.length > 0 && allStudents.value.every(s => selectedStudentIds.value.includes(s.studentId))
})
// 已选学生列表(按选择顺序排序)
const selectedStudents = computed(() => {
return selectedStudentIds.value
.map(id => allStudents.value.find(s => s.id === id))
.map(id => allStudents.value.find(s => s.studentId === id))
.filter((s) : s is Student => s !== undefined)
})
// 选择单泳道
const selectSingleLane = () => {
courseData.value.laneType = 'single'
}
// 选择多泳道
const selectMultiLane = () => {
courseData.value.laneType = 'multi'
}
// 获取学生列表(模拟接口)
const getStudentList = async () => {
loading.value = true
try {
// TODO: 实际项目中应从接口获取
// const res = await Service.Request('/api/students', 'GET', {})
// allStudents.value = res.data
// 模拟接口延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 假数据
allStudents.value = [
{ id: '001', name: '张三', gender: '男', age: '12', school: '第一小学' },
{ id: '002', name: '李四', gender: '女', age: '13', school: '第二小学' },
{ id: '003', name: '王五', gender: '男', age: '11', school: '第一小学' },
{ id: '004', name: '赵六', gender: '女', age: '12', school: '第三小学' },
{ id: '005', name: '钱七', gender: '男', age: '14', school: '第二小学' },
{ id: '006', name: '孙八', gender: '女', age: '10', school: '第一小学' },
{ id: '007', name: '周九', gender: '男', age: '13', school: '第四小学' },
{ id: '008', name: '吴十', gender: '女', age: '12', school: '第二小学' }
]
} catch (error) {
Service.Msg('获取学生列表失败')
console.error(error)
} finally {
loading.value = false
}
studentService.GetStudentList().then(res => {
if (res.code == 0) {
allStudents.value = res.data
loading.value = false
} else {
Service.Msg(res.msg)
}
})
}
// 切换学生选中状态
@@ -267,7 +260,7 @@
selectedStudentIds.value = []
} else {
// 全选
selectedStudentIds.value = allStudents.value.map(s => s.id)
selectedStudentIds.value = allStudents.value.map(s => s.studentId)
}
}
@@ -279,11 +272,70 @@
}
}
// 保存项目
const confirmCreate = () => {
if (!courseData.value.projectName) {
Service.Msg('请输入项目名称!')
return
}
if (courseData.value.startType == 'interval' && !courseData.value.intervalSeconds) {
Service.Msg('请输入正确时间间隔!')
return
}
if (!courseData.value.groupPersonCount && type.value=='1') {
Service.Msg('请输入每组人数!')
return
}
if (!courseData.value.segmentDistance && type.value=='2') {
Service.Msg('请输入分段距离')
return
}
if (!courseData.value.segmentCount && type.value=='2') {
Service.Msg('请输入分段数')
return
}
let users=[]
selectedStudents.value.map((item:any)=>{
users.push({
studentId:item.studentId,
name:item.name
})
})
const data = {
planId: planId.value,
name: courseData.value.projectName,
planType:type.value=='1'?'计时项目':'分段项目',
departType: courseData.value.startType=='together'?'一起出发':'间隔出发',
interval: courseData.value.startType=='together'?0:courseData.value.intervalSeconds,
groupInt: courseData.value.groupPersonCount?courseData.value.groupPersonCount:'',
subsectionDistance: courseData.value.segmentDistance?courseData.value.segmentDistance:'',
subsectionInt: courseData.value.segmentCount?courseData.value.segmentCount:'',
users: JSON.stringify(users),
project: '',
group: '',
}
console.log(data);
PlanService.AddPlan(data).then(res=>{
if(res.code==0){
Service.Msg('添加成功!')
setTimeout(()=>{
Service.GoPageBack()
},1000)
}else{
Service.Msg(res.msg)
}
})
}
</script>
@@ -446,15 +498,14 @@
}
}
/* 多泳道选项 */
.multi-lane-options {
padding-top: 20rpx;
/* 组别选项 */
.group-options {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.lane-input-wrapper,
.group-input-wrapper,
.person-input-wrapper {
background-color: #f5f5f5;
border-radius: 16rpx;
@@ -463,7 +514,7 @@
align-items: center;
justify-content: space-between;
.lane-label,
.group-label,
.person-label {
font-size: 28rpx;
font-weight: 600;
@@ -471,7 +522,7 @@
}
}
.lane-input,
.group-input,
.person-input {
flex: 1;
margin-left: 24rpx;
@@ -495,6 +546,54 @@
}
}
/* 分段设置选项 */
.segment-options {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.segment-input-wrapper,
.segment-count-wrapper {
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
.segment-label,
.segment-count-label {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
}
.segment-input,
.segment-count-input {
flex: 1;
margin-left: 24rpx;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 20rpx;
height: 72rpx;
.number-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.unit-text {
font-size: 24rpx;
color: #999;
margin-left: 12rpx;
}
}
/* 学生列表头部 */
.student-header {
display: flex;
@@ -785,7 +884,7 @@
.cancel-btn,
.confirm-btn {
flex: 1;
flex: 1;
height: 88rpx;
border-radius: 16rpx;
border: none;

View File

@@ -1,902 +0,0 @@
<template>
<view class="course-container">
<!-- 表单区域 -->
<view class="form-section">
<!-- 项目名称 -->
<view class="form-card">
<view class="form-title">项目信息</view>
<view class="form-group">
<text class="form-label">项目名称</text>
<input class="form-input" v-model="courseData.projectName" placeholder="请输入项目名称" placeholder-class="input-placeholder" />
</view>
</view>
<!-- 出发方式 -->
<view class="form-card">
<view class="form-title">出发方式</view>
<view class="radio-group">
<view class="radio-item" :class="{ active: courseData.startType === 'together' }" @click="courseData.startType = 'together'">
<view class="radio-icon">
<view v-if="courseData.startType === 'together'" class="radio-inner"></view>
</view>
<text>一起出发</text>
</view>
<view class="radio-item" :class="{ active: courseData.startType === 'interval' }" @click="courseData.startType = 'interval'">
<view class="radio-icon">
<view v-if="courseData.startType === 'interval'" class="radio-inner"></view>
</view>
<text>间隔出发</text>
</view>
</view>
<view v-if="courseData.startType === 'interval'" class="interval-input-wrapper">
<text class="interval-label">间隔时间</text>
<view class="interval-input">
<input class="number-input" v-model="courseData.intervalSeconds" type="digit" placeholder="请输入秒数" />
<text class="unit-text"></text>
</view>
</view>
</view>
<!-- 泳道设置 -->
<view class="form-card">
<view class="form-title">泳道设置</view>
<view class="radio-group">
<view class="radio-item" :class="{ active: courseData.laneType === 'single' }" @click="selectSingleLane">
<view class="radio-icon">
<view v-if="courseData.laneType === 'single'" class="radio-inner"></view>
</view>
<text>一个泳道</text>
</view>
<view class="radio-item" :class="{ active: courseData.laneType === 'multi' }" @click="selectMultiLane">
<view class="radio-icon">
<view v-if="courseData.laneType === 'multi'" class="radio-inner"></view>
</view>
<text>多个泳道</text>
</view>
</view>
<view v-if="courseData.laneType === 'multi'" class="multi-lane-options">
<view class="sub-option-item" :class="{ active: courseData.multiLaneMode === 'onePerson' }" @click="courseData.multiLaneMode = 'onePerson'">
<view class="sub-option-icon">
<view v-if="courseData.multiLaneMode === 'onePerson'" class="option-inner"></view>
</view>
<text>一个泳道一个人</text>
</view>
<view class="sub-option-item" :class="{ active: courseData.multiLaneMode === 'multiPerson' }" @click="courseData.multiLaneMode = 'multiPerson'">
<view class="sub-option-icon">
<view v-if="courseData.multiLaneMode === 'multiPerson'" class="option-inner"></view>
</view>
<text>一个泳道几个人</text>
</view>
<view v-if="courseData.multiLaneMode === 'multiPerson'" class="multi-person-input-wrapper">
<text class="multi-label">每个泳道人数</text>
<view class="multi-input">
<input class="number-input" v-model="courseData.lanePersonCount" type="number" placeholder="请输入人数" />
<text class="unit-text"></text>
</view>
</view>
</view>
</view>
<!-- 学生列表 -->
<view class="form-card">
<view class="student-header">
<view class="header-left">
<text class="form-title">选择学生</text>
<text class="student-count">已选({{ selectedStudentIds.length }}/{{ allStudents.length }})</text>
</view>
<view class="header-actions">
<view class="select-all-btn" @click="toggleSelectAll">
<view class="checkbox-icon" :class="{ checked: allSelected }">
<u-icon v-if="allSelected" name="checkmark" size="14" color="#fff"></u-icon>
</view>
<text class="select-all-text">{{ allSelected ? '取消全选' : '全选' }}</text>
</view>
</view>
</view>
<view v-if="loading" class="loading-state">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<view v-else-if="allStudents.length === 0" class="empty-student-state">
<view class="empty-icon">
<u-icon name="account" size="48" color="#ddd"></u-icon>
</view>
<text class="empty-text">暂无学生</text>
<text class="empty-desc">请先在学员管理中添加学生</text>
</view>
<view v-else class="student-list">
<view v-for="student in allStudents" :key="student.id" class="student-item" @click="toggleStudentSelect(student.id)">
<view class="student-checkbox" :class="{ checked: selectedStudentIds.includes(student.id) }">
<u-icon v-if="selectedStudentIds.includes(student.id)" name="checkmark" size="14" color="#fff"></u-icon>
</view>
<view class="student-avatar">
<text class="avatar-text">{{ student.name.charAt(0) }}</text>
</view>
<view class="student-info">
<view class="student-name">{{ student.name }}</view>
<view class="student-meta">
<text class="gender-badge" :class="student.gender === '男' ? 'male' : 'female'">
{{ student.gender }}
</text>
<text class="age-text">{{ student.age }}</text>
</view>
</view>
</view>
</view>
<!-- 已选学生预览 -->
<view v-if="selectedStudents.length > 0" class="selected-preview">
<view class="preview-header">
<text class="preview-title">已选学生</text>
</view>
<view class="preview-list">
<view v-for="(student, index) in selectedStudents" :key="student.id" class="preview-item">
<text class="preview-index">{{ index + 1 }}</text>
<text class="preview-name">{{ student.name }}</text>
<view class="preview-remove" @click.stop="removeSelectedStudent(student.id)">
<u-icon name="close" size="14" color="#ff4d4f"></u-icon>
</view>
</view>
</view>
</view>
</view>
</view>
<!-- 底部按钮区域 -->
<view class="bottom-actions">
<view class="action-buttons">
<button class="cancel-btn" @click="goBack">取消</button>
<button class="confirm-btn" @click="confirmCreate">创建项目</button>
</view>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Service } from '@/Service/Service'
// 学生类型
interface Student {
id: string
name: string
gender: string
age: string
school?: string
address?: string
}
// 课程数据
const courseData = ref({
projectName: '',
startType: 'together', // together | interval
intervalSeconds: '',
intervalSeconds: '',
laneType: 'single', // single | multi
multiLaneMode: 'onePerson', // onePerson | multiPerson
lanePersonCount: ''
})
// 所有学生列表
const allStudents = ref<Student[]>([])
// 已选学生ID列表
const selectedStudentIds = ref<string[]>([])
// 加载状态
const loading = ref(false)
// 是否全选
const allSelected = computed(() => {
return allStudents.value.length > 0 && allStudents.value.every(s => selectedStudentIds.value.includes(s.id))
})
// 已选学生列表
const selectedStudents = computed(() => {
return allStudents.value.filter(s => selectedStudentIds.value.includes(s.id))
})
// 选择单泳道
const selectSingleLane = () => {
courseData.value.laneType = 'single'
courseData.value.multiLaneMode = 'onePerson'
}
// 选择多泳道
const selectMultiLane = () => {
courseData.value.laneType = 'multi'
}
// 获取学生列表(模拟接口)
const getStudentList = async () => {
loading.value = true
try {
// TODO: 实际项目中应从接口获取
// const res = await Service.Request('/api/students', 'GET', {})
// allStudents.value = res.data
// 模拟接口延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 假数据
allStudents.value = [
{ id: '001', name: '张三', gender: '男', age: '12', school: '第一小学' },
{ id: '002', name: '李四', gender: '女', age: '13', school: '第二小学' },
{ id: '003', name: '王五', gender: '男', age: '11', school: '第一小学' },
{ id: '004', name: '赵六', gender: '女', age: '12', school: '第三小学' },
{ id: '005', name: '钱七', gender: '男', age: '14', school: '第二小学' },
{ id: '006', name: '孙八', gender: '女', age: '10', school: '第一小学' },
{ id: '007', name: '周九', gender: '男', age: '13', school: '第四小学' },
{ id: '008', name: '吴十', gender: '女', age: '12', school: '第二小学' }
]
} catch (error) {
Service.Msg('获取学生列表失败')
console.error(error)
} finally {
loading.value = false
}
}
// 切换学生选中状态
const toggleStudentSelect = (id: string) => {
const index = selectedStudentIds.value.indexOf(id)
if (index > -1) {
selectedStudentIds.value.splice(index, 1)
} else {
selectedStudentIds.value.push(id)
}
}
// 全选/取消全选
const toggleSelectAll = () => {
if (allSelected.value) {
// 取消全选
selectedStudentIds.value = []
} else {
// 全选
selectedStudentIds.value = allStudents.value.map(s => s.id)
}
}
// 移除已选学生
const removeSelectedStudent = (id: string) => {
const index = selectedStudentIds.value.indexOf(id)
if (index > -1) {
selectedStudentIds.value.splice(index, 1)
}
}
// 返回
const goBack = () => {
Service.GoPageBack()
}
// 创建项目
const confirmCreate = () => {
if (!courseData.value.projectName.trim()) {
Service.Msg('请输入项目名称')
return
}
if (courseData.value.startType === 'interval' && !courseData.value.intervalSeconds.trim()) {
Service.Msg('请输入间隔秒数')
return
}
if (courseData.value.laneType === 'multi' && courseData.value.multiLaneMode === 'multiPerson' && !courseData.value.lanePersonCount.trim()) {
Service.Msg('请输入每个泳道人数')
return
}
if (selectedStudentIds.value.length === 0) {
Service.Msg('请至少选择一位学生')
return
}
// TODO: 调用API创建项目
console.log('创建项目数据:', {
course: courseData.value,
studentIds: selectedStudentIds.value,
students: selectedStudents.value
})
Service.Msg('创建成功', 'success')
setTimeout(() => {
Service.GoPageBack()
}, 1500)
}
// 页面加载时获取学生列表
onMounted(() => {
getStudentList()
})
</script>
<style lang="scss" scoped>
page {
background-color: #f5f5f5;
}
.course-container {
min-height: 100vh;
padding-bottom: 140rpx;
}
/* 表单区域 */
.form-section {
padding: 20rpx;
}
.form-card {
background-color: #fff;
border-radius: 24rpx;
padding: 32rpx 28rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
.form-title {
font-size: 32rpx;
font-weight: 700;
color: #333;
margin-bottom: 28rpx;
display: block;
}
}
/* 表单输入 */
.form-group {
margin-bottom: 32rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 14rpx;
}
.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);
}
}
}
/* 单选按钮组 */
.radio-group {
display: flex;
gap: 20rpx;
margin-bottom: 24rpx;
.radio-item {
flex: 1;
height: 88rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
font-size: 28rpx;
color: #666;
transition: all 0.25s ease;
border: 2rpx solid transparent;
&:active {
transform: scale(0.96);
}
&.active {
font-weight: 600;
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
.radio-icon {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 3rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
&.active .radio-icon {
border-color: #1890ff;
background-color: #1890ff;
}
.radio-inner {
width: 12rpx;
height: 12rpx;
background-color: #fff;
border-radius: 50%;
}
}
}
/* 间隔时间输入 */
.interval-input-wrapper {
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
.interval-label {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.interval-input {
flex: 1;
margin-left: 24rpx;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 20rpx;
height: 72rpx;
.number-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.unit-text {
font-size: 24rpx;
color: #999;
margin-left: 12rpx;
}
}
}
/* 多泳道选项 */
.multi-lane-options {
padding-top: 20rpx;
.sub-option-item {
padding: 20rpx 24rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
display: flex;
align-items: center;
gap: 12rpx;
font-size: 26rpx;
color: #666;
margin-bottom: 16rpx;
transition: all 0.2s ease;
border: 2rpx solid transparent;
&:active {
transform: scale(0.98);
}
&.active {
font-weight: 600;
background-color: #e6f7ff;
border-color: #1890ff;
color: #1890ff;
}
&:last-child {
margin-bottom: 0;
}
.sub-option-icon {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
border: 2rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
&.active .sub-option-icon {
border-color: #1890ff;
background-color: #1890ff;
}
.option-inner {
width: 10rpx;
height: 10rpx;
background-color: #fff;
border-radius: 50%;
}
}
}
/* 多泳道人数输入 */
.multi-person-input-wrapper {
margin-top: 20rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
.multi-label {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.multi-input {
flex: 1;
margin-left: 24rpx;
display: flex;
align-items: center;
background-color: #fff;
border-radius: 12rpx;
padding: 0 20rpx;
height: 72rpx;
.number-input {
flex: 1;
font-size: 28rpx;
color: #333;
}
.unit-text {
font-size: 24rpx;
color: #999;
margin-left: 12rpx;
}
}
}
/* 学生列表头部 */
.student-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24rpx;
.header-left {
display: flex;
align-items: center;
gap: 8rpx;
.student-count {
font-size: 26rpx;
color: #999;
font-weight: 500;
}
}
.header-actions {
.select-all-btn {
display: flex;
align-items: center;
gap: 8rpx;
padding: 8rpx 16rpx;
border-radius: 12rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.96);
}
.checkbox-icon {
width: 32rpx;
height: 32rpx;
border-radius: 50%;
border: 2rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&.checked {
border-color: #1890ff;
background-color: #1890ff;
}
}
.select-all-text {
font-size: 24rpx;
color: #666;
}
}
}
}
/* 加载状态 */
.loading-state {
padding: 80rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.loading-spinner {
width: 60rpx;
height: 60rpx;
border: 4rpx solid #f0f0f0;
border-top-color: #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20rpx;
}
.loading-text {
font-size: 26rpx;
color: #999;
}
}
/* 空状态 */
.empty-student-state {
padding: 60rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
.empty-icon {
margin-bottom: 20rpx;
opacity: 0.6;
}
.empty-text {
font-size: 28rpx;
font-weight: 600;
color: #666;
margin-bottom: 10rpx;
}
.empty-desc {
font-size: 24rpx;
color: #999;
}
}
/* 学生列表 */
.student-list {
display: flex;
flex-direction: column;
gap: 12rpx;
.student-item {
display: flex;
align-items: center;
gap: 16rpx;
padding: 20rpx;
background-color: #f5f5f5;
border-radius: 16rpx;
transition: all 0.2s ease;
&:active {
transform: scale(0.98);
background-color: #e8e8e8;
}
.student-checkbox {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
border: 2rpx solid #d9d9d9;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.2s ease;
&.checked {
border-color: #1890ff;
background-color: #1890ff;
}
}
.student-avatar {
width: 64rpx;
height: 64rpx;
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.avatar-text {
color: #fff;
font-size: 28rpx;
font-weight: 700;
}
}
.student-info {
flex: 1;
min-width: 0;
.student-name {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 6rpx;
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.student-meta {
display: flex;
align-items: center;
gap: 12rpx;
.gender-badge {
font-size: 22rpx;
padding: 4rpx 10rpx;
border-radius: 8rpx;
font-weight: 500;
&.male {
color: #1890ff;
background-color: #e6f7ff;
}
&.female {
color: #fa8c16;
background-color: #fff7e6;
}
}
.age-text {
font-size: 22rpx;
color: #999;
}
}
}
}
}
/* 已选学生预览 */
.selected-preview {
margin-top: 32rpx;
padding-top: 24rpx;
border-top: 1rpx solid #f0f0f0;
.preview-header {
margin-bottom: 16rpx;
.preview-title {
font-size: 26rpx;
font-weight: 600;
color: #666;
}
}
.preview-list {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.preview-item {
display: flex;
align-items: center;
gap: 8rpx;
padding: 10rpx 16rpx;
background-color: #e6f7ff;
border-radius: 12rpx;
.preview-index {
width: 24rpx;
height: 24rpx;
background-color: #1890ff;
color: #fff;
font-size: 18rpx;
font-weight: 600;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.preview-name {
font-size: 26rpx;
color: #1890ff;
font-weight: 500;
}
.preview-remove {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
&:active {
transform: scale(0.9);
}
}
}
}
/* 底部操作按钮 */
.bottom-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 99;
.action-buttons {
display: flex;
gap: 16rpx;
}
}
.cancel-btn,
.confirm-btn {
flex: 1;
height: 88rpx;
border-radius: 16rpx;
border: none;
font-size: 30rpx;
font-weight: 600;
transition: all 0.25s ease;
}
.cancel-btn {
background-color: #f5f5f5;
color: #666;
&:active {
background-color: #e8e8e8;
transform: scale(0.96);
}
}
.confirm-btn {
background: linear-gradient(135deg, #1890ff 0%, #096dd9 100%);
color: #fff;
box-shadow: 0 6rpx 16rpx rgba(24, 144, 255, 0.35);
&:active {
transform: scale(0.96);
box-shadow: 0 3rpx 8rpx rgba(24, 144, 255, 0.25);
}
}
/* 动画 */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>

View File

@@ -27,18 +27,18 @@
<view class="student-details">
<view class="detail-row">
<text class="student-name">{{ student.name }}</text>
<view class="student-gender" :class="student.gender === '男' ? 'male' : 'female'">
<u-icon :name="student.gender === '男' ? 'man' : 'woman'" size="14"></u-icon>
{{ student.gender }}
<view class="student-gender" :class="student.sex === '男' ? 'male' : 'female'">
<u-icon :name="student.sex === '男' ? 'man' : 'woman'" size="14"></u-icon>
{{ student.sex }}
</view>
</view>
<view class="detail-row">
<text class="detail-label">出生日期</text>
<text class="detail-value">{{ student.birthDate }}</text>
<text class="detail-value">{{ student.birthday }}</text>
</view>
<view class="detail-row">
<text class="detail-label">年龄</text>
<text class="detail-value">{{ student.age }}</text>
<text class="detail-value">{{ calculateAge(student.birthday) }}</text>
</view>
<view v-if="student.school" class="detail-row">
<text class="detail-label">学校</text>
@@ -54,14 +54,14 @@
<view class="action-btn edit-btn" @click="openEditModal(student)">
<u-icon name="edit-pen" size="18" color="#1890ff"></u-icon>
</view>
<view class="action-btn delete-btn" @click="confirmDelete(student)">
<u-icon name="trash" size="18" color="#ff4d4f"></u-icon>
</view>
</view>
</view>
</view>
</view>
<view class="" style="width: 100%; height: 100rpx;" >
</view>
<!-- 底部添加按钮 -->
<view class="bottom-add-btn">
<button class="add-btn" @click="showAddModal = true">
@@ -143,54 +143,37 @@
</template>
<script setup lang="ts">
import { onShow, onLoad } from "@dcloudio/uni-app"
import { onShow, onLoad, onReachBottom } from "@dcloudio/uni-app"
import { Service } from '@/Service/Service'
import { studentService } from '@/Service/swimming/studentService'
import { ref } from "vue"
// 定义学员类型
interface Student {
id : string
name : string
gender : string
age : string
birthDate : string
school : string
address : string
}
// 分页相关
let page = ref(1)
let status = ref('loadmore')
// 学员列表
const students = ref<Student[]>([
{ id: '001', name: '张三', gender: '男', age: '12', birthDate: '2012-05-15', school: '第一小学', address: '北京市朝阳区' },
{ id: '002', name: '李四', gender: '女', age: '13', birthDate: '2011-08-20', school: '', address: '' },
{ id: '003', name: '王五', gender: '男', age: '11', birthDate: '2013-03-10', school: '第二小学', address: '' },
{ id: '004', name: '赵六', gender: '女', age: '12', birthDate: '2012-11-25', school: '', address: '上海市浦东新区' }
])
const students = ref<Array<any>>([])
// 弹窗状态
const showAddModal = ref(false)
const showEditModal = ref(false)
// 表单数据
const formData = ref({
id: '',
name: '',
gender: '男',
age: '',
birthDate: '',
school: '',
address: ''
})
const formData = ref<any>({})
// 当前编辑的学员
const editingStudent = ref<Student | null>(null)
const editingStudent = ref<any>({})
onLoad(() => {
getData()
})
onShow(() => {
onReachBottom(() => {
getList()
})
// 计算年龄
@@ -213,14 +196,13 @@
}
// 打开编辑弹窗
const openEditModal = (student : Student) => {
const openEditModal = (student :any) => {
editingStudent.value = student
formData.value = {
id: student.id,
id: student.studentId,
name: student.name,
gender: student.gender,
age: student.age,
birthDate: student.birthDate,
gender: student.sex?student.sex: '男',
birthDate: student.birthday,
school: student.school,
address: student.address
}
@@ -243,9 +225,34 @@
}
}
// 获取学员列表数据
const getData = () => {
students.value = []
page.value = 1
status.value = 'loadmore'
getList()
}
// 获取学员列表
const getList = () => {
if (status.value == 'loading' || status.value == 'nomore') {
return
}
status.value = 'loading'
studentService.GetStudentListPage(page.value.toString()).then(res => {
if (res.code == 0) {
students.value = [...students.value, ...res.data]
status.value = res.data.length == 10 ? 'loadmore' : 'nomore'
page.value++
} else {
Service.Msg(res.msg)
}
})
}
// 保存学员
const saveStudent = () => {
if (!formData.value.name.trim()) {
if (!formData.value.name) {
Service.Msg('请输入学员姓名')
return
}
@@ -255,35 +262,26 @@
return
}
if (showAddModal.value) {
// 添加新学员
const newStudent : Student = {
id: Date.now().toString().slice(-6),
name: formData.value.name.trim(),
gender: formData.value.gender,
age: formData.value.age,
birthDate: formData.value.birthDate,
school: formData.value.school.trim(),
address: formData.value.address.trim()
}
students.value.push(newStudent)
Service.Msg('添加成功', 'success')
} else if (showEditModal.value && editingStudent.value) {
// 编辑学员
const index = students.value.findIndex(s => s.id === editingStudent.value!.id)
if (index !== -1) {
students.value[index] = {
...students.value[index],
name: formData.value.name.trim(),
gender: formData.value.gender,
age: formData.value.age,
birthDate: formData.value.birthDate,
school: formData.value.school.trim(),
address: formData.value.address.trim()
}
Service.Msg('修改成功', 'success')
}
// 构造接口请求数据
const requestData = {
studentId:formData.value.id,
name: formData.value.name,
sex: formData.value.gender,
birthday: formData.value.birthDate,
school: formData.value.school ,
address: formData.value.address
}
console.log(requestData);
// 调用添加学员接口
studentService.Add(requestData).then((content) => {
if (content.code == 0) {
Service.Msg('添加成功')
getData()
} else {
Service.Msg(content.msg)
}
})
closeModal()
}
@@ -304,8 +302,14 @@
// 删除学员
const deleteStudent = (id : string) => {
students.value = students.value.filter(s => s.id !== id)
Service.Msg('删除成功', 'success')
studentService.Delete(id).then(res => {
if (res.code == 0) {
students.value = students.value.filter(s => s.id !== id)
Service.Msg('删除成功', 'success')
} else {
Service.Msg(res.msg)
}
})
}
</script>
@@ -793,8 +797,7 @@
left: 0;
right: 0;
background-color: #fff;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
padding: 20rpx 20rpx;
border-top: 1rpx solid #f0f0f0;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.08);
z-index: 100;

File diff suppressed because it is too large Load Diff