Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c682984ecb | ||
|
|
7c34e41f7e | ||
|
|
c1589e4792 | ||
|
|
bdfaf01e10 | ||
|
|
b87ef27004 | ||
|
|
ba6debb4de | ||
|
|
9bbfade412 | ||
|
|
a1e49561b9 | ||
|
|
923a23b89f | ||
|
|
d82a73d2a9 | ||
|
|
9fd8d40da8 | ||
|
|
dc41b204ef | ||
|
|
33dbd9ac37 |
Generated
+10
@@ -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
@@ -0,0 +1 @@
|
||||
南邮工会羽毛球报名
|
||||
Generated
+10
@@ -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>
|
||||
Generated
+18
@@ -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>
|
||||
Generated
+7
@@ -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>
|
||||
Generated
+15
@@ -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>
|
||||
Generated
+9
@@ -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
@@ -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>
|
||||
Generated
+30
@@ -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>
|
||||
Generated
+14
@@ -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>
|
||||
Generated
+8
@@ -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
@@ -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
@@ -23,7 +23,7 @@
|
||||
<pinyin4j.veision>2.5.1</pinyin4j.veision>
|
||||
<poi.version>4.1.1</poi.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>
|
||||
</properties>
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import lombok.EqualsAndHashCode;
|
||||
import org.nutz.dao.entity.annotation.*;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@@ -34,10 +35,15 @@ public class Client_user extends BaseModel implements Serializable {
|
||||
@Comment("手机号")
|
||||
private String phone;
|
||||
|
||||
// @Column
|
||||
// @ColDefine(type = ColType.VARCHAR, width = 18)
|
||||
// @Comment("身份证")
|
||||
// private String idCard;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 18)
|
||||
@Comment("身份证")
|
||||
private String idCard;
|
||||
@ColDefine(type = ColType.DATE)
|
||||
@Comment("生日")
|
||||
private Date birthday;
|
||||
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 255)
|
||||
|
||||
@@ -13,9 +13,11 @@ public interface ClientAuthService {
|
||||
String doLogin(ClientLoginBody loginBody);
|
||||
|
||||
/**
|
||||
* 注册
|
||||
* @param registerBody 注册信息
|
||||
* @return token
|
||||
* 注册手机端用户,并在所选单位尚无简称时完成首次简称登记。
|
||||
*
|
||||
* @param registerBody 注册信息;userName为姓名、phone为手机号、sex为性别、unitId为单位ID、
|
||||
* unitAliasName为单位简称、smsCode为短信验证码
|
||||
* @return 注册成功后的客户端登录token字符串
|
||||
*/
|
||||
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.models.Client_user;
|
||||
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.service.ValidateService;
|
||||
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.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.lang.Strings;
|
||||
@@ -25,6 +30,8 @@ public class ClientAuthServiceImpl implements ClientAuthService {
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private ValidateService validateService;
|
||||
@Inject
|
||||
private SysUnitService sysUnitService;
|
||||
|
||||
@Override
|
||||
public String doLogin(ClientLoginBody loginBody) {
|
||||
@@ -49,6 +56,9 @@ public class ClientAuthServiceImpl implements ClientAuthService {
|
||||
}
|
||||
|
||||
if (loginBody.getLoginMethod().equals("password")) {
|
||||
if (Strings.isBlank(loginBody.getPassword())) {
|
||||
throw new BaseException("请输入密码");
|
||||
}
|
||||
String hashedPassword = PwdUtil.getPassword(loginBody.getPassword(), user.getSalt());
|
||||
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
|
||||
throw new BaseException("密码不正确");
|
||||
@@ -61,17 +71,43 @@ public class ClientAuthServiceImpl implements ClientAuthService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public String doRegister(ClientRegisterBody registerBody) {
|
||||
int count = dao.count(Client_user.class, Cnd.where(Client_user::getPhone, "=", registerBody.getPhone()));
|
||||
if (count > 0) {
|
||||
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());
|
||||
|
||||
// 只允许首次登记简称;并发注册时通过数据库条件保证已有简称不会被后续请求覆盖。
|
||||
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);
|
||||
int gender = IdcardUtil.getGenderByIdCard(user.getIdCard());
|
||||
user.setSex(gender==1?"男":"女");
|
||||
// int gender = IdcardUtil.getGenderByIdCard(user.getIdCard());
|
||||
// user.setSex(gender==1?"男":"女");
|
||||
dao.insert(user);
|
||||
StpClientUtil.STP.login(user.getId());
|
||||
StpClientUtil.STP.checkLogin();
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.budwk.app.client.auth.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
public class ClientRegisterBody {
|
||||
|
||||
@@ -11,11 +13,15 @@ public class ClientRegisterBody {
|
||||
|
||||
private String phone;
|
||||
|
||||
private String idCard;
|
||||
// private String idCard;
|
||||
private Date birthday;
|
||||
|
||||
private String avatar;
|
||||
|
||||
private String unitId;
|
||||
|
||||
/** 注册时填写的单位简称,仅在所选单位尚未设置简称时写入单位资料。 */
|
||||
private String unitAliasName;
|
||||
|
||||
private String smsCode;
|
||||
}
|
||||
|
||||
+72
-1
@@ -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.module.activity.models.ActUser;
|
||||
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 lombok.Synchronized;
|
||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
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.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.mvc.adaptor.JsonAdaptor;
|
||||
import org.nutz.mvc.annotation.AdaptBy;
|
||||
@@ -38,6 +41,8 @@ public class ClientActivityController {
|
||||
@Inject
|
||||
private ClientActivityService clientActivityService;
|
||||
@Inject
|
||||
private ActivityRegistrationFieldService activityRegistrationFieldService;
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@At
|
||||
@@ -54,6 +59,7 @@ public class ClientActivityController {
|
||||
t1.signStartTime,
|
||||
t1.signEndTime,
|
||||
t1.coHostUnit,
|
||||
t1.hostUnit,
|
||||
t1.cover
|
||||
FROM
|
||||
activity t1
|
||||
@@ -61,6 +67,8 @@ public class ClientActivityController {
|
||||
""");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.and(Cnd.likeEX("t1.title", pageForm.getSearchKeyword()));
|
||||
// 客户端活动列表只展示后台已开启的活动。
|
||||
cnd.and("t1.isOpen", "=", true);
|
||||
sql.setCondition(cnd);
|
||||
Pagination pagination = clientActivityService.listPageMap(pageForm.getPageNumber(), pageForm.getPageSize(), sql);
|
||||
return Result.success(pagination);
|
||||
@@ -70,7 +78,13 @@ public class ClientActivityController {
|
||||
@GET
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
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");
|
||||
return Result.success(activity);
|
||||
}
|
||||
@@ -83,6 +97,7 @@ public class ClientActivityController {
|
||||
@AdaptBy(type = JsonAdaptor.class)
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
@Synchronized
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public synchronized Result signUp(ClientActivitySignVO signVO) {
|
||||
clientActivityService.signUp(signVO);
|
||||
return Result.success();
|
||||
@@ -108,6 +123,62 @@ public class ClientActivityController {
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否报名
|
||||
*/
|
||||
|
||||
+68
-7
@@ -1,6 +1,7 @@
|
||||
package com.budwk.app.client.module.activity.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.client.module.activity.service.ClientActivityService;
|
||||
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.Ok;
|
||||
|
||||
import java.util.List;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author zhf
|
||||
@@ -63,7 +66,6 @@ public class ClientGroupController {
|
||||
LEFT JOIN act_user t2 ON t1.id = t2.activityId
|
||||
WHERE
|
||||
t2.userId = @userId
|
||||
AND t2.isSignIn =1
|
||||
""").setParam("userId", StpClientUtil.STP.getLoginIdAsLong());
|
||||
List<NutMap> list = clientActivityService.listMap(sql);
|
||||
return Result.success(list);
|
||||
@@ -84,9 +86,14 @@ public class ClientGroupController {
|
||||
// return Result.error("您还没加入任何团队");
|
||||
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());
|
||||
NutMap map = (NutMap) dao.execute(sql).getResult();
|
||||
|
||||
if (Lang.isEmpty(map)) {
|
||||
map = NutMap.NEW();
|
||||
}
|
||||
|
||||
Sql sqlGroupUser = Sqls.create("""
|
||||
SELECT
|
||||
t1.*,
|
||||
@@ -102,7 +109,7 @@ public class ClientGroupController {
|
||||
WHERE t1.groupId=@groupId
|
||||
ORDER BY
|
||||
t1.isLeader DESC
|
||||
""").setParam("groupId", map.getString("id"));
|
||||
""").setParam("groupId", map.getString("id", ""));
|
||||
List<NutMap> groupUsers = clientActivityService.listMap(sqlGroupUser);
|
||||
map.put("groupUsers", groupUsers);
|
||||
NutMap leader = groupUsers.stream().filter(nutMap -> nutMap.getBoolean("isLeader")).findFirst().orElse(null);
|
||||
@@ -118,7 +125,11 @@ public class ClientGroupController {
|
||||
@At("/apply-leader/?")
|
||||
@AdaptBy(type = JsonAdaptor.class)
|
||||
@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)
|
||||
.and(ActGroupUser::getIsLeader, "=", 1));
|
||||
if (count > 0){
|
||||
@@ -165,7 +176,57 @@ public class ClientGroupController {
|
||||
@At("/list-group-rank/?")
|
||||
@SaCheckLogin(type = StpClientUtil.TYPE)
|
||||
public Result listGroupRank(Long activityId) {
|
||||
List<ActGroup> groupList = dao.query(ActGroup.class, Cnd.where(ActGroup::getActivityId, "=", activityId).desc(ActGroup::getTotalScore));
|
||||
return Result.success(groupList);
|
||||
if (activityId == null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+24
@@ -19,6 +19,30 @@ public interface ClientActivityService extends BaseService<Activity> {
|
||||
*/
|
||||
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);
|
||||
|
||||
/**
|
||||
* 报名签到
|
||||
*/
|
||||
|
||||
+254
-17
@@ -2,29 +2,39 @@ package com.budwk.app.client.module.activity.service.impl;
|
||||
|
||||
import cn.hutool.core.util.IdcardUtil;
|
||||
import com.budwk.app.base.exception.BaseException;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.client.auth.models.Client_user;
|
||||
import com.budwk.app.client.module.activity.service.ClientActivityService;
|
||||
import com.budwk.app.client.module.activity.vo.ClientActivitySignVO;
|
||||
import com.budwk.app.module.activity.models.ActUser;
|
||||
import com.budwk.app.module.activity.models.Activity;
|
||||
import com.budwk.app.module.activity.enums.RegistrationFieldDefinition;
|
||||
import com.budwk.app.module.activity.service.ActivityRegistrationFieldService;
|
||||
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.Dao;
|
||||
import org.nutz.dao.Sqls;
|
||||
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.lang.Lang;
|
||||
import org.nutz.lang.Strings;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@IocBean(args = {"refer:dao"})
|
||||
public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> implements ClientActivityService {
|
||||
@Inject
|
||||
private ActivityRegistrationFieldService activityRegistrationFieldService;
|
||||
|
||||
public ClientActivityServiceImpl(Dao dao) {
|
||||
super(dao);
|
||||
}
|
||||
@@ -46,24 +56,139 @@ public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> impleme
|
||||
}
|
||||
|
||||
@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) {
|
||||
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()
|
||||
.collect(Collectors.groupingBy(ActUser::getIdCard, Collectors.counting()))
|
||||
.values().stream()
|
||||
.anyMatch(count -> count > 1);
|
||||
// boolean hasDuplicateIdCard = userList.stream()
|
||||
// .collect(Collectors.groupingBy(ActUser::getIdCard, Collectors.counting()))
|
||||
// .values().stream()
|
||||
// .anyMatch(count -> count > 1);
|
||||
|
||||
// 检查是否有重复的手机号
|
||||
boolean hasDuplicatePhone = userList.stream()
|
||||
.filter(item -> Strings.isNotBlank(item.getPhone()))
|
||||
.collect(Collectors.groupingBy(ActUser::getPhone, Collectors.counting()))
|
||||
.values().stream()
|
||||
.anyMatch(count -> count > 1);
|
||||
|
||||
if(hasDuplicateIdCard){
|
||||
throw new BaseException("身份证号重复");
|
||||
}
|
||||
// if(hasDuplicateIdCard){
|
||||
// throw new BaseException("身份证号重复");
|
||||
// }
|
||||
|
||||
if(hasDuplicatePhone){
|
||||
throw new BaseException("手机号重复");
|
||||
@@ -71,22 +196,24 @@ public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> impleme
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
Client_user selfUser = dao().fetch(Client_user.class, StpClientUtil.STP.getLoginIdAsLong());
|
||||
dao().fetchLinks(selfUser, "unit");
|
||||
|
||||
// 首先检查报名人员是否已经在用户表中
|
||||
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) {
|
||||
// 创建用户
|
||||
clientUser = new Client_user();
|
||||
clientUser.setUserName(user.getUserName());
|
||||
clientUser.setSex(user.getSex());
|
||||
clientUser.setPhone(user.getPhone());
|
||||
clientUser.setIdCard(user.getIdCard());
|
||||
clientUser.setBirthday(user.getBirthday());
|
||||
// clientUser.setIdCard(user.getIdCard());
|
||||
clientUser.setUnitId(selfUser.getUnitId());
|
||||
int gender = IdcardUtil.getGenderByIdCard(clientUser.getIdCard());
|
||||
clientUser.setSex(gender==1?"男":"女");
|
||||
// int gender = IdcardUtil.getGenderByIdCard(clientUser.getIdCard());
|
||||
// clientUser.setSex(gender==1?"男":"女");
|
||||
clientUser.setSex(user.getSex());
|
||||
dao().insert(clientUser);
|
||||
} else {
|
||||
// 验证用户之前是不是已经报名 别人帮报 自己报 管理员报
|
||||
@@ -107,9 +234,15 @@ public class ClientActivityServiceImpl extends BaseServiceImpl<Activity> impleme
|
||||
user.setSignUpUserId(StpClientUtil.STP.getLoginIdAsLong());
|
||||
user.setSignUpTime(now);
|
||||
|
||||
if(!user.getTraffic().equals("自驾")){
|
||||
if (!"自驾".equals(user.getTraffic())) {
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按活动字段配置校验一名报名人员。
|
||||
* 字段未启用时不校验;关联字段仅在父字段满足显示条件时参与必填校验。
|
||||
*/
|
||||
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
|
||||
public void signIn(Long activityId) {
|
||||
ActUser actUser = dao().fetch(ActUser.class, Cnd.where(ActUser::getActivityId, "=", activityId).and(ActUser::getUserId, "=", StpClientUtil.STP.getLoginIdAsLong()));
|
||||
|
||||
+2
-2
@@ -60,7 +60,7 @@ public class ActBigScreenController {
|
||||
@Ok("json:full")
|
||||
public Result score(Long activityId) {
|
||||
if (activityId == null) {
|
||||
return Result.error("活动ID不能为空");
|
||||
return Result.error("未获取到报名活动");
|
||||
}
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
@@ -106,7 +106,7 @@ public class ActBigScreenController {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
+98
-4
@@ -1,13 +1,14 @@
|
||||
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.hutool.core.util.StrUtil;
|
||||
import com.budwk.app.base.page.PageForm;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.module.activity.models.ActGroup;
|
||||
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.models.*;
|
||||
import com.budwk.app.module.activity.service.ActGroupService;
|
||||
import com.budwk.app.module.activity.service.ActUserService;
|
||||
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.web.commons.slog.annotation.SLog;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
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.Sqls;
|
||||
@@ -29,7 +32,10 @@ import org.nutz.mvc.annotation.At;
|
||||
import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.validation.Valid;
|
||||
import java.math.BigDecimal;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
@@ -154,6 +160,7 @@ public class ActGroupController {
|
||||
Sql sql = Sqls.create("""
|
||||
SELECT
|
||||
u.*,
|
||||
TIMESTAMPDIFF(YEAR, STR_TO_DATE(SUBSTRING(u.idCard, 7, 8), '%Y%m%d'), CURDATE()) AS age,
|
||||
agu.isLeader
|
||||
FROM
|
||||
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
|
||||
@SaCheckPermission("activity.group")
|
||||
public Result queryUnGroupUser(String activityId) {
|
||||
@@ -230,6 +251,8 @@ public class ActGroupController {
|
||||
for (int i = 0; i < actGroups.length; i++) {
|
||||
actGroups[i].setActivityId(activityId);
|
||||
actGroups[i].setSort(i + 1);
|
||||
// 设置为非公开
|
||||
actGroups[i].setIsOpen(false);
|
||||
dao.insert(actGroups[i]);
|
||||
for (ActGroupUser user : actGroups[i].getGroupUsers()) {
|
||||
user.setActivityId(activityId);
|
||||
@@ -274,4 +297,75 @@ public class ActGroupController {
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-5
@@ -65,11 +65,7 @@ public class ActScoreEnterController {
|
||||
@SaCheckPermission("activity.scoreEnter")
|
||||
@ApiOperation(value = "获取活动列表", nickname = "/list-activity", httpMethod = "GET")
|
||||
public Result listAct(Integer year) {
|
||||
Sql sql = Sqls.create("select id,title from activity $condition");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("YEAR(startTime)", "=", year);
|
||||
sql.setCondition(cnd);
|
||||
return Result.success(activityService.listMap(sql));
|
||||
return Result.success(activityService.listByYearOrderByLatest(year));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+4
-6
@@ -23,6 +23,7 @@ import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@IocBean
|
||||
@At("/platform/activity/scoreEntry")
|
||||
@@ -45,11 +46,7 @@ public class ActScoreEntryController {
|
||||
@SaCheckPermission("activity.scoreEntry")
|
||||
@ApiOperation(value = "获取活动列表", nickname = "/list-activity", httpMethod = "GET")
|
||||
public Result listAct(Integer year) {
|
||||
Sql sql = Sqls.create("select id,title from activity $condition");
|
||||
Cnd cnd = Cnd.NEW();
|
||||
cnd.andEX("YEAR(startTime)", "=", year);
|
||||
sql.setCondition(cnd);
|
||||
return Result.success(activityService.listMap(sql));
|
||||
return Result.success(activityService.listByYearOrderByLatest(year));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +103,8 @@ public class ActScoreEntryController {
|
||||
//在找出这个队伍下面所有的分数
|
||||
List<ActItemGroupScore> groupScoreList = scoreList.stream().filter(itemScore -> itemScore.getGroupId().equals(groupId)).toList();
|
||||
BigDecimal totalScore = groupScoreList.stream()
|
||||
.map(ActItemGroupScore::getScore) // 提取每个 score(BigDecimal)
|
||||
.map(ActItemGroupScore::getScore)
|
||||
.filter(Objects::nonNull) // 把 null 踢掉
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
dao.update(ActGroup.class, Chain.make("totalScore", totalScore), Cnd.where(ActGroup::getId, "=", groupId));
|
||||
}
|
||||
|
||||
+74
-3
@@ -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.Activity;
|
||||
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.ActUserPageForm;
|
||||
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.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
@@ -61,6 +66,10 @@ public class ActUserController {
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private ActUserService actUserService;
|
||||
@Inject
|
||||
private ActivityManageService activityManageService;
|
||||
@Inject
|
||||
private ActivityRegistrationFieldService activityRegistrationFieldService;
|
||||
|
||||
@At("")
|
||||
@SaCheckPermission("activity.user")
|
||||
@@ -124,7 +133,9 @@ public class ActUserController {
|
||||
user.setActivityId(activityId);
|
||||
user.setUnitName(vo.getUnitName());
|
||||
}
|
||||
dao.insertOrUpdate(doSignUserList);
|
||||
if (Lang.isNotEmpty(doSignUserList)) {
|
||||
dao.insertOrUpdate(doSignUserList);
|
||||
}
|
||||
}
|
||||
|
||||
// 找出不在系统的人,添加到client_user表中,再添加报名记录
|
||||
@@ -202,11 +213,26 @@ public class ActUserController {
|
||||
*/
|
||||
@At
|
||||
@SaCheckPermission("activity.user")
|
||||
public Result getActivityList(String year) {
|
||||
List<Activity> activityList = dao.query(Activity.class, Cnd.NEW().andEX("YEAR(startTime)", "=", year).and(Activity::getDelFlag, "=", false));
|
||||
public Result getActivityList(Integer year) {
|
||||
List<Activity> activityList = activityManageService.listByYearOrderByLatest(year);
|
||||
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
|
||||
@SaCheckPermission("activity.user")
|
||||
@@ -232,16 +258,60 @@ public class ActUserController {
|
||||
|
||||
Sql sql = actUserService.getSignUpSql(pageForm);
|
||||
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<>() {{
|
||||
// add(new ExcelExportEntity("报名号", "actUserNo", 20));
|
||||
add(new ExcelExportEntity("姓名", "userName", 20));
|
||||
add(new ExcelExportEntity("性别", "sex", 20));
|
||||
add(new ExcelExportEntity("人员类型", "personTypeDesc", 20));
|
||||
add(new ExcelExportEntity("项目类型", "projectTypeDesc", 30));
|
||||
add(new ExcelExportEntity("身份证号码", "idCard", 20));
|
||||
add(new ExcelExportEntity("联系方式", "phone", 20));
|
||||
add(new ExcelExportEntity("职务", "position", 20));
|
||||
add(new ExcelExportEntity("所属单位", "unitName", 20));
|
||||
add(new ExcelExportEntity("报名时间", "signUpTime2", 20));
|
||||
add(new ExcelExportEntity("是否签到", "isSignInDesc", 20));
|
||||
add(new ExcelExportEntity("是否住宿", "isStayDesc", 20));
|
||||
// add(new ExcelExportEntity("是否展示才艺", "isShowSkillDesc", 20));
|
||||
// add(new ExcelExportEntity("才艺名称", "skillName", 20));
|
||||
// add(new ExcelExportEntity("所需器材", "equip", 20));
|
||||
@@ -251,6 +321,7 @@ public class ActUserController {
|
||||
add(new ExcelExportEntity("接站地址", "stationAddress", 20));
|
||||
|
||||
add(new ExcelExportEntity("是否参加茶叙会", "isPartyDesc", 20));
|
||||
add(new ExcelExportEntity("是否联络人", "isSIgnUpUser", 20));
|
||||
}};
|
||||
response.setContentType("application/octet-stream");
|
||||
response.setHeader("Content-Disposition", "attachment;filename=" + new String((activity.getTitle() + "报名数据表.xlsx").getBytes(StandardCharsets.UTF_8), "ISO8859-1"));
|
||||
|
||||
+10
-7
@@ -86,9 +86,7 @@ public class ActWonderfulMomentController {
|
||||
|
||||
@At
|
||||
public Result getWonderfulMoment(@Param("id") Long id) {
|
||||
Cnd cnd = Cnd.where("published", "=", true).andEX("id", "=", id);
|
||||
cnd.desc("publishTime");
|
||||
List<ActWonderfulMoment> momentList = wonderfulMomentService.query(cnd);
|
||||
List<ActWonderfulMoment> momentList = wonderfulMomentService.listClientVisible(id);
|
||||
List<NutMap> result = new ArrayList<>();
|
||||
for (ActWonderfulMoment wonderfulMoment : momentList) {
|
||||
NutMap nutMap = Lang.obj2map(wonderfulMoment, NutMap.class);
|
||||
@@ -106,15 +104,20 @@ public class ActWonderfulMomentController {
|
||||
|
||||
@At("/getWonderfulMomentById/?")
|
||||
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);
|
||||
}
|
||||
|
||||
@At("/handleWonderfulMoment/?")
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public Result handleWonderfulMoment(Long id) {
|
||||
ActWonderfulMoment moment = wonderfulMomentService.fetch(id);
|
||||
moment.setReadCount(moment.getReadCount() + 1);
|
||||
wonderfulMomentService.update(moment);
|
||||
int updatedRows = wonderfulMomentService.increaseClientReadCount(id);
|
||||
if (updatedRows == 0) {
|
||||
return Result.error("精彩瞬间不存在或所属活动已关闭");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -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.ActivityItem;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
@@ -42,6 +43,8 @@ public class ActivityItemApplyQueryController {
|
||||
|
||||
@Inject
|
||||
private ActivitySignInQueryService activitySignInQueryService;
|
||||
@Inject
|
||||
private ActivityManageService activityManageService;
|
||||
|
||||
@At("")
|
||||
@SaCheckPermission("activity.itemApplyQuery")
|
||||
@@ -52,7 +55,7 @@ public class ActivityItemApplyQueryController {
|
||||
@At()
|
||||
@SaCheckPermission("activity.signInUserQuery")
|
||||
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()
|
||||
|
||||
+62
-16
@@ -2,12 +2,13 @@ package com.budwk.app.module.activity.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
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.Pagination;
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.module.activity.models.Activity;
|
||||
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.web.commons.auth.satoken.StpClientUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -22,7 +23,7 @@ import org.nutz.mvc.annotation.Ok;
|
||||
import org.nutz.mvc.annotation.Param;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@IocBean
|
||||
@@ -35,6 +36,8 @@ public class ActivityManageController {
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private ActivityManageService activityService;
|
||||
@Inject
|
||||
private ActivityRegistrationFieldService activityRegistrationFieldService;
|
||||
|
||||
@At("")
|
||||
@SaCheckPermission("activity.manager")
|
||||
@@ -91,6 +94,27 @@ public class ActivityManageController {
|
||||
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)
|
||||
@SaCheckPermission("activity.manager")
|
||||
public Result updateItem(Long id, @Param("items") ActivityItem[] items) {
|
||||
// 查询出旧的活动信息
|
||||
List<ActivityItem> oldItems = dao.query(ActivityItem.class, Cnd.where(ActivityItem::getActivityId, "=", id));
|
||||
// 更新项目信息
|
||||
List<Long> oldItemIdList = oldItems.stream().map(ActivityItem::getId).toList();
|
||||
// 更新项目信息
|
||||
for (ActivityItem item : items) {
|
||||
item.setActivityId(id);
|
||||
activityService.updateItems(id,
|
||||
items == null ? Collections.emptyList() : Arrays.asList(items));
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询活动报名字段配置。
|
||||
*
|
||||
* @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);
|
||||
// 找出需要新增的 找出需要删除的
|
||||
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));
|
||||
return Result.success(activityRegistrationFieldService.getFieldConfig(id, false, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存活动报名字段配置。
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
||||
@At
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -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.ActivityItem;
|
||||
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 lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
@@ -39,6 +41,10 @@ public class ActivitySignInQueryController {
|
||||
|
||||
@Inject
|
||||
private ActivitySignInQueryService activitySignInQueryService;
|
||||
@Inject
|
||||
private ActivityManageService activityManageService;
|
||||
@Inject
|
||||
private ActivityRegistrationFieldService activityRegistrationFieldService;
|
||||
|
||||
@At("")
|
||||
@SaCheckPermission("activity.signInUserQuery")
|
||||
@@ -50,7 +56,22 @@ public class ActivitySignInQueryController {
|
||||
@At()
|
||||
@SaCheckPermission("activity.signInUserQuery")
|
||||
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()
|
||||
|
||||
+69
@@ -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")
|
||||
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 java.time.LocalDateTime;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* @version 1.0
|
||||
@@ -55,6 +56,16 @@ public class ActUser extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 10)
|
||||
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
|
||||
@Comment("职务")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
@@ -65,6 +76,11 @@ public class ActUser extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||
private String idCard;
|
||||
|
||||
@Column
|
||||
@Comment("出生日期")
|
||||
@ColDefine(type = ColType.DATE)
|
||||
private Date birthday;
|
||||
|
||||
@Column
|
||||
@Comment("联系方式")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 30)
|
||||
|
||||
@@ -23,6 +23,10 @@ public class ActWonderfulMoment extends BaseModel {
|
||||
@Comment("ID")
|
||||
private Long id;
|
||||
|
||||
@Column
|
||||
@Comment("所属活动ID")
|
||||
private Long activityId;
|
||||
|
||||
@Column
|
||||
@Comment("标题")
|
||||
@ColDefine(type = ColType.VARCHAR, width = 100)
|
||||
|
||||
@@ -90,6 +90,12 @@ public class Activity extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 500)
|
||||
private String cover;
|
||||
|
||||
@Comment("是否开启")
|
||||
@Column
|
||||
@Default(value = "1")
|
||||
@ColDefine(type = ColType.BOOLEAN)
|
||||
private Boolean isOpen = true;
|
||||
|
||||
@Comment("附件")
|
||||
@Column
|
||||
@ColDefine(type = ColType.MYSQL_JSON)
|
||||
|
||||
@@ -26,6 +26,16 @@ public class ActivityItem extends BaseModel {
|
||||
@ColDefine(type = ColType.VARCHAR, width = 30)
|
||||
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("比赛方法")
|
||||
@Column
|
||||
@ColDefine(type = ColType.VARCHAR, width = 300)
|
||||
|
||||
@@ -24,6 +24,14 @@ public interface ActGroupService extends BaseService<ActGroup> {
|
||||
*/
|
||||
void groupSetting(ActGroupVo groupVo);
|
||||
|
||||
/**
|
||||
* 按单位重建活动全部有效报名人员的分组,同一单位只生成一个组。
|
||||
*
|
||||
* @param activityId 活动ID;不限制人员签到状态
|
||||
* @throws com.budwk.app.base.exception.BaseException 活动或报名数据不满足分组条件时抛出
|
||||
*/
|
||||
void groupByUnit(Long activityId);
|
||||
|
||||
/**
|
||||
* 设置未分组人员
|
||||
* @param unGroupUserVo
|
||||
|
||||
+26
@@ -3,6 +3,8 @@ package com.budwk.app.module.activity.service;
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
import com.budwk.app.module.activity.models.ActWonderfulMoment;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @ClassName ActWonderfulMomentService
|
||||
* @Author JyuHsin
|
||||
@@ -11,4 +13,28 @@ import com.budwk.app.module.activity.models.ActWonderfulMoment;
|
||||
* @Description TODO
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
+38
@@ -2,6 +2,44 @@ package com.budwk.app.module.activity.service;
|
||||
|
||||
import com.budwk.app.base.service.BaseService;
|
||||
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> {
|
||||
|
||||
/**
|
||||
* 查询指定年度的有效活动,并将最后创建的活动排在最前面。
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
+105
@@ -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);
|
||||
}
|
||||
+196
-27
@@ -1,7 +1,10 @@
|
||||
package com.budwk.app.module.activity.service.impl;
|
||||
|
||||
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.exception.BaseException;
|
||||
import com.budwk.app.module.activity.models.*;
|
||||
import com.budwk.app.module.activity.service.ActGroupService;
|
||||
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.loader.annotation.IocBean;
|
||||
import org.nutz.lang.util.NutMap;
|
||||
import org.nutz.lang.Strings;
|
||||
|
||||
import java.util.*;
|
||||
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())
|
||||
.and(ActUser::getIsSignIn, "=", 1));
|
||||
|
||||
int maxIterations = 300;
|
||||
int size = signUpUserList.size();
|
||||
Integer oneGroupLimitUserNum = groupVo.getOneGroupLimitUserNum();
|
||||
|
||||
@@ -57,44 +62,26 @@ public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements Ac
|
||||
groups.add(new ArrayList<>());
|
||||
}
|
||||
|
||||
// 分离男女用户
|
||||
// 分离男女
|
||||
Map<String, List<ActUser>> genderMap = signUpUserList.stream()
|
||||
.collect(Collectors.groupingBy(user -> "男".equals(user.getSex()) ? "男" : "女"));
|
||||
|
||||
List<ActUser> males = genderMap.getOrDefault("男", Collections.emptyList());
|
||||
List<ActUser> females = genderMap.getOrDefault("女", Collections.emptyList());
|
||||
List<ActUser> males = new ArrayList<>(genderMap.getOrDefault("男", Collections.emptyList()));
|
||||
List<ActUser> females = new ArrayList<>(genderMap.getOrDefault("女", Collections.emptyList()));
|
||||
|
||||
// 随机化列表
|
||||
// 保持原始顺序或打乱(避免系统性偏差)
|
||||
Collections.shuffle(males);
|
||||
Collections.shuffle(females);
|
||||
|
||||
// 尝试分配
|
||||
List<ActUser> unassignedUsers = new ArrayList<>();
|
||||
|
||||
int maleIndex = 0, femaleIndex = 0;
|
||||
while (maleIndex < males.size() || femaleIndex < females.size()) {
|
||||
boolean assigned = false;
|
||||
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();
|
||||
// 第一阶段:贪心初始化分配
|
||||
assignUsersGreedy(groups, males, groupVo.getManUserNum(), oneGroupLimitUserNum, unassignedUsers);
|
||||
assignUsersGreedy(groups, females, groupVo.getWomanUserNum(), oneGroupLimitUserNum, unassignedUsers);
|
||||
|
||||
if (maleIndex < males.size() && (groupVo.getManUserNum() == -1 || currentMaleCount < groupVo.getManUserNum())) {
|
||||
group.add(males.get(maleIndex++));
|
||||
assigned = true;
|
||||
} else if (femaleIndex < females.size() && (groupVo.getWomanUserNum() == -1 || currentFemaleCount < groupVo.getWomanUserNum())) {
|
||||
group.add(females.get(femaleIndex++));
|
||||
assigned = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!assigned) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 第二阶段:组间交换优化(同性别交换,降低方差)
|
||||
optimizeBySwapping(groups, maxIterations);
|
||||
|
||||
unassignedUsers.addAll(males.subList(maleIndex, males.size()));
|
||||
unassignedUsers.addAll(females.subList(femaleIndex, females.size()));
|
||||
|
||||
// 往组别表里面添加记录
|
||||
List<ActGroup> groupList = IntStream.range(0, groups.size())
|
||||
@@ -112,6 +99,7 @@ public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements Ac
|
||||
}).collect(Collectors.toList());
|
||||
|
||||
actGroup.setGroupUsers(userList);
|
||||
actGroup.setIsOpen(false);
|
||||
return actGroup;
|
||||
}).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
|
||||
@Aop(TransAop.READ_COMMITTED)
|
||||
public void unGroupUserSetting(ActUnGroupUserVo unGroupUserVo) {
|
||||
@@ -252,4 +296,129 @@ public class ActGroupServiceImpl extends BaseServiceImpl<ActGroup> implements Ac
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -59,6 +59,10 @@ public class ActUserServiceImpl extends BaseServiceImpl<ActUser> implements ActU
|
||||
WHEN 1 THEN '是'
|
||||
WHEN 0 THEN ''
|
||||
END AS isPartyDesc,
|
||||
CASE
|
||||
WHEN u.signUpUserId = u.userId THEN
|
||||
'是' ELSE ''
|
||||
END AS isSIgnUpUser,
|
||||
act.`title` AS activityName
|
||||
FROM
|
||||
act_user u
|
||||
@@ -75,6 +79,15 @@ public class ActUserServiceImpl extends BaseServiceImpl<ActUser> implements ActU
|
||||
}
|
||||
cnd.andEX("u.isSignIn", "=", pageForm.getIsSignIn());
|
||||
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);
|
||||
return sql;
|
||||
}
|
||||
|
||||
+44
@@ -3,9 +3,15 @@ package com.budwk.app.module.activity.service.impl;
|
||||
import com.budwk.app.base.service.impl.BaseServiceImpl;
|
||||
import com.budwk.app.module.activity.models.ActWonderfulMoment;
|
||||
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.util.cri.Static;
|
||||
import org.nutz.ioc.aop.Aop;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @ClassName ActWonderfulMomentServiceImpl
|
||||
* @Author JyuHsin
|
||||
@@ -18,4 +24,42 @@ public class ActWonderfulMomentServiceImpl extends BaseServiceImpl<ActWonderfulM
|
||||
public ActWonderfulMomentServiceImpl(Dao 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)"));
|
||||
}
|
||||
}
|
||||
|
||||
+173
@@ -1,14 +1,187 @@
|
||||
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.module.activity.models.Activity;
|
||||
import com.budwk.app.module.activity.models.ActivityItem;
|
||||
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.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.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"})
|
||||
public class ActivityManageServiceImpl extends BaseServiceImpl<Activity> implements ActivityManageService {
|
||||
@Inject
|
||||
private SysDictService sysDictService;
|
||||
|
||||
public ActivityManageServiceImpl(Dao 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()));
|
||||
}
|
||||
}
|
||||
|
||||
+634
@@ -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 {
|
||||
Sys_user user = sysUserService.fetch(id);
|
||||
String salt = R.UU32();
|
||||
String pwd = R.captchaNumber(6);
|
||||
String pwd = "wdxxmm@123";
|
||||
String hashedPasswordBase64 = PwdUtil.getPassword(pwd, salt);
|
||||
sysUserService.update(Chain.make("salt", salt).add("password", hashedPasswordBase64), Cnd.where("id", "=", id));
|
||||
sysUserService.deleteCache(user.getId());
|
||||
|
||||
@@ -261,6 +261,9 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
||||
if (user.isDisabled()) {
|
||||
throw new BaseException("用户被禁用");
|
||||
}
|
||||
if (Strings.isBlank(passowrd)) {
|
||||
throw new BaseException("请输入密码");
|
||||
}
|
||||
String hashedPassword = PwdUtil.getPassword(passowrd, user.getSalt());
|
||||
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
|
||||
throw new BaseException("密码不正确");
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.budwk.app.web.controllers.open;
|
||||
|
||||
import com.budwk.app.base.result.Result;
|
||||
import com.budwk.app.module.activity.service.ActivityManageService;
|
||||
import com.budwk.app.sys.models.Sys_unit;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
@@ -19,10 +20,24 @@ public class OpenCommonController {
|
||||
@Inject
|
||||
private Dao dao;
|
||||
|
||||
@Inject
|
||||
private ActivityManageService activityManageService;
|
||||
|
||||
@At
|
||||
public Result units() {
|
||||
List<Sys_unit> units = dao.query(Sys_unit.class, Cnd.where(Sys_unit::getIsEnable, "=", 1).asc(Sys_unit::getId));
|
||||
return Result.success(units);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端登录页显示的当前最新开启活动单位信息。
|
||||
*
|
||||
* @return JSON 结果;data.title 为活动标题,data.hostUnit 为主办单位,
|
||||
* data.coHostUnit 为承办单位
|
||||
*/
|
||||
@At
|
||||
public Result latestActivityOrganizer() {
|
||||
return Result.success(activityManageService.getLatestOpenActivityOrganizer());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-3
@@ -135,10 +135,17 @@ public class UploadController {
|
||||
}
|
||||
|
||||
|
||||
//下载文件
|
||||
/**
|
||||
* 下载或在线预览上传文件。
|
||||
*
|
||||
* @param filepath 数据库保存的相对文件路径,例如 /upload/image/20260905/example.png
|
||||
* @param inline 传 true 时以内联方式返回,供图片等资源在浏览器中直接预览;其他值按附件下载
|
||||
* @param response Servlet 响应对象,文件内容通过响应流输出
|
||||
* @throws IOException 文件读取或响应输出失败时抛出
|
||||
*/
|
||||
@At
|
||||
@Ok("void")
|
||||
@SaCheckAnyLogin
|
||||
// @SaCheckAnyLogin
|
||||
public void download(String filepath, String inline, HttpServletResponse response) throws IOException {
|
||||
//校验文件路径是否合法
|
||||
boolean isValid = filepath != null && !filepath.contains("../") && !filepath.contains("..\\");
|
||||
@@ -146,7 +153,9 @@ public class UploadController {
|
||||
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
|
||||
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);
|
||||
if (!file.exists() || !file.isFile()) {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
|
||||
@@ -23,7 +23,7 @@ jetty:
|
||||
#结合ftp使用,或用nginx代理ftp路径
|
||||
#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:
|
||||
tokenName: token
|
||||
@@ -54,7 +54,7 @@ redis:
|
||||
jdbc:
|
||||
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
|
||||
password: dd3s@#2025
|
||||
password: wqnmd2331
|
||||
validationQuery: select 1
|
||||
maxActive: 150
|
||||
testWhileIdle: true
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
>
|
||||
<i slot="default" class="el-icon-plus"></i>
|
||||
<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">
|
||||
<span class="el-upload-list__item-actions">
|
||||
<span class="el-upload-list__item-preview" @click="handlePictureCardPreview(file)">
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
</el-upload>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -66,8 +66,8 @@ module.exports = {
|
||||
console.log(v)
|
||||
if (v.code === 0) {
|
||||
console.log(v.data)
|
||||
insertImgFn('https://3skj.ltd/api/open/file/upload/download?filepath='+v.data)
|
||||
// insertImgFn(v.data)
|
||||
// insertImgFn('https://3skj.ltd/api/open/file/upload/download?filepath='+v.data)
|
||||
insertImgFn('/open/file/upload/download?filepath='+v.data)
|
||||
} else {
|
||||
this.$message.error(v.msg)
|
||||
}
|
||||
|
||||
@@ -1646,8 +1646,14 @@
|
||||
computed: {
|
||||
sortedTeams() {
|
||||
if (!this.teamScores) return []
|
||||
// 根据总分排序
|
||||
return [...this.teamScores].sort((a, b) => this.calculateTotalScore(b) - this.calculateTotalScore(a))
|
||||
return [...this.teamScores].sort((a, b) => {
|
||||
const scoreA = this.calculateTotalScore(a);
|
||||
const scoreB = this.calculateTotalScore(b);
|
||||
if (scoreB !== scoreA) {
|
||||
return scoreB - scoreA; // 总分降序
|
||||
}
|
||||
return a.groupId - b.groupId; // 总分相同按 groupId 升序
|
||||
});
|
||||
}
|
||||
},
|
||||
created() {
|
||||
|
||||
@@ -62,6 +62,7 @@ layout("/layouts/platform_1.html"){
|
||||
<el-card shadow="never" class="mt-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" :loading="unitGrouping" @click="groupByUnit">按单位分组</el-button>
|
||||
<!-- <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">-->
|
||||
<!-- 未分组人员-->
|
||||
@@ -73,6 +74,10 @@ layout("/layouts/platform_1.html"){
|
||||
手动分组
|
||||
</el-button>
|
||||
</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-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>
|
||||
@@ -91,6 +96,12 @@ layout("/layouts/platform_1.html"){
|
||||
<el-tag v-if="row.leaderPhone">{{row.leaderPhone}}</el-tag>
|
||||
</template>
|
||||
</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">
|
||||
<template #default="scope">
|
||||
<el-button type="text" @click="openView(scope.row)">查看</el-button>
|
||||
@@ -129,16 +140,59 @@ layout("/layouts/platform_1.html"){
|
||||
year: new Date().getFullYear() + ""
|
||||
},
|
||||
activityList: [],
|
||||
unGroupUser: []
|
||||
unGroupUser: [],
|
||||
unitGrouping: false
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
openView(row){
|
||||
this.$refs.viewInfoRef.onOpen(this.pageForm.activityId, row.id)
|
||||
},
|
||||
|
||||
doExportGroup() {
|
||||
window.open("/platform/activity/group/doExportGroup?activityId=" + this.pageForm.activityId)
|
||||
},
|
||||
|
||||
// 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) {
|
||||
this.$confirm('此操作将会删除与该小队相关的所有信息,确定要删除吗?', '提示', {
|
||||
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(){
|
||||
if (!this.pageForm.activityId) {
|
||||
|
||||
@@ -6,6 +6,8 @@ const VIEW_INFO = {
|
||||
width="50"></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="age"></el-table-column>
|
||||
<el-table-column label="体重" prop="weight"></el-table-column>
|
||||
<el-table-column label="身份" prop="isLeader">
|
||||
<template #default="{row}">
|
||||
<el-tag size="small" v-if="row.isLeader">{{ '队长' }}</el-tag>
|
||||
|
||||
@@ -2,6 +2,25 @@
|
||||
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">
|
||||
<el-card shadow="never">
|
||||
<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="startTime"></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">
|
||||
<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.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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -48,12 +78,14 @@ layout("/layouts/platform_1.html"){
|
||||
</el-card>
|
||||
<add-or-update ref="addOrUpdateRef" @refresh="doSearch"></add-or-update>
|
||||
<item-form ref="itemFormRef"></item-form>
|
||||
<registration-field-setting ref="registrationFieldSettingRef"></registration-field-setting>
|
||||
<act-qr-code ref="actQrCodeRef"></act-qr-code>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<!--#include('addOrUpdate.js'){}#-->
|
||||
<!--#include('itemForm.js'){}#-->
|
||||
<!--#include('registrationFieldSetting.js'){}#-->
|
||||
<!--#include('actQrCode.js'){}#-->
|
||||
new Vue({
|
||||
el: "#app",
|
||||
@@ -61,6 +93,7 @@ layout("/layouts/platform_1.html"){
|
||||
components: {
|
||||
"add-or-update": addOrUpdate,
|
||||
"item-form": itemForm,
|
||||
"registration-field-setting": registrationFieldSetting,
|
||||
"act-qr-code": actQrCode
|
||||
},
|
||||
data() {
|
||||
@@ -71,6 +104,28 @@ layout("/layouts/platform_1.html"){
|
||||
},
|
||||
created() {
|
||||
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>
|
||||
|
||||
@@ -23,6 +23,21 @@ const itemForm = {
|
||||
<el-input v-model="item.name" placeholder="请输入项目名称" maxlength="50"
|
||||
show-word-limit></el-input>
|
||||
</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-radio-group v-model="item.isTeamApply" size="medium" @change="item.num=0">
|
||||
<el-radio-button :label="true">是</el-radio-button>
|
||||
@@ -83,6 +98,36 @@ const itemForm = {
|
||||
maxlength="500" show-word-limit></el-input>
|
||||
</el-form-item>
|
||||
</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-form-item>
|
||||
<image-upload v-model="item.img" :maxCount="1"></image-upload>
|
||||
@@ -115,6 +160,8 @@ const itemForm = {
|
||||
return {
|
||||
visible: false,
|
||||
submitting: false,
|
||||
projectTypeLoading: false,
|
||||
projectTypeOptions: [],
|
||||
form: {
|
||||
tableData: []
|
||||
},
|
||||
@@ -126,8 +173,28 @@ const itemForm = {
|
||||
onOpen(id) {
|
||||
this.visible = true
|
||||
this.resetForm()
|
||||
this.fetchProjectTypeOptions()
|
||||
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) {
|
||||
$.post("/platform/activity/manage/getItems", { id }).then((res) => {
|
||||
if (res.code === 0) {
|
||||
@@ -136,6 +203,9 @@ const itemForm = {
|
||||
// 确保每个项目都有必要的初始属性
|
||||
const formattedData = data.map(item => ({
|
||||
...item,
|
||||
projectType: item.projectType || "",
|
||||
registrationRules: this.parseRegistrationRule(item.registrationRule),
|
||||
registrationRuleLoading: false,
|
||||
isTeamApply: item.isTeamApply === undefined ? false : item.isTeamApply,
|
||||
num: item.num || 0,
|
||||
form: item.form || "",
|
||||
@@ -144,7 +214,10 @@ const itemForm = {
|
||||
img: item.img || ""
|
||||
}))
|
||||
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 {
|
||||
this.$message.error(res.msg || "加载项目数据失败")
|
||||
}
|
||||
@@ -160,6 +233,10 @@ const itemForm = {
|
||||
}
|
||||
this.form.tableData.push({
|
||||
name: "",
|
||||
projectType: "",
|
||||
registrationRule: "",
|
||||
registrationRules: [],
|
||||
registrationRuleLoading: false,
|
||||
isTeamApply: false,
|
||||
num: 1,
|
||||
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) {
|
||||
this.$confirm("确定要删除此项目吗?", "提示", {
|
||||
confirmButtonText: "确定",
|
||||
@@ -205,11 +332,23 @@ const itemForm = {
|
||||
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", {
|
||||
id: this.form.id,
|
||||
items: JSON.stringify(this.form.tableData)
|
||||
items: JSON.stringify(submitItems)
|
||||
}).then((res) => {
|
||||
this.submitting = false
|
||||
if (res.code === 0) {
|
||||
this.$message.success("保存成功")
|
||||
this.visible = false
|
||||
@@ -217,10 +356,11 @@ const itemForm = {
|
||||
} else {
|
||||
this.$message.error(res.msg || "保存失败")
|
||||
}
|
||||
}).catch(err => {
|
||||
this.submitting = false
|
||||
}, (err) => {
|
||||
this.$message.error("网络错误,请稍后重试")
|
||||
console.error(err)
|
||||
}).always(() => {
|
||||
this.submitting = false
|
||||
})
|
||||
} else {
|
||||
this.$message.warning("请完善表单信息")
|
||||
@@ -327,5 +467,15 @@ const itemForm = {
|
||||
.project-card .el-tabs--border-card{
|
||||
border: none;
|
||||
}
|
||||
|
||||
.registration-rule-empty {
|
||||
padding: 35px 0;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.registration-rule-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
+241
@@ -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-label">所属活动</div>
|
||||
<div class="search-item-option">
|
||||
<el-select v-model="pageForm.activityId" placeholder="请选择活动">
|
||||
<el-select v-model="pageForm.activityId" placeholder="请选择活动" @change="onActivityChange">
|
||||
<el-option
|
||||
v-for="item in activityOptions"
|
||||
:key="item.id"
|
||||
@@ -85,6 +85,16 @@ layout("/layouts/platform_1.html"){
|
||||
<span>{{scope.$index+(pageForm.pageNumber - 1) * pageForm.pageSize + 1}} </span>
|
||||
</template>
|
||||
</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="sex" min-width="60px"></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 ? '是':''}}
|
||||
</template>
|
||||
</el-table-column>
|
||||
</template>
|
||||
<el-table-column label="签到时间" prop="signInTime" min-width="120px"></el-table-column>
|
||||
<!-- <el-table-column label="物品发放" prop="isGrant">-->
|
||||
<!-- <template slot-scope="scope">-->
|
||||
<!-- <el-tag v-if="scope.row.isGrant" type="success">已发放</el-tag>-->
|
||||
@@ -146,10 +158,64 @@ layout("/layouts/platform_1.html"){
|
||||
year: new Date().getFullYear() + ""
|
||||
},
|
||||
activityOptions: [],
|
||||
registrationFields: [],
|
||||
units: [],
|
||||
}
|
||||
},
|
||||
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) {
|
||||
this.$confirm('您确定要设置成' + (isGrant ? '已发放' : '未发放') + '状态吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
@@ -180,7 +246,9 @@ layout("/layouts/platform_1.html"){
|
||||
this.$set(this.pageForm, "activityId", "")
|
||||
}
|
||||
this.activityOptions = res.data;
|
||||
this.doSearch()
|
||||
this.loadRegistrationFields(() => {
|
||||
this.doSearch()
|
||||
})
|
||||
} else {
|
||||
this.$message.error(res.msg);
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ layout("/layouts/platform_1.html"){
|
||||
<div class="search-item">
|
||||
<div class="search-item-label">所属活动</div>
|
||||
<div class="search-item-option">
|
||||
<el-select @change="doSearch"
|
||||
<el-select @change="onActivityChange"
|
||||
placeholder="请选择活动查询"
|
||||
style="width: 100%;"
|
||||
v-model="pageForm.activityId">
|
||||
@@ -54,7 +54,7 @@ layout("/layouts/platform_1.html"){
|
||||
<el-select placeholder="请选择所属单位"
|
||||
style="width: 100%;" clearable
|
||||
filterable
|
||||
@change="setModifyFormUnit"
|
||||
:loading="unitLoading"
|
||||
v-model="pageForm.unitId">
|
||||
<el-option :label="item.name" :value="item.id"
|
||||
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-download" type="primary" size="small" @click="doExport">导出</el-button>
|
||||
</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"
|
||||
fixed="left"
|
||||
:index="indexMethod"
|
||||
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="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="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="unitName" 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="isStationDesc"></el-table-column>
|
||||
<el-table-column label="接站地址" prop="stationAddress"></el-table-column>
|
||||
<!-- <el-table-column label="所需器材" prop="equip">-->
|
||||
<!-- <template slot-scope="{row}">-->
|
||||
<!-- <span>{{row.isShowEquip && row.equip?row.equip:'--'}}</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<!-- <el-table-column label="所需器材" prop="equip">-->
|
||||
<!-- <template slot-scope="{row}">-->
|
||||
<!-- <span>{{row.isShowEquip && row.equip?row.equip:'--'}}</span>-->
|
||||
<!-- </template>-->
|
||||
<!-- </el-table-column>-->
|
||||
<el-table-column label="是否参加茶叙会" prop="isParty">
|
||||
<template slot-scope="{row}">
|
||||
{{row.isParty ? '是':''}}
|
||||
</template>
|
||||
</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">
|
||||
<template #default="scope">
|
||||
<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 label="参加单位" prop="unitName">
|
||||
<el-select placeholder="请选择所属单位"
|
||||
style="width: 100%;" clearable
|
||||
style="width: 100%;"
|
||||
clearable filterable
|
||||
@change="setFormDataUnit"
|
||||
v-model="formData.unitId">
|
||||
<el-option :label="item.name" :value="item.id"
|
||||
@@ -172,12 +198,15 @@ layout("/layouts/platform_1.html"){
|
||||
show-word-limit placeholder="请输入联系方式"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="身份证件号" prop="idCard" width="250">
|
||||
<template slot-scope="{row}">
|
||||
<el-input v-model="row.idCard" maxlength="18" type="text"
|
||||
show-word-limit placeholder="请输入身份证件号"></el-input>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-form-item label="体重(kg)" prop="weight">
|
||||
<el-input-number v-model="modifyForm.weight"></el-input-number>
|
||||
</el-form-item>
|
||||
<!-- <el-table-column label="身份证件号" prop="idCard" width="250">-->
|
||||
<!-- <template slot-scope="{row}">-->
|
||||
<!-- <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">
|
||||
<template slot-scope="{row}">
|
||||
<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-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-form-item>
|
||||
<el-form-item label="性别" prop="sex">
|
||||
<el-radio-group v-model="modifyForm.sex">
|
||||
<el-radio label="男"></el-radio>
|
||||
<el-radio label="女"></el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="姓名" prop="userName">
|
||||
<el-input v-model="modifyForm.userName"></el-input>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<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-input v-model="modifyForm.phone"></el-input>
|
||||
</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-select placeholder="请选择所属单位"
|
||||
style="width: 100%;" clearable
|
||||
@@ -244,6 +275,62 @@ layout("/layouts/platform_1.html"){
|
||||
</el-select>
|
||||
<!-- <el-input v-model="modifyForm.unitName"></el-input>-->
|
||||
</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>
|
||||
<span slot="footer" class="dialog-footer">
|
||||
<el-button @click="modifyDialog = false">取消</el-button>
|
||||
@@ -263,6 +350,7 @@ layout("/layouts/platform_1.html"){
|
||||
year: new Date().getFullYear() + '',
|
||||
},
|
||||
activityList: [],
|
||||
registrationFields: [],
|
||||
// 修改表单
|
||||
modifyDialog: false,
|
||||
modifyForm: {},
|
||||
@@ -334,9 +422,81 @@ layout("/layouts/platform_1.html"){
|
||||
},
|
||||
|
||||
units: [],
|
||||
unitLoading: false,
|
||||
}
|
||||
},
|
||||
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) {
|
||||
if (!val) {
|
||||
this.formData.unitName = ''
|
||||
@@ -414,7 +574,7 @@ layout("/layouts/platform_1.html"){
|
||||
{ key: 'userName', message: '姓名不能为空' },
|
||||
{ key: 'sex', message: '性别不能为空' },
|
||||
{ key: 'phone', message: '联系方式不能为空' },
|
||||
{ key: 'idCard', message: '身份证件号不能为空' },
|
||||
// { key: 'idCard', message: '身份证件号不能为空' },
|
||||
{ key: 'position', message: '职务不能为空' }
|
||||
];
|
||||
|
||||
@@ -507,23 +667,41 @@ layout("/layouts/platform_1.html"){
|
||||
}
|
||||
})
|
||||
},
|
||||
async getActivityList(){
|
||||
const resp = await $.post('/platform/activity/user/getActivityList', {
|
||||
getActivityList(){
|
||||
$.post('/platform/activity/user/getActivityList', {
|
||||
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(){
|
||||
const resp = await $.post('/open/common/units')
|
||||
if (resp.code === 0) {
|
||||
this.units = resp.data
|
||||
}
|
||||
// 查询面板和代报名弹窗共用启用单位列表,按数据库序号location升序排列。
|
||||
getUnits(){
|
||||
this.unitLoading = true
|
||||
$.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() {
|
||||
|
||||
@@ -2,6 +2,16 @@ const addOrUpdate = {
|
||||
template: /*language=HTML*/ `
|
||||
<el-dialog :title="form.id ? '修改' : '新增'" :visible.sync="visible" width="70%">
|
||||
<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-input v-model="form.title" placeholder="请输入标题"></el-input>
|
||||
</el-form-item>
|
||||
@@ -25,6 +35,12 @@ const addOrUpdate = {
|
||||
</template>
|
||||
</el-dialog>
|
||||
`,
|
||||
props: {
|
||||
activityList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: '',
|
||||
@@ -33,6 +49,7 @@ const addOrUpdate = {
|
||||
published: true,
|
||||
},
|
||||
rules: {
|
||||
activityId: [{required: true, message: '请选择所属活动', trigger: ['blur', 'change']}],
|
||||
title: [{required: true, message: '请输入标题', trigger: ['blur', 'change']}],
|
||||
content: [{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"){}#-->
|
||||
</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>
|
||||
<script>
|
||||
@@ -123,14 +123,19 @@ layout("/layouts/platform_1.html"){
|
||||
}
|
||||
})
|
||||
},
|
||||
async getActivityList(){
|
||||
const resp = await $.post('/platform/activity/user/getActivityList')
|
||||
this.activityList = resp.data
|
||||
getActivityList(){
|
||||
$.post('/platform/activity/user/getActivityList').then((res) => {
|
||||
if (res.code === 0) {
|
||||
this.activityList = res.data
|
||||
} else {
|
||||
this.$message.error(res.msg)
|
||||
}
|
||||
})
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
created() {
|
||||
this.getActivityList()
|
||||
await this.pageData()
|
||||
this.pageData()
|
||||
}
|
||||
})
|
||||
</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.ImportParams;
|
||||
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
|
||||
import cn.hutool.core.util.NumberUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
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.services.SysDictService;
|
||||
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.runner.RunWith;
|
||||
import org.nutz.boot.test.junit4.NbJUnit4Runner;
|
||||
import org.nutz.dao.Cnd;
|
||||
import org.nutz.dao.Dao;
|
||||
import org.nutz.ioc.loader.annotation.Inject;
|
||||
import org.nutz.ioc.loader.annotation.IocBean;
|
||||
@@ -34,6 +37,7 @@ import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
@IocBean
|
||||
@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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ export default {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 活动详情富文本保留自身字体及继承关系,其他区域沿用原有默认字体。 */
|
||||
:where(*:not(.activity-rich-content *)) {
|
||||
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"enable": false
|
||||
},
|
||||
"h5": {
|
||||
"title": "趣味运动会",
|
||||
"title": "赛事管理",
|
||||
"router": {
|
||||
"mode": "history"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "趣味运动会"
|
||||
"navigationBarTitleText": "赛事管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -101,13 +101,13 @@
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "#000000",
|
||||
"navigationBarTitleText": "趣味运动会",
|
||||
"navigationBarTitleText": "赛事管理",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#000",
|
||||
"selectedColor": "#f45a5a",
|
||||
"selectedColor": "#1989fa",
|
||||
"borderStyle": "white",
|
||||
"backgroundColor": "#ffffff",
|
||||
"height": "60px",
|
||||
@@ -124,7 +124,7 @@
|
||||
"selectedText": "\ue64d",
|
||||
"fontSize": "17px",
|
||||
"color": "#333",
|
||||
"selectedColor": "#f45a5a"
|
||||
"selectedColor": "#1989fa"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -135,7 +135,7 @@
|
||||
"selectedText": "\ue8b3",
|
||||
"fontSize": "17px",
|
||||
"color": "#333",
|
||||
"selectedColor": "#f45a5a"
|
||||
"selectedColor": "#1989fa"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -146,7 +146,7 @@
|
||||
"selectedText": "\ue600",
|
||||
"fontSize": "17px",
|
||||
"color": "#333",
|
||||
"selectedColor": "#f45a5a"
|
||||
"selectedColor": "#1989fa"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<view class="event-info-section">
|
||||
<!-- 报名时间 -->
|
||||
<view class="info-item">
|
||||
<t-icon name="calendar" size="20px"/>
|
||||
<CalendarIcon size="20px"/>
|
||||
<view class="info-text-container">
|
||||
<text class="info-text-title">报名时间</text>
|
||||
<text class="info-text">{{ formatDate(detailData.signStartTime) }} 至
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<!-- 活动时间 -->
|
||||
<view class="info-item">
|
||||
<t-icon name="alarm" size="20px"/>
|
||||
<AlarmIcon size="20px"/>
|
||||
<view class="info-text-container">
|
||||
<text class="info-text-title">活动时间</text>
|
||||
<text class="info-text">{{ formatDate(detailData.startTime) }} 至 {{
|
||||
@@ -41,7 +41,7 @@
|
||||
|
||||
<!-- 活动地点 -->
|
||||
<view class="info-item">
|
||||
<t-icon name="location" size="20px"/>
|
||||
<LocationIcon size="20px"/>
|
||||
<view class="info-text-container">
|
||||
<text class="info-text-title">活动地点</text>
|
||||
<text class="info-text">{{ detailData.address }}</text>
|
||||
@@ -52,7 +52,7 @@
|
||||
<!-- 活动详情区域 -->
|
||||
<view class="activity-detail-section">
|
||||
<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>
|
||||
|
||||
<!-- 比赛项目 -->
|
||||
@@ -76,7 +76,7 @@
|
||||
<view class="event-item-info">
|
||||
<view class="event-item-section">
|
||||
<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>
|
||||
</view>
|
||||
<view class="event-item-section-content">{{ item.form }}</view>
|
||||
@@ -84,15 +84,15 @@
|
||||
|
||||
<view class="event-item-section">
|
||||
<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>
|
||||
</view>
|
||||
<view class="event-item-section-content">{{ item.rule }}</view>
|
||||
</view>
|
||||
|
||||
|
||||
<view class="event-item-section">
|
||||
<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>
|
||||
</view>
|
||||
<view class="event-item-section-content">{{ item.ruleForm }}</view>
|
||||
@@ -125,11 +125,19 @@
|
||||
</template>
|
||||
|
||||
<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 {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 {config} from "@/utils/config";
|
||||
import moment from "moment";
|
||||
|
||||
const request = useRequest();
|
||||
const actId = ref(null);
|
||||
@@ -139,28 +147,46 @@ const detailData = ref(null);
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const date = new Date(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;
|
||||
}
|
||||
return moment(dateStr).format('YYYY.MM.DD HH:mm');
|
||||
// try {
|
||||
// // 手动解析日期部分
|
||||
// const [datePart, timePart] = dateStr.split(' ');
|
||||
// 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 {signStartTime, signEndTime} = detailData.value
|
||||
if (signStartTime && signEndTime) {
|
||||
const now = new Date();
|
||||
const startTime = new Date(signStartTime);
|
||||
const endTime = new Date(signEndTime);
|
||||
if (now >= startTime && now <= endTime) {
|
||||
// const now = new Date();
|
||||
// const startTime = new Date(signStartTime);
|
||||
// const endTime = new Date(signEndTime);
|
||||
|
||||
const now = moment();
|
||||
const startTime = moment(signStartTime);
|
||||
const endTime = moment(signEndTime);
|
||||
|
||||
if (now.isBetween(startTime, endTime, null, '[]')) {
|
||||
return {
|
||||
text: isSignUp.value ? '修改报名信息' : '立即报名',
|
||||
disabled: false,
|
||||
onClick: navigateToSignUp
|
||||
}
|
||||
} else if (now < startTime) {
|
||||
} else if (now.isBefore(startTime)) {
|
||||
return {
|
||||
text: `报名开始时间:${formatDate(signStartTime)}`,
|
||||
disabled: true,
|
||||
@@ -262,7 +288,7 @@ onLoad((option) => {
|
||||
.event-tag {
|
||||
display: inline-flex;
|
||||
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);
|
||||
font-size: 24rpx;
|
||||
padding: 6rpx 12rpx;
|
||||
@@ -345,6 +371,20 @@ onLoad((option) => {
|
||||
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 {
|
||||
width: 100%;
|
||||
@@ -510,7 +550,7 @@ onLoad((option) => {
|
||||
max-height: 200rpx;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
@@ -369,7 +369,7 @@ async function publishMoment() {
|
||||
|
||||
.location-icon {
|
||||
font-size: 36rpx;
|
||||
color: #ff5777;
|
||||
color: var(--td-brand-color);
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
@@ -395,7 +395,7 @@ async function publishMoment() {
|
||||
|
||||
.location-icon {
|
||||
font-size: 32rpx;
|
||||
color: #ff5777;
|
||||
color: var(--td-brand-color);
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<view class="time">{{ moment(item.publish).format('MM-DD HH:mm') }}</view>
|
||||
<view @click="onPraise(item)">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -49,10 +49,9 @@
|
||||
style="flex-shrink:0;width: 120px;height: 90px;border-radius: 5px;" fit="fill"/>
|
||||
<view
|
||||
style="margin-left:10px;flex:1;overflow:hidden;display: flex;flex-direction: column;justify-content:space-around;color: #534d4d;">
|
||||
<view
|
||||
style="display: flex; align-items: center; column-gap: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<view class="activity-date-tag">
|
||||
<t-icon name="time"></t-icon>
|
||||
{{ moment(item.startTime).format('MM-DD') }}
|
||||
<text>{{ moment(item.startTime).format('YYYY年MM月DD日') }}</text>
|
||||
</view>
|
||||
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<t-icon name="location"></t-icon>
|
||||
@@ -60,7 +59,7 @@
|
||||
</view>
|
||||
<view style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<t-icon name="castle-7"></t-icon>
|
||||
{{ item.coHostUnit }}
|
||||
{{ item.hostUnit }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -68,7 +67,12 @@
|
||||
<view>
|
||||
<!-- 100人已报名-->
|
||||
</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>
|
||||
@@ -205,6 +209,13 @@ const navigateToDetail = (id) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 报名按钮直达表单,活动ID沿用详情页的报名入口参数。
|
||||
const navigateToSignUp = (id) => {
|
||||
uni.navigateTo({
|
||||
url: `/pages/activity/registration-form/index?id=${id}`
|
||||
});
|
||||
};
|
||||
|
||||
// 初始化
|
||||
onMounted(() => {
|
||||
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 {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
|
||||
@@ -181,7 +181,7 @@ onMounted(async () => {
|
||||
.bg-circle {
|
||||
position: absolute;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ onMounted(async () => {
|
||||
|
||||
.rank-header {
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(135deg, #f45a5a, #f78484);
|
||||
background: linear-gradient(135deg, var(--td-brand-color), var(--td-brand-color-4));
|
||||
height: 200rpx;
|
||||
padding: 20rpx 32rpx;
|
||||
color: #fff;
|
||||
@@ -235,7 +235,7 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
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-right-radius: 30rpx;
|
||||
z-index: 1;
|
||||
@@ -292,7 +292,7 @@ onMounted(async () => {
|
||||
padding: 24rpx 16rpx;
|
||||
font-weight: bold;
|
||||
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);
|
||||
margin: 16rpx;
|
||||
border-radius: 16rpx;
|
||||
@@ -353,8 +353,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.my-team {
|
||||
background-color: rgba(244, 90, 90, 0.05);
|
||||
border-left: 8rpx solid #f45a5a;
|
||||
background-color: rgba(25, 137, 250, 0.05);
|
||||
border-left: 8rpx solid var(--td-brand-color);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -367,7 +367,7 @@ onMounted(async () => {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
background-color: rgba(244, 90, 90, 0.1);
|
||||
background-color: rgba(25, 137, 250, 0.1);
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
@@ -389,8 +389,8 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.my-rank-number {
|
||||
background-color: rgba(244, 90, 90, 0.1);
|
||||
color: #f45a5a;
|
||||
background-color: rgba(25, 137, 250, 0.1);
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
|
||||
.team-info {
|
||||
@@ -432,9 +432,9 @@ onMounted(async () => {
|
||||
|
||||
.my-team-tag {
|
||||
margin-left: 10rpx;
|
||||
background-color: rgba(244, 90, 90, 0.1);
|
||||
color: #f45a5a;
|
||||
border: 1px solid rgba(244, 90, 90, 0.2);
|
||||
background-color: rgba(25, 137, 250, 0.1);
|
||||
color: var(--td-brand-color);
|
||||
border: 1px solid rgba(25, 137, 250, 0.2);
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
@@ -454,14 +454,14 @@ onMounted(async () => {
|
||||
.progress-bar-fill {
|
||||
position: absolute;
|
||||
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;
|
||||
transition: width 0.8s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-weight: bold;
|
||||
color: #f45a5a;
|
||||
color: var(--td-brand-color);
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
@@ -477,8 +477,8 @@ onMounted(async () => {
|
||||
width: 90rpx;
|
||||
height: 90rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #f45a5a, #f78484);
|
||||
box-shadow: 0 4rpx 20rpx rgba(244, 90, 90, 0.4);
|
||||
background: linear-gradient(135deg, var(--td-brand-color), var(--td-brand-color-4));
|
||||
box-shadow: 0 4rpx 20rpx rgba(25, 137, 250, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -489,15 +489,15 @@ onMounted(async () => {
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
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% {
|
||||
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% {
|
||||
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 {
|
||||
color: rgba(244, 90, 90, 0.5);
|
||||
color: rgba(25, 137, 250, 0.5);
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -141,14 +141,14 @@ const navigateToEdit = () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: rgba(255, 95, 92, 0.1);
|
||||
background-color: rgba(25, 137, 250, 0.1);
|
||||
border-radius: 100px;
|
||||
padding: 10px 20px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 40px;
|
||||
|
||||
.t-icon {
|
||||
color: #ff5f5c;
|
||||
color: var(--td-brand-color);
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
@@ -171,13 +171,13 @@ const navigateToEdit = () => {
|
||||
}
|
||||
|
||||
:deep(.t-button--primary) {
|
||||
background-color: #ff5f5c !important;
|
||||
border-color: #ff5f5c !important;
|
||||
background-color: var(--td-brand-color) !important;
|
||||
border-color: var(--td-brand-color) !important;
|
||||
}
|
||||
|
||||
:deep(.t-button--default) {
|
||||
border-color: #ff5f5c !important;
|
||||
color: #ff5f5c !important;
|
||||
border-color: var(--td-brand-color) !important;
|
||||
color: var(--td-brand-color) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -550,7 +550,7 @@ const submitRegistration = async () => {
|
||||
|
||||
.picker-subtitle {
|
||||
font-size: 14px;
|
||||
color: #ff5f5c;
|
||||
color: var(--td-brand-color);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
@@ -355,7 +355,7 @@ onLoad(() => {
|
||||
|
||||
.picker-subtitle {
|
||||
font-size: 14px;
|
||||
color: #ff5f5c;
|
||||
color: var(--td-brand-color);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
@@ -403,7 +403,7 @@ onLoad(() => {
|
||||
background-color: #fff;
|
||||
border-radius: 16px;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -411,8 +411,8 @@ onLoad(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: linear-gradient(to right, rgba(255, 95, 92, 0.05), rgba(255, 95, 92, 0.02));
|
||||
border-bottom: 1px solid rgba(255, 95, 92, 0.1);
|
||||
background: linear-gradient(to right, rgba(25, 137, 250, 0.05), rgba(25, 137, 250, 0.02));
|
||||
border-bottom: 1px solid rgba(25, 137, 250, 0.1);
|
||||
}
|
||||
|
||||
.team-logo {
|
||||
@@ -424,14 +424,14 @@ onLoad(() => {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.team-logo i {
|
||||
font-size: 24px;
|
||||
color: #ff5f5c;
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
|
||||
.team-logo img {
|
||||
@@ -472,12 +472,12 @@ onLoad(() => {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 18px;
|
||||
background-color: rgba(255, 95, 92, 0.1);
|
||||
background-color: rgba(25, 137, 250, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
color: #ff5f5c;
|
||||
color: var(--td-brand-color);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -528,12 +528,12 @@ onLoad(() => {
|
||||
color: #666;
|
||||
line-height: 1.5;
|
||||
position: relative;
|
||||
background-color: #fff8f8;
|
||||
background-color: #f5faff;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.team-desc i {
|
||||
color: #ffdddd;
|
||||
color: var(--td-brand-color-2);
|
||||
font-size: 18px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
@@ -548,9 +548,9 @@ onLoad(() => {
|
||||
.register-btn {
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
background-color: #ff5f5c;
|
||||
border-color: #ff5f5c;
|
||||
box-shadow: 0 6px 16px rgba(255, 95, 92, 0.2);
|
||||
background-color: var(--td-brand-color);
|
||||
border-color: var(--td-brand-color);
|
||||
box-shadow: 0 6px 16px rgba(25, 137, 250, 0.2);
|
||||
}
|
||||
|
||||
/* 成员列表 */
|
||||
@@ -579,7 +579,7 @@ onLoad(() => {
|
||||
|
||||
.section-title i {
|
||||
margin-right: 8px;
|
||||
color: #ff5f5c;
|
||||
color: var(--td-brand-color);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
@@ -589,8 +589,8 @@ onLoad(() => {
|
||||
justify-content: center;
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
background-color: #fff0f0;
|
||||
color: #ff5f5c;
|
||||
background-color: var(--td-brand-color-light);
|
||||
color: var(--td-brand-color);
|
||||
border-radius: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
@@ -647,11 +647,11 @@ onLoad(() => {
|
||||
.role-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #fff8f8;
|
||||
background-color: #f5faff;
|
||||
border-radius: 12px;
|
||||
padding: 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 {
|
||||
@@ -664,7 +664,7 @@ onLoad(() => {
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
//box-shadow: 0 4px 12px rgba(255, 95, 92, 0.2);
|
||||
color: #ff5f5c;
|
||||
color: var(--td-brand-color);
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
@@ -689,8 +689,8 @@ onLoad(() => {
|
||||
margin-top: 8px;
|
||||
border-radius: 12px;
|
||||
height: 48px;
|
||||
border-color: #ff5f5c;
|
||||
color: #ff5f5c;
|
||||
border-color: var(--td-brand-color);
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
|
||||
/* 弹窗内容样式 */
|
||||
|
||||
@@ -24,7 +24,8 @@ const viewData = ref(null);
|
||||
function getWonderfulMoment() {
|
||||
request.post('/platform/activity/wonderfulMoment/getWonderfulMomentById/' + momentId.value).then(res=>{
|
||||
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
|
||||
}else{
|
||||
Toast({message: res.msg, theme: 'error'});
|
||||
|
||||
+372
-269
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<view class="page-container">
|
||||
<view class="header">
|
||||
<image class="header-bg"
|
||||
src="/static/img/index2.png"
|
||||
mode="aspectFill"></image>
|
||||
<image
|
||||
class="header-bg"
|
||||
src="/static/img/index4.jpg"
|
||||
mode="aspectFill"
|
||||
></image>
|
||||
<!-- html文字临时屏蔽
|
||||
<view v-if="currentActivityTitle" class="header-activity-title">
|
||||
{{ displayActivityTitle }}
|
||||
</view>
|
||||
-->
|
||||
</view>
|
||||
|
||||
<view class="menu-container">
|
||||
@@ -13,54 +20,79 @@
|
||||
</view>
|
||||
<view class="menu-text">赛事报名</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-icon-wrapper yellow-bg">
|
||||
<view class="menu-icon team-icon"></view>
|
||||
<!-- <view class="badge">3</view>-->
|
||||
<!-- <view class="badge">3</view>-->
|
||||
</view>
|
||||
<view class="menu-text">我的团队</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 star-icon"></view>
|
||||
</view>
|
||||
<view class="menu-text">项目报名</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 class="menu-item menu-item-disabled" aria-disabled="true">
|
||||
<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/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 class="content-section">
|
||||
<view class="section-header">
|
||||
<view class="section-title">近期活动</view>
|
||||
<view class="section-more" @click="navigateTo('/pages/activity/list/index')">更多</view>
|
||||
<view class="section-title">活动详情</view>
|
||||
<view
|
||||
class="section-more"
|
||||
@click="navigateTo('/pages/activity/list/index')"
|
||||
>更多</view
|
||||
>
|
||||
</view>
|
||||
|
||||
<t-swiper
|
||||
v-if="actList.length > 0"
|
||||
: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">
|
||||
<view class="activity-list" @click="navigateTo('/pages/activity/detail/index?id=' + item.id)">
|
||||
<t-swiper-item
|
||||
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">
|
||||
<t-image :src=config.fileDomain+item.cover shape="round"
|
||||
class="activity-image" fit="fill"/>
|
||||
<t-image
|
||||
:src="getActivityCoverUrl(item.cover)"
|
||||
:lazy="index > 0"
|
||||
shape="round"
|
||||
class="activity-image"
|
||||
fit="fill"
|
||||
/>
|
||||
<view class="activity-info">
|
||||
<view class="activity-title">{{ item.title }}</view>
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
@@ -68,11 +100,7 @@
|
||||
</view>
|
||||
</t-swiper-item>
|
||||
</t-swiper>
|
||||
<t-empty
|
||||
v-if="actList.length === 0"
|
||||
description="暂无近期活动"
|
||||
/>
|
||||
|
||||
<t-empty v-if="actList.length === 0" description="暂无近期活动" />
|
||||
</view>
|
||||
|
||||
<view class="content-section">
|
||||
@@ -81,14 +109,23 @@
|
||||
</view>
|
||||
|
||||
<view class="activity-list" v-if="wonderfulMomentList.length > 0">
|
||||
<view class="moment-item" v-for="item in wonderfulMomentList" :key="item.id"
|
||||
@click="handleWonderfulMoment(item)">
|
||||
<t-image :src=config.fileDomain+item.cover shape="round"
|
||||
class="moment-image" fit="fill"/>
|
||||
<view
|
||||
class="moment-item"
|
||||
v-for="item in wonderfulMomentList"
|
||||
: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-title">{{ item.title }}</view>
|
||||
<view class="moment-sub-content">
|
||||
<view style="float: right;">{{ item.time }}</view>
|
||||
<view style="float: right">{{ item.time }}</view>
|
||||
<view>阅读 {{ item.readCount }}</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -98,280 +135,346 @@
|
||||
v-if="wonderfulMomentList.length === 0"
|
||||
description="暂无精彩瞬间"
|
||||
/>
|
||||
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {onLoad, onShow} from "@dcloudio/uni-app";
|
||||
import {Toast} from "tdesign-mobile-vue";
|
||||
import {useRequest} from "@/utils/request";
|
||||
import moment from "moment";
|
||||
import {config} from "@/utils/config";
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { Toast } from 'tdesign-mobile-vue'
|
||||
import { useRequest } from '@/utils/request'
|
||||
import moment from 'moment'
|
||||
import { config } from '@/utils/config'
|
||||
|
||||
const request = useRequest()
|
||||
const request = useRequest()
|
||||
|
||||
const actList = ref([])
|
||||
const wonderfulMomentList = ref([])
|
||||
|
||||
function navigateTo(url) {
|
||||
uni.navigateTo({
|
||||
url
|
||||
const actList = ref([])
|
||||
const wonderfulMomentList = ref([])
|
||||
// 首页头图展示当前最新开启活动的标题,数据口径与登录页保持一致。
|
||||
const currentActivityTitle = ref('')
|
||||
// 当前活动标题在“(公司)”后分行展示,其他标题仍保留原文并按容器宽度自然换行。
|
||||
const displayActivityTitle = computed(() => {
|
||||
return currentActivityTitle.value.replace('(公司)', '(公司)\n')
|
||||
})
|
||||
}
|
||||
|
||||
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'});
|
||||
// 后台仅保存封面相对路径,首页通过文件预览接口读取活动管理中上传的实际图片。
|
||||
const getActivityCoverUrl = (cover) => {
|
||||
if (!cover) {
|
||||
return ''
|
||||
}
|
||||
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>
|
||||
|
||||
<style lang="less" scoped>
|
||||
uni-page-body {
|
||||
height: auto;
|
||||
}
|
||||
// 头部区域样式
|
||||
.header {
|
||||
position: relative;
|
||||
height: 245px;
|
||||
overflow: hidden;
|
||||
}
|
||||
uni-page-body {
|
||||
height: auto;
|
||||
}
|
||||
// 头部区域样式
|
||||
.header {
|
||||
position: relative;
|
||||
height: 245px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.header-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
// 菜单区域样式
|
||||
.menu-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
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;
|
||||
}
|
||||
.header-activity-title {
|
||||
position: absolute;
|
||||
top: 61%;
|
||||
left: 50%;
|
||||
z-index: 1;
|
||||
width: 62%;
|
||||
transform: translate(-50%, -50%);
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
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;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 22%;
|
||||
}
|
||||
// 菜单区域样式
|
||||
.menu-container {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
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 {
|
||||
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);
|
||||
}
|
||||
.menu-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 22%;
|
||||
}
|
||||
|
||||
.red-bg {
|
||||
background: linear-gradient(135deg, #ff7676, #f45a5a);
|
||||
}
|
||||
.menu-icon-wrapper {
|
||||
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 {
|
||||
background: linear-gradient(135deg, #ffa76a, #f89f58);
|
||||
}
|
||||
.red-bg {
|
||||
background: linear-gradient(135deg, var(--td-brand-color-4), var(--td-brand-color));
|
||||
}
|
||||
|
||||
.yellow-bg {
|
||||
background: linear-gradient(135deg, #ffd166, #f8c158);
|
||||
}
|
||||
.orange-bg {
|
||||
background: linear-gradient(135deg, #ffa76a, #f89f58);
|
||||
}
|
||||
|
||||
.blue-bg {
|
||||
background: linear-gradient(135deg, #72a1ff, #5c7cfa);
|
||||
}
|
||||
.yellow-bg {
|
||||
background: linear-gradient(135deg, #ffd166, #f8c158);
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.blue-bg {
|
||||
background: linear-gradient(135deg, #72a1ff, #5c7cfa);
|
||||
}
|
||||
|
||||
.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='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");
|
||||
}
|
||||
.menu-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.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='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");
|
||||
}
|
||||
.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='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 {
|
||||
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");
|
||||
}
|
||||
.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='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 {
|
||||
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");
|
||||
}
|
||||
.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='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 {
|
||||
position: absolute;
|
||||
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);
|
||||
}
|
||||
.star-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");
|
||||
}
|
||||
|
||||
.menu-text {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
.badge {
|
||||
position: absolute;
|
||||
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);
|
||||
}
|
||||
|
||||
// 内容区域样式
|
||||
.content-section {
|
||||
margin: 0 0 15px;
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.menu-text {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.menu-item-disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.menu-item-disabled .menu-icon-wrapper::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: inherit;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.section-title::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 16px;
|
||||
background: linear-gradient(to bottom, #ff6b6b, #e83a3a);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.menu-item-disabled .menu-text {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
// 内容区域样式
|
||||
.content-section {
|
||||
margin: 0 0 15px;
|
||||
padding: 15px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
// 活动列表样式
|
||||
.activity-list {
|
||||
width: 100%;
|
||||
}
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.activity-item {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.activity-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.section-title::before {
|
||||
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 {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
.section-more {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
padding: 10px 0;
|
||||
}
|
||||
// 活动列表样式
|
||||
.activity-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.activity-item {
|
||||
width: 100%;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.activity-meta {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.activity-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.activity-image {
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.activity-info {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.activity-title {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.activity-meta {
|
||||
display: flex;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.activity-time {
|
||||
margin-right: 15px;
|
||||
}
|
||||
.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>
|
||||
|
||||
@@ -6,15 +6,15 @@
|
||||
<pattern id="pattern-hex" x="0" y="0" width="10" height="10" patternUnits="userSpaceOnUse"
|
||||
patternTransform="scale(0.8)">
|
||||
<rect x="0" y="0" width="10" height="10" fill="none"/>
|
||||
<path d="M0,5 L2.5,0 L7.5,0 L10,5 L7.5,10 L2.5,10 Z" stroke-width="0.5" stroke="rgba(244, 90, 90, 0.2)"
|
||||
fill="rgba(244, 90, 90, 0.1)">
|
||||
<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(25, 137, 250, 0.1)">
|
||||
<animate attributeName="stroke-opacity" values="0.1;0.3;0.1" dur="3s" repeatCount="indefinite"/>
|
||||
</path>
|
||||
</pattern>
|
||||
<rect x="0" y="0" width="100%" height="100%" fill="url(#pattern-hex)"/>
|
||||
|
||||
<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"
|
||||
values="M20,50 Q60,20 100,50 T180,50 T260,50;
|
||||
M20,50 Q60,80 100,50 T180,50 T260,50;
|
||||
@@ -22,7 +22,7 @@
|
||||
dur="10s" repeatCount="indefinite"/>
|
||||
</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"
|
||||
values="M20,70 Q60,40 100,70 T180,70 T260,70;
|
||||
M20,70 Q60,100 100,70 T180,70 T260,70;
|
||||
@@ -31,19 +31,19 @@
|
||||
</path>
|
||||
</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="cy" values="50;30;80;50" dur="16s" repeatCount="indefinite"/>
|
||||
<animate attributeName="r" values="1;1.5;1" dur="5s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
|
||||
<circle cx="150" cy="30" r="1" fill="rgba(244, 90, 90, 0.8)">
|
||||
<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="cy" values="30;60;20;30" dur="18s" repeatCount="indefinite"/>
|
||||
<animate attributeName="r" values="1;1.8;1" dur="6s" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
|
||||
<circle cx="250" cy="70" r="1" fill="rgba(244, 90, 90, 0.8)">
|
||||
<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="cy" values="70;40;70" dur="15s" repeatCount="indefinite"/>
|
||||
<animate attributeName="r" values="1;1.2;1" dur="4s" repeatCount="indefinite"/>
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
|
||||
<view class="title-content">
|
||||
<text class="main-title">第二届"钟声杯"直属工会趣味运动会</text>
|
||||
<text class="main-title">{{ activityOrganizer.title || '赛事管理' }}</text>
|
||||
<view class="sub-title-container">
|
||||
<text class="sub-title">欢迎使用</text>
|
||||
</view>
|
||||
@@ -61,18 +61,18 @@
|
||||
|
||||
<view class="login-card">
|
||||
<view class="login-tabs">
|
||||
<view
|
||||
:class="['tab', loginForm.loginMethod === 'sms' ? 'active' : '']"
|
||||
@click="loginForm.loginMethod = 'sms'"
|
||||
>
|
||||
验证码登录
|
||||
</view>
|
||||
<view
|
||||
:class="['tab', loginForm.loginMethod === 'password' ? 'active' : '']"
|
||||
@click="loginForm.loginMethod = 'password'"
|
||||
>
|
||||
密码登录
|
||||
</view>
|
||||
<!-- <view-->
|
||||
<!-- :class="['tab', loginForm.loginMethod === 'sms' ? 'active' : '']"-->
|
||||
<!-- @click="loginForm.loginMethod = 'sms'"-->
|
||||
<!-- >-->
|
||||
<!-- 验证码登录-->
|
||||
<!-- </view>-->
|
||||
<!-- <view-->
|
||||
<!-- :class="['tab', loginForm.loginMethod === 'password' ? 'active' : '']"-->
|
||||
<!-- @click="loginForm.loginMethod = 'password'"-->
|
||||
<!-- >-->
|
||||
<!-- 密码登录-->
|
||||
<!-- </view>-->
|
||||
</view>
|
||||
|
||||
<!-- 登录表单区域 -->
|
||||
@@ -89,7 +89,7 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 验证码输入框 -->
|
||||
<!-- 登录验证码输入和发送入口 -->
|
||||
<view class="input-box" v-if="loginForm.loginMethod==='sms'">
|
||||
<t-icon name="shield-error" size="20px" class="input-icon"/>
|
||||
<input
|
||||
@@ -105,7 +105,7 @@
|
||||
</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"/>
|
||||
<input
|
||||
class="input"
|
||||
@@ -131,8 +131,8 @@
|
||||
|
||||
<!-- 底部信息 -->
|
||||
<view class="footer-info">
|
||||
<view class="footer-item">主办单位:浙江省教育工会</view>
|
||||
<view class="footer-item">承办单位:杭州医学院工会</view>
|
||||
<view class="footer-item">主办单位:{{ activityOrganizer.hostUnit || '--' }}</view>
|
||||
<view class="footer-item">承办单位:{{ activityOrganizer.coHostUnit || '--' }}</view>
|
||||
<view class="footer-item">技术支持:武汉地大三思科技有限公司(智慧工会提供商)</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -155,6 +155,27 @@ const loginForm = reactive({
|
||||
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 timer = ref(null)
|
||||
@@ -238,6 +259,7 @@ const startCountDown = () => {
|
||||
// 在页面挂载时恢复倒计时状态
|
||||
onMounted(() => {
|
||||
restoreCountdownState()
|
||||
getLatestActivityOrganizer()
|
||||
})
|
||||
|
||||
// 在页面卸载前清除定时器
|
||||
@@ -248,41 +270,37 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
})
|
||||
|
||||
// 获取验证码
|
||||
async function getSmsCode() {
|
||||
// 获取登录短信验证码:手机号通过路径参数传递,接口返回 code 和 msg 表示发送结果。
|
||||
const getSmsCode = () => {
|
||||
if (!loginForm.phone) {
|
||||
Toast('请输入手机号码');
|
||||
return
|
||||
}
|
||||
|
||||
// 验证手机号码
|
||||
if (!/^1[3-9]\d{9}$/.test(loginForm.phone)) {
|
||||
Toast('请输入正确的手机号码');
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if (isCountingDown.value) return
|
||||
try {
|
||||
const {code, data, msg} = await request.post('/client/auth/send-sms-code/' + loginForm.phone)
|
||||
request.post('/client/auth/send-sms-code/' + loginForm.phone).then(({code, msg}) => {
|
||||
if (code === 0) {
|
||||
Toast({message: '验证码发送成功', theme: 'success'})
|
||||
// 开始倒计时
|
||||
startCountDown()
|
||||
} else {
|
||||
Toast({message: msg, theme: 'error'})
|
||||
}
|
||||
} catch (e) {
|
||||
Toast({message: e.message, theme: 'error'})
|
||||
}
|
||||
}).catch((error) => {
|
||||
Toast({message: error?.message || error?.msg || error?.errMsg || '验证码发送失败,请稍后重试', theme: 'error'})
|
||||
})
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
const handleLogin = () => {
|
||||
if (!loginForm.phone) {
|
||||
Toast('请输入手机号码');
|
||||
return
|
||||
}
|
||||
if (!loginForm.smsCode && loginForm.loginMethod === 'sms') {
|
||||
if (!loginForm.smsCode.trim() && loginForm.loginMethod === 'sms') {
|
||||
Toast('请输入验证码');
|
||||
return
|
||||
}
|
||||
@@ -291,39 +309,34 @@ async function handleLogin() {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
userStore.smsLogin(loginForm).then(() => {
|
||||
userStore.getUserInfo().then(() => {
|
||||
Toast({message: '登录成功', theme: 'success'})
|
||||
const redirect = uni.getStorageSync("REDIRECT_URL")
|
||||
if(redirect){
|
||||
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{
|
||||
userStore.smsLogin(loginForm).then(() => {
|
||||
userStore.getUserInfo().then(() => {
|
||||
Toast({message: '登录成功', theme: 'success'})
|
||||
const redirect = uni.getStorageSync("REDIRECT_URL")
|
||||
if(redirect){
|
||||
if(redirect.startsWith('/pages/index/index') || redirect.startsWith('/pages/activity/rank/index') || redirect.startsWith('/pages/profile/index')){
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
url: redirect
|
||||
})
|
||||
}else{
|
||||
uni.navigateTo({
|
||||
url: redirect
|
||||
})
|
||||
}
|
||||
}).catch(e => {
|
||||
console.log(e)
|
||||
Toast({message: e.message, theme: 'error'})
|
||||
})
|
||||
}).catch(e => {
|
||||
uni.removeStorageSync("REDIRECT_URL")
|
||||
}else{
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
Toast({message: e.message, theme: 'error'})
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}).catch((e) => {
|
||||
console.log(e)
|
||||
Toast({message: e.message, theme: 'error'})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function navigateToRegister() {
|
||||
@@ -335,32 +348,25 @@ function navigateToRegister() {
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
background: #f4f2f2;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.title-area {
|
||||
padding: 0;
|
||||
height: 30vh;
|
||||
// 固定顶部高度,长表单通过整页滚动展示,避免挤压标题区域。
|
||||
height: 260px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
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 {
|
||||
@@ -384,7 +390,7 @@ body {
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 36px;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
@@ -410,7 +416,7 @@ body {
|
||||
}
|
||||
|
||||
.login-card {
|
||||
flex: 1;
|
||||
flex: 1 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 30px 30px 0 0;
|
||||
margin-top: -30px;
|
||||
@@ -436,9 +442,9 @@ body {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 15px;
|
||||
color: #f45a5a;
|
||||
color: var(--td-brand-color);
|
||||
font-weight: 500;
|
||||
background-color: #fff0f0;
|
||||
background-color: var(--td-brand-color-light);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
@@ -456,7 +462,7 @@ body {
|
||||
|
||||
.tab.active {
|
||||
color: #fff;
|
||||
background-color: #f45a5a;
|
||||
background-color: var(--td-brand-color);
|
||||
}
|
||||
|
||||
.form-container {
|
||||
@@ -491,7 +497,7 @@ body {
|
||||
}
|
||||
|
||||
.verify-code-btn {
|
||||
color: #f45a5a;
|
||||
color: var(--td-brand-color);
|
||||
font-size: 14px;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -503,7 +509,7 @@ body {
|
||||
.submit-btn {
|
||||
margin-top: 32px;
|
||||
height: 50px;
|
||||
background-color: #f45a5a;
|
||||
background-color: var(--td-brand-color);
|
||||
border-radius: 25px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
@@ -519,13 +525,14 @@ body {
|
||||
color: #666;
|
||||
|
||||
.link {
|
||||
color: #f45a5a;
|
||||
color: var(--td-brand-color);
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 底部信息样式 */
|
||||
.footer-info {
|
||||
flex-shrink: 0;
|
||||
padding: 20px 30px;
|
||||
text-align: center;
|
||||
background-color: #ffffff;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -41,7 +41,7 @@
|
||||
</view>
|
||||
</view>
|
||||
<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>
|
||||
<view
|
||||
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 style="white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
|
||||
<t-icon name="castle-7"></t-icon>
|
||||
{{ item.coHostUnit }}
|
||||
{{ item.hostUnit }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -118,6 +118,14 @@ const loading = ref(false);
|
||||
const hasMore = ref(true);
|
||||
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) => {
|
||||
pageForm.status = status;
|
||||
|
||||
@@ -123,7 +123,7 @@ async function updateAvatar(avatar) {
|
||||
</view>
|
||||
|
||||
<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 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
@@ -1,14 +1,14 @@
|
||||
:root, :root[theme-mode="light"] {
|
||||
--td-brand-color-1: #fff0ed;
|
||||
--td-brand-color-2: #ffd8d3;
|
||||
--td-brand-color-3: #ffb8b1;
|
||||
--td-brand-color-4: #ff928c;
|
||||
--td-brand-color-5: #ff5f5c;
|
||||
--td-brand-color-6: #df3d3f;
|
||||
--td-brand-color-7: #b6292d;
|
||||
--td-brand-color-8: #8f121b;
|
||||
--td-brand-color-9: #6b000a;
|
||||
--td-brand-color-10: #490005;
|
||||
--td-brand-color-1: #e8f3ff;
|
||||
--td-brand-color-2: #cfe7ff;
|
||||
--td-brand-color-3: #a4d2ff;
|
||||
--td-brand-color-4: #70b7ff;
|
||||
--td-brand-color-5: #1989fa;
|
||||
--td-brand-color-6: #0873d9;
|
||||
--td-brand-color-7: #005bb5;
|
||||
--td-brand-color-8: #00468c;
|
||||
--td-brand-color-9: #003568;
|
||||
--td-brand-color-10: #002447;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
@@ -119,16 +119,16 @@
|
||||
}
|
||||
|
||||
:root[theme-mode="dark"] {
|
||||
--td-brand-color-1: #ff505020;
|
||||
--td-brand-color-2: #6b000a;
|
||||
--td-brand-color-3: #8f121b;
|
||||
--td-brand-color-4: #b6292d;
|
||||
--td-brand-color-5: #df3d3f;
|
||||
--td-brand-color-6: #ff5050;
|
||||
--td-brand-color-7: #ff928c;
|
||||
--td-brand-color-8: #ffb8b1;
|
||||
--td-brand-color-9: #ffd8d3;
|
||||
--td-brand-color-10: #fff0ed;
|
||||
--td-brand-color-1: #1989fa20;
|
||||
--td-brand-color-2: #003568;
|
||||
--td-brand-color-3: #00468c;
|
||||
--td-brand-color-4: #005bb5;
|
||||
--td-brand-color-5: #0873d9;
|
||||
--td-brand-color-6: #1989fa;
|
||||
--td-brand-color-7: #70b7ff;
|
||||
--td-brand-color-8: #a4d2ff;
|
||||
--td-brand-color-9: #cfe7ff;
|
||||
--td-brand-color-10: #e8f3ff;
|
||||
--td-brand-color-light: var(--td-brand-color-1);
|
||||
--td-brand-color-focus: var(--td-brand-color-2);
|
||||
--td-brand-color-disabled: var(--td-brand-color-3);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export const config = {
|
||||
fileDomain: 'https://3skj.ltd',
|
||||
}
|
||||
// 后台图片字段保存相对路径,统一通过当前环境的 API 代理读取,避免绑定固定部署域名。
|
||||
fileDomain: '/api/open/file/upload/download?inline=true&filepath=',
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -35,7 +35,7 @@ export default defineConfig({
|
||||
proxy: {
|
||||
'/api': {
|
||||
// target: 'http://119.91.224.39:9090/',
|
||||
target: 'http://192.168.118.1:9090/',
|
||||
target: 'http://127.0.0.1:9090/',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user