Skip to content

Commit 383df74

Browse files
fix(chatui): refactor routing and layout to drive UI mode from URL and scope state to sessionStorage (#6535)
* 移除所有使用 localStorage 的路由,改为使用 sessionStorage * 增加修正 * 增加修正 * 增加修正 * 增加修正 * 增加修正 * 回退修正 就這樣吧 * 小修正
1 parent 2662788 commit 383df74

4 files changed

Lines changed: 147 additions & 106 deletions

File tree

dashboard/src/composables/useSessions.ts

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ export function useSessions(chatboxMode: boolean = false) {
1919
const selectedSessions = ref<string[]>([]);
2020
const currSessionId = ref('');
2121
const pendingSessionId = ref<string | null>(null);
22-
2322
// 编辑标题相关
2423
const editTitleDialog = ref(false);
2524
const editingTitle = ref('');
@@ -30,29 +29,16 @@ export function useSessions(chatboxMode: boolean = false) {
3029
return sessions.value.find(s => s.session_id === currSessionId.value);
3130
});
3231

32+
33+
3334
async function getSessions() {
3435
try {
3536
const response = await axios.get('/api/chat/sessions');
3637
sessions.value = response.data.data;
3738

38-
// 处理待加载的会话
39-
if (pendingSessionId.value) {
40-
const session = sessions.value.find(s => s.session_id === pendingSessionId.value);
41-
if (session) {
42-
selectedSessions.value = [pendingSessionId.value];
43-
pendingSessionId.value = null;
44-
}
45-
} else if (currSessionId.value) {
46-
// 如果当前有选中的会话,确保它在列表中并被选中
47-
const session = sessions.value.find(s => s.session_id === currSessionId.value);
48-
if (session) {
49-
selectedSessions.value = [currSessionId.value];
50-
}
51-
} else if (sessions.value.length > 0) {
52-
// 默认选择第一个会话
53-
const firstSession = sessions.value[0];
54-
selectedSessions.value = [firstSession.session_id];
55-
}
39+
40+
41+
5642
} catch (err: any) {
5743
if (err.response?.status === 401) {
5844
router.push('/auth/login?redirect=/chatbox');

dashboard/src/layouts/full/FullLayout.vue

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,10 @@ const customizer = useCustomizerStore();
1717
const { locale } = useI18n();
1818
const route = useRoute();
1919
const routerLoadingStore = useRouterLoadingStore();
20+
const isCurrentChatRoute = computed(() => route.path === '/chat' || route.path.startsWith('/chat/'));
2021
21-
const isChatPage = computed(() => {
22-
return route.path.startsWith('/chat');
23-
});
24-
25-
const showSidebar = computed(() => {
26-
return customizer.viewMode === 'bot';
27-
});
2822
29-
const showChatPage = computed(() => {
30-
return customizer.viewMode === 'chat';
31-
});
23+
const showSidebar = computed(() => !isCurrentChatRoute.value)
3224
3325
const migrationDialog = ref<InstanceType<typeof MigrationDialog> | null>(null);
3426
const showFirstNoticeDialog = ref(false);
@@ -111,20 +103,20 @@ onMounted(() => {
111103
<VerticalHeaderVue />
112104
<VerticalSidebarVue v-if="showSidebar" />
113105
<v-main :style="{
114-
height: showChatPage ? 'calc(100vh - 55px)' : undefined,
115-
overflow: showChatPage ? 'hidden' : undefined
106+
height: isCurrentChatRoute ? 'calc(100vh - 55px)' : undefined,
107+
overflow: isCurrentChatRoute ? 'hidden' : undefined
116108
}">
117109
<v-container
118110
fluid
119111
class="page-wrapper"
120-
:class="{ 'chat-mode-container': showChatPage }"
112+
:class="{ 'chat-mode-container': isCurrentChatRoute }"
121113
:style="{
122-
height: showChatPage ? '100%' : 'calc(100% - 8px)',
123-
padding: (isChatPage || showChatPage) ? '0' : undefined,
124-
minHeight: showChatPage ? 'unset' : undefined
114+
height: isCurrentChatRoute ? '100%' : 'calc(100% - 8px)',
115+
padding: isCurrentChatRoute ? '0' : undefined,
116+
minHeight: isCurrentChatRoute ? 'unset' : undefined
125117
}">
126-
<div :style="{ height: '100%', width: '100%', overflow: showChatPage ? 'hidden' : undefined }">
127-
<div v-if="showChatPage" style="height: 100%; width: 100%; overflow: hidden;">
118+
<div :style="{ height: '100%', width: '100%', overflow: isCurrentChatRoute ? 'hidden' : undefined }">
119+
<div v-if="isCurrentChatRoute" style="height: 100%; width: 100%; overflow: hidden;">
128120
<Chat />
129121
</div>
130122
<RouterView v-else />

dashboard/src/layouts/full/vertical-header/VerticalHeader.vue

Lines changed: 131 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const theme = useTheme();
2828
const { t } = useI18n();
2929
const route = useRoute();
3030
const LAST_BOT_ROUTE_KEY = 'astrbot:last_bot_route';
31+
const LAST_CHAT_ROUTE_KEY = 'astrbot:last_chat_route';
3132
let dialog = ref(false);
3233
let accountWarning = ref(false)
3334
let updateStatusDialog = ref(false);
@@ -58,7 +59,9 @@ const desktopUpdateHasNewVersion = ref(false);
5859
const desktopUpdateCurrentVersion = ref('-');
5960
const desktopUpdateLatestVersion = ref('-');
6061
const desktopUpdateStatus = ref('');
61-
62+
const isChatPath = computed(() =>
63+
route.path === '/chat' || route.path.startsWith('/chat/')
64+
);
6265
const getAppUpdaterBridge = (): AstrBotAppUpdaterBridge | null => {
6366
if (typeof window === 'undefined') {
6467
return null;
@@ -380,7 +383,7 @@ function openReleaseNotesDialog(body: string, tag: string) {
380383
}
381384
382385
function handleLogoClick() {
383-
if (customizer.viewMode === 'chat') {
386+
if (isChatPath.value) {
384387
aboutDialog.value = true;
385388
} else {
386389
router.push('/about');
@@ -395,37 +398,84 @@ commonStore.createEventSource(); // log
395398
commonStore.getStartTime();
396399
397400
// 视图模式切换
398-
const viewMode = computed({
399-
get: () => customizer.viewMode,
400-
set: (value: 'bot' | 'chat') => {
401-
customizer.SET_VIEW_MODE(value);
401+
onMounted(() => {
402+
// 初次加載時保存當前路由
403+
if (typeof window !== 'undefined') {
404+
if (isChatPath.value) {
405+
// 保存 chat ID
406+
const parts = route.fullPath.split('/');
407+
const sessionId = parts[2];
408+
if (sessionId) {
409+
sessionStorage.setItem(LAST_CHAT_ROUTE_KEY, sessionId);
410+
console.log('Initial save chat ID:', sessionId);
411+
}
412+
} else {
413+
// 保存 bot 路由(非 chat 頁面)
414+
sessionStorage.setItem(LAST_BOT_ROUTE_KEY, route.fullPath);
415+
console.log('Initial save bot route:', route.fullPath);
416+
}
402417
}
403418
});
404419
405420
// 监听 viewMode 变化,切换到 bot 模式时跳转到首页
406421
// 保存 bot 模式的最後路由
407422
// 監聽 route 變化,保存最後一次 bot 路由
408423
watch(() => route.fullPath, (newPath) => {
409-
if (customizer.viewMode === 'bot' && typeof window !== 'undefined') {
410-
try {
411-
localStorage.setItem(LAST_BOT_ROUTE_KEY, newPath);
412-
} catch (e) {
413-
console.error('Failed to save last bot route to localStorage:', e);
424+
if (typeof window === 'undefined') return;
425+
console.log('Route changed:', {
426+
newPath,
427+
isChat: isChatPath.value,
428+
currentChatId: route.params.id
429+
});
430+
try {
431+
// 使用現有的 isChatPath 計算屬性來避免名稱衝突
432+
const isChat = isChatPath.value; // 這裡使用已經計算好的 isChatPath
433+
434+
// ✅ bot:只存「非 chat 頁」
435+
if (!isChat) {
436+
sessionStorage.setItem(LAST_BOT_ROUTE_KEY, newPath);
414437
}
438+
439+
// ✅ chat:只存 sessionId
440+
if (isChat) {
441+
const parts = newPath.split('/');
442+
const sessionId = parts[2];
443+
444+
if (sessionId) {
445+
sessionStorage.setItem(LAST_CHAT_ROUTE_KEY, sessionId);
446+
}
447+
}
448+
449+
} catch (e) {
450+
console.error('Failed to save route:', e);
415451
}
416452
});
417453
418-
// 監聽 viewMode 切換
419-
watch(() => customizer.viewMode, (newMode, oldMode) => {
420-
if (newMode === 'bot' && oldMode === 'chat' && typeof window !== 'undefined') {
421-
// 從 chat 切換回 bot,跳轉到最後一次的 bot 路由
422-
let lastBotRoute = '/';
454+
const currentMode = computed({
455+
get: () => (isChatPath.value ? 'chat' : 'bot'),
456+
set: (val: 'chat' | 'bot') => {
423457
try {
424-
lastBotRoute = localStorage.getItem(LAST_BOT_ROUTE_KEY) || '/';
458+
// 檢查 window 和 sessionStorage 是否存在
459+
if (typeof window === 'undefined' || typeof sessionStorage === 'undefined') {
460+
// 如果在非瀏覽器環境中,不做任何 sessionStorage 操作
461+
console.warn('sessionStorage is not available in this environment');
462+
return;
463+
}
464+
465+
if (val === 'chat') {
466+
const lastSessionId = sessionStorage.getItem(LAST_CHAT_ROUTE_KEY);
467+
router.push(lastSessionId ? `/chat/${lastSessionId}` : '/chat');
468+
} else {
469+
let lastBotRoute = sessionStorage.getItem(LAST_BOT_ROUTE_KEY) || '/';
470+
if (lastBotRoute.startsWith('/chat')) {
471+
lastBotRoute = '/';
472+
}
473+
router.push(lastBotRoute);
474+
}
425475
} catch (e) {
426-
console.error('Failed to read last bot route from localStorage:', e);
476+
// 在受限隱私模式等環境中,sessionStorage 操作可能會拋出 SecurityError
477+
console.warn('Failed to access sessionStorage in currentMode setter:', e);
427478
}
428-
router.push(lastBotRoute);
429479
}
430480
});
431481
@@ -465,29 +515,46 @@ onMounted(async () => {
465515
<v-app-bar elevation="0" height="50" class="top-header">
466516

467517
<!-- 桌面端 menu 按钮 - 仅在 bot 模式下显示 -->
468-
<v-btn v-if="customizer.viewMode === 'bot'"
469-
style="margin-left: 16px;"
470-
class="hidden-md-and-down" icon rounded="sm" variant="flat"
471-
@click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)">
472-
<v-icon>mdi-menu</v-icon>
473-
</v-btn>
474-
<!-- 移动端 menu 按钮 - 仅在 bot 模式下显示 -->
475-
<v-btn v-if="customizer.viewMode === 'bot'" class="hidden-lg-and-up ms-3" icon rounded="sm" variant="flat"
476-
@click.stop="customizer.SET_SIDEBAR_DRAWER">
477-
<v-icon>mdi-menu</v-icon>
478-
</v-btn>
479-
480-
<!-- 移动端 chat sidebar 展开按钮 - 仅在 chat 模式下的小屏幕显示 -->
481-
<v-btn v-if="customizer.viewMode === 'chat'" class="hidden-lg-and-up ms-1" icon rounded="sm" variant="flat"
482-
@click.stop="customizer.TOGGLE_CHAT_SIDEBAR()">
483-
<v-icon>mdi-menu</v-icon>
484-
</v-btn>
485-
486-
<div class="logo-container" :class="{ 'mobile-logo': $vuetify.display.xs, 'chat-mode-logo': customizer.viewMode === 'chat' }" @click="handleLogoClick">
518+
<v-btn
519+
v-if="!isChatPath"
520+
style="margin-left: 16px;"
521+
class="hidden-md-and-down"
522+
icon
523+
rounded="sm"
524+
variant="flat"
525+
@click.stop="customizer.SET_MINI_SIDEBAR(!customizer.mini_sidebar)"
526+
>
527+
<v-icon>mdi-menu</v-icon>
528+
</v-btn>
529+
530+
<!-- 移动端 menu 按钮 -->
531+
<v-btn
532+
v-if="!isChatPath"
533+
class="hidden-lg-and-up ms-3"
534+
icon
535+
rounded="sm"
536+
variant="flat"
537+
@click.stop="customizer.SET_SIDEBAR_DRAWER"
538+
>
539+
<v-icon>mdi-menu</v-icon>
540+
</v-btn>
541+
542+
<v-btn
543+
v-if="isChatPath"
544+
class="hidden-lg-and-up ms-1"
545+
icon
546+
rounded="sm"
547+
variant="flat"
548+
@click.stop="customizer.TOGGLE_CHAT_SIDEBAR()"
549+
>
550+
<v-icon>mdi-menu</v-icon>
551+
</v-btn>
552+
553+
<div class="logo-container" :class="{ 'mobile-logo': $vuetify.display.xs, 'chat-mode-logo': isChatPath }" @click="handleLogoClick">
487554
<span class="logo-text Outfit">Astr<span class="logo-text bot-text-wrapper">Bot
488555
<img v-if="isChristmas" src="@/assets/images/xmas-hat.png" alt="Christmas hat" class="xmas-hat" />
489556
</span></span>
490-
<span class="logo-text logo-text-light Outfit" style="color: grey;" v-if="customizer.viewMode === 'chat'">ChatUI</span>
557+
<span class="logo-text logo-text-light Outfit" style="color: grey;" v-if="isChatPath">ChatUI</span>
491558
<span class="version-text hidden-xs">{{ botCurrVersion }}</span>
492559
</div>
493560

@@ -504,23 +571,23 @@ onMounted(async () => {
504571
</div>
505572

506573
<!-- Bot/Chat 模式切换按钮 - 手机端隐藏,移入 ... 菜单 -->
507-
<v-btn-toggle
508-
v-model="viewMode"
509-
mandatory
510-
variant="outlined"
511-
density="compact"
512-
class="mr-4 hidden-xs"
513-
color="primary"
514-
>
515-
<v-btn value="bot" size="small">
516-
<v-icon start>mdi-robot</v-icon>
517-
Bot
518-
</v-btn>
519-
<v-btn value="chat" size="small">
520-
<v-icon start>mdi-chat</v-icon>
521-
Chat
522-
</v-btn>
523-
</v-btn-toggle>
574+
<v-btn-toggle
575+
v-model="currentMode"
576+
mandatory
577+
variant="outlined"
578+
density="compact"
579+
class="mr-4 hidden-xs"
580+
color="primary"
581+
>
582+
<v-btn value="bot" size="small">
583+
<v-icon start>mdi-robot</v-icon>
584+
Bot
585+
</v-btn>
586+
<v-btn value="chat" size="small">
587+
<v-icon start>mdi-chat</v-icon>
588+
Chat
589+
</v-btn>
590+
</v-btn-toggle>
524591

525592

526593
<!-- 功能菜单 -->
@@ -542,14 +609,14 @@ onMounted(async () => {
542609
<!-- Bot/Chat 模式切换 - 仅在手机端显示 -->
543610
<template v-if="$vuetify.display.xs">
544611
<div class="mobile-mode-toggle-wrapper">
545-
<v-btn-toggle
546-
v-model="viewMode"
547-
mandatory
548-
variant="outlined"
549-
density="compact"
550-
color="primary"
551-
class="mobile-mode-toggle"
552-
>
612+
<v-btn-toggle
613+
v-model="currentMode"
614+
mandatory
615+
variant="outlined"
616+
density="compact"
617+
class="mobile-mode-toggle"
618+
color="primary"
619+
>
553620
<v-btn value="bot" size="small">
554621
<v-icon start>mdi-robot</v-icon>
555622
Bot

dashboard/src/stores/customizer.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ export const useCustomizerStore = defineStore({
1010
fontTheme: "Poppins",
1111
uiTheme: config.uiTheme,
1212
inputBg: config.inputBg,
13-
viewMode: (localStorage.getItem('viewMode') as 'bot' | 'chat') || 'bot', // 'bot' 或 'chat'
1413
chatSidebarOpen: false // chat mode mobile sidebar state
1514
}),
1615

@@ -29,10 +28,7 @@ export const useCustomizerStore = defineStore({
2928
this.uiTheme = payload;
3029
localStorage.setItem("uiTheme", payload);
3130
},
32-
SET_VIEW_MODE(payload: 'bot' | 'chat') {
33-
this.viewMode = payload;
34-
localStorage.setItem('viewMode', payload);
35-
},
31+
3632
TOGGLE_CHAT_SIDEBAR() {
3733
this.chatSidebarOpen = !this.chatSidebarOpen;
3834
},

0 commit comments

Comments
 (0)