feat: 子Tab支持路由状态缓存

This commit is contained in:
digua
2025-12-14 23:51:13 +08:00
parent 5c2357c410
commit ba86905df5
7 changed files with 76 additions and 28 deletions
+35 -1
View File
@@ -1,9 +1,11 @@
<script setup lang="ts">
import { computed } from 'vue'
import { computed, watch, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
/**
* 轻量级子标签页组件
* 适用于页面内部的二级导航,使用原生样式
* 支持通过 persistKey 将选中状态同步到 URL 查询参数
*/
interface TabItem {
id: string
@@ -14,6 +16,8 @@ interface TabItem {
interface Props {
modelValue: string
items: TabItem[]
/** 持久化 key,设置后会将当前 tab 状态同步到 URL 查询参数 */
persistKey?: string
}
interface Emits {
@@ -24,6 +28,9 @@ interface Emits {
const props = defineProps<Props>()
const emit = defineEmits<Emits>()
const route = useRoute()
const router = useRouter()
// 计算内部值
const activeTab = computed({
get: () => props.modelValue,
@@ -37,6 +44,33 @@ const activeTab = computed({
const handleTabClick = (tabId: string) => {
activeTab.value = tabId
}
// 从 URL 查询参数恢复 tab 状态
onMounted(() => {
if (props.persistKey) {
const savedTab = route.query[props.persistKey] as string
// 验证 savedTab 是否在 items 中存在
if (savedTab && props.items.some((item) => item.id === savedTab)) {
activeTab.value = savedTab
}
}
})
// 监听 tab 变化,同步到 URL 查询参数
watch(
() => props.modelValue,
(newValue) => {
if (props.persistKey && newValue) {
// 使用 replace 而不是 push,避免产生大量历史记录
router.replace({
query: {
...route.query,
[props.persistKey]: newValue,
},
})
}
}
)
</script>
<template>
+23 -4
View File
@@ -1,5 +1,6 @@
<script setup lang="ts">
import { ref } from 'vue'
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import { SubTabs } from '@/components/UI'
import ChatExplorer from './ai/ChatExplorer.vue'
import SQLLabTab from './SQLLabTab.vue'
@@ -12,8 +13,16 @@ const props = defineProps<{
chatType?: 'group' | 'private'
}>()
// 子 Tab 配置
const subTabs = [
const route = useRoute()
// 判断是否为群聊(通过路由名称判断)
const isGroupChat = computed(() => route.name === 'group-chat')
// 仅群聊显示的功能 ID
const groupOnlyTabs = ['mbti', 'cyber-friend', 'campus']
// 所有子 Tab 配置
const allSubTabs = [
{ id: 'chat-explorer', label: '对话式探索', icon: 'i-heroicons-chat-bubble-left-ellipsis' },
{ id: 'sql-lab', label: 'SQL实验室', icon: 'i-heroicons-command-line' },
{
@@ -42,6 +51,16 @@ const subTabs = [
},
]
// 根据聊天类型过滤显示的子 Tab
const subTabs = computed(() => {
if (isGroupChat.value) {
// 群聊显示所有 Tab
return allSubTabs
}
// 私聊过滤掉群聊专属功能
return allSubTabs.filter((tab) => !groupOnlyTabs.includes(tab.id))
})
const activeSubTab = ref('chat-explorer')
// ChatExplorer 组件引用
@@ -61,7 +80,7 @@ defineExpose({
<template>
<div class="flex h-full flex-col">
<!-- Tab 导航 -->
<SubTabs v-model="activeSubTab" :items="subTabs" />
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="aiTab" />
<!-- Tab 内容 -->
<div class="flex-1 min-h-0 overflow-hidden">
+15 -14
View File
@@ -196,23 +196,24 @@ function getSessionAvatarText(session: AnalysisSession): string {
</div>
<!-- Session List -->
<div class="flex-1 relative min-h-0">
<div class="h-full overflow-y-auto px-3">
<div class="flex-1 relative min-h-0 px-4 flex flex-col">
<!-- 聊天记录标题 - 固定在顶部不随列表滚动 -->
<UTooltip
v-if="!isCollapsed && sessions.length > 0"
text="右键可删除或重命名聊天记录"
:popper="{ placement: 'right' }"
>
<div class="px-3 mb-2 flex items-center gap-1">
<div class="text-sm font-medium text-gray-500">聊天记录</div>
<UIcon name="i-heroicons-question-mark-circle" class="size-3.5 text-gray-400" />
</div>
</UTooltip>
<!-- 聊天记录列表 - 可滚动区域 -->
<div class="flex-1 overflow-y-auto">
<div v-if="sessions.length === 0 && !isCollapsed" class="py-8 text-center text-sm text-gray-500">暂无记录</div>
<div class="space-y-1 pb-8">
<!-- Session List Header - Sticky -->
<UTooltip
v-if="!isCollapsed && sessions.length > 0"
text="右键可删除或重命名聊天记录"
:popper="{ placement: 'right' }"
>
<div class="sticky top-0 bg-gray-50 dark:bg-gray-900 mb-4 px-2 flex items-center gap-1 z-1">
<div class="text-sm font-medium text-gray-500">聊天记录</div>
<UIcon name="i-heroicons-question-mark-circle" class="size-3.5 text-gray-400" />
</div>
</UTooltip>
<UTooltip
v-for="session in sessions"
:key="session.id"
@@ -26,7 +26,7 @@ const activeSubTab = ref('catchphrase')
<template>
<div class="flex h-full flex-col">
<!-- Tab 导航 -->
<SubTabs v-model="activeSubTab" :items="subTabs" />
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="quotesTab" />
<!-- Tab 内容 -->
<div class="flex-1 min-h-0 overflow-auto">
@@ -65,4 +65,3 @@ const activeSubTab = ref('catchphrase')
opacity: 0;
}
</style>
-5
View File
@@ -246,11 +246,6 @@ function getProgressDetail(): string {
</template>
</FileDropZone>
<!-- Supported Formats Text -->
<p class="text-sm text-gray-400 dark:text-gray-500">
支持 QQ微信DiscordSnapchatRedditTikTok 等聊天记录
</p>
<!-- Error Message -->
<div
v-if="importError"
@@ -25,7 +25,7 @@ const activeSubTab = ref('catchphrase')
<template>
<div class="flex h-full flex-col">
<!-- Tab 导航 -->
<SubTabs v-model="activeSubTab" :items="subTabs" />
<SubTabs v-model="activeSubTab" :items="subTabs" persist-key="quotesTab" />
<!-- Tab 内容 -->
<div class="flex-1 min-h-0 overflow-auto">
+1 -1
View File
@@ -16,7 +16,7 @@ const activeTab = ref('merge')
<PageHeader title="实用工具" description="提供聊天记录处理的实用工具" icon="i-heroicons-wrench-screwdriver" />
<!-- Tabs -->
<SubTabs v-model="activeTab" :items="tabs" />
<SubTabs v-model="activeTab" :items="tabs" persist-key="toolTab" />
<!-- Tab Content -->
<div class="flex-1 overflow-auto p-6">