From d461476889716b39668bbef43c000269791ac191 Mon Sep 17 00:00:00 2001 From: liangzai <2440983361@qq.com> Date: Tue, 30 Jun 2026 14:24:48 +0800 Subject: [PATCH] f --- .eslintrc-auto-import.json | 1 - auto-imports.d.ts | 1 - src/constants/portal.js | 63 +++++++++++++++++++++++++++++++++ src/pages/admin/usage/index.vue | 37 +++++++++++++++---- src/router/index.js | 13 +++---- src/stores/user.js | 11 +++++- vite.config.js | 14 ++++---- 7 files changed, 118 insertions(+), 22 deletions(-) diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index 55a19f9..666770a 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -366,7 +366,6 @@ "useThrottleFn": true, "useThrottledRefHistory": true, "useTimeAgo": true, - "useTimeAgoIntl": true, "useTimeout": true, "useTimeoutFn": true, "useTimeoutPoll": true, diff --git a/auto-imports.d.ts b/auto-imports.d.ts index 516d1ce..1af4034 100644 --- a/auto-imports.d.ts +++ b/auto-imports.d.ts @@ -817,7 +817,6 @@ declare module 'vue' { readonly useThrottleFn: UnwrapRef readonly useThrottledRefHistory: UnwrapRef readonly useTimeAgo: UnwrapRef - readonly useTimeAgoIntl: UnwrapRef readonly useTimeout: UnwrapRef readonly useTimeoutFn: UnwrapRef readonly useTimeoutPoll: UnwrapRef diff --git a/src/constants/portal.js b/src/constants/portal.js index a654e07..0ad8b3c 100644 --- a/src/constants/portal.js +++ b/src/constants/portal.js @@ -25,3 +25,66 @@ export const isCurrentOrigin = (origin) => { if (!origin || typeof window === 'undefined') return false return window.location.origin === origin } + +const PORTAL_HANDOFF_HASH = '_ph' + +const encodeHandoffPayload = (payload) => { + return btoa(JSON.stringify(payload)) + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=+$/, '') +} + +const decodeHandoffPayload = (encoded) => { + const normalized = encoded.replace(/-/g, '+').replace(/_/g, '/') + const padding = normalized.length % 4 === 0 ? '' : '='.repeat(4 - (normalized.length % 4)) + return JSON.parse(atob(normalized + padding)) +} + +/** 构建跨域跳转 URL(token 经 hash 一次性传递,不发给服务端) */ +export const buildPortalRedirectURL = (targetOrigin, path, credentials) => { + const url = new URL(path || '/', targetOrigin) + if (credentials?.access_token) { + url.hash = `${PORTAL_HANDOFF_HASH}=${encodeHandoffPayload({ + access_token: credentials.access_token, + token_type: credentials.token_type || 'Bearer' + })}` + } + return url.toString() +} + +/** 读取并清除 URL 中的跨域 handoff token */ +export const consumePortalHandoff = () => { + if (typeof window === 'undefined') return null + + const rawHash = window.location.hash.replace(/^#/, '') + if (!rawHash.startsWith(`${PORTAL_HANDOFF_HASH}=`)) return null + + try { + const payload = decodeHandoffPayload(rawHash.slice(PORTAL_HANDOFF_HASH.length + 1)) + if (!payload?.access_token) return null + + const cleanUrl = window.location.pathname + window.location.search + window.history.replaceState(null, '', cleanUrl) + + return { + access_token: payload.access_token, + token_type: payload.token_type || 'Bearer' + } + } catch { + return null + } +} + +/** 清除当前域登录态后跳转到目标域(避免子/主域 localStorage 互相污染) */ +export const redirectCrossPortal = (targetOrigin, path, userStore) => { + const credentials = userStore.isLoggedIn + ? { access_token: userStore.accessToken, token_type: userStore.tokenType } + : null + + userStore.logout() + + window.location.replace( + buildPortalRedirectURL(targetOrigin, path, credentials) + ) +} diff --git a/src/pages/admin/usage/index.vue b/src/pages/admin/usage/index.vue index 697ee47..0e0fafb 100644 --- a/src/pages/admin/usage/index.vue +++ b/src/pages/admin/usage/index.vue @@ -317,13 +317,11 @@ - - +

请求参数

-
- {{ selectedApiCall?.request_params || '-' }} -
-
--> +
{{ formatRequestParams(selectedApiCall.request_params) }}
+
@@ -365,6 +363,9 @@ const route = useRoute() // 移动端检测 const { isMobile, isTablet } = useMobileTable() +// 开发环境标识 +const isDevelopment = import.meta.env.DEV + // 响应式数据 const loading = ref(false) const apiCalls = ref([]) @@ -472,6 +473,16 @@ const formatDateTime = (date) => { return new Date(date).toLocaleString('zh-CN') } +// 格式化请求参数 JSON +const formatRequestParams = (params) => { + if (!params) return '-' + try { + return JSON.stringify(JSON.parse(params), null, 2) + } catch { + return params + } +} + // 获取状态类型 const getStatusType = (status) => { @@ -683,6 +694,20 @@ const handleExport = async (options) => { flex: 1; } +.request-params-json { + margin: 0; + padding: 0.75rem; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 0.375rem; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.8125rem; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-all; + color: #111827; +} + /* 错误信息样式 */ .error-info { background: rgba(254, 242, 242, 0.8); diff --git a/src/router/index.js b/src/router/index.js index 54efc54..eb04ae4 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -1,4 +1,4 @@ -import { isCurrentOrigin, isPortalDomainConfigReady, isSubPortal, mainPortalOrigin, subPortalOrigin } from '@/constants/portal' +import { isCurrentOrigin, isPortalDomainConfigReady, isSubPortal, mainPortalOrigin, redirectCrossPortal, subPortalOrigin } from '@/constants/portal' import { useUserStore } from '@/stores/user' import { createRouter, createWebHistory } from 'vue-router' import { statisticsRoutes } from './modules/statistics' @@ -413,8 +413,9 @@ router.beforeEach(async (to, from, next) => { const isAuthRoute = to.path.startsWith('/auth') || to.path.startsWith('/sub/auth') const requiresAuth = to.meta.requiresAuth - // 只有在需要认证的路由上才等待初始化 - if (requiresAuth && !userStore.initialized) { + // 只有在需要认证的路由,或本地已有 token 时,才等待 init(避免登录页无法触发跨域隔离) + const hasStoredToken = typeof localStorage !== 'undefined' && !!localStorage.getItem('access_token') + if ((requiresAuth || hasStoredToken) && !userStore.initialized) { await userStore.init() } else if (!userStore.initialized) { // 对于不需要认证的路由,异步初始化但不阻塞 @@ -446,16 +447,16 @@ router.beforeEach(async (to, from, next) => { console.warn('[router] 未配置主/子域名,当前以单域模式运行,跳过域名隔离跳转') } - // 域名隔离:子账号登录态必须在子账号专属域名 + // 域名隔离:子账号登录态必须在子账号专属域名(跳转前清当前域 token,避免主站无法再次登录) if (userStore.isLoggedIn && userStore.accountKind === 'subordinate' && subPortalOrigin && !isCurrentOrigin(subPortalOrigin)) { - window.location.replace(`${subPortalOrigin}${to.fullPath}`) + redirectCrossPortal(subPortalOrigin, to.fullPath, userStore) return } // 域名隔离:普通/管理员账号不应停留在子账号专属域名 if (userStore.isLoggedIn && userStore.accountKind !== 'subordinate' && subPortalOrigin && isCurrentOrigin(subPortalOrigin)) { if (mainPortalOrigin) { - window.location.replace(`${mainPortalOrigin}${to.fullPath}`) + redirectCrossPortal(mainPortalOrigin, to.fullPath, userStore) return } next('/products') diff --git a/src/stores/user.js b/src/stores/user.js index cdbc1d1..acb54cd 100644 --- a/src/stores/user.js +++ b/src/stores/user.js @@ -92,7 +92,7 @@ */ import { userApi } from '@/api' -import { isSubPortal } from '@/constants/portal' +import { consumePortalHandoff, isSubPortal } from '@/constants/portal' import router from '@/router' import { authEventBus } from '@/utils/request' import { generateSMSRequest } from '@/utils/smsSignature' @@ -453,6 +453,15 @@ export const useUserStore = defineStore('user', () => { isInitializing = true try { + // 跨域跳转后接收 hash 中的一次性 token + const handoff = consumePortalHandoff() + if (handoff?.access_token) { + accessToken.value = handoff.access_token + tokenType.value = handoff.token_type || 'Bearer' + localStorage.setItem('access_token', handoff.access_token) + localStorage.setItem('token_type', handoff.token_type || 'Bearer') + } + // 监听认证错误事件(只注册一次) if (!authEventBus.listeners.includes(handleAuthError)) { authEventBus.onAuthError(handleAuthError) diff --git a/vite.config.js b/vite.config.js index a0d4de8..42bc040 100644 --- a/vite.config.js +++ b/vite.config.js @@ -124,18 +124,18 @@ export default defineConfig({ server: { proxy: { // // 本地开发时将 /api/v1 的请求代理到 8080 端口 - '/api/v1': { - target: 'http://localhost:8080', - changeOrigin: true, - // 保持路径不变 - rewrite: path => path, - }, // '/api/v1': { - // target: 'https://console.tianyuanapi.com', + // target: 'http://localhost:8080', // changeOrigin: true, // // 保持路径不变 // rewrite: path => path, // }, + '/api/v1': { + target: 'https://console.tianyuanapi.com', + changeOrigin: true, + // 保持路径不变 + rewrite: path => path, + }, }, }, })