|
|
@@ -0,0 +1,173 @@
|
|
|
+<script setup lang="ts">
|
|
|
+/**
|
|
|
+ * 微信 webview 授权落地页(views/wechat/callback.vue;公开路由,守卫 meta.public)。
|
|
|
+ *
|
|
|
+ * 职责:移动端 App 内微信授权(snsapi_userinfo)的回调落地——
|
|
|
+ * 1. 从 URL query 读取微信重定向回带的 code + state;
|
|
|
+ * 2. 调 GET /api/auth/wechat/callback?code=&state=(后端 code 换 openid → 查 t_identity(wechat) →
|
|
|
+ * 发 loginTicket 写回 state=success);
|
|
|
+ * 3. success → 拿 loginTicket + memberships 走**与账密登录完全同一套** ② /api/auth/issue 签 token
|
|
|
+ * (移动端自动取 memberships[0] 立即 ②)→ applyLogin → scope.set → 跳首页;
|
|
|
+ * 4. unbound/fail/error → 展示后端 message 直显,提供"返回登录"按钮。
|
|
|
+ *
|
|
|
+ * 与 PC 扫码差异:PC 端前端轮询 status 拿到 success 的 loginTicket;本端是微信直接重定向回来,
|
|
|
+ * 前端落地页直接调 callback 端点拿 loginTicket(后端 state 状态机同一套,callback 对两 scope 通用)。
|
|
|
+ *
|
|
|
+ * SFC 骨架顺序(编码规范):imports → store 注入 → 状态 → 生命周期(onMounted) → 方法。
|
|
|
+ *
|
|
|
+ * 注:模板 van-* 组件经 main.ts 的 app.use(Vant) 全局注册,本页**不 import Vant 组件**
|
|
|
+ * (仅 import Vant 命令式 API 作按需增强;与 PC 端"组件走全局、命令式按需引"一致)。
|
|
|
+ */
|
|
|
+import { fetchWechatCallback } from '@unified/shared/api/auth/wechat';
|
|
|
+import { issue, type MembershipView } from '@unified/shared/api/auth/login';
|
|
|
+import { useLogin } from '@unified/shared/store/login';
|
|
|
+import { useScope } from '@unified/shared/store/scope';
|
|
|
+
|
|
|
+const loginStore = useLogin();
|
|
|
+const scope = useScope();
|
|
|
+/** 路由实例(SPA 跳转,免全页刷新;裸标识符经 vite define 注入为 VueRouter.useRouter) */
|
|
|
+const router = useRouter();
|
|
|
+/** 当前路由 query(code/state 由微信重定向回带) */
|
|
|
+const route = useRoute();
|
|
|
+
|
|
|
+/** 页面状态机:processing 处理中 / error 失败(含 unbound/fail/网络异常) */
|
|
|
+const status = ref<'processing' | 'error'>('processing');
|
|
|
+/** 错误文案(后端 message 直显;processing 时为空) */
|
|
|
+const errorMsg = ref('');
|
|
|
+
|
|
|
+/**
|
|
|
+ * 执行回调换票 + ② issue(onMounted 触发;幂等——已登录直接跳首页,防重复消费 state)。
|
|
|
+ *
|
|
|
+ * 出参:无(副作用:跳首页或写 errorMsg)。
|
|
|
+ * 异常场景:
|
|
|
+ * - 缺 code/state:errorMsg 提示"授权参数缺失",status=error;
|
|
|
+ * - callback code=1001(state 失效/已终态):errorMsg 提示"授权会话已失效,请重新授权";
|
|
|
+ * - callback code=4003(微信换 openid 失败):errorMsg 提示"微信授权失败,请重试";
|
|
|
+ * - callback status=unbound:errorMsg 提示"该微信号未绑定账号,请使用账号密码登录";
|
|
|
+ * - ② issue code≠200(1001 票据失效 / 1003 越权):errorMsg = 后端 message;
|
|
|
+ * - 网络层 reject:errorMsg = "网络连接异常"。
|
|
|
+ */
|
|
|
+async function handleCallback(): Promise<void> {
|
|
|
+ // 幂等:已登录直接跳首页(防刷新/重复访问二次消费 state)
|
|
|
+ if (loginStore.isLoggedIn) {
|
|
|
+ await router.replace('/');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const code = (route.query.code as string) ?? '';
|
|
|
+ const state = (route.query.state as string) ?? '';
|
|
|
+ if (!code || !state) {
|
|
|
+ status.value = 'error';
|
|
|
+ errorMsg.value = '授权参数缺失(缺 code 或 state),请重新发起微信授权';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ status.value = 'processing';
|
|
|
+ errorMsg.value = '';
|
|
|
+ try {
|
|
|
+ // ① 后端 callback:code 换 openid → 查绑定 → 发 loginTicket 写回 state=success
|
|
|
+ const cb = await fetchWechatCallback(code, state);
|
|
|
+ if (cb.code !== 200 || !cb.data) {
|
|
|
+ status.value = 'error';
|
|
|
+ errorMsg.value = cb.message || '微信授权失败';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const { status: cbStatus, loginTicket, memberships } = cb.data;
|
|
|
+ // unbound:该微信号未绑定本平台账号
|
|
|
+ if (cbStatus === 'unbound') {
|
|
|
+ status.value = 'error';
|
|
|
+ errorMsg.value = '该微信号未绑定账号,请使用账号密码登录';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // fail:微信换 openid 失败
|
|
|
+ if (cbStatus === 'fail') {
|
|
|
+ status.value = 'error';
|
|
|
+ errorMsg.value = '微信授权失败,请重试';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // success:携 loginTicket + memberships 走与账密登录同一套 ② issue
|
|
|
+ if (cbStatus !== 'success' || !loginTicket) {
|
|
|
+ status.value = 'error';
|
|
|
+ errorMsg.value = '授权状态异常,请重试';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const list: MembershipView[] = memberships ?? [];
|
|
|
+ if (list.length === 0) {
|
|
|
+ status.value = 'error';
|
|
|
+ errorMsg.value = '该账号暂无机构归属,请联系管理员分配机构';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 移动端无独立作用域选择页:自动取第一条作用域完成 ②(与账密登录同源)
|
|
|
+ const m = list[0];
|
|
|
+ const pair = await issue({
|
|
|
+ loginTicket,
|
|
|
+ institutionId: m.institutionId,
|
|
|
+ campusId: m.campusId,
|
|
|
+ });
|
|
|
+ if (pair.code === 200 && pair.data) {
|
|
|
+ loginStore.applyLogin(pair.data); // 写 token + roles/permissions/tabTree
|
|
|
+ scope.set({ institutionId: pair.data.institutionId, campusId: pair.data.campusId });
|
|
|
+ await router.replace('/');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ status.value = 'error';
|
|
|
+ errorMsg.value = pair.message || '登录失败';
|
|
|
+ } catch {
|
|
|
+ status.value = 'error';
|
|
|
+ errorMsg.value = '网络连接异常';
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 微信重定向回来后页面首帧即读 code/state 执行换票(onMounted 一次性触发)
|
|
|
+onMounted(() => {
|
|
|
+ void handleCallback();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div class="wechat-callback">
|
|
|
+ <!-- 处理中:占位加载态(微信重定向→换票→② issue 全程) -->
|
|
|
+ <div v-if="status === 'processing'" class="wechat-callback__status">
|
|
|
+ <van-loading size="32px" color="#07c160" />
|
|
|
+ <p class="wechat-callback__text">微信授权处理中,请稍候…</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 失败:后端 message 直显 + 返回登录 -->
|
|
|
+ <div v-else class="wechat-callback__status">
|
|
|
+ <van-icon name="warning" size="56" color="#ee0a24" />
|
|
|
+ <p class="wechat-callback__text wechat-callback__text--error">{{ errorMsg }}</p>
|
|
|
+ <van-button round type="primary" class="wechat-callback__btn" @click="router.push('/login')">
|
|
|
+ 返回登录
|
|
|
+ </van-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.wechat-callback {
|
|
|
+ min-height: 100vh;
|
|
|
+ padding: var(--app-spacing-lg);
|
|
|
+ background: var(--app-bg-page);
|
|
|
+ box-sizing: border-box;
|
|
|
+}
|
|
|
+.wechat-callback__status {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ gap: var(--app-spacing-md);
|
|
|
+ padding-top: 30vh;
|
|
|
+}
|
|
|
+.wechat-callback__text {
|
|
|
+ margin: 0;
|
|
|
+ font-size: var(--app-font-md);
|
|
|
+ color: var(--app-text-secondary);
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.wechat-callback__text--error {
|
|
|
+ color: var(--app-text-error, #ee0a24);
|
|
|
+}
|
|
|
+.wechat-callback__btn {
|
|
|
+ margin-top: var(--app-spacing-md);
|
|
|
+ min-width: 200px;
|
|
|
+}
|
|
|
+</style>
|