mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-12 01:01:26 +08:00
feat: 重构设置弹窗
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AIConfigTab from './settings/AIConfigTab.vue'
|
||||
import AIPromptConfigTab from './settings/AIPromptConfigTab.vue'
|
||||
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()
|
||||
|
||||
@@ -23,18 +23,15 @@ const emit = defineEmits<{
|
||||
// Tab 配置(使用 computed 以便语言切换时自动更新)
|
||||
const tabs = computed(() => [
|
||||
{ id: 'settings', label: t('settings.tabs.basic'), icon: 'i-heroicons-cog-6-tooth' },
|
||||
{ id: 'ai-config', label: t('settings.tabs.aiConfig'), icon: 'i-heroicons-sparkles' },
|
||||
{ id: 'ai-prompt', label: t('settings.tabs.aiPrompt'), icon: 'i-heroicons-document-text' },
|
||||
{ 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')
|
||||
// Template refs - used via ref="xxx" in template
|
||||
const aiConfigRef = ref<InstanceType<typeof AIConfigTab> | null>(null)
|
||||
const storageTabRef = ref<InstanceType<typeof StorageTab> | null>(null)
|
||||
// Ensure refs are tracked for vue-tsc
|
||||
void aiConfigRef
|
||||
void storageTabRef
|
||||
|
||||
// AI 配置变更回调
|
||||
@@ -81,23 +78,8 @@ watch(
|
||||
</div>
|
||||
|
||||
<!-- Tab 导航 -->
|
||||
<div class="mb-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
:key="tab.id"
|
||||
class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-sm font-medium transition-colors"
|
||||
:class="[
|
||||
activeTab === tab.id
|
||||
? 'border-primary-500 text-primary-600 dark:text-primary-400'
|
||||
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300',
|
||||
]"
|
||||
@click="activeTab = tab.id"
|
||||
>
|
||||
<UIcon :name="tab.icon" class="h-4 w-4" />
|
||||
<span>{{ tab.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-6 -mx-6">
|
||||
<SubTabs v-model="activeTab" :items="tabs" />
|
||||
</div>
|
||||
|
||||
<!-- Tab 内容 -->
|
||||
@@ -106,14 +88,10 @@ watch(
|
||||
<div v-show="activeTab === 'settings'">
|
||||
<BasicSettingsTab />
|
||||
</div>
|
||||
<!-- 模型配置 -->
|
||||
<div v-show="activeTab === 'ai-config'">
|
||||
<AIConfigTab ref="aiConfigRef" @config-changed="handleAIConfigChanged" />
|
||||
</div>
|
||||
|
||||
<!-- AI对话配置 -->
|
||||
<div v-show="activeTab === 'ai-prompt'">
|
||||
<AIPromptConfigTab @config-changed="handleAIConfigChanged" />
|
||||
<!-- AI 设置 -->
|
||||
<div v-show="activeTab === 'ai'" class="h-full">
|
||||
<AISettingsTab @config-changed="handleAIConfigChanged" />
|
||||
</div>
|
||||
|
||||
<!-- 存储管理 -->
|
||||
|
||||
+15
-8
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AIConfigEditModal from './AIConfigEditModal.vue'
|
||||
import AIModelEditModal from './AIModelEditModal.vue'
|
||||
import AlertTips from './AlertTips.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -148,6 +148,11 @@ onMounted(() => {
|
||||
|
||||
<!-- 配置列表视图 -->
|
||||
<div v-else class="space-y-4">
|
||||
<!-- 标题 -->
|
||||
<h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-sparkles" class="h-4 w-4 text-violet-500" />
|
||||
{{ t('settings.aiConfig.title') }}
|
||||
</h4>
|
||||
<AlertTips v-if="configs.length === 0 && aiTips.configTab?.show" :content="aiTips.configTab?.content" />
|
||||
<!-- 配置列表 -->
|
||||
<div v-if="configs.length > 0" class="space-y-2">
|
||||
@@ -198,12 +203,14 @@ onMounted(() => {
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-1 opacity-0 transition-opacity group-hover:opacity-100" @click.stop>
|
||||
<UButton size="xs" color="gray" variant="ghost" @click="openEditModal(config)">
|
||||
{{ t('settings.aiConfig.edit') }}
|
||||
</UButton>
|
||||
<UButton size="xs" color="error" variant="ghost" @click="deleteConfig(config.id)">
|
||||
{{ t('settings.aiConfig.delete') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
size="xs"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
icon="i-heroicons-pencil-square"
|
||||
@click="openEditModal(config)"
|
||||
/>
|
||||
<UButton size="xs" color="error" variant="ghost" icon="i-heroicons-trash" @click="deleteConfig(config.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -228,7 +235,7 @@ onMounted(() => {
|
||||
</div>
|
||||
|
||||
<!-- 编辑/添加弹窗 -->
|
||||
<AIConfigEditModal
|
||||
<AIModelEditModal
|
||||
v-model:open="showEditModal"
|
||||
:mode="editMode"
|
||||
:config="editingConfig"
|
||||
@@ -0,0 +1,141 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { usePromptStore } from '@/stores/prompt'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Store
|
||||
const promptStore = usePromptStore()
|
||||
const { aiGlobalSettings } = storeToRefs(promptStore)
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'config-changed': []
|
||||
}>()
|
||||
|
||||
// 发送条数限制
|
||||
const globalMaxMessages = computed({
|
||||
get: () => aiGlobalSettings.value.maxMessagesPerRequest,
|
||||
set: (val: number) => {
|
||||
const clampedVal = Math.max(10, Math.min(10000, val || 200))
|
||||
promptStore.updateAIGlobalSettings({ maxMessagesPerRequest: clampedVal })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
// AI上下文限制
|
||||
const globalMaxHistoryRounds = computed({
|
||||
get: () => aiGlobalSettings.value.maxHistoryRounds ?? 10,
|
||||
set: (val: number) => {
|
||||
const clampedVal = Math.max(1, Math.min(50, val || 10))
|
||||
promptStore.updateAIGlobalSettings({ maxHistoryRounds: clampedVal })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
// 导出格式选项(AI 对话)
|
||||
const exportFormatTabs = computed(() => [
|
||||
{ label: 'Markdown', value: 'markdown' },
|
||||
{ label: t('settings.aiPrompt.exportFormat.txtLabel'), value: 'txt' },
|
||||
])
|
||||
|
||||
// 当前选中的导出格式(AI 对话)
|
||||
const exportFormat = computed({
|
||||
get: () => aiGlobalSettings.value.exportFormat ?? 'markdown',
|
||||
set: (val: string) => {
|
||||
promptStore.updateAIGlobalSettings({ exportFormat: val as 'markdown' | 'txt' })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
// SQL Lab 导出格式选项
|
||||
const sqlExportFormatTabs = computed(() => [
|
||||
{ label: 'CSV', value: 'csv' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
])
|
||||
|
||||
// 当前选中的 SQL Lab 导出格式
|
||||
const sqlExportFormat = computed({
|
||||
get: () => aiGlobalSettings.value.sqlExportFormat ?? 'csv',
|
||||
set: (val: string) => {
|
||||
promptStore.updateAIGlobalSettings({ sqlExportFormat: val as 'csv' | 'json' })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 对话设置 -->
|
||||
<div>
|
||||
<h4 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-chat-bubble-left-right" class="h-4 w-4 text-green-500" />
|
||||
{{ t('settings.aiPrompt.chatSettings.title') }}
|
||||
</h4>
|
||||
<div class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<!-- 发送条数限制 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.maxMessages.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.maxMessages.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<UInput v-model.number="globalMaxMessages" type="number" min="1" max="10000" class="w-24" />
|
||||
</div>
|
||||
|
||||
<!-- AI上下文限制 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.maxHistory.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.maxHistory.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<UInput v-model.number="globalMaxHistoryRounds" type="number" min="1" max="50" class="w-24" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导出设置 -->
|
||||
<div>
|
||||
<h4 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-arrow-down-tray" class="h-4 w-4 text-blue-500" />
|
||||
{{ t('settings.aiPrompt.exportSettings.title') }}
|
||||
</h4>
|
||||
<div class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<!-- 导出格式(AI 对话) -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.exportFormat.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.exportFormat.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<UTabs v-model="exportFormat" :items="exportFormatTabs" size="xs" />
|
||||
</div>
|
||||
|
||||
<!-- SQL Lab 导出格式 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.sqlExportFormat.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.sqlExportFormat.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<UTabs v-model="sqlExportFormat" :items="sqlExportFormatTabs" size="xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
+36
-136
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { PromptPreset } from '@/types/ai'
|
||||
@@ -11,7 +11,7 @@ const { t } = useI18n()
|
||||
|
||||
// Store
|
||||
const promptStore = usePromptStore()
|
||||
const { groupPresets, privatePresets, aiPromptSettings, aiGlobalSettings } = storeToRefs(promptStore)
|
||||
const { groupPresets, privatePresets, aiPromptSettings } = storeToRefs(promptStore)
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
@@ -25,56 +25,6 @@ const editMode = ref<'add' | 'edit'>('add')
|
||||
const editingPreset = ref<PromptPreset | null>(null)
|
||||
const defaultChatType = ref<'group' | 'private'>('group')
|
||||
|
||||
// 发送条数限制
|
||||
const globalMaxMessages = computed({
|
||||
get: () => aiGlobalSettings.value.maxMessagesPerRequest,
|
||||
set: (val: number) => {
|
||||
const clampedVal = Math.max(10, Math.min(10000, val || 200))
|
||||
promptStore.updateAIGlobalSettings({ maxMessagesPerRequest: clampedVal })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
// AI上下文限制
|
||||
const globalMaxHistoryRounds = computed({
|
||||
get: () => aiGlobalSettings.value.maxHistoryRounds ?? 10,
|
||||
set: (val: number) => {
|
||||
const clampedVal = Math.max(1, Math.min(50, val || 10))
|
||||
promptStore.updateAIGlobalSettings({ maxHistoryRounds: clampedVal })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
// 导出格式选项(AI 对话)
|
||||
const exportFormatTabs = computed(() => [
|
||||
{ label: 'Markdown', value: 'markdown' },
|
||||
{ label: t('settings.aiPrompt.exportFormat.txtLabel'), value: 'txt' },
|
||||
])
|
||||
|
||||
// 当前选中的导出格式(AI 对话)
|
||||
const exportFormat = computed({
|
||||
get: () => aiGlobalSettings.value.exportFormat ?? 'markdown',
|
||||
set: (val: string) => {
|
||||
promptStore.updateAIGlobalSettings({ exportFormat: val as 'markdown' | 'txt' })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
// SQL Lab 导出格式选项
|
||||
const sqlExportFormatTabs = computed(() => [
|
||||
{ label: 'CSV', value: 'csv' },
|
||||
{ label: 'JSON', value: 'json' },
|
||||
])
|
||||
|
||||
// 当前选中的 SQL Lab 导出格式
|
||||
const sqlExportFormat = computed({
|
||||
get: () => aiGlobalSettings.value.sqlExportFormat ?? 'csv',
|
||||
set: (val: string) => {
|
||||
promptStore.updateAIGlobalSettings({ sqlExportFormat: val as 'csv' | 'json' })
|
||||
emit('config-changed')
|
||||
},
|
||||
})
|
||||
|
||||
/** 打开新增预设弹窗 */
|
||||
function openAddModal(chatType: 'group' | 'private') {
|
||||
editMode.value = 'add'
|
||||
@@ -134,70 +84,6 @@ function handleImportPresetAdded() {
|
||||
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<!-- 对话设置 -->
|
||||
<div>
|
||||
<h4 class="mb-3 flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<UIcon name="i-heroicons-adjustments-horizontal" class="h-4 w-4 text-green-500" />
|
||||
{{ t('settings.aiPrompt.title') }}
|
||||
</h4>
|
||||
<div class="space-y-4 rounded-lg border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-800/50">
|
||||
<!-- 发送条数限制 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.maxMessages.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.maxMessages.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<UInput v-model.number="globalMaxMessages" type="number" min="1" max="10000" class="w-24" />
|
||||
</div>
|
||||
|
||||
<!-- AI上下文限制 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.maxHistory.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.maxHistory.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<UInput v-model.number="globalMaxHistoryRounds" type="number" min="1" max="50" class="w-24" />
|
||||
</div>
|
||||
|
||||
<!-- 导出格式(AI 对话) -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.exportFormat.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.exportFormat.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<UTabs v-model="exportFormat" :items="exportFormatTabs" size="xs" />
|
||||
</div>
|
||||
|
||||
<!-- SQL Lab 导出格式 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 pr-4">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ t('settings.aiPrompt.sqlExportFormat.title') }}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
{{ t('settings.aiPrompt.sqlExportFormat.description') }}
|
||||
</p>
|
||||
</div>
|
||||
<UTabs v-model="sqlExportFormat" :items="sqlExportFormatTabs" size="xs" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-700"></div>
|
||||
|
||||
<!-- 系统提示词标题和导入按钮 -->
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="flex items-center gap-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
@@ -210,8 +96,8 @@ function handleImportPresetAdded() {
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<!-- 群聊和私聊系统提示词并排 -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- 群聊和私聊系统提示词 -->
|
||||
<div class="space-y-6">
|
||||
<!-- 群聊预设组 -->
|
||||
<div>
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
@@ -261,21 +147,28 @@ function handleImportPresetAdded() {
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100" @click.stop>
|
||||
<UButton color="gray" variant="ghost" size="xs" @click="openEditModal(preset)">
|
||||
{{ preset.isBuiltIn ? t('settings.aiPrompt.preset.view') : t('settings.aiPrompt.preset.edit') }}
|
||||
</UButton>
|
||||
<UButton color="gray" variant="ghost" size="xs" @click="duplicatePreset(preset.id)">
|
||||
{{ t('settings.aiPrompt.preset.copy') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
:icon="preset.isBuiltIn ? 'i-heroicons-eye' : 'i-heroicons-pencil-square'"
|
||||
@click="openEditModal(preset)"
|
||||
/>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-heroicons-document-duplicate"
|
||||
@click="duplicatePreset(preset.id)"
|
||||
/>
|
||||
<UButton
|
||||
v-if="!preset.isBuiltIn"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-heroicons-trash"
|
||||
@click="deletePreset(preset.id)"
|
||||
>
|
||||
{{ t('settings.aiPrompt.preset.delete') }}
|
||||
</UButton>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -330,21 +223,28 @@ function handleImportPresetAdded() {
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="flex items-center gap-0.5 opacity-0 transition-opacity group-hover:opacity-100" @click.stop>
|
||||
<UButton color="gray" variant="ghost" size="xs" @click="openEditModal(preset)">
|
||||
{{ preset.isBuiltIn ? t('settings.aiPrompt.preset.view') : t('settings.aiPrompt.preset.edit') }}
|
||||
</UButton>
|
||||
<UButton color="gray" variant="ghost" size="xs" @click="duplicatePreset(preset.id)">
|
||||
{{ t('settings.aiPrompt.preset.copy') }}
|
||||
</UButton>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
:icon="preset.isBuiltIn ? 'i-heroicons-eye' : 'i-heroicons-pencil-square'"
|
||||
@click="openEditModal(preset)"
|
||||
/>
|
||||
<UButton
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-heroicons-document-duplicate"
|
||||
@click="duplicatePreset(preset.id)"
|
||||
/>
|
||||
<UButton
|
||||
v-if="!preset.isBuiltIn"
|
||||
color="error"
|
||||
variant="ghost"
|
||||
size="xs"
|
||||
icon="i-heroicons-trash"
|
||||
@click="deletePreset(preset.id)"
|
||||
>
|
||||
{{ t('settings.aiPrompt.preset.delete') }}
|
||||
</UButton>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,138 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import AIModelConfigTab from './AI/AIModelConfigTab.vue'
|
||||
import AIPromptConfigTab from './AI/AIPromptConfigTab.vue'
|
||||
import AIPromptPresetTab from './AI/AIPromptPresetTab.vue'
|
||||
import SubTabs from '@/components/UI/SubTabs.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits<{
|
||||
'config-changed': []
|
||||
}>()
|
||||
|
||||
// 导航配置
|
||||
const navItems = computed(() => [
|
||||
{ id: 'model', label: t('settings.tabs.aiConfig') },
|
||||
{ id: 'chat', label: t('settings.tabs.aiPrompt') },
|
||||
{ id: 'preset', label: t('settings.tabs.aiPreset') },
|
||||
])
|
||||
|
||||
// 当前激活的导航项
|
||||
const activeNav = ref('model')
|
||||
|
||||
// 是否由用户点击触发(用于区分点击滚动和手动滚动)
|
||||
const isUserClick = ref(false)
|
||||
|
||||
// 滚动容器引用
|
||||
const scrollContainerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// Section 引用
|
||||
const sectionRefs = ref<Record<string, HTMLElement | null>>({
|
||||
model: null,
|
||||
chat: null,
|
||||
preset: null,
|
||||
})
|
||||
|
||||
// AI 配置变更回调
|
||||
function handleAIConfigChanged() {
|
||||
emit('config-changed')
|
||||
}
|
||||
|
||||
// 处理导航点击(通过 @change 事件)
|
||||
function handleNavChange(id: string) {
|
||||
const section = sectionRefs.value[id]
|
||||
if (section && scrollContainerRef.value) {
|
||||
// 标记为用户点击触发
|
||||
isUserClick.value = true
|
||||
section.scrollIntoView({ behavior: 'smooth', block: 'start' })
|
||||
// 滚动动画结束后恢复
|
||||
setTimeout(() => {
|
||||
isUserClick.value = false
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听滚动更新当前激活项
|
||||
function handleScroll() {
|
||||
// 如果是用户点击触发的滚动,不更新 activeNav(避免冲突)
|
||||
if (isUserClick.value || !scrollContainerRef.value) return
|
||||
|
||||
const container = scrollContainerRef.value
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
const offset = 50 // 偏移量,提前触发
|
||||
|
||||
// 检查是否滚动到底部(误差范围 5px)
|
||||
const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 5
|
||||
if (isAtBottom) {
|
||||
// 滚动到底部时,激活最后一个导航项
|
||||
const lastItem = navItems.value[navItems.value.length - 1]
|
||||
if (lastItem) {
|
||||
activeNav.value = lastItem.id
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 检查每个 section 的位置
|
||||
for (const item of navItems.value) {
|
||||
const section = sectionRefs.value[item.id]
|
||||
if (section) {
|
||||
const rect = section.getBoundingClientRect()
|
||||
// 如果 section 顶部在容器可视区域内
|
||||
if (rect.top <= containerRect.top + offset && rect.bottom > containerRect.top + offset) {
|
||||
activeNav.value = item.id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Template refs
|
||||
const aiModelConfigRef = ref<InstanceType<typeof AIModelConfigTab> | null>(null)
|
||||
void aiModelConfigRef
|
||||
|
||||
onMounted(() => {
|
||||
scrollContainerRef.value?.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
scrollContainerRef.value?.removeEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full gap-6">
|
||||
<!-- 左侧锚点导航 -->
|
||||
<div class="w-28 shrink-0">
|
||||
<SubTabs v-model="activeNav" :items="navItems" orientation="vertical" @change="handleNavChange" />
|
||||
</div>
|
||||
|
||||
<!-- 右侧内容区域 -->
|
||||
<div ref="scrollContainerRef" class="min-w-0 flex-1 overflow-y-auto">
|
||||
<div class="space-y-8">
|
||||
<!-- 模型配置 -->
|
||||
<div :ref="(el) => (sectionRefs.model = el as HTMLElement)">
|
||||
<AIModelConfigTab ref="aiModelConfigRef" @config-changed="handleAIConfigChanged" />
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
<!-- 对话配置 -->
|
||||
<div :ref="(el) => (sectionRefs.chat = el as HTMLElement)">
|
||||
<AIPromptConfigTab @config-changed="handleAIConfigChanged" />
|
||||
</div>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="border-t border-gray-200 dark:border-gray-700" />
|
||||
|
||||
<!-- 提示词配置 -->
|
||||
<div :ref="(el) => (sectionRefs.preset = el as HTMLElement)">
|
||||
<AIPromptPresetTab @config-changed="handleAIConfigChanged" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,5 +1,6 @@
|
||||
export { default as AIConfigTab } from './AIConfigTab.vue'
|
||||
export { default as AIConfigEditModal } from './AIConfigEditModal.vue'
|
||||
export { default as AIPromptConfigTab } from './AIPromptConfigTab.vue'
|
||||
export { default as AIPromptEditModal } from './AIPromptEditModal.vue'
|
||||
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 AIPromptEditModal } from './AI/AIPromptEditModal.vue'
|
||||
export { default as CacheManageTab } from './CacheManageTab.vue'
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
"title": "Settings",
|
||||
"tabs": {
|
||||
"basic": "General",
|
||||
"ai": "AI Settings",
|
||||
"aiConfig": "AI Models",
|
||||
"aiPrompt": "AI Chat Config",
|
||||
"aiPrompt": "Chat Config",
|
||||
"aiPreset": "Prompts",
|
||||
"storage": "Storage",
|
||||
"about": "About"
|
||||
},
|
||||
@@ -42,6 +44,7 @@
|
||||
}
|
||||
},
|
||||
"aiConfig": {
|
||||
"title": "Model Configuration",
|
||||
"loading": "Loading...",
|
||||
"inUse": "Active",
|
||||
"defaultModel": "Default Model",
|
||||
@@ -129,7 +132,12 @@
|
||||
}
|
||||
},
|
||||
"aiPrompt": {
|
||||
"title": "Chat Settings",
|
||||
"chatSettings": {
|
||||
"title": "Chat Settings"
|
||||
},
|
||||
"exportSettings": {
|
||||
"title": "Export Settings"
|
||||
},
|
||||
"maxMessages": {
|
||||
"title": "Max Messages per Request",
|
||||
"description": "Max number of messages sent to AI per request. Higher values consume more tokens but provide more accurate context (recommended: 2000)."
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
{
|
||||
"title": "全局设置",
|
||||
"title": "设置",
|
||||
"tabs": {
|
||||
"basic": "基础设置",
|
||||
"ai": "AI 设置",
|
||||
"aiConfig": "模型配置",
|
||||
"aiPrompt": "AI 对话配置",
|
||||
"aiPrompt": "对话配置",
|
||||
"aiPreset": "提示词配置",
|
||||
"storage": "存储管理",
|
||||
"about": "关于"
|
||||
},
|
||||
@@ -42,6 +44,7 @@
|
||||
}
|
||||
},
|
||||
"aiConfig": {
|
||||
"title": "模型配置",
|
||||
"loading": "加载中...",
|
||||
"inUse": "使用中",
|
||||
"defaultModel": "默认模型",
|
||||
@@ -129,7 +132,12 @@
|
||||
}
|
||||
},
|
||||
"aiPrompt": {
|
||||
"title": "对话设置",
|
||||
"chatSettings": {
|
||||
"title": "对话设置"
|
||||
},
|
||||
"exportSettings": {
|
||||
"title": "导出设置"
|
||||
},
|
||||
"maxMessages": {
|
||||
"title": "发送条数限制",
|
||||
"description": "每次提交给 AI 的最大消息数,数值越大 Token 消耗越多,分析也更准确(建议2000)"
|
||||
|
||||
Reference in New Issue
Block a user