From 09ee0adc1eb8e2891d40232dd837028330f31fc1 Mon Sep 17 00:00:00 2001 From: ailuckly Date: Tue, 21 Apr 2026 09:42:18 +0800 Subject: [PATCH 01/14] feat(ui): P3 foundation - dual theme system + new logo + shell upgrade MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - theme.css: full dual-mode token system (light/dark) using oklch, new brand color (purple-blue, GPT-style), added surface-raised, surface-overlay, line-subtle, text-muted, danger, success tokens, shadow-sm/md/lg scale, radius-sm/md/lg/xl scale - logo.svg: new SVG icon (mic + soundwave arcs + gradient) - useTheme.ts: composable for dark/light toggle with localStorage persistence and prefers-color-scheme detection - main.ts: init theme before app mount to prevent flash - AppSidebar: new logo with gradient wordmark, icon+text nav items, theme toggle button in footer, width 288→260px - BasicLayout: grid column 288→260px, clean bg token --- vocata-web/src/assets/logo.svg | 19 ++ vocata-web/src/assets/styles/theme.css | 79 ++++++-- .../src/components/shell/AppSidebar.vue | 173 +++++++++++++----- vocata-web/src/composables/useTheme.ts | 24 +++ vocata-web/src/layouts/BasicLayout.vue | 6 +- vocata-web/src/main.ts | 5 + 6 files changed, 248 insertions(+), 58 deletions(-) create mode 100644 vocata-web/src/assets/logo.svg create mode 100644 vocata-web/src/composables/useTheme.ts diff --git a/vocata-web/src/assets/logo.svg b/vocata-web/src/assets/logo.svg new file mode 100644 index 0000000..d1dcdb7 --- /dev/null +++ b/vocata-web/src/assets/logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/vocata-web/src/assets/styles/theme.css b/vocata-web/src/assets/styles/theme.css index 008d387..92fc681 100644 --- a/vocata-web/src/assets/styles/theme.css +++ b/vocata-web/src/assets/styles/theme.css @@ -1,27 +1,84 @@ :root { color-scheme: light; - --vt-bg: oklch(98% 0.01 190); + + /* Backgrounds */ + --vt-bg: oklch(98% 0.005 240); --vt-surface: oklch(100% 0 0); - --vt-surface-muted: oklch(97% 0.01 190); - --vt-line: oklch(90% 0.02 190); - --vt-text: oklch(28% 0.03 210); - --vt-text-soft: oklch(48% 0.03 210); - --vt-brand: oklch(74% 0.09 183); - --vt-brand-strong: oklch(64% 0.11 183); - --vt-accent: oklch(74% 0.14 32); - --vt-radius-xl: 28px; - --vt-radius-lg: 20px; - --vt-shadow: 0 24px 60px color-mix(in srgb, var(--vt-brand) 12%, transparent); + --vt-surface-raised: oklch(99% 0.005 240); + --vt-surface-overlay: oklch(96% 0.008 240); + + /* Borders */ + --vt-line: oklch(91% 0.01 240); + --vt-line-subtle: oklch(95% 0.008 240); + + /* Text */ + --vt-text: oklch(15% 0.02 240); + --vt-text-soft: oklch(45% 0.02 240); + --vt-text-muted: oklch(65% 0.015 240); + + /* Brand (紫蓝,类 GPT) */ + --vt-brand: oklch(55% 0.18 270); + --vt-brand-soft: oklch(92% 0.05 270); + --vt-brand-strong: oklch(45% 0.20 270); + + /* Accent & Semantic */ + --vt-accent: oklch(68% 0.16 32); + --vt-danger: oklch(60% 0.20 25); + --vt-success: oklch(60% 0.16 145); + + /* Radius */ + --vt-radius-sm: 8px; + --vt-radius-md: 12px; + --vt-radius-lg: 16px; + --vt-radius-xl: 24px; + + /* Shadows */ + --vt-shadow-sm: 0 1px 3px oklch(0% 0 0 / 0.08); + --vt-shadow-md: 0 4px 16px oklch(0% 0 0 / 0.10); + --vt-shadow-lg: 0 12px 40px oklch(0% 0 0 / 0.12); + + /* Legacy aliases (backward compat) */ + --vt-surface-muted: var(--vt-surface-overlay); + --vt-brand-strong-legacy: var(--vt-brand-strong); + --vt-shadow: var(--vt-shadow-lg); + --vt-radius: var(--vt-radius-xl); + + /* Typography */ --vt-font-body: 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Noto Sans CJK SC', sans-serif; + font-synthesis: none; text-rendering: optimizeLegibility; } +[data-theme="dark"] { + color-scheme: dark; + + --vt-bg: oklch(12% 0.01 240); + --vt-surface: oklch(16% 0.01 240); + --vt-surface-raised: oklch(20% 0.01 240); + --vt-surface-overlay: oklch(22% 0.015 240); + + --vt-line: oklch(28% 0.01 240); + --vt-line-subtle: oklch(24% 0.01 240); + + --vt-text: oklch(95% 0.005 240); + --vt-text-soft: oklch(70% 0.01 240); + --vt-text-muted: oklch(50% 0.01 240); + + --vt-brand: oklch(65% 0.18 270); + --vt-brand-soft: oklch(25% 0.08 270); + --vt-brand-strong: oklch(75% 0.20 270); + + --vt-shadow-sm: 0 1px 3px oklch(0% 0 0 / 0.3); + --vt-shadow-md: 0 4px 16px oklch(0% 0 0 / 0.4); + --vt-shadow-lg: 0 12px 40px oklch(0% 0 0 / 0.5); +} + * { box-sizing: border-box; } diff --git a/vocata-web/src/components/shell/AppSidebar.vue b/vocata-web/src/components/shell/AppSidebar.vue index b538a59..9e3c845 100644 --- a/vocata-web/src/components/shell/AppSidebar.vue +++ b/vocata-web/src/components/shell/AppSidebar.vue @@ -1,25 +1,36 @@ @@ -36,6 +55,7 @@ import { chatHistoryStore } from '@/store' import { isMobile } from '@/utils/isMobile' import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' +import { useTheme } from '@/composables/useTheme' import AppSidebarHistory from './AppSidebarHistory.vue' import AppSidebarProfile from './AppSidebarProfile.vue' @@ -51,9 +71,10 @@ const router = useRouter() const route = useRoute() const isMobileDevice = isMobile() const historyStore = chatHistoryStore() +const { isDark, toggle: toggleTheme } = useTheme() const userInfo = ref({ - nickname: '语Ta 用户', + nickname: 'VocaTa 用户', avatar: '', }) @@ -75,10 +96,9 @@ const loadSidebarData = async () => { userApi.getUserInfo(), historyStore.getChatHistory(), ]) - if (userRes.code === 200 && userRes.data) { userInfo.value = { - nickname: userRes.data.nickname || '语Ta 用户', + nickname: userRes.data.nickname || 'VocaTa 用户', avatar: userRes.data.avatar || '', } } @@ -94,71 +114,136 @@ onMounted(() => { diff --git a/vocata-web/src/components/chat/ChatMessageList.vue b/vocata-web/src/components/chat/ChatMessageList.vue index cba1786..904eea8 100644 --- a/vocata-web/src/components/chat/ChatMessageList.vue +++ b/vocata-web/src/components/chat/ChatMessageList.vue @@ -6,27 +6,23 @@ class="chat-message-list__item" :class="item.type === 'send' ? 'is-user' : 'is-ai'" > -
- - - {{ item.type === 'send' ? userInitial : characterInitial }} +
+ + {{ characterInitial }}
-
+

{{ item.content }}

+ {{ formatTime(item.createDate) }}
-
+
{{ characterInitial }}
-
+
@@ -51,109 +47,113 @@ defineProps<{ diff --git a/vocata-web/src/components/chat/ChatStageHeader.vue b/vocata-web/src/components/chat/ChatStageHeader.vue index e0b1c09..7cfc102 100644 --- a/vocata-web/src/components/chat/ChatStageHeader.vue +++ b/vocata-web/src/components/chat/ChatStageHeader.vue @@ -6,11 +6,12 @@ {{ initials }}
-

{{ eyebrow }}

+

{{ eyebrow }}

{{ characterName }}

+ {{ status }}
@@ -36,30 +37,33 @@ const initials = computed(() => props.characterName.slice(0, 2).toUpperCase()) align-items: center; justify-content: space-between; gap: 16px; - padding: 4px 0 8px; - border-bottom: 1px solid color-mix(in srgb, var(--vt-line) 72%, white); + padding: 12px 0; + border-bottom: 1px solid var(--vt-line); max-width: 760px; margin: 0 auto; + width: 100%; } .chat-stage-header__identity { display: flex; align-items: center; - gap: 12px; + gap: 10px; min-width: 0; } .chat-stage-header__avatar { display: grid; - width: 40px; - height: 40px; + width: 38px; + height: 38px; + flex-shrink: 0; place-items: center; border-radius: 50%; overflow: hidden; - background: color-mix(in srgb, var(--vt-brand) 18%, white); + background: var(--vt-brand-soft); color: var(--vt-brand-strong); font-size: 13px; font-weight: 700; + border: 1.5px solid var(--vt-line); } .chat-stage-header__avatar img { @@ -78,29 +82,49 @@ const initials = computed(() => props.characterName.slice(0, 2).toUpperCase()) } .chat-stage-header__copy p { - color: var(--vt-text-soft); + color: var(--vt-text-muted); font-size: 11px; text-transform: uppercase; - letter-spacing: 0.08em; + letter-spacing: 0.06em; } .chat-stage-header__copy h1 { font-size: 15px; - line-height: 1.2; - font-weight: 700; + font-weight: 600; + line-height: 1.3; + color: var(--vt-text); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .chat-stage-header__status { - padding: 6px 10px; + display: flex; + align-items: center; + gap: 6px; + padding: 5px 10px; border-radius: 999px; - background: color-mix(in srgb, var(--vt-surface) 88%, var(--vt-brand) 8%); - color: var(--vt-text-soft); - font-size: 11px; + background: var(--vt-surface-overlay); + color: var(--vt-text-muted); + font-size: 12px; white-space: nowrap; + flex-shrink: 0; +} + +.chat-stage-header__dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--vt-text-muted); + flex-shrink: 0; } .chat-stage-header__status.is-connected { - background: color-mix(in srgb, var(--vt-brand) 14%, white); + background: var(--vt-brand-soft); color: var(--vt-brand-strong); + + .chat-stage-header__dot { + background: var(--vt-brand); + } } diff --git a/vocata-web/src/components/chat/VoiceCallPanel.vue b/vocata-web/src/components/chat/VoiceCallPanel.vue index 1ea670e..131aa84 100644 --- a/vocata-web/src/components/chat/VoiceCallPanel.vue +++ b/vocata-web/src/components/chat/VoiceCallPanel.vue @@ -1,20 +1,46 @@ @@ -116,42 +116,37 @@ onMounted(() => { .app-sidebar { display: flex; flex-direction: column; - gap: 8px; - width: 260px; - min-width: 260px; - max-width: 260px; - flex: none; + width: 280px; + min-width: 280px; height: 100vh; - padding: 20px 12px; background: var(--vt-surface); border-right: 1px solid var(--vt-line); - overflow-y: auto; - overflow-x: hidden; + overflow: hidden; } -/* Brand */ -.app-sidebar__brand { +/* Top */ +.app-sidebar__top { display: flex; align-items: center; justify-content: space-between; - padding: 4px 8px 12px; + padding: 16px 16px 8px; + flex-shrink: 0; } -.app-sidebar__brand-link { +.app-sidebar__logo { display: flex; align-items: center; - gap: 10px; + gap: 8px; text-decoration: none; } .app-sidebar__logo-icon { - width: 28px; - height: 28px; - flex-shrink: 0; + width: 26px; + height: 26px; } .app-sidebar__logo-text { - font-size: 17px; + font-size: 16px; font-weight: 700; letter-spacing: -0.3px; background: linear-gradient(135deg, var(--vt-brand) 0%, oklch(65% 0.18 200) 100%); @@ -160,22 +155,19 @@ onMounted(() => { background-clip: text; } -.app-sidebar__toggle { - display: flex; - align-items: center; - justify-content: center; +.app-sidebar__new-btn { + display: grid; + place-items: center; width: 32px; height: 32px; - border: 0; border-radius: var(--vt-radius-sm); - background: transparent; - color: var(--vt-text-soft); - cursor: pointer; + background: var(--vt-brand-soft); + color: var(--vt-brand-strong); + font-size: 16px; + text-decoration: none; transition: background 0.15s; - &:hover { - background: var(--vt-surface-overlay); - } + &:hover { background: var(--vt-line); } } /* Nav */ @@ -183,82 +175,221 @@ onMounted(() => { display: flex; flex-direction: column; gap: 2px; - padding: 4px 0; + padding: 4px 8px; + flex-shrink: 0; } .app-sidebar__nav-item { display: flex; align-items: center; gap: 10px; - padding: 9px 12px; - border-radius: var(--vt-radius-md); + padding: 8px 10px; + border-radius: var(--vt-radius-sm); color: var(--vt-text-soft); font-size: 14px; font-weight: 500; text-decoration: none; - transition: background 0.15s, color 0.15s; + transition: background 0.12s, color 0.12s; + + .el-icon { font-size: 16px; flex-shrink: 0; } + + &:hover { background: var(--vt-surface-overlay); color: var(--vt-text); } + &.router-link-active { background: var(--vt-brand-soft); color: var(--vt-brand-strong); } +} + +/* Divider */ +.app-sidebar__divider { + height: 1px; + background: var(--vt-line-subtle); + margin: 4px 16px; + flex-shrink: 0; +} - &:hover { - background: var(--vt-surface-overlay); - color: var(--vt-text); +/* History */ +.app-sidebar__history-header { + padding: 8px 18px 4px; + flex-shrink: 0; + + span { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--vt-text-muted); } +} + +.app-sidebar__history { + flex: 1; + overflow-y: auto; + padding: 2px 8px; + display: flex; + flex-direction: column; + gap: 1px; - &.router-link-active { + &::-webkit-scrollbar { width: 4px; } + &::-webkit-scrollbar-thumb { background: var(--vt-line); border-radius: 2px; } +} + +.app-sidebar__history-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 7px 10px; + border: 0; + border-radius: var(--vt-radius-sm); + background: transparent; + cursor: pointer; + text-align: left; + transition: background 0.12s; + min-width: 0; + + &:hover { background: var(--vt-surface-overlay); } + + &.is-active { background: var(--vt-brand-soft); - color: var(--vt-brand-strong); + + strong { color: var(--vt-brand-strong); } } } -.app-sidebar__nav-icon { - font-size: 16px; +.app-sidebar__history-avatar { + display: grid; + width: 30px; + height: 30px; flex-shrink: 0; + place-items: center; + border-radius: 8px; + overflow: hidden; + background: var(--vt-surface-overlay); + color: var(--vt-text-soft); + font-size: 12px; + font-weight: 700; + + img { width: 100%; height: 100%; object-fit: cover; } +} + +.app-sidebar__history-body { + display: flex; + flex-direction: column; + gap: 1px; + min-width: 0; + + strong, span { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + strong { font-size: 13px; font-weight: 500; color: var(--vt-text); } + span { font-size: 11px; color: var(--vt-text-muted); } +} + +.app-sidebar__history-empty { + margin: 0; + padding: 12px 10px; + font-size: 12px; + color: var(--vt-text-muted); } /* Footer */ .app-sidebar__footer { - margin-top: auto; - padding-top: 8px; + display: flex; + align-items: center; + gap: 8px; + padding: 10px 12px; border-top: 1px solid var(--vt-line-subtle); + flex-shrink: 0; } -.app-sidebar__theme-btn { +.app-sidebar__user { display: flex; align-items: center; - gap: 10px; - width: 100%; - padding: 9px 12px; + gap: 8px; + flex: 1; + min-width: 0; + padding: 6px 8px; + border-radius: var(--vt-radius-sm); + text-decoration: none; + transition: background 0.12s; + + &:hover { background: var(--vt-surface-overlay); } +} + +.app-sidebar__user-avatar { + display: grid; + width: 28px; + height: 28px; + flex-shrink: 0; + place-items: center; + border-radius: 50%; + overflow: hidden; + background: var(--vt-brand-soft); + color: var(--vt-brand-strong); + font-size: 12px; + font-weight: 700; + + img { width: 100%; height: 100%; object-fit: cover; } +} + +.app-sidebar__user-name { + font-size: 13px; + font-weight: 500; + color: var(--vt-text); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app-sidebar__theme-btn { + display: grid; + place-items: center; + width: 32px; + height: 32px; + flex-shrink: 0; border: 0; - border-radius: var(--vt-radius-md); + border-radius: var(--vt-radius-sm); background: transparent; - color: var(--vt-text-soft); - font-size: 14px; - font-weight: 500; + color: var(--vt-text-muted); + font-size: 16px; cursor: pointer; - transition: background 0.15s, color 0.15s; + transition: background 0.12s, color 0.12s; - &:hover { - background: var(--vt-surface-overlay); - color: var(--vt-text); - } + &:hover { background: var(--vt-surface-overlay); color: var(--vt-text); } +} - .el-icon { - font-size: 16px; - } +/* Mobile close */ +.app-sidebar__close { + position: absolute; + top: 14px; + right: 14px; + display: grid; + place-items: center; + width: 32px; + height: 32px; + border: 0; + border-radius: var(--vt-radius-sm); + background: var(--vt-surface-overlay); + color: var(--vt-text-soft); + cursor: pointer; } +/* Mobile */ @media (max-width: 768px) { .app-sidebar { position: fixed; inset: 0 auto 0 0; - z-index: 30; - width: min(84vw, 280px); - min-width: min(84vw, 280px); - max-width: min(84vw, 280px); + z-index: 40; + width: min(82vw, 280px); + min-width: unset; box-shadow: var(--vt-shadow-lg); + transition: transform 0.25s ease; } .app-sidebar.is-hidden { - display: none; + transform: translateX(-100%); } } diff --git a/vocata-web/src/layouts/BasicLayout.vue b/vocata-web/src/layouts/BasicLayout.vue index 59f99df..5c7d3e1 100644 --- a/vocata-web/src/layouts/BasicLayout.vue +++ b/vocata-web/src/layouts/BasicLayout.vue @@ -1,13 +1,26 @@