修复鉴权:401拦截器加refresh重试 + admin refresh接口 + 路由守卫完善

根因:access_token每2小时过期,前端收到401直接清token跳登录,没有用refresh_token续期

后端修复:
- adminauth模块新增 POST /admin/auth/refresh 接口
- Service 注入 JWTManager,支持 admin refresh token 换新 token pair
- Refresh 方法验证 subjectType=admin + tokenType=refresh

前端修复:
- 401 拦截器核心改造:收到401先调 refresh 接口续期
- 加 isRefreshing 锁 + pendingRequests 队列防止并发刷新
- refresh 用原生 axios.post 避免拦截器递归
- 成功则更新 localStorage + 重试原请求,失败才清 token 跳登录
- 排除 /auth/refresh 自身避免死循环
- 支持 /admin/ 请求独立 token 管理
- auth.ts/adminAuth.ts 新增手动 refreshUserToken/refreshAdminSession
- 路由守卫给所有需登录路由添加 meta.requiresAuth
- 守卫同时支持 PC 端 /login 和移动端 /m/login
This commit is contained in:
yml2213
2026-05-24 07:00:13 +08:00
parent 3631e70321
commit cdee93c7c5
61 changed files with 1735 additions and 722 deletions
+65 -16
View File
@@ -78,11 +78,14 @@ const visibleListings = computed(() => {
.join(" ")
.toLowerCase();
if (keyword && !text.includes(keyword)) return false;
if (filters.server && getServerRegion(item) !== filters.server) return false;
if (filters.loginMethod && getLoginMethod(item) !== filters.loginMethod) return false;
if (filters.server && getServerRegion(item) !== filters.server)
return false;
if (filters.loginMethod && getLoginMethod(item) !== filters.loginMethod)
return false;
if (filters.rank && item.rank_level !== filters.rank) return false;
if (filters.minCoin && getCoinM(item) < filters.minCoin) return false;
if (filters.maxPrice && getListingDisplayPrice(item) > filters.maxPrice) return false;
if (filters.maxPrice && getListingDisplayPrice(item) > filters.maxPrice)
return false;
return true;
});
@@ -103,7 +106,11 @@ const visibleListings = computed(() => {
});
const statCards = computed(() => [
{ label: "可租账号", value: `${visibleListings.value.length}`, hint: "当前筛选结果" },
{
label: "可租账号",
value: `${visibleListings.value.length}`,
hint: "当前筛选结果",
},
{
label: "高哈夫币",
value: `${listings.value.filter((item) => getCoinM(item) >= 100).length}`,
@@ -111,7 +118,10 @@ const statCards = computed(() => [
},
{
label: "账密登录",
value: `${listings.value.filter((item) => getLoginMethod(item) === "账号密码").length}`,
value: `${
listings.value.filter((item) => getLoginMethod(item) === "账号密码")
.length
}`,
hint: "交接更快",
},
]);
@@ -125,7 +135,7 @@ async function loadHome() {
fetchListings(),
fetchMobileHomeConfig(),
]);
listings.value = nextListings;
listings.value = nextListings.items;
announcements.value = config.announcements;
banners.value = config.banners;
publishOptions.value = config.publish_options;
@@ -165,7 +175,10 @@ function uniqueOptions(values: string[]) {
</div>
<div class="home-searchbar">
<el-icon><Search /></el-icon>
<input v-model="filters.keyword" placeholder="搜区服 / 段位 / 哈夫币 / 皮肤" />
<input
v-model="filters.keyword"
placeholder="搜区服 / 段位 / 哈夫币 / 皮肤"
/>
</div>
<RouterLink class="home-service" to="/notifications">
<el-icon><Headset /></el-icon>
@@ -205,13 +218,27 @@ function uniqueOptions(values: string[]) {
<label>
<span>区服</span>
<el-select v-model="filters.server" clearable placeholder="全部区服">
<el-option v-for="item in serverOptions" :key="item" :label="item" :value="item" />
<el-option
v-for="item in serverOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</label>
<label>
<span>上号方式</span>
<el-select v-model="filters.loginMethod" clearable placeholder="全部方式">
<el-option v-for="item in loginMethodOptions" :key="item" :label="item" :value="item" />
<el-select
v-model="filters.loginMethod"
clearable
placeholder="全部方式"
>
<el-option
v-for="item in loginMethodOptions"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</label>
<label>
@@ -249,18 +276,30 @@ function uniqueOptions(values: string[]) {
<div class="quick-chip-group">
<button type="button" @click="filters.minCoin = 100">100M+</button>
<button type="button" @click="filters.minCoin = 300">300M+</button>
<button type="button" @click="filters.loginMethod = '账号密码'">账密优先</button>
<button type="button" @click="filters.loginMethod = '账号密码'">
账密优先
</button>
<button type="button" @click="filters.server = 'QQ'">QQ 区服</button>
</div>
</aside>
<main class="home-main">
<section class="hero-board">
<el-carousel height="210px" indicator-position="outside" :interval="3600">
<el-carousel-item v-for="slide in banners" :key="slide.title || slide.image_url">
<el-carousel
height="210px"
indicator-position="outside"
:interval="3600"
>
<el-carousel-item
v-for="slide in banners"
:key="slide.title || slide.image_url"
>
<div
class="hero-slide"
:class="[`tone-${slide.tone}`, { 'has-image': slide.image_url }]"
:class="[
`tone-${slide.tone}`,
{ 'has-image': slide.image_url },
]"
>
<img
v-if="slide.image_url"
@@ -335,7 +374,13 @@ function uniqueOptions(values: string[]) {
<div class="desktop-title">
<strong>{{ getListingTitle(item) }}</strong>
</div>
<p>{{ getListingSubtitle(item) || item.description || "平台担保交接,资料以截图为准。" }}</p>
<p>
{{
getListingSubtitle(item) ||
item.description ||
"平台担保交接,资料以截图为准。"
}}
</p>
<div class="desktop-tags">
<span>{{ getServerRegion(item) }}</span>
<span>{{ getLoginMethod(item) }}</span>
@@ -598,7 +643,11 @@ function uniqueOptions(values: string[]) {
position: absolute;
inset: 0;
content: "";
background: linear-gradient(90deg, rgba(10, 18, 32, 0.68), rgba(10, 18, 32, 0.12));
background: linear-gradient(
90deg,
rgba(10, 18, 32, 0.68),
rgba(10, 18, 32, 0.12)
);
}
.hero-copy {