品牌活动增加签到可以领礼品
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
+6
-5
@@ -54,8 +54,8 @@ public class BrandTrainScanCodeController {
|
|||||||
* 用户在签到地图中手动确认签到,浏览页面及获取定位本身均不写入签到记录。
|
* 用户在签到地图中手动确认签到,浏览页面及获取定位本身均不写入签到记录。
|
||||||
*
|
*
|
||||||
* @param courseId 二维码携带的品牌课程ID,当前用户从服务端登录态取得
|
* @param courseId 二维码携带的品牌课程ID,当前用户从服务端登录态取得
|
||||||
* @param longitude 当前腾讯定位的经度,须与地图显示点位同为GCJ-02坐标
|
* @param longitude 保留兼容的GCJ-02经度,腾讯地图余额耗尽、临时停用定位校验期间可不传
|
||||||
* @param latitude 当前定位的纬度,与经度共同用于服务端配置半径的范围校验
|
* @param latitude 保留兼容的GCJ-02纬度,临时停用定位和范围校验期间可不传
|
||||||
* @return Result的code/msg/data;0签到成功,99业务提示,已签到时data仍保留当前状态和达标事项
|
* @return Result的code/msg/data;0签到成功,99业务提示,已签到时data仍保留当前状态和达标事项
|
||||||
*/
|
*/
|
||||||
@At
|
@At
|
||||||
@@ -67,9 +67,10 @@ public class BrandTrainScanCodeController {
|
|||||||
if (StrUtil.isBlank(courseId)) {
|
if (StrUtil.isBlank(courseId)) {
|
||||||
return Result.error(99, "缺少签到点位信息");
|
return Result.error(99, "缺少签到点位信息");
|
||||||
}
|
}
|
||||||
if (longitude == null || latitude == null) {
|
// 腾讯地图余额耗尽,暂不要求定位;恢复时需同步恢复前端定位及service点位范围校验。
|
||||||
return Result.error(99, "请先获取当前定位后再签到");
|
// if (longitude == null || latitude == null) {
|
||||||
}
|
// return Result.error(99, "请先获取当前定位后再签到");
|
||||||
|
// }
|
||||||
try {
|
try {
|
||||||
NutMap result = brandTrainSignUpActivityService.handleScanCodeSign(courseId, ShiroUtil.getPlatformUid(), longitude, latitude);
|
NutMap result = brandTrainSignUpActivityService.handleScanCodeSign(courseId, ShiroUtil.getPlatformUid(), longitude, latitude);
|
||||||
return Result.NEW().addCode(result.getInt("code")).addMsg(result.getString("msg")).addData(result.get("data"));
|
return Result.NEW().addCode(result.getInt("code")).addMsg(result.getString("msg")).addData(result.get("data"));
|
||||||
|
|||||||
+4
-3
@@ -143,12 +143,13 @@ public interface BrandTrainSignUpActivityService extends ViService<BrandTrainSig
|
|||||||
NutMap getScanCodeSignData(String courseId, String userId);
|
NutMap getScanCodeSignData(String courseId, String userId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当前用户在地图页手动签到,校验定位后累计不重复的课程点位,并处理原有达标事项。
|
* 当前用户在地图页手动签到,累计不重复的课程点位,并处理原有达标事项。
|
||||||
|
* 腾讯地图余额耗尽期间临时停用定位和范围校验,恢复时需同步恢复前端、controller和service中的校验。
|
||||||
*
|
*
|
||||||
* @param courseId 品牌课程ID,不接受普通培训课程或非扫码签到课程
|
* @param courseId 品牌课程ID,不接受普通培训课程或非扫码签到课程
|
||||||
* @param userId 当前登录用户ID,由服务端登录态取得
|
* @param userId 当前登录用户ID,由服务端登录态取得
|
||||||
* @param longitude 当前腾讯地图GCJ-02定位经度,范围为[-180,180],不能为NaN或无穷值
|
* @param longitude 保留兼容的GCJ-02经度,临时停用定位校验期间可不传
|
||||||
* @param latitude 当前腾讯地图GCJ-02定位纬度,范围为[-90,90],与课程坐标相距不得超过配置的点位半径
|
* @param latitude 保留兼容的GCJ-02纬度,临时停用定位和范围校验期间可不传
|
||||||
* @return code/msg/data结构;data含签到进度、动态表单、福利跳转及达标处理状态
|
* @return code/msg/data结构;data含签到进度、动态表单、福利跳转及达标处理状态
|
||||||
*/
|
*/
|
||||||
NutMap handleScanCodeSign(String courseId, String userId, Double longitude, Double latitude);
|
NutMap handleScanCodeSign(String courseId, String userId, Double longitude, Double latitude);
|
||||||
|
|||||||
+67
-2
@@ -45,6 +45,7 @@ import java.util.List;
|
|||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.Objects;
|
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;
|
||||||
@@ -756,8 +757,8 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrai
|
|||||||
public NutMap handleScanCodeSign(String courseId, String userId, Double longitude, Double latitude) {
|
public NutMap handleScanCodeSign(String courseId, String userId, Double longitude, Double latitude) {
|
||||||
lockScanCodeUser(userId);
|
lockScanCodeUser(userId);
|
||||||
BrandTrainSignUpCourse course = requireScanCodeCourse(courseId, userId);
|
BrandTrainSignUpCourse course = requireScanCodeCourse(courseId, userId);
|
||||||
// 地点配置和用户定位均在补报名、课次写入与福利处理之前校验,校验失败不产生业务数据。
|
// 腾讯地图余额耗尽期间,仅当前扫码入口临时停用定位和范围校验;恢复时需同步恢复前端定位及controller经纬度校验。
|
||||||
requireScanCodeLocation(course.getCourseLocationCoordinates(), longitude, latitude, getSignRadius(course));
|
// requireScanCodeLocation(course.getCourseLocationCoordinates(), longitude, latitude, getSignRadius(course));
|
||||||
BrandTrainSignUpActivity activity = dao().fetch(BrandTrainSignUpActivity.class, course.getActivityId());
|
BrandTrainSignUpActivity activity = dao().fetch(BrandTrainSignUpActivity.class, course.getActivityId());
|
||||||
ensureScanCodeSignUpUser(activity, course, userId);
|
ensureScanCodeSignUpUser(activity, course, userId);
|
||||||
List<BrandTrainSignUpUserCourse> records = dao().query(BrandTrainSignUpUserCourse.class,
|
List<BrandTrainSignUpUserCourse> records = dao().query(BrandTrainSignUpUserCourse.class,
|
||||||
@@ -1015,11 +1016,14 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrai
|
|||||||
: "FILE".equals(column.getColumnFormType()) ? new ArrayList<>() : "");
|
: "FILE".equals(column.getColumnFormType()) ? new ArrayList<>() : "");
|
||||||
dynamicColumns.add(columnMap);
|
dynamicColumns.add(columnMap);
|
||||||
}
|
}
|
||||||
|
List<NutMap> signedPoints = buildScanCodeSignedPoints(activity.getId(), attendRecords);
|
||||||
NutMap data = NutMap.NEW().addv("activityName", activity.getActivityName()).addv("courseName", course.getCourseName())
|
NutMap data = NutMap.NEW().addv("activityName", activity.getActivityName()).addv("courseName", course.getCourseName())
|
||||||
.addv("courseLocation", course.getCourseLocation()).addv("courseLocationCoordinates", course.getCourseLocationCoordinates())
|
.addv("courseLocation", course.getCourseLocation()).addv("courseLocationCoordinates", course.getCourseLocationCoordinates())
|
||||||
.addv("currentPointSigned", attendRecords.stream().anyMatch(item -> Objects.equals(item.getCourseId(), course.getId())))
|
.addv("currentPointSigned", attendRecords.stream().anyMatch(item -> Objects.equals(item.getCourseId(), course.getId())))
|
||||||
.addv("signRadius", getSignRadius(course))
|
.addv("signRadius", getSignRadius(course))
|
||||||
.addv("signCount", signCount).addv("signPointCount", activity.getSignPointCount())
|
.addv("signCount", signCount).addv("signPointCount", activity.getSignPointCount())
|
||||||
|
.addv("signedPoints", signedPoints)
|
||||||
|
.addv("signPoints", buildScanCodePoints(activity.getId(), signedPoints))
|
||||||
.addv("signHandleType", handleType).addv("welfareProjectId", activity.getWelfareProjectId())
|
.addv("signHandleType", handleType).addv("welfareProjectId", activity.getWelfareProjectId())
|
||||||
.addv("isReachTarget", reached).addv("dynamicColumnsData", dynamicColumns)
|
.addv("isReachTarget", reached).addv("dynamicColumnsData", dynamicColumns)
|
||||||
.addv("needDynamicForm", reached && handleType == 1 && !dynamicColumns.isEmpty())
|
.addv("needDynamicForm", reached && handleType == 1 && !dynamicColumns.isEmpty())
|
||||||
@@ -1040,6 +1044,67 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl<BrandTrai
|
|||||||
return 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)
|
@Aop(TransAop.READ_COMMITTED)
|
||||||
private void completeScanCodeWelfare(String projectId, String userId, NutMap data) {
|
private void completeScanCodeWelfare(String projectId, String userId, NutMap data) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class TMapService {
|
|||||||
|
|
||||||
// 地点搜索仅访问腾讯固定服务;系统TencentMapKey可覆盖参考工程的默认Key。
|
// 地点搜索仅访问腾讯固定服务;系统TencentMapKey可覆盖参考工程的默认Key。
|
||||||
private static final String TENCENT_MAP_SUGGESTION_URL = "https://apis.map.qq.com/ws/place/v1/suggestion";
|
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 = "L5WBZ-3DDKH-KW2DC-WCDWL-Y3SUV-WNBDI";
|
private static final String DEFAULT_TENCENT_MAP_KEY = "NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取腾讯地图地址候选,仅访问固定的地点搜索服务,不修改本地数据。
|
* 读取腾讯地图地址候选,仅访问固定的地点搜索服务,不修改本地数据。
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})()
|
||||||
+56
@@ -148,6 +148,24 @@ module.exports = {
|
|||||||
event: "updateDynamicData"
|
event: "updateDynamicData"
|
||||||
},
|
},
|
||||||
watch: {
|
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: {
|
dynamicData: {
|
||||||
handler(newValue) {
|
handler(newValue) {
|
||||||
this.initAddressStates(newValue)
|
this.initAddressStates(newValue)
|
||||||
@@ -162,6 +180,8 @@ module.exports = {
|
|||||||
return {
|
return {
|
||||||
//所有的picker状态
|
//所有的picker状态
|
||||||
pickerStates: {},
|
pickerStates: {},
|
||||||
|
popupHistory: null,
|
||||||
|
imagePreview: null,
|
||||||
fileLists: {},
|
fileLists: {},
|
||||||
fileValueSnapshots: {},
|
fileValueSnapshots: {},
|
||||||
addressStates: {},
|
addressStates: {},
|
||||||
@@ -454,6 +474,10 @@ module.exports = {
|
|||||||
this.$set(column, "columnValue", addressValue)
|
this.$set(column, "columnValue", addressValue)
|
||||||
},
|
},
|
||||||
selectFieldClick(column) {
|
selectFieldClick(column) {
|
||||||
|
// 普通输入框也绑定了点击事件,只有实际存在选择弹框的字段才允许打开历史层。
|
||||||
|
if (column.columnFormType !== 'SELECT') {
|
||||||
|
return
|
||||||
|
}
|
||||||
this.$set(this.pickerStates, column.columnCode + "Picker", true)
|
this.$set(this.pickerStates, column.columnCode + "Picker", true)
|
||||||
if (!["DATE", "DATETIME"].includes(column.columnType)) {
|
if (!["DATE", "DATETIME"].includes(column.columnType)) {
|
||||||
//说明是普通的下拉框
|
//说明是普通的下拉框
|
||||||
@@ -613,13 +637,37 @@ module.exports = {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (item.isImage) {
|
if (item.isImage) {
|
||||||
|
// 图片预览位于表单之上,也作为独立历史层,返回时先关预览再关表单。
|
||||||
|
if (!this.popupHistory) {
|
||||||
vant.ImagePreview([item.url])
|
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 {
|
} else {
|
||||||
window.open(item.url, '_blank')
|
window.open(item.url, '_blank')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
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)
|
// console.log(this.dynamicData)
|
||||||
// this.dynamicData.forEach(v => {
|
// this.dynamicData.forEach(v => {
|
||||||
// if (v.columnFormType === 'SELECT') {
|
// if (v.columnFormType === 'SELECT') {
|
||||||
@@ -627,6 +675,14 @@ module.exports = {
|
|||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
// console.log(this.dynamicData)
|
// console.log(this.dynamicData)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.imagePreview) {
|
||||||
|
this.imagePreview.close()
|
||||||
|
}
|
||||||
|
if (this.popupHistory) {
|
||||||
|
this.popupHistory.unregister()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
<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 进入后直接使用。 -->
|
<!-- 参考工程在公共布局中静态引入腾讯地图,供活动选点及 PJAX 进入后直接使用。 -->
|
||||||
<script src="https://map.qq.com/api/gljs?v=2.exp&key=L5WBZ-3DDKH-KW2DC-WCDWL-Y3SUV-WNBDI"></script>
|
<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>
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ layout("/mobile/platform.html"){
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow-x: hidden;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: #f4f7fc;
|
background: #f4f7fc;
|
||||||
}
|
}
|
||||||
@@ -20,12 +23,14 @@ layout("/mobile/platform.html"){
|
|||||||
.brand-train-sign-content {
|
.brand-train-sign-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0 auto;
|
flex: 1 0 auto;
|
||||||
|
min-width: 0;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-train-sign-map {
|
.brand-train-sign-map {
|
||||||
/* 固定 SDK 容器定位,防止地图控件溢出到信息区和底部按钮。 */
|
/* 固定 SDK 容器定位,防止地图控件溢出到信息区和底部按钮。 */
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
|
z-index: 0;
|
||||||
flex: 1 0 240px;
|
flex: 1 0 240px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 240px;
|
min-height: 240px;
|
||||||
@@ -34,6 +39,9 @@ layout("/mobile/platform.html"){
|
|||||||
|
|
||||||
.brand-train-sign-details {
|
.brand-train-sign-details {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
}
|
}
|
||||||
@@ -46,6 +54,24 @@ layout("/mobile/platform.html"){
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-signed {
|
||||||
|
color: #07c160;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-signed .van-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.brand-train-sign-location-tools {
|
.brand-train-sign-location-tools {
|
||||||
display: flex;
|
display: flex;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
@@ -56,6 +82,10 @@ layout("/mobile/platform.html"){
|
|||||||
|
|
||||||
.brand-train-sign-location-tools .van-button {
|
.brand-train-sign-location-tools .van-button {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 2px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-train-sign-location-status {
|
.brand-train-sign-location-status {
|
||||||
@@ -65,6 +95,20 @@ layout("/mobile/platform.html"){
|
|||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-location-status.is-in-range {
|
||||||
|
color: #263548;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-location-status.is-in-range::before {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
margin-right: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #07c160;
|
||||||
|
content: '';
|
||||||
|
}
|
||||||
|
|
||||||
.brand-train-sign-text {
|
.brand-train-sign-text {
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
color: #78869a;
|
color: #78869a;
|
||||||
@@ -79,28 +123,218 @@ layout("/mobile/platform.html"){
|
|||||||
}
|
}
|
||||||
|
|
||||||
.brand-train-sign-progress {
|
.brand-train-sign-progress {
|
||||||
display: flex;
|
min-width: 0;
|
||||||
margin-top: 10px;
|
max-width: 100%;
|
||||||
align-items: center;
|
box-sizing: border-box;
|
||||||
flex-wrap: wrap;
|
margin-top: 14px;
|
||||||
gap: 8px;
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #f4f7fc;
|
||||||
color: #78869a;
|
color: #78869a;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-progress-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-progress-heading {
|
||||||
|
color: #263548;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-record-link {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 32px;
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 0;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: #1989fa;
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-count {
|
||||||
|
margin: 0 3px;
|
||||||
|
color: #1989fa;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-progress-track {
|
||||||
|
/* 使用容器百分比宽度,避免旧组件缓存像素宽度后在屏幕尺寸变化时越出卡片。 */
|
||||||
|
width: 100%;
|
||||||
|
height: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #dce3ee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-progress-fill {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: #1989fa;
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-progress-message {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
.brand-train-sign-map-warning {
|
.brand-train-sign-map-warning {
|
||||||
color: #ee0a24;
|
color: #ee0a24;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-train-sign-footer {
|
.brand-train-sign-footer {
|
||||||
|
/* 状态留在信息区,底栏仅容纳操作;双按钮平分宽度,单按钮居中且不铺满屏幕。 */
|
||||||
|
position: sticky;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 3;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||||
|
border-top: 1px solid #edf0f5;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-footer .van-button {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-footer .van-button:only-child {
|
||||||
|
flex: 0 1 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-points-popup {
|
||||||
|
/* 从右侧滑入并覆盖整个视口;导航和底部提示保留高度,点位卡片区域独立滚动。 */
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
height: 100dvh;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-points-popup > .van-nav-bar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-points-list {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: 16px;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #e9edf4;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point .brand-train-sign-title {
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point .brand-train-sign-text {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point-header .brand-train-sign-title-row {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff7e9;
|
||||||
|
color: #d98a13;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 20px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point-status.is-signed {
|
||||||
|
background: #e8fbf2;
|
||||||
|
color: #07c160;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point-status .van-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point-time {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #edf0f5;
|
||||||
|
color: #78869a;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-point-time-value {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
color: #3b4b60;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 20px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-train-sign-points-note {
|
||||||
|
flex-shrink: 0;
|
||||||
padding: 12px 16px;
|
padding: 12px 16px;
|
||||||
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
padding-bottom: calc(12px + env(safe-area-inset-bottom));
|
||||||
|
border-top: 1px solid #edf0f5;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
color: #78869a;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-train-sign-result {
|
.brand-train-sign-result {
|
||||||
@@ -143,12 +377,13 @@ layout("/mobile/platform.html"){
|
|||||||
<div class="brand-train-sign-details">
|
<div class="brand-train-sign-details">
|
||||||
<div class="brand-train-sign-location-tools">
|
<div class="brand-train-sign-location-tools">
|
||||||
<span class="brand-train-sign-location-status" role="status"
|
<span class="brand-train-sign-location-status" role="status"
|
||||||
:class="{'brand-train-sign-map-warning': mapError || locationError || distance != null && !isInRange}">
|
:class="{'brand-train-sign-map-warning': mapError || locationError || distance != null && !isInRange,
|
||||||
|
'is-in-range': !mapError && !locationError && !locating && isInRange}">
|
||||||
<template v-if="mapError">{{mapError}}</template>
|
<template v-if="mapError">{{mapError}}</template>
|
||||||
<template v-else-if="locating">正在获取您的当前位置</template>
|
<template v-else-if="locating">正在获取您的当前位置</template>
|
||||||
<template v-else-if="locationError">{{locationError}}</template>
|
<template v-else-if="locationError">{{locationError}}</template>
|
||||||
<template v-else-if="distance != null">距签到地点约{{distanceText}}米 · {{isInRange ? '已进入签到范围' : '尚未进入签到范围'}}</template>
|
<template v-else-if="distance != null">{{isInRange ? '已进入签到范围' : '尚未进入签到范围'}}</template>
|
||||||
<template v-else>正在准备地图和定位服务</template>
|
<template v-else>可直接点击下方按钮签到</template>
|
||||||
</span>
|
</span>
|
||||||
<van-button v-if="mapError && siteCoordinates.length === 2" size="small" plain type="info"
|
<van-button v-if="mapError && siteCoordinates.length === 2" size="small" plain type="info"
|
||||||
:loading="mapLoading" @click="initSignMap">重新加载地图</van-button>
|
:loading="mapLoading" @click="initSignMap">重新加载地图</van-button>
|
||||||
@@ -156,20 +391,31 @@ layout("/mobile/platform.html"){
|
|||||||
:loading="locating" :disabled="!mapReady || signLoading"
|
:loading="locating" :disabled="!mapReady || signLoading"
|
||||||
@click="refreshPosition">重新定位</van-button>
|
@click="refreshPosition">重新定位</van-button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="brand-train-sign-title-row">
|
||||||
<div class="brand-train-sign-title">{{signData.courseName}}</div>
|
<div class="brand-train-sign-title">{{signData.courseName}}</div>
|
||||||
<div class="brand-train-sign-text">
|
<span v-if="currentPointSigned" class="brand-train-sign-signed" role="status">
|
||||||
<van-icon name="location-o"></van-icon>{{signData.courseLocation || '未填写地点名称'}}
|
<van-icon name="checked"></van-icon>当前点位已签到
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="brand-train-sign-text">签到范围:{{signRadius}} 米</div>
|
<div class="brand-train-sign-text">签到范围:{{signRadius}} 米</div>
|
||||||
<div v-if="locationAccuracy != null" class="brand-train-sign-text">定位精度约 {{Math.ceil(locationAccuracy)}} 米</div>
|
<div v-if="distance != null" class="brand-train-sign-text">距签到地点约 {{distanceText}} 米</div>
|
||||||
<!-- 活动达标与当前点位签到分别展示,避免已达标时误认为所有点位都已签到。 -->
|
<!-- 活动达标与当前点位签到分别展示,避免已达标时误认为所有点位都已签到。 -->
|
||||||
<div class="brand-train-sign-progress">
|
<div class="brand-train-sign-progress">
|
||||||
<span>已签到 {{signData.signCount}} 个点位 / 达标需 {{signData.signPointCount}} 个</span>
|
<div class="brand-train-sign-progress-header">
|
||||||
|
<span class="brand-train-sign-progress-heading">活动签到进度</span>
|
||||||
<van-tag v-if="isReachTarget" plain type="success">活动已达标</van-tag>
|
<van-tag v-if="isReachTarget" plain type="success">活动已达标</van-tag>
|
||||||
<van-tag v-else-if="currentPointSigned" plain type="success">当前点位已签到</van-tag>
|
<button type="button" class="brand-train-sign-record-link" @click="openSignedPoints">
|
||||||
|
点位打卡情况<van-icon name="arrow"></van-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>已打卡 <strong class="brand-train-sign-count">{{signData.signCount}}</strong> 个 / 达标需 {{signData.signPointCount}} 个</div>
|
||||||
|
<div class="brand-train-sign-progress-track" role="progressbar" aria-label="活动签到进度"
|
||||||
|
aria-valuemin="0" aria-valuemax="100" :aria-valuenow="signProgressPercent"
|
||||||
|
:aria-valuetext="'已打卡 ' + signData.signCount + ' 个点位,达标需 ' + signData.signPointCount + ' 个'">
|
||||||
|
<span class="brand-train-sign-progress-fill" :style="{width: signProgressPercent + '%'}"></span>
|
||||||
|
</div>
|
||||||
|
<div class="brand-train-sign-progress-message" role="status">{{signProgressMessage}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="resultMessage && resultMessage !== '当前活动已达标' && (isReachTarget || currentPointSigned || result.code !== 0)"
|
|
||||||
class="brand-train-sign-text" role="status">{{resultMessage}}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="brand-train-sign-result">
|
<div v-else class="brand-train-sign-result">
|
||||||
@@ -178,19 +424,45 @@ layout("/mobile/platform.html"){
|
|||||||
<div class="brand-train-sign-title">{{isReachTarget ? '当前活动已达标' : currentPointSigned ? '当前点位已签到' : '签到提示'}}</div>
|
<div class="brand-train-sign-title">{{isReachTarget ? '当前活动已达标' : currentPointSigned ? '当前点位已签到' : '签到提示'}}</div>
|
||||||
<div class="brand-train-sign-text">{{resultMessage}}</div>
|
<div class="brand-train-sign-text">{{resultMessage}}</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="hasSignProgress || signData.needDynamicForm || signData.needWelfareSelect" class="brand-train-sign-footer">
|
<div v-if="showSignButton || showDynamicFormButton || signData.needWelfareSelect" class="brand-train-sign-footer">
|
||||||
<van-button v-if="hasSignProgress" block round type="info" loading-text="签到中..."
|
<van-button v-if="showDynamicFormButton" :plain="showSignButton" type="info"
|
||||||
:loading="signLoading" :disabled="signDisabled" @click="submitSign">
|
:disabled="signLoading || formLoading" @click="openDynamicForm">填写详细信息</van-button>
|
||||||
{{currentPointSigned ? '当前点位已签到' : '确认签到'}}
|
<van-button v-if="signData.needWelfareSelect" :plain="showSignButton" type="info"
|
||||||
</van-button>
|
:disabled="signLoading || formLoading" @click="goWelfareSelect">去选择福利</van-button>
|
||||||
<van-button v-if="signData.needDynamicForm" block round type="info"
|
<van-button v-if="showSignButton" type="info" loading-text="签到中..."
|
||||||
@click="openDynamicForm">填写详细信息</van-button>
|
:loading="signLoading" :disabled="signDisabled" @click="submitSign">确认签到</van-button>
|
||||||
<van-button v-if="signData.needWelfareSelect" block round type="info"
|
|
||||||
@click="goWelfareSelect">去选择福利</van-button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<van-popup v-model="dynamicFormPopup" class="brand-train-sign-form-popup" position="right">
|
<van-popup :value="signedPointsPopup" class="brand-train-sign-points-popup" position="right"
|
||||||
|
@input="(visible) => syncSignPopup('signedPointsPopup', visible)">
|
||||||
|
<van-nav-bar title="点位打卡情况" left-text="返回" left-arrow @click-left="closeSignedPoints"></van-nav-bar>
|
||||||
|
<div ref="signPointsList" class="brand-train-sign-points-list">
|
||||||
|
<van-empty v-if="!displaySignPoints.length" description="暂无点位信息"></van-empty>
|
||||||
|
<div v-for="point in displaySignPoints" :key="point.courseId" class="brand-train-sign-point">
|
||||||
|
<div class="brand-train-sign-point-header">
|
||||||
|
<div class="brand-train-sign-title-row">
|
||||||
|
<div class="brand-train-sign-title">{{point.courseName || '点位名称未填写'}}</div>
|
||||||
|
<van-tag v-if="point.courseId === courseId" plain type="primary">当前点位</van-tag>
|
||||||
|
</div>
|
||||||
|
<span class="brand-train-sign-point-status" :class="{'is-signed': point.signed === true}">
|
||||||
|
<van-icon :name="point.signed === true ? 'checked' : 'clock-o'"></van-icon>{{point.signed === true ? '已打卡' : '未打卡'}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="brand-train-sign-text"><van-icon name="location-o"></van-icon>地点:{{point.courseLocation || '未填写地点名称'}}</div>
|
||||||
|
<div v-if="point.signed === true" class="brand-train-sign-point-time">
|
||||||
|
签到时间
|
||||||
|
<span class="brand-train-sign-point-time-value">{{formatAttendTime(point.attendTime)}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="brand-train-sign-points-note">
|
||||||
|
{{isReachTarget ? '活动已达标,仍可继续打卡其他点位' : '完成 ' + signData.signPointCount + ' 个不同点位打卡即可达标'}}
|
||||||
|
</div>
|
||||||
|
</van-popup>
|
||||||
|
|
||||||
|
<van-popup :value="dynamicFormPopup" class="brand-train-sign-form-popup" position="right"
|
||||||
|
:close-on-click-overlay="!formLoading" @input="(visible) => syncSignPopup('dynamicFormPopup', visible)">
|
||||||
<van-nav-bar title="填写详细信息" left-text="返回" left-arrow fixed placeholder
|
<van-nav-bar title="填写详细信息" left-text="返回" left-arrow fixed placeholder
|
||||||
@click-left="closeDynamicForm"></van-nav-bar>
|
@click-left="closeDynamicForm"></van-nav-bar>
|
||||||
<div class="brand-train-sign-form-content">
|
<div class="brand-train-sign-form-content">
|
||||||
@@ -206,6 +478,7 @@ layout("/mobile/platform.html"){
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
|
<script src="${base!}/assets/platform/plugins/axios/axios.js"></script>
|
||||||
|
<script src="${base!}/assets/mobile/js/popupHistory.js"></script>
|
||||||
<script>
|
<script>
|
||||||
<!--#include("/mobile/welfare/include/areaList.js"){}#-->
|
<!--#include("/mobile/welfare/include/areaList.js"){}#-->
|
||||||
window.brandTrainSignUpAreaList = areaList
|
window.brandTrainSignUpAreaList = areaList
|
||||||
@@ -230,7 +503,7 @@ layout("/mobile/platform.html"){
|
|||||||
Vue.prototype.$moment = moment
|
Vue.prototype.$moment = moment
|
||||||
|
|
||||||
// 腾讯地图与定位组件仅在本页加载;定位和坐标转换统一由腾讯组件处理,实例不进入 Vue 响应式数据。
|
// 腾讯地图与定位组件仅在本页加载;定位和坐标转换统一由腾讯组件处理,实例不进入 Vue 响应式数据。
|
||||||
const brandTrainTencentKey = 'L5WBZ-3DDKH-KW2DC-WCDWL-Y3SUV-WNBDI'
|
const brandTrainTencentKey = 'NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU'
|
||||||
const brandTrainTencentLoads = {}
|
const brandTrainTencentLoads = {}
|
||||||
let brandTrainMap = null
|
let brandTrainMap = null
|
||||||
let brandTrainGeolocation = null
|
let brandTrainGeolocation = null
|
||||||
@@ -240,6 +513,7 @@ layout("/mobile/platform.html"){
|
|||||||
let brandTrainLocationTimer = null
|
let brandTrainLocationTimer = null
|
||||||
let brandTrainLocationRequestId = 0
|
let brandTrainLocationRequestId = 0
|
||||||
let brandTrainMapDestroyed = false
|
let brandTrainMapDestroyed = false
|
||||||
|
let brandTrainPopupHistory = null
|
||||||
|
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#app',
|
el: '#app',
|
||||||
@@ -259,9 +533,9 @@ layout("/mobile/platform.html"){
|
|||||||
mapError: '',
|
mapError: '',
|
||||||
locationError: '',
|
locationError: '',
|
||||||
currentPosition: [],
|
currentPosition: [],
|
||||||
locationAccuracy: null,
|
|
||||||
distance: null,
|
distance: null,
|
||||||
dynamicFormPopup: false,
|
dynamicFormPopup: false,
|
||||||
|
signedPointsPopup: false,
|
||||||
dynamicColumnsData: []
|
dynamicColumnsData: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -278,7 +552,46 @@ layout("/mobile/platform.html"){
|
|||||||
currentPointSigned() {
|
currentPointSigned() {
|
||||||
return this.signData.currentPointSigned === true
|
return this.signData.currentPointSigned === true
|
||||||
},
|
},
|
||||||
// 坐标按经度、纬度排列;缺失或超出有效范围时不创建地图,也不允许提交签到。
|
// 达标不影响新点位签到;已签点位仅展示状态,不再占用底部按钮位置。
|
||||||
|
showSignButton() {
|
||||||
|
return this.hasSignProgress && !this.currentPointSigned
|
||||||
|
},
|
||||||
|
showDynamicFormButton() {
|
||||||
|
return this.isReachTarget && this.signData.needDynamicForm === true
|
||||||
|
},
|
||||||
|
signedPoints() {
|
||||||
|
return Array.isArray(this.signData.signedPoints) ? this.signData.signedPoints : []
|
||||||
|
},
|
||||||
|
// 全部点位由活动课程配置提供,已报名但未签到的课程仍属于未打卡。
|
||||||
|
allSignPoints() {
|
||||||
|
return Array.isArray(this.signData.signPoints) ? this.signData.signPoints : []
|
||||||
|
},
|
||||||
|
displaySignPoints() {
|
||||||
|
const signed = this.allSignPoints.filter((point) => point.signed === true)
|
||||||
|
const unsigned = this.allSignPoints.filter((point) => point.signed !== true)
|
||||||
|
// 始终展示全部点位,仅将未打卡项排在前面;组内保持接口顺序,不修改原数组。
|
||||||
|
return unsigned.concat(signed)
|
||||||
|
},
|
||||||
|
// 仅进度条封顶100%,实际点位计数保留超过达标数量的值。
|
||||||
|
signProgressPercent() {
|
||||||
|
const target = Number(this.signData.signPointCount)
|
||||||
|
const count = Number(this.signData.signCount)
|
||||||
|
return target > 0 && Number.isFinite(count) ? Math.min(100, Math.max(0, count / target * 100)) : 0
|
||||||
|
},
|
||||||
|
signProgressMessage() {
|
||||||
|
// 福利失败等服务端提示优先展示,避免美化布局后掩盖业务错误。
|
||||||
|
if (this.signData.handleStatus === 'FAILED' || this.result.code !== 0 && this.result.code !== 99) {
|
||||||
|
return this.resultMessage
|
||||||
|
}
|
||||||
|
if (this.isReachTarget && !this.currentPointSigned) {
|
||||||
|
return '已达标,可继续打卡其他点位。'
|
||||||
|
}
|
||||||
|
if (this.isReachTarget) {
|
||||||
|
return this.resultMessage === '当前活动已达标' ? '当前活动已达标,可继续打卡其他点位。' : this.resultMessage
|
||||||
|
}
|
||||||
|
return this.currentPointSigned ? '请继续完成其他点位签到。' : '到达点位后,点击确认签到。'
|
||||||
|
},
|
||||||
|
// 坐标按经度、纬度排列;缺失或超出有效范围时不创建地图,临时取消定位限制期间不影响签到。
|
||||||
siteCoordinates() {
|
siteCoordinates() {
|
||||||
const coordinates = this.signData.courseLocationCoordinates
|
const coordinates = this.signData.courseLocationCoordinates
|
||||||
if (!Array.isArray(coordinates) || coordinates.length !== 2
|
if (!Array.isArray(coordinates) || coordinates.length !== 2
|
||||||
@@ -300,15 +613,17 @@ layout("/mobile/platform.html"){
|
|||||||
return this.distance != null && this.distance <= this.signRadius
|
return this.distance != null && this.distance <= this.signRadius
|
||||||
},
|
},
|
||||||
signDisabled() {
|
signDisabled() {
|
||||||
return this.currentPointSigned || this.signLoading || this.locating || !this.mapReady
|
// 腾讯定位服务余额耗尽,临时取消地图、定位和范围限制;保留已签到及提交中的防重复判断。
|
||||||
|| this.siteCoordinates.length !== 2 || this.currentPosition.length !== 2 || !this.isInRange
|
return this.currentPointSigned || this.signLoading || this.formLoading
|
||||||
|
// || this.locating || !this.mapReady
|
||||||
|
// || this.siteCoordinates.length !== 2 || this.currentPosition.length !== 2 || !this.isInRange
|
||||||
},
|
},
|
||||||
resultMessage() {
|
resultMessage() {
|
||||||
if (this.isReachTarget) {
|
if (this.isReachTarget) {
|
||||||
return this.signData.handleMsg || '当前活动已达标'
|
return this.signData.handleMsg || '当前活动已达标'
|
||||||
}
|
}
|
||||||
if (this.hasSignProgress && this.result.code === 0) {
|
if (this.hasSignProgress && this.result.code === 0) {
|
||||||
return this.currentPointSigned ? '请继续完成其他点位签到。' : '请在签到范围内点击下方按钮完成签到。'
|
return this.currentPointSigned ? '请继续完成其他点位签到。' : '请点击下方按钮完成签到。'
|
||||||
}
|
}
|
||||||
return this.result.msg || ''
|
return this.result.msg || ''
|
||||||
}
|
}
|
||||||
@@ -316,15 +631,45 @@ layout("/mobile/platform.html"){
|
|||||||
methods: {
|
methods: {
|
||||||
// 沿用浏览器返回;从二维码直接进入且没有上一页时,回到当前系统手机首页。
|
// 沿用浏览器返回;从二维码直接进入且没有上一页时,回到当前系统手机首页。
|
||||||
historyBack() {
|
historyBack() {
|
||||||
|
if (window.mobilePopupHistory.back()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
window.history.back()
|
window.history.back()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
window.location.replace('/mobile/index')
|
window.location.replace('/mobile/index')
|
||||||
},
|
},
|
||||||
|
/** key 为当前页注册的弹框字段名,visible 为 Vant 关闭事件;历史回退后统一通过 $set 同步。 */
|
||||||
|
syncSignPopup(key, visible) {
|
||||||
|
if (!visible) {
|
||||||
|
brandTrainPopupHistory.close(key)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
openSignedPoints() {
|
||||||
|
if (!brandTrainPopupHistory.open('signedPointsPopup')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 每次打开从首个点位展示,滚动位置不写入浏览器历史栈。
|
||||||
|
this.$nextTick(() => {
|
||||||
|
if (this.$refs.signPointsList) {
|
||||||
|
this.$refs.signPointsList.scrollTop = 0
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
closeSignedPoints() {
|
||||||
|
brandTrainPopupHistory.close('signedPointsPopup')
|
||||||
|
},
|
||||||
|
/** attendTime 为服务端的首次签到时间;返回本地展示字符串,历史缺失或非法时间显示“未记录”。 */
|
||||||
|
formatAttendTime(attendTime) {
|
||||||
|
const time = attendTime ? moment(attendTime) : null
|
||||||
|
return time && time.isValid() ? time.format('YYYY-MM-DD HH:mm:ss') : '未记录'
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* courseId 为二维码中的课程点位 ID;本接口只预览,不签到、不补报名,也不写入福利名单。
|
* courseId 为二维码中的课程点位 ID;本接口只预览,不签到、不补报名,也不写入福利名单。
|
||||||
* 返回 Result.data,包含进度、地点坐标 [经度, 纬度]、currentPointSigned、signRadius 及达标后的操作配置。
|
* 返回 Result.data,包含进度、地点坐标 [经度, 纬度]、currentPointSigned、signRadius 及达标后的操作配置。
|
||||||
|
* signedPoints 为当前用户已打卡点位数组,每项包含 courseId、courseName、courseLocation、attendTime。
|
||||||
|
* signPoints 为全部点位数组,在上述字段基础上增加 signed 布尔值;未打卡点位 attendTime 为空。
|
||||||
*/
|
*/
|
||||||
getSignData() {
|
getSignData() {
|
||||||
if (!this.courseId) {
|
if (!this.courseId) {
|
||||||
@@ -547,7 +892,7 @@ layout("/mobile/platform.html"){
|
|||||||
unsupported: '当前环境不支持此定位途径,请更换浏览器或在微信中重试。',
|
unsupported: '当前环境不支持此定位途径,请更换浏览器或在微信中重试。',
|
||||||
sensor: '设备定位传感器未开启,请开启设备定位后重试。',
|
sensor: '设备定位传感器未开启,请开启设备定位后重试。',
|
||||||
geocoder: '腾讯逆地址解析服务异常(10601),请联系管理员检查该 Key 的服务权限、额度及访问限制。',
|
geocoder: '腾讯逆地址解析服务异常(10601),请联系管理员检查该 Key 的服务权限、额度及访问限制。',
|
||||||
conversion: '腾讯坐标转换服务异常(10602),请联系管理员检查该 Key 的服务权限、额度及访问限制。'
|
// conversion: '腾讯坐标转换服务异常(10602),请联系管理员检查该 Key 的服务权限、额度及访问限制。'
|
||||||
}
|
}
|
||||||
let category = categories[status]
|
let category = categories[status]
|
||||||
if (status === 10001 || status === 0) {
|
if (status === 10001 || status === 0) {
|
||||||
@@ -557,6 +902,8 @@ layout("/mobile/platform.html"){
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const error = new Error(messages[category] || '腾讯定位未成功,请重新定位;若持续失败,请联系管理员查看定位诊断。')
|
const error = new Error(messages[category] || '腾讯定位未成功,请重新定位;若持续失败,请联系管理员查看定位诊断。')
|
||||||
|
// 临时取消定位限制期间,坐标转换失败仅保留诊断,不在页面或 Toast 中提示。
|
||||||
|
error.silent = category === 'conversion'
|
||||||
error.diagnostic = {
|
error.diagnostic = {
|
||||||
status: status,
|
status: status,
|
||||||
message: typeof source.message === 'string' ? source.message : '',
|
message: typeof source.message === 'string' ? source.message : '',
|
||||||
@@ -579,7 +926,6 @@ layout("/mobile/platform.html"){
|
|||||||
this.$set(this, 'locating', true)
|
this.$set(this, 'locating', true)
|
||||||
this.$set(this, 'locationError', '')
|
this.$set(this, 'locationError', '')
|
||||||
this.$set(this, 'currentPosition', [])
|
this.$set(this, 'currentPosition', [])
|
||||||
this.$set(this, 'locationAccuracy', null)
|
|
||||||
this.$set(this, 'distance', null)
|
this.$set(this, 'distance', null)
|
||||||
if (brandTrainCurrentMarker) {
|
if (brandTrainCurrentMarker) {
|
||||||
brandTrainCurrentMarker.setGeometries([])
|
brandTrainCurrentMarker.setGeometries([])
|
||||||
@@ -615,9 +961,6 @@ layout("/mobile/platform.html"){
|
|||||||
}
|
}
|
||||||
this.$set(this, 'currentPosition', coordinates)
|
this.$set(this, 'currentPosition', coordinates)
|
||||||
this.$set(this, 'distance', distance)
|
this.$set(this, 'distance', distance)
|
||||||
// 精度仅供用户判断设备定位质量,不抵扣距离,也不放宽管理员配置的签到半径。
|
|
||||||
this.$set(this, 'locationAccuracy', Number.isFinite(res.accuracy) && res.accuracy >= 0
|
|
||||||
? res.accuracy : null)
|
|
||||||
brandTrainCurrentMarker.setGeometries([{
|
brandTrainCurrentMarker.setGeometries([{
|
||||||
id: 'current-user',
|
id: 'current-user',
|
||||||
styleId: 'current',
|
styleId: 'current',
|
||||||
@@ -648,9 +991,8 @@ layout("/mobile/platform.html"){
|
|||||||
}
|
}
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
if (!brandTrainMapDestroyed) {
|
if (!brandTrainMapDestroyed) {
|
||||||
this.$set(this, 'locationError', err.message || '定位失败,请重新定位。')
|
this.$set(this, 'locationError', err.silent ? '' : err.message || '定位失败,请重新定位。')
|
||||||
this.$set(this, 'currentPosition', [])
|
this.$set(this, 'currentPosition', [])
|
||||||
this.$set(this, 'locationAccuracy', null)
|
|
||||||
this.$set(this, 'distance', null)
|
this.$set(this, 'distance', null)
|
||||||
if (brandTrainCurrentMarker) {
|
if (brandTrainCurrentMarker) {
|
||||||
brandTrainCurrentMarker.setGeometries([])
|
brandTrainCurrentMarker.setGeometries([])
|
||||||
@@ -688,36 +1030,38 @@ layout("/mobile/platform.html"){
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
return this.locatePosition().catch((err) => {
|
return this.locatePosition().catch((err) => {
|
||||||
if (!brandTrainMapDestroyed) {
|
if (!brandTrainMapDestroyed && !err.silent) {
|
||||||
this.$toast(err.message || '定位失败,请重新定位。')
|
this.$toast(err.message || '定位失败,请重新定位。')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 点击后重新定位并提交 courseId、longitude(经度)、latitude(纬度);不能复用打开页面时的旧位置。
|
* 点击后提交 courseId(当前二维码的课程点位 ID);临时取消定位限制期间不传 longitude、latitude。
|
||||||
* 返回 Result,code=0 表示签到成功、99 为业务提示,data 沿用预览结构;服务端再次校验配置半径。
|
* 返回 Result,code=0 表示签到成功、99 为业务提示,data 沿用预览结构,包含签到状态与活动进度。
|
||||||
*/
|
*/
|
||||||
submitSign() {
|
submitSign() {
|
||||||
if (this.signDisabled) {
|
if (this.signDisabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$set(this, 'signLoading', true)
|
this.$set(this, 'signLoading', true)
|
||||||
return this.locatePosition().then((coordinates) => {
|
// 腾讯定位服务余额耗尽,临时跳过提交前重新定位和距离判断;恢复时需同步恢复后端校验。
|
||||||
if (!this.isInRange) {
|
// return this.locatePosition().then((coordinates) => {
|
||||||
throw new Error('您距签到地点约' + this.distanceText + '米,请在' + this.signRadius + '米范围内签到。')
|
// if (!this.isInRange) {
|
||||||
}
|
// throw new Error('您距签到地点约' + this.distanceText + '米,请在' + this.signRadius + '米范围内签到。')
|
||||||
|
// }
|
||||||
return this.$axios.post('/platform/mobile/brandTrain/scanCode/doSign', {
|
return this.$axios.post('/platform/mobile/brandTrain/scanCode/doSign', {
|
||||||
courseId: this.courseId,
|
courseId: this.courseId,
|
||||||
longitude: coordinates[0],
|
// longitude: coordinates[0],
|
||||||
latitude: coordinates[1]
|
// latitude: coordinates[1]
|
||||||
}).then((res) => {
|
}).then((res) => {
|
||||||
// 业务失败且未返回预览数据时保留地图和现有结果,用户可重新定位后重试。
|
// 业务失败且未返回预览数据时保留地图和现有结果,用户可再次提交重试。
|
||||||
if (res.data) {
|
if (res.data) {
|
||||||
this.$set(this, 'result', res)
|
this.$set(this, 'result', res)
|
||||||
}
|
}
|
||||||
this.$toast(res.msg || (res.code === 0 ? '签到成功' : '签到失败,请稍后重试'))
|
this.$toast(res.msg || (res.code === 0 ? '签到成功' : '签到失败,请稍后重试'))
|
||||||
})
|
})
|
||||||
}).catch((err) => {
|
// }) // 恢复定位时,与上方 locatePosition 回调及经纬度参数一起解除注释。
|
||||||
|
.catch((err) => {
|
||||||
if (!brandTrainMapDestroyed) {
|
if (!brandTrainMapDestroyed) {
|
||||||
this.$toast(err.msg || err.message || '签到失败,请稍后重试')
|
this.$toast(err.msg || err.message || '签到失败,请稍后重试')
|
||||||
}
|
}
|
||||||
@@ -729,17 +1073,19 @@ layout("/mobile/platform.html"){
|
|||||||
},
|
},
|
||||||
// 使用字段副本编辑,取消填写时保留后端返回的原始字段和已保存内容。
|
// 使用字段副本编辑,取消填写时保留后端返回的原始字段和已保存内容。
|
||||||
openDynamicForm() {
|
openDynamicForm() {
|
||||||
|
if (!this.showDynamicFormButton || this.signLoading || this.formLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
if (!Array.isArray(this.signData.dynamicColumnsData) || this.signData.dynamicColumnsData.length === 0) {
|
if (!Array.isArray(this.signData.dynamicColumnsData) || this.signData.dynamicColumnsData.length === 0) {
|
||||||
this.$toast('当前课程未配置动态表单')
|
this.$toast('当前课程未配置动态表单')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
this.$set(this, 'dynamicColumnsData', JSON.parse(JSON.stringify(this.signData.dynamicColumnsData)))
|
this.$set(this, 'dynamicColumnsData', JSON.parse(JSON.stringify(this.signData.dynamicColumnsData)))
|
||||||
this.$set(this, 'dynamicFormPopup', true)
|
brandTrainPopupHistory.open('dynamicFormPopup')
|
||||||
},
|
},
|
||||||
closeDynamicForm() {
|
closeDynamicForm() {
|
||||||
if (!this.formLoading) {
|
// NavBar 与系统返回一致:只关闭最上层;保存未完成时由 formLoading 禁止重新发起操作。
|
||||||
this.$set(this, 'dynamicFormPopup', false)
|
window.mobilePopupHistory.back()
|
||||||
}
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* 提交当前 courseId 及 mobileColumnsValue 字段值 JSON 数组;字段沿用现有动态表单的名称、编码和控件类型。
|
* 提交当前 courseId 及 mobileColumnsValue 字段值 JSON 数组;字段沿用现有动态表单的名称、编码和控件类型。
|
||||||
@@ -773,8 +1119,8 @@ layout("/mobile/platform.html"){
|
|||||||
this.$set(this.signData, 'dynamicColumnsData', JSON.parse(JSON.stringify(this.dynamicColumnsData)))
|
this.$set(this.signData, 'dynamicColumnsData', JSON.parse(JSON.stringify(this.dynamicColumnsData)))
|
||||||
this.$set(this.signData, 'needDynamicForm', false)
|
this.$set(this.signData, 'needDynamicForm', false)
|
||||||
this.$set(this.signData, 'handleMsg', '当前活动已达标,详细信息已保存')
|
this.$set(this.signData, 'handleMsg', '当前活动已达标,详细信息已保存')
|
||||||
this.$set(this, 'dynamicFormPopup', false)
|
// 等待弹框及其子选择器的历史全部退出后再结束提交,返回时不残留已关闭表单。
|
||||||
this.$toast('保存成功')
|
return brandTrainPopupHistory.clear().then(() => this.$toast('保存成功'))
|
||||||
} else {
|
} else {
|
||||||
this.$toast(res.msg || '保存失败,请稍后重试')
|
this.$toast(res.msg || '保存失败,请稍后重试')
|
||||||
}
|
}
|
||||||
@@ -794,14 +1140,21 @@ layout("/mobile/platform.html"){
|
|||||||
this.$toast('当前活动未配置福利跳转地址')
|
this.$toast('当前活动未配置福利跳转地址')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
return brandTrainPopupHistory.clear().then(() => {
|
||||||
window.location.href = this.signData.welfareJumpUrl
|
window.location.href = this.signData.welfareJumpUrl
|
||||||
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
|
// 页面和表单子组件共用全局历史栈,页面销毁时注销本页回调。
|
||||||
|
brandTrainPopupHistory = window.mobilePopupHistory.register('brand-train-sign-' + this._uid, (key, visible) => {
|
||||||
|
this.$set(this, key, visible)
|
||||||
|
})
|
||||||
this.$set(this, 'courseId', GetQueryString('courseId') || '')
|
this.$set(this, 'courseId', GetQueryString('courseId') || '')
|
||||||
this.getSignData()
|
this.getSignData()
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
|
brandTrainPopupHistory.unregister()
|
||||||
brandTrainMapDestroyed = true
|
brandTrainMapDestroyed = true
|
||||||
brandTrainLocationRequestId += 1
|
brandTrainLocationRequestId += 1
|
||||||
window.clearTimeout(brandTrainLocationTimer)
|
window.clearTimeout(brandTrainLocationTimer)
|
||||||
|
|||||||
@@ -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,6 +213,8 @@ layout("/layouts/platform.html"){
|
|||||||
return {
|
return {
|
||||||
url: '',
|
url: '',
|
||||||
courseDialog: false,
|
courseDialog: false,
|
||||||
|
courseCodeDialog: false,
|
||||||
|
courseCodeImage: '',
|
||||||
trainType: '',
|
trainType: '',
|
||||||
trainTypeList: [],
|
trainTypeList: [],
|
||||||
welfareProjectList: [],
|
welfareProjectList: [],
|
||||||
@@ -254,11 +266,13 @@ layout("/layouts/platform.html"){
|
|||||||
const qrText = this.isSignExtensionEnabled(this.clickRow)
|
const qrText = this.isSignExtensionEnabled(this.clickRow)
|
||||||
? window.location.origin + '/platform/mobile/brandTrain/scanCode?courseId=' + encodeURIComponent(row.id)
|
? window.location.origin + '/platform/mobile/brandTrain/scanCode?courseId=' + encodeURIComponent(row.id)
|
||||||
: JSON.stringify(o)
|
: JSON.stringify(o)
|
||||||
const content = jrQrcode.getQrBase64(qrText)
|
// 每次先更新当前课程的二维码图片,再打开弹框,避免切换课程时显示上一张二维码。
|
||||||
let image = new Image()
|
this.$set(this, 'courseCodeImage', jrQrcode.getQrBase64(qrText))
|
||||||
image.src = content
|
this.$set(this, 'courseCodeDialog', true)
|
||||||
let viewer = new Viewer(image)
|
},
|
||||||
viewer.show()
|
// 关闭二维码时保留课程列表,便于继续生成其他课程的签到二维码。
|
||||||
|
closeCourseCode() {
|
||||||
|
this.$set(this, 'courseCodeDialog', false)
|
||||||
},
|
},
|
||||||
// 只有全部课程签到且配置了正整数点位数的活动,才启用手机端扩展流程。
|
// 只有全部课程签到且配置了正整数点位数的活动,才启用手机端扩展流程。
|
||||||
isSignExtensionEnabled(activity) {
|
isSignExtensionEnabled(activity) {
|
||||||
|
|||||||
Reference in New Issue
Block a user