mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-10-26 02:04:37 +08:00
feat: 优化设置弹窗弹出方式
This commit is contained in:
1 parent
c3e165409a
commit
ad62f9e727
29 files changed
+243
-34
No files matched your search
+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')
|
||||
}
|
||||
|
||||
// 导出当前对话
|
||||
|
||||
File renamed without changes.
+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">
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
+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">
|
||||
File renamed without changes.
+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">
|
||||
File renamed without changes.
+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">
|
||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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