mirror of
https://github.com/hellodigua/ChatLab.git
synced 2026-05-12 09:11:13 +08:00
feat: 优化AI对话状态栏
This commit is contained in:
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 要跳转的 Tab(settings, 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,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user