feat: 添加全局人设与头像设置并优化对话及弹窗交互

This commit is contained in:
2026-09-05 22:26:12 +08:00
parent 7551716e13
commit a1ab1024f0
18 changed files with 450 additions and 22 deletions
@@ -4,6 +4,25 @@ import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import SecondarySidebar from './SecondarySidebar.vue'
it('keeps conversation and file widths separate across route changes', async () => {
localStorage.setItem('chat-sidebar-width', '320')
localStorage.setItem('workspace-sidebar-width', '240')
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: { template: '<div />' } }] })
await router.push('/')
const wrapper = mount(SecondarySidebar, {props:{component:'conversation-list'}, global:{plugins:[router],stubs:{ConversationListPanel:true,FileTreePanel:true}}})
await wrapper.vm.$nextTick()
expect(wrapper.get('aside').attributes('style')).toContain('320px')
await wrapper.get('[role="separator"]').trigger('keydown', {key:'ArrowRight'})
expect(localStorage.getItem('chat-sidebar-width')).toBe('336')
await wrapper.setProps({component:'file-tree'})
expect(wrapper.get('aside').attributes('style')).toContain('240px')
await wrapper.setProps({component:'conversation-list'})
expect(wrapper.get('aside').attributes('style')).toContain('336px')
wrapper.unmount()
localStorage.removeItem('chat-sidebar-width')
localStorage.removeItem('workspace-sidebar-width')
})
it('resizes by keyboard, clamps bounds and restores the saved width', async () => {
localStorage.removeItem('workspace-sidebar-width')
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: { template: '<div />' } }] })
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import FileTreePanel from '@/features/workspace/FileTreePanel.vue'
import ConversationListPanel from '@/features/chat/ConversationListPanel.vue'
import RunListPanel from '@/features/agent/RunListPanel.vue'
@@ -16,10 +16,12 @@ const props = defineProps<{
const route = useRoute()
const routeName = computed(() => route.name as string)
const sidebar = ref<HTMLElement | null>(null)
const resizable = computed(() => ['file-tree', 'conversation-list'].includes(props.component ?? ''))
const storageKey = computed(() => props.component === 'conversation-list' ? 'chat-sidebar-width' : 'workspace-sidebar-width')
const width = ref(272)
const maxWidth = ref(520)
let dragging = false
function saveWidth() { try { localStorage.setItem('workspace-sidebar-width', String(width.value)) } catch { /* Keep resizing available when storage is unavailable. */ } }
function saveWidth() { try { localStorage.setItem(storageKey.value, String(width.value)) } catch { /* Keep resizing available when storage is unavailable. */ } }
function clampWidth(value: number) { return Math.max(200, Math.min(maxWidth.value, value)) }
function updateBounds() {
maxWidth.value = Math.max(200, Math.min(520, window.innerWidth - (sidebar.value?.getBoundingClientRect().left ?? 0) - 320))
@@ -43,9 +45,14 @@ function resizeWithKeyboard(event: KeyboardEvent) {
width.value = event.key === 'Home' ? 200 : event.key === 'End' ? maxWidth.value : clampWidth(width.value + (event.key === 'ArrowLeft' ? -16 : 16))
saveWidth()
}
onMounted(() => {
try { const saved = Number(localStorage.getItem('workspace-sidebar-width')); if (saved >= 200 && Number.isFinite(saved)) width.value = saved } catch { /* Use default width. */ }
function restoreWidth() {
width.value = 272
try { const saved = Number(localStorage.getItem(storageKey.value)); if (saved >= 200 && Number.isFinite(saved)) width.value = saved } catch { /* Use default width. */ }
updateBounds()
}
watch(() => props.component, () => { dragging = false; restoreWidth() })
onMounted(() => {
restoreWidth()
window.addEventListener('resize', updateBounds)
})
onBeforeUnmount(() => { endResize(); window.removeEventListener('resize', updateBounds) })
@@ -66,7 +73,7 @@ const showSkillToggle = computed(() => routeName.value === 'skills' || routeName
</script>
<template>
<aside ref="sidebar" class="secondary-sidebar" :style="component === 'file-tree' ? { width: `${width}px` } : undefined">
<aside ref="sidebar" class="secondary-sidebar" :style="resizable ? { width: `${width}px` } : undefined">
<div v-if="component !== 'file-tree'" class="sidebar-header">
<h3 class="sidebar-title">{{ sidebarTitle }}</h3>
<div v-if="showSkillToggle" class="sidebar-tabs">
@@ -82,7 +89,7 @@ const showSkillToggle = computed(() => routeName.value === 'skills' || routeName
<TaskFiltersPanel v-else-if="component === 'task-filters'" />
<ExtensionListPanel v-else-if="component === 'extension-list'" />
</div>
<div v-if="component === 'file-tree'" class="sidebar-resizer" role="separator" aria-orientation="vertical" :aria-label="t('调整文件侧栏宽度', 'Resize file sidebar')" :aria-valuenow="width" :aria-valuemin="200" :aria-valuemax="maxWidth" tabindex="0" @pointerdown="beginResize" @pointermove="resize" @pointerup="endResize" @pointercancel="endResize" @lostpointercapture="endResize" @keydown="resizeWithKeyboard" @dblclick="width = clampWidth(272); saveWidth()" />
<div v-if="resizable" class="sidebar-resizer" role="separator" aria-orientation="vertical" :aria-label="t('调整侧栏宽度', 'Resize sidebar')" :aria-valuenow="width" :aria-valuemin="200" :aria-valuemax="maxWidth" tabindex="0" @pointerdown="beginResize" @pointermove="resize" @pointerup="endResize" @pointercancel="endResize" @lostpointercapture="endResize" @keydown="resizeWithKeyboard" @dblclick="width = clampWidth(272); saveWidth()" />
</aside>
</template>