Merge branch 'main' of https://dd3skj.picp.vip/zhaoxinyu/hmc_gbhd
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
package com.budwk.app.client.auth.controller;
|
package com.budwk.app.client.auth.controller;
|
||||||
|
|
||||||
|
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||||
import cn.dev33.satoken.stp.StpUtil;
|
import cn.dev33.satoken.stp.StpUtil;
|
||||||
import com.budwk.app.base.constant.RedisConstant;
|
import com.budwk.app.base.constant.RedisConstant;
|
||||||
import com.budwk.app.base.exception.BaseException;
|
import com.budwk.app.base.exception.BaseException;
|
||||||
@@ -72,6 +73,9 @@ public class ClientAuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送短信验证码
|
||||||
|
*/
|
||||||
@At("/send-sms-code/?")
|
@At("/send-sms-code/?")
|
||||||
@Ok("json")
|
@Ok("json")
|
||||||
public Object sendSmsCode(String phone) {
|
public Object sendSmsCode(String phone) {
|
||||||
@@ -83,4 +87,16 @@ public class ClientAuthController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
*/
|
||||||
|
@At("/logout")
|
||||||
|
@Ok("json")
|
||||||
|
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||||
|
public Result logout() {
|
||||||
|
StpClientUtil.STP.logout();
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
@@ -5,6 +5,8 @@ import com.budwk.app.base.page.PageForm;
|
|||||||
import com.budwk.app.base.page.Pagination;
|
import com.budwk.app.base.page.Pagination;
|
||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.client.module.activity.service.ClientActivityService;
|
import com.budwk.app.client.module.activity.service.ClientActivityService;
|
||||||
|
import com.budwk.app.client.module.activity.vo.ClientActivitySignVO;
|
||||||
|
import com.budwk.app.module.activity.models.Activity;
|
||||||
import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
|
import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
@@ -14,6 +16,7 @@ import org.nutz.ioc.loader.annotation.IocBean;
|
|||||||
import org.nutz.mvc.adaptor.JsonAdaptor;
|
import org.nutz.mvc.adaptor.JsonAdaptor;
|
||||||
import org.nutz.mvc.annotation.AdaptBy;
|
import org.nutz.mvc.annotation.AdaptBy;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.GET;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
|
||||||
@IocBean
|
@IocBean
|
||||||
@@ -48,4 +51,24 @@ public class ClientActivityController {
|
|||||||
return Result.success(pagination);
|
return Result.success(pagination);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@At("/get/?")
|
||||||
|
@GET
|
||||||
|
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||||
|
public Result get(Long id) {
|
||||||
|
Activity activity = clientActivityService.fetch(id);
|
||||||
|
clientActivityService.fetchLinks(activity, "items");
|
||||||
|
return Result.success(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报名
|
||||||
|
*/
|
||||||
|
@At("/sign")
|
||||||
|
@AdaptBy(type = JsonAdaptor.class)
|
||||||
|
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||||
|
public Result sign(ClientActivitySignVO signVO) {
|
||||||
|
return Result.success();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+22
@@ -0,0 +1,22 @@
|
|||||||
|
package com.budwk.app.client.module.activity.vo;
|
||||||
|
|
||||||
|
import com.budwk.app.module.activity.models.ActUser;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 报名
|
||||||
|
*/
|
||||||
|
public class ClientActivitySignVO {
|
||||||
|
|
||||||
|
private String activityId;
|
||||||
|
|
||||||
|
private String unitName;
|
||||||
|
|
||||||
|
private String contactName;
|
||||||
|
|
||||||
|
private String contactPhone;
|
||||||
|
|
||||||
|
private List<ActUser> userList;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ public class ValidateService {
|
|||||||
String text = R.captchaNumber(6);
|
String text = R.captchaNumber(6);
|
||||||
String codeFromRedis = redisService.get(RedisConstant.UCENTER_SMSCODE + mobile + ":LOCK");
|
String codeFromRedis = redisService.get(RedisConstant.UCENTER_SMSCODE + mobile + ":LOCK");
|
||||||
if (Strings.isNotBlank(codeFromRedis)) {
|
if (Strings.isNotBlank(codeFromRedis)) {
|
||||||
throw new BaseException("请1分钟之后再试");
|
// throw new BaseException("请1分钟之后再试");
|
||||||
}
|
}
|
||||||
ThreadUtil.execute(()->{
|
ThreadUtil.execute(()->{
|
||||||
smsSendServer.sendMsg(mobile, "验证码为:"+text);
|
smsSendServer.sendMsg(mobile, "验证码为:"+text);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import com.budwk.app.base.exception.BaseException;
|
|||||||
import com.budwk.app.base.result.Result;
|
import com.budwk.app.base.result.Result;
|
||||||
import com.budwk.app.base.result.ResultCode;
|
import com.budwk.app.base.result.ResultCode;
|
||||||
import com.budwk.app.base.utils.WebUtil;
|
import com.budwk.app.base.utils.WebUtil;
|
||||||
|
import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
|
||||||
import org.nutz.castor.FailToCastObjectException;
|
import org.nutz.castor.FailToCastObjectException;
|
||||||
import org.nutz.dao.DaoException;
|
import org.nutz.dao.DaoException;
|
||||||
import org.nutz.ioc.IocException;
|
import org.nutz.ioc.IocException;
|
||||||
@@ -66,7 +67,8 @@ public class WkFailProcessor extends ViewProcessor {
|
|||||||
new ForwardView(errorUri).render(ac.getRequest(), ac.getResponse(), null);
|
new ForwardView(errorUri).render(ac.getRequest(), ac.getResponse(), null);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
} else if (e instanceof NotLoginException) { // 如果是未登录异常
|
} else if (e instanceof NotLoginException) {
|
||||||
|
// 如果是未登录异常
|
||||||
if (WebUtil.isAjax(ac.getRequest())) {
|
if (WebUtil.isAjax(ac.getRequest())) {
|
||||||
WebUtil.rendAjaxResp(ac.getRequest(), ac.getResponse(), Result.error(ResultCode.USER_NOT_LOGIN.getCode(), ResultCode.USER_NOT_LOGIN.getMsg()));
|
WebUtil.rendAjaxResp(ac.getRequest(), ac.getResponse(), Result.error(ResultCode.USER_NOT_LOGIN.getCode(), ResultCode.USER_NOT_LOGIN.getMsg()));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Generated
+10
@@ -24,6 +24,7 @@
|
|||||||
"@dcloudio/uni-mp-weixin": "3.0.0-4050720250324001",
|
"@dcloudio/uni-mp-weixin": "3.0.0-4050720250324001",
|
||||||
"@dcloudio/uni-mp-xhs": "3.0.0-4050720250324001",
|
"@dcloudio/uni-mp-xhs": "3.0.0-4050720250324001",
|
||||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4050720250324001",
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4050720250324001",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"tdesign-mobile-vue": "^1.8.3",
|
"tdesign-mobile-vue": "^1.8.3",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-i18n": "^9.1.9"
|
"vue-i18n": "^9.1.9"
|
||||||
@@ -8773,6 +8774,15 @@
|
|||||||
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
|
"integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/moment": {
|
||||||
|
"version": "2.30.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
|
||||||
|
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
"@dcloudio/uni-mp-weixin": "3.0.0-4050720250324001",
|
"@dcloudio/uni-mp-weixin": "3.0.0-4050720250324001",
|
||||||
"@dcloudio/uni-mp-xhs": "3.0.0-4050720250324001",
|
"@dcloudio/uni-mp-xhs": "3.0.0-4050720250324001",
|
||||||
"@dcloudio/uni-quickapp-webview": "3.0.0-4050720250324001",
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4050720250324001",
|
||||||
|
"moment": "^2.30.1",
|
||||||
"tdesign-mobile-vue": "^1.8.3",
|
"tdesign-mobile-vue": "^1.8.3",
|
||||||
"vue": "^3.4.21",
|
"vue": "^3.4.21",
|
||||||
"vue-i18n": "^9.1.9"
|
"vue-i18n": "^9.1.9"
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
<script>
|
<script>
|
||||||
|
import { setupRouterInterceptor, getDefaultWhiteList } from './utils/routerInterceptor'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onLaunch: function () {
|
onLaunch: function () {
|
||||||
console.log('App Launch')
|
console.log('App Launch')
|
||||||
|
|
||||||
|
// 初始化路由拦截器
|
||||||
|
setupRouterInterceptor(getDefaultWhiteList())
|
||||||
},
|
},
|
||||||
onShow: function () {
|
onShow: function () {
|
||||||
console.log('App Show')
|
console.log('App Show')
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import 'tdesign-mobile-vue/es/style/index.css';
|
|||||||
// 自定义组件库样式
|
// 自定义组件库样式
|
||||||
import './theme.css'
|
import './theme.css'
|
||||||
|
|
||||||
|
// 引入moment
|
||||||
|
import moment from 'moment';
|
||||||
|
import 'moment/dist/locale/zh-cn'
|
||||||
|
moment.locale('zh-cn')
|
||||||
|
|
||||||
export function createApp() {
|
export function createApp() {
|
||||||
const app = createSSRApp(App);
|
const app = createSSRApp(App);
|
||||||
app.use(TDesign); // 使用TDesign组件库
|
app.use(TDesign); // 使用TDesign组件库
|
||||||
|
|||||||
+42
-7
@@ -6,6 +6,14 @@
|
|||||||
"navigationBarTitleText": "uni-app"
|
"navigationBarTitleText": "uni-app"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "登录",
|
||||||
|
"navigationStyle": "custom",
|
||||||
|
"disableScroll": true
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"path": "pages/activity/list/index",
|
"path": "pages/activity/list/index",
|
||||||
"style": {
|
"style": {
|
||||||
@@ -29,31 +37,58 @@
|
|||||||
"style": {
|
"style": {
|
||||||
"navigationBarTitleText": "我的团队"
|
"navigationBarTitleText": "我的团队"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/profile/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "我的"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/activity/scan-sign/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "扫码签到"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"globalStyle": {
|
"globalStyle": {
|
||||||
"navigationBarTextStyle": "#FFFFFF",
|
"navigationBarTextStyle": "#FFFFFF",
|
||||||
"navigationBarTitleText": "uni-app",
|
"navigationBarTitleText": "uni-app",
|
||||||
"navigationBarBackgroundColor": "#ff5050",
|
"navigationBarBackgroundColor": "#f45a5a",
|
||||||
"backgroundColor": "#F8F8F8"
|
"backgroundColor": "#F8F8F8"
|
||||||
},
|
},
|
||||||
"tabBar": {
|
"tabBar": {
|
||||||
"color": "#000",
|
"color": "#000",
|
||||||
"selectedColor": "#ff5050",
|
"selectedColor": "#f45a5a",
|
||||||
"borderStyle": "black",
|
"borderStyle": "white",
|
||||||
"backgroundColor": "#ffffff",
|
"backgroundColor": "#ffffff",
|
||||||
"height": "60px",
|
"height": "60px",
|
||||||
"fontSize": "12px",
|
"fontSize": "14px",
|
||||||
"iconWidth": "24px",
|
"iconWidth": "24px",
|
||||||
"spacing": "3px",
|
"spacing": "3px",
|
||||||
|
"iconfontSrc": "static/fonts/iconfont.ttf",
|
||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"pagePath": "pages/index/index",
|
"pagePath": "pages/index/index",
|
||||||
"text": "组件"
|
"text": "组件",
|
||||||
|
"iconfont": {
|
||||||
|
"text": "\ue64d",
|
||||||
|
"selectedText": "\ue64d",
|
||||||
|
"fontSize": "17px",
|
||||||
|
"color": "#333",
|
||||||
|
"selectedColor": "#f45a5a"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/my/index",
|
"pagePath": "pages/profile/index",
|
||||||
"text": "接口"
|
"text": "我的",
|
||||||
|
"iconfont": {
|
||||||
|
"text": "\ue600",
|
||||||
|
"selectedText": "\ue600",
|
||||||
|
"fontSize": "17px",
|
||||||
|
"color": "#333",
|
||||||
|
"selectedColor": "#f45a5a"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,36 +2,26 @@
|
|||||||
import {ref, onMounted, reactive} from 'vue';
|
import {ref, onMounted, reactive} from 'vue';
|
||||||
import {onLoad} from '@dcloudio/uni-app';
|
import {onLoad} from '@dcloudio/uni-app';
|
||||||
import {Toast} from 'tdesign-mobile-vue';
|
import {Toast} from 'tdesign-mobile-vue';
|
||||||
|
import {useRequest} from "@/utils/request";
|
||||||
|
|
||||||
|
const request = useRequest()
|
||||||
|
|
||||||
// 项目ID
|
// 项目ID
|
||||||
const itemId = ref(null);
|
const actId = ref(null);
|
||||||
// 详情数据
|
// 详情数据
|
||||||
const detailData = ref(null);
|
const detailData = ref(null);
|
||||||
// 加载状态
|
// 加载状态
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
// 当前活动选项卡
|
|
||||||
const activeTab = ref('detail');
|
|
||||||
// 是否显示PDF预览
|
// 是否显示PDF预览
|
||||||
const showPdfPreview = ref(false);
|
const showPdfPreview = ref(false);
|
||||||
// 当前预览的文件
|
// 当前预览的文件
|
||||||
const currentFile = ref({});
|
const currentFile = ref({});
|
||||||
// 是否显示报名表单
|
|
||||||
const showRegistrationForm = ref(false);
|
|
||||||
// 相关推荐列表
|
|
||||||
const relatedList = ref([]);
|
|
||||||
// 相关推荐是否加载中
|
|
||||||
const relatedLoading = ref(null);
|
|
||||||
// 是否有更多相关推荐
|
|
||||||
const hasMoreRelated = ref(true);
|
|
||||||
// 相关推荐页码
|
|
||||||
const relatedPage = ref(1);
|
|
||||||
|
|
||||||
// 获取路由参数
|
// 获取路由参数
|
||||||
onLoad((option) => {
|
onLoad((option) => {
|
||||||
if (option.id) {
|
if (option.id) {
|
||||||
itemId.value = option.id;
|
actId.value = option.id;
|
||||||
getDetailData(option.id);
|
getDetailData(option.id);
|
||||||
getRelatedList();
|
|
||||||
} else {
|
} else {
|
||||||
Toast({message: 'ID不存在', theme: 'error'});
|
Toast({message: 'ID不存在', theme: 'error'});
|
||||||
}
|
}
|
||||||
@@ -41,96 +31,104 @@ onLoad((option) => {
|
|||||||
const getDetailData = async (id) => {
|
const getDetailData = async (id) => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
// 模拟网络请求延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// 随机生成地点
|
const {code, data} = await request.get('/client/activity/get/' + id)
|
||||||
const locations = [
|
if (code === 0) {
|
||||||
'南京奥体中心',
|
detailData.value = data
|
||||||
'苏州国际博览中心',
|
} else {
|
||||||
'无锡太湖国际博览中心',
|
Toast({message: '获取详情失败', theme: 'error'});
|
||||||
'常州体育中心',
|
}
|
||||||
'镇江国际会展中心'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 模拟详情数据
|
// // 模拟网络请求延迟
|
||||||
detailData.value = {
|
// await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
id,
|
//
|
||||||
title: '奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑',
|
// // 随机生成地点
|
||||||
status: 1, // 1-报名中 2-已结束 3-即将开始
|
// const locations = [
|
||||||
banner: 'https://img0.baidu.com/it/u=1693652779,2163403752&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=800',
|
// '南京奥体中心',
|
||||||
price: 200,
|
// '苏州国际博览中心',
|
||||||
participants: 500,
|
// '无锡太湖国际博览中心',
|
||||||
maxParticipants: 800,
|
// '常州体育中心',
|
||||||
registrationPeriod: '2025.04.03 00:00:00\n2025.04.08 20:00:00',
|
// '镇江国际会展中心'
|
||||||
date: '2024.04.19 09:00:00\n2024.04.19 19:00:00',
|
// ];
|
||||||
location: '武汉市江夏区梁子湖龙湾半岛景区',
|
//
|
||||||
organizer: '靖江市文化广电和旅游局,靖江市体育总会',
|
// // 模拟详情数据
|
||||||
coorganizer: '靖江市疾病预防控制中心',
|
// detailData.value = {
|
||||||
desc: '本次活动是为了推动全民健身,促进城市体育文化建设的重要赛事。活动全程约5公里,沿途设有多个补给站和打卡点,参与者完成全程将获得完赛奖牌和证书。\n\n参赛者需提前30分钟到达集合地点,并携带有效身份证件。组委会将提供统一的参赛装备,包括荧光手环、荧光贴纸等。\n\n活动期间,参与者需遵守赛事规则,服从工作人员指挥,确保安全有序参与。',
|
// id,
|
||||||
competitions: [
|
// title: '奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑',
|
||||||
{
|
// status: 1, // 1-报名中 2-已结束 3-即将开始
|
||||||
name: '同心接力 聚力前行——飓风跑',
|
// banner: 'https://img0.baidu.com/it/u=1693652779,2163403752&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=800',
|
||||||
format: '6名参赛选手,横队站于起跑线后,双手握住长杆,共同向前跑,赛程为25米绕桩跑',
|
// price: 200,
|
||||||
rules: '以各小组完成赛程所用时间作为评分依据,用时越短,得分越高'
|
// participants: 500,
|
||||||
},
|
// maxParticipants: 800,
|
||||||
{
|
// registrationPeriod: '2025.04.03 00:00:00\n2025.04.08 20:00:00',
|
||||||
name: '鼓韵凝心 协作共赢——击鼓颠球',
|
// date: '2024.04.19 09:00:00\n2024.04.19 19:00:00',
|
||||||
format: '10名参赛选手,共同牵拉一面四周绑有若干绳索的圆形鼓。通过协作牵拉绳索,让鼓面有节奏地上下颠动,使排球在鼓面上持续跳动。',
|
// location: '武汉市江夏区梁子湖龙湾半岛景区',
|
||||||
rules: '以3分钟内各小组有效击球次数作为评分依据,击球次数越多,得分越高'
|
// organizer: '靖江市文化广电和旅游局,靖江市体育总会',
|
||||||
},
|
// coorganizer: '靖江市疾病预防控制中心',
|
||||||
{
|
// desc: '本次活动是为了推动全民健身,促进城市体育文化建设的重要赛事。活动全程约5公里,沿途设有多个补给站和打卡点,参与者完成全程将获得完赛奖牌和证书。\n\n参赛者需提前30分钟到达集合地点,并携带有效身份证件。组委会将提供统一的参赛装备,包括荧光手环、荧光贴纸等。\n\n活动期间,参与者需遵守赛事规则,服从工作人员指挥,确保安全有序参与。',
|
||||||
name: '慧眼寻同 携手并进——连连看',
|
// competitions: [
|
||||||
format: '一次派2位选手进入挑战区域,每位选手各自掀开一顶斗笠,核实斗笠内物品是否一致,一致可将物品带回,斗笠掀开;如物品不同则原位盖好',
|
// {
|
||||||
rules: '结合参赛小组的物品获得数量进行纵向排名'
|
// name: '同心接力 聚力前行——飓风跑',
|
||||||
},
|
// format: '6名参赛选手,横队站于起跑线后,双手握住长杆,共同向前跑,赛程为25米绕桩跑',
|
||||||
{
|
// rules: '以各小组完成赛程所用时间作为评分依据,用时越短,得分越高'
|
||||||
name: '方块解码 共绘家韵——智俄罗斯方块',
|
// },
|
||||||
format: '一次派2位选手携带一块拼图进场,进场途中跟守关裁判猜拳"输"返回起点线重新进入,"赢"携带道具进场拼图',
|
// {
|
||||||
rules: '结合参赛小组的用时长短进行纵向排名'
|
// name: '鼓韵凝心 协作共赢——击鼓颠球',
|
||||||
},
|
// format: '10名参赛选手,共同牵拉一面四周绑有若干绳索的圆形鼓。通过协作牵拉绳索,让鼓面有节奏地上下颠动,使排球在鼓面上持续跳动。',
|
||||||
{
|
// rules: '以3分钟内各小组有效击球次数作为评分依据,击球次数越多,得分越高'
|
||||||
name: '极速接力 暖心传递——小轮车',
|
// },
|
||||||
format: '6位参赛选手折返接力,直至6位选手互换场地',
|
// {
|
||||||
rules: '结合参赛小组的用时长短进行纵向排名'
|
// name: '慧眼寻同 携手并进——连连看',
|
||||||
},
|
// format: '一次派2位选手进入挑战区域,每位选手各自掀开一顶斗笠,核实斗笠内物品是否一致,一致可将物品带回,斗笠掀开;如物品不同则原位盖好',
|
||||||
{
|
// rules: '结合参赛小组的物品获得数量进行纵向排名'
|
||||||
name: '众志筑塔 逐梦远航——通天塔',
|
// },
|
||||||
format: '小组成员团队合作将8块立方体进行纵向叠高垒起,全部拼装完成后5秒倒计时,读秒结束高塔不倒即为完成',
|
// {
|
||||||
rules: '结合参赛小组的用时长短进行纵向排名'
|
// name: '方块解码 共绘家韵——智俄罗斯方块',
|
||||||
}
|
// format: '一次派2位选手携带一块拼图进场,进场途中跟守关裁判猜拳"输"返回起点线重新进入,"赢"携带道具进场拼图',
|
||||||
],
|
// rules: '结合参赛小组的用时长短进行纵向排名'
|
||||||
files: [
|
// },
|
||||||
{
|
// {
|
||||||
id: 1,
|
// name: '极速接力 暖心传递——小轮车',
|
||||||
name: '活动规则及参赛须知.pdf',
|
// format: '6位参赛选手折返接力,直至6位选手互换场地',
|
||||||
url: 'https://www.africau.edu/images/default/sample.pdf',
|
// rules: '结合参赛小组的用时长短进行纵向排名'
|
||||||
size: '1.2MB',
|
// },
|
||||||
date: '2024-03-15'
|
// {
|
||||||
},
|
// name: '众志筑塔 逐梦远航——通天塔',
|
||||||
{
|
// format: '小组成员团队合作将8块立方体进行纵向叠高垒起,全部拼装完成后5秒倒计时,读秒结束高塔不倒即为完成',
|
||||||
id: 2,
|
// rules: '结合参赛小组的用时长短进行纵向排名'
|
||||||
name: '参赛安全责任书.pdf',
|
// }
|
||||||
url: 'https://www.africau.edu/images/default/sample.pdf',
|
// ],
|
||||||
size: '580KB',
|
// files: [
|
||||||
date: '2024-03-15'
|
// {
|
||||||
},
|
// id: 1,
|
||||||
{
|
// name: '活动规则及参赛须知.pdf',
|
||||||
id: 3,
|
// url: 'https://www.africau.edu/images/default/sample.pdf',
|
||||||
name: '活动流程安排.pdf',
|
// size: '1.2MB',
|
||||||
url: 'https://www.africau.edu/images/default/sample.pdf',
|
// date: '2024-03-15'
|
||||||
size: '1.5MB',
|
// },
|
||||||
date: '2024-03-16'
|
// {
|
||||||
},
|
// id: 2,
|
||||||
{
|
// name: '参赛安全责任书.pdf',
|
||||||
id: 4,
|
// url: 'https://www.africau.edu/images/default/sample.pdf',
|
||||||
name: '比赛评分标准.pdf',
|
// size: '580KB',
|
||||||
url: 'https://www.africau.edu/images/default/sample.pdf',
|
// date: '2024-03-15'
|
||||||
size: '890KB',
|
// },
|
||||||
date: '2024-03-16'
|
// {
|
||||||
}
|
// id: 3,
|
||||||
]
|
// name: '活动流程安排.pdf',
|
||||||
};
|
// url: 'https://www.africau.edu/images/default/sample.pdf',
|
||||||
|
// size: '1.5MB',
|
||||||
|
// date: '2024-03-16'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// id: 4,
|
||||||
|
// name: '比赛评分标准.pdf',
|
||||||
|
// url: 'https://www.africau.edu/images/default/sample.pdf',
|
||||||
|
// size: '890KB',
|
||||||
|
// date: '2024-03-16'
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取详情失败', error);
|
console.error('获取详情失败', error);
|
||||||
Toast({message: '获取详情失败', theme: 'error'});
|
Toast({message: '获取详情失败', theme: 'error'});
|
||||||
@@ -139,85 +137,6 @@ const getDetailData = async (id) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 报名表单数据
|
|
||||||
const registration = reactive({
|
|
||||||
organization: '',
|
|
||||||
contactName: '',
|
|
||||||
contactPhone: '',
|
|
||||||
agreementChecked: false,
|
|
||||||
participants: [
|
|
||||||
{
|
|
||||||
name: '',
|
|
||||||
gender: '男',
|
|
||||||
position: '',
|
|
||||||
idNumber: '',
|
|
||||||
phone: '',
|
|
||||||
accommodation: false
|
|
||||||
}
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// 获取相关推荐列表
|
|
||||||
const getRelatedList = async () => {
|
|
||||||
relatedLoading.value = 'loading';
|
|
||||||
try {
|
|
||||||
// 模拟网络请求延迟
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
// 随机生成地点
|
|
||||||
const locations = [
|
|
||||||
'南京奥体中心',
|
|
||||||
'苏州国际博览中心',
|
|
||||||
'无锡太湖国际博览中心',
|
|
||||||
'常州体育中心',
|
|
||||||
'镇江国际会展中心'
|
|
||||||
];
|
|
||||||
|
|
||||||
// 模拟数据
|
|
||||||
const mockData = Array.from({length: 5}, (_, index) => {
|
|
||||||
const id = relatedPage.value * 5 + index;
|
|
||||||
const randomPrice = Math.floor(Math.random() * 150) + 30;
|
|
||||||
const randomParticipants = Math.floor(Math.random() * 500) + 50;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
title: index % 2 === 0 ? '2023江苏省青少年体育锦标赛' : '2023年冬季室内马拉松挑战赛',
|
|
||||||
status: Math.floor(Math.random() * 3) + 1,
|
|
||||||
date: `2023.${Math.floor(Math.random() * 12) + 1}.${Math.floor(Math.random() * 28) + 1}`,
|
|
||||||
endDate: `2023.${Math.floor(Math.random() * 12) + 1}.${Math.floor(Math.random() * 28) + 1}`,
|
|
||||||
image: `https://picsum.photos/800/450?random=${id + 100}`,
|
|
||||||
price: randomPrice,
|
|
||||||
participants: randomParticipants,
|
|
||||||
location: locations[Math.floor(Math.random() * locations.length)]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
relatedList.value = [...relatedList.value, ...mockData];
|
|
||||||
|
|
||||||
// 模拟无更多数据
|
|
||||||
if (relatedPage.value >= 2) {
|
|
||||||
hasMoreRelated.value = false;
|
|
||||||
} else {
|
|
||||||
relatedPage.value++;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取相关推荐失败', error);
|
|
||||||
} finally {
|
|
||||||
relatedLoading.value = null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 加载更多相关推荐
|
|
||||||
const loadMoreRelated = () => {
|
|
||||||
if (relatedLoading.value === 'loading' || !hasMoreRelated.value) return;
|
|
||||||
getRelatedList();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 切换选项卡
|
|
||||||
const switchTab = (tab) => {
|
|
||||||
activeTab.value = tab;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 打开PDF预览
|
// 打开PDF预览
|
||||||
const previewPdf = (file) => {
|
const previewPdf = (file) => {
|
||||||
currentFile.value = file;
|
currentFile.value = file;
|
||||||
@@ -229,101 +148,13 @@ const closePdfPreview = () => {
|
|||||||
showPdfPreview.value = false;
|
showPdfPreview.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 打开报名表单
|
function openRegistrationForm() {
|
||||||
const openRegistrationForm = () => {
|
|
||||||
// showRegistrationForm.value = true;
|
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: '/pages/activity/registration-form/index'
|
url: '/pages/activity/registration-form/index?id=' + actId.value
|
||||||
})
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
// 关闭报名表单
|
|
||||||
const closeRegistrationForm = () => {
|
|
||||||
showRegistrationForm.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 添加参赛人员
|
|
||||||
const addParticipant = () => {
|
|
||||||
registration.participants.push({
|
|
||||||
name: '',
|
|
||||||
gender: '男',
|
|
||||||
position: '',
|
|
||||||
idNumber: '',
|
|
||||||
phone: '',
|
|
||||||
accommodation: false
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 移除参赛人员
|
|
||||||
const removeParticipant = (index) => {
|
|
||||||
if (registration.participants.length > 1) {
|
|
||||||
registration.participants.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
Toast({message: '至少需要一名参赛人员', theme: 'warning'});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 提交报名信息
|
|
||||||
const submitRegistration = () => {
|
|
||||||
// 检查是否同意承诺书
|
|
||||||
if (!registration.agreementChecked) {
|
|
||||||
Toast({message: '请阅读并同意参赛承诺书', theme: 'warning'});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 这里可以添加表单验证逻辑
|
|
||||||
|
|
||||||
// 模拟提交成功
|
|
||||||
Toast({message: '报名成功!', theme: 'success'});
|
|
||||||
closeRegistrationForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
// 分享
|
|
||||||
const handleShare = () => {
|
|
||||||
Toast({message: '分享功能开发中', theme: 'warning'});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 切换到首页
|
|
||||||
const navigateToHome = () => {
|
|
||||||
uni.switchTab({
|
|
||||||
url: '/pages/index/index'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 状态标签文字
|
|
||||||
const getStatusText = (status) => {
|
|
||||||
switch (status) {
|
|
||||||
case 1:
|
|
||||||
return '报名中';
|
|
||||||
case 2:
|
|
||||||
return '已结束';
|
|
||||||
case 3:
|
|
||||||
return '即将开始';
|
|
||||||
default:
|
|
||||||
return '未知状态';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 报名或预约
|
|
||||||
const handleSignup = () => {
|
|
||||||
if (detailData.value.status === 2) {
|
|
||||||
Toast({message: '活动已结束', theme: 'warning'});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (detailData.value.status === 1) {
|
|
||||||
Toast({message: '报名成功', theme: 'success'});
|
|
||||||
} else {
|
|
||||||
Toast({message: '预约提醒成功', theme: 'success'});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 跳转到详情页
|
|
||||||
const navigateToDetail = (id) => {
|
|
||||||
uni.navigateTo({
|
|
||||||
url: `/pages/activity/detail/index?id=${id}`
|
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -332,7 +163,7 @@ const navigateToDetail = (id) => {
|
|||||||
<template v-if="detailData">
|
<template v-if="detailData">
|
||||||
<!-- 顶部图片 -->
|
<!-- 顶部图片 -->
|
||||||
<view class="event-header">
|
<view class="event-header">
|
||||||
<image :src="detailData.banner" mode="aspectFill" class="event-cover"/>
|
<image :src="detailData.cover" mode="aspectFill" class="event-cover"/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 活动标题卡片 -->
|
<!-- 活动标题卡片 -->
|
||||||
@@ -403,13 +234,13 @@ const navigateToDetail = (id) => {
|
|||||||
|
|
||||||
<t-tabs default-value="detail">
|
<t-tabs default-value="detail">
|
||||||
<t-tab-panel value="detail" label="详细信息">
|
<t-tab-panel value="detail" label="详细信息">
|
||||||
<view class="detail-text">{{ detailData.desc }}</view>
|
<view class="detail-text" v-html="detailData.content"></view>
|
||||||
</t-tab-panel>
|
</t-tab-panel>
|
||||||
<t-tab-panel value="events" label="比赛项目">
|
<t-tab-panel value="events" label="比赛项目">
|
||||||
<view class="event-table-mobile">
|
<view class="event-table-mobile">
|
||||||
<view
|
<view
|
||||||
class="project-card"
|
class="project-card"
|
||||||
v-for="(item, index) in detailData.competitions"
|
v-for="(item, index) in detailData.items"
|
||||||
:key="index"
|
:key="index"
|
||||||
>
|
>
|
||||||
<view class="project-header">
|
<view class="project-header">
|
||||||
@@ -419,11 +250,11 @@ const navigateToDetail = (id) => {
|
|||||||
<view class="project-details">
|
<view class="project-details">
|
||||||
<view class="detail-item">
|
<view class="detail-item">
|
||||||
<view class="detail-label">活动形式</view>
|
<view class="detail-label">活动形式</view>
|
||||||
<view class="detail-content">{{ item.format }}</view>
|
<view class="detail-content">{{ item.form }}</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="detail-item">
|
<view class="detail-item">
|
||||||
<view class="detail-label">评分规则</view>
|
<view class="detail-label">评分规则</view>
|
||||||
<view class="detail-content">{{ item.rules }}</view>
|
<view class="detail-content">{{ item.rule }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -459,238 +290,6 @@ const navigateToDetail = (id) => {
|
|||||||
立即报名
|
立即报名
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 报名表单 -->
|
|
||||||
<t-popup
|
|
||||||
v-model:visible="showRegistrationForm"
|
|
||||||
placement="center"
|
|
||||||
:close-on-overlay-click="true"
|
|
||||||
:prevent-scroll-through="true"
|
|
||||||
:show-overlay="true"
|
|
||||||
class="registration-dialog"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<view class="registration-container">
|
|
||||||
<view class="registration-header">
|
|
||||||
<view class="registration-title">活动报名</view>
|
|
||||||
<view class="registration-close" @click="closeRegistrationForm">
|
|
||||||
<t-icon name="close"/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<scroll-view class="registration-content" :scroll-y="true">
|
|
||||||
<view class="form-block">
|
|
||||||
<view class="form-block-title">
|
|
||||||
<t-icon name="user-circle"/>
|
|
||||||
单位信息
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">单位名称
|
|
||||||
<text class="required-mark">*</text>
|
|
||||||
</text>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="field-input"
|
|
||||||
placeholder="请输入单位名称"
|
|
||||||
v-model="registration.organization"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-row">
|
|
||||||
<view class="field-col">
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">联系人
|
|
||||||
<text class="required-mark">*</text>
|
|
||||||
</text>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="field-input"
|
|
||||||
placeholder="请输入姓名"
|
|
||||||
v-model="registration.contactName"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="field-col">
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">联系电话
|
|
||||||
<text class="required-mark">*</text>
|
|
||||||
</text>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="field-input"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
v-model="registration.contactPhone"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-block">
|
|
||||||
<view class="form-block-title">
|
|
||||||
<t-icon name="usergroup"/>
|
|
||||||
参赛人员
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view
|
|
||||||
class="participant-card"
|
|
||||||
v-for="(participant, index) in registration.participants"
|
|
||||||
:key="index"
|
|
||||||
>
|
|
||||||
<view class="participant-header">
|
|
||||||
<view class="participant-title">
|
|
||||||
<text>{{ index + 1 }}</text>
|
|
||||||
参赛人员信息
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="remove-participant"
|
|
||||||
v-if="registration.participants.length > 1"
|
|
||||||
@click="removeParticipant(index)"
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
<t-icon name="close-circle" size="16px"/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">姓名
|
|
||||||
<text class="required-mark">*</text>
|
|
||||||
</text>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="field-input"
|
|
||||||
placeholder="请输入姓名"
|
|
||||||
v-model="participant.name"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-field-horizontal">
|
|
||||||
<text class="field-label-horizontal">性别
|
|
||||||
<text class="required-mark">*</text>
|
|
||||||
</text>
|
|
||||||
<view class="field-content">
|
|
||||||
<view class="radio-buttons">
|
|
||||||
<view
|
|
||||||
class="radio-button"
|
|
||||||
:class="{ active: participant.gender === '男' }"
|
|
||||||
@click="participant.gender = '男'"
|
|
||||||
>
|
|
||||||
<t-icon name="check-circle" v-if="participant.gender === '男'"/>
|
|
||||||
<t-icon name="circle" v-else/>
|
|
||||||
<text>男</text>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="radio-button"
|
|
||||||
:class="{ active: participant.gender === '女' }"
|
|
||||||
@click="participant.gender = '女'"
|
|
||||||
>
|
|
||||||
<t-icon name="check-circle" v-if="participant.gender === '女'"/>
|
|
||||||
<t-icon name="circle" v-else/>
|
|
||||||
<text>女</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">职务</text>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="field-input"
|
|
||||||
placeholder="请输入职务"
|
|
||||||
v-model="participant.position"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-row">
|
|
||||||
<view class="field-col">
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">身份证号
|
|
||||||
<text class="required-mark">*</text>
|
|
||||||
</text>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="field-input"
|
|
||||||
placeholder="请输入身份证号"
|
|
||||||
v-model="participant.idNumber"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<view class="field-col">
|
|
||||||
<view class="form-field">
|
|
||||||
<text class="field-label">联系电话
|
|
||||||
<text class="required-mark">*</text>
|
|
||||||
</text>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="field-input"
|
|
||||||
placeholder="请输入手机号"
|
|
||||||
v-model="participant.phone"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="form-field-horizontal">
|
|
||||||
<text class="field-label-horizontal">是否住宿</text>
|
|
||||||
<view class="field-content">
|
|
||||||
<view class="radio-buttons">
|
|
||||||
<view
|
|
||||||
class="radio-button"
|
|
||||||
:class="{ active: participant.accommodation }"
|
|
||||||
@click="participant.accommodation = true"
|
|
||||||
>
|
|
||||||
<t-icon name="check-circle" v-if="participant.accommodation"/>
|
|
||||||
<t-icon name="circle" v-else/>
|
|
||||||
<text>是</text>
|
|
||||||
</view>
|
|
||||||
<view
|
|
||||||
class="radio-button"
|
|
||||||
:class="{ active: !participant.accommodation }"
|
|
||||||
@click="participant.accommodation = false"
|
|
||||||
>
|
|
||||||
<t-icon name="check-circle" v-if="!participant.accommodation"/>
|
|
||||||
<t-icon name="circle" v-else/>
|
|
||||||
<text>否</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="add-participant" @click="addParticipant">
|
|
||||||
<t-icon name="add-circle"/>
|
|
||||||
添加参赛人员
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<t-input label="标签文字" layout="vertical" placeholder="请输入姓名"></t-input>
|
|
||||||
|
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<view class="registration-footer">
|
|
||||||
<view class="agreement-checkbox">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
v-model="registration.agreementChecked"
|
|
||||||
/>
|
|
||||||
<label>我已阅读并同意
|
|
||||||
<text class="agreement-link">《参赛承诺书》</text>
|
|
||||||
</label>
|
|
||||||
</view>
|
|
||||||
<t-button
|
|
||||||
block
|
|
||||||
theme="primary"
|
|
||||||
size="large"
|
|
||||||
shape="round"
|
|
||||||
@click="submitRegistration"
|
|
||||||
>
|
|
||||||
提交报名
|
|
||||||
</t-button>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</t-popup>
|
|
||||||
</template>
|
</template>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
@@ -1113,268 +712,4 @@ const navigateToDetail = (id) => {
|
|||||||
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
|
box-shadow: 0 2px 8px rgba(231, 76, 60, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 报名表单样式 */
|
|
||||||
.registration-dialog {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-container {
|
|
||||||
background: white;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
max-height: 100vh;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px;
|
|
||||||
background-color: #e74c3c;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-title {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-close {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-content {
|
|
||||||
flex: 1;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-block {
|
|
||||||
padding: 20px 16px;
|
|
||||||
border-bottom: 10px solid #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-block-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
color: #333;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-block-title :deep(i) {
|
|
||||||
margin-right: 8px;
|
|
||||||
color: #e74c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 左右布局表单 */
|
|
||||||
.form-field-horizontal {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field-horizontal:last-child {
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-label-horizontal {
|
|
||||||
width: 80px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
font-weight: 500;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-content {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-label {
|
|
||||||
display: block;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-input {
|
|
||||||
width: 100%;
|
|
||||||
height: 48px;
|
|
||||||
border: none;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
font-size: 15px;
|
|
||||||
color: #333;
|
|
||||||
background: transparent;
|
|
||||||
transition: border-color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-input:focus {
|
|
||||||
border-color: #e74c3c;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-input::placeholder {
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-row {
|
|
||||||
display: flex;
|
|
||||||
gap: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.field-col {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-button {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 8px 12px;
|
|
||||||
border-radius: 6px;
|
|
||||||
flex: none;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-button.active {
|
|
||||||
border-color: #e74c3c;
|
|
||||||
background-color: rgba(231, 76, 60, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-button :deep(i) {
|
|
||||||
font-size: 18px;
|
|
||||||
margin-right: 8px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-button.active :deep(i) {
|
|
||||||
color: #e74c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participant-card {
|
|
||||||
background: white;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
position: relative;
|
|
||||||
border-left: 3px solid #e74c3c;
|
|
||||||
padding-left: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participant-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participant-title {
|
|
||||||
font-weight: 500;
|
|
||||||
color: #333;
|
|
||||||
font-size: 15px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.participant-title text {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
background-color: #e74c3c;
|
|
||||||
color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
font-size: 14px;
|
|
||||||
margin-right: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.remove-participant {
|
|
||||||
color: #999;
|
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-participant {
|
|
||||||
padding: 15px;
|
|
||||||
text-align: center;
|
|
||||||
color: #e74c3c;
|
|
||||||
font-weight: 500;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-participant :deep(i) {
|
|
||||||
margin-right: 8px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-footer {
|
|
||||||
padding: 16px;
|
|
||||||
background-color: white;
|
|
||||||
box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.required-mark {
|
|
||||||
color: #e74c3c;
|
|
||||||
margin-left: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agreement-checkbox {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding: 0 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agreement-checkbox input[type="checkbox"] {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
margin-right: 8px;
|
|
||||||
accent-color: #e74c3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agreement-checkbox label {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
|
||||||
.registration-container {
|
|
||||||
max-width: 500px;
|
|
||||||
max-height: 90vh;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.registration-header {
|
|
||||||
border-radius: 12px 12px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,224 +1,248 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container">
|
<view class="page-container">
|
||||||
<!-- 顶部搜索区域 -->
|
<!-- 顶部搜索和筛选 -->
|
||||||
<view class="header">
|
<view class="header">
|
||||||
|
<view class="search-container">
|
||||||
<t-search
|
<t-search
|
||||||
v-model="pageForm.searchKeyword"
|
v-model="pageForm.searchKeyword"
|
||||||
placeholder="搜索活动"
|
placeholder="搜索活动名称"
|
||||||
shape="round"
|
shape="round"
|
||||||
class="search"
|
class="search-input"
|
||||||
>
|
@search="handleSearch"
|
||||||
<template #action>
|
/>
|
||||||
<t-button variant="text" @click="filterVisible = true">筛选</t-button>
|
|
||||||
</template>
|
|
||||||
</t-search>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 筛选弹出层 -->
|
<!-- 状态筛选标签组 -->
|
||||||
<t-popup
|
<view class="filter-tabs">
|
||||||
v-model:visible="filterVisible"
|
<scroll-view scroll-x class="tabs-scroll-view" :show-scrollbar="false">
|
||||||
placement="top"
|
<view class="tabs-container">
|
||||||
>
|
<view
|
||||||
<view class="filter">
|
|
||||||
<view class="filter-header">
|
|
||||||
<t-button variant="text" @click="handleClearFilter">重置</t-button>
|
|
||||||
<text class="filter-title">筛选条件</text>
|
|
||||||
<t-button theme="primary" variant="text" @click="handleSearch">确定</t-button>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<t-cell-group title="状态">
|
|
||||||
<t-radio-group v-model="pageForm.status">
|
|
||||||
<t-radio
|
|
||||||
v-for="option in statusOptions"
|
v-for="option in statusOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
:value="option.value"
|
class="filter-tab"
|
||||||
:label="option.label"
|
:class="{ active: pageForm.status === option.value }"
|
||||||
borderless
|
@click="selectStatusFilter(option.value)"
|
||||||
/>
|
|
||||||
</t-radio-group>
|
|
||||||
</t-cell-group>
|
|
||||||
</view>
|
|
||||||
</t-popup>
|
|
||||||
|
|
||||||
<!-- 活动列表 -->
|
|
||||||
<scroll-view
|
|
||||||
style="overflow-y: auto;flex: 1"
|
|
||||||
:scroll-y="true"
|
|
||||||
:show-scrollbar="false"
|
|
||||||
@scrolltolower="onLoadMore"
|
|
||||||
:refresher-enabled="true"
|
|
||||||
@refresherrefresh="getListData(true)"
|
|
||||||
:refresher-triggered="refreshIng"
|
|
||||||
refresher-background="#f5f5f5"
|
|
||||||
>
|
>
|
||||||
<view class="card"
|
{{ option.label }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 活动列表 - 使用scroll-view实现 -->
|
||||||
|
<scroll-view
|
||||||
|
scroll-y
|
||||||
|
class="list-scroll-view"
|
||||||
|
@scrolltolower="handleScrollToBottom"
|
||||||
|
refresher-enabled
|
||||||
|
:refresher-triggered="refresherTriggered"
|
||||||
|
@refresherrefresh="handleRefresh"
|
||||||
|
refresher-background="#f2f3f5"
|
||||||
|
>
|
||||||
|
<view class="list-container">
|
||||||
|
<view
|
||||||
|
class="activity-card"
|
||||||
v-for="item in dataList"
|
v-for="item in dataList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
@click="navigateToDetail(item.id)"
|
@click="navigateToDetail(item.id)"
|
||||||
>
|
>
|
||||||
<view class="card-head">
|
<view class="image-container">
|
||||||
<image :src="item.image" mode="aspectFill" class="card-img"/>
|
<image :src="item.cover" mode="aspectFill" class="activity-image"/>
|
||||||
<text class="card-tag" :class="getStatusClass(item.status)">
|
<view class="activity-time">
|
||||||
{{ getStatusText(item.status) }}
|
{{ formatTimeRange(item.startTime, item.endTime) }}
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<view class="status-tag-container">
|
||||||
<view class="card-body">
|
<t-tag
|
||||||
<view class="card-title">{{ item.title }}</view>
|
:theme="getTagTheme(getActivityStatus(item))"
|
||||||
|
size="medium"
|
||||||
<view class="card-info">
|
shape="mark"
|
||||||
<view class="info-item">
|
>{{ getStatusText(getActivityStatus(item)) }}
|
||||||
<t-icon name="alarm" size="12px"/>
|
</t-tag>
|
||||||
<text>{{ item.date }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="info-item">
|
|
||||||
<t-icon name="location" size="12px"/>
|
|
||||||
<text>{{ item.location }}</text>
|
|
||||||
</view>
|
|
||||||
<view class="info-item">
|
|
||||||
<t-icon name="user" size="12px"/>
|
|
||||||
<text>{{ item.participants }}人已报名</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="card-footer">
|
<view class="activity-info">
|
||||||
<view class="price">
|
<text class="activity-title">{{ item.title }}</text>
|
||||||
<text class="price-symbol">¥</text>
|
<view class="activity-address">
|
||||||
<text class="price-value">{{ item.price }}</text>
|
<t-icon name="location" size="small"/>
|
||||||
|
<text>{{ item.address || '暂无地址信息' }}</text>
|
||||||
</view>
|
</view>
|
||||||
<t-button
|
|
||||||
size="small"
|
|
||||||
shape="round"
|
|
||||||
:theme="item.status === 2 ? 'default' : 'primary'"
|
|
||||||
:disabled="item.status === 2"
|
|
||||||
>
|
|
||||||
{{ item.status === 1 ? '立即报名' : item.status === 3 ? '预约提醒' : '已结束' }}
|
|
||||||
</t-button>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="loading-area">
|
<!-- 加载状态 -->
|
||||||
<t-loading v-if="loading" text="加载中" layout="vertical"/>
|
<view class="loading-container">
|
||||||
<t-divider v-else-if="!hasMore && dataList.length > 0" content="没有更多了"/>
|
<t-loading v-if="loading" text="加载中..." layout="vertical" theme="circular" size="40rpx"/>
|
||||||
|
<t-divider v-else-if="!hasMore && dataList.length > 0" content="没有更多活动了"/>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<t-empty v-if="!loading && !refreshIng && dataList.length === 0"
|
<!-- 空状态 -->
|
||||||
image="https://tdesign.gtimg.com/mobile/demos/empty1.png" description="暂无活动">
|
<t-empty
|
||||||
<template #action>
|
v-if="!loading && dataList.length === 0"
|
||||||
<t-button theme="primary" size="small" shape="round">刷新试试</t-button>
|
description="暂无活动信息"
|
||||||
</template>
|
/>
|
||||||
</t-empty>
|
|
||||||
</scroll-view>
|
</scroll-view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref, reactive, onMounted} from 'vue';
|
import {ref, reactive, onMounted} from 'vue';
|
||||||
|
// 移除不再需要的onReachBottom和onPullDownRefresh
|
||||||
|
// import {onReachBottom, onPullDownRefresh} from "@dcloudio/uni-app";
|
||||||
import {Toast} from 'tdesign-mobile-vue';
|
import {Toast} from 'tdesign-mobile-vue';
|
||||||
import {useRequest} from "@/util/request";
|
import {useRequest} from "@/utils/request";
|
||||||
|
import moment from "moment";
|
||||||
|
|
||||||
// 筛选选项
|
// 活动状态筛选选项
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{value: '', label: '全部'},
|
{value: '', label: '全部'},
|
||||||
{value: '1', label: '进行中'},
|
{value: 'notStartSign', label: '未开始报名'},
|
||||||
{value: '2', label: '已结束'},
|
{value: 'signing', label: '报名中'},
|
||||||
{value: '3', label: '未开始'}
|
{value: 'notStartActivity', label: '活动未开始'},
|
||||||
|
{value: 'activitying', label: '活动进行中'},
|
||||||
|
{value: 'ended', label: '已结束'}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// 页面表单数据
|
||||||
const pageForm = reactive({
|
const pageForm = reactive({
|
||||||
pageNumber: 1,
|
pageNumber: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
totalCount: 0,
|
totalCount: 0,
|
||||||
searchKeyword: null,
|
searchKeyword: '',
|
||||||
status: null
|
status: '' // 全部
|
||||||
})
|
});
|
||||||
|
|
||||||
// 数据与状态
|
// 数据与状态
|
||||||
const dataList = ref([]);
|
const dataList = ref([]);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const hasMore = ref(true);
|
const hasMore = ref(true);
|
||||||
const refreshIng = ref(false);
|
// 添加下拉刷新状态
|
||||||
const filterVisible = ref(false);
|
const refresherTriggered = ref(false);
|
||||||
|
|
||||||
// 搜索表单
|
// 获取活动状态
|
||||||
const searchForm = reactive({
|
const getActivityStatus = (item) => {
|
||||||
keyword: '',
|
const now = moment();
|
||||||
status: ''
|
const signStartTime = moment(item.signStartTime);
|
||||||
});
|
const signEndTime = moment(item.signEndTime);
|
||||||
|
const activityStartTime = moment(item.startTime);
|
||||||
|
const activityEndTime = moment(item.endTime);
|
||||||
|
|
||||||
// 获取状态样式类
|
if (now.isBefore(signStartTime)) {
|
||||||
const getStatusClass = (status) => {
|
return 'notStartSign'; // 未开始报名
|
||||||
return {
|
} else if (now.isBetween(signStartTime, signEndTime)) {
|
||||||
1: 'status-active',
|
return 'signing'; // 报名中
|
||||||
2: 'status-ended',
|
} else if (now.isBetween(signEndTime, activityStartTime)) {
|
||||||
3: 'status-upcoming'
|
return 'notStartActivity'; // 活动未开始
|
||||||
}[status] || '';
|
} else if (now.isBetween(activityStartTime, activityEndTime)) {
|
||||||
|
return 'activitying'; // 活动进行中
|
||||||
|
} else {
|
||||||
|
return 'ended'; // 已结束
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 获取状态文本
|
// 获取状态文本
|
||||||
const getStatusText = (status) => {
|
const getStatusText = (status) => {
|
||||||
return {
|
return {
|
||||||
1: '报名中',
|
'notStartSign': '未开始报名',
|
||||||
2: '已结束',
|
'signing': '报名中',
|
||||||
3: '即将开始'
|
'notStartActivity': '活动未开始',
|
||||||
|
'activitying': '活动进行中',
|
||||||
|
'ended': '已结束'
|
||||||
}[status] || '未知状态';
|
}[status] || '未知状态';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 获取标签主题
|
||||||
|
const getTagTheme = (status) => {
|
||||||
|
return {
|
||||||
|
'notStartSign': 'primary',
|
||||||
|
'signing': 'success',
|
||||||
|
'notStartActivity': 'warning',
|
||||||
|
'activitying': 'info',
|
||||||
|
'ended': 'default'
|
||||||
|
}[status] || 'default';
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化时间范围
|
||||||
|
const formatTimeRange = (startTime, endTime) => {
|
||||||
|
return `${moment(startTime).format('MM-DD')} ~ ${moment(endTime).format('MM-DD')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 选择状态筛选
|
||||||
|
const selectStatusFilter = (status) => {
|
||||||
|
pageForm.status = status;
|
||||||
|
handleSearch();
|
||||||
|
};
|
||||||
|
|
||||||
// 获取列表数据
|
// 获取列表数据
|
||||||
const getListData = async (isRefresh = false) => {
|
const getListData = async (isRefresh = false) => {
|
||||||
|
if (loading.value) return;
|
||||||
|
|
||||||
|
loading.value = true;
|
||||||
|
|
||||||
if (isRefresh) {
|
if (isRefresh) {
|
||||||
refreshIng.value = true;
|
|
||||||
pageForm.pageNumber = 1;
|
pageForm.pageNumber = 1;
|
||||||
} else {
|
|
||||||
loading.value = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 延时1秒,模拟网络请求
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {code, data, msg} = await useRequest().post('/client/activity/pageData', {
|
const response = await useRequest().post('/client/activity/pageData', {
|
||||||
...pageForm
|
...pageForm
|
||||||
})
|
});
|
||||||
|
|
||||||
|
if (response.code === 0) {
|
||||||
|
const {list, totalCount} = response.data;
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
if (isRefresh) {
|
if (isRefresh) {
|
||||||
dataList.value = data.list;
|
dataList.value = list;
|
||||||
} else {
|
} else {
|
||||||
dataList.value = [...dataList.value, ...data.list];
|
dataList.value = [...dataList.value, ...list];
|
||||||
}
|
}
|
||||||
hasMore.value = dataList.value.length < data.totalCount;
|
|
||||||
|
pageForm.totalCount = totalCount;
|
||||||
|
hasMore.value = dataList.value.length < totalCount;
|
||||||
pageForm.pageNumber++;
|
pageForm.pageNumber++;
|
||||||
console.log(hasMore.value)
|
|
||||||
} else {
|
} else {
|
||||||
console.error('获取列表失败', msg)
|
Toast({message: '获取活动列表失败', theme: 'error'});
|
||||||
Toast({message: '获取列表失败', theme: 'error'});
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取列表失败', error);
|
console.error('获取活动列表失败', error);
|
||||||
Toast({message: '获取列表失败', theme: 'error'});
|
Toast({message: '获取活动列表失败', theme: 'error'});
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
refreshIng.value = false;
|
|
||||||
|
// 重置下拉刷新状态
|
||||||
|
if (refresherTriggered.value) {
|
||||||
|
refresherTriggered.value = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// scroll-view 下拉刷新
|
||||||
|
const handleRefresh = () => {
|
||||||
|
if (loading.value) {
|
||||||
|
refresherTriggered.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
refresherTriggered.value = true;
|
||||||
|
pageForm.pageNumber = 1;
|
||||||
|
getListData(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 搜索处理
|
// 搜索处理
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
|
pageForm.pageNumber = 1;
|
||||||
hasMore.value = true;
|
hasMore.value = true;
|
||||||
dataList.value = [];
|
dataList.value = [];
|
||||||
getListData();
|
getListData(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清空筛选
|
// scroll-view 触底加载更多
|
||||||
const handleClearFilter = () => {
|
const handleScrollToBottom = () => {
|
||||||
pageForm.keyword = null
|
|
||||||
pageForm.status = null
|
|
||||||
filterVisible.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
// 加载更多
|
|
||||||
const onLoadMore = () => {
|
|
||||||
if (loading.value || !hasMore.value) return;
|
if (loading.value || !hasMore.value) return;
|
||||||
getListData();
|
getListData();
|
||||||
};
|
};
|
||||||
@@ -236,137 +260,141 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style lang="less" scoped>
|
||||||
.header {
|
.header {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
padding: 10px 16px;
|
padding: 16rpx 0;
|
||||||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search-container {
|
||||||
--td-search-bg-color: #f0f0f0;
|
padding: 0 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter {
|
.search-input {
|
||||||
background-color: #fff;
|
width: 100%;
|
||||||
border-radius: 0 0 16px 16px;
|
|
||||||
padding: 0 16px 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-header {
|
.filter-tabs {
|
||||||
|
margin-top: 20rpx;
|
||||||
|
padding: 0 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-scroll-view {
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-container {
|
||||||
|
display: inline-flex;
|
||||||
|
padding: 0 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-tab {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10rpx 24rpx;
|
||||||
|
margin: 0 12rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 48rpx;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: #fff;
|
||||||
|
background-color: var(--td-brand-color);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-scroll-view {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-direction: column;
|
||||||
align-items: center;
|
padding: 20rpx 24rpx;
|
||||||
padding: 16px 0;
|
|
||||||
border-bottom: 1px solid #eee;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-title {
|
.activity-card {
|
||||||
font-weight: bold;
|
width: 100%;
|
||||||
font-size: 16px;
|
margin-bottom: 24rpx;
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
border-radius: 12px;
|
border-radius: 16rpx;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||||
margin: 10px;
|
transition: transform 0.3s;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-head {
|
.image-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
height: 160px;
|
height: 360rpx;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-img {
|
.activity-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
transition: transform 0.4s;
|
||||||
|
|
||||||
|
.activity-card:hover & {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-tag {
|
.activity-time {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
left: 16rpx;
|
||||||
right: 0;
|
bottom: 16rpx;
|
||||||
padding: 4px 12px;
|
padding: 6rpx 16rpx;
|
||||||
|
background-color: rgba(0, 0, 0, 0.6);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 12px;
|
font-size: 22rpx;
|
||||||
font-weight: bold;
|
border-radius: 6rpx;
|
||||||
border-radius: 4px 0 0 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-active {
|
.activity-info {
|
||||||
background-color: #28c445;
|
padding: 20rpx 24rpx 10rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-ended {
|
.activity-title {
|
||||||
background-color: #999;
|
width: 100%;
|
||||||
}
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
.status-upcoming {
|
|
||||||
background-color: #ff9800;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-body {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-title {
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: #333;
|
color: #333;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-info {
|
.activity-address {
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-size: 13px;
|
font-size: 26rpx;
|
||||||
color: #666;
|
color: #666;
|
||||||
margin-bottom: 6px;
|
margin-top: 12rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item :deep(i) {
|
.activity-address :deep(.t-icon) {
|
||||||
margin-right: 6px;
|
margin-right: 8rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-footer {
|
.status-tag-container {
|
||||||
display: flex;
|
position: absolute;
|
||||||
justify-content: space-between;
|
right: 0;
|
||||||
align-items: center;
|
top: 16rpx;
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
padding-top: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.price {
|
.loading-container {
|
||||||
display: flex;
|
padding: 40rpx 0;
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-symbol {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #ff5050;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-value {
|
|
||||||
font-size: 22px;
|
|
||||||
color: #ff5050;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-area {
|
|
||||||
padding: 16px 0;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="page-container">
|
<view class="page-container">
|
||||||
|
|
||||||
<view class="act-info">
|
<!-- <view class="act-info">-->
|
||||||
<view class="act-title">奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑</view>
|
<!-- <view class="act-title">奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑</view>-->
|
||||||
<view class="act-meta">
|
<!-- <view class="act-meta">-->
|
||||||
<view class="act-meta-item">22222</view>
|
<!-- <view class="act-meta-item">22222</view>-->
|
||||||
<view class="act-meta-item">22222222222222222222</view>
|
<!-- <view class="act-meta-item">22222222222222222222</view>-->
|
||||||
</view>
|
<!-- </view>-->
|
||||||
</view>
|
<!-- </view>-->
|
||||||
|
|
||||||
<view class="form-area">
|
<view class="form-area">
|
||||||
<t-form
|
<t-form
|
||||||
@@ -49,9 +49,9 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<t-form-item label="姓名" :name="`userList[${index}].name`"
|
<t-form-item label="姓名" :name="`userList[${index}].userName`"
|
||||||
:rules="[{required:true,message: '请输入姓名'}]">
|
:rules="[{required:true,message: '请输入姓名'}]">
|
||||||
<t-input v-model="user.name" borderless placeholder="请输入内容"></t-input>
|
<t-input v-model="user.userName" borderless placeholder="请输入内容"></t-input>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="性别" :name="`userList[${index}].sex`" :rules="[{required:true,message: '请选择性别'}]">
|
<t-form-item label="性别" :name="`userList[${index}].sex`" :rules="[{required:true,message: '请选择性别'}]">
|
||||||
<t-radio-group v-model="user.sex" borderless class="radio-box">
|
<t-radio-group v-model="user.sex" borderless class="radio-box">
|
||||||
@@ -59,8 +59,8 @@
|
|||||||
<t-radio :block="false" label="女" value="女"/>
|
<t-radio :block="false" label="女" value="女"/>
|
||||||
</t-radio-group>
|
</t-radio-group>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="职务" :name="`userList[${index}].job`" :rules="[{required:true,message: '请输入职务'}]">
|
<t-form-item label="职务" :name="`userList[${index}].position`" :rules="[{required:true,message: '请输入职务'}]">
|
||||||
<t-input v-model="user.job" borderless placeholder="请输入职务"></t-input>
|
<t-input v-model="user.position" borderless placeholder="请输入职务"></t-input>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="身份证号" :name="`userList[${index}].idCard`"
|
<t-form-item label="身份证号" :name="`userList[${index}].idCard`"
|
||||||
:rules="[{required:true,message: '请输入身份证号码'},{idcard:true, message: '请输入正确的身份证号码'}]">
|
:rules="[{required:true,message: '请输入身份证号码'},{idcard:true, message: '请输入正确的身份证号码'}]">
|
||||||
@@ -70,9 +70,9 @@
|
|||||||
:rules="[{required:true,message: '请输入联系电话'},{telnumber:true, message: '请输入正确的联系电话'}]">
|
:rules="[{required:true,message: '请输入联系电话'},{telnumber:true, message: '请输入正确的联系电话'}]">
|
||||||
<t-input v-model="user.phone" borderless placeholder="请输入联系电话"></t-input>
|
<t-input v-model="user.phone" borderless placeholder="请输入联系电话"></t-input>
|
||||||
</t-form-item>
|
</t-form-item>
|
||||||
<t-form-item label="是否住宿" :name="`userList[${index}].sleep`"
|
<t-form-item label="是否住宿" :name="`userList[${index}].isStay`"
|
||||||
:rules="[{required:true,message: '请选择是否住宿'}]">
|
:rules="[{required:true,message: '请选择是否住宿'}]">
|
||||||
<t-radio-group v-model="user.sleep" class="radio-box">
|
<t-radio-group v-model="user.isStay" class="radio-box">
|
||||||
<t-radio :block="false" name="radio" value="1" label="是"/>
|
<t-radio :block="false" name="radio" value="1" label="是"/>
|
||||||
<t-radio :block="false" name="radio" value="2" label="否"/>
|
<t-radio :block="false" name="radio" value="2" label="否"/>
|
||||||
</t-radio-group>
|
</t-radio-group>
|
||||||
@@ -108,7 +108,10 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import {ref} from 'vue'
|
import {ref} from 'vue'
|
||||||
|
import {useRequest} from '@/utils/request';
|
||||||
|
import {Toast} from "tdesign-mobile-vue";
|
||||||
|
|
||||||
|
const request = useRequest()
|
||||||
const agreementChecked = ref(false)
|
const agreementChecked = ref(false)
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const formData = ref({
|
const formData = ref({
|
||||||
@@ -116,10 +119,24 @@ const formData = ref({
|
|||||||
userList: []
|
userList: []
|
||||||
})
|
})
|
||||||
|
|
||||||
function onSubmit() {
|
async function onSubmit() {
|
||||||
console.log(formRef)
|
console.log(formRef)
|
||||||
console.log(formRef.value)
|
console.log(formRef.value)
|
||||||
formRef.value.validate()
|
formRef.value.validate()
|
||||||
|
try{
|
||||||
|
const {code,data} = await request.post('/client/activity/sign', formData.value)
|
||||||
|
if (code === 0) {
|
||||||
|
Toast.success('报名成功')
|
||||||
|
// setTimeout(() => {
|
||||||
|
// uni.navigateBack()
|
||||||
|
// }, 1500)
|
||||||
|
}else{
|
||||||
|
Toast.error(data.msg)
|
||||||
|
}
|
||||||
|
}catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
Toast.error(e.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -171,7 +188,7 @@ function onSubmit() {
|
|||||||
.form-area{
|
.form-area{
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
margin-top: 100px;
|
//margin-top: 100px;
|
||||||
margin-bottom: 130px;
|
margin-bottom: 130px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,516 @@
|
|||||||
|
<template>
|
||||||
|
<view class="sign-page">
|
||||||
|
<!-- 背景装饰 -->
|
||||||
|
<view class="bg-decoration"></view>
|
||||||
|
|
||||||
|
<!-- 主内容区 -->
|
||||||
|
<view class="content-container">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<view class="title-area">
|
||||||
|
<text class="title">活动签到</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 活动选择区 -->
|
||||||
|
<view class="selector-card">
|
||||||
|
<t-dropdown-menu class="activity-dropdown">
|
||||||
|
<t-dropdown-item
|
||||||
|
v-model="selectedActivity"
|
||||||
|
label="请选择活动"
|
||||||
|
:options="activityOptions"
|
||||||
|
@change="handleActivityChange"
|
||||||
|
/>
|
||||||
|
</t-dropdown-menu>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 签到区域 -->
|
||||||
|
<view class="sign-card" v-if="currentActivity">
|
||||||
|
<view class="activity-title">{{ currentActivity.title }}</view>
|
||||||
|
|
||||||
|
<view class="qr-scan-area">
|
||||||
|
<view class="qr-frame">
|
||||||
|
<view class="corner top-left"></view>
|
||||||
|
<view class="corner top-right"></view>
|
||||||
|
<view class="corner bottom-left"></view>
|
||||||
|
<view class="corner bottom-right"></view>
|
||||||
|
<t-icon name="qrcode" size="120rpx" class="qr-icon" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<t-button size="large" class="scan-btn" @tap="handleScan" :disabled="isSignedIn">
|
||||||
|
{{ isSignedIn ? '已完成签到' : '点击扫码签到' }}
|
||||||
|
</t-button>
|
||||||
|
|
||||||
|
<view class="sign-status" v-if="signStatus !== 'unsigned'">
|
||||||
|
<t-icon name="check-circle-filled" size="48rpx" class="status-icon success" v-if="signStatus === 'signed'" />
|
||||||
|
<t-icon name="time" size="48rpx" class="status-icon warning" v-else-if="signStatus === 'late'" />
|
||||||
|
<text class="status-text">{{ signStatusText }}</text>
|
||||||
|
<text class="sign-time" v-if="latestSignTime">{{ latestSignTime }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 无活动状态 -->
|
||||||
|
<view class="empty-state" v-if="!currentActivity">
|
||||||
|
<t-icon name="folder" size="120rpx" class="empty-icon" />
|
||||||
|
<text class="empty-text">暂无可签到的活动</text>
|
||||||
|
<t-button size="small" variant="outline" class="refresh-btn" @tap="getMyActivities">刷新</t-button>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue';
|
||||||
|
import { useRequest } from '@/utils/request';
|
||||||
|
import { Toast } from 'tdesign-mobile-vue';
|
||||||
|
import moment from 'moment';
|
||||||
|
|
||||||
|
// 模拟活动数据
|
||||||
|
const mockActivities = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
title: '2023年度篮球联赛',
|
||||||
|
cover: 'https://img01.71360.com/file/read/www/M00/4C/98/rBsAFWQUdqaABB-oAAHYfGPYE3Y799.jpg',
|
||||||
|
startTime: '2023-12-10 09:00:00',
|
||||||
|
endTime: '2023-12-15 18:00:00',
|
||||||
|
address: '市体育中心主场馆',
|
||||||
|
signStartTime: '2023-11-20 00:00:00',
|
||||||
|
signEndTime: '2023-12-05 23:59:59',
|
||||||
|
signedIn: false,
|
||||||
|
signTime: null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
title: '企业文化交流会',
|
||||||
|
cover: 'https://img.zcool.cn/community/01a4d75b04a27ea801209e1f0fb01d.jpg@1280w_1l_2o_100sh.jpg',
|
||||||
|
startTime: '2023-11-25 13:30:00',
|
||||||
|
endTime: '2023-11-25 17:30:00',
|
||||||
|
address: '国际会议中心3号厅',
|
||||||
|
signStartTime: '2023-11-01 00:00:00',
|
||||||
|
signEndTime: '2023-11-24 23:59:59',
|
||||||
|
signedIn: true,
|
||||||
|
signTime: '2023-11-25 13:20:14',
|
||||||
|
status: 'signed'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3',
|
||||||
|
title: '技术创新大赛决赛',
|
||||||
|
cover: 'https://img.zcool.cn/community/01b1f95b04a255a8012182a7ebfe26.jpg@1280w_1l_2o_100sh.jpg',
|
||||||
|
startTime: '2023-12-05 09:00:00',
|
||||||
|
endTime: '2023-12-05 18:00:00',
|
||||||
|
address: '科技园区创新中心',
|
||||||
|
signStartTime: '2023-11-10 00:00:00',
|
||||||
|
signEndTime: '2023-12-04 23:59:59',
|
||||||
|
signedIn: true,
|
||||||
|
signTime: '2023-12-05 09:15:22',
|
||||||
|
status: 'late'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '4',
|
||||||
|
title: '2023年迎新晚会',
|
||||||
|
cover: 'https://img.zcool.cn/community/01f2b35bd123b1a801208f8bd8710a.jpg@1280w_1l_2o_100sh.jpg',
|
||||||
|
startTime: '2023-12-31 19:00:00',
|
||||||
|
endTime: '2024-01-01 00:30:00',
|
||||||
|
address: '文化广场中心舞台',
|
||||||
|
signStartTime: '2023-12-15 00:00:00',
|
||||||
|
signEndTime: '2023-12-30 23:59:59',
|
||||||
|
signedIn: false,
|
||||||
|
signTime: null
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// 判断环境是否为开发环境
|
||||||
|
const isDev = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
// 活动列表
|
||||||
|
const myActivities = ref([]);
|
||||||
|
const activityOptions = computed(() => {
|
||||||
|
return myActivities.value.map(item => ({
|
||||||
|
label: item.title,
|
||||||
|
value: item.id
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 选中的活动ID
|
||||||
|
const selectedActivity = ref('');
|
||||||
|
const currentActivity = computed(() => {
|
||||||
|
return myActivities.value.find(item => item.id === selectedActivity.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 是否已签到
|
||||||
|
const isSignedIn = computed(() => {
|
||||||
|
return currentActivity.value?.signedIn || false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 最近签到时间
|
||||||
|
const latestSignTime = computed(() => {
|
||||||
|
if (!currentActivity.value?.signTime) return null;
|
||||||
|
return `签到时间: ${moment(currentActivity.value.signTime).format('YYYY-MM-DD HH:mm:ss')}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 签到状态
|
||||||
|
const signStatus = computed(() => {
|
||||||
|
return currentActivity.value?.status || 'unsigned';
|
||||||
|
});
|
||||||
|
|
||||||
|
const signStatusText = computed(() => {
|
||||||
|
switch(signStatus.value) {
|
||||||
|
case 'signed':
|
||||||
|
return '签到成功';
|
||||||
|
case 'late':
|
||||||
|
return '迟到签到';
|
||||||
|
default:
|
||||||
|
return '未签到';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取我的活动
|
||||||
|
const getMyActivities = async () => {
|
||||||
|
try {
|
||||||
|
Toast({ message: '加载中...', theme: 'loading' });
|
||||||
|
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境使用模拟数据
|
||||||
|
setTimeout(() => {
|
||||||
|
myActivities.value = mockActivities;
|
||||||
|
if (myActivities.value.length > 0) {
|
||||||
|
selectedActivity.value = myActivities.value[0].id;
|
||||||
|
}
|
||||||
|
Toast({ message: '加载成功', theme: 'success' });
|
||||||
|
}, 800);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境调用接口
|
||||||
|
const response = await useRequest().get('/client/activity/myActivities');
|
||||||
|
if (response.code === 0) {
|
||||||
|
myActivities.value = response.data || [];
|
||||||
|
if (myActivities.value.length > 0) {
|
||||||
|
selectedActivity.value = myActivities.value[0].id;
|
||||||
|
}
|
||||||
|
Toast({ message: '加载成功', theme: 'success' });
|
||||||
|
} else {
|
||||||
|
Toast({ message: '获取活动失败', theme: 'error' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取活动失败', error);
|
||||||
|
Toast({ message: '网络异常,请重试', theme: 'error' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 活动切换
|
||||||
|
const handleActivityChange = (value) => {
|
||||||
|
// 切换时不需要额外操作
|
||||||
|
};
|
||||||
|
|
||||||
|
// 扫码
|
||||||
|
const handleScan = () => {
|
||||||
|
if (!currentActivity.value) {
|
||||||
|
Toast({ message: '请先选择活动', theme: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSignedIn.value) {
|
||||||
|
Toast({ message: '已完成签到', theme: 'warning' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast({ message: '准备扫码', theme: 'loading' });
|
||||||
|
setTimeout(() => {
|
||||||
|
if (isDev) {
|
||||||
|
// 开发环境模拟扫码成功
|
||||||
|
setTimeout(() => {
|
||||||
|
Toast({ message: '签到中...', theme: 'loading' });
|
||||||
|
setTimeout(() => {
|
||||||
|
const randomSuccess = Math.random() > 0.2;
|
||||||
|
if (randomSuccess) {
|
||||||
|
const isLate = Math.random() > 0.7;
|
||||||
|
const status = isLate ? 'late' : 'signed';
|
||||||
|
const statusText = isLate ? '迟到签到' : '签到成功';
|
||||||
|
|
||||||
|
// 更新活动签到状态
|
||||||
|
const activity = myActivities.value.find(a => a.id === selectedActivity.value);
|
||||||
|
if (activity) {
|
||||||
|
activity.signedIn = true;
|
||||||
|
activity.signTime = moment().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
activity.status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast({ message: statusText, theme: isLate ? 'warning' : 'success' });
|
||||||
|
} else {
|
||||||
|
Toast({ message: '二维码无效', theme: 'error' });
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
}, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境实际扫码
|
||||||
|
uni.scanCode({
|
||||||
|
success: async (res) => {
|
||||||
|
try {
|
||||||
|
Toast({ message: '签到中...', theme: 'loading' });
|
||||||
|
// 将扫码结果发送到后端
|
||||||
|
const response = await useRequest().post('/client/activity/sign', {
|
||||||
|
activityId: selectedActivity.value,
|
||||||
|
qrCode: res.result
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.code === 0) {
|
||||||
|
// 更新活动签到状态
|
||||||
|
const activity = myActivities.value.find(a => a.id === selectedActivity.value);
|
||||||
|
if (activity) {
|
||||||
|
activity.signedIn = true;
|
||||||
|
activity.signTime = moment().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
activity.status = response.data.status || 'signed';
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusText = activity.status === 'late' ? '迟到签到' : '签到成功';
|
||||||
|
Toast({ message: statusText, theme: activity.status === 'late' ? 'warning' : 'success' });
|
||||||
|
} else {
|
||||||
|
Toast({ message: response.message || '签到失败', theme: 'error' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('签到失败', error);
|
||||||
|
Toast({ message: '网络异常,请重试', theme: 'error' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
Toast({ message: '扫码取消', theme: 'warning' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getMyActivities();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.sign-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
padding-top: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg-decoration {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 380rpx;
|
||||||
|
background: linear-gradient(to bottom, rgb(244, 90, 90, 0.8), rgb(244, 90, 90, 0.2));
|
||||||
|
border-bottom-left-radius: 40rpx;
|
||||||
|
border-bottom-right-radius: 40rpx;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0 30rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-area {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 48rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-card {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 24rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-dropdown {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-menu) {
|
||||||
|
background-color: transparent !important;
|
||||||
|
height: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-menu__item) {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.t-dropdown-item__content) {
|
||||||
|
border-radius: 12rpx !important;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-card {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
padding: 40rpx 30rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.06);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activity-title {
|
||||||
|
font-size: 34rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-scan-area {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
padding: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-frame {
|
||||||
|
width: 280rpx;
|
||||||
|
height: 280rpx;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(244, 90, 90, 0.03);
|
||||||
|
border-radius: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 40rpx;
|
||||||
|
height: 40rpx;
|
||||||
|
border-color: rgb(244, 90, 90);
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-left {
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
border-top-width: 6rpx;
|
||||||
|
border-left-width: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top-right {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
border-top-width: 6rpx;
|
||||||
|
border-right-width: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-left {
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
border-bottom-width: 6rpx;
|
||||||
|
border-left-width: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom-right {
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
border-bottom-width: 6rpx;
|
||||||
|
border-right-width: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.qr-icon {
|
||||||
|
color: rgb(244, 90, 90);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-btn {
|
||||||
|
width: 70%;
|
||||||
|
height: 90rpx;
|
||||||
|
background-color: rgb(244, 90, 90);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
border-radius: 45rpx;
|
||||||
|
box-shadow: 0 8rpx 20rpx rgba(244, 90, 90, 0.3);
|
||||||
|
transition: all 0.3s;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-btn:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
box-shadow: 0 4rpx 10rpx rgba(244, 90, 90, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-btn[disabled] {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
color: #bbbbbb;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-status {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.success {
|
||||||
|
color: #07c160;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-icon.warning {
|
||||||
|
color: #ff9f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sign-time {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
color: #ddd;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.refresh-btn {
|
||||||
|
color: rgb(244, 90, 90);
|
||||||
|
border-color: rgb(244, 90, 90);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -59,7 +59,6 @@
|
|||||||
<div class="member-name">
|
<div class="member-name">
|
||||||
{{ member.name }}
|
{{ member.name }}
|
||||||
<span class="member-role" v-if="member.role === 'leader'">队长</span>
|
<span class="member-role" v-if="member.role === 'leader'">队长</span>
|
||||||
<span class="member-role" v-else-if="member.role === 'admin'" style="background-color: #ff9a5a;">管理员</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="member-detail">
|
<div class="member-detail">
|
||||||
<div class="member-detail-item">
|
<div class="member-detail-item">
|
||||||
@@ -80,17 +79,18 @@
|
|||||||
|
|
||||||
<!-- 当前角色与申请队长 -->
|
<!-- 当前角色与申请队长 -->
|
||||||
<div v-if="currentUserRole !== 'leader'">
|
<div v-if="currentUserRole !== 'leader'">
|
||||||
<div class="role-notice" v-if="currentUserRole === 'admin'">
|
<div class="role-notice">
|
||||||
<i class="bi bi-info-circle"></i>
|
|
||||||
您当前是管理员身份,拥有团队管理权限
|
|
||||||
</div>
|
|
||||||
<div class="role-notice" v-else>
|
|
||||||
<i class="bi bi-info-circle"></i>
|
<i class="bi bi-info-circle"></i>
|
||||||
您当前是普通成员身份
|
您当前是普通成员身份
|
||||||
</div>
|
</div>
|
||||||
<button class="apply-leader-btn" @click="showLeaderConfirm">
|
<t-button block
|
||||||
|
theme="primary"
|
||||||
|
variant="outline"
|
||||||
|
size="large"
|
||||||
|
shape="round"
|
||||||
|
@click="leaderConfirmVisible = true">
|
||||||
<i class="bi bi-award"></i>申请成为队长
|
<i class="bi bi-award"></i>申请成为队长
|
||||||
</button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="role-notice" v-else>
|
<div class="role-notice" v-else>
|
||||||
<i class="bi bi-check-circle"></i>
|
<i class="bi bi-check-circle"></i>
|
||||||
@@ -100,15 +100,32 @@
|
|||||||
|
|
||||||
<!-- 项目报名按钮 -->
|
<!-- 项目报名按钮 -->
|
||||||
<div class="team-section">
|
<div class="team-section">
|
||||||
<button class="register-btn" @click="goToEventRegistration">
|
<t-button block
|
||||||
|
theme="primary"
|
||||||
|
size="large"
|
||||||
|
shape="round" @click="goToEventRegistration">
|
||||||
<i class="bi bi-calendar-check"></i>
|
<i class="bi bi-calendar-check"></i>
|
||||||
项目报名
|
项目报名
|
||||||
</button>
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<t-dialog
|
||||||
|
v-model:visible="leaderConfirmVisible"
|
||||||
|
title="申请成为队长"
|
||||||
|
cancel-btn="取消"
|
||||||
|
confirm-btn="确认"
|
||||||
|
@confirm="onLeaderConfirm"
|
||||||
|
@cancel="leaderConfirmVisible = false"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<p>申请成为队长前,请确认您已与团队其他成员商议并达成一致意见。</p>
|
||||||
|
<p>一个团队仅能有一名队长,作为队长,您可以帮助队员一键报名项目。</p>
|
||||||
|
</template>
|
||||||
|
</t-dialog>
|
||||||
|
|
||||||
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -124,7 +141,7 @@ const teamId = ''
|
|||||||
|
|
||||||
const team = reactive({
|
const team = reactive({
|
||||||
id: teamId,
|
id: teamId,
|
||||||
name: '江苏健康跑团',
|
name: 'xx队',
|
||||||
logoUrl: null,
|
logoUrl: null,
|
||||||
createdDate: '2023-10-15',
|
createdDate: '2023-10-15',
|
||||||
eventId: '',
|
eventId: '',
|
||||||
@@ -132,7 +149,7 @@ const team = reactive({
|
|||||||
memberCount: 12,
|
memberCount: 12,
|
||||||
rank: 5,
|
rank: 5,
|
||||||
score: 256,
|
score: 256,
|
||||||
description: '我们是一支来自江苏省的健康跑团队,致力于推广健康跑步文化,鼓励大家积极参与体育活动,共同展现江苏的活力风采。',
|
description: 'xxxxxxxxxxxxxxxxxxx',
|
||||||
|
|
||||||
// 团队成员
|
// 团队成员
|
||||||
members: [
|
members: [
|
||||||
@@ -179,6 +196,9 @@ const team = reactive({
|
|||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const currentUserRole = ref('')
|
||||||
|
// 申请成为队长弹框提示
|
||||||
|
const leaderConfirmVisible = ref(false)
|
||||||
|
|
||||||
function handleMenuOpened(){
|
function handleMenuOpened(){
|
||||||
|
|
||||||
@@ -190,6 +210,12 @@ function actChange(val){
|
|||||||
console.log(val)
|
console.log(val)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 申请成为队长
|
||||||
|
function onLeaderConfirm(){
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToEventRegistration(){}
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -310,4 +336,127 @@ function actChange(val){
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: #666;
|
color: #666;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* 团队成员列表 */
|
||||||
|
.team-section {
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title i {
|
||||||
|
margin-right: 8px;
|
||||||
|
color: #e74c3c;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list {
|
||||||
|
background: white;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-item {
|
||||||
|
display: flex;
|
||||||
|
padding: 15px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
margin-right: 15px;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-avatar img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-role {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 12px;
|
||||||
|
background-color: #e74c3c;
|
||||||
|
color: white;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-detail {
|
||||||
|
display: flex;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-detail-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-detail-item i {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-actions {
|
||||||
|
color: #999;
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-notice {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
padding: 12px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #666;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.role-notice i {
|
||||||
|
color: #ffa63f;
|
||||||
|
margin-right: 8px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view class="menu-text">赛事报名</view>
|
<view class="menu-text">赛事报名</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="menu-item" @click="navigateTo('/pages/scan/index')">
|
<view class="menu-item" @click="navigateTo('/pages/activity/scan-sign/index')">
|
||||||
<view class="menu-icon-wrapper orange-bg">
|
<view class="menu-icon-wrapper orange-bg">
|
||||||
<view class="menu-icon qrcode-icon"></view>
|
<view class="menu-icon qrcode-icon"></view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
<template>
|
||||||
|
<view class="login-container">
|
||||||
|
<image
|
||||||
|
class="banner-image"
|
||||||
|
src="https://res.firstui.cn/template/loginB/login/img_banner_3x.png"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<view class="login-form">
|
||||||
|
<text class="title">登录</text>
|
||||||
|
|
||||||
|
<t-input
|
||||||
|
class="input-item"
|
||||||
|
v-model="loginForm.phone"
|
||||||
|
placeholder="请输入手机号码"
|
||||||
|
>
|
||||||
|
</t-input>
|
||||||
|
|
||||||
|
<t-input
|
||||||
|
class="input-item"
|
||||||
|
v-model="loginForm.smsCode"
|
||||||
|
placeholder="请输入验证码"
|
||||||
|
>
|
||||||
|
<template #suffix>
|
||||||
|
<t-button variant="text" class="verify-btn" @click="getSmsCode" :disabled="isCountingDown">
|
||||||
|
{{ countDownText }}
|
||||||
|
</t-button>
|
||||||
|
</template>
|
||||||
|
</t-input>
|
||||||
|
|
||||||
|
<!-- <view class="login-options">-->
|
||||||
|
<!-- <text>账号登录</text>-->
|
||||||
|
<!-- <text>注册</text>-->
|
||||||
|
<!-- </view>-->
|
||||||
|
|
||||||
|
<t-button block theme="primary" size="large" class="login-btn" @click="handleLogin">
|
||||||
|
登录
|
||||||
|
</t-button>
|
||||||
|
|
||||||
|
<!-- <view class="other-login">-->
|
||||||
|
<!-- <text class="other-text">其他登录方式</text>-->
|
||||||
|
<!-- <view class="login-methods">-->
|
||||||
|
<!-- <t-button shape="circle" variant="outline" icon="qq" class="method-btn" />-->
|
||||||
|
<!-- <t-button shape="circle" variant="outline" icon="wechat" class="method-btn" />-->
|
||||||
|
<!-- <t-button shape="circle" variant="outline" icon="image" class="method-btn" />-->
|
||||||
|
<!-- </view>-->
|
||||||
|
<!-- </view>-->
|
||||||
|
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {ref, reactive, computed, onMounted, onBeforeUnmount} from 'vue'
|
||||||
|
import {Toast} from 'tdesign-mobile-vue';
|
||||||
|
import {useRequest} from "@/utils/request";
|
||||||
|
|
||||||
|
const request = useRequest()
|
||||||
|
|
||||||
|
const loginForm = reactive({
|
||||||
|
phone: '18571562330',
|
||||||
|
smsCode: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 倒计时相关
|
||||||
|
const countdown = ref(0)
|
||||||
|
const timer = ref(null)
|
||||||
|
|
||||||
|
// 本地存储键名
|
||||||
|
const SMS_COUNTDOWN_KEY = '__SMS_COUNTDOWN__'
|
||||||
|
const SMS_PHONE_KEY = '__SMS_PHONE__'
|
||||||
|
|
||||||
|
// 是否在倒计时中
|
||||||
|
const isCountingDown = computed(() => countdown.value > 0)
|
||||||
|
// 验证码按钮文字
|
||||||
|
const countDownText = computed(() => {
|
||||||
|
return isCountingDown.value ? `${countdown.value}秒后重试` : '获取验证码'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 保存倒计时状态到本地存储
|
||||||
|
const saveCountdownState = (seconds, phone) => {
|
||||||
|
if (seconds <= 0) {
|
||||||
|
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
|
||||||
|
uni.removeStorageSync(SMS_PHONE_KEY)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存结束时间戳和手机号
|
||||||
|
const endTime = Date.now() + seconds * 1000
|
||||||
|
uni.setStorageSync(SMS_COUNTDOWN_KEY, endTime)
|
||||||
|
uni.setStorageSync(SMS_PHONE_KEY, phone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从本地存储恢复倒计时状态
|
||||||
|
const restoreCountdownState = () => {
|
||||||
|
const endTime = uni.getStorageSync(SMS_COUNTDOWN_KEY)
|
||||||
|
const savedPhone = uni.getStorageSync(SMS_PHONE_KEY)
|
||||||
|
|
||||||
|
if (endTime && savedPhone && savedPhone === loginForm.phone) {
|
||||||
|
const now = Date.now()
|
||||||
|
const remainingSeconds = Math.floor((endTime - now) / 1000)
|
||||||
|
|
||||||
|
if (remainingSeconds > 0) {
|
||||||
|
countdown.value = remainingSeconds
|
||||||
|
startCountDownTimer() // 重新启动定时器
|
||||||
|
} else {
|
||||||
|
// 倒计时已结束,清除存储
|
||||||
|
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
|
||||||
|
uni.removeStorageSync(SMS_PHONE_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始倒计时定时器
|
||||||
|
const startCountDownTimer = () => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
timer.value = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
|
||||||
|
// 清除存储
|
||||||
|
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
|
||||||
|
uni.removeStorageSync(SMS_PHONE_KEY)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开始倒计时
|
||||||
|
const startCountDown = () => {
|
||||||
|
countdown.value = 60
|
||||||
|
saveCountdownState(60, loginForm.phone)
|
||||||
|
startCountDownTimer()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 获取验证码
|
||||||
|
async function getSmsCode() {
|
||||||
|
if (!loginForm.phone) {
|
||||||
|
Toast.error('请输入手机号码');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCountingDown.value) return
|
||||||
|
try {
|
||||||
|
const {code, data, msg} = await request.post('/client/auth/send-sms-code/' + loginForm.phone)
|
||||||
|
if (code === 0) {
|
||||||
|
Toast.success('验证码发送成功')
|
||||||
|
// 开始倒计时
|
||||||
|
startCountDown()
|
||||||
|
} else {
|
||||||
|
Toast.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
if (!loginForm.phone) {
|
||||||
|
Toast.error('请输入手机号码');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!loginForm.smsCode) {
|
||||||
|
Toast.error('请输入验证码');
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {data, code, msg} = await request.post('/client/auth/sms-login', loginForm)
|
||||||
|
if (code === 0) {
|
||||||
|
Toast.success('登录成功')
|
||||||
|
// 保存token
|
||||||
|
uni.setStorageSync('token', data)
|
||||||
|
|
||||||
|
// 获取之前保存的重定向URL
|
||||||
|
const redirectUrl = uni.getStorageSync('REDIRECT_URL') || '/pages/index/index'
|
||||||
|
uni.removeStorageSync('REDIRECT_URL') // 使用后删除
|
||||||
|
|
||||||
|
// 延迟跳转,让用户看到成功提示
|
||||||
|
setTimeout(() => {
|
||||||
|
if (redirectUrl.startsWith('/pages/index/') || redirectUrl.startsWith('/pages/profile/')) {
|
||||||
|
// 主页和个人中心,使用switchTab
|
||||||
|
uni.switchTab({
|
||||||
|
url: redirectUrl
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// 其他页面使用reLaunch
|
||||||
|
uni.reLaunch({
|
||||||
|
url: redirectUrl
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
} else {
|
||||||
|
Toast.error(msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
Toast.error(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时恢复倒计时状态
|
||||||
|
onMounted(() => {
|
||||||
|
restoreCountdownState()
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面卸载前清理定时器
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (timer.value) {
|
||||||
|
clearInterval(timer.value)
|
||||||
|
timer.value = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.login-container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #fff;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 260px;
|
||||||
|
background-color: #4080ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-form {
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 20px 0;
|
||||||
|
display: block;
|
||||||
|
color: var(--td-brand-color-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.country-code {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-btn {
|
||||||
|
color: var(--td-brand-color-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-login {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.other-text {
|
||||||
|
color: #999;
|
||||||
|
font-size: 14px;
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 60px;
|
||||||
|
height: 1px;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: -70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
right: -70px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-methods {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 30px;
|
||||||
|
margin-top: 20px;
|
||||||
|
|
||||||
|
.method-btn {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
我的页面
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
|
|
||||||
</style>
|
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
<script setup>
|
||||||
|
import {ref, onMounted} from 'vue';
|
||||||
|
import {Toast, Dialog} from 'tdesign-mobile-vue';
|
||||||
|
import {useRequest} from "@/utils/request";
|
||||||
|
|
||||||
|
// 用户资料数据
|
||||||
|
const userInfo = ref({
|
||||||
|
id: '10001',
|
||||||
|
name: '张三',
|
||||||
|
avatar: '/static/avatar.png',
|
||||||
|
phone: '188****8888',
|
||||||
|
company: '腾讯科技有限公司'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 我的活动数据
|
||||||
|
const activities = ref({
|
||||||
|
enrolled: 6
|
||||||
|
});
|
||||||
|
|
||||||
|
// 退出登录
|
||||||
|
const handleLogout = () => {
|
||||||
|
Dialog.confirm({
|
||||||
|
title: '退出登录',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
confirmBtn: '确定',
|
||||||
|
cancelBtn: '取消',
|
||||||
|
onConfirm: () => {
|
||||||
|
useRequest().post('/client/auth/logout').then(res=>{
|
||||||
|
if(res.code===0){
|
||||||
|
uni.clearStorageSync()
|
||||||
|
Toast({message: '退出登录成功', theme: 'success'});
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login/index'
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}else{
|
||||||
|
Toast({message: '退出登录失败', theme: 'error'});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 跳转页面
|
||||||
|
const navigateTo = (path) => {
|
||||||
|
uni.navigateTo({url: path});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理编辑资料
|
||||||
|
const handleEditProfile = () => {
|
||||||
|
navigateTo('/pages/profile/edit/index');
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// 获取用户信息
|
||||||
|
console.log('个人资料页面已加载');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<view class="profile-page">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<t-navbar title="个人中心" titleAlign="center"/>
|
||||||
|
|
||||||
|
<!-- 用户基本信息区域 -->
|
||||||
|
<view class="user-profile-section">
|
||||||
|
<view class="user-info-container">
|
||||||
|
<view class="avatar-wrapper">
|
||||||
|
<t-avatar :image="userInfo.avatar" size="large"/>
|
||||||
|
</view>
|
||||||
|
<view class="user-info">
|
||||||
|
<view class="user-name-row">
|
||||||
|
<text class="user-name">{{ userInfo.name }}</text>
|
||||||
|
<view class="edit-profile" @click="handleEditProfile">
|
||||||
|
<t-icon name="edit" size="20"/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="user-company">{{ userInfo.company }}</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 功能区域 -->
|
||||||
|
<view class="function-section">
|
||||||
|
<t-cell-group>
|
||||||
|
<t-cell
|
||||||
|
title="已报名活动"
|
||||||
|
arrow
|
||||||
|
@click="navigateTo('/pages/activity/list/index?type=enrolled')"
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
<template #leftIcon>
|
||||||
|
<t-icon name="calendar" size="24"/>
|
||||||
|
</template>
|
||||||
|
<template #rightIcon>
|
||||||
|
<view class="badge">{{ activities.enrolled }}</view>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
|
||||||
|
<t-cell
|
||||||
|
title="我的团队"
|
||||||
|
arrow
|
||||||
|
@click="navigateTo('/pages/activity/team/index')"
|
||||||
|
hover
|
||||||
|
>
|
||||||
|
<template #leftIcon>
|
||||||
|
<t-icon name="usergroup" size="24"/>
|
||||||
|
</template>
|
||||||
|
</t-cell>
|
||||||
|
</t-cell-group>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 退出登录按钮 -->
|
||||||
|
<view class="logout-btn">
|
||||||
|
<t-button theme="danger" size="large" block @click="handleLogout">退出登录</t-button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 底部版权信息 -->
|
||||||
|
<view class="footer">
|
||||||
|
<t-divider>活动报名系统 v1.0.0</t-divider>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.profile-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
padding-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-section {
|
||||||
|
padding: 30rpx 40rpx;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
background: linear-gradient(120deg, #ffffff, #f8f8f8);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-profile-section::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: -15rpx;
|
||||||
|
height: 30rpx;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border-radius: 50% 50% 0 0 / 100% 100% 0 0;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
margin-right: 30rpx;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper :deep(.t-avatar) {
|
||||||
|
width: 150rpx !important;
|
||||||
|
height: 150rpx !important;
|
||||||
|
border: 4rpx solid #ffffff;
|
||||||
|
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
padding-top: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-name {
|
||||||
|
font-size: 44rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #000000;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-profile {
|
||||||
|
width: 44rpx;
|
||||||
|
height: 44rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #0052d9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-company {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-department {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.function-section {
|
||||||
|
margin: 30rpx 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
background-color: #ffffff;
|
||||||
|
box-shadow: 0 4rpx 10rpx rgba(0, 0, 0, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background-color: #ff4d4f;
|
||||||
|
color: white;
|
||||||
|
font-size: 22rpx;
|
||||||
|
height: 34rpx;
|
||||||
|
min-width: 34rpx;
|
||||||
|
line-height: 34rpx;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 17rpx;
|
||||||
|
padding: 0 10rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-btn {
|
||||||
|
margin: 60rpx 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: 40rpx 0;
|
||||||
|
color: #999;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Binary file not shown.
@@ -23,6 +23,7 @@ export function useRequest() {
|
|||||||
const token = uni.getStorageSync('token')
|
const token = uni.getStorageSync('token')
|
||||||
const headers = {
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest',
|
||||||
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
||||||
...options.header
|
...options.header
|
||||||
}
|
}
|
||||||
@@ -35,9 +36,21 @@ export function useRequest() {
|
|||||||
timeout: options.timeout || 10000,
|
timeout: options.timeout || 10000,
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
if(res.data.code === 0){
|
||||||
resolve(res.data as T)
|
resolve(res.data as T)
|
||||||
|
}else if(res.data.code === 600098){
|
||||||
|
// 未登录
|
||||||
|
uni.removeStorageSync('token')
|
||||||
|
uni.reLaunch({
|
||||||
|
url: '/pages/login/index'
|
||||||
|
})
|
||||||
|
}else {
|
||||||
|
const err = new Error(res.data?.msg || '请求失败')
|
||||||
|
error.value = err
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
const err = new Error(res.data?.message || '请求失败')
|
const err = new Error(res.data?.msg || '请求失败')
|
||||||
error.value = err
|
error.value = err
|
||||||
reject(err)
|
reject(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,317 @@
|
|||||||
|
import { reactive } from "vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局请求状态管理
|
||||||
|
*/
|
||||||
|
export const requestState = reactive({
|
||||||
|
// 所有正在加载的请求ID映射表
|
||||||
|
loadingRequests: {},
|
||||||
|
|
||||||
|
// 是否有任何请求正在加载中
|
||||||
|
get isLoading() {
|
||||||
|
return Object.keys(this.loadingRequests).length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 当前正在加载的请求数量
|
||||||
|
get loadingCount() {
|
||||||
|
return Object.keys(this.loadingRequests).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 所有错误请求ID映射表
|
||||||
|
errorRequests: {},
|
||||||
|
|
||||||
|
// 是否有任何请求出错
|
||||||
|
get hasError() {
|
||||||
|
return Object.keys(this.errorRequests).length > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 当前错误的请求数量
|
||||||
|
get errorCount() {
|
||||||
|
return Object.keys(this.errorRequests).length;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 重置所有状态
|
||||||
|
reset() {
|
||||||
|
this.loadingRequests = {};
|
||||||
|
this.errorRequests = {};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成唯一的请求ID
|
||||||
|
* @returns {string} 唯一ID
|
||||||
|
*/
|
||||||
|
function generateRequestId() {
|
||||||
|
return Date.now() + "-" + Math.random().toString(36).substr(2, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求开始处理
|
||||||
|
* @param {string} requestId - 请求ID
|
||||||
|
*/
|
||||||
|
function handleRequestStart(requestId) {
|
||||||
|
requestState.loadingRequests[requestId] = true;
|
||||||
|
// 从错误列表中删除,因为请求重新开始了
|
||||||
|
delete requestState.errorRequests[requestId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求结束处理
|
||||||
|
* @param {string} requestId - 请求ID
|
||||||
|
*/
|
||||||
|
function handleRequestEnd(requestId) {
|
||||||
|
delete requestState.loadingRequests[requestId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求错误处理
|
||||||
|
* @param {string} requestId - 请求ID
|
||||||
|
* @param {object} error - 错误对象
|
||||||
|
*/
|
||||||
|
function handleRequestError(requestId, error) {
|
||||||
|
requestState.errorRequests[requestId] = error;
|
||||||
|
handleRequestEnd(requestId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示错误提示
|
||||||
|
* @param {object} error - 错误对象
|
||||||
|
* @param {boolean} showErrorToast - 是否显示错误提示
|
||||||
|
*/
|
||||||
|
function showError(error, showErrorToast) {
|
||||||
|
if (showErrorToast) {
|
||||||
|
// 获取错误消息
|
||||||
|
const message = error.errMsg || error.message || "请求失败";
|
||||||
|
uni.showToast({
|
||||||
|
title: message,
|
||||||
|
icon: "none",
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
console.error("Request Error:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用增强的请求功能
|
||||||
|
* @param {object} options - 请求配置
|
||||||
|
* @returns {object} 包含请求方法的对象
|
||||||
|
*/
|
||||||
|
export function useRequest(options = {}) {
|
||||||
|
const defaultOptions = {
|
||||||
|
baseUrl: "/api",
|
||||||
|
showErrorToast: true,
|
||||||
|
timeout: 60000,
|
||||||
|
retryCount: 0,
|
||||||
|
retryDelay: 1000,
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行HTTP请求
|
||||||
|
* @param {object} config - 请求配置
|
||||||
|
* @returns {Promise} 请求Promise
|
||||||
|
*/
|
||||||
|
const request = (config) => {
|
||||||
|
const requestId = generateRequestId();
|
||||||
|
let retries = 0;
|
||||||
|
|
||||||
|
// 合并配置
|
||||||
|
const mergedConfig = {
|
||||||
|
...defaultOptions,
|
||||||
|
...config,
|
||||||
|
header: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Requested-With": "XMLHttpRequest",
|
||||||
|
...defaultOptions.header,
|
||||||
|
...config.header
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 构建完整URL
|
||||||
|
if (mergedConfig.url && !mergedConfig.url.startsWith("http")) {
|
||||||
|
mergedConfig.url = `${mergedConfig.baseUrl}${mergedConfig.url}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行实际请求
|
||||||
|
* @returns {Promise} 请求Promise
|
||||||
|
*/
|
||||||
|
const executeRequest = () => {
|
||||||
|
handleRequestStart(requestId);
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
uni.request({
|
||||||
|
...mergedConfig,
|
||||||
|
success: (response) => {
|
||||||
|
const { statusCode, data } = response;
|
||||||
|
|
||||||
|
// 成功状态码处理
|
||||||
|
if (statusCode >= 200 && statusCode < 300) {
|
||||||
|
// API业务逻辑成功
|
||||||
|
if (data.code === 0 || data.code === 200 || data.status === 200) {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
// API业务逻辑失败
|
||||||
|
else {
|
||||||
|
const error = {
|
||||||
|
code: data.code || statusCode,
|
||||||
|
message: data.message || data.msg || "请求失败",
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 特殊错误码处理
|
||||||
|
if (data.code === 600098) {
|
||||||
|
// Token失效,清除存储并跳转登录
|
||||||
|
uni.removeStorageSync("token");
|
||||||
|
uni.removeStorageSync("userInfo");
|
||||||
|
|
||||||
|
uni.showToast({
|
||||||
|
title: "登录已过期,请重新登录",
|
||||||
|
icon: "none",
|
||||||
|
duration: 2000
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.navigateTo({ url: "/pages/login/index" });
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
showError(error, mergedConfig.showErrorToast);
|
||||||
|
handleRequestError(requestId, error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// HTTP错误状态码
|
||||||
|
else {
|
||||||
|
const error = {
|
||||||
|
code: statusCode,
|
||||||
|
message: `HTTP错误: ${statusCode}`,
|
||||||
|
data: data
|
||||||
|
};
|
||||||
|
|
||||||
|
showError(error, mergedConfig.showErrorToast);
|
||||||
|
|
||||||
|
// 是否需要重试
|
||||||
|
if (retries < mergedConfig.retryCount) {
|
||||||
|
retries++;
|
||||||
|
setTimeout(() => {
|
||||||
|
handleRequestEnd(requestId);
|
||||||
|
retryRequest();
|
||||||
|
}, mergedConfig.retryDelay);
|
||||||
|
} else {
|
||||||
|
handleRequestError(requestId, error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (error) => {
|
||||||
|
showError(error, mergedConfig.showErrorToast);
|
||||||
|
|
||||||
|
// 是否需要重试
|
||||||
|
if (retries < mergedConfig.retryCount) {
|
||||||
|
retries++;
|
||||||
|
setTimeout(() => {
|
||||||
|
handleRequestEnd(requestId);
|
||||||
|
retryRequest();
|
||||||
|
}, mergedConfig.retryDelay);
|
||||||
|
} else {
|
||||||
|
handleRequestError(requestId, error);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
complete: () => {
|
||||||
|
if (retries === 0 || retries >= mergedConfig.retryCount) {
|
||||||
|
handleRequestEnd(requestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重试请求
|
||||||
|
* @returns {Promise} 重试请求Promise
|
||||||
|
*/
|
||||||
|
const retryRequest = () => {
|
||||||
|
console.log(`重试请求 (${retries}/${mergedConfig.retryCount})...`);
|
||||||
|
return executeRequest();
|
||||||
|
};
|
||||||
|
|
||||||
|
return executeRequest();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET请求
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} params - 查询参数
|
||||||
|
* @param {object} config - 其他配置
|
||||||
|
* @returns {Promise} 请求Promise
|
||||||
|
*/
|
||||||
|
const get = (url, params = {}, config = {}) => {
|
||||||
|
return request({
|
||||||
|
method: "GET",
|
||||||
|
url,
|
||||||
|
data: params,
|
||||||
|
...config
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST请求
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} data - 请求数据
|
||||||
|
* @param {object} config - 其他配置
|
||||||
|
* @returns {Promise} 请求Promise
|
||||||
|
*/
|
||||||
|
const post = (url, data = {}, config = {}) => {
|
||||||
|
return request({
|
||||||
|
method: "POST",
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
...config
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT请求
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} data - 请求数据
|
||||||
|
* @param {object} config - 其他配置
|
||||||
|
* @returns {Promise} 请求Promise
|
||||||
|
*/
|
||||||
|
const put = (url, data = {}, config = {}) => {
|
||||||
|
return request({
|
||||||
|
method: "PUT",
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
...config
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE请求
|
||||||
|
* @param {string} url - 请求URL
|
||||||
|
* @param {object} data - 请求数据
|
||||||
|
* @param {object} config - 其他配置
|
||||||
|
* @returns {Promise} 请求Promise
|
||||||
|
*/
|
||||||
|
const del = (url, data = {}, config = {}) => {
|
||||||
|
return request({
|
||||||
|
method: "DELETE",
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
...config
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 返回所有HTTP方法
|
||||||
|
return {
|
||||||
|
request,
|
||||||
|
get,
|
||||||
|
post,
|
||||||
|
put,
|
||||||
|
delete: del
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import {Dialog} from "tdesign-mobile-vue";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由拦截器配置
|
||||||
|
* @param {Array} whiteList - 不需要登录就可以访问的页面路径列表
|
||||||
|
*/
|
||||||
|
export const setupRouterInterceptor = (whiteList = []) => {
|
||||||
|
// 添加各种导航方法的拦截器
|
||||||
|
addNavigationInterceptors(whiteList)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加所有导航方法的拦截器
|
||||||
|
* @param {Array} whiteList - 白名单页面列表
|
||||||
|
*/
|
||||||
|
function addNavigationInterceptors(whiteList) {
|
||||||
|
// 页面跳转拦截
|
||||||
|
uni.addInterceptor('navigateTo', {
|
||||||
|
invoke: (args) => routeInterceptor(args, whiteList)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面重定向拦截
|
||||||
|
uni.addInterceptor('redirectTo', {
|
||||||
|
invoke: (args) => routeInterceptor(args, whiteList)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 页面重启动拦截
|
||||||
|
uni.addInterceptor('reLaunch', {
|
||||||
|
invoke: (args) => routeInterceptor(args, whiteList)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Tab切换拦截
|
||||||
|
uni.addInterceptor('switchTab', {
|
||||||
|
invoke: (args) => routeInterceptor(args, whiteList)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 路由拦截处理函数
|
||||||
|
* @param {Object} args - 导航参数
|
||||||
|
* @param {Array} whiteList - 白名单页面列表
|
||||||
|
* @returns {Object|boolean} - 继续导航或阻止导航
|
||||||
|
*/
|
||||||
|
function routeInterceptor(args, whiteList) {
|
||||||
|
// 获取目标路径
|
||||||
|
const token = uni.getStorageSync('token')
|
||||||
|
const targetUrl = args.url
|
||||||
|
|
||||||
|
// 检查目标路径是否在白名单中
|
||||||
|
const isInWhiteList = whiteList.some(path => {
|
||||||
|
return targetUrl.startsWith(path) || targetUrl === path
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log('路由拦截检查:', {url: targetUrl, isInWhiteList, hasToken: !!token})
|
||||||
|
|
||||||
|
// 如果不在白名单且未登录,则跳转到登录页
|
||||||
|
if (!isInWhiteList && !token) {
|
||||||
|
console.log('拦截导航,跳转到登录页')
|
||||||
|
|
||||||
|
// 保存用户尝试访问的页面,以便登录后跳回
|
||||||
|
try {
|
||||||
|
uni.setStorageSync('REDIRECT_URL', targetUrl)
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
|
||||||
|
Dialog.confirm({
|
||||||
|
title: '提示',
|
||||||
|
content: '您尚未登录,是否前往登录页面?',
|
||||||
|
cancelBtn: '取消',
|
||||||
|
confirmBtn: '确认',
|
||||||
|
onConfirm: () => {
|
||||||
|
uni.navigateTo({
|
||||||
|
url: '/pages/login/index'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
console.log('取消登录')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
// 阻止原导航
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// 继续原导航
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取白名单页面配置
|
||||||
|
* @returns {Array} 默认白名单页面列表
|
||||||
|
*/
|
||||||
|
export const getDefaultWhiteList = () => {
|
||||||
|
return [
|
||||||
|
'/',
|
||||||
|
'/pages/index/index',
|
||||||
|
'/pages/login/index',
|
||||||
|
'/pages/profile/index'
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import uni from '@dcloudio/vite-plugin-uni'
|
|||||||
import AutoImport from 'unplugin-auto-import/vite';
|
import AutoImport from 'unplugin-auto-import/vite';
|
||||||
import Components from 'unplugin-vue-components/vite';
|
import Components from 'unplugin-vue-components/vite';
|
||||||
import { TDesignResolver } from 'unplugin-vue-components/resolvers';
|
import { TDesignResolver } from 'unplugin-vue-components/resolvers';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
@@ -20,10 +21,15 @@ export default defineConfig({
|
|||||||
})],
|
})],
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src')
|
||||||
|
}
|
||||||
|
},
|
||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://192.168.41.118:9090',
|
target: 'http://192.168.0.107:9090',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
rewrite: (path) => path.replace(/^\/api/, '')
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user