批量更新
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>正在完成登录…</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
min-height: 100vh; margin: 0; color: #5a6072; font-size: 14px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
.box {
|
||||
background: #fff; border-radius: 12px; padding: 32px 40px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,.2); text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="box">正在完成登录,请稍候…</div>
|
||||
<script>
|
||||
/**
|
||||
* 统一认证中心回调中转页。
|
||||
*
|
||||
* 为什么不直接用 hash 路由 /#/auth/callback 作为 redirect_uri:
|
||||
* URL 中的 "#" 会被浏览器当作片段标识符,其后的内容不会作为查询参数发送到服务端,
|
||||
* 导致认证中心收到的 redirect_uri 被截断(只剩根路径),换不到授权码。
|
||||
*
|
||||
* 因此用一个不带 # 的静态页接收 code,再转发给 SPA 的 hash 路由处理。
|
||||
*/
|
||||
(function () {
|
||||
var query = window.location.search || '';
|
||||
var target = window.location.origin + '/#/auth/callback' + query;
|
||||
window.location.replace(target);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createRouter, createWebHashHistory } from "vue-router";
|
||||
import { convertMenusToRoutes } from "./dynamicRoutes";
|
||||
import { isSSOEnabled, redirectToAuthorize } from "@/utils/authClient";
|
||||
import { isSSOEnabled, redirectToAuthorize, ensureUserInfo } from "@/utils/authClient";
|
||||
|
||||
// 静态子路由:需要在 Main 框架内显示的页面
|
||||
const staticMainChildren = [
|
||||
@@ -335,6 +335,17 @@ router.beforeEach(async (to, from, next) => {
|
||||
next({ path: "/login", query: { redirect: to.path } });
|
||||
return;
|
||||
}
|
||||
|
||||
// 统一认证模式:补全用户信息(旧会话可能只有 token 没有 userInfo,
|
||||
// 或迁移后 userInfo 缺失 id,会导致菜单等接口报「用户ID不存在」)。
|
||||
// 令牌失效时 ensureUserInfo 会清空登录态并返回 null,此时重新登录。
|
||||
if (isSSOEnabled()) {
|
||||
const info = await ensureUserInfo();
|
||||
if (!info) {
|
||||
await redirectToAuthorize();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!dynamicRoutesAdded) {
|
||||
await loadAndAddDynamicRoutes();
|
||||
|
||||
@@ -171,6 +171,48 @@ export async function logoutSSO() {
|
||||
window.location.href = `${AUTH_BASE}/logout?${params.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保本地存在完整的用户信息(含 id / group_id)。
|
||||
*
|
||||
* 场景:统一认证上线或 ID 全量迁移后,浏览器里可能残留旧会话——
|
||||
* 只有 token 没有 userInfo,或 userInfo 缺少 id,菜单等接口会报
|
||||
*「用户ID不存在」。这里主动补全;若令牌已失效则清空登录态返回 null,
|
||||
* 由调用方跳转登录。
|
||||
*/
|
||||
export async function ensureUserInfo() {
|
||||
let info = null;
|
||||
try {
|
||||
const raw = localStorage.getItem('userInfo');
|
||||
info = raw ? JSON.parse(raw) : null;
|
||||
} catch (e) {
|
||||
info = null;
|
||||
}
|
||||
if (info && info.id) {
|
||||
return info;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fetchUserInfo();
|
||||
const normalized = {
|
||||
id: data.id || data.sub || '',
|
||||
account: data.account || data.mobile || '',
|
||||
name: data.name || data.nickname || '',
|
||||
group_id: data.group_id || '',
|
||||
tid: data.tid || '',
|
||||
tenant_name: (data.tenants || []).map((t) => t.tenant_name).join('、'),
|
||||
avatar: data.avatar || '',
|
||||
type: 'backend'
|
||||
};
|
||||
localStorage.setItem('userInfo', JSON.stringify(normalized));
|
||||
return normalized;
|
||||
} catch (e) {
|
||||
// 令牌无效(例如迁移前的旧令牌):清掉本地状态,让调用方重新登录
|
||||
clearTokens();
|
||||
localStorage.removeItem('userInfo');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 拉取当前登录用户(认证中心视角) */
|
||||
export async function fetchUserInfo() {
|
||||
const res = await fetch(`${AUTH_BASE}/userinfo`, {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import axios from 'axios';
|
||||
import { isSSOEnabled, refreshAccessToken, clearTokens } from '@/utils/authClient';
|
||||
import { isSSOEnabled, refreshAccessToken, clearTokens, redirectToAuthorize } from '@/utils/authClient';
|
||||
|
||||
// 统一认证模式下 401 后正在跳转登录的标志,避免并发请求同时触发跳转造成死循环
|
||||
let ssoRedirecting = false;
|
||||
|
||||
// 获取API基础URL,添加调试信息
|
||||
const apiBaseURL = import.meta.env.VITE_API_BASE_URL;
|
||||
@@ -56,10 +59,14 @@ service.interceptors.response.use(
|
||||
error.config.headers['Authorization'] = `Bearer ${newToken}`;
|
||||
return service.request(error.config);
|
||||
} catch (e) {
|
||||
// 续期失败:清空登录态并跳认证中心重新登录(只跳一次,避免死循环)
|
||||
clearTokens();
|
||||
localStorage.removeItem('userInfo');
|
||||
window.location.href = '#/login';
|
||||
return Promise.reject(new Error('token无效'));
|
||||
if (!ssoRedirecting) {
|
||||
ssoRedirecting = true;
|
||||
redirectToAuthorize();
|
||||
}
|
||||
return Promise.reject(new Error('token无效,请重新登录'));
|
||||
}
|
||||
}
|
||||
console.error('未授权,请重新登录');
|
||||
|
||||
@@ -327,6 +327,14 @@ const previewList = computed(() =>
|
||||
imageList.value.map(img => resolveUrl(img.url))
|
||||
);
|
||||
|
||||
// ---------- 绑定提醒的状态回显与反馈 ----------
|
||||
// 注意:这两个 ref 必须声明在下方 watch 之前。
|
||||
// watch 带 immediate:true,会在 setup 执行到该处时立即回调 loadReminderInfo(),
|
||||
// 而 loadReminderInfo 同步写入 reminderInfo / reminderLoading,
|
||||
// 若此时它们尚未初始化,会抛出 "Cannot access 'x' before initialization"(暂时性死区)。
|
||||
const reminderInfo = ref(null);
|
||||
const reminderLoading = ref(false);
|
||||
|
||||
// ---------- 备注:创建 / 修改 / 完成后都可编辑 ----------
|
||||
const remarkText = ref("");
|
||||
const remarkSaving = ref(false);
|
||||
@@ -374,10 +382,7 @@ async function saveRemark() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 绑定提醒的状态回显与反馈 ----------
|
||||
const reminderInfo = ref(null);
|
||||
const reminderLoading = ref(false);
|
||||
|
||||
// ---------- 提醒渠道文案映射 ----------
|
||||
const channelTextMap = {
|
||||
SMS: "短信",
|
||||
EMAIL: "邮件",
|
||||
|
||||
@@ -16,10 +16,39 @@
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import { handleAuthorizeCallback, redirectToAuthorize } from "@/utils/authClient";
|
||||
import {
|
||||
handleAuthorizeCallback,
|
||||
redirectToAuthorize,
|
||||
fetchUserInfo,
|
||||
} from "@/utils/authClient";
|
||||
|
||||
const error = ref("");
|
||||
|
||||
/**
|
||||
* 写入本地用户信息。
|
||||
* 关键:id 必须用 legacy_uid(老表 yz_system_tenant_user.uid),
|
||||
* 因为菜单、权限等现有业务接口都以它识别用户。
|
||||
*/
|
||||
async function saveUserInfo() {
|
||||
try {
|
||||
const info = await fetchUserInfo();
|
||||
const userInfo = {
|
||||
id: info.id || info.sub || "",
|
||||
account: info.account || info.mobile || "",
|
||||
name: info.name || info.nickname || "",
|
||||
group_id: info.group_id || "",
|
||||
tid: info.tid || "",
|
||||
tenant_name: (info.tenants || []).map((t) => t.tenant_name).join("、"),
|
||||
avatar: info.avatar || "",
|
||||
type: "backend",
|
||||
};
|
||||
localStorage.setItem("userInfo", JSON.stringify(userInfo));
|
||||
} catch (e) {
|
||||
// 用户信息拉取失败不阻断登录,只记录;业务接口可能提示重新登录
|
||||
console.error("获取用户信息失败:", e);
|
||||
}
|
||||
}
|
||||
|
||||
async function finish() {
|
||||
// hash 路由:参数位于 location.hash 之后,这里统一从 URL 中取出
|
||||
const raw = window.location.href;
|
||||
@@ -28,6 +57,7 @@ async function finish() {
|
||||
|
||||
try {
|
||||
await handleAuthorizeCallback(query);
|
||||
await saveUserInfo();
|
||||
// 登录成功:清掉 URL 上的 code/state,进入首页
|
||||
window.location.href = "#/home";
|
||||
window.location.reload();
|
||||
|
||||
@@ -23,7 +23,9 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
host: '127.0.0.1',
|
||||
port: 4001,
|
||||
port: 4401,
|
||||
// 端口被占用时直接报错,避免自动漂移导致统一登录回跳地址不匹配
|
||||
strictPort: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:9000',
|
||||
|
||||
Reference in New Issue
Block a user