feat: 设置页面重构

This commit is contained in:
digua
2026-03-12 21:15:11 +08:00
committed by digua
parent 4df938a152
commit 5124b26028
32 changed files with 165 additions and 232 deletions

View File

@@ -5,12 +5,10 @@ import { useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import TitleBar from '@/components/common/TitleBar.vue'
import Sidebar from '@/components/common/Sidebar.vue'
import SettingModal from '@/components/common/SettingModal.vue'
import ScreenCaptureModal from '@/components/common/ScreenCaptureModal.vue'
import { ChatRecordDrawer } from '@/components/common/ChatRecord'
import { useSessionStore } from '@/stores/session'
import { useLayoutStore } from '@/stores/layout'
import { usePromptStore } from '@/stores/prompt'
import { useSettingsStore } from '@/stores/settings'
import { useLLMStore } from '@/stores/llm'
@@ -18,7 +16,6 @@ const { t } = useI18n()
const sessionStore = useSessionStore()
const layoutStore = useLayoutStore()
const promptStore = usePromptStore()
const settingsStore = useSettingsStore()
const llmStore = useLLMStore()
const { isInitialized } = storeToRefs(sessionStore)
@@ -72,7 +69,6 @@ onMounted(async () => {
</main>
</template>
</div>
<SettingModal v-model:open="layoutStore.showSettingModal" @ai-config-saved="promptStore.notifyAIConfigChanged" />
<ScreenCaptureModal
:open="layoutStore.showScreenCaptureModal"
:image-data="layoutStore.screenCaptureImage"

View File

@@ -3,14 +3,15 @@ import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { useToast } from '@nuxt/ui/runtime/composables/useToast.js'
import { useRouter } from 'vue-router'
import { usePromptStore } from '@/stores/prompt'
import { useLayoutStore } from '@/stores/layout'
import { useLLMStore } from '@/stores/llm'
import { exportConversation, type ExportFormat } from '@/utils/conversationExport'
import type { AgentRuntimeStatus } from '@electron/shared/types'
const { t, locale } = useI18n()
const toast = useToast()
const router = useRouter()
// Props
const props = defineProps<{
@@ -24,7 +25,6 @@ const props = defineProps<{
// Store
const promptStore = usePromptStore()
const layoutStore = useLayoutStore()
const llmStore = useLLMStore()
const { aiPromptSettings, activePreset, aiGlobalSettings } = storeToRefs(promptStore)
const { configs, activeConfig, isLoading: isLoadingLLM } = storeToRefs(llmStore)
@@ -112,15 +112,13 @@ function setActivePreset(presetId: string) {
isPresetPopoverOpen.value = false
}
// 打开设置弹窗并跳转到预设配置
function openPresetSettings() {
isPresetPopoverOpen.value = false
layoutStore.openSettingAt('ai', 'preset')
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'preset' } })
}
// 打开设置弹窗并跳转到对话配置(消息条数限制)
function openChatSettings() {
layoutStore.openSettingAt('ai', 'chat')
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'chat' } })
}
// 切换 AI 模型配置
@@ -138,10 +136,9 @@ async function switchModelConfig(configId: string) {
}
}
// 打开设置弹窗并跳转到模型配置
function openModelSettings() {
isModelPopoverOpen.value = false
layoutStore.openSettingAt('ai', 'model')
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'model' } })
}
// 导出当前对话

View File

