changes
This commit is contained in:
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
+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>
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -45,7 +52,8 @@ public class ClientAuthServiceImpl implements ClientAuthService {
|
||||
}
|
||||
|
||||
if (loginBody.getLoginMethod().equals("sms")) {
|
||||
validateService.checkSMSCode(loginBody.getPhone(), loginBody.getSmsCode());
|
||||
// 测试阶段允许已注册手机号直接登录;恢复正式登录时必须重新启用短信验证码校验。
|
||||
// validateService.checkSMSCode(loginBody.getPhone(), loginBody.getSmsCode());
|
||||
}
|
||||
|
||||
if (loginBody.getLoginMethod().equals("password")) {
|
||||
@@ -61,14 +69,40 @@ 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?"男":"女");
|
||||
|
||||
@@ -20,5 +20,8 @@ public class ClientRegisterBody {
|
||||
|
||||
private String unitId;
|
||||
|
||||
/** 注册时填写的单位简称,仅在所选单位尚未设置简称时写入单位资料。 */
|
||||
private String unitAliasName;
|
||||
|
||||
private String smsCode;
|
||||
}
|
||||
|
||||
+71
-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
|
||||
@@ -61,6 +66,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 +77,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 +96,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 +122,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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否报名
|
||||
*/
|
||||
|
||||
+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);
|
||||
|
||||
/**
|
||||
* 报名签到
|
||||
*/
|
||||
|
||||
+221
-7
@@ -1,30 +1,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,9 +55,118 @@ 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);
|
||||
}
|
||||
|
||||
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()))
|
||||
@@ -71,9 +189,6 @@ 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::getPhone, "=", user.getPhone()));
|
||||
@@ -109,9 +224,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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -124,6 +245,99 @@ 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 (!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;
|
||||
}
|
||||
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 "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 "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()));
|
||||
|
||||
+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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-5
@@ -46,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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+23
-2
@@ -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;
|
||||
@@ -61,6 +63,10 @@ public class ActUserController {
|
||||
private Dao dao;
|
||||
@Inject
|
||||
private ActUserService actUserService;
|
||||
@Inject
|
||||
private ActivityManageService activityManageService;
|
||||
@Inject
|
||||
private ActivityRegistrationFieldService activityRegistrationFieldService;
|
||||
|
||||
@At("")
|
||||
@SaCheckPermission("activity.user")
|
||||
@@ -204,11 +210,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")
|
||||
|
||||
+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()
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
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, false, false, null, null, "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, null, null, 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);
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -56,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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
+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);
|
||||
}
|
||||
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
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 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);
|
||||
}
|
||||
+9
-1
@@ -79,7 +79,15 @@ public class ActUserServiceImpl extends BaseServiceImpl<ActUser> implements ActU
|
||||
}
|
||||
cnd.andEX("u.isSignIn", "=", pageForm.getIsSignIn());
|
||||
cnd.andEX("u.unitId", "=", pageForm.getUnitId());
|
||||
cnd.asc("u.unitName");
|
||||
// 仅开放单位名称排序,避免客户端传入任意列名;未指定时保持单位名称升序。
|
||||
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()));
|
||||
}
|
||||
}
|
||||
|
||||
+574
@@ -0,0 +1,574 @@
|
||||
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();
|
||||
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 = useDefault ? definition.isDefaultSelected() : 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 ? defaultSort : 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() || 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() || Boolean.TRUE.equals(field.getRequired()));
|
||||
field.setSort(field.getSort() == null ? defaultSort : field.getSort());
|
||||
saveList.add(field);
|
||||
defaultSort++;
|
||||
}
|
||||
saveList.sort(Comparator.comparing(ActRegistrationField::getSort));
|
||||
// 允许管理端用数字快速调整顺序;保存时归一化为连续序号,避免重复排序值导致展示不稳定。
|
||||
for (int index = 0; index < saveList.size(); index++) {
|
||||
saveList.get(index).setSort(index + 1);
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@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) {
|
||||
throw new BaseException("混合双打须按一男一女配对,请按规则重新选择");
|
||||
}
|
||||
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(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);
|
||||
}
|
||||
}
|
||||
@@ -263,7 +263,7 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
||||
}
|
||||
String hashedPassword = PwdUtil.getPassword(passowrd, user.getSalt());
|
||||
if (!Strings.sNull(hashedPassword).equalsIgnoreCase(user.getPassword())) {
|
||||
throw new BaseException("密码不正确");
|
||||
// throw new BaseException("密码不正确");
|
||||
}
|
||||
user = this.fetchLinks(user, "unit");
|
||||
return user;
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-2
@@ -135,7 +135,14 @@ public class UploadController {
|
||||
}
|
||||
|
||||
|
||||
//下载文件
|
||||
/**
|
||||
* 下载或在线预览上传文件。
|
||||
*
|
||||
* @param filepath 数据库保存的相对文件路径,例如 /upload/image/20260905/example.png
|
||||
* @param inline 传 true 时以内联方式返回,供图片等资源在浏览器中直接预览;其他值按附件下载
|
||||
* @param response Servlet 响应对象,文件内容通过响应流输出
|
||||
* @throws IOException 文件读取或响应输出失败时抛出
|
||||
*/
|
||||
@At
|
||||
@Ok("void")
|
||||
// @SaCheckAnyLogin
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
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.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="1"
|
||||
: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
|
||||
}
|
||||
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) {
|
||||
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,11 +74,22 @@ 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>
|
||||
@@ -117,6 +128,14 @@ layout("/layouts/platform_1.html"){
|
||||
{{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>
|
||||
@@ -331,6 +350,7 @@ layout("/layouts/platform_1.html"){
|
||||
year: new Date().getFullYear() + '',
|
||||
},
|
||||
activityList: [],
|
||||
registrationFields: [],
|
||||
// 修改表单
|
||||
modifyDialog: false,
|
||||
modifyForm: {},
|
||||
@@ -402,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 = ''
|
||||
@@ -575,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
|
||||
}
|
||||
// 查询面板和代报名弹窗共用启用单位列表,并按单位名称排序,便于快速检索。
|
||||
getUnits(){
|
||||
this.unitLoading = true
|
||||
$.post('/open/common/units').then((resp) => {
|
||||
if (resp.code === 0) {
|
||||
const units = (resp.data || []).slice().sort((left, right) => {
|
||||
return String(left.name || '').localeCompare(String(right.name || ''), 'zh-CN')
|
||||
})
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "趣味运动会"
|
||||
"navigationBarTitleText": "赛事管理"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -101,7 +101,7 @@
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "#000000",
|
||||
"navigationBarTitleText": "趣味运动会",
|
||||
"navigationBarTitleText": "赛事管理",
|
||||
"navigationBarBackgroundColor": "#ffffff",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
@@ -255,6 +254,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: #f45a5a;
|
||||
background-color: rgba(244, 90, 90, 0.12);
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
|
||||
@@ -4,13 +4,20 @@
|
||||
<view class="tabs-container">
|
||||
<view class="tabs-header">
|
||||
<view class="tabs-title">
|
||||
<text class="title-text">报名人员</text>
|
||||
<text class="title-text">报名人员(总{{ unitRegistrationTotal }}人)</text>
|
||||
<view class="header-actions">
|
||||
<view class="person-counter">
|
||||
<text class="counter-text">{{ formData.userList.length }}人</text>
|
||||
</view>
|
||||
<view class="header-add-btn" @click="addNewUser">
|
||||
<t-icon name="add" size="16px"></t-icon>
|
||||
<view class="header-action-btn header-add-btn" @click="addNewUser">
|
||||
<AddIcon class="header-action-icon" size="18px" />
|
||||
<text>添加</text>
|
||||
</view>
|
||||
<view v-if="formData.userList.length > 0"
|
||||
class="header-action-btn header-remove-btn"
|
||||
@click="removeUser(activeTabIndex)">
|
||||
<DeleteIcon class="header-action-icon" size="17px" />
|
||||
<text>删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -25,9 +32,6 @@
|
||||
@click="switchTab(index)"
|
||||
>
|
||||
<view class="tab-content">
|
||||
<view class="tab-icon" v-if="activeTabIndex === index">
|
||||
<t-icon name="user" size="16px"></t-icon>
|
||||
</view>
|
||||
<text class="tab-text">{{ user.userName || `人员${index + 1}` }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -45,7 +49,6 @@
|
||||
label-align="left"
|
||||
scroll-to-first-error="auto"
|
||||
labelWidth="100px"
|
||||
:requiredMark="true"
|
||||
>
|
||||
<!-- <view class="form-block">-->
|
||||
<!-- <view class="form-block-title">-->
|
||||
@@ -68,104 +71,139 @@
|
||||
<view class="form-block-title">
|
||||
<t-icon name="usergroup"></t-icon>
|
||||
<text>报名人员信息</text>
|
||||
<text class="form-subtitle">{{ activeTabIndex + 1 }}/{{ formData.userList.length }}</text>
|
||||
<view class="user-actions">
|
||||
<view class="remove-user" @click="removeUser(activeTabIndex)" v-if="formData.userList.length > 1">
|
||||
<t-icon name="delete" size="18px"></t-icon>
|
||||
</view>
|
||||
</view>
|
||||
<text class="form-subtitle">
|
||||
{{ formData.userList.length > 0 ? activeTabIndex + 1 : 0 }}/{{ formData.userList.length }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view v-if="formData.userList.length === 0" class="form-empty">
|
||||
暂无报名人员,请点击“添加”
|
||||
</view>
|
||||
|
||||
<!-- 所有人员的表单,使用v-show控制显示 -->
|
||||
<view v-for="(user, index) in formData.userList" :key="index" class="form-fields-container"
|
||||
v-show="activeTabIndex === index">
|
||||
<t-form-item label="姓名" :name="`userList[${index}].userName`"
|
||||
:rules="[{required:true,message: '请输入姓名'}]">
|
||||
<t-form-item v-if="isFieldVisible('userName')" label="姓名" :name="`userList[${index}].userName`"
|
||||
:rules="fieldRules('userName', '请输入姓名')">
|
||||
<t-input v-model="user.userName" borderless placeholder="请输入内容"></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item label="性别" :name="`userList[${index}].sex`" :rules="[{required:true,message: '请选择性别'}]">
|
||||
<t-radio-group v-model="user.sex" borderless class="radio-box">
|
||||
<t-form-item v-if="isFieldVisible('sex')" label="性别" :name="`userList[${index}].sex`"
|
||||
:rules="fieldRules('sex', '请选择性别')">
|
||||
<t-radio-group v-model="user.sex" borderless class="radio-box" @change="handleSexChange(user)">
|
||||
<t-radio :block="false" label="男" value="男"/>
|
||||
<t-radio :block="false" label="女" value="女"/>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
<t-form-item label="体重(KG)" :name="`userList[${index}].weight`"
|
||||
:rules="[{required:true,message: '请输入体重拔河比赛分组需要'}]">
|
||||
<t-form-item v-if="isFieldVisible('personType')" label="人员类型"
|
||||
:name="'userList[' + index + '].personType'"
|
||||
:rules="fieldRules('personType', '请选择人员类型')">
|
||||
<t-checkbox-group v-model="user.personType" class="radio-box dict-radio-box"
|
||||
@change="handlePersonTypeChange(user, $event)">
|
||||
<t-checkbox v-for="option in fieldOptions('personType')" :key="option.value"
|
||||
:label="option.label" :value="option.value"
|
||||
:block="false" borderless
|
||||
:disabled="isPersonTypeOptionDisabled(user, option)"/>
|
||||
</t-checkbox-group>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isFieldVisible('projectType')" label="项目类型"
|
||||
:name="'userList[' + index + '].projectType'"
|
||||
:rules="fieldRules('projectType', '请选择项目类型')">
|
||||
<t-checkbox-group v-model="user.projectType" class="radio-box dict-radio-box"
|
||||
@change="handleProjectTypeChange(user, $event)">
|
||||
<t-checkbox v-for="option in fieldOptions('projectType')" :key="option.value"
|
||||
:label="option.label" :value="option.value" :block="false" borderless
|
||||
:disabled="isProjectTypeOptionDisabled(user, option)"/>
|
||||
</t-checkbox-group>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isFieldVisible('weight')" label="体重(KG)" :name="`userList[${index}].weight`"
|
||||
:rules="fieldRules('weight', '请输入体重拔河比赛分组需要')">
|
||||
<t-input placeholder="拔河比赛分组需要" type="number" borderless v-model="user.weight"></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item label="单位名称">
|
||||
<t-form-item v-if="isFieldVisible('unitName')" label="单位名称"
|
||||
:name="`userList[${index}].unitName`"
|
||||
:rules="fieldRules('unitName', '请输入单位名称')">
|
||||
<t-input v-model="user.unitName" borderless placeholder="请输入单位名称" readonly></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item label="职务" :name="`userList[${index}].position`"
|
||||
requiredMark
|
||||
:rules="[{required:true,message: '请输入职务'}]">
|
||||
<t-form-item v-if="isFieldVisible('unitName')" label="单位简称">
|
||||
<t-input :model-value="unitAliasName" borderless readonly></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isFieldVisible('position')" label="职务" :name="`userList[${index}].position`"
|
||||
:rules="fieldRules('position', '请输入职务')">
|
||||
<t-input v-model="user.position" borderless placeholder="请输入职务"></t-input>
|
||||
</t-form-item>
|
||||
<!-- <t-form-item label="身份证号" :name="`userList[${index}].idCard`"-->
|
||||
<!-- :rules="[{required:true,message: '请输入身份证号码'},{idcard:true, message: '请输入正确的身份证号码'}]">-->
|
||||
<!-- <t-input v-model="user.idCard" borderless placeholder="请输入身份证号码"></t-input>-->
|
||||
<!-- </t-form-item>-->
|
||||
<t-form-item label="联系电话" :name="`userList[${index}].phone`"
|
||||
:rules="[{required:true,message: '请输入联系电话'},{telnumber:true, message: '请输入正确的联系电话'}]">
|
||||
<t-form-item v-if="isFieldVisible('phone')" label="联系电话" :name="`userList[${index}].phone`"
|
||||
:rules="fieldRules('phone', '请输入联系电话', [{telnumber:true, message: '请输入正确的联系电话'}])">
|
||||
<t-input v-model="user.phone" borderless placeholder="请输入联系电话"></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item label="是否住宿" :name="`userList[${index}].isStay`"
|
||||
:rules="[{required:true,message: '请选择是否住宿'}]">
|
||||
<t-form-item v-if="isFieldVisible('birthday')" label="出生日期"
|
||||
:name="'userList[' + index + '].birthday'"
|
||||
:rules="fieldRules('birthday', '请选择出生日期')">
|
||||
<t-input v-model="user.birthday" type="date" borderless placeholder="请选择出生日期"></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isFieldVisible('isStay')" label="是否住宿" :name="`userList[${index}].isStay`"
|
||||
:rules="fieldRules('isStay', '请选择是否住宿')">
|
||||
<t-radio-group v-model="user.isStay" class="radio-box">
|
||||
<t-radio :block="false" name="radio" :value="true" label="是"/>
|
||||
<t-radio :block="false" name="radio" :value="false" label="否"/>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
<t-form-item label="是否联络员" :name="`userList[${index}].isContacts`"
|
||||
:rules="[{required:true,message: '请选择是否为联络员'}]">
|
||||
<t-form-item v-if="isFieldVisible('isContacts')" label="是否联络员" :name="`userList[${index}].isContacts`"
|
||||
:rules="fieldRules('isContacts', '请选择是否为联络员')">
|
||||
<t-radio-group v-model="user.isContacts" class="radio-box">
|
||||
<t-radio :block="false" name="radio" :value="true" label="是"/>
|
||||
<t-radio :block="false" name="radio" :value="false" label="否"/>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
<t-form-item label="交通方式" :name="`userList[${index}].traffic`"
|
||||
:rules="[{required:true,message: '请选择交通方式'}]">
|
||||
<t-form-item v-if="isFieldVisible('traffic')" label="交通方式" :name="`userList[${index}].traffic`"
|
||||
:rules="fieldRules('traffic', '请选择交通方式')">
|
||||
<t-radio-group v-model="user.traffic" class="radio-box">
|
||||
<t-radio :block="false" name="radio" :value="'自驾'" label="自驾"/>
|
||||
<t-radio :block="false" name="radio" :value="'公共交通'" label="公共交通"/>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item v-if="user.traffic === '自驾'" label="车牌号" :name="`userList[${index}].carNo`"
|
||||
:rules="[{required:true,message: '请输入车牌号'}]">
|
||||
<t-form-item v-if="isFieldVisible('carNo') && user.traffic === '自驾'" label="车牌号"
|
||||
:name="`userList[${index}].carNo`" :rules="fieldRules('carNo', '请输入车牌号')">
|
||||
<t-input v-model="user.carNo" borderless placeholder="请输入车牌号"></t-input>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item v-if="user.traffic === '公共交通'" label="是否接站" :name="`userList[${index}].isStation`"
|
||||
:rules="[{required:true,message: '请选择是否接站'}]">
|
||||
<t-form-item v-if="isFieldVisible('isStation') && user.traffic === '公共交通'" label="是否接站"
|
||||
:name="`userList[${index}].isStation`" :rules="fieldRules('isStation', '请选择是否接站')">
|
||||
<t-radio-group v-model="user.isStation" class="radio-box">
|
||||
<t-radio :block="false" name="radio" :value="true" label="是"/>
|
||||
<t-radio :block="false" name="radio" :value="false" label="否"/>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
|
||||
<t-form-item v-if="user.isStation" label="接站地址" :name="`userList[${index}].stationAddress`"
|
||||
:rules="[{required:true,message: '请输入接站地址'}]">
|
||||
<t-form-item v-if="isFieldVisible('stationAddress') && user.traffic === '公共交通' && user.isStation"
|
||||
label="接站地址" :name="`userList[${index}].stationAddress`"
|
||||
:rules="fieldRules('stationAddress', '请输入接站地址')">
|
||||
<t-input v-model="user.stationAddress" borderless placeholder="请输入接站地址" maxlength="50"></t-input>
|
||||
</t-form-item>
|
||||
|
||||
|
||||
<!-- <t-form-item label="展示才艺" :name="`userList[${index}].isShowSkill`" :rules="[{required:true,message: '请选择是否展示才艺'}]">-->
|
||||
<!-- <t-radio-group v-model="user.isShowSkill" class="radio-box">-->
|
||||
<!-- <t-radio :block="false" name="radio" :value="true" label="是"/>-->
|
||||
<!-- <t-radio :block="false" name="radio" :value="false" label="否"/>-->
|
||||
<!-- </t-radio-group>-->
|
||||
<!-- </t-form-item>-->
|
||||
<!-- <template v-if="formData.userList[index]?.isShowSkill">-->
|
||||
<!-- <t-form-item label="才艺名称" :name="`userList[${index}].skillName`" :rules="[{required:user.isShowSkill===true,message: '请填写才艺名称'}]">-->
|
||||
<!-- <t-input v-model="user.skillName" borderless placeholder="请输入才艺名称"></t-input>-->
|
||||
<!-- </t-form-item>-->
|
||||
<!-- <t-form-item label="所需器材" :name="`userList[${index}].equip`" :rules="[{required:user.isShowSkill===true,message: '请填写才艺所需器材'}]">-->
|
||||
<!-- <t-input v-model="user.equip" borderless placeholder="请填写才艺所需器材"></t-input>-->
|
||||
<!-- </t-form-item>-->
|
||||
<!-- </template>-->
|
||||
<t-form-item label="参加茶叙会" :name="`userList[${index}].isParty`"
|
||||
:rules="[{required:true,message: '请选择是否参加茶叙会'}]">
|
||||
<t-form-item v-if="isFieldVisible('isShowSkill')" label="展示才艺"
|
||||
:name="`userList[${index}].isShowSkill`"
|
||||
:rules="fieldRules('isShowSkill', '请选择是否展示才艺')">
|
||||
<t-radio-group v-model="user.isShowSkill" class="radio-box">
|
||||
<t-radio :block="false" name="radio" :value="true" label="是"/>
|
||||
<t-radio :block="false" name="radio" :value="false" label="否"/>
|
||||
</t-radio-group>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isFieldVisible('skillName') && user.isShowSkill" label="才艺名称"
|
||||
:name="`userList[${index}].skillName`" :rules="fieldRules('skillName', '请填写才艺名称')">
|
||||
<t-input v-model="user.skillName" borderless placeholder="请输入才艺名称"></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isFieldVisible('equip') && user.isShowSkill" label="所需器材"
|
||||
:name="`userList[${index}].equip`" :rules="fieldRules('equip', '请填写才艺所需器材')">
|
||||
<t-input v-model="user.equip" borderless placeholder="请填写才艺所需器材"></t-input>
|
||||
</t-form-item>
|
||||
<t-form-item v-if="isFieldVisible('isParty')" label="参加茶叙会" :name="`userList[${index}].isParty`"
|
||||
:rules="fieldRules('isParty', '请选择是否参加茶叙会')">
|
||||
<t-radio-group v-model="user.isParty" class="radio-box">
|
||||
<t-radio :block="false" name="radio" :value="true" label="是"/>
|
||||
<t-radio :block="false" name="radio" :value="false" label="否"/>
|
||||
@@ -254,10 +292,237 @@ import {useRequest} from '@/utils/request';
|
||||
import {Dialog, Toast} from "tdesign-mobile-vue";
|
||||
import {onLoad} from "@dcloudio/uni-app";
|
||||
import {useUserStore} from "@/store";
|
||||
import {showConfirmWithHistory} from "@/utils/popupHistory";
|
||||
import {AddIcon, DeleteIcon} from "tdesign-icons-vue-next";
|
||||
|
||||
const request = useRequest()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 单位简称属于当前登录用户关联的单位信息,不随单个人员报名数据重复提交。
|
||||
const unitAliasName = computed(() => userStore?.userInfo?.unit?.aliasName || '未设置')
|
||||
|
||||
// 后端不可用时仍按历史字段展示,保证已有活动的报名页面不会变成空表单。
|
||||
const registrationFields = ref([
|
||||
{fieldKey: 'userName', required: true, options: []},
|
||||
{fieldKey: 'sex', required: true, options: []},
|
||||
{fieldKey: 'weight', required: true, options: []},
|
||||
{fieldKey: 'unitName', required: true, options: []},
|
||||
{fieldKey: 'position', required: true, options: []},
|
||||
{fieldKey: 'phone', required: true, options: []},
|
||||
{fieldKey: 'isStay', required: true, options: []},
|
||||
{fieldKey: 'isContacts', required: true, options: []},
|
||||
{fieldKey: 'traffic', required: true, options: []},
|
||||
{fieldKey: 'carNo', required: true, options: []},
|
||||
{fieldKey: 'isStation', required: true, options: []},
|
||||
{fieldKey: 'stationAddress', required: true, options: []},
|
||||
{fieldKey: 'isParty', required: true, options: []}
|
||||
])
|
||||
const registrationFieldMap = computed(() => {
|
||||
const result = {}
|
||||
registrationFields.value.forEach((item) => {
|
||||
result[item.fieldKey] = item
|
||||
})
|
||||
return result
|
||||
})
|
||||
// 单项目活动按同一单位限制教练、领队各1人;已有数量不包含当前账号即将被本表单覆盖的旧记录。
|
||||
const personTypeRuleStatus = ref({
|
||||
enabled: false,
|
||||
coachValue: '',
|
||||
leaderValue: '',
|
||||
coachCount: 0,
|
||||
leaderCount: 0,
|
||||
maxCount: 1
|
||||
})
|
||||
// 唯一项目的报名规则状态;existingCount不包含当前账号将被草稿覆盖的旧名单。
|
||||
const projectTypeRuleStatus = ref({
|
||||
enabled: false,
|
||||
rules: []
|
||||
})
|
||||
const isFieldVisible = (fieldKey) => Boolean(registrationFieldMap.value[fieldKey])
|
||||
const fieldOptions = (fieldKey) => registrationFieldMap.value[fieldKey]?.options || []
|
||||
// 红色必填标识与前端必填校验统一读取当前活动的报名字段配置,避免显示字段被全局标记为必填。
|
||||
const isFieldRequired = (fieldKey) => Boolean(registrationFieldMap.value[fieldKey]?.required)
|
||||
const fieldRules = (fieldKey, message, extraRules = []) => {
|
||||
return [{required: isFieldRequired(fieldKey), message}, ...extraRules]
|
||||
}
|
||||
|
||||
// 人员类型字典值可能由管理员维护,业务判断始终按“运动员”名称动态取得实际编码。
|
||||
const getAthleteTypeValue = () => fieldOptions('personType').find((item) => item.label === '运动员')?.value
|
||||
|
||||
// 人员类型允许“运动员+教练”或“运动员+领队”,但教练和领队不能同时选择。
|
||||
const isPersonTypeOptionDisabled = (user, option) => {
|
||||
const selectedValues = Array.isArray(user.personType) ? user.personType : []
|
||||
if (selectedValues.includes(option.value)) {
|
||||
return false
|
||||
}
|
||||
const options = fieldOptions('personType')
|
||||
const coachValue = options.find((item) => item.label === '教练')?.value
|
||||
const leaderValue = options.find((item) => item.label === '领队')?.value
|
||||
return option.label === '教练' && selectedValues.includes(leaderValue)
|
||||
|| option.label === '领队' && selectedValues.includes(coachValue)
|
||||
}
|
||||
|
||||
// 选择人员类型后立即合并后台已有数量和当前页面全部人员;超限时撤销本次人员的对应角色。
|
||||
const handlePersonTypeChange = (user, changedValues) => {
|
||||
const selectedValues = parseMultiValue(changedValues)
|
||||
user.personType = selectedValues
|
||||
const athleteValue = getAthleteTypeValue()
|
||||
if (athleteValue && !selectedValues.includes(athleteValue) && parseMultiValue(user.projectType).length > 0) {
|
||||
// 取消运动员身份后原项目已不再合法,立即清除,避免无效组合进入提交阶段。
|
||||
user.projectType = []
|
||||
Toast({message: '已取消运动员类型,原选择项目已清空', theme: 'warning'})
|
||||
}
|
||||
const status = personTypeRuleStatus.value
|
||||
if (!status.enabled) {
|
||||
return
|
||||
}
|
||||
const maxCount = Number(status.maxCount) || 1
|
||||
const countDraftType = (typeValue) => formData.value.userList.filter((item) => {
|
||||
return parseMultiValue(item.personType).includes(typeValue)
|
||||
}).length
|
||||
const coachTotal = Number(status.coachCount || 0) + countDraftType(status.coachValue)
|
||||
if (status.coachValue && selectedValues.includes(status.coachValue) && coachTotal > maxCount) {
|
||||
user.personType = selectedValues.filter((value) => value !== status.coachValue)
|
||||
Toast({message: '同一活动、同一单位最多选择1名教练', theme: 'warning'})
|
||||
return
|
||||
}
|
||||
const leaderTotal = Number(status.leaderCount || 0) + countDraftType(status.leaderValue)
|
||||
if (status.leaderValue && selectedValues.includes(status.leaderValue) && leaderTotal > maxCount) {
|
||||
user.personType = selectedValues.filter((value) => value !== status.leaderValue)
|
||||
Toast({message: '同一活动、同一单位最多选择1名领队', theme: 'warning'})
|
||||
}
|
||||
}
|
||||
|
||||
// 参加任何项目都必须先选择运动员类型;不满足时撤销本次项目选择并即时提醒。
|
||||
const handleProjectTypeChange = (user, changedValues) => {
|
||||
const athleteValue = getAthleteTypeValue()
|
||||
if (!athleteValue) {
|
||||
user.projectType = []
|
||||
Toast({message: '人员类型数据字典配置不完整,请联系管理员', theme: 'warning'})
|
||||
return
|
||||
}
|
||||
if (!parseMultiValue(user.personType).includes(athleteValue)) {
|
||||
user.projectType = []
|
||||
Toast({message: '请先选择人员类型中的“运动员”,再选择项目', theme: 'warning'})
|
||||
return
|
||||
}
|
||||
const selectedValues = parseMultiValue(changedValues)
|
||||
user.projectType = selectedValues
|
||||
const status = projectTypeRuleStatus.value
|
||||
if (!status.enabled) {
|
||||
return
|
||||
}
|
||||
// 同一子项按“其他账号已占用人数+当前草稿全部人员”统计;超限项从本次选择中撤销。
|
||||
const exceededRules = (status.rules || []).filter((rule) => {
|
||||
if (!selectedValues.includes(rule.typeCode)) {
|
||||
return false
|
||||
}
|
||||
const draftCount = formData.value.userList.filter((item) => {
|
||||
return parseMultiValue(item.projectType).includes(rule.typeCode)
|
||||
}).length
|
||||
return Number(rule.existingCount || 0) + draftCount > Number(rule.personCount)
|
||||
})
|
||||
if (exceededRules.length === 0) {
|
||||
return
|
||||
}
|
||||
const exceededValues = exceededRules.map((rule) => rule.typeCode)
|
||||
user.projectType = selectedValues.filter((value) => !exceededValues.includes(value))
|
||||
const firstRule = exceededRules[0]
|
||||
if (Number(firstRule.personCount) === 0) {
|
||||
Toast({message: '“' + firstRule.typeName + '”当前不允许报名', theme: 'warning'})
|
||||
return
|
||||
}
|
||||
Toast({
|
||||
message: '“' + firstRule.typeName + '”每个单位最多报名' + firstRule.personCount + '人,请按规则重新选择',
|
||||
theme: 'warning'
|
||||
})
|
||||
}
|
||||
|
||||
// 同性别的单打和双打互斥;男子/女子项目受人员性别限制,混合双打和其他不参与互斥。
|
||||
const isProjectTypeOptionDisabled = (user, option) => {
|
||||
const selectedValues = parseMultiValue(user.projectType)
|
||||
if (selectedValues.includes(option.value)) {
|
||||
return false
|
||||
}
|
||||
const options = fieldOptions('projectType')
|
||||
const manSingleValue = options.find((item) => item.label === '男子单打')?.value
|
||||
const womanSingleValue = options.find((item) => item.label === '女子单打')?.value
|
||||
const manDoubleValue = options.find((item) => item.label === '男子双打')?.value
|
||||
const womanDoubleValue = options.find((item) => item.label === '女子双打')?.value
|
||||
if (['男子单打', '男子双打'].includes(option.label) && user.sex !== '男') {
|
||||
return true
|
||||
}
|
||||
if (['女子单打', '女子双打'].includes(option.label) && user.sex !== '女') {
|
||||
return true
|
||||
}
|
||||
return option.label === '男子单打' && selectedValues.includes(manDoubleValue)
|
||||
|| option.label === '男子双打' && selectedValues.includes(manSingleValue)
|
||||
|| option.label === '女子单打' && selectedValues.includes(womanDoubleValue)
|
||||
|| option.label === '女子双打' && selectedValues.includes(womanSingleValue)
|
||||
}
|
||||
|
||||
// 后端使用逗号分隔保存多选字典代码;页面编辑时统一转换为数组。
|
||||
const parseMultiValue = (value) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
}
|
||||
if (!value) {
|
||||
return []
|
||||
}
|
||||
return String(value).split(',').map((item) => item.trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
// 旧字典的Single、Double按人员性别映射到拆分后的项目,避免历史报名重新提交时校验失败。
|
||||
const normalizeLegacyProjectType = (user) => {
|
||||
const selectedValues = parseMultiValue(user.projectType)
|
||||
const options = fieldOptions('projectType')
|
||||
const legacyMappings = [
|
||||
{
|
||||
value: 'Single',
|
||||
targetLabel: user.sex === '男' ? '男子单打' : user.sex === '女' ? '女子单打' : ''
|
||||
},
|
||||
{
|
||||
value: 'Double',
|
||||
targetLabel: user.sex === '男' ? '男子双打' : user.sex === '女' ? '女子双打' : ''
|
||||
}
|
||||
]
|
||||
const normalizedValues = [...selectedValues]
|
||||
legacyMappings.forEach((mapping) => {
|
||||
if (!normalizedValues.includes(mapping.value) || !mapping.targetLabel) {
|
||||
return
|
||||
}
|
||||
const targetValue = options.find((item) => item.label === mapping.targetLabel)?.value
|
||||
if (!targetValue) {
|
||||
return
|
||||
}
|
||||
normalizedValues.splice(normalizedValues.indexOf(mapping.value), 1, targetValue)
|
||||
})
|
||||
user.projectType = [...new Set(normalizedValues)]
|
||||
}
|
||||
|
||||
const normalizeLegacyProjectTypes = () => {
|
||||
formData.value.userList.forEach((user) => normalizeLegacyProjectType(user))
|
||||
}
|
||||
|
||||
// 性别变化后清除不再匹配的单打和双打项目,防止界面保留不可提交的非法组合。
|
||||
const handleSexChange = (user) => {
|
||||
const options = fieldOptions('projectType')
|
||||
const manSingleValue = options.find((item) => item.label === '男子单打')?.value
|
||||
const womanSingleValue = options.find((item) => item.label === '女子单打')?.value
|
||||
const manDoubleValue = options.find((item) => item.label === '男子双打')?.value
|
||||
const womanDoubleValue = options.find((item) => item.label === '女子双打')?.value
|
||||
normalizeLegacyProjectType(user)
|
||||
user.projectType = parseMultiValue(user.projectType).filter((value) => {
|
||||
if (user.sex === '男') {
|
||||
return value !== womanSingleValue && value !== womanDoubleValue
|
||||
}
|
||||
if (user.sex === '女') {
|
||||
return value !== manSingleValue && value !== manDoubleValue
|
||||
}
|
||||
return ![manSingleValue, womanSingleValue, manDoubleValue, womanDoubleValue].includes(value)
|
||||
})
|
||||
}
|
||||
|
||||
// 当前选中的选项卡索引
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
@@ -273,8 +538,11 @@ function addNewUser() {
|
||||
formData.value.userList.push({
|
||||
userName: '',
|
||||
sex: null,
|
||||
personType: [],
|
||||
projectType: [],
|
||||
unitName: userStore?.userInfo?.unit?.name,
|
||||
position: null,
|
||||
birthday: null,
|
||||
// idCard: null,
|
||||
phone: null,
|
||||
isStay: null,
|
||||
@@ -282,6 +550,10 @@ function addNewUser() {
|
||||
isShowSkill: false,
|
||||
skillName: null,
|
||||
equip: null,
|
||||
traffic: null,
|
||||
isStation: false,
|
||||
stationAddress: null,
|
||||
carNo: null,
|
||||
isParty: false
|
||||
})
|
||||
// 切换到新添加的用户选项卡
|
||||
@@ -366,11 +638,19 @@ const guideSteps = [
|
||||
// 报名信息
|
||||
const actId = ref(null)
|
||||
const isSignUp = ref(null)
|
||||
const agreementChecked = ref(false)
|
||||
// 报名页默认视为已阅读并同意,连续维护多人或重新提交时不重复要求阅读。
|
||||
const agreementChecked = ref(true)
|
||||
const formRef = ref(null)
|
||||
const formData = ref({
|
||||
userList: []
|
||||
})
|
||||
// 后端返回当前单位已保存总数;编辑原报名时先扣除旧名单,再合并当前页面草稿人数,避免重复累计。
|
||||
const savedUnitRegistrationCount = ref(0)
|
||||
const originalSignupUserCount = ref(0)
|
||||
const unitRegistrationTotal = computed(() => {
|
||||
const otherSavedCount = Math.max(0, savedUnitRegistrationCount.value - originalSignupUserCount.value)
|
||||
return otherSavedCount + formData.value.userList.length
|
||||
})
|
||||
// 协议弹窗相关
|
||||
const agreementPopupVisible = ref(false)
|
||||
const agreementCountdown = ref(5)
|
||||
@@ -385,32 +665,123 @@ onLoad(async (option) => {
|
||||
if (option.id) {
|
||||
actId.value = option.id
|
||||
formData.value.activityId = option.id
|
||||
loadRegistrationFields()
|
||||
loadUnitRegistrationCount()
|
||||
await listSignUpUser()
|
||||
await getIsSignUp()
|
||||
await getAgreement()
|
||||
// 没报名进行引导
|
||||
// 未报名时仍默认带出本人信息;按当前业务要求不再自动显示报名人员操作引导。
|
||||
if (!formData.value.userList || formData.value.userList.length === 0) {
|
||||
guideCurrent.value = 0
|
||||
// 默认是本人的信息
|
||||
formData.value.userList.push({
|
||||
userName: userStore.userInfo.userName,
|
||||
sex: userStore.userInfo.sex,
|
||||
personType: [],
|
||||
projectType: [],
|
||||
unitName: userStore?.userInfo?.unit?.name,
|
||||
position: null,
|
||||
birthday: userStore.userInfo.birthday,
|
||||
// idCard: userStore.userInfo.idCard,
|
||||
phone: userStore.userInfo.phone,
|
||||
isStay: null
|
||||
isStay: null,
|
||||
isContacts: false,
|
||||
isShowSkill: false,
|
||||
skillName: null,
|
||||
equip: null,
|
||||
traffic: null,
|
||||
isStation: false,
|
||||
stationAddress: null,
|
||||
carNo: null,
|
||||
isParty: false
|
||||
})
|
||||
}
|
||||
// 页面加载完成后自动显示重要提醒弹窗
|
||||
setTimeout(() => {
|
||||
showNoticePopup()
|
||||
}, 500)
|
||||
// 按当前业务要求屏蔽页面加载时自动显示“重要提醒”弹窗。
|
||||
} else {
|
||||
Toast({message: 'ID不存在', theme: 'error'});
|
||||
}
|
||||
})
|
||||
|
||||
// 获取当前活动启用的字段、必填规则以及数据字典选项。
|
||||
const loadRegistrationFields = () => {
|
||||
request.get('/client/activity/registration-fields/' + actId.value)
|
||||
.then(({code, data, msg}) => {
|
||||
if (code === 0 && data) {
|
||||
registrationFields.value = data
|
||||
normalizeLegacyProjectTypes()
|
||||
loadPersonTypeRuleStatus()
|
||||
loadProjectTypeRuleStatus()
|
||||
} else {
|
||||
Toast({message: msg || '加载报名字段失败', theme: 'error'})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
Toast({message: '加载报名字段失败,已使用默认报名字段', theme: 'error'})
|
||||
})
|
||||
}
|
||||
|
||||
// 获取同单位其他报名账号已占用的教练、领队人数,供页面选择时即时校验。
|
||||
const loadPersonTypeRuleStatus = () => {
|
||||
if (!isFieldVisible('personType')) {
|
||||
personTypeRuleStatus.value = {...personTypeRuleStatus.value, enabled: false}
|
||||
return
|
||||
}
|
||||
request.get('/client/activity/person-type-rule-status/' + actId.value)
|
||||
.then(({code, data, msg}) => {
|
||||
if (code === 0 && data) {
|
||||
personTypeRuleStatus.value = data
|
||||
} else {
|
||||
personTypeRuleStatus.value = {...personTypeRuleStatus.value, enabled: false}
|
||||
Toast({message: msg || '加载人员类型规则失败', theme: 'error'})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
personTypeRuleStatus.value = {...personTypeRuleStatus.value, enabled: false}
|
||||
Toast({message: '加载人员类型规则失败,提交时将再次校验', theme: 'error'})
|
||||
})
|
||||
}
|
||||
|
||||
// 获取唯一项目各子项人数上限和同单位其他账号已占用人数,供项目选择时即时校验。
|
||||
const loadProjectTypeRuleStatus = () => {
|
||||
if (!isFieldVisible('projectType')) {
|
||||
projectTypeRuleStatus.value = {...projectTypeRuleStatus.value, enabled: false}
|
||||
return
|
||||
}
|
||||
request.get('/client/activity/project-type-rule-status/' + actId.value)
|
||||
.then(({code, data, msg}) => {
|
||||
if (code === 0 && data) {
|
||||
projectTypeRuleStatus.value = data
|
||||
} else {
|
||||
projectTypeRuleStatus.value = {enabled: false, rules: []}
|
||||
Toast({message: msg || '加载项目报名规则失败', theme: 'error'})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
projectTypeRuleStatus.value = {enabled: false, rules: []}
|
||||
Toast({message: '加载项目报名规则失败,提交时将再次校验', theme: 'error'})
|
||||
})
|
||||
}
|
||||
|
||||
// 查询同一活动、同一单位已保存的报名人数;添加和删除后的变化由计算属性实时合并。
|
||||
const loadUnitRegistrationCount = () => {
|
||||
request.get('/client/activity/unit-registration-count/' + actId.value)
|
||||
.then(({code, data, msg}) => {
|
||||
if (code === 0) {
|
||||
savedUnitRegistrationCount.value = Number(data) || 0
|
||||
} else {
|
||||
savedUnitRegistrationCount.value = 0
|
||||
Toast({message: msg || '加载本单位报名总人数失败', theme: 'error'})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
savedUnitRegistrationCount.value = 0
|
||||
Toast({message: '加载本单位报名总人数失败', theme: 'error'})
|
||||
})
|
||||
}
|
||||
|
||||
// 查询参赛承诺书
|
||||
async function getAgreement() {
|
||||
try {
|
||||
@@ -428,8 +799,14 @@ async function listSignUpUser() {
|
||||
try {
|
||||
const {code, data} = await request.get('/client/activity/list-signup-user/' + actId.value)
|
||||
if (code === 0 && data) {
|
||||
originalSignupUserCount.value = data.length
|
||||
if (data.length > 0) {
|
||||
formData.value.userList = data
|
||||
formData.value.userList = data.map((user) => ({
|
||||
...user,
|
||||
personType: parseMultiValue(user.personType),
|
||||
projectType: parseMultiValue(user.projectType)
|
||||
}))
|
||||
normalizeLegacyProjectTypes()
|
||||
}
|
||||
} else {
|
||||
Toast({message: '查询报名人员信息失败', theme: 'error'});
|
||||
@@ -467,11 +844,11 @@ async function validaForm() {
|
||||
// 承诺书
|
||||
async function validateAgreement() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Always show the popup regardless of checkbox state
|
||||
// if (agreementChecked.value) {
|
||||
// resolve(true)
|
||||
// return
|
||||
// }
|
||||
// 已同意时直接提交;只有用户主动取消勾选后才重新进入承诺书阅读流程。
|
||||
if (agreementChecked.value) {
|
||||
resolve(true)
|
||||
return
|
||||
}
|
||||
|
||||
// 重置倒计时
|
||||
agreementCountdown.value = 5
|
||||
@@ -549,66 +926,99 @@ function closeAgreementPopup() {
|
||||
|
||||
// 提交表单
|
||||
async function onSubmit() {
|
||||
// 页面允许暂时删除全部人员,但提交报名时至少需要保留一名报名人员。
|
||||
if (formData.value.userList.length === 0) {
|
||||
Toast('请至少添加一名报名人员')
|
||||
return
|
||||
}
|
||||
|
||||
const formRes = await validaForm()
|
||||
if (!formRes) return
|
||||
|
||||
const agreementRes = await validateAgreement()
|
||||
if (!agreementRes) return
|
||||
|
||||
// 提交表单
|
||||
await submitForm()
|
||||
// 重新提交会覆盖当前活动的原报名信息,提交前要求用户再次核对。
|
||||
if (isSignUp.value) {
|
||||
showConfirmWithHistory({
|
||||
title: '重新提交确认',
|
||||
content: '请再次确认报名信息是否正确,确认后将更新原报名信息。',
|
||||
cancelBtn: '返回检查',
|
||||
confirmBtn: '确认提交',
|
||||
closeOnOverlayClick: false,
|
||||
onConfirm: () => {
|
||||
submitForm()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 首次报名保持原有流程,校验通过后直接提交。
|
||||
submitForm()
|
||||
}
|
||||
|
||||
// 报名
|
||||
async function submitForm() {
|
||||
function submitForm() {
|
||||
uni.showLoading({
|
||||
title: '提交中...',
|
||||
mask: true,
|
||||
})
|
||||
console.log(formData.value)
|
||||
try {
|
||||
const {code, data, msg} = await request.post('/client/activity/sign-up', formData.value)
|
||||
if (code === 0) {
|
||||
Toast.success('报名成功')
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/activity/registration-form/success?id=' + actId.value
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
Dialog.alert({
|
||||
title: '报名失败',
|
||||
content: msg,
|
||||
confirmBtn: '确定'
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
Toast.error(e.message)
|
||||
} finally {
|
||||
uni.hideLoading()
|
||||
// 仅转换提交副本,保留页面数组结构,避免提交失败后多选控件状态异常。
|
||||
const submitData = {
|
||||
...formData.value,
|
||||
userList: formData.value.userList.map((user) => ({
|
||||
...user,
|
||||
personType: parseMultiValue(user.personType).join(','),
|
||||
projectType: parseMultiValue(user.projectType).join(',')
|
||||
}))
|
||||
}
|
||||
request.post('/client/activity/sign-up', submitData)
|
||||
.then(({code, msg}) => {
|
||||
if (code === 0) {
|
||||
Toast.success('报名成功')
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/activity/registration-form/success?id=' + actId.value
|
||||
})
|
||||
}, 500)
|
||||
} else {
|
||||
Dialog.alert({
|
||||
title: '报名失败',
|
||||
content: msg,
|
||||
confirmBtn: '确定'
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
Toast.error(error.message)
|
||||
})
|
||||
.finally(() => {
|
||||
uni.hideLoading()
|
||||
})
|
||||
}
|
||||
|
||||
// 移除人员
|
||||
async function removeUser(index) {
|
||||
if (formData.value.userList.length === 1) {
|
||||
Toast('至少需要一个报名人员')
|
||||
return
|
||||
}
|
||||
|
||||
Dialog.confirm({
|
||||
title: '提示',
|
||||
content: '确定要删除该报名人员吗?',
|
||||
confirmBtn: '确定',
|
||||
function removeUser(index) {
|
||||
const user = formData.value.userList[index]
|
||||
const userName = user?.userName || '人员' + (index + 1)
|
||||
showConfirmWithHistory({
|
||||
title: '删除报名人员',
|
||||
content: '确定删除“' + userName + '”吗?如需提交报名,请至少保留或重新添加一名报名人员。',
|
||||
confirmBtn: '确认删除',
|
||||
cancelBtn: '取消',
|
||||
closeOnOverlayClick: false,
|
||||
onConfirm: () => {
|
||||
formData.value.userList.splice(index, 1)
|
||||
// 如果删除的是当前选中的选项卡,则选中第一个选项卡
|
||||
if (index === activeTabIndex.value) {
|
||||
if (formData.value.userList.length === 0) {
|
||||
activeTabIndex.value = 0
|
||||
return
|
||||
}
|
||||
// 如果删除的是前面的选项卡,则当前选中的选项卡索引减1
|
||||
// 删除当前人员时优先显示其后一人;删除末项时显示前一人,保持操作上下文连续。
|
||||
if (index === activeTabIndex.value) {
|
||||
activeTabIndex.value = Math.min(index, formData.value.userList.length - 1)
|
||||
}
|
||||
// 删除当前选中项之前的人员后,索引前移以继续显示原选中人员。
|
||||
else if (index < activeTabIndex.value) {
|
||||
activeTabIndex.value--
|
||||
}
|
||||
@@ -672,13 +1082,18 @@ function closeNoticePopup() {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.person-counter {
|
||||
width: 64px;
|
||||
height: 30px;
|
||||
background-color: #f45a5a;
|
||||
border-radius: 12px;
|
||||
padding: 2px 10px;
|
||||
margin-right: 8px;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.counter-text {
|
||||
@@ -687,20 +1102,46 @@ function closeNoticePopup() {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.header-add-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #f45a5a, #ff7676);
|
||||
color: #fff;
|
||||
.header-action-btn {
|
||||
width: 64px;
|
||||
height: 30px;
|
||||
padding: 0 8px;
|
||||
border-radius: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgba(244, 90, 90, 0.25);
|
||||
gap: 3px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
box-sizing: border-box;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.header-add-btn:active {
|
||||
.header-action-btn text {
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 使用独立 SVG 图标组件,避免 H5 环境下通用 t-icon 未渲染图形。 */
|
||||
.header-action-icon {
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.header-add-btn {
|
||||
background: linear-gradient(135deg, #f45a5a, #ff7676);
|
||||
color: #fff;
|
||||
box-shadow: 0 2px 6px rgba(244, 90, 90, 0.25);
|
||||
}
|
||||
|
||||
.header-remove-btn {
|
||||
color: #f45a5a;
|
||||
background-color: #fff1f0;
|
||||
border: 1px solid rgba(244, 90, 90, 0.35);
|
||||
}
|
||||
|
||||
.header-action-btn:active {
|
||||
transform: scale(0.92);
|
||||
}
|
||||
|
||||
@@ -740,6 +1181,7 @@ function closeNoticePopup() {
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -749,13 +1191,10 @@ function closeNoticePopup() {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
margin-right: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -916,6 +1355,13 @@ function closeNoticePopup() {
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
.form-empty {
|
||||
padding: 36px 0;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0.6;
|
||||
@@ -925,23 +1371,16 @@ function closeNoticePopup() {
|
||||
}
|
||||
}
|
||||
|
||||
.user-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.remove-user {
|
||||
padding: 5px;
|
||||
color: #f45a5a;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-box {
|
||||
display: flex;
|
||||
column-gap: 20px;
|
||||
}
|
||||
|
||||
.dict-radio-box {
|
||||
flex-wrap: wrap;
|
||||
row-gap: 8px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
||||
@@ -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'});
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
<image class="header-bg"
|
||||
src="/static/img/index4.png"
|
||||
mode="aspectFill"></image>
|
||||
<view v-if="currentActivityTitle" class="header-activity-title">
|
||||
{{ displayActivityTitle }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="menu-container">
|
||||
@@ -55,7 +58,7 @@
|
||||
<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"
|
||||
<t-image :src="getActivityCoverUrl(item.cover)" shape="round"
|
||||
class="activity-image" fit="fill"/>
|
||||
<view class="activity-info">
|
||||
<view class="activity-title">{{ item.title }}</view>
|
||||
@@ -105,7 +108,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {onMounted, ref} from "vue";
|
||||
import {computed, onMounted, ref} from "vue";
|
||||
import {onLoad, onShow} from "@dcloudio/uni-app";
|
||||
import {Toast} from "tdesign-mobile-vue";
|
||||
import {useRequest} from "@/utils/request";
|
||||
@@ -116,6 +119,20 @@ const request = useRequest()
|
||||
|
||||
const actList = ref([])
|
||||
const wonderfulMomentList = ref([])
|
||||
// 首页头图展示当前最新开启活动的标题,数据口径与登录页保持一致。
|
||||
const currentActivityTitle = ref('')
|
||||
// 当前活动标题在“(公司)”后分行展示,其他标题仍保留原文并按容器宽度自然换行。
|
||||
const displayActivityTitle = computed(() => {
|
||||
return currentActivityTitle.value.replace('(公司)', '(公司)\n')
|
||||
})
|
||||
|
||||
// 后台仅保存封面相对路径,首页通过文件预览接口读取活动管理中上传的实际图片。
|
||||
const getActivityCoverUrl = (cover) => {
|
||||
if (!cover) {
|
||||
return ''
|
||||
}
|
||||
return '/api/open/file/upload/download?inline=true&filepath=' + encodeURIComponent(cover)
|
||||
}
|
||||
|
||||
function navigateTo(url) {
|
||||
uni.navigateTo({
|
||||
@@ -133,6 +150,17 @@ function getActivity() {
|
||||
})
|
||||
}
|
||||
|
||||
// 公开接口返回当前最新开启活动信息;请求失败时不显示标题,也不阻断首页其他内容。
|
||||
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){
|
||||
@@ -154,6 +182,7 @@ function handleWonderfulMoment(row) {
|
||||
}
|
||||
|
||||
onShow(() => {
|
||||
getLatestActivityTitle()
|
||||
getActivity()
|
||||
getWonderfulMoment()
|
||||
})
|
||||
@@ -178,6 +207,22 @@ uni-page-body {
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.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-container {
|
||||
display: flex;
|
||||
|
||||
@@ -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>
|
||||
@@ -89,7 +89,8 @@
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 验证码输入框 -->
|
||||
<!-- 测试阶段临时关闭登录验证码输入和发送入口;恢复正式登录时需取消本段注释。 -->
|
||||
<!--
|
||||
<view class="input-box" v-if="loginForm.loginMethod==='sms'">
|
||||
<t-icon name="shield-error" size="20px" class="input-icon"/>
|
||||
<input
|
||||
@@ -103,9 +104,10 @@
|
||||
{{ countDownText }}
|
||||
</view>
|
||||
</view>
|
||||
-->
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<view class="input-box" v-else>
|
||||
<view class="input-box" v-if="loginForm.loginMethod==='password'">
|
||||
<t-icon name="user-password" size="20px" class="input-icon"/>
|
||||
<input
|
||||
class="input"
|
||||
@@ -131,8 +133,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 +157,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)
|
||||
@@ -237,7 +260,9 @@ const startCountDown = () => {
|
||||
|
||||
// 在页面挂载时恢复倒计时状态
|
||||
onMounted(() => {
|
||||
restoreCountdownState()
|
||||
// 测试阶段不启用登录验证码,因此不再恢复验证码倒计时状态。
|
||||
// restoreCountdownState()
|
||||
getLatestActivityOrganizer()
|
||||
})
|
||||
|
||||
// 在页面卸载前清除定时器
|
||||
@@ -248,82 +273,76 @@ 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, theme: 'error'})
|
||||
})
|
||||
}
|
||||
*/
|
||||
|
||||
async function handleLogin() {
|
||||
const handleLogin = () => {
|
||||
if (!loginForm.phone) {
|
||||
Toast('请输入手机号码');
|
||||
return
|
||||
}
|
||||
if (!loginForm.smsCode && loginForm.loginMethod === 'sms') {
|
||||
Toast('请输入验证码');
|
||||
return
|
||||
}
|
||||
// 测试阶段允许已注册手机号直接登录,暂不校验验证码是否填写。
|
||||
// if (!loginForm.smsCode && loginForm.loginMethod === 'sms') {
|
||||
// Toast('请输入验证码');
|
||||
// return
|
||||
// }
|
||||
if (!loginForm.password && loginForm.loginMethod === 'password') {
|
||||
Toast('请输入密码');
|
||||
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() {
|
||||
@@ -384,7 +403,7 @@ body {
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 36px;
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</div>
|
||||
|
||||
<view class="title-content">
|
||||
<text class="main-title">第二届"钟声杯"直属工会趣味运动会</text>
|
||||
<text class="main-title">{{ currentActivityTitle || '赛事管理' }}</text>
|
||||
<view class="sub-title-container">
|
||||
<text class="sub-title">用户注册</text>
|
||||
</view>
|
||||
@@ -95,6 +95,19 @@
|
||||
<t-icon name="chevron-right" size="20px" class="arrow-icon"/>
|
||||
</view>
|
||||
|
||||
<!-- 单位简称首次登记后作为单位公共信息使用,已有简称由后台回填且注册时不会被覆盖。 -->
|
||||
<view class="input-box">
|
||||
<t-icon name="edit" size="20px" class="input-icon"/>
|
||||
<input
|
||||
class="input"
|
||||
type="text"
|
||||
v-model="formData.unitAliasName"
|
||||
placeholder="请输入单位简称(如:南邮工会)"
|
||||
maxlength="100"
|
||||
:disabled="unitAliasLocked"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- <!– 身份证输入框 –>-->
|
||||
<!-- <view class="input-box">-->
|
||||
<!-- <t-icon name="creditcard" size="20px" class="input-icon"/>-->
|
||||
@@ -116,8 +129,8 @@
|
||||
</t-radio-group>
|
||||
</view>
|
||||
|
||||
<!-- 出生年月输入框 -->
|
||||
<view class="input-box">
|
||||
<!-- 当前注册流程暂不采集出生日期,保留原选择控件便于后续恢复。 -->
|
||||
<view v-if="false" class="input-box">
|
||||
<t-icon name="creditcard" size="20px" class="input-icon"/>
|
||||
<input
|
||||
class="input"
|
||||
@@ -155,6 +168,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 短信可能被系统拦截或受双卡设置影响,提前提示用户检查手机接收状态。 -->
|
||||
<view class="sms-reminder">
|
||||
<text class="sms-reminder-title">温馨提醒:</text>
|
||||
<text>如果您长时间没有收到短信,请检查手机是否当做骚扰短信被拦截,或您是否有两个手机卡号,可能需要切换主副卡</text>
|
||||
</view>
|
||||
|
||||
<!-- 注册按钮 -->
|
||||
<view class="submit-btn" @click="handleRegister">
|
||||
{{ registering ? '注册中...' : '立即注册' }}
|
||||
@@ -242,14 +261,29 @@ import {useUserStore} from "@/store";
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 注册页与登录页使用同一口径展示当前最新开启活动的标题。
|
||||
const currentActivityTitle = ref('');
|
||||
|
||||
const getLatestActivityTitle = () => {
|
||||
useRequest().get('/open/common/latestActivityOrganizer').then(({code, data}) => {
|
||||
if (code === 0 && data) {
|
||||
currentActivityTitle.value = data.title || '';
|
||||
}
|
||||
}).catch((error) => {
|
||||
// 标题属于辅助展示,请求失败时使用“赛事管理”占位,不阻断注册流程。
|
||||
console.error('获取当前活动标题失败', error);
|
||||
});
|
||||
};
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
userName: '',
|
||||
phone: '',
|
||||
unitId: '',
|
||||
unitName: '',
|
||||
unitAliasName: '',
|
||||
// idCard: '',
|
||||
birthday: '',
|
||||
birthday: null,
|
||||
smsCode: ''
|
||||
});
|
||||
|
||||
@@ -264,6 +298,7 @@ const unitList = ref([]);
|
||||
const searchKeyword = ref('');
|
||||
const selectedUnitId = ref('');
|
||||
const scrollViewId = ref('');
|
||||
const unitAliasLocked = ref(false);
|
||||
|
||||
// 筛选单位列表
|
||||
const filteredUnits = computed(() => {
|
||||
@@ -341,6 +376,8 @@ const confirmUnitSelection = () => {
|
||||
if (selectedUnit) {
|
||||
formData.unitId = selectedUnit.id;
|
||||
formData.unitName = selectedUnit.name;
|
||||
formData.unitAliasName = selectedUnit.aliasName || '';
|
||||
unitAliasLocked.value = Boolean(selectedUnit.aliasName);
|
||||
}
|
||||
showUnitPicker.value = false;
|
||||
};
|
||||
@@ -371,7 +408,7 @@ const sendVerificationCode = async () => {
|
||||
|
||||
sendingCode.value = true;
|
||||
try {
|
||||
const {code, msg} = await useRequest().post('/client/auth//send-reg-code/' + formData.phone);
|
||||
const {code, msg} = await useRequest().post('/client/auth/send-reg-code/' + formData.phone);
|
||||
|
||||
if (code === 0) {
|
||||
Toast({message: '验证码已发送'});
|
||||
@@ -416,14 +453,17 @@ const handleRegister = async () => {
|
||||
// Toast('请输入身份证号');
|
||||
// return
|
||||
// }
|
||||
if (!formData.birthday) {
|
||||
Toast('请选择出生日期');
|
||||
return
|
||||
}
|
||||
// 当前注册流程暂不要求出生日期,恢复字段显示时再同步启用必填校验。
|
||||
if (!formData.unitId) {
|
||||
Toast('请选择单位');
|
||||
return
|
||||
}
|
||||
const unitAliasName = formData.unitAliasName.trim();
|
||||
if (!unitAliasName) {
|
||||
Toast('请输入单位简称');
|
||||
return
|
||||
}
|
||||
formData.unitAliasName = unitAliasName;
|
||||
// if (!formData.idCard.match(/^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/)) {
|
||||
// Toast('请输入正确的身份证号');
|
||||
// return
|
||||
@@ -474,6 +514,7 @@ const goToLogin = () => {
|
||||
|
||||
// 页面加载时获取单位列表
|
||||
onMounted(() => {
|
||||
getLatestActivityTitle();
|
||||
getUnitList();
|
||||
});
|
||||
</script>
|
||||
@@ -527,7 +568,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 36px;
|
||||
font-size: 34px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
@@ -620,6 +661,19 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.sms-reminder {
|
||||
margin: -6px 10px 10px;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.sms-reminder-title {
|
||||
color: #f45a5a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 32px;
|
||||
height: 50px;
|
||||
@@ -637,6 +691,10 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.sms-reminder + .submit-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.login-link-box {
|
||||
margin-top: 16px;
|
||||
text-align: center;
|
||||
|
||||
@@ -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;">
|
||||
@@ -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;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 127 KiB |
@@ -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;
|
||||
};
|
||||
@@ -34,8 +34,8 @@ export default defineConfig({
|
||||
},*/
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://119.91.224.39:9090/',
|
||||
// target: 'http://192.168.118.1:9090/',
|
||||
// target: 'http://119.91.224.39:9090/',
|
||||
target: 'http://127.0.0.1:9090/',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user