f
This commit is contained in:
@@ -366,7 +366,6 @@
|
|||||||
"useThrottleFn": true,
|
"useThrottleFn": true,
|
||||||
"useThrottledRefHistory": true,
|
"useThrottledRefHistory": true,
|
||||||
"useTimeAgo": true,
|
"useTimeAgo": true,
|
||||||
"useTimeAgoIntl": true,
|
|
||||||
"useTimeout": true,
|
"useTimeout": true,
|
||||||
"useTimeoutFn": true,
|
"useTimeoutFn": true,
|
||||||
"useTimeoutPoll": 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 useThrottleFn: UnwrapRef<typeof import('@vueuse/core')['useThrottleFn']>
|
||||||
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
readonly useThrottledRefHistory: UnwrapRef<typeof import('@vueuse/core')['useThrottledRefHistory']>
|
||||||
readonly useTimeAgo: UnwrapRef<typeof import('@vueuse/core')['useTimeAgo']>
|
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 useTimeout: UnwrapRef<typeof import('@vueuse/core')['useTimeout']>
|
||||||
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
readonly useTimeoutFn: UnwrapRef<typeof import('@vueuse/core')['useTimeoutFn']>
|
||||||
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
readonly useTimeoutPoll: UnwrapRef<typeof import('@vueuse/core')['useTimeoutPoll']>
|
||||||
|
|||||||
@@ -25,3 +25,66 @@ export const isCurrentOrigin = (origin) => {
|
|||||||
if (!origin || typeof window === 'undefined') return false
|
if (!origin || typeof window === 'undefined') return false
|
||||||
return window.location.origin === origin
|
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>
|
</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>
|
<h4 class="text-lg font-semibold text-gray-900 mb-4">请求参数</h4>
|
||||||
<div class="info-item">
|
<pre class="request-params-json">{{ formatRequestParams(selectedApiCall.request_params) }}</pre>
|
||||||
<span class="info-value">{{ selectedApiCall?.request_params || '-' }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex justify-center items-center py-8">
|
<div v-else class="flex justify-center items-center py-8">
|
||||||
<el-loading size="large" />
|
<el-loading size="large" />
|
||||||
@@ -365,6 +363,9 @@ const route = useRoute()
|
|||||||
// 移动端检测
|
// 移动端检测
|
||||||
const { isMobile, isTablet } = useMobileTable()
|
const { isMobile, isTablet } = useMobileTable()
|
||||||
|
|
||||||
|
// 开发环境标识
|
||||||
|
const isDevelopment = import.meta.env.DEV
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const apiCalls = ref([])
|
const apiCalls = ref([])
|
||||||
@@ -472,6 +473,16 @@ const formatDateTime = (date) => {
|
|||||||
return new Date(date).toLocaleString('zh-CN')
|
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) => {
|
const getStatusType = (status) => {
|
||||||
@@ -683,6 +694,20 @@ const handleExport = async (options) => {
|
|||||||
flex: 1;
|
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 {
|
.error-info {
|
||||||
background: rgba(254, 242, 242, 0.8);
|
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 { useUserStore } from '@/stores/user'
|
||||||
import { createRouter, createWebHistory } from 'vue-router'
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
import { statisticsRoutes } from './modules/statistics'
|
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 isAuthRoute = to.path.startsWith('/auth') || to.path.startsWith('/sub/auth')
|
||||||
const requiresAuth = to.meta.requiresAuth
|
const requiresAuth = to.meta.requiresAuth
|
||||||
|
|
||||||
// 只有在需要认证的路由上才等待初始化
|
// 只有在需要认证的路由,或本地已有 token 时,才等待 init(避免登录页无法触发跨域隔离)
|
||||||
if (requiresAuth && !userStore.initialized) {
|
const hasStoredToken = typeof localStorage !== 'undefined' && !!localStorage.getItem('access_token')
|
||||||
|
if ((requiresAuth || hasStoredToken) && !userStore.initialized) {
|
||||||
await userStore.init()
|
await userStore.init()
|
||||||
} else if (!userStore.initialized) {
|
} else if (!userStore.initialized) {
|
||||||
// 对于不需要认证的路由,异步初始化但不阻塞
|
// 对于不需要认证的路由,异步初始化但不阻塞
|
||||||
@@ -446,16 +447,16 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
console.warn('[router] 未配置主/子域名,当前以单域模式运行,跳过域名隔离跳转')
|
console.warn('[router] 未配置主/子域名,当前以单域模式运行,跳过域名隔离跳转')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 域名隔离:子账号登录态必须在子账号专属域名
|
// 域名隔离:子账号登录态必须在子账号专属域名(跳转前清当前域 token,避免主站无法再次登录)
|
||||||
if (userStore.isLoggedIn && userStore.accountKind === 'subordinate' && subPortalOrigin && !isCurrentOrigin(subPortalOrigin)) {
|
if (userStore.isLoggedIn && userStore.accountKind === 'subordinate' && subPortalOrigin && !isCurrentOrigin(subPortalOrigin)) {
|
||||||
window.location.replace(`${subPortalOrigin}${to.fullPath}`)
|
redirectCrossPortal(subPortalOrigin, to.fullPath, userStore)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 域名隔离:普通/管理员账号不应停留在子账号专属域名
|
// 域名隔离:普通/管理员账号不应停留在子账号专属域名
|
||||||
if (userStore.isLoggedIn && userStore.accountKind !== 'subordinate' && subPortalOrigin && isCurrentOrigin(subPortalOrigin)) {
|
if (userStore.isLoggedIn && userStore.accountKind !== 'subordinate' && subPortalOrigin && isCurrentOrigin(subPortalOrigin)) {
|
||||||
if (mainPortalOrigin) {
|
if (mainPortalOrigin) {
|
||||||
window.location.replace(`${mainPortalOrigin}${to.fullPath}`)
|
redirectCrossPortal(mainPortalOrigin, to.fullPath, userStore)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
next('/products')
|
next('/products')
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { userApi } from '@/api'
|
import { userApi } from '@/api'
|
||||||
import { isSubPortal } from '@/constants/portal'
|
import { consumePortalHandoff, isSubPortal } from '@/constants/portal'
|
||||||
import router from '@/router'
|
import router from '@/router'
|
||||||
import { authEventBus } from '@/utils/request'
|
import { authEventBus } from '@/utils/request'
|
||||||
import { generateSMSRequest } from '@/utils/smsSignature'
|
import { generateSMSRequest } from '@/utils/smsSignature'
|
||||||
@@ -453,6 +453,15 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
isInitializing = true
|
isInitializing = true
|
||||||
|
|
||||||
try {
|
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)) {
|
if (!authEventBus.listeners.includes(handleAuthError)) {
|
||||||
authEventBus.onAuthError(handleAuthError)
|
authEventBus.onAuthError(handleAuthError)
|
||||||
|
|||||||
@@ -124,18 +124,18 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
// // 本地开发时将 /api/v1 的请求代理到 8080 端口
|
// // 本地开发时将 /api/v1 的请求代理到 8080 端口
|
||||||
'/api/v1': {
|
|
||||||
target: 'http://localhost:8080',
|
|
||||||
changeOrigin: true,
|
|
||||||
// 保持路径不变
|
|
||||||
rewrite: path => path,
|
|
||||||
},
|
|
||||||
// '/api/v1': {
|
// '/api/v1': {
|
||||||
// target: 'https://console.tianyuanapi.com',
|
// target: 'http://localhost:8080',
|
||||||
// changeOrigin: true,
|
// changeOrigin: true,
|
||||||
// // 保持路径不变
|
// // 保持路径不变
|
||||||
// rewrite: path => path,
|
// rewrite: path => path,
|
||||||
// },
|
// },
|
||||||
|
'/api/v1': {
|
||||||
|
target: 'https://console.tianyuanapi.com',
|
||||||
|
changeOrigin: true,
|
||||||
|
// 保持路径不变
|
||||||
|
rewrite: path => path,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user