@@ -1,146 +0,0 @@
<script setup lang="ts">
import { ref, watch, computed, nextTick } from 'vue'
import { useI18n } from 'vue-i18n'
import { useLayoutStore } from '@/stores/layout'
import AISettingsTab from './settings/AISettingsTab.vue'
import BasicSettingsTab from './settings/BasicSettingsTab.vue'
import StorageTab from './settings/StorageTab.vue'
import AboutTab from './settings/AboutTab.vue'
import SubTabs from '@/components/UI/SubTabs.vue'
const { t } = useI18n()
const layoutStore = useLayoutStore()
// 可滚动 Tab 的通用接口(支持 section 跳转的 Tab 需实现此接口)
interface ScrollableTab {
scrollToSection?: (sectionId: string) => void
refresh?: () => void
}
// Props
const props = defineProps<{
open: boolean
}>()
// Emits
const emit = defineEmits<{
'update:open': [value: boolean]
'ai-config-saved': []
}>()
// Tab 配置(使用 computed 以便语言切换时自动更新)
const tabs = computed(() => [
{ id: 'settings', label: t('settings.tabs.basic'), icon: 'i-heroicons-cog-6-tooth' },
{ id: 'ai', label: t('settings.tabs.ai'), icon: 'i-heroicons-sparkles' },
{ id: 'storage', label: t('settings.tabs.storage'), icon: 'i-heroicons-folder-open' },
{ id: 'about', label: t('settings.tabs.about'), icon: 'i-heroicons-information-circle' },
])
const activeTab = ref('settings')
// 统一的 Tab 引用管理(通过 setTabRef 动态设置)
const tabRefs = ref<Record<string, ScrollableTab | null>>({})
/**
* 设置 Tab 引用(在模板中通过 :ref 调用)
*/
function setTabRef(tabId: string, el: unknown) {
tabRefs.value[tabId] = el as ScrollableTab | null
}
// AI 配置变更回调
function handleAIConfigChanged() {
emit('ai-config-saved')
}
// 关闭弹窗
function closeModal() {
emit('update:open', false)
layoutStore.clearSettingTarget()
}
// 监听打开状态
watch(
() => props.open,
async (newVal) => {
if (newVal) {
// 检查是否有指定的跳转目标
const target = layoutStore.settingTarget
if (target) {
activeTab.value = target.tab
// 如果有指定 section等待渲染后滚动通用逻辑
if (target.section) {
await nextTick()
// 延迟一下确保目标 Tab 已渲染
setTimeout(() => {
const tabRef = tabRefs.value[target.tab]
tabRef?.scrollToSection?.(target.section!)
}, 100)
}
} else {
activeTab.value = 'settings' // 默认打开基础设置 Tab
}
// 刷新存储管理
tabRefs.value['storage']?.refresh?.()
} else {
// 弹窗关闭时清空 target
layoutStore.clearSettingTarget()
}
}
)
// 监听 Tab 切换,刷新对应数据
watch(
() => activeTab.value,
(newTab) => {
// 通用刷新逻辑
tabRefs.value[newTab]?.refresh?.()
}
)
</script>
<template>
<UModal
:open="open"
:ui="{ overlay: 'app-region-no-drag', content: 'md:w-full max-w-2xl app-region-no-drag' }"
@update:open="emit('update:open', $event)"
>
<template #content>
<div class="p-6">
<!-- Header -->
<div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">{{ t('settings.title') }}</h2>
<UButton icon="i-heroicons-x-mark" variant="ghost" size="sm" @click="closeModal" />
</div>
<!-- Tab 导航 -->
<div class="mb-6 -mx-6">
<SubTabs v-model="activeTab" :items="tabs" />
</div>
<!-- Tab 内容 -->
<div class="h-[500px] overflow-y-auto">
<!-- 基础设置 -->
<div v-show="activeTab === 'settings'">
<BasicSettingsTab />
</div>
<!-- AI 设置 -->
<div v-show="activeTab === 'ai'" class="h-full">
<AISettingsTab :ref="(el) => setTabRef('ai', el)" @config-changed="handleAIConfigChanged" />
</div>
<!-- 存储管理 -->
<div v-show="activeTab === 'storage'" class="h-full">
<StorageTab :ref="(el) => setTabRef('storage', el)" />
</div>
<!-- 关于 -->
<div v-show="activeTab === 'about'">
<AboutTab />
</div>
</div>
</div>
</template>
</UModal>
</template>

View File

@@ -2,35 +2,23 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter, useRoute } from 'vue-router'
import { useLayoutStore } from '@/stores/layout'
import SidebarButton from './SidebarButton.vue'
const { t } = useI18n()
const router = useRouter()
const route = useRoute()
const layoutStore = useLayoutStore()
// 是否在管理页面
const isManagePage = computed(() => route.name === 'manage')
// 注意:帮助和反馈功能已迁移到首页 Footer (HomeFooter.vue)
// 如需恢复,请参考 git 历史记录
const isSettingsPage = computed(() => route.name === 'settings')
</script>
<template>
<div class="px-4 py-2 dark:border-gray-800 space-y-2 mb-2">
<!-- 管理 -->
<SidebarButton
icon="i-heroicons-rectangle-stack"
:title="t('tools.title')"
:active="isManagePage"
@click="router.push({ name: 'manage' })"
/>
<!-- 设置 -->
<SidebarButton
icon="i-heroicons-cog-6-tooth"
:title="t('layout.footer.settings')"
@click="layoutStore.showSettingModal = true"
:active="isSettingsPage"
@click="router.push({ name: 'settings' })"
/>
</div>
</template>

