mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-22 06:10:37 +08:00
feat: 优化设置弹窗弹出方式
This commit is contained in:
+21
-1
@@ -1,11 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
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 ScreenCaptureModal from '@/components/common/ScreenCaptureModal.vue'
|
||||
import SettingsModal from '@/components/common/SettingsModal.vue'
|
||||
import { ChatRecordDrawer } from '@/components/common/ChatRecord'
|
||||
import GlobalTaskBar from '@/components/AIChat/GlobalTaskBar.vue'
|
||||
import { useSessionStore } from '@/stores/session'
|
||||
@@ -32,8 +33,21 @@ const toaster = {
|
||||
duration: 2000,
|
||||
}
|
||||
|
||||
// Cmd+, (macOS) / Ctrl+, (Windows/Linux) 打开设置
|
||||
function handleGlobalKeydown(e: KeyboardEvent) {
|
||||
const isMeta = navigator.platform.toLowerCase().includes('mac') ? e.metaKey : e.ctrlKey
|
||||
if (isMeta && e.key === ',') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!layoutStore.showSettings) {
|
||||
layoutStore.openSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 应用启动时初始化
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', handleGlobalKeydown)
|
||||
// 平台检测 - 设置 CSS 类名以驱动平台差异化样式(如标题栏安全区域高度)
|
||||
const platform = navigator.platform.toLowerCase()
|
||||
if (platform.includes('win')) {
|
||||
@@ -49,6 +63,10 @@ onMounted(async () => {
|
||||
// 从数据库加载会话列表
|
||||
await sessionStore.loadSessions()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('keydown', handleGlobalKeydown)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -81,6 +99,8 @@ onMounted(async () => {
|
||||
:image-data="layoutStore.screenCaptureImage"
|
||||
@update:open="(v) => (v ? null : layoutStore.closeScreenCaptureModal())"
|
||||
/>
|
||||
<!-- 全局设置弹窗 -->
|
||||
<SettingsModal />
|
||||
<!-- 全局聊天记录查看器 -->
|
||||
<ChatRecordDrawer />
|
||||
<!-- 全局 AI 后台任务条:允许用户离开当前页面后仍然快速返回进行中的对话。 -->
|
||||
|
||||
@@ -3,15 +3,15 @@ import { ref, computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
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 } = useI18n()
|
||||
const toast = useToast()
|
||||
const router = useRouter()
|
||||
const layoutStore = useLayoutStore()
|
||||
|
||||
// Props
|
||||
const props = defineProps<{
|
||||
@@ -86,7 +86,7 @@ const agentCompactTitle = computed(() => {
|
||||
})
|
||||
|
||||
function openChatSettings() {
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'chat' } })
|
||||
layoutStore.openSettings('ai', 'chat')
|
||||
}
|
||||
|
||||
// 切换 AI 模型配置
|
||||
@@ -101,7 +101,7 @@ async function switchModelConfig(configId: string) {
|
||||
|
||||
function openModelSettings() {
|
||||
isModelPopoverOpen.value = false
|
||||
router.push({ name: 'settings', query: { tab: 'ai', subTab: 'model' } })
|
||||
layoutStore.openSettings('ai', 'model')
|
||||
}
|
||||
|
||||
// 导出当前对话
|
||||
|
||||
+15
-3
@@ -67,7 +67,11 @@ function closeModal() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="open" :ui="{ content: 'max-w-2xl' }" @update:open="emit('update:open', $event)">
|
||||
<UModal
|
||||
:open="open"
|
||||
:ui="{ content: 'max-w-2xl z-[101]', overlay: 'z-[100]' }"
|
||||
@update:open="emit('update:open', $event)"
|
||||
>
|
||||
<template #content>
|
||||
<div class="flex min-h-[min(640px,80vh)] max-h-[80vh] flex-col p-6">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">{{ modalTitle }}</h3>
|
||||
@@ -419,7 +423,11 @@ function closeModal() {
|
||||
</UModal>
|
||||
|
||||
<!-- 验证失败确认弹窗 -->
|
||||
<UModal :open="showValidationFailConfirm" @update:open="showValidationFailConfirm = $event">
|
||||
<UModal
|
||||
:open="showValidationFailConfirm"
|
||||
:ui="{ content: 'z-[102]', overlay: 'z-[101]' }"
|
||||
@update:open="showValidationFailConfirm = $event"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-6">
|
||||
<div class="mb-4 flex items-start gap-3">
|
||||
@@ -445,7 +453,11 @@ function closeModal() {
|
||||
</UModal>
|
||||
|
||||
<!-- 添加自定义模型小弹窗 -->
|
||||
<UModal :open="showAddModelDialog" @update:open="showAddModelDialog = $event">
|
||||
<UModal
|
||||
:open="showAddModelDialog"
|
||||
:ui="{ content: 'z-[102]', overlay: 'z-[101]' }"
|
||||
@update:open="showAddModelDialog = $event"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-5">
|
||||
<h4 class="mb-4 text-base font-semibold text-gray-900 dark:text-white">
|
||||
+1
-1
@@ -133,7 +133,7 @@ function formatMessageCount(count?: number): string {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="open" @update:open="emit('update:open', $event)">
|
||||
<UModal :open="open" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }" @update:open="emit('update:open', $event)">
|
||||
<template #content>
|
||||
<div class="p-6" style="min-width: 480px; max-height: 80vh; overflow-y: auto">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
+1
-1
@@ -48,7 +48,7 @@ function save() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :open="open" @update:open="emit('update:open', $event)">
|
||||
<UModal :open="open" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }" @update:open="emit('update:open', $event)">
|
||||
<template #content>
|
||||
<div class="p-6" style="min-width: 420px">
|
||||
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
+1
-1
@@ -468,7 +468,7 @@ function subscribedRemoteIds(ds: DataSource): Set<string> {
|
||||
/>
|
||||
|
||||
<!-- Delete confirmation modal -->
|
||||
<UModal v-model:open="showDeleteModal">
|
||||
<UModal v-model:open="showDeleteModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<h3 class="mb-3 font-semibold text-gray-900 dark:text-white">
|
||||
+2
-2
@@ -710,7 +710,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 合并确认弹窗 -->
|
||||
<UModal v-model:open="showMergeModal" :ui="{ content: 'z-100' }">
|
||||
<UModal v-model:open="showMergeModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
@@ -781,7 +781,7 @@ onMounted(() => {
|
||||
</UModal>
|
||||
|
||||
<!-- 删除确认弹窗 -->
|
||||
<UModal v-model:open="showDeleteModal">
|
||||
<UModal v-model:open="showDeleteModal" :ui="{ content: 'z-[101]', overlay: 'z-[100]' }">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
<div class="mb-4 flex items-center gap-3">
|
||||
@@ -2,4 +2,3 @@ export { default as AISettingsTab } from './AISettingsTab.vue'
|
||||
export { default as AIModelConfigTab } from './AI/AIModelConfigTab.vue'
|
||||
export { default as AIModelEditModal } from './AI/AIModelEditModal.vue'
|
||||
export { default as AIPromptConfigTab } from './AI/AIPromptConfigTab.vue'
|
||||
export { default as CacheManageTab } from './CacheManageTab.vue'
|
||||
@@ -0,0 +1,162 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import AISettingsTab from './Settings/AISettingsTab.vue'
|
||||
import BasicSettingsTab from './Settings/BasicSettingsTab.vue'
|
||||
import BatchManageTab from './Settings/BatchManageTab.vue'
|
||||
import StorageTab from './Settings/StorageTab.vue'
|
||||
import AboutTab from './Settings/AboutTab.vue'
|
||||
import ApiSettingsTab from './Settings/ApiSettingsTab.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
|
||||
const { t } = useI18n()
|
||||
const promptStore = usePromptStore()
|
||||
const layoutStore = useLayoutStore()
|
||||
const { showSettings, settingsTab, settingsSubTab } = storeToRefs(layoutStore)
|
||||
|
||||
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: 'api', label: t('settings.tabs.api'), icon: 'i-heroicons-server-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('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
|
||||
nextTick(() => {
|
||||
tabRefs.value[tabId]?.refresh?.()
|
||||
})
|
||||
}
|
||||
|
||||
function scrollToSubTab(subTab: string) {
|
||||
const tabRef = tabRefs.value[activeTab.value]
|
||||
if (tabRef?.scrollToSection) {
|
||||
tabRef.scrollToSection(subTab)
|
||||
}
|
||||
}
|
||||
|
||||
watch(showSettings, async (visible) => {
|
||||
if (visible) {
|
||||
activeTab.value = settingsTab.value || 'settings'
|
||||
if (settingsSubTab.value) {
|
||||
await nextTick()
|
||||
setTimeout(() => scrollToSubTab(settingsSubTab.value!), 100)
|
||||
}
|
||||
nextTick(() => {
|
||||
tabRefs.value[activeTab.value]?.refresh?.()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal v-model:open="showSettings" :ui="{ content: 'sm:max-w-[900px] z-[100]', overlay: 'backdrop-blur-sm z-[99]' }">
|
||||
<template #content>
|
||||
<div class="flex min-h-[650px] h-[85vh] flex-col overflow-hidden">
|
||||
<!-- Header -->
|
||||
<div class="shrink-0 border-b border-gray-200 px-6 pt-5 pb-0 dark:border-gray-800">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-600 dark:bg-primary-500">
|
||||
<UIcon name="i-heroicons-cog-6-tooth" class="h-4 w-4 text-white" />
|
||||
</div>
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{{ t('settings.title') }}
|
||||
</h2>
|
||||
</div>
|
||||
<UButton
|
||||
icon="i-heroicons-x-mark"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="layoutStore.closeSettings()"
|
||||
/>
|
||||
</div>
|
||||
<!-- Tabs -->
|
||||
<div class="mt-4 flex items-center gap-1 overflow-x-auto pb-3 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>
|
||||
</div>
|
||||
|
||||
<div class="relative flex-1">
|
||||
<div class="absolute inset-0 p-6">
|
||||
<Transition name="tab-slide" mode="out-in">
|
||||
<div v-if="activeTab === 'settings'" key="settings" class="h-full overflow-y-auto">
|
||||
<BasicSettingsTab />
|
||||
</div>
|
||||
<AISettingsTab
|
||||
v-else-if="activeTab === 'ai'"
|
||||
key="ai"
|
||||
:ref="(el: unknown) => setTabRef('ai', el)"
|
||||
@config-changed="handleAIConfigChanged"
|
||||
/>
|
||||
<BatchManageTab v-else-if="activeTab === 'data'" key="data" />
|
||||
<ApiSettingsTab v-else-if="activeTab === 'api'" key="api" />
|
||||
<StorageTab
|
||||
v-else-if="activeTab === 'storage'"
|
||||
key="storage"
|
||||
:ref="(el: unknown) => setTabRef('storage', el)"
|
||||
/>
|
||||
<div v-else-if="activeTab === 'about'" key="about" class="h-full overflow-y-auto">
|
||||
<AboutTab />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UModal>
|
||||
</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>
|
||||
@@ -259,7 +259,7 @@ function getSessionAvatar(session: AnalysisSession): string | null {
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
@click="router.push({ name: 'settings', query: { tab: 'data' } })"
|
||||
@click="layoutStore.openSettings('data')"
|
||||
/>
|
||||
</UTooltip>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useLayoutStore } from '@/stores/layout'
|
||||
import SidebarButton from './SidebarButton.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const isSettingsPage = computed(() => route.name === 'settings')
|
||||
const layoutStore = useLayoutStore()
|
||||
const { showSettings } = storeToRefs(layoutStore)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -17,8 +15,8 @@ const isSettingsPage = computed(() => route.name === 'settings')
|
||||
<SidebarButton
|
||||
icon="i-heroicons-cog-6-tooth"
|
||||
:title="t('layout.footer.settings')"
|
||||
:active="isSettingsPage"
|
||||
@click="router.push({ name: 'settings' })"
|
||||
:active="showSettings"
|
||||
@click="layoutStore.openSettings()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,12 +3,12 @@ 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 ApiSettingsTab from './components/ApiSettingsTab.vue'
|
||||
import AISettingsTab from '@/components/common/Settings/AISettingsTab.vue'
|
||||
import BasicSettingsTab from '@/components/common/Settings/BasicSettingsTab.vue'
|
||||
import BatchManageTab from '@/components/common/Settings/BatchManageTab.vue'
|
||||
import StorageTab from '@/components/common/Settings/StorageTab.vue'
|
||||
import AboutTab from '@/components/common/Settings/AboutTab.vue'
|
||||
import ApiSettingsTab from '@/components/common/Settings/ApiSettingsTab.vue'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -17,11 +17,6 @@ export const router = createRouter({
|
||||
name: 'private-chat',
|
||||
component: () => import('@/pages/private-chat/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/pages/settings/index.vue'),
|
||||
},
|
||||
],
|
||||
history: createWebHashHistory(),
|
||||
})
|
||||
|
||||
@@ -20,6 +20,11 @@ export const useLayoutStore = defineStore(
|
||||
// 截图设置
|
||||
const screenshotMobileAdapt = ref(false) // 截图时开启移动端适配,默认关闭
|
||||
|
||||
// 设置弹窗
|
||||
const showSettings = ref(false)
|
||||
const settingsTab = ref<string>('settings')
|
||||
const settingsSubTab = ref<string | null>(null)
|
||||
|
||||
/**
|
||||
* 切换侧边栏展开/折叠状态
|
||||
*/
|
||||
@@ -67,6 +72,19 @@ export const useLayoutStore = defineStore(
|
||||
isToolsPanelLocked.value = !isToolsPanelLocked.value
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开设置弹窗,可选指定 Tab 和 SubTab
|
||||
*/
|
||||
function openSettings(tab?: string, subTab?: string) {
|
||||
settingsTab.value = tab || 'settings'
|
||||
settingsSubTab.value = subTab || null
|
||||
showSettings.value = true
|
||||
}
|
||||
|
||||
function closeSettings() {
|
||||
showSettings.value = false
|
||||
}
|
||||
|
||||
function toggleToolsPanelMini() {
|
||||
isToolsPanelMini.value = !isToolsPanelMini.value
|
||||
if (isToolsPanelMini.value) {
|
||||
@@ -83,6 +101,9 @@ export const useLayoutStore = defineStore(
|
||||
showChatRecordDrawer,
|
||||
chatRecordQuery,
|
||||
screenshotMobileAdapt,
|
||||
showSettings,
|
||||
settingsTab,
|
||||
settingsSubTab,
|
||||
toggleSidebar,
|
||||
toggleToolsPanelLock,
|
||||
toggleToolsPanelMini,
|
||||
@@ -90,6 +111,8 @@ export const useLayoutStore = defineStore(
|
||||
closeScreenCaptureModal,
|
||||
openChatRecordDrawer,
|
||||
closeChatRecordDrawer,
|
||||
openSettings,
|
||||
closeSettings,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user