From ef03a8f7458d450dc5d09496e97679b9d14f8f1e Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sun, 30 Aug 2026 20:13:29 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat(frontend):=20=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E8=A7=86=E8=A7=89=E4=B8=8E=E8=BD=BB=E9=87=8F?= =?UTF-8?q?=E5=8A=A8=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/common/AppShell.vue | 3 +- .../src/components/common/CommandPalette.vue | 12 ++- .../src/components/common/PrimarySidebar.vue | 23 +++-- .../components/common/SecondarySidebar.vue | 17 ++-- frontend/src/components/common/StatusBar.vue | 11 ++- frontend/src/components/common/TitleBar.vue | 27 ++++-- frontend/src/features/agent/AgentView.vue | 12 ++- frontend/src/features/chat/ChatView.vue | 17 ++-- frontend/src/features/search/SearchView.vue | 5 +- .../src/features/settings/SettingsView.vue | 5 +- frontend/src/features/tasks/TasksView.vue | 7 +- frontend/src/features/vault/VaultEntry.vue | 34 +++++-- .../src/features/workspace/FileTreePanel.vue | 2 +- .../src/features/workspace/WorkspaceView.vue | 7 ++ frontend/src/styles/features.css | 93 +++++++++++++------ frontend/src/styles/motion.spec.ts | 27 ++++++ frontend/src/styles/tokens.css | 67 ++++++++++--- 17 files changed, 275 insertions(+), 94 deletions(-) create mode 100644 frontend/src/styles/motion.spec.ts diff --git a/frontend/src/components/common/AppShell.vue b/frontend/src/components/common/AppShell.vue index fe7c007..2b1ac95 100644 --- a/frontend/src/components/common/AppShell.vue +++ b/frontend/src/components/common/AppShell.vue @@ -73,7 +73,7 @@ defineExpose({ openCitation }) flex-direction: column; height: 100vh; width: 100vw; - background: var(--color-background-primary); + background: var(--color-background-secondary); color: var(--color-text-primary); } @@ -89,5 +89,6 @@ defineExpose({ openCitation }) min-width: 0; overflow: hidden; background: var(--color-background-primary); + isolation: isolate; } diff --git a/frontend/src/components/common/CommandPalette.vue b/frontend/src/components/common/CommandPalette.vue index 33e238d..9ab6aa5 100644 --- a/frontend/src/components/common/CommandPalette.vue +++ b/frontend/src/components/common/CommandPalette.vue @@ -89,13 +89,17 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleKeydown)) diff --git a/frontend/src/components/common/PrimarySidebar.vue b/frontend/src/components/common/PrimarySidebar.vue index 5ac01c4..052f0b4 100644 --- a/frontend/src/components/common/PrimarySidebar.vue +++ b/frontend/src/components/common/PrimarySidebar.vue @@ -60,13 +60,14 @@ function toggleExpanded() { diff --git a/frontend/src/components/common/SecondarySidebar.vue b/frontend/src/components/common/SecondarySidebar.vue index 4cd09c3..00509c2 100644 --- a/frontend/src/components/common/SecondarySidebar.vue +++ b/frontend/src/components/common/SecondarySidebar.vue @@ -53,7 +53,7 @@ const showSkillToggle = computed(() => routeName.value === 'skills' || routeName diff --git a/frontend/src/components/common/StatusBar.vue b/frontend/src/components/common/StatusBar.vue index cb897c2..992191e 100644 --- a/frontend/src/components/common/StatusBar.vue +++ b/frontend/src/components/common/StatusBar.vue @@ -107,8 +107,8 @@ const showEditorInfo = computed(() => route.name === 'workspace') display: flex; align-items: center; justify-content: space-between; - padding: 0 var(--space-md); - background: var(--color-background-secondary); + padding: 0 var(--space-lg); + background: var(--color-surface-secondary); border-top: 1px solid var(--color-border-subtle); font-size: var(--font-size-xs); color: var(--color-text-secondary); @@ -129,6 +129,7 @@ const showEditorInfo = computed(() => route.name === 'workspace') gap: 6px; white-space: nowrap; cursor: default; + transition: color var(--motion-fast); &:hover { color: var(--color-text-primary); @@ -140,6 +141,7 @@ const showEditorInfo = computed(() => route.name === 'workspace') height: 6px; border-radius: 50%; flex-shrink: 0; + box-shadow: 0 0 0 2px var(--color-surface-secondary); } .agent-status { @@ -162,4 +164,9 @@ const showEditorInfo = computed(() => route.name === 'workspace') .provider-info { color: var(--color-text-tertiary); } + +@media (max-width: 760px) { + .statusbar-left, .statusbar-right { gap: var(--space-sm); } + .provider-info { display: none; } +} diff --git a/frontend/src/components/common/TitleBar.vue b/frontend/src/components/common/TitleBar.vue index bfd5043..e004ecf 100644 --- a/frontend/src/components/common/TitleBar.vue +++ b/frontend/src/components/common/TitleBar.vue @@ -71,8 +71,8 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore display: flex; align-items: center; justify-content: space-between; - padding: 0 var(--space-md); - background: var(--color-background-secondary); + padding: 0 var(--space-lg); + background: var(--color-surface-secondary); border-bottom: 1px solid var(--color-border-subtle); font-size: var(--font-size-sm); flex-shrink: 0; @@ -129,7 +129,13 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore } .app-name { - font-weight: 500; + padding: 3px 10px; + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-full); + background: var(--color-surface-primary); + color: var(--color-text-secondary); + font-weight: 650; + letter-spacing: .04em; } .titlebar-right { @@ -141,8 +147,8 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore } .icon-btn { - width: 28px; - height: 28px; + width: 30px; + height: 30px; display: flex; align-items: center; justify-content: center; @@ -150,11 +156,12 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore color: var(--color-text-secondary); font-size: 14px; -webkit-app-region: no-drag; - transition: background var(--motion-fast); + transition: background-color var(--motion-fast), color var(--motion-fast), transform var(--motion-fast); &:hover { background: var(--color-background-hover); color: var(--color-text-primary); + transform: rotate(8deg); } } @@ -179,7 +186,7 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore color: var(--color-text-secondary); border-radius: var(--radius-sm); cursor: pointer; - transition: background var(--motion-fast); + transition: background-color var(--motion-fast), color var(--motion-fast); &:hover { background: var(--color-background-hover); @@ -190,4 +197,10 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore color: white; } } + +@media (max-width: 760px) { + .titlebar-left, .titlebar-right { min-width: 0; } + .titlebar-center, .window-controls { display: none; } + .file-name { max-width: 42vw; } +} diff --git a/frontend/src/features/agent/AgentView.vue b/frontend/src/features/agent/AgentView.vue index 06b5aa3..a7f41fd 100644 --- a/frontend/src/features/agent/AgentView.vue +++ b/frontend/src/features/agent/AgentView.vue @@ -108,16 +108,22 @@ function eventText(event: AgentEvent) { diff --git a/frontend/src/features/search/SearchView.vue b/frontend/src/features/search/SearchView.vue index d078c0e..f1bb835 100644 --- a/frontend/src/features/search/SearchView.vue +++ b/frontend/src/features/search/SearchView.vue @@ -66,13 +66,16 @@ async function openResult(result: SearchResult) { diff --git a/frontend/src/features/workspace/WorkspaceView.vue b/frontend/src/features/workspace/WorkspaceView.vue index 82e2853..05ada11 100644 --- a/frontend/src/features/workspace/WorkspaceView.vue +++ b/frontend/src/features/workspace/WorkspaceView.vue @@ -59,6 +59,11 @@ onMounted(() => { .empty-content { text-align: center; + padding: var(--space-3xl); + border: 1px dashed var(--color-border-default); + border-radius: var(--radius-xl); + background: var(--color-background-secondary); + animation: workspace-empty-in var(--motion-normal) both; h2 { font-size: 18px; @@ -75,4 +80,6 @@ onMounted(() => { font-size: 48px; opacity: 0.5; } + +@keyframes workspace-empty-in { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } } diff --git a/frontend/src/styles/features.css b/frontend/src/styles/features.css index 10ea4b9..782c0c2 100644 --- a/frontend/src/styles/features.css +++ b/frontend/src/styles/features.css @@ -1,9 +1,18 @@ .feature-page { height: 100%; overflow: auto; - padding: var(--space-2xl); - background: var(--color-background-primary); + padding: clamp(var(--space-lg), 3vw, var(--space-3xl)); + background: + radial-gradient(circle at 92% -8%, var(--color-accent-soft) 0, transparent 28%), + var(--color-background-primary); user-select: text; + scrollbar-gutter: stable; + animation: page-in var(--motion-normal) both; +} + +.chat-page, +.workspace-view { + animation: page-in var(--motion-normal) both; } .feature-header { @@ -11,13 +20,15 @@ align-items: flex-start; justify-content: space-between; gap: var(--space-lg); - margin-bottom: var(--space-xl); + margin: 0 auto var(--space-2xl); + max-width: 1180px; } -.feature-header h1 { font-size: var(--font-size-3xl); line-height: 1.2; } -.feature-header p { margin-top: var(--space-xs); color: var(--color-text-secondary); } +.feature-header h1 { font-size: clamp(var(--font-size-2xl), 2.3vw, 30px); line-height: 1.15; letter-spacing: -.025em; } +.feature-header p { max-width: 680px; margin-top: var(--space-sm); color: var(--color-text-secondary); } .feature-actions, .inline-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--space-sm); } -.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-lg); } +.feature-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: var(--space-lg); max-width: 1180px; margin-inline: auto; } +.feature-page > .panel { max-width: 1180px; margin-inline: auto; } .split-view { display: grid; grid-template-columns: minmax(260px, .8fr) minmax(360px, 1.7fr); gap: var(--space-lg); min-height: 0; } .panel, .item-card { @@ -27,10 +38,13 @@ box-shadow: var(--shadow-sm); } -.panel { padding: var(--space-xl); } -.item-card { padding: var(--space-lg); transition: border-color var(--motion-fast), transform var(--motion-fast); } -.item-card:hover { border-color: var(--color-accent-secondary); } -.item-card.selected { border-color: var(--color-accent-primary); box-shadow: 0 0 0 2px var(--color-accent-soft); } +.panel { padding: clamp(var(--space-lg), 2.2vw, var(--space-2xl)); } +.item-card { + padding: var(--space-lg); + transition: border-color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast), background-color var(--motion-fast); +} +.item-card:hover { border-color: var(--color-accent-secondary); box-shadow: var(--shadow-md); transform: translateY(-2px); } +.item-card.selected { border-color: var(--color-accent-primary); box-shadow: 0 0 0 3px var(--color-accent-soft), var(--shadow-md); } .panel-title { margin-bottom: var(--space-md); font-size: var(--font-size-xl); } .muted { color: var(--color-text-secondary); } .subtle { color: var(--color-text-tertiary); font-size: var(--font-size-sm); } @@ -41,14 +55,18 @@ border: 1px solid transparent; border-radius: var(--radius-md); font-weight: 600; + line-height: 1; + transition: background-color var(--motion-fast), border-color var(--motion-fast), color var(--motion-fast), box-shadow var(--motion-fast), transform var(--motion-fast); } -.button-primary { background: var(--color-accent-primary); color: var(--color-text-inverse); } -.button-primary:hover { background: var(--color-accent-primary-hover); } +.button-primary { background: var(--color-accent-primary); color: var(--color-text-inverse); box-shadow: 0 4px 12px color-mix(in srgb, var(--color-accent-primary) 24%, transparent); } +.button-primary:hover { background: var(--color-accent-primary-hover); transform: translateY(-1px); box-shadow: 0 7px 18px color-mix(in srgb, var(--color-accent-primary) 28%, transparent); } .button-secondary { border-color: var(--color-border-default); background: var(--color-surface-primary); } -.button-secondary:hover, .icon-button:hover { background: var(--color-background-hover); } +.button-secondary:hover, .icon-button:hover { border-color: var(--color-accent-secondary); background: var(--color-background-hover); color: var(--color-accent-primary); transform: translateY(-1px); } .button-danger { background: var(--color-error-soft); color: var(--color-error); } -button:disabled { cursor: not-allowed; opacity: .55; } +.button-danger:hover { transform: translateY(-1px); box-shadow: 0 5px 14px color-mix(in srgb, var(--color-error) 18%, transparent); } +button:active:not(:disabled) { transform: translateY(0); } +button:disabled { cursor: not-allowed; opacity: .55; box-shadow: none; transform: none; } .field { display: grid; gap: var(--space-xs); } .field label { color: var(--color-text-secondary); font-size: var(--font-size-sm); font-weight: 600; } @@ -59,10 +77,12 @@ button:disabled { cursor: not-allowed; opacity: .55; } outline: none; background: var(--color-background-primary); color: var(--color-text-primary); + transition: border-color var(--motion-fast), box-shadow var(--motion-fast), background-color var(--motion-fast); } -.input, .select { height: 36px; padding: 0 var(--space-md); } +.input, .select { height: 38px; padding: 0 var(--space-md); } .textarea { min-height: 100px; padding: var(--space-md); resize: vertical; } -.input:focus, .select:focus, .textarea:focus { border-color: var(--color-border-focus); box-shadow: 0 0 0 2px var(--color-accent-soft); } +.input:hover, .select:hover, .textarea:hover { border-color: var(--color-text-tertiary); } +.input:focus, .select:focus, .textarea:focus { border-color: var(--color-border-focus); box-shadow: 0 0 0 3px var(--color-accent-soft); background: var(--color-surface-primary); } .form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: var(--space-md); } .badge { @@ -75,6 +95,7 @@ button:disabled { cursor: not-allowed; opacity: .55; } color: var(--color-text-secondary); font-size: var(--font-size-xs); font-weight: 600; + letter-spacing: .01em; } .badge.success { background: var(--color-success-soft); color: var(--color-success); } .badge.warning { background: var(--color-warning-soft); color: var(--color-warning); } @@ -82,25 +103,45 @@ button:disabled { cursor: not-allowed; opacity: .55; } .badge.info { background: var(--color-info-soft); color: var(--color-info); } .tag-list { display: flex; flex-wrap: wrap; gap: var(--space-xs); } -.empty-state { display: grid; place-items: center; min-height: 220px; padding: var(--space-2xl); text-align: center; color: var(--color-text-secondary); } +.empty-state { display: grid; place-items: center; min-height: 220px; padding: var(--space-3xl); border: 1px dashed var(--color-border-default); border-radius: var(--radius-xl); background: color-mix(in srgb, var(--color-background-secondary) 74%, transparent); text-align: center; color: var(--color-text-secondary); } .empty-state strong { display: block; margin-bottom: var(--space-xs); color: var(--color-text-primary); font-size: var(--font-size-xl); } -.error-banner { margin-bottom: var(--space-lg); padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-error-soft); color: var(--color-error); } -.notice-banner { margin-bottom: var(--space-lg); padding: var(--space-md); border-radius: var(--radius-md); background: var(--color-info-soft); color: var(--color-info); } +.error-banner, .notice-banner { border: 1px solid transparent; animation: notice-in var(--motion-normal) both; } +.error-banner { margin-bottom: var(--space-lg); padding: var(--space-md) var(--space-lg); border-color: color-mix(in srgb, var(--color-error) 22%, transparent); border-radius: var(--radius-md); background: var(--color-error-soft); color: var(--color-error); } +.notice-banner { margin-bottom: var(--space-lg); padding: var(--space-md) var(--space-lg); border-color: color-mix(in srgb, var(--color-info) 22%, transparent); border-radius: var(--radius-md); background: var(--color-info-soft); color: var(--color-info); } .sidebar-panel { padding: var(--space-md); } .sidebar-panel .input, .sidebar-panel .select { margin-bottom: var(--space-sm); } .sidebar-list { display: grid; gap: var(--space-xs); } -.sidebar-list-item { padding: var(--space-sm); border-radius: var(--radius-md); cursor: pointer; } -.sidebar-list-item:hover, .sidebar-list-item.active { background: var(--color-background-hover); } +.sidebar-list-item { padding: 9px var(--space-sm); border: 1px solid transparent; border-radius: var(--radius-md); cursor: pointer; transition: color var(--motion-fast), background-color var(--motion-fast), border-color var(--motion-fast), transform var(--motion-fast); } +.sidebar-list-item:hover { background: var(--color-background-hover); transform: translateX(2px); } +.sidebar-list-item.active { border-color: color-mix(in srgb, var(--color-accent-primary) 18%, transparent); background: var(--color-accent-soft); color: var(--color-accent-primary); } -.modal-backdrop { position: fixed; inset: 0; z-index: var(--z-modal); display: grid; place-items: center; padding: var(--space-xl); background: var(--color-background-overlay); } -.modal { width: min(560px, 100%); max-height: 85vh; overflow: auto; padding: var(--space-xl); border-radius: var(--radius-lg); background: var(--color-surface-elevated); box-shadow: var(--shadow-xl); } +.modal-backdrop { position: fixed; inset: 0; z-index: var(--z-modal); display: grid; place-items: center; padding: var(--space-xl); background: var(--color-background-overlay); animation: backdrop-in var(--motion-fast) both; } +.modal { width: min(560px, 100%); max-height: 85vh; overflow: auto; padding: var(--space-2xl); border: 1px solid var(--color-border-default); border-radius: var(--radius-xl); background: var(--color-surface-elevated); box-shadow: var(--shadow-xl); animation: modal-in var(--motion-normal) both; } .modal h2 { margin-bottom: var(--space-lg); } .modal form { display: grid; gap: var(--space-md); } -.settings-nav { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-bottom: var(--space-xl); border-bottom: 1px solid var(--color-border-default); } -.settings-nav button { padding: var(--space-sm) var(--space-md); border-bottom: 2px solid transparent; color: var(--color-text-secondary); } -.settings-nav button.active { border-color: var(--color-accent-primary); color: var(--color-accent-primary); } +.settings-nav { display: flex; flex-wrap: wrap; gap: var(--space-xs); margin-bottom: var(--space-xl); padding: var(--space-xs); border: 1px solid var(--color-border-default); border-radius: var(--radius-lg); background: var(--color-background-secondary); } +.settings-nav button { padding: 8px var(--space-md); border-radius: var(--radius-md); color: var(--color-text-secondary); transition: color var(--motion-fast), background-color var(--motion-fast), box-shadow var(--motion-fast); } +.settings-nav button:hover { color: var(--color-text-primary); background: var(--color-background-hover); } +.settings-nav button.active { background: var(--color-surface-primary); color: var(--color-accent-primary); box-shadow: var(--shadow-sm); } + +@keyframes page-in { + from { opacity: 0; transform: translateY(6px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes notice-in { + from { opacity: 0; transform: translateY(-4px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes backdrop-in { from { opacity: 0; } to { opacity: 1; } } + +@keyframes modal-in { + from { opacity: 0; transform: translateY(8px) scale(.985); } + to { opacity: 1; transform: translateY(0) scale(1); } +} @media (max-width: 900px) { .feature-page { padding: var(--space-lg); } diff --git a/frontend/src/styles/motion.spec.ts b/frontend/src/styles/motion.spec.ts new file mode 100644 index 0000000..083a9d4 --- /dev/null +++ b/frontend/src/styles/motion.spec.ts @@ -0,0 +1,27 @@ +/// + +import { readFileSync } from 'node:fs' +import { describe, expect, it } from 'vitest' + +const globalStyles = [ + new URL('./tokens.css', import.meta.url), + new URL('./features.css', import.meta.url), +].map((path) => readFileSync(path, 'utf8')).join('\n') + +describe('轻量动效基线', () => { + it('为减少动态效果偏好提供全局回退', () => { + expect(globalStyles).toContain('@media (prefers-reduced-motion: reduce)') + }) + + it('不使用全属性过渡或高成本模糊滤镜', () => { + expect(globalStyles).not.toMatch(/transition:\s*all\b/) + expect(globalStyles).not.toMatch(/(?:backdrop-)?filter\s*:/) + }) + + it('页面入场只改变透明度和变换', () => { + const pageAnimation = globalStyles.match(/@keyframes page-in\s*{[\s\S]*?\n}/)?.[0] ?? '' + expect(pageAnimation).toContain('opacity') + expect(pageAnimation).toContain('transform') + expect(pageAnimation).not.toMatch(/(?:width|height|margin|padding|top|left)\s*:/) + }) +}) diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index 8e3156b..5703776 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -45,16 +45,16 @@ --color-border-disabled: #eef0f3; /* Shadow */ - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); - --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.12); - --shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.16); + --shadow-sm: 0 1px 2px rgba(31, 35, 40, 0.05), 0 1px 5px rgba(31, 35, 40, 0.03); + --shadow-md: 0 8px 22px rgba(31, 35, 40, 0.08), 0 2px 6px rgba(31, 35, 40, 0.04); + --shadow-lg: 0 16px 36px rgba(31, 35, 40, 0.11), 0 4px 12px rgba(31, 35, 40, 0.05); + --shadow-xl: 0 24px 64px rgba(31, 35, 40, 0.18), 0 8px 20px rgba(31, 35, 40, 0.08); /* Radius */ - --radius-sm: 4px; - --radius-md: 6px; - --radius-lg: 10px; - --radius-xl: 14px; + --radius-sm: 6px; + --radius-md: 9px; + --radius-lg: 13px; + --radius-xl: 18px; --radius-full: 9999px; /* Spacing */ @@ -100,16 +100,16 @@ --z-titlebar: 500; /* Motion */ - --motion-fast: 120ms cubic-bezier(0.4, 0, 0.2, 1); - --motion-normal: 200ms cubic-bezier(0.4, 0, 0.2, 1); - --motion-slow: 300ms cubic-bezier(0.4, 0, 0.2, 1); + --motion-fast: 120ms cubic-bezier(0.2, 0, 0, 1); + --motion-normal: 190ms cubic-bezier(0.2, 0, 0, 1); + --motion-slow: 260ms cubic-bezier(0.2, 0, 0, 1); /* Layout */ - --titlebar-height: 38px; - --sidebar-primary-width: 52px; + --titlebar-height: 42px; + --sidebar-primary-width: 58px; --sidebar-primary-width-expanded: 180px; - --sidebar-secondary-width: 260px; - --statusbar-height: 26px; + --sidebar-secondary-width: 272px; + --statusbar-height: 28px; } [data-theme='dark'] { @@ -221,6 +221,25 @@ button { color: inherit; } +button, +a, +input, +textarea, +select { + -webkit-tap-highlight-color: transparent; +} + +input[type='checkbox'], +input[type='radio'], +input[type='range'] { + accent-color: var(--color-accent-primary); +} + +:focus-visible { + outline: 2px solid var(--color-border-focus); + outline-offset: 2px; +} + input, textarea, select { @@ -261,7 +280,25 @@ ol { color: var(--color-accent-primary); } +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + scroll-behavior: auto !important; + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + [data-theme='dark'] ::selection { background: var(--color-accent-primary); color: var(--color-text-inverse); } + +@media (max-width: 900px) { + :root { + --sidebar-primary-width-expanded: 160px; + --sidebar-secondary-width: 224px; + } +} From 7fa2e9c404a02f2f9b4f232e9e69ff89cb9e1105 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sun, 30 Aug 2026 20:14:08 +0800 Subject: [PATCH 2/8] =?UTF-8?q?docs(frontend):=20=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=A7=86=E8=A7=89=E4=BC=98=E5=8C=96=E4=B8=8E=E5=8A=A8=E6=95=88?= =?UTF-8?q?=E7=BA=A6=E6=9D=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/前端壳子与接口层开发说明.md | 4 +- docs/前端视觉与轻量动效优化开发说明.md | 112 +++++++++++++++++++++++++ docs/第一阶段测试验证操作手册.md | 7 +- 4 files changed, 118 insertions(+), 6 deletions(-) create mode 100644 docs/前端视觉与轻量动效优化开发说明.md diff --git a/README.md b/README.md index b5c8405..f83380c 100644 --- a/README.md +++ b/README.md @@ -136,6 +136,7 @@ pnpm test | [前端页面需求](docs/前端页面需求说明-开发版.md) | 页面、交互、状态与验收基线 | | [前端实现说明](docs/前端壳子与接口层开发说明.md) | 当前前端目录、Service、SSE 和运行边界 | | [前端写作体验](docs/前端写作体验优化开发说明.md) | Milkdown、CodeMirror、格式栏和 Shiki | +| [前端视觉与轻量动效](docs/前端视觉与轻量动效优化开发说明.md) | Design Token、页面美化、性能边界与主题注入约定 | | [Git 使用细则](docs/Git使用细则-团队开发版.md) | 分支、提交、PR、Review 与合并流程 | | [后端审阅复盘](docs/后端全面审阅问题与修复复盘.md) | 后端问题原因、后果与修复方案 | | [Knowledge/Retrieval 复盘](docs/Knowledge与Retrieval-Core问题与修复复盘.md) | 检索与事务问题复盘 | diff --git a/docs/前端壳子与接口层开发说明.md b/docs/前端壳子与接口层开发说明.md index f5ad5a8..16df6c9 100644 --- a/docs/前端壳子与接口层开发说明.md +++ b/docs/前端壳子与接口层开发说明.md @@ -184,13 +184,13 @@ pnpm build ```text pnpm build passed -pnpm test 14 passed +pnpm test 17 passed uv run pytest 71 passed preview smoke HTTP 200 git diff --check passed ``` -当前前端使用 Vitest 执行 Store、Workspace、文件树和编辑器组件测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 71 项测试结果,也不涉及产品代码。 +当前前端使用 Vitest 执行 Store、Workspace、文件树、编辑器组件、智能体标签和轻量动效约束测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 71 项测试结果,也不涉及产品代码。 Vite 当前会提示 Chat 与 Workspace 的部分异步 Chunk 超过 500 kB,这是 Milkdown、CodeMirror、KaTeX 和 Shiki 等编辑/渲染依赖带来的性能优化项,不影响构建成功或功能正确性;进入桌面打包前应通过手动分包或更细粒度动态加载继续优化。 diff --git a/docs/前端视觉与轻量动效优化开发说明.md b/docs/前端视觉与轻量动效优化开发说明.md new file mode 100644 index 0000000..417798e --- /dev/null +++ b/docs/前端视觉与轻量动效优化开发说明.md @@ -0,0 +1,112 @@ +# 前端视觉与轻量动效优化开发说明 + +> 更新日期:2026-08-30 +> 适用范围:全局 Design Token、App Shell、功能页、卡片、表单、弹窗和轻量交互动效 + +## 1. 目标 + +本轮优化不改变页面功能和前后端契约,主要解决原界面层级偏平、组件间距不统一、交互反馈不足的问题,同时为后续主题商店 CSS 注入保留稳定边界。 + +设计原则: + +- 颜色、圆角、阴影、间距和速度继续使用 CSS 变量; +- 页面与弹窗动画只改变 `opacity` 和 `transform`; +- 不使用背景模糊、连续粒子、视差、复杂 SVG 或大范围布局动画; +- 不使用 `transition: all`,只声明需要变化的属性; +- 尊重系统 `prefers-reduced-motion` 设置; +- 主题只需覆盖现有 Token,不需要了解组件内部动画实现。 + +## 2. 全局视觉基线 + +更新 `tokens.css`: + +- 调整四级圆角和阴影,使卡片、弹窗与导航层次更清楚; +- 调整标题栏、状态栏和双侧栏尺寸; +- 统一更短的运动时间与缓动曲线; +- 增加键盘 `focus-visible` 焦点环; +- 为 checkbox、radio 和 range 使用主题强调色; +- 窄窗口下收缩辅助侧栏与展开导航宽度; +- 在减少动态效果模式下,把动画和过渡缩短到近似即时完成。 + +更新 `features.css`: + +- 功能页增加受控内容宽度、响应式留白和低强度主题渐变; +- 卡片增加边框、阴影与最多 2px 的悬浮位移; +- 按钮、输入框、Badge、空状态和通知统一交互反馈; +- 设置页导航改为分段式卡片导航; +- 弹窗与页面增加一次性淡入和轻微位移动画。 + +## 3. 页面与布局优化 + +### 3.1 App Shell + +- 主侧栏增加明确的 Active 标记、悬浮反馈和宽度过渡; +- 辅助侧栏统一为次级 Surface,并改善标题与标签层级; +- 标题栏应用名改为轻量胶囊标识; +- 状态栏强化状态点和窄窗口降级; +- 命令面板增加轻量入场、圆角、阴影和列表反馈。 + +### 3.2 业务页面 + +- Search 结果卡片增加左侧强调线和统一内容宽度; +- Chat 增加消息容器、头像层级、Citation 悬浮反馈和 Composer 顶部阴影; +- Agent Tool 选择卡增加选中状态,Trace 使用轻量时间线; +- Task 完成按钮增加主题化状态反馈; +- Settings 行在悬浮时提供背景提示; +- Workspace 空状态与 Vault 入口增加清晰的层级和一次性入场动画。 + +## 4. 动效性能边界 + +允许的常规动效: + +```text +opacity +transform: translate / scale / rotate +background-color +border-color +color +box-shadow +``` + +默认禁止: + +```text +transition: all +backdrop-filter / filter 模糊 +持续改变 width / height / margin / padding 的动画 +无限循环的装饰动画 +全屏高频渐变或粒子动画 +``` + +状态栏 Spinner 和 AI Core 检查状态点属于有明确状态含义的循环动画,并会被 `prefers-reduced-motion` 全局规则降级。 + +## 5. 主题商店接入约定 + +自定义主题优先覆盖以下 Token: + +- `--color-background-*` +- `--color-surface-*` +- `--color-text-*` +- `--color-accent-*` +- `--color-border-*` +- `--shadow-*` +- `--radius-*` +- `--motion-*` + +主题 CSS 不应给通配选择器增加动画,不应重新启用高成本滤镜,也不应覆盖 `prefers-reduced-motion` 的降级规则。若主题需要完全静态的界面,可把三个 `--motion-*` Token 设置为接近 0ms。 + +## 6. 验证 + +新增 `styles/motion.spec.ts`,防止全局样式重新引入 `transition: all`、高成本模糊滤镜或布局型页面入场动画。 + +当前验证结果: + +```text +pnpm test 7 files / 17 tests passed +pnpm build passed +git diff --check passed +``` + +生产构建仍有已有的大 Chunk 警告,主要来自 Milkdown、CodeMirror、KaTeX 和 Shiki;本轮样式及动效未增加 JavaScript 动画库或运行时依赖。 + +本轮已完成样式静态检查、自动化测试和生产构建。由于本机内置浏览器运行资源路径缺失,亮色/暗色主题的人工页面巡检需在 PR 验收环境补做。 diff --git a/docs/第一阶段测试验证操作手册.md b/docs/第一阶段测试验证操作手册.md index 344add0..a70afdd 100644 --- a/docs/第一阶段测试验证操作手册.md +++ b/docs/第一阶段测试验证操作手册.md @@ -83,11 +83,11 @@ pnpm test 当前基线: ```text -6 test files passed -14 tests passed +7 test files passed +17 tests passed ``` -通过标准:退出码为 0、失败数为 0。测试覆盖 Provider Store、Workspace、文件树、文件切换、可视化编辑器和智能体中文标签。 +通过标准:退出码为 0、失败数为 0。测试覆盖 Provider Store、Workspace、文件树、文件切换、可视化编辑器、智能体中文标签和轻量动效性能约束。 ### 3.3 类型检查与生产构建 @@ -373,4 +373,3 @@ Invoke-RestMethod -Method Delete -Uri "$apiBase/notes/$noteId" - 通过 / 有条件通过 / 不通过 ``` - From 668139706bcf92c6b32ffa3a4ab73cc262e74336 Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sun, 30 Aug 2026 20:23:22 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix(frontend):=20=E6=8F=90=E5=8D=87=20Markd?= =?UTF-8?q?own=20=E8=A1=A8=E6=A0=BC=E4=B8=8E=E5=88=97=E8=A1=A8=E5=AF=B9?= =?UTF-8?q?=E6=AF=94=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/前端壳子与接口层开发说明.md | 4 ++-- docs/前端视觉与轻量动效优化开发说明.md | 4 ++-- docs/第一阶段测试验证操作手册.md | 4 ++-- frontend/src/components/common/MarkdownContent.vue | 4 +++- .../src/features/editor/VisualMarkdownEditor.vue | 11 ++++++----- frontend/src/styles/motion.spec.ts | 12 ++++++++++++ frontend/src/styles/tokens.css | 12 ++++++++++++ 7 files changed, 39 insertions(+), 12 deletions(-) diff --git a/docs/前端壳子与接口层开发说明.md b/docs/前端壳子与接口层开发说明.md index 16df6c9..5168630 100644 --- a/docs/前端壳子与接口层开发说明.md +++ b/docs/前端壳子与接口层开发说明.md @@ -184,13 +184,13 @@ pnpm build ```text pnpm build passed -pnpm test 17 passed +pnpm test 18 passed uv run pytest 71 passed preview smoke HTTP 200 git diff --check passed ``` -当前前端使用 Vitest 执行 Store、Workspace、文件树、编辑器组件、智能体标签和轻量动效约束测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 71 项测试结果,也不涉及产品代码。 +当前前端使用 Vitest 执行 Store、Workspace、文件树、编辑器组件、智能体标签、轻量动效约束和 Markdown 对比度 Token 测试;`pnpm build` 同时执行 `vue-tsc -b` 与 Vite 生产构建。后端测试出现过 `.pytest_cache` 无法写入的 Windows 权限警告,不影响 71 项测试结果,也不涉及产品代码。 Vite 当前会提示 Chat 与 Workspace 的部分异步 Chunk 超过 500 kB,这是 Milkdown、CodeMirror、KaTeX 和 Shiki 等编辑/渲染依赖带来的性能优化项,不影响构建成功或功能正确性;进入桌面打包前应通过手动分包或更细粒度动态加载继续优化。 diff --git a/docs/前端视觉与轻量动效优化开发说明.md b/docs/前端视觉与轻量动效优化开发说明.md index 417798e..6e764d2 100644 --- a/docs/前端视觉与轻量动效优化开发说明.md +++ b/docs/前端视觉与轻量动效优化开发说明.md @@ -97,12 +97,12 @@ backdrop-filter / filter 模糊 ## 6. 验证 -新增 `styles/motion.spec.ts`,防止全局样式重新引入 `transition: all`、高成本模糊滤镜或布局型页面入场动画。 +新增 `styles/motion.spec.ts`,防止全局样式重新引入 `transition: all`、高成本模糊滤镜或布局型页面入场动画,并约束 Markdown 表格与列表使用独立的高对比度主题变量。 当前验证结果: ```text -pnpm test 7 files / 17 tests passed +pnpm test 7 files / 18 tests passed pnpm build passed git diff --check passed ``` diff --git a/docs/第一阶段测试验证操作手册.md b/docs/第一阶段测试验证操作手册.md index a70afdd..029c671 100644 --- a/docs/第一阶段测试验证操作手册.md +++ b/docs/第一阶段测试验证操作手册.md @@ -84,10 +84,10 @@ pnpm test ```text 7 test files passed -17 tests passed +18 tests passed ``` -通过标准:退出码为 0、失败数为 0。测试覆盖 Provider Store、Workspace、文件树、文件切换、可视化编辑器、智能体中文标签和轻量动效性能约束。 +通过标准:退出码为 0、失败数为 0。测试覆盖 Provider Store、Workspace、文件树、文件切换、可视化编辑器、智能体中文标签、轻量动效性能约束和 Markdown 对比度 Token。 ### 3.3 类型检查与生产构建 diff --git a/frontend/src/components/common/MarkdownContent.vue b/frontend/src/components/common/MarkdownContent.vue index b0a225d..3e3f173 100644 --- a/frontend/src/components/common/MarkdownContent.vue +++ b/frontend/src/components/common/MarkdownContent.vue @@ -23,12 +23,14 @@ watch(() => props.source, async (source) => { .markdown-content :deep(h1), .markdown-content :deep(h2), .markdown-content :deep(h3) { margin: 1em 0 .5em; line-height: var(--line-height-tight); } .markdown-content :deep(ul) { padding-left: 1.5em; list-style: disc; } .markdown-content :deep(ol) { padding-left: 1.5em; list-style: decimal; } +.markdown-content :deep(li::marker) { color: var(--color-markdown-marker); font-weight: 700; } .markdown-content :deep(.shiki) { overflow: auto; padding: var(--space-md); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); } .markdown-content :deep(code) { padding: .1em .3em; border-radius: var(--radius-sm); background: var(--color-background-tertiary); font-family: var(--font-ui-mono); } .markdown-content :deep(pre code) { padding: 0; background: transparent; } .markdown-content :deep(blockquote) { padding-left: 1em; border-left: 3px solid var(--color-accent-primary); color: var(--color-text-secondary); } .markdown-content :deep(table) { width: 100%; margin: .65em 0; border-collapse: collapse; } -.markdown-content :deep(th), .markdown-content :deep(td) { padding: .45em .65em; border: 1px solid var(--color-border-default); text-align: left; } +.markdown-content :deep(th), .markdown-content :deep(td) { padding: .45em .65em; border: 1px solid var(--color-markdown-grid); text-align: left; } +.markdown-content :deep(th) { background: var(--color-markdown-table-header); font-weight: 700; } .markdown-content :deep(img) { max-width: 100%; } .markdown-content :deep(hr) { margin: 1em 0; border: 0; border-top: 1px solid var(--color-border-default); } :global([data-theme='dark']) .markdown-content :deep(.shiki), diff --git a/frontend/src/features/editor/VisualMarkdownEditor.vue b/frontend/src/features/editor/VisualMarkdownEditor.vue index c3d323f..a339f4a 100644 --- a/frontend/src/features/editor/VisualMarkdownEditor.vue +++ b/frontend/src/features/editor/VisualMarkdownEditor.vue @@ -250,7 +250,7 @@ defineExpose({ getEditor: () => crepe?.editor }) --crepe-color-surface-low: var(--color-background-secondary); --crepe-color-on-surface: var(--color-text-primary); --crepe-color-on-surface-variant: var(--color-text-secondary); - --crepe-color-outline: var(--color-border-default); + --crepe-color-outline: var(--color-markdown-grid); --crepe-color-primary: var(--color-accent-primary); --crepe-color-secondary: var(--color-accent-soft); --crepe-color-on-secondary: var(--color-text-primary); @@ -272,10 +272,11 @@ defineExpose({ getEditor: () => crepe?.editor }) :global(.milkdown-toolbar) { border: 1px solid var(--color-border-default) !important; background: var(--color-surface-elevated) !important; box-shadow: var(--shadow-md) !important; } :global(.milkdown-toolbar .toolbar-item svg), :global(.milkdown-toolbar .toolbar-item.active svg) { color: var(--color-text-primary) !important; fill: var(--color-text-primary) !important; opacity: 1 !important; } :global(.milkdown-toolbar .toolbar-item:hover svg), :global(.milkdown-toolbar .toolbar-item.active svg) { color: var(--color-accent-primary) !important; fill: var(--color-accent-primary) !important; } -:global([data-theme='light']) .milkdown-host :deep(.milkdown-table-block th), -:global([data-theme='light']) .milkdown-host :deep(.milkdown-table-block td) { border-color: var(--color-text-tertiary); } -:global([data-theme='light']) .milkdown-host :deep(.milkdown-list-item-block li .label-wrapper) { color: var(--color-text-secondary); font-weight: 600; } -:global([data-theme='light']) .milkdown-host :deep(.milkdown-list-item-block li .label-wrapper svg) { fill: var(--color-text-secondary); } +.milkdown-host :deep(.milkdown-table-block th), +.milkdown-host :deep(.milkdown-table-block td) { border-color: var(--color-markdown-grid); } +.milkdown-host :deep(.milkdown-table-block th) { background: var(--color-markdown-table-header); font-weight: 700; } +.milkdown-host :deep(.milkdown-list-item-block li .label-wrapper) { color: var(--color-markdown-marker); font-weight: 700; } +.milkdown-host :deep(.milkdown-list-item-block li .label-wrapper svg) { fill: var(--color-markdown-marker); } .milkdown-host :deep(code) { font-family: var(--font-editor-mono); } :global([data-theme='dark']) .milkdown-host :deep(.milkdown) { color-scheme: dark; } @media (max-width: 680px) { .toolbar-select select { min-width: 46px; width: 46px; } } diff --git a/frontend/src/styles/motion.spec.ts b/frontend/src/styles/motion.spec.ts index 083a9d4..59f8e82 100644 --- a/frontend/src/styles/motion.spec.ts +++ b/frontend/src/styles/motion.spec.ts @@ -8,6 +8,11 @@ const globalStyles = [ new URL('./features.css', import.meta.url), ].map((path) => readFileSync(path, 'utf8')).join('\n') +const markdownStyles = [ + new URL('../features/editor/VisualMarkdownEditor.vue', import.meta.url), + new URL('../components/common/MarkdownContent.vue', import.meta.url), +].map((path) => readFileSync(path, 'utf8')).join('\n') + describe('轻量动效基线', () => { it('为减少动态效果偏好提供全局回退', () => { expect(globalStyles).toContain('@media (prefers-reduced-motion: reduce)') @@ -24,4 +29,11 @@ describe('轻量动效基线', () => { expect(pageAnimation).toContain('transform') expect(pageAnimation).not.toMatch(/(?:width|height|margin|padding|top|left)\s*:/) }) + + it('Markdown 序号和表格使用独立的高对比度主题变量', () => { + expect(globalStyles).toContain('--color-markdown-grid:') + expect(globalStyles).toContain('--color-markdown-marker:') + expect(markdownStyles).toContain('var(--color-markdown-grid)') + expect(markdownStyles).toContain('var(--color-markdown-marker)') + }) }) diff --git a/frontend/src/styles/tokens.css b/frontend/src/styles/tokens.css index 5703776..a13cbfc 100644 --- a/frontend/src/styles/tokens.css +++ b/frontend/src/styles/tokens.css @@ -44,6 +44,11 @@ --color-border-focus: #5b67f1; --color-border-disabled: #eef0f3; + /* Markdown */ + --color-markdown-grid: #8b949e; + --color-markdown-marker: #343b44; + --color-markdown-table-header: #eef0f3; + /* Shadow */ --shadow-sm: 0 1px 2px rgba(31, 35, 40, 0.05), 0 1px 5px rgba(31, 35, 40, 0.03); --shadow-md: 0 8px 22px rgba(31, 35, 40, 0.08), 0 2px 6px rgba(31, 35, 40, 0.04); @@ -152,6 +157,10 @@ --color-border-focus: #7d8bff; --color-border-disabled: #21262d; + --color-markdown-grid: #6e7681; + --color-markdown-marker: #c9d1d9; + --color-markdown-table-header: #21262d; + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); --shadow-lg: 0 8px 24px rgba(0, 0, 0, 0.5); @@ -172,6 +181,9 @@ --color-text-tertiary: #9a8a72; --color-border-default: #ddcfad; --color-border-subtle: #eadfc4; + --color-markdown-grid: #9c8353; + --color-markdown-marker: #554735; + --color-markdown-table-header: #eadbb8; --color-accent-primary: #8a5b32; --color-accent-primary-hover: #704724; --color-accent-soft: #edddbd; From 7803ae51105a2af391d278f8b8d048bbee337c0a Mon Sep 17 00:00:00 2001 From: KiriAky 107 Date: Sun, 30 Aug 2026 20:25:51 +0800 Subject: [PATCH 4/8] =?UTF-8?q?chore(frontend):=20=E5=B0=86=E6=9A=82?= =?UTF-8?q?=E5=AE=9A=E5=93=81=E7=89=8C=E5=90=8D=E7=BB=9F=E4=B8=80=E4=B8=BA?= =?UTF-8?q?=20NotesAgent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/index.html | 2 +- frontend/src/components/common/TitleBar.vue | 4 ++-- frontend/src/features/editor/EditorPane.spec.ts | 4 ++-- frontend/src/features/vault/VaultEntry.vue | 2 +- frontend/src/features/workspace/WorkspaceView.vue | 6 +++--- frontend/src/router/index.ts | 2 +- frontend/src/services/pluginService.ts | 4 ++-- frontend/src/services/skillService.ts | 4 ++-- frontend/src/services/workspaceService.ts | 8 ++++---- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index baa01e3..a43eb97 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,7 +4,7 @@ - Notes Agent + NotesAgent
diff --git a/frontend/src/components/common/TitleBar.vue b/frontend/src/components/common/TitleBar.vue index e004ecf..8aa31cf 100644 --- a/frontend/src/components/common/TitleBar.vue +++ b/frontend/src/components/common/TitleBar.vue @@ -25,7 +25,7 @@ const pageTitle = computed(() => { themes: '主题管理', settings: '设置', } - return titles[name] || '知笔知己' + return titles[name] || 'NotesAgent' }) const currentFileName = computed(() => { @@ -50,7 +50,7 @@ const isDirty = computed(() => editorStore.saveStatus === 'dirty' || editorStore
- 知笔知己 + NotesAgent