diff --git a/pom.xml b/pom.xml index 34586ee..11deb30 100644 --- a/pom.xml +++ b/pom.xml @@ -22,7 +22,7 @@ 2.5.1 4.1.2 4.0.11 - 1.18.12 + 1.18.32 UTF-8 1.0.1 @@ -463,9 +463,12 @@ maven-compiler-plugin 3.7.0 - 15 - 15 - --enable-preview + 17 + 17 + + --add-opens + java.base/java.lang=ALL-UNNAMED + false diff --git a/src/main/java/io/v/nutz/brandtrainactivity/controller/mobile/BrandTrainScanCodeController.java b/src/main/java/io/v/nutz/brandtrainactivity/controller/mobile/BrandTrainScanCodeController.java index 4725683..def3f9f 100644 --- a/src/main/java/io/v/nutz/brandtrainactivity/controller/mobile/BrandTrainScanCodeController.java +++ b/src/main/java/io/v/nutz/brandtrainactivity/controller/mobile/BrandTrainScanCodeController.java @@ -54,8 +54,8 @@ public class BrandTrainScanCodeController { * 用户在签到地图中手动确认签到,浏览页面及获取定位本身均不写入签到记录。 * * @param courseId 二维码携带的品牌课程ID,当前用户从服务端登录态取得 - * @param longitude 当前腾讯定位的经度,须与地图显示点位同为GCJ-02坐标 - * @param latitude 当前定位的纬度,与经度共同用于服务端配置半径的范围校验 + * @param longitude 保留兼容的GCJ-02经度,腾讯地图余额耗尽、临时停用定位校验期间可不传 + * @param latitude 保留兼容的GCJ-02纬度,临时停用定位和范围校验期间可不传 * @return Result的code/msg/data;0签到成功,99业务提示,已签到时data仍保留当前状态和达标事项 */ @At @@ -67,9 +67,10 @@ public class BrandTrainScanCodeController { if (StrUtil.isBlank(courseId)) { return Result.error(99, "缺少签到点位信息"); } - if (longitude == null || latitude == null) { - 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")); diff --git a/src/main/java/io/v/nutz/brandtrainactivity/service/BrandTrainSignUpActivityService.java b/src/main/java/io/v/nutz/brandtrainactivity/service/BrandTrainSignUpActivityService.java index 66e22fa..0bf33da 100644 --- a/src/main/java/io/v/nutz/brandtrainactivity/service/BrandTrainSignUpActivityService.java +++ b/src/main/java/io/v/nutz/brandtrainactivity/service/BrandTrainSignUpActivityService.java @@ -143,12 +143,13 @@ public interface BrandTrainSignUpActivityService extends ViService records = dao().query(BrandTrainSignUpUserCourse.class, @@ -1015,11 +1016,14 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl() : ""); dynamicColumns.add(columnMap); } + List 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()) @@ -1040,6 +1044,67 @@ public class BrandTrainSignUpActivityServiceImpl extends ViServiceImpl buildScanCodeSignedPoints(String activityId, List attendRecords) { + Map 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 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 buildScanCodePoints(String activityId, List signedPoints) { + Map signedByCourse = new LinkedHashMap<>(); + signedPoints.forEach(point -> signedByCourse.put(point.getString("courseId"), point)); + Map points = new LinkedHashMap<>(); + List 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) { diff --git a/src/main/java/io/v/nutz/web/commons/ext/map/TMapService.java b/src/main/java/io/v/nutz/web/commons/ext/map/TMapService.java index d88e6fa..7b77974 100644 --- a/src/main/java/io/v/nutz/web/commons/ext/map/TMapService.java +++ b/src/main/java/io/v/nutz/web/commons/ext/map/TMapService.java @@ -21,7 +21,7 @@ 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 = "L5WBZ-3DDKH-KW2DC-WCDWL-Y3SUV-WNBDI"; + private static final String DEFAULT_TENCENT_MAP_KEY = "NYNBZ-K62KI-DQJGU-U4NJ5-CSME7-ULFMU"; /** * 读取腾讯地图地址候选,仅访问固定的地点搜索服务,不修改本地数据。 diff --git a/src/main/resources/static/assets/mobile/js/popupHistory.js b/src/main/resources/static/assets/mobile/js/popupHistory.js new file mode 100644 index 0000000..c84a1c8 --- /dev/null +++ b/src/main/resources/static/assets/mobile/js/popupHistory.js @@ -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 + } + } +})() diff --git a/src/main/resources/static/components/module/brandTrainSignUp/BrandTrainDynamicForm.vue b/src/main/resources/static/components/module/brandTrainSignUp/BrandTrainDynamicForm.vue index 0437b71..74a0cea 100644 --- a/src/main/resources/static/components/module/brandTrainSignUp/BrandTrainDynamicForm.vue +++ b/src/main/resources/static/components/module/brandTrainSignUp/BrandTrainDynamicForm.vue @@ -148,6 +148,24 @@ module.exports = { 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) @@ -162,6 +180,8 @@ module.exports = { return { //所有的picker状态 pickerStates: {}, + popupHistory: null, + imagePreview: null, fileLists: {}, fileValueSnapshots: {}, addressStates: {}, @@ -454,6 +474,10 @@ module.exports = { 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)) { //说明是普通的下拉框 @@ -613,13 +637,37 @@ module.exports = { return } if (item.isImage) { - vant.ImagePreview([item.url]) + // 图片预览位于表单之上,也作为独立历史层,返回时先关预览再关表单。 + 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') { @@ -627,6 +675,14 @@ module.exports = { // } // }) // console.log(this.dynamicData) + }, + beforeDestroy() { + if (this.imagePreview) { + this.imagePreview.close() + } + if (this.popupHistory) { + this.popupHistory.unregister() + } } } diff --git a/src/main/resources/views/layouts/platform.html b/src/main/resources/views/layouts/platform.html index aaf730c..133bd89 100644 --- a/src/main/resources/views/layouts/platform.html +++ b/src/main/resources/views/layouts/platform.html @@ -71,7 +71,7 @@ - + diff --git a/src/main/resources/views/mobile/brandTrain/scanCode.html b/src/main/resources/views/mobile/brandTrain/scanCode.html index e8acebb..e1fb415 100644 --- a/src/main/resources/views/mobile/brandTrain/scanCode.html +++ b/src/main/resources/views/mobile/brandTrain/scanCode.html @@ -7,8 +7,11 @@ layout("/mobile/platform.html"){ display: flex; height: 100vh; height: 100dvh; + width: 100%; + min-width: 0; min-height: 0; flex-direction: column; + overflow-x: hidden; overflow-y: auto; background: #f4f7fc; } @@ -20,12 +23,14 @@ layout("/mobile/platform.html"){ .brand-train-sign-content { display: flex; flex: 1 0 auto; + min-width: 0; flex-direction: column; } .brand-train-sign-map { /* 固定 SDK 容器定位,防止地图控件溢出到信息区和底部按钮。 */ position: relative !important; + z-index: 0; flex: 1 0 240px; width: 100%; min-height: 240px; @@ -34,6 +39,9 @@ layout("/mobile/platform.html"){ .brand-train-sign-details { flex-shrink: 0; + min-width: 0; + max-width: 100%; + box-sizing: border-box; padding: 16px; background: #fff; } @@ -46,6 +54,24 @@ layout("/mobile/platform.html"){ 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 { display: flex; margin-bottom: 12px; @@ -56,6 +82,10 @@ layout("/mobile/platform.html"){ .brand-train-sign-location-tools .van-button { flex-shrink: 0; + height: 32px; + padding: 0 2px; + border: 0; + background: transparent; } .brand-train-sign-location-status { @@ -65,6 +95,20 @@ layout("/mobile/platform.html"){ 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 { margin-top: 6px; color: #78869a; @@ -79,28 +123,218 @@ layout("/mobile/platform.html"){ } .brand-train-sign-progress { - display: flex; - margin-top: 10px; - align-items: center; - flex-wrap: wrap; - gap: 8px; + min-width: 0; + max-width: 100%; + box-sizing: border-box; + margin-top: 14px; + padding: 12px; + border-radius: 10px; + background: #f4f7fc; color: #78869a; font-size: 13px; 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 { color: #ee0a24; } .brand-train-sign-footer { + /* 状态留在信息区,底栏仅容纳操作;双按钮平分宽度,单按钮居中且不铺满屏幕。 */ + position: sticky; + bottom: 0; + z-index: 3; display: flex; 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; - 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-bottom: calc(12px + env(safe-area-inset-bottom)); + border-top: 1px solid #edf0f5; background: #fff; + color: #78869a; + font-size: 12px; + line-height: 18px; + text-align: center; } .brand-train-sign-result { @@ -143,12 +377,13 @@ layout("/mobile/platform.html"){
+ :class="{'brand-train-sign-map-warning': mapError || locationError || distance != null && !isInRange, + 'is-in-range': !mapError && !locationError && !locating && isInRange}"> - - + + 重新加载地图 @@ -156,20 +391,31 @@ layout("/mobile/platform.html"){ :loading="locating" :disabled="!mapReady || signLoading" @click="refreshPosition">重新定位
-
{{signData.courseName}}
-
- {{signData.courseLocation || '未填写地点名称'}} +
+
{{signData.courseName}}
+ + 当前点位已签到 +
签到范围:{{signRadius}} 米
-
定位精度约 {{Math.ceil(locationAccuracy)}} 米
+
距签到地点约 {{distanceText}} 米
- 已签到 {{signData.signCount}} 个点位 / 达标需 {{signData.signPointCount}} 个 - 活动已达标 - 当前点位已签到 +
+ 活动签到进度 + 活动已达标 + +
+
已打卡 {{signData.signCount}} 个 / 达标需 {{signData.signPointCount}} 个
+
+ +
+
{{signProgressMessage}}
-
{{resultMessage}}
@@ -178,19 +424,45 @@ layout("/mobile/platform.html"){
{{isReachTarget ? '当前活动已达标' : currentPointSigned ? '当前点位已签到' : '签到提示'}}
{{resultMessage}}
- - + + +
+ +
+
+
+
{{point.courseName || '点位名称未填写'}}
+ 当前点位 +
+ + {{point.signed === true ? '已打卡' : '未打卡'}} + +
+
地点:{{point.courseLocation || '未填写地点名称'}}
+
+ 签到时间 + {{formatAttendTime(point.attendTime)}} +
+
+
+
+ {{isReachTarget ? '活动已达标,仍可继续打卡其他点位' : '完成 ' + signData.signPointCount + ' 个不同点位打卡即可达标'}} +
+
+ +
@@ -206,6 +478,7 @@ layout("/mobile/platform.html"){
+