Compare commits

..
13 Commits
Author SHA1 Message Date
c-chengc1 c682984ecb feat:优化初版 2026-09-29 13:41:27 +08:00
c-zhangr1 7c34e41f7e changes 2026-09-07 10:04:35 +08:00
Paidax c1589e4792 分组 2025-11-06 20:41:19 +08:00
zhengziru bdfaf01e10 server_commit 2025-11-06 19:45:53 +08:00
Paidax b87ef27004 commit 2025-11-03 16:44:49 +08:00
zhengziru ba6debb4de server_commit 2025-11-03 13:57:23 +08:00
那些花儿 9bbfade412 .. 2025-09-28 15:07:33 +08:00
那些花儿 a1e49561b9 Merge remote-tracking branch 'origin/main' 2025-09-28 13:56:00 +08:00
zhengziru 923a23b89f commit 2025-09-28 13:52:01 +08:00
Paidax d82a73d2a9 分组 2025-09-19 17:48:29 +08:00
那些花儿 9fd8d40da8 Merge branch 'main' of https://dd3skj.picp.vip/zhaoxinyu/hmc_gbhd 2025-09-19 14:45:06 +08:00
那些花儿 dc41b204ef Merge branch 'main' of https://dd3skj.picp.vip/zhaoxinyu/hmc_gbhd
# Conflicts:
#	frontend/src/pages/activity/registration-form/index.vue
2025-09-18 18:28:56 +08:00
那些花儿 33dbd9ac37 .. 2025-09-18 18:28:14 +08:00
89 changed files with 5528 additions and 1388 deletions
+10
View File
@@ -0,0 +1,10 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Environment-dependent path to Maven home directory
/mavenHomeManager.xml
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
Generated
+1
View File
@@ -0,0 +1 @@
南邮工会羽毛球报名
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="cn.fjdmy.uniapp.UniappProjectDataService">
<option name="generalBasePath" value="$PROJECT_DIR$" />
<option name="manifestPath" value="$PROJECT_DIR$/manifest.json" />
<option name="pagesPath" value="$PROJECT_DIR$/pages.json" />
<option name="scanNum" value="1" />
<option name="type" value="store" />
</component>
</project>
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="mini" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="mini" options="-parameters" />
</option>
</component>
</project>
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/backend/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/backend/src/main/resources" charset="UTF-8" />
</component>
</project>
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxProjectSettings">
<option name="commitMessageIssueKeyValidationOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
<option name="commitMessageValidationEnabledOverride">
<BoolValueOverride>
<option name="enabled" value="true" />
</BoolValueOverride>
</option>
</component>
</project>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
+68
View File
@@ -0,0 +1,68 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="AliAccessStaticViaInstance" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliArrayNamingShouldHaveBracket" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliControlFlowStatementWithoutBraces" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliDeprecation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliEqualsAvoidNull" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliLongLiteralsEndingWithLowercaseL" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliMissingOverrideAnnotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AliWrapperTypeEquality" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAbstractClassShouldStartWithAbstractNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAbstractMethodOrInterfaceMethodMustUseJavadoc" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidApacheBeanUtilsCopy" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidCallStaticSimpleDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidCommentBehindStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidComplexCondition" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidConcurrentCompetitionRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidDoubleOrFloatEqualCompare" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidManuallyCreateThread" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidMissUseOfMathRandom" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidNegationOperator" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidNewDateGetTime" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidPatternCompileInMethod" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidReturnInFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidStartWithDollarAndUnderLineNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaAvoidUseTimer" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaBigDecimalAvoidDoubleConstructor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaBooleanPropertyShouldNotStartWithIs" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassCastExceptionWithSubListToArrayList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassCastExceptionWithToArray" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassMustHaveAuthor" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaClassNamingShouldBeCamel" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCollectionInitShouldAssignCapacity" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCommentsMustBeJavadocFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaConcurrentExceptionWithModifyOriginSubList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaConstantFieldShouldBeUpperCase" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaCountDownShouldInFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaDontModifyInForeachCircle" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaEnumConstantsMustHaveComment" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaExceptionClassShouldEndWithException" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaIbatisMethodQueryForList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaLockShouldWithTryFinally" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaLowerCamelCaseVariableNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaMethodReturnWrapperType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaMethodTooLong" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPackageNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoMustOverrideToString" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoMustUsePrimitiveField" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaPojoNoDefaultValue" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaRemoveCommentedCode" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaServiceOrDaoClassShouldEndWithImpl" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSneakyThrowsWithoutExceptionType" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaStringConcat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSwitchExpression" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaSwitchStatement" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaTestClassShouldEndWithTestNaming" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadLocalShouldRemove" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadPoolCreation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaThreadShouldSetName" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaTransactionMustHaveRollback" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUndefineMagicConstant" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUnsupportedExceptionWithModifyAsList" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUseQuietReferenceNotation" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="AlibabaUseRightCaseForDateFormat" enabled="true" level="WARNING" enabled_by_default="true" />
<inspection_tool class="MapOrSetKeyShouldOverrideHashCodeEquals" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="nutz-snapshots" />
<option name="name" value="nutz-snapshots" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
<remote-repository>
<option name="id" value="nutz" />
<option name="name" value="nutz" />
<option name="url" value="https://maven.aliyun.com/repository/public" />
</remote-repository>
</component>
</project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/backend/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/hmc_gbhd.iml" filepath="$PROJECT_DIR$/.idea/hmc_gbhd.iml" />
</modules>
</component>
</project>
Generated
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+1 -1
View File
@@ -23,7 +23,7 @@
<pinyin4j.veision>2.5.1</pinyin4j.veision> <pinyin4j.veision>2.5.1</pinyin4j.veision>
<poi.version>4.1.1</poi.version> <poi.version>4.1.1</poi.version>
<tencentcloud-sdk-java.version>4.0.11</tencentcloud-sdk-java.version> <tencentcloud-sdk-java.version>4.0.11</tencentcloud-sdk-java.version>
<lombok.version>1.18.18</lombok.version> <lombok.version>1.18.22</lombok.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@@ -7,6 +7,7 @@ import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*; import org.nutz.dao.entity.annotation.*;
import java.io.Serializable; import java.io.Serializable;
import java.util.Date;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@@ -34,10 +35,15 @@ public class Client_user extends BaseModel implements Serializable {
@Comment("手机号") @Comment("手机号")
private String phone; private String phone;
// @Column
// @ColDefine(type = ColType.VARCHAR, width = 18)
// @Comment("身份证")
// private String idCard;
@Column @Column
@ColDefine(type = ColType.VARCHAR, width = 18) @ColDefine(type = ColType.DATE)
@Comment("身份证") @Comment("生日")
private String idCard; private Date birthday;
@Column @Column
@ColDefine(type = ColType.VARCHAR, width = 255) @ColDefine(type = ColType.VARCHAR, width = 255)
@@ -13,9 +13,11 @@ public interface ClientAuthService {
String doLogin(ClientLoginBody loginBody); String doLogin(ClientLoginBody loginBody);
/** /**
* 注册 * 注册手机端用户,并在所选单位尚无简称时完成首次简称登记。
* @param registerBody 注册信息 *
* @return token * @param registerBody 注册信息;userName为姓名、phone为手机号、sex为性别、unitId为单位ID、
* unitAliasName为单位简称、smsCode为短信验证码
* @return 注册成功后的客户端登录token字符串
*/ */
String doRegister(ClientRegisterBody registerBody); String doRegister(ClientRegisterBody registerBody);
} }
@@ -8,10 +8,15 @@ import com.budwk.app.base.utils.PwdUtil;
import com.budwk.app.client.auth.vo.ClientLoginBody; import com.budwk.app.client.auth.vo.ClientLoginBody;
import com.budwk.app.client.auth.models.Client_user; import com.budwk.app.client.auth.models.Client_user;
import com.budwk.app.client.auth.vo.ClientRegisterBody; import com.budwk.app.client.auth.vo.ClientRegisterBody;
import com.budwk.app.sys.models.Sys_unit;
import com.budwk.app.sys.services.SysUnitService;
import com.budwk.app.web.commons.auth.satoken.StpClientUtil; import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
import com.budwk.app.web.commons.auth.service.ValidateService; import com.budwk.app.web.commons.auth.service.ValidateService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Strings; import org.nutz.lang.Strings;
@@ -25,6 +30,8 @@ public class ClientAuthServiceImpl implements ClientAuthService {
private Dao dao; private Dao dao;
@Inject @Inject
private ValidateService validateService; private ValidateService validateService;
@Inject
private SysUnitService sysUnitService;
@Override @Override
public String doLogin(ClientLoginBody loginBody) { public String doLogin(ClientLoginBody loginBody) {
@@ -49,6 +56,9 @@ public class ClientAuthServiceImpl implements ClientAuthService {
} }
if (loginBody.getLoginMethod().equals("password")) { if (loginBody.getLoginMethod().equals("password")) {
if (Strings.isBlank(loginBody.getPassword())) {
throw new BaseException("请输入密码");
}
String hashedPassword = PwdUtil.getPassword(loginBody.getPassword(), user.getSalt()); String hashedPassword = PwdUtil.getPassword(loginBody.getPassword(), user.getSalt());
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) { if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
throw new BaseException("密码不正确"); throw new BaseException("密码不正确");
@@ -61,17 +71,43 @@ public class ClientAuthServiceImpl implements ClientAuthService {
} }
@Override @Override
@Aop(TransAop.READ_COMMITTED)
public String doRegister(ClientRegisterBody registerBody) { public String doRegister(ClientRegisterBody registerBody) {
int count = dao.count(Client_user.class, Cnd.where(Client_user::getPhone, "=", registerBody.getPhone())); int count = dao.count(Client_user.class, Cnd.where(Client_user::getPhone, "=", registerBody.getPhone()));
if (count > 0) { if (count > 0) {
throw new BaseException("手机号码已注册,请直接登录"); throw new BaseException("手机号码已注册,请直接登录");
} }
// unitId必须对应启用且未删除的单位;unitAliasName为首次登记的单位公共简称。
Sys_unit unit = sysUnitService.fetch(Cnd.where(Sys_unit::getId, "=", registerBody.getUnitId())
.and(Sys_unit::getIsEnable, "=", true)
.and(Sys_unit::getDelFlag, "=", false));
if (unit == null) {
throw new BaseException("请选择有效单位");
}
String unitAliasName = Strings.sNull(registerBody.getUnitAliasName()).trim();
if (Strings.isBlank(unitAliasName)) {
throw new BaseException("请输入单位简称");
}
if (unitAliasName.length() > 100) {
throw new BaseException("单位简称长度不能超过100个字符");
}
validateService.checkSMSCode(registerBody.getPhone(), registerBody.getSmsCode()); validateService.checkSMSCode(registerBody.getPhone(), registerBody.getSmsCode());
// 只允许首次登记简称;并发注册时通过数据库条件保证已有简称不会被后续请求覆盖。
if (Strings.isBlank(unit.getAliasName())) {
Cnd aliasEmptyCnd = Cnd.where(Sys_unit::getId, "=", unit.getId())
.and(Cnd.exps("aliasName", "is", null).or("aliasName", "=", ""));
int updatedRows = sysUnitService.update(Chain.make("aliasName", unitAliasName), aliasEmptyCnd);
if (updatedRows > 0) {
sysUnitService.clearCache();
}
}
Client_user user = BeanUtil.copyProperties(registerBody, Client_user.class); Client_user user = BeanUtil.copyProperties(registerBody, Client_user.class);
int gender = IdcardUtil.getGenderByIdCard(user.getIdCard()); // int gender = IdcardUtil.getGenderByIdCard(user.getIdCard());
user.setSex(gender==1?"男":"女"); // user.setSex(gender==1?"男":"女");
dao.insert(user); dao.insert(user);
StpClientUtil.STP.login(user.getId()); StpClientUtil.STP.login(user.getId());
StpClientUtil.STP.checkLogin(); StpClientUtil.STP.checkLogin();
@@ -2,6 +2,8 @@ package com.budwk.app.client.auth.vo;
import lombok.Data; import lombok.Data;
import java.util.Date;
@Data @Data
public class ClientRegisterBody { public class ClientRegisterBody {
@@ -11,11 +13,15 @@ public class ClientRegisterBody {
private String phone; private String phone;
private String idCard; // private String idCard;
private Date birthday;
private String avatar; private String avatar;
private String unitId; private String unitId;
/** 注册时填写的单位简称,仅在所选单位尚未设置简称时写入单位资料。 */
private String unitAliasName;
private String smsCode; private String smsCode;
} }
@@ -8,8 +8,10 @@ import com.budwk.app.client.module.activity.service.ClientActivityService;
import com.budwk.app.client.module.activity.vo.ClientActivitySignVO; import com.budwk.app.client.module.activity.vo.ClientActivitySignVO;
import com.budwk.app.module.activity.models.ActUser; import com.budwk.app.module.activity.models.ActUser;
import com.budwk.app.module.activity.models.Activity; import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.service.ActivityRegistrationFieldService;
import com.budwk.app.web.commons.auth.satoken.StpClientUtil; import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
import lombok.Synchronized; import lombok.Synchronized;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.FieldFilter; import org.nutz.dao.FieldFilter;
@@ -19,6 +21,7 @@ import org.nutz.dao.util.Daos;
import org.nutz.dao.util.cri.SqlExpressionGroup; import org.nutz.dao.util.cri.SqlExpressionGroup;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.ioc.aop.Aop;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.mvc.adaptor.JsonAdaptor; import org.nutz.mvc.adaptor.JsonAdaptor;
import org.nutz.mvc.annotation.AdaptBy; import org.nutz.mvc.annotation.AdaptBy;
@@ -38,6 +41,8 @@ public class ClientActivityController {
@Inject @Inject
private ClientActivityService clientActivityService; private ClientActivityService clientActivityService;
@Inject @Inject
private ActivityRegistrationFieldService activityRegistrationFieldService;
@Inject
private Dao dao; private Dao dao;
@At @At
@@ -54,6 +59,7 @@ public class ClientActivityController {
t1.signStartTime, t1.signStartTime,
t1.signEndTime, t1.signEndTime,
t1.coHostUnit, t1.coHostUnit,
t1.hostUnit,
t1.cover t1.cover
FROM FROM
activity t1 activity t1
@@ -61,6 +67,8 @@ public class ClientActivityController {
"""); """);
Cnd cnd = Cnd.NEW(); Cnd cnd = Cnd.NEW();
cnd.and(Cnd.likeEX("t1.title", pageForm.getSearchKeyword())); cnd.and(Cnd.likeEX("t1.title", pageForm.getSearchKeyword()));
// 客户端活动列表只展示后台已开启的活动。
cnd.and("t1.isOpen", "=", true);
sql.setCondition(cnd); sql.setCondition(cnd);
Pagination pagination = clientActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql); Pagination pagination = clientActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
return Result.success(pagination); return Result.success(pagination);
@@ -70,7 +78,13 @@ public class ClientActivityController {
@GET @GET
@SaCheckLogin(type = StpClientUtil.TYPE) @SaCheckLogin(type = StpClientUtil.TYPE)
public Result get(Long id) { public Result get(Long id) {
Activity activity = clientActivityService.fetch(id); // 详情接口必须再次校验开启状态,避免通过历史链接访问已关闭活动。
Activity activity = clientActivityService.fetch(Cnd.where(Activity::getId, "=", id)
.and(Activity::getIsOpen, "=", true)
.and(Activity::getDelFlag, "=", false));
if (activity == null) {
return Result.error("活动不存在或已关闭");
}
clientActivityService.fetchLinks(activity, "items"); clientActivityService.fetchLinks(activity, "items");
return Result.success(activity); return Result.success(activity);
} }
@@ -83,6 +97,7 @@ public class ClientActivityController {
@AdaptBy(type = JsonAdaptor.class) @AdaptBy(type = JsonAdaptor.class)
@SaCheckLogin(type = StpClientUtil.TYPE) @SaCheckLogin(type = StpClientUtil.TYPE)
@Synchronized @Synchronized
@Aop(TransAop.READ_COMMITTED)
public synchronized Result signUp(ClientActivitySignVO signVO) { public synchronized Result signUp(ClientActivitySignVO signVO) {
clientActivityService.signUp(signVO); clientActivityService.signUp(signVO);
return Result.success(); return Result.success();
@@ -108,6 +123,62 @@ public class ClientActivityController {
return Result.success(list); return Result.success(list);
} }
/**
* 查询指定活动在移动端启用的报名字段。
*
* @param activityId 活动ID
* @return Result JSON结构;data为按sort排序的字段列表,字典字段同时包含启用选项
*/
@At("/registration-fields/?")
@SaCheckLogin(type = StpClientUtil.TYPE)
public Result registrationFields(Long activityId) {
Activity activity = clientActivityService.fetch(Cnd.where(Activity::getId, "=", activityId)
.and(Activity::getIsOpen, "=", true)
.and(Activity::getDelFlag, "=", false));
if (activity == null) {
return Result.error("活动不存在或已关闭");
}
return Result.success(activityRegistrationFieldService.getFieldConfig(activityId, true, true));
}
/**
* 查询当前单位在指定活动中的教练、领队人数限制状态。
*
* @param activityId 活动ID;应传当前报名页面对应的已开启活动ID
* @return Result JSON结构;data.enabled表示单项目人数规则是否生效,coachValue和leaderValue为人员类型字典值,
* coachCount和leaderCount为其他报名账号已占用人数,maxCount为各类型人数上限
*/
@At("/person-type-rule-status/?")
@SaCheckLogin(type = StpClientUtil.TYPE)
public Result personTypeRuleStatus(Long activityId) {
return Result.success(clientActivityService.getPersonTypeRuleStatus(activityId));
}
/**
* 查询当前单位在指定活动中的项目类型报名人数限制状态。
*
* @param activityId 活动ID;应传当前报名页面对应的已开启活动ID
* @return Result JSON结构;data.enabled表示单项目报名规则是否生效,data.rules包含类型编码、名称、
* 人数上限和排除当前账号旧名单后的其他报名账号已占用人数
*/
@At("/project-type-rule-status/?")
@SaCheckLogin(type = StpClientUtil.TYPE)
public Result projectTypeRuleStatus(Long activityId) {
return Result.success(clientActivityService.getProjectTypeRuleStatus(activityId));
}
/**
* 查询当前登录用户所属单位在指定活动中的已保存报名人数。
*
* @param activityId 活动ID;应传当前报名页面对应的已开启活动ID
* @return Result JSON结构;data为同一活动、同一单位当前已保存且未删除的报名人员数量
*/
@At("/unit-registration-count/?")
@SaCheckLogin(type = StpClientUtil.TYPE)
public Result unitRegistrationCount(Long activityId) {
return Result.success(clientActivityService.getUnitRegistrationCount(activityId));
}
/** /**
* 是否报名 * 是否报名
*/ */
@@ -1,6 +1,7 @@
package com.budwk.app.client.module.activity.controller; package com.budwk.app.client.module.activity.controller;
import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.client.module.activity.service.ClientActivityService; import com.budwk.app.client.module.activity.service.ClientActivityService;
import com.budwk.app.module.activity.models.ActGroup; import com.budwk.app.module.activity.models.ActGroup;
@@ -22,7 +23,9 @@ import org.nutz.mvc.annotation.AdaptBy;
import org.nutz.mvc.annotation.At; import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import java.util.List; import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;
/** /**
* @author zhf * @author zhf
@@ -63,7 +66,6 @@ public class ClientGroupController {
LEFT JOIN act_user t2 ON t1.id = t2.activityId LEFT JOIN act_user t2 ON t1.id = t2.activityId
WHERE WHERE
t2.userId = @userId t2.userId = @userId
AND t2.isSignIn =1
""").setParam("userId", StpClientUtil.STP.getLoginIdAsLong()); """).setParam("userId", StpClientUtil.STP.getLoginIdAsLong());
List<NutMap> list = clientActivityService.listMap(sql); List<NutMap> list = clientActivityService.listMap(sql);
return Result.success(list); return Result.success(list);
@@ -84,9 +86,14 @@ public class ClientGroupController {
// return Result.error("您还没加入任何团队"); // return Result.error("您还没加入任何团队");
return Result.success(); return Result.success();
} }
Sql sql = Sqls.create("select * from act_group where id=@id").setParam("id", groupUser.getGroupId()); Sql sql = Sqls.create("select * from act_group where id=@id and isOpen = 1").setParam("id", groupUser.getGroupId());
sql.setCallback(Sqls.callback.map()); sql.setCallback(Sqls.callback.map());
NutMap map = (NutMap) dao.execute(sql).getResult(); NutMap map = (NutMap) dao.execute(sql).getResult();
if (Lang.isEmpty(map)) {
map = NutMap.NEW();
}
Sql sqlGroupUser = Sqls.create(""" Sql sqlGroupUser = Sqls.create("""
SELECT SELECT
t1.*, t1.*,
@@ -102,7 +109,7 @@ public class ClientGroupController {
WHERE t1.groupId=@groupId WHERE t1.groupId=@groupId
ORDER BY ORDER BY
t1.isLeader DESC t1.isLeader DESC
""").setParam("groupId", map.getString("id")); """).setParam("groupId", map.getString("id", ""));
List<NutMap> groupUsers = clientActivityService.listMap(sqlGroupUser); List<NutMap> groupUsers = clientActivityService.listMap(sqlGroupUser);
map.put("groupUsers", groupUsers); map.put("groupUsers", groupUsers);
NutMap leader = groupUsers.stream().filter(nutMap -> nutMap.getBoolean("isLeader")).findFirst().orElse(null); NutMap leader = groupUsers.stream().filter(nutMap -> nutMap.getBoolean("isLeader")).findFirst().orElse(null);
@@ -118,7 +125,11 @@ public class ClientGroupController {
@At("/apply-leader/?") @At("/apply-leader/?")
@AdaptBy(type = JsonAdaptor.class) @AdaptBy(type = JsonAdaptor.class)
@SaCheckLogin(type = StpClientUtil.TYPE) @SaCheckLogin(type = StpClientUtil.TYPE)
public synchronized Result applyLeader(Long groupId) { public synchronized Result applyLeader(String groupId) {
if (StrUtil.isNotBlank(groupId)) {
return Result.error("未获取到团队信息");
}
int count = clientActivityService.dao().count(ActGroupUser.class, Cnd.where(ActGroupUser::getGroupId, "=", groupId) int count = clientActivityService.dao().count(ActGroupUser.class, Cnd.where(ActGroupUser::getGroupId, "=", groupId)
.and(ActGroupUser::getIsLeader, "=", 1)); .and(ActGroupUser::getIsLeader, "=", 1));
if (count > 0){ if (count > 0){
@@ -165,7 +176,57 @@ public class ClientGroupController {
@At("/list-group-rank/?") @At("/list-group-rank/?")
@SaCheckLogin(type = StpClientUtil.TYPE) @SaCheckLogin(type = StpClientUtil.TYPE)
public Result listGroupRank(Long activityId) { public Result listGroupRank(Long activityId) {
List<ActGroup> groupList = dao.query(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", activityId).desc(ActGroup::getTotalScore)); if (activityId == null) {
return Result.success(groupList); return Result.error("未获取到报名活动");
}
Sql sql = Sqls.create("""
SELECT
t1.itemId,
t1.groupId,
t1.score,
t1.ranking,
t2.groupName\s
FROM
act_item_group_score t1
LEFT JOIN act_group t2 ON t2.id = t1.groupId\s
WHERE
t1.activityId = @activityId
AND t2.isOpen = true
""");
sql.setParam("activityId", activityId);
List<NutMap> list = clientActivityService.listMap(sql);
// 2. 按 groupId 分组
Map<String, List<NutMap>> groupMap = list.stream()
.collect(Collectors.groupingBy(v -> v.getString("groupId")));
// 3. 把每组封装成 VO,算总分,排序,填总排名
List<ActGroup> rankList = new ArrayList<>();
groupMap.forEach((groupIdStr, rows) -> {
// 3.1 组总分
BigDecimal totalScore = rows.stream()
.map(r -> r.getDouble("score", 0d)) // 先拿 double
.map(BigDecimal::valueOf)
.reduce(BigDecimal.ZERO, BigDecimal::add);
// 3.2 基本信息
ActGroup vo = new ActGroup();
vo.setActivityId(activityId);
vo.setId(Long.valueOf(groupIdStr));
vo.setGroupName(rows.get(0).getString("groupName"));
vo.setTotalScore(totalScore);
rankList.add(vo);
});
// 3.3 排序:总分降序 → groupId 升序(总分相同按 groupId 升序)
rankList.sort(
Comparator.comparing(ActGroup::getTotalScore).reversed()
.thenComparing(ActGroup::getId)
);
// List<ActGroup> groupList = dao.query(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", activityId).desc(ActGroup::getTotalScore));
return Result.success(rankList);
} }
} }
@@ -19,6 +19,30 @@ public interface ClientActivityService extends BaseService<Activity> {
*/ */
void signUp(ClientActivitySignVO signVO); void signUp(ClientActivitySignVO signVO);
/**
* 获取当前登录用户所属单位在指定活动中的人员类型限制状态。
*
* @param activityId 活动ID,必须为已开启且未删除的活动
* @return NutMap结构;包含规则是否启用、教练/领队字典值、其他报名账号已占用人数和人数上限
*/
NutMap getPersonTypeRuleStatus(Long activityId);
/**
* 获取当前登录用户所属单位在指定活动中的项目类型报名规则状态。
*
* @param activityId 活动ID,必须为已开启且未删除的活动
* @return NutMap结构;包含规则是否启用,以及各子项上限和其他报名账号已占用人数
*/
NutMap getProjectTypeRuleStatus(Long activityId);
/**
* 获取当前登录用户所属单位在指定活动中已保存的报名人员总数。
*
* @param activityId 活动ID,必须为已开启且未删除的活动
* @return 当前单位在该活动中未删除的报名人员数量
*/
int getUnitRegistrationCount(Long activityId);
/** /**
* 报名签到 * 报名签到
*/ */
@@ -2,29 +2,39 @@ package com.budwk.app.client.module.activity.service.impl;
import cn.hutool.core.util.IdcardUtil; import cn.hutool.core.util.IdcardUtil;
import com.budwk.app.base.exception.BaseException; 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.base.service.impl.BaseServiceImpl;
import com.budwk.app.client.auth.models.Client_user; 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.service.ClientActivityService;
import com.budwk.app.client.module.activity.vo.ClientActivitySignVO; import com.budwk.app.client.module.activity.vo.ClientActivitySignVO;
import com.budwk.app.module.activity.models.ActUser; import com.budwk.app.module.activity.models.ActUser;
import com.budwk.app.module.activity.models.Activity; import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.enums.RegistrationFieldDefinition;
import com.budwk.app.module.activity.service.ActivityRegistrationFieldService;
import com.budwk.app.web.commons.auth.satoken.StpClientUtil; import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
import org.nutz.dao.sql.Sql; import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.Lang; import org.nutz.lang.Lang;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> implements ClientActivityService { public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> implements ClientActivityService {
@Inject
private ActivityRegistrationFieldService activityRegistrationFieldService;
public ClientActivityServiceImpl(Dao dao) { public ClientActivityServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@@ -46,24 +56,139 @@ public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> impleme
} }
@Override @Override
public NutMap getPersonTypeRuleStatus(Long activityId) {
Activity activity = this.fetch(Cnd.where(Activity::getId, "=", activityId)
.and(Activity::getIsOpen, "=", true)
.and(Activity::getDelFlag, "=", false));
if (activity == null) {
throw new BaseException("活动不存在或已关闭");
}
Client_user selfUser = dao().fetch(Client_user.class, StpClientUtil.STP.getLoginIdAsLong());
if (selfUser == null || Strings.isBlank(selfUser.getUnitId())) {
throw new BaseException("当前用户未设置所属单位,无法校验人员类型");
}
return activityRegistrationFieldService.getPersonTypeRuleStatus(activityId, selfUser.getUnitId(),
StpClientUtil.STP.getLoginIdAsLong());
}
@Override
public NutMap getProjectTypeRuleStatus(Long activityId) {
Activity activity = this.fetch(Cnd.where(Activity::getId, "=", activityId)
.and(Activity::getIsOpen, "=", true)
.and(Activity::getDelFlag, "=", false));
if (activity == null) {
throw new BaseException("活动不存在或已关闭");
}
Client_user selfUser = dao().fetch(Client_user.class, StpClientUtil.STP.getLoginIdAsLong());
if (selfUser == null || Strings.isBlank(selfUser.getUnitId())) {
throw new BaseException("当前用户未设置所属单位,无法校验项目报名人数");
}
return activityRegistrationFieldService.getProjectTypeRuleStatus(activityId, selfUser.getUnitId(),
StpClientUtil.STP.getLoginIdAsLong());
}
@Override
public int getUnitRegistrationCount(Long activityId) {
Activity activity = this.fetch(Cnd.where(Activity::getId, "=", activityId)
.and(Activity::getIsOpen, "=", true)
.and(Activity::getDelFlag, "=", false));
if (activity == null) {
throw new BaseException("活动不存在或已关闭");
}
Client_user selfUser = dao().fetch(Client_user.class, StpClientUtil.STP.getLoginIdAsLong());
if (selfUser == null || Strings.isBlank(selfUser.getUnitId())) {
throw new BaseException("当前用户未设置所属单位,无法统计报名人数");
}
// 统计口径固定为同一活动、同一单位的有效报名人员,供移动端与当前草稿人数合并展示。
return dao().count(ActUser.class, Cnd.where(ActUser::getActivityId, "=", activityId)
.and(ActUser::getUnitId, "=", selfUser.getUnitId())
.and(ActUser::getDelFlag, "=", false));
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void signUp(ClientActivitySignVO signVO) { public void signUp(ClientActivitySignVO signVO) {
List<ActUser> userList = signVO.getUserList(); List<ActUser> userList = signVO.getUserList();
if (signVO.getActivityId() == null || userList == null || userList.isEmpty()) {
throw new BaseException("活动ID和报名人员不能为空");
}
Activity activity = this.fetch(Cnd.where(Activity::getId, "=", signVO.getActivityId())
.and(Activity::getIsOpen, "=", true)
.and(Activity::getDelFlag, "=", false));
if (activity == null) {
throw new BaseException("活动不存在或已关闭");
}
Client_user selfUser = dao().fetch(Client_user.class, StpClientUtil.STP.getLoginIdAsLong());
if (selfUser == null) {
throw new BaseException("当前登录用户不存在");
}
dao().fetchLinks(selfUser, "unit");
if (selfUser.getUnit() == null) {
throw new BaseException("当前用户未设置所属单位,无法报名");
}
Map<String, Boolean> requiredMap = activityRegistrationFieldService.getRequiredMap(signVO.getActivityId());
List<ActUser> oldUsers = dao().query(ActUser.class,
Cnd.where(ActUser::getActivityId, "=", signVO.getActivityId())
.and(ActUser::getSignUpUserId, "=", StpClientUtil.STP.getLoginIdAsLong()));
Map<Long, ActUser> oldUserById = oldUsers.stream().filter(item -> item.getId() != null)
.collect(Collectors.toMap(ActUser::getId, item -> item, (left, right) -> left, LinkedHashMap::new));
Map<String, ActUser> oldUserByPhone = oldUsers.stream().filter(item -> Strings.isNotBlank(item.getPhone()))
.collect(Collectors.toMap(ActUser::getPhone, item -> item, (left, right) -> left, LinkedHashMap::new));
for (ActUser user : userList) {
// 单位信息由当前登录用户所属单位统一带出,不接收客户端任意修改。
user.setUnitId(selfUser.getUnitId());
user.setUnitName(selfUser.getUnit().getName());
ActUser oldUser = oldUserById.get(user.getId());
if (oldUser == null && Strings.isNotBlank(user.getPhone())) {
oldUser = oldUserByPhone.get(user.getPhone());
}
mergeHiddenFields(user, oldUser, requiredMap);
// 无手机号人员再次提交时沿用本人旧记录关联的账号,不信任客户端传入的账号ID。
user.setUserId(oldUser == null ? null : oldUser.getUserId());
if (!conditionMatches(user, RegistrationFieldDefinition.PHONE)) {
user.setPhone(null);
}
}
if (requiredMap.containsKey("personType") || requiredMap.containsKey("projectType")) {
// 以活动和单位为边界构造最终报名集合;当前账号旧记录会被本次提交整体覆盖,因此排除后再加入草稿。
List<ActUser> unitUsers = new ArrayList<>(dao().query(ActUser.class,
Cnd.where(ActUser::getActivityId, "=", signVO.getActivityId())
.and(ActUser::getUnitId, "=", selfUser.getUnitId())
.and(Cnd.exps("signUpUserId", "!=", StpClientUtil.STP.getLoginIdAsLong())
.or("signUpUserId", "is", null))
.and(ActUser::getDelFlag, "=", false)));
unitUsers.addAll(userList);
if (requiredMap.containsKey("personType")) {
activityRegistrationFieldService.validatePersonTypeRules(signVO.getActivityId(), unitUsers);
}
if (requiredMap.containsKey("projectType")) {
activityRegistrationFieldService.validateProjectTypeRules(signVO.getActivityId(), unitUsers);
}
}
for (ActUser user : userList) {
validateRegistrationFields(signVO.getActivityId(), user, requiredMap);
}
// 检查是否有重复的身份证号 // 检查是否有重复的身份证号
boolean hasDuplicateIdCard = userList.stream() // boolean hasDuplicateIdCard = userList.stream()
.collect(Collectors.groupingBy(ActUser::getIdCard, Collectors.counting())) // .collect(Collectors.groupingBy(ActUser::getIdCard, Collectors.counting()))
.values().stream() // .values().stream()
.anyMatch(count -> count > 1); // .anyMatch(count -> count > 1);
// 检查是否有重复的手机号 // 检查是否有重复的手机号
boolean hasDuplicatePhone = userList.stream() boolean hasDuplicatePhone = userList.stream()
.filter(item -> Strings.isNotBlank(item.getPhone()))
.collect(Collectors.groupingBy(ActUser::getPhone, Collectors.counting())) .collect(Collectors.groupingBy(ActUser::getPhone, Collectors.counting()))
.values().stream() .values().stream()
.anyMatch(count -> count > 1); .anyMatch(count -> count > 1);
if(hasDuplicateIdCard){ // if(hasDuplicateIdCard){
throw new BaseException("身份证号重复"); // throw new BaseException("身份证号重复");
} // }
if(hasDuplicatePhone){ if(hasDuplicatePhone){
throw new BaseException("手机号重复"); throw new BaseException("手机号重复");
@@ -71,22 +196,24 @@ public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> impleme
LocalDateTime now = LocalDateTime.now(); LocalDateTime now = LocalDateTime.now();
Client_user selfUser = dao().fetch(Client_user.class, StpClientUtil.STP.getLoginIdAsLong());
dao().fetchLinks(selfUser, "unit");
// 首先检查报名人员是否已经在用户表中 // 首先检查报名人员是否已经在用户表中
for (ActUser user : userList) { for (ActUser user : userList) {
Client_user clientUser = dao().fetch(Client_user.class, Cnd.where(Client_user::getIdCard, "=", user.getIdCard()).or(Client_user::getPhone, "=", user.getPhone())); // 未兼任教练或领队的运动员允许不填手机号,不能用空手机号查找并错误关联其他人的账号。
Client_user clientUser = Strings.isNotBlank(user.getPhone())
? dao().fetch(Client_user.class, Cnd.where(Client_user::getPhone, "=", user.getPhone()))
: (user.getUserId() == null ? null : dao().fetch(Client_user.class, user.getUserId()));
if (clientUser == null) { if (clientUser == null) {
// 创建用户 // 创建用户
clientUser = new Client_user(); clientUser = new Client_user();
clientUser.setUserName(user.getUserName()); clientUser.setUserName(user.getUserName());
clientUser.setSex(user.getSex()); clientUser.setSex(user.getSex());
clientUser.setPhone(user.getPhone()); clientUser.setPhone(user.getPhone());
clientUser.setIdCard(user.getIdCard()); clientUser.setBirthday(user.getBirthday());
// clientUser.setIdCard(user.getIdCard());
clientUser.setUnitId(selfUser.getUnitId()); clientUser.setUnitId(selfUser.getUnitId());
int gender = IdcardUtil.getGenderByIdCard(clientUser.getIdCard()); // int gender = IdcardUtil.getGenderByIdCard(clientUser.getIdCard());
clientUser.setSex(gender==1?"男":"女"); // clientUser.setSex(gender==1?"男":"女");
clientUser.setSex(user.getSex());
dao().insert(clientUser); dao().insert(clientUser);
} else { } else {
// 验证用户之前是不是已经报名 别人帮报 自己报 管理员报 // 验证用户之前是不是已经报名 别人帮报 自己报 管理员报
@@ -107,9 +234,15 @@ public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> impleme
user.setSignUpUserId(StpClientUtil.STP.getLoginIdAsLong()); user.setSignUpUserId(StpClientUtil.STP.getLoginIdAsLong());
user.setSignUpTime(now); user.setSignUpTime(now);
if(!user.getTraffic().equals("自驾")){ if (!"自驾".equals(user.getTraffic())) {
user.setCarNo(null); user.setCarNo(null);
} }
if (!"公共交通".equals(user.getTraffic())) {
user.setIsStation(false);
user.setStationAddress(null);
} else if (!Boolean.TRUE.equals(user.getIsStation())) {
user.setStationAddress(null);
}
} }
@@ -122,6 +255,110 @@ public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> impleme
dao().insert(userList); dao().insert(userList);
} }
/**
* 按活动字段配置校验一名报名人员。
* 字段未启用时不校验;关联字段仅在父字段满足显示条件时参与必填校验。
*/
private void validateRegistrationFields(Long activityId, ActUser user, Map<String, Boolean> requiredMap) {
for (Map.Entry<String, Boolean> entry : requiredMap.entrySet()) {
RegistrationFieldDefinition definition = RegistrationFieldDefinition.fromKey(entry.getKey());
if (definition == null || !conditionMatches(user, definition)) {
continue;
}
Object value = getFieldValue(user, entry.getKey());
if (Boolean.TRUE.equals(entry.getValue()) && isEmptyValue(value)) {
throw new BaseException("请填写" + definition.getFieldLabel());
}
// 服务端复核身份证,防止绕过页面校验提交无效号码。
if ("idCard".equals(entry.getKey()) && !isEmptyValue(value)
&& (String.valueOf(value).length() != 18 || !IdcardUtil.isValidCard18(String.valueOf(value)))) {
throw new BaseException("请输入正确的18位身份证号码");
}
if (!isEmptyValue(value)
&& !activityRegistrationFieldService.isValidDictValue(activityId, entry.getKey(), String.valueOf(value))) {
throw new BaseException(definition.getFieldLabel() + "不属于当前可用的数据字典选项");
}
if ("personType".equals(entry.getKey()) && !isEmptyValue(value)
&& !activityRegistrationFieldService.isValidPersonTypeCombination(String.valueOf(value))) {
throw new BaseException("人员类型中的教练和领队不能同时选择");
}
}
}
private boolean conditionMatches(ActUser user, RegistrationFieldDefinition definition) {
if (Strings.isBlank(definition.getParentField())) {
return true;
}
// 人员类型为字典多选,按名称解析编码后匹配任意目标类型。
if ("personType".equals(definition.getParentField())) {
return activityRegistrationFieldService.matchesPersonTypes(user.getPersonType(), definition.getShowWhenValue());
}
Object parentValue = getFieldValue(user, definition.getParentField());
return parentValue != null && definition.getShowWhenValue().equals(String.valueOf(parentValue));
}
private boolean isEmptyValue(Object value) {
return value == null || value instanceof String && Strings.isBlank((String) value);
}
private Object getFieldValue(ActUser user, String fieldKey) {
return switch (fieldKey) {
case "userName" -> user.getUserName();
case "sex" -> user.getSex();
case "personType" -> user.getPersonType();
case "projectType" -> user.getProjectType();
case "weight" -> user.getWeight();
case "unitName" -> user.getUnitName();
case "position" -> user.getPosition();
case "phone" -> user.getPhone();
case "idCard" -> user.getIdCard();
case "birthday" -> user.getBirthday();
case "isStay" -> user.getIsStay();
case "isContacts" -> user.getIsContacts();
case "traffic" -> user.getTraffic();
case "carNo" -> user.getCarNo();
case "isStation" -> user.getIsStation();
case "stationAddress" -> user.getStationAddress();
case "isShowSkill" -> user.getIsShowSkill();
case "skillName" -> user.getSkillName();
case "equip" -> user.getEquip();
case "isParty" -> user.getIsParty();
default -> null;
};
}
/**
* 重新提交报名时保留当前活动未展示字段的旧值,防止管理员调整配置后历史信息被清空。
*/
private void mergeHiddenFields(ActUser target, ActUser source, Map<String, Boolean> selectedFields) {
for (RegistrationFieldDefinition definition : RegistrationFieldDefinition.values()) {
if (selectedFields.containsKey(definition.getFieldKey())) {
continue;
}
switch (definition.getFieldKey()) {
case "sex" -> target.setSex(source == null ? null : source.getSex());
case "personType" -> target.setPersonType(source == null ? null : source.getPersonType());
case "projectType" -> target.setProjectType(source == null ? null : source.getProjectType());
case "weight" -> target.setWeight(source == null ? null : source.getWeight());
case "position" -> target.setPosition(source == null ? null : source.getPosition());
case "idCard" -> target.setIdCard(source == null ? null : source.getIdCard());
case "birthday" -> target.setBirthday(source == null ? null : source.getBirthday());
case "isStay" -> target.setIsStay(source == null ? null : source.getIsStay());
case "isContacts" -> target.setIsContacts(source == null ? false : source.getIsContacts());
case "traffic" -> target.setTraffic(source == null ? null : source.getTraffic());
case "carNo" -> target.setCarNo(source == null ? null : source.getCarNo());
case "isStation" -> target.setIsStation(source == null ? false : source.getIsStation());
case "stationAddress" -> target.setStationAddress(source == null ? null : source.getStationAddress());
case "isShowSkill" -> target.setIsShowSkill(source == null ? false : source.getIsShowSkill());
case "skillName" -> target.setSkillName(source == null ? null : source.getSkillName());
case "equip" -> target.setEquip(source == null ? null : source.getEquip());
case "isParty" -> target.setIsParty(source == null ? false : source.getIsParty());
default -> {
}
}
}
}
@Override @Override
public void signIn(Long activityId) { public void signIn(Long activityId) {
ActUser actUser = dao().fetch(ActUser.class, Cnd.where(ActUser::getActivityId, "=", activityId).and(ActUser::getUserId, "=", StpClientUtil.STP.getLoginIdAsLong())); ActUser actUser = dao().fetch(ActUser.class, Cnd.where(ActUser::getActivityId, "=", activityId).and(ActUser::getUserId, "=", StpClientUtil.STP.getLoginIdAsLong()));
@@ -60,7 +60,7 @@ public class ActBigScreenController {
@Ok("json:full") @Ok("json:full")
public Result score(Long activityId) { public Result score(Long activityId) {
if (activityId == null) { if (activityId == null) {
return Result.error("活动ID不能为空"); return Result.error("未获取到报名活动");
} }
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
@@ -106,7 +106,7 @@ public class ActBigScreenController {
itemScores.add(scoreItem); itemScores.add(scoreItem);
} }
ArrayList<Map<String, Object>> groupScores = new ArrayList<>(groupMap.values()); List<Map<String, Object>> groupScores = new ArrayList<>(groupMap.values());
return Result.success(groupScores); return Result.success(groupScores);
} }
@@ -1,13 +1,14 @@
package com.budwk.app.module.activity.controller; package com.budwk.app.module.activity.controller;
import cn.afterturn.easypoi.excel.ExcelExportUtil;
import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.page.PageForm; import com.budwk.app.base.page.PageForm;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.module.activity.models.ActGroup; import com.budwk.app.module.activity.models.*;
import com.budwk.app.module.activity.models.ActGroupUser;
import com.budwk.app.module.activity.models.ActItemGroupScore;
import com.budwk.app.module.activity.models.ActivityItem;
import com.budwk.app.module.activity.service.ActGroupService; import com.budwk.app.module.activity.service.ActGroupService;
import com.budwk.app.module.activity.service.ActUserService; import com.budwk.app.module.activity.service.ActUserService;
import com.budwk.app.module.activity.vo.ActGroupVo; import com.budwk.app.module.activity.vo.ActGroupVo;
@@ -15,7 +16,9 @@ import com.budwk.app.module.activity.vo.ActManualGroupVo;
import com.budwk.app.module.activity.vo.ActUnGroupUserVo; import com.budwk.app.module.activity.vo.ActUnGroupUserVo;
import com.budwk.app.web.commons.slog.annotation.SLog; import com.budwk.app.web.commons.slog.annotation.SLog;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook;
import org.nutz.aop.interceptor.ioc.TransAop; import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.Sqls; import org.nutz.dao.Sqls;
@@ -29,7 +32,10 @@ import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok; import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@@ -154,6 +160,7 @@ public class ActGroupController {
Sql sql = Sqls.create(""" Sql sql = Sqls.create("""
SELECT SELECT
u.*, u.*,
TIMESTAMPDIFF(YEAR, STR_TO_DATE(SUBSTRING(u.idCard, 7, 8), '%Y%m%d'), CURDATE()) AS age,
agu.isLeader agu.isLeader
FROM FROM
act_group_user agu act_group_user agu
@@ -180,6 +187,20 @@ public class ActGroupController {
} }
/**
* 按单位对活动全部有效报名人员进行分组。
*
* @param activityId 活动ID
* @return 分组成功结果;数据不完整时由业务异常返回错误提示
*/
@At
@SaCheckPermission("activity.group")
@SLog(tag = "活动分组", msg = "按单位分组")
public Result groupByUnit(Long activityId) {
actGroupService.groupByUnit(activityId);
return Result.success();
}
@At @At
@SaCheckPermission("activity.group") @SaCheckPermission("activity.group")
public Result queryUnGroupUser(String activityId) { public Result queryUnGroupUser(String activityId) {
@@ -230,6 +251,8 @@ public class ActGroupController {
for (int i = 0; i < actGroups.length; i++) { for (int i = 0; i < actGroups.length; i++) {
actGroups[i].setActivityId(activityId); actGroups[i].setActivityId(activityId);
actGroups[i].setSort(i + 1); actGroups[i].setSort(i + 1);
// 设置为非公开
actGroups[i].setIsOpen(false);
dao.insert(actGroups[i]); dao.insert(actGroups[i]);
for (ActGroupUser user : actGroups[i].getGroupUsers()) { for (ActGroupUser user : actGroups[i].getGroupUsers()) {
user.setActivityId(activityId); user.setActivityId(activityId);
@@ -274,4 +297,75 @@ public class ActGroupController {
return Result.success(); return Result.success();
} }
@At
@SaCheckPermission("activity.group")
@SLog(tag = "活动分组", msg = "发布分组结果")
public Result releaseGroupResult(@Valid String activityId){
dao.update(ActGroup.class, Chain.make("isOpen", true),
Cnd.where(ActGroup::getActivityId, "=", activityId));
return Result.success();
}
@At
@SaCheckPermission("activity.group")
@SLog(tag = "活动分组", msg = "发布分组结果")
public Result unReleaseGroupResult(@Valid String activityId){
dao.update(ActGroup.class, Chain.make("isOpen", false),
Cnd.where(ActGroup::getActivityId, "=", activityId));
return Result.success();
}
@At
@Ok("void")
@SaCheckPermission("activity.group")
public void doExportGroup(String activityId, HttpServletResponse response){
try {
Activity activity = dao.fetch(Activity.class, Cnd.NEW().and("id", "=", activityId));
Sql sql = Sqls.create("""
SELECT
u.userName,
u.sex,
u.position,
u.mobile,
u.unitName,
g.groupName
FROM
`act_group_user` agu
LEFT JOIN act_user u ON u.userId = agu.userId
LEFT JOIN act_group g ON g.id = agu.groupId
$condition
""");
Cnd cnd = Cnd.NEW();
cnd.and("u.activityId", "=", activityId);
cnd.and("u.isSignIn", "=", true);
cnd.orderBy("g.id", "asc");
sql.setCondition(cnd);
List<NutMap> list = actUserService.listMap(sql);
List<ExcelExportEntity> entityList = new ArrayList<>() {{
add(new ExcelExportEntity("姓名", "userName", 20));
add(new ExcelExportEntity("性别", "sex", 20));
add(new ExcelExportEntity("职务", "position", 20));
add(new ExcelExportEntity("联系方式", "mobile", 20));
add(new ExcelExportEntity("参加单位名称", "unitName", 20));
add(new ExcelExportEntity("队伍名称", "groupName", 20));
}};
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + new String((activity.getTitle() + "分组结果表.xlsx").getBytes(StandardCharsets.UTF_8), "ISO8859-1"));
ExportParams exportParams = new ExportParams();
exportParams.setType(ExcelType.XSSF);
Workbook workbook = ExcelExportUtil.exportExcel(exportParams, entityList, list);
workbook.write(response.getOutputStream());
} catch (Exception e) {
e.printStackTrace();
}
}
} }
@@ -65,11 +65,7 @@ public class ActScoreEnterController {
@SaCheckPermission("activity.scoreEnter") @SaCheckPermission("activity.scoreEnter")
@ApiOperation(value = "获取活动列表", nickname = "/list-activity", httpMethod = "GET") @ApiOperation(value = "获取活动列表", nickname = "/list-activity", httpMethod = "GET")
public Result listAct(Integer year) { public Result listAct(Integer year) {
Sql sql = Sqls.create("select id,title from activity $condition"); return Result.success(activityService.listByYearOrderByLatest(year));
Cnd cnd = Cnd.NEW();
cnd.andEX("YEAR(startTime)", "=", year);
sql.setCondition(cnd);
return Result.success(activityService.listMap(sql));
} }
/** /**
@@ -23,6 +23,7 @@ import org.nutz.mvc.annotation.Param;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.List; import java.util.List;
import java.util.Objects;
@IocBean @IocBean
@At("/platform/activity/scoreEntry") @At("/platform/activity/scoreEntry")
@@ -45,11 +46,7 @@ public class ActScoreEntryController {
@SaCheckPermission("activity.scoreEntry") @SaCheckPermission("activity.scoreEntry")
@ApiOperation(value = "获取活动列表", nickname = "/list-activity", httpMethod = "GET") @ApiOperation(value = "获取活动列表", nickname = "/list-activity", httpMethod = "GET")
public Result listAct(Integer year) { public Result listAct(Integer year) {
Sql sql = Sqls.create("select id,title from activity $condition"); return Result.success(activityService.listByYearOrderByLatest(year));
Cnd cnd = Cnd.NEW();
cnd.andEX("YEAR(startTime)", "=", year);
sql.setCondition(cnd);
return Result.success(activityService.listMap(sql));
} }
/** /**
@@ -106,7 +103,8 @@ public class ActScoreEntryController {
//在找出这个队伍下面所有的分数 //在找出这个队伍下面所有的分数
List<ActItemGroupScore> groupScoreList = scoreList.stream().filter(itemScore -> itemScore.getGroupId().equals(groupId)).toList(); List<ActItemGroupScore> groupScoreList = scoreList.stream().filter(itemScore -> itemScore.getGroupId().equals(groupId)).toList();
BigDecimal totalScore = groupScoreList.stream() BigDecimal totalScore = groupScoreList.stream()
.map(ActItemGroupScore::getScore) // 提取每个 score(BigDecimal) .map(ActItemGroupScore::getScore)
.filter(Objects::nonNull) // 把 null 踢掉
.reduce(BigDecimal.ZERO, BigDecimal::add); .reduce(BigDecimal.ZERO, BigDecimal::add);
dao.update(ActGroup.class, Chain.make("totalScore", totalScore), Cnd.where(ActGroup::getId, "=", groupId)); dao.update(ActGroup.class, Chain.make("totalScore", totalScore), Cnd.where(ActGroup::getId, "=", groupId));
} }
@@ -13,6 +13,8 @@ import com.budwk.app.client.module.activity.vo.ClientActivitySignVO;
import com.budwk.app.module.activity.models.ActUser; import com.budwk.app.module.activity.models.ActUser;
import com.budwk.app.module.activity.models.Activity; import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.service.ActUserService; import com.budwk.app.module.activity.service.ActUserService;
import com.budwk.app.module.activity.service.ActivityManageService;
import com.budwk.app.module.activity.service.ActivityRegistrationFieldService;
import com.budwk.app.module.activity.vo.page.ActSignInUserPageForm; import com.budwk.app.module.activity.vo.page.ActSignInUserPageForm;
import com.budwk.app.module.activity.vo.page.ActUserPageForm; import com.budwk.app.module.activity.vo.page.ActUserPageForm;
import com.budwk.app.web.commons.slog.annotation.SLog; import com.budwk.app.web.commons.slog.annotation.SLog;
@@ -38,6 +40,9 @@ import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@@ -61,6 +66,10 @@ public class ActUserController {
private Dao dao; private Dao dao;
@Inject @Inject
private ActUserService actUserService; private ActUserService actUserService;
@Inject
private ActivityManageService activityManageService;
@Inject
private ActivityRegistrationFieldService activityRegistrationFieldService;
@At("") @At("")
@SaCheckPermission("activity.user") @SaCheckPermission("activity.user")
@@ -124,7 +133,9 @@ public class ActUserController {
user.setActivityId(activityId); user.setActivityId(activityId);
user.setUnitName(vo.getUnitName()); user.setUnitName(vo.getUnitName());
} }
dao.insertOrUpdate(doSignUserList); if (Lang.isNotEmpty(doSignUserList)) {
dao.insertOrUpdate(doSignUserList);
}
} }
// 找出不在系统的人,添加到client_user表中,再添加报名记录 // 找出不在系统的人,添加到client_user表中,再添加报名记录
@@ -202,11 +213,26 @@ public class ActUserController {
*/ */
@At @At
@SaCheckPermission("activity.user") @SaCheckPermission("activity.user")
public Result getActivityList(String year) { public Result getActivityList(Integer year) {
List<Activity> activityList = dao.query(Activity.class, Cnd.NEW().andEX("YEAR(startTime)", "=", year).and(Activity::getDelFlag, "=", false)); List<Activity> activityList = activityManageService.listByYearOrderByLatest(year);
return Result.success(activityList); return Result.success(activityList);
} }
/**
* 获取报名人员列表需要显示的活动报名字段。
*
* @param activityId 活动ID,必须传入当前列表选中的活动
* @return Result JSON结构;data为当前活动已启用的字段,并包含字典字段的可用选项
*/
@At
@SaCheckPermission("activity.user")
public Result getRegistrationFields(Long activityId) {
if (activityId == null) {
return Result.error("请选择活动");
}
return Result.success(activityRegistrationFieldService.getFieldConfig(activityId, true, true));
}
@At @At
@SaCheckPermission("activity.user") @SaCheckPermission("activity.user")
@@ -232,16 +258,60 @@ public class ActUserController {
Sql sql = actUserService.getSignUpSql(pageForm); Sql sql = actUserService.getSignUpSql(pageForm);
List<NutMap> nutMaps = actUserService.listMap(sql); List<NutMap> nutMaps = actUserService.listMap(sql);
// 历史表列名为ProjectType,统一导出键名供项目排序及名称转换使用。
for (NutMap row : nutMaps) {
if (row.getString("projectType", "").isEmpty()) {
row.put("projectType", row.getString("ProjectType", ""));
}
}
// 导出按单位、领队、教练、项目、男女分组;人员类型按字典名称解析,兼任类型也参与排序。
Map<String, Integer> personTypeOrder = nutMaps.stream()
.map(row -> row.getString("personType", "")).distinct()
.collect(Collectors.toMap(type -> type, type ->
activityRegistrationFieldService.matchesPersonTypes(type, "领队") ? 0
: activityRegistrationFieldService.matchesPersonTypes(type, "教练") ? 1 : 2));
nutMaps.sort(Comparator.comparing((NutMap row) -> row.getString("unitName", ""))
.thenComparing(row -> row.getString("unitId", ""))
.thenComparingInt(row -> personTypeOrder.get(row.getString("personType", "")))
.thenComparing(row -> Arrays.stream(row.getString("projectType", "").split(","))
.map(String::trim).filter(value -> !value.isEmpty()).distinct().sorted()
.collect(Collectors.joining(",")))
.thenComparingInt(row -> "男".equals(row.getString("sex")) ? 0
: "女".equals(row.getString("sex")) ? 1 : 2));
// 多选字典编码转换为名称;保留无法匹配的旧编码,避免历史报名信息在导出时丢失。
for (NutMap field : activityRegistrationFieldService.getFieldConfig(pageForm.getActivityId(), false, true)) {
String fieldKey = field.getString("fieldKey");
if (!"personType".equals(fieldKey) && !"projectType".equals(fieldKey)) {
continue;
}
Map<String, String> optionNames = new HashMap<>();
if (field.get("options") instanceof List<?> options) {
for (Object option : options) {
if (option instanceof Map<?, ?> optionMap) {
optionNames.put(String.valueOf(optionMap.get("value")), String.valueOf(optionMap.get("label")));
}
}
}
for (NutMap row : nutMaps) {
row.put(fieldKey + "Desc", Arrays.stream(row.getString(fieldKey, "").split(","))
.map(String::trim).filter(value -> !value.isEmpty())
.map(value -> optionNames.getOrDefault(value, value))
.collect(Collectors.joining("、")));
}
}
List<ExcelExportEntity> entityList = new ArrayList<>() {{ List<ExcelExportEntity> entityList = new ArrayList<>() {{
// add(new ExcelExportEntity("报名号", "actUserNo", 20)); // add(new ExcelExportEntity("报名号", "actUserNo", 20));
add(new ExcelExportEntity("姓名", "userName", 20)); add(new ExcelExportEntity("姓名", "userName", 20));
add(new ExcelExportEntity("性别", "sex", 20)); add(new ExcelExportEntity("性别", "sex", 20));
add(new ExcelExportEntity("人员类型", "personTypeDesc", 20));
add(new ExcelExportEntity("项目类型", "projectTypeDesc", 30));
add(new ExcelExportEntity("身份证号码", "idCard", 20)); add(new ExcelExportEntity("身份证号码", "idCard", 20));
add(new ExcelExportEntity("联系方式", "phone", 20)); add(new ExcelExportEntity("联系方式", "phone", 20));
add(new ExcelExportEntity("职务", "position", 20)); add(new ExcelExportEntity("职务", "position", 20));
add(new ExcelExportEntity("所属单位", "unitName", 20)); add(new ExcelExportEntity("所属单位", "unitName", 20));
add(new ExcelExportEntity("报名时间", "signUpTime2", 20)); add(new ExcelExportEntity("报名时间", "signUpTime2", 20));
add(new ExcelExportEntity("是否签到", "isSignInDesc", 20)); add(new ExcelExportEntity("是否签到", "isSignInDesc", 20));
add(new ExcelExportEntity("是否住宿", "isStayDesc", 20));
// add(new ExcelExportEntity("是否展示才艺", "isShowSkillDesc", 20)); // add(new ExcelExportEntity("是否展示才艺", "isShowSkillDesc", 20));
// add(new ExcelExportEntity("才艺名称", "skillName", 20)); // add(new ExcelExportEntity("才艺名称", "skillName", 20));
// add(new ExcelExportEntity("所需器材", "equip", 20)); // add(new ExcelExportEntity("所需器材", "equip", 20));
@@ -251,6 +321,7 @@ public class ActUserController {
add(new ExcelExportEntity("接站地址", "stationAddress", 20)); add(new ExcelExportEntity("接站地址", "stationAddress", 20));
add(new ExcelExportEntity("是否参加茶叙会", "isPartyDesc", 20)); add(new ExcelExportEntity("是否参加茶叙会", "isPartyDesc", 20));
add(new ExcelExportEntity("是否联络人", "isSIgnUpUser", 20));
}}; }};
response.setContentType("application/octet-stream"); response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + new String((activity.getTitle() + "报名数据表.xlsx").getBytes(StandardCharsets.UTF_8), "ISO8859-1")); response.setHeader("Content-Disposition", "attachment;filename=" + new String((activity.getTitle() + "报名数据表.xlsx").getBytes(StandardCharsets.UTF_8), "ISO8859-1"));
@@ -86,9 +86,7 @@ public class ActWonderfulMomentController {
@At @At
public Result getWonderfulMoment(@Param("id") Long id) { public Result getWonderfulMoment(@Param("id") Long id) {
Cnd cnd = Cnd.where("published", "=", true).andEX("id", "=", id); List<ActWonderfulMoment> momentList = wonderfulMomentService.listClientVisible(id);
cnd.desc("publishTime");
List<ActWonderfulMoment> momentList = wonderfulMomentService.query(cnd);
List<NutMap> result = new ArrayList<>(); List<NutMap> result = new ArrayList<>();
for (ActWonderfulMoment wonderfulMoment : momentList) { for (ActWonderfulMoment wonderfulMoment : momentList) {
NutMap nutMap = Lang.obj2map(wonderfulMoment, NutMap.class); NutMap nutMap = Lang.obj2map(wonderfulMoment, NutMap.class);
@@ -106,15 +104,20 @@ public class ActWonderfulMomentController {
@At("/getWonderfulMomentById/?") @At("/getWonderfulMomentById/?")
public Result getWonderfulMomentById(Long id) { public Result getWonderfulMomentById(Long id) {
ActWonderfulMoment moment = wonderfulMomentService.fetch(id); ActWonderfulMoment moment = wonderfulMomentService.fetchClientVisible(id);
if (moment == null) {
return Result.error("精彩瞬间不存在或所属活动已关闭");
}
return Result.success(moment); return Result.success(moment);
} }
@At("/handleWonderfulMoment/?") @At("/handleWonderfulMoment/?")
@Aop(TransAop.READ_COMMITTED)
public Result handleWonderfulMoment(Long id) { public Result handleWonderfulMoment(Long id) {
ActWonderfulMoment moment = wonderfulMomentService.fetch(id); int updatedRows = wonderfulMomentService.increaseClientReadCount(id);
moment.setReadCount(moment.getReadCount() + 1); if (updatedRows == 0) {
wonderfulMomentService.update(moment); return Result.error("精彩瞬间不存在或所属活动已关闭");
}
return Result.success(); return Result.success();
} }
} }
@@ -11,6 +11,7 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.module.activity.models.Activity; import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.models.ActivityItem; import com.budwk.app.module.activity.models.ActivityItem;
import com.budwk.app.module.activity.service.ActivitySignInQueryService; import com.budwk.app.module.activity.service.ActivitySignInQueryService;
import com.budwk.app.module.activity.service.ActivityManageService;
import com.budwk.app.module.activity.vo.page.ActSignInUserPageForm; import com.budwk.app.module.activity.vo.page.ActSignInUserPageForm;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.Workbook;
@@ -42,6 +43,8 @@ public class ActivityItemApplyQueryController {
@Inject @Inject
private ActivitySignInQueryService activitySignInQueryService; private ActivitySignInQueryService activitySignInQueryService;
@Inject
private ActivityManageService activityManageService;
@At("") @At("")
@SaCheckPermission("activity.itemApplyQuery") @SaCheckPermission("activity.itemApplyQuery")
@@ -52,7 +55,7 @@ public class ActivityItemApplyQueryController {
@At() @At()
@SaCheckPermission("activity.signInUserQuery") @SaCheckPermission("activity.signInUserQuery")
public Result getActivityOptions(Integer year) { public Result getActivityOptions(Integer year) {
return Result.success(activitySignInQueryService.dao().query(Activity.class, Cnd.NEW().andEX("YEAR(startTime)", "=", year))); return Result.success(activityManageService.listByYearOrderByLatest(year));
} }
@At() @At()
@@ -2,12 +2,13 @@ package com.budwk.app.module.activity.controller;
import cn.dev33.satoken.annotation.SaCheckLogin; import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.collection.CollUtil;
import com.budwk.app.base.page.PageForm; import com.budwk.app.base.page.PageForm;
import com.budwk.app.base.page.Pagination; import com.budwk.app.base.page.Pagination;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.module.activity.models.Activity; import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.models.ActivityItem; import com.budwk.app.module.activity.models.ActivityItem;
import com.budwk.app.module.activity.models.ActRegistrationField;
import com.budwk.app.module.activity.service.ActivityRegistrationFieldService;
import com.budwk.app.module.activity.service.ActivityManageService; import com.budwk.app.module.activity.service.ActivityManageService;
import com.budwk.app.web.commons.auth.satoken.StpClientUtil; import com.budwk.app.web.commons.auth.satoken.StpClientUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@@ -22,7 +23,7 @@ import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.Param; import org.nutz.mvc.annotation.Param;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collections;
import java.util.List; import java.util.List;
@IocBean @IocBean
@@ -35,6 +36,8 @@ public class ActivityManageController {
private Dao dao; private Dao dao;
@Inject @Inject
private ActivityManageService activityService; private ActivityManageService activityService;
@Inject
private ActivityRegistrationFieldService activityRegistrationFieldService;
@At("") @At("")
@SaCheckPermission("activity.manager") @SaCheckPermission("activity.manager")
@@ -91,6 +94,27 @@ public class ActivityManageController {
return Result.success(); return Result.success();
} }
/**
* 更新活动在客户端的开启状态。
*
* @param id 活动ID,必须传入后台活动列表中的有效ID
* @param isOpen 是否开启;true 表示客户端显示,false 表示客户端隐藏
* @return Result JSON结构;code为0表示更新成功,非0时msg说明失败原因
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("activity.manager")
public Result updateOpenStatus(@Param("id") Long id, @Param("isOpen") Boolean isOpen) {
if (id == null || isOpen == null) {
return Result.error("活动ID和开启状态不能为空");
}
int updatedRows = activityService.updateOpenStatus(id, isOpen);
if (updatedRows == 0) {
return Result.error("活动不存在或状态更新失败");
}
return Result.success();
}
/** /**
* 删除活动 * 删除活动
*/ */
@@ -148,27 +172,49 @@ public class ActivityManageController {
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("activity.manager") @SaCheckPermission("activity.manager")
public Result updateItem(Long id, @Param("items") ActivityItem[] items) { public Result updateItem(Long id, @Param("items") ActivityItem[] items) {
// 查询出旧的活动信息 activityService.updateItems(id,
List<ActivityItem> oldItems = dao.query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", id)); items == null ? Collections.emptyList() : Arrays.asList(items));
// 更新项目信息 return Result.success();
List<Long> oldItemIdList = oldItems.stream().map(ActivityItem::getId).toList(); }
// 更新项目信息
for (ActivityItem item : items) { /**
item.setActivityId(id); * 查询活动报名字段配置。
*
* @param id 活动ID
* @return Result JSON结构;data为完整安全字段目录,并包含选中、必填、顺序和依赖信息
*/
@At
@SaCheckPermission("activity.manager")
public Result getRegistrationFields(@Param("id") Long id) {
if (id == null || activityService.fetch(id) == null) {
return Result.error("活动不存在");
} }
dao.insertOrUpdate(items); return Result.success(activityRegistrationFieldService.getFieldConfig(id, false, false));
// 找出需要新增的 找出需要删除的 }
List<Long> newItemIdList = Arrays.stream(items).map(ActivityItem::getId).toList();
// 删除旧的项目信息 /**
Collection<Long> deleteItemIdList = CollUtil.subtract(oldItemIdList, newItemIdList); * 保存活动报名字段配置。
dao.clear(ActivityItem.class, Cnd.where("id", "in", deleteItemIdList)); *
* @param id 活动ID
* @param fields 已选择字段数组;fieldKey为字段编码、required为是否必填、sort为显示顺序
* @return Result JSON结构;code为0表示保存成功,非0时msg说明失败原因
*/
@At
@Aop(TransAop.READ_COMMITTED)
@SaCheckPermission("activity.manager")
public Result saveRegistrationFields(@Param("id") Long id,
@Param("fields") ActRegistrationField[] fields) {
activityRegistrationFieldService.saveFieldConfig(id,
fields == null ? Collections.emptyList() : Arrays.asList(fields));
return Result.success(); return Result.success();
} }
@At @At
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public Result getActivity() { public Result getActivity() {
List<Activity> activityList = activityService.query(Cnd.NEW()); // 客户端首页的活动详情只展示后台已开启且未删除的活动。
List<Activity> activityList = activityService.query(Cnd.where(Activity::getIsOpen, "=", true)
.and(Activity::getDelFlag, "=", false));
return Result.success(activityList); return Result.success(activityList);
} }
} }
@@ -12,6 +12,8 @@ import com.budwk.app.base.result.Result;
import com.budwk.app.module.activity.models.Activity; import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.models.ActivityItem; import com.budwk.app.module.activity.models.ActivityItem;
import com.budwk.app.module.activity.service.ActivitySignInQueryService; import com.budwk.app.module.activity.service.ActivitySignInQueryService;
import com.budwk.app.module.activity.service.ActivityManageService;
import com.budwk.app.module.activity.service.ActivityRegistrationFieldService;
import com.budwk.app.module.activity.vo.page.ActSignInUserPageForm; import com.budwk.app.module.activity.vo.page.ActSignInUserPageForm;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.Workbook;
@@ -39,6 +41,10 @@ public class ActivitySignInQueryController {
@Inject @Inject
private ActivitySignInQueryService activitySignInQueryService; private ActivitySignInQueryService activitySignInQueryService;
@Inject
private ActivityManageService activityManageService;
@Inject
private ActivityRegistrationFieldService activityRegistrationFieldService;
@At("") @At("")
@SaCheckPermission("activity.signInUserQuery") @SaCheckPermission("activity.signInUserQuery")
@@ -50,7 +56,22 @@ public class ActivitySignInQueryController {
@At() @At()
@SaCheckPermission("activity.signInUserQuery") @SaCheckPermission("activity.signInUserQuery")
public Result getActivityOptions(Integer year) { public Result getActivityOptions(Integer year) {
return Result.success(activitySignInQueryService.dao().query(Activity.class, Cnd.NEW().andEX("YEAR(startTime)", "=", year))); return Result.success(activityManageService.listByYearOrderByLatest(year));
}
/**
* 获取签字人员列表需要显示的活动报名字段。
*
* @param activityId 活动ID,必须传入当前列表选中的活动
* @return Result JSON结构;data为当前活动已启用的字段,并包含字典字段的可用选项
*/
@At()
@SaCheckPermission("activity.signInUserQuery")
public Result getRegistrationFields(Long activityId) {
if (activityId == null) {
return Result.error("请选择活动");
}
return Result.success(activityRegistrationFieldService.getFieldConfig(activityId, true, true));
} }
@At() @At()
@@ -0,0 +1,69 @@
package com.budwk.app.module.activity.enums;
import lombok.Getter;
import java.util.Arrays;
import java.util.List;
/**
* 报名字段安全目录。
* 这里显式声明允许管理员配置的字段,避免把主键、签到状态等系统字段暴露到移动端。
*/
@Getter
public enum RegistrationFieldDefinition {
USER_NAME("userName", "姓名", "input", true, true, true, null, null, null),
SEX("sex", "性别", "radio", true, true, false, null, null, null),
PERSON_TYPE("personType", "人员类型", "dict-checkbox", false, false, false, null, null, "person_type"),
PROJECT_TYPE("projectType", "项目类型", "dict-checkbox", false, true, false, "personType", "运动员", "project_type"),
WEIGHT("weight", "体重(KG)", "number", true, true, false, null, null, null),
UNIT_NAME("unitName", "单位名称", "readonly", true, true, true, null, null, null),
POSITION("position", "职务", "input", true, true, false, null, null, null),
PHONE("phone", "联系电话", "tel", true, true, true, "personType", "教练,领队", null),
BIRTHDAY("birthday", "出生日期", "date", false, false, false, null, null, null),
IS_STAY("isStay", "是否住宿", "boolean-radio", true, true, false, null, null, null),
IS_CONTACTS("isContacts", "是否联络员", "boolean-radio", true, true, false, null, null, null),
TRAFFIC("traffic", "交通方式", "radio", true, true, false, null, null, null),
CAR_NO("carNo", "车牌号", "input", true, true, false, "traffic", "自驾", null),
IS_STATION("isStation", "是否接站", "boolean-radio", true, true, false, "traffic", "公共交通", null),
STATION_ADDRESS("stationAddress", "接站地址", "input", true, true, false, "isStation", "true", null),
IS_SHOW_SKILL("isShowSkill", "是否展示才艺", "boolean-radio", false, false, false, null, null, null),
SKILL_NAME("skillName", "才艺名称", "input", false, false, false, "isShowSkill", "true", null),
EQUIP("equip", "所需器材", "input", false, false, false, "isShowSkill", "true", null),
IS_PARTY("isParty", "参加茶叙会", "boolean-radio", true, true, false, null, null, null),
ID_CARD("idCard", "身份证", "input", true, true, true, "personType", "运动员", null);
private final String fieldKey;
private final String fieldLabel;
private final String componentType;
private final boolean defaultSelected;
private final boolean defaultRequired;
private final boolean locked;
private final String parentField;
private final String showWhenValue;
private final String dictCode;
RegistrationFieldDefinition(String fieldKey, String fieldLabel, String componentType,
boolean defaultSelected, boolean defaultRequired, boolean locked,
String parentField, String showWhenValue, String dictCode) {
this.fieldKey = fieldKey;
this.fieldLabel = fieldLabel;
this.componentType = componentType;
this.defaultSelected = defaultSelected;
this.defaultRequired = defaultRequired;
this.locked = locked;
this.parentField = parentField;
this.showWhenValue = showWhenValue;
this.dictCode = dictCode;
}
public static RegistrationFieldDefinition fromKey(String fieldKey) {
return Arrays.stream(values())
.filter(item -> item.fieldKey.equals(fieldKey))
.findFirst()
.orElse(null);
}
public static List<RegistrationFieldDefinition> defaultFields() {
return Arrays.stream(values()).filter(RegistrationFieldDefinition::isDefaultSelected).toList();
}
}
@@ -54,4 +54,8 @@ public class ActGroup extends BaseModel {
@Many(field = "groupId") @Many(field = "groupId")
private List<ActGroupUser> groupUsers; private List<ActGroupUser> groupUsers;
@Column
@Comment("是否公开")
@ColDefine(type = ColType.BOOLEAN)
private Boolean isOpen;
} }
@@ -0,0 +1,52 @@
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.ColDefine;
import org.nutz.dao.entity.annotation.ColType;
import org.nutz.dao.entity.annotation.Column;
import org.nutz.dao.entity.annotation.Comment;
import org.nutz.dao.entity.annotation.Id;
import org.nutz.dao.entity.annotation.Index;
import org.nutz.dao.entity.annotation.Table;
import org.nutz.dao.entity.annotation.TableIndexes;
import org.nutz.dao.entity.annotation.TableMeta;
/**
* 活动报名字段配置。
* 每条记录表示某个活动启用了一个安全字段目录中的报名字段。
*/
@Data
@Table("act_registration_field")
@Comment("活动报名字段配置")
@EqualsAndHashCode(callSuper = true)
@TableMeta("{'mysql-charset':'utf8mb4'}")
@TableIndexes({
@Index(name = "UK_ACT_REG_FIELD", fields = {"activityId", "fieldKey"}, unique = true)
})
public class ActRegistrationField extends BaseModel {
@Id
@Comment("ID")
private Long id;
@Column
@Comment("活动ID")
@ColDefine(customType = "bigint")
private Long activityId;
@Column
@Comment("字段编码")
@ColDefine(type = ColType.VARCHAR, width = 50)
private String fieldKey;
@Column
@Comment("是否必填")
@ColDefine(type = ColType.BOOLEAN)
private Boolean required;
@Column
@Comment("显示顺序")
private Integer sort;
}
@@ -6,6 +6,7 @@ import lombok.EqualsAndHashCode;
import org.nutz.dao.entity.annotation.*; import org.nutz.dao.entity.annotation.*;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
/** /**
* @version 1.0 * @version 1.0
@@ -55,6 +56,16 @@ public class ActUser extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 10) @ColDefine(type = ColType.VARCHAR, width = 10)
private String sex; private String sex;
@Column
@Comment("人员类型字典代码,多个代码以逗号分隔")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String personType;
@Column
@Comment("项目类型字典代码,多个代码以逗号分隔")
@ColDefine(type = ColType.VARCHAR, width = 255)
private String projectType;
@Column @Column
@Comment("职务") @Comment("职务")
@ColDefine(type = ColType.VARCHAR, width = 100) @ColDefine(type = ColType.VARCHAR, width = 100)
@@ -65,6 +76,11 @@ public class ActUser extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 20) @ColDefine(type = ColType.VARCHAR, width = 20)
private String idCard; private String idCard;
@Column
@Comment("出生日期")
@ColDefine(type = ColType.DATE)
private Date birthday;
@Column @Column
@Comment("联系方式") @Comment("联系方式")
@ColDefine(type = ColType.VARCHAR, width = 30) @ColDefine(type = ColType.VARCHAR, width = 30)
@@ -23,6 +23,10 @@ public class ActWonderfulMoment extends BaseModel {
@Comment("ID") @Comment("ID")
private Long id; private Long id;
@Column
@Comment("所属活动ID")
private Long activityId;
@Column @Column
@Comment("标题") @Comment("标题")
@ColDefine(type = ColType.VARCHAR, width = 100) @ColDefine(type = ColType.VARCHAR, width = 100)
@@ -90,6 +90,12 @@ public class Activity extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 500) @ColDefine(type = ColType.VARCHAR, width = 500)
private String cover; private String cover;
@Comment("是否开启")
@Column
@Default(value = "1")
@ColDefine(type = ColType.BOOLEAN)
private Boolean isOpen = true;
@Comment("附件") @Comment("附件")
@Column @Column
@ColDefine(type = ColType.MYSQL_JSON) @ColDefine(type = ColType.MYSQL_JSON)
@@ -26,6 +26,16 @@ public class ActivityItem extends BaseModel {
@ColDefine(type = ColType.VARCHAR, width = 30) @ColDefine(type = ColType.VARCHAR, width = 30)
private String name; private String name;
@Comment("项目类型(project_type数据字典编码)")
@Column
@ColDefine(type = ColType.VARCHAR, width = 20)
private String projectType;
@Comment("报名规则(项目类型子项编码及人数上限JSON)")
@Column
@ColDefine(type = ColType.TEXT)
private String registrationRule;
@Comment("比赛方法") @Comment("比赛方法")
@Column @Column
@ColDefine(type = ColType.VARCHAR, width = 300) @ColDefine(type = ColType.VARCHAR, width = 300)
@@ -24,6 +24,14 @@ public interface ActGroupService extends BaseService<ActGroup> {
*/ */
void groupSetting(ActGroupVo groupVo); void groupSetting(ActGroupVo groupVo);
/**
* 按单位重建活动全部有效报名人员的分组,同一单位只生成一个组。
*
* @param activityId 活动ID;不限制人员签到状态
* @throws com.budwk.app.base.exception.BaseException 活动或报名数据不满足分组条件时抛出
*/
void groupByUnit(Long activityId);
/** /**
* 设置未分组人员 * 设置未分组人员
* @param unGroupUserVo * @param unGroupUserVo
@@ -3,6 +3,8 @@ package com.budwk.app.module.activity.service;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.module.activity.models.ActWonderfulMoment; import com.budwk.app.module.activity.models.ActWonderfulMoment;
import java.util.List;
/** /**
* @ClassName ActWonderfulMomentService * @ClassName ActWonderfulMomentService
* @Author JyuHsin * @Author JyuHsin
@@ -11,4 +13,28 @@ import com.budwk.app.module.activity.models.ActWonderfulMoment;
* @Description TODO * @Description TODO
*/ */
public interface ActWonderfulMomentService extends BaseService<ActWonderfulMoment> { public interface ActWonderfulMomentService extends BaseService<ActWonderfulMoment> {
/**
* 查询客户端可见的精彩瞬间。
*
* @param id 精彩瞬间ID;传空时查询全部可见数据,传值时只查询指定数据
* @return 精彩瞬间列表;仅包含已发布且所属活动已开启的数据
*/
List<ActWonderfulMoment> listClientVisible(Long id);
/**
* 查询一条客户端可见的精彩瞬间。
*
* @param id 精彩瞬间ID,不能为空
* @return 精彩瞬间;未发布、所属活动关闭或数据不存在时返回null
*/
ActWonderfulMoment fetchClientVisible(Long id);
/**
* 增加客户端可见精彩瞬间的阅读次数。
*
* @param id 精彩瞬间ID,不能为空
* @return 更新记录数;1表示成功,0表示数据当前不可见或不存在
*/
int increaseClientReadCount(Long id);
} }
@@ -2,6 +2,44 @@ package com.budwk.app.module.activity.service;
import com.budwk.app.base.service.BaseService; import com.budwk.app.base.service.BaseService;
import com.budwk.app.module.activity.models.Activity; import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.models.ActivityItem;
import org.nutz.lang.util.NutMap;
import java.util.List;
public interface ActivityManageService extends BaseService<Activity> { public interface ActivityManageService extends BaseService<Activity> {
/**
* 查询指定年度的有效活动,并将最后创建的活动排在最前面。
*
* @param year 活动开始时间所属年度;传空时查询全部年度
* @return 未删除活动列表,按照创建时间和活动ID倒序排列
*/
List<Activity> listByYearOrderByLatest(Integer year);
/**
* 查询客户端登录页需要展示的当前最新开启活动单位信息。
*
* @return 当前活动展示信息;title 为活动标题,hostUnit 为主办单位,
* coHostUnit 为承办单位,没有符合条件的活动时各字段均为空字符串
*/
NutMap getLatestOpenActivityOrganizer();
/**
* 更新活动在客户端的开启状态。
*
* @param id 活动ID,必须对应已存在的活动
* @param isOpen 是否开启;true 表示客户端显示,false 表示客户端隐藏
* @return 实际更新的数据行数
*/
int updateOpenStatus(Long id, Boolean isOpen);
/**
* 保存指定活动的项目,并删除本次提交中已经移除的旧项目。
*
* @param activityId 活动ID,必须对应已存在的活动
* @param items 项目列表;每项的projectType必须是启用的project_type字典编码
* @throws com.budwk.app.base.exception.BaseException 活动不存在、项目类型为空或字典值无效时抛出
*/
void updateItems(Long activityId, List<ActivityItem> items);
} }
@@ -0,0 +1,105 @@
package com.budwk.app.module.activity.service;
import com.budwk.app.base.service.BaseService;
import com.budwk.app.module.activity.models.ActRegistrationField;
import com.budwk.app.module.activity.models.ActUser;
import org.nutz.lang.util.NutMap;
import java.util.List;
import java.util.Map;
public interface ActivityRegistrationFieldService extends BaseService<ActRegistrationField> {
/**
* 获取活动报名字段目录及配置状态。
*
* @param activityId 活动ID
* @param selectedOnly true只返回已启用字段,false返回完整安全字段目录
* @param includeOptions true时为数据字典字段附带可用选项
* @return 字段配置列表;包含字段编码、名称、组件类型、必填状态、顺序、依赖和字典选项
*/
List<NutMap> getFieldConfig(Long activityId, boolean selectedOnly, boolean includeOptions);
/**
* 覆盖保存某活动的报名字段配置。
*
* @param activityId 活动ID
* @param fields 管理端选中的字段配置
*/
void saveFieldConfig(Long activityId, List<ActRegistrationField> fields);
/**
* 获取当前活动有效字段的必填规则。
*
* @param activityId 活动ID
* @return key为字段编码,value表示是否必填
*/
Map<String, Boolean> getRequiredMap(Long activityId);
/**
* 校验数据字典字段提交值。
*
* @param activityId 活动ID;项目类型需要据此定位唯一项目配置的下级字典
* @param fieldKey 报名字段编码
* @param value 单个字典子项code,或使用英文逗号分隔的多个code
* @return 字段不是字典字段,或全部值均属于启用字典项时返回true
*/
boolean isValidDictValue(Long activityId, String fieldKey, String value);
/**
* 校验人员类型的业务组合。
*
* @param value 使用英文逗号分隔的人员类型字典code
* @return 未同时选择“教练”和“领队”时返回true;运动员可以与其中任意一种同时选择
*/
boolean isValidPersonTypeCombination(String value);
/**
* 根据字典名称判断多选人员类型是否满足字段联动条件。
*
* @param personType 逗号分隔的人员类型字典编码
* @param labels 逗号分隔的人员类型名称,匹配任意一项即满足条件
* @return 已选人员类型中包含目标类型时返回true
*/
boolean matchesPersonTypes(String personType, String labels);
/**
* 获取同一活动、同一单位的人员类型人数限制状态。
*
* @param activityId 活动ID;只有活动恰好一个项目且已启用人员类型字段时规则才生效
* @param unitId 单位ID,用于限定已有报名人员的统计范围
* @param excludedSignUpUserId 当前报名操作账号ID;其旧报名记录会被本次页面数据覆盖,因此统计时排除
* @return NutMap结构;enabled表示规则是否生效,coachValue/leaderValue为字典值,
* coachCount/leaderCount为排除当前账号旧记录后的已有报名人数,maxCount固定为1
*/
NutMap getPersonTypeRuleStatus(Long activityId, String unitId, Long excludedSignUpUserId);
/**
* 获取唯一活动项目的各项目类型报名人数限制状态。
*
* @param activityId 活动ID;只有活动恰好一个项目且配置了报名规则时规则才生效
* @param unitId 单位ID,用于统计同一活动、同一单位的已报名人员
* @param excludedSignUpUserId 当前报名操作账号ID;其旧报名记录会被当前草稿覆盖,因此统计时排除
* @return NutMap结构;enabled表示规则是否生效,rules包含类型编码、名称、人数上限和其他账号已占用人数
*/
NutMap getProjectTypeRuleStatus(Long activityId, String unitId, Long excludedSignUpUserId);
/**
* 校验单项目活动中同一单位最终报名人员的教练、领队人数。
*
* @param activityId 活动ID;非单项目活动直接跳过
* @param users 同一活动、同一单位最终的全部报名人员
* @throws com.budwk.app.base.exception.BaseException 教练或领队超过1人、人员类型字典配置不完整时抛出
*/
void validatePersonTypeRules(Long activityId, List<ActUser> users);
/**
* 校验同一活动、同一单位全部报名人员的项目类型组合。
* 校验前会将历史“单打”“双打”代码按人员性别转换为当前男子或女子单打、双打代码。
*
* @param activityId 活动ID,用于读取唯一项目的项目类型下级字典
* @param users 同一活动、同一单位的全部报名人员,包含本次提交人员和其他账号已提交人员
* @throws com.budwk.app.base.exception.BaseException 未选择运动员类型、同性别单双打冲突、项目性别不符或混双男女数量不一致时抛出
*/
void validateProjectTypeRules(Long activityId, List<ActUser> users);
}
@@ -1,7 +1,10 @@
package com.budwk.app.module.activity.service.impl; package com.budwk.app.module.activity.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.IdcardUtil;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.module.activity.models.*; import com.budwk.app.module.activity.models.*;
import com.budwk.app.module.activity.service.ActGroupService; import com.budwk.app.module.activity.service.ActGroupService;
import com.budwk.app.module.activity.vo.ActGroupVo; import com.budwk.app.module.activity.vo.ActGroupVo;
@@ -14,6 +17,7 @@ import org.nutz.dao.sql.Sql;
import org.nutz.ioc.aop.Aop; import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.lang.util.NutMap; import org.nutz.lang.util.NutMap;
import org.nutz.lang.Strings;
import java.util.*; import java.util.*;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -47,6 +51,7 @@ public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements Ac
List<ActUser> signUpUserList = dao().query(ActUser.class, Cnd.where(ActUser::getActivityId, "=", groupVo.getActivityId()) List<ActUser> signUpUserList = dao().query(ActUser.class, Cnd.where(ActUser::getActivityId, "=", groupVo.getActivityId())
.and(ActUser::getIsSignIn, "=", 1)); .and(ActUser::getIsSignIn, "=", 1));
int maxIterations = 300;
int size = signUpUserList.size(); int size = signUpUserList.size();
Integer oneGroupLimitUserNum = groupVo.getOneGroupLimitUserNum(); Integer oneGroupLimitUserNum = groupVo.getOneGroupLimitUserNum();
@@ -57,44 +62,26 @@ public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements Ac
groups.add(new ArrayList<>()); groups.add(new ArrayList<>());
} }
// 分离男女用户 // 分离男女
Map<String, List<ActUser>> genderMap = signUpUserList.stream() Map<String, List<ActUser>> genderMap = signUpUserList.stream()
.collect(Collectors.groupingBy(user -> "男".equals(user.getSex()) ? "男" : "女")); .collect(Collectors.groupingBy(user -> "男".equals(user.getSex()) ? "男" : "女"));
List<ActUser> males = genderMap.getOrDefault("男", Collections.emptyList()); List<ActUser> males = new ArrayList<>(genderMap.getOrDefault("男", Collections.emptyList()));
List<ActUser> females = genderMap.getOrDefault("女", Collections.emptyList()); List<ActUser> females = new ArrayList<>(genderMap.getOrDefault("女", Collections.emptyList()));
// 随机化列表 // 保持原始顺序或打乱(避免系统性偏差)
Collections.shuffle(males); Collections.shuffle(males);
Collections.shuffle(females); Collections.shuffle(females);
// 尝试分配
List<ActUser> unassignedUsers = new ArrayList<>(); List<ActUser> unassignedUsers = new ArrayList<>();
int maleIndex = 0, femaleIndex = 0; // 第一阶段:贪心初始化分配
while (maleIndex < males.size() || femaleIndex < females.size()) { assignUsersGreedy(groups, males, groupVo.getManUserNum(), oneGroupLimitUserNum, unassignedUsers);
boolean assigned = false; assignUsersGreedy(groups, females, groupVo.getWomanUserNum(), oneGroupLimitUserNum, unassignedUsers);
for (List<ActUser> group : groups) {
if (group.size() < groupVo.getOneGroupLimitUserNum()) {
int currentMaleCount = (int) group.stream().filter(u -> "男".equals(u.getSex())).count();
int currentFemaleCount = (int) group.stream().filter(u -> "女".equals(u.getSex())).count();
if (maleIndex < males.size() && (groupVo.getManUserNum() == -1 || currentMaleCount < groupVo.getManUserNum())) { // 第二阶段:组间交换优化(同性别交换,降低方差)
group.add(males.get(maleIndex++)); optimizeBySwapping(groups, maxIterations);
assigned = true;
} else if (femaleIndex < females.size() && (groupVo.getWomanUserNum() == -1 || currentFemaleCount < groupVo.getWomanUserNum())) {
group.add(females.get(femaleIndex++));
assigned = true;
}
}
}
if (!assigned) {
break;
}
}
unassignedUsers.addAll(males.subList(maleIndex, males.size()));
unassignedUsers.addAll(females.subList(femaleIndex, females.size()));
// 往组别表里面添加记录 // 往组别表里面添加记录
List<ActGroup> groupList = IntStream.range(0, groups.size()) List<ActGroup> groupList = IntStream.range(0, groups.size())
@@ -112,6 +99,7 @@ public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements Ac
}).collect(Collectors.toList()); }).collect(Collectors.toList());
actGroup.setGroupUsers(userList); actGroup.setGroupUsers(userList);
actGroup.setIsOpen(false);
return actGroup; return actGroup;
}).toList(); }).toList();
@@ -147,6 +135,62 @@ public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements Ac
} }
/**
* 将活动全部有效报名人员按单位ID重建为一单位一组,并初始化各项目得分记录。
*
* @param activityId 活动ID
* @throws BaseException 活动不存在、无有效报名人员或人员缺少单位、关联账号时抛出
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void groupByUnit(Long activityId) {
if (activityId == null || dao().fetch(Activity.class,
Cnd.where(Activity::getId, "=", activityId).and(Activity::getDelFlag, "=", false)) == null) {
throw new BaseException("请选择有效活动");
}
List<ActUser> users = dao().query(ActUser.class,
Cnd.where(ActUser::getActivityId, "=", activityId).and(ActUser::getDelFlag, "=", false)
.asc(ActUser::getUnitName).asc(ActUser::getId));
if (users.isEmpty()) {
throw new BaseException("当前活动暂无有效报名人员");
}
Map<String, List<ActUser>> unitUsers = new LinkedHashMap<>();
for (ActUser user : users) {
if (Strings.isBlank(user.getUnitId()) || user.getUserId() == null) {
throw new BaseException("报名人员【" + user.getUserName() + "】缺少所属单位或关联账号,请补齐后再分组");
}
unitUsers.computeIfAbsent(user.getUnitId(), key -> new ArrayList<>()).add(user);
}
List<ActivityItem> items = dao().query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", activityId));
// 完成数据检查后再覆盖旧分组,整个重建过程在同一事务内完成。
dao().clear(ActItemGroupScore.class, Cnd.where(ActItemGroupScore::getActivityId, "=", activityId));
dao().clear(ActGroupUser.class, Cnd.where(ActGroupUser::getActivityId, "=", activityId));
dao().clear(ActUnGroupUser.class, Cnd.where(ActUnGroupUser::getActivityId, "=", activityId));
dao().clear(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", activityId));
int sort = 1;
for (List<ActUser> members : unitUsers.values()) {
ActGroup group = new ActGroup();
group.setActivityId(activityId);
group.setGroupName(Strings.sBlank(members.get(0).getUnitName(), members.get(0).getUnitId()));
group.setSort(sort++);
group.setIsOpen(false);
group.setGroupUsers(members.stream().map(ActUser::getUserId).distinct().map(userId -> {
ActGroupUser member = new ActGroupUser();
member.setActivityId(activityId);
member.setUserId(userId);
return member;
}).collect(Collectors.toList()));
dao().insertWith(group, "groupUsers");
for (ActivityItem item : items) {
ActItemGroupScore score = new ActItemGroupScore();
score.setActivityId(activityId);
score.setItemId(item.getId());
score.setGroupId(group.getId());
dao().insert(score);
}
}
}
@Override @Override
@Aop(TransAop.READ_COMMITTED) @Aop(TransAop.READ_COMMITTED)
public void unGroupUserSetting(ActUnGroupUserVo unGroupUserVo) { public void unGroupUserSetting(ActUnGroupUserVo unGroupUserVo) {
@@ -252,4 +296,129 @@ public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements Ac
} }
return groupFull; return groupFull;
} }
// 贪心分配(简化版,不计算方差,只满足约束,快速初始化)
private static void assignUsersGreedy(
List<List<ActUser>> groups,
List<ActUser> users,
int genderLimit,
int groupLimit,
List<ActUser> unassigned
) {
int[] genderCounts = new int[groups.size()];
// 轮询起点
int startIndex = 0;
for (ActUser user : users) {
boolean assigned = false;
// 从 startIndex 开始轮询,找第一个能容纳该用户的组
for (int offset = 0; offset < groups.size(); offset++) {
int i = (startIndex + offset) % groups.size();
if (groups.get(i).size() < groupLimit &&
(genderLimit == -1 || genderCounts[i] < genderLimit)) {
groups.get(i).add(user);
genderCounts[i]++;
// 下次从下一个组开始
startIndex = (i + 1) % groups.size();
assigned = true;
break;
}
}
if (!assigned) {
unassigned.add(user);
}
}
}
// 第二阶段:组间交换优化(核心!)
private static void optimizeBySwapping(List<List<ActUser>> groups, int maxIterations) {
Random rand = new Random();
double currentVariance = calculateTotalVariance(groups);
for (int iter = 0; iter < maxIterations; iter++) {
// 随机选两个不同组
int g1 = rand.nextInt(groups.size());
int g2 = rand.nextInt(groups.size());
if (g1 == g2 || groups.get(g1).isEmpty() || groups.get(g2).isEmpty()) {
continue;
}
// 随机选一个性别(男或女)
String sex = rand.nextBoolean() ? "男" : "女";
// 获取两组中该性别的用户
List<ActUser> users1 = groups.get(g1).stream().filter(u -> sex.equals(u.getSex())).toList();
List<ActUser> users2 = groups.get(g2).stream().filter(u -> sex.equals(u.getSex())).toList();
if (users1.isEmpty() || users2.isEmpty()) {
continue;
}
// 随机选一个用户交换
ActUser u1 = users1.get(rand.nextInt(users1.size()));
ActUser u2 = users2.get(rand.nextInt(users2.size()));
// 临时交换
groups.get(g1).remove(u1);
groups.get(g2).remove(u2);
groups.get(g1).add(u2);
groups.get(g2).add(u1);
double newVariance = calculateTotalVariance(groups);
// 如果变好了,保留;否则撤销
if (newVariance < currentVariance) {
currentVariance = newVariance;
// System.out.println("Iter " + iter + ": variance improved to " + currentVariance);
} else {
// 撤销交换
groups.get(g1).remove(u2);
groups.get(g2).remove(u1);
groups.get(g1).add(u1);
groups.get(g2).add(u2);
}
}
}
// 计算年龄总和方差 + 体重总和方差(标准化后加权)
private static double calculateTotalVariance(List<List<ActUser>> groups) {
if (groups.isEmpty()) return 0.0;
double[] ageSums = groups.stream()
.mapToDouble(g -> g.stream().mapToDouble(v -> DateUtil.ageOfNow(v.getBirthday())).sum())
.toArray();
double[] weightSums = groups.stream()
.mapToDouble(g -> g.stream().mapToDouble(ActUser::getWeight).sum())
.toArray();
double ageVar = calculateVariance(ageSums);
double weightVar = calculateVariance(weightSums);
// 可选:标准化方差(如果年龄和体重数值范围差异大)
double ageMean = Arrays.stream(ageSums).average().orElse(0.0);
double weightMean = Arrays.stream(weightSums).average().orElse(0.0);
// 如果均值 > 0,用变异系数(标准差/均值)更公平
double ageCV = ageMean > 0 ? Math.sqrt(ageVar) / ageMean : 0.0;
double weightCV = weightMean > 0 ? Math.sqrt(weightVar) / weightMean : 0.0;
// 使用变异系数,避免量纲影响
return ageCV + weightCV;
}
private static double calculateVariance(double[] values) {
if (values.length == 0) return 0.0;
double mean = Arrays.stream(values).average().orElse(0.0);
return Arrays.stream(values)
.map(v -> Math.pow(v - mean, 2))
.average()
.orElse(0.0);
}
} }
@@ -59,6 +59,10 @@ public class ActUserServiceImpl extends BaseServiceImpl<ActUser> implements ActU
WHEN 1 THEN '是' WHEN 1 THEN '是'
WHEN 0 THEN '' WHEN 0 THEN ''
END AS isPartyDesc, END AS isPartyDesc,
CASE
WHEN u.signUpUserId = u.userId THEN
'是' ELSE ''
END AS isSIgnUpUser,
act.`title` AS activityName act.`title` AS activityName
FROM FROM
act_user u act_user u
@@ -75,6 +79,15 @@ public class ActUserServiceImpl extends BaseServiceImpl<ActUser> implements ActU
} }
cnd.andEX("u.isSignIn", "=", pageForm.getIsSignIn()); cnd.andEX("u.isSignIn", "=", pageForm.getIsSignIn());
cnd.andEX("u.unitId", "=", pageForm.getUnitId()); cnd.andEX("u.unitId", "=", pageForm.getUnitId());
// 仅开放单位名称排序,避免客户端传入任意列名;未指定时保持单位名称升序。
if ("unitName".equals(pageForm.getPageOrderName()) && "descending".equals(pageForm.getPageOrderBy())) {
cnd.desc("u.unitName");
} else {
cnd.asc("u.unitName");
}
// 单位名称相同时固定按姓名和记录ID排序,确保翻页结果稳定。
cnd.asc("u.userName");
cnd.asc("u.id");
sql.setCondition(cnd); sql.setCondition(cnd);
return sql; return sql;
} }
@@ -3,9 +3,15 @@ package com.budwk.app.module.activity.service.impl;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.module.activity.models.ActWonderfulMoment; import com.budwk.app.module.activity.models.ActWonderfulMoment;
import com.budwk.app.module.activity.service.ActWonderfulMomentService; import com.budwk.app.module.activity.service.ActWonderfulMomentService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.dao.util.cri.Static;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import java.util.List;
/** /**
* @ClassName ActWonderfulMomentServiceImpl * @ClassName ActWonderfulMomentServiceImpl
* @Author JyuHsin * @Author JyuHsin
@@ -18,4 +24,42 @@ public class ActWonderfulMomentServiceImpl extends BaseServiceImpl<ActWonderfulM
public ActWonderfulMomentServiceImpl(Dao dao) { public ActWonderfulMomentServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@Override
public List<ActWonderfulMoment> listClientVisible(Long id) {
Cnd cnd = buildClientVisibleCnd();
cnd.andEX(ActWonderfulMoment::getId, "=", id);
cnd.desc(ActWonderfulMoment::getPublishTime);
return this.query(cnd);
}
@Override
public ActWonderfulMoment fetchClientVisible(Long id) {
if (id == null) {
return null;
}
return this.fetch(buildClientVisibleCnd().and(ActWonderfulMoment::getId, "=", id));
}
@Override
@Aop(TransAop.READ_COMMITTED)
public int increaseClientReadCount(Long id) {
ActWonderfulMoment moment = this.fetchClientVisible(id);
if (moment == null) {
return 0;
}
Integer readCount = moment.getReadCount();
moment.setReadCount(readCount == null ? 1 : readCount + 1);
return this.update(moment);
}
/**
* 构造客户端可见条件:精彩瞬间已发布,并且关联的活动处于开启且未删除状态。
*
* @return 可直接追加业务条件的Cnd对象
*/
private Cnd buildClientVisibleCnd() {
return Cnd.where(ActWonderfulMoment::getPublished, "=", true)
.and(new Static("activityId IN (SELECT id FROM activity WHERE isOpen = 1 AND delFlag = 0)"));
}
} }
@@ -1,14 +1,187 @@
package com.budwk.app.module.activity.service.impl; package com.budwk.app.module.activity.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl; import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.module.activity.models.Activity; import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.models.ActivityItem;
import com.budwk.app.module.activity.service.ActivityManageService; import com.budwk.app.module.activity.service.ActivityManageService;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
import org.nutz.aop.interceptor.ioc.TransAop;
import org.nutz.dao.Chain;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.aop.Aop;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.json.JsonFormat;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@IocBean(args = {"refer:dao"}) @IocBean(args = {"refer:dao"})
public class ActivityManageServiceImpl extends BaseServiceImpl<Activity> implements ActivityManageService { public class ActivityManageServiceImpl extends BaseServiceImpl<Activity> implements ActivityManageService {
@Inject
private SysDictService sysDictService;
public ActivityManageServiceImpl(Dao dao) { public ActivityManageServiceImpl(Dao dao) {
super(dao); super(dao);
} }
@Override
public List<Activity> listByYearOrderByLatest(Integer year) {
Cnd cnd = Cnd.where(Activity::getDelFlag, "=", false);
cnd.andEX("YEAR(startTime)", "=", year);
cnd.desc(Activity::getCreatedAt);
cnd.desc(Activity::getId);
return this.query(cnd);
}
/**
* 登录页只使用已开启、未删除活动的单位信息,避免已关闭活动继续影响客户端展示。
* 当创建时间相同时再按活动ID倒序,确保“最新活动”的选择结果稳定。
*
* @return title 为活动标题,hostUnit 为主办单位,coHostUnit 为承办单位;
* 无可用活动时各字段均返回空字符串
*/
@Override
public NutMap getLatestOpenActivityOrganizer() {
Cnd cnd = Cnd.where(Activity::getDelFlag, "=", false);
cnd.and(Activity::getIsOpen, "=", true);
cnd.desc(Activity::getCreatedAt);
cnd.desc(Activity::getId);
Activity activity = this.fetch(cnd);
if (activity == null) {
return NutMap.NEW()
.addv("title", "")
.addv("hostUnit", "")
.addv("coHostUnit", "");
}
return NutMap.NEW()
.addv("title", Strings.sNull(activity.getTitle()))
.addv("hostUnit", Strings.sNull(activity.getHostUnit()))
.addv("coHostUnit", Strings.sNull(activity.getCoHostUnit()));
}
/**
* 仅更新活动的客户端开启状态,避免列表开关提交并覆盖活动的其他业务字段。
*
* @param id 活动ID
* @param isOpen 是否开启;true 表示客户端显示,false 表示客户端隐藏
* @return 实际更新的数据行数
*/
@Override
public int updateOpenStatus(Long id, Boolean isOpen) {
return this.update(Chain.make("isOpen", isOpen), Cnd.where(Activity::getId, "=", id));
}
/**
* 项目类型只保存project_type字典编码,服务端再次校验可防止绕过页面提交停用或非法值。
*
* @param activityId 活动ID
* @param items 本次保存的完整项目列表
*/
@Override
@Aop(TransAop.READ_COMMITTED)
public void updateItems(Long activityId, List<ActivityItem> items) {
if (activityId == null || this.fetch(activityId) == null) {
throw new BaseException("活动不存在");
}
List<ActivityItem> submittedItems = items == null ? Collections.emptyList() : items;
Set<String> enabledProjectTypes = new LinkedHashSet<>();
for (Sys_dict dict : sysDictService.getSubListByCode("project_type")) {
if (!dict.isDisabled() && Strings.isNotBlank(dict.getCode())) {
enabledProjectTypes.add(dict.getCode());
}
}
if (enabledProjectTypes.isEmpty()) {
throw new BaseException("项目类型数据字典未配置可用选项");
}
List<ActivityItem> oldItems = dao().query(ActivityItem.class,
Cnd.where(ActivityItem::getActivityId, "=", activityId));
List<Long> oldItemIds = oldItems.stream().map(ActivityItem::getId).toList();
for (ActivityItem item : submittedItems) {
if (Strings.isBlank(item.getProjectType())) {
throw new BaseException("请选择项目类型");
}
if (!enabledProjectTypes.contains(item.getProjectType())) {
throw new BaseException("项目类型不属于当前可用的数据字典选项");
}
validateAndNormalizeRegistrationRule(item);
item.setActivityId(activityId);
}
dao().insertOrUpdate(submittedItems);
List<Long> submittedItemIds = submittedItems.stream().map(ActivityItem::getId).toList();
Collection<Long> deletedItemIds = CollUtil.subtract(oldItemIds, submittedItemIds);
if (!deletedItemIds.isEmpty()) {
dao().clear(ActivityItem.class, Cnd.where("id", "in", deletedItemIds));
}
}
/**
* 校验项目类型下每个启用子项都设置了非负人数,并统一保存为只含编码和人数的紧凑JSON。
* 各子项人数相互独立,不与ActivityItem.num比较,允许合计超过项目报名人数。
*/
private void validateAndNormalizeRegistrationRule(ActivityItem item) {
List<Sys_dict> children = sysDictService.getSubListByCode(item.getProjectType()).stream()
.filter(dict -> !dict.isDisabled() && Strings.isNotBlank(dict.getCode()))
.toList();
if (children.isEmpty()) {
item.setRegistrationRule("[]");
return;
}
List<NutMap> submittedRules;
try {
submittedRules = Strings.isBlank(item.getRegistrationRule())
? Collections.emptyList()
: Json.fromJsonAsList(NutMap.class, item.getRegistrationRule());
} catch (RuntimeException e) {
throw new BaseException("项目“" + Strings.sNull(item.getName()) + "”的报名规则格式不正确");
}
if (submittedRules == null) {
submittedRules = Collections.emptyList();
}
Set<String> childCodes = children.stream().map(Sys_dict::getCode).collect(java.util.stream.Collectors.toSet());
Map<String, NutMap> submittedRuleMap = new LinkedHashMap<>();
for (NutMap rule : submittedRules) {
String typeCode = rule.getString("typeCode");
if (Strings.isBlank(typeCode) || !childCodes.contains(typeCode)
|| submittedRuleMap.put(typeCode, rule) != null) {
throw new BaseException("项目“" + Strings.sNull(item.getName()) + "”存在重复或无效的报名规则");
}
}
List<NutMap> normalizedRules = new ArrayList<>();
for (Sys_dict child : children) {
NutMap rule = submittedRuleMap.get(child.getCode());
Object personCountValue = rule == null ? null : rule.get("personCount");
Integer personCount = null;
if (personCountValue instanceof Number number
&& Double.isFinite(number.doubleValue())
&& number.doubleValue() == Math.rint(number.doubleValue())) {
personCount = number.intValue();
}
if (personCount == null || personCount < 0 || personCount > 9999) {
throw new BaseException("请设置项目“" + Strings.sNull(item.getName())
+ "”中“" + child.getName() + "”的报名人数(0至9999)");
}
normalizedRules.add(NutMap.NEW()
.addv("typeCode", child.getCode())
.addv("personCount", personCount));
}
item.setRegistrationRule(Json.toJson(normalizedRules, JsonFormat.compact()));
}
} }
@@ -0,0 +1,634 @@
package com.budwk.app.module.activity.service.impl;
import com.budwk.app.base.exception.BaseException;
import com.budwk.app.base.service.impl.BaseServiceImpl;
import com.budwk.app.module.activity.enums.RegistrationFieldDefinition;
import com.budwk.app.module.activity.models.ActRegistrationField;
import com.budwk.app.module.activity.models.ActUser;
import com.budwk.app.module.activity.models.Activity;
import com.budwk.app.module.activity.models.ActivityItem;
import com.budwk.app.module.activity.service.ActivityRegistrationFieldService;
import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService;
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.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.lang.Strings;
import org.nutz.lang.util.NutMap;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@IocBean(args = {"refer:dao"})
public class ActivityRegistrationFieldServiceImpl extends BaseServiceImpl<ActRegistrationField>
implements ActivityRegistrationFieldService {
@Inject
private SysDictService sysDictService;
public ActivityRegistrationFieldServiceImpl(Dao dao) {
super(dao);
}
@Override
public List<NutMap> getFieldConfig(Long activityId, boolean selectedOnly, boolean includeOptions) {
ProjectTypeContext projectTypeContext = getProjectTypeContext(activityId);
List<ActRegistrationField> storedFields = this.query(Cnd.where(ActRegistrationField::getActivityId, "=", activityId)
.and(ActRegistrationField::getDelFlag, "=", false)
.asc(ActRegistrationField::getSort));
Map<String, ActRegistrationField> storedMap = storedFields.stream()
.collect(Collectors.toMap(ActRegistrationField::getFieldKey, item -> item, (left, right) -> left,
LinkedHashMap::new));
boolean useDefault = storedFields.isEmpty();
ActRegistrationField identityField = storedMap.get("idCard");
boolean legacyIdentitySort = identityField == null || identityField.getSort() == null
|| identityField.getSort() == 0;
// 兼容身份证原来排在0的活动配置,仅在旧顺序下为第6位腾出位置,避免重复顺延。
if (legacyIdentitySort) {
for (ActRegistrationField field : storedMap.values()) {
if (field.getSort() != null && field.getSort() >= 6) {
field.setSort(field.getSort() + 1);
}
}
if (identityField != null) {
identityField.setSort(6);
}
if (storedMap.containsKey("phone")) {
storedMap.get("phone").setSort(7);
}
if (storedMap.containsKey("position")) {
storedMap.get("position").setSort(8);
}
}
// 旧活动也补齐新增固定字段及其父字段,使人员类型联动无需重新保存即可生效。
for (RegistrationFieldDefinition definition : RegistrationFieldDefinition.values()) {
if (definition.isLocked() || useDefault && definition.isDefaultSelected()) {
storedMap.putIfAbsent(definition.getFieldKey(), buildDefaultField(definition));
}
}
for (String fieldKey : new LinkedHashSet<>(storedMap.keySet())) {
addParentField(storedMap, RegistrationFieldDefinition.fromKey(fieldKey));
}
List<NutMap> result = new ArrayList<>();
int defaultSort = 1;
for (RegistrationFieldDefinition definition : RegistrationFieldDefinition.values()) {
boolean projectTypeField = RegistrationFieldDefinition.PROJECT_TYPE == definition;
ActRegistrationField stored = storedMap.get(definition.getFieldKey());
boolean selected = stored != null;
// 多项目、无项目或唯一项目未配置有效类型时,历史配置也不再向客户端开放。
if (projectTypeField && !projectTypeContext.configurable) {
selected = false;
}
if (selectedOnly && !selected) {
defaultSort++;
continue;
}
boolean required = stored == null ? definition.isDefaultRequired() : Boolean.TRUE.equals(stored.getRequired());
int sort = stored == null || stored.getSort() == null
? buildDefaultField(definition).getSort() : stored.getSort();
NutMap item = NutMap.NEW()
.addv("fieldKey", definition.getFieldKey())
.addv("fieldLabel", definition.getFieldLabel())
.addv("componentType", definition.getComponentType())
.addv("selected", selected)
.addv("required", definition.isLocked() || projectTypeField || required)
.addv("locked", definition.isLocked())
.addv("sort", sort)
.addv("parentField", definition.getParentField())
.addv("showWhenValue", definition.getShowWhenValue())
.addv("dictCode", projectTypeField ? projectTypeContext.dictCode : definition.getDictCode())
.addv("configurable", !projectTypeField || projectTypeContext.configurable)
.addv("ruleMessage", projectTypeField ? projectTypeContext.message : "");
if (includeOptions && Strings.isNotBlank(definition.getDictCode())) {
item.addv("options", projectTypeField
? projectTypeContext.options
: getEnabledDictOptions(definition.getDictCode()));
}
result.add(item);
defaultSort++;
}
result.sort(Comparator.comparingInt(item -> item.getInt("sort", Integer.MAX_VALUE)));
return result;
}
@Override
@Aop(TransAop.READ_COMMITTED)
public void saveFieldConfig(Long activityId, List<ActRegistrationField> fields) {
if (activityId == null || dao().fetch(Activity.class, activityId) == null) {
throw new BaseException("活动不存在");
}
Map<String, ActRegistrationField> selectedMap = new LinkedHashMap<>();
ProjectTypeContext projectTypeContext = getProjectTypeContext(activityId);
if (fields != null) {
for (ActRegistrationField field : fields) {
RegistrationFieldDefinition definition = RegistrationFieldDefinition.fromKey(field.getFieldKey());
if (definition == null) {
throw new BaseException("报名字段不在允许配置范围内:" + field.getFieldKey());
}
if (RegistrationFieldDefinition.PROJECT_TYPE == definition && !projectTypeContext.configurable) {
throw new BaseException(projectTypeContext.message);
}
field.setRequired(Boolean.TRUE.equals(field.getRequired()));
selectedMap.put(field.getFieldKey(), field);
}
}
// 固定字段始终启用;关联子字段启用时自动补齐父字段,防止形成无法显示的孤立配置。
for (RegistrationFieldDefinition definition : RegistrationFieldDefinition.values()) {
if (definition.isLocked()) {
selectedMap.putIfAbsent(definition.getFieldKey(), buildDefaultField(definition));
}
}
Set<String> pendingKeys = new LinkedHashSet<>(selectedMap.keySet());
for (String fieldKey : pendingKeys) {
addParentField(selectedMap, RegistrationFieldDefinition.fromKey(fieldKey));
}
dao().clear(ActRegistrationField.class, Cnd.where(ActRegistrationField::getActivityId, "=", activityId));
int defaultSort = 1;
List<ActRegistrationField> saveList = new ArrayList<>();
for (ActRegistrationField field : selectedMap.values()) {
RegistrationFieldDefinition definition = RegistrationFieldDefinition.fromKey(field.getFieldKey());
field.setId(null);
field.setActivityId(activityId);
field.setRequired(definition.isLocked() || definition == RegistrationFieldDefinition.PROJECT_TYPE || Boolean.TRUE.equals(field.getRequired()));
field.setSort(field.getSort() == null ? buildDefaultField(definition).getSort() : field.getSort());
saveList.add(field);
defaultSort++;
}
saveList.sort(Comparator.comparing(ActRegistrationField::getSort));
// 保留配置序号,避免未启用字段被跳过后将身份证的第6位重新编号。
dao().insert(saveList);
}
@Override
public Map<String, Boolean> getRequiredMap(Long activityId) {
Map<String, Boolean> result = new LinkedHashMap<>();
for (NutMap item : getFieldConfig(activityId, true, false)) {
result.put(item.getString("fieldKey"), item.getBoolean("required", false));
}
return result;
}
@Override
public boolean isValidDictValue(Long activityId, String fieldKey, String value) {
RegistrationFieldDefinition definition = RegistrationFieldDefinition.fromKey(fieldKey);
if (definition == null || Strings.isBlank(definition.getDictCode())) {
return true;
}
List<NutMap> options = RegistrationFieldDefinition.PROJECT_TYPE == definition
? getProjectTypeContext(activityId).options
: getEnabledDictOptions(definition.getDictCode());
Set<String> enabledValues = options.stream()
.map(item -> item.getString("value"))
.collect(Collectors.toSet());
return splitMultiValue(value).stream().allMatch(enabledValues::contains);
}
/**
* 根据字典名称判断多选人员类型是否满足字段联动条件。
*
* @param personType 逗号分隔的人员类型字典编码
* @param labels 逗号分隔的人员类型名称,匹配任意一项即满足条件
* @return 已选人员类型中包含目标类型时返回true
*/
@Override
public boolean matchesPersonTypes(String personType, String labels) {
List<String> selected = splitMultiValue(personType);
List<String> expectedLabels = splitMultiValue(labels);
return getEnabledDictOptions(RegistrationFieldDefinition.PERSON_TYPE.getDictCode()).stream()
.anyMatch(option -> expectedLabels.contains(option.getString("label"))
&& selected.contains(option.getString("value")));
}
@Override
public boolean isValidPersonTypeCombination(String value) {
if (Strings.isBlank(value)) {
return true;
}
List<NutMap> options = getEnabledDictOptions(RegistrationFieldDefinition.PERSON_TYPE.getDictCode());
String coachValue = findOptionValue(options, "教练");
String leaderValue = findOptionValue(options, "领队");
Set<String> selectedValues = new LinkedHashSet<>(splitMultiValue(value));
return Strings.isBlank(coachValue) || Strings.isBlank(leaderValue)
|| !selectedValues.contains(coachValue) || !selectedValues.contains(leaderValue);
}
@Override
public NutMap getPersonTypeRuleStatus(Long activityId, String unitId, Long excludedSignUpUserId) {
NutMap result = NutMap.NEW()
.addv("enabled", false)
.addv("coachValue", "")
.addv("leaderValue", "")
.addv("coachCount", 0)
.addv("leaderCount", 0)
.addv("maxCount", 1);
if (!hasSingleActivityItem(activityId) || Strings.isBlank(unitId)
|| !getRequiredMap(activityId).containsKey(RegistrationFieldDefinition.PERSON_TYPE.getFieldKey())) {
return result;
}
List<NutMap> options = getEnabledDictOptions(RegistrationFieldDefinition.PERSON_TYPE.getDictCode());
String coachValue = findOptionValue(options, "教练");
String leaderValue = findOptionValue(options, "领队");
if (Strings.isBlank(coachValue) || Strings.isBlank(leaderValue)) {
throw new BaseException("人员类型数据字典配置不完整");
}
Cnd cnd = Cnd.where(ActUser::getActivityId, "=", activityId)
.and(ActUser::getUnitId, "=", unitId)
.and(ActUser::getDelFlag, "=", false);
if (excludedSignUpUserId != null) {
// 当前账号的旧记录会由本次表单整体覆盖;空报名账号记录属于其他来源,仍需纳入单位人数。
cnd.and(Cnd.exps("signUpUserId", "!=", excludedSignUpUserId)
.or("signUpUserId", "is", null));
}
List<ActUser> existingUsers = dao().query(ActUser.class, cnd);
result.setv("enabled", true);
result.setv("coachValue", coachValue);
result.setv("leaderValue", leaderValue);
result.setv("coachCount", countPersonType(existingUsers, coachValue));
result.setv("leaderCount", countPersonType(existingUsers, leaderValue));
return result;
}
@Override
public NutMap getProjectTypeRuleStatus(Long activityId, String unitId, Long excludedSignUpUserId) {
NutMap result = NutMap.NEW().addv("enabled", false).addv("rules", Collections.emptyList());
List<ActivityItem> items = dao().query(ActivityItem.class,
Cnd.where(ActivityItem::getActivityId, "=", activityId));
if (items.size() != 1 || Strings.isBlank(unitId)
|| Strings.isBlank(items.get(0).getRegistrationRule())) {
return result;
}
List<NutMap> options = getProjectTypeContext(activityId).options;
Map<String, String> optionNames = options.stream().collect(Collectors.toMap(
option -> option.getString("value"),
option -> option.getString("label"),
(left, right) -> left,
LinkedHashMap::new));
List<NutMap> registrationRules = parseRegistrationRules(items.get(0).getRegistrationRule());
Cnd cnd = Cnd.where(ActUser::getActivityId, "=", activityId)
.and(ActUser::getUnitId, "=", unitId)
.and(ActUser::getDelFlag, "=", false);
if (excludedSignUpUserId != null) {
// 当前账号旧名单将由页面草稿整体覆盖;只把同单位其他来源的记录作为已占用人数。
cnd.and(Cnd.exps("signUpUserId", "!=", excludedSignUpUserId)
.or("signUpUserId", "is", null));
}
Map<String, Integer> existingCounts = countProjectTypes(dao().query(ActUser.class, cnd), options);
List<NutMap> rules = new ArrayList<>();
for (NutMap registrationRule : registrationRules) {
String typeCode = registrationRule.getString("typeCode");
int personCount = parseRegistrationRulePersonCount(registrationRule);
rules.add(NutMap.NEW()
.addv("typeCode", typeCode)
.addv("typeName", Strings.sBlank(optionNames.get(typeCode), typeCode))
.addv("personCount", personCount)
.addv("existingCount", existingCounts.getOrDefault(typeCode, 0)));
}
result.setv("enabled", true);
result.setv("rules", rules);
return result;
}
@Override
public void validatePersonTypeRules(Long activityId, List<ActUser> users) {
if (users == null || users.isEmpty() || !hasSingleActivityItem(activityId)) {
return;
}
List<NutMap> options = getEnabledDictOptions(RegistrationFieldDefinition.PERSON_TYPE.getDictCode());
String coachValue = findOptionValue(options, "教练");
String leaderValue = findOptionValue(options, "领队");
if (Strings.isBlank(coachValue) || Strings.isBlank(leaderValue)) {
throw new BaseException("人员类型数据字典配置不完整");
}
if (countPersonType(users, coachValue) > 1) {
throw new BaseException("同一活动、同一单位最多报名1名教练,请重新选择");
}
if (countPersonType(users, leaderValue) > 1) {
throw new BaseException("同一活动、同一单位最多报名1名领队,请重新选择");
}
}
/**
* 按人员类型字典编码统计报名人数;同一人员的重复编码只计一次。
*/
private int countPersonType(List<ActUser> users, String typeValue) {
int count = 0;
for (ActUser user : users) {
if (new LinkedHashSet<>(splitMultiValue(user.getPersonType())).contains(typeValue)) {
count++;
}
}
return count;
}
/**
* 人员数量规则仅适用于恰好配置一个项目的活动。
*/
private boolean hasSingleActivityItem(Long activityId) {
return activityId != null && dao().count(ActivityItem.class,
Cnd.where(ActivityItem::getActivityId, "=", activityId)) == 1;
}
@Override
public void validateProjectTypeRules(Long activityId, List<ActUser> users) {
if (users == null || users.isEmpty()) {
return;
}
List<NutMap> options = getProjectTypeContext(activityId).options;
String manSingleValue = findOptionValue(options, "男子单打");
String womanSingleValue = findOptionValue(options, "女子单打");
String manDoubleValue = findOptionValue(options, "男子双打");
String womanDoubleValue = findOptionValue(options, "女子双打");
String mixedDoubleValue = findOptionValue(options, "混合双打");
String athleteValue = findOptionValue(
getEnabledDictOptions(RegistrationFieldDefinition.PERSON_TYPE.getDictCode()), "运动员");
if (Strings.isBlank(manSingleValue) || Strings.isBlank(womanSingleValue)
|| Strings.isBlank(manDoubleValue)
|| Strings.isBlank(womanDoubleValue) || Strings.isBlank(mixedDoubleValue)) {
throw new BaseException("项目类型数据字典配置不完整");
}
int mixedMaleCount = 0;
int mixedFemaleCount = 0;
for (ActUser user : users) {
Set<String> selectedValues = new LinkedHashSet<>(splitMultiValue(user.getProjectType()));
if (!selectedValues.isEmpty()) {
if (Strings.isBlank(athleteValue)) {
throw new BaseException("人员类型数据字典配置不完整,请联系管理员");
}
// 项目报名只允许运动员参加;教练或领队可兼任运动员,但不能单独选择项目。
if (!splitMultiValue(user.getPersonType()).contains(athleteValue)) {
throw new BaseException("选择项目类型前,请先选择人员类型中的“运动员”");
}
}
// 兼容字典拆分前保存的Single、Double:按性别转换本次校验数据,不直接批量更新历史记录。
if ("男".equals(user.getSex())) {
replaceLegacyProjectType(selectedValues, "Single", manSingleValue);
replaceLegacyProjectType(selectedValues, "Double", manDoubleValue);
} else if ("女".equals(user.getSex())) {
replaceLegacyProjectType(selectedValues, "Single", womanSingleValue);
replaceLegacyProjectType(selectedValues, "Double", womanDoubleValue);
}
user.setProjectType(String.join(",", selectedValues));
if (selectedValues.contains(manSingleValue) && selectedValues.contains(manDoubleValue)) {
throw new BaseException("男子单打和男子双打不能同时选择,请按规则重新选择");
}
if (selectedValues.contains(womanSingleValue) && selectedValues.contains(womanDoubleValue)) {
throw new BaseException("女子单打和女子双打不能同时选择,请按规则重新选择");
}
if ((selectedValues.contains(manSingleValue) || selectedValues.contains(manDoubleValue))
&& !"男".equals(user.getSex())) {
throw new BaseException("男子单打和男子双打只能选择男性报名人员,请按规则重新选择");
}
if ((selectedValues.contains(womanSingleValue) || selectedValues.contains(womanDoubleValue))
&& !"女".equals(user.getSex())) {
throw new BaseException("女子单打和女子双打只能选择女性报名人员,请按规则重新选择");
}
if (selectedValues.contains(mixedDoubleValue)) {
if ("男".equals(user.getSex())) {
mixedMaleCount++;
} else if ("女".equals(user.getSex())) {
mixedFemaleCount++;
} else {
throw new BaseException("混合双打报名人员必须选择性别,请按规则重新选择");
}
}
}
if (mixedMaleCount != mixedFemaleCount) {
String currentSelection = "当前已选择" + mixedMaleCount + "名男选手、" + mixedFemaleCount + "名女选手";
if (mixedMaleCount == 2 && mixedFemaleCount == 0) {
currentSelection = "当前两名选手均为男选手,未选择女选手";
} else if (mixedMaleCount == 0 && mixedFemaleCount == 2) {
currentSelection = "当前两名选手均为女选手,未选择男选手";
}
// 差额按当前全部混双报名人数计算,明确未能配对的性别和人数。
String missingSex = mixedMaleCount > mixedFemaleCount ? "女" : "男";
int missingCount = Math.abs(mixedMaleCount - mixedFemaleCount);
throw new BaseException("混合双打:" + currentSelection + ",按现有人数配对还缺少"
+ missingCount + "名" + missingSex + "选手。请调整报名选择,确保每对为一男一女");
}
validateProjectTypeRegistrationLimits(activityId, users, options);
}
/**
* 将历史项目类型编码替换为按性别拆分后的字典编码。
*
* @param selectedValues 当前人员已选择的项目类型编码集合
* @param legacyValue 历史编码,当前支持Single或Double
* @param targetValue 根据人员性别确定的新字典编码
*/
private void replaceLegacyProjectType(Set<String> selectedValues, String legacyValue, String targetValue) {
if (selectedValues.remove(legacyValue) && Strings.isNotBlank(targetValue)) {
selectedValues.add(targetValue);
}
}
/**
* 校验唯一项目各类型子项的单位报名人数上限。
* 调用方传入同一活动、同一单位的最终报名人员集合;每名人员对其选中的每个子项各计一人。
* 规则人数为0表示该子项不允许报名,子项人数之间互不累加,也不与项目总报名人数比较。
*/
private void validateProjectTypeRegistrationLimits(Long activityId, List<ActUser> users,
List<NutMap> options) {
List<ActivityItem> items = dao().query(ActivityItem.class,
Cnd.where(ActivityItem::getActivityId, "=", activityId));
if (items.size() != 1 || Strings.isBlank(items.get(0).getRegistrationRule())) {
return;
}
List<NutMap> registrationRules = parseRegistrationRules(items.get(0).getRegistrationRule());
if (registrationRules == null || registrationRules.isEmpty()) {
return;
}
Map<String, Integer> selectedCounts = countProjectTypes(users, options);
Map<String, String> optionNames = options.stream().collect(Collectors.toMap(
option -> option.getString("value"),
option -> option.getString("label"),
(left, right) -> left,
LinkedHashMap::new));
for (NutMap rule : registrationRules) {
String typeCode = rule.getString("typeCode");
int personCount = parseRegistrationRulePersonCount(rule);
int selectedCount = selectedCounts.getOrDefault(typeCode, 0);
if (selectedCount > personCount) {
String typeName = Strings.sBlank(optionNames.get(typeCode), typeCode);
throw new BaseException("“" + typeName + "”每个单位最多报名" + personCount
+ "人,请按规则重新选择");
}
}
}
/**
* 解析项目管理保存的报名规则JSON。
*
* @param registrationRuleJson JSON数组,每项包含typeCode和非负整数personCount
* @return 报名规则列表;格式错误时抛出可直接展示给管理员的业务异常
*/
private List<NutMap> parseRegistrationRules(String registrationRuleJson) {
try {
List<NutMap> rules = Json.fromJsonAsList(NutMap.class, registrationRuleJson);
return rules == null ? Collections.emptyList() : rules;
} catch (RuntimeException e) {
throw new BaseException("项目报名规则配置不正确,请联系管理员");
}
}
/**
* 读取并校验一条项目类型规则的人数上限。
*/
private int parseRegistrationRulePersonCount(NutMap rule) {
String typeCode = rule.getString("typeCode");
Object personCountValue = rule.get("personCount");
Integer personCount = null;
if (personCountValue instanceof Number number
&& Double.isFinite(number.doubleValue())
&& number.doubleValue() == Math.rint(number.doubleValue())) {
personCount = number.intValue();
}
if (Strings.isBlank(typeCode) || personCount == null || personCount < 0) {
throw new BaseException("项目报名规则配置不正确,请联系管理员");
}
return personCount;
}
/**
* 按项目类型统计人员数;每名人员在每个子项最多计一次,并兼容历史Single、Double编码。
*/
private Map<String, Integer> countProjectTypes(List<ActUser> users, List<NutMap> options) {
Map<String, Integer> counts = new LinkedHashMap<>();
String manSingleValue = findOptionValue(options, "男子单打");
String womanSingleValue = findOptionValue(options, "女子单打");
String manDoubleValue = findOptionValue(options, "男子双打");
String womanDoubleValue = findOptionValue(options, "女子双打");
for (ActUser user : users) {
Set<String> selectedValues = new LinkedHashSet<>(splitMultiValue(user.getProjectType()));
if ("男".equals(user.getSex())) {
replaceLegacyProjectType(selectedValues, "Single", manSingleValue);
replaceLegacyProjectType(selectedValues, "Double", manDoubleValue);
} else if ("女".equals(user.getSex())) {
replaceLegacyProjectType(selectedValues, "Single", womanSingleValue);
replaceLegacyProjectType(selectedValues, "Double", womanDoubleValue);
}
for (String typeCode : selectedValues) {
counts.merge(typeCode, 1, Integer::sum);
}
}
return counts;
}
/**
* 报名项目类型只能由唯一活动项目决定:activity_item.projectType是父级字典编码,
* 移动端实际选项取该编码的启用子项。其他项目数量均禁止配置报名项目类型。
*/
private ProjectTypeContext getProjectTypeContext(Long activityId) {
List<ActivityItem> items = dao().query(ActivityItem.class,
Cnd.where(ActivityItem::getActivityId, "=", activityId));
if (items.isEmpty()) {
return new ProjectTypeContext(false, "", Collections.emptyList(),
"当前活动尚未配置项目,项目类型禁止选择,请先完成项目管理。");
}
if (items.size() > 1) {
return new ProjectTypeContext(false, "", Collections.emptyList(),
"当前活动包含多个项目,项目类型禁止选择;请通过项目报名区分参赛项目。");
}
ActivityItem item = items.get(0);
if (Strings.isBlank(item.getProjectType())) {
return new ProjectTypeContext(false, "", Collections.emptyList(),
"当前活动只有一个项目,但尚未设置项目类型,项目类型禁止选择,请先完成项目管理。");
}
List<NutMap> options = getEnabledDictOptions(item.getProjectType());
if (options.isEmpty()) {
return new ProjectTypeContext(false, item.getProjectType(), options,
"当前唯一项目“" + Strings.sNull(item.getName()) + "”的项目类型没有可用下级字典,项目类型禁止选择。");
}
return new ProjectTypeContext(true, item.getProjectType(), options,
"当前活动只有一个项目,报名项目类型来自项目“" + Strings.sNull(item.getName())
+ "”所选类型的下级数据字典。");
}
/**
* 唯一项目与报名项目类型字典之间的解析结果,供管理端展示、移动端选项和提交校验共同使用。
*/
private static class ProjectTypeContext {
private final boolean configurable;
private final String dictCode;
private final List<NutMap> options;
private final String message;
private ProjectTypeContext(boolean configurable, String dictCode, List<NutMap> options, String message) {
this.configurable = configurable;
this.dictCode = dictCode;
this.options = options;
this.message = message;
}
}
private void addParentField(Map<String, ActRegistrationField> selectedMap,
RegistrationFieldDefinition definition) {
if (definition == null || Strings.isBlank(definition.getParentField())) {
return;
}
RegistrationFieldDefinition parent = RegistrationFieldDefinition.fromKey(definition.getParentField());
if (parent != null && !selectedMap.containsKey(parent.getFieldKey())) {
selectedMap.put(parent.getFieldKey(), buildDefaultField(parent));
addParentField(selectedMap, parent);
}
}
private ActRegistrationField buildDefaultField(RegistrationFieldDefinition definition) {
ActRegistrationField field = new ActRegistrationField();
field.setFieldKey(definition.getFieldKey());
field.setRequired(definition.isDefaultRequired());
field.setSort(switch (definition) {
case UNIT_NAME -> 5;
case ID_CARD -> 6;
case PHONE -> 7;
case POSITION -> 8;
default -> definition.ordinal() >= RegistrationFieldDefinition.BIRTHDAY.ordinal()
? definition.ordinal() + 2 : definition.ordinal() + 1;
});
return field;
}
private List<NutMap> getEnabledDictOptions(String dictCode) {
return sysDictService.getSubListByCode(dictCode).stream()
.filter(item -> !item.isDisabled())
.sorted(Comparator.comparing(Sys_dict::getLocation))
.map(item -> NutMap.NEW().addv("label", item.getName()).addv("value", item.getCode()))
.toList();
}
private List<String> splitMultiValue(String value) {
if (Strings.isBlank(value)) {
return List.of();
}
return java.util.Arrays.stream(value.split(","))
.map(String::trim)
.filter(Strings::isNotBlank)
.distinct()
.toList();
}
private String findOptionValue(List<NutMap> options, String label) {
return options.stream()
.filter(item -> label.equals(item.getString("label")))
.map(item -> item.getString("value"))
.findFirst()
.orElse(null);
}
}
@@ -129,7 +129,7 @@ public class SysUserController {
try { try {
Sys_user user = sysUserService.fetch(id); Sys_user user = sysUserService.fetch(id);
String salt = R.UU32(); String salt = R.UU32();
String pwd = R.captchaNumber(6); String pwd = "wdxxmm@123";
String hashedPasswordBase64 = PwdUtil.getPassword(pwd, salt); String hashedPasswordBase64 = PwdUtil.getPassword(pwd, salt);
sysUserService.update(Chain.make("salt", salt).add("password", hashedPasswordBase64), Cnd.where("id", "=", id)); sysUserService.update(Chain.make("salt", salt).add("password", hashedPasswordBase64), Cnd.where("id", "=", id));
sysUserService.deleteCache(user.getId()); sysUserService.deleteCache(user.getId());
@@ -261,6 +261,9 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
if (user.isDisabled()) { if (user.isDisabled()) {
throw new BaseException("用户被禁用"); throw new BaseException("用户被禁用");
} }
if (Strings.isBlank(passowrd)) {
throw new BaseException("请输入密码");
}
String hashedPassword = PwdUtil.getPassword(passowrd, user.getSalt()); String hashedPassword = PwdUtil.getPassword(passowrd, user.getSalt());
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) { if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
throw new BaseException("密码不正确"); throw new BaseException("密码不正确");
@@ -1,6 +1,7 @@
package com.budwk.app.web.controllers.open; package com.budwk.app.web.controllers.open;
import com.budwk.app.base.result.Result; import com.budwk.app.base.result.Result;
import com.budwk.app.module.activity.service.ActivityManageService;
import com.budwk.app.sys.models.Sys_unit; import com.budwk.app.sys.models.Sys_unit;
import org.nutz.dao.Cnd; import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
@@ -19,10 +20,24 @@ public class OpenCommonController {
@Inject @Inject
private Dao dao; private Dao dao;
@Inject
private ActivityManageService activityManageService;
@At @At
public Result units() { public Result units() {
List<Sys_unit> units = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getIsEnable, "=", 1).asc(Sys_unit::getId)); List<Sys_unit> units = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getIsEnable, "=", 1).asc(Sys_unit::getId));
return Result.success(units); return Result.success(units);
} }
/**
* 获取客户端登录页显示的当前最新开启活动单位信息。
*
* @return JSON 结果;data.title 为活动标题,data.hostUnit 为主办单位,
* data.coHostUnit 为承办单位
*/
@At
public Result latestActivityOrganizer() {
return Result.success(activityManageService.getLatestOpenActivityOrganizer());
}
} }
@@ -135,10 +135,17 @@ public class UploadController {
} }
//下载文件 /**
* 下载或在线预览上传文件。
*
* @param filepath 数据库保存的相对文件路径,例如 /upload/image/20260905/example.png
* @param inline 传 true 时以内联方式返回,供图片等资源在浏览器中直接预览;其他值按附件下载
* @param response Servlet 响应对象,文件内容通过响应流输出
* @throws IOException 文件读取或响应输出失败时抛出
*/
@At @At
@Ok("void") @Ok("void")
@SaCheckAnyLogin // @SaCheckAnyLogin
public void download(String filepath, String inline, HttpServletResponse response) throws IOException { public void download(String filepath, String inline, HttpServletResponse response) throws IOException {
//校验文件路径是否合法 //校验文件路径是否合法
boolean isValid = filepath != null && !filepath.contains("../") && !filepath.contains("..\\"); boolean isValid = filepath != null && !filepath.contains("../") && !filepath.contains("..\\");
@@ -146,7 +153,9 @@ public class UploadController {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST); response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("Invalid file path"); response.getWriter().write("Invalid file path");
} }
String fullPath = "C:/files" + filepath; // 下载与上传必须使用同一静态文件根目录,避免开发环境盘符不同时无法回显已上传文件。
String staticPath = conf.get("jetty.staticPath", "/files");
String fullPath = staticPath + filepath;
File file = new File(fullPath); File file = new File(fullPath);
if (!file.exists() || !file.isFile()) { if (!file.exists() || !file.isFile()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND); response.setStatus(HttpServletResponse.SC_NOT_FOUND);
+2 -2
View File
@@ -23,7 +23,7 @@ jetty:
#结合ftp使用,或用nginx代理ftp路径 #结合ftp使用,或用nginx代理ftp路径
#staticPath: E:/project/NutzWk/upload #staticPath: E:/project/NutzWk/upload
#开发模式静态资源 #开发模式静态资源
staticPathLocal: C:/dev/projects/hmc_gbhd/src/main/resources/static staticPathLocal: G:/projects/hmc_gbhd/src/main/resources/static
security: security:
tokenName: token tokenName: token
@@ -54,7 +54,7 @@ redis:
jdbc: jdbc:
url: jdbc:mysql://127.0.0.1:3306/hmc_gbhd?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true url: jdbc:mysql://127.0.0.1:3306/hmc_gbhd?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
username: root username: root
password: dd3s@#2025 password: wqnmd2331
validationQuery: select 1 validationQuery: select 1
maxActive: 150 maxActive: 150
testWhileIdle: true testWhileIdle: true
@@ -14,7 +14,7 @@
> >
<i slot="default" class="el-icon-plus"></i> <i slot="default" class="el-icon-plus"></i>
<div slot="file" slot-scope="{file}"> <div slot="file" slot-scope="{file}">
<img class="el-upload-list__item-thumbnail" :src="'/open/file/upload/download?filepath=' + file.url" alt="" <img class="el-upload-list__item-thumbnail" :src="'/open/file/upload/download?inline=true&filepath=' + file.url" alt=""
style="var(--box-width, '100px') !important;height: var(--box-height, '100px') !important"> style="var(--box-width, '100px') !important;height: var(--box-height, '100px') !important">
<span class="el-upload-list__item-actions"> <span class="el-upload-list__item-actions">
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)"> <span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
@@ -27,7 +27,7 @@
</div> </div>
</el-upload> </el-upload>
<el-dialog :visible.sync="dialogVisible" append-to-body> <el-dialog :visible.sync="dialogVisible" append-to-body>
<img width="100%" :src="'/open/file/upload/download?filepath=' + dialogImageUrl" alt=""> <img width="100%" :src="'/open/file/upload/download?inline=true&filepath=' + dialogImageUrl" alt="">
</el-dialog> </el-dialog>
</div> </div>
</template> </template>
@@ -66,8 +66,8 @@ module.exports = {
console.log(v) console.log(v)
if (v.code === 0) { if (v.code === 0) {
console.log(v.data) console.log(v.data)
insertImgFn('https://3skj.ltd/api/open/file/upload/download?filepath='+v.data) // insertImgFn('https://3skj.ltd/api/open/file/upload/download?filepath='+v.data)
// insertImgFn(v.data) insertImgFn('/open/file/upload/download?filepath='+v.data)
} else { } else {
this.$message.error(v.msg) this.$message.error(v.msg)
} }
@@ -1646,8 +1646,14 @@
computed: { computed: {
sortedTeams() { sortedTeams() {
if (!this.teamScores) return [] if (!this.teamScores) return []
// 根据总分排序 return [...this.teamScores].sort((a, b) => {
return [...this.teamScores].sort((a, b) => this.calculateTotalScore(b) - this.calculateTotalScore(a)) const scoreA = this.calculateTotalScore(a);
const scoreB = this.calculateTotalScore(b);
if (scoreB !== scoreA) {
return scoreB - scoreA; // 总分降序
}
return a.groupId - b.groupId; // 总分相同按 groupId 升序
});
} }
}, },
created() { created() {
@@ -62,6 +62,7 @@ layout("/layouts/platform_1.html"){
<el-card shadow="never" class="mt-3"> <el-card shadow="never" class="mt-3">
<el-row type="flex" style="justify-content: flex-start" class="mb-3"> <el-row type="flex" style="justify-content: flex-start" class="mb-3">
<el-button type="primary" size="small" @click="openGrouping">开始分组</el-button> <el-button type="primary" size="small" @click="openGrouping">开始分组</el-button>
<el-button type="primary" size="small" :loading="unitGrouping" @click="groupByUnit">按单位分组</el-button>
<!-- <div v-if="unGroupUser && unGroupUser.length > 0" style="margin-left: 10px; display: inline-block;">--> <!-- <div v-if="unGroupUser && unGroupUser.length > 0" style="margin-left: 10px; display: inline-block;">-->
<!-- <el-button type="warning" size="small" style="position: relative;" @click="doUnGroupUserSetting">--> <!-- <el-button type="warning" size="small" style="position: relative;" @click="doUnGroupUserSetting">-->
<!-- 未分组人员--> <!-- 未分组人员-->
@@ -73,6 +74,10 @@ layout("/layouts/platform_1.html"){
手动分组 手动分组
</el-button> </el-button>
</div> </div>
<el-button type="primary" size="small" @click="releaseGroupResult" style="margin-left: 10px">发布分组结果</el-button>
<el-button type="warning" size="small" @click="unReleaseGroupResult" style="margin-left: 10px">取消分组结果</el-button>
<el-button type="primary" size="small" @click="doExportGroup" style="margin-left: 10px">导出分组结果</el-button>
</el-row> </el-row>
<el-table v-loading="tableLoading" show-overflow-tooltip :data="tableData"> <el-table v-loading="tableLoading" show-overflow-tooltip :data="tableData">
<el-table-column label="序号" type="index" header-align="center" align="center" width="50"></el-table-column> <el-table-column label="序号" type="index" header-align="center" align="center" width="50"></el-table-column>
@@ -91,6 +96,12 @@ layout("/layouts/platform_1.html"){
<el-tag v-if="row.leaderPhone">{{row.leaderPhone}}</el-tag> <el-tag v-if="row.leaderPhone">{{row.leaderPhone}}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="是否发布" prop="isOpen">
<template #default="{row}">
<el-tag v-if="row.isOpen" type="success">已发布</el-tag>
<el-tag v-else type="danger">未发布</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200"> <el-table-column label="操作" fixed="right" width="200">
<template #default="scope"> <template #default="scope">
<el-button type="text" @click="openView(scope.row)">查看</el-button> <el-button type="text" @click="openView(scope.row)">查看</el-button>
@@ -129,16 +140,59 @@ layout("/layouts/platform_1.html"){
year: new Date().getFullYear() + "" year: new Date().getFullYear() + ""
}, },
activityList: [], activityList: [],
unGroupUser: [] unGroupUser: [],
unitGrouping: false
} }
}, },
methods: { methods: {
openView(row){ openView(row){
this.$refs.viewInfoRef.onOpen(this.pageForm.activityId, row.id) this.$refs.viewInfoRef.onOpen(this.pageForm.activityId, row.id)
}, },
doExportGroup() {
window.open("/platform/activity/group/doExportGroup?activityId=" + this.pageForm.activityId)
},
// openAdjustGroup(row){ // openAdjustGroup(row){
// //
// }, // },
/**
* 发布分组结果
*/
releaseGroupResult() {
this.$confirm('确定要发布分组结果吗?', '提示', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(async ()=> {
const resp = await $.post('/platform/activity/group/releaseGroupResult', {
activityId: this.pageForm.activityId
})
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.pageData()
} else {
this.$message.error(resp.msg)
}
})
},
unReleaseGroupResult() {
this.$confirm('确定要取消发布分组结果吗?', '提示', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(async ()=> {
const resp = await $.post('/platform/activity/group/unReleaseGroupResult', {
activityId: this.pageForm.activityId
})
if (resp.code === 0) {
this.$message.success(resp.msg)
await this.pageData()
} else {
this.$message.error(resp.msg)
}
})
},
async doRemove(row) { async doRemove(row) {
this.$confirm('此操作将会删除与该小队相关的所有信息,确定要删除吗?', '提示', { this.$confirm('此操作将会删除与该小队相关的所有信息,确定要删除吗?', '提示', {
type: 'warning', type: 'warning',
@@ -171,6 +225,36 @@ layout("/layouts/platform_1.html"){
}) })
}) })
}, },
// 按单位重建会清空旧分组及得分,操作前明确提示并防止重复提交。
async groupByUnit() {
if (this.unitGrouping) return
if (!this.pageForm.activityId) {
this.$message.error("请选择活动")
return
}
const activityId = this.pageForm.activityId
try {
await this.$confirm('将全部有效报名人员按单位分组,同一单位生成一个组。此操作会清空该活动现有分组及项目得分,是否继续?', '按单位分组', {
type: 'warning', confirmButtonText: '确定分组', cancelButtonText: '取消'
})
} catch (error) {
return
}
this.unitGrouping = true
try {
const resp = await $.post('/platform/activity/group/groupByUnit', {activityId})
if (resp.code === 0) {
this.$message.success('按单位分组成功')
await this.successGroup()
} else {
this.$message.error(resp.msg || '按单位分组失败')
}
} catch (error) {
this.$message.error('按单位分组失败')
} finally {
this.unitGrouping = false
}
},
// 开始分组 // 开始分组
openGrouping(){ openGrouping(){
if (!this.pageForm.activityId) { if (!this.pageForm.activityId) {
@@ -6,6 +6,8 @@ const VIEW_INFO = {
width="50"></el-table-column> width="50"></el-table-column>
<el-table-column label="姓名" prop="userName"></el-table-column> <el-table-column label="姓名" prop="userName"></el-table-column>
<el-table-column label="性别" prop="sex"></el-table-column> <el-table-column label="性别" prop="sex"></el-table-column>
<el-table-column label="年龄" prop="age"></el-table-column>
<el-table-column label="体重" prop="weight"></el-table-column>
<el-table-column label="身份" prop="isLeader"> <el-table-column label="身份" prop="isLeader">
<template #default="{row}"> <template #default="{row}">
<el-tag size="small" v-if="row.isLeader">{{ '队长' }}</el-tag> <el-tag size="small" v-if="row.isLeader">{{ '队长' }}</el-tag>
@@ -2,6 +2,25 @@
layout("/layouts/platform_1.html"){ layout("/layouts/platform_1.html"){
#--> #-->
<style>
.registration-field-setting {
height: calc(100vh - 80px);
padding: 0 20px 70px;
overflow-y: auto;
box-sizing: border-box;
}
.registration-field-actions {
position: absolute;
right: 0;
bottom: 0;
left: 0;
padding: 14px 20px;
text-align: right;
background: #fff;
border-top: 1px solid #ebeef5;
}
</style>
<div id="app"> <div id="app">
<el-card shadow="never"> <el-card shadow="never">
<div class="search"> <div class="search">
@@ -35,11 +54,22 @@ layout("/layouts/platform_1.html"){
<el-table-column label="报名结束时间" prop="signEndTime"></el-table-column> <el-table-column label="报名结束时间" prop="signEndTime"></el-table-column>
<el-table-column label="活动开始时间" prop="startTime"></el-table-column> <el-table-column label="活动开始时间" prop="startTime"></el-table-column>
<el-table-column label="活动结束时间" prop="endTime"></el-table-column> <el-table-column label="活动结束时间" prop="endTime"></el-table-column>
<el-table-column label="操作" fixed="right" width="250"> <el-table-column label="是否开启" prop="isOpen" header-align="center" align="center" width="100">
<template #default="scope">
<el-switch
v-model="scope.row.isOpen"
active-color="#13ce66"
inactive-color="#ff4949"
@change="(val) => {onOpenStatusChange(val, scope.row)}"
></el-switch>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="350">
<template #default="scope"> <template #default="scope">
<el-button type="text" @click="$refs.actQrCodeRef.onOpen(scope.row.id)">二维码</el-button> <el-button type="text" @click="$refs.actQrCodeRef.onOpen(scope.row.id)">二维码</el-button>
<el-button type="text" @click="$refs.addOrUpdateRef.onOpen(scope.row.id)">修改</el-button> <el-button type="text" @click="$refs.addOrUpdateRef.onOpen(scope.row.id)">修改</el-button>
<el-button type="text" @click="$refs.itemFormRef.onOpen(scope.row.id)">项目管理</el-button> <el-button type="text" @click="$refs.itemFormRef.onOpen(scope.row.id)">项目管理</el-button>
<el-button type="text" @click="$refs.registrationFieldSettingRef.onOpen(scope.row.id)">设置报名信息</el-button>
<el-button type="text" class="text-danger" @click="onTableRowDelete(scope.row.id)">删除</el-button> <el-button type="text" class="text-danger" @click="onTableRowDelete(scope.row.id)">删除</el-button>
</template> </template>
</el-table-column> </el-table-column>
@@ -48,12 +78,14 @@ layout("/layouts/platform_1.html"){
</el-card> </el-card>
<add-or-update ref="addOrUpdateRef" @refresh="doSearch"></add-or-update> <add-or-update ref="addOrUpdateRef" @refresh="doSearch"></add-or-update>
<item-form ref="itemFormRef"></item-form> <item-form ref="itemFormRef"></item-form>
<registration-field-setting ref="registrationFieldSettingRef"></registration-field-setting>
<act-qr-code ref="actQrCodeRef"></act-qr-code> <act-qr-code ref="actQrCodeRef"></act-qr-code>
</div> </div>
<script> <script>
<!--#include('addOrUpdate.js'){}#--> <!--#include('addOrUpdate.js'){}#-->
<!--#include('itemForm.js'){}#--> <!--#include('itemForm.js'){}#-->
<!--#include('registrationFieldSetting.js'){}#-->
<!--#include('actQrCode.js'){}#--> <!--#include('actQrCode.js'){}#-->
new Vue({ new Vue({
el: "#app", el: "#app",
@@ -61,6 +93,7 @@ layout("/layouts/platform_1.html"){
components: { components: {
"add-or-update": addOrUpdate, "add-or-update": addOrUpdate,
"item-form": itemForm, "item-form": itemForm,
"registration-field-setting": registrationFieldSetting,
"act-qr-code": actQrCode "act-qr-code": actQrCode
}, },
data() { data() {
@@ -71,6 +104,28 @@ layout("/layouts/platform_1.html"){
}, },
created() { created() {
this.doSearch() this.doSearch()
},
methods: {
// isOpen=true 时活动在独立 Vue 客户端显示,false 时仅从客户端活动列表隐藏。
onOpenStatusChange(val, row) {
this.tableLoading = true
$.post('/platform/activity/manage/updateOpenStatus', {
id: row.id,
isOpen: val
}).then((res) => {
if (res.code === 0) {
this.$message.success('状态修改成功')
} else {
this.$set(row, 'isOpen', !val)
this.$message.error(res.msg)
}
}, () => {
this.$set(row, 'isOpen', !val)
this.$message.error('状态修改失败')
}).always(() => {
this.tableLoading = false
})
}
} }
}) })
</script> </script>
@@ -23,6 +23,21 @@ const itemForm = {
<el-input v-model="item.name" placeholder="请输入项目名称" maxlength="50" <el-input v-model="item.name" placeholder="请输入项目名称" maxlength="50"
show-word-limit></el-input> show-word-limit></el-input>
</el-form-item> </el-form-item>
<el-form-item
:prop="'tableData.' + index + '.projectType'"
:rules="[
{ required: true, message: '请选择项目类型', trigger: 'change' }
]"
label="项目类型"
label-width="150px"
>
<el-select v-model="item.projectType" placeholder="请选择项目类型" clearable filterable
@change="onProjectTypeChange(item)"
:loading="projectTypeLoading" style="width: 100%">
<el-option v-for="option in projectTypeOptions" :key="option.code"
:label="option.name" :value="option.code"></el-option>
</el-select>
</el-form-item>
<el-form-item label="是否团队合作" label-width="150px"> <el-form-item label="是否团队合作" label-width="150px">
<el-radio-group v-model="item.isTeamApply" size="medium" @change="item.num=0"> <el-radio-group v-model="item.isTeamApply" size="medium" @change="item.num=0">
<el-radio-button :label="true">是</el-radio-button> <el-radio-button :label="true">是</el-radio-button>
@@ -83,6 +98,36 @@ const itemForm = {
maxlength="500" show-word-limit></el-input> maxlength="500" show-word-limit></el-input>
</el-form-item> </el-form-item>
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="报名规则">
<el-alert
title="各子项报名人数独立计算,人数合计可以超过本项目报名人数;人数设置为0表示该子项不允许报名。"
type="info"
:closable="false"
show-icon>
</el-alert>
<div v-if="!item.projectType" class="registration-rule-empty">
请先选择项目类型
</div>
<el-table v-else v-loading="item.registrationRuleLoading"
:data="item.registrationRules" class="mt-3" border>
<el-table-column label="项目类型子项" prop="typeName"></el-table-column>
<el-table-column label="报名人数" width="260" align="center">
<template #default="ruleScope">
<el-form-item
:prop="'tableData.' + index + '.registrationRules.' + ruleScope.$index + '.personCount'"
:rules="[
{ required: true, message: '请设置报名人数', trigger: 'change' },
{ type: 'number', min: 0, message: '报名人数不能小于0', trigger: 'change' }
]"
class="registration-rule-form-item">
<el-input-number v-model="ruleScope.row.personCount" :min="0" :max="9999"
:precision="0"
controls-position="right"></el-input-number>
</el-form-item>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="项目图片"> <el-tab-pane label="项目图片">
<el-form-item> <el-form-item>
<image-upload v-model="item.img" :maxCount="1"></image-upload> <image-upload v-model="item.img" :maxCount="1"></image-upload>
@@ -115,6 +160,8 @@ const itemForm = {
return { return {
visible: false, visible: false,
submitting: false, submitting: false,
projectTypeLoading: false,
projectTypeOptions: [],
form: { form: {
tableData: [] tableData: []
}, },
@@ -126,8 +173,28 @@ const itemForm = {
onOpen(id) { onOpen(id) {
this.visible = true this.visible = true
this.resetForm() this.resetForm()
this.fetchProjectTypeOptions()
this.fetchItems(id) this.fetchItems(id)
}, },
// 加载project_type字典:下拉框显示名称,项目数据中保存字典编码。
fetchProjectTypeOptions() {
this.projectTypeLoading = true
$.post("/platform/sys/dict/dictOptions", {code: "project_type"})
.then((res) => {
if (res.code === 0) {
this.projectTypeOptions = (res.data || []).filter((item) => !item.disabled)
} else {
this.projectTypeOptions = []
this.$message.error(res.msg || "加载项目类型失败")
}
}, () => {
this.projectTypeOptions = []
this.$message.error("加载项目类型失败")
})
.always(() => {
this.projectTypeLoading = false
})
},
fetchItems(id) { fetchItems(id) {
$.post("/platform/activity/manage/getItems", { id }).then((res) => { $.post("/platform/activity/manage/getItems", { id }).then((res) => {
if (res.code === 0) { if (res.code === 0) {
@@ -136,6 +203,9 @@ const itemForm = {
// 确保每个项目都有必要的初始属性 // 确保每个项目都有必要的初始属性
const formattedData = data.map(item => ({ const formattedData = data.map(item => ({
...item, ...item,
projectType: item.projectType || "",
registrationRules: this.parseRegistrationRule(item.registrationRule),
registrationRuleLoading: false,
isTeamApply: item.isTeamApply === undefined ? false : item.isTeamApply, isTeamApply: item.isTeamApply === undefined ? false : item.isTeamApply,
num: item.num || 0, num: item.num || 0,
form: item.form || "", form: item.form || "",
@@ -144,7 +214,10 @@ const itemForm = {
img: item.img || "" img: item.img || ""
})) }))
this.$set(this.form, "tableData", formattedData) this.$set(this.form, "tableData", formattedData)
this.originalData = JSON.parse(JSON.stringify(formattedData)) // 保存原始数据的副本 const ruleRequests = formattedData.map((item) => this.loadRegistrationRules(item))
Promise.all(ruleRequests).then(() => {
this.originalData = JSON.parse(JSON.stringify(formattedData))
})
} else { } else {
this.$message.error(res.msg || "加载项目数据失败") this.$message.error(res.msg || "加载项目数据失败")
} }
@@ -160,6 +233,10 @@ const itemForm = {
} }
this.form.tableData.push({ this.form.tableData.push({
name: "", name: "",
projectType: "",
registrationRule: "",
registrationRules: [],
registrationRuleLoading: false,
isTeamApply: false, isTeamApply: false,
num: 1, num: 1,
form: "", form: "",
@@ -175,6 +252,56 @@ const itemForm = {
} }
}) })
}, },
// 项目类型变化后按新父级字典重新加载子项;编码相同的既有规则继续保留。
onProjectTypeChange(item) {
this.loadRegistrationRules(item)
},
parseRegistrationRule(value) {
if (!value) {
return []
}
try {
const rules = JSON.parse(value)
return Array.isArray(rules) ? rules : []
} catch (error) {
console.error(error)
return []
}
},
// activity_item.projectType是父级字典编码,报名规则逐项绑定其启用子项。
loadRegistrationRules(item) {
if (!item.projectType) {
this.$set(item, "registrationRules", [])
return Promise.resolve()
}
this.$set(item, "registrationRuleLoading", true)
return $.post("/platform/sys/dict/dictOptions", {code: item.projectType})
.then((res) => {
if (res.code === 0) {
const oldRuleMap = {}
;(item.registrationRules || []).forEach((rule) => {
oldRuleMap[rule.typeCode] = rule
})
const rules = (res.data || []).filter((option) => !option.disabled).map((option) => ({
typeCode: option.code,
typeName: option.name,
personCount: oldRuleMap[option.code] === undefined
? null
: oldRuleMap[option.code].personCount
}))
this.$set(item, "registrationRules", rules)
} else {
this.$set(item, "registrationRules", [])
this.$message.error(res.msg || "加载报名规则失败")
}
}, () => {
this.$set(item, "registrationRules", [])
this.$message.error("加载报名规则失败")
})
.always(() => {
this.$set(item, "registrationRuleLoading", false)
})
},
removeRow(index) { removeRow(index) {
this.$confirm("确定要删除此项目吗?", "提示", { this.$confirm("确定要删除此项目吗?", "提示", {
confirmButtonText: "确定", confirmButtonText: "确定",
@@ -205,11 +332,23 @@ const itemForm = {
return return
} }
// 仅提交稳定的子项编码和人数;字典名称在回显时实时读取。
const submitItems = this.form.tableData.map((item) => {
const submitItem = {
...item,
registrationRule: JSON.stringify((item.registrationRules || []).map((rule) => ({
typeCode: rule.typeCode,
personCount: rule.personCount
})))
}
delete submitItem.registrationRules
delete submitItem.registrationRuleLoading
return submitItem
})
$.post("/platform/activity/manage/updateItem", { $.post("/platform/activity/manage/updateItem", {
id: this.form.id, id: this.form.id,
items: JSON.stringify(this.form.tableData) items: JSON.stringify(submitItems)
}).then((res) => { }).then((res) => {
this.submitting = false
if (res.code === 0) { if (res.code === 0) {
this.$message.success("保存成功") this.$message.success("保存成功")
this.visible = false this.visible = false
@@ -217,10 +356,11 @@ const itemForm = {
} else { } else {
this.$message.error(res.msg || "保存失败") this.$message.error(res.msg || "保存失败")
} }
}).catch(err => { }, (err) => {
this.submitting = false
this.$message.error("网络错误,请稍后重试") this.$message.error("网络错误,请稍后重试")
console.error(err) console.error(err)
}).always(() => {
this.submitting = false
}) })
} else { } else {
this.$message.warning("请完善表单信息") this.$message.warning("请完善表单信息")
@@ -327,5 +467,15 @@ const itemForm = {
.project-card .el-tabs--border-card{ .project-card .el-tabs--border-card{
border: none; border: none;
} }
.registration-rule-empty {
padding: 35px 0;
color: #909399;
text-align: center;
}
.registration-rule-form-item {
margin-bottom: 0;
}
` `
} }
@@ -0,0 +1,241 @@
const registrationFieldSetting = {
template: /*language=HTML*/ `
<el-drawer
title="设置报名信息"
:visible.sync="visible"
size="58%"
:wrapper-closable="false"
:before-close="handleClose">
<div class="registration-field-setting" v-loading="loading">
<el-alert
title="身份证、姓名、联系电话和单位名称为系统固定字段;关联字段会自动保持完整的父子关系。"
type="info"
:closable="false"
show-icon>
</el-alert>
<el-alert
v-if="projectTypeRuleMessage"
:title="projectTypeRuleMessage"
:type="projectTypeConfigurable ? 'success' : 'warning'"
class="mt-3"
:closable="false"
show-icon>
</el-alert>
<el-table :data="fieldList" class="mt-3" border>
<el-table-column label="显示" width="80" align="center">
<template #default="scope">
<el-checkbox
v-model="scope.row.selected"
:disabled="scope.row.locked || scope.row.configurable === false"
@change="(val) => onSelectedChange(scope.row, val)">
</el-checkbox>
</template>
</el-table-column>
<el-table-column label="报名字段" prop="fieldLabel" min-width="130"></el-table-column>
<el-table-column label="字段类型" min-width="120">
<template #default="scope">
{{ componentTypeLabel(scope.row.componentType) }}
</template>
</el-table-column>
<el-table-column label="数据来源" min-width="140">
<template #default="scope">
{{ dataSourceLabel(scope.row) }}
</template>
</el-table-column>
<el-table-column label="必填" width="90" align="center">
<template #default="scope">
<el-switch
v-model="scope.row.required"
:disabled="!scope.row.selected || scope.row.locked || scope.row.fieldKey === 'projectType' || scope.row.configurable === false">
</el-switch>
</template>
</el-table-column>
<el-table-column label="顺序" width="120" align="center">
<template #default="scope">
<el-input-number
v-model="scope.row.sort"
:disabled="!scope.row.selected || scope.row.configurable === false"
:min="0"
:max="99"
controls-position="right"
size="small">
</el-input-number>
</template>
</el-table-column>
<el-table-column label="联动说明" min-width="170">
<template #default="scope">
{{ dependencyLabel(scope.row) }}
</template>
</el-table-column>
</el-table>
<div class="registration-field-actions">
<el-button @click="handleClose">取消</el-button>
<el-button type="primary" :loading="submitting" @click="onSubmit">确定</el-button>
</div>
</div>
</el-drawer>
`,
data() {
return {
visible: false,
loading: false,
submitting: false,
activityId: null,
fieldList: [],
projectTypeConfigurable: false,
projectTypeRuleMessage: "",
originalData: ""
}
},
methods: {
onOpen(activityId) {
this.activityId = activityId
this.visible = true
this.loading = true
$.post("/platform/activity/manage/getRegistrationFields", {id: activityId})
.then((res) => {
if (res.code === 0) {
const fields = res.data || []
const projectTypeField = fields.find((item) => item.fieldKey === "projectType")
this.projectTypeConfigurable = Boolean(projectTypeField && projectTypeField.configurable)
this.projectTypeRuleMessage = projectTypeField ? projectTypeField.ruleMessage : ""
this.$set(this, "fieldList", fields)
this.originalData = JSON.stringify(fields)
} else {
this.$message.error(res.msg || "加载报名字段失败")
}
}, () => {
this.$message.error("加载报名字段失败")
})
.always(() => {
this.loading = false
})
},
// 选中子字段时补齐父字段;取消父字段时同步取消所有依赖字段。
onSelectedChange(row, selected) {
if (row.configurable === false) {
this.$set(row, "selected", false)
return
}
if (selected) {
this.selectParent(row)
return
}
if (this.fieldList.some((item) => item.parentField === row.fieldKey && item.locked)) {
this.$set(row, "selected", true)
this.$message.warning("系统固定字段依赖此字段,不能取消显示")
return
}
this.unselectChildren(row.fieldKey)
},
selectParent(row) {
if (!row.parentField) {
return
}
const parent = this.fieldList.find((item) => item.fieldKey === row.parentField)
if (parent) {
this.$set(parent, "selected", true)
this.selectParent(parent)
}
},
unselectChildren(fieldKey) {
this.fieldList.filter((item) => item.parentField === fieldKey).forEach((item) => {
this.$set(item, "selected", false)
this.$set(item, "required", false)
this.unselectChildren(item.fieldKey)
})
},
onSubmit() {
const selectedFields = this.fieldList.filter((item) => item.selected)
.map((item) => ({
fieldKey: item.fieldKey,
required: item.required,
sort: item.sort
}))
this.$confirm("保存后将影响该活动后续新增报名和重新提交,已有报名数据不会被删除。确定保存吗?", "再次确认", {
confirmButtonText: "确定保存",
cancelButtonText: "取消",
type: "warning"
}).then(() => {
this.submitting = true
$.post("/platform/activity/manage/saveRegistrationFields", {
id: this.activityId,
fields: JSON.stringify(selectedFields)
}).then((res) => {
if (res.code === 0) {
this.$message.success("报名字段保存成功")
this.visible = false
this.reset()
} else {
this.$message.error(res.msg || "报名字段保存失败")
}
}, () => {
this.$message.error("报名字段保存失败")
}).always(() => {
this.submitting = false
})
}).catch(() => {
})
},
handleClose(done) {
const close = () => {
this.visible = false
this.reset()
if (typeof done === "function") {
done()
}
}
if (JSON.stringify(this.fieldList) !== this.originalData) {
this.$confirm("有未保存的修改,确定关闭吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(close).catch(() => {
})
return
}
close()
},
reset() {
this.activityId = null
this.fieldList = []
this.projectTypeConfigurable = false
this.projectTypeRuleMessage = ""
this.originalData = ""
this.loading = false
this.submitting = false
},
componentTypeLabel(componentType) {
const labels = {
input: "文本输入",
number: "数字输入",
tel: "手机号",
radio: "单选",
"boolean-radio": "是/否单选",
"dict-radio": "字典单选",
"dict-checkbox": "字典多选",
date: "日期",
readonly: "自动带出"
}
return labels[componentType] || componentType
},
dataSourceLabel(row) {
if (row.fieldKey === "projectType") {
return row.configurable
? "数据字典:" + row.dictCode + " 的下级"
: "当前项目数量不支持"
}
return row.dictCode ? "数据字典:" + row.dictCode : "固定表单字段"
},
dependencyLabel(row) {
if (row.parentField === "personType") {
return "当人员类型为" + row.showWhenValue.split(",").join("或者") + "时才需要填写"
}
if (!row.parentField) {
return "-"
}
const parent = this.fieldList.find((item) => item.fieldKey === row.parentField)
return parent ? parent.fieldLabel + " = " + row.showWhenValue : "-"
}
}
}
@@ -22,7 +22,7 @@ layout("/layouts/platform_1.html"){
<div class="search-item"> <div class="search-item">
<div class="search-item-label">所属活动</div> <div class="search-item-label">所属活动</div>
<div class="search-item-option"> <div class="search-item-option">
<el-select v-model="pageForm.activityId" placeholder="请选择活动"> <el-select v-model="pageForm.activityId" placeholder="请选择活动" @change="onActivityChange">
<el-option <el-option
v-for="item in activityOptions" v-for="item in activityOptions"
:key="item.id" :key="item.id"
@@ -85,6 +85,16 @@ layout("/layouts/platform_1.html"){
<span>{{scope.$index+(pageForm.pageNumber - 1) * pageForm.pageSize + 1}} </span> <span>{{scope.$index+(pageForm.pageNumber - 1) * pageForm.pageSize + 1}} </span>
</template> </template>
</el-table-column> </el-table-column>
<!-- 报名信息列根据当前活动的字段配置生成。 -->
<el-table-column v-for="field in registrationFields" :key="field.fieldKey"
:label="field.fieldLabel" :prop="field.fieldKey"
:min-width="getRegistrationFieldWidth(field)">
<template slot-scope="{row}">
{{formatRegistrationField(row, field)}}
</template>
</el-table-column>
<!-- 原固定报名字段列仅屏蔽显示,保留代码便于后续恢复或对照。 -->
<template v-if="false">
<el-table-column label="姓名" prop="userName" min-width="100px"></el-table-column> <el-table-column label="姓名" prop="userName" min-width="100px"></el-table-column>
<el-table-column label="性别" prop="sex" min-width="60px"></el-table-column> <el-table-column label="性别" prop="sex" min-width="60px"></el-table-column>
<el-table-column label="职务" prop="position" min-width="100px"></el-table-column> <el-table-column label="职务" prop="position" min-width="100px"></el-table-column>
@@ -115,6 +125,8 @@ layout("/layouts/platform_1.html"){
{{row.isParty ? '是':''}} {{row.isParty ? '是':''}}
</template> </template>
</el-table-column> </el-table-column>
</template>
<el-table-column label="签到时间" prop="signInTime" min-width="120px"></el-table-column>
<!-- <el-table-column label="物品发放" prop="isGrant">--> <!-- <el-table-column label="物品发放" prop="isGrant">-->
<!-- <template slot-scope="scope">--> <!-- <template slot-scope="scope">-->
<!-- <el-tag v-if="scope.row.isGrant" type="success">已发放</el-tag>--> <!-- <el-tag v-if="scope.row.isGrant" type="success">已发放</el-tag>-->
@@ -146,10 +158,64 @@ layout("/layouts/platform_1.html"){
year: new Date().getFullYear() + "" year: new Date().getFullYear() + ""
}, },
activityOptions: [], activityOptions: [],
registrationFields: [],
units: [], units: [],
} }
}, },
methods: { methods: {
// 活动切换后同步刷新动态列和已签到人员列表。
onActivityChange() {
this.loadRegistrationFields(() => {
this.doSearch()
})
},
// 获取当前活动启用的报名字段;字典选项用于把保存的code转换成名称。
loadRegistrationFields(callback) {
if (!this.pageForm.activityId) {
this.registrationFields = []
if (callback) callback()
return
}
$.post('/platform/activity/signInUserQuery/getRegistrationFields', {
activityId: this.pageForm.activityId
}).then((res) => {
if (res.code === 0) {
this.registrationFields = res.data || []
} else {
this.registrationFields = []
this.$message.error(res.msg || '加载报名字段失败')
}
}).fail(() => {
this.registrationFields = []
this.$message.error('加载报名字段失败')
}).always(() => {
if (callback) callback()
})
},
formatRegistrationField(row, field) {
const value = row[field.fieldKey]
if (value === null || value === undefined || value === '') return ''
if (field.dictCode) {
const optionMap = {}
;(field.options || []).forEach((option) => {
optionMap[option.value] = option.label
})
return String(value).split(',').map((code) => {
const dictValue = code.trim()
return optionMap[dictValue] || dictValue
}).join('、')
}
if (field.componentType === 'boolean-radio') {
const checked = value === true || value === 1 || value === '1'
return field.fieldKey === 'isStay' ? (checked ? '住宿' : '不住宿') : (checked ? '是' : '否')
}
return value
},
getRegistrationFieldWidth(field) {
if (field.fieldKey === 'unitName') return 180
if (field.fieldKey === 'phone' || field.componentType === 'dict-checkbox') return 140
return 100
},
async doGrant(row, isGrant) { async doGrant(row, isGrant) {
this.$confirm('您确定要设置成' + (isGrant ? '已发放' : '未发放') + '状态吗?', '提示', { this.$confirm('您确定要设置成' + (isGrant ? '已发放' : '未发放') + '状态吗?', '提示', {
confirmButtonText: '确定', confirmButtonText: '确定',
@@ -180,7 +246,9 @@ layout("/layouts/platform_1.html"){
this.$set(this.pageForm, "activityId", "") this.$set(this.pageForm, "activityId", "")
} }
this.activityOptions = res.data; this.activityOptions = res.data;
this.doSearch() this.loadRegistrationFields(() => {
this.doSearch()
})
} else { } else {
this.$message.error(res.msg); this.$message.error(res.msg);
} }
@@ -21,7 +21,7 @@ layout("/layouts/platform_1.html"){
<div class="search-item"> <div class="search-item">
<div class="search-item-label">所属活动</div> <div class="search-item-label">所属活动</div>
<div class="search-item-option"> <div class="search-item-option">
<el-select @change="doSearch" <el-select @change="onActivityChange"
placeholder="请选择活动查询" placeholder="请选择活动查询"
style="width: 100%;" style="width: 100%;"
v-model="pageForm.activityId"> v-model="pageForm.activityId">
@@ -54,7 +54,7 @@ layout("/layouts/platform_1.html"){
<el-select placeholder="请选择所属单位" <el-select placeholder="请选择所属单位"
style="width: 100%;" clearable style="width: 100%;" clearable
filterable filterable
@change="setModifyFormUnit" :loading="unitLoading"
v-model="pageForm.unitId"> v-model="pageForm.unitId">
<el-option :label="item.name" :value="item.id" <el-option :label="item.name" :value="item.id"
v-for="item in units"></el-option> v-for="item in units"></el-option>
@@ -74,15 +74,27 @@ layout("/layouts/platform_1.html"){
<el-button icon="el-icon-plus" type="primary" size="small" @click="openAddUser">添加报名人员</el-button> <el-button icon="el-icon-plus" type="primary" size="small" @click="openAddUser">添加报名人员</el-button>
<el-button icon="el-icon-download" type="primary" size="small" @click="doExport">导出</el-button> <el-button icon="el-icon-download" type="primary" size="small" @click="doExport">导出</el-button>
</el-row> </el-row>
<el-table v-loading="tableLoading" show-overflow-tooltip :data="tableData"> <el-table v-loading="tableLoading" show-overflow-tooltip :data="tableData" @sort-change="pageOrder">
<el-table-column label="序号" type="index" header-align="center" align="center" <el-table-column label="序号" type="index" header-align="center" align="center"
fixed="left" fixed="left"
:index="indexMethod" :index="indexMethod"
width="50"></el-table-column> width="50"></el-table-column>
<!-- 报名信息列根据当前活动的字段配置生成。 -->
<el-table-column v-for="field in registrationFields" :key="field.fieldKey"
:label="field.fieldLabel" :prop="field.fieldKey"
:sortable="field.fieldKey === 'unitName' ? 'custom' : false"
:min-width="getRegistrationFieldWidth(field)">
<template slot-scope="{row}">
{{formatRegistrationField(row, field)}}
</template>
</el-table-column>
<!-- 原固定报名字段列仅屏蔽显示,保留代码便于后续恢复或对照。 -->
<template v-if="false">
<el-table-column label="姓名" prop="userName" width="100px" fixed="left"></el-table-column> <el-table-column label="姓名" prop="userName" width="100px" fixed="left"></el-table-column>
<el-table-column label="性别" prop="sex" width="60px" fixed="left"></el-table-column> <el-table-column label="性别" prop="sex" width="60px" fixed="left"></el-table-column>
<el-table-column label="体重(kg)" prop="weight" width="100px"></el-table-column>
<el-table-column label="职务" prop="position" width="150px"></el-table-column> <el-table-column label="职务" prop="position" width="150px"></el-table-column>
<el-table-column label="身份证号码" prop="idCard" width="180px"></el-table-column> <!-- <el-table-column label="身份证号码" prop="idCard" width="180px"></el-table-column>-->
<el-table-column label="联系方式" prop="phone" width="130px"></el-table-column> <el-table-column label="联系方式" prop="phone" width="130px"></el-table-column>
<el-table-column label="单位" prop="unitName" width="150px"></el-table-column> <el-table-column label="单位" prop="unitName" width="150px"></el-table-column>
<el-table-column label="报名时间" prop="signUpTime" width="150px"></el-table-column> <el-table-column label="报名时间" prop="signUpTime" width="150px"></el-table-column>
@@ -101,16 +113,29 @@ layout("/layouts/platform_1.html"){
<el-table-column label="自驾车牌号" prop="carNo"></el-table-column> <el-table-column label="自驾车牌号" prop="carNo"></el-table-column>
<el-table-column label="是否接站" prop="isStationDesc"></el-table-column> <el-table-column label="是否接站" prop="isStationDesc"></el-table-column>
<el-table-column label="接站地址" prop="stationAddress"></el-table-column> <el-table-column label="接站地址" prop="stationAddress"></el-table-column>
<!-- <el-table-column label="所需器材" prop="equip">--> <!-- <el-table-column label="所需器材" prop="equip">-->
<!-- <template slot-scope="{row}">--> <!-- <template slot-scope="{row}">-->
<!-- <span>{{row.isShowEquip && row.equip?row.equip:'&#45;&#45;'}}</span>--> <!-- <span>{{row.isShowEquip && row.equip?row.equip:'&#45;&#45;'}}</span>-->
<!-- </template>--> <!-- </template>-->
<!-- </el-table-column>--> <!-- </el-table-column>-->
<el-table-column label="是否参加茶叙会" prop="isParty"> <el-table-column label="是否参加茶叙会" prop="isParty">
<template slot-scope="{row}"> <template slot-scope="{row}">
{{row.isParty ? '是':''}} {{row.isParty ? '是':''}}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="是否联络人" prop="isSIgnUpUser">
<template slot-scope="{row}">
{{row.isSIgnUpUser ? '是':''}}
</template>
</el-table-column>
</template>
<el-table-column label="报名时间" prop="signUpTime" width="150px"></el-table-column>
<el-table-column label="是否签到" prop="isSignIn" width="90px">
<template slot-scope="{row}">
<el-tag v-if="row.isSignIn" size="mini" type="success">已签到</el-tag>
<el-tag v-else size="mini" type="info">未签到</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="200"> <el-table-column label="操作" fixed="right" width="200">
<template #default="scope"> <template #default="scope">
<el-button type="text" @click="openModify(scope.row)">修改</el-button> <el-button type="text" @click="openModify(scope.row)">修改</el-button>
@@ -130,7 +155,8 @@ layout("/layouts/platform_1.html"){
</el-form-item> </el-form-item>
<el-form-item label="参加单位" prop="unitName"> <el-form-item label="参加单位" prop="unitName">
<el-select placeholder="请选择所属单位" <el-select placeholder="请选择所属单位"
style="width: 100%;" clearable style="width: 100%;"
clearable filterable
@change="setFormDataUnit" @change="setFormDataUnit"
v-model="formData.unitId"> v-model="formData.unitId">
<el-option :label="item.name" :value="item.id" <el-option :label="item.name" :value="item.id"
@@ -172,12 +198,15 @@ layout("/layouts/platform_1.html"){
show-word-limit placeholder="请输入联系方式"></el-input> show-word-limit placeholder="请输入联系方式"></el-input>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="身份证件号" prop="idCard" width="250"> <el-form-item label="体重(kg)" prop="weight">
<template slot-scope="{row}"> <el-input-number v-model="modifyForm.weight"></el-input-number>
<el-input v-model="row.idCard" maxlength="18" type="text" </el-form-item>
show-word-limit placeholder="请输入身份证件号"></el-input> <!-- <el-table-column label="身份证件号" prop="idCard" width="250">-->
</template> <!-- <template slot-scope="{row}">-->
</el-table-column> <!-- <el-input v-model="row.idCard" maxlength="18" type="text"-->
<!-- show-word-limit placeholder="请输入身份证件号"></el-input>-->
<!-- </template>-->
<!-- </el-table-column>-->
<el-table-column label="职务" prop="position" width="200"> <el-table-column label="职务" prop="position" width="200">
<template slot-scope="{row}"> <template slot-scope="{row}">
<el-input v-model="row.position" maxlength="100" <el-input v-model="row.position" maxlength="100"
@@ -216,24 +245,26 @@ layout("/layouts/platform_1.html"){
<el-dialog title="修改报名信息" :visible.sync="modifyDialog" width="40%"> <el-dialog title="修改报名信息" :visible.sync="modifyDialog" width="40%">
<el-form :model="modifyForm" :rules="modifyRules" ref="modifyFormRef" label-width="100px"> <el-form :model="modifyForm" :rules="modifyRules" ref="modifyFormRef" label-width="100px">
<el-form-item label="姓名" prop="userName">
<el-input v-model="modifyForm.userName"></el-input> <el-row :gutter="20">
</el-form-item> <el-col :span="12">
<el-form-item label="性别" prop="sex"> <el-form-item label="姓名" prop="userName">
<el-radio-group v-model="modifyForm.sex"> <el-input v-model="modifyForm.userName"></el-input>
<el-radio label="男"></el-radio> </el-form-item>
<el-radio label="女"></el-radio> </el-col>
</el-radio-group> <el-col :span="12">
</el-form-item> <el-form-item label="性别" prop="sex">
<el-radio-group v-model="modifyForm.sex" size="small">
<el-radio label="男" border></el-radio>
<el-radio label="女" border></el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="联系方式" prop="phone"> <el-form-item label="联系方式" prop="phone">
<el-input v-model="modifyForm.phone"></el-input> <el-input v-model="modifyForm.phone"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="身份证件号" prop="idCard">
<el-input v-model="modifyForm.idCard"></el-input>
</el-form-item>
<el-form-item label="职务" prop="position">
<el-input v-model="modifyForm.position"></el-input>
</el-form-item>
<el-form-item label="所属单位" prop="unitName"> <el-form-item label="所属单位" prop="unitName">
<el-select placeholder="请选择所属单位" <el-select placeholder="请选择所属单位"
style="width: 100%;" clearable style="width: 100%;" clearable
@@ -244,6 +275,62 @@ layout("/layouts/platform_1.html"){
</el-select> </el-select>
<!-- <el-input v-model="modifyForm.unitName"></el-input>--> <!-- <el-input v-model="modifyForm.unitName"></el-input>-->
</el-form-item> </el-form-item>
<el-form-item label="职务" prop="position">
<el-input v-model="modifyForm.position" placeholder="请填写职务"></el-input>
</el-form-item>
<el-form-item label="体重KG" prop="weight">
<el-input-number v-model="modifyForm.weight" min="0" style="width: 100%" placeholder="请输入体重KG"></el-input-number>
</el-form-item>
<el-row>
<el-col :span="12">
<el-form-item label="是否住宿" prop="isStay">
<el-radio-group v-model="modifyForm.isStay" size="small">
<el-radio :label="true" border>住宿</el-radio>
<el-radio :label="false" border>不住宿</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="茶叙会" prop="isParty">
<el-radio-group v-model="modifyForm.isParty" size="small">
<el-radio :label="true" border>参加</el-radio>
<el-radio :label="false" border>不参加</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否联络人" prop="isContacts">
<el-radio-group v-model="modifyForm.isContacts" size="small">
<el-radio :label="true" border>是</el-radio>
<el-radio :label="false" border>否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="是否接站" prop="isStation">
<el-radio-group v-model="modifyForm.isStation" size="small">
<el-radio :label="true" border>是</el-radio>
<el-radio :label="false" border>否</el-radio>
</el-radio-group>
</el-form-item>
</el-col>
<template v-if="modifyForm.isStation">
<el-col :span="24">
<el-form-item label="接站地址" prop="stationAddress">
<el-input v-model="modifyForm.stationAddress" placeholder="请填写接站地址"></el-input>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form> </el-form>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="modifyDialog = false">取消</el-button> <el-button @click="modifyDialog = false">取消</el-button>
@@ -263,6 +350,7 @@ layout("/layouts/platform_1.html"){
year: new Date().getFullYear() + '', year: new Date().getFullYear() + '',
}, },
activityList: [], activityList: [],
registrationFields: [],
// 修改表单 // 修改表单
modifyDialog: false, modifyDialog: false,
modifyForm: {}, modifyForm: {},
@@ -334,9 +422,81 @@ layout("/layouts/platform_1.html"){
}, },
units: [], units: [],
unitLoading: false,
} }
}, },
methods: { methods: {
// 活动切换后同步刷新动态列和列表数据。
onActivityChange() {
this.loadRegistrationFields(() => {
this.doSearch()
})
},
// 获取当前活动启用的报名字段;字典选项用于把保存的code转换成名称。
loadRegistrationFields(callback) {
if (!this.pageForm.activityId) {
this.registrationFields = []
if (callback) callback()
return
}
$.post('/platform/activity/user/getRegistrationFields', {
activityId: this.pageForm.activityId
}).then((res) => {
if (res.code === 0) {
this.registrationFields = res.data || []
} else {
this.registrationFields = []
this.$message.error(res.msg || '加载报名字段失败')
}
}).fail(() => {
this.registrationFields = []
this.$message.error('加载报名字段失败')
}).always(() => {
if (callback) callback()
})
},
formatRegistrationField(row, field) {
// act_user历史表字段为大写开头的ProjectType,原生SQL返回时需兼容其实际列名。
const fieldValue = row[field.fieldKey]
const value = field.fieldKey === 'projectType' && (fieldValue === null || fieldValue === undefined || fieldValue === '')
? row.ProjectType
: fieldValue
if (value === null || value === undefined || value === '') {
// 项目类型仅适用于运动员;区分正常的不适用状态与运动员漏选,便于管理员核对报名数据。
if (field.fieldKey === 'projectType') {
return this.isAthleteRegistration(row) ? '未选择' : '无需选择'
}
return ''
}
if (field.dictCode) {
const optionMap = {}
;(field.options || []).forEach((option) => {
optionMap[option.value] = option.label
})
return String(value).split(',').map((code) => {
const dictValue = code.trim()
return optionMap[dictValue] || dictValue
}).join('、')
}
if (field.componentType === 'boolean-radio') {
const checked = value === true || value === 1 || value === '1'
return field.fieldKey === 'isStay' ? (checked ? '住宿' : '不住宿') : (checked ? '是' : '否')
}
return value
},
// 人员类型保存的是逗号分隔的字典编码,运动员编码从当前字段配置中动态解析,避免硬编码字典值。
isAthleteRegistration(row) {
const personTypeField = this.registrationFields.find((item) => item.fieldKey === 'personType')
const athleteOption = (personTypeField && personTypeField.options || [])
.find((option) => option.label === '运动员')
if (!athleteOption || !row.personType) return false
return String(row.personType).split(',').map((code) => code.trim()).includes(athleteOption.value)
},
getRegistrationFieldWidth(field) {
if (field.fieldKey === 'unitName') return 180
if (field.fieldKey === 'phone' || field.componentType === 'dict-checkbox') return 140
return 100
},
setFormDataUnit(val) { setFormDataUnit(val) {
if (!val) { if (!val) {
this.formData.unitName = '' this.formData.unitName = ''
@@ -414,7 +574,7 @@ layout("/layouts/platform_1.html"){
{ key: 'userName', message: '姓名不能为空' }, { key: 'userName', message: '姓名不能为空' },
{ key: 'sex', message: '性别不能为空' }, { key: 'sex', message: '性别不能为空' },
{ key: 'phone', message: '联系方式不能为空' }, { key: 'phone', message: '联系方式不能为空' },
{ key: 'idCard', message: '身份证件号不能为空' }, // { key: 'idCard', message: '身份证件号不能为空' },
{ key: 'position', message: '职务不能为空' } { key: 'position', message: '职务不能为空' }
]; ];
@@ -507,23 +667,41 @@ layout("/layouts/platform_1.html"){
} }
}) })
}, },
async getActivityList(){ getActivityList(){
const resp = await $.post('/platform/activity/user/getActivityList', { $.post('/platform/activity/user/getActivityList', {
year: this.pageForm.year year: this.pageForm.year
}).then((resp) => {
if (resp.code === 0 && resp.data && resp.data.length > 0) {
this.activityList = resp.data
this.$set(this.pageForm, 'activityId', this.activityList[0].id)
} else {
this.activityList = []
this.$set(this.pageForm, 'activityId', '')
}
this.loadRegistrationFields(() => {
this.pageData()
})
}) })
if (resp.code === 0 && resp.data && resp.data.length > 0) {
this.activityList = resp.data
this.$set(this.pageForm, 'activityId', this.activityList[0].id)
} else {
this.$set(this.pageForm, 'activityId', '')
}
this.pageData()
}, },
async getUnits(){ // 查询面板和代报名弹窗共用启用单位列表,按数据库序号location升序排列。
const resp = await $.post('/open/common/units') getUnits(){
if (resp.code === 0) { this.unitLoading = true
this.units = resp.data $.post('/open/common/units').then((resp) => {
} if (resp.code === 0) {
const units = (resp.data || []).slice().sort((left, right) => {
return Number(left.location ?? 0) - Number(right.location ?? 0)
})
this.$set(this, 'units', units)
} else {
this.$set(this, 'units', [])
this.$message.error(resp.msg || '加载单位列表失败')
}
}).fail(() => {
this.$set(this, 'units', [])
this.$message.error('加载单位列表失败')
}).always(() => {
this.unitLoading = false
})
} }
}, },
created() { created() {
@@ -2,6 +2,16 @@ const addOrUpdate = {
template: /*language=HTML*/ ` template: /*language=HTML*/ `
<el-dialog :title="form.id ? '修改' : '新增'" :visible.sync="visible" width="70%"> <el-dialog :title="form.id ? '修改' : '新增'" :visible.sync="visible" width="70%">
<el-form ref="form" :model="form" :rules="rules" label-width="80px"> <el-form ref="form" :model="form" :rules="rules" label-width="80px">
<el-form-item label="所属活动" prop="activityId">
<el-select v-model="form.activityId" filterable placeholder="请选择所属活动" style="width: 100%">
<el-option
v-for="activity in activityList"
:key="activity.id"
:label="activity.title"
:value="activity.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="标题" prop="title"> <el-form-item label="标题" prop="title">
<el-input v-model="form.title" placeholder="请输入标题"></el-input> <el-input v-model="form.title" placeholder="请输入标题"></el-input>
</el-form-item> </el-form-item>
@@ -25,6 +35,12 @@ const addOrUpdate = {
</template> </template>
</el-dialog> </el-dialog>
`, `,
props: {
activityList: {
type: Array,
default: () => []
}
},
data() { data() {
return { return {
title: '', title: '',
@@ -33,6 +49,7 @@ const addOrUpdate = {
published: true, published: true,
}, },
rules: { rules: {
activityId: [{required: true, message: '请选择所属活动', trigger: ['blur', 'change']}],
title: [{required: true, message: '请输入标题', trigger: ['blur', 'change']}], title: [{required: true, message: '请输入标题', trigger: ['blur', 'change']}],
content: [{required: true, message: '请输入内容', trigger: ['blur', 'change']}], content: [{required: true, message: '请输入内容', trigger: ['blur', 'change']}],
cover: [{required: true, message: '请上传封面图', trigger: ['blur', 'change']}], cover: [{required: true, message: '请上传封面图', trigger: ['blur', 'change']}],
@@ -72,7 +72,7 @@ layout("/layouts/platform_1.html"){
<!--#include("/layouts/pagination.html"){}#--> <!--#include("/layouts/pagination.html"){}#-->
</el-card> </el-card>
<add-or-update ref="addOrUpdateRef" @refresh="doSearch"></add-or-update> <add-or-update ref="addOrUpdateRef" :activity-list="activityList" @refresh="doSearch"></add-or-update>
</div> </div>
<script> <script>
@@ -123,14 +123,19 @@ layout("/layouts/platform_1.html"){
} }
}) })
}, },
async getActivityList(){ getActivityList(){
const resp = await $.post('/platform/activity/user/getActivityList') $.post('/platform/activity/user/getActivityList').then((res) => {
this.activityList = resp.data if (res.code === 0) {
this.activityList = res.data
} else {
this.$message.error(res.msg)
}
})
}, },
}, },
async created() { created() {
this.getActivityList() this.getActivityList()
await this.pageData() this.pageData()
} }
}) })
</script> </script>
@@ -6,8 +6,10 @@ import cn.afterturn.easypoi.excel.annotation.Excel;
import cn.afterturn.easypoi.excel.entity.ExportParams; import cn.afterturn.easypoi.excel.entity.ExportParams;
import cn.afterturn.easypoi.excel.entity.ImportParams; import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity; import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.utils.PwdUtil; import com.budwk.app.base.utils.PwdUtil;
import com.budwk.app.module.activity.models.ActUser;
import com.budwk.app.sys.models.Sys_dict; import com.budwk.app.sys.models.Sys_dict;
import com.budwk.app.sys.services.SysDictService; import com.budwk.app.sys.services.SysDictService;
import com.budwk.app.web.commons.auth.utils.SecurityUtil; import com.budwk.app.web.commons.auth.utils.SecurityUtil;
@@ -21,6 +23,7 @@ import org.apache.poi.xssf.usermodel.*;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.nutz.boot.test.junit4.NbJUnit4Runner; import org.nutz.boot.test.junit4.NbJUnit4Runner;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao; import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject; import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean; import org.nutz.ioc.loader.annotation.IocBean;
@@ -34,6 +37,7 @@ import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
@IocBean @IocBean
@RunWith(NbJUnit4Runner.class) @RunWith(NbJUnit4Runner.class)
@@ -379,4 +383,13 @@ public class Test1 {
} }
@Test
public void test0001(){
List<ActUser> query = dao.query(ActUser.class, Cnd.NEW());
for (ActUser actUser : query) {
int i = ThreadLocalRandom.current().nextInt(0, 201);
actUser.setWeight((double) i);
}
dao.update(query, "weight");
}
} }
+4
View File
@@ -27,6 +27,10 @@ export default {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
}
/* 活动详情富文本保留自身字体及继承关系,其他区域沿用原有默认字体。 */
:where(*:not(.activity-rich-content *)) {
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
} }
+1 -1
View File
@@ -69,7 +69,7 @@
"enable": false "enable": false
}, },
"h5": { "h5": {
"title": "趣味运动会", "title": "赛事管理",
"router": { "router": {
"mode": "history" "mode": "history"
} }
+6 -6
View File
@@ -3,7 +3,7 @@
{ {
"path": "pages/index/index", "path": "pages/index/index",
"style": { "style": {
"navigationBarTitleText": "趣味运动会" "navigationBarTitleText": "赛事管理"
} }
}, },
{ {
@@ -101,13 +101,13 @@
], ],
"globalStyle": { "globalStyle": {
"navigationBarTextStyle": "#000000", "navigationBarTextStyle": "#000000",
"navigationBarTitleText": "趣味运动会", "navigationBarTitleText": "赛事管理",
"navigationBarBackgroundColor": "#ffffff", "navigationBarBackgroundColor": "#ffffff",
"backgroundColor": "#F8F8F8" "backgroundColor": "#F8F8F8"
}, },
"tabBar": { "tabBar": {
"color": "#000", "color": "#000",
"selectedColor": "#f45a5a", "selectedColor": "#1989fa",
"borderStyle": "white", "borderStyle": "white",
"backgroundColor": "#ffffff", "backgroundColor": "#ffffff",
"height": "60px", "height": "60px",
@@ -124,7 +124,7 @@
"selectedText": "\ue64d", "selectedText": "\ue64d",
"fontSize": "17px", "fontSize": "17px",
"color": "#333", "color": "#333",
"selectedColor": "#f45a5a" "selectedColor": "#1989fa"
} }
}, },
{ {
@@ -135,7 +135,7 @@
"selectedText": "\ue8b3", "selectedText": "\ue8b3",
"fontSize": "17px", "fontSize": "17px",
"color": "#333", "color": "#333",
"selectedColor": "#f45a5a" "selectedColor": "#1989fa"
} }
}, },
{ {
@@ -146,7 +146,7 @@
"selectedText": "\ue600", "selectedText": "\ue600",
"fontSize": "17px", "fontSize": "17px",
"color": "#333", "color": "#333",
"selectedColor": "#f45a5a" "selectedColor": "#1989fa"
} }
} }
] ]
+62 -22
View File
@@ -18,7 +18,7 @@
<view class="event-info-section"> <view class="event-info-section">
<!-- 报名时间 --> <!-- 报名时间 -->
<view class="info-item"> <view class="info-item">
<t-icon name="calendar" size="20px"/> <CalendarIcon size="20px"/>
<view class="info-text-container"> <view class="info-text-container">
<text class="info-text-title">报名时间</text> <text class="info-text-title">报名时间</text>
<text class="info-text">{{ formatDate(detailData.signStartTime) }} 至 <text class="info-text">{{ formatDate(detailData.signStartTime) }} 至
@@ -29,7 +29,7 @@
<!-- 活动时间 --> <!-- 活动时间 -->
<view class="info-item"> <view class="info-item">
<t-icon name="alarm" size="20px"/> <AlarmIcon size="20px"/>
<view class="info-text-container"> <view class="info-text-container">
<text class="info-text-title">活动时间</text> <text class="info-text-title">活动时间</text>
<text class="info-text">{{ formatDate(detailData.startTime) }} 至 {{ <text class="info-text">{{ formatDate(detailData.startTime) }} 至 {{
@@ -41,7 +41,7 @@
<!-- 活动地点 --> <!-- 活动地点 -->
<view class="info-item"> <view class="info-item">
<t-icon name="location" size="20px"/> <LocationIcon size="20px"/>
<view class="info-text-container"> <view class="info-text-container">
<text class="info-text-title">活动地点</text> <text class="info-text-title">活动地点</text>
<text class="info-text">{{ detailData.address }}</text> <text class="info-text">{{ detailData.address }}</text>
@@ -52,7 +52,7 @@
<!-- 活动详情区域 --> <!-- 活动详情区域 -->
<view class="activity-detail-section"> <view class="activity-detail-section">
<view class="section-title">活动详情</view> <view class="section-title">活动详情</view>
<rich-text class="rich-content" :nodes="detailData.content"></rich-text> <rich-text class="rich-content activity-rich-content" :nodes="detailData.content"></rich-text>
</view> </view>
<!-- 比赛项目 --> <!-- 比赛项目 -->
@@ -76,7 +76,7 @@
<view class="event-item-info"> <view class="event-item-info">
<view class="event-item-section"> <view class="event-item-section">
<view class="event-item-section-title"> <view class="event-item-section-title">
<t-icon name="flag" size="16px" color="var(--td-brand-color-5)"/> <FlagIcon size="16px" color="var(--td-brand-color-5)"/>
<text>比赛方法</text> <text>比赛方法</text>
</view> </view>
<view class="event-item-section-content">{{ item.form }}</view> <view class="event-item-section-content">{{ item.form }}</view>
@@ -84,15 +84,15 @@
<view class="event-item-section"> <view class="event-item-section">
<view class="event-item-section-title"> <view class="event-item-section-title">
<t-icon name="chart-bar" size="16px" color="var(--td-brand-color-5)"/> <ChartBarIcon size="16px" color="var(--td-brand-color-5)"/>
<text>评分标准</text> <text>评分标准</text>
</view> </view>
<view class="event-item-section-content">{{ item.rule }}</view> <view class="event-item-section-content">{{ item.rule }}</view>
</view> </view>
<view class="event-item-section"> <view class="event-item-section">
<view class="event-item-section-title"> <view class="event-item-section-title">
<t-icon name="help-circle" size="16px" color="var(--td-brand-color-5)"/> <HelpCircleIcon size="16px" color="var(--td-brand-color-5)"/>
<text>比赛规则</text> <text>比赛规则</text>
</view> </view>
<view class="event-item-section-content">{{ item.ruleForm }}</view> <view class="event-item-section-content">{{ item.ruleForm }}</view>
@@ -125,11 +125,19 @@
</template> </template>
<script setup> <script setup>
// 固定图标按文件引入,避免通用 Icon 加载整套图标。
import AlarmIcon from 'tdesign-icons-vue-next/esm/components/alarm.js';
import CalendarIcon from 'tdesign-icons-vue-next/esm/components/calendar.js';
import ChartBarIcon from 'tdesign-icons-vue-next/esm/components/chart-bar.js';
import FlagIcon from 'tdesign-icons-vue-next/esm/components/flag.js';
import HelpCircleIcon from 'tdesign-icons-vue-next/esm/components/help-circle.js';
import LocationIcon from 'tdesign-icons-vue-next/esm/components/location.js';
import {ref, onMounted, computed} from 'vue'; import {ref, onMounted, computed} from 'vue';
import {onLoad} from '@dcloudio/uni-app'; import {onLoad} from '@dcloudio/uni-app';
import {Toast} from 'tdesign-mobile-vue'; import {ToastPlugin as Toast} from 'tdesign-mobile-vue/esm/toast/index.js';
import {useRequest} from "@/utils/request"; import {useRequest} from "@/utils/request";
import {config} from "@/utils/config"; import {config} from "@/utils/config";
import moment from "moment";
const request = useRequest(); const request = useRequest();
const actId = ref(null); const actId = ref(null);
@@ -139,28 +147,46 @@ const detailData = ref(null);
// 格式化日期 // 格式化日期
const formatDate = (dateStr) => { const formatDate = (dateStr) => {
if (!dateStr) return ''; if (!dateStr) return '';
try { return moment(dateStr).format('YYYY.MM.DD HH:mm');
const date = new Date(dateStr); // try {
return `${date.getFullYear()}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; // // 手动解析日期部分
} catch (error) { // const [datePart, timePart] = dateStr.split(' ');
return dateStr; // const [year, month, day] = datePart.split('-').map(Number);
} // const [hours, minutes, seconds] = timePart.split(':').map(Number);
//
// // 创建日期对象(月份需要减1)
// const date = new Date(year, month - 1, day, hours, minutes, seconds);
//
// // 检查日期是否有效
// if (isNaN(date.getTime())) {
// return dateStr;
// }
//
// return `${date.getFullYear()}.${(date.getMonth() + 1).toString().padStart(2, '0')}.${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
// } catch (error) {
// return dateStr;
// }
}; };
// 底部按钮文字 // 底部按钮文字
const bottomButton = computed(() => { const bottomButton = computed(() => {
const {signStartTime, signEndTime} = detailData.value const {signStartTime, signEndTime} = detailData.value
if (signStartTime && signEndTime) { if (signStartTime && signEndTime) {
const now = new Date(); // const now = new Date();
const startTime = new Date(signStartTime); // const startTime = new Date(signStartTime);
const endTime = new Date(signEndTime); // const endTime = new Date(signEndTime);
if (now >= startTime && now <= endTime) {
const now = moment();
const startTime = moment(signStartTime);
const endTime = moment(signEndTime);
if (now.isBetween(startTime, endTime, null, '[]')) {
return { return {
text: isSignUp.value ? '修改报名信息' : '立即报名', text: isSignUp.value ? '修改报名信息' : '立即报名',
disabled: false, disabled: false,
onClick: navigateToSignUp onClick: navigateToSignUp
} }
} else if (now < startTime) { } else if (now.isBefore(startTime)) {
return { return {
text: `报名开始时间:${formatDate(signStartTime)}`, text: `报名开始时间:${formatDate(signStartTime)}`,
disabled: true, disabled: true,
@@ -262,7 +288,7 @@ onLoad((option) => {
.event-tag { .event-tag {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
background-color: rgba(var(--td-brand-color-5), 0.1); background-color: var(--td-brand-color-light);
color: var(--td-brand-color-5); color: var(--td-brand-color-5);
font-size: 24rpx; font-size: 24rpx;
padding: 6rpx 12rpx; padding: 6rpx 12rpx;
@@ -345,6 +371,20 @@ onLoad((option) => {
color: #333333; color: #333333;
} }
/* 移动端覆盖富文本内联大字号,仅调整字号和行高,不改写字体、字重或颜色。 */
@media screen and (max-width: 767px) {
.rich-content {
font-size: 15px;
line-height: 1.75;
overflow-wrap: anywhere;
:deep(*) {
font-size: 15px !important;
line-height: 1.75 !important;
}
}
}
/* 比赛项目 */ /* 比赛项目 */
.event-items-scroll { .event-items-scroll {
width: 100%; width: 100%;
@@ -510,7 +550,7 @@ onLoad((option) => {
max-height: 200rpx; max-height: 200rpx;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
&::after { &::after {
content: ''; content: '';
position: absolute; position: absolute;
@@ -369,7 +369,7 @@ async function publishMoment() {
.location-icon { .location-icon {
font-size: 36rpx; font-size: 36rpx;
color: #ff5777; color: var(--td-brand-color);
margin-right: 10rpx; margin-right: 10rpx;
} }
@@ -395,7 +395,7 @@ async function publishMoment() {
.location-icon { .location-icon {
font-size: 32rpx; font-size: 32rpx;
color: #ff5777; color: var(--td-brand-color);
margin-right: 10rpx; margin-right: 10rpx;
} }
} }
@@ -41,7 +41,7 @@
<view class="time">{{ moment(item.publish).format('MM-DD HH:mm') }}</view> <view class="time">{{ moment(item.publish).format('MM-DD HH:mm') }}</view>
<view @click="onPraise(item)"> <view @click="onPraise(item)">
<t-badge :count="item.praiseCount" :offset="[-2, -2]" class="badge-item" :showZero="true"> <t-badge :count="item.praiseCount" :offset="[-2, -2]" class="badge-item" :showZero="true">
<t-icon :style="item.praiseCount > 0 ? 'color: red' : ''" name="thumb-up-2-filled"/> <t-icon :style="item.praiseCount > 0 ? 'color: var(--td-brand-color)' : ''" name="thumb-up-2-filled"/>
</t-badge> </t-badge>
</view> </view>
</view> </view>
+32 -5
View File
@@ -49,10 +49,9 @@
style="flex-shrink:0;width: 120px;height: 90px;border-radius: 5px;" fit="fill"/> style="flex-shrink:0;width: 120px;height: 90px;border-radius: 5px;" fit="fill"/>
<view <view
style="margin-left:10px;flex:1;overflow:hidden;display: flex;flex-direction: column;justify-content:space-around;color: #534d4d;"> style="margin-left:10px;flex:1;overflow:hidden;display: flex;flex-direction: column;justify-content:space-around;color: #534d4d;">
<view <view class="activity-date-tag">
style="display: flex; align-items: center; column-gap: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<t-icon name="time"></t-icon> <t-icon name="time"></t-icon>
{{ moment(item.startTime).format('MM-DD') }} <text>{{ moment(item.startTime).format('YYYY年MM月DD日') }}</text>
</view> </view>
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> <view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<t-icon name="location"></t-icon> <t-icon name="location"></t-icon>
@@ -60,7 +59,7 @@
</view> </view>
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> <view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<t-icon name="castle-7"></t-icon> <t-icon name="castle-7"></t-icon>
{{ item.coHostUnit }} {{ item.hostUnit }}
</view> </view>
</view> </view>
</view> </view>
@@ -68,7 +67,12 @@
<view> <view>
<!-- 100人已报名--> <!-- 100人已报名-->
</view> </view>
<t-button theme="primary" shape="round" size="small">立即报名</t-button> <view style="display: flex;gap: 10px" @click.stop>
<t-button theme="default" variant="outline" shape="round" size="small"
@click="navigateToDetail(item.id)"><t-icon name="browse" size="16px" style="margin-right: 4px"/>查看详情</t-button>
<t-button theme="primary" shape="round" size="small"
@click="navigateToSignUp(item.id)"><t-icon name="edit" size="16px" style="margin-right: 4px"/>立即报名</t-button>
</view>
</view> </view>
</view> </view>
</view> </view>
@@ -205,6 +209,13 @@ const navigateToDetail = (id) => {
}); });
}; };
// 报名按钮直达表单,活动ID沿用详情页的报名入口参数。
const navigateToSignUp = (id) => {
uni.navigateTo({
url: `/pages/activity/registration-form/index?id=${id}`
});
};
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
getListData(); getListData();
@@ -255,6 +266,22 @@ onMounted(() => {
} }
} }
.activity-date-tag {
align-self: flex-start;
display: inline-flex;
align-items: center;
column-gap: 4px;
max-width: 100%;
padding: 4px 8px;
border-radius: 12px;
box-sizing: border-box;
color: var(--td-brand-color);
background-color: rgba(25, 137, 250, 0.12);
font-size: 12px;
line-height: 1.2;
white-space: nowrap;
}
.loading-container { .loading-container {
padding: 40rpx 0; padding: 40rpx 0;
text-align: center; text-align: center;
+20 -20
View File
@@ -181,7 +181,7 @@ onMounted(async () => {
.bg-circle { .bg-circle {
position: absolute; position: absolute;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #f45a5a, #f78484); background: linear-gradient(135deg, var(--td-brand-color), var(--td-brand-color-4));
opacity: 0.06; opacity: 0.06;
} }
@@ -227,7 +227,7 @@ onMounted(async () => {
.rank-header { .rank-header {
flex-shrink: 0; flex-shrink: 0;
background: linear-gradient(135deg, #f45a5a, #f78484); background: linear-gradient(135deg, var(--td-brand-color), var(--td-brand-color-4));
height: 200rpx; height: 200rpx;
padding: 20rpx 32rpx; padding: 20rpx 32rpx;
color: #fff; color: #fff;
@@ -235,7 +235,7 @@ onMounted(async () => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
position: relative; position: relative;
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.3); box-shadow: 0 4rpx 20rpx rgba(25, 137, 250, 0.3);
border-bottom-left-radius: 30rpx; border-bottom-left-radius: 30rpx;
border-bottom-right-radius: 30rpx; border-bottom-right-radius: 30rpx;
z-index: 1; z-index: 1;
@@ -292,7 +292,7 @@ onMounted(async () => {
padding: 24rpx 16rpx; padding: 24rpx 16rpx;
font-weight: bold; font-weight: bold;
color: #666; color: #666;
border-bottom: 1px solid rgba(244, 90, 90, 0.1); border-bottom: 1px solid rgba(25, 137, 250, 0.1);
background-color: rgba(255, 255, 255, 0.8); background-color: rgba(255, 255, 255, 0.8);
margin: 16rpx; margin: 16rpx;
border-radius: 16rpx; border-radius: 16rpx;
@@ -353,8 +353,8 @@ onMounted(async () => {
} }
.my-team { .my-team {
background-color: rgba(244, 90, 90, 0.05); background-color: rgba(25, 137, 250, 0.05);
border-left: 8rpx solid #f45a5a; border-left: 8rpx solid var(--td-brand-color);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
@@ -367,7 +367,7 @@ onMounted(async () => {
width: 60rpx; width: 60rpx;
height: 60rpx; height: 60rpx;
border-radius: 50%; border-radius: 50%;
background-color: rgba(244, 90, 90, 0.1); background-color: rgba(25, 137, 250, 0.1);
z-index: 0; z-index: 0;
} }
@@ -389,8 +389,8 @@ onMounted(async () => {
} }
.my-rank-number { .my-rank-number {
background-color: rgba(244, 90, 90, 0.1); background-color: rgba(25, 137, 250, 0.1);
color: #f45a5a; color: var(--td-brand-color);
} }
.team-info { .team-info {
@@ -432,9 +432,9 @@ onMounted(async () => {
.my-team-tag { .my-team-tag {
margin-left: 10rpx; margin-left: 10rpx;
background-color: rgba(244, 90, 90, 0.1); background-color: rgba(25, 137, 250, 0.1);
color: #f45a5a; color: var(--td-brand-color);
border: 1px solid rgba(244, 90, 90, 0.2); border: 1px solid rgba(25, 137, 250, 0.2);
} }
.progress-bar { .progress-bar {
@@ -454,14 +454,14 @@ onMounted(async () => {
.progress-bar-fill { .progress-bar-fill {
position: absolute; position: absolute;
height: 100%; height: 100%;
background: linear-gradient(to right, #f45a5a, #f78484); background: linear-gradient(to right, var(--td-brand-color), var(--td-brand-color-4));
border-radius: 3rpx; border-radius: 3rpx;
transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1); transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1);
} }
.score-value { .score-value {
font-weight: bold; font-weight: bold;
color: #f45a5a; color: var(--td-brand-color);
font-size: 32rpx; font-size: 32rpx;
} }
@@ -477,8 +477,8 @@ onMounted(async () => {
width: 90rpx; width: 90rpx;
height: 90rpx; height: 90rpx;
border-radius: 50%; border-radius: 50%;
background: linear-gradient(135deg, #f45a5a, #f78484); background: linear-gradient(135deg, var(--td-brand-color), var(--td-brand-color-4));
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.4); box-shadow: 0 4rpx 20rpx rgba(25, 137, 250, 0.4);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -489,15 +489,15 @@ onMounted(async () => {
@keyframes pulse { @keyframes pulse {
0% { 0% {
transform: scale(1); transform: scale(1);
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.4); box-shadow: 0 4rpx 20rpx rgba(25, 137, 250, 0.4);
} }
50% { 50% {
transform: scale(1.05); transform: scale(1.05);
box-shadow: 0 4rpx 25rpx rgba(244, 90, 90, 0.6); box-shadow: 0 4rpx 25rpx rgba(25, 137, 250, 0.6);
} }
100% { 100% {
transform: scale(1); transform: scale(1);
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.4); box-shadow: 0 4rpx 20rpx rgba(25, 137, 250, 0.4);
} }
} }
@@ -516,7 +516,7 @@ onMounted(async () => {
} }
.empty-icon { .empty-icon {
color: rgba(244, 90, 90, 0.5); color: rgba(25, 137, 250, 0.5);
} }
.empty-text { .empty-text {
File diff suppressed because it is too large Load Diff
@@ -141,14 +141,14 @@ const navigateToEdit = () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
background-color: rgba(255, 95, 92, 0.1); background-color: rgba(25, 137, 250, 0.1);
border-radius: 100px; border-radius: 100px;
padding: 10px 20px; padding: 10px 20px;
margin-top: 20px; margin-top: 20px;
margin-bottom: 40px; margin-bottom: 40px;
.t-icon { .t-icon {
color: #ff5f5c; color: var(--td-brand-color);
margin-right: 6px; margin-right: 6px;
} }
@@ -171,13 +171,13 @@ const navigateToEdit = () => {
} }
:deep(.t-button--primary) { :deep(.t-button--primary) {
background-color: #ff5f5c !important; background-color: var(--td-brand-color) !important;
border-color: #ff5f5c !important; border-color: var(--td-brand-color) !important;
} }
:deep(.t-button--default) { :deep(.t-button--default) {
border-color: #ff5f5c !important; border-color: var(--td-brand-color) !important;
color: #ff5f5c !important; color: var(--td-brand-color) !important;
} }
} }
</style> </style>
@@ -550,7 +550,7 @@ const submitRegistration = async () => {
.picker-subtitle { .picker-subtitle {
font-size: 14px; font-size: 14px;
color: #ff5f5c; color: var(--td-brand-color);
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
+21 -21
View File
@@ -355,7 +355,7 @@ onLoad(() => {
.picker-subtitle { .picker-subtitle {
font-size: 14px; font-size: 14px;
color: #ff5f5c; color: var(--td-brand-color);
margin-bottom: 20px; margin-bottom: 20px;
padding-bottom: 12px; padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0; border-bottom: 1px solid #f0f0f0;
@@ -403,7 +403,7 @@ onLoad(() => {
background-color: #fff; background-color: #fff;
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 16px rgba(255, 95, 92, 0.08); box-shadow: 0 4px 16px rgba(25, 137, 250, 0.08);
margin: 12px 0 20px; margin: 12px 0 20px;
} }
@@ -411,8 +411,8 @@ onLoad(() => {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 16px; padding: 16px;
background: linear-gradient(to right, rgba(255, 95, 92, 0.05), rgba(255, 95, 92, 0.02)); background: linear-gradient(to right, rgba(25, 137, 250, 0.05), rgba(25, 137, 250, 0.02));
border-bottom: 1px solid rgba(255, 95, 92, 0.1); border-bottom: 1px solid rgba(25, 137, 250, 0.1);
} }
.team-logo { .team-logo {
@@ -424,14 +424,14 @@ onLoad(() => {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
border: 2px solid #fff; border: 2px solid #fff;
box-shadow: 0 2px 8px rgba(255, 95, 92, 0.15); box-shadow: 0 2px 8px rgba(25, 137, 250, 0.15);
overflow: hidden; overflow: hidden;
flex-shrink: 0; flex-shrink: 0;
} }
.team-logo i { .team-logo i {
font-size: 24px; font-size: 24px;
color: #ff5f5c; color: var(--td-brand-color);
} }
.team-logo img { .team-logo img {
@@ -472,12 +472,12 @@ onLoad(() => {
width: 36px; width: 36px;
height: 36px; height: 36px;
border-radius: 18px; border-radius: 18px;
background-color: rgba(255, 95, 92, 0.1); background-color: rgba(25, 137, 250, 0.1);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
margin-right: 10px; margin-right: 10px;
color: #ff5f5c; color: var(--td-brand-color);
font-size: 18px; font-size: 18px;
} }
@@ -528,12 +528,12 @@ onLoad(() => {
color: #666; color: #666;
line-height: 1.5; line-height: 1.5;
position: relative; position: relative;
background-color: #fff8f8; background-color: #f5faff;
border-top: 1px solid #f0f0f0; border-top: 1px solid #f0f0f0;
} }
.team-desc i { .team-desc i {
color: #ffdddd; color: var(--td-brand-color-2);
font-size: 18px; font-size: 18px;
position: absolute; position: absolute;
top: 8px; top: 8px;
@@ -548,9 +548,9 @@ onLoad(() => {
.register-btn { .register-btn {
height: 48px; height: 48px;
font-size: 16px; font-size: 16px;
background-color: #ff5f5c; background-color: var(--td-brand-color);
border-color: #ff5f5c; border-color: var(--td-brand-color);
box-shadow: 0 6px 16px rgba(255, 95, 92, 0.2); box-shadow: 0 6px 16px rgba(25, 137, 250, 0.2);
} }
/* 成员列表 */ /* 成员列表 */
@@ -579,7 +579,7 @@ onLoad(() => {
.section-title i { .section-title i {
margin-right: 8px; margin-right: 8px;
color: #ff5f5c; color: var(--td-brand-color);
font-size: 18px; font-size: 18px;
} }
@@ -589,8 +589,8 @@ onLoad(() => {
justify-content: center; justify-content: center;
min-width: 24px; min-width: 24px;
height: 24px; height: 24px;
background-color: #fff0f0; background-color: var(--td-brand-color-light);
color: #ff5f5c; color: var(--td-brand-color);
border-radius: 12px; border-radius: 12px;
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 500;
@@ -647,11 +647,11 @@ onLoad(() => {
.role-card { .role-card {
display: flex; display: flex;
align-items: center; align-items: center;
background-color: #fff8f8; background-color: #f5faff;
border-radius: 12px; border-radius: 12px;
padding: 16px; padding: 16px;
margin-bottom: 16px; margin-bottom: 16px;
box-shadow: 0 4px 12px rgba(255, 95, 92, 0.05); box-shadow: 0 4px 12px rgba(25, 137, 250, 0.05);
} }
.role-icon { .role-icon {
@@ -664,7 +664,7 @@ onLoad(() => {
justify-content: center; justify-content: center;
margin-right: 12px; margin-right: 12px;
//box-shadow: 0 4px 12px rgba(255, 95, 92, 0.2); //box-shadow: 0 4px 12px rgba(255, 95, 92, 0.2);
color: #ff5f5c; color: var(--td-brand-color);
font-size: 22px; font-size: 22px;
} }
@@ -689,8 +689,8 @@ onLoad(() => {
margin-top: 8px; margin-top: 8px;
border-radius: 12px; border-radius: 12px;
height: 48px; height: 48px;
border-color: #ff5f5c; border-color: var(--td-brand-color);
color: #ff5f5c; color: var(--td-brand-color);
} }
/* 弹窗内容样式 */ /* 弹窗内容样式 */
@@ -24,7 +24,8 @@ const viewData = ref(null);
function getWonderfulMoment() { function getWonderfulMoment() {
request.post('/platform/activity/wonderfulMoment/getWonderfulMomentById/' + momentId.value).then(res=>{ request.post('/platform/activity/wonderfulMoment/getWonderfulMomentById/' + momentId.value).then(res=>{
if(res.code === 0){ if(res.code === 0){
res.data.content = res?.data?.content.replaceAll('/open/file/upload/download?filepath=','') // 后台富文本保存的是后端相对预览地址,H5 端需通过 /api 代理访问实际上传文件。
res.data.content = res?.data?.content.replaceAll('/open/file/upload/download?filepath=', '/api/open/file/upload/download?inline=true&filepath=')
viewData.value = res.data viewData.value = res.data
}else{ }else{
Toast({message: res.msg, theme: 'error'}); Toast({message: res.msg, theme: 'error'});
+372 -269
View File
@@ -1,9 +1,16 @@
<template> <template>
<view class="page-container"> <view class="page-container">
<view class="header"> <view class="header">
<image class="header-bg" <image
src="/static/img/index2.png" class="header-bg"
mode="aspectFill"></image> src="/static/img/index4.jpg"
mode="aspectFill"
></image>
<!-- html文字临时屏蔽
<view v-if="currentActivityTitle" class="header-activity-title">
{{ displayActivityTitle }}
</view>
-->
</view> </view>
<view class="menu-container"> <view class="menu-container">
@@ -13,54 +20,79 @@
</view> </view>
<view class="menu-text">赛事报名</view> <view class="menu-text">赛事报名</view>
</view> </view>
<view class="menu-item" @click="navigateTo('/pages/activity/scan-sign/index')">
<view class="menu-icon-wrapper orange-bg">
<view class="menu-icon qrcode-icon"></view>
</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/index')">
<view class="menu-icon-wrapper yellow-bg"> <view class="menu-icon-wrapper yellow-bg">
<view class="menu-icon team-icon"></view> <view class="menu-icon team-icon"></view>
<!-- <view class="badge">3</view>--> <!-- <view class="badge">3</view>-->
</view> </view>
<view class="menu-text">我的团队</view> <view class="menu-text">我的团队</view>
</view> </view>
<view class="menu-item" @click="navigateTo('/pages/activity/team-form/index')"> <!-- 暂未开放的快捷入口保留展示,不绑定跳转事件。 -->
<view class="menu-item menu-item-disabled" aria-disabled="true">
<view class="menu-icon-wrapper blue-bg"> <view class="menu-icon-wrapper blue-bg">
<view class="menu-icon star-icon"></view> <view class="menu-icon star-icon"></view>
</view> </view>
<view class="menu-text">项目报名</view> <view class="menu-text">项目报名</view>
</view> </view>
<!-- <view class="menu-item" @click="navigateTo('/pages/activity/dynamic/index')">--> <view class="menu-item menu-item-disabled" aria-disabled="true">
<!-- <view class="menu-icon-wrapper blue-bg">--> <view class="menu-icon-wrapper orange-bg">
<!-- <view class="menu-icon star-icon"></view>--> <view class="menu-icon qrcode-icon"></view>
<!-- </view>--> </view>
<!-- <view class="menu-text">精彩瞬间</view>--> <view class="menu-text">扫码签到</view>
<!-- </view>--> </view>
<!-- <view class="menu-item" @click="navigateTo('/pages/activity/dynamic/index')">-->
<!-- <view class="menu-icon-wrapper blue-bg">-->
<!-- <view class="menu-icon star-icon"></view>-->
<!-- </view>-->
<!-- <view class="menu-text">精彩瞬间</view>-->
<!-- </view>-->
</view> </view>
<!-- 精选内容区 --> <!-- 精选内容区 -->
<view class="content-section"> <view class="content-section">
<view class="section-header"> <view class="section-header">
<view class="section-title">近期活动</view> <view class="section-title">活动详情</view>
<view class="section-more" @click="navigateTo('/pages/activity/list/index')">更多</view> <view
class="section-more"
@click="navigateTo('/pages/activity/list/index')"
>更多</view
>
</view> </view>
<t-swiper <t-swiper
v-if="actList.length > 0" v-if="actList.length > 0"
:autoplay="actList.length > 1" :autoplay="actList.length > 1"
:navigation="{ type: 'fraction', paginationPosition: 'bottom-right', showControls: true }" :navigation="{
type: 'fraction',
paginationPosition: 'bottom-right',
showControls: true,
}"
> >
<t-swiper-item v-for="(item, index) in actList" :key="index" style="height: 192px"> <t-swiper-item
<view class="activity-list" @click="navigateTo('/pages/activity/detail/index?id=' + item.id)"> v-for="(item, index) in actList"
:key="index"
style="height: 192px"
>
<view
class="activity-list"
@click="navigateTo('/pages/activity/detail/index?id=' + item.id)"
>
<view class="activity-item"> <view class="activity-item">
<t-image :src=config.fileDomain+item.cover shape="round" <t-image
class="activity-image" fit="fill"/> :src="getActivityCoverUrl(item.cover)"
:lazy="index > 0"
shape="round"
class="activity-image"
fit="fill"
/>
<view class="activity-info"> <view class="activity-info">
<view class="activity-title">{{ item.title }}</view> <view class="activity-title">{{ item.title }}</view>
<view class="activity-meta"> <view class="activity-meta">
<text class="activity-time">{{ moment(item.startTime).format('M月DD日') + '-' + moment(item.endTime).format('DD日') }}</text> <text class="activity-time">{{
moment(item.startTime).format('M月DD日') +
'-' +
moment(item.endTime).format('DD日')
}}</text>
<text class="activity-location">{{ item.address }}</text> <text class="activity-location">{{ item.address }}</text>
</view> </view>
</view> </view>
@@ -68,11 +100,7 @@
</view> </view>
</t-swiper-item> </t-swiper-item>
</t-swiper> </t-swiper>
<t-empty <t-empty v-if="actList.length === 0" description="暂无近期活动" />
v-if="actList.length === 0"
description="暂无近期活动"
/>
</view> </view>
<view class="content-section"> <view class="content-section">
@@ -81,14 +109,23 @@
</view> </view>
<view class="activity-list" v-if="wonderfulMomentList.length > 0"> <view class="activity-list" v-if="wonderfulMomentList.length > 0">
<view class="moment-item" v-for="item in wonderfulMomentList" :key="item.id" <view
@click="handleWonderfulMoment(item)"> class="moment-item"
<t-image :src=config.fileDomain+item.cover shape="round" v-for="item in wonderfulMomentList"
class="moment-image" fit="fill"/> :key="item.id"
@click="handleWonderfulMoment(item)"
>
<t-image
:src="config.fileDomain + item.cover"
lazy
shape="round"
class="moment-image"
fit="fill"
/>
<view class="moment-content"> <view class="moment-content">
<view class="moment-title">{{ item.title }}</view> <view class="moment-title">{{ item.title }}</view>
<view class="moment-sub-content"> <view class="moment-sub-content">
<view style="float: right;">{{ item.time }}</view> <view style="float: right">{{ item.time }}</view>
<view>阅读 {{ item.readCount }}</view> <view>阅读 {{ item.readCount }}</view>
</view> </view>
</view> </view>
@@ -98,280 +135,346 @@
v-if="wonderfulMomentList.length === 0" v-if="wonderfulMomentList.length === 0"
description="暂无精彩瞬间" description="暂无精彩瞬间"
/> />
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import {onMounted, ref} from "vue"; import { computed, onMounted, ref } from 'vue'
import {onLoad, onShow} from "@dcloudio/uni-app"; import { onLoad, onShow } from '@dcloudio/uni-app'
import {Toast} from "tdesign-mobile-vue"; import { Toast } from 'tdesign-mobile-vue'
import {useRequest} from "@/utils/request"; import { useRequest } from '@/utils/request'
import moment from "moment"; import moment from 'moment'
import {config} from "@/utils/config"; import { config } from '@/utils/config'
const request = useRequest() const request = useRequest()
const actList = ref([]) const actList = ref([])
const wonderfulMomentList = ref([]) const wonderfulMomentList = ref([])
// 首页头图展示当前最新开启活动的标题,数据口径与登录页保持一致。
function navigateTo(url) { const currentActivityTitle = ref('')
uni.navigateTo({ // 当前活动标题在“(公司)”后分行展示,其他标题仍保留原文并按容器宽度自然换行。
url const displayActivityTitle = computed(() => {
return currentActivityTitle.value.replace('(公司)', '(公司)\n')
}) })
}
function getActivity() { // 后台仅保存封面相对路径,首页通过文件预览接口读取活动管理中上传的实际图片。
request.post('/platform/activity/manage/getActivity').then(res=>{ const getActivityCoverUrl = (cover) => {
if(res.code === 0){ if (!cover) {
actList.value = res.data return ''
}else{
Toast({message: res.msg, theme: 'error'});
} }
return (
'/api/open/file/upload/download?inline=true&filepath=' +
encodeURIComponent(cover)
)
}
function navigateTo(url) {
uni.navigateTo({
url,
})
}
function getActivity() {
request.post('/platform/activity/manage/getActivity').then((res) => {
if (res.code === 0) {
actList.value = res.data
} else {
Toast({ message: res.msg, theme: 'error' })
}
})
}
// 公开接口返回当前最新开启活动信息;请求失败时不显示标题,也不阻断首页其他内容。
function getLatestActivityTitle() {
request
.get('/open/common/latestActivityOrganizer')
.then(({ code, data }) => {
if (code === 0 && data) {
currentActivityTitle.value = data.title || ''
}
})
.catch((error) => {
console.error('获取当前活动标题失败', error)
})
}
function getWonderfulMoment() {
request
.post('/platform/activity/wonderfulMoment/getWonderfulMoment')
.then((res) => {
if (res.code === 0) {
wonderfulMomentList.value = res.data
} else {
Toast({ message: res.msg, theme: 'error' })
}
})
}
function handleWonderfulMoment(row) {
request
.post(
'/platform/activity/wonderfulMoment/handleWonderfulMoment/' + row.id,
)
.then((res) => {
if (res.code === 0) {
getWonderfulMoment()
navigateTo('/pages/activity/wonderfulMoment/index?id=' + row.id)
} else {
Toast({ message: res.msg, theme: 'error' })
}
})
}
onShow(() => {
getLatestActivityTitle()
getActivity()
getWonderfulMoment()
}) })
}
function getWonderfulMoment() {
request.post('/platform/activity/wonderfulMoment/getWonderfulMoment').then(res=>{
if(res.code === 0){
wonderfulMomentList.value = res.data
}else{
Toast({message: res.msg, theme: 'error'});
}
})
}
function handleWonderfulMoment(row) {
request.post('/platform/activity/wonderfulMoment/handleWonderfulMoment/' + row.id).then(res=>{
if(res.code === 0){
getWonderfulMoment()
navigateTo('/pages/activity/wonderfulMoment/index?id=' + row.id)
}else{
Toast({message: res.msg, theme: 'error'});
}
})
}
onShow(() => {
getActivity()
getWonderfulMoment()
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
uni-page-body { uni-page-body {
height: auto; height: auto;
} }
// 头部区域样式 // 头部区域样式
.header { .header {
position: relative; position: relative;
height: 245px; height: 245px;
overflow: hidden; overflow: hidden;
} }
.header-bg { .header-bg {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
} }
// 菜单区域样式 .header-activity-title {
.menu-container { position: absolute;
display: flex; top: 61%;
justify-content: space-around; left: 50%;
padding: 20px; z-index: 1;
background-color: #fff; width: 62%;
border-radius: 15px; transform: translate(-50%, -50%);
margin: -30px 15px 15px; color: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); font-size: 16px;
z-index: 10; font-weight: 600;
} line-height: 1.5;
text-align: center;
white-space: pre-line;
text-shadow: 0 1px 3px rgba(122, 24, 24, 0.45);
}
.menu-item { // 菜单区域样式
display: flex; .menu-container {
flex-direction: column; display: flex;
align-items: center; justify-content: space-around;
width: 22%; padding: 20px;
} background-color: #fff;
border-radius: 15px;
margin: -30px 15px 15px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
z-index: 10;
}
.menu-icon-wrapper { .menu-item {
width: 50px; display: flex;
height: 50px; flex-direction: column;
border-radius: 12px; align-items: center;
display: flex; width: 22%;
justify-content: center; }
align-items: center;
position: relative;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.red-bg { .menu-icon-wrapper {
background: linear-gradient(135deg, #ff7676, #f45a5a); width: 50px;
} height: 50px;
border-radius: 12px;
display: flex;
justify-content: center;
align-items: center;
position: relative;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.orange-bg { .red-bg {
background: linear-gradient(135deg, #ffa76a, #f89f58); background: linear-gradient(135deg, var(--td-brand-color-4), var(--td-brand-color));
} }
.yellow-bg { .orange-bg {
background: linear-gradient(135deg, #ffd166, #f8c158); background: linear-gradient(135deg, #ffa76a, #f89f58);
} }
.blue-bg { .yellow-bg {
background: linear-gradient(135deg, #72a1ff, #5c7cfa); background: linear-gradient(135deg, #ffd166, #f8c158);
} }
.menu-icon { .blue-bg {
width: 28px; background: linear-gradient(135deg, #72a1ff, #5c7cfa);
height: 28px; }
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.trophy-icon { .menu-icon {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94.63 1.5 1.98 2.63 3.61 2.96V19H7v2h10v-2h-4v-3.1c1.63-.33 2.98-1.46 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z'/%3E%3C/svg%3E"); width: 28px;
} height: 28px;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.qrcode-icon { .trophy-icon {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zM13 3v8h8V3h-8zm6 6h-4V5h4v4zM13 13h2v2h-2zM15 15h2v2h-2zM13 17h2v2h-2zM17 17h2v2h-2zM19 19h2v2h-2zM15 19h2v2h-2zM17 13h2v2h-2zM19 15h2v2h-2z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M19 5h-2V3H7v2H5c-1.1 0-2 .9-2 2v1c0 2.55 1.92 4.63 4.39 4.94.63 1.5 1.98 2.63 3.61 2.96V19H7v2h10v-2h-4v-3.1c1.63-.33 2.98-1.46 3.61-2.96C19.08 12.63 21 10.55 21 8V7c0-1.1-.9-2-2-2zM5 8V7h2v3.82C5.84 10.4 5 9.3 5 8zm14 0c0 1.3-.84 2.4-2 2.82V7h2v1z'/%3E%3C/svg%3E");
} }
.team-icon { .qrcode-icon {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M3 11h8V3H3v8zm2-6h4v4H5V5zM3 21h8v-8H3v8zm2-6h4v4H5v-4zM13 3v8h8V3h-8zm6 6h-4V5h4v4zM13 13h2v2h-2zM15 15h2v2h-2zM13 17h2v2h-2zM17 17h2v2h-2zM19 19h2v2h-2zM15 19h2v2h-2zM17 13h2v2h-2zM19 15h2v2h-2z'/%3E%3C/svg%3E");
} }
.star-icon { .team-icon {
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z'/%3E%3C/svg%3E"); background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z'/%3E%3C/svg%3E");
} }
.badge { .star-icon {
position: absolute; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='%23ffffff'%3E%3Cpath d='M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z'/%3E%3C/svg%3E");
top: -5px; }
right: -5px;
background-color: #ff4d4f;
color: white;
border-radius: 10px;
min-width: 18px;
height: 18px;
font-size: 12px;
line-height: 18px;
text-align: center;
padding: 0 5px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.3);
}
.menu-text { .badge {
margin-top: 10px; position: absolute;
font-size: 14px; top: -5px;
color: #333; right: -5px;
font-weight: 500; background-color: #ff4d4f;
} color: white;
border-radius: 10px;
min-width: 18px;
height: 18px;
font-size: 12px;
line-height: 18px;
text-align: center;
padding: 0 5px;
font-weight: bold;
box-shadow: 0 2px 4px rgba(255, 77, 79, 0.3);
}
// 内容区域样式 .menu-text {
.content-section { margin-top: 10px;
margin: 0 0 15px; font-size: 14px;
padding: 15px; color: #333;
background-color: #fff; font-weight: 500;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); }
}
.section-header { .menu-item-disabled {
display: flex; cursor: not-allowed;
justify-content: space-between; }
align-items: center;
margin-bottom: 15px;
}
.section-title { .menu-item-disabled .menu-icon-wrapper::after {
font-size: 16px; content: '';
font-weight: bold; position: absolute;
color: #333; inset: 0;
position: relative; border-radius: inherit;
padding-left: 10px; background: rgba(255, 255, 255, 0.55);
} }
.section-title::before { .menu-item-disabled .menu-text {
content: ''; opacity: 0.45;
position: absolute; }
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: linear-gradient(to bottom, #ff6b6b, #e83a3a);
border-radius: 3px;
}
.section-more { // 内容区域样式
font-size: 14px; .content-section {
color: #666; margin: 0 0 15px;
} padding: 15px;
background-color: #fff;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
}
// 活动列表样式 .section-header {
.activity-list { display: flex;
width: 100%; justify-content: space-between;
} align-items: center;
margin-bottom: 15px;
}
.activity-item { .section-title {
width: 100%; font-size: 18px;
margin-bottom: 15px; font-weight: bold;
} color: #333;
position: relative;
padding-left: 10px;
}
.activity-item:last-child { .section-title::before {
margin-bottom: 0; content: '';
} position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 16px;
background: linear-gradient(to bottom, var(--td-brand-color-4), var(--td-brand-color-active));
border-radius: 3px;
}
.activity-image { .section-more {
height: 200px; font-size: 14px;
width: 100%; color: #666;
} }
.activity-info { // 活动列表样式
padding: 10px 0; .activity-list {
} width: 100%;
}
.activity-title { .activity-item {
font-size: 16px; width: 100%;
font-weight: 500; margin-bottom: 15px;
color: #333; }
margin-bottom: 5px;
}
.activity-meta { .activity-item:last-child {
display: flex; margin-bottom: 0;
font-size: 12px; }
color: #999;
}
.activity-time { .activity-image {
margin-right: 15px; height: 200px;
} width: 100%;
.moment-image { }
width: 180px;
height: 90px; .activity-info {
margin-right: 8px; padding: 10px 0;
} }
.moment-item {
width: 100%; .activity-title {
display: flex; font-size: 16px;
margin-bottom: 16px; font-weight: 500;
} color: #333;
.moment-title { margin-bottom: 5px;
font-weight: bold; }
}
.moment-content { .activity-meta {
display: flex; display: flex;
width: 100%; font-size: 12px;
justify-content: space-between; color: #999;
flex-direction: column; }
}
.moment-sub-content { .activity-time {
font-size: 12px; margin-right: 15px;
color: grey; }
} .moment-image {
width: 180px;
height: 90px;
margin-right: 8px;
}
.moment-item {
width: 100%;
display: flex;
margin-bottom: 16px;
}
.moment-title {
font-weight: bold;
}
.moment-content {
display: flex;
width: 100%;
justify-content: space-between;
flex-direction: column;
}
.moment-sub-content {
font-size: 12px;
color: grey;
}
</style> </style>
+89 -82
View File
@@ -6,15 +6,15 @@
<pattern id="pattern-hex" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse" <pattern id="pattern-hex" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"
patternTransform="scale(0.8)"> patternTransform="scale(0.8)">
<rect x="0" y="0" width="10" height="10" fill="none"/> <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)" <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(25, 137, 250, 0.2)"
fill="rgba(244, 90, 90, 0.1)"> fill="rgba(25, 137, 250, 0.1)">
<animate attributeName="stroke-opacity" values="0.1;0.3;0.1" dur="3s" repeatCount="indefinite"/> <animate attributeName="stroke-opacity" values="0.1;0.3;0.1" dur="3s" repeatCount="indefinite"/>
</path> </path>
</pattern> </pattern>
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-hex)"/> <rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-hex)"/>
<g fill="none" stroke-width="0.5"> <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)"> <path d="M20,50 Q60,20 100,50 T180,50 T260,50" stroke="rgba(25, 137, 250, 0.6)">
<animate attributeName="d" <animate attributeName="d"
values="M20,50 Q60,20 100,50 T180,50 T260,50; values="M20,50 Q60,20 100,50 T180,50 T260,50;
M20,50 Q60,80 100,50 T180,50 T260,50; M20,50 Q60,80 100,50 T180,50 T260,50;
@@ -22,7 +22,7 @@
dur="10s" repeatCount="indefinite"/> dur="10s" repeatCount="indefinite"/>
</path> </path>
<path d="M20,70 Q60,40 100,70 T180,70 T260,70" stroke="rgba(244, 90, 90, 0.4)"> <path d="M20,70 Q60,40 100,70 T180,70 T260,70" stroke="rgba(25, 137, 250, 0.4)">
<animate attributeName="d" <animate attributeName="d"
values="M20,70 Q60,40 100,70 T180,70 T260,70; values="M20,70 Q60,40 100,70 T180,70 T260,70;
M20,70 Q60,100 100,70 T180,70 T260,70; M20,70 Q60,100 100,70 T180,70 T260,70;
@@ -31,19 +31,19 @@
</path> </path>
</g> </g>
<circle cx="50" cy="50" r="1" fill="rgba(244, 90, 90, 0.8)"> <circle cx="50" cy="50" r="1" fill="rgba(25, 137, 250, 0.8)">
<animate attributeName="cx" values="50;250;50" dur="20s" repeatCount="indefinite"/> <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="cy" values="50;30;80;50" dur="16s" repeatCount="indefinite"/>
<animate attributeName="r" values="1;1.5;1" dur="5s" repeatCount="indefinite"/> <animate attributeName="r" values="1;1.5;1" dur="5s" repeatCount="indefinite"/>
</circle> </circle>
<circle cx="150" cy="30" r="1" fill="rgba(244, 90, 90, 0.8)"> <circle cx="150" cy="30" r="1" fill="rgba(25, 137, 250, 0.8)">
<animate attributeName="cx" values="150;100;200;150" dur="25s" repeatCount="indefinite"/> <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="cy" values="30;60;20;30" dur="18s" repeatCount="indefinite"/>
<animate attributeName="r" values="1;1.8;1" dur="6s" repeatCount="indefinite"/> <animate attributeName="r" values="1;1.8;1" dur="6s" repeatCount="indefinite"/>
</circle> </circle>
<circle cx="250" cy="70" r="1" fill="rgba(244, 90, 90, 0.8)"> <circle cx="250" cy="70" r="1" fill="rgba(25, 137, 250, 0.8)">
<animate attributeName="cx" values="250;150;250" dur="22s" repeatCount="indefinite"/> <animate attributeName="cx" values="250;150;250" dur="22s" repeatCount="indefinite"/>
<animate attributeName="cy" values="70;40;70" dur="15s" 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"/> <animate attributeName="r" values="1;1.2;1" dur="4s" repeatCount="indefinite"/>
@@ -52,7 +52,7 @@
</div> </div>
<view class="title-content"> <view class="title-content">
<text class="main-title">第二届"钟声杯"直属工会趣味运动会</text> <text class="main-title">{{ activityOrganizer.title || '赛事管理' }}</text>
<view class="sub-title-container"> <view class="sub-title-container">
<text class="sub-title">欢迎使用</text> <text class="sub-title">欢迎使用</text>
</view> </view>
@@ -61,18 +61,18 @@
<view class="login-card"> <view class="login-card">
<view class="login-tabs"> <view class="login-tabs">
<view <!-- <view-->
:class="['tab', loginForm.loginMethod === 'sms' ? 'active' : '']" <!-- :class="['tab', loginForm.loginMethod === 'sms' ? 'active' : '']"-->
@click="loginForm.loginMethod = 'sms'" <!-- @click="loginForm.loginMethod = 'sms'"-->
> <!-- >-->
验证码登录 <!-- 验证码登录-->
</view> <!-- </view>-->
<view <!-- <view-->
:class="['tab', loginForm.loginMethod === 'password' ? 'active' : '']" <!-- :class="['tab', loginForm.loginMethod === 'password' ? 'active' : '']"-->
@click="loginForm.loginMethod = 'password'" <!-- @click="loginForm.loginMethod = 'password'"-->
> <!-- >-->
密码登录 <!-- 密码登录-->
</view> <!-- </view>-->
</view> </view>
<!-- 登录表单区域 --> <!-- 登录表单区域 -->
@@ -89,7 +89,7 @@
/> />
</view> </view>
<!-- 验证码输入框 --> <!-- 登录验证码输入和发送入口 -->
<view class="input-box" v-if="loginForm.loginMethod==='sms'"> <view class="input-box" v-if="loginForm.loginMethod==='sms'">
<t-icon name="shield-error" size="20px" class="input-icon"/> <t-icon name="shield-error" size="20px" class="input-icon"/>
<input <input
@@ -105,7 +105,7 @@
</view> </view>
<!-- 密码输入框 --> <!-- 密码输入框 -->
<view class="input-box" v-else> <view class="input-box" v-if="loginForm.loginMethod==='password'">
<t-icon name="user-password" size="20px" class="input-icon"/> <t-icon name="user-password" size="20px" class="input-icon"/>
<input <input
class="input" class="input"
@@ -131,8 +131,8 @@
<!-- 底部信息 --> <!-- 底部信息 -->
<view class="footer-info"> <view class="footer-info">
<view class="footer-item">主办单位:浙江省教育工会</view> <view class="footer-item">主办单位:{{ activityOrganizer.hostUnit || '--' }}</view>
<view class="footer-item">承办单位:杭州医学院工会</view> <view class="footer-item">承办单位:{{ activityOrganizer.coHostUnit || '--' }}</view>
<view class="footer-item">技术支持:武汉地大三思科技有限公司(智慧工会提供商)</view> <view class="footer-item">技术支持:武汉地大三思科技有限公司(智慧工会提供商)</view>
</view> </view>
</view> </view>
@@ -155,6 +155,27 @@ const loginForm = reactive({
loginMethod: 'sms' loginMethod: 'sms'
}) })
// 当前最新开启活动的主办、承办单位;没有可用活动时页面显示“--”。
const activityOrganizer = reactive({
title: '',
hostUnit: '',
coHostUnit: ''
})
// 登录页处于未登录状态,因此通过公开接口获取当前最新开启活动的单位信息。
const getLatestActivityOrganizer = () => {
request.get('/open/common/latestActivityOrganizer').then(({code, data}) => {
if (code === 0 && data) {
activityOrganizer.title = data.title || ''
activityOrganizer.hostUnit = data.hostUnit || ''
activityOrganizer.coHostUnit = data.coHostUnit || ''
}
}).catch((error) => {
// 单位信息属于辅助展示,请求失败时保留占位内容,不阻断登录流程。
console.error('获取当前活动单位信息失败', error)
})
}
// 倒计时相关 // 倒计时相关
const countdown = ref(0) const countdown = ref(0)
const timer = ref(null) const timer = ref(null)
@@ -238,6 +259,7 @@ const startCountDown = () => {
// 在页面挂载时恢复倒计时状态 // 在页面挂载时恢复倒计时状态
onMounted(() => { onMounted(() => {
restoreCountdownState() restoreCountdownState()
getLatestActivityOrganizer()
}) })
// 在页面卸载前清除定时器 // 在页面卸载前清除定时器
@@ -248,41 +270,37 @@ onBeforeUnmount(() => {
} }
}) })
// 获取验证码 // 获取登录短信验证码:手机号通过路径参数传递,接口返回 code 和 msg 表示发送结果。
async function getSmsCode() { const getSmsCode = () => {
if (!loginForm.phone) { if (!loginForm.phone) {
Toast('请输入手机号码'); Toast('请输入手机号码');
return return
} }
// 验证手机号码
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) { if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
Toast('请输入正确的手机号码'); Toast('请输入正确的手机号码');
return return
} }
if (isCountingDown.value) return if (isCountingDown.value) return
try { request.post('/client/auth/send-sms-code/' + loginForm.phone).then(({code, msg}) => {
const {code, data, msg} = await request.post('/client/auth/send-sms-code/' + loginForm.phone)
if (code === 0) { if (code === 0) {
Toast({message: '验证码发送成功', theme: 'success'}) Toast({message: '验证码发送成功', theme: 'success'})
// 开始倒计时
startCountDown() startCountDown()
} else { } else {
Toast({message: msg, theme: 'error'}) Toast({message: msg, theme: 'error'})
} }
} catch (e) { }).catch((error) => {
Toast({message: e.message, theme: 'error'}) Toast({message: error?.message || error?.msg || error?.errMsg || '验证码发送失败,请稍后重试', theme: 'error'})
} })
} }
async function handleLogin() { const handleLogin = () => {
if (!loginForm.phone) { if (!loginForm.phone) {
Toast('请输入手机号码'); Toast('请输入手机号码');
return return
} }
if (!loginForm.smsCode && loginForm.loginMethod === 'sms') { if (!loginForm.smsCode.trim() && loginForm.loginMethod === 'sms') {
Toast('请输入验证码'); Toast('请输入验证码');
return return
} }
@@ -291,39 +309,34 @@ async function handleLogin() {
return return
} }
try { userStore.smsLogin(loginForm).then(() => {
userStore.smsLogin(loginForm).then(() => { userStore.getUserInfo().then(() => {
userStore.getUserInfo().then(() => { Toast({message: '登录成功', theme: 'success'})
Toast({message: '登录成功', theme: 'success'}) const redirect = uni.getStorageSync("REDIRECT_URL")
const redirect = uni.getStorageSync("REDIRECT_URL") if(redirect){
if(redirect){ if(redirect.startsWith('/pages/index/index') || redirect.startsWith('/pages/activity/rank/index') || redirect.startsWith('/pages/profile/index')){
if(redirect.startsWith('/pages/index/index') || redirect.startsWith('/pages/activity/rank/index') || redirect.startsWith('/pages/profile/index')){
uni.switchTab({
url: redirect
})
}else{
uni.navigateTo({
url: redirect
})
}
uni.removeStorageSync("REDIRECT_URL")
}else{
uni.switchTab({ uni.switchTab({
url: '/pages/index/index' url: redirect
})
}else{
uni.navigateTo({
url: redirect
}) })
} }
}).catch(e => { uni.removeStorageSync("REDIRECT_URL")
console.log(e) }else{
Toast({message: e.message, theme: 'error'}) uni.switchTab({
}) url: '/pages/index/index'
}).catch(e => { })
}
}).catch((e) => {
console.log(e) console.log(e)
Toast({message: e.message, theme: 'error'}) Toast({message: e.message, theme: 'error'})
}) })
} catch (e) { }).catch((e) => {
console.error(e) console.log(e)
Toast({message: e.message, theme: 'error'}) Toast({message: e.message, theme: 'error'})
} })
} }
function navigateToRegister() { function navigateToRegister() {
@@ -335,32 +348,25 @@ function navigateToRegister() {
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
body {
height: 100%;
overflow: hidden;
position: fixed;
width: 100%;
top: 0;
left: 0;
}
.login-container { .login-container {
height: 100vh; min-height: 100vh;
background: #f4f2f2; background: #f4f2f2;
position: relative; position: relative;
overflow: hidden;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.title-area { .title-area {
padding: 0; padding: 0;
height: 30vh; // 固定顶部高度,长表单通过整页滚动展示,避免挤压标题区域。
height: 260px;
flex-shrink: 0;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
align-items: center; align-items: center;
background: linear-gradient(135deg, #7a1818 0%, #d03e3e 50%, #f45a5a 100%); background: linear-gradient(135deg, #00468c 0%, #0873d9 50%, var(--td-brand-color) 100%);
} }
.tech-animation { .tech-animation {
@@ -384,7 +390,7 @@ body {
} }
.main-title { .main-title {
font-size: 36px; font-size: 28px;
font-weight: bold; font-weight: bold;
margin-bottom: 8px; margin-bottom: 8px;
display: block; display: block;
@@ -410,7 +416,7 @@ body {
} }
.login-card { .login-card {
flex: 1; flex: 1 0 auto;
background-color: #ffffff; background-color: #ffffff;
border-radius: 30px 30px 0 0; border-radius: 30px 30px 0 0;
margin-top: -30px; margin-top: -30px;
@@ -436,9 +442,9 @@ body {
justify-content: center; justify-content: center;
align-items: center; align-items: center;
font-size: 15px; font-size: 15px;
color: #f45a5a; color: var(--td-brand-color);
font-weight: 500; font-weight: 500;
background-color: #fff0f0; background-color: var(--td-brand-color-light);
transition: all 0.3s ease; transition: all 0.3s ease;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
@@ -456,7 +462,7 @@ body {
.tab.active { .tab.active {
color: #fff; color: #fff;
background-color: #f45a5a; background-color: var(--td-brand-color);
} }
.form-container { .form-container {
@@ -491,7 +497,7 @@ body {
} }
.verify-code-btn { .verify-code-btn {
color: #f45a5a; color: var(--td-brand-color);
font-size: 14px; font-size: 14px;
white-space: nowrap; white-space: nowrap;
@@ -503,7 +509,7 @@ body {
.submit-btn { .submit-btn {
margin-top: 32px; margin-top: 32px;
height: 50px; height: 50px;
background-color: #f45a5a; background-color: var(--td-brand-color);
border-radius: 25px; border-radius: 25px;
color: white; color: white;
font-size: 16px; font-size: 16px;
@@ -519,13 +525,14 @@ body {
color: #666; color: #666;
.link { .link {
color: #f45a5a; color: var(--td-brand-color);
margin-left: 4px; margin-left: 4px;
} }
} }
/* 底部信息样式 */ /* 底部信息样式 */
.footer-info { .footer-info {
flex-shrink: 0;
padding: 20px 30px; padding: 20px 30px;
text-align: center; text-align: center;
background-color: #ffffff; background-color: #ffffff;
File diff suppressed because it is too large Load Diff
+10 -2
View File
@@ -41,7 +41,7 @@
</view> </view>
</view> </view>
<view style="display: flex;"> <view style="display: flex;">
<image :src="item.cover" mode="aspectFill" <image :src="getActivityCoverUrl(item.cover)" mode="aspectFill"
style="flex-shrink:0;width: 120px;height: 90px;border-radius: 5px;"></image> style="flex-shrink:0;width: 120px;height: 90px;border-radius: 5px;"></image>
<view <view
style="margin-left:10px;flex:1;overflow:hidden;display: flex;flex-direction: column;justify-content:space-around;color: #534d4d;"> style="margin-left:10px;flex:1;overflow:hidden;display: flex;flex-direction: column;justify-content:space-around;color: #534d4d;">
@@ -56,7 +56,7 @@
</view> </view>
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;"> <view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
<t-icon name="castle-7"></t-icon> <t-icon name="castle-7"></t-icon>
{{ item.coHostUnit }} {{ item.hostUnit }}
</view> </view>
</view> </view>
</view> </view>
@@ -118,6 +118,14 @@ const loading = ref(false);
const hasMore = ref(true); const hasMore = ref(true);
const refresherTriggered = ref(false); const refresherTriggered = ref(false);
// 后台保存活动封面的相对路径,H5端统一通过当前环境的文件预览代理读取。
const getActivityCoverUrl = (cover) => {
if (!cover) {
return '';
}
return '/api/open/file/upload/download?inline=true&filepath=' + encodeURIComponent(cover);
};
// 选择状态筛选 // 选择状态筛选
const selectStatusFilter = (status) => { const selectStatusFilter = (status) => {
pageForm.status = status; pageForm.status = status;
+1 -1
View File
@@ -123,7 +123,7 @@ async function updateAvatar(avatar) {
</view> </view>
<view class="logout-btn"> <view class="logout-btn">
<t-button theme="danger" size="large" block @click="handleLogout">退出登录</t-button> <t-button theme="primary" size="large" block @click="handleLogout">退出登录</t-button>
</view> </view>
<view class="footer"> <view class="footer">
Binary file not shown.

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 922 KiB

+20 -20
View File
@@ -1,14 +1,14 @@
:root, :root[theme-mode="light"] { :root, :root[theme-mode="light"] {
--td-brand-color-1: #fff0ed; --td-brand-color-1: #e8f3ff;
--td-brand-color-2: #ffd8d3; --td-brand-color-2: #cfe7ff;
--td-brand-color-3: #ffb8b1; --td-brand-color-3: #a4d2ff;
--td-brand-color-4: #ff928c; --td-brand-color-4: #70b7ff;
--td-brand-color-5: #ff5f5c; --td-brand-color-5: #1989fa;
--td-brand-color-6: #df3d3f; --td-brand-color-6: #0873d9;
--td-brand-color-7: #b6292d; --td-brand-color-7: #005bb5;
--td-brand-color-8: #8f121b; --td-brand-color-8: #00468c;
--td-brand-color-9: #6b000a; --td-brand-color-9: #003568;
--td-brand-color-10: #490005; --td-brand-color-10: #002447;
--td-brand-color-light: var(--td-brand-color-1); --td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2); --td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3); --td-brand-color-disabled: var(--td-brand-color-3);
@@ -119,16 +119,16 @@
} }
:root[theme-mode="dark"] { :root[theme-mode="dark"] {
--td-brand-color-1: #ff505020; --td-brand-color-1: #1989fa20;
--td-brand-color-2: #6b000a; --td-brand-color-2: #003568;
--td-brand-color-3: #8f121b; --td-brand-color-3: #00468c;
--td-brand-color-4: #b6292d; --td-brand-color-4: #005bb5;
--td-brand-color-5: #df3d3f; --td-brand-color-5: #0873d9;
--td-brand-color-6: #ff5050; --td-brand-color-6: #1989fa;
--td-brand-color-7: #ff928c; --td-brand-color-7: #70b7ff;
--td-brand-color-8: #ffb8b1; --td-brand-color-8: #a4d2ff;
--td-brand-color-9: #ffd8d3; --td-brand-color-9: #cfe7ff;
--td-brand-color-10: #fff0ed; --td-brand-color-10: #e8f3ff;
--td-brand-color-light: var(--td-brand-color-1); --td-brand-color-light: var(--td-brand-color-1);
--td-brand-color-focus: var(--td-brand-color-2); --td-brand-color-focus: var(--td-brand-color-2);
--td-brand-color-disabled: var(--td-brand-color-3); --td-brand-color-disabled: var(--td-brand-color-3);
+3 -2
View File
@@ -1,3 +1,4 @@
export const config = { export const config = {
fileDomain: 'https://3skj.ltd', // 后台图片字段保存相对路径,统一通过当前环境的 API 代理读取,避免绑定固定部署域名。
} fileDomain: '/api/open/file/upload/download?inline=true&filepath=',
}
+100
View File
@@ -0,0 +1,100 @@
import {Dialog} from 'tdesign-mobile-vue';
const popupHistoryStack = [];
let popupHistorySequence = 0;
let popstateListenerRegistered = false;
const canUseBrowserHistory = () => typeof window !== 'undefined' && Boolean(window.history);
// 浏览器返回时只关闭最后打开的弹框,避免直接离开当前业务页面。
const handlePopState = () => {
const popupEntry = popupHistoryStack.pop();
if (popupEntry && !popupEntry.programmaticClosing) {
popupEntry.closePopup();
}
};
const ensurePopstateListener = () => {
if (!canUseBrowserHistory() || popstateListenerRegistered) {
return;
}
window.addEventListener('popstate', handlePopState);
popstateListenerRegistered = true;
};
/**
* 为弹框增加一层浏览器历史。
*
* @param {Function} closePopup 用户通过浏览器返回键关闭弹框时执行的方法
* @return {number|null} 弹框历史记录ID;非浏览器环境返回null
*/
const pushPopupHistory = (closePopup) => {
if (!canUseBrowserHistory()) {
return null;
}
ensurePopstateListener();
popupHistorySequence += 1;
const id = popupHistorySequence;
popupHistoryStack.push({id, closePopup, programmaticClosing: false});
window.history.pushState({popupHistoryId: id}, '', window.location.href);
return id;
};
/**
* 用户点击弹框按钮时同步回退对应的浏览器历史。
*
* @param {number|null} id pushPopupHistory返回的弹框历史记录ID
* @return {void}
*/
const closePopupHistory = (id) => {
if (!canUseBrowserHistory() || id === null) {
return;
}
const popupEntry = popupHistoryStack[popupHistoryStack.length - 1];
if (!popupEntry || popupEntry.id !== id || popupEntry.programmaticClosing) {
return;
}
popupEntry.programmaticClosing = true;
window.history.back();
};
/**
* 显示带H5返回键支持的确认框。
*
* @param {Object} options TDesign确认框参数;onConfirm、onCancel、onClose分别处理确认、取消和关闭
* @return {Object|undefined} TDesign Dialog实例;服务端渲染环境可能返回undefined
*/
export const showConfirmWithHistory = (options = {}) => {
let popupHistoryId = null;
let dialogInstance = null;
const closeHistory = () => {
closePopupHistory(popupHistoryId);
};
dialogInstance = Dialog.confirm({
...options,
onConfirm: (context) => {
closeHistory();
if (options.onConfirm) {
options.onConfirm(context);
}
},
onCancel: (context) => {
closeHistory();
if (options.onCancel) {
options.onCancel(context);
}
},
onClose: (context) => {
closeHistory();
if (options.onClose) {
options.onClose(context);
}
}
});
popupHistoryId = pushPopupHistory(() => {
if (dialogInstance) {
dialogInstance.hide();
}
});
return dialogInstance;
};
+1 -1
View File
@@ -35,7 +35,7 @@ export default defineConfig({
proxy: { proxy: {
'/api': { '/api': {
// target: 'http://119.91.224.39:9090/', // target: 'http://119.91.224.39:9090/',
target: 'http://192.168.118.1:9090/', target: 'http://127.0.0.1:9090/',
changeOrigin: true, changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '') rewrite: (path) => path.replace(/^\/api/, '')
} }