Merge branch 'main' into dev
# Conflicts: # web/admin/src/views/ai/ChannelDetailPage.vue
This commit is contained in:
commit
4bf835394d
15 files changed
+286
-75
No files matched your search
@@ -11,7 +11,7 @@
|
||||
- **后端**:Go + GoFrame (v2) + goose(数据库迁移)
|
||||
- **数据库**:PostgreSQL(唯一,不用 MySQL/SQLite)
|
||||
- **缓存**:Redis + 内存缓存(双层缓存)
|
||||
- **前端**:Vue 3 + Vite + TailwindCSS + Naive UI(管理后台)
|
||||
- **前端**:Vue 3 + Vite + TailwindCSS + Arco Design Vue(管理后台)
|
||||
- **包管理**:bun
|
||||
- **对象存储**:S3 / OSS / COS / MinIO(禁止纯本地磁盘作为生产存储)
|
||||
|
||||
@@ -383,9 +383,9 @@ wallets 表包含 `frozen_balance` 字段,用于支付中/退款中的金额
|
||||
### 前端
|
||||
|
||||
- Vue 3 Composition API + `<script setup>` 语法
|
||||
- **管理后台**:使用 **Naive UI** 组件库,快速构建数据密集型页面(表格、表单、图表等),可参考 [Soybean Admin](https://github.com/soybeanjs/soybean-admin) 的架构设计(路由、权限、布局)
|
||||
- **管理后台**:使用 **Arco Design Vue** 组件库(`@arco-design/web-vue`),快速构建数据密集型页面(表格、表单、图表等),可参考 [Soybean Admin](https://github.com/soybeanjs/soybean-admin) 的架构设计(路由、权限、布局)
|
||||
- **租户控制台**:TailwindCSS 原子类 + 自建组件,面向客户,注重品牌调性和视觉差异化
|
||||
- TailwindCSS 作为基础样式层,两个控制台共用(Naive UI 的 CSS-in-JS 方案与 TailwindCSS 无冲突)
|
||||
- TailwindCSS 作为基础样式层,两个控制台共用(注意与 Arco 组件内置样式共存,避免全局 reset 冲突)
|
||||
- 状态管理用 Pinia
|
||||
- HTTP 请求封装 Axios,JWT 自动刷新,统一错误处理
|
||||
- 按钮级权限控制:无权限的按钮不渲染(`v-if="hasPermission('xxx')"`)
|
||||
|
||||
@@ -73,6 +73,7 @@ type ChannelUpdateReq struct {
|
||||
g.Meta `path:"/channels/{id}" method:"put" mime:"json" tags:"管理后台-渠道" summary:"更新渠道"`
|
||||
ID int64 `json:"id" in:"path" v:"required" dc:"渠道ID"`
|
||||
Name string `json:"name" dc:"渠道名称"`
|
||||
Type *int `json:"type" v:"min:1#请选择供应商类型" dc:"供应商类型(留空不修改)"`
|
||||
BaseURL string `json:"base_url" dc:"API 基础地址"`
|
||||
ApiKey *string `json:"api_key" dc:"更新 API Key(留空不更新)"`
|
||||
Priority int `json:"priority" dc:"优先级"`
|
||||
|
||||
@@ -95,6 +95,15 @@ func queryPage(ctx context.Context, table, fields, where, orderBy string, page,
|
||||
if where != "" {
|
||||
m = m.Where(where, args...)
|
||||
}
|
||||
|
||||
total, err := m.Count()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if total == 0 {
|
||||
return []map[string]any{}, 0, nil
|
||||
}
|
||||
|
||||
if fields != "" {
|
||||
m = m.Fields(fields)
|
||||
}
|
||||
@@ -102,16 +111,23 @@ func queryPage(ctx context.Context, table, fields, where, orderBy string, page,
|
||||
m = m.Order(orderBy)
|
||||
}
|
||||
|
||||
var items []map[string]any
|
||||
var total int
|
||||
err := m.Page(page, pageSize).ScanAndCount(&items, &total, false)
|
||||
result, err := m.Page(page, pageSize).All()
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if items == nil {
|
||||
items = []map[string]any{}
|
||||
return toMapList(result), total, nil
|
||||
}
|
||||
|
||||
// toMapList converts database records to JSON-safe maps.
|
||||
func toMapList(result gdb.Result) []map[string]any {
|
||||
if len(result) == 0 {
|
||||
return []map[string]any{}
|
||||
}
|
||||
return items, total, nil
|
||||
list := make([]map[string]any, 0, len(result))
|
||||
for _, row := range result {
|
||||
list = append(list, row.Map())
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
// queryAuditPage 查询审计表分页数据,自动计数、分页、转换并批量填充关联名称。
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/gogf/gf/v2/container/gvar"
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
)
|
||||
|
||||
func TestToMapListProducesJSONObjects(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
result := gdb.Result{
|
||||
gdb.Record{
|
||||
"id": gvar.New(9),
|
||||
"error_message": gvar.New("request failed"),
|
||||
},
|
||||
}
|
||||
|
||||
list := toMapList(result)
|
||||
if len(list) != 1 {
|
||||
t.Fatalf("toMapList() length = %d, want 1", len(list))
|
||||
}
|
||||
if list[0] == nil {
|
||||
t.Fatal("toMapList() returned a nil map")
|
||||
}
|
||||
|
||||
data, err := json.Marshal(list)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal list: %v", err)
|
||||
}
|
||||
if got, want := string(data), `[{"error_message":"request failed","id":9}]`; got != want {
|
||||
t.Fatalf("marshal list = %s, want %s", got, want)
|
||||
}
|
||||
}
|
||||
@@ -309,6 +309,9 @@ func (s *sAdmin) UpdateChannel(ctx context.Context, req *v1.ChannelUpdateReq) (*
|
||||
if req.Name != "" {
|
||||
data.Name = req.Name
|
||||
}
|
||||
if req.Type != nil {
|
||||
data.Type = *req.Type
|
||||
}
|
||||
if req.BaseURL != "" {
|
||||
data.BaseUrl = req.BaseURL
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
|
||||
@@ -196,19 +195,3 @@ func (s *sAdmin) ChannelErrorCategories(ctx context.Context, req *v1.ChannelErro
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// toMapList 将 Result 转为 []map[string]any
|
||||
func toMapList(result gdb.Result) []map[string]any {
|
||||
if result == nil || len(result) == 0 {
|
||||
return []map[string]any{}
|
||||
}
|
||||
list := make([]map[string]any, len(result))
|
||||
for i, row := range result {
|
||||
data := make(map[string]any, len(row))
|
||||
for k, v := range row {
|
||||
data[k] = v.Val()
|
||||
}
|
||||
list[i] = data
|
||||
}
|
||||
return list
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { onErrorCaptured, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const error = ref<unknown>(null)
|
||||
|
||||
onErrorCaptured((caughtError, _instance, info) => {
|
||||
error.value = caughtError
|
||||
console.error('[RouteErrorBoundary]', info, caughtError)
|
||||
return false
|
||||
})
|
||||
|
||||
function reloadPage() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
async function goToDashboard() {
|
||||
error.value = null
|
||||
await router.push({ name: 'AdminDashboard' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot v-if="!error" />
|
||||
<div v-else class="route-error">
|
||||
<AResult status="error" title="页面加载失败" subtitle="页面数据异常,请重试或返回仪表盘">
|
||||
<template #extra>
|
||||
<ASpace>
|
||||
<AButton type="primary" @click="reloadPage">重新加载</AButton>
|
||||
<AButton @click="goToDashboard">返回仪表盘</AButton>
|
||||
</ASpace>
|
||||
</template>
|
||||
</AResult>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.route-error {
|
||||
display: flex;
|
||||
min-height: min(520px, calc(100vh - 160px));
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -27,6 +27,9 @@ console.log(
|
||||
)
|
||||
|
||||
const app = createApp(App)
|
||||
app.config.errorHandler = (error, _instance, info) => {
|
||||
console.error('[VueError]', info, error)
|
||||
}
|
||||
app.use(createPinia())
|
||||
app.use(ArcoVue)
|
||||
app.use(router)
|
||||
|
||||
@@ -2,10 +2,9 @@ import { createRouter, createWebHashHistory } from 'vue-router'
|
||||
import type { RouteRecordRaw } from 'vue-router'
|
||||
import adminRoutes from './admin'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { shouldRefresh, getRefreshToken } from '@/utils/request'
|
||||
import request, { shouldRefresh, getRefreshToken } from '@/utils/request'
|
||||
import { useSiteName } from '@/composables/useSiteName'
|
||||
import { useTopProgress } from '@/composables/useTopProgress'
|
||||
import axios from 'axios'
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
@@ -35,10 +34,11 @@ export function markSystemInitialized(): void {
|
||||
async function checkSetupStatus(): Promise<boolean> {
|
||||
if (setupChecked) return systemInitialized === true
|
||||
try {
|
||||
const res = await axios.get('/api/setup/status', { timeout: 5000 })
|
||||
const res = await request.get('/setup/status', { timeout: 5000 })
|
||||
systemInitialized = res.data?.data?.initialized === true
|
||||
} catch {
|
||||
systemInitialized = true
|
||||
// Allow the current navigation, but retry the setup check next time.
|
||||
return true
|
||||
}
|
||||
setupChecked = true
|
||||
return systemInitialized === true
|
||||
|
||||
@@ -82,7 +82,12 @@ export function shouldRefresh(): boolean {
|
||||
}
|
||||
|
||||
let isRefreshing = false
|
||||
let pendingRequests: Array<(token: string) => void> = []
|
||||
interface PendingRequest {
|
||||
resolve: (token: string) => void
|
||||
reject: (error: unknown) => void
|
||||
}
|
||||
|
||||
let pendingRequests: PendingRequest[] = []
|
||||
let tokenRefreshedCallback: ((tokens: TokenPair) => void) | null = null
|
||||
|
||||
export function onTokenRefreshed(cb: (tokens: TokenPair) => void): void {
|
||||
@@ -90,14 +95,21 @@ export function onTokenRefreshed(cb: (tokens: TokenPair) => void): void {
|
||||
}
|
||||
|
||||
function replayPending(token: string): void {
|
||||
pendingRequests.forEach((cb) => cb(token))
|
||||
pendingRequests.forEach(({ resolve }) => resolve(token))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
function clearPending(): void {
|
||||
function rejectPending(error: unknown): void {
|
||||
pendingRequests.forEach(({ reject }) => reject(error))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
function waitForRefresh(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.push({ resolve, reject })
|
||||
})
|
||||
}
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30_000,
|
||||
@@ -132,18 +144,18 @@ request.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
if (shouldRefresh() && !config.url?.includes('/auth/refresh')) {
|
||||
if (isRefreshing) {
|
||||
await new Promise<void>((resolve) => {
|
||||
pendingRequests.push(() => resolve())
|
||||
})
|
||||
await waitForRefresh()
|
||||
} else {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newTokens = await doRefresh()
|
||||
replayPending(newTokens.accessToken)
|
||||
} catch {
|
||||
clearPending()
|
||||
} catch (error) {
|
||||
rejectPending(error)
|
||||
clearTokens()
|
||||
showRequestError(error, config)
|
||||
window.location.hash = '#/admin/login'
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
@@ -168,6 +180,26 @@ function showErrorToast(message: string, config?: InternalAxiosRequestConfig) {
|
||||
Message.error(message)
|
||||
}
|
||||
|
||||
function showRequestError(error: unknown, config?: InternalAxiosRequestConfig) {
|
||||
if (config?._suppressErrorMsg || !axios.isAxiosError(error)) return
|
||||
if ((error as any)._errorToastShown) return
|
||||
;(error as any)._errorToastShown = true
|
||||
const responseData = error.response?.data
|
||||
if (responseData && typeof responseData === 'object' && 'message' in responseData) {
|
||||
Message.error(String(responseData.message || '请求失败'))
|
||||
return
|
||||
}
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||
Message.error('请求超时,请稍后重试')
|
||||
return
|
||||
}
|
||||
if (!error.response) {
|
||||
Message.error('网络连接异常,请检查后端服务是否正常')
|
||||
return
|
||||
}
|
||||
Message.error(`服务器请求失败(${error.response.status})`)
|
||||
}
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data
|
||||
@@ -204,18 +236,20 @@ request.interceptors.response.use(
|
||||
;(err as any).isDemoModeError = error.response.data.code === DEMO_MODE_CODE
|
||||
return Promise.reject(err)
|
||||
}
|
||||
showRequestError(error, originalRequest)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
pendingRequests.push((newToken: string) => {
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest._retry = true
|
||||
resolve(request(originalRequest))
|
||||
})
|
||||
})
|
||||
try {
|
||||
const newToken = await waitForRefresh()
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest._retry = true
|
||||
return request(originalRequest)
|
||||
} catch (refreshError) {
|
||||
return Promise.reject(refreshError)
|
||||
}
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
@@ -228,11 +262,12 @@ request.interceptors.response.use(
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`
|
||||
return request(originalRequest)
|
||||
} catch {
|
||||
clearPending()
|
||||
} catch (refreshError) {
|
||||
rejectPending(refreshError)
|
||||
clearTokens()
|
||||
showRequestError(refreshError, originalRequest)
|
||||
window.location.hash = '#/admin/login'
|
||||
return Promise.reject(error)
|
||||
return Promise.reject(refreshError)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Message } from '@arco-design/web-vue'
|
||||
import axios from 'axios'
|
||||
import { marked } from 'marked'
|
||||
import request from '@/utils/request'
|
||||
import RouteErrorBoundary from '@/components/RouteErrorBoundary.vue'
|
||||
import { useWatermark } from '@/composables/useWatermark'
|
||||
import { useSiteName } from '@/composables/useSiteName'
|
||||
import {
|
||||
@@ -612,9 +613,11 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Content -->
|
||||
<main class="admin-content page-bg">
|
||||
<router-view v-slot="{ Component }">
|
||||
<router-view v-slot="{ Component, route: currentRoute }">
|
||||
<Transition name="page-fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
<RouteErrorBoundary :key="currentRoute.fullPath">
|
||||
<component :is="Component" />
|
||||
</RouteErrorBoundary>
|
||||
</Transition>
|
||||
</router-view>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<script setup lang="ts">
|
||||
import { onErrorCaptured, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const error = ref<unknown>(null)
|
||||
|
||||
onErrorCaptured((caughtError, _instance, info) => {
|
||||
error.value = caughtError
|
||||
console.error('[RouteErrorBoundary]', info, caughtError)
|
||||
return false
|
||||
})
|
||||
|
||||
function reloadPage() {
|
||||
window.location.reload()
|
||||
}
|
||||
|
||||
async function goToDashboard() {
|
||||
error.value = null
|
||||
await router.push({ name: 'TenantDashboard' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot v-if="!error" />
|
||||
<div v-else class="flex min-h-[min(520px,calc(100vh-10rem))] items-center justify-center px-4">
|
||||
<div class="w-full max-w-lg text-center">
|
||||
<div class="mx-auto flex h-14 w-14 items-center justify-center rounded-full bg-red-50 text-red-500">
|
||||
<Icon name="exclamationCircle" size="xl" />
|
||||
</div>
|
||||
<h2 class="mt-5 text-lg font-semibold text-gray-900">页面加载失败</h2>
|
||||
<p class="mt-2 text-sm text-gray-500">页面数据异常,请重试或返回仪表盘</p>
|
||||
<div class="mt-6 flex flex-wrap justify-center gap-3">
|
||||
<button type="button" class="btn btn-primary" @click="reloadPage">
|
||||
<Icon name="refresh" size="sm" />
|
||||
重新加载
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary" @click="goToDashboard">
|
||||
<Icon name="chart" size="sm" />
|
||||
返回仪表盘
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -29,6 +29,9 @@ console.log(
|
||||
)
|
||||
|
||||
const app = createApp(App)
|
||||
app.config.errorHandler = (error, _instance, info) => {
|
||||
console.error('[VueError]', info, error)
|
||||
}
|
||||
app.use(head)
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
@@ -82,7 +82,12 @@ export function shouldRefresh(): boolean {
|
||||
}
|
||||
|
||||
let isRefreshing = false
|
||||
let pendingRequests: Array<(token: string) => void> = []
|
||||
interface PendingRequest {
|
||||
resolve: (token: string) => void
|
||||
reject: (error: unknown) => void
|
||||
}
|
||||
|
||||
let pendingRequests: PendingRequest[] = []
|
||||
let tokenRefreshedCallback: ((tokens: TokenPair) => void) | null = null
|
||||
|
||||
export function onTokenRefreshed(cb: (tokens: TokenPair) => void): void {
|
||||
@@ -90,14 +95,21 @@ export function onTokenRefreshed(cb: (tokens: TokenPair) => void): void {
|
||||
}
|
||||
|
||||
function replayPending(token: string): void {
|
||||
pendingRequests.forEach((cb) => cb(token))
|
||||
pendingRequests.forEach(({ resolve }) => resolve(token))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
function clearPending(): void {
|
||||
function rejectPending(error: unknown): void {
|
||||
pendingRequests.forEach(({ reject }) => reject(error))
|
||||
pendingRequests = []
|
||||
}
|
||||
|
||||
function waitForRefresh(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingRequests.push({ resolve, reject })
|
||||
})
|
||||
}
|
||||
|
||||
const request: AxiosInstance = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 30_000,
|
||||
@@ -149,18 +161,18 @@ request.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
if (!isPublicPath(config.url) && shouldRefresh() && !config.url?.includes('/auth/refresh')) {
|
||||
if (isRefreshing) {
|
||||
await new Promise<void>((resolve) => {
|
||||
pendingRequests.push(() => resolve())
|
||||
})
|
||||
await waitForRefresh()
|
||||
} else {
|
||||
isRefreshing = true
|
||||
try {
|
||||
const newTokens = await doRefresh()
|
||||
replayPending(newTokens.accessToken)
|
||||
} catch {
|
||||
clearPending()
|
||||
} catch (error) {
|
||||
rejectPending(error)
|
||||
clearTokens()
|
||||
showRequestError(error, config)
|
||||
window.location.hash = '#/tenant/login'
|
||||
return Promise.reject(error)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
@@ -185,6 +197,27 @@ function showErrorToast(message: string, config?: InternalAxiosRequestConfig | A
|
||||
toast.error(message)
|
||||
}
|
||||
|
||||
function showRequestError(error: unknown, config?: InternalAxiosRequestConfig | AxiosRequestConfig) {
|
||||
if (config && '_suppressErrorMsg' in config && (config as any)._suppressErrorMsg) return
|
||||
if (!axios.isAxiosError(error)) return
|
||||
if ((error as any)._errorToastShown) return
|
||||
;(error as any)._errorToastShown = true
|
||||
const responseData = error.response?.data
|
||||
if (responseData && typeof responseData === 'object' && 'message' in responseData) {
|
||||
toast.error(String(responseData.message || '请求失败'))
|
||||
return
|
||||
}
|
||||
if (error.code === 'ECONNABORTED' || error.code === 'ETIMEDOUT') {
|
||||
toast.error('请求超时,请稍后重试')
|
||||
return
|
||||
}
|
||||
if (!error.response) {
|
||||
toast.error('网络连接异常,请检查后端服务是否正常')
|
||||
return
|
||||
}
|
||||
toast.error(`服务器请求失败(${error.response.status})`)
|
||||
}
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
const data = response.data
|
||||
@@ -222,22 +255,20 @@ request.interceptors.response.use(
|
||||
;(err as any).isDemoModeError = error.response.data.code === DEMO_MODE_CODE
|
||||
return Promise.reject(err)
|
||||
}
|
||||
// Network or other unexpected errors
|
||||
if (!error.response) {
|
||||
showErrorToast('网络连接异常,请稍后重试', originalRequest)
|
||||
}
|
||||
showRequestError(error, originalRequest)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
||||
if (isRefreshing) {
|
||||
return new Promise((resolve) => {
|
||||
pendingRequests.push((newToken: string) => {
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest._retry = true
|
||||
resolve(request(originalRequest))
|
||||
})
|
||||
})
|
||||
try {
|
||||
const newToken = await waitForRefresh()
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newToken}`
|
||||
originalRequest._retry = true
|
||||
return request(originalRequest)
|
||||
} catch (refreshError) {
|
||||
return Promise.reject(refreshError)
|
||||
}
|
||||
}
|
||||
|
||||
originalRequest._retry = true
|
||||
@@ -250,11 +281,12 @@ request.interceptors.response.use(
|
||||
originalRequest.headers = originalRequest.headers ?? {}
|
||||
originalRequest.headers.Authorization = `Bearer ${newTokens.accessToken}`
|
||||
return request(originalRequest)
|
||||
} catch {
|
||||
clearPending()
|
||||
} catch (refreshError) {
|
||||
rejectPending(refreshError)
|
||||
clearTokens()
|
||||
showRequestError(refreshError, originalRequest)
|
||||
window.location.hash = '#/tenant/login'
|
||||
return Promise.reject(error)
|
||||
return Promise.reject(refreshError)
|
||||
} finally {
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { toast } from '@/utils/toast'
|
||||
import Icon from '@/components/common/Icon.vue'
|
||||
import MaintenanceBanner from '@/components/common/MaintenanceBanner.vue'
|
||||
import AnnouncementBanner from '@/components/common/AnnouncementBanner.vue'
|
||||
import RouteErrorBoundary from '@/components/common/RouteErrorBoundary.vue'
|
||||
import { marked } from 'marked'
|
||||
import request from '@/utils/request'
|
||||
|
||||
@@ -510,7 +511,11 @@ onBeforeUnmount(() => {
|
||||
class="tenant-main px-3 pb-5 pt-4 md:px-5 md:pb-6 lg:pt-5"
|
||||
:class="{ 'tenant-table-main': route.meta.viewportTable }"
|
||||
>
|
||||
<router-view />
|
||||
<router-view v-slot="{ Component, route: currentRoute }">
|
||||
<RouteErrorBoundary :key="currentRoute.fullPath">
|
||||
<component :is="Component" />
|
||||
</RouteErrorBoundary>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user