Feat/UI redesign p3#21
Conversation
- 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
…anel ChatStageHeader: status dot indicator, brand-soft avatar bg, clean tokens ChatMessageList: token-based bubbles (surface-raised/brand-soft), streaming cursor animation, no hardcoded colors, AI avatar only on AI messages ChatComposer: focus ring with brand color, el-icon buttons, textarea auto-resize, primary btn activates on text input VoiceCallPanel: slide-in transition, pulse animation on speaking, mute/hangup icon buttons, transcript strip, all CSS tokens
AppSidebar: - Width 260→280px - Top: logo + quick-create (+) button - Main nav: 3 items with icon+text, active state - Divider - History section: compact list (avatar 30px, name+preview, no card bg) scrollable, 12 items max, active item uses brand-soft bg - Footer: user avatar+name (→ profile) + theme toggle icon button - Mobile: slide-in with transform (not display:none), close button BasicLayout: - Grid column 260→280px - Mobile: overlay backdrop on sidebar open, topbar with hamburger + logo - view padding moved into .app-layout__view (not __main) - chat view: overflow:hidden + padding:0
AppSidebar: - Remove "我的空间" from main nav - Footer user button opens popup menu (个人资料 / 设置 / 退出登录) - Chevron rotates when menu open, slide-up transition ProfilePage: full rewrite - hero (avatar+name+email), stats row, recent conversations list, favorite roles list, all token-based SettingsPage: new page - theme toggle (light/dark segmented control), logout button, accessible at /settings routes.ts: add /settings route
New utils/avatar.ts: generates colorful letter-based SVG avatars locally (no network, no external service). Uses name-based color palette (10 colors, oklch-inspired), 2-letter initials, rounded rect. Added @error="onAvatarError($event, name)" to all img tags: - AppSidebar: history items + user avatar - ChatStageHeader: character avatar - ChatMessageList: character avatar (both instances) - ProfilePage: user + conversation + role avatars - RoleShelf: character card avatars - RoleDialog: character detail avatar Fixes local dev SSL cert errors from Qiniu CDN (t333x1e4b.hb-bkt.clouddn.com)
Layout: - Left hero: single featured role, full-bleed image, gradient overlay, role name + desc + CTA button - Right featured grid: 5 cards in 2-row grid (2+3 layout) - Tag filter bar: horizontal scroll pills with emoji, backend API filter - Role grid: auto-fill minmax(160px), 2:3 aspect ratio cards, gradient overlay with name + chat count - Load more button (replaces pagination) Design: - All cards use gradient overlay (dark bottom) for text legibility in both light and dark themes - hover: translateY(-2px) + shadow-md on grid cards - hover: scale(1.03) on featured cards - Tag pills: brand color when active, surface-overlay on hover - Responsive: 1024px (4 featured), 768px (mobile stack)
Hero Banner (420px): - Left: eyebrow + large title + desc + CTA button + chat count - Right: character image with left-to-right gradient overlay - Smooth fade transition when switching hero via carousel Carousel: - 6 featured roles, horizontal scroll, 72x72px rounded thumbs - Click to switch Hero image + copy - Active state: brand border + glow ring + bold name Tabs: - Horizontal pill tabs (推荐/女/男/奇幻/游戏/动漫/影视/历史/科幻) - Active: brand background + white text - Backend API filter via tags param Card Grid: - auto-fill minmax(170px), 2:3 aspect ratio - Full-bleed image, dark gradient overlay bottom - Name + chat count overlaid on image - hover: translateY(-3px) + shadow-md - Short desc below card Dark mode: #0b1020 page bg, #111827 hero bg Responsive: 900px (stack hero), 640px (smaller cards)
…rchy Structural changes only (no color/theme changes): 1. Page header: added title + subtitle above Hero 2. Hero Banner restructured: - Left/right split: copy area (badge+name+desc+CTA) | character image - Character switcher moved INSIDE Banner (was external carousel) - Switcher: 48px thumbnails + left/right arrow buttons + active ring - heroIndex drives both switcher highlight and Hero content 3. Unified content width: max-width 960px, all modules left-aligned 4. Category tabs: independent module below Hero, pill style with border 5. Content grid: same card design, consistent gap/alignment 6. Removed standalone carousel section 7. Mobile: Hero stacks vertically (image on top), switcher stays inside Layout flow: Header → Hero[Main + Switcher] → Tabs → Grid → Load More
…s grid Key fixes: - hero__main: min-height→height 420px (was too short, looked like card) - hero__visual: removed position:absolute on img, let grid child fill naturally (was causing empty right side) - hero__name: 32→36px, hero__desc: 14→15px, hero__cta: 10→12px padding - hero__copy padding: 36→40px - discovery gap: 24→32px (more breathing room between sections) - category-tabs: added padding-top 8px for separation from hero - hero__visual-fade gradient: adjusted to 50% at 25% for cleaner blend - Mobile: hero height auto, visual 240px, name 26px
Template restructure:
- Page title: "✨ 欢迎来到 VocaTa" (simplified, removed subtitle)
- Hero Banner: added blurred background layer (hero__bg-wrap) with
full-bleed image + overlay for depth/atmosphere
- Hero right side: changed from plain image to card-style visual
(hero__visual-card) with rounded corners, overlay gradient, and
character name/tagline overlaid on the image
- Character switcher: moved from Banner bottom bar into left copy
area (hero__switcher-inline), making it part of the content flow
rather than a separate footer section
- Section header: added "✨ 角色聊天" title above category tabs,
wrapped in discovery__section for grouping
- Content cards: moved desc text inside the gradient overlay (on top
of image) instead of below the card, making cards more compact
Script changes:
- Added slideDirection ref for left/right slide transition control
- switchHero: changed from clamped to wrapping (infinite carousel)
- Added selectHero(i) for direct index selection with direction calc
- Added watch on heroIndex to auto-scroll active thumbnail into view
using scrollIntoView({ behavior: 'smooth', inline: 'center' })
- Added nextTick import for scroll timing
- Tab labels: added emoji prefixes (🔮 奇幻, 🎮 游戏, etc.)
Style overhaul:
- Hero: position relative container with blurred bg layer (filter:
blur(60px) saturate(1.6) scale(1.2)), semi-transparent overlay
- Hero main: grid 1fr 1fr (equal split), min-height 400px, z-index
layering for bg/content separation
- Hero visual card: 280px wide, aspect-ratio 3/4, border-radius 20px,
box-shadow, bottom gradient overlay with name/tagline
- Switcher inline: flex row with 44px round thumbnails, active state
with brand border + glow ring, smooth scroll track
- Slide transitions: slide-left and slide-right with translateX
animations for hero image switching
- Hero bg transition: opacity fade for background blur layer
- Section header: 20px title with bottom margin
- Category tabs: gap increased, font-size 14px
- Content grid: minmax(180px, 1fr), gap 20px
- Content cards: border-radius 16px, gradient overlay covers bottom
40%, desc text inside overlay (white, 12px, 2-line clamp)
- Load more button: updated to match new spacing
- Responsive (768px): hero stacks vertically, visual card 200px
height, grid 2-column min 140px
- Removed old hero__visual, hero__visual-fade, hero__switcher
(bottom bar), hero-img transition, standalone card desc
There was a problem hiding this comment.
Pull request overview
This PR continues the “UI redesign” work by refreshing the main shell (layout + sidebar), discovery/search, login/profile pages, and chat UI components, while also adding dark/light theme support, a settings page, and a shared avatar fallback utility.
Changes:
- Add theme system (CSS variables +
useThemecomposable) and a new Settings page with theme toggle + logout. - Redesign discovery (
SearchRole), profile, login, sidebar/layout, and several chat components’ UI. - Introduce a reusable avatar fallback (
onAvatarError) and apply it across multiple avatar<img>usages.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| vocata-web/src/views/components/RoleDialog.vue | Add avatar error fallback handler. |
| vocata-web/src/views/SettingsPage.vue | New settings page for theme + logout. |
| vocata-web/src/views/SearchRole.vue | Major discovery page redesign (hero, tabs, grid, load-more). |
| vocata-web/src/views/ProfilePage.vue | Profile page layout redesign and data shaping changes. |
| vocata-web/src/views/LoginPage.vue | Login/register UI redesign. |
| vocata-web/src/utils/avatar.ts | New SVG avatar generator + onAvatarError. |
| vocata-web/src/router/routes.ts | Add /settings route and update profile title. |
| vocata-web/src/main.ts | Initialize theme before app mount to reduce flicker. |
| vocata-web/src/layouts/BasicLayout.vue | Layout styling + mobile overlay/topbar adjustments. |
| vocata-web/src/components/shell/AppSidebar.vue | Sidebar redesign with history list and user menu actions. |
| vocata-web/src/components/profile/ProfileRecentConversations.vue | Styling adjustments to match new theme tokens. |
| vocata-web/src/components/profile/ProfileOverview.vue | Styling adjustments to match new theme tokens. |
| vocata-web/src/components/profile/ProfileFavoriteRoles.vue | Styling adjustments to match new theme tokens. |
| vocata-web/src/components/discovery/RoleShelf.vue | Add avatar fallback handler in role cards. |
| vocata-web/src/components/chat/VoiceCallPanel.vue | Voice panel UI redesign with transitions and icon buttons. |
| vocata-web/src/components/chat/ChatStageHeader.vue | Header polish + avatar fallback + status dot. |
| vocata-web/src/components/chat/ChatMessageList.vue | Message list UI changes + streaming cursor indicator. |
| vocata-web/src/components/chat/ChatComposer.vue | Composer UI polish and icon-based actions. |
| vocata-web/src/assets/styles/theme.css | Expanded theme tokens + dark theme variables. |
| vocata-web/src/assets/logo.svg | New logo asset. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -1,13 +1,26 @@ | |||
| <template> | |||
| <div class="app-layout" :class="{ 'is-mobile': isMobileDevice, 'is-chat-route': isChatRoute }"> | |||
There was a problem hiding this comment.
isChatRoute is derived from route.path in the script, but in BasicLayout the route changes while the layout component stays mounted. Ensure the value used by this class binding is reactive (e.g., a computed based on useRoute()), otherwise the layout won’t update when navigating into/out of /chat/....
| <p class="brand-copy__eyebrow">Bright companion space</p> | ||
| <h1>把每一次对话留在更舒服的空间里</h1> | ||
| <span>登录后继续你最近的聊天,或者创建一个新的陪伴角色。</span> | ||
| <div class="login-page" :class="isMobileDevice ? 'is-mobile' : 'is-desktop'"> |
There was a problem hiding this comment.
The template no longer uses layoutClass, but it is still declared in the script section. This will fail no-unused-vars linting; remove layoutClass (and any now-unused related code).
| <div class="login-page" :class="isMobileDevice ? 'is-mobile' : 'is-desktop'"> | |
| <div class="login-page" :class="layoutClass"> |
| <div class="discovery"> | ||
| <!-- ── 页面标题 ──────────────────────────────────────── --> | ||
| <div class="discovery__header"> | ||
| <h1 class="discovery__title">✨ 欢迎来到 VocaTa</h1> | ||
| </div> |
There was a problem hiding this comment.
The redesign removed the data-test hooks that existing unit tests assert on (e.g. discovery-hero, role-filter-bar, role-shelf, role-card in src/tests/discovery-page.spec.ts). Restore equivalent data-test attributes in the new markup or update the tests, otherwise CI will fail.
| const getRoleList = async (reset = false) => { | ||
| if (loading.value) return | ||
| loading.value = true | ||
| try { | ||
| const res = await roleApi.getPublicRoleList({ |
There was a problem hiding this comment.
getRoleList has no catch: if roleApi.getPublicRoleList throws, the UI silently fails and the user can’t tell why the list didn’t update. Add error handling (e.g., ElMessage.error) and consider how to keep pagination state consistent on failures.
| const loadMore = async () => { | ||
| pageNum.value++ | ||
| await getRoleList(false) |
There was a problem hiding this comment.
loadMore increments pageNum before fetching; if the request fails, the next retry will skip a page. Increment only after a successful fetch, or roll back pageNum in a failure path.
| <div class="profile-page"> | ||
| <!-- 顶部用户信息 --> | ||
| <div class="profile-page__hero"> | ||
| <div class="profile-page__avatar"> | ||
| <img v-if="userInfo.avatar" :src="userInfo.avatar" :alt="userInfo.nickname" |
There was a problem hiding this comment.
The profile page redesign removed data-test hooks that existing unit tests assert on (see src/tests/app-shell.spec.ts for profile-overview, profile-recents, profile-favorites). Add equivalent data-test attributes to the new sections or update the tests, otherwise CI will fail.
| <div v-if="isMobileDevice" class="app-layout__topbar"> | ||
| <button type="button" class="app-layout__menu-btn" @click="handleToggleSidebar" aria-label="打开菜单"> | ||
| <el-icon><Expand /></el-icon> | ||
| </button> | ||
| <button type="button" @click="handleExplore">去探索</button> | ||
| <RouterLink to="/searchRole" class="app-layout__topbar-logo"> |
There was a problem hiding this comment.
The template no longer uses handleExplore (and potentially router), so the remaining script code will likely fail no-unused-vars linting. Remove the unused handler/variables or wire them back into the UI.
| const route = useRoute() | ||
| const isMobileDevice = isMobile() | ||
| const historyStore = chatHistoryStore() | ||
| const { isDark } = useTheme() |
There was a problem hiding this comment.
isDark is destructured from useTheme() but never used, which will fail no-unused-vars linting. Either use it (e.g., display theme state) or remove the destructuring/import.
| <div v-if="hasMore" class="discovery__more"> | ||
| <button @click="loadMore" :disabled="loading">{{ loading ? '加载中…' : '加载更多' }}</button> | ||
| </div> | ||
|
|
||
| <RoleDialog :item="roleSelected" v-if="infoShow && roleSelected" @close="infoShow = false" /> |
There was a problem hiding this comment.
RoleDialog is still gated by infoShow && roleSelected, but the new UI never sets infoShow true or assigns roleSelected (the old detail handler was removed). Either reintroduce a way to open the dialog or remove the dialog + related state to avoid dead UI.
| const msg = ElMessage.info('正在创建对话…') | ||
| const uuid = await chatHistoryStore().addChatHistory(characterId) | ||
| msg.close() | ||
| router.push(`/chat/${uuid}`) | ||
| } catch { |
There was a problem hiding this comment.
The “正在创建对话…” info message is only closed on success. If addChatHistory throws, the info message will linger while an error message is shown. Close the handler in a finally (or in catch) so it’s always dismissed.
📌 变更内容
✅ 测试验证
PR 提交规范提醒: