feat: 优化AI对话状态栏

This commit is contained in:
digua
2026-01-13 00:18:00 +08:00
parent 2b04c377d6
commit aa1023a014
5 changed files with 148 additions and 19 deletions
@@ -3,6 +3,7 @@ import { ref, computed } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { usePromptStore } from '@/stores/prompt'
import { useLayoutStore } from '@/stores/layout'
const { t } = useI18n()
@@ -16,7 +17,8 @@ const props = defineProps<{
// Store
const promptStore = usePromptStore()
const { aiPromptSettings, activePreset } = storeToRefs(promptStore)
const layoutStore = useLayoutStore()
const { aiPromptSettings, activePreset, aiGlobalSettings } = storeToRefs(promptStore)
// 当前类型对应的预设列表(根据 applicableTo 过滤)
const currentPresets = computed(() => promptStore.getPresetsForChatType(props.chatType))
@@ -38,6 +40,17 @@ function setActivePreset(presetId: string) {
promptStore.setActivePreset(presetId)
isPresetPopoverOpen.value = false
}
// 打开设置弹窗并跳转到预设配置
function openPresetSettings() {
isPresetPopoverOpen.value = false
layoutStore.openSettingAt('ai', 'preset')
}
// 打开设置弹窗并跳转到对话配置(消息条数限制)
function openChatSettings() {
layoutStore.openSettingAt('ai', 'chat')
}
</script>
<template>
@@ -76,12 +89,32 @@ function setActivePreset(presetId: string) {
/>
<span class="truncate">{{ preset.name }}</span>
</button>
<!-- 分隔线 -->
<div class="my-1 border-t border-gray-200 dark:border-gray-700" />
<!-- 新增预设按钮 -->
<button
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-300"
@click="openPresetSettings"
>
<UIcon name="i-heroicons-plus" class="h-4 w-4 shrink-0" />
<span>{{ t('preset.new') }}</span>
</button>
</div>
</template>
</UPopover>
<!-- 右侧Token 使用量 + 配置状态指示 -->
<!-- 右侧配置状态指示 -->
<div class="flex items-center gap-3">
<!-- 消息条数限制点击跳转设置 -->
<button
class="flex items-center gap-1 rounded-md px-2 py-1 text-xs text-gray-400 transition-colors hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-gray-800 dark:hover:text-gray-300"
:title="t('messageLimit.title')"
@click="openChatSettings"
>
<span>{{ t('messageLimit.label') }}{{ aiGlobalSettings.maxMessagesPerRequest }}</span>
</button>
<!-- Token 使用量 -->
<div
v-if="sessionTokenUsage.totalTokens > 0"
@@ -111,7 +144,12 @@ function setActivePreset(presetId: string) {
"preset": {
"default": "默认预设",
"groupTitle": "群聊提示词预设",
"privateTitle": "私聊提示词预设"
"privateTitle": "私聊提示词预设",
"new": "新增提示词"
},
"messageLimit": {
"label": "消息上限:",
"title": "每次发送的最大消息条数,点击配置"
},
"tokenUsageTitle": "本次会话累计 Token 使用量",
"status": {
@@ -123,7 +161,12 @@ function setActivePreset(presetId: string) {
"preset": {
"default": "Default Preset",
"groupTitle": "Group Chat Presets",
"privateTitle": "Private Chat Presets"
"privateTitle": "Private Chat Presets",
"new": "New Preset"
},
"messageLimit": {
"label": "Limit: ",
"title": "Max messages per request, click to configure"
},
"tokenUsageTitle": "Total token usage in this session",
"status": {
+46 -14
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref, watch, computed } from 'vue'
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'
@@ -8,6 +9,13 @@ 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<{
@@ -29,10 +37,16 @@ const tabs = computed(() => [
])
const activeTab = ref('settings')
// Template refs - used via ref="xxx" in template
const storageTabRef = ref<InstanceType<typeof StorageTab> | null>(null)
// Ensure refs are tracked for vue-tsc
void storageTabRef
// 统一的 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() {
@@ -42,16 +56,35 @@ function handleAIConfigChanged() {
// 关闭弹窗
function closeModal() {
emit('update:open', false)
layoutStore.clearSettingTarget()
}
// 监听打开状态
watch(
() => props.open,
(newVal) => {
async (newVal) => {
if (newVal) {
activeTab.value = 'settings' // 默认打开基础设置 Tab
// 刷新存储管理(如果需要的话,或者在切换到 storage tab 时刷新)
storageTabRef.value?.refresh()
// 检查是否有指定的跳转目标
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()
}
}
)
@@ -60,9 +93,8 @@ watch(
watch(
() => activeTab.value,
(newTab) => {
if (newTab === 'storage') {
storageTabRef.value?.refresh()
}
// 通用刷新逻辑
tabRefs.value[newTab]?.refresh?.()
}
)
</script>
@@ -91,12 +123,12 @@ watch(
<!-- AI 设置 -->
<div v-show="activeTab === 'ai'" class="h-full">
<AISettingsTab @config-changed="handleAIConfigChanged" />
<AISettingsTab :ref="(el) => setTabRef('ai', el)" @config-changed="handleAIConfigChanged" />
</div>
<!-- 存储管理 -->
<div v-show="activeTab === 'storage'" class="h-full">
<StorageTab ref="storageTabRef" />
<StorageTab :ref="(el) => setTabRef('storage', el)" />
</div>
<!-- 关于 -->
@@ -22,7 +22,7 @@ const navItems = computed(() => [
])
// 使用二级导航滚动联动 composable
const { activeNav, scrollContainerRef, setSectionRef, handleNavChange } = useSubTabsScroll(navItems)
const { activeNav, scrollContainerRef, setSectionRef, handleNavChange, scrollToId } = useSubTabsScroll(navItems)
void scrollContainerRef // 在模板中通过 ref="scrollContainerRef" 使用
// AI 配置变更回调
@@ -30,6 +30,18 @@ function handleAIConfigChanged() {
emit('config-changed')
}
/**
* 滚动到指定 section(供外部调用)
*/
function scrollToSection(sectionId: string) {
scrollToId(sectionId)
}
// 暴露方法供父组件调用
defineExpose({
scrollToSection,
})
// Template refs
const aiModelConfigRef = ref<InstanceType<typeof AIModelConfigTab> | null>(null)
void aiModelConfigRef
+16
View File
@@ -94,11 +94,27 @@ export function useSubTabsScroll(navItems: ComputedRef<SubTabNavItem[]> | Ref<Su
scrollContainerRef.value?.removeEventListener('scroll', handleScroll)
})
/**
* 程序化滚动到指定 section(供外部调用)
*/
function scrollToId(id: string) {
const section = sectionRefs.value[id]
if (section && scrollContainerRef.value) {
isUserClick.value = true
section.scrollIntoView({ behavior: 'smooth', block: 'start' })
activeNav.value = id
setTimeout(() => {
isUserClick.value = false
}, 500)
}
}
return {
activeNav,
scrollContainerRef,
sectionRefs,
setSectionRef,
handleNavChange,
scrollToId,
}
}
+26
View File
@@ -15,6 +15,12 @@ export const useLayoutStore = defineStore(
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) // 截图时开启移动端适配,默认开启
@@ -61,6 +67,23 @@ 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,
@@ -68,12 +91,15 @@ export const useLayoutStore = defineStore(
screenCaptureImage,
showChatRecordDrawer,
chatRecordQuery,
settingTarget,
screenshotMobileAdapt,
toggleSidebar,
openScreenCaptureModal,
closeScreenCaptureModal,
openChatRecordDrawer,
closeChatRecordDrawer,
openSettingAt,
clearSettingTarget,
}
},
{