|
|
@@ -2,20 +2,27 @@
|
|
|
/**
|
|
|
* 登录页(views/login/index.vue;公开路由,守卫 meta.public)。
|
|
|
*
|
|
|
- * 职责:两种登录方式 tab 切换——① 账号密码(POST /api/auth/login,真)② 微信 per-campus 授权
|
|
|
- * (占位接口,后端端点未就绪,UI 骨架完整可联调);成功则写 token-store + 初始化作用域
|
|
|
- * (移动端作用域来自登录响应 institutionId/campusId,无独立选择页)+ router.push 进首页;
|
|
|
- * 失败展示后端 message(直显,不包装)。
|
|
|
+ * 职责:两种登录方式 tab 切换——① 账号密码(两步:POST /api/auth/login → POST /api/auth/issue)
|
|
|
+ * ② 微信授权(占位:后端微信扫码端点为 PC 端扫码流程,移动端"App 内微信授权"接入方案待拍板,
|
|
|
+ * 此处显占位提示,不做假轮询);成功则写 token-store + 初始化作用域 + router.push 进首页;
|
|
|
+ * 失败展示后端 message(直显,不包装)。
|
|
|
+ *
|
|
|
+ * 两步登录流程(对齐后端 identity ADR-014 / T4 契约,2026-11-06):
|
|
|
+ * ① POST /login(验账密)→ loginTicket + user + memberships[](**不签 token**):
|
|
|
+ * - code=200 且 memberships 非空:移动端**无**独立作用域选择页 → 自动取 memberships[0]
|
|
|
+ * 立即 ② /issue(取该条 institution/campus)→ applyLogin → scope.set → 跳首页;
|
|
|
+ * - code=200 且 memberships 空:提示"该账号暂无机构归属,请联系管理员";
|
|
|
+ * - code≠200(1001 账号密码错 / 2001 参数 / 5002 系统):errorMsg 直显后端 message。
|
|
|
+ * ② POST /issue(凭 loginTicket + 选定作用域)→ TokenPair(access+refresh+roles+menuTree+permissions):
|
|
|
+ * - applyLogin 写 token + 同步 roles/permissions/tabTree;scope.set 设机构/校区作用域;跳首页。
|
|
|
* SFC 骨架顺序(编码规范):imports → props/emits(无)→ store 注入 → 状态 → 生命周期 → 方法。
|
|
|
*
|
|
|
* 注:模板 van-* 组件经 main.ts 的 app.use(Vant) 全局注册,本页**不 import Vant 组件**
|
|
|
* (仅 import Vant Message/Toast 命令式 API 作按需增强;与 PC 端"组件走全局、命令式按需引"一致)。
|
|
|
*/
|
|
|
-import { login } from '@unified/shared/api/auth/login';
|
|
|
-import { authorizeWechat, pollWechatStatus } from '@unified/shared/api/auth/wechat';
|
|
|
+import { issue, login } from '@unified/shared/api/auth/login';
|
|
|
import { useLogin } from '@unified/shared/store/login';
|
|
|
import { useScope } from '@unified/shared/store/scope';
|
|
|
-import { showToast } from 'vant';
|
|
|
import type { FieldRule } from 'vant';
|
|
|
|
|
|
const loginStore = useLogin();
|
|
|
@@ -32,43 +39,17 @@ const form = reactive({
|
|
|
password: '',
|
|
|
});
|
|
|
|
|
|
-/** 提交中(防重复提交;密码 tab 用) */
|
|
|
+/** 提交中(防重复提交;覆盖 ①② 两步全程) */
|
|
|
const submitting = ref(false);
|
|
|
/** 登录失败文案(后端 message 直显;空串 = 无错误;两 tab 共用) */
|
|
|
const errorMsg = ref('');
|
|
|
|
|
|
-/**
|
|
|
- * 微信授权态(占位,后端端点未就绪——接口路径/字段见 shared/api/auth/wechat.ts 占位注释)。
|
|
|
- * 入参:无。
|
|
|
- * 出参:
|
|
|
- * - authUrl string:授权 URL('' = 未取到,扫码/授权区展示"暂未开放"占位);
|
|
|
- * - polling boolean:是否正在轮询(驱动"授权中"文案)。
|
|
|
- * 异常场景:
|
|
|
- * - 后端未就绪(authorizeWechat reject):authUrl 保持 '',授权区展示占位文案。
|
|
|
- */
|
|
|
-const authUrl = ref('');
|
|
|
-const polling = ref(false);
|
|
|
-/** 轮询定时器句柄(组件卸载时必须清,防泄漏;占位接口下逻辑完整仍保留) */
|
|
|
-let pollTimer: ReturnType<typeof setInterval> | undefined;
|
|
|
-
|
|
|
/** 表单校验规则(显式 rules 对象,编码规范;trigger 为 Vant FieldValidateTrigger:onBlur/onChange/onSubmit) */
|
|
|
const rules: Record<'username' | 'password', FieldRule[]> = {
|
|
|
username: [{ required: true, message: '请输入用户名', trigger: 'onBlur' }],
|
|
|
password: [{ required: true, message: '请输入密码', trigger: 'onBlur' }],
|
|
|
};
|
|
|
|
|
|
-/** 登录成功后统一落盘:applyLogin(写 token + 视图)+ 初始化作用域 + 跳首页 */
|
|
|
-async function onLoginSuccess(res: {
|
|
|
- institutionId?: number;
|
|
|
- campusId?: number;
|
|
|
-}): Promise<void> {
|
|
|
- if (res.institutionId) {
|
|
|
- // 移动端作用域来自登录响应(无独立选择页);institutionId 缺失则由守卫④拦回登录
|
|
|
- scope.set({ institutionId: res.institutionId, campusId: res.campusId ?? 0 });
|
|
|
- }
|
|
|
- await router.push('/');
|
|
|
-}
|
|
|
-
|
|
|
/**
|
|
|
* 提交账密登录(handle 前缀:用户触发的提交类方法,编码规范命名规则)。
|
|
|
*
|
|
|
@@ -76,7 +57,9 @@ async function onLoginSuccess(res: {
|
|
|
* - Promise<void>:恒 resolve;成功进首页,失败写 errorMsg。
|
|
|
* 异常场景:
|
|
|
* - 表单校验失败:不发请求(van-form 标红);
|
|
|
- * - code !== 200(如 1005 账号密码错误):errorMsg = 后端 message;
|
|
|
+ * - ① code≠200(如 1001 账号密码错):errorMsg = 后端 message;
|
|
|
+ * - ① 成功但 memberships 空:errorMsg 提示无机构归属;
|
|
|
+ * - ② code≠200(1001 票据失效 / 1003 越权):errorMsg = 后端 message;
|
|
|
* - 网络层 reject:errorMsg = "网络连接异常"。
|
|
|
*/
|
|
|
async function handleSubmit(): Promise<void> {
|
|
|
@@ -84,87 +67,39 @@ async function handleSubmit(): Promise<void> {
|
|
|
errorMsg.value = '';
|
|
|
submitting.value = true;
|
|
|
try {
|
|
|
- const res = await login({ username: form.username, password: form.password });
|
|
|
- if (res.code === 200 && res.data) {
|
|
|
- loginStore.applyLogin(res.data);
|
|
|
- await onLoginSuccess(res.data);
|
|
|
+ // ① 验账密:拿一次性票据 + 作用域列表(不签 token)
|
|
|
+ const step1 = await login({ username: form.username, password: form.password });
|
|
|
+ if (step1.code !== 200 || !step1.data) {
|
|
|
+ errorMsg.value = step1.message || '登录失败';
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const memberships = step1.data.memberships ?? [];
|
|
|
+ // 无机构归属:提示联系管理员(该账号无法进入业务域)
|
|
|
+ if (memberships.length === 0) {
|
|
|
+ errorMsg.value = '该账号暂无机构归属,请联系管理员分配机构';
|
|
|
return;
|
|
|
}
|
|
|
- errorMsg.value = res.message || '登录失败';
|
|
|
+ // 移动端无独立作用域选择页:自动取第一条作用域完成 ②(多机构切换为后续待拍板项)
|
|
|
+ const m = memberships[0];
|
|
|
+ const pair = await issue({
|
|
|
+ loginTicket: step1.data.loginTicket,
|
|
|
+ institutionId: m.institutionId,
|
|
|
+ campusId: m.campusId,
|
|
|
+ });
|
|
|
+ if (pair.code === 200 && pair.data) {
|
|
|
+ loginStore.applyLogin(pair.data); // 写 token + roles/permissions/tabTree
|
|
|
+ // 移动端作用域来自 ② 响应(institutionId/campusId);scope.set 失败(平台级 institutionId=0)由守卫④兜底
|
|
|
+ scope.set({ institutionId: pair.data.institutionId, campusId: pair.data.campusId });
|
|
|
+ await router.push('/');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ errorMsg.value = pair.message || '登录失败';
|
|
|
} catch {
|
|
|
errorMsg.value = '网络连接异常';
|
|
|
} finally {
|
|
|
submitting.value = false;
|
|
|
}
|
|
|
}
|
|
|
-
|
|
|
-/** 停止微信授权轮询(切 tab / 组件卸载 / 终态时调用;幂等) */
|
|
|
-function stopPolling(): void {
|
|
|
- if (pollTimer !== undefined) {
|
|
|
- clearInterval(pollTimer);
|
|
|
- pollTimer = undefined;
|
|
|
- }
|
|
|
- polling.value = false;
|
|
|
-}
|
|
|
-
|
|
|
-/**
|
|
|
- * 切换登录方式 tab(切到微信时发起授权 + 启动轮询;切走时停轮询)。
|
|
|
- *
|
|
|
- * 入参:tab 'password' | 'wechat'(van-tabs @change 回调值)。
|
|
|
- * 出参:无(异步启动授权流程)。
|
|
|
- * 异常场景:
|
|
|
- * - 后端未就绪:authorizeWechat reject → 停轮询,授权区展示占位文案。
|
|
|
- */
|
|
|
-async function handleTabChange(tab: string | number): Promise<void> {
|
|
|
- activeTab.value = tab === 'wechat' ? 'wechat' : 'password';
|
|
|
- stopPolling();
|
|
|
- errorMsg.value = '';
|
|
|
- if (activeTab.value === 'wechat') {
|
|
|
- authUrl.value = '';
|
|
|
- // 取当前作用域校区(未登录时 institutionId/campusId 为 0,后端取默认 appid)
|
|
|
- const campusId = scope.current.campusId ?? 0;
|
|
|
- try {
|
|
|
- const auth = await authorizeWechat(campusId);
|
|
|
- if (auth.code === 200 && auth.data) {
|
|
|
- authUrl.value = auth.data.url;
|
|
|
- // 轮询授权状态(1.5s 间隔;占位节奏,后端就绪后可调)
|
|
|
- const state = auth.data.state;
|
|
|
- polling.value = true;
|
|
|
- pollTimer = setInterval(async () => {
|
|
|
- try {
|
|
|
- const s = await pollWechatStatus(state);
|
|
|
- if (s.code !== 200 || !s.data) return; // 单次轮询业务错误:静默,下轮重试
|
|
|
- const { status, token } = s.data;
|
|
|
- if (status === 'success' && token) {
|
|
|
- stopPolling();
|
|
|
- loginStore.applyLogin(token);
|
|
|
- await onLoginSuccess(token);
|
|
|
- } else if (status === 'cancel' || status === 'expire') {
|
|
|
- stopPolling();
|
|
|
- authUrl.value = ''; // 终态:清空授权态,提示重试
|
|
|
- }
|
|
|
- } catch {
|
|
|
- // 单次轮询网络故障:静默(不中断轮询,下轮重试)
|
|
|
- }
|
|
|
- }, 1500);
|
|
|
- }
|
|
|
- } catch {
|
|
|
- // 后端未就绪 / 取授权失败:占位(保持 authUrl 空,授权区显"暂未开放")
|
|
|
- stopPolling();
|
|
|
- showToast('微信授权暂未开放');
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-// 组件卸载清轮询定时器(防内存泄漏;登录页被守卫跳转销毁时必清)
|
|
|
-onUnmounted(() => {
|
|
|
- stopPolling();
|
|
|
-});
|
|
|
-
|
|
|
-/** 打开微信授权页(新窗口;占位——真实 per-campus 授权多为整页跳转,此处新窗口便于联调观察) */
|
|
|
-function openWechatAuth(): void {
|
|
|
- if (authUrl.value) window.open(authUrl.value, '_blank');
|
|
|
-}
|
|
|
</script>
|
|
|
|
|
|
<template>
|
|
|
@@ -175,8 +110,8 @@ function openWechatAuth(): void {
|
|
|
|
|
|
<van-notice-bar v-if="errorMsg" color="#ee0a24" background="#fff1f0" :text="errorMsg" class="login-error" />
|
|
|
|
|
|
- <!-- 登录方式切换(账号密码 / 微信授权):@change 切到微信时发起授权 + 启动轮询 -->
|
|
|
- <van-tabs v-model:active="activeTab" @change="handleTabChange" class="login-tabs">
|
|
|
+ <!-- 登录方式切换(账号密码 / 微信授权) -->
|
|
|
+ <van-tabs v-model:active="activeTab" class="login-tabs">
|
|
|
<!-- 账号密码 -->
|
|
|
<van-tab title="账号密码" name="password">
|
|
|
<van-form @submit="handleSubmit">
|
|
|
@@ -207,20 +142,11 @@ function openWechatAuth(): void {
|
|
|
</van-form>
|
|
|
</van-tab>
|
|
|
|
|
|
- <!-- 微信授权(占位:后端端点未就绪,UI 骨架完整;授权 URL 取到前显占位框) -->
|
|
|
+ <!-- 微信授权(占位:后端微信扫码为 PC 端扫码流程;移动端 App 内授权接入方案待拍板,显占位提示) -->
|
|
|
<van-tab title="微信授权" name="wechat">
|
|
|
<div class="login-wechat">
|
|
|
- <van-icon v-if="authUrl" name="wechat" size="64" color="#07c160" />
|
|
|
- <van-icon v-else name="cross" size="64" color="var(--app-text-secondary)" />
|
|
|
- <p class="login-wechat__tip">
|
|
|
- {{ polling ? '请使用微信完成授权…' : '切换至此自动发起授权' }}
|
|
|
- </p>
|
|
|
- <van-button v-if="authUrl" type="primary" round @click="openWechatAuth">
|
|
|
- 打开微信授权
|
|
|
- </van-button>
|
|
|
- <van-button v-else type="default" round plain disabled>
|
|
|
- 微信授权暂未开放
|
|
|
- </van-button>
|
|
|
+ <van-icon name="wechat" size="64" color="#07c160" />
|
|
|
+ <p class="login-wechat__tip">微信授权暂未开放,请使用账号密码登录</p>
|
|
|
</div>
|
|
|
</van-tab>
|
|
|
</van-tabs>
|