..
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
nutz:
|
||||
application:
|
||||
name: budwk-v5-mini-plus
|
||||
mvc:
|
||||
ignore: ^(.+[.])(jsp|png|gif|jpg|js|css|jspx|jpeg|html|mp3|mp4|ico|svg|vue)$
|
||||
exclusions: /favicon/*,/assets/*,/druid/*,/upload/*
|
||||
server:
|
||||
port: 9090
|
||||
host: 0.0.0.0
|
||||
|
||||
jetty:
|
||||
contextPath: /
|
||||
threadpool:
|
||||
idleTimeout: 60000
|
||||
minThreads: 10
|
||||
maxThreads: 200
|
||||
gzip:
|
||||
enable: true
|
||||
page:
|
||||
403: /error/403.html
|
||||
404: /error/404.html
|
||||
500: /error/500.html
|
||||
#结合ftp使用,或用nginx代理ftp路径
|
||||
#staticPath: E:/project/NutzWk/upload
|
||||
#开发模式静态资源
|
||||
staticPathLocal: E:/project/company/hmc_gbhd/src/main/resources/static
|
||||
|
||||
security:
|
||||
tokenName: token
|
||||
timeout: 3600
|
||||
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
|
||||
# 允许同一账号并发登录,则系统参数 SessionOnlyOne 有效,亦可踢人下线并多了弹框提示功能
|
||||
isConcurrent: true
|
||||
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
|
||||
isShare: true
|
||||
|
||||
redis:
|
||||
host: 192.168.21.207
|
||||
port: 6379
|
||||
timeout: 2000
|
||||
max_redir: 10
|
||||
database: 10
|
||||
maxTotal: 150
|
||||
pool:
|
||||
maxTotal: 150
|
||||
maxIdle: 50
|
||||
minIdle: 10
|
||||
#password: test123
|
||||
mode:
|
||||
normal
|
||||
#cluster
|
||||
#nodes=192.168.6.31:6377,192.168.6.31:6378,192.168.6.28:6377,192.168.6.28:6378,192.168.6.34:6377,192.168.6.34:6378
|
||||
|
||||
jdbc:
|
||||
url: jdbc:mysql://192.168.21.207:3306/hmc_gbhd?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
|
||||
username: root
|
||||
password: root
|
||||
validationQuery: select 1
|
||||
maxActive: 150
|
||||
testWhileIdle: true
|
||||
connectionProperties: druid.stat.slowSqlMillis=2000
|
||||
defaultAutoCommit: true
|
||||
|
||||
|
||||
xsssql:
|
||||
ignore:
|
||||
urls: /platform/cms/content/article/addDo,/platform/sys/app/conf/addDo,/platform/sys/app/conf/editDo
|
||||
|
||||
beetl:
|
||||
RESOURCE:
|
||||
#本地路径,开发时设置本地路径,便于调试
|
||||
rootLocal: E:/project/company/hmc_gbhd/src/main/resources/views/
|
||||
root: views/
|
||||
DELIMITER_STATEMENT_START: '<!--#'
|
||||
DELIMITER_STATEMENT_END: '#-->'
|
||||
FT:
|
||||
#用法: ${"wizzer",escape}
|
||||
escape: com.budwk.app.web.commons.ext.beetl.HtmlEscapeFormat
|
||||
#用法: ${10241024,filesize}
|
||||
filesize: com.budwk.app.web.commons.ext.beetl.FileSizeFormat
|
||||
#用法: ${"",html2txt="100"} 截取100字符
|
||||
html2txt: com.budwk.app.web.commons.ext.beetl.Html2TxtFormat
|
||||
#用法: ${"",strlen="100"} 截取100字符
|
||||
strlen: com.budwk.app.web.commons.ext.beetl.StrlenFormat
|
||||
|
||||
#==============================================================
|
||||
#Configure Main Scheduler Properties
|
||||
#==============================================================
|
||||
#quartz延迟启动秒数
|
||||
quartz:
|
||||
startupDelay: 10
|
||||
scheduler:
|
||||
instanceName: defaultScheduler
|
||||
instanceId: AUTO
|
||||
#==============================================================
|
||||
#Skip Check Update
|
||||
#update:true
|
||||
#not update:false
|
||||
#==============================================================
|
||||
skipUpdateCheck: true
|
||||
#==============================================================
|
||||
#Configure JobStore isClustered=启用集群模式
|
||||
#==============================================================
|
||||
jobStore:
|
||||
class: org.quartz.impl.jdbcjobstore.JobStoreTX
|
||||
driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
|
||||
#driverDelegateClass: org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
|
||||
#driverDelegateClass: org.quartz.impl.jdbcjobstore.oracle.OracleDelegate
|
||||
#Other delegates can see: http://www.quartz-scheduler.org/documentation/quartz-2.x/tutorials/tutorial-lesson-09.html
|
||||
dataSource: myDS
|
||||
tablePrefix: SYS_QRTZ_
|
||||
isClustered: false
|
||||
clusterCheckinInterval: 20000
|
||||
maxMisfiresToHandleAtATime: 120
|
||||
misfireThreshold: 120000
|
||||
txIsolationLevelSerializable: false
|
||||
|
||||
#==============================================================
|
||||
#Configure ThreadPool
|
||||
#==============================================================
|
||||
threadPool:
|
||||
class: org.quartz.simpl.SimpleThreadPool
|
||||
threadCount: 2
|
||||
threadPriority: 5
|
||||
threadsInheritContextClassLoaderOfInitializingThread: true
|
||||
|
||||
#============================================================================
|
||||
# Configure Plugins
|
||||
#============================================================================
|
||||
plugin:
|
||||
triggHistory:
|
||||
class: org.quartz.plugins.history.LoggingJobHistoryPlugin
|
||||
shutdownhook:
|
||||
class: org.quartz.plugins.management.ShutdownHookPlugin
|
||||
cleanShutdown: true
|
||||
#============================================================================
|
||||
# NutDao dataSource
|
||||
#============================================================================
|
||||
dataSource:
|
||||
myDS:
|
||||
connectionProvider:
|
||||
class: com.budwk.app.task.conn.NutConnectionProvider
|
||||
|
||||
|
||||
sms:
|
||||
enabled: false
|
||||
#腾讯云短信配置
|
||||
tencent:
|
||||
secret-id: 1
|
||||
secret-key: 2
|
||||
appid: 1 #应用ID
|
||||
sign: '萌发开源' #短信签名
|
||||
tpl:
|
||||
#验证码模板ID
|
||||
code: 1
|
||||
#短信通知模板ID
|
||||
msg: 2
|
||||
|
||||
@@ -33,6 +33,11 @@ public class Client_user extends BaseModel implements Serializable {
|
||||
@Comment("手机号")
|
||||
private String phone;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 18)
|
||||
@Comment("身份证")
|
||||
private String idCard;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 255)
|
||||
@Comment("openId")
|
||||
|
||||
+44
-2
@@ -13,6 +13,7 @@ import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.dao.util.cri.SqlExpressionGroup;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
@@ -22,6 +23,7 @@ import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.GET;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@IocBean
|
||||
@@ -71,13 +73,25 @@ public class ClientActivityController {
|
||||
/**
|
||||
* 报名
|
||||
*/
|
||||
@At("/sign")
|
||||
@At("/sign-up")
|
||||
@AdaptBy(type = JsonAdaptor.class)
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public Result sign(ClientActivitySignVO signVO) {
|
||||
public synchronized Result signUp(ClientActivitySignVO signVO) {
|
||||
clientActivityService.signUp(signVO);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询报名人员
|
||||
* 报名人不见得参加活动
|
||||
*/
|
||||
@At("/list-signup-user/?")
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public Result listSignUpUser(Long activityId) {
|
||||
List<ActUser> list = dao.query(ActUser.class, Cnd.where(ActUser::getActivityId, "=", activityId).and(ActUser::getSignUpUserId, "=", StpClientUtil.STP.getLoginIdAsLong()));
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询我报名的活动
|
||||
*/
|
||||
@@ -89,11 +103,39 @@ public class ClientActivityController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 个人、报名人都可以查询
|
||||
* 查询报名签到信息
|
||||
*/
|
||||
@At("/sign-info/?")
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public Result signInfo(Long activityId) {
|
||||
SqlExpressionGroup seg = new SqlExpressionGroup();
|
||||
seg.or(ActUser::getUserId, "=", StpClientUtil.STP.getLoginIdAsLong());
|
||||
seg.or(ActUser::getSignUpUserId, "=", StpClientUtil.STP.getLoginIdAsLong());
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and(seg);
|
||||
cnd.and(ActUser::getActivityId, "=", activityId);
|
||||
ActUser actUser = dao.fetch(ActUser.class, cnd);
|
||||
return Result.success(actUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 签到活动
|
||||
*/
|
||||
@At("/sign-in/?")
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public Result signIn(Long activityId) {
|
||||
clientActivityService.signIn(activityId);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 个人签到信息
|
||||
*/
|
||||
@At("/sign-in-info/?")
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public Result signInInfo(Long activityId) {
|
||||
ActUser actUser = dao.fetch(ActUser.class, Cnd.where(ActUser::getActivityId, "=", activityId).and(ActUser::getUserId, "=", StpClientUtil.STP.getLoginIdAsLong()));
|
||||
return Result.success(actUser);
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
package com.budwk.app.client.module.activity.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.client.module.activity.service.ClientItemService;
|
||||
import com.budwk.app.client.module.activity.vo.ClientActivitySignVO;
|
||||
import com.budwk.app.client.module.activity.vo.ClientItemSignUpVO;
|
||||
import com.budwk.app.module.activity.models.ActUser;
|
||||
import com.budwk.app.module.activity.models.ActivityItem;
|
||||
import com.budwk.app.module.activity.models.ActivityItemUser;
|
||||
import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.mvc.adaptor.JsonAdaptor;
|
||||
import org.nutz.mvc.annotation.AdaptBy;
|
||||
import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 项目报名
|
||||
*/
|
||||
@IocBean
|
||||
@At("/client/item")
|
||||
@Ok("json:full")
|
||||
public class ClientItemController {
|
||||
|
||||
@Inject
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private ClientItemService clientItemService;
|
||||
|
||||
@At("/sign-up")
|
||||
@AdaptBy(type = JsonAdaptor.class)
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public synchronized Result signUp(ClientItemSignUpVO signVO) {
|
||||
clientItemService.signUp(signVO);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取项目列表
|
||||
*/
|
||||
@At("/list/?")
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public Result list(Long activityId) {
|
||||
List<ActivityItem> items = dao.query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", activityId));
|
||||
return Result.success(items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询团队成员
|
||||
*/
|
||||
@At("/list-team-user/?")
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public Result listTeamUser(Long activityId){
|
||||
List<ActUser> list = dao.query(ActUser.class, Cnd.where(ActUser::getActivityId, "=", activityId).and(ActUser::getIsSignIn, "=", 1));
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询项目报名人员
|
||||
*/
|
||||
@At("/list-signup-user")
|
||||
@SaCheckLogin
|
||||
public Result listSignUpUser(Long activityId,Long itemId) {
|
||||
List<ActivityItemUser> itemUsers = dao.query(ActivityItemUser.class, Cnd.where(ActivityItemUser::getActivityId, "=", activityId).and(ActivityItemUser::getItemId, "=", itemId));
|
||||
return Result.success(itemUsers);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+12
@@ -1,6 +1,7 @@
|
||||
package com.budwk.app.client.module.activity.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.client.module.activity.vo.ClientActivitySignVO;
|
||||
import com.budwk.app.module.activity.models.Activity;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
@@ -13,4 +14,15 @@ public interface ClientActivityService extends BaseService<Activity> {
|
||||
*/
|
||||
List<NutMap> listMineAct();
|
||||
|
||||
/**
|
||||
* 报名
|
||||
*/
|
||||
void signUp(ClientActivitySignVO signVO);
|
||||
|
||||
/**
|
||||
* 报名签到
|
||||
*/
|
||||
void signIn(Long activityId);
|
||||
|
||||
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package com.budwk.app.client.module.activity.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.client.module.activity.vo.ClientItemSignUpVO;
|
||||
import com.budwk.app.module.activity.models.ActivityItemUser;
|
||||
|
||||
public interface ClientItemService extends BaseService<ActivityItemUser> {
|
||||
void signUp(ClientItemSignUpVO signVO);
|
||||
}
|
||||
+67
@@ -1,15 +1,23 @@
|
||||
package com.budwk.app.client.module.activity.service.impl;
|
||||
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.client.auth.models.Client_user;
|
||||
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.ActUser;
|
||||
import com.budwk.app.module.activity.models.Activity;
|
||||
import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
import org.nutz.dao.sql.Sql;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@IocBean(args = {"refer:dao"})
|
||||
@@ -33,4 +41,63 @@ public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> impleme
|
||||
sql.setParam("userId", StpClientUtil.STP.getLoginIdAsLong());
|
||||
return listMap(sql);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signUp(ClientActivitySignVO signVO) {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
List<ActUser> userList = signVO.getUserList();
|
||||
// 首先检查报名人员是否已经在用户表中
|
||||
for (ActUser user : userList) {
|
||||
Client_user clientUser = dao().fetch(Client_user.class, Cnd.where(Client_user::getIdCard, "=", user.getIdCard()));
|
||||
if (clientUser == null) {
|
||||
// 创建用户
|
||||
clientUser = new Client_user();
|
||||
clientUser.setUserName(user.getUserName());
|
||||
clientUser.setSex(user.getSex());
|
||||
clientUser.setPhone(user.getPhone());
|
||||
clientUser.setIdCard(user.getIdCard());
|
||||
dao().insert(clientUser);
|
||||
} else {
|
||||
// 验证用户之前是不是已经报名 别人帮报 自己报 管理员报
|
||||
ActUser hisActUser = dao().fetch(ActUser.class, Cnd.where(ActUser::getUserId, "=", clientUser.getId()).and(ActUser::getActivityId, "=", signVO.getActivityId()));
|
||||
if (hisActUser != null) {
|
||||
if (!hisActUser.getSignUpUserId().equals(StpClientUtil.STP.getLoginIdAsLong())) {
|
||||
throw new BaseException("【" + hisActUser.getUnitName() + "】" + clientUser.getUserName() + "已经报名了,请勿重复报名!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
user.setActivityId(signVO.getActivityId());
|
||||
user.setUserId(clientUser.getId());
|
||||
user.setUnitName(signVO.getUnitName());
|
||||
user.setSignUpContacts(signVO.getContactName());
|
||||
user.setSignUpMobile(signVO.getContactPhone());
|
||||
user.setSignUpUserId(StpClientUtil.STP.getLoginIdAsLong());
|
||||
user.setSignUpTime(now);
|
||||
}
|
||||
|
||||
// delete then insert so easy
|
||||
|
||||
// 删除旧的报名人员
|
||||
dao().clear(ActUser.class, Cnd.where(ActUser::getActivityId, "=", signVO.getActivityId()).and(ActUser::getSignUpUserId, "=", StpClientUtil.STP.getLoginIdAsLong()));
|
||||
|
||||
// 插入新的报名人员
|
||||
dao().insert(userList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void signIn(Long activityId) {
|
||||
ActUser actUser = dao().fetch(ActUser.class, Cnd.where(ActUser::getActivityId, "=", activityId).and(ActUser::getUserId, "=", StpClientUtil.STP.getLoginIdAsLong()));
|
||||
if (actUser == null) {
|
||||
throw new BaseException("您未报名该活动");
|
||||
}
|
||||
if (actUser.getIsSignIn()) {
|
||||
throw new BaseException("您已签到,请勿重复签到");
|
||||
}
|
||||
// 更新签到信息
|
||||
actUser.setIsSignIn(true);
|
||||
actUser.setSignInTime(LocalDateTime.now());
|
||||
dao().update(actUser, "isSignIn|signInTime");
|
||||
}
|
||||
}
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.budwk.app.client.module.activity.service.impl;
|
||||
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.client.module.activity.service.ClientItemService;
|
||||
import com.budwk.app.client.module.activity.vo.ClientItemSignUpVO;
|
||||
import com.budwk.app.module.activity.models.ActivityItemUser;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class ClientItemServiceImpl extends BaseServiceImpl<ActivityItemUser> implements ClientItemService {
|
||||
public ClientItemServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void signUp(ClientItemSignUpVO signVO) {
|
||||
// 删除掉之前的
|
||||
clear(Cnd.where(ActivityItemUser::getActivityId, "=", signVO.getActivityId())
|
||||
.and(ActivityItemUser::getGroupId, "=", signVO.getGroupId())
|
||||
.and(ActivityItemUser::getItemId, "=", signVO.getItemId())
|
||||
);
|
||||
|
||||
List<ActivityItemUser> userList = signVO.getUserList();
|
||||
for (ActivityItemUser itemUser : userList) {
|
||||
itemUser.setActivityId(signVO.getActivityId());
|
||||
itemUser.setItemId(signVO.getItemId());
|
||||
itemUser.setGroupId(signVO.getGroupId());
|
||||
}
|
||||
insert(userList);
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
package com.budwk.app.client.module.activity.vo;
|
||||
|
||||
public class ClientActivitySignInVO {
|
||||
}
|
||||
+1
-1
@@ -11,7 +11,7 @@ import java.util.List;
|
||||
@Data
|
||||
public class ClientActivitySignVO {
|
||||
|
||||
private String activityId;
|
||||
private Long activityId;
|
||||
|
||||
private String unitName;
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.budwk.app.client.module.activity.vo;
|
||||
|
||||
import com.budwk.app.module.activity.models.ActivityItemUser;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ClientItemSignUpVO {
|
||||
|
||||
private Long activityId;
|
||||
|
||||
private Long itemId;
|
||||
|
||||
private Long groupId;
|
||||
|
||||
private List<ActivityItemUser> userList;
|
||||
|
||||
}
|
||||
+1
-1
@@ -87,7 +87,7 @@ public class ActUserController {
|
||||
// 报名时间
|
||||
LocalDateTime localDateTime = DateUtil.date().toLocalDateTime();
|
||||
// 活动id
|
||||
Long activityId = Long.parseLong(vo.getActivityId());
|
||||
Long activityId = vo.getActivityId();
|
||||
|
||||
List<ActUser> userList = vo.getUserList();
|
||||
|
||||
|
||||
+2
-2
@@ -54,7 +54,7 @@ public class ActivityManageController {
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("activity.manager")
|
||||
public Result save(Activity activity) {
|
||||
public Result save(@Param("activity") Activity activity) {
|
||||
// activityService.insertWith(activity, "items");
|
||||
activityService.insert(activity);
|
||||
return Result.success();
|
||||
@@ -66,7 +66,7 @@ public class ActivityManageController {
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
@SaCheckPermission("activity.manager")
|
||||
public Result update(Activity activity) {
|
||||
public Result update(@Param("activity")Activity activity) {
|
||||
activityService.updateIgnoreNull(activity);
|
||||
|
||||
|
||||
|
||||
@@ -98,6 +98,7 @@ public class ActUser extends BaseModel {
|
||||
@Column
|
||||
@Comment("是否签到")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
@Default("0")
|
||||
private Boolean isSignIn;
|
||||
|
||||
@Column
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.budwk.app.module.activity.models;
|
||||
|
||||
import cn.hutool.json.JSONObject;
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@@ -69,11 +70,21 @@ public class Activity extends BaseModel {
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
private String content;
|
||||
|
||||
@Comment("承诺书")
|
||||
@Column
|
||||
@ColDefine(type = ColType.TEXT)
|
||||
private String protocol;
|
||||
|
||||
@Comment("封面")
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String cover;
|
||||
|
||||
@Comment("附件")
|
||||
@Column
|
||||
@ColDefine(type = ColType.MYSQL_JSON)
|
||||
private List<JSONObject> files;
|
||||
|
||||
/**
|
||||
* 项目列表
|
||||
*/
|
||||
|
||||
@@ -36,6 +36,16 @@ public class ActivityItem extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 300)
|
||||
private String rule;
|
||||
|
||||
@Comment("图片")
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 300)
|
||||
private String img;
|
||||
|
||||
@Comment("项目人数")
|
||||
@Column
|
||||
@ColDefine(type = ColType.INT)
|
||||
private Integer num;
|
||||
|
||||
@Comment("排序")
|
||||
@Column
|
||||
@ColDefine(type = ColType.INT)
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.budwk.app.module.activity.models;
|
||||
|
||||
import com.budwk.app.base.model.BaseModel;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
|
||||
@Data
|
||||
@Table("act_item_user")
|
||||
@Comment("项目报名人员")
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableMeta("{'mysql-charset':'utf8mb4'}")
|
||||
public class ActivityItemUser extends BaseModel {
|
||||
|
||||
@Id
|
||||
@Comment("ID")
|
||||
private Long id;
|
||||
|
||||
@Column
|
||||
@Comment("活动ID")
|
||||
private Long activityId;
|
||||
|
||||
@Column
|
||||
@Comment("项目ID")
|
||||
private Long itemId;
|
||||
|
||||
@Column
|
||||
@Comment("报名人Id")
|
||||
private Long userId;
|
||||
|
||||
@Column
|
||||
@Comment("分组ID")
|
||||
private Long groupId;
|
||||
}
|
||||
+2
-1
@@ -3,6 +3,7 @@ package com.budwk.app.web.commons.auth.satoken.aop;
|
||||
import cn.dev33.satoken.SaManager;
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
|
||||
import org.nutz.aop.InterceptorChain;
|
||||
import org.nutz.aop.MethodInterceptor;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
@@ -15,7 +16,7 @@ public class SaCheckAnyLoginInterceptor implements MethodInterceptor {
|
||||
public void filter(InterceptorChain chain) throws Throwable {
|
||||
Method method = chain.getCallingMethod();
|
||||
|
||||
boolean mini = SaManager.getStpLogic("mini").isLogin();
|
||||
boolean mini = SaManager.getStpLogic(StpClientUtil.TYPE).isLogin();
|
||||
boolean web = SaManager.getStpLogic("login").isLogin();
|
||||
if (mini || web) {
|
||||
chain.doChain();
|
||||
|
||||
@@ -45,7 +45,7 @@ public class UploadController {
|
||||
@Ok("json")
|
||||
@SaCheckLogin
|
||||
//AdaptorErrorContext必须是最后一个参数
|
||||
public Object file(@Param("Filedata") TempFile tf, HttpServletRequest req, AdaptorErrorContext err) {
|
||||
public Object file(@Param("file") TempFile tf, HttpServletRequest req, AdaptorErrorContext err) {
|
||||
try {
|
||||
if (err != null && err.getAdaptorErr() != null) {
|
||||
return NutMap.NEW().addv("code", 1).addv("msg", "文件不合法");
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
<template>
|
||||
<div class="upload-container">
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
ref="upload"
|
||||
action="/open/file/upload/file"
|
||||
:on-preview="handlePreview"
|
||||
:on-remove="handleRemove"
|
||||
:before-remove="beforeRemove"
|
||||
:on-success="handleSuccess"
|
||||
:on-error="handleError"
|
||||
:on-exceed="handleExceed"
|
||||
:file-list="internalFileList"
|
||||
:auto-upload="true"
|
||||
:limit="limit"
|
||||
:multiple="multiple"
|
||||
:accept="accept"
|
||||
:before-upload="beforeUpload"
|
||||
:on-change="handleChange"
|
||||
>
|
||||
<el-button slot="trigger" size="small" type="primary">选取文件</el-button>
|
||||
<div slot="tip" class="el-upload__tip">
|
||||
<!-- <slot name="tip">-->
|
||||
<!-- 请上传符合要求的文件,单个文件不超过{{ maxSize }}MB-->
|
||||
<!-- </slot>-->
|
||||
{{ uploadTips }}
|
||||
</div>
|
||||
</el-upload>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
module.exports = {
|
||||
name: 'index',
|
||||
model: {
|
||||
prop: 'value',
|
||||
event: 'change'
|
||||
},
|
||||
props: {
|
||||
// v-model 绑定的值
|
||||
value: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 最大上传数量
|
||||
limit: {
|
||||
type: Number,
|
||||
default: 5
|
||||
},
|
||||
// 是否支持多选
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
// 接受的文件类型
|
||||
accept: {
|
||||
type: String,
|
||||
default: '.doc,.docx,.pdf,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png,.zip,.rar'
|
||||
},
|
||||
// 最大文件大小(MB)
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: 50
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
uploadTips(){
|
||||
if (this.$slots.tip) {
|
||||
return this.$slots.tip[0].text
|
||||
}
|
||||
let tips = `请上传符合要求的文件,单个文件不超过${this.maxSize}MB`
|
||||
if(this.accept){
|
||||
tips += `,支持格式:${this.accept}`
|
||||
}
|
||||
return tips
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
internalFileList: []
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(newVal) {
|
||||
// 同步外部传入的值到内部状态
|
||||
this.internalFileList = newVal.map(file => ({
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
uid: file.uid || this.generateUid(),
|
||||
status: 'success'
|
||||
}))
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// 生成唯一ID
|
||||
generateUid() {
|
||||
return Date.now().toString(36) + Math.random().toString(36).substr(2)
|
||||
},
|
||||
|
||||
// 点击文件列表中已上传的文件时的钩子
|
||||
handlePreview(file) {
|
||||
if (file.url) {
|
||||
// 既然看到这里吧 把预览写了吧
|
||||
}
|
||||
},
|
||||
|
||||
// 文件列表移除文件
|
||||
handleRemove(file, fileList) {
|
||||
this.internalFileList = fileList
|
||||
this.emitChange(fileList)
|
||||
this.$emit('file-removed', file, fileList)
|
||||
},
|
||||
|
||||
// 文件删除
|
||||
beforeRemove(file) {
|
||||
if(file.status === 'success'){
|
||||
return this.$confirm(`确定移除 ${file.name}?`, '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 文件上传成功时的钩子
|
||||
handleSuccess(response, file, fileList) {
|
||||
// 更新文件状态
|
||||
const index = this.internalFileList.findIndex(f => f.uid === file.uid)
|
||||
if (index !== -1) {
|
||||
if(response.code === 0){
|
||||
debugger
|
||||
this.$set(this.internalFileList, index, {
|
||||
// ...file,
|
||||
// status: 'success',
|
||||
// ...response.data
|
||||
name: file.name,
|
||||
url: response.data?.file_url,
|
||||
type: response.data?.file_type,
|
||||
size: response.data?.file_size
|
||||
})
|
||||
this.$message.success(`${file.name} 上传成功`)
|
||||
}else{
|
||||
this.$message.error(`${file.name} 上传失败`)
|
||||
}
|
||||
}
|
||||
console.log(this.internalFileList)
|
||||
this.emitChange(this.internalFileList)
|
||||
this.$emit('upload-success', response, file, fileList)
|
||||
this.$message.success(`${file.name} 上传成功`)
|
||||
},
|
||||
|
||||
// 文件上传失败时的钩子
|
||||
handleError(err, file, fileList) {
|
||||
const index = this.internalFileList.findIndex(f => f.uid === file.uid)
|
||||
if (index !== -1) {
|
||||
this.$set(this.internalFileList, index, {
|
||||
...file,
|
||||
status: 'fail'
|
||||
})
|
||||
}
|
||||
|
||||
this.emitChange(this.internalFileList)
|
||||
this.$emit('upload-error', err, file, fileList)
|
||||
this.$message.error(`${file.name} 上传失败`)
|
||||
},
|
||||
|
||||
// 文件超出数量限制时的钩子
|
||||
handleExceed(files, fileList) {
|
||||
this.$message.warning(
|
||||
`当前限制选择 ${this.limit} 个文件,本次选择了 ${files.length} 个文件,共选择了 ${
|
||||
files.length + fileList.length
|
||||
} 个文件`
|
||||
)
|
||||
},
|
||||
|
||||
// 上传前的校验
|
||||
beforeUpload(file) {
|
||||
const isLt = file.size / 1024 / 1024 < this.maxSize
|
||||
if (!isLt) {
|
||||
this.$message.error(`上传文件大小不能超过 ${this.maxSize}MB!`)
|
||||
return false
|
||||
}
|
||||
const isValid = this.validateFileType(file)
|
||||
if(!isValid){
|
||||
this.$message.error(`只能上传 ${this.accept} 格式的文件`)
|
||||
return false
|
||||
}
|
||||
return isValid
|
||||
},
|
||||
|
||||
validateFileType(file){
|
||||
const allowedExtensions = this.accept.split(',').map(ext => ext.trim().toLowerCase()).filter(ext => ext.startsWith('.'))
|
||||
const fileExtension = file.name.substring(file.name.lastIndexOf('.')).toLowerCase()
|
||||
if (allowedExtensions.includes(fileExtension)) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
// 文件状态改变
|
||||
handleChange(file, fileList) {
|
||||
this.internalFileList = fileList
|
||||
// 如果是新增文件但还未上传成功,不触发change事件
|
||||
// if (file.status !== 'ready') {
|
||||
// this.emitChange(fileList)
|
||||
// }
|
||||
|
||||
if(file.status === 'ready'){
|
||||
const isValid = this.validateFileType(file)
|
||||
if (!isValid) {
|
||||
// 从文件列表中移除不符合的文件
|
||||
this.internalFileList = fileList.filter(f => f.uid !== file.uid)
|
||||
return false
|
||||
}
|
||||
}
|
||||
this.internalFileList = fileList
|
||||
return true
|
||||
},
|
||||
|
||||
// 触发v-model更新
|
||||
emitChange(fileList) {
|
||||
// 格式化输出数据,只保留必要信息
|
||||
debugger
|
||||
const formattedList = fileList.map(file => ({
|
||||
name: file.name,
|
||||
url: file.url,
|
||||
type: file.type,
|
||||
size: file.size
|
||||
}))
|
||||
|
||||
this.$emit('change', formattedList)
|
||||
},
|
||||
|
||||
// 手动清空文件列表
|
||||
clearFiles() {
|
||||
this.$refs.upload.clearFiles()
|
||||
this.internalFileList = []
|
||||
this.emitChange([])
|
||||
},
|
||||
|
||||
// 获取当前文件列表
|
||||
getFileList() {
|
||||
return this.internalFileList
|
||||
},
|
||||
|
||||
// 手动提交上传
|
||||
submit() {
|
||||
this.$refs.upload.submit()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.upload-container {
|
||||
padding: 20px;
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background-color: #fbfdff;
|
||||
}
|
||||
|
||||
.upload-container:hover {
|
||||
border-color: #409eff;
|
||||
}
|
||||
|
||||
.el-upload__tip {
|
||||
margin-top: 10px;
|
||||
color: #606266;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -89,6 +89,7 @@ module.exports = {
|
||||
handleSuccess(response, file, fileList) {
|
||||
if (response.code === 0) {
|
||||
if (this.maxCount === 1) {
|
||||
debugger
|
||||
this.$emit('input', response.data)
|
||||
} else {
|
||||
|
||||
@@ -122,9 +123,5 @@ module.exports = {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-upload--picture-card {
|
||||
//width: 100px;
|
||||
//height: 100px;
|
||||
//line-height: 100px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -57,6 +57,7 @@
|
||||
Vue.component("table-tool", httpVueLoader("/components/plugins/sysTableTool/index.vue?v=" + new Date().getTime()))
|
||||
Vue.component("text-editor", httpVueLoader("/components/plugins/textEditor/index.vue?v=" + new Date().getTime()))
|
||||
Vue.component("image-upload", httpVueLoader("/components/plugins/imageUpload/index.vue?v=" + new Date().getTime()))
|
||||
Vue.component("file-upload", httpVueLoader("/components/plugins/fileUpload/index.vue?v=" + new Date().getTime()))
|
||||
</script>
|
||||
</head>
|
||||
<body class="hold-transition sidebar-mini layout-fixed">
|
||||
|
||||
@@ -55,6 +55,13 @@ const addOrUpdate = {
|
||||
<el-form-item label="活动内容" prop="content">
|
||||
<text-editor v-model="form.content" :height="200"></text-editor>
|
||||
</el-form-item>
|
||||
<el-form-item label="承诺书" prop="protocol">
|
||||
<text-editor v-model="form.protocol" :height="200"></text-editor>
|
||||
</el-form-item>
|
||||
<el-form-item label="活动附件" prop="files">
|
||||
{{form.files}}
|
||||
<file-upload v-model="form.files" accept=".pdf"></file-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="visible = false">取消</el-button>
|
||||
@@ -91,7 +98,8 @@ const addOrUpdate = {
|
||||
hostUnit: [{required: true, message: '请输入主办单位', trigger: ['blur', 'change']}],
|
||||
coHostUnit: [{required: true, message: '请输入承办单位', trigger: ['blur', 'change']}],
|
||||
contact: [{required: true, message: '请输入联系人', trigger: ['blur', 'change']}],
|
||||
content: [{required: true, message: '请输入活动内容', trigger: ['blur', 'change']}]
|
||||
content: [{required: true, message: '请输入活动内容', trigger: ['blur', 'change']}],
|
||||
protocol: [{required: true, message: '请输入承诺书', trigger: ['blur', 'change']}],
|
||||
},
|
||||
stepActive: 0
|
||||
}
|
||||
@@ -117,7 +125,7 @@ const addOrUpdate = {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
}).then(() => {
|
||||
$.post('/platform/activity/manage/' + (this.form.id ? 'update' : 'save'), this.form).then(res => {
|
||||
$.post('/platform/activity/manage/' + (this.form.id ? 'update' : 'save'), {"activity": JSON.stringify(this.form)}).then(res => {
|
||||
if (res.code === 0) {
|
||||
this.$message.success(this.form.id ? '修改成功' : '新增成功')
|
||||
this.$emit('refresh')
|
||||
|
||||
@@ -20,6 +20,12 @@ const itemForm = {
|
||||
<el-input v-model="row.rule" placeholder="请输入评分规则"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="图片">
|
||||
<template #default="{row}">
|
||||
{{row.img}}
|
||||
<image-upload v-model="row.img" :max-count="1"></image-upload>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" fixed="right" width="100px">
|
||||
<template #default="{$index}">
|
||||
<el-button type="text" class="text-danger" @click="removeRow($index)">删除</el-button>
|
||||
|
||||
@@ -50,4 +50,19 @@ uni-page-body{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 隐藏所有页面的滚动条(包括 web 和 小程序) */
|
||||
::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
-webkit-appearance: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* 针对 H5 的额外设置 */
|
||||
page, html, body {
|
||||
scrollbar-width: none; /* Firefox */
|
||||
-ms-overflow-style: none; /* IE 10+ */
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,14 @@
|
||||
<template>
|
||||
<!-- <div class="scanCode" v-if="isScanning">-->
|
||||
<!-- <div class="btn-container">-->
|
||||
<!-- <div class="left-back" @click.stop="handleBack">-->
|
||||
<!-- <t-icon name="arrow-left" />-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="container">-->
|
||||
<!-- <div class="qrcode">-->
|
||||
<!-- <div ref="qrCodeElement" id="reader"></div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="page-box" v-if="isScanning">-->
|
||||
<!--<!– <div class="btn-box">–>-->
|
||||
<!--<!– <div>–>-->
|
||||
<!--<!– <t-icon name="arrow-left" @click="handleBack"/>–>-->
|
||||
<!--<!– </div>–>-->
|
||||
<!--<!– </div>–>-->
|
||||
<!-- <div id="reader"></div>-->
|
||||
<!-- </div>-->
|
||||
<div class="page-box" v-if="isScanning">
|
||||
<div class="btn-box">
|
||||
<div>
|
||||
<t-icon name="arrow-left" @click="handleBack"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qr-container">
|
||||
<div class="qr-box">
|
||||
<div id="reader"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container" v-show="isScanning">
|
||||
<div id="reader"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -32,10 +19,8 @@ import {Toast} from "tdesign-mobile-vue";
|
||||
|
||||
const emit = defineEmits(['scan-success', 'back'])
|
||||
const html5QrCode = ref(null)
|
||||
const fileList = ref([])
|
||||
const isScanning = ref(false)
|
||||
const hasCameraPermission = ref(false)
|
||||
const qrCodeElement = ref(null)
|
||||
|
||||
const startScan = async () => {
|
||||
try {
|
||||
@@ -45,50 +30,35 @@ const startScan = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 显示扫描界面
|
||||
isScanning.value = true
|
||||
|
||||
// 3. 等待DOM更新完成
|
||||
await nextTick()
|
||||
console.log(document.getElementById('reader'))
|
||||
|
||||
// 5. 初始化扫码器
|
||||
html5QrCode.value = new Html5Qrcode('reader')
|
||||
|
||||
console.log(html5QrCode.value)
|
||||
|
||||
// 6. 开始扫描
|
||||
await html5QrCode.value.start(
|
||||
{facingMode: 'environment'},
|
||||
{fps: 10, qrbox: {width: 200, height: 200}},
|
||||
{
|
||||
fps: 10,
|
||||
qrbox: {width: 250, height: 250},
|
||||
aspectRatio: 2
|
||||
},
|
||||
(decodedText) => {
|
||||
stopScan()
|
||||
emit('scan-success', decodedText)
|
||||
}
|
||||
)
|
||||
|
||||
} catch (err) {
|
||||
isScanning.value = false
|
||||
handleCameraError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始摄像头扫描
|
||||
const startCameraScan = () => {
|
||||
return html5QrCode.value.start(
|
||||
{facingMode: 'environment'},
|
||||
{
|
||||
fps: 1,
|
||||
qrbox: {width: 250, height: 250}
|
||||
},
|
||||
(decodedText) => {
|
||||
emit('scan-success', decodedText)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// 停止扫描
|
||||
const stopScan = async () => {
|
||||
alert(2)
|
||||
isScanning.value = false
|
||||
if (html5QrCode.value?.isScanning) {
|
||||
try {
|
||||
@@ -102,7 +72,6 @@ const stopScan = async () => {
|
||||
|
||||
// 处理摄像头错误
|
||||
const handleCameraError = (err) => {
|
||||
alert(JSON.stringify(err))
|
||||
let message = '摄像头启动失败'
|
||||
if (err.name === 'NotAllowedError') {
|
||||
message = '请允许摄像头访问权限'
|
||||
@@ -132,106 +101,21 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
//.scanCode {
|
||||
// //background: rgba(0, 0, 0);
|
||||
// position: fixed;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// z-index: 999;
|
||||
//}
|
||||
//
|
||||
//.btn-container {
|
||||
// position: absolute;
|
||||
// top: 16px;
|
||||
// left: 16px;
|
||||
// z-index: 1000;
|
||||
//}
|
||||
//
|
||||
//.left-back {
|
||||
// color: #fff;
|
||||
// font-size: 24px;
|
||||
// padding: 8px;
|
||||
// background: rgba(0, 0, 0, 0.3);
|
||||
// border-radius: 50%;
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
// width: 40px;
|
||||
// height: 40px;
|
||||
//}
|
||||
//
|
||||
//.container {
|
||||
// //height: 100vh;
|
||||
// //position: relative;
|
||||
// //width: 100%;
|
||||
// position: fixed;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// z-index: 999;
|
||||
//}
|
||||
//
|
||||
//.qrcode {
|
||||
// height: 100%;
|
||||
// position: fixed;
|
||||
// left: 0;
|
||||
// right: 0;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// z-index: 999;
|
||||
//}
|
||||
|
||||
//#reader {
|
||||
// //top: 20%;
|
||||
// //left: 0;
|
||||
// //transform: translateY(-50%);
|
||||
// //height: 100%;
|
||||
// width: 500rpx;
|
||||
// height: 500rpx;
|
||||
//}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
.page-box {
|
||||
//display: flex;
|
||||
//flex-direction: column;
|
||||
//height: 100%;
|
||||
background: #000;
|
||||
.container {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
background: rgba(#000000,0.48);
|
||||
z-index: 999;
|
||||
}
|
||||
.qr-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.qr-box {
|
||||
height: 100%;
|
||||
}
|
||||
#reader {
|
||||
top: 50%;
|
||||
height: 100vh;
|
||||
//top: 50%;
|
||||
left: 0;
|
||||
}
|
||||
.btn-box {
|
||||
//flex: 1;
|
||||
//display: flex;
|
||||
//justify-content: space-around;
|
||||
//align-items: flex-start;
|
||||
//margin-top: auto;
|
||||
//padding: 12px;
|
||||
//color: #fff;
|
||||
//font-size: 28px;
|
||||
position: a;
|
||||
}
|
||||
//transform: translateY(-50%);
|
||||
}
|
||||
</style>
|
||||
|
||||
+21
-4
@@ -10,8 +10,7 @@
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录",
|
||||
"navigationStyle": "custom",
|
||||
"disableScroll": true
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -26,12 +25,24 @@
|
||||
"navigationBarTitleText": "活动详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/detail/detail2",
|
||||
"style": {
|
||||
"navigationBarTitleText": "活动详情"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/registration-form/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "活动报名"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/registration-form/success",
|
||||
"style": {
|
||||
"navigationBarTitleText": "报名成功"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/team/index",
|
||||
"style": {
|
||||
@@ -49,12 +60,18 @@
|
||||
"style": {
|
||||
"navigationBarTitleText": "扫码签到"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/activity/team-form/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "项目报名"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "#FFFFFF",
|
||||
"navigationBarTextStyle": "#000000",
|
||||
"navigationBarTitleText": "uni-app",
|
||||
"navigationBarBackgroundColor": "#f45a5a",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"tabBar": {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view style="position:relative;width: 100%;height: 400rpx;overflow: hidden;margin-bottom: 0rpx">
|
||||
<image :src="actData.cover" mode="aspectFill" style="width: 100%"></image>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onLoad, onPageScroll} from "@dcloudio/uni-app";
|
||||
import {Toast} from "tdesign-mobile-vue";
|
||||
import {ref} from 'vue'
|
||||
import {useRequest} from "@/utils/request";
|
||||
import moment from "moment/moment";
|
||||
|
||||
const request = useRequest()
|
||||
|
||||
const actId = ref('');
|
||||
const actData = ref({});
|
||||
const signUpInfo = ref({})
|
||||
|
||||
|
||||
onLoad((option) => {
|
||||
if (option.id) {
|
||||
actId.value = option.id;
|
||||
getDetailData(option.id);
|
||||
getSignUpInfo(option.id);
|
||||
} else {
|
||||
Toast({message: 'ID不存在', theme: 'error'});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取详情
|
||||
const getDetailData = async (id) => {
|
||||
const loading = Toast({
|
||||
theme: 'loading',
|
||||
duration: 0
|
||||
});
|
||||
try {
|
||||
const {code, data} = await request.get('/client/activity/get/' + id)
|
||||
if (code === 0) {
|
||||
actData.value = data;
|
||||
|
||||
// 如果活动未开始,开始倒计时
|
||||
// if (activityStatus.value === 'notStarted') {
|
||||
// startCountdown();
|
||||
// }
|
||||
} else {
|
||||
Toast({message: '获取详情失败', theme: 'error'});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取详情失败', error);
|
||||
Toast({message: '获取详情失败', theme: 'error'});
|
||||
} finally {
|
||||
loading.clear()
|
||||
}
|
||||
};
|
||||
|
||||
// 查询报名信息
|
||||
const getSignUpInfo = async () => {
|
||||
try {
|
||||
const {code, data} = await request.get('/client/activity/sign-info/' + actId.value);
|
||||
if (code === 0 && data) {
|
||||
signUpInfo.value = data;
|
||||
} else {
|
||||
Toast({message: '查询报名信息失败', theme: 'error'});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.page-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,5 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 顶部搜索和筛选 -->
|
||||
<view class="header">
|
||||
<view class="search-container">
|
||||
<t-search
|
||||
@@ -8,65 +7,66 @@
|
||||
placeholder="搜索活动名称"
|
||||
shape="round"
|
||||
class="search-input"
|
||||
@search="handleSearch"
|
||||
@onSearch="handleSearch"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 状态筛选标签组 -->
|
||||
<view class="filter-tabs">
|
||||
<scroll-view scroll-x class="tabs-scroll-view" :show-scrollbar="false">
|
||||
<view class="tabs-container">
|
||||
<view
|
||||
v-for="option in statusOptions"
|
||||
:key="option.value"
|
||||
class="filter-tab"
|
||||
:class="{ active: pageForm.status === option.value }"
|
||||
@click="selectStatusFilter(option.value)"
|
||||
>
|
||||
{{ 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"
|
||||
<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"
|
||||
class="card-item"
|
||||
v-for="item in dataList"
|
||||
:key="item.id"
|
||||
@click="navigateToDetail(item.id)"
|
||||
>
|
||||
<view class="image-container">
|
||||
<image :src="item.cover" mode="aspectFill" class="activity-image"/>
|
||||
<view class="activity-time">
|
||||
{{ formatTimeRange(item.startTime, item.endTime) }}
|
||||
</view>
|
||||
<view class="status-tag-container">
|
||||
<t-tag
|
||||
:theme="getTagTheme(getActivityStatus(item))"
|
||||
size="medium"
|
||||
shape="mark"
|
||||
>{{ getStatusText(getActivityStatus(item)) }}
|
||||
</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="activity-info">
|
||||
<text class="activity-title">{{ item.title }}</text>
|
||||
<view class="activity-address">
|
||||
<t-icon name="location" size="small"/>
|
||||
<text>{{ item.address || '暂无地址信息' }}</text>
|
||||
<view>
|
||||
<view style="display: flex;align-items: center;margin-bottom: 10px">
|
||||
<image
|
||||
src="https://assets.cumen.fun/upload/017cdaf1-c5fa-788e-eea2-d0ee249f902b/0187376a-3cfa-07b3-3cd6-7479bf463865?x-oss-process=style/720-jpg"
|
||||
style="width: 30px;height: 30px;flex-shrink: 0"
|
||||
></image>
|
||||
<view
|
||||
style="margin-left: 10px;font-weight: 600;white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
{{ item.title }}
|
||||
</view>
|
||||
</view>
|
||||
<view style="display: flex;">
|
||||
<image :src="item.cover" mode="aspectFill" style="flex-shrink:0;width: 120px;height: 90px;border-radius: 5px;"></image>
|
||||
<view
|
||||
style="margin-left:10px;flex:1;overflow:hidden;display: flex;flex-direction: column;justify-content:space-around;color: #534d4d;">
|
||||
<view
|
||||
style="display: flex; align-items: center; column-gap: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<t-icon name="time"></t-icon>
|
||||
{{ moment(item.startTime).format('MM-DD') }}
|
||||
</view>
|
||||
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<t-icon name="location"></t-icon>
|
||||
{{ item.address }}
|
||||
</view>
|
||||
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<t-icon name="castle-7"></t-icon>
|
||||
{{ item.address }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view style="border-top: 1px solid #f0f0f0;margin-top: 10px;padding-top: 12px;display: flex;align-items: center;justify-content: space-between">
|
||||
<view>
|
||||
100人已报名
|
||||
</view>
|
||||
<!-- <image src="https://assets.cumen.fun/upload/017cdaf1-c5fa-788e-eea2-d0ee249f902b/01892580-5db0-be0b-5643-35d6c5a83591"-->
|
||||
<!-- style="width: 105px;height: 40px"-->
|
||||
<!-- ></image>-->
|
||||
<t-button theme="primary" shape="round" size="small">立即报名</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -89,8 +89,6 @@
|
||||
|
||||
<script setup>
|
||||
import {ref, reactive, onMounted} from 'vue';
|
||||
// 移除不再需要的onReachBottom和onPullDownRefresh
|
||||
// import {onReachBottom, onPullDownRefresh} from "@dcloudio/uni-app";
|
||||
import {Toast} from 'tdesign-mobile-vue';
|
||||
import {useRequest} from "@/utils/request";
|
||||
import moment from "moment";
|
||||
@@ -118,57 +116,8 @@ const pageForm = reactive({
|
||||
const dataList = ref([]);
|
||||
const loading = ref(false);
|
||||
const hasMore = ref(true);
|
||||
// 添加下拉刷新状态
|
||||
const refresherTriggered = ref(false);
|
||||
|
||||
// 获取活动状态
|
||||
const getActivityStatus = (item) => {
|
||||
const now = moment();
|
||||
const signStartTime = moment(item.signStartTime);
|
||||
const signEndTime = moment(item.signEndTime);
|
||||
const activityStartTime = moment(item.startTime);
|
||||
const activityEndTime = moment(item.endTime);
|
||||
|
||||
if (now.isBefore(signStartTime)) {
|
||||
return 'notStartSign'; // 未开始报名
|
||||
} else if (now.isBetween(signStartTime, signEndTime)) {
|
||||
return 'signing'; // 报名中
|
||||
} else if (now.isBetween(signEndTime, activityStartTime)) {
|
||||
return 'notStartActivity'; // 活动未开始
|
||||
} else if (now.isBetween(activityStartTime, activityEndTime)) {
|
||||
return 'activitying'; // 活动进行中
|
||||
} else {
|
||||
return 'ended'; // 已结束
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
return {
|
||||
'notStartSign': '未开始报名',
|
||||
'signing': '报名中',
|
||||
'notStartActivity': '活动未开始',
|
||||
'activitying': '活动进行中',
|
||||
'ended': '已结束'
|
||||
}[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;
|
||||
@@ -221,13 +170,13 @@ const getListData = async (isRefresh = false) => {
|
||||
}
|
||||
};
|
||||
|
||||
// scroll-view 下拉刷新
|
||||
//下拉刷新
|
||||
const handleRefresh = () => {
|
||||
if (loading.value) {
|
||||
refresherTriggered.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
refresherTriggered.value = true;
|
||||
pageForm.pageNumber = 1;
|
||||
getListData(true);
|
||||
@@ -241,7 +190,7 @@ const handleSearch = () => {
|
||||
getListData(true);
|
||||
};
|
||||
|
||||
// scroll-view 触底加载更多
|
||||
// 触底加载更多
|
||||
const handleScrollToBottom = () => {
|
||||
if (loading.value || !hasMore.value) return;
|
||||
getListData();
|
||||
@@ -276,38 +225,6 @@ onMounted(() => {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.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;
|
||||
@@ -319,82 +236,26 @@ onMounted(() => {
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.activity-card {
|
||||
.card-item {
|
||||
width: 100%;
|
||||
margin-bottom: 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
transition: transform 0.3s;
|
||||
|
||||
//background: url("https://assets.cumen.fun/upload/017cdaf1-c5fa-788e-eea2-d0ee249f902b/0189257e-fc75-5b89-b900-104c706f2d63") top / 100% auto no-repeat,
|
||||
//linear-gradient(to right, white, #ecb5b5);
|
||||
//background: no-repeat, linear-gradient(to right, white, #f6d4d4);
|
||||
background: #ffffff;
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.image-container {
|
||||
position: relative;
|
||||
height: 360rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.activity-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.4s;
|
||||
|
||||
.activity-card:hover & {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
position: absolute;
|
||||
left: 16rpx;
|
||||
bottom: 16rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
color: #fff;
|
||||
font-size: 22rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
padding: 20rpx 24rpx 10rpx;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
width: 100%;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.activity-address {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.activity-address :deep(.t-icon) {
|
||||
margin-right: 8rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.status-tag-container {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 16rpx;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
|
||||
<!-- <view class="act-info">-->
|
||||
<!-- <view class="act-title">奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑</view>-->
|
||||
<!-- <view class="act-meta">-->
|
||||
<!-- <view class="act-meta-item">22222</view>-->
|
||||
<!-- <view class="act-meta-item">22222222222222222222</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
<!-- <view class="act-info">-->
|
||||
<!-- <view class="act-title">奋进新征程 运动促健康 2024晴江第六届"牧城之夜"荧光跑</view>-->
|
||||
<!-- <view class="act-meta">-->
|
||||
<!-- <view class="act-meta-item">22222</view>-->
|
||||
<!-- <view class="act-meta-item">22222222222222222222</view>-->
|
||||
<!-- </view>-->
|
||||
<!-- </view>-->
|
||||
|
||||
<view class="form-area">
|
||||
<t-form
|
||||
@@ -44,7 +44,7 @@
|
||||
<view class="user-card-header-title">
|
||||
<span>{{ index + 1 }}</span>
|
||||
</view>
|
||||
<view class="remove" @click="formData.userList.splice(index, 1)">
|
||||
<view class="remove" @click="removeUser(index)">
|
||||
<t-icon name="delete" size="18px"></t-icon>
|
||||
</view>
|
||||
</view>
|
||||
@@ -59,7 +59,8 @@
|
||||
<t-radio :block="false" label="女" value="女"/>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
<t-form-item label="职务" :name="`userList[${index}].position`" :rules="[{required:true,message: '请输入职务'}]">
|
||||
<t-form-item label="职务" :name="`userList[${index}].position`"
|
||||
:rules="[{required:true,message: '请输入职务'}]">
|
||||
<t-input v-model="user.position" borderless placeholder="请输入职务"></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item label="身份证号" :name="`userList[${index}].idCard`"
|
||||
@@ -74,7 +75,7 @@
|
||||
:rules="[{required:true,message: '请选择是否住宿'}]">
|
||||
<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="2" label="否"/>
|
||||
<t-radio :block="false" name="radio" value="0" label="否"/>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
</view>
|
||||
@@ -109,36 +110,141 @@
|
||||
<script setup>
|
||||
import {ref} from 'vue'
|
||||
import {useRequest} from '@/utils/request';
|
||||
import {Toast} from "tdesign-mobile-vue";
|
||||
import {Dialog, Toast} from "tdesign-mobile-vue";
|
||||
import {onLoad} from "@dcloudio/uni-app";
|
||||
|
||||
const request = useRequest()
|
||||
const actId = ref(null)
|
||||
const agreementChecked = ref(false)
|
||||
const formRef = ref(null)
|
||||
const formData = ref({
|
||||
unitName: null,
|
||||
userList: []
|
||||
unitName: "测试",
|
||||
contactName: "测试",
|
||||
contactPhone: "18899998888",
|
||||
userList: [{
|
||||
userName: "测试",
|
||||
sex: "男",
|
||||
position: "测试",
|
||||
idCard: "421087200004040010",
|
||||
phone: "18899998888",
|
||||
isStay: "1"
|
||||
}]
|
||||
})
|
||||
|
||||
onLoad((option) => {
|
||||
if (option.id) {
|
||||
actId.value = option.id
|
||||
formData.value.activityId = option.id
|
||||
listSignUpUser()
|
||||
} else {
|
||||
Toast({message: 'ID不存在', theme: 'error'});
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// 查询报名人员信息
|
||||
async function listSignUpUser() {
|
||||
try {
|
||||
const {code, data} = await request.get('/client/activity/list-signup-user/' + actId.value)
|
||||
if (code === 0 && data) {
|
||||
if (data.length > 0) {
|
||||
formData.value.userList = data
|
||||
}
|
||||
} else {
|
||||
Toast({message: '查询报名人员信息失败', theme: 'error'});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
async function validaForm() {
|
||||
return new Promise((resolve, reject) => {
|
||||
formRef.value.validate().then(async (valid) => {
|
||||
resolve(valid === true)
|
||||
}).catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 承诺书
|
||||
async function validateAgreement() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (agreementChecked.value) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
Dialog.confirm({
|
||||
title: '提示',
|
||||
content: '参赛条款与免责声明:阅读并同意本承诺书内容,点击"确定并报名"即表示您已充分理解并接受所有条款,承担相应法律责任。',
|
||||
confirmBtn: '确定并报名',
|
||||
cancelBtn: '取消',
|
||||
onConfirm: () => {
|
||||
agreementChecked.value = true
|
||||
resolve(true)
|
||||
},
|
||||
onCancel: () => {
|
||||
agreementChecked.value = false
|
||||
resolve(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
async function onSubmit() {
|
||||
console.log(formRef)
|
||||
console.log(formRef.value)
|
||||
formRef.value.validate()
|
||||
try{
|
||||
const {code,data} = await request.post('/client/activity/sign', formData.value)
|
||||
const formRes = await validaForm()
|
||||
if (!formRes) return
|
||||
|
||||
const agreementRes = await validateAgreement()
|
||||
if (!agreementRes) return
|
||||
|
||||
// 提交表单
|
||||
await submitForm()
|
||||
}
|
||||
|
||||
// 报名
|
||||
async function submitForm() {
|
||||
uni.showLoading({
|
||||
title: '提交中...',
|
||||
mask: true,
|
||||
})
|
||||
try {
|
||||
const {code, data, msg} = await request.post('/client/activity/sign-up', formData.value)
|
||||
if (code === 0) {
|
||||
Toast.success('报名成功')
|
||||
// setTimeout(() => {
|
||||
// uni.navigateBack()
|
||||
// }, 1500)
|
||||
}else{
|
||||
Toast.error(data.msg)
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/activity/registration-form/success?id=' + actId.value
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
Dialog.alert({
|
||||
title: '报名失败',
|
||||
content: msg,
|
||||
confirmBtn: '确定'
|
||||
})
|
||||
}
|
||||
}catch (e) {
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
Toast.error(e.message)
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 移除人员
|
||||
async function removeUser(index) {
|
||||
if (formData.value.userList.length === 1) {
|
||||
Toast('至少需要一个报名人员')
|
||||
return
|
||||
}
|
||||
formData.value.userList.splice(index, 1)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@@ -185,7 +291,7 @@ async function onSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
.form-area{
|
||||
.form-area {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
//margin-top: 100px;
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 成功提示区域 -->
|
||||
<view class="success-content">
|
||||
<!-- 成功图片 -->
|
||||
<view class="success-image">
|
||||
<image src="https://cdn-icons-png.flaticon.com/512/10613/10613684.png" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<!-- 成功信息 -->
|
||||
<view class="success-info">
|
||||
<view class="success-title">报名成功</view>
|
||||
<view class="success-subtitle">您已成功报名参加"{{ activityTitle }}"</view>
|
||||
</view>
|
||||
|
||||
<!-- 小提示 -->
|
||||
<view class="success-tips">
|
||||
<t-icon name="info-circle-filled" size="16px" />
|
||||
<text>活动开始前我们将通过短信通知您</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="action-buttons">
|
||||
<t-button theme="default" shape="round" @click="navigateToHome">返回首页</t-button>
|
||||
<t-button theme="primary" shape="round" @click="viewRegistrationDetails">查看报名详情</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import { onLoad } from '@dcloudio/uni-app';
|
||||
import { Toast } from 'tdesign-mobile-vue';
|
||||
import { useRequest } from '@/utils/request';
|
||||
|
||||
const request = useRequest();
|
||||
const actId = ref(null);
|
||||
const activityTitle = ref('活动名称');
|
||||
|
||||
// 获取路由参数
|
||||
onLoad((option) => {
|
||||
if (option.id) {
|
||||
actId.value = option.id;
|
||||
// 获取活动信息和标题
|
||||
getActivityInfo(option.id);
|
||||
} else {
|
||||
Toast({message: 'ID不存在', theme: 'error'});
|
||||
}
|
||||
});
|
||||
|
||||
// 获取活动信息(只获取标题)
|
||||
const getActivityInfo = async (id) => {
|
||||
try {
|
||||
const {code, data} = await request.get('/client/activity/get/' + id);
|
||||
if (code === 0 && data) {
|
||||
activityTitle.value = data.title;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取活动信息失败', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 返回首页
|
||||
const navigateToHome = () => {
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
};
|
||||
|
||||
// 查看报名详情
|
||||
const viewRegistrationDetails = () => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/activity/registration-details/index?id=' + actId.value
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.page-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 50px;
|
||||
background-color: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 成功内容区域 */
|
||||
.success-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 成功图片 */
|
||||
.success-image {
|
||||
margin-bottom: 30px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
|
||||
image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
animation: float 3s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 成功信息 */
|
||||
.success-info {
|
||||
position: relative;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.success-title {
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.success-subtitle {
|
||||
color: #666;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 成功提示 */
|
||||
.success-tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(255, 95, 92, 0.1);
|
||||
border-radius: 100px;
|
||||
padding: 10px 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.t-icon {
|
||||
color: #ff5f5c;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
text {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 按钮区域 */
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
margin-top: 20px;
|
||||
|
||||
:deep(.t-button) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
:deep(.t-button--primary) {
|
||||
background-color: #ff5f5c !important;
|
||||
border-color: #ff5f5c !important;
|
||||
}
|
||||
|
||||
:deep(.t-button--default) {
|
||||
border-color: #ff5f5c !important;
|
||||
color: #ff5f5c !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,53 +1,60 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<t-dropdown-menu>
|
||||
<t-dropdown-menu class="top-dropdown">
|
||||
<t-dropdown-item :options="actOptions" :value="selectActId" @change="getSignInfo"/>
|
||||
</t-dropdown-menu>
|
||||
<view class="content-container">
|
||||
<view class="scan-area" @click="handleScan">
|
||||
<view class="scan-icon-wrapper">
|
||||
<view class="scan-area animate__animated animate__fadeIn" @click="handleScan">
|
||||
<view class="scan-icon-wrapper pulse-animation">
|
||||
<t-icon name="scan" size="80" color="#f45a5a"></t-icon>
|
||||
<view class="scan-circles"></view>
|
||||
</view>
|
||||
<t-button shape="round"
|
||||
theme="primary"
|
||||
size="medium">扫描二维码签到
|
||||
size="medium"
|
||||
class="scan-button">扫描二维码签到
|
||||
</t-button>
|
||||
</view>
|
||||
<view class="guide-area">
|
||||
<view class="guide-area animate__animated animate__fadeInUp">
|
||||
<view class="guide-title">签到说明</view>
|
||||
<view class="guide-list">
|
||||
<view class="guide-item">
|
||||
<uni-icons class="guide-icon" type="info" size="16" color="#999999"/>
|
||||
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.1s">
|
||||
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
|
||||
<text class="guide-text">请对准活动二维码进行扫描</text>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<uni-icons class="guide-icon" type="info" size="16" color="#999999"/>
|
||||
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.2s">
|
||||
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
|
||||
<text class="guide-text">确保手机相机权限已开启</text>
|
||||
</view>
|
||||
<view class="guide-item">
|
||||
<uni-icons class="guide-icon" type="info" size="16" color="#999999"/>
|
||||
<view class="guide-item animate__animated animate__fadeInLeft" style="animation-delay: 0.3s">
|
||||
<t-icon name="notification" size="16" color="#ff5f5c" class="guide-icon"></t-icon>
|
||||
<text class="guide-text">保持二维码清晰可见</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="current-status-area">
|
||||
<view class="current-status-area animate__animated animate__fadeInUp" style="animation-delay: 0.2s">
|
||||
<view class="status-header">
|
||||
<text class="status-title">当前签到状态</text>
|
||||
</view>
|
||||
<view v-if="signInfo" class="status-content">
|
||||
<template v-if="signInfo.isSignIn">
|
||||
<view class="sign-success">
|
||||
<t-icon name="task-checked-1" size="48" color="#07c160"></t-icon>
|
||||
<div class="success-icon-wrapper">
|
||||
<t-icon name="task-checked-1" size="48" color="#07c160"></t-icon>
|
||||
<svg class="checkmark-circle" viewBox="0 0 52 52">
|
||||
<circle class="checkmark-circle__circle" cx="26" cy="26" r="25" fill="none" />
|
||||
</svg>
|
||||
</div>
|
||||
<text class="success-text">已完成签到</text>
|
||||
<view class="sign-detail">
|
||||
<view class="detail-item">
|
||||
<uni-icons type="calendar" size="14" color="#666666" />
|
||||
<uni-icons type="calendar" size="14" color="#666666"/>
|
||||
<text>签到时间:{{ signInfo.signInTime }}</text>
|
||||
</view>
|
||||
<view class="detail-item">
|
||||
<uni-icons type="location" size="14" color="#666666" />
|
||||
<text>签到地点:{{ signInfo.location }}</text>
|
||||
</view>
|
||||
<!-- <view class="detail-item">-->
|
||||
<!-- <uni-icons type="location" size="14" color="#666666"/>-->
|
||||
<!-- <text>签到地点:{{ signInfo.location }}</text>-->
|
||||
<!-- </view>-->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -61,6 +68,29 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<div class="bg-decoration">
|
||||
<svg class="wave" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
|
||||
<path fill="#f45a5a" fill-opacity="0.1" d="M0,160L48,154.7C96,149,192,139,288,144C384,149,480,171,576,176C672,181,768,171,864,154.7C960,139,1056,117,1152,128C1248,139,1344,181,1392,202.7L1440,224L1440,0L1392,0C1344,0,1248,0,1152,0C1056,0,960,0,864,0C768,0,672,0,576,0C480,0,384,0,288,0C192,0,96,0,48,0L0,0Z"></path>
|
||||
</svg>
|
||||
<svg class="dots" width="100" height="100" viewBox="0 0 100 100">
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="10" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="10" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="10" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="10" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="10" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="30" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="30" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="30" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="30" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="30" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="10" cy="50" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="30" cy="50" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="50" cy="50" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="70" cy="50" r="3" />
|
||||
<circle fill="#f45a5a" fill-opacity="0.2" cx="90" cy="50" r="3" />
|
||||
</svg>
|
||||
</div>
|
||||
</view>
|
||||
|
||||
<ScanQRCode
|
||||
@@ -73,7 +103,7 @@
|
||||
<script setup>
|
||||
import {ref, computed, onMounted} from 'vue';
|
||||
import {useRequest} from '@/utils/request';
|
||||
import {Toast} from 'tdesign-mobile-vue';
|
||||
import {Dialog, Toast} from 'tdesign-mobile-vue';
|
||||
import moment from 'moment';
|
||||
import ScanQRCode from '@/components/scanQrCode.vue'
|
||||
|
||||
@@ -99,15 +129,16 @@ onMounted(() => {
|
||||
// 获取活动列表
|
||||
async function listMineAct() {
|
||||
try {
|
||||
const {data, code} = await request.get('/client/activity/list-mine-act')
|
||||
const {data, code, msg} = await request.get('/client/activity/list-mine-act')
|
||||
if (code === 0) {
|
||||
list.value = data
|
||||
if (list.value.length) {
|
||||
selectActId.value = list.value[0].id
|
||||
console.log(selectActId.value)
|
||||
await getSignInfo();
|
||||
}
|
||||
} else {
|
||||
Toast.error(data.msg)
|
||||
Toast.error(msg)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
@@ -116,9 +147,9 @@ async function listMineAct() {
|
||||
}
|
||||
|
||||
// 获取签到信息
|
||||
async function getSignInfo(val){
|
||||
async function getSignInfo(val) {
|
||||
try {
|
||||
const {code, data, msg} = await request.get('/client/activity/sign-info/' + (val ? val :selectActId.value))
|
||||
const {code, data, msg} = await request.get('/client/activity/sign-in-info/' + (val ? val : selectActId.value))
|
||||
if (code === 0) {
|
||||
signInfo.value = data
|
||||
} else {
|
||||
@@ -135,26 +166,125 @@ function handleScan() {
|
||||
qrScannerRef.value.startScan()
|
||||
}
|
||||
|
||||
function handleScanSuccess(val){
|
||||
console.log(val)
|
||||
Toast.success(val)
|
||||
// 扫码成功回调
|
||||
function handleScanSuccess(val) {
|
||||
console.log('扫描成功,活动ID为' + val, "当前选择活动ID为" + selectActId.value)
|
||||
if (!val) {
|
||||
Toast.error("请扫描合法的二维码")
|
||||
return
|
||||
}
|
||||
if (String(val) !== String(selectActId.value)) {
|
||||
Toast.error('请扫码签到当前活动,扫描成功,活动ID为' + val + ",当前选择活动ID为" + selectActId.value)
|
||||
return
|
||||
}
|
||||
signIn()
|
||||
}
|
||||
|
||||
// 签到
|
||||
async function signIn() {
|
||||
try {
|
||||
await uni.showLoading({
|
||||
title: '签到中...',
|
||||
mask: true,
|
||||
})
|
||||
const {code, msg} = await request.post('/client/activity/sign-in/' + selectActId.value)
|
||||
uni.hideLoading()
|
||||
Dialog.alert({
|
||||
title: code === 0 ? '签到成功' : '签到失败',
|
||||
content: code === 0 ? '祝您活动顺利' : msg,
|
||||
confirmBtn: '确定'
|
||||
})
|
||||
if (code === 0) {
|
||||
await getSignInfo()
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e.message)
|
||||
console.error(e)
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes ripple {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.5);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes checkmark-circle {
|
||||
0% {
|
||||
stroke-dashoffset: 166;
|
||||
}
|
||||
100% {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.top-dropdown {
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 15px;
|
||||
z-index: 9;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.scan-area {
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 64rpx 0;
|
||||
border-radius: 16px;
|
||||
padding: 64px 0;
|
||||
margin-bottom: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
}
|
||||
|
||||
.scan-icon-wrapper {
|
||||
@@ -164,6 +294,62 @@ function handleScanSuccess(val){
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 32rpx;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pulse-animation {
|
||||
animation: pulse 2s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.scan-circles {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
&:before, &:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid rgba(244, 90, 90, 0.3);
|
||||
animation: ripple 2s infinite;
|
||||
}
|
||||
|
||||
&:after {
|
||||
animation-delay: 0.5s;
|
||||
}
|
||||
}
|
||||
|
||||
.scan-button {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:active {
|
||||
transform: translateY(2px);
|
||||
}
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
transform: rotate(30deg);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover:after {
|
||||
transform: rotate(30deg) translate(-30%, -30%);
|
||||
}
|
||||
}
|
||||
|
||||
.guide-area {
|
||||
@@ -171,6 +357,9 @@ function handleScanSuccess(val){
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.guide-title {
|
||||
@@ -178,18 +367,37 @@ function handleScanSuccess(val){
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 24rpx;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8rpx;
|
||||
left: 0;
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background-color: #f45a5a;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.guide-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(244, 90, 90, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.guide-icon {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -202,7 +410,9 @@ function handleScanSuccess(val){
|
||||
background-color: #ffffff;
|
||||
border-radius: 16rpx;
|
||||
padding: 32rpx;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-header {
|
||||
@@ -214,6 +424,18 @@ function handleScanSuccess(val){
|
||||
font-weight: 500;
|
||||
color: #333333;
|
||||
margin-bottom: 0.75rem;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -8rpx;
|
||||
left: 0;
|
||||
width: 40rpx;
|
||||
height: 4rpx;
|
||||
background-color: #f45a5a;
|
||||
border-radius: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
.status-content {
|
||||
@@ -230,6 +452,34 @@ function handleScanSuccess(val){
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success-icon-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
}
|
||||
|
||||
.checkmark-circle {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
|
||||
&__circle {
|
||||
stroke-dasharray: 166;
|
||||
stroke-dashoffset: 166;
|
||||
stroke-width: 2;
|
||||
stroke: #07c160;
|
||||
stroke-miterlimit: 10;
|
||||
fill: none;
|
||||
animation: checkmark-circle 0.6s cubic-bezier(0.65, 0, 0.45, 1) forwards;
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
.success-text {
|
||||
font-size: 20px;
|
||||
color: #07c160;
|
||||
@@ -239,6 +489,9 @@ function handleScanSuccess(val){
|
||||
|
||||
.sign-detail {
|
||||
margin-top: 32rpx;
|
||||
background-color: rgba(7, 193, 96, 0.05);
|
||||
padding: 16rpx 24rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
@@ -247,6 +500,7 @@ function handleScanSuccess(val){
|
||||
margin-bottom: 16rpx;
|
||||
color: #666666;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.detail-item uni-icons {
|
||||
@@ -265,4 +519,117 @@ function handleScanSuccess(val){
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.bg-decoration {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.wave {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.dots {
|
||||
position: absolute;
|
||||
bottom: 5%;
|
||||
right: 5%;
|
||||
animation: float 4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* 导入Animate.css的部分动画 */
|
||||
.animate__animated {
|
||||
animation-duration: 1s;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.animate__fadeIn {
|
||||
animation-name: fadeIn;
|
||||
}
|
||||
|
||||
.animate__fadeInUp {
|
||||
animation-name: fadeInUp;
|
||||
}
|
||||
|
||||
.animate__fadeInLeft {
|
||||
animation-name: fadeInLeft;
|
||||
}
|
||||
|
||||
.animate__bounceIn {
|
||||
animation-name: bounceIn;
|
||||
}
|
||||
|
||||
.animate__pulse {
|
||||
animation-name: pulse;
|
||||
}
|
||||
|
||||
.animate__infinite {
|
||||
animation-iteration-count: infinite;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(0, 40px, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInLeft {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translate3d(-40px, 0, 0);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bounceIn {
|
||||
from, 20%, 40%, 60%, 80%, to {
|
||||
animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale3d(0.3, 0.3, 0.3);
|
||||
}
|
||||
20% {
|
||||
transform: scale3d(1.1, 1.1, 1.1);
|
||||
}
|
||||
40% {
|
||||
transform: scale3d(0.9, 0.9, 0.9);
|
||||
}
|
||||
60% {
|
||||
opacity: 1;
|
||||
transform: scale3d(1.03, 1.03, 1.03);
|
||||
}
|
||||
80% {
|
||||
transform: scale3d(0.97, 0.97, 0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale3d(1, 1, 1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,442 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<t-dropdown-menu class="top-dropdown">
|
||||
<t-dropdown-item :options="actOptions" :value="selectActId"/>
|
||||
</t-dropdown-menu>
|
||||
|
||||
<view class="content-container">
|
||||
<view class="page-header">
|
||||
<text class="page-title">团队报名</text>
|
||||
<text class="page-subtitle">选择您感兴趣的项目进行报名</text>
|
||||
</view>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
<view class="project-cards">
|
||||
<view
|
||||
v-for="project in itemOptions"
|
||||
:key="project.id"
|
||||
class="project-card"
|
||||
:class="{'project-card--registered': registrationStatus[project.id]?.length}"
|
||||
@click="selectProject(project.id)"
|
||||
>
|
||||
<view class="project-card__content">
|
||||
<view class="project-card__title">{{ project.name }}</view>
|
||||
<view class="project-card__desc">{{ project.description }}</view>
|
||||
</view>
|
||||
<view class="project-card__status">
|
||||
<t-tag v-if="registrationStatus[project.id]?.length" theme="success" variant="light-outline">
|
||||
已报名 {{ registrationStatus[project.id]?.length }}人
|
||||
</t-tag>
|
||||
<view v-else class="project-card__action">
|
||||
<t-icon name="chevron-right" color="#999"/>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="registrationStatus[project.id]?.length" class="project-card__checkmark">
|
||||
<t-icon name="check" color="#52c41a"/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<t-popup v-model:visible="showMemberPicker" placement="bottom" destroy-on-close>
|
||||
<view class="member-picker">
|
||||
<view class="picker-header">
|
||||
<text class="picker-title">选择项目参与成员</text>
|
||||
<t-icon name="close" @click="showMemberPicker = false"/>
|
||||
</view>
|
||||
|
||||
<view class="picker-subtitle" v-if="selectedItemId">
|
||||
{{ itemOptions.find(p => p.id === selectedItemId)?.name }}
|
||||
</view>
|
||||
|
||||
<view class="member-list">
|
||||
<t-checkbox-group v-model="selectedMemberIds">
|
||||
<view
|
||||
v-for="member in teamMembers"
|
||||
:key="member.id"
|
||||
>
|
||||
<t-checkbox
|
||||
:value="member.id"
|
||||
:label="member.userName"
|
||||
:content="member.unitName"
|
||||
:disabled="registrationStatus[selectedItemId]?.includes(member.id)"
|
||||
icon-size="20px"
|
||||
/>
|
||||
<view class="member-status" v-if="registrationStatus[selectedItemId]?.includes(member.id)">
|
||||
已报名
|
||||
</view>
|
||||
</view>
|
||||
</t-checkbox-group>
|
||||
</view>
|
||||
|
||||
<view class="picker-footer">
|
||||
<t-button block theme="primary" @click="submitRegistration" :disabled="!selectedMemberIds.length">
|
||||
确认报名 ({{ selectedMemberIds.length }}人)
|
||||
</t-button>
|
||||
</view>
|
||||
</view>
|
||||
</t-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, onMounted, computed} from 'vue';
|
||||
import {useRequest} from '@/utils/request';
|
||||
import {Dialog, Toast} from 'tdesign-mobile-vue';
|
||||
import {onLoad} from '@dcloudio/uni-app';
|
||||
|
||||
const request = useRequest();
|
||||
|
||||
// 活动数据
|
||||
const actOptions = ref([])
|
||||
// 选中的活动ID
|
||||
const selectActId = ref(null);
|
||||
// 项目列表数据
|
||||
const itemOptions = ref([
|
||||
{
|
||||
id: 1,
|
||||
name: '篮球比赛',
|
||||
description: '3v3篮球友谊赛'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '足球比赛',
|
||||
description: '5人制足球赛'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '羽毛球比赛',
|
||||
description: '双打友谊赛'
|
||||
}
|
||||
]);
|
||||
// 选中的项目ID
|
||||
const selectedItemId = ref(null);
|
||||
// 团队成员列表
|
||||
const teamMembers = ref([]);
|
||||
|
||||
onLoad((option) => {
|
||||
if (option.id) {
|
||||
selectActId.value = option.id;
|
||||
}
|
||||
});
|
||||
onMounted(async () => {
|
||||
await listActs()
|
||||
await listItems();
|
||||
listTeamUser()
|
||||
// fetchRegistrationStatus();
|
||||
});
|
||||
|
||||
// 选中的团队成员
|
||||
const selectedMemberIds = ref([]);
|
||||
|
||||
// 成员选择弹窗显示状态
|
||||
const showMemberPicker = ref(false);
|
||||
|
||||
// 当前项目的报名状态
|
||||
const registrationStatus = ref({});
|
||||
|
||||
// 获取活动列表
|
||||
async function listActs() {
|
||||
try {
|
||||
const {data, code, msg} = await request.get('/client/activity/list-mine-act')
|
||||
if (code === 0) {
|
||||
actOptions.value = data && data.map(item => {
|
||||
return {
|
||||
value: item.id,
|
||||
label: item.title
|
||||
}
|
||||
})
|
||||
if(selectActId.value === null && data.length){
|
||||
selectActId.value = data[0].id
|
||||
}
|
||||
} else {
|
||||
Toast.error(msg)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
Toast.error(e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取项目列表
|
||||
async function listItems() {
|
||||
try {
|
||||
const {code, data, msg} = await request.get('/client/item/list/' + selectActId.value)
|
||||
if (code === 0) {
|
||||
itemOptions.value = data;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取团队成员
|
||||
async function listTeamUser(){
|
||||
try {
|
||||
const {code, data, msg} = await request.get('/client/item/list-team-user/'+selectActId.value)
|
||||
if (code === 0) {
|
||||
teamMembers.value = data;
|
||||
}else{
|
||||
Toast.error(msg)
|
||||
}
|
||||
}catch (e){
|
||||
Toast.error(msg)
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取报名状态 - 使用静态数据
|
||||
const fetchRegistrationStatus = async () => {
|
||||
|
||||
};
|
||||
|
||||
// 选择项目
|
||||
const selectProject = (projectId) => {
|
||||
selectedItemId.value = projectId;
|
||||
selectedMemberIds.value = [];
|
||||
showMemberPicker.value = true;
|
||||
};
|
||||
|
||||
// 提交报名 - 使用静态数据模拟
|
||||
const submitRegistration = async () => {
|
||||
if (!selectedMemberIds.value.length) {
|
||||
Toast.warning('请选择团队成员');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(selectedMemberIds.value)
|
||||
console.log(selectedItemId.value)
|
||||
console.log(selectActId.value)
|
||||
|
||||
const teamUser = teamMembers.value.filter(v=>selectedMemberIds.value.includes(v.id))
|
||||
|
||||
// const {code, msg} = await request.post('/client/item/sign-up', {
|
||||
// activityId: selectActId.value,
|
||||
// itemId: selectedItemId.value,
|
||||
// userList: selectedMemberIds.value
|
||||
// })
|
||||
// if (code === 0) {
|
||||
// Toast.success('报名成功')
|
||||
// showMemberPicker.value = false;
|
||||
// selectedMemberIds.value = []
|
||||
// }else{
|
||||
// Toast.error(msg)
|
||||
// }
|
||||
}catch (e){
|
||||
Toast.error(e.message)
|
||||
console.error(e)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.top-dropdown {
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
padding: 15px;
|
||||
z-index: 9;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
padding: 0 4px;
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-subtitle {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.project-cards {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.project-card {
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&:active {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&--registered {
|
||||
background: #fff;
|
||||
|
||||
.project-card__title,
|
||||
.project-card__desc {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
&__content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__status {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&__action {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__checkmark {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
color: #52c41a;
|
||||
}
|
||||
}
|
||||
|
||||
.member-picker {
|
||||
background-color: #fff;
|
||||
border-radius: 16px 16px 0 0;
|
||||
padding: 20px;
|
||||
|
||||
.picker-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.picker-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-subtitle {
|
||||
font-size: 14px;
|
||||
color: #ff5f5c;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
max-height: 50vh;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 20px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.member-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.member-status {
|
||||
font-size: 12px;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
:deep(.t-checkbox) {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-footer {
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加动画效果
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.project-card {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
animation-fill-mode: both;
|
||||
}
|
||||
|
||||
.project-card:nth-child(1) {
|
||||
animation-delay: 0.05s;
|
||||
}
|
||||
|
||||
.project-card:nth-child(2) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.project-card:nth-child(3) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.project-card:nth-child(4) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.project-card:nth-child(5) {
|
||||
animation-delay: 0.25s;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,112 +1,116 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<t-dropdown-menu @menu-opened="handleMenuOpened" @menu-closed="handleMenuClosed">
|
||||
<t-dropdown-item :options="actOptions" :value="pageForm.actId" @change="actChange" />
|
||||
</t-dropdown-menu>
|
||||
<view class="page-container" :style="{ backgroundColor: '#f7f7f9' }">
|
||||
<t-sticky>
|
||||
<t-dropdown-menu class="top-dropdown-menu">
|
||||
<t-dropdown-item :options="actOptions" :value="pageForm.actId" @change="actChange" />
|
||||
</t-dropdown-menu>
|
||||
</t-sticky>
|
||||
|
||||
<!-- 团队卡片 -->
|
||||
<div class="content-area">
|
||||
<!-- 团队信息卡片 -->
|
||||
<div class="team-card">
|
||||
<div class="team-banner">
|
||||
<div class="team-header">
|
||||
<div class="team-logo">
|
||||
<i class="bi bi-people-fill" v-if="!team.logoUrl"></i>
|
||||
<img :src="team.logoUrl" alt="团队logo" v-if="team.logoUrl">
|
||||
</div>
|
||||
<div class="team-info">
|
||||
|
||||
<div class="team-basic-info">
|
||||
<div class="team-name">{{ team.name }}</div>
|
||||
<div class="team-meta">创建于 {{ team.createdDate }}</div>
|
||||
<div class="team-id">团队ID: {{ team.id }}</div>
|
||||
</div>
|
||||
<div class="event-name">{{ team.eventName }}</div>
|
||||
</div>
|
||||
|
||||
<div class="team-detail">
|
||||
<div class="team-stat-row">
|
||||
<div class="team-stat-item">
|
||||
<div class="team-stat-value">{{ team.memberCount }}</div>
|
||||
<div class="team-stat-label">团队成员</div>
|
||||
</div>
|
||||
<div class="team-stat-item">
|
||||
<div class="team-stat-value">{{ team.rank }}</div>
|
||||
<div class="team-stat-label">当前排名</div>
|
||||
</div>
|
||||
<div class="team-stat-item">
|
||||
<div class="team-stat-value">{{ team.score }}</div>
|
||||
<div class="team-stat-label">团队积分</div>
|
||||
|
||||
<div class="team-stats">
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon"><i class="bi bi-people"></i></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ team.memberCount }}</div>
|
||||
<div class="stat-label">成员</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="font-size: 14px; color: #666; line-height: 1.6;">
|
||||
{{ team.description }}
|
||||
<div class="stat-divider"></div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-icon"><i class="bi bi-trophy"></i></div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ team.rank }}</div>
|
||||
<div class="stat-label">排名</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="team.description" class="team-desc-toggle" @click="showDesc = !showDesc">
|
||||
<span>团队介绍</span>
|
||||
<i class="bi" :class="showDesc ? 'bi-chevron-up' : 'bi-chevron-down'"></i>
|
||||
</div>
|
||||
|
||||
<div v-if="showDesc && team.description" class="team-desc">
|
||||
<i class="bi bi-quote"></i>
|
||||
{{ team.description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 团队成员 -->
|
||||
<div class="team-section">
|
||||
<!-- 项目报名按钮 -->
|
||||
<div class="action-section">
|
||||
<t-button block theme="danger" size="large" class="register-btn" @click="goToEventRegistration">
|
||||
参与项目报名
|
||||
</t-button>
|
||||
</div>
|
||||
|
||||
<!-- 团队成员列表 -->
|
||||
<div class="members-section">
|
||||
<div class="section-header">
|
||||
<div class="section-title">
|
||||
<i class="bi bi-people"></i>团队成员
|
||||
<i class="bi bi-people-fill"></i>
|
||||
团队成员
|
||||
</div>
|
||||
<div class="section-badge">{{ team.memberCount }}</div>
|
||||
</div>
|
||||
|
||||
<div class="member-list">
|
||||
<div class="member-item" v-for="member in team.members" :key="member.id">
|
||||
<div class="member-avatar">
|
||||
<img :src="member.avatar" :alt="member.name" onerror="this.src='data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIj48cmVjdCB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgZmlsbD0iI2YwZjBmMCIvPjx0ZXh0IHg9IjUwJSIgeT0iNTAlIiBmb250LXNpemU9IjIwIiB0ZXh0LWFuY2hvcj0ibWlkZGxlIiBhbGlnbm1lbnQtYmFzZWxpbmU9Im1pZGRsZSIgZm9udC1mYW1pbHk9IkFyaWFsLCBzYW5zLXNlcmlmIiBmaWxsPSIjNTU1Ij57eyBtZW1iZXIubmFtZS5jaGFyQXQoMCkgfX08L3RleHQ+PC9zdmc+'">
|
||||
<div class="member-grid">
|
||||
<div class="member-grid-item" v-for="member in team.members" :key="member.id">
|
||||
<div class="member-name">
|
||||
{{ member.name }}
|
||||
<t-tag theme="danger" size="small" v-if="member.role === 'leader'">队长</t-tag>
|
||||
</div>
|
||||
<div class="member-info">
|
||||
<div class="member-name">
|
||||
{{ member.name }}
|
||||
<span class="member-role" v-if="member.role === 'leader'">队长</span>
|
||||
</div>
|
||||
<div class="member-detail">
|
||||
<div class="member-detail-item">
|
||||
<i class="bi bi-gender-ambiguous"></i>
|
||||
<span>{{ member.gender }}</span>
|
||||
</div>
|
||||
<div class="member-detail-item">
|
||||
<i class="bi bi-building"></i>
|
||||
<span>{{ member.organization }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="member-actions">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
<div class="member-details">
|
||||
<span><i class="bi bi-gender-ambiguous"></i> {{ member.gender }}</span>
|
||||
<span><i class="bi bi-building"></i> {{ member.organization }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前角色与申请队长 -->
|
||||
<div v-if="currentUserRole !== 'leader'">
|
||||
<div class="role-notice">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
您当前是普通成员身份
|
||||
<div class="role-section">
|
||||
<div class="role-card" v-if="currentUserRole === 'leader'">
|
||||
<div class="role-icon">
|
||||
<i class="bi bi-trophy"></i>
|
||||
</div>
|
||||
<div class="role-info">
|
||||
<div class="role-title">您是队长</div>
|
||||
<div class="role-desc">拥有团队所有管理权限</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div class="role-card">
|
||||
<div class="role-icon">
|
||||
<i class="bi bi-person"></i>
|
||||
</div>
|
||||
<div class="role-info">
|
||||
<div class="role-title">您是普通成员</div>
|
||||
<div class="role-desc">申请成为队长可获得更多权限</div>
|
||||
</div>
|
||||
</div>
|
||||
<t-button block
|
||||
class="leader-apply-btn"
|
||||
theme="danger"
|
||||
variant="outline"
|
||||
@click="leaderConfirmVisible = true">
|
||||
申请成为队长
|
||||
</t-button>
|
||||
</div>
|
||||
<t-button block
|
||||
theme="primary"
|
||||
variant="outline"
|
||||
size="large"
|
||||
shape="round"
|
||||
@click="leaderConfirmVisible = true">
|
||||
<i class="bi bi-award"></i>申请成为队长
|
||||
</t-button>
|
||||
</div>
|
||||
<div class="role-notice" v-else>
|
||||
<i class="bi bi-check-circle"></i>
|
||||
您当前是队长身份,拥有团队所有权限
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 项目报名按钮 -->
|
||||
<div class="team-section">
|
||||
<t-button block
|
||||
theme="primary"
|
||||
size="large"
|
||||
shape="round" @click="goToEventRegistration">
|
||||
<i class="bi bi-calendar-check"></i>
|
||||
项目报名
|
||||
</t-button>
|
||||
</div>
|
||||
</div>
|
||||
</view>
|
||||
@@ -120,18 +124,24 @@
|
||||
@cancel="leaderConfirmVisible = false"
|
||||
>
|
||||
<template #content>
|
||||
<p>申请成为队长前,请确认您已与团队其他成员商议并达成一致意见。</p>
|
||||
<p>一个团队仅能有一名队长,作为队长,您可以帮助队员一键报名项目。</p>
|
||||
<div class="dialog-content">
|
||||
<div class="dialog-icon">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
</div>
|
||||
<div class="dialog-text">
|
||||
<p>申请成为队长前,请确认您已与团队其他成员商议并达成一致意见。</p>
|
||||
<p>一个团队仅能有一名队长,作为队长,您可以帮助队员一键报名项目。</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</t-dialog>
|
||||
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref,reactive} from "vue";
|
||||
import {ref, reactive} from "vue";
|
||||
|
||||
const actOptions = ref([])
|
||||
const showDesc = ref(false)
|
||||
|
||||
const pageForm = reactive({
|
||||
actId: null,
|
||||
@@ -216,45 +226,54 @@ function onLeaderConfirm(){
|
||||
}
|
||||
|
||||
function goToEventRegistration(){}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
/* 主体内容区 */
|
||||
:deep(.top-dropdown-menu) {
|
||||
background-color: #fff;
|
||||
padding: 10px 0;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.content-area {
|
||||
padding: 0 16px 24px;
|
||||
}
|
||||
|
||||
/* 团队信息卡片 */
|
||||
/* 团队卡片 */
|
||||
.team-card {
|
||||
background-color: white;
|
||||
margin: 15px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(255, 95, 92, 0.08);
|
||||
margin: 12px 0 20px;
|
||||
}
|
||||
|
||||
.team-banner {
|
||||
position: relative;
|
||||
height: 120px;
|
||||
background: linear-gradient(135deg, #e74c3c, #ff7675);
|
||||
.team-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
padding: 16px;
|
||||
background: linear-gradient(to right, rgba(255, 95, 92, 0.05), rgba(255, 95, 92, 0.02));
|
||||
border-bottom: 1px solid rgba(255, 95, 92, 0.1);
|
||||
}
|
||||
|
||||
.team-logo {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.8);
|
||||
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.1);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(255, 95, 92, 0.15);
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-logo i {
|
||||
font-size: 24px;
|
||||
color: #ff5f5c;
|
||||
}
|
||||
|
||||
.team-logo img {
|
||||
@@ -263,93 +282,137 @@ function goToEventRegistration(){}
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.team-logo i {
|
||||
font-size: 40px;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.team-info {
|
||||
.team-basic-info {
|
||||
margin-left: 12px;
|
||||
flex: 1;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.team-name {
|
||||
font-size: 20px;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.team-meta {
|
||||
font-size: 13px;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
.team-id {
|
||||
display: inline-block;
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.event-name {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 15px;
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
padding: 3px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 12px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* 团队详情内容 */
|
||||
.team-detail {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.team-stat-row {
|
||||
.team-stats {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
padding-bottom: 15px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.team-stat-item {
|
||||
.stat-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.team-stat-value {
|
||||
font-size: 22px;
|
||||
.stat-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
background-color: rgba(255, 95, 92, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
color: #ff5f5c;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 1px;
|
||||
height: 36px;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0 16px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #e74c3c;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.team-stat-label {
|
||||
font-size: 13px;
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.team-desc-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
background-color: #f9f9f9;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.team-desc-toggle i {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 团队成员列表 */
|
||||
.team-section {
|
||||
.team-desc {
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
background-color: #fff8f8;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.team-desc i {
|
||||
color: #ffdddd;
|
||||
font-size: 18px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 10px;
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.action-section {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.register-btn {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
background-color: #ff5f5c;
|
||||
border-color: #ff5f5c;
|
||||
box-shadow: 0 6px 16px rgba(255, 95, 92, 0.2);
|
||||
}
|
||||
|
||||
/* 成员列表 */
|
||||
.members-section {
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f5f5f5;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 17px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
display: flex;
|
||||
@@ -358,105 +421,138 @@ function goToEventRegistration(){}
|
||||
|
||||
.section-title i {
|
||||
margin-right: 8px;
|
||||
color: #e74c3c;
|
||||
font-size: 20px;
|
||||
color: #ff5f5c;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.member-list {
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.member-item {
|
||||
.section-badge {
|
||||
display: flex;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
background-color: #fff0f0;
|
||||
color: #ff5f5c;
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.member-item:last-child {
|
||||
border-bottom: none;
|
||||
.member-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.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-grid-item {
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.member-name {
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
margin-bottom: 2px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.member-role {
|
||||
display: inline-block;
|
||||
.member-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 12px;
|
||||
background-color: #e74c3c;
|
||||
color: white;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
margin-left: 8px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.member-detail {
|
||||
display: flex;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.member-detail-item {
|
||||
.member-details span {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 15px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.member-detail-item i {
|
||||
.member-details i {
|
||||
margin-right: 4px;
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.member-actions {
|
||||
color: #999;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
/* 角色相关 */
|
||||
.role-section {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.role-notice {
|
||||
background-color: #f8f9fa;
|
||||
padding: 12px 15px;
|
||||
border-radius: 8px;
|
||||
margin-top: 15px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
.role-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff8f8;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 4px 12px rgba(255, 95, 92, 0.05);
|
||||
}
|
||||
|
||||
.role-notice i {
|
||||
color: #ffa63f;
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
.role-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 20px;
|
||||
background: linear-gradient(120deg, #ff5f5c, #ff8c8a);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
box-shadow: 0 4px 12px rgba(255, 95, 92, 0.2);
|
||||
}
|
||||
|
||||
.role-icon i {
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.role-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.role-title {
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.role-desc {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.leader-apply-btn {
|
||||
margin-top: 8px;
|
||||
border-radius: 12px;
|
||||
height: 48px;
|
||||
border-color: #ff5f5c;
|
||||
color: #ff5f5c;
|
||||
}
|
||||
|
||||
/* 弹窗内容样式 */
|
||||
.dialog-content {
|
||||
display: flex;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.dialog-icon {
|
||||
margin-right: 16px;
|
||||
font-size: 24px;
|
||||
color: #ff9800;
|
||||
}
|
||||
|
||||
.dialog-text p {
|
||||
margin: 0 0 8px;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<!-- 顶部背景和内容区 -->
|
||||
<view class="header">
|
||||
<image class="header-bg" src="https://erp-bucket.pek3b.qingstor.com/assistant/89c4b573-c54f-467a-b703-0bc44679ab08.png" mode="aspectFill"></image>
|
||||
</view>
|
||||
|
||||
<!-- 功能菜单区 -->
|
||||
<view class="menu-container">
|
||||
<view class="menu-item" @click="navigateTo('/pages/activity/list/index')">
|
||||
<view class="menu-icon-wrapper red-bg">
|
||||
@@ -26,7 +24,7 @@
|
||||
</view>
|
||||
<view class="menu-text">我的团队</view>
|
||||
</view>
|
||||
<view class="menu-item" @click="navigateTo('/pages/activity/team/index')">
|
||||
<view class="menu-item" @click="navigateTo('/pages/activity/team-form/index')">
|
||||
<view class="menu-icon-wrapper blue-bg">
|
||||
<view class="menu-icon star-icon"></view>
|
||||
</view>
|
||||
|
||||
+330
-151
@@ -1,51 +1,118 @@
|
||||
<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="title-area">
|
||||
<div class="tech-animation">
|
||||
<svg preserveAspectRatio="xMidYMid slice" viewBox="0 0 300 100">
|
||||
<pattern id="pattern-hex" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse" patternTransform="scale(0.8)">
|
||||
<rect x="0" y="0" width="10" height="10" fill="none"/>
|
||||
<path d="M0,5 L2.5,0 L7.5,0 L10,5 L7.5,10 L2.5,10 Z" stroke-width="0.5" stroke="rgba(244, 90, 90, 0.2)" fill="rgba(244, 90, 90, 0.1)">
|
||||
<animate attributeName="stroke-opacity" values="0.1;0.3;0.1" dur="3s" repeatCount="indefinite" />
|
||||
</path>
|
||||
</pattern>
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-hex)" />
|
||||
|
||||
<view class="login-form">
|
||||
<text class="title">登录</text>
|
||||
<g fill="none" stroke-width="0.5">
|
||||
<path d="M20,50 Q60,20 100,50 T180,50 T260,50" stroke="rgba(244, 90, 90, 0.6)">
|
||||
<animate attributeName="d"
|
||||
values="M20,50 Q60,20 100,50 T180,50 T260,50;
|
||||
M20,50 Q60,80 100,50 T180,50 T260,50;
|
||||
M20,50 Q60,20 100,50 T180,50 T260,50"
|
||||
dur="10s" repeatCount="indefinite" />
|
||||
</path>
|
||||
|
||||
<t-input
|
||||
class="input-item"
|
||||
v-model="loginForm.phone"
|
||||
placeholder="请输入手机号码"
|
||||
>
|
||||
</t-input>
|
||||
<path d="M20,70 Q60,40 100,70 T180,70 T260,70" stroke="rgba(244, 90, 90, 0.4)">
|
||||
<animate attributeName="d"
|
||||
values="M20,70 Q60,40 100,70 T180,70 T260,70;
|
||||
M20,70 Q60,100 100,70 T180,70 T260,70;
|
||||
M20,70 Q60,40 100,70 T180,70 T260,70"
|
||||
dur="12s" repeatCount="indefinite" />
|
||||
</path>
|
||||
</g>
|
||||
|
||||
<t-input
|
||||
class="input-item"
|
||||
v-model="loginForm.smsCode"
|
||||
placeholder="请输入验证码"
|
||||
>
|
||||
<template #suffix>
|
||||
<t-button variant="text" class="verify-btn" @click="getSmsCode" :disabled="isCountingDown">
|
||||
<circle cx="50" cy="50" r="1" fill="rgba(244, 90, 90, 0.8)">
|
||||
<animate attributeName="cx" values="50;250;50" dur="20s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values="50;30;80;50" dur="16s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="1;1.5;1" dur="5s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
|
||||
<circle cx="150" cy="30" r="1" fill="rgba(244, 90, 90, 0.8)">
|
||||
<animate attributeName="cx" values="150;100;200;150" dur="25s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values="30;60;20;30" dur="18s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="1;1.8;1" dur="6s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
|
||||
<circle cx="250" cy="70" r="1" fill="rgba(244, 90, 90, 0.8)">
|
||||
<animate attributeName="cx" values="250;150;250" dur="22s" repeatCount="indefinite" />
|
||||
<animate attributeName="cy" values="70;40;70" dur="15s" repeatCount="indefinite" />
|
||||
<animate attributeName="r" values="1;1.2;1" dur="4s" repeatCount="indefinite" />
|
||||
</circle>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<view class="title-content">
|
||||
<text class="main-title">HELLO,</text>
|
||||
<view class="sub-title-container">
|
||||
<text class="sub-title">欢迎使用</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="login-card">
|
||||
<view class="login-tabs">
|
||||
<view
|
||||
:class="['tab', activeMethod === 'phone' ? 'active' : '']"
|
||||
@click="setActiveMethod('phone')"
|
||||
>
|
||||
手机快速登录
|
||||
</view>
|
||||
<view
|
||||
:class="['tab', activeMethod === 'password' ? 'active' : '']"
|
||||
@click="setActiveMethod('password')"
|
||||
>
|
||||
账号密码登录
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录表单区域 -->
|
||||
<view class="form-container">
|
||||
<!-- 手机号码输入框 -->
|
||||
<view class="input-box">
|
||||
<t-icon name="mobile" size="20px" class="input-icon" />
|
||||
<input
|
||||
class="input"
|
||||
type="tel"
|
||||
v-model="loginForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 验证码输入框 -->
|
||||
<view class="input-box">
|
||||
<t-icon name="shield-error" size="20px" class="input-icon" />
|
||||
<input
|
||||
class="input"
|
||||
type="number"
|
||||
v-model="loginForm.smsCode"
|
||||
placeholder="请输入验证码"
|
||||
maxlength="6"
|
||||
/>
|
||||
<view class="verify-code-btn" @click="getSmsCode" :class="{ disabled: isCountingDown }">
|
||||
{{ countDownText }}
|
||||
</t-button>
|
||||
</template>
|
||||
</t-input>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- <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 class="submit-btn" @click="handleLogin">
|
||||
立即登录
|
||||
</view>
|
||||
|
||||
<!-- <!– 注册链接 –>-->
|
||||
<!-- <view class="register-link">-->
|
||||
<!-- <text>没有账号?</text>-->
|
||||
<!-- <text class="link">去注册</text>-->
|
||||
<!-- </view>-->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -56,12 +123,17 @@ import {Toast} from 'tdesign-mobile-vue';
|
||||
import {useRequest} from "@/utils/request";
|
||||
|
||||
const request = useRequest()
|
||||
const activeMethod = ref('phone')
|
||||
|
||||
const loginForm = reactive({
|
||||
phone: '18571562330',
|
||||
phone: '',
|
||||
smsCode: ''
|
||||
})
|
||||
|
||||
const setActiveMethod = (method) => {
|
||||
activeMethod.value = method
|
||||
}
|
||||
|
||||
// 倒计时相关
|
||||
const countdown = ref(0)
|
||||
const timer = ref(null)
|
||||
@@ -84,7 +156,7 @@ const saveCountdownState = (seconds, phone) => {
|
||||
uni.removeStorageSync(SMS_PHONE_KEY)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 保存结束时间戳和手机号
|
||||
const endTime = Date.now() + seconds * 1000
|
||||
uni.setStorageSync(SMS_COUNTDOWN_KEY, endTime)
|
||||
@@ -96,13 +168,18 @@ const restoreCountdownState = () => {
|
||||
const endTime = uni.getStorageSync(SMS_COUNTDOWN_KEY)
|
||||
const savedPhone = uni.getStorageSync(SMS_PHONE_KEY)
|
||||
|
||||
if (endTime && savedPhone && savedPhone === loginForm.phone) {
|
||||
if (endTime && savedPhone) {
|
||||
const now = Date.now()
|
||||
const remainingSeconds = Math.floor((endTime - now) / 1000)
|
||||
|
||||
if (remainingSeconds > 0) {
|
||||
countdown.value = remainingSeconds
|
||||
startCountDownTimer() // 重新启动定时器
|
||||
|
||||
// 如果用户还没输入手机号,填入保存的手机号
|
||||
if (!loginForm.phone) {
|
||||
loginForm.phone = savedPhone
|
||||
}
|
||||
} else {
|
||||
// 倒计时已结束,清除存储
|
||||
uni.removeStorageSync(SMS_COUNTDOWN_KEY)
|
||||
@@ -116,13 +193,13 @@ 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)
|
||||
@@ -137,50 +214,69 @@ const startCountDown = () => {
|
||||
startCountDownTimer()
|
||||
}
|
||||
|
||||
// 在页面挂载时恢复倒计时状态
|
||||
onMounted(() => {
|
||||
restoreCountdownState()
|
||||
})
|
||||
|
||||
// 在页面卸载前清除定时器
|
||||
onBeforeUnmount(() => {
|
||||
if (timer.value) {
|
||||
clearInterval(timer.value)
|
||||
timer.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// 获取验证码
|
||||
async function getSmsCode() {
|
||||
if (!loginForm.phone) {
|
||||
Toast.error('请输入手机号码');
|
||||
Toast('请输入手机号码');
|
||||
return
|
||||
}
|
||||
|
||||
// 验证手机号码
|
||||
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
|
||||
Toast('请输入正确的手机号码');
|
||||
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('验证码发送成功')
|
||||
Toast({message: '验证码发送成功', theme: 'success'})
|
||||
// 开始倒计时
|
||||
startCountDown()
|
||||
} else {
|
||||
Toast.error(msg)
|
||||
Toast({message: msg, theme: 'error'})
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e.message)
|
||||
Toast({message: e.message, theme: 'error'})
|
||||
}
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!loginForm.phone) {
|
||||
Toast.error('请输入手机号码');
|
||||
Toast('请输入手机号码');
|
||||
return
|
||||
}
|
||||
if (!loginForm.smsCode) {
|
||||
Toast.error('请输入验证码');
|
||||
Toast('请输入验证码');
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const {data, code, msg} = await request.post('/client/auth/sms-login', loginForm)
|
||||
if (code === 0) {
|
||||
Toast.success('登录成功')
|
||||
Toast({message: '登录成功', theme: '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/')) {
|
||||
@@ -196,119 +292,202 @@ async function handleLogin() {
|
||||
}
|
||||
}, 1000)
|
||||
} else {
|
||||
Toast.error(msg)
|
||||
Toast({message: msg, theme: 'error'})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
Toast.error(e.message)
|
||||
Toast({message: e.message, theme: 'error'})
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时恢复倒计时状态
|
||||
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 {
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: 260px;
|
||||
background-color: #4080ff;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
padding: 20px;
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
background: #f4f2f2;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin: 20px 0;
|
||||
display: block;
|
||||
color: var(--td-brand-color-5);
|
||||
.title-area {
|
||||
padding: 0;
|
||||
height: 30vh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #7a1818 0%, #d03e3e 50%, #f45a5a 100%);
|
||||
}
|
||||
|
||||
.tech-animation {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.tech-animation svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.title-content {
|
||||
padding: 0 30px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 36px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
color: #ffffff;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.sub-title-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 18px;
|
||||
color: #ffffff;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
flex: 1;
|
||||
background-color: #ffffff;
|
||||
border-radius: 30px 30px 0 0;
|
||||
margin-top: -30px;
|
||||
padding: 30px 0 0;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
box-shadow: 0 -5px 20px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.login-tabs {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
gap: 10px;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
width: calc(50% - 5px);
|
||||
height: 55px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
color: #f45a5a;
|
||||
font-weight: 500;
|
||||
background-color: #fff0f0;
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab:first-child {
|
||||
border-radius: 25px 15px 30px 25px;
|
||||
clip-path: polygon(0 0, 100% 0, 90% 100%, 0 100%);
|
||||
}
|
||||
|
||||
.tab:last-child {
|
||||
border-radius: 30px 25px 25px 15px;
|
||||
clip-path: polygon(10% 0, 100% 0, 100% 100%, 0 100%);
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #fff;
|
||||
background-color: #f45a5a;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
height: 50px;
|
||||
background-color: #eceaea;
|
||||
border-radius: 25px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 15px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
color: #999;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
border: none;
|
||||
font-size: 15px;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.input::placeholder {
|
||||
color: #bcbcbc;
|
||||
}
|
||||
|
||||
.verify-code-btn {
|
||||
color: #f45a5a;
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
|
||||
&.disabled {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
.input-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.submit-btn {
|
||||
margin-top: 32px;
|
||||
height: 50px;
|
||||
background-color: #f45a5a;
|
||||
border-radius: 25px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.country-code {
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
.register-link {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
.link {
|
||||
color: #f45a5a;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 836 KiB |
@@ -0,0 +1,9 @@
|
||||
// KB转为MB的函数 取整
|
||||
export function convertKBToMB(kb) {
|
||||
return Math.floor(kb / 1024) + 'MB'
|
||||
}
|
||||
|
||||
// 字节转为MB的函数
|
||||
export function convertBytesToMB(bytes) {
|
||||
return (bytes / 1024 / 1024).toFixed(2) + 'MB'
|
||||
}
|
||||
+60
-177
@@ -15,31 +15,11 @@ export const requestState = reactive({
|
||||
// 当前正在加载的请求数量
|
||||
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);
|
||||
@@ -47,73 +27,52 @@ function generateRequestId() {
|
||||
|
||||
/**
|
||||
* 请求开始处理
|
||||
* @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);
|
||||
function handleUnauthorized() {
|
||||
// Token失效,清除存储并跳转登录
|
||||
uni.removeStorageSync("token");
|
||||
uni.removeStorageSync("userInfo");
|
||||
|
||||
uni.showToast({
|
||||
title: "登录已过期,请重新登录",
|
||||
icon: "none",
|
||||
duration: 2000
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: "/pages/login/index" });
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示错误提示
|
||||
* @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 = {
|
||||
@@ -132,121 +91,57 @@ export function useRequest(options = {}) {
|
||||
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);
|
||||
handleRequestStart(requestId);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
...mergedConfig,
|
||||
success: (response) => {
|
||||
const { statusCode, data } = response;
|
||||
|
||||
// HTTP状态码成功
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
if (data.code === 600098) {
|
||||
handleUnauthorized();
|
||||
reject(data);
|
||||
}else{
|
||||
resolve(data);
|
||||
}
|
||||
// // 业务逻辑成功
|
||||
// if (data.code === 0) {
|
||||
// resolve(data);
|
||||
// }
|
||||
// // 未登录
|
||||
// else if (data.code === 600098) {
|
||||
// handleUnauthorized();
|
||||
// reject(data);
|
||||
// }
|
||||
// // 其他业务错误,直接返回,由业务代码处理
|
||||
// else {
|
||||
// reject(data);
|
||||
// }
|
||||
}
|
||||
// HTTP错误
|
||||
else {
|
||||
reject({
|
||||
code: statusCode,
|
||||
message: `HTTP错误: ${statusCode}`,
|
||||
data: data
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
fail: (error) => {
|
||||
reject(error);
|
||||
},
|
||||
complete: () => {
|
||||
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({
|
||||
@@ -259,10 +154,6 @@ export function useRequest(options = {}) {
|
||||
|
||||
/**
|
||||
* POST请求
|
||||
* @param {string} url - 请求URL
|
||||
* @param {object} data - 请求数据
|
||||
* @param {object} config - 其他配置
|
||||
* @returns {Promise} 请求Promise
|
||||
*/
|
||||
const post = (url, data = {}, config = {}) => {
|
||||
return request({
|
||||
@@ -275,10 +166,6 @@ export function useRequest(options = {}) {
|
||||
|
||||
/**
|
||||
* PUT请求
|
||||
* @param {string} url - 请求URL
|
||||
* @param {object} data - 请求数据
|
||||
* @param {object} config - 其他配置
|
||||
* @returns {Promise} 请求Promise
|
||||
*/
|
||||
const put = (url, data = {}, config = {}) => {
|
||||
return request({
|
||||
@@ -291,10 +178,6 @@ export function useRequest(options = {}) {
|
||||
|
||||
/**
|
||||
* DELETE请求
|
||||
* @param {string} url - 请求URL
|
||||
* @param {object} data - 请求数据
|
||||
* @param {object} config - 其他配置
|
||||
* @returns {Promise} 请求Promise
|
||||
*/
|
||||
const del = (url, data = {}, config = {}) => {
|
||||
return request({
|
||||
|
||||
@@ -28,10 +28,10 @@ export default defineConfig({
|
||||
}
|
||||
},
|
||||
server: {
|
||||
https: {
|
||||
cert: fs.readFileSync(path.join(__dirname, 'src/ssl/localhost+2.pem')),
|
||||
key: fs.readFileSync(path.join(__dirname, 'src/ssl/localhost+2-key.pem'))
|
||||
},
|
||||
// https: {
|
||||
// cert: fs.readFileSync(path.join(__dirname, 'src/ssl/localhost+2.pem')),
|
||||
// key: fs.readFileSync(path.join(__dirname, 'src/ssl/localhost+2-key.pem'))
|
||||
// },
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://192.168.41.118:9090',
|
||||
|
||||
Reference in New Issue
Block a user