diff --git a/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/mp/controller/MpH5AuthController.java b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/mp/controller/MpH5AuthController.java index dcc2244..6bf330f 100644 --- a/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/mp/controller/MpH5AuthController.java +++ b/backend/xt-server/src/main/java/cn/tianbaotong/fill/module/mp/controller/MpH5AuthController.java @@ -125,29 +125,44 @@ public class MpH5AuthController { /** * 302 回 H5 页面。token 放 fragment(不发给服务器),错误信息放 query。 */ + /** + * 302 回 H5 页面。 + * + *

token 放在 hash 路由的 query 里,形如 + * {@code http://host/#/pages/index/index?token=X&scene=Y}。这个格式同时满足两件事: + *

+ * 换句话说:参数要么放外层 query(会进服务器日志),要么放 hash 路由的 query, + * 别放裸 hash。 + */ private ResponseEntity redirectToApp(String token, String scene, String error) { String base = props.mpH5() == null ? null : props.mpH5().h5BaseUrlOrDefault(); if (base == null || base.isBlank()) { base = "http://127.0.0.1:5174"; } - StringBuilder sb = new StringBuilder(base); - String frag = ""; + StringBuilder q = new StringBuilder(); if (token != null) { - frag += "token=" + enc(token); + q.append("token=").append(enc(token)); } if (scene != null && !scene.isBlank()) { - frag += (frag.isEmpty() ? "" : "&") + "scene=" + enc(scene); + q.append(q.isEmpty() ? "" : "&").append("scene=").append(enc(scene)); } + String url = base; if (error != null) { - // 错误放 query:它不敏感,而且我们要让页面能读到 - sb.append("?authError=").append(enc(error)); + // 错误放外层 query:它不敏感,而且页面要能读到 + url += "?authError=" + enc(error); } - if (!frag.isEmpty()) { - sb.append("#").append(frag); + if (!q.isEmpty()) { + // hash 路由格式:uni-app 能解析,且整段不发给服务器 + url += "#/pages/index/index?" + q; } - return ResponseEntity.status(HttpStatus.FOUND) - .header("Location", sb.toString()) - .build(); + return ResponseEntity.status(HttpStatus.FOUND).header("Location", url).build(); } private static String enc(String s) { diff --git a/frontend-mp/src/pages.json b/frontend-mp/src/pages.json index e5f5701..36be32e 100644 --- a/frontend-mp/src/pages.json +++ b/frontend-mp/src/pages.json @@ -8,6 +8,14 @@ "navigationBarTextStyle": "white", "enablePullDownRefresh": true } + }, + { + "path": "pages/verify/verify", + "style": { + "navigationBarTitleText": "核对身份", + "navigationBarBackgroundColor": "#0052D9", + "navigationBarTextStyle": "white" + } } ], "globalStyle": { diff --git a/frontend-mp/src/pages/index/index.vue b/frontend-mp/src/pages/index/index.vue index 16ec0f9..109fc47 100644 --- a/frontend-mp/src/pages/index/index.vue +++ b/frontend-mp/src/pages/index/index.vue @@ -89,6 +89,25 @@ const isError = ref(false) const loading = ref(false) const title = ref('材料填报') +/** + * 当前 URL 里的 scene(授权回调回来时在 hash 路由的 query 里)。 + * + * ⚠️ 为什么跳转时要把它带上:**H5 的页面跳转不保留 URL 参数**, + * 直接 navigateTo 到核验页的话,那边读不到 scene、就不知道是哪个任务。 + * 这是 H5 与小程序的一个真实差异(小程序可以靠 scene 直进,H5 得自己带)。 + * 所以这里显式拼参数,核验页再从自己的 URL 里读出来。 + */ +function sceneFromUrl(): string { + try { + const hash = typeof location !== 'undefined' ? location.hash : '' + const q = hash.includes('?') ? hash.slice(hash.indexOf('?') + 1) : '' + const params = new URLSearchParams(q) + return params.get('scene') || '' + } catch { + return '' + } +} + const badgeClass = computed(() => { switch (ctx.value.taskStatus) { case 1: return 'land__badge--ok' @@ -154,18 +173,42 @@ async function load() { } function start() { - // 填报主流程属 M3(/mp/login → 门禁核验 → 表单),当前骨架先给出明确预期, - // 而不是静默什么都不做 —— 后者会被当成"按钮坏了" - uni.showModal({ - title: '填报功能开发中', - content: `已识别任务「${ctx.value.taskName ?? ''}」。填报主流程(身份核验与表单填写)属下一阶段开发。`, - showCancel: false - }) + const s = scene.value || sceneFromUrl() + if (!s) { + uni.showModal({ + title: '无法继续', + content: '没有取到任务信息,请重新扫班主任发的二维码进入。', + showCancel: false + }) + return + } + // 带上 scene —— 见 sceneFromUrl 的说明(H5 跳转不保留 URL 参数) + uni.navigateTo({ url: `/pages/verify/verify?scene=${encodeURIComponent(s)}` }) } onLoad((options) => { rawScene.value = options?.scene || options?.q || '' scene.value = pickScene(options) + + // 授权回调失败时后端会把原因放在外层 query 的 authError 里(不敏感,可以进服务器日志) + const authErr = options?.authError + if (authErr) { + isError.value = true + message.value = decodeURIComponent(String(authErr)) + return + } + + // 授权回调会把会话 token 带回来(在 hash 路由的 query 里,不进服务器日志)。 + // 存下来供后续页面用 —— 门禁核验、提交都要靠它。 + const t = options?.token + if (t) { + try { + uni.setStorageSync('token', t) + } catch { + // 存不了也不该让页面挂掉:落地页本身不需要 token + } + } + load() }) @@ -192,6 +235,12 @@ page { min-height: 100vh; padding: 32rpx; + /* 桌面浏览器打开时不至于拉满全屏 —— 主战场是手机, + 但自动化验证与临时桌面查看都跑在桌面浏览器里,窄一点更像真实形态 */ + max-width: 720rpx; + margin: 0 auto; + box-sizing: border-box; + &__hero { padding: 48rpx 32rpx; background: linear-gradient(135deg, $primary 0%, #2b6fe0 100%); diff --git a/frontend-mp/src/pages/verify/verify.vue b/frontend-mp/src/pages/verify/verify.vue new file mode 100644 index 0000000..c9f92a2 --- /dev/null +++ b/frontend-mp/src/pages/verify/verify.vue @@ -0,0 +1,382 @@ + + + + +