Compare commits
5
Commits
09832d6346
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e01856cc2b | ||
|
|
6903d39c68 | ||
|
|
a92d3c99c5 | ||
|
|
f1cbd97b86 | ||
|
|
a3c77b4f85 |
@@ -0,0 +1,8 @@
|
|||||||
|
@echo off
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
"C:\Program Files\AdoptOpenJDK\jdk-15.0.2.7-hotspot\bin\java.exe" --enable-preview -Dnutz.boot.configure.properties=D:\work\project --add-opens java.base/java.lang=ALL-UNNAMED -jar "%~dp0mini-5.3.0.jar"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo 程序已退出,错误码:%errorlevel%
|
||||||
|
pause
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<pinyin4j.veision>2.5.1</pinyin4j.veision>
|
<pinyin4j.veision>2.5.1</pinyin4j.veision>
|
||||||
<poi.version>4.1.2</poi.version>
|
<poi.version>4.1.2</poi.version>
|
||||||
<tencentcloud-sdk-java.version>4.0.11</tencentcloud-sdk-java.version>
|
<tencentcloud-sdk-java.version>4.0.11</tencentcloud-sdk-java.version>
|
||||||
<lombok.version>1.18.12</lombok.version>
|
<lombok.version>1.18.32</lombok.version>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<v.version>1.0.1</v.version>
|
<v.version>1.0.1</v.version>
|
||||||
</properties>
|
</properties>
|
||||||
@@ -463,9 +463,12 @@
|
|||||||
<artifactId>maven-compiler-plugin</artifactId>
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
<version>3.7.0</version>
|
<version>3.7.0</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<source>15</source>
|
<source>17</source>
|
||||||
<target>15</target>
|
<target>17</target>
|
||||||
<compilerArgs>--enable-preview</compilerArgs>
|
<compilerArgs>
|
||||||
|
<arg>--add-opens</arg>
|
||||||
|
<arg>java.base/java.lang=ALL-UNNAMED</arg>
|
||||||
|
</compilerArgs>
|
||||||
<useIncrementalCompilation>false</useIncrementalCompilation>
|
<useIncrementalCompilation>false</useIncrementalCompilation>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|||||||
+27
@@ -5,12 +5,15 @@ import io.v.nutz.annontation.ViReturn;
|
|||||||
import io.v.nutz.brandtrainactivity.models.BrandTrainSignUpActivity;
|
import io.v.nutz.brandtrainactivity.models.BrandTrainSignUpActivity;
|
||||||
import io.v.nutz.brandtrainactivity.models.BrandTrainSignUpUser;
|
import io.v.nutz.brandtrainactivity.models.BrandTrainSignUpUser;
|
||||||
import io.v.nutz.brandtrainactivity.service.BrandTrainSignUpActivityService;
|
import io.v.nutz.brandtrainactivity.service.BrandTrainSignUpActivityService;
|
||||||
|
import io.v.nutz.welfare.service.WelfareProjectService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.dao.Sqls;
|
import org.nutz.dao.Sqls;
|
||||||
import org.nutz.dao.sql.Sql;
|
import org.nutz.dao.sql.Sql;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.json.Json;
|
import org.nutz.json.Json;
|
||||||
@@ -38,16 +41,26 @@ public class BrandTrainSignUpActivityAddController {
|
|||||||
@Inject
|
@Inject
|
||||||
private BrandTrainSignUpActivityService brandTrainSignUpActivityService;
|
private BrandTrainSignUpActivityService brandTrainSignUpActivityService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private WelfareProjectService welfareProjectService;
|
||||||
|
|
||||||
@At("/add")
|
@At("/add")
|
||||||
@RequiresPermissions("brandTrainSingUp.manage.activity.add")
|
@RequiresPermissions("brandTrainSingUp.manage.activity.add")
|
||||||
@Ok("beetl:/platform/brandTrainSingUp/manage/activity_add.html")
|
@Ok("beetl:/platform/brandTrainSingUp/manage/activity_add.html")
|
||||||
public void add() {
|
public void add() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存品牌活动及课程配置,新增与编辑共用同一份活动JSON。
|
||||||
|
*
|
||||||
|
* @param data 品牌活动实体JSON,包含课程列表、范围限制及可选的签到扩展配置;id为空时新增
|
||||||
|
* @return 无业务数据,由ViReturn包装为包含code、msg、data的操作结果
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@ViReturn
|
@ViReturn
|
||||||
@Ok("json:full")
|
@Ok("json:full")
|
||||||
@POST
|
@POST
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
@RequiresPermissions("brandTrainSingUp.manage.activity.add")
|
@RequiresPermissions("brandTrainSingUp.manage.activity.add")
|
||||||
public Object doHandle(@Param(value = "data", required = false) String data) {
|
public Object doHandle(@Param(value = "data", required = false) String data) {
|
||||||
BrandTrainSignUpActivity activity = Json.fromJson(BrandTrainSignUpActivity.class, data);
|
BrandTrainSignUpActivity activity = Json.fromJson(BrandTrainSignUpActivity.class, data);
|
||||||
@@ -99,4 +112,18 @@ public class BrandTrainSignUpActivityAddController {
|
|||||||
List<BrandTrainSignUpActivity> query = dao.query(BrandTrainSignUpActivity.class, Cnd.NEW().desc("activityStartTime"));
|
List<BrandTrainSignUpActivity> query = dao.query(BrandTrainSignUpActivity.class, Cnd.NEW().desc("activityStartTime"));
|
||||||
return query;
|
return query;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取品牌活动签到达标后可引导选择的福利项目,本接口不接收参数。
|
||||||
|
*
|
||||||
|
* @return 福利项目列表,由ViReturn包装为code、msg、data;data每项的id、name供表单选择
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@ViReturn
|
||||||
|
@Ok("json:full")
|
||||||
|
@RequiresPermissions("brandTrainSingUp.manage.activity")
|
||||||
|
public Object getAvailableWelfareProjectList() {
|
||||||
|
return welfareProjectService.getAvailableSignWelfareProjects();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
package io.v.nutz.brandtrainactivity.controller.mobile;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import io.v.nutz.base.result.Result;
|
||||||
|
import io.v.nutz.brandtrainactivity.service.BrandTrainSignUpActivityService;
|
||||||
|
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.POST;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
import org.nutz.trans.Trans;
|
||||||
|
|
||||||
|
/** 已开启签到扩展的品牌活动点位扫码入口,登录与页面渲染沿用当前手机端方式。 */
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/mobile/brandTrain/scanCode")
|
||||||
|
@Ok("json:full")
|
||||||
|
public class BrandTrainScanCodeController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private BrandTrainSignUpActivityService brandTrainSignUpActivityService;
|
||||||
|
|
||||||
|
@At("")
|
||||||
|
@RequiresAuthentication
|
||||||
|
@Ok("beetl:/mobile/brandTrain/scanCode.html")
|
||||||
|
public void scanCode() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param courseId 二维码携带的课程ID,用户ID从当前登录态获取
|
||||||
|
* @return Result的code/msg/data;code=0时data包含签到地点坐标、当前点位签到状态、活动进度及达标事项
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@RequiresAuthentication
|
||||||
|
public Result getSignData(@Param("courseId") String courseId) {
|
||||||
|
if (StrUtil.isBlank(courseId)) {
|
||||||
|
return Result.error(99, "缺少签到点位信息");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
NutMap data = brandTrainSignUpActivityService.getScanCodeSignData(courseId, ShiroUtil.getPlatformUid());
|
||||||
|
return Result.success().addData(data);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户在签到地图中手动确认签到,浏览页面及获取定位本身均不写入签到记录。
|
||||||
|
*
|
||||||
|
* @param courseId 二维码携带的品牌课程ID,当前用户从服务端登录态取得
|
||||||
|
* @param longitude 保留兼容的GCJ-02经度,腾讯地图余额耗尽、临时停用定位校验期间可不传
|
||||||
|
* @param latitude 保留兼容的GCJ-02纬度,临时停用定位和范围校验期间可不传
|
||||||
|
* @return Result的code/msg/data;0签到成功,99业务提示,已签到时data仍保留当前状态和达标事项
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@RequiresAuthentication
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result doSign(@Param("courseId") String courseId, @Param("longitude") Double longitude,
|
||||||
|
@Param("latitude") Double latitude) {
|
||||||
|
if (StrUtil.isBlank(courseId)) {
|
||||||
|
return Result.error(99, "缺少签到点位信息");
|
||||||
|
}
|
||||||
|
// 腾讯地图余额耗尽,暂不要求定位;恢复时需同步恢复前端定位及service点位范围校验。
|
||||||
|
// if (longitude == null || latitude == null) {
|
||||||
|
// return Result.error(99, "请先获取当前定位后再签到");
|
||||||
|
// }
|
||||||
|
try {
|
||||||
|
NutMap result = brandTrainSignUpActivityService.handleScanCodeSign(courseId, ShiroUtil.getPlatformUid(), longitude, latitude);
|
||||||
|
return Result.NEW().addCode(result.getInt("code")).addMsg(result.getString("msg")).addData(result.get("data"));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 当前Nutz嵌套事务未提供rollbackOnly;转换业务响应前回滚当前连接,保留外层AOP的事务计数。
|
||||||
|
if (Trans.get() != null) {
|
||||||
|
brandTrainSignUpActivityService.dao().run(connection -> connection.rollback());
|
||||||
|
}
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param courseId 当前扫码课程ID,用于定位活动及动态字段配置
|
||||||
|
* @param mobileColumnsValue JSON数组,每项包含columnCode及columnValue;附件值保留数组结构
|
||||||
|
* @return Result,code=0表示保存成功,code=99表示权限、达标或字段校验未通过
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@RequiresAuthentication
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public Result saveDynamicForm(@Param("courseId") String courseId,
|
||||||
|
@Param("mobileColumnsValue") String mobileColumnsValue) {
|
||||||
|
if (StrUtil.isBlank(courseId) || StrUtil.isBlank(mobileColumnsValue)) {
|
||||||
|
return Result.error(99, "请填写当前签到点位的信息");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
brandTrainSignUpActivityService.saveScanCodeDynamicForm(courseId, ShiroUtil.getPlatformUid(), mobileColumnsValue);
|
||||||
|
return Result.success("保存成功");
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// 显式回滚已加入外层事务的写入,避免捕获service异常后被controller外层提交。
|
||||||
|
if (Trans.get() != null) {
|
||||||
|
brandTrainSignUpActivityService.dao().run(connection -> connection.rollback());
|
||||||
|
}
|
||||||
|
return Result.error(99, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+33
-19
@@ -11,10 +11,12 @@ import io.v.nutz.data.service.MatchConditionStructureService;
|
|||||||
import io.v.nutz.modules.models.PageForm;
|
import io.v.nutz.modules.models.PageForm;
|
||||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||||
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
||||||
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Chain;
|
import org.nutz.dao.Chain;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
import org.nutz.integration.jedis.RedisService;
|
import org.nutz.integration.jedis.RedisService;
|
||||||
|
import org.nutz.ioc.aop.Aop;
|
||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
@@ -166,27 +168,24 @@ public class MBrandTrainSignUpActivityController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GPS签到,半径与坐标以服务端课程配置为准。
|
||||||
|
* @param id 当前登录用户的课次签到记录ID
|
||||||
|
* @param courseId 当前课程ID,service核对其与签到记录的对应关系
|
||||||
|
* @param points 当前GCJ-02定位坐标,JSON数组[经度,纬度]
|
||||||
|
* @return ViReturn包装的code/msg/data;成功无业务data,参数或距离错误返回业务提示
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("json:full")
|
@Ok("json:full")
|
||||||
@POST
|
@POST
|
||||||
@ViReturn
|
@ViReturn
|
||||||
@RequiresAuthentication
|
@RequiresAuthentication
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public Object doQd(@Param("id") String id, @Param("courseId") String courseId, @Param("point") Double[] points) {
|
public Object doQd(@Param("id") String id, @Param("courseId") String courseId, @Param("point") Double[] points) {
|
||||||
BrandTrainSignUpCourse course = dao.fetch(BrandTrainSignUpCourse.class, courseId);
|
if (StrUtil.isBlank(id) || StrUtil.isBlank(courseId) || points == null || points.length != 2) {
|
||||||
List<Double> coursePoints = course.getCourseLocationCoordinates();
|
return Result.error("请获取当前定位后再签到");
|
||||||
// if (Lang.isNotEmpty(coursePoints)) {
|
}
|
||||||
// //需要签到
|
brandTrainSignUpActivityService.doQd(id, courseId, ShiroUtil.getPlatformUid(), points[0], points[1], 3);
|
||||||
// if (ArrayUtil.isEmpty(points) || ArrayUtil.hasNull(points)) {
|
|
||||||
// return Result.error().addMsg("请获取当前的坐标信息");
|
|
||||||
// }
|
|
||||||
// Double[] coursePointArray = coursePoints.toArray(new Double[]{});
|
|
||||||
// float distance = AMapUtils.calculateLineDistance(new LatLng(points[0], points[1]), new LatLng(coursePointArray[0], coursePointArray[1]));
|
|
||||||
//
|
|
||||||
// if (distance > 500) {
|
|
||||||
// return Result.error().addMsg("请到签到点位附近签到");
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
brandTrainSignUpActivityService.doQd(id);
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -285,19 +284,34 @@ public class MBrandTrainSignUpActivityController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旧课程二维码签到,与GPS签到复用service的用户归属和配置半径校验。
|
||||||
|
* @param id 当前登录用户的课次签到记录ID
|
||||||
|
* @param codeCourseId 二维码解析出的课程ID
|
||||||
|
* @param clickCourseId 页面点击的课程ID,必须与二维码课程一致
|
||||||
|
* @param longitude 当前GCJ-02定位经度
|
||||||
|
* @param latitude 当前GCJ-02定位纬度
|
||||||
|
* @return ViReturn包装的code/msg/data;成功无业务data,校验失败返回业务提示
|
||||||
|
*/
|
||||||
@At
|
@At
|
||||||
@Ok("json:full")
|
@Ok("json:full")
|
||||||
|
@POST
|
||||||
@ViReturn
|
@ViReturn
|
||||||
@RequiresAuthentication
|
@RequiresAuthentication
|
||||||
public Object codeSign(String id, String codeCourseId, String clickCourseId) {
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
if(StrUtil.isBlank(codeCourseId) || StrUtil.isBlank(clickCourseId)) {
|
public Object codeSign(@Param("id") String id, @Param("codeCourseId") String codeCourseId,
|
||||||
|
@Param("clickCourseId") String clickCourseId, @Param("longitude") Double longitude,
|
||||||
|
@Param("latitude") Double latitude) {
|
||||||
|
if(StrUtil.isBlank(id) || StrUtil.isBlank(codeCourseId) || StrUtil.isBlank(clickCourseId)) {
|
||||||
return Result.error("签到失败,没有获取到扫描信息");
|
return Result.error("签到失败,没有获取到扫描信息");
|
||||||
}
|
}
|
||||||
if(!codeCourseId.equals(clickCourseId)) {
|
if(!codeCourseId.equals(clickCourseId)) {
|
||||||
return Result.error("签到失败,二维码与您当前签到信息不符");
|
return Result.error("签到失败,二维码与您当前签到信息不符");
|
||||||
}
|
}
|
||||||
dao.update(BrandTrainSignUpUserCourse.class, Chain.make("isAttend", true)
|
if (longitude == null || latitude == null) {
|
||||||
.add("attendTime", new Date()), Cnd.where("id", "=", id));
|
return Result.error("请获取当前定位后再签到");
|
||||||
|
}
|
||||||
|
brandTrainSignUpActivityService.doQd(id, clickCourseId, ShiroUtil.getPlatformUid(), longitude, latitude, 1);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
@@ -1,5 +1,8 @@
|
|||||||
package io.v.nutz.brandtrainactivity.controller.statistics;
|
package io.v.nutz.brandtrainactivity.controller.statistics;
|
||||||
|
|
||||||
|
import io.v.nutz.base.result.Result;
|
||||||
|
import io.v.nutz.base.utils.CommonDownloadUtil;
|
||||||
|
|
||||||
|
|
||||||
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||||
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
||||||
@@ -347,4 +350,27 @@ public class BrandTrainSignUpActivityStatisticsController {
|
|||||||
zipOutputStream.flush();
|
zipOutputStream.flush();
|
||||||
zipOutputStream.close();
|
zipOutputStream.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载活动签到后填写的信息,不接收人员ID或动态字段定义。
|
||||||
|
*
|
||||||
|
* @param activityId 活动主键,导出该活动下按人员合并的填写记录
|
||||||
|
* @param response 成功返回xlsx文件流;参数错误返回包含code、msg的JSON对象
|
||||||
|
* @throws IOException 无法写入HTTP响应时抛出
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@Ok("void")
|
||||||
|
@RequiresPermissions("brandTrainSingUp.statistics.activity")
|
||||||
|
public void exportFillForm(@Param("activityId") String activityId,
|
||||||
|
HttpServletResponse response) throws IOException {
|
||||||
|
try {
|
||||||
|
Workbook workbook = brandTrainSignUpActivityStatisticsService.exportFillFormWorkbook(activityId);
|
||||||
|
CommonDownloadUtil.download("填写信息.xlsx", workbook, response);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
response.reset();
|
||||||
|
response.setCharacterEncoding("UTF-8");
|
||||||
|
response.setContentType("application/json");
|
||||||
|
response.getWriter().write(Result.error(99, e.getMessage()).toJsonString());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,4 +118,19 @@ public class BrandTrainSignUpActivity extends BaseModel implements Serializable
|
|||||||
@Comment("活动类型")
|
@Comment("活动类型")
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
private String trainType;
|
private String trainType;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
@Comment("签到达标点位数,空或0表示未开启签到扩展")
|
||||||
|
private Integer signPointCount;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
@Comment("签到达标处理方式:1填写信息,2选择福利,3无")
|
||||||
|
private Integer signHandleType;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.VARCHAR, width = 32)
|
||||||
|
@Comment("签到达标后关联的福利项目ID")
|
||||||
|
private String welfareProjectId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,6 +63,11 @@ public class BrandTrainSignUpCourse extends BaseModel implements Serializable {
|
|||||||
@Comment("课程地点坐标")
|
@Comment("课程地点坐标")
|
||||||
private List<Double> courseLocationCoordinates;
|
private List<Double> courseLocationCoordinates;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
@ColDefine(type = ColType.INT)
|
||||||
|
@Comment("签到点位半径,单位米;未配置时使用250米")
|
||||||
|
private Integer signRadius;
|
||||||
|
|
||||||
@Column
|
@Column
|
||||||
@ColDefine(type = ColType.VARCHAR, width = 20)
|
@ColDefine(type = ColType.VARCHAR, width = 20)
|
||||||
@Comment("课程讲师")
|
@Comment("课程讲师")
|
||||||
|
|||||||
+39
-2
@@ -113,11 +113,17 @@ public interface BrandTrainSignUpActivityService extends ViService<BrandTrainSig
|
|||||||
boolean isSignCourseByUser(String courseId, String userId);
|
boolean isSignCourseByUser(String courseId, String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手机端签到
|
* 原手机端扫码/GPS签到,共用服务端点位范围校验。
|
||||||
*
|
*
|
||||||
* @param id 每个培训班每节课每个用户的记录ID
|
* @param id 每个培训班每节课每个用户的记录ID
|
||||||
|
* @param courseId 当前签到课程ID,必须与签到记录所属课程一致
|
||||||
|
* @param userId 当前登录用户ID,由服务端登录态取得
|
||||||
|
* @param longitude 当前GCJ-02经度,范围[-180,180]
|
||||||
|
* @param latitude 当前GCJ-02纬度,范围[-90,90]
|
||||||
|
* @param signType 当前入口的签到方式,1扫码、3 GPS,必须与课程配置一致
|
||||||
|
* @return 无返回值;校验失败抛出业务异常,成功更新签到状态和时间
|
||||||
*/
|
*/
|
||||||
void doQd(String id);
|
void doQd(String id, String courseId, String userId, Double longitude, Double latitude, int signType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 某个用户的签到信息
|
* 某个用户的签到信息
|
||||||
@@ -127,6 +133,37 @@ public interface BrandTrainSignUpActivityService extends ViService<BrandTrainSig
|
|||||||
*/
|
*/
|
||||||
List<NutMap> qdInfoByUserId(String userId, String activityId);
|
List<NutMap> qdInfoByUserId(String userId, String activityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 只读预览扫码点位、当前点位签到状态和活动进度,不补报名、不签到、不新增福利名单。
|
||||||
|
*
|
||||||
|
* @param courseId 品牌课程ID,仅支持已开启签到扩展的扫码签到课程
|
||||||
|
* @param userId 当前登录用户ID,由服务端登录态取得
|
||||||
|
* @return data对象;courseLocationCoordinates为[经度,纬度],currentPointSigned为当前课程是否已签,signRadius为米数
|
||||||
|
*/
|
||||||
|
NutMap getScanCodeSignData(String courseId, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前用户在地图页手动签到,累计不重复的课程点位,并处理原有达标事项。
|
||||||
|
* 腾讯地图余额耗尽期间临时停用定位和范围校验,恢复时需同步恢复前端、controller和service中的校验。
|
||||||
|
*
|
||||||
|
* @param courseId 品牌课程ID,不接受普通培训课程或非扫码签到课程
|
||||||
|
* @param userId 当前登录用户ID,由服务端登录态取得
|
||||||
|
* @param longitude 保留兼容的GCJ-02经度,临时停用定位校验期间可不传
|
||||||
|
* @param latitude 保留兼容的GCJ-02纬度,临时停用定位和范围校验期间可不传
|
||||||
|
* @return code/msg/data结构;data含签到进度、动态表单、福利跳转及达标处理状态
|
||||||
|
*/
|
||||||
|
NutMap handleScanCodeSign(String courseId, String userId, Double longitude, Double latitude);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存品牌活动达标后的动态字段,同步至该用户在本活动已签到课程的报名记录。
|
||||||
|
*
|
||||||
|
* @param courseId 当前扫码品牌课程ID,用于确定活动及统一的课程类型
|
||||||
|
* @param userId 当前登录用户ID,由服务端登录态取得
|
||||||
|
* @param mobileColumnsValue JSON数组,每项包含columnCode和columnValue,附件保留数组结构
|
||||||
|
* @throws IllegalArgumentException 未达标、无参与权限或字段校验不通过时抛出业务提示
|
||||||
|
*/
|
||||||
|
void saveScanCodeDynamicForm(String courseId, String userId, String mobileColumnsValue);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询活动的报名条件
|
* 查询活动的报名条件
|
||||||
* @param activityId
|
* @param activityId
|
||||||
|
|||||||
+11
@@ -1,5 +1,7 @@
|
|||||||
package io.v.nutz.brandtrainactivity.service;
|
package io.v.nutz.brandtrainactivity.service;
|
||||||
|
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
|
||||||
import cn.wizzer.framework.page.Pagination;
|
import cn.wizzer.framework.page.Pagination;
|
||||||
import io.v.nutz.modules.models.PageForm;
|
import io.v.nutz.modules.models.PageForm;
|
||||||
import io.v.nutz.service.ViService;
|
import io.v.nutz.service.ViService;
|
||||||
@@ -55,4 +57,13 @@ public interface BrandTrainSignUpActivityStatisticsService extends ViService<Bra
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
List<NutMap> baoMingUserList(String activityId);
|
List<NutMap> baoMingUserList(String activityId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按活动、人员汇总签到后填写的信息,同一人员仅导出一行。
|
||||||
|
*
|
||||||
|
* @param activityId 活动主键;活动内课程须使用同一种动态字段配置
|
||||||
|
* @return XSSF 工作簿,包含人员信息、已签到课程及课程类型配置的动态字段
|
||||||
|
*/
|
||||||
|
Workbook exportFillFormWorkbook(String activityId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+569
-1
@@ -2,6 +2,7 @@ package io.v.nutz.brandtrainactivity.service.impl;
|
|||||||
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.wizzer.framework.page.Pagination;
|
import cn.wizzer.framework.page.Pagination;
|
||||||
|
import io.v.nutz.activity.models.ActivityUserScope;
|
||||||
import io.v.nutz.base.utils.DateUtil;
|
import io.v.nutz.base.utils.DateUtil;
|
||||||
import io.v.nutz.base.utils.Vi;
|
import io.v.nutz.base.utils.Vi;
|
||||||
import io.v.nutz.brandtrainactivity.models.*;
|
import io.v.nutz.brandtrainactivity.models.*;
|
||||||
@@ -15,6 +16,9 @@ import io.v.nutz.modules.models.PageForm;
|
|||||||
import io.v.nutz.service.impl.ViServiceImpl;
|
import io.v.nutz.service.impl.ViServiceImpl;
|
||||||
import io.v.nutz.sys.models.Sys_user;
|
import io.v.nutz.sys.models.Sys_user;
|
||||||
import io.v.nutz.web.commons.utils.ShiroUtil;
|
import io.v.nutz.web.commons.utils.ShiroUtil;
|
||||||
|
import io.v.nutz.welfare.model.WelfareList;
|
||||||
|
import io.v.nutz.welfare.model.WelfareProject;
|
||||||
|
import io.v.nutz.welfare.service.WelfareListService;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Chain;
|
import org.nutz.dao.Chain;
|
||||||
@@ -29,13 +33,20 @@ import org.nutz.ioc.loader.annotation.Inject;
|
|||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Lang;
|
import org.nutz.lang.Lang;
|
||||||
import org.nutz.lang.Mirror;
|
import org.nutz.lang.Mirror;
|
||||||
|
import org.nutz.json.Json;
|
||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -43,6 +54,9 @@ import java.util.stream.Collectors;
|
|||||||
@IocBean(args = {"refer:dao"})
|
@IocBean(args = {"refer:dao"})
|
||||||
public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrainSignUpActivity> implements BrandTrainSignUpActivityService {
|
public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrainSignUpActivity> implements BrandTrainSignUpActivityService {
|
||||||
|
|
||||||
|
// 历史课程未配置半径时沿用250米;扫码与GPS均以当前课程保存的半径为准。
|
||||||
|
private static final int DEFAULT_SIGN_RADIUS = 250;
|
||||||
|
|
||||||
public BrandTrainSignUpActivityServiceImpl(Dao dao) {
|
public BrandTrainSignUpActivityServiceImpl(Dao dao) {
|
||||||
super(dao);
|
super(dao);
|
||||||
}
|
}
|
||||||
@@ -50,9 +64,13 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrai
|
|||||||
@Inject
|
@Inject
|
||||||
private MatchConditionStructureService matchConditionStructureService;
|
private MatchConditionStructureService matchConditionStructureService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private WelfareListService welfareListService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void add(BrandTrainSignUpActivity activity, BrandTrainSignUpCourse course) {
|
public void add(BrandTrainSignUpActivity activity, BrandTrainSignUpCourse course) {
|
||||||
|
validateSignExtension(activity);
|
||||||
if (checkCnd(activity.getConditionStructure())) {
|
if (checkCnd(activity.getConditionStructure())) {
|
||||||
activity.setConditionStructureId(matchConditionStructureService.save(activity.getConditionStructure()).getId());
|
activity.setConditionStructureId(matchConditionStructureService.save(activity.getConditionStructure()).getId());
|
||||||
} else {
|
} else {
|
||||||
@@ -108,6 +126,7 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrai
|
|||||||
@Override
|
@Override
|
||||||
@Aop(TransAop.READ_COMMITTED)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
public void edit(BrandTrainSignUpActivity activity) {
|
public void edit(BrandTrainSignUpActivity activity) {
|
||||||
|
validateSignExtension(activity);
|
||||||
if (checkCnd(activity.getConditionStructure())) {
|
if (checkCnd(activity.getConditionStructure())) {
|
||||||
activity.setConditionStructureId(matchConditionStructureService.save(activity.getConditionStructure()).getId());
|
activity.setConditionStructureId(matchConditionStructureService.save(activity.getConditionStructure()).getId());
|
||||||
} else {
|
} else {
|
||||||
@@ -519,7 +538,24 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrai
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void doQd(String id) {
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void doQd(String id, String courseId, String userId, Double longitude, Double latitude, int signType) {
|
||||||
|
// 从签到记录和登录态确认课程及用户,禁止借用其他课程的大半径或替他人签到。
|
||||||
|
BrandTrainSignUpUserCourse record = StrUtil.isBlank(id) ? null : dao().fetch(BrandTrainSignUpUserCourse.class, id);
|
||||||
|
if (record == null || StrUtil.isBlank(userId) || !Objects.equals(userId, ShiroUtil.getPrincipalProperty("id"))
|
||||||
|
|| !Objects.equals(record.getUserId(), userId) || !Objects.equals(record.getCourseId(), courseId)) {
|
||||||
|
throw new IllegalArgumentException("当前签到记录无效或不属于当前用户");
|
||||||
|
}
|
||||||
|
BrandTrainSignUpCourse course = dao().fetch(BrandTrainSignUpCourse.class, record.getCourseId());
|
||||||
|
if (course == null || !course.isMobileSign() || (signType != 1 && signType != 3)
|
||||||
|
|| !Integer.valueOf(signType).equals(course.getSignType())) {
|
||||||
|
throw new IllegalArgumentException("当前课程未开启此签到方式");
|
||||||
|
}
|
||||||
|
requireScanCodeLocation(course.getCourseLocationCoordinates(), longitude, latitude, getSignRadius(course));
|
||||||
|
// 重复提交不覆盖首次签到时间;范围校验通过后才允许写入签到结果。
|
||||||
|
if (record.isAttend()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
NutMap updateMap = NutMap.NEW();
|
NutMap updateMap = NutMap.NEW();
|
||||||
updateMap.put("isAttend", true);
|
updateMap.put("isAttend", true);
|
||||||
updateMap.put("attendTime", new Date());
|
updateMap.put("attendTime", new Date());
|
||||||
@@ -532,6 +568,7 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrai
|
|||||||
SELECT
|
SELECT
|
||||||
c.*,
|
c.*,
|
||||||
t.courseLocationCoordinates,
|
t.courseLocationCoordinates,
|
||||||
|
t.signRadius,
|
||||||
t.isMobileSign,
|
t.isMobileSign,
|
||||||
t.signType,
|
t.signType,
|
||||||
t.isReceiveGift,
|
t.isReceiveGift,
|
||||||
@@ -649,6 +686,537 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrai
|
|||||||
return resultMap;
|
return resultMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅显式填写正数点位数的活动开启签到扩展,历史未配置活动仍沿用原报名和签到。
|
||||||
|
* 校验在写入活动及课程之前执行,避免只靠前端控制而保存无效的达标配置。
|
||||||
|
*/
|
||||||
|
private void validateSignExtension(BrandTrainSignUpActivity activity) {
|
||||||
|
List<BrandTrainSignUpCourse> courses = activity.getCourseList();
|
||||||
|
// 扫码课程均须保存有效地点;校验不能放在扩展配置之后,避免未开启扩展时绕过坐标要求。
|
||||||
|
if (courses != null) {
|
||||||
|
for (BrandTrainSignUpCourse course : courses) {
|
||||||
|
// 旧客户端和历史课程缺少半径时补默认值;显式非法值不得静默放行。
|
||||||
|
if (course.getSignRadius() == null) {
|
||||||
|
course.setSignRadius(DEFAULT_SIGN_RADIUS);
|
||||||
|
} else if (course.getSignRadius() <= 0) {
|
||||||
|
throw new IllegalArgumentException("课程“" + course.getCourseName() + "”的点位半径必须为正整数(米)");
|
||||||
|
}
|
||||||
|
if (course.isMobileSign() && Integer.valueOf(1).equals(course.getSignType())
|
||||||
|
&& !validScanCodeCoordinates(course.getCourseLocationCoordinates())) {
|
||||||
|
throw new IllegalArgumentException("请为课程“" + course.getCourseName() + "”设置有效的签到地点坐标");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Integer pointCount = activity.getSignPointCount();
|
||||||
|
Integer handleType = activity.getSignHandleType();
|
||||||
|
if (pointCount == null || pointCount == 0) {
|
||||||
|
if ((handleType != null && handleType != 3) || StrUtil.isNotBlank(activity.getWelfareProjectId())) {
|
||||||
|
throw new IllegalArgumentException("请先配置签到达标点位数");
|
||||||
|
}
|
||||||
|
activity.setSignHandleType(3);
|
||||||
|
activity.setWelfareProjectId(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (pointCount < 0 || courses == null || courses.isEmpty() || pointCount > courses.size()) {
|
||||||
|
throw new IllegalArgumentException("签到点位数必须为不超过课程数量的正整数");
|
||||||
|
}
|
||||||
|
if (courses.stream().anyMatch(item -> !item.isMobileSign())) {
|
||||||
|
throw new IllegalArgumentException("全部课程开启手机签到后才能配置签到扩展");
|
||||||
|
}
|
||||||
|
if (handleType == null || handleType < 1 || handleType > 3) {
|
||||||
|
throw new IllegalArgumentException("请选择正确的签到达标处理方式");
|
||||||
|
}
|
||||||
|
if (handleType == 1 && (courses.stream().anyMatch(item -> StrUtil.isBlank(item.getCourseType()))
|
||||||
|
|| courses.stream().map(BrandTrainSignUpCourse::getCourseType).distinct().count() != 1)) {
|
||||||
|
throw new IllegalArgumentException("填写信息方式要求全部课程使用同一种课程类型");
|
||||||
|
}
|
||||||
|
if (handleType == 2) {
|
||||||
|
WelfareProject project = StrUtil.isBlank(activity.getWelfareProjectId()) ? null
|
||||||
|
: dao().fetch(WelfareProject.class, activity.getWelfareProjectId());
|
||||||
|
if (project == null || project.getChoiceTimeEnd() == null || project.getChoiceTimeEnd().before(new Date())) {
|
||||||
|
throw new IllegalArgumentException("请选择选择时间未结束的福利项目");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activity.setWelfareProjectId(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 进入扫码页只读取已有进度,历史尚未设置坐标的课程仍可预览,由页面提示补齐配置。 */
|
||||||
|
@Override
|
||||||
|
public NutMap getScanCodeSignData(String courseId, String userId) {
|
||||||
|
if (StrUtil.isBlank(userId) || !Objects.equals(userId, ShiroUtil.getPrincipalProperty("id"))) {
|
||||||
|
throw new IllegalArgumentException("未获取到当前登录用户");
|
||||||
|
}
|
||||||
|
BrandTrainSignUpCourse course = requireScanCodeCourse(courseId, userId);
|
||||||
|
BrandTrainSignUpActivity activity = dao().fetch(BrandTrainSignUpActivity.class, course.getActivityId());
|
||||||
|
return buildScanCodeData(activity, course, userId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public NutMap handleScanCodeSign(String courseId, String userId, Double longitude, Double latitude) {
|
||||||
|
lockScanCodeUser(userId);
|
||||||
|
BrandTrainSignUpCourse course = requireScanCodeCourse(courseId, userId);
|
||||||
|
// 腾讯地图余额耗尽期间,仅当前扫码入口临时停用定位和范围校验;恢复时需同步恢复前端定位及controller经纬度校验。
|
||||||
|
// requireScanCodeLocation(course.getCourseLocationCoordinates(), longitude, latitude, getSignRadius(course));
|
||||||
|
BrandTrainSignUpActivity activity = dao().fetch(BrandTrainSignUpActivity.class, course.getActivityId());
|
||||||
|
ensureScanCodeSignUpUser(activity, course, userId);
|
||||||
|
List<BrandTrainSignUpUserCourse> records = dao().query(BrandTrainSignUpUserCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("courseId", "=", courseId)
|
||||||
|
.and("userId", "=", userId).asc("courseStartTime").asc("id"));
|
||||||
|
|
||||||
|
// 达标按不同课程点位累计,同一课程的多节课或重复扫描不重复增加点位数。
|
||||||
|
if (records.stream().anyMatch(BrandTrainSignUpUserCourse::isAttend)) {
|
||||||
|
return scanCodeResponse(99, "此点位,您已经签到", buildScanCodeData(activity, course, userId, true));
|
||||||
|
}
|
||||||
|
List<BrandTrainSignUpActivityCourse> lessons = dao().query(BrandTrainSignUpActivityCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("courseId", "=", courseId)
|
||||||
|
.asc("courseStartTime").asc("id"));
|
||||||
|
Date now = new Date();
|
||||||
|
// 单课次点位沿用源功能的活动时间范围;多课次必须匹配当前课次,不能任取一条签到。
|
||||||
|
List<BrandTrainSignUpActivityCourse> currentLessons = lessons.stream()
|
||||||
|
.filter(item -> item.getCourseStartTime() != null && item.getCourseEndTime() != null
|
||||||
|
&& !now.before(item.getCourseStartTime()) && !now.after(item.getCourseEndTime()))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
if (lessons.isEmpty() || lessons.size() > 1 && currentLessons.size() != 1) {
|
||||||
|
throw new IllegalArgumentException("当前点位没有唯一可签到的课次时间配置");
|
||||||
|
}
|
||||||
|
BrandTrainSignUpActivityCourse lesson = lessons.size() == 1 ? lessons.get(0) : currentLessons.get(0);
|
||||||
|
BrandTrainSignUpUserCourse userCourse = records.stream()
|
||||||
|
.filter(item -> Objects.equals(item.getActivityCourseId(), lesson.getId())).findFirst().orElse(null);
|
||||||
|
if (userCourse == null) {
|
||||||
|
userCourse = new BrandTrainSignUpUserCourse();
|
||||||
|
userCourse.setActivityId(activity.getId());
|
||||||
|
userCourse.setCourseId(courseId);
|
||||||
|
userCourse.setUserId(userId);
|
||||||
|
userCourse.setActivityCourseId(lesson.getId());
|
||||||
|
userCourse.setCourseStartTime(lesson.getCourseStartTime());
|
||||||
|
userCourse.setCourseEndTime(lesson.getCourseEndTime());
|
||||||
|
dao().insert(userCourse);
|
||||||
|
}
|
||||||
|
userCourse.setAttend(true);
|
||||||
|
userCourse.setAttendTime(now);
|
||||||
|
dao().update(userCourse);
|
||||||
|
if (Integer.valueOf(1).equals(activity.getSignHandleType())) {
|
||||||
|
syncAttendMobileColumnsValue(activity.getId(), userId, getLatestScanCodeForm(activity.getId(), userId));
|
||||||
|
}
|
||||||
|
return scanCodeResponse(0, "签到成功", buildScanCodeData(activity, course, userId, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 坐标统一为[经度,纬度]两个有限数,排除缺失、越界及异常数字,避免距离比较被NaN绕过。 */
|
||||||
|
private boolean validScanCodeCoordinates(List<Double> coordinates) {
|
||||||
|
return coordinates != null && coordinates.size() == 2
|
||||||
|
&& validScanCodePosition(coordinates.get(0), coordinates.get(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 腾讯地图GCJ-02坐标按经度[-180,180]、纬度[-90,90]校验,零值本身属于有效坐标。 */
|
||||||
|
private boolean validScanCodePosition(Double longitude, Double latitude) {
|
||||||
|
return longitude != null && latitude != null && Double.isFinite(longitude) && Double.isFinite(latitude)
|
||||||
|
&& longitude >= -180 && longitude <= 180 && latitude >= -90 && latitude <= 90;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** course为数据库课程;返回生效半径(米),仅缺失配置使用历史默认值,非法配置禁止签到。 */
|
||||||
|
private int getSignRadius(BrandTrainSignUpCourse course) {
|
||||||
|
Integer radius = course.getSignRadius();
|
||||||
|
if (radius == null) {
|
||||||
|
return DEFAULT_SIGN_RADIUS;
|
||||||
|
}
|
||||||
|
if (radius <= 0) {
|
||||||
|
throw new IllegalArgumentException("当前点位半径配置无效,请联系管理员");
|
||||||
|
}
|
||||||
|
return radius;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 服务端按同坐标系经纬度计算球面距离;signRadius为数据库半径(米),边界以内允许签到。 */
|
||||||
|
private void requireScanCodeLocation(List<Double> coordinates, Double longitude, Double latitude, int signRadius) {
|
||||||
|
if (!validScanCodeCoordinates(coordinates)) {
|
||||||
|
throw new IllegalArgumentException("当前点位未配置有效的签到地点坐标,请联系管理员");
|
||||||
|
}
|
||||||
|
if (!validScanCodePosition(longitude, latitude)) {
|
||||||
|
throw new IllegalArgumentException("当前定位坐标无效,请重新获取定位后再签到");
|
||||||
|
}
|
||||||
|
double currentLatitude = Math.toRadians(latitude);
|
||||||
|
double targetLatitude = Math.toRadians(coordinates.get(1));
|
||||||
|
double latitudeDifference = targetLatitude - currentLatitude;
|
||||||
|
double longitudeDifference = Math.toRadians(coordinates.get(0) - longitude);
|
||||||
|
double haversine = Math.pow(Math.sin(latitudeDifference / 2), 2)
|
||||||
|
+ Math.cos(currentLatitude) * Math.cos(targetLatitude) * Math.pow(Math.sin(longitudeDifference / 2), 2);
|
||||||
|
// 浮点计算误差可能使结果略超[0,1],夹紧后再求距离,保证远距离点也得到有效校验结果。
|
||||||
|
double distance = 6371000D * 2 * Math.asin(Math.sqrt(Math.max(0, Math.min(1, haversine))));
|
||||||
|
if (distance > signRadius) {
|
||||||
|
throw new IllegalArgumentException("请在签到点位" + signRadius + "米内签到");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
public void saveScanCodeDynamicForm(String courseId, String userId, String mobileColumnsValue) {
|
||||||
|
lockScanCodeUser(userId);
|
||||||
|
BrandTrainSignUpCourse course = requireScanCodeCourse(courseId, userId);
|
||||||
|
BrandTrainSignUpActivity activity = dao().fetch(BrandTrainSignUpActivity.class, course.getActivityId());
|
||||||
|
if (!Integer.valueOf(1).equals(activity.getSignHandleType())) {
|
||||||
|
throw new IllegalArgumentException("当前活动无需填写动态表单");
|
||||||
|
}
|
||||||
|
if (!buildScanCodeData(activity, course, userId, false).getBoolean("isReachTarget", false)) {
|
||||||
|
throw new IllegalArgumentException("当前活动尚未达标,暂不能填写动态表单");
|
||||||
|
}
|
||||||
|
List<NutMap> formData;
|
||||||
|
try {
|
||||||
|
formData = Json.fromJsonAsList(NutMap.class, mobileColumnsValue);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("填写信息格式不正确");
|
||||||
|
}
|
||||||
|
List<BrandTrainMobileSignColumn> columns = getScanCodeColumns(course);
|
||||||
|
if (formData == null || columns.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("请填写当前活动配置的信息");
|
||||||
|
}
|
||||||
|
Map<String, NutMap> inputMap = new LinkedHashMap<>();
|
||||||
|
for (NutMap item : formData) {
|
||||||
|
if (item == null || StrUtil.isBlank(item.getString("columnCode"))
|
||||||
|
|| inputMap.put(item.getString("columnCode"), item) != null) {
|
||||||
|
throw new IllegalArgumentException("填写信息包含无效或重复的字段");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> columnCodes = columns.stream().map(BrandTrainMobileSignColumn::getColumnCode).collect(Collectors.toList());
|
||||||
|
if (!columnCodes.containsAll(inputMap.keySet())) {
|
||||||
|
throw new IllegalArgumentException("填写信息包含未配置的字段");
|
||||||
|
}
|
||||||
|
List<NutMap> normalizedData = new ArrayList<>();
|
||||||
|
for (BrandTrainMobileSignColumn column : columns) {
|
||||||
|
Object value = inputMap.containsKey(column.getColumnCode()) ? inputMap.get(column.getColumnCode()).get("columnValue") : null;
|
||||||
|
value = validateScanCodeColumnValue(column, value);
|
||||||
|
normalizedData.add(NutMap.NEW().addv("columnCode", column.getColumnCode())
|
||||||
|
.addv("columnName", column.getColumnName()).addv("columnFormType", column.getColumnFormType())
|
||||||
|
.addv("columnValue", value));
|
||||||
|
}
|
||||||
|
syncAttendMobileColumnsValue(activity.getId(), userId, normalizedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 新入口在事务内锁定登录用户行,锁持有到事务提交,避免同用户并发扫码先查后插产生重复记录。
|
||||||
|
* 不改变普通报名和普通签到路径,也不增加表、索引或数据库脚本。
|
||||||
|
*/
|
||||||
|
private void lockScanCodeUser(String userId) {
|
||||||
|
if (StrUtil.isBlank(userId) || !Objects.equals(userId, ShiroUtil.getPrincipalProperty("id"))) {
|
||||||
|
throw new IllegalArgumentException("未获取到当前登录用户");
|
||||||
|
}
|
||||||
|
NutMap user = fetch(Sqls.create("SELECT id FROM sys_user WHERE id = @userId FOR UPDATE").setParam("userId", userId));
|
||||||
|
if (user == null || user.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("未找到当前用户信息");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 校验新点位入口的启用状态、活动时间及当前项目原有人员范围、条件和课程开放规则。 */
|
||||||
|
private BrandTrainSignUpCourse requireScanCodeCourse(String courseId, String userId) {
|
||||||
|
BrandTrainSignUpCourse course = StrUtil.isBlank(courseId) ? null : dao().fetch(BrandTrainSignUpCourse.class, courseId);
|
||||||
|
if (course == null || !course.isMobileSign() || !Integer.valueOf(1).equals(course.getSignType())) {
|
||||||
|
throw new IllegalArgumentException("当前课程未开启扫码签到");
|
||||||
|
}
|
||||||
|
BrandTrainSignUpActivity activity = dao().fetch(BrandTrainSignUpActivity.class, course.getActivityId());
|
||||||
|
if (activity == null || activity.getSignPointCount() == null || activity.getSignPointCount() <= 0) {
|
||||||
|
throw new IllegalArgumentException("当前活动未开启签到扩展,请从原签到入口操作");
|
||||||
|
}
|
||||||
|
Date now = new Date();
|
||||||
|
if (activity.isDisabled() || activity.getActivityStartTime() == null || activity.getActivityEndTime() == null
|
||||||
|
|| now.before(activity.getActivityStartTime()) || now.after(activity.getActivityEndTime())) {
|
||||||
|
throw new IllegalArgumentException("当前活动未开放或不在活动时间范围内");
|
||||||
|
}
|
||||||
|
// 已有报名沿用原页面仅正常状态1/3可签到的限制,候补、废弃等状态不能经新入口达标或填表。
|
||||||
|
// 没有报名记录时继续允许源点位扫码自动补报名,不将该规则扩展到原有报名状态。
|
||||||
|
List<BrandTrainSignUpUser> signUpUsers = dao().query(BrandTrainSignUpUser.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("courseId", "=", courseId).and("userId", "=", userId));
|
||||||
|
if (signUpUsers.stream().anyMatch(item -> !Integer.valueOf(1).equals(item.getState())
|
||||||
|
&& !Integer.valueOf(3).equals(item.getState()))) {
|
||||||
|
throw new IllegalArgumentException("当前报名状态不能签到或填写信息");
|
||||||
|
}
|
||||||
|
if (activity.getActivityGroupId() != null && activity.getActivityGroupId() > 0
|
||||||
|
&& dao().count(ActivityUserScope.class, Cnd.where("groupId", "=", activity.getActivityGroupId())
|
||||||
|
.and("userId", "=", userId)) == 0) {
|
||||||
|
throw new IllegalArgumentException("您没有权限参与此次活动");
|
||||||
|
}
|
||||||
|
if ((StrUtil.isNotBlank(activity.getConditionStructureId())
|
||||||
|
&& !validConditionStructure(activity.getConditionStructureId()).getBoolean("flag", false))
|
||||||
|
|| (StrUtil.isNotBlank(course.getConditionStructureId())
|
||||||
|
&& !validConditionStructure(course.getConditionStructureId()).getBoolean("flag", false))) {
|
||||||
|
throw new IllegalArgumentException("您不满足当前活动或课程的参与条件");
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotBlank(course.getInterTime()) && Boolean.FALSE.equals(course.getOpenOtherUnion())
|
||||||
|
&& !Objects.equals(Vi.getUnionId(), course.getHostUnionId())
|
||||||
|
&& now.before(cn.hutool.core.date.DateUtil.parse(course.getInterTime()))) {
|
||||||
|
throw new IllegalArgumentException("当前课程暂未开放给其他工会");
|
||||||
|
}
|
||||||
|
List<BrandTrainSignUpCourse> courses = dao().query(BrandTrainSignUpCourse.class, Cnd.where("activityId", "=", activity.getId()));
|
||||||
|
if (courses.stream().anyMatch(item -> !item.isMobileSign()) || activity.getSignPointCount() > courses.size()) {
|
||||||
|
throw new IllegalArgumentException("当前活动签到扩展配置无效,请联系管理员");
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(1).equals(activity.getSignHandleType())
|
||||||
|
&& (courses.stream().anyMatch(item -> StrUtil.isBlank(item.getCourseType()))
|
||||||
|
|| courses.stream().map(BrandTrainSignUpCourse::getCourseType).distinct().count() != 1)) {
|
||||||
|
throw new IllegalArgumentException("填写信息方式要求全部课程使用同一种课程类型");
|
||||||
|
}
|
||||||
|
return course;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 未报名的扫码用户须满足品牌原有总名额、分工会名额和类型/活动报名数量限制,候补不能直接签到。
|
||||||
|
* 新入口锁定当前课程行后复用品牌报名方法,沿用品牌同步生成全部课次记录的原有行为。
|
||||||
|
* @param activity 当前已校验开放状态的品牌活动
|
||||||
|
* @param course 当前扫码品牌课程,不计入尚未填写的家属人数
|
||||||
|
* @param userId 当前登录用户ID
|
||||||
|
*/
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
private void ensureScanCodeSignUpUser(BrandTrainSignUpActivity activity, BrandTrainSignUpCourse course, String userId) {
|
||||||
|
if (dao().count(BrandTrainSignUpUser.class, Cnd.where("activityId", "=", activity.getId())
|
||||||
|
.and("courseId", "=", course.getId()).and("userId", "=", userId)) > 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 同课程不同用户的新扫码补报名串行校验名额,避免两个新入口同时占用最后一个正常名额。
|
||||||
|
fetch(Sqls.create("SELECT id FROM brand_train_sign_up_course WHERE id = @courseId FOR UPDATE")
|
||||||
|
.setParam("courseId", course.getId()));
|
||||||
|
if (isSignFull(course.getId(), 0)) {
|
||||||
|
throw new IllegalArgumentException("当前报名人数已满");
|
||||||
|
}
|
||||||
|
if (isSignFullByUnionId(course.getId(), 0)) {
|
||||||
|
throw new IllegalArgumentException("该活动您所在的分工会名额已报满");
|
||||||
|
}
|
||||||
|
if (!isSignCourse(course.getId(), activity.getId())) {
|
||||||
|
throw new IllegalArgumentException("您已达到当前品牌活动或课程类型的报名数量上限");
|
||||||
|
}
|
||||||
|
if (Integer.valueOf(2).equals(course.getReserveMode())
|
||||||
|
&& dao().count(BrandTrainSignUpUser.class, Cnd.where("courseId", "=", course.getId()).and("state", "!=", 2))
|
||||||
|
>= course.getCoursePeopleNumber()) {
|
||||||
|
throw new IllegalArgumentException("当前仅剩候补名额,请先完成正常报名后再签到");
|
||||||
|
}
|
||||||
|
BrandTrainSignUpUser signUpUser = new BrandTrainSignUpUser();
|
||||||
|
signUpUser.setActivityId(activity.getId());
|
||||||
|
signUpUser.setCourseId(course.getId());
|
||||||
|
try {
|
||||||
|
doSignUp(signUpUser);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalStateException("补齐品牌活动报名记录失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 组装地图、当前点位状态、活动进度和表单回显;当前点位已签与整个活动达标分别计算。
|
||||||
|
* 页面预览和表单保存均使用allowHandle=false,只能回显已有福利名单,不新增任何业务记录。
|
||||||
|
*/
|
||||||
|
private NutMap buildScanCodeData(BrandTrainSignUpActivity activity, BrandTrainSignUpCourse course, String userId, boolean allowHandle) {
|
||||||
|
List<BrandTrainSignUpUserCourse> attendRecords = dao().query(BrandTrainSignUpUserCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activity.getId()).and("userId", "=", userId).and("isAttend", "=", true));
|
||||||
|
long signCount = attendRecords.stream().map(BrandTrainSignUpUserCourse::getCourseId).filter(StrUtil::isNotBlank).distinct().count();
|
||||||
|
boolean reached = signCount >= activity.getSignPointCount();
|
||||||
|
Integer handleType = activity.getSignHandleType() == null ? 3 : activity.getSignHandleType();
|
||||||
|
Map<String, Object> values = new LinkedHashMap<>();
|
||||||
|
getLatestScanCodeForm(activity.getId(), userId).forEach(item -> values.put(item.getString("columnCode"), item.get("columnValue")));
|
||||||
|
List<NutMap> dynamicColumns = new ArrayList<>();
|
||||||
|
for (BrandTrainMobileSignColumn column : getScanCodeColumns(course)) {
|
||||||
|
NutMap columnMap = Lang.obj2nutmap(column);
|
||||||
|
columnMap.put("columnValue", values.containsKey(column.getColumnCode()) ? values.get(column.getColumnCode())
|
||||||
|
: "FILE".equals(column.getColumnFormType()) ? new ArrayList<>() : "");
|
||||||
|
dynamicColumns.add(columnMap);
|
||||||
|
}
|
||||||
|
List<NutMap> signedPoints = buildScanCodeSignedPoints(activity.getId(), attendRecords);
|
||||||
|
NutMap data = NutMap.NEW().addv("activityName", activity.getActivityName()).addv("courseName", course.getCourseName())
|
||||||
|
.addv("courseLocation", course.getCourseLocation()).addv("courseLocationCoordinates", course.getCourseLocationCoordinates())
|
||||||
|
.addv("currentPointSigned", attendRecords.stream().anyMatch(item -> Objects.equals(item.getCourseId(), course.getId())))
|
||||||
|
.addv("signRadius", getSignRadius(course))
|
||||||
|
.addv("signCount", signCount).addv("signPointCount", activity.getSignPointCount())
|
||||||
|
.addv("signedPoints", signedPoints)
|
||||||
|
.addv("signPoints", buildScanCodePoints(activity.getId(), signedPoints))
|
||||||
|
.addv("signHandleType", handleType).addv("welfareProjectId", activity.getWelfareProjectId())
|
||||||
|
.addv("isReachTarget", reached).addv("dynamicColumnsData", dynamicColumns)
|
||||||
|
.addv("needDynamicForm", reached && handleType == 1 && !dynamicColumns.isEmpty())
|
||||||
|
.addv("needWelfareSelect", false).addv("welfareJumpUrl", "")
|
||||||
|
.addv("handleStatus", "NONE").addv("handleMsg", reached ? "当前活动已达标" : "请继续完成其他点位签到");
|
||||||
|
if (reached && handleType == 1 && !dynamicColumns.isEmpty()) {
|
||||||
|
// 达标处理覆盖默认状态和提示;addv 会将同名字段累加为数组,导致手机端直接显示数组内容。
|
||||||
|
data.setv("handleStatus", "PENDING_FORM").setv("handleMsg", "请点击下方按钮填写详细信息。");
|
||||||
|
} else if (reached && handleType == 2) {
|
||||||
|
if (allowHandle) {
|
||||||
|
completeScanCodeWelfare(activity.getWelfareProjectId(), userId, data);
|
||||||
|
} else if (dao().count(WelfareList.class, Cnd.where("projectId", "=", activity.getWelfareProjectId())
|
||||||
|
.and("userId", "=", userId)) > 0) {
|
||||||
|
// 已达标用户刷新后保留已有福利入口,只读查询不能为尚未入名单的用户补建福利记录。
|
||||||
|
buildScanCodeWelfareData(activity.getWelfareProjectId(), data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* activityId 为已校验的当前活动,attendRecords 仅含当前登录用户在本活动的已签到课次。
|
||||||
|
* 返回点位列表,每项为 courseId、courseName、courseLocation、attendTime(首次签到时间);按时间倒序。
|
||||||
|
* 同一课程的多个课次只展示一次,与 signCount 计数口径一致;历史课程被删除时保留记录并提示名称。
|
||||||
|
*/
|
||||||
|
private List<NutMap> buildScanCodeSignedPoints(String activityId, List<BrandTrainSignUpUserCourse> attendRecords) {
|
||||||
|
Map<String, BrandTrainSignUpUserCourse> firstAttends = new LinkedHashMap<>();
|
||||||
|
attendRecords.stream().filter(item -> StrUtil.isNotBlank(item.getCourseId()))
|
||||||
|
.sorted(Comparator.comparing(BrandTrainSignUpUserCourse::getAttendTime,
|
||||||
|
Comparator.nullsLast(Comparator.naturalOrder()))
|
||||||
|
.thenComparing(BrandTrainSignUpUserCourse::getId, Comparator.nullsLast(Comparator.naturalOrder())))
|
||||||
|
.forEach(item -> firstAttends.putIfAbsent(item.getCourseId(), item));
|
||||||
|
if (firstAttends.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
// 批量读取点位名称和地点,避免每条记录单独查询;不返回人员、附件或其他报名数据。
|
||||||
|
Map<String, BrandTrainSignUpCourse> courses = dao().query(BrandTrainSignUpCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).and("id", "in", new ArrayList<>(firstAttends.keySet())))
|
||||||
|
.stream().collect(Collectors.toMap(BrandTrainSignUpCourse::getId, item -> item));
|
||||||
|
return firstAttends.values().stream()
|
||||||
|
.sorted(Comparator.comparing(BrandTrainSignUpUserCourse::getAttendTime,
|
||||||
|
Comparator.nullsLast(Comparator.reverseOrder())))
|
||||||
|
.map(item -> {
|
||||||
|
BrandTrainSignUpCourse signedCourse = courses.get(item.getCourseId());
|
||||||
|
return NutMap.NEW().addv("courseId", item.getCourseId())
|
||||||
|
.addv("courseName", signedCourse == null ? "点位已删除" : signedCourse.getCourseName())
|
||||||
|
.addv("courseLocation", signedCourse == null ? "" : signedCourse.getCourseLocation())
|
||||||
|
.addv("attendTime", item.getAttendTime() == null ? null : DateUtil.formatDateTime(item.getAttendTime()));
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* activityId 为当前已校验的活动ID;signedPoints 为当前登录用户按课程去重后的已打卡列表。
|
||||||
|
* 返回活动全部点位,每项包含 courseId、courseName、courseLocation、attendTime、signed(是否已打卡)。
|
||||||
|
* 未报名或仅报名的点位均为未打卡,attendTime 为空;课程按配置序号和ID排序,列表不触发任何签到写入。
|
||||||
|
*/
|
||||||
|
private List<NutMap> buildScanCodePoints(String activityId, List<NutMap> signedPoints) {
|
||||||
|
Map<String, NutMap> signedByCourse = new LinkedHashMap<>();
|
||||||
|
signedPoints.forEach(point -> signedByCourse.put(point.getString("courseId"), point));
|
||||||
|
Map<String, NutMap> points = new LinkedHashMap<>();
|
||||||
|
List<BrandTrainSignUpCourse> courses = dao().query(BrandTrainSignUpCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).asc("orderNum").asc("id"));
|
||||||
|
for (BrandTrainSignUpCourse pointCourse : courses) {
|
||||||
|
NutMap signedPoint = signedByCourse.get(pointCourse.getId());
|
||||||
|
points.put(pointCourse.getId(), NutMap.NEW().addv("courseId", pointCourse.getId())
|
||||||
|
.addv("courseName", pointCourse.getCourseName()).addv("courseLocation", pointCourse.getCourseLocation())
|
||||||
|
.addv("signed", signedPoint != null)
|
||||||
|
.addv("attendTime", signedPoint == null ? null : signedPoint.get("attendTime")));
|
||||||
|
}
|
||||||
|
// 已删除课程的历史签到仍计入原达标数;保留为已打卡项,避免弹框数量与主页面进度不一致。
|
||||||
|
for (NutMap signedPoint : signedPoints) {
|
||||||
|
if (!points.containsKey(signedPoint.getString("courseId"))) {
|
||||||
|
NutMap historicalPoint = NutMap.NEW();
|
||||||
|
historicalPoint.putAll(signedPoint);
|
||||||
|
historicalPoint.setv("signed", true);
|
||||||
|
points.put(signedPoint.getString("courseId"), historicalPoint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new ArrayList<>(points.values());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已达标用户复用本项目福利名单服务,并由现有福利选择页处理时间、选项和签收。 */
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
private void completeScanCodeWelfare(String projectId, String userId, NutMap data) {
|
||||||
|
buildScanCodeWelfareData(projectId, data);
|
||||||
|
if (!data.getBoolean("needWelfareSelect", false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dao().count(WelfareList.class, Cnd.where("projectId", "=", projectId).and("userId", "=", userId)) == 0) {
|
||||||
|
welfareListService.joinWelfareList(projectId, Collections.singletonList(userId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 福利入口组装仅校验项目并设置返回字段,供手动签到和已有名单的只读预览共同使用。 */
|
||||||
|
private void buildScanCodeWelfareData(String projectId, NutMap data) {
|
||||||
|
WelfareProject project = StrUtil.isBlank(projectId) ? null : dao().fetch(WelfareProject.class, projectId);
|
||||||
|
if (project == null || project.getChoiceTimeEnd() == null || project.getChoiceTimeEnd().before(new Date())) {
|
||||||
|
// 福利校验失败时覆盖默认结果,确保状态和提示仍为单个字符串。
|
||||||
|
data.setv("handleStatus", "FAILED").setv("handleMsg", "关联福利项目不存在或选择时间已结束,请联系管理员");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 覆盖默认福利结果,保持选择标记为布尔值、跳转地址为字符串,避免数组转换后生成错误地址。
|
||||||
|
data.setv("handleStatus", "PENDING_WELFARE").setv("handleMsg", "当前活动已达标,请前往选择福利")
|
||||||
|
.setv("needWelfareSelect", true).setv("welfareJumpUrl", "/mobile/welfare/list/receive?projectId=" + projectId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 动态字段沿用当前课程类型配置,排序与既有报名表单保持一致。 */
|
||||||
|
private List<BrandTrainMobileSignColumn> getScanCodeColumns(BrandTrainSignUpCourse course) {
|
||||||
|
if (StrUtil.isBlank(course.getCourseType())) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return dao().query(BrandTrainMobileSignColumn.class, Cnd.where("typeId", "=", course.getCourseType()).asc("columnIndex"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 新点位继续使用活动内最近一份已保存表单,按报名时间和ID排序保证回显结果稳定。 */
|
||||||
|
private List<NutMap> getLatestScanCodeForm(String activityId, String userId) {
|
||||||
|
List<BrandTrainSignUpUser> users = dao().query(BrandTrainSignUpUser.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).and("userId", "=", userId).desc("signUpTime").desc("id"));
|
||||||
|
return users.stream().map(BrandTrainSignUpUser::getMobileColumnsValue).filter(item -> item != null && !item.isEmpty())
|
||||||
|
.findFirst().orElseGet(ArrayList::new);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 在上层扫码或表单保存事务内同步已签到课程,未签到的普通报名记录保持原值。 */
|
||||||
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
|
private void syncAttendMobileColumnsValue(String activityId, String userId, List<NutMap> formData) {
|
||||||
|
if (formData == null || formData.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<BrandTrainSignUpUserCourse> attends = dao().query(BrandTrainSignUpUserCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).and("userId", "=", userId).and("isAttend", "=", true));
|
||||||
|
List<String> courseIds = attends.stream().map(BrandTrainSignUpUserCourse::getCourseId).distinct().collect(Collectors.toList());
|
||||||
|
if (courseIds.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<BrandTrainSignUpUser> users = dao().query(BrandTrainSignUpUser.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).and("userId", "=", userId).and("courseId", "in", courseIds));
|
||||||
|
users.forEach(item -> item.setMobileColumnsValue(Json.fromJsonAsList(NutMap.class, Json.toJson(formData))));
|
||||||
|
dao().update(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 字段定义以服务端配置为准,校验必填、整数/小数、日期、选项和附件数组,保留附件原有结构。 */
|
||||||
|
private Object validateScanCodeColumnValue(BrandTrainMobileSignColumn column, Object value) {
|
||||||
|
boolean empty = value == null || value instanceof String && StrUtil.isBlank((String) value)
|
||||||
|
|| value instanceof List && ((List<?>) value).isEmpty();
|
||||||
|
if (empty) {
|
||||||
|
if (Boolean.TRUE.equals(column.getIsRequired())) {
|
||||||
|
throw new IllegalArgumentException("请填写" + column.getColumnName());
|
||||||
|
}
|
||||||
|
return "FILE".equals(column.getColumnFormType()) ? new ArrayList<>() : "";
|
||||||
|
}
|
||||||
|
String text = String.valueOf(value);
|
||||||
|
String type = column.getColumnType();
|
||||||
|
try {
|
||||||
|
if (!"JSON".equals(type) && !"MYSQL_JSON".equals(type) && (value instanceof List || value instanceof Map)) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
if ("INT".equals(type) || "INT2".equals(type) || "INT4".equals(type) || "INT8".equals(type)) {
|
||||||
|
new BigDecimal(text).toBigIntegerExact();
|
||||||
|
} else if ("FLOAT".equals(type) || "DOUBLE".equals(type) || "DECIMAL".equals(type)) {
|
||||||
|
new BigDecimal(text);
|
||||||
|
} else if ("DATE".equals(type)) {
|
||||||
|
java.time.LocalDate.parse(text);
|
||||||
|
} else if ("DATETIME".equals(type) || "TIMESTAMP".equals(type)) {
|
||||||
|
java.time.LocalDateTime.parse(text, java.time.format.DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss")
|
||||||
|
.withResolverStyle(java.time.format.ResolverStyle.STRICT));
|
||||||
|
} else if ("BOOLEAN".equals(type) && !"true".equalsIgnoreCase(text) && !"false".equalsIgnoreCase(text)) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
} else if ("JSON".equals(type) || "MYSQL_JSON".equals(type)) {
|
||||||
|
value = value instanceof String ? Json.fromJson(text) : value;
|
||||||
|
if (!(value instanceof List) && !(value instanceof Map)) {
|
||||||
|
throw new IllegalArgumentException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException(column.getColumnName() + "格式不正确");
|
||||||
|
}
|
||||||
|
if ("SELECT".equals(column.getColumnFormType()) && !"DATE".equals(type) && !"DATETIME".equals(type)
|
||||||
|
&& (column.getSelectValues() == null || !column.getSelectValues().contains(text))) {
|
||||||
|
throw new IllegalArgumentException("请选择有效的" + column.getColumnName());
|
||||||
|
}
|
||||||
|
if ("FILE".equals(column.getColumnFormType()) && (!(value instanceof List)
|
||||||
|
|| column.getFileNumber() != null && ((List<?>) value).size() > column.getFileNumber()
|
||||||
|
|| Boolean.TRUE.equals(column.getIsRequired()) && ((List<?>) value).isEmpty())) {
|
||||||
|
throw new IllegalArgumentException(column.getColumnName() + "附件数量不正确");
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 扫码接口保持code/msg/data结构,重复签到也返回进度供手机端继续办理达标事项。 */
|
||||||
|
private NutMap scanCodeResponse(int code, String msg, NutMap data) {
|
||||||
|
return NutMap.NEW().addv("code", code).addv("msg", msg).addv("data", data);
|
||||||
|
}
|
||||||
|
|
||||||
private Boolean checkCnd(MatchConditionStructure cnd) {
|
private Boolean checkCnd(MatchConditionStructure cnd) {
|
||||||
return cnd != null && cnd.getConditions() != null && cnd.getMethod() != null && cnd.getConditions().stream().allMatch(v -> v.getField() != null && v.getOperational() != null && v.getValue() != null);
|
return cnd != null && cnd.getConditions() != null && cnd.getMethod() != null && cnd.getConditions().stream().allMatch(v -> v.getField() != null && v.getOperational() != null && v.getValue() != null);
|
||||||
}
|
}
|
||||||
|
|||||||
+124
@@ -1,5 +1,20 @@
|
|||||||
package io.v.nutz.brandtrainactivity.service.impl;
|
package io.v.nutz.brandtrainactivity.service.impl;
|
||||||
|
|
||||||
|
import cn.afterturn.easypoi.excel.ExcelExportUtil;
|
||||||
|
import cn.afterturn.easypoi.excel.entity.ExportParams;
|
||||||
|
import cn.afterturn.easypoi.excel.entity.enmus.ExcelType;
|
||||||
|
import cn.afterturn.easypoi.excel.entity.params.ExcelExportEntity;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import io.v.nutz.brandtrainactivity.models.BrandTrainMobileSignColumn;
|
||||||
|
import io.v.nutz.brandtrainactivity.models.BrandTrainSignUpActivity;
|
||||||
|
import io.v.nutz.brandtrainactivity.models.BrandTrainSignUpCourse;
|
||||||
|
import io.v.nutz.brandtrainactivity.models.BrandTrainSignUpType;
|
||||||
|
import org.apache.poi.ss.usermodel.Workbook;
|
||||||
|
import org.nutz.dao.Cnd;
|
||||||
|
import org.nutz.json.Json;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
|
||||||
import cn.wizzer.framework.page.Pagination;
|
import cn.wizzer.framework.page.Pagination;
|
||||||
import io.v.nutz.base.utils.Vi;
|
import io.v.nutz.base.utils.Vi;
|
||||||
import io.v.nutz.dao.CndPlus;
|
import io.v.nutz.dao.CndPlus;
|
||||||
@@ -175,4 +190,113 @@ public class BrandTrainSignUpActivityStatisticsServiceImpl extends ViServiceImpl
|
|||||||
sql.setParam("activityId", activityId);
|
sql.setParam("activityId", activityId);
|
||||||
return listMap(sql);
|
return listMap(sql);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按活动汇总人员填写的信息;同一人员报名多个点位时只输出一行。
|
||||||
|
*
|
||||||
|
* @param activityId 需要导出的品牌活动ID,不可为空
|
||||||
|
* @return XSSF工作簿;固定列为人员信息和已签到课程,后续列为该活动课程类型的动态字段
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Workbook exportFillFormWorkbook(String activityId) {
|
||||||
|
if (StrUtil.isBlank(activityId)) {
|
||||||
|
throw new IllegalArgumentException("请选择需要导出的活动");
|
||||||
|
}
|
||||||
|
BrandTrainSignUpActivity activity = dao().fetch(BrandTrainSignUpActivity.class, activityId);
|
||||||
|
if (activity == null) {
|
||||||
|
throw new IllegalArgumentException("未找到当前活动");
|
||||||
|
}
|
||||||
|
List<BrandTrainSignUpCourse> courseList = dao().query(BrandTrainSignUpCourse.class,
|
||||||
|
Cnd.where("activityId", "=", activityId).asc("orderNum"));
|
||||||
|
List<String> courseTypes = courseList.stream().map(BrandTrainSignUpCourse::getCourseType)
|
||||||
|
.filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
|
||||||
|
// 多种课程类型可能复用字段编码但含义不同,不能按同一份动态表头合并导出。
|
||||||
|
if (courseTypes.size() > 1) {
|
||||||
|
throw new IllegalArgumentException("当前活动存在多个课程类型,不能导出填写信息");
|
||||||
|
}
|
||||||
|
List<BrandTrainMobileSignColumn> columns = new ArrayList<>();
|
||||||
|
if (!courseTypes.isEmpty()) {
|
||||||
|
BrandTrainSignUpType type = dao().fetch(BrandTrainSignUpType.class, courseTypes.get(0));
|
||||||
|
if (type != null) {
|
||||||
|
dao().fetchLinks(type, "trainMobileSignColumnList", Cnd.NEW().asc("columnIndex"));
|
||||||
|
if (type.getTrainMobileSignColumnList() != null) {
|
||||||
|
columns = type.getTrainMobileSignColumnList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Sql userSql = Sqls.create("""
|
||||||
|
SELECT ts.userId, u.username, u.loginname, u.unitname, u.unionname, u.sex,
|
||||||
|
ts.mobileColumnsValue, ts.signUpTime
|
||||||
|
FROM brand_train_sign_up_user ts
|
||||||
|
LEFT JOIN `user` u ON u.id = ts.userId
|
||||||
|
WHERE ts.activityId = @activityId
|
||||||
|
ORDER BY ts.signUpTime DESC, ts.id
|
||||||
|
""").setParam("activityId", activityId);
|
||||||
|
Map<String, List<NutMap>> users = listMap(userSql).stream()
|
||||||
|
.filter(item -> StrUtil.isNotBlank(item.getString("userId")))
|
||||||
|
.collect(Collectors.groupingBy(item -> item.getString("userId"),
|
||||||
|
LinkedHashMap::new, Collectors.toList()));
|
||||||
|
|
||||||
|
Sql signSql = Sqls.create("""
|
||||||
|
SELECT uc.userId,
|
||||||
|
GROUP_CONCAT(DISTINCT c.courseName ORDER BY c.orderNum SEPARATOR '、') AS signCourseNames
|
||||||
|
FROM brand_train_sign_up_user_course uc
|
||||||
|
LEFT JOIN brand_train_sign_up_course c ON c.id = uc.courseId
|
||||||
|
WHERE uc.activityId = @activityId AND uc.isAttend = 1
|
||||||
|
GROUP BY uc.userId
|
||||||
|
""").setParam("activityId", activityId);
|
||||||
|
Map<String, String> signCourseNames = listMap(signSql).stream()
|
||||||
|
.collect(Collectors.toMap(item -> item.getString("userId"),
|
||||||
|
item -> item.getString("signCourseNames", ""), (first, second) -> first));
|
||||||
|
|
||||||
|
List<NutMap> rows = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<NutMap>> entry : users.entrySet()) {
|
||||||
|
NutMap user = entry.getValue().get(0);
|
||||||
|
NutMap row = NutMap.NEW().addv("username", user.getString("username"))
|
||||||
|
.addv("loginname", user.getString("loginname"))
|
||||||
|
.addv("unitname", user.getString("unitname"))
|
||||||
|
.addv("unionname", user.getString("unionname"))
|
||||||
|
.addv("sex", user.getString("sex"))
|
||||||
|
.addv("signCourseNames", signCourseNames.getOrDefault(entry.getKey(), ""));
|
||||||
|
Map<String, Object> formValues = new HashMap<>();
|
||||||
|
// 签到表单会同步到各已签到点位;历史记录未同步时使用最近一份非空表单。
|
||||||
|
for (NutMap record : entry.getValue()) {
|
||||||
|
String json = record.getString("mobileColumnsValue");
|
||||||
|
if (StrUtil.isBlank(json)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<NutMap> values = Json.fromJsonAsList(NutMap.class, json);
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (NutMap value : values) {
|
||||||
|
formValues.put(value.getString("columnCode"), value.get("columnValue"));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (BrandTrainMobileSignColumn column : columns) {
|
||||||
|
Object value = formValues.get(column.getColumnCode());
|
||||||
|
// 动态字段单独命名,防止配置的字段编码覆盖姓名等固定导出列。
|
||||||
|
row.addv("form_" + column.getColumnCode(), value == null ? "" : String.valueOf(value));
|
||||||
|
}
|
||||||
|
rows.add(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ExcelExportEntity> entities = new ArrayList<>();
|
||||||
|
entities.add(new ExcelExportEntity("姓名", "username", 20));
|
||||||
|
entities.add(new ExcelExportEntity("工号", "loginname", 20));
|
||||||
|
entities.add(new ExcelExportEntity("单位", "unitname", 30));
|
||||||
|
entities.add(new ExcelExportEntity("分工会", "unionname", 30));
|
||||||
|
entities.add(new ExcelExportEntity("性别", "sex", 10));
|
||||||
|
entities.add(new ExcelExportEntity("已签到课程", "signCourseNames", 40));
|
||||||
|
for (BrandTrainMobileSignColumn column : columns) {
|
||||||
|
entities.add(new ExcelExportEntity(column.getColumnName(), "form_" + column.getColumnCode(), 25));
|
||||||
|
}
|
||||||
|
ExportParams exportParams = new ExportParams();
|
||||||
|
exportParams.setType(ExcelType.XSSF);
|
||||||
|
exportParams.setTitle(activity.getActivityName() + "填写信息");
|
||||||
|
exportParams.setSheetName("填写信息");
|
||||||
|
return ExcelExportUtil.exportExcel(exportParams, entities, rows);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+102
-14
@@ -4,6 +4,8 @@ import cn.hutool.core.util.StrUtil;
|
|||||||
import cn.wizzer.framework.base.Result;
|
import cn.wizzer.framework.base.Result;
|
||||||
import com.google.common.base.Joiner;
|
import com.google.common.base.Joiner;
|
||||||
import io.v.nutz.annontation.ViReturn;
|
import io.v.nutz.annontation.ViReturn;
|
||||||
|
import io.v.nutz.base.event.user.UserChangeMsg;
|
||||||
|
import io.v.nutz.base.event.user.UserChangePublisher;
|
||||||
import io.v.nutz.base.utils.RCSCloudAPI;
|
import io.v.nutz.base.utils.RCSCloudAPI;
|
||||||
import io.v.nutz.base.utils.Vi;
|
import io.v.nutz.base.utils.Vi;
|
||||||
import io.v.nutz.dao.CndPlus;
|
import io.v.nutz.dao.CndPlus;
|
||||||
@@ -27,6 +29,7 @@ import io.v.nutz.sys.models.Sys_unit;
|
|||||||
import io.v.nutz.sys.models.Sys_user;
|
import io.v.nutz.sys.models.Sys_user;
|
||||||
import io.v.nutz.sys.models.User;
|
import io.v.nutz.sys.models.User;
|
||||||
import io.v.nutz.sys.services.SysDictService;
|
import io.v.nutz.sys.services.SysDictService;
|
||||||
|
import io.v.nutz.sys.services.SysRoleService;
|
||||||
import io.v.nutz.sys.services.SysUnitService;
|
import io.v.nutz.sys.services.SysUnitService;
|
||||||
import io.v.nutz.sys.services.SysUserService;
|
import io.v.nutz.sys.services.SysUserService;
|
||||||
import io.v.nutz.util.ShiroUtil;
|
import io.v.nutz.util.ShiroUtil;
|
||||||
@@ -43,6 +46,7 @@ import org.nutz.ioc.aop.Aop;
|
|||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
|
import org.nutz.lang.random.R;
|
||||||
import org.nutz.mvc.annotation.At;
|
import org.nutz.mvc.annotation.At;
|
||||||
import org.nutz.mvc.annotation.Ok;
|
import org.nutz.mvc.annotation.Ok;
|
||||||
import org.nutz.mvc.annotation.Param;
|
import org.nutz.mvc.annotation.Param;
|
||||||
@@ -84,6 +88,9 @@ public class MemberChangeManageController {
|
|||||||
@Inject
|
@Inject
|
||||||
private SysUnitService sysUnitService;
|
private SysUnitService sysUnitService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private SysRoleService sysRoleService;
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@ViReturn
|
@ViReturn
|
||||||
@RequiresPermissions("member.change.mange")
|
@RequiresPermissions("member.change.mange")
|
||||||
@@ -126,6 +133,48 @@ public class MemberChangeManageController {
|
|||||||
.setParam("success", MemberChangeApplyState.SUCCESS)
|
.setParam("success", MemberChangeApplyState.SUCCESS)
|
||||||
.setVar("failStates", Joiner.on(",").join(List.of(MemberChangeApplyState.UNION_FAIL, MemberChangeApplyState.SCHOOL_UNION_FAIL)));
|
.setVar("failStates", Joiner.on(",").join(List.of(MemberChangeApplyState.UNION_FAIL, MemberChangeApplyState.SCHOOL_UNION_FAIL)));
|
||||||
|
|
||||||
|
sql.setCondition(memberSelectionCondition(unionId, unitId, threeUnitId, unionGroupId,
|
||||||
|
changeDateBefore, changeDateEnd, pageForm, campus, sex, personType, userState,
|
||||||
|
changeType, changed, isMember));
|
||||||
|
return memberService.list(pageForm, sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
@At
|
||||||
|
@ViReturn
|
||||||
|
@RequiresPermissions("member.change.mange")
|
||||||
|
public Object selectAllUsers(@Param(value = "unionId", required = false) String unionId,
|
||||||
|
@Param(value = "unitId", required = false) String unitId,
|
||||||
|
@Param(value = "threeUnitId", required = false) String threeUnitId,
|
||||||
|
@Param(value = "unionGroupId", required = false) String unionGroupId,
|
||||||
|
@Param(value = "changeDateBefore", required = false) String changeDateBefore,
|
||||||
|
@Param(value = "changeDateEnd", required = false) String changeDateEnd,
|
||||||
|
PageForm pageForm,
|
||||||
|
@Param(value = "campus", required = false) String campus,
|
||||||
|
@Param(value = "sex", required = false) String sex,
|
||||||
|
@Param(value = "personType", required = false) String personType,
|
||||||
|
@Param(value = "userState", required = false) String userState,
|
||||||
|
@Param(value = "changeType", required = false) String changeType,
|
||||||
|
@Param(value = "changed", required = false) Integer changed,
|
||||||
|
@Param(value = "isMember", required = false) Integer isMember) {
|
||||||
|
Sql sql = Sqls.create("""
|
||||||
|
SELECT u.id, u.loginname, u.username, u.unitname, u.unionname,
|
||||||
|
u.threeUnitName, u.campusId
|
||||||
|
FROM `user` u
|
||||||
|
LEFT JOIN user_history_latest his ON his.loginname = u.loginname
|
||||||
|
LEFT JOIN member_change_record_latest mcr ON mcr.userId = u.id
|
||||||
|
LEFT JOIN audit_state state ON mcr.stateId = state.stateId
|
||||||
|
$condition
|
||||||
|
""");
|
||||||
|
sql.setCondition(memberSelectionCondition(unionId, unitId, threeUnitId, unionGroupId,
|
||||||
|
changeDateBefore, changeDateEnd, pageForm, campus, sex, personType, userState,
|
||||||
|
changeType, changed, isMember));
|
||||||
|
return memberService.listMap(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CndPlus memberSelectionCondition(String unionId, String unitId, String threeUnitId,
|
||||||
|
String unionGroupId, String changeDateBefore, String changeDateEnd,
|
||||||
|
PageForm pageForm, String campus, String sex, String personType,
|
||||||
|
String userState, String changeType, Integer changed, Integer isMember) {
|
||||||
CndPlus cnd = CndPlus.create();
|
CndPlus cnd = CndPlus.create();
|
||||||
|
|
||||||
SqlExpressionGroup group = new SqlExpressionGroup();
|
SqlExpressionGroup group = new SqlExpressionGroup();
|
||||||
@@ -181,8 +230,7 @@ public class MemberChangeManageController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cnd.desc("his.changeTime");
|
cnd.desc("his.changeTime");
|
||||||
sql.setCondition(cnd);
|
return cnd;
|
||||||
return memberService.list(pageForm, sql);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@At
|
@At
|
||||||
@@ -424,35 +472,75 @@ public class MemberChangeManageController {
|
|||||||
@RequiresPermissions("member.change.mange")
|
@RequiresPermissions("member.change.mange")
|
||||||
public Object userUnitMove(String threeUnitId, String unitId, @Param("userId[]") String[] userId) {
|
public Object userUnitMove(String threeUnitId, String unitId, @Param("userId[]") String[] userId) {
|
||||||
|
|
||||||
|
if (userId == null || userId.length == 0) {
|
||||||
|
return Result.error("请选择需要调整的用户");
|
||||||
|
}
|
||||||
Sys_unit unit = memberService.dao().fetch(Sys_unit.class, unitId);
|
Sys_unit unit = memberService.dao().fetch(Sys_unit.class, unitId);
|
||||||
|
if (unit == null) {
|
||||||
|
return Result.error("所选单位不存在");
|
||||||
|
}
|
||||||
|
|
||||||
Trans.exec(() -> {
|
Trans.exec(() -> {
|
||||||
|
|
||||||
Chain updateUnitChain = Chain.make("unitid", unit.getId())
|
List<Sys_user> users = dao.query(Sys_user.class, Cnd.where("id", "in", userId));
|
||||||
.add("unionid", unit.getUnionid())
|
if (users.size() != new HashSet<>(Arrays.asList(userId)).size()) {
|
||||||
.add("threeUnitId", threeUnitId);
|
throw new IllegalArgumentException("部分用户不存在,请重新选择");
|
||||||
memberService.update("sys_user", updateUnitChain, Cnd.where("id", "in", userId));
|
}
|
||||||
|
List<Audit> audits = new ArrayList<>();
|
||||||
|
List<MemberChangeRecord> records = new ArrayList<>();
|
||||||
|
List<UserHistory> histories = new ArrayList<>();
|
||||||
|
Map<String, List<String>> usersByOriginUnit = new LinkedHashMap<>();
|
||||||
|
Date applyTime = new Date();
|
||||||
|
|
||||||
|
for (Sys_user user : users) {
|
||||||
|
UserHistory history = new UserHistory();
|
||||||
|
BeanUtils.copyProperties(user, history);
|
||||||
|
history.setId(R.UU32());
|
||||||
|
history.setChangeTime(applyTime);
|
||||||
|
history.setChangeType(UserChangeType.UNIT_CHANGE.getCode());
|
||||||
|
histories.add(history);
|
||||||
|
|
||||||
for (String uid : userId) {
|
|
||||||
MemberChangeRecord record = new MemberChangeRecord();
|
MemberChangeRecord record = new MemberChangeRecord();
|
||||||
record.setRecordMode(MemberChangeRecordMode.MEMBER.getCode());
|
record.setRecordMode(MemberChangeRecordMode.MEMBER.getCode());
|
||||||
record.setType(UserChangeType.UNIT_CHANGE.getCode());
|
record.setType(UserChangeType.UNIT_CHANGE.getCode());
|
||||||
record.setUserId(uid);
|
record.setUserId(user.getId());
|
||||||
record.setApplyTime(new Date());
|
record.setOriginUnitId(user.getUnitid());
|
||||||
|
record.setOriginUnionId(user.getUnionid());
|
||||||
|
record.setOriginThreeUnitId(user.getThreeUnitId());
|
||||||
|
record.setCurrentUnitId(unit.getId());
|
||||||
|
record.setCurrentUnionId(unit.getUnionid());
|
||||||
|
record.setCurrentThreeUnitId(threeUnitId);
|
||||||
|
record.setApplyTime(applyTime);
|
||||||
record.setApplyOrigin(MemberChangeOrigin.SCHOOL.getCode());
|
record.setApplyOrigin(MemberChangeOrigin.SCHOOL.getCode());
|
||||||
record.setRecordMode(MemberChangeRecordMode.MEMBER.getCode());
|
|
||||||
record.setStateId(MemberChangeApplyState.SUCCESS);
|
record.setStateId(MemberChangeApplyState.SUCCESS);
|
||||||
|
|
||||||
Audit audit = new Audit();
|
Audit audit = new Audit();
|
||||||
audit.setAuditPass(true);
|
audit.setAuditPass(true);
|
||||||
audit.setAuditOpinion(MemberOrigin.BATCH.getDescription());
|
audit.setAuditOpinion(MemberOrigin.BATCH.getDescription());
|
||||||
String id = auditService.insert(audit).getId();
|
audits.add(audit);
|
||||||
|
records.add(record);
|
||||||
record.setSchoolAuditId(id);
|
usersByOriginUnit.computeIfAbsent(user.getUnitid(), key -> new ArrayList<>()).add(user.getId());
|
||||||
memberService.insert(record);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dao.insert(audits);
|
||||||
|
for (int i = 0; i < records.size(); i++) {
|
||||||
|
records.get(i).setSchoolAuditId(audits.get(i).getId());
|
||||||
|
}
|
||||||
|
dao.insert(records);
|
||||||
|
dao.fastInsert(histories);
|
||||||
|
|
||||||
|
usersByOriginUnit.forEach((originUnitId, ids) ->
|
||||||
|
UserChangePublisher.broadcast(new UserChangeMsg(ids,
|
||||||
|
UserChangeMsg.UNIT_CHANGE_OPERATION, originUnitId, unit.getId())));
|
||||||
|
|
||||||
|
Chain updateUnitChain = Chain.make("unitid", unit.getId())
|
||||||
|
.add("unionid", unit.getUnionid())
|
||||||
|
.add("threeUnitId", threeUnitId);
|
||||||
|
memberService.update("sys_user", updateUnitChain, Cnd.where("id", "in", userId));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
userService.clearCache();
|
||||||
|
sysRoleService.clearCache();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ import org.springframework.util.CollectionUtils;
|
|||||||
|
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.time.format.DateTimeParseException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -310,13 +313,51 @@ public class MemberGroupInfoController {
|
|||||||
|
|
||||||
List<Sys_user> userList = new ArrayList<>();
|
List<Sys_user> userList = new ArrayList<>();
|
||||||
|
|
||||||
List<MemberImportTemp> errorInfos = new ArrayList<>();
|
List<NutMap> errorInfos = new ArrayList<>();
|
||||||
memberImportList.forEach(v -> {
|
memberImportList.forEach(v -> {
|
||||||
Sys_user user = sysUserService.fetch(Cnd.where("loginname", "=", v.getLoginName()));
|
Sys_user user = sysUserService.fetch(Cnd.where("loginname", "=", v.getLoginName()));
|
||||||
user = user == null ? new Sys_user() : user;
|
user = user == null ? new Sys_user() : user;
|
||||||
Sys_unit unit = unitMap.get(v.getUnitName());
|
Sys_unit unit = unitMap.get(v.getUnitName());
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
if (!isfg && user.getId() != null) {
|
||||||
|
errors.add("一卡通号已存在,仅追加模式不能重复导入");
|
||||||
|
}
|
||||||
if (unit == null) {
|
if (unit == null) {
|
||||||
errorInfos.add(v);
|
errors.add("请核对该人员的单位信息");
|
||||||
|
}
|
||||||
|
String schoolTime = StrUtil.trim(v.getSchoolTime());
|
||||||
|
LocalDate schoolDate = null;
|
||||||
|
if (StrUtil.isNotBlank(schoolTime)) {
|
||||||
|
if (!schoolTime.matches("[0-9]{8}|[0-9]{4}-[0-9]{2}-[0-9]{2}")) {
|
||||||
|
errors.add("入校时间【" + schoolTime + "】格式错误,请填写yyyy-MM-dd或yyyyMMdd");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
schoolDate = LocalDate.parse(schoolTime, schoolTime.length() == 8
|
||||||
|
? DateTimeFormatter.BASIC_ISO_DATE : DateTimeFormatter.ISO_LOCAL_DATE);
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
errors.add("入校时间【" + schoolTime + "】不是有效日期,请核对年月日");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String memberJoinTime = StrUtil.trim(v.getMemberJoinTime());
|
||||||
|
LocalDate memberJoinDate = null;
|
||||||
|
if (StrUtil.isNotBlank(memberJoinTime)) {
|
||||||
|
if (!memberJoinTime.matches("[0-9]{8}|[0-9]{4}-[0-9]{2}-[0-9]{2}")) {
|
||||||
|
errors.add("入会时间【" + memberJoinTime + "】格式错误,请填写yyyy-MM-dd或yyyyMMdd");
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
memberJoinDate = LocalDate.parse(memberJoinTime, memberJoinTime.length() == 8
|
||||||
|
? DateTimeFormatter.BASIC_ISO_DATE : DateTimeFormatter.ISO_LOCAL_DATE);
|
||||||
|
} catch (DateTimeParseException e) {
|
||||||
|
errors.add("入会时间【" + memberJoinTime + "】不是有效日期,请核对年月日");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
errorInfos.add(NutMap.NEW().addv("工号", v.getLoginName()).addv("姓名", v.getUserName())
|
||||||
|
.addv("性别", v.getSex()).addv("所属工会", v.getUnionName()).addv("所属单位", v.getUnitName())
|
||||||
|
.addv("入校时间", v.getSchoolTime()).addv("入会时间", v.getMemberJoinTime())
|
||||||
|
.addv("错误原因", String.join(";", errors)));
|
||||||
} else {
|
} else {
|
||||||
user.setId(user.getId() == null ? R.UU32() : user.getId());
|
user.setId(user.getId() == null ? R.UU32() : user.getId());
|
||||||
user.setLoginname(v.getLoginName());
|
user.setLoginname(v.getLoginName());
|
||||||
@@ -330,8 +371,8 @@ public class MemberGroupInfoController {
|
|||||||
user.setPolitical(v.getPolitical());
|
user.setPolitical(v.getPolitical());
|
||||||
user.setEducation(v.getEducation());
|
user.setEducation(v.getEducation());
|
||||||
user.setJobTitle(v.getJobTitle());
|
user.setJobTitle(v.getJobTitle());
|
||||||
user.setSchoolTime(v.getSchoolTime());
|
user.setSchoolTime(schoolDate == null ? null : schoolDate.toString());
|
||||||
user.setMemberJoinTime(v.getMemberJoinTime() != null ? DateUtil.parseDate(v.getMemberJoinTime()) : null);
|
user.setMemberJoinTime(memberJoinDate == null ? null : DateUtil.parseDate(memberJoinDate.toString()));
|
||||||
user.setPersonType(v.getPersonType());
|
user.setPersonType(v.getPersonType());
|
||||||
user.setPreparedBy(v.getPreparedBy());
|
user.setPreparedBy(v.getPreparedBy());
|
||||||
user.setRemarks(v.getRemarks());
|
user.setRemarks(v.getRemarks());
|
||||||
@@ -350,16 +391,12 @@ public class MemberGroupInfoController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String errMsg = "请核对该人员的单位信息";
|
|
||||||
|
|
||||||
if (!CollectionUtils.isEmpty(errorInfos)) {
|
if (!CollectionUtils.isEmpty(errorInfos)) {
|
||||||
NutMap nutMap = NutMap.NEW();
|
NutMap nutMap = NutMap.NEW();
|
||||||
nutMap.setv("totalCount", memberImportList.size());
|
nutMap.setv("totalCount", memberImportList.size());
|
||||||
nutMap.setv("successCount", successUserList.size());
|
nutMap.setv("successCount", successUserList.size());
|
||||||
nutMap.setv("errorCount", errorInfos.size());
|
nutMap.setv("errorCount", errorInfos.size());
|
||||||
nutMap.setv("errorList", errorInfos.stream().map(v -> {
|
nutMap.setv("errorList", errorInfos);
|
||||||
return NutMap.NEW().addv("工号", v.getLoginName()).addv("姓名", v.getUserName()).addv("性别", v.getSex()).addv("所属工会", v.getUnionName()).addv("所属单位", v.getUnitName()).addv("错误原因", errMsg);
|
|
||||||
}).collect(Collectors.toList()));
|
|
||||||
return Result.success(nutMap);
|
return Result.success(nutMap);
|
||||||
}
|
}
|
||||||
return Result.success();
|
return Result.success();
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ package io.v.nutz.sys.services;
|
|||||||
import io.v.nutz.base.service.BaseService;
|
import io.v.nutz.base.service.BaseService;
|
||||||
import io.v.nutz.sys.models.Sys_menu;
|
import io.v.nutz.sys.models.Sys_menu;
|
||||||
import io.v.nutz.sys.models.Sys_user;
|
import io.v.nutz.sys.models.Sys_user;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -11,6 +12,15 @@ import java.util.List;
|
|||||||
* Created by wizzer on 2016/12/22.
|
* Created by wizzer on 2016/12/22.
|
||||||
*/
|
*/
|
||||||
public interface SysUserService extends BaseService<Sys_user> {
|
public interface SysUserService extends BaseService<Sys_user> {
|
||||||
|
/**
|
||||||
|
* 无参读取当前Shiro Subject,为H5布局返回可公开的登录用户视图,不缓存当前用户。
|
||||||
|
*
|
||||||
|
* @return 非null的NutMap:id/loginname/username/avatar/unitid/unionid为身份字符串,
|
||||||
|
* roles为仅含code的对象数组,permissions为权限字符串数组;匿名、未知Principal或禁用用户
|
||||||
|
* 返回空身份字符串及两个空数组,不返回密码、盐、完整角色或用户实体。
|
||||||
|
*/
|
||||||
|
NutMap getH5LogonUser();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询用户的角色
|
* 查询用户的角色
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import io.v.nutz.sys.models.Sys_role;
|
|||||||
import io.v.nutz.sys.models.Sys_user;
|
import io.v.nutz.sys.models.Sys_user;
|
||||||
import io.v.nutz.sys.services.SysMenuService;
|
import io.v.nutz.sys.services.SysMenuService;
|
||||||
import io.v.nutz.sys.services.SysUserService;
|
import io.v.nutz.sys.services.SysUserService;
|
||||||
|
import org.apache.shiro.SecurityUtils;
|
||||||
|
import org.apache.shiro.UnavailableSecurityManagerException;
|
||||||
|
import org.apache.shiro.subject.Subject;
|
||||||
import org.nutz.aop.interceptor.ioc.TransAop;
|
import org.nutz.aop.interceptor.ioc.TransAop;
|
||||||
import org.nutz.dao.Cnd;
|
import org.nutz.dao.Cnd;
|
||||||
import org.nutz.dao.Dao;
|
import org.nutz.dao.Dao;
|
||||||
@@ -16,6 +19,7 @@ import org.nutz.ioc.aop.Aop;
|
|||||||
import org.nutz.ioc.loader.annotation.Inject;
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
import org.nutz.ioc.loader.annotation.IocBean;
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheRemove;
|
import org.nutz.plugins.wkcache.annotation.CacheRemove;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
import org.nutz.plugins.wkcache.annotation.CacheRemoveAll;
|
||||||
@@ -23,6 +27,7 @@ import org.nutz.plugins.wkcache.annotation.CacheResult;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -39,6 +44,60 @@ public class SysUserServiceImpl extends BaseServiceImpl<Sys_user> implements Sys
|
|||||||
@Inject
|
@Inject
|
||||||
private SysMenuService sysMenuService;
|
private SysMenuService sysMenuService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无参从当前Shiro登录态读取用户,仅在H5模板实际调用时组装安全视图。
|
||||||
|
* 返回身份白名单、roles:[{code}]和permissions:[字符串];空登录态、未知Principal、
|
||||||
|
* 无有效用户ID或禁用用户均返回空身份和空数组,避免模板json(null)生成无效脚本。
|
||||||
|
* 角色及菜单权限复用现有查询的禁用过滤规则,用户原始字符串由模板注入点负责脚本转义。
|
||||||
|
*
|
||||||
|
* @return 每次调用独立创建的NutMap,不包含密码、盐或完整角色/用户关联对象
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public NutMap getH5LogonUser() {
|
||||||
|
List<NutMap> roles = new ArrayList<>();
|
||||||
|
List<String> permissions = new ArrayList<>();
|
||||||
|
NutMap result = NutMap.NEW().addv("id", "").addv("loginname", "").addv("username", "")
|
||||||
|
.addv("avatar", "").addv("unitid", "").addv("unionid", "")
|
||||||
|
.addv("roles", roles).addv("permissions", permissions);
|
||||||
|
Subject subject;
|
||||||
|
try {
|
||||||
|
subject = SecurityUtils.getSubject();
|
||||||
|
} catch (UnavailableSecurityManagerException e) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Object principal = subject == null ? null : subject.getPrincipal();
|
||||||
|
if (!(principal instanceof Sys_user)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
Sys_user user = (Sys_user) principal;
|
||||||
|
if (user.isDisabled() || Strings.isBlank(user.getId())) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
result.setv("id", user.getId()).setv("loginname", Strings.sNull(user.getLoginname()))
|
||||||
|
.setv("username", Strings.sNull(user.getUsername())).setv("avatar", Strings.sNull(user.getAvatar()))
|
||||||
|
.setv("unitid", Strings.sNull(user.getUnitid())).setv("unionid", Strings.sNull(user.getUnionid()));
|
||||||
|
List<String> roleCodes = getRoleCodeList(user);
|
||||||
|
if (roleCodes != null) {
|
||||||
|
LinkedHashSet<String> uniqueRoles = new LinkedHashSet<>(roleCodes);
|
||||||
|
for (String roleCode : uniqueRoles) {
|
||||||
|
if (!Strings.isBlank(roleCode)) {
|
||||||
|
roles.add(NutMap.NEW().addv("code", roleCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Sys_menu> menus = getMenusAndButtons(user.getId());
|
||||||
|
if (menus != null) {
|
||||||
|
LinkedHashSet<String> uniquePermissions = new LinkedHashSet<>();
|
||||||
|
for (Sys_menu menu : menus) {
|
||||||
|
if (menu != null && !menu.isDisabled() && !Strings.isBlank(menu.getPermission())) {
|
||||||
|
uniquePermissions.add(menu.getPermission());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
permissions.addAll(uniquePermissions);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询用户角色code列表
|
* 查询用户角色code列表
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package io.v.nutz.web.commons.controller;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import io.v.nutz.annontation.ViReturn;
|
||||||
|
import io.v.nutz.base.result.Result;
|
||||||
|
import io.v.nutz.web.commons.ext.map.TMapService;
|
||||||
|
import org.apache.shiro.authz.annotation.RequiresAuthentication;
|
||||||
|
import org.nutz.ioc.loader.annotation.Inject;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.mvc.annotation.At;
|
||||||
|
import org.nutz.mvc.annotation.Ok;
|
||||||
|
import org.nutz.mvc.annotation.POST;
|
||||||
|
import org.nutz.mvc.annotation.Param;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯地图公共接口,供已登录用户在各业务模块中搜索地点。
|
||||||
|
*/
|
||||||
|
@IocBean
|
||||||
|
@At("/platform/TMap")
|
||||||
|
@Ok("json:full")
|
||||||
|
public class TMapController {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private TMapService tMapService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询腾讯地图地址候选,仅接收关键词;地图Key及搜索区域由公共服务读取配置。
|
||||||
|
*
|
||||||
|
* @param keyword 地址关键词,去除首尾空白后须为1至100个字符
|
||||||
|
* @return Result的code/msg/data;成功时data含腾讯status、message和地点data数组,
|
||||||
|
* 地点包含title、address、location.lat(纬度)和location.lng(经度)
|
||||||
|
*/
|
||||||
|
@At
|
||||||
|
@POST
|
||||||
|
@ViReturn
|
||||||
|
@RequiresAuthentication
|
||||||
|
public Result suggestion(@Param(value = "keyword", required = false) String keyword) {
|
||||||
|
if (StrUtil.isBlank(keyword)) {
|
||||||
|
return Result.error("请输入地址关键词");
|
||||||
|
}
|
||||||
|
String searchKeyword = keyword.trim();
|
||||||
|
if (searchKeyword.length() > 100) {
|
||||||
|
return Result.error("地址关键词不能超过100个字符");
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Result.success(tMapService.suggestion(searchKeyword));
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
return Result.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package io.v.nutz.web.commons.ext.map;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.http.HttpResponse;
|
||||||
|
import cn.hutool.http.HttpUtil;
|
||||||
|
import io.v.nutz.web.commons.base.Globals;
|
||||||
|
import org.nutz.ioc.loader.annotation.IocBean;
|
||||||
|
import org.nutz.json.Json;
|
||||||
|
import org.nutz.lang.util.NutMap;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 腾讯地图公共地点搜索服务,负责请求参数编码、服务调用和响应校验。
|
||||||
|
*/
|
||||||
|
@IocBean
|
||||||
|
public class TMapService {
|
||||||
|
|
||||||
|
// 地点搜索仅访问腾讯固定服务;系统TencentMapKey可覆盖参考工程的默认Key。
|
||||||
|
private static final String TENCENT_MAP_SUGGESTION_URL = "https://apis.map.qq.com/ws/place/v1/suggestion";
|
||||||
|
private static final String DEFAULT_TENCENT_MAP_KEY = "NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取腾讯地图地址候选,仅访问固定的地点搜索服务,不修改本地数据。
|
||||||
|
*
|
||||||
|
* @param keyword 地址关键词,去除首尾空白后须为1至100个字符,搜索区域沿用系统CityName配置
|
||||||
|
* @return 腾讯响应对象;status=0表示成功,data为地点数组,location.lat为纬度、location.lng为经度
|
||||||
|
* @throws IllegalArgumentException 参数、网络或腾讯地图响应校验失败时提供可读业务提示
|
||||||
|
*/
|
||||||
|
public NutMap suggestion(String keyword) {
|
||||||
|
if (StrUtil.isBlank(keyword) || keyword.trim().length() > 100) {
|
||||||
|
throw new IllegalArgumentException("地址关键词须为1至100个字符");
|
||||||
|
}
|
||||||
|
String mapKey = Globals.MyConfig.getString("TencentMapKey");
|
||||||
|
if (StrUtil.isBlank(mapKey)) {
|
||||||
|
mapKey = DEFAULT_TENCENT_MAP_KEY;
|
||||||
|
}
|
||||||
|
Map<String, Object> params = new LinkedHashMap<>();
|
||||||
|
params.put("key", mapKey.trim());
|
||||||
|
params.put("keyword", keyword.trim());
|
||||||
|
params.put("region", StrUtil.trimToEmpty(Globals.MyConfig.getString("CityName")));
|
||||||
|
// 参数交由HTTP库编码,避免中文、&等字符改变查询内容;禁用重定向,保持固定服务地址。
|
||||||
|
String responseBody;
|
||||||
|
try (HttpResponse response = HttpUtil.createGet(TENCENT_MAP_SUGGESTION_URL)
|
||||||
|
.charset(StandardCharsets.UTF_8).form(params).timeout(5000).setFollowRedirects(false).execute()) {
|
||||||
|
if (response.getStatus() != 200) {
|
||||||
|
throw new IllegalStateException("腾讯地图地址搜索请求未成功");
|
||||||
|
}
|
||||||
|
responseBody = response.body();
|
||||||
|
} catch (Exception e) {
|
||||||
|
// HTTP库异常可能携带完整URL,禁止向前端或日志透传,以免暴露地图Key。
|
||||||
|
throw new IllegalArgumentException("腾讯地图地址搜索失败,请稍后重试");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(responseBody)) {
|
||||||
|
throw new IllegalArgumentException("腾讯地图地址搜索响应为空,请稍后重试");
|
||||||
|
}
|
||||||
|
NutMap result;
|
||||||
|
try {
|
||||||
|
result = Json.fromJson(NutMap.class, responseBody);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new IllegalArgumentException("腾讯地图地址搜索响应异常,请稍后重试");
|
||||||
|
}
|
||||||
|
// 缺少状态不能按默认0当作成功;地址候选必须为数组,供前端自动完成组件直接使用。
|
||||||
|
if (result == null || !(result.get("status") instanceof Number)) {
|
||||||
|
throw new IllegalArgumentException("腾讯地图地址搜索响应异常,请稍后重试");
|
||||||
|
}
|
||||||
|
if (result.getInt("status") != 0) {
|
||||||
|
throw new IllegalArgumentException("腾讯地图地址搜索失败,请检查地图服务配置");
|
||||||
|
}
|
||||||
|
if (!(result.get("data") instanceof List)) {
|
||||||
|
throw new IllegalArgumentException("腾讯地图地址搜索响应异常,请稍后重试");
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package io.v.nutz.web.commons.proc;
|
|||||||
|
|
||||||
import io.v.nutz.base.utils.DateUtil;
|
import io.v.nutz.base.utils.DateUtil;
|
||||||
import io.v.nutz.base.utils.StringUtil;
|
import io.v.nutz.base.utils.StringUtil;
|
||||||
|
import io.v.nutz.sys.services.SysUserService;
|
||||||
import io.v.nutz.util.ShiroUtil;
|
import io.v.nutz.util.ShiroUtil;
|
||||||
import io.v.nutz.web.commons.base.Globals;
|
import io.v.nutz.web.commons.base.Globals;
|
||||||
import org.nutz.lang.Strings;
|
import org.nutz.lang.Strings;
|
||||||
@@ -18,6 +19,7 @@ public class GlobalsSettingProcessor extends AbstractProcessor {
|
|||||||
private static ShiroUtil ShiroUtil;
|
private static ShiroUtil ShiroUtil;
|
||||||
private static DateUtil dateUtil;
|
private static DateUtil dateUtil;
|
||||||
private static StringUtil stringUtil;
|
private static StringUtil stringUtil;
|
||||||
|
private SysUserService h5UserService;
|
||||||
|
|
||||||
public void init(NutConfig config, ActionInfo ai) throws Throwable {
|
public void init(NutConfig config, ActionInfo ai) throws Throwable {
|
||||||
if (ShiroUtil == null) {
|
if (ShiroUtil == null) {
|
||||||
@@ -29,6 +31,8 @@ public class GlobalsSettingProcessor extends AbstractProcessor {
|
|||||||
if (stringUtil == null) {
|
if (stringUtil == null) {
|
||||||
stringUtil = new StringUtil();
|
stringUtil = new StringUtil();
|
||||||
}
|
}
|
||||||
|
// 仅取得当前应用的服务代理,不在处理器初始化时读取登录态或查询用户权限。
|
||||||
|
h5UserService = config.getIoc().getByType(SysUserService.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("rawtypes")
|
@SuppressWarnings("rawtypes")
|
||||||
@@ -41,6 +45,8 @@ public class GlobalsSettingProcessor extends AbstractProcessor {
|
|||||||
ac.getRequest().setAttribute("AppFileDomain", Globals.AppFileDomain);
|
ac.getRequest().setAttribute("AppFileDomain", Globals.AppFileDomain);
|
||||||
ac.getRequest().setAttribute("config", Globals.MyConfig);
|
ac.getRequest().setAttribute("config", Globals.MyConfig);
|
||||||
ac.getRequest().setAttribute("shiro", ShiroUtil);
|
ac.getRequest().setAttribute("shiro", ShiroUtil);
|
||||||
|
// H5 布局渲染时再调用服务生成安全用户视图,其余请求只绑定对象,不额外查询用户。
|
||||||
|
ac.getRequest().setAttribute("h5UserService", h5UserService);
|
||||||
ac.getRequest().setAttribute("date", dateUtil);
|
ac.getRequest().setAttribute("date", dateUtil);
|
||||||
ac.getRequest().setAttribute("string", stringUtil);
|
ac.getRequest().setAttribute("string", stringUtil);
|
||||||
// 如果url中有语言属性则设置
|
// 如果url中有语言属性则设置
|
||||||
|
|||||||
@@ -25,4 +25,11 @@ public interface WelfareProjectService extends ViService<WelfareProject> {
|
|||||||
|
|
||||||
List<WelfareProject> getSchoolWelfareList(Integer year);
|
List<WelfareProject> getSchoolWelfareList(Integer year);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取签到扩展可关联的福利项目,包含选择尚未开始和正在进行的项目。
|
||||||
|
*
|
||||||
|
* @return 选择结束时间未过期的项目列表;每项的 id、name 用于活动配置的福利选择框
|
||||||
|
*/
|
||||||
|
List<WelfareProject> getAvailableSignWelfareProjects();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.nutz.lang.Strings;
|
|||||||
import org.nutz.lang.util.NutMap;
|
import org.nutz.lang.util.NutMap;
|
||||||
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
import org.nutz.plugins.wkcache.annotation.CacheDefaults;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -76,4 +77,15 @@ public class WelfareProjectServiceImpl extends ViServiceImpl<WelfareProject> imp
|
|||||||
|
|
||||||
return projectList;
|
return projectList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 当前项目没有福利禁用字段,按已有的选择结束时间筛选可关联项目。
|
||||||
|
*
|
||||||
|
* @return 选择时间未结束的福利项目,按开始时间及创建时间排列
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<WelfareProject> getAvailableSignWelfareProjects() {
|
||||||
|
return query(Cnd.where("choiceTimeEnd", ">=", new Date())
|
||||||
|
.asc("choiceTimeStart").desc("createdAt"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
/* 手机弹框共用一份历史栈;页面和子组件只注册状态回调,不各自监听 popstate。 */
|
||||||
|
;(() => {
|
||||||
|
if (window.mobilePopupHistory) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const stateKey = '__mobilePopupHistory'
|
||||||
|
const session = Date.now() + '-' + Math.random().toString(36).slice(2)
|
||||||
|
const entries = []
|
||||||
|
const owners = new Map()
|
||||||
|
let pending = null
|
||||||
|
|
||||||
|
// 保留页面原有 history.state;标记只用于本次页面内的弹框层级,不携带业务数据。
|
||||||
|
const markState = (depth) => Object.assign({}, window.history.state || {}, {
|
||||||
|
[stateKey]: {session: session, depth: depth}
|
||||||
|
})
|
||||||
|
const notify = (entry, visible) => {
|
||||||
|
const callback = owners.get(entry.owner)
|
||||||
|
if (callback) {
|
||||||
|
callback(entry.key, visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/** 回退 count 层历史并等待 popstate;关闭按钮、提交成功及跳转均复用此方法。 */
|
||||||
|
const retreat = (count) => {
|
||||||
|
if (pending) {
|
||||||
|
return pending.promise.then(() => retreat(Math.min(count, entries.length)))
|
||||||
|
}
|
||||||
|
if (count <= 0) {
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
let complete = null
|
||||||
|
const promise = new Promise((resolve) => {
|
||||||
|
complete = resolve
|
||||||
|
})
|
||||||
|
pending = {promise: promise, complete: complete}
|
||||||
|
window.history.go(-count)
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
// 唯一的浏览器返回监听:从最上层开始同步关闭,前进到旧记录也不重新打开已关闭弹框。
|
||||||
|
window.addEventListener('popstate', (event) => {
|
||||||
|
const marker = event.state && event.state[stateKey]
|
||||||
|
const depth = marker && marker.session === session ? Math.max(0, marker.depth) : 0
|
||||||
|
while (entries.length > depth) {
|
||||||
|
notify(entries.pop(), false)
|
||||||
|
}
|
||||||
|
if (depth > entries.length) {
|
||||||
|
window.history.replaceState(markState(entries.length), '', window.location.href)
|
||||||
|
}
|
||||||
|
if (pending) {
|
||||||
|
const completed = pending
|
||||||
|
pending = null
|
||||||
|
completed.complete()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.mobilePopupHistory = {
|
||||||
|
/**
|
||||||
|
* owner 为页面/组件唯一标识;sync(key, visible) 通过 $set 同步对应弹框状态。
|
||||||
|
* 返回 open/close/has/clear/unregister;关闭和清理返回 Promise,完成后才能跳转。
|
||||||
|
*/
|
||||||
|
register(owner, sync) {
|
||||||
|
owners.set(owner, sync)
|
||||||
|
return {
|
||||||
|
// 先写历史再打开弹框;正在回退时拒绝打开,避免异步 popstate 误关新弹框。
|
||||||
|
open(key) {
|
||||||
|
if (pending || entries.some((item) => item.owner === owner && item.key === key)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const entry = {owner: owner, key: key}
|
||||||
|
window.history.pushState(markState(entries.length + 1), '', window.location.href)
|
||||||
|
entries.push(entry)
|
||||||
|
notify(entry, true)
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
has(key) {
|
||||||
|
return entries.some((item) => item.owner === owner && item.key === key)
|
||||||
|
},
|
||||||
|
// 关闭父弹框时同时关闭其上方的子弹框,始终遵循后进先出。
|
||||||
|
close(key) {
|
||||||
|
if (pending) {
|
||||||
|
return pending.promise.then(() => this.close(key))
|
||||||
|
}
|
||||||
|
const index = entries.findIndex((item) => item.owner === owner && item.key === key)
|
||||||
|
return retreat(index < 0 ? 0 : entries.length - index)
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
if (pending) {
|
||||||
|
return pending.promise.then(() => this.clear())
|
||||||
|
}
|
||||||
|
const index = entries.findIndex((item) => item.owner === owner)
|
||||||
|
return retreat(index < 0 ? 0 : entries.length - index)
|
||||||
|
},
|
||||||
|
// 销毁即注销回调,避免迟到的返回事件访问已销毁的 Vue 实例。
|
||||||
|
unregister() {
|
||||||
|
owners.delete(owner)
|
||||||
|
return this.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 无参数;有弹框时仅关闭最上层,返回 true;否则返回 false,由页面正常返回。 */
|
||||||
|
back() {
|
||||||
|
if (pending) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (!entries.length) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
retreat(1)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
+690
@@ -0,0 +1,690 @@
|
|||||||
|
<style>
|
||||||
|
.field .van-field__error-message {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<van-form @submit="onSubmit" ref="form">
|
||||||
|
|
||||||
|
<template v-for="column in dynamicData">
|
||||||
|
<!-- 文本、数值、时间 -->
|
||||||
|
<template v-if="['VARCHAR', 'TEXT', 'INT', 'DATE', 'DATETIME'].includes(column.columnType)">
|
||||||
|
<template v-if="isAddressField(column)">
|
||||||
|
<van-field
|
||||||
|
class="field"
|
||||||
|
:value="getAddressAreaText(column)"
|
||||||
|
clickable
|
||||||
|
:label="column.columnName"
|
||||||
|
:name="column.columnCode + 'Area'"
|
||||||
|
:placeholder="'请选择' + column.columnName"
|
||||||
|
readonly
|
||||||
|
:rules="buildAddressAreaRules(column)"
|
||||||
|
:required="column.isRequired"
|
||||||
|
@click="openAddressPicker(column)"
|
||||||
|
></van-field>
|
||||||
|
<van-popup
|
||||||
|
v-model="pickerStates[column.columnCode + 'Picker']"
|
||||||
|
position="bottom"
|
||||||
|
get-container="body"
|
||||||
|
>
|
||||||
|
<van-area
|
||||||
|
:area-list="addressAreaList"
|
||||||
|
:value="getAddressAreaCode(column)"
|
||||||
|
:title="'选择' + column.columnName"
|
||||||
|
:area-columns-placeholder="['请选择', '请选择', '请选择']"
|
||||||
|
@cancel="closeAddressPicker(column)"
|
||||||
|
@confirm="
|
||||||
|
(values) => {
|
||||||
|
addressPickerConfirm(values, column)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
></van-area>
|
||||||
|
</van-popup>
|
||||||
|
<van-field
|
||||||
|
class="field"
|
||||||
|
v-model="addressStates[column.columnCode].detail"
|
||||||
|
label="详细地址"
|
||||||
|
:name="column.columnCode"
|
||||||
|
placeholder="请填写详细地址"
|
||||||
|
:rules="buildAddressDetailRules(column)"
|
||||||
|
:required="column.isRequired"
|
||||||
|
@input="addressDetailInput(column)"
|
||||||
|
></van-field>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<van-field
|
||||||
|
class="field"
|
||||||
|
v-model="column.columnValue"
|
||||||
|
:clickable="['SELECT'].includes(column.columnFormType)"
|
||||||
|
:label="column.columnName"
|
||||||
|
:name="column.columnCode"
|
||||||
|
:type="['INT'].includes(column.columnType) ? 'digit' : ''"
|
||||||
|
:placeholder="(['SELECT'].includes(column.columnFormType) ? '请选择' : '请填写') + column.columnName"
|
||||||
|
:readonly="['SELECT'].includes(column.columnFormType)"
|
||||||
|
:rules="[{ required: column.isRequired, message: (['SELECT'].includes(column.columnFormType) ? '请选择' : '请填写') + column.columnName }]"
|
||||||
|
:required="column.isRequired"
|
||||||
|
@click="selectFieldClick(column)"
|
||||||
|
></van-field>
|
||||||
|
<van-popup
|
||||||
|
v-if="['SELECT'].includes(column.columnFormType)"
|
||||||
|
v-model="pickerStates[column.columnCode + 'Picker']"
|
||||||
|
position="bottom"
|
||||||
|
get-container="body"
|
||||||
|
>
|
||||||
|
<template v-if="['DATETIME', 'DATE'].includes(column.columnType)">
|
||||||
|
<van-datetime-picker
|
||||||
|
v-model="column.currentDate"
|
||||||
|
:min-date="column.minDate"
|
||||||
|
:title="column.columnName"
|
||||||
|
:type="column.columnType.toLocaleLowerCase()"
|
||||||
|
show-toolbar
|
||||||
|
@cancel="dateTimePickerCancel(column)"
|
||||||
|
@confirm="
|
||||||
|
(time) => {
|
||||||
|
dateTimePickerConfirm(time, column)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<van-picker
|
||||||
|
:columns="column.selectValues"
|
||||||
|
:title="column.columnName"
|
||||||
|
show-toolbar
|
||||||
|
@cancel="ordinaryPickerCancel(column)"
|
||||||
|
@confirm="
|
||||||
|
(value) => {
|
||||||
|
ordinaryPickerConfirm(value, column)
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</van-popup>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<!-- 单选框 -->
|
||||||
|
<template v-else-if="['BOOLEAN'].includes(column.columnType)"></template>
|
||||||
|
<!-- 文件 -->
|
||||||
|
<template v-else-if="['JSON'].includes(column.columnType)">
|
||||||
|
<van-field
|
||||||
|
:required="column.isRequired"
|
||||||
|
:label="column.columnName"
|
||||||
|
:name="column.columnCode"
|
||||||
|
:rules="[{ validator: () => !column.isRequired || getSavedFileValues(column).length > 0, message: '请上传' + column.columnName }]"
|
||||||
|
>
|
||||||
|
<template #input>
|
||||||
|
<van-uploader
|
||||||
|
v-model="fileLists[column.columnCode]"
|
||||||
|
multiple
|
||||||
|
:max-count="getFileMaxCount(column)"
|
||||||
|
:accept="getFileAccept(column)"
|
||||||
|
:before-read="(files) => fileBeforeRead(files, column)"
|
||||||
|
:after-read="(files) => fileAfterRead(files, column)"
|
||||||
|
:preview-full-image="false"
|
||||||
|
@click-preview="previewFile"
|
||||||
|
@delete="() => syncFileColumnValue(column)"
|
||||||
|
></van-uploader>
|
||||||
|
</template>
|
||||||
|
</van-field>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</van-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 外层通过组件的 ref.form.validate() 校验,避免再次嵌套 van-form。 -->
|
||||||
|
|
||||||
|
<script>
|
||||||
|
module.exports = {
|
||||||
|
name: "BrandTrainDynamicForm",
|
||||||
|
components: {},
|
||||||
|
props: {
|
||||||
|
dynamicData: []
|
||||||
|
},
|
||||||
|
model: {
|
||||||
|
prop: "dynamicData",
|
||||||
|
event: "updateDynamicData"
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
// 地址、日期及普通选项共用选择器状态;取消、遮罩关闭也必须退出对应历史层。
|
||||||
|
pickerStates: {
|
||||||
|
handler(states) {
|
||||||
|
if (!this.popupHistory) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Object.keys(states).forEach((key) => {
|
||||||
|
if (states[key] && !this.popupHistory.has(key)) {
|
||||||
|
if (!this.popupHistory.open(key)) {
|
||||||
|
this.$set(this.pickerStates, key, false)
|
||||||
|
}
|
||||||
|
} else if (!states[key] && this.popupHistory.has(key)) {
|
||||||
|
this.popupHistory.close(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
dynamicData: {
|
||||||
|
handler(newValue) {
|
||||||
|
this.initAddressStates(newValue)
|
||||||
|
this.initFileStates(newValue)
|
||||||
|
this.$emit("updateDynamicData", newValue)
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
//所有的picker状态
|
||||||
|
pickerStates: {},
|
||||||
|
popupHistory: null,
|
||||||
|
imagePreview: null,
|
||||||
|
fileLists: {},
|
||||||
|
fileValueSnapshots: {},
|
||||||
|
addressStates: {},
|
||||||
|
addressAreaList: window.brandTrainSignUpAreaList || { province_list: {}, city_list: {}, county_list: {} }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
// 父页面提交前读取该状态,上传中的本地文件不能作为已保存附件提交。
|
||||||
|
hasUploading() {
|
||||||
|
return Object.keys(this.fileLists).some((key) => this.fileLists[key].some((item) => item.status === 'uploading'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async validForm() {
|
||||||
|
try {
|
||||||
|
this.$refs.form.validate().then(() => {
|
||||||
|
return true
|
||||||
|
}).catch(() => {
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async onSubmit() {
|
||||||
|
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 初始化地址字段的联动状态。
|
||||||
|
* 动态表单只保存 columnValue 字符串,这里需要额外拆成省市区和详细地址,方便前端进行地区联动选择。
|
||||||
|
*
|
||||||
|
* @param columns 当前动态字段列表
|
||||||
|
*/
|
||||||
|
initAddressStates(columns) {
|
||||||
|
if (!columns || columns.length === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
columns.forEach((column) => {
|
||||||
|
if (!this.isAddressField(column)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const existsState = this.addressStates[column.columnCode]
|
||||||
|
if (existsState && existsState.initializedValue === column.columnValue) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const addressState = this.parseAddressValue(column.columnValue)
|
||||||
|
addressState.initializedValue = column.columnValue || ""
|
||||||
|
this.$set(this.addressStates, column.columnCode, addressState)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 判断当前动态字段是否属于收货地址类字段。
|
||||||
|
* 目前后台没有独立的地址控件类型,因此这里按字段名称和字段编码做兼容识别。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
* @return true-按地址控件渲染;false-沿用原有输入方式
|
||||||
|
*/
|
||||||
|
isAddressField(column) {
|
||||||
|
const columnName = (column && column.columnName) ? column.columnName : ""
|
||||||
|
const columnCode = (column && column.columnCode) ? column.columnCode.toLowerCase() : ""
|
||||||
|
return ["收货地址", "邮寄地址", "礼品邮递地址", "邮递地址"].some((item) => columnName.indexOf(item) > -1)
|
||||||
|
|| ["receiveaddress", "mailaddress", "shippingaddress"].some((item) => columnCode.indexOf(item) > -1)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 将已保存的地址字符串拆分为省市区和详细地址。
|
||||||
|
* 如果旧数据无法可靠拆分,就保留原始值作为兼容显示,避免历史文本地址丢失。
|
||||||
|
*
|
||||||
|
* @param addressValue 当前字段已保存的地址字符串
|
||||||
|
* @return 地址状态对象
|
||||||
|
*/
|
||||||
|
parseAddressValue(addressValue) {
|
||||||
|
const emptyState = {
|
||||||
|
province: "",
|
||||||
|
city: "",
|
||||||
|
county: "",
|
||||||
|
detail: "",
|
||||||
|
displayArea: "",
|
||||||
|
areaCode: "",
|
||||||
|
legacyValue: addressValue || "",
|
||||||
|
initializedValue: addressValue || ""
|
||||||
|
}
|
||||||
|
if (!addressValue) {
|
||||||
|
return emptyState
|
||||||
|
}
|
||||||
|
const provinceMatch = this.findAreaOption(addressValue, this.addressAreaList.province_list)
|
||||||
|
if (!provinceMatch) {
|
||||||
|
return emptyState
|
||||||
|
}
|
||||||
|
let remainValue = addressValue.substring(provinceMatch.name.length)
|
||||||
|
const cityMatch = this.findAreaOption(remainValue, this.addressAreaList.city_list, provinceMatch.code.substring(0, 2))
|
||||||
|
if (!cityMatch) {
|
||||||
|
return emptyState
|
||||||
|
}
|
||||||
|
remainValue = remainValue.substring(cityMatch.name.length)
|
||||||
|
const countyMatch = this.findAreaOption(remainValue, this.addressAreaList.county_list, cityMatch.code.substring(0, 4))
|
||||||
|
if (!countyMatch) {
|
||||||
|
return {
|
||||||
|
province: provinceMatch.name,
|
||||||
|
city: cityMatch.name,
|
||||||
|
county: "",
|
||||||
|
detail: remainValue,
|
||||||
|
displayArea: provinceMatch.name + cityMatch.name,
|
||||||
|
areaCode: cityMatch.code,
|
||||||
|
legacyValue: "",
|
||||||
|
initializedValue: addressValue || ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
remainValue = remainValue.substring(countyMatch.name.length)
|
||||||
|
return {
|
||||||
|
province: provinceMatch.name,
|
||||||
|
city: cityMatch.name,
|
||||||
|
county: countyMatch.name,
|
||||||
|
detail: remainValue,
|
||||||
|
displayArea: provinceMatch.name + cityMatch.name + countyMatch.name,
|
||||||
|
areaCode: countyMatch.code,
|
||||||
|
legacyValue: "",
|
||||||
|
initializedValue: addressValue || ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 根据地址字符串前缀匹配地区代码和值。
|
||||||
|
*
|
||||||
|
* @param addressValue 地址字符串
|
||||||
|
* @param areaMap 地区编码映射
|
||||||
|
* @param codePrefix 编码前缀,用于限制省/市/区层级
|
||||||
|
* @return 匹配到的地区对象
|
||||||
|
*/
|
||||||
|
findAreaOption(addressValue, areaMap, codePrefix) {
|
||||||
|
if (!addressValue || !areaMap) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let match = null
|
||||||
|
Object.keys(areaMap).forEach((code) => {
|
||||||
|
if (codePrefix && code.indexOf(codePrefix) !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const name = areaMap[code]
|
||||||
|
if (addressValue.indexOf(name) !== 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!match || name.length > match.name.length) {
|
||||||
|
match = {
|
||||||
|
code: code,
|
||||||
|
name: name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return match
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取地址字段当前展示的省市区文本。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
* @return 省市区展示文本
|
||||||
|
*/
|
||||||
|
getAddressAreaText(column) {
|
||||||
|
const state = this.addressStates[column.columnCode]
|
||||||
|
if (!state) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return state.displayArea || state.legacyValue || ""
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 获取地区选择器默认定位的地区编码。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
* @return 地区编码
|
||||||
|
*/
|
||||||
|
getAddressAreaCode(column) {
|
||||||
|
const state = this.addressStates[column.columnCode]
|
||||||
|
return state ? state.areaCode : ""
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 地址地区选择字段校验。
|
||||||
|
* 已有历史纯文本地址时允许直接通过,避免旧数据因无法拆分省市区而被强制重填。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
* @return van-field 规则数组
|
||||||
|
*/
|
||||||
|
buildAddressAreaRules(column) {
|
||||||
|
if (!column.isRequired) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [{
|
||||||
|
validator: () => {
|
||||||
|
const state = this.addressStates[column.columnCode]
|
||||||
|
if (!state) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !!(state.displayArea || state.legacyValue)
|
||||||
|
},
|
||||||
|
message: "请选择" + column.columnName
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 地址详细地址字段校验。
|
||||||
|
* 新填写的数据要求必须包含详细地址,历史纯文本地址继续兼容通过。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
* @return van-field 规则数组
|
||||||
|
*/
|
||||||
|
buildAddressDetailRules(column) {
|
||||||
|
if (!column.isRequired) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return [{
|
||||||
|
validator: () => {
|
||||||
|
const state = this.addressStates[column.columnCode]
|
||||||
|
if (!state) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !!(state.detail || state.legacyValue)
|
||||||
|
},
|
||||||
|
message: "请填写详细地址"
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 打开地址地区选择器。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
*/
|
||||||
|
openAddressPicker(column) {
|
||||||
|
this.$set(this.pickerStates, column.columnCode + "Picker", true)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 关闭地址地区选择器。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
*/
|
||||||
|
closeAddressPicker(column) {
|
||||||
|
this.$set(this.pickerStates, column.columnCode + "Picker", false)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 地址地区确认后同步 columnValue。
|
||||||
|
*
|
||||||
|
* @param values van-area 返回的省市区数组
|
||||||
|
* @param column 动态字段配置
|
||||||
|
*/
|
||||||
|
addressPickerConfirm(values, column) {
|
||||||
|
const validValues = (values || []).filter((item) => item && item.name)
|
||||||
|
const province = validValues[0] ? validValues[0].name : ""
|
||||||
|
const city = validValues[1] ? validValues[1].name : ""
|
||||||
|
const county = validValues[2] ? validValues[2].name : ""
|
||||||
|
const areaCode = validValues[2] ? validValues[2].code : (validValues[1] ? validValues[1].code : "")
|
||||||
|
const currentState = this.addressStates[column.columnCode] || {}
|
||||||
|
this.$set(this.addressStates, column.columnCode, {
|
||||||
|
province: province,
|
||||||
|
city: city,
|
||||||
|
county: county,
|
||||||
|
detail: currentState.detail || "",
|
||||||
|
displayArea: province + city + county,
|
||||||
|
areaCode: areaCode,
|
||||||
|
legacyValue: "",
|
||||||
|
initializedValue: currentState.initializedValue || ""
|
||||||
|
})
|
||||||
|
this.syncAddressColumnValue(column)
|
||||||
|
this.closeAddressPicker(column)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 详细地址输入后同步完整地址字符串。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
*/
|
||||||
|
addressDetailInput(column) {
|
||||||
|
const currentState = this.addressStates[column.columnCode] || {}
|
||||||
|
this.$set(this.addressStates, column.columnCode, {
|
||||||
|
province: currentState.province || "",
|
||||||
|
city: currentState.city || "",
|
||||||
|
county: currentState.county || "",
|
||||||
|
detail: currentState.detail || "",
|
||||||
|
displayArea: currentState.displayArea || "",
|
||||||
|
areaCode: currentState.areaCode || "",
|
||||||
|
legacyValue: "",
|
||||||
|
initializedValue: currentState.initializedValue || ""
|
||||||
|
})
|
||||||
|
this.syncAddressColumnValue(column)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 将省市区和详细地址重新拼装回 columnValue。
|
||||||
|
* 后端仍然只接收一个 columnValue 字符串,因此这里保持原有存储结构不变。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
*/
|
||||||
|
syncAddressColumnValue(column) {
|
||||||
|
const state = this.addressStates[column.columnCode]
|
||||||
|
if (!state) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const addressValue = (state.displayArea || "") + (state.detail || "")
|
||||||
|
this.$set(column, "columnValue", addressValue)
|
||||||
|
},
|
||||||
|
selectFieldClick(column) {
|
||||||
|
// 普通输入框也绑定了点击事件,只有实际存在选择弹框的字段才允许打开历史层。
|
||||||
|
if (column.columnFormType !== 'SELECT') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(this.pickerStates, column.columnCode + "Picker", true)
|
||||||
|
if (!["DATE", "DATETIME"].includes(column.columnType)) {
|
||||||
|
//说明是普通的下拉框
|
||||||
|
// column.selectValues = ['1', '2', '3']
|
||||||
|
}else{
|
||||||
|
this.$set(column, "minDate", new Date(1900, 0, 1))
|
||||||
|
this.$set(column, "currentDate", new Date(column.columnValue))
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 时间picker取消后,清空当前字段值。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
*/
|
||||||
|
dateTimePickerCancel(column) {
|
||||||
|
this.$set(this.pickerStates, column.columnCode + "Picker", false)
|
||||||
|
this.$set(column, "columnValue", "")
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 时间picker确认
|
||||||
|
* @param time
|
||||||
|
* @param column
|
||||||
|
*/
|
||||||
|
dateTimePickerConfirm(time, column) {
|
||||||
|
if (column.columnType === "DATE") {
|
||||||
|
this.$set(column, "columnValue", this.$moment(time).format("YYYY-MM-DD"))
|
||||||
|
} else if (column.columnType === "DATETIME") {
|
||||||
|
this.$set(column, "columnValue", this.$moment(time).format("YYYY-MM-DD HH:mm:ss"))
|
||||||
|
} else {
|
||||||
|
//预留别的类型
|
||||||
|
this.$set(column, "columnValue", this.$moment(time).format("YYYY-MM-DD HH:mm:ss"))
|
||||||
|
}
|
||||||
|
this.$set(this.pickerStates, column.columnCode + "Picker", false)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 普通picker取消后,清空当前字段值。
|
||||||
|
*
|
||||||
|
* @param column 动态字段配置
|
||||||
|
*/
|
||||||
|
ordinaryPickerCancel(column) {
|
||||||
|
this.$set(this.pickerStates, column.columnCode + "Picker", false)
|
||||||
|
this.$set(column, "columnValue", "")
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 普通picker确认
|
||||||
|
* @param value
|
||||||
|
* @param column
|
||||||
|
*/
|
||||||
|
ordinaryPickerConfirm(value, column) {
|
||||||
|
this.$set(column, "columnValue", value)
|
||||||
|
this.$set(this.pickerStates, column.columnCode + "Picker", false)
|
||||||
|
},
|
||||||
|
/** 读取已保存附件数组,兼容历史 JSON 字符串;本地 File 和上传状态只保存在 fileLists。 */
|
||||||
|
getSavedFileValues(column) {
|
||||||
|
let values = column.columnValue
|
||||||
|
if (typeof values === 'string') {
|
||||||
|
try {
|
||||||
|
values = values ? JSON.parse(values) : []
|
||||||
|
} catch (err) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.isArray(values) ? values.filter((item) => item && (item.id || item.url || item.filepath)) : []
|
||||||
|
},
|
||||||
|
/** 回填当前 Sys_file 或源系统 name/url 附件,快照未变化时保留正在上传、失败的本地预览项。 */
|
||||||
|
initFileStates(columns) {
|
||||||
|
(columns || []).forEach((column) => {
|
||||||
|
if (column.columnType !== 'JSON') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const snapshot = JSON.stringify(column.columnValue || [])
|
||||||
|
if (this.fileValueSnapshots[column.columnCode] === snapshot) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(this.fileValueSnapshots, column.columnCode, snapshot)
|
||||||
|
this.$set(this.fileLists, column.columnCode, this.getSavedFileValues(column).map((item) => this.buildFilePreview(item)))
|
||||||
|
})
|
||||||
|
},
|
||||||
|
/** 附件展示使用本站文件预览地址;旧 name/url 记录保留原 URL,不改变已保存的业务结构。 */
|
||||||
|
buildFilePreview(data) {
|
||||||
|
const name = data.filename || data.name || '附件'
|
||||||
|
return {
|
||||||
|
data: data,
|
||||||
|
name: name,
|
||||||
|
url: data.id ? '/file_server/fileStreamPreview?id=' + encodeURIComponent(data.id) : (data.url || data.filepath || ''),
|
||||||
|
isImage: /\.(jpg|jpeg|png|gif|bmp|webp|svg)$/i.test(name),
|
||||||
|
status: ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getFileMaxCount(column) {
|
||||||
|
return Number(column.fileNumber) > 0 ? Number(column.fileNumber) : 1
|
||||||
|
},
|
||||||
|
getFileAccept(column) {
|
||||||
|
const types = Array.isArray(column.fileType) ? column.fileType : []
|
||||||
|
return types.length > 0 ? types.map((type) => type.indexOf('/') > -1 ? type : '.' + type.replace(/^\./, '')).join(',') : '*/*'
|
||||||
|
},
|
||||||
|
/** 单选或多选均按当前字段 fileType 校验;未配置类型时沿用允许任意类型的规则。 */
|
||||||
|
fileBeforeRead(files, column) {
|
||||||
|
const types = Array.isArray(column.fileType) ? column.fileType.map((type) => type.toLowerCase().replace(/^\./, '')) : []
|
||||||
|
const valid = (Array.isArray(files) ? files : [files]).every((file) => {
|
||||||
|
const extension = file.name.indexOf('.') > -1 ? file.name.split('.').pop().toLowerCase() : ''
|
||||||
|
return types.length === 0 || types.some((type) => type === extension || type === file.type
|
||||||
|
|| type.slice(-2) === '/*' && file.type.indexOf(type.slice(0, -1)) === 0)
|
||||||
|
})
|
||||||
|
if (!valid) {
|
||||||
|
this.$toast('请上传' + types.join('/') + '格式的附件')
|
||||||
|
}
|
||||||
|
return valid
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Vant 回调参数可能是单文件或数组;每个文件分别上传,只有服务端返回的 Sys_file 进入 columnValue。
|
||||||
|
* /file_server/uploadFile 接收 FormData.file,返回 Result.data={id,filename,filepath,...};失败项可删除后重选。
|
||||||
|
*/
|
||||||
|
fileAfterRead(files, column) {
|
||||||
|
return Promise.all((Array.isArray(files) ? files : [files]).map((item) => {
|
||||||
|
this.$set(item, 'status', 'uploading')
|
||||||
|
this.$set(item, 'message', '上传中...')
|
||||||
|
const data = new FormData()
|
||||||
|
data.append('file', item.file, item.file.name)
|
||||||
|
return this.$axios.post('/file_server/uploadFile', data, {
|
||||||
|
headers: {'Content-Type': 'multipart/form-data'}
|
||||||
|
}).then((res) => {
|
||||||
|
if (res.code !== 0 || !res.data || !res.data.id) {
|
||||||
|
throw new Error(res.msg || '上传失败')
|
||||||
|
}
|
||||||
|
// 上传期间已删除的预览项不再加入业务附件,也不删除服务端文件。
|
||||||
|
if ((this.fileLists[column.columnCode] || []).indexOf(item) === -1) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const preview = this.buildFilePreview(res.data)
|
||||||
|
Object.keys(preview).forEach((key) => this.$set(item, key, preview[key]))
|
||||||
|
this.$delete(item, 'file')
|
||||||
|
this.$delete(item, 'content')
|
||||||
|
this.syncFileColumnValue(column)
|
||||||
|
}).catch((err) => {
|
||||||
|
this.$set(item, 'status', 'failed')
|
||||||
|
this.$set(item, 'message', '上传失败,请删除后重试')
|
||||||
|
this.$toast(err.message || '上传失败,请稍后重试')
|
||||||
|
}).finally(() => {
|
||||||
|
if (item.status === 'uploading') {
|
||||||
|
this.$set(item, 'status', 'failed')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
},
|
||||||
|
/** 删除仅移除当前业务引用;失败、上传中的本地项永远不进入待保存的附件值。 */
|
||||||
|
syncFileColumnValue(column) {
|
||||||
|
const values = (this.fileLists[column.columnCode] || [])
|
||||||
|
.filter((item) => item.data && item.status !== 'uploading' && item.status !== 'failed')
|
||||||
|
.map((item) => item.data)
|
||||||
|
this.$set(this.fileValueSnapshots, column.columnCode, JSON.stringify(values))
|
||||||
|
this.$set(column, 'columnValue', values)
|
||||||
|
},
|
||||||
|
previewFile(item) {
|
||||||
|
if (!item.data || !item.url) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (item.isImage) {
|
||||||
|
// 图片预览位于表单之上,也作为独立历史层,返回时先关预览再关表单。
|
||||||
|
if (!this.popupHistory) {
|
||||||
|
vant.ImagePreview([item.url])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.popupHistory.open('imagePreview')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(this, 'imagePreview', vant.ImagePreview({
|
||||||
|
images: [item.url],
|
||||||
|
closeOnPopstate: false,
|
||||||
|
onClose: () => this.popupHistory.close('imagePreview')
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
window.open(item.url, '_blank')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (window.mobilePopupHistory) {
|
||||||
|
this.$set(this, 'popupHistory', window.mobilePopupHistory.register('brand-train-form-' + this._uid, (key, visible) => {
|
||||||
|
if (key === 'imagePreview') {
|
||||||
|
if (!visible && this.imagePreview) {
|
||||||
|
this.imagePreview.close()
|
||||||
|
this.$set(this, 'imagePreview', null)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.$set(this.pickerStates, key, visible)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
// console.log(this.dynamicData)
|
||||||
|
// this.dynamicData.forEach(v => {
|
||||||
|
// if (v.columnFormType === 'SELECT') {
|
||||||
|
// v.columnPickerName = v.columnCode+'Picker'
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// console.log(this.dynamicData)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.imagePreview) {
|
||||||
|
this.imagePreview.close()
|
||||||
|
}
|
||||||
|
if (this.popupHistory) {
|
||||||
|
this.popupHistory.unregister()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
@@ -70,6 +70,8 @@
|
|||||||
<!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/wangeditor@4.6.15/dist/wangEditor.min.js"></script>-->
|
<!-- <script type="text/javascript" src="https://cdn.jsdelivr.net/npm/wangeditor@4.6.15/dist/wangEditor.min.js"></script>-->
|
||||||
<script type="text/javascript"
|
<script type="text/javascript"
|
||||||
src="https://webapi.amap.com/maps?v=1.4.15&key=b15158fab2e62a38638ffe27b45cfc6d&plugin=AMap.PolyEditor,AMap.PlaceSearch"></script>
|
src="https://webapi.amap.com/maps?v=1.4.15&key=b15158fab2e62a38638ffe27b45cfc6d&plugin=AMap.PolyEditor,AMap.PlaceSearch"></script>
|
||||||
|
<!-- 参考工程在公共布局中静态引入腾讯地图,供活动选点及 PJAX 进入后直接使用。 -->
|
||||||
|
<script src="https://map.qq.com/api/gljs?v=2.exp&key=NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU"></script>
|
||||||
|
|
||||||
<!--viewer-->
|
<!--viewer-->
|
||||||
<script src="${base!}/assets/platform/plugins/viewer/viewer.min.js"></script>
|
<script src="${base!}/assets/platform/plugins/viewer/viewer.min.js"></script>
|
||||||
|
|||||||
@@ -226,7 +226,17 @@
|
|||||||
|
|
||||||
<!--vuex相关-->
|
<!--vuex相关-->
|
||||||
<script>
|
<script>
|
||||||
const logonUser = ${json(@auth.getLogonUser())}
|
<!--#
|
||||||
|
var h5LogonUserJson = json(@h5UserService.getH5LogonUser());
|
||||||
|
h5LogonUserJson = strutil.replace(h5LogonUserJson, "<", "\\u003c");
|
||||||
|
h5LogonUserJson = strutil.replace(h5LogonUserJson, ">", "\\u003e");
|
||||||
|
h5LogonUserJson = strutil.replace(h5LogonUserJson, "&", "\\u0026");
|
||||||
|
h5LogonUserJson = strutil.replace(h5LogonUserJson, @java.lang.Character.toString(8232), "\\u2028");
|
||||||
|
h5LogonUserJson = strutil.replace(h5LogonUserJson, @java.lang.Character.toString(8233), "\\u2029");
|
||||||
|
#-->
|
||||||
|
// 仅输出服务筛选的用户字段、角色编码和权限标识,不将完整登录实体写入页面。
|
||||||
|
// JSON 中的 HTML 标记和 Unicode 行分隔符已转义,避免用户字段提前结束当前脚本。
|
||||||
|
const logonUser = ${h5LogonUserJson}
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
plugins: [createPersistedState({
|
plugins: [createPersistedState({
|
||||||
key: "zhgh-h5-vuex",
|
key: "zhgh-h5-vuex",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -285,6 +285,7 @@ layout("/mobile/platform.html"){
|
|||||||
未签到
|
未签到
|
||||||
</van-button>
|
</van-button>
|
||||||
<van-button type="primary" color="#1867b0" round size="mini"
|
<van-button type="primary" color="#1867b0" round size="mini"
|
||||||
|
:loading="formLoading" :disabled="formLoading"
|
||||||
v-if="(item.isAttend === false && findTime(item) === true && item.state === 1)
|
v-if="(item.isAttend === false && findTime(item) === true && item.state === 1)
|
||||||
|| (moment().unix() > moment(item.courseEndTime).unix() && item.state === 3 && item.isAttend === false)"
|
|| (moment().unix() > moment(item.courseEndTime).unix() && item.state === 3 && item.isAttend === false)"
|
||||||
style="position: absolute; right: 0; top: 25%; width: 60px; height: 25px"
|
style="position: absolute; right: 0; top: 25%; width: 60px; height: 25px"
|
||||||
@@ -358,6 +359,7 @@ layout("/mobile/platform.html"){
|
|||||||
assortList: [],
|
assortList: [],
|
||||||
allColumnsData: [],
|
allColumnsData: [],
|
||||||
dynamicColumnsData: [],
|
dynamicColumnsData: [],
|
||||||
|
formLoading: false,
|
||||||
trainType: '培训班',
|
trainType: '培训班',
|
||||||
trainTypeList: [],
|
trainTypeList: [],
|
||||||
actionShow: false,
|
actionShow: false,
|
||||||
@@ -448,27 +450,51 @@ layout("/mobile/platform.html"){
|
|||||||
return day.isAttend ? 2 : 1
|
return day.isAttend ? 2 : 1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async signIn(o) {
|
signIn(o) {
|
||||||
|
if (this.formLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (o.signType === 1) {
|
if (o.signType === 1) {
|
||||||
//调相机扫一扫
|
if (this.formLoading) {
|
||||||
wxScannerQrCode.scanner().then(async res => {
|
return
|
||||||
if (res) {
|
}
|
||||||
const course = JSON.parse(res)
|
// 新点位二维码为本站签到页地址;旧课程二维码仍为 {courseId} JSON,并沿用原签到接口。
|
||||||
const resp = await $.post('/platform/mobile/brandTrainSignUpActivity/codeSign', {
|
wxScannerQrCode.scanner().then((res) => {
|
||||||
codeCourseId: course.courseId,
|
if (!res) {
|
||||||
clickCourseId: o.courseId,
|
return
|
||||||
id: o.id,
|
|
||||||
})
|
|
||||||
if(resp.code === 0) {
|
|
||||||
vant.Dialog.alert({message: '签到成功'})
|
|
||||||
await this.pageData()
|
|
||||||
o.isAttend = true
|
|
||||||
} else {
|
|
||||||
vant.Dialog.alert({message: resp.msg})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}).catch(err => {
|
const qrContent = String(res).trim()
|
||||||
vant.Dialog.alert({message: err})
|
let course
|
||||||
|
try {
|
||||||
|
if (qrContent.charAt(0) === '{') {
|
||||||
|
course = JSON.parse(qrContent)
|
||||||
|
} else {
|
||||||
|
const signUrl = new URL(qrContent, window.location.origin)
|
||||||
|
const courseId = signUrl.searchParams.get('courseId')
|
||||||
|
// 只接受本站品牌活动签到页,且课程必须与当前点击的签到记录一致,避免任意地址跳转。
|
||||||
|
if (signUrl.origin !== window.location.origin || signUrl.pathname !== '/platform/mobile/brandTrain/scanCode') {
|
||||||
|
vant.Dialog.alert({message: '请扫描当前品牌活动课程的签到二维码'})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!courseId || courseId !== o.courseId) {
|
||||||
|
vant.Dialog.alert({message: '签到失败,二维码与您当前签到信息不符'})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
window.location.href = signUrl.origin + signUrl.pathname + '?courseId=' + encodeURIComponent(courseId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
vant.Dialog.alert({message: '二维码格式不正确,请重新扫描'})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!course || !course.courseId || course.courseId !== o.courseId) {
|
||||||
|
vant.Dialog.alert({message: '签到失败,二维码与您当前签到信息不符'})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 旧二维码仍使用原接口,但必须提交本次定位,服务端按课程配置半径校验。
|
||||||
|
this.submitLocatedSign(o, course.courseId)
|
||||||
|
}).catch((err) => {
|
||||||
|
vant.Dialog.alert({message: typeof err === 'string' ? err : '签到失败,请稍后重试'})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if (o.signType === 2) {
|
if (o.signType === 2) {
|
||||||
@@ -489,35 +515,86 @@ layout("/mobile/platform.html"){
|
|||||||
vant.Toast('请在规定时间签到')
|
vant.Toast('请在规定时间签到')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const that = this
|
// 范围判断交给服务端,避免浏览器地图距离取整后误放行边界外的点。
|
||||||
geolocation.getCurrentPosition(async function (status, result) {
|
return this.submitLocatedSign(o)
|
||||||
if (status === 'complete') {
|
|
||||||
locations = [result.position.getLng(), result.position.getLat()]
|
|
||||||
//用户当前位置
|
|
||||||
const userPoint = new AMap.LngLat(result.position.getLng(), result.position.getLat())
|
|
||||||
//课程位置
|
|
||||||
const coursePoint = new AMap.LngLat(that.course.courseLocationCoordinates[0], that.course.courseLocationCoordinates[1])
|
|
||||||
const distance = Math.round(userPoint.distance(coursePoint))
|
|
||||||
if (distance > 250) {
|
|
||||||
vant.Toast('请在250米内签到')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const array = [result.position.getLng(), result.position.getLat()]
|
|
||||||
const resp = await $.post('/platform/mobile/brandTrainSignUpActivity/doQd', {
|
|
||||||
id: o.id,
|
|
||||||
courseId: that.course.id,
|
|
||||||
point: JSON.stringify(array),
|
|
||||||
})
|
|
||||||
if (resp.code === 0) {
|
|
||||||
await that.pageData()
|
|
||||||
}
|
|
||||||
vant.Toast(resp.msg)
|
|
||||||
that.popupShow = false
|
|
||||||
} else {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 无参数;返回jQuery Promise,成功值为本次GCJ-02定位的[经度,纬度]。
|
||||||
|
* 每次签到重新定位,扫码入口无需先打开GPS地图;超时或定位失败不发送签到请求。
|
||||||
|
*/
|
||||||
|
getSignPosition() {
|
||||||
|
const deferred = $.Deferred()
|
||||||
|
const timer = window.setTimeout(() => deferred.reject('定位超时,请重试'), 12000)
|
||||||
|
deferred.always(() => window.clearTimeout(timer))
|
||||||
|
try {
|
||||||
|
if (!window.AMap || !AMap.plugin) {
|
||||||
|
deferred.reject('定位服务未加载,请刷新页面重试')
|
||||||
|
return deferred.promise()
|
||||||
|
}
|
||||||
|
AMap.plugin('AMap.Geolocation', () => {
|
||||||
|
if (deferred.state() !== 'pending') return
|
||||||
|
try {
|
||||||
|
const locator = new AMap.Geolocation({enableHighAccuracy: true, timeout: 10000, convert: true})
|
||||||
|
locator.getCurrentPosition((status, result) => {
|
||||||
|
if (status !== 'complete' || !result || !result.position) {
|
||||||
|
deferred.reject('定位失败,请开启定位权限后重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const coordinates = [result.position.getLng(), result.position.getLat()]
|
||||||
|
if (!coordinates.every(value => Number.isFinite(value))
|
||||||
|
|| coordinates[0] < -180 || coordinates[0] > 180
|
||||||
|
|| coordinates[1] < -90 || coordinates[1] > 90) {
|
||||||
|
deferred.reject('定位坐标无效,请重新定位')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
deferred.resolve(coordinates)
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
deferred.reject('定位服务不可用,请刷新页面重试')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
deferred.reject('定位服务不可用,请刷新页面重试')
|
||||||
|
}
|
||||||
|
return deferred.promise()
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* o为当前用户的课次签到记录;codeCourseId为旧二维码课程ID,缺省时走GPS入口。
|
||||||
|
* 返回jQuery Promise;响应code=0成功,msg为提示,data为空,半径始终由服务端读取。
|
||||||
|
*/
|
||||||
|
submitLocatedSign(o, codeCourseId) {
|
||||||
|
if (this.formLoading) return
|
||||||
|
this.$set(this, 'formLoading', true)
|
||||||
|
return this.getSignPosition().then(coordinates => {
|
||||||
|
if (codeCourseId) {
|
||||||
|
return $.post('/platform/mobile/brandTrainSignUpActivity/codeSign', {
|
||||||
|
id: o.id, codeCourseId: codeCourseId, clickCourseId: o.courseId,
|
||||||
|
longitude: coordinates[0], latitude: coordinates[1]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return $.post('/platform/mobile/brandTrainSignUpActivity/doQd', {
|
||||||
|
id: o.id, courseId: o.courseId, point: JSON.stringify(coordinates)
|
||||||
|
})
|
||||||
|
}).then(resp => {
|
||||||
|
if (resp.code === 0) {
|
||||||
|
this.$set(o, 'isAttend', true)
|
||||||
|
this.pageData()
|
||||||
|
}
|
||||||
|
vant.Toast(resp.msg || (resp.code === 0 ? '签到成功' : '签到失败'))
|
||||||
|
if (!codeCourseId) {
|
||||||
|
this.$set(this, 'popupShow', false)
|
||||||
|
}
|
||||||
|
}, err => {
|
||||||
|
vant.Toast(typeof err === 'string' ? err : '签到失败,请稍后重试')
|
||||||
|
}).always(() => {
|
||||||
|
this.$set(this, 'formLoading', false)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// course为课程对象;旧数据无半径时沿用250米,与服务端默认值保持一致。
|
||||||
|
getSignRadius(course) {
|
||||||
|
return course.signRadius == null ? 250 : Number(course.signRadius)
|
||||||
|
},
|
||||||
//获取当前坐标点
|
//获取当前坐标点
|
||||||
getPoint() {
|
getPoint() {
|
||||||
/*return new Promise((resolve, reject) => {
|
/*return new Promise((resolve, reject) => {
|
||||||
@@ -705,7 +782,7 @@ layout("/mobile/platform.html"){
|
|||||||
|
|
||||||
var circle = new AMap.Circle({
|
var circle = new AMap.Circle({
|
||||||
center: new AMap.LngLat(o.courseLocationCoordinates[0], o.courseLocationCoordinates[1]), // 圆心位置
|
center: new AMap.LngLat(o.courseLocationCoordinates[0], o.courseLocationCoordinates[1]), // 圆心位置
|
||||||
radius: 250, // 圆半径
|
radius: this.getSignRadius(o), // 当前课程配置的签到半径,单位为米
|
||||||
borderWeight: 1,
|
borderWeight: 1,
|
||||||
strokeOpacity: 1,
|
strokeOpacity: 1,
|
||||||
fillOpacity: 0.4,
|
fillOpacity: 0.4,
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ layout("/mobile/platform.html"){
|
|||||||
<van-sticky>
|
<van-sticky>
|
||||||
<van-nav-bar title="扫描二维码"
|
<van-nav-bar title="扫描二维码"
|
||||||
left-arrow
|
left-arrow
|
||||||
|
@click-left="history.back()"
|
||||||
></van-nav-bar>
|
></van-nav-bar>
|
||||||
</van-sticky>
|
</van-sticky>
|
||||||
|
|
||||||
|
|||||||
@@ -186,6 +186,16 @@ layout("/layouts/platform.html"){
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 二维码弹框独立于课程列表,关闭后可继续选择其他课程。 -->
|
||||||
|
<el-dialog title="签到二维码" :visible.sync="courseCodeDialog" :close-on-click-modal="false" width="40%" append-to-body>
|
||||||
|
<div style="width: 100%;">
|
||||||
|
<img :src="courseCodeImage" alt="签到二维码" style="display: block; width: 70%; margin: auto;">
|
||||||
|
</div>
|
||||||
|
<span slot="footer" class="dialog-footer">
|
||||||
|
<el-button @click="closeCourseCode" type="primary">关 闭</el-button>
|
||||||
|
</span>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<send-msg ref="sendMsg" :url="url"></send-msg>
|
<send-msg ref="sendMsg" :url="url"></send-msg>
|
||||||
<open-qr-code ref="openQRCode" :url="url"></open-qr-code>
|
<open-qr-code ref="openQRCode" :url="url"></open-qr-code>
|
||||||
|
|
||||||
@@ -203,8 +213,12 @@ layout("/layouts/platform.html"){
|
|||||||
return {
|
return {
|
||||||
url: '',
|
url: '',
|
||||||
courseDialog: false,
|
courseDialog: false,
|
||||||
|
courseCodeDialog: false,
|
||||||
|
courseCodeImage: '',
|
||||||
trainType: '',
|
trainType: '',
|
||||||
trainTypeList: [],
|
trainTypeList: [],
|
||||||
|
welfareProjectList: [],
|
||||||
|
welfareProjectLoading: false,
|
||||||
campusList: [],
|
campusList: [],
|
||||||
tableColumns: [
|
tableColumns: [
|
||||||
{label: '活动名称', prop: 'activityName'},
|
{label: '活动名称', prop: 'activityName'},
|
||||||
@@ -238,7 +252,8 @@ layout("/layouts/platform.html"){
|
|||||||
this.$refs.openQRCode.openCode("/platform/mobile/brandTrainSignUpActivity/trainList")
|
this.$refs.openQRCode.openCode("/platform/mobile/brandTrainSignUpActivity/trainList")
|
||||||
},
|
},
|
||||||
makeCode(row) {
|
makeCode(row) {
|
||||||
this.clickRow = row
|
// 保留活动级配置,生成课程二维码时据此区分普通签到与签到扩展。
|
||||||
|
this.$set(this, 'clickRow', row)
|
||||||
const type = this.trainTypeList.find(o => o.code === row.trainType)
|
const type = this.trainTypeList.find(o => o.code === row.trainType)
|
||||||
this.trainType = type ? type.name : '培训班'
|
this.trainType = type ? type.name : '培训班'
|
||||||
this.courseDialog = true
|
this.courseDialog = true
|
||||||
@@ -247,12 +262,44 @@ layout("/layouts/platform.html"){
|
|||||||
const o = {
|
const o = {
|
||||||
courseId: row.id
|
courseId: row.id
|
||||||
}
|
}
|
||||||
// 生成二维码的 base64 编码
|
// 未启用扩展的品牌活动保留原 JSON 二维码;扩展活动进入手机端达标处理页面。
|
||||||
const content = jrQrcode.getQrBase64(JSON.stringify(o))
|
const qrText = this.isSignExtensionEnabled(this.clickRow)
|
||||||
let image = new Image()
|
? window.location.origin + '/platform/mobile/brandTrain/scanCode?courseId=' + encodeURIComponent(row.id)
|
||||||
image.src = content
|
: JSON.stringify(o)
|
||||||
let viewer = new Viewer(image)
|
// 每次先更新当前课程的二维码图片,再打开弹框,避免切换课程时显示上一张二维码。
|
||||||
viewer.show()
|
this.$set(this, 'courseCodeImage', jrQrcode.getQrBase64(qrText))
|
||||||
|
this.$set(this, 'courseCodeDialog', true)
|
||||||
|
},
|
||||||
|
// 关闭二维码时保留课程列表,便于继续生成其他课程的签到二维码。
|
||||||
|
closeCourseCode() {
|
||||||
|
this.$set(this, 'courseCodeDialog', false)
|
||||||
|
},
|
||||||
|
// 只有全部课程签到且配置了正整数点位数的活动,才启用手机端扩展流程。
|
||||||
|
isSignExtensionEnabled(activity) {
|
||||||
|
return activity && Number.isInteger(activity.signPointCount) && activity.signPointCount > 0
|
||||||
|
&& Array.isArray(activity.courseList) && activity.courseList.length > 0
|
||||||
|
&& activity.courseList.every(item => item.isMobileSign === true)
|
||||||
|
},
|
||||||
|
// type 为活动签到处理方式编码,历史未配置时按无处理显示。
|
||||||
|
getActivitySignHandleTypeName(type) {
|
||||||
|
const typeNames = {1: '系统自动弹出填写信息', 2: '系统自动选择礼物或福利', 3: '无'}
|
||||||
|
return typeNames[type] || '无'
|
||||||
|
},
|
||||||
|
// welfareProjectId 为已关联项目 ID,项目失效不在候选列表时仍展示原 ID。
|
||||||
|
getWelfareProjectName(welfareProjectId) {
|
||||||
|
const project = this.welfareProjectList.find(item => item.id === welfareProjectId)
|
||||||
|
return project ? project.name : (welfareProjectId || '-')
|
||||||
|
},
|
||||||
|
// 无请求参数;响应 data 为含 id、name 的福利项目数组,供查看页解析项目名称。
|
||||||
|
getAvailableWelfareProjectList() {
|
||||||
|
this.welfareProjectLoading = true
|
||||||
|
return $.post('/platform/brandTrainSingUp/manage/activity/getAvailableWelfareProjectList', {})
|
||||||
|
.then(resp => {
|
||||||
|
this.$set(this, 'welfareProjectList', resp.code === 0 ? resp.data : [])
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
this.welfareProjectLoading = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
openAdd() {
|
openAdd() {
|
||||||
sublime.jumpPagePjax('/platform/brandTrainSingUp/manage/activity/add')
|
sublime.jumpPagePjax('/platform/brandTrainSingUp/manage/activity/add')
|
||||||
@@ -346,6 +393,7 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
this.getAvailableWelfareProjectList()
|
||||||
this.pageData()
|
this.pageData()
|
||||||
this.trainTypeList = await getDictOptions("trainSignUpType")
|
this.trainTypeList = await getDictOptions("trainSignUpType")
|
||||||
this.campusList = await getCampus()
|
this.campusList = await getCampus()
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ layout("/layouts/platform.html"){
|
|||||||
<script>
|
<script>
|
||||||
let pointX = '${config.AppMapCenterPointX!}'
|
let pointX = '${config.AppMapCenterPointX!}'
|
||||||
let pointY = '${config.AppMapCenterPointY!}'
|
let pointY = '${config.AppMapCenterPointY!}'
|
||||||
let map = null
|
// SDK 图对象保留在脚本作用域,避免 Vue 递归监听其图层与内部状态。
|
||||||
let marker = null
|
let brandTrainSignMapInstance = null
|
||||||
let geocoder = null
|
let brandTrainSignMarkerLayer = null
|
||||||
let E = window.wangEditor
|
let E = window.wangEditor
|
||||||
const vue = new Vue({
|
const vue = new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
@@ -49,14 +49,20 @@ layout("/layouts/platform.html"){
|
|||||||
use: false,
|
use: false,
|
||||||
historicalActList: [],
|
historicalActList: [],
|
||||||
historicalAct: '',
|
historicalAct: '',
|
||||||
|
welfareProjectList: [],
|
||||||
|
welfareProjectLoading: false,
|
||||||
|
formLoading: false,
|
||||||
unionList: [],
|
unionList: [],
|
||||||
trainType: '培训班',
|
trainType: '培训班',
|
||||||
trainTypeList: [],
|
trainTypeList: [],
|
||||||
fromEdit: '',
|
fromEdit: '',
|
||||||
formData: {
|
formData: {
|
||||||
notice: false,
|
notice: false,
|
||||||
|
signPointCount: null,
|
||||||
|
signHandleType: 3,
|
||||||
|
welfareProjectId: '',
|
||||||
courseList: [
|
courseList: [
|
||||||
{orderNum: 1, courseName: '', courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1},
|
{orderNum: 1, courseName: '', courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, signRadius: 250},
|
||||||
],
|
],
|
||||||
conditionStructure: {
|
conditionStructure: {
|
||||||
method: "AND",
|
method: "AND",
|
||||||
@@ -83,6 +89,13 @@ layout("/layouts/platform.html"){
|
|||||||
setUpUnionLimitDialog: false,
|
setUpUnionLimitDialog: false,
|
||||||
viewMapDialog: false,
|
viewMapDialog: false,
|
||||||
mapDialog: false,
|
mapDialog: false,
|
||||||
|
mapOpened: false,
|
||||||
|
mapLoading: false,
|
||||||
|
mapError: '',
|
||||||
|
signMapReady: false,
|
||||||
|
mapRequestVersion: 0,
|
||||||
|
mapSearchVersion: 0,
|
||||||
|
mapSearchCallback: null,
|
||||||
courseTypeList: [],
|
courseTypeList: [],
|
||||||
campusList: [],
|
campusList: [],
|
||||||
courseTimeOneKeySet: {
|
courseTimeOneKeySet: {
|
||||||
@@ -107,7 +120,6 @@ layout("/layouts/platform.html"){
|
|||||||
moreInfoDrawer: false,
|
moreInfoDrawer: false,
|
||||||
moreInfoRow: {},
|
moreInfoRow: {},
|
||||||
moreInfoIndex: 0,
|
moreInfoIndex: 0,
|
||||||
placeSearchComponent: null,
|
|
||||||
addressValue: '',
|
addressValue: '',
|
||||||
|
|
||||||
courseBzDialog: false,
|
courseBzDialog: false,
|
||||||
@@ -115,6 +127,11 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
// 活动内全部课程均启用手机签到后,才允许设置活动级达标处理。
|
||||||
|
allCourseSignEnabled() {
|
||||||
|
return Array.isArray(this.formData.courseList) && this.formData.courseList.length > 0
|
||||||
|
&& this.formData.courseList.every(item => item.isMobileSign === true)
|
||||||
|
},
|
||||||
unionUserNumCalcTips() {
|
unionUserNumCalcTips() {
|
||||||
return this.unionUserNumCalc.map(v => {
|
return this.unionUserNumCalc.map(v => {
|
||||||
return (v.startNum ? v.startNum : '?') + '-' + (v.endNum ? v.endNum : '?') + '人的分工会,限报' + (v.resultNum ? v.resultNum : '?') + '人'
|
return (v.startNum ? v.startNum : '?') + '-' + (v.endNum ? v.endNum : '?') + '人的分工会,限报' + (v.resultNum ? v.resultNum : '?') + '人'
|
||||||
@@ -122,6 +139,45 @@ layout("/layouts/platform.html"){
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
// 旧活动缺少扩展字段时补齐默认值,兼容编辑与沿用历史活动。
|
||||||
|
initSignExtensionDefaults() {
|
||||||
|
// 编辑和沿用历史活动时,只为缺失半径补默认值,保留已有自定义范围。
|
||||||
|
this.formData.courseList.forEach(course => {
|
||||||
|
if (course.signRadius === undefined || course.signRadius === null) {
|
||||||
|
this.$set(course, 'signRadius', 250)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (this.formData.signPointCount === undefined || this.formData.signPointCount === null
|
||||||
|
|| this.formData.signPointCount === 0) {
|
||||||
|
this.$set(this.formData, 'signPointCount', null)
|
||||||
|
}
|
||||||
|
if (this.formData.signHandleType === undefined || this.formData.signHandleType === null) {
|
||||||
|
this.$set(this.formData, 'signHandleType', 3)
|
||||||
|
}
|
||||||
|
if (this.formData.welfareProjectId === undefined || this.formData.welfareProjectId === null) {
|
||||||
|
this.$set(this.formData, 'welfareProjectId', '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// val 为处理方式;只有方式 2 使用福利项目,切换后清除不再生效的项目。
|
||||||
|
handleActivitySignHandleTypeChange(val) {
|
||||||
|
if (val !== 2) {
|
||||||
|
this.$set(this.formData, 'welfareProjectId', '')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 无请求参数;返回 Promise,响应 data 为可选福利项目数组,每项包含 id 和 name。
|
||||||
|
* 加载期间禁用项目选择,避免将尚未加载的项目列表当作无可用项目。
|
||||||
|
*/
|
||||||
|
getAvailableWelfareProjectList() {
|
||||||
|
this.welfareProjectLoading = true
|
||||||
|
return $.post('/platform/brandTrainSingUp/manage/activity/getAvailableWelfareProjectList', {})
|
||||||
|
.then(resp => {
|
||||||
|
this.$set(this, 'welfareProjectList', resp.code === 0 ? resp.data : [])
|
||||||
|
})
|
||||||
|
.always(() => {
|
||||||
|
this.welfareProjectLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
openCourseBz(row, index) {
|
openCourseBz(row, index) {
|
||||||
this.moreInfoRow = row
|
this.moreInfoRow = row
|
||||||
this.courseBzDialog = true
|
this.courseBzDialog = true
|
||||||
@@ -177,7 +233,7 @@ layout("/layouts/platform.html"){
|
|||||||
o.id = ''
|
o.id = ''
|
||||||
this.formData.courseList.push(o)
|
this.formData.courseList.push(o)
|
||||||
} else {
|
} else {
|
||||||
this.formData.courseList.push({orderNum: index, courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1})
|
this.formData.courseList.push({orderNum: index, courseTimeList: [], isMobileSign: false, isReceiveGift: false, reserveMode: 1, signRadius: 250})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
//删除培训班
|
//删除培训班
|
||||||
@@ -311,55 +367,139 @@ layout("/layouts/platform.html"){
|
|||||||
this.setUpUnionLimitDialog = false
|
this.setUpUnionLimitDialog = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
//设置签到点位
|
/**
|
||||||
|
* coordinates 为 [经度, 纬度] 数值数组;返回布尔值,表示两项坐标有限且位于合法经纬度范围。
|
||||||
|
* 扫码签到保存、提交与地图回显共用校验,避免缺失点位或将默认地图中心作为签到坐标。
|
||||||
|
*/
|
||||||
|
isValidSignCoordinates(coordinates) {
|
||||||
|
return Array.isArray(coordinates) && coordinates.length === 2
|
||||||
|
&& coordinates.every(value => typeof value === 'number' && Number.isFinite(value))
|
||||||
|
&& coordinates[0] >= -180 && coordinates[0] <= 180
|
||||||
|
&& coordinates[1] >= -90 && coordinates[1] <= 90
|
||||||
|
},
|
||||||
|
// index 为当前课程行;维持选点即写入的原规则,关闭弹框不会回滚已选坐标。
|
||||||
openMap(index) {
|
openMap(index) {
|
||||||
this.mapIndex = index
|
this.destroySignMap()
|
||||||
this.mapDialog = true
|
this.$set(this, 'mapIndex', index)
|
||||||
|
this.$set(this, 'addressValue', '')
|
||||||
|
this.$set(this, 'mapError', '')
|
||||||
|
this.$set(this, 'mapDialog', true)
|
||||||
|
},
|
||||||
|
// 等弹框显示动画结束再初始化,避免在隐藏容器中创建地图或重复打开后保留旧实例。
|
||||||
|
onMapOpened() {
|
||||||
|
if (!this.mapDialog) return
|
||||||
|
this.$set(this, 'mapOpened', true)
|
||||||
|
this.$set(this, 'mapRequestVersion', this.mapRequestVersion + 1)
|
||||||
|
const version = this.mapRequestVersion
|
||||||
|
this.$set(this, 'mapLoading', true)
|
||||||
|
this.$set(this, 'mapError', '')
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
map = new AMap.Map("signMap", {
|
if (!this.mapDialog || !this.mapOpened || version !== this.mapRequestVersion) return
|
||||||
resizeEnable: true,
|
try {
|
||||||
center: [pointX, pointY],
|
// SDK 由公共布局按参考工程静态加载;未就绪时提示刷新,不再自行创建脚本。
|
||||||
zoom: 16
|
if (!window.TMap || !TMap.Map || !TMap.LatLng || !TMap.MultiMarker) {
|
||||||
})
|
throw new Error('腾讯地图脚本未加载成功,请检查网络后刷新页面')
|
||||||
this.placeSearchComponent = new AMap.PlaceSearch({
|
}
|
||||||
// city 指定搜索所在城市,支持传入格式有:城市名、citycode和adcode
|
const coordinates = this.formData.courseList[this.mapIndex].courseLocationCoordinates
|
||||||
city: ''
|
const selectedPoint = this.isValidSignCoordinates(coordinates) ? coordinates : null
|
||||||
})
|
const defaultPoint = [Number(pointX), Number(pointY)]
|
||||||
const coordinateArray = this.formData.courseList[this.mapIndex].courseLocationCoordinates
|
const centerPoint = selectedPoint || (String(pointX).trim() && String(pointY).trim()
|
||||||
this.clearMarker()
|
&& this.isValidSignCoordinates(defaultPoint) ? defaultPoint : null)
|
||||||
this.createMarker(coordinateArray)
|
if (!centerPoint) throw new Error('地图默认中心未配置或无效,请检查 AppMapCenterPointX(经度)、AppMapCenterPointY(纬度)')
|
||||||
map.on('click', (e) => {
|
const mapInstance = new TMap.Map('brandTrainSignMap', {
|
||||||
this.clearMarker()
|
zoom: 16,
|
||||||
const position = [e.lnglat.getLng(), e.lnglat.getLat()]
|
center: new TMap.LatLng(centerPoint[1], centerPoint[0])
|
||||||
this.createMarker(position)
|
|
||||||
this.formData.courseList[this.mapIndex].courseLocationCoordinates = position
|
|
||||||
})
|
|
||||||
})
|
|
||||||
},
|
|
||||||
handleSelect(item) {
|
|
||||||
if(item.location) {
|
|
||||||
//定位到中心点
|
|
||||||
map.setCenter(item.location);
|
|
||||||
//TODO 获取数据,对数据进行操作如:添加marker等
|
|
||||||
this.clearMarker()
|
|
||||||
this.createMarker(item.location)
|
|
||||||
this.formData.courseList[this.mapIndex].courseLocationCoordinates = [item.location.lng, item.location.lat]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
querySearchAsync(queryString, cb) {
|
|
||||||
if(!queryString) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.placeSearchComponent.search(queryString, function (status, result) {
|
|
||||||
// 查询成功时,result即对应匹配的POI信息
|
|
||||||
if(status !== 'error') {
|
|
||||||
result.poiList.pois.forEach(item => {
|
|
||||||
item.value = item.name + '【详细地址:' + item.address + '】'
|
|
||||||
})
|
})
|
||||||
cb(result.poiList.pois)
|
brandTrainSignMapInstance = mapInstance
|
||||||
|
brandTrainSignMarkerLayer = new TMap.MultiMarker({map: mapInstance, geometries: []})
|
||||||
|
this.$set(this, 'signMapReady', true)
|
||||||
|
// 默认中心仅供浏览,只有已配置点位才回显标记,不把默认中心写回课程。
|
||||||
|
if (selectedPoint) this.createMarker(selectedPoint)
|
||||||
|
mapInstance.on('click', (event) => {
|
||||||
|
if (version !== this.mapRequestVersion || !this.mapDialog || !this.mapOpened) return
|
||||||
|
this.selectSignMapPosition([event.latLng.getLng(), event.latLng.getLat()])
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
if (version !== this.mapRequestVersion || !this.mapDialog) return
|
||||||
|
this.$set(this, 'mapError', error && error.message ? error.message : '地图初始化失败,请重新打开地图重试')
|
||||||
|
this.destroySignMap()
|
||||||
|
} finally {
|
||||||
|
if (version === this.mapRequestVersion) this.$set(this, 'mapLoading', false)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
closeMapDialog() {
|
||||||
|
this.$set(this, 'mapDialog', false)
|
||||||
|
this.onMapClosed()
|
||||||
|
},
|
||||||
|
onMapClosed() {
|
||||||
|
this.destroySignMap()
|
||||||
|
},
|
||||||
|
// 关闭、初始化失败或 PJAX 离开时使旧响应失效,并完成待处理的自动补全回调后释放地图。
|
||||||
|
destroySignMap() {
|
||||||
|
this.$set(this, 'mapOpened', false)
|
||||||
|
this.$set(this, 'mapLoading', false)
|
||||||
|
this.$set(this, 'mapRequestVersion', this.mapRequestVersion + 1)
|
||||||
|
this.$set(this, 'mapSearchVersion', this.mapSearchVersion + 1)
|
||||||
|
if (this.mapSearchCallback) this.mapSearchCallback([])
|
||||||
|
const mapInstance = brandTrainSignMapInstance
|
||||||
|
brandTrainSignMapInstance = null
|
||||||
|
brandTrainSignMarkerLayer = null
|
||||||
|
this.$set(this, 'signMapReady', false)
|
||||||
|
if (mapInstance) mapInstance.destroy()
|
||||||
|
},
|
||||||
|
// position 始终为 [经度, 纬度];仅调用腾讯 LatLng 构造方法时交换顺序。
|
||||||
|
selectSignMapPosition(position) {
|
||||||
|
if (!this.mapDialog || !this.mapOpened || !brandTrainSignMapInstance || !this.isValidSignCoordinates(position)) return
|
||||||
|
this.createMarker(position)
|
||||||
|
this.$set(this.formData.courseList[this.mapIndex], 'courseLocationCoordinates', position)
|
||||||
|
},
|
||||||
|
// item.location 提供搜索结果的 lng/lat;仅搜索选点移动地图中心,普通点击保持当前视野。
|
||||||
|
handleSelect(item) {
|
||||||
|
if (!item || !item.location || !this.mapDialog || !this.mapOpened || !brandTrainSignMapInstance) return
|
||||||
|
const position = [item.location.lng, item.location.lat]
|
||||||
|
if (!this.isValidSignCoordinates(position)) return
|
||||||
|
brandTrainSignMapInstance.setCenter(new window.TMap.LatLng(position[1], position[0]))
|
||||||
|
this.selectSignMapPosition(position)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* queryString 为地点关键词,POST 参数 keyword;响应 code/msg/data 中 data 为腾讯 status/message/data。
|
||||||
|
* cb 接收含 value/title/address/location 的候选项数组;空输入、请求失败及弹框关闭均以空数组结束回调。
|
||||||
|
*/
|
||||||
|
querySearchAsync(queryString, cb) {
|
||||||
|
if (this.mapSearchCallback) this.mapSearchCallback([])
|
||||||
|
this.$set(this, 'mapSearchVersion', this.mapSearchVersion + 1)
|
||||||
|
const version = this.mapSearchVersion
|
||||||
|
const keyword = typeof queryString === 'string' ? queryString.trim() : ''
|
||||||
|
let finished = false
|
||||||
|
const finish = (items) => {
|
||||||
|
if (finished) return
|
||||||
|
finished = true
|
||||||
|
cb(items)
|
||||||
|
if (this.mapSearchCallback === finish) this.$set(this, 'mapSearchCallback', null)
|
||||||
|
}
|
||||||
|
this.$set(this, 'mapSearchCallback', finish)
|
||||||
|
if (!keyword || !this.mapDialog || !this.mapOpened || !brandTrainSignMapInstance) {
|
||||||
|
finish([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let suggestions = []
|
||||||
|
return $.post('/platform/TMap/suggestion', {keyword})
|
||||||
|
.then((res) => {
|
||||||
|
if (version !== this.mapSearchVersion || !this.mapDialog || !this.mapOpened) return
|
||||||
|
if (res && res.code === 0 && res.data && res.data.status === 0 && Array.isArray(res.data.data)) {
|
||||||
|
suggestions = res.data.data.filter(item => item && item.location
|
||||||
|
&& this.isValidSignCoordinates([item.location.lng, item.location.lat]))
|
||||||
|
.map(item => Object.assign({}, item, {value: item.title + '【详细地址:' + (item.address || '') + '】'}))
|
||||||
|
} else {
|
||||||
|
this.$message.warning(res && res.data && res.data.message ? res.data.message : (res && res.msg) || '地点搜索失败')
|
||||||
|
}
|
||||||
|
}).fail(() => {
|
||||||
|
if (version === this.mapSearchVersion && this.mapDialog && this.mapOpened) this.$message.warning('地点搜索失败,请稍后重试')
|
||||||
|
}).always(() => {
|
||||||
|
finish(version === this.mapSearchVersion && this.mapDialog && this.mapOpened ? suggestions : [])
|
||||||
|
})
|
||||||
|
},
|
||||||
//删除一行培训时间记录
|
//删除一行培训时间记录
|
||||||
removeSetUpTableCourseRow(row, index) {
|
removeSetUpTableCourseRow(row, index) {
|
||||||
this.formData.courseList[this.courseIndex].courseTimeList.splice(index, 1)
|
this.formData.courseList[this.courseIndex].courseTimeList.splice(index, 1)
|
||||||
@@ -372,30 +512,35 @@ layout("/layouts/platform.html"){
|
|||||||
return courseDateArray.includes(moment(v).format('YYYY-MM-DD'))
|
return courseDateArray.includes(moment(v).format('YYYY-MM-DD'))
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
//清楚签到点位
|
// 单个图层只更新当前签到点,不移动地图视野;业务坐标仍为 [经度, 纬度]。
|
||||||
clearMarker() {
|
|
||||||
if (marker) {
|
|
||||||
map.remove(marker)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
//创建签到点位
|
|
||||||
createMarker(position) {
|
createMarker(position) {
|
||||||
marker = new AMap.Marker({
|
if (!brandTrainSignMarkerLayer || !brandTrainSignMapInstance || !this.isValidSignCoordinates(position)) return
|
||||||
position: position,
|
const center = new window.TMap.LatLng(position[1], position[0])
|
||||||
offset: new AMap.Pixel(-13, -30)
|
brandTrainSignMarkerLayer.setGeometries([{id: 'sign-point', position: center}])
|
||||||
})
|
|
||||||
map.add(marker)
|
|
||||||
this.getAddress(position)
|
|
||||||
map.setFitView(null, false, [150, 60, 100, 60]);
|
|
||||||
},
|
|
||||||
getAddress() {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
courseOrderNumChange() {
|
courseOrderNumChange() {
|
||||||
this.formData.courseList = this.formData.courseList.sort((a, b) => a.orderNum - b.orderNum)
|
this.formData.courseList = this.formData.courseList.sort((a, b) => a.orderNum - b.orderNum)
|
||||||
},
|
},
|
||||||
//提交
|
//提交
|
||||||
async doHandle(flag) {
|
async doHandle(flag) {
|
||||||
|
if (this.formLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 保存与提交都校验半径;上限与后端Integer字段保持一致,禁止清空后绕过必填校验。
|
||||||
|
const invalidRadiusIndex = this.formData.courseList.findIndex(item => item.isMobileSign === true
|
||||||
|
&& (item.signType === 1 || item.signType === 3)
|
||||||
|
&& (!Number.isInteger(item.signRadius) || item.signRadius <= 0 || item.signRadius > 2147483647))
|
||||||
|
if (invalidRadiusIndex !== -1) {
|
||||||
|
this.$message.warning('第' + (invalidRadiusIndex + 1) + '行' + this.trainType + '请填写有效的点位半径(正整数,单位:米)')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 已开启扫码签到的课程在保存与提交时均须配置真实点位;GPS仍沿用原提交校验。
|
||||||
|
const invalidScanCourseIndex = this.formData.courseList.findIndex(item => item.isMobileSign === true
|
||||||
|
&& item.signType === 1 && !this.isValidSignCoordinates(item.courseLocationCoordinates))
|
||||||
|
if (invalidScanCourseIndex !== -1) {
|
||||||
|
this.$message.warning('第' + (invalidScanCourseIndex + 1) + '行' + this.trainType + '请设置有效的扫码签到地点坐标')
|
||||||
|
return
|
||||||
|
}
|
||||||
if(flag === false) {
|
if(flag === false) {
|
||||||
let valid = false
|
let valid = false
|
||||||
this.$refs.form.validateField('activityName', (errMsg) => {
|
this.$refs.form.validateField('activityName', (errMsg) => {
|
||||||
@@ -448,7 +593,42 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 点位数按不同课程累计;填写信息复用课程类型字段,因此仅支持一种课程类型。
|
||||||
|
if (this.allCourseSignEnabled) {
|
||||||
|
const hasSignPointCount = this.formData.signPointCount !== undefined
|
||||||
|
&& this.formData.signPointCount !== null && this.formData.signPointCount !== ''
|
||||||
|
// 历史活动未配置点位且处理方式为无时,继续保留原签到方式。
|
||||||
|
if ((hasSignPointCount || this.formData.signHandleType !== 3)
|
||||||
|
&& (!Number.isInteger(this.formData.signPointCount) || this.formData.signPointCount <= 0
|
||||||
|
|| this.formData.signPointCount > this.formData.courseList.length)) {
|
||||||
|
this.$message.warning('签到点位数量必须为正整数,且不能超过当前课程数量')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (![1, 2, 3].includes(this.formData.signHandleType)) {
|
||||||
|
this.$message.warning('请选择签到处理方式')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (this.formData.signHandleType === 1) {
|
||||||
|
const courseTypes = new Set(this.formData.courseList.map(item => item.courseType).filter(item => item))
|
||||||
|
if (courseTypes.size !== 1 || this.formData.courseList.some(item => !item.courseType)) {
|
||||||
|
this.$message.warning('填写信息仅支持同一种课程类型,请检查课程类型')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.formData.signHandleType === 2 && !this.formData.welfareProjectId) {
|
||||||
|
this.$message.warning('请选择福利项目')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
const cloneData = clone(this.formData)
|
const cloneData = clone(this.formData)
|
||||||
|
// 任一课程关闭签到时不保存活动级扩展,避免隐藏配置继续触发手机端达标处理。
|
||||||
|
if (!this.allCourseSignEnabled) {
|
||||||
|
cloneData.signPointCount = null
|
||||||
|
cloneData.signHandleType = 3
|
||||||
|
cloneData.welfareProjectId = ''
|
||||||
|
} else if (cloneData.signHandleType !== 2) {
|
||||||
|
cloneData.welfareProjectId = ''
|
||||||
|
}
|
||||||
|
|
||||||
//cloneData.conditionStructure.conditions = cloneData.conditionStructure.conditions.filter(v => v.field && v.fieldObj && v.operational && v.value)
|
//cloneData.conditionStructure.conditions = cloneData.conditionStructure.conditions.filter(v => v.field && v.fieldObj && v.operational && v.value)
|
||||||
|
|
||||||
@@ -484,53 +664,70 @@ layout("/layouts/platform.html"){
|
|||||||
})
|
})
|
||||||
cloneData.typeLimits = limitNumArray
|
cloneData.typeLimits = limitNumArray
|
||||||
|
|
||||||
const resp = await $.post('/platform/brandTrainSingUp/manage/activity/doHandle', {data: JSON.stringify(cloneData)})
|
// data 为完整活动 JSON;响应 code 为 0 表示保存成功,msg 用于页面提示。
|
||||||
if (resp.code === 0) {
|
this.formLoading = true
|
||||||
this.$notify({
|
$.post('/platform/brandTrainSingUp/manage/activity/doHandle', {data: JSON.stringify(cloneData)})
|
||||||
title: '成功',
|
.then(resp => {
|
||||||
message: resp.msg,
|
if (resp.code === 0) {
|
||||||
type: 'success',
|
this.$notify({
|
||||||
duration: 500,
|
title: '成功',
|
||||||
onClose: () => {
|
message: resp.msg,
|
||||||
sublime.jumpPagePjax('/platform/brandTrainSingUp/manage/activity')
|
type: 'success',
|
||||||
|
duration: 500,
|
||||||
|
onClose: () => {
|
||||||
|
sublime.jumpPagePjax('/platform/brandTrainSingUp/manage/activity')
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$message.warning(resp.msg)
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
}
|
.always(() => {
|
||||||
|
this.formLoading = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
async initData() {
|
initData() {
|
||||||
const id = GetQueryString("id")
|
const id = GetQueryString("id")
|
||||||
if (id) {
|
if (id) {
|
||||||
const resp = await $.get('/platform/brandTrainSingUp/manage/activity/findOne', {id})
|
// id 为待编辑活动 ID;响应 data 为含课程列表与签到扩展字段的活动对象。
|
||||||
if (resp.code === 0) {
|
this.formLoading = true
|
||||||
resp.data.cover = [resp.data.cover]
|
return $.get('/platform/brandTrainSingUp/manage/activity/findOne', {id})
|
||||||
if (resp.data.wechat) {
|
.then(resp => {
|
||||||
resp.data.wechat = [resp.data.wechat]
|
if (resp.code === 0) {
|
||||||
}
|
resp.data.cover = [resp.data.cover]
|
||||||
this.formData = resp.data
|
if (resp.data.wechat) {
|
||||||
const type = this.trainTypeList.find(o => o.code === this.formData.trainType)
|
resp.data.wechat = [resp.data.wechat]
|
||||||
this.trainType = type ? type.name : '培训班'
|
|
||||||
this.$nextTick(() => {
|
|
||||||
$("#introduce").html("")
|
|
||||||
var editor = new E("#introduce")
|
|
||||||
editor.config.onchange = (html) => {
|
|
||||||
this.formData.introduce = html.replace(/<p\><\/p>/g, '')
|
|
||||||
}
|
|
||||||
editor.create()
|
|
||||||
editor.txt.html(resp.data.introduce)
|
|
||||||
})
|
|
||||||
|
|
||||||
this.formData.courseList.forEach(item => {
|
|
||||||
const type = this.courseTypeList.find(o => o.id === item.courseType)
|
|
||||||
if(type !== null && type !== undefined) {
|
|
||||||
if(type.isBringFamily === true) {
|
|
||||||
this.$set(item, 'isBringFamily', true)
|
|
||||||
item.reserveMode = 1
|
|
||||||
} else {
|
|
||||||
this.$set(item, 'isBringFamily', false)
|
|
||||||
}
|
}
|
||||||
|
this.$set(this, 'formData', resp.data)
|
||||||
|
const type = this.trainTypeList.find(o => o.code === this.formData.trainType)
|
||||||
|
this.trainType = type ? type.name : '培训班'
|
||||||
|
this.$nextTick(() => {
|
||||||
|
$("#introduce").html("")
|
||||||
|
var editor = new E("#introduce")
|
||||||
|
editor.config.onchange = (html) => {
|
||||||
|
this.$set(this.formData, 'introduce', html.replace(/<p\><\/p>/g, ''))
|
||||||
|
}
|
||||||
|
editor.create()
|
||||||
|
editor.txt.html(resp.data.introduce)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.formData.courseList.forEach(item => {
|
||||||
|
const type = this.courseTypeList.find(o => o.id === item.courseType)
|
||||||
|
if(type !== null && type !== undefined) {
|
||||||
|
if(type.isBringFamily === true) {
|
||||||
|
this.$set(item, 'isBringFamily', true)
|
||||||
|
item.reserveMode = 1
|
||||||
|
} else {
|
||||||
|
this.$set(item, 'isBringFamily', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.initSignExtensionDefaults()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
.always(() => {
|
||||||
|
this.formLoading = false
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
$("#introduce").html("")
|
$("#introduce").html("")
|
||||||
@@ -704,40 +901,57 @@ layout("/layouts/platform.html"){
|
|||||||
const resp = await $.post('/platform/brandTrainSingUp/manage/activity/getHistoricalActList', {})
|
const resp = await $.post('/platform/brandTrainSingUp/manage/activity/getHistoricalActList', {})
|
||||||
return resp.data
|
return resp.data
|
||||||
},
|
},
|
||||||
async historicalActChange(val) {
|
historicalActChange(val) {
|
||||||
const resp = await $.get('/platform/brandTrainSingUp/manage/activity/findOne', {id: val})
|
// val 为沿用的活动 ID;取回完整活动后清空 ID,保留原上传、介绍和课程回显规则。
|
||||||
if (resp.code === 0) {
|
this.formLoading = true
|
||||||
resp.data.cover = [resp.data.cover]
|
return $.get('/platform/brandTrainSingUp/manage/activity/findOne', {id: val})
|
||||||
if (resp.data.wechat) {
|
.then(resp => {
|
||||||
resp.data.wechat = [resp.data.wechat]
|
if (resp.code === 0) {
|
||||||
}
|
resp.data.cover = [resp.data.cover]
|
||||||
this.formData = resp.data
|
if (resp.data.wechat) {
|
||||||
const type = this.trainTypeList.find(o => o.code === this.formData.trainType)
|
resp.data.wechat = [resp.data.wechat]
|
||||||
this.trainType = type ? type.name : '培训班'
|
}
|
||||||
this.$nextTick(() => {
|
this.$set(this, 'formData', resp.data)
|
||||||
$("#introduce").html("")
|
const type = this.trainTypeList.find(o => o.code === this.formData.trainType)
|
||||||
var editor = new E("#introduce")
|
this.trainType = type ? type.name : '培训班'
|
||||||
editor.config.onchange = (html) => {
|
this.$nextTick(() => {
|
||||||
this.formData.introduce = html.replace(/<p\><\/p>/g, '')
|
$("#introduce").html("")
|
||||||
}
|
var editor = new E("#introduce")
|
||||||
editor.create()
|
editor.config.onchange = (html) => {
|
||||||
editor.txt.html(resp.data.introduce)
|
this.$set(this.formData, 'introduce', html.replace(/<p\><\/p>/g, ''))
|
||||||
})
|
}
|
||||||
|
editor.create()
|
||||||
|
editor.txt.html(resp.data.introduce)
|
||||||
|
})
|
||||||
|
|
||||||
this.formData.courseList.forEach(item => {
|
this.formData.courseList.forEach(item => {
|
||||||
const type = this.courseTypeList.find(o => o.id === item.courseType)
|
const type = this.courseTypeList.find(o => o.id === item.courseType)
|
||||||
if (type.isBringFamily === true) {
|
if (type.isBringFamily === true) {
|
||||||
this.$set(item, 'isBringFamily', true)
|
this.$set(item, 'isBringFamily', true)
|
||||||
item.reserveMode = 1
|
item.reserveMode = 1
|
||||||
} else {
|
} else {
|
||||||
this.$set(item, 'isBringFamily', false)
|
this.$set(item, 'isBringFamily', false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.$set(this.formData, 'id', '')
|
||||||
|
this.initSignExtensionDefaults()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
this.formData.id = ''
|
.always(() => {
|
||||||
}
|
this.formLoading = false
|
||||||
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
mounted() {
|
||||||
|
// PJAX 替换页面前释放地图;具名事件在再次进入时只保留当前页面的一次清理回调。
|
||||||
|
$(document).off('pjax:beforeReplace.brandTrainSignMap').one('pjax:beforeReplace.brandTrainSignMap', () => this.onMapClosed())
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
$(document).off('pjax:beforeReplace.brandTrainSignMap')
|
||||||
|
this.onMapClosed()
|
||||||
|
},
|
||||||
async created() {
|
async created() {
|
||||||
|
this.getAvailableWelfareProjectList()
|
||||||
this.unionList = await getUnions()
|
this.unionList = await getUnions()
|
||||||
await this.initCnd()
|
await this.initCnd()
|
||||||
this.courseTypeList = await this.getAllType()
|
this.courseTypeList = await this.getAllType()
|
||||||
|
|||||||
+46
-6
@@ -174,7 +174,7 @@
|
|||||||
<el-table-column :label="trainType + '人数'" prop="coursePeopleNumber" width="110px">
|
<el-table-column :label="trainType + '人数'" prop="coursePeopleNumber" width="110px">
|
||||||
<template scope="{row}">
|
<template scope="{row}">
|
||||||
<el-input-number :controls="false"
|
<el-input-number :controls="false"
|
||||||
:max="1000"
|
:max="10000"
|
||||||
:min="1"
|
:min="1"
|
||||||
:precision="0"
|
:precision="0"
|
||||||
step-strictly
|
step-strictly
|
||||||
@@ -279,11 +279,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<vi-title v-if="allCourseSignEnabled" class="mt10" title="签到扩展配置"></vi-title>
|
||||||
|
<el-form-item v-if="allCourseSignEnabled" label="签到点位数量" prop="signPointCount">
|
||||||
|
<!-- Element 会将 null 夹为最小值;空配置传 undefined,避免编辑历史活动时自动启用扩展。 -->
|
||||||
|
<el-input-number :value="formData.signPointCount === null ? undefined : formData.signPointCount"
|
||||||
|
@input="value => this.$set(formData, 'signPointCount', value)"
|
||||||
|
:precision="0" :step="1" :min="1" :max="formData.courseList.length"
|
||||||
|
placeholder="请输入签到点位数量" style="width: 220px"></el-input-number>
|
||||||
|
<span style="color: #c64120; font-size: 12px;">达标点位数不能超过课程数量;不配置点位且处理方式为无时,保留原签到方式</span>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="allCourseSignEnabled" label="处理方式" prop="signHandleType">
|
||||||
|
<el-radio-group v-model="formData.signHandleType" size="small" @change="handleActivitySignHandleTypeChange">
|
||||||
|
<el-radio-button :label="1">系统自动弹出填写信息</el-radio-button>
|
||||||
|
<el-radio-button :label="2">系统自动选择礼物或福利</el-radio-button>
|
||||||
|
<el-radio-button :label="3">无</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="allCourseSignEnabled && formData.signHandleType === 2" label="福利项目" prop="welfareProjectId">
|
||||||
|
<el-select v-model="formData.welfareProjectId" placeholder="请选择福利项目" style="width: 100%"
|
||||||
|
:loading="welfareProjectLoading" :disabled="welfareProjectLoading">
|
||||||
|
<el-option :label="item.name" :value="item.id" :key="item.id" v-for="item in welfareProjectList"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<el-row class="mt10" justify="end" type="flex">
|
<el-row class="mt10" justify="end" type="flex">
|
||||||
<el-button v-if="GetQueryString('id') !== ''" @click="doBack">返回</el-button>
|
<el-button v-if="GetQueryString('id') !== ''" @click="doBack">返回</el-button>
|
||||||
<el-button @click="doHandle(false)" type="primary">保存</el-button>
|
<el-button @click="doHandle(false)" type="primary" :loading="formLoading">保存</el-button>
|
||||||
<el-button @click="doHandle(true)" type="primary">提交</el-button>
|
<el-button @click="doHandle(true)" type="primary" :loading="formLoading">提交</el-button>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<condition :fields="fields" ref="joinCnd" :show_filter_result="true" v-model="conditionStructure"></condition>
|
<condition :fields="fields" ref="joinCnd" :show_filter_result="true" v-model="conditionStructure"></condition>
|
||||||
@@ -379,8 +401,9 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog :close-on-click-modal="false" :visible.sync="mapDialog" title="位置信息">
|
<el-dialog :close-on-click-modal="false" :visible.sync="mapDialog" title="位置信息"
|
||||||
<!--#include("/platform/trainSingUp/manage/include_activity_map.html"){}#-->
|
@opened="onMapOpened" @close="onMapClosed">
|
||||||
|
<!--#include("/platform/brandTrainSingUp/manage/include_activity_map.html"){}#-->
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog :close-on-click-modal="false" :visible.sync="signUpDialog" title="设置报名限制">
|
<el-dialog :close-on-click-modal="false" :visible.sync="signUpDialog" title="设置报名限制">
|
||||||
@@ -553,7 +576,8 @@
|
|||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row v-if="formData.courseList[moreInfoIndex].isMobileSign === true
|
<el-row v-if="formData.courseList[moreInfoIndex].isMobileSign === true
|
||||||
&& formData.courseList[moreInfoIndex].signType == 3" :gutter="50" type="flex">
|
&& (formData.courseList[moreInfoIndex].signType == 1
|
||||||
|
|| formData.courseList[moreInfoIndex].signType == 3)" :gutter="50" type="flex">
|
||||||
<el-col :span="4">
|
<el-col :span="4">
|
||||||
<span>地点坐标</span>
|
<span>地点坐标</span>
|
||||||
</el-col>
|
</el-col>
|
||||||
@@ -562,6 +586,22 @@
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<!-- 半径属于当前课程点位,扫码和GPS签到共用;被扫签到不使用定位范围。 -->
|
||||||
|
<el-row v-if="formData.courseList[moreInfoIndex].isMobileSign === true
|
||||||
|
&& (formData.courseList[moreInfoIndex].signType == 1
|
||||||
|
|| formData.courseList[moreInfoIndex].signType == 3)" :gutter="50" type="flex">
|
||||||
|
<el-col :span="4">
|
||||||
|
<span>点位半径</span>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="20">
|
||||||
|
<el-input-number :value="formData.courseList[moreInfoIndex].signRadius"
|
||||||
|
@input="value => this.$set(formData.courseList[moreInfoIndex], 'signRadius', value)"
|
||||||
|
:min="1" :max="2147483647" :precision="0" :step="1" size="small"></el-input-number>
|
||||||
|
<span style="margin-left: 8px">米</span>
|
||||||
|
<div class="text-muted">以签到点为中心,距离不超过此半径允许签到,默认250米。</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
<el-row :gutter="50" type="flex">
|
<el-row :gutter="50" type="flex">
|
||||||
<el-col :span="4">
|
<el-col :span="4">
|
||||||
<span>是否领取礼品</span>
|
<span>是否领取礼品</span>
|
||||||
|
|||||||
@@ -2,11 +2,15 @@
|
|||||||
<el-autocomplete
|
<el-autocomplete
|
||||||
v-model="addressValue" style="width: 100%"
|
v-model="addressValue" style="width: 100%"
|
||||||
:fetch-suggestions="querySearchAsync"
|
:fetch-suggestions="querySearchAsync"
|
||||||
|
:trigger-on-focus="false"
|
||||||
|
:disabled="mapLoading || !signMapReady"
|
||||||
placeholder="请输入关键词查询地址"
|
placeholder="请输入关键词查询地址"
|
||||||
@select="handleSelect"></el-autocomplete>
|
@select="handleSelect"></el-autocomplete>
|
||||||
</div>
|
</div>
|
||||||
<div id="signMap" style="width: 100%;height: 500px"></div>
|
<el-alert v-if="mapError" :title="mapError" type="error" :closable="false" show-icon style="margin-bottom: 10px"></el-alert>
|
||||||
|
<!-- 固定地图定位容器,避免 loading 结束移除临时定位样式后,地图控件越出边界。 -->
|
||||||
|
<div v-if="mapDialog" id="brandTrainSignMap" v-loading="mapLoading" element-loading-text="腾讯地图加载中..."
|
||||||
|
style="position: relative !important; overflow: hidden; width: 100%; height: 500px; padding: 0; margin: 0"></div>
|
||||||
<el-row class="mt10" justify="end" type="flex">
|
<el-row class="mt10" justify="end" type="flex">
|
||||||
<el-button @click="mapDialog = false" type="danger">关闭</el-button>
|
<el-button @click="closeMapDialog" type="danger">关闭</el-button>
|
||||||
<!-- <el-button @click="mapConfirm" type="primary">确定</el-button>-->
|
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|||||||
+10
-1
@@ -22,6 +22,15 @@
|
|||||||
<el-descriptions-item label="报名结束时间">
|
<el-descriptions-item label="报名结束时间">
|
||||||
{{viewData.activitySignUpEndTime}}
|
{{viewData.activitySignUpEndTime}}
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="签到点位数量">
|
||||||
|
{{isSignExtensionEnabled(viewData) ? viewData.signPointCount : '-'}}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="处理方式">
|
||||||
|
{{isSignExtensionEnabled(viewData) ? getActivitySignHandleTypeName(viewData.signHandleType) : '-'}}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="福利项目">
|
||||||
|
{{isSignExtensionEnabled(viewData) && viewData.signHandleType === 2 ? getWelfareProjectName(viewData.welfareProjectId) : '-'}}
|
||||||
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane :label="trainType + '信息'">
|
<el-tab-pane :label="trainType + '信息'">
|
||||||
@@ -62,4 +71,4 @@
|
|||||||
|
|
||||||
<el-dialog :visible.sync="viewMapDialog" title="地点" width="60%">
|
<el-dialog :visible.sync="viewMapDialog" title="地点" width="60%">
|
||||||
<div id="viewMap" style="width: 100%;height: 500px"></div>
|
<div id="viewMap" style="width: 100%;height: 500px"></div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ layout("/layouts/platform.html"){
|
|||||||
<el-button type="primary" size="small" @click="exportSignUser" :disabled="!pageForm.activityId">
|
<el-button type="primary" size="small" @click="exportSignUser" :disabled="!pageForm.activityId">
|
||||||
按校区导出报名人员
|
按校区导出报名人员
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button type="primary" size="small" @click="exportFillForm" :disabled="!pageForm.activityId" :loading="exportFillFormLoading">
|
||||||
|
导出填写信息
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
<el-table :data="tableData" :size="tableSize">
|
<el-table :data="tableData" :size="tableSize">
|
||||||
@@ -143,6 +146,7 @@ layout("/layouts/platform.html"){
|
|||||||
{label: '报名状态', prop: 'state'},
|
{label: '报名状态', prop: 'state'},
|
||||||
],
|
],
|
||||||
userSignInfo: {},
|
userSignInfo: {},
|
||||||
|
exportFillFormLoading: false,
|
||||||
trainType: '培训班',
|
trainType: '培训班',
|
||||||
trainTypeList: [],
|
trainTypeList: [],
|
||||||
clickRow: {},
|
clickRow: {},
|
||||||
@@ -169,6 +173,58 @@ layout("/layouts/platform.html"){
|
|||||||
exportSignUserByZip() {
|
exportSignUserByZip() {
|
||||||
window.open(loc() + '/exportSignUserByZip?activityId=' + this.pageForm.activityId)
|
window.open(loc() + '/exportSignUserByZip?activityId=' + this.pageForm.activityId)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* activityId 为当前品牌活动 ID;成功响应为按用户汇总的 Excel 文件流。
|
||||||
|
* 业务校验失败返回 JSON Blob,通过箭头回调读取 msg,避免将错误信息下载为 Excel。
|
||||||
|
*/
|
||||||
|
exportFillForm() {
|
||||||
|
if (!this.pageForm.activityId || this.exportFillFormLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.exportFillFormLoading = true
|
||||||
|
axios.post(loc() + '/exportFillForm', $.param({activityId: this.pageForm.activityId}), {
|
||||||
|
responseType: 'blob',
|
||||||
|
headers: {'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8'}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.data.type && response.data.type.indexOf('application/json') > -1) {
|
||||||
|
const reader = new FileReader()
|
||||||
|
reader.onload = () => {
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(reader.result)
|
||||||
|
this.$message.warning(result.msg || '导出填写信息失败')
|
||||||
|
} catch (error) {
|
||||||
|
this.$message.error('读取导出结果失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.onerror = () => {
|
||||||
|
this.$message.error('读取导出结果失败')
|
||||||
|
}
|
||||||
|
reader.readAsText(response.data, 'UTF-8')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const downloadUrl = window.URL.createObjectURL(response.data)
|
||||||
|
const link = document.createElement('a')
|
||||||
|
link.href = downloadUrl
|
||||||
|
link.download = '填写信息.xlsx'
|
||||||
|
document.body.appendChild(link)
|
||||||
|
try {
|
||||||
|
link.click()
|
||||||
|
} finally {
|
||||||
|
document.body.removeChild(link)
|
||||||
|
// 下载触发后释放临时地址,避免多次导出占用浏览器内存。
|
||||||
|
setTimeout(() => {
|
||||||
|
window.URL.revokeObjectURL(downloadUrl)
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
this.$message.error('导出填写信息失败,请稍后重试')
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
this.exportFillFormLoading = false
|
||||||
|
})
|
||||||
|
},
|
||||||
async yearChange() {
|
async yearChange() {
|
||||||
this.pageForm.activityId = null
|
this.pageForm.activityId = null
|
||||||
await this.getActivityList()
|
await this.getActivityList()
|
||||||
|
|||||||
@@ -2,10 +2,42 @@
|
|||||||
layout("/layouts/platform.html"){
|
layout("/layouts/platform.html"){
|
||||||
#-->
|
#-->
|
||||||
<style>
|
<style>
|
||||||
|
.select-all-users.is-bordered.is-checked {
|
||||||
|
border-color: #dcdfe6;
|
||||||
|
}
|
||||||
|
.select-all-users .el-checkbox__input.is-checked + .el-checkbox__label {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.unit-move-drawer .el-drawer__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.unit-move-drawer .el-drawer__body > .el-row {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.unit-move-drawer .unit-move-fields {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.unit-move-drawer .unit-move-fields > .el-col {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.unit-move-drawer .el-drawer__body > .unit-move-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
.unit-move-drawer .unit-move-footer {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div id="app" v-cloak>
|
<div id="app" v-cloak v-loading.fullscreen.lock="selectAllLoading"
|
||||||
|
element-loading-text="正在全选,请稍候"
|
||||||
|
:inert="selectAllLoading ? '' : null" :aria-busy="selectAllLoading ? 'true' : 'false'">
|
||||||
<guava ref="guava">
|
<guava ref="guava">
|
||||||
<template>
|
<template>
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
@@ -248,13 +280,13 @@ layout("/layouts/platform.html"){
|
|||||||
</el-dropdown-menu>
|
</el-dropdown-menu>
|
||||||
</el-dropdown>
|
</el-dropdown>
|
||||||
|
|
||||||
<el-button :disabled="!checkUsers.length" type="primary" size="small"
|
<el-button :disabled="selectAllLoading || !checkUsers.length" type="primary" size="small"
|
||||||
icon="el-icon-edit"
|
icon="el-icon-edit"
|
||||||
@click="openBatchEdit" class="ml5">
|
@click="openBatchEdit" class="ml5">
|
||||||
设置校区
|
设置校区
|
||||||
</el-button>
|
</el-button>
|
||||||
|
|
||||||
<el-button :disabled="!checkUsers.length" type="primary" size="small"
|
<el-button :disabled="selectAllLoading || !checkUsers.length" type="primary" size="small"
|
||||||
icon="el-icon-edit"
|
icon="el-icon-edit"
|
||||||
@click="openUnitMove" class="ml5">
|
@click="openUnitMove" class="ml5">
|
||||||
单位调整
|
单位调整
|
||||||
@@ -263,6 +295,16 @@ layout("/layouts/platform.html"){
|
|||||||
</template>
|
</template>
|
||||||
</table-tool>
|
</table-tool>
|
||||||
|
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 10px;">
|
||||||
|
<el-checkbox :value="allUsersSelected" :indeterminate="someUsersSelected"
|
||||||
|
:disabled="tableLoading || selectAllLoading" size="small" border
|
||||||
|
@change="changeAllUsersSelection" class="select-all-users">
|
||||||
|
<i v-if="selectAllLoading" class="el-icon-loading"></i>
|
||||||
|
全选({{allUsersSelected ? '全部选中' : (someUsersSelected ? '部分选中' : '未选择')}})
|
||||||
|
</el-checkbox>
|
||||||
|
<span style="margin-left: 10px;">已选中 {{checkUsers.length}} 人</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<el-table ref="table" :data="tableData" style="width: 100%" row-key="id"
|
<el-table ref="table" :data="tableData" style="width: 100%" row-key="id"
|
||||||
@sort-change="pageOrder"
|
@sort-change="pageOrder"
|
||||||
v-loading="tableLoading" :size="tableSize" class="vi-table"
|
v-loading="tableLoading" :size="tableSize" class="vi-table"
|
||||||
@@ -463,12 +505,12 @@ layout("/layouts/platform.html"){
|
|||||||
<el-drawer
|
<el-drawer
|
||||||
title="单位调整"
|
title="单位调整"
|
||||||
:visible.sync="unitMoveVisibile"
|
:visible.sync="unitMoveVisibile"
|
||||||
:before-close="handleClose" size="70%">
|
:before-close="handleClose" size="70%" custom-class="unit-move-drawer">
|
||||||
|
|
||||||
<el-row class="text-primary p20">
|
<el-row class="text-primary p20">
|
||||||
选择新单位
|
选择新单位
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row type="flex" :gutter="20" class="pl20 pr20">
|
<el-row type="flex" :gutter="20" class="pl20 pr20 unit-move-fields">
|
||||||
<el-col :span="12">
|
<el-col :span="12">
|
||||||
<el-select placeholder="请选择新工会" v-model="unitMoveFormData.unionId"
|
<el-select placeholder="请选择新工会" v-model="unitMoveFormData.unionId"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
@@ -510,10 +552,10 @@ layout("/layouts/platform.html"){
|
|||||||
</el-col>
|
</el-col>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<el-row class="mt10">
|
<el-row class="mt10 unit-move-list">
|
||||||
<el-table :data="checkUsers" style="width: 100%;padding: 20px" class="vi-table"
|
<el-table :data="checkUsers" style="width: 100%" class="vi-table"
|
||||||
border
|
border
|
||||||
height="70vh">
|
height="100%">
|
||||||
<el-table-column show-overflow-tooltip align="center" header-align="center"
|
<el-table-column show-overflow-tooltip align="center" header-align="center"
|
||||||
prop="loginname"
|
prop="loginname"
|
||||||
width="120"
|
width="120"
|
||||||
@@ -552,10 +594,12 @@ layout("/layouts/platform.html"){
|
|||||||
</el-table>
|
</el-table>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
<span style="position: absolute;bottom: 20px;right: 20px">
|
<div class="unit-move-footer">
|
||||||
<el-button @click="unitMoveVisibile = false">取 消</el-button>
|
<el-button @click="unitMoveVisibile = false">取 消</el-button>
|
||||||
<el-button type="primary" @click="doUnitMove">确定</el-button>
|
<el-button type="primary" :loading="unitMoveLoading" :disabled="unitMoveLoading" @click="doUnitMove">
|
||||||
</span>
|
{{unitMoveLoading ? '迁移中...' : '确定'}}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
</el-drawer>
|
</el-drawer>
|
||||||
|
|
||||||
|
|
||||||
@@ -612,11 +656,13 @@ layout("/layouts/platform.html"){
|
|||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
checkUsers: [],
|
checkUsers: [],
|
||||||
|
selectAllLoading: false,
|
||||||
batchEditVisible: false,
|
batchEditVisible: false,
|
||||||
batchEditLoading: false,
|
batchEditLoading: false,
|
||||||
batchFormData: {},
|
batchFormData: {},
|
||||||
|
|
||||||
unitMoveVisibile: false,
|
unitMoveVisibile: false,
|
||||||
|
unitMoveLoading: false,
|
||||||
unitMoveUnions: [],
|
unitMoveUnions: [],
|
||||||
unitMoveUnits: [],
|
unitMoveUnits: [],
|
||||||
unitMoveFormData: {
|
unitMoveFormData: {
|
||||||
@@ -670,6 +716,25 @@ layout("/layouts/platform.html"){
|
|||||||
selectUserLoading: false,
|
selectUserLoading: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
allUsersSelected() {
|
||||||
|
return this.pageForm.totalCount > 0 && this.checkUsers.length === this.pageForm.totalCount
|
||||||
|
},
|
||||||
|
someUsersSelected() {
|
||||||
|
return this.checkUsers.length > 0 && !this.allUsersSelected
|
||||||
|
},
|
||||||
|
selectionFilters() {
|
||||||
|
const {pageNumber, pageSize, totalCount, pageOrderName, pageOrderBy, ...filters} = this.pageForm
|
||||||
|
return JSON.stringify(filters)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
selectionFilters() {
|
||||||
|
if (this.$refs.table) {
|
||||||
|
this.$refs.table.clearSelection()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
'guava': httpVueLoader('/components/plugins/Guava.vue'),
|
'guava': httpVueLoader('/components/plugins/Guava.vue'),
|
||||||
'member': httpVueLoader('/components/member/MemberInfo.vue?v=1.0.3'),
|
'member': httpVueLoader('/components/member/MemberInfo.vue?v=1.0.3'),
|
||||||
@@ -772,6 +837,9 @@ layout("/layouts/platform.html"){
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
async doUnitMove() {
|
async doUnitMove() {
|
||||||
|
if (this.unitMoveLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (this.unitMoveFormData.unitId === '') {
|
if (this.unitMoveFormData.unitId === '') {
|
||||||
this.notifyWarning('请选择新单位!')
|
this.notifyWarning('请选择新单位!')
|
||||||
return
|
return
|
||||||
@@ -787,14 +855,22 @@ layout("/layouts/platform.html"){
|
|||||||
userId: this.checkUsers.map(v => v.id)
|
userId: this.checkUsers.map(v => v.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
const resp = await $.post(loc() + '/userUnitMove', params)
|
this.unitMoveLoading = true
|
||||||
if (resp.code === 0) {
|
try {
|
||||||
this.pageData()
|
const resp = await $.post(loc() + '/userUnitMove', params)
|
||||||
this.getNumData()
|
if (resp.code === 0) {
|
||||||
this.$refs.table.clearSelection();
|
this.pageData()
|
||||||
this.unitMoveVisibile = false
|
this.getNumData()
|
||||||
} else {
|
this.$refs.table.clearSelection();
|
||||||
this.notifyWarning(resp.msg)
|
this.unitMoveVisibile = false
|
||||||
|
this.notifySuccess('迁移完成')
|
||||||
|
} else {
|
||||||
|
this.notifyWarning(resp.msg)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyWarning('请求异常,无法确认迁移结果')
|
||||||
|
} finally {
|
||||||
|
this.unitMoveLoading = false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
changeDateRangeChange(val) {
|
changeDateRangeChange(val) {
|
||||||
@@ -834,6 +910,41 @@ layout("/layouts/platform.html"){
|
|||||||
this.batchFormData = {}
|
this.batchFormData = {}
|
||||||
this.batchEditVisible = true
|
this.batchEditVisible = true
|
||||||
},
|
},
|
||||||
|
changeAllUsersSelection(checked) {
|
||||||
|
if (checked) {
|
||||||
|
this.selectAllUsers()
|
||||||
|
} else {
|
||||||
|
this.$refs.table.clearSelection()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async selectAllUsers() {
|
||||||
|
if (this.selectAllLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.selectAllLoading = true
|
||||||
|
const snapshot = this.selectionFilters
|
||||||
|
try {
|
||||||
|
const resp = await $.post(loc() + '/selectAllUsers', JSON.parse(snapshot))
|
||||||
|
if (resp.code !== 0) {
|
||||||
|
this.notifyWarning(resp.msg || '全部勾选失败')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (snapshot !== this.selectionFilters) {
|
||||||
|
this.notifyWarning('查询条件已变化,请重新全选')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentRows = new Map(this.tableData.map(v => [v.id, v]))
|
||||||
|
const selectedUsers = new Map(resp.data.map(v => [v.id, currentRows.get(v.id) || v]))
|
||||||
|
this.pageForm.totalCount = selectedUsers.size
|
||||||
|
this.$refs.table.clearSelection()
|
||||||
|
selectedUsers.forEach(v => this.$refs.table.toggleRowSelection(v, true))
|
||||||
|
this.notifySuccess('已勾选 ' + selectedUsers.size + ' 人')
|
||||||
|
} catch (e) {
|
||||||
|
this.notifyWarning('全部勾选失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
this.selectAllLoading = false
|
||||||
|
}
|
||||||
|
},
|
||||||
tableHandleSelectionChange(val) {
|
tableHandleSelectionChange(val) {
|
||||||
this.checkUsers = val;
|
this.checkUsers = val;
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user