Skip to content

Commit 21557d8

Browse files
committed
perf: logout open tabs
1 parent 8990e94 commit 21557d8

File tree

13 files changed

+281
-113
lines changed

13 files changed

+281
-113
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,5 @@ storybook-static
2626
/dist-vite
2727
/dist-vite-zip
2828
/dist-zip
29+
/playwright-report
30+
/test-results

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
"test:coverage": "jest --coverage",
7676
"test:e2e": "playwright test --config=playwright.config.ts",
7777
"test:watch": "jest --watch",
78+
"test:report": "npx playwright show-report",
7879
"update:yarn": "yarn upgrade --latest",
7980
"update:npm": "ncu -u && npm install",
8081
"lint:fix": "npm run eslint:fix && npm run stylelint:fix && npm run prettier:fix",

src/pages/layout/proContent/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ const ProContent = () => {
4545
})
4646
setTabActiveKey(tabKey)
4747
// eslint-disable-next-line react-hooks/exhaustive-deps
48-
}, [pathname, search, panes, activeKey])
48+
}, [pathname, search])
4949

5050
return (
5151
<Layout className={styles.layout} id="fullScreen">

src/pages/layout/proHeader/index.jsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,11 @@ const ProHeader = ({ layout, onSettingClick, children, isMobile, onMobileMenuCli
9494
const { isAuthenticated, user, isLoading } = useAuth()
9595

9696
const handleLogout = () => {
97+
try {
98+
// 兼容“测试账号登录”(仅 token) 的登出路径,确保权限缓存被清理
99+
permissionService.logoutCleanup()
100+
} catch (e) {}
97101
authService.logout()
98-
removeLocalStorage('token')
99-
redirectTo('/signin')
100102
}
101103

102104
const tokenValue = getLocalStorage('token')?.token || 'wkylin.w'
@@ -139,6 +141,10 @@ const ProHeader = ({ layout, onSettingClick, children, isMobile, onMobileMenuCli
139141
if (isAuthenticated && user) {
140142
authService.logout()
141143
} else {
144+
try {
145+
// 测试账号登录不会进入 authService 的“已登录态”,这里也要清理权限缓存
146+
permissionService.logoutCleanup()
147+
} catch (e) {}
142148
removeLocalStorage('token')
143149
redirectTo('/signin')
144150
}
@@ -340,7 +346,7 @@ const ProHeader = ({ layout, onSettingClick, children, isMobile, onMobileMenuCli
340346
)}
341347
<Dropdown arrow menu={{ items }} trigger={['click']}>
342348
{isAuthenticated && user ? (
343-
<Avatar src={user.avatar_url} />
349+
<Avatar src={user.avatar_url || undefined} icon={<UserOutlined style={{ fontSize: 16 }} />} />
344350
) : (
345351
<Button
346352
icon={<UserOutlined style={{ fontSize: 16 }} />}

src/pages/layout/proTabs/index.jsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { Tabs, Dropdown, Space, theme, Button } from 'antd'
55
import StickyBox from 'react-sticky-box'
66
import { SyncOutlined, FireOutlined, DownOutlined } from '@ant-design/icons'
77
import ErrorBoundary from '@/components/ErrorBoundary'
8-
import { nanoid } from 'nanoid'
98
import { useTranslation } from 'react-i18next'
109
import { useProTabContext } from '@app-hooks/proTabsContext'
1110
import Loading from '@src/components/stateless/Loading'
@@ -30,12 +29,7 @@ const ProTabs = (props) => {
3029

3130
const renderTabBar = (props, DefaultTabBar) => (
3231
<StickyBox offsetTop={0} style={{ zIndex: 10 }}>
33-
<DefaultTabBar
34-
key={nanoid()}
35-
{...props}
36-
className="pro-tabs"
37-
style={{ ...props.style, backgroundColor: colorBgContainer }}
38-
/>
32+
<DefaultTabBar {...props} className="pro-tabs" style={{ ...props.style, backgroundColor: colorBgContainer }} />
3933
</StickyBox>
4034
)
4135

@@ -89,7 +83,7 @@ const ProTabs = (props) => {
8983
}
9084

9185
const onTabScroll = ({ direction }) => {
92-
console.log('direction', direction)
86+
// no-op: avoid logging on scroll (can cause jank)
9387
}
9488

9589
const onEdit = (targetKey, action) => {

src/pages/signin/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,9 @@ const SignIn = () => {
9696
console.warn('清除手动设置角色失败:', e)
9797
}
9898

99-
// 同步权限
99+
// 纳入统一登录态(测试账号也算已登录),并同步权限
100100
try {
101-
await permissionService.syncPermissions()
101+
await authService.setTestAccountAuthenticated(email)
102102
const routes = await permissionService.getAccessibleRoutes(true)
103103

104104
message.success(`登录成功!欢迎 ${testAccounts[email].name}`)

src/service/authService.ts

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ interface GitHubEmailResponse {
3333
visibility: string | null
3434
}
3535

36+
function isLikelyEmail(value: string): boolean {
37+
return /^\S+@\S+\.\S+$/.test(value)
38+
}
39+
40+
function buildTestAccountUser(email: string): GitHubUser {
41+
// 用于“测试账号登录”展示;不代表真实 GitHub 用户
42+
const login = email
43+
return {
44+
id: 0,
45+
login,
46+
name: login,
47+
email,
48+
avatar_url: '',
49+
html_url: '',
50+
}
51+
}
52+
3653
// ✅ 修复 2: parseGitHubUser - 正确的类型守卫
3754
function parseGitHubUser(jsonLike: unknown): GitHubUser | null {
3855
if (typeof jsonLike !== 'string') return null
@@ -121,6 +138,19 @@ class AuthService {
121138
isAuthenticated: true,
122139
isLoading: false,
123140
}
141+
return
142+
}
143+
144+
// 兼容“测试账号登录”:localStorage.token = { token: email }
145+
const testTokenData = localStorage.getItem('token')
146+
const testToken = parseToken(testTokenData)
147+
if (testToken && isLikelyEmail(testToken)) {
148+
this.authState = {
149+
token: testToken,
150+
user: buildTestAccountUser(testToken),
151+
isAuthenticated: true,
152+
isLoading: false,
153+
}
124154
}
125155
} catch (error) {
126156
logger.error('Failed to load auth state from storage:', error)
@@ -185,6 +215,37 @@ class AuthService {
185215
return new Promise((resolve) => setTimeout(resolve, 100))
186216
}
187217

218+
/**
219+
* 测试账号登录:将 demo 登录态也纳入 authService(避免 Header/菜单逻辑分裂)。
220+
* - 不写入 github_token/github_user,仅依赖 localStorage.token
221+
*/
222+
async setTestAccountAuthenticated(email: string): Promise<void> {
223+
this.authState = {
224+
user: buildTestAccountUser(email),
225+
token: email,
226+
isAuthenticated: true,
227+
isLoading: false,
228+
}
229+
230+
// 避免残留 GitHub 登录态影响鉴权逻辑
231+
try {
232+
localStorage.removeItem('github_token')
233+
localStorage.removeItem('github_user')
234+
} catch (e) {
235+
// ignore
236+
}
237+
238+
this.notifyListeners()
239+
240+
try {
241+
await permissionService.syncPermissions()
242+
} catch (e) {
243+
logger.warn('同步权限失败:', e)
244+
}
245+
246+
return new Promise((resolve) => setTimeout(resolve, 100))
247+
}
248+
188249
setLoading(isLoading: boolean): Promise<void> {
189250
return new Promise((resolve) => {
190251
this.authState.isLoading = isLoading
@@ -305,17 +366,15 @@ class AuthService {
305366
isAuthenticated: false,
306367
isLoading: false,
307368
}
308-
// 清除权限缓存,确保下次登录时重新获取并刷新菜单
369+
// 登出时清理权限相关缓存与覆盖,避免下次登录残留旧权限
309370
try {
310-
permissionService.clearCache()
371+
permissionService.logoutCleanup()
311372
} catch (e) {
312-
logger.warn('清除权限缓存失败:', e)
373+
logger.warn('清除权限相关缓存失败:', e)
313374
}
314-
// 清除所有相关的 localStorage 键
375+
376+
// 清除所有相关的 localStorage 键(token 等)
315377
try {
316-
localStorage.removeItem('user_permissions')
317-
localStorage.removeItem('user_role')
318-
localStorage.removeItem('permissions_fetch_time')
319378
localStorage.removeItem('token') // 测试账号登录的 token
320379
localStorage.removeItem('github_token') // GitHub OAuth token
321380
localStorage.removeItem('github_user') // GitHub user info
@@ -326,7 +385,7 @@ class AuthService {
326385
this.notifyListeners()
327386
// SPA 跳转到登录页
328387
if (typeof window !== 'undefined') {
329-
window.location.href = '/signin'
388+
window.location.href = '#/signin'
330389
}
331390
}
332391

src/service/permissionService.ts

Lines changed: 111 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,74 @@ class PermissionService {
1414
private readonly cacheExpireTime: number = 30 * 60 * 1000 // 30分钟
1515
private lastFetchTime: number = 0
1616

17+
private readonly STORAGE_KEYS = {
18+
PERMISSIONS: 'user_permissions',
19+
FETCH_TIME: 'permissions_fetch_time',
20+
AUTH_KEY: 'permissions_auth_key',
21+
// 用于开发/演示的本地覆盖(不应污染真实登录态)
22+
ROLE_OVERRIDE: 'user_role',
23+
FORCE_DEMO_SWITCH: 'force_demo_switch',
24+
} as const
25+
1726
private constructor() {
1827
// 从 localStorage 恢复权限信息
1928
this.loadFromStorage()
2029
}
2130

31+
/**
32+
* 当前登录身份指纹(用于避免“切换账号/登出后仍复用旧权限缓存”)
33+
* - 测试账号:localStorage.token = { token: email }
34+
* - GitHub OAuth:github_user / github_token
35+
* - 开发覆盖:user_role(会影响 mock 权限计算)
36+
*/
37+
private getCurrentAuthKey(): string {
38+
const safeRead = (key: string) => {
39+
try {
40+
return localStorage.getItem(key) || ''
41+
} catch {
42+
return ''
43+
}
44+
}
45+
46+
const roleOverride = safeRead(this.STORAGE_KEYS.ROLE_OVERRIDE)
47+
48+
const githubUser = safeRead('github_user')
49+
const githubToken = safeRead('github_token')
50+
if (githubUser) {
51+
try {
52+
const obj = JSON.parse(githubUser)
53+
const email = obj?.email || ''
54+
const login = obj?.login || ''
55+
const id = obj?.id || ''
56+
return `github:${email || login || id}${roleOverride ? `|role:${roleOverride}` : ''}`
57+
} catch {
58+
return `github:${githubUser}${roleOverride ? `|role:${roleOverride}` : ''}`
59+
}
60+
}
61+
if (githubToken) {
62+
return `githubToken:${githubToken}${roleOverride ? `|role:${roleOverride}` : ''}`
63+
}
64+
65+
// 测试账号 token
66+
const rawToken = safeRead('token')
67+
if (rawToken) {
68+
try {
69+
const obj = JSON.parse(rawToken)
70+
const token = obj?.token || rawToken
71+
return `token:${token}${roleOverride ? `|role:${roleOverride}` : ''}`
72+
} catch {
73+
return `token:${rawToken}${roleOverride ? `|role:${roleOverride}` : ''}`
74+
}
75+
}
76+
77+
// 未登录但存在 roleOverride 的情况(演示页)
78+
if (roleOverride) {
79+
return `anonymous|role:${roleOverride}`
80+
}
81+
82+
return 'anonymous'
83+
}
84+
2285
static getInstance(): PermissionService {
2386
if (!PermissionService.instance) {
2487
PermissionService.instance = new PermissionService()
@@ -31,8 +94,22 @@ class PermissionService {
3194
*/
3295
private loadFromStorage(): void {
3396
try {
34-
const stored = localStorage.getItem('user_permissions')
35-
const lastFetch = localStorage.getItem('permissions_fetch_time')
97+
const stored = localStorage.getItem(this.STORAGE_KEYS.PERMISSIONS)
98+
const lastFetch = localStorage.getItem(this.STORAGE_KEYS.FETCH_TIME)
99+
const storedAuthKey = localStorage.getItem(this.STORAGE_KEYS.AUTH_KEY)
100+
const currentAuthKey = this.getCurrentAuthKey()
101+
102+
// 如果登录身份发生变化,直接丢弃旧缓存,避免残留旧权限
103+
if (storedAuthKey && storedAuthKey !== currentAuthKey) {
104+
this.clearCache()
105+
return
106+
}
107+
108+
// 兼容旧版本:若存在权限缓存但没有 authKey,视为不可信,直接丢弃
109+
if (!storedAuthKey && stored) {
110+
this.clearCache()
111+
return
112+
}
36113

37114
if (stored && lastFetch) {
38115
this.userPermissions = JSON.parse(stored)
@@ -55,8 +132,9 @@ class PermissionService {
55132
*/
56133
private saveToStorage(permissions: UserPermission): void {
57134
try {
58-
localStorage.setItem('user_permissions', JSON.stringify(permissions))
59-
localStorage.setItem('permissions_fetch_time', Date.now().toString())
135+
localStorage.setItem(this.STORAGE_KEYS.PERMISSIONS, JSON.stringify(permissions))
136+
localStorage.setItem(this.STORAGE_KEYS.FETCH_TIME, Date.now().toString())
137+
localStorage.setItem(this.STORAGE_KEYS.AUTH_KEY, this.getCurrentAuthKey())
60138
this.userPermissions = permissions
61139
this.lastFetchTime = Date.now()
62140
} catch (error) {
@@ -70,8 +148,23 @@ class PermissionService {
70148
clearCache(): void {
71149
this.userPermissions = null
72150
this.lastFetchTime = 0
73-
localStorage.removeItem('user_permissions')
74-
localStorage.removeItem('permissions_fetch_time')
151+
localStorage.removeItem(this.STORAGE_KEYS.PERMISSIONS)
152+
localStorage.removeItem(this.STORAGE_KEYS.FETCH_TIME)
153+
localStorage.removeItem(this.STORAGE_KEYS.AUTH_KEY)
154+
}
155+
156+
/**
157+
* 登出时清理(仅清理“权限相关”本地状态,不直接清理 token)
158+
* - 解决:登录/登出或 401 跳转时权限缓存残留,导致下个账号复用旧权限
159+
*/
160+
logoutCleanup(): void {
161+
this.clearCache()
162+
try {
163+
localStorage.removeItem(this.STORAGE_KEYS.ROLE_OVERRIDE)
164+
localStorage.removeItem(this.STORAGE_KEYS.FORCE_DEMO_SWITCH)
165+
} catch (e) {
166+
console.warn('清理权限相关本地覆盖失败:', e)
167+
}
75168
}
76169

77170
/**
@@ -80,6 +173,18 @@ class PermissionService {
80173
* @param userId 用户ID(可选,用于获取特定用户的权限)
81174
*/
82175
async getPermissions(forceRefresh: boolean = false, userId?: string): Promise<UserPermission> {
176+
// 若登录身份发生变化,强制清理旧缓存并重新获取
177+
try {
178+
const storedAuthKey = localStorage.getItem(this.STORAGE_KEYS.AUTH_KEY)
179+
const currentAuthKey = this.getCurrentAuthKey()
180+
if (storedAuthKey && storedAuthKey !== currentAuthKey) {
181+
this.clearCache()
182+
forceRefresh = true
183+
}
184+
} catch {
185+
// ignore
186+
}
187+
83188
// 如果强制刷新或缓存过期,重新获取
84189
const now = Date.now()
85190
const isExpired = now - this.lastFetchTime > this.cacheExpireTime

src/service/request.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,17 @@ axiosInstance.interceptors.request.use(
273273
// 处理未授权
274274
function handleUnauthorized(message) {
275275
// authService.logout() // 移除以避免循环依赖
276+
// 清理权限缓存与开发覆盖,避免跳转登录后仍沿用旧权限
277+
try {
278+
localStorage.removeItem('user_permissions')
279+
localStorage.removeItem('permissions_fetch_time')
280+
localStorage.removeItem('permissions_auth_key')
281+
localStorage.removeItem('user_role')
282+
localStorage.removeItem('force_demo_switch')
283+
} catch (e) {
284+
// ignore
285+
}
286+
276287
localStorage.removeItem('token')
277288
localStorage.removeItem('github_token')
278289
localStorage.removeItem('github_user')

0 commit comments

Comments
 (0)