View File

@@ -8,7 +8,8 @@
"aiPrompt": "Chat Config",
"aiPreset": "Prompts",
"aiPreprocess": "Preprocess",
"storage": "Data & Storage",
"dataManage": "Data Management",
"storage": "Storage",
"storageManage": "Storage",
"sessionManage": "Sessions",
"about": "About"

View File

@@ -8,7 +8,8 @@
"aiPrompt": "チャット設定",
"aiPreset": "プロンプト設定",
"aiPreprocess": "前処理",
"storage": "データとストレージ",
"dataManage": "データ管理",
"storage": "ストレージ管理",
"storageManage": "ストレージ管理",
"sessionManage": "セッション管理",
"about": "ChatLabについて"

View File

@@ -8,7 +8,8 @@
"aiPrompt": "对话配置",
"aiPreset": "提示词配置",
"aiPreprocess": "预处理",
"storage": "数据和存储",
"dataManage": "数据管理",
"storage": "存储管理",
"storageManage": "存储管理",
"sessionManage": "会话管理",
"about": "关于"

View File

@@ -8,7 +8,8 @@
"aiPrompt": "聊天設定",
"aiPreset": "提示詞設定",
"aiPreprocess": "前處理",
"storage": "資料與儲存",
"dataManage": "資料管理",
"storage": "儲存管理",
"storageManage": "儲存管理",
"sessionManage": "會話管理",
"about": "關於 ChatLab"

View File

@@ -1,24 +0,0 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import BatchManageTab from './components/BatchManageTab.vue'
import PageHeader from '@/components/layout/PageHeader.vue'
const { t } = useI18n()
</script>
<template>
<div class="relative z-0 flex h-full flex-col bg-white pt-4 dark:bg-[var(--color-page-dark)]">
<!-- Header -->
<PageHeader
:title="t('tools.title')"
:description="t('tools.description')"
icon="i-heroicons-rectangle-stack"
icon-class="bg-primary-600 dark:bg-primary-500"
/>
<!-- Content -->
<div class="flex-1 overflow-auto p-6">
<BatchManageTab />
</div>
</div>
</template>

View File

@@ -0,0 +1,146 @@
<script setup lang="ts">
import { ref, watch, onMounted, computed, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import PageHeader from '@/components/layout/PageHeader.vue'
import AISettingsTab from './components/AISettingsTab.vue'
import BasicSettingsTab from './components/BasicSettingsTab.vue'
import BatchManageTab from './components/BatchManageTab.vue'
import StorageTab from './components/StorageTab.vue'
import AboutTab from './components/AboutTab.vue'
import { usePromptStore } from '@/stores/prompt'
const { t } = useI18n()
const route = useRoute()
const router = useRouter()
const promptStore = usePromptStore()
interface ScrollableTab {
scrollToSection?: (sectionId: string) => void
refresh?: () => void
}
const tabs = computed(() => [
{ id: 'settings', label: t('settings.tabs.basic'), icon: 'i-heroicons-cog-6-tooth' },
{ id: 'ai', label: t('settings.tabs.ai'), icon: 'i-heroicons-sparkles' },
{ id: 'data', label: t('settings.tabs.dataManage'), icon: 'i-heroicons-rectangle-stack' },
{ id: 'storage', label: t('settings.tabs.storage'), icon: 'i-heroicons-folder-open' },
{ id: 'about', label: t('settings.tabs.about'), icon: 'i-heroicons-information-circle' },
])
const activeTab = ref((route.query.tab as string) || 'settings')
const tabRefs = ref<Record<string, ScrollableTab | null>>({})
function setTabRef(tabId: string, el: unknown) {
tabRefs.value[tabId] = el as ScrollableTab | null
}
function handleAIConfigChanged() {
promptStore.notifyAIConfigChanged()
}
function switchTab(tabId: string) {
activeTab.value = tabId
const query: Record<string, string> = { tab: tabId }
router.replace({ query })
nextTick(() => {
tabRefs.value[tabId]?.refresh?.()
})
}
function scrollToSubTab(subTab: string) {
const tabRef = tabRefs.value[activeTab.value]
if (tabRef?.scrollToSection) {
tabRef.scrollToSection(subTab)
}
}
watch(
() => route.query,
async (query) => {
const tab = (query.tab as string) || 'settings'
if (tab !== activeTab.value) {
activeTab.value = tab
}
const subTab = query.subTab as string
if (subTab) {
await nextTick()
setTimeout(() => scrollToSubTab(subTab), 100)
}
}
)
onMounted(async () => {
const subTab = route.query.subTab as string
if (subTab) {
await nextTick()
setTimeout(() => scrollToSubTab(subTab), 100)
}
tabRefs.value[activeTab.value]?.refresh?.()
})
</script>
<template>
<div class="flex h-full flex-col bg-white dark:bg-gray-900" style="padding-top: var(--titlebar-area-height)">
<PageHeader
:title="t('settings.title')"
icon="i-heroicons-cog-6-tooth"
icon-class="bg-primary-600 text-white dark:bg-primary-500 dark:text-white"
>
<div class="mt-4 flex items-center gap-1 overflow-x-auto scrollbar-hide">
<button
v-for="tab in tabs"
:key="tab.id"
class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium transition-all"
:class="[
activeTab === tab.id
? 'bg-pink-500 text-white dark:bg-pink-900/30 dark:text-pink-300'
: 'text-gray-600 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-gray-800',
]"
@click="switchTab(tab.id)"
>
<UIcon :name="tab.icon" class="h-4 w-4" />
<span class="whitespace-nowrap">{{ tab.label }}</span>
</button>
</div>
</PageHeader>
<div class="relative flex-1 overflow-y-auto">
<div class="h-full p-6">
<Transition name="tab-slide" mode="out-in">
<BasicSettingsTab v-if="activeTab === 'settings'" key="settings" />
<AISettingsTab
v-else-if="activeTab === 'ai'"
key="ai"
:ref="(el) => setTabRef('ai', el)"
@config-changed="handleAIConfigChanged"
/>
<BatchManageTab v-else-if="activeTab === 'data'" key="data" />
<StorageTab v-else-if="activeTab === 'storage'" key="storage" :ref="(el) => setTabRef('storage', el)" />
<AboutTab v-else-if="activeTab === 'about'" key="about" />
</Transition>
</div>
</div>
</div>
</template>
<style scoped>
.tab-slide-enter-active,
.tab-slide-leave-active {
transition:
opacity 0.2s ease,
transform 0.2s ease;
}
.tab-slide-enter-from {
opacity: 0;
transform: translateY(10px);
}
.tab-slide-leave-to {
opacity: 0;
transform: translateY(-10px);
}
</style>

