f
This commit is contained in:
@@ -366,7 +366,6 @@
|
||||
"useThrottleFn": true,
|
||||
"useThrottledRefHistory": true,
|
||||
"useTimeAgo": true,
|
||||
"useTimeAgoIntl": true,
|
||||
"useTimeout": true,
|
||||
"useTimeoutFn": true,
|
||||
"useTimeoutPoll": true,
|
||||
|
||||
1
auto-imports.d.ts
vendored
1
auto-imports.d.ts
vendored
@@ -817,7 +817,6 @@ declare module 'vue' {
|
||||
readonly useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
||||
readonly useTimeAgoIntl: UnwrapRef<typeof import('@vueuse/core')['useTimeAgoIntl']>
|
||||
readonly useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -317,13 +317,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 请求参数 -->
|
||||
<!-- <div class="info-section">
|
||||
<!-- 请求参数(仅开发环境) -->
|
||||
<div v-if="isDevelopment && selectedApiCall?.request_params" class="info-section">
|
||||
<h4 class="text-lg font-semibold text-gray-900 mb-4">请求参数</h4>
|
||||
<div class="info-item">
|
||||
<span class="info-value">{{ selectedApiCall?.request_params || '-' }}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
<pre class="request-params-json">{{ formatRequestParams(selectedApiCall.request_params) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="flex justify-center items-center py-8">
|
||||
<el-loading size="large" />
|
||||
@@ -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);
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user