View File

@@ -18,9 +18,9 @@ export const router = createRouter({
component: () => import('@/pages/private-chat/index.vue'),
},
{
path: '/manage',
name: 'manage',
component: () => import('@/pages/manage/index.vue'),
path: '/settings',
name: 'settings',
component: () => import('@/pages/settings/index.vue'),
},
],
history: createWebHashHistory(),

View File

@@ -9,18 +9,11 @@ export const useLayoutStore = defineStore(
'layout',
() => {
const isSidebarCollapsed = ref(false)
const showSettingModal = ref(false)
const showScreenCaptureModal = ref(false)
const screenCaptureImage = ref<string | null>(null)
const showChatRecordDrawer = ref(false)
const chatRecordQuery = ref<ChatRecordQuery | null>(null)
// 设置弹窗定位目标(用于从外部跳转到设置的特定位置)
const settingTarget = ref<{
tab: 'settings' | 'ai' | 'storage' | 'about'
section?: string // AI tab 下的子锚点,如 'model', 'chat', 'preset'
} | null>(null)
// 截图设置
const screenshotMobileAdapt = ref(true) // 截图时开启移动端适配,默认开启
@@ -67,39 +60,18 @@ export const useLayoutStore = defineStore(
}, 300)
}
/**
* 打开设置弹窗并定位到指定位置
* @param tab 要跳转的 Tabsettings, ai, storage, about
* @param section 子锚点(仅 ai tab 支持model, chat, preset
*/
function openSettingAt(tab: 'settings' | 'ai' | 'storage' | 'about', section?: string) {
settingTarget.value = { tab, section }
showSettingModal.value = true
}
/**
* 清空设置目标(弹窗关闭后调用)
*/
function clearSettingTarget() {
settingTarget.value = null
}
return {
isSidebarCollapsed,
showSettingModal,
showScreenCaptureModal,
screenCaptureImage,
showChatRecordDrawer,
chatRecordQuery,
settingTarget,
screenshotMobileAdapt,
toggleSidebar,
openScreenCaptureModal,
closeScreenCaptureModal,
openChatRecordDrawer,
closeChatRecordDrawer,
openSettingAt,
clearSettingTarget,
}
},
{