From 7dc7888869ec8cc3b2956ab4f470b71aa938529f Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 4 May 2026 12:38:14 +0800 Subject: [PATCH 01/23] refactor(ui): ChatGPT-style visual overhaul for app shell and analytics pages --- src/App.scss | 98 +---- src/App.tsx | 26 +- src/components/ChatAnalysisHeader.scss | 51 +-- src/components/Sidebar.scss | 340 ++++++++-------- src/components/TitleBar.scss | 48 +-- src/pages/AnalyticsPage.scss | 162 ++++---- src/pages/AnalyticsWelcomePage.scss | 202 +++++----- src/pages/AnalyticsWelcomePage.tsx | 44 +-- src/pages/ChatAnalyticsHubPage.scss | 156 ++++---- src/pages/ChatAnalyticsHubPage.tsx | 53 ++- src/pages/HomePage.scss | 148 ++++--- src/pages/HomePage.tsx | 42 +- src/styles/main.scss | 515 ++++++------------------- 13 files changed, 756 insertions(+), 1129 deletions(-) diff --git a/src/App.scss b/src/App.scss index 5cffbbd..4c73248 100644 --- a/src/App.scss +++ b/src/App.scss @@ -3,56 +3,15 @@ display: flex; flex-direction: column; background: var(--bg-primary); - animation: appFadeIn 0.35s ease-out; position: relative; overflow: hidden; } -// 繁花如梦:底色层(::before)+ 光晕层(::after)分离,避免 blur 吃掉边缘 -[data-theme="blossom-dream"] .app-container { - background: transparent; -} - -// ::before 纯底色,不模糊 -[data-theme="blossom-dream"] .app-container::before { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - z-index: -2; - background: var(--bg-primary); -} - -// ::after 光晕层,模糊叠加在底色上 -[data-theme="blossom-dream"] .app-container::after { - content: ''; - position: absolute; - inset: 0; - pointer-events: none; - z-index: -1; - background: - radial-gradient(ellipse 55% 45% at 15% 20%, var(--blossom-pink) 0%, transparent 70%), - radial-gradient(ellipse 50% 40% at 85% 75%, var(--blossom-peach) 0%, transparent 65%), - radial-gradient(ellipse 45% 50% at 80% 10%, var(--blossom-blue) 0%, transparent 60%); - filter: blur(80px); - opacity: 0.75; -} - -// 深色模式光晕更克制 -[data-theme="blossom-dream"][data-mode="dark"] .app-container::after { - background: - radial-gradient(ellipse 55% 45% at 15% 20%, var(--blossom-pink) 0%, transparent 70%), - radial-gradient(ellipse 50% 40% at 85% 75%, var(--blossom-purple) 0%, transparent 65%), - radial-gradient(ellipse 45% 50% at 80% 10%, var(--blossom-blue) 0%, transparent 60%); - filter: blur(100px); - opacity: 0.2; -} - .window-drag-region { position: fixed; top: 0; left: 0; - right: 150px; // 预留系统最小化/最大化/关闭按钮区域 + right: 150px; height: 40px; -webkit-app-region: drag; pointer-events: auto; @@ -68,8 +27,9 @@ .content { flex: 1; overflow: auto; - padding: 24px; + padding: 24px 32px; position: relative; + background: var(--bg-primary); } .export-keepalive-page { @@ -84,18 +44,7 @@ display: none; } -@keyframes appFadeIn { - from { - opacity: 0; - transform: translateY(8px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -// 更新提示条 +// ---- Update banner ---- .update-banner { display: flex; align-items: center; @@ -107,7 +56,7 @@ .update-text { flex: 1; - + strong { font-weight: 600; } @@ -124,7 +73,7 @@ color: white; font-size: 13px; cursor: pointer; - transition: background 0.2s; + transition: background 0.15s; &:hover { background: rgba(255, 255, 255, 0.3); @@ -143,7 +92,7 @@ color: white; cursor: pointer; opacity: 0.7; - transition: opacity 0.2s; + transition: opacity 0.15s; &:hover { opacity: 1; @@ -178,29 +127,31 @@ } } -// 用户协议弹窗 +// ---- Agreement modal ---- .agreement-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; - background: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; + backdrop-filter: blur(4px); } .agreement-modal { width: 520px; max-height: 80vh; background: var(--bg-primary); + border: 1px solid var(--border-color); border-radius: 16px; overflow: hidden; display: flex; flex-direction: column; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2); } .agreement-header { @@ -241,8 +192,8 @@ margin-bottom: 16px; padding: 12px 14px; border-radius: 10px; - border: 1px solid rgba(255, 160, 0, 0.35); - background: rgba(255, 160, 0, 0.12); + border: 1px solid rgba(245, 158, 11, 0.3); + background: rgba(245, 158, 11, 0.08); color: var(--text-primary); strong { @@ -291,19 +242,6 @@ color: var(--text-secondary); line-height: 1.6; } - - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 3px; - } } .agreement-footer { @@ -347,21 +285,21 @@ border-radius: 8px; font-size: 14px; cursor: pointer; - transition: background 0.2s; + transition: background 0.15s; &:hover { - background: var(--border-color); + background: var(--bg-hover); } } .btn-primary { background: var(--primary); - color: white; + color: var(--on-primary); border: none; border-radius: 8px; font-size: 14px; cursor: pointer; - transition: opacity 0.2s; + transition: opacity 0.15s; &:disabled { opacity: 0.5; diff --git a/src/App.tsx b/src/App.tsx index 5834978..f544cdf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,7 @@ import AccountManagementPage from './pages/AccountManagementPage' import BackupPage from './pages/BackupPage' import { useAppStore } from './stores/appStore' -import { themes, useThemeStore, type ThemeId, type ThemeMode } from './stores/themeStore' +import { useThemeStore, type ThemeMode } from './stores/themeStore' import * as configService from './services/config' import * as cloudControl from './services/cloudControl' import { Download, X, Shield } from 'lucide-react' @@ -74,7 +74,7 @@ function App() { setLocked } = useAppStore() - const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore() + const { themeMode, setThemeMode } = useThemeStore() const isAgreementWindow = location.pathname === '/agreement-window' const isOnboardingWindow = location.pathname === '/onboarding-window' const isVideoPlayerWindow = location.pathname === '/video-player-window' @@ -149,12 +149,11 @@ function App() { } }, [isOnboardingWindow, isNotificationWindow, isAnnualReportWindow, isDualReportWindow]) - // 应用主题 + // 应用主题模式 (light / dark / system) useEffect(() => { const mq = window.matchMedia('(prefers-color-scheme: dark)') const applyMode = (mode: ThemeMode, systemDark?: boolean) => { const effectiveMode = mode === 'system' ? (systemDark ?? mq.matches ? 'dark' : 'light') : mode - document.documentElement.setAttribute('data-theme', currentTheme) document.documentElement.setAttribute('data-mode', effectiveMode) } @@ -168,19 +167,13 @@ function App() { } mq.addEventListener('change', handler) return () => mq.removeEventListener('change', handler) - }, [currentTheme, themeMode, isOnboardingWindow, isNotificationWindow, isAnnualReportWindow, isDualReportWindow]) + }, [themeMode, isOnboardingWindow, isNotificationWindow, isAnnualReportWindow, isDualReportWindow]) // 读取已保存的主题设置 useEffect(() => { const loadTheme = async () => { try { - const [savedThemeId, savedThemeMode] = await Promise.all([ - configService.getThemeId(), - configService.getTheme() - ]) - if (savedThemeId && themes.some((theme) => theme.id === savedThemeId)) { - setTheme(savedThemeId as ThemeId) - } + const savedThemeMode = await configService.getTheme() if (savedThemeMode === 'light' || savedThemeMode === 'dark' || savedThemeMode === 'system') { setThemeMode(savedThemeMode) } @@ -191,23 +184,20 @@ function App() { } } loadTheme() - }, [setTheme, setThemeMode]) + }, [setThemeMode]) // 保存主题设置 useEffect(() => { if (!themeHydrated) return const saveTheme = async () => { try { - await Promise.all([ - configService.setThemeId(currentTheme), - configService.setTheme(themeMode) - ]) + await configService.setTheme(themeMode) } catch (e) { console.error('保存主题配置失败:', e) } } saveTheme() - }, [currentTheme, themeMode, themeHydrated]) + }, [themeMode, themeHydrated]) // 检查是否已同意协议 useEffect(() => { diff --git a/src/components/ChatAnalysisHeader.scss b/src/components/ChatAnalysisHeader.scss index 4e5e99f..f920d6d 100644 --- a/src/components/ChatAnalysisHeader.scss +++ b/src/components/ChatAnalysisHeader.scss @@ -4,28 +4,29 @@ align-items: center; justify-content: space-between; gap: 16px; - min-height: 28px; + min-height: 32px; padding: 4px 0; background: transparent; border: none; - border-radius: 0; flex-shrink: 0; } .chat-analysis-back { display: inline-flex; align-items: center; - gap: 6px; - padding: 0; + gap: 4px; + padding: 4px 8px 4px 4px; border: none; + border-radius: 6px; background: transparent; - color: var(--text-secondary); + color: var(--text-tertiary); cursor: pointer; - transition: color 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; font-size: 13px; - font-weight: 600; + font-weight: 500; &:hover { + background: var(--bg-hover); color: var(--text-primary); } } @@ -33,12 +34,13 @@ .chat-analysis-breadcrumb { display: flex; align-items: center; - gap: 8px; + gap: 4px; font-size: 13px; - color: var(--text-secondary); + color: var(--text-tertiary); .chat-analysis-breadcrumb-separator { - opacity: 0.6; + opacity: 0.5; + font-size: 12px; } } @@ -49,25 +51,27 @@ .chat-analysis-current-trigger { display: inline-flex; align-items: center; - gap: 6px; - padding: 0; + gap: 4px; + padding: 4px 8px; border: none; + border-radius: 6px; background: transparent; - color: var(--text-secondary); + color: var(--text-tertiary); cursor: pointer; font-size: 13px; font-weight: 600; - transition: color 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; .current { color: var(--text-primary); } svg { - transition: transform 0.2s ease; + transition: transform 0.15s ease; } &:hover { + background: var(--bg-hover); color: var(--text-primary); } @@ -78,34 +82,33 @@ .chat-analysis-menu { position: absolute; - top: calc(100% + 10px); + top: calc(100% + 6px); right: 0; min-width: 120px; - padding: 6px; - background: var(--card-bg); + padding: 4px; + background: var(--bg-secondary-solid, var(--bg-secondary)); border: 1px solid var(--border-color); - border-radius: 12px; - box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12); + border-radius: 10px; + box-shadow: var(--shadow-md); z-index: 20; } .chat-analysis-menu-item { width: 100%; display: block; - padding: 9px 12px; + padding: 8px 12px; border: none; - border-radius: 8px; + border-radius: 6px; background: transparent; color: var(--text-primary); text-align: left; cursor: pointer; font-size: 13px; font-weight: 500; - transition: background 0.2s ease, color 0.2s ease; + transition: background 0.15s ease; &:hover { background: var(--bg-hover); - color: var(--primary); } } diff --git a/src/components/Sidebar.scss b/src/components/Sidebar.scss index 5f153ee..d082bd4 100644 --- a/src/components/Sidebar.scss +++ b/src/components/Sidebar.scss @@ -1,14 +1,16 @@ +// ChatGPT-style sidebar .sidebar { - width: 220px; - background: var(--bg-secondary); - border-right: 1px solid var(--border-color); + width: var(--sidebar-width, 260px); + background: var(--bg-sidebar, var(--bg-secondary)); display: flex; flex-direction: column; - padding: 16px 0; - transition: width 0.25s ease; + padding: 8px 0; + transition: width 0.2s ease; + flex-shrink: 0; + overflow: hidden; &.collapsed { - width: 64px; + width: 68px; .sidebar-user-card-wrap { margin: 0 8px 8px; @@ -40,156 +42,15 @@ } } -.sidebar-user-card-wrap { - position: relative; - margin: 0 12px 10px; - --sidebar-user-menu-width: 172px; -} - -.sidebar-user-menu { - position: absolute; - left: 0; - right: auto; - bottom: calc(100% + 8px); - width: max(100%, var(--sidebar-user-menu-width)); - z-index: 12; - border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--bg-secondary-solid, var(--bg-primary)); - display: flex; - flex-direction: column; - gap: 4px; - padding: 6px; - box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12); - opacity: 0; - transform: translateY(8px) scale(0.95); - pointer-events: none; - transition: opacity 0.2s ease, transform 0.2s ease; - - &.open { - opacity: 1; - transform: translateY(0) scale(1); - pointer-events: auto; - } -} - -.sidebar-user-menu-item { - width: 100%; - border: none; - border-radius: 10px; - background: transparent; - color: var(--text-primary); - padding: 9px 10px; - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; - font-weight: 500; - cursor: pointer; - text-align: left; - transition: background 0.2s ease, color 0.2s ease; - - &:hover { - background: var(--bg-tertiary); - } - - &.danger { - color: #d93025; - - &:hover { - background: rgba(255, 59, 48, 0.08); - } - } -} - -.sidebar-user-card { - width: 100%; - padding: 10px; - border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--bg-secondary); - display: flex; - align-items: center; - gap: 10px; - min-height: 56px; - cursor: pointer; - transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease; - - &:hover { - border-color: rgba(99, 102, 241, 0.32); - background: var(--bg-tertiary); - } - - &.menu-open { - border-color: rgba(99, 102, 241, 0.44); - box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.12); - } - - .user-avatar { - width: 36px; - height: 36px; - border-radius: 10px; - overflow: hidden; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); - display: flex; - align-items: center; - justify-content: center; - flex-shrink: 0; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } - - span { - color: var(--on-primary); - font-size: 14px; - font-weight: 600; - } - } - - .user-meta { - min-width: 0; - flex: 1; - } - - .user-name { - font-size: 13px; - color: var(--text-primary); - font-weight: 600; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .user-wxid { - margin-top: 2px; - font-size: 11px; - color: var(--text-tertiary); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .user-menu-caret { - color: var(--text-tertiary); - display: inline-flex; - transition: transform 0.2s ease, color 0.2s ease; - - &.open { - transform: rotate(180deg); - color: var(--text-secondary); - } - } -} - +// ---- Navigation ---- .nav-menu { flex: 1; display: flex; flex-direction: column; - gap: 4px; + gap: 2px; padding: 0 12px; + overflow-y: auto; + overflow-x: hidden; } .nav-item { @@ -197,24 +58,26 @@ align-items: center; gap: 12px; padding: 10px 12px; - border-radius: 9999px; + border-radius: 8px; color: var(--text-secondary); text-decoration: none; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; white-space: nowrap; border: none; background: transparent; cursor: pointer; font-family: inherit; + font-size: 14px; &:hover { - background: var(--bg-tertiary); + background: var(--bg-hover); color: var(--text-primary); } &.active { - background: var(--primary); - color: var(--on-primary); + background: var(--bg-hover); + color: var(--text-primary); + font-weight: 600; } } @@ -242,7 +105,7 @@ height: 20px; border-radius: 999px; padding: 0 6px; - background: #ff3b30; + background: #ef4444; color: #ffffff; font-size: 11px; font-weight: 700; @@ -250,7 +113,6 @@ align-items: center; justify-content: center; line-height: 1; - box-shadow: 0 0 0 2px rgba(255, 59, 48, 0.18); } .nav-badge.icon-badge { @@ -262,42 +124,158 @@ height: 16px; padding: 0 4px; font-size: 10px; - box-shadow: 0 0 0 2px var(--bg-secondary); + box-shadow: 0 0 0 2px var(--bg-sidebar, var(--bg-secondary)); } +// ---- Footer ---- .sidebar-footer { padding: 0 12px; border-top: 1px solid var(--border-color); - padding-top: 12px; - margin-top: 8px; + padding-top: 8px; + margin-top: 4px; display: flex; flex-direction: column; - gap: 4px; + gap: 2px; } -// 繁花如梦主题:侧边栏毛玻璃 + 激活项用主品牌色 -[data-theme="blossom-dream"] .sidebar { - background: rgba(255, 255, 255, 0.6); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-right: 1px solid rgba(255, 255, 255, 0.4); +// ---- User card ---- +.sidebar-user-card-wrap { + position: relative; + margin: 0 12px 8px; + --sidebar-user-menu-width: 172px; } -[data-theme="blossom-dream"][data-mode="dark"] .sidebar { - background: rgba(34, 30, 36, 0.75); - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); - border-right: 1px solid rgba(255, 255, 255, 0.06); +.sidebar-user-menu { + position: absolute; + left: 0; + right: auto; + bottom: calc(100% + 8px); + width: max(100%, var(--sidebar-user-menu-width)); + z-index: 12; + border: 1px solid var(--border-color); + border-radius: 10px; + background: var(--bg-secondary-solid, var(--bg-secondary)); + display: flex; + flex-direction: column; + gap: 2px; + padding: 4px; + box-shadow: var(--shadow-md); + opacity: 0; + transform: translateY(6px) scale(0.97); + pointer-events: none; + transition: opacity 0.15s ease, transform 0.15s ease; + + &.open { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; + } } -// 激活项:主品牌色纵向微渐变 -[data-theme="blossom-dream"] .nav-item.active { - background: linear-gradient(180deg, #D4849A 0%, #C4748A 100%); +.sidebar-user-menu-item { + width: 100%; + border: none; + border-radius: 8px; + background: transparent; + color: var(--text-primary); + padding: 8px 10px; + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + text-align: left; + transition: background 0.15s ease; + + &:hover { + background: var(--bg-hover); + } + + &.danger { + color: #ef4444; + + &:hover { + background: rgba(239, 68, 68, 0.08); + } + } } -// 深色激活项:用藕粉色,背景深灰底 + 粉色文字/图标(高阶玩法) -[data-theme="blossom-dream"][data-mode="dark"] .nav-item.active { - background: rgba(209, 158, 187, 0.15); - color: #D19EBB; - border: 1px solid rgba(209, 158, 187, 0.2); +.sidebar-user-card { + width: 100%; + padding: 10px; + border-radius: 10px; + background: transparent; + display: flex; + align-items: center; + gap: 10px; + min-height: 52px; + cursor: pointer; + border: none; + transition: background 0.15s ease; + + &:hover { + background: var(--bg-hover); + } + + &.menu-open { + background: var(--bg-hover); + } + + .user-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + overflow: hidden; + background: var(--primary); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + span { + color: var(--on-primary); + font-size: 13px; + font-weight: 600; + } + } + + .user-meta { + min-width: 0; + flex: 1; + } + + .user-name { + font-size: 13px; + color: var(--text-primary); + font-weight: 600; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .user-wxid { + margin-top: 1px; + font-size: 11px; + color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .user-menu-caret { + color: var(--text-tertiary); + display: inline-flex; + transition: transform 0.15s ease; + + &.open { + transform: rotate(180deg); + } + } } diff --git a/src/components/TitleBar.scss b/src/components/TitleBar.scss index 8c3c9b8..50ebb61 100644 --- a/src/components/TitleBar.scss +++ b/src/components/TitleBar.scss @@ -1,12 +1,11 @@ .title-bar { - height: 41px; - background: var(--bg-secondary); + height: 48px; + background: transparent; display: flex; align-items: center; justify-content: space-between; padding-left: 16px; - padding-right: 16px; - border-bottom: 1px solid var(--border-color); + padding-right: 8px; -webkit-app-region: drag; flex-shrink: 0; gap: 8px; @@ -14,12 +13,6 @@ z-index: 2101; } -// 繁花如梦:标题栏毛玻璃 -[data-theme="blossom-dream"] .title-bar { - backdrop-filter: blur(20px); - -webkit-backdrop-filter: blur(20px); -} - .title-brand { display: inline-flex; align-items: center; @@ -33,16 +26,15 @@ } .titles { - font-size: 15px; - font-weight: 500; + font-size: 14px; + font-weight: 600; color: var(--text-secondary); - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; + letter-spacing: -0.01em; } .title-sidebar-toggle { - width: 28px; - height: 28px; + width: 32px; + height: 32px; padding: 0; border: none; border-radius: 8px; @@ -52,11 +44,11 @@ align-items: center; justify-content: center; cursor: pointer; - transition: background 0.2s ease, color 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; -webkit-app-region: no-drag; &:hover { - background: var(--bg-tertiary); + background: var(--bg-hover); color: var(--text-primary); } } @@ -64,26 +56,26 @@ .title-window-controls { display: inline-flex; align-items: center; - gap: 6px; + gap: 2px; -webkit-app-region: no-drag; } .title-window-control-btn { - width: 28px; + width: 36px; height: 28px; padding: 0; border: none; - border-radius: 8px; + border-radius: 6px; background: transparent; color: var(--text-tertiary); display: inline-flex; align-items: center; justify-content: center; cursor: pointer; - transition: background 0.2s ease, color 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { - background: var(--bg-tertiary); + background: var(--bg-hover); color: var(--text-primary); } @@ -107,14 +99,14 @@ color: var(--text-secondary); cursor: pointer; padding: 6px; - border-radius: 4px; + border-radius: 6px; display: flex; align-items: center; justify-content: center; - transition: all 0.2s; + transition: all 0.15s; &:hover { - background: var(--bg-tertiary); + background: var(--bg-hover); color: var(--text-primary); } @@ -124,8 +116,8 @@ } &.live-play-btn.active { - background: rgba(var(--primary-rgb, 76, 132, 255), 0.16); - color: var(--primary, #4c84ff); + background: var(--primary-light); + color: var(--primary); } } diff --git a/src/pages/AnalyticsPage.scss b/src/pages/AnalyticsPage.scss index f905f4d..ae19cf4 100644 --- a/src/pages/AnalyticsPage.scss +++ b/src/pages/AnalyticsPage.scss @@ -10,7 +10,7 @@ } } -// 加载和错误状态 +// Loading and error states .loading-container, .error-container { display: flex; @@ -23,7 +23,7 @@ color: var(--text-secondary); .spin { - animation: spin 1s linear infinite; + animation: analyticsSpin 1s linear infinite; } p.loading-status { @@ -33,13 +33,12 @@ } .progress-bar-wrapper { - width: 300px; - height: 8px; + width: 280px; + height: 4px; background: var(--bg-tertiary); border-radius: 999px; overflow: hidden; position: relative; - border: 1px solid var(--border-color); } .progress-bar-fill { @@ -47,9 +46,9 @@ left: 0; top: 0; height: 100%; - background: var(--primary-gradient); - transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); - box-shadow: 0 0 10px rgba(139, 115, 85, 0.3); + background: var(--primary); + transition: width 0.3s ease; + border-radius: 999px; } .progress-percent { @@ -65,57 +64,82 @@ } } -@keyframes spin { - from { - transform: rotate(0deg); - } +@keyframes analyticsSpin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} - to { - transform: rotate(360deg); +// Page scroll content +.page-scroll { + display: flex; + flex-direction: column; + gap: 24px; +} + +.page-section { + display: flex; + flex-direction: column; + gap: 16px; +} + +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + + h2 { + display: flex; + align-items: center; + gap: 8px; + font-size: 16px; + font-weight: 600; + color: var(--text-primary); + margin: 0; } } -// 统计卡片 +// Stats overview cards .stats-overview { display: grid; grid-template-columns: repeat(4, 1fr); - gap: 16px; - margin-bottom: 24px; + gap: 12px; } .stat-card { display: flex; align-items: center; - gap: 16px; - padding: 20px; + gap: 14px; + padding: 18px 16px; background: var(--card-bg); border-radius: 12px; border: 1px solid var(--border-color); .stat-icon { - width: 48px; - height: 48px; + width: 40px; + height: 40px; display: flex; align-items: center; justify-content: center; background: var(--primary-light); - border-radius: 12px; + border-radius: 10px; color: var(--primary); + flex-shrink: 0; } .stat-info { display: flex; flex-direction: column; - gap: 4px; + gap: 2px; .stat-value { - font-size: 24px; - font-weight: 600; + font-size: 22px; + font-weight: 700; color: var(--text-primary); + letter-spacing: -0.5px; } .stat-label { - font-size: 13px; + font-size: 12px; color: var(--text-tertiary); } } @@ -125,23 +149,23 @@ display: flex; align-items: center; gap: 8px; - padding: 12px 16px; + padding: 10px 14px; background: var(--bg-tertiary); border-radius: 8px; - margin-bottom: 24px; font-size: 13px; color: var(--text-secondary); svg { color: var(--text-tertiary); + flex-shrink: 0; } } +// Charts .charts-grid { display: grid; grid-template-columns: repeat(2, 1fr); - gap: 16px; - margin-bottom: 24px; + gap: 12px; } .chart-card { @@ -155,30 +179,30 @@ } h3 { - font-size: 15px; - font-weight: 500; + font-size: 14px; + font-weight: 600; color: var(--text-primary); - margin: 0 0 16px; + margin: 0 0 12px; } } +// Rankings .rankings-list { display: flex; flex-direction: column; - gap: 8px; + gap: 4px; } .ranking-item { display: flex; align-items: center; gap: 12px; - padding: 12px 16px; - background: var(--bg-primary); + padding: 10px 14px; border-radius: 8px; - transition: background 0.2s; + transition: background 0.15s ease; &:hover { - background: var(--bg-tertiary); + background: var(--bg-hover); } .rank { @@ -196,13 +220,13 @@ &.top { background: var(--primary); - color: white; + color: var(--on-primary); } } .contact-avatar { - width: 40px; - height: 40px; + width: 36px; + height: 36px; flex-shrink: 0; position: relative; @@ -228,8 +252,8 @@ position: absolute; right: -4px; bottom: -4px; - width: 18px; - height: 18px; + width: 16px; + height: 16px; display: flex; align-items: center; justify-content: center; @@ -239,24 +263,21 @@ &.medal-1 { background: linear-gradient(135deg, #ffd700, #ffb800); color: #fff; - box-shadow: 0 2px 4px rgba(255, 184, 0, 0.4); } &.medal-2 { background: linear-gradient(135deg, #c0c0c0, #a8a8a8); color: #fff; - box-shadow: 0 2px 4px rgba(168, 168, 168, 0.4); } &.medal-3 { background: linear-gradient(135deg, #cd7f32, #b87333); color: #fff; - box-shadow: 0 2px 4px rgba(184, 115, 51, 0.4); } svg { - width: 10px; - height: 10px; + width: 8px; + height: 8px; } } } @@ -265,7 +286,7 @@ flex: 1; display: flex; flex-direction: column; - gap: 2px; + gap: 1px; min-width: 0; .contact-name { @@ -284,14 +305,14 @@ } .message-count { - font-size: 14px; - font-weight: 500; + font-size: 13px; + font-weight: 600; color: var(--primary); flex-shrink: 0; } } -// 响应式 +// Responsive @media (max-width: 1200px) { .stats-overview { grid-template-columns: repeat(2, 1fr); @@ -312,11 +333,11 @@ } } -// 排除好友弹窗 +// Exclude friends modal .exclude-modal-overlay { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.45); + background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; @@ -325,13 +346,13 @@ } .exclude-modal { - width: 560px; + width: 520px; max-width: calc(100vw - 48px); - background: var(--card-bg); + background: var(--bg-secondary-solid, var(--bg-secondary)); border-radius: 16px; border: 1px solid var(--border-color); padding: 20px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2); + box-shadow: 0 24px 48px rgba(0, 0, 0, 0.2); .exclude-modal-header { display: flex; @@ -342,6 +363,7 @@ h3 { margin: 0; font-size: 16px; + font-weight: 600; color: var(--text-primary); } } @@ -349,14 +371,14 @@ .modal-close { width: 32px; height: 32px; - border-radius: 50%; + border-radius: 8px; border: none; - background: var(--bg-tertiary); + background: transparent; display: flex; align-items: center; justify-content: center; cursor: pointer; - color: var(--text-secondary); + color: var(--text-tertiary); transition: all 0.15s; &:hover { @@ -370,7 +392,7 @@ align-items: center; gap: 8px; padding: 8px 12px; - border-radius: 10px; + border-radius: 8px; border: 1px solid var(--border-color); background: var(--bg-primary); margin-bottom: 12px; @@ -399,7 +421,7 @@ } .exclude-modal-body { - max-height: 420px; + max-height: 380px; overflow: auto; padding-right: 4px; } @@ -419,7 +441,7 @@ .exclude-list { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; } .exclude-item { @@ -427,23 +449,23 @@ align-items: center; gap: 12px; padding: 8px 10px; - border-radius: 10px; + border-radius: 8px; cursor: pointer; border: 1px solid transparent; transition: all 0.15s; - background: var(--bg-primary); &:hover { - background: var(--bg-tertiary); + background: var(--bg-hover); } &.active { - border-color: rgba(7, 193, 96, 0.4); - background: rgba(7, 193, 96, 0.08); + border-color: rgba(16, 163, 127, 0.3); + background: rgba(16, 163, 127, 0.06); } input { margin: 0; + accent-color: var(--primary); } } @@ -455,7 +477,7 @@ display: flex; flex-direction: column; min-width: 0; - gap: 2px; + gap: 1px; } .exclude-name { @@ -479,7 +501,7 @@ display: flex; align-items: center; justify-content: space-between; - margin-top: 16px; + margin-top: 14px; } .exclude-footer-left { diff --git a/src/pages/AnalyticsWelcomePage.scss b/src/pages/AnalyticsWelcomePage.scss index 0e698bc..4e14705 100644 --- a/src/pages/AnalyticsWelcomePage.scss +++ b/src/pages/AnalyticsWelcomePage.scss @@ -1,146 +1,116 @@ -.analytics-entry-page { +.analytics-welcome-shell { display: flex; flex-direction: column; gap: 16px; min-height: 100%; } -.analytics-welcome-container { +.analytics-welcome-body { display: flex; flex-direction: column; flex: 1; align-items: center; justify-content: center; min-height: 0; - padding: 40px; - background: var(--bg-primary); + padding: 40px 24px; + animation: welcomeFadeIn 0.4s ease-out; +} + +.analytics-welcome-content { + text-align: center; + max-width: 480px; + width: 100%; +} + +.analytics-welcome-icon { + width: 56px; + height: 56px; + margin: 0 auto 20px; + background: var(--primary-light); + border-radius: 14px; + display: flex; + align-items: center; + justify-content: center; + color: var(--primary); +} + +.analytics-welcome-content h1 { + font-size: 24px; + font-weight: 700; + margin: 0 0 10px; color: var(--text-primary); - animation: fadeIn 0.4s ease-out; - overflow-y: auto; + letter-spacing: -0.3px; +} - &.analytics-welcome-container--mode { - border-radius: 20px; - border: 1px solid var(--border-color); - background: - radial-gradient(circle at top, rgba(7, 193, 96, 0.06), transparent 48%), - var(--bg-primary); +.analytics-welcome-content p { + color: var(--text-secondary); + margin: 0 0 32px; + font-size: 14px; + line-height: 1.7; +} + +.analytics-welcome-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} + +.analytics-welcome-card { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 18px; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + cursor: pointer; + text-align: left; + color: var(--text-secondary); + transition: background 0.15s ease, border-color 0.15s ease; + + &:hover { + background: var(--bg-hover); + border-color: var(--text-tertiary); + color: var(--primary); } - .welcome-content { - text-align: center; - max-width: 600px; - - .icon-wrapper { - width: 80px; - height: 80px; - margin: 0 auto 24px; - background: rgba(7, 193, 96, 0.1); - border-radius: 20px; - display: flex; - align-items: center; - justify-content: center; - color: #07c160; - - svg { - width: 40px; - height: 40px; - } - } - - h1 { - font-size: 28px; - margin-bottom: 12px; - font-weight: 600; - } - - p { - color: var(--text-secondary); - margin-bottom: 40px; - font-size: 16px; - line-height: 1.6; - } - - .action-cards { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - margin-top: 20px; - - button { - display: flex; - flex-direction: column; - align-items: center; - padding: 30px 20px; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 12px; - cursor: pointer; - transition: all 0.2s ease; - text-align: center; - - &:hover:not(:disabled) { - transform: translateY(-2px); - border-color: #07c160; - box-shadow: 0 4px 12px rgba(7, 193, 96, 0.1); - - .card-icon { - color: #07c160; - background: rgba(7, 193, 96, 0.1); - } - } - - &:disabled { - opacity: 0.6; - cursor: not-allowed; - filter: grayscale(100%); - } - - .card-icon { - width: 50px; - height: 50px; - border-radius: 12px; - background: var(--bg-tertiary); - display: flex; - align-items: center; - justify-content: center; - margin-bottom: 16px; - color: var(--text-secondary); - transition: all 0.2s ease; - } - - h3 { - font-size: 18px; - margin-bottom: 8px; - color: var(--text-primary); - } - - span { - font-size: 13px; - color: var(--text-tertiary); - } - } - } + svg { + flex-shrink: 0; } } -@media (max-width: 768px) { - .analytics-welcome-container { - padding: 28px 18px; +.analytics-welcome-card-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} - .welcome-content { - .action-cards { - grid-template-columns: 1fr; - } - } +.analytics-welcome-card-title { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.analytics-welcome-card-meta { + font-size: 12px; + color: var(--text-tertiary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@media (max-width: 540px) { + .analytics-welcome-actions { + grid-template-columns: 1fr; } } -@keyframes fadeIn { +@keyframes welcomeFadeIn { from { opacity: 0; transform: translateY(10px); } - to { opacity: 1; transform: translateY(0); diff --git a/src/pages/AnalyticsWelcomePage.tsx b/src/pages/AnalyticsWelcomePage.tsx index e5344ae..44da825 100644 --- a/src/pages/AnalyticsWelcomePage.tsx +++ b/src/pages/AnalyticsWelcomePage.tsx @@ -6,12 +6,6 @@ import './AnalyticsWelcomePage.scss' function AnalyticsWelcomePage() { const navigate = useNavigate() - // 检查是否有任何缓存数据加载或基本的存储状态表明它已准备好。 - // 实际上,如果 store 没有持久化,`isLoaded` 可能会在应用刷新时重置。 - // 如果用户点击“加载缓存”但缓存为空,AnalyticsPage 的逻辑(loadData 不带 force)将尝试从后端缓存加载。 - // 如果后端缓存也为空,则会重新计算。 - - // 我们也可以检查 `lastLoadTime` 来显示“上次更新:xxx”(如果已持久化)。 const { lastLoadTime } = useAnalyticsStore() const handleLoadCache = () => { @@ -28,35 +22,37 @@ function AnalyticsWelcomePage() { } return ( -
+
-
-
-
- +
+
+
+

私聊数据分析

- WeFlow 可以分析你的好友聊天记录,生成详细的统计报表。
- 你可以选择加载上次的分析结果,或者重新开始一次新的私聊分析。 + 分析你的好友聊天记录,生成详细统计报表。
+ 选择加载上次结果或开始新分析。

-
- -
diff --git a/src/pages/ChatAnalyticsHubPage.scss b/src/pages/ChatAnalyticsHubPage.scss index 4d970cd..f6621a7 100644 --- a/src/pages/ChatAnalyticsHubPage.scss +++ b/src/pages/ChatAnalyticsHubPage.scss @@ -1,4 +1,4 @@ -.chat-analytics-hub-page { +.analytics-hub { min-height: 100%; display: flex; align-items: center; @@ -6,118 +6,128 @@ padding: 40px 24px; } -.chat-analytics-hub-content { - width: min(860px, 100%); +.analytics-hub-inner { + width: min(720px, 100%); display: flex; flex-direction: column; align-items: center; text-align: center; + animation: analyticsHubFadeIn 0.4s ease-out; } -.chat-analytics-hub-badge { - display: inline-flex; - align-items: center; - gap: 8px; - padding: 8px 14px; - border-radius: 999px; - background: var(--primary-light); - color: var(--primary); - font-size: 13px; - font-weight: 600; -} - -.chat-analytics-hub-content h1 { - margin: 20px 0 12px; - font-size: 32px; - line-height: 1.2; +.analytics-hub-title { + font-size: 28px; + font-weight: 700; color: var(--text-primary); + margin: 0 0 12px; + letter-spacing: -0.5px; } -.chat-analytics-hub-desc { - max-width: 620px; - margin: 0 0 32px; +.analytics-hub-desc { + max-width: 520px; + margin: 0 0 36px; color: var(--text-secondary); font-size: 15px; line-height: 1.7; } -.chat-analytics-hub-grid { +.analytics-hub-grid { width: 100%; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 20px; + gap: 16px; } -.chat-analytics-entry-card { +.analytics-hub-card { display: flex; flex-direction: column; align-items: flex-start; text-align: left; - gap: 14px; - min-height: 260px; - padding: 28px; + gap: 16px; + padding: 24px; border: 1px solid var(--border-color); - border-radius: 20px; - background: - linear-gradient(180deg, rgba(7, 193, 96, 0.08) 0%, rgba(7, 193, 96, 0.02) 100%), - var(--card-bg); + border-radius: 12px; + background: var(--card-bg); color: var(--text-primary); cursor: pointer; - transition: transform 0.2s ease, box-shadow 0.2s ease, border-color 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease; &:hover { - transform: translateY(-4px); - border-color: rgba(7, 193, 96, 0.35); - box-shadow: 0 20px 36px rgba(7, 193, 96, 0.12); - } + background: var(--bg-hover); + border-color: var(--text-tertiary); - .entry-card-icon { - width: 52px; - height: 52px; - border-radius: 16px; - display: flex; - align-items: center; - justify-content: center; - background: rgba(7, 193, 96, 0.12); - color: #07c160; - - &.group { - background: rgba(24, 119, 242, 0.12); - color: #1877f2; + .analytics-hub-card-arrow { + transform: translateX(3px); } } +} - .entry-card-header { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - gap: 12px; +.analytics-hub-card-icon { + width: 44px; + height: 44px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + background: var(--primary-light); + color: var(--primary); + + &--group { + background: rgba(59, 130, 246, 0.1); + color: #3b82f6; } +} + +[data-mode="dark"] .analytics-hub-card-icon--group { + background: rgba(96, 165, 250, 0.12); + color: #60a5fa; +} + +.analytics-hub-card-body { + display: flex; + flex-direction: column; + gap: 8px; + width: 100%; +} + +.analytics-hub-card-header { + display: flex; + align-items: center; + justify-content: space-between; h2 { margin: 0; - font-size: 24px; - line-height: 1.2; - } - - p { - margin: 0; - color: var(--text-secondary); - font-size: 14px; - line-height: 1.7; - } - - .entry-card-cta { - margin-top: auto; - color: var(--primary); - font-size: 13px; + font-size: 18px; font-weight: 600; + line-height: 1.2; } } -@media (max-width: 900px) { - .chat-analytics-hub-grid { +.analytics-hub-card-arrow { + color: var(--text-tertiary); + transition: transform 0.15s ease; +} + +.analytics-hub-card-body p { + margin: 0; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.6; +} + +@media (max-width: 640px) { + .analytics-hub-grid { grid-template-columns: 1fr; } } + +@keyframes analyticsHubFadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/pages/ChatAnalyticsHubPage.tsx b/src/pages/ChatAnalyticsHubPage.tsx index 6c4456e..3e21c73 100644 --- a/src/pages/ChatAnalyticsHubPage.tsx +++ b/src/pages/ChatAnalyticsHubPage.tsx @@ -1,4 +1,4 @@ -import { ArrowRight, BarChart3, MessageSquare, Users } from 'lucide-react' +import { ArrowRight, MessageSquare, Users } from 'lucide-react' import { useNavigate } from 'react-router-dom' import './ChatAnalyticsHubPage.scss' @@ -6,49 +6,46 @@ function ChatAnalyticsHubPage() { const navigate = useNavigate() return ( -
-
-
- - 聊天分析 -
- -

选择你要进入的分析视角

-

- 私聊分析更适合看好友聊天统计和趋势,群聊分析则用于查看群成员、发言排行和活跃时段。 +

+
+

聊天分析

+

+ 选择你要进入的分析视角。私聊分析适合查看好友聊天统计,群聊分析则用于查看群成员活跃度。

-
+
diff --git a/src/pages/HomePage.scss b/src/pages/HomePage.scss index cd4cb78..40a4ee8 100644 --- a/src/pages/HomePage.scss +++ b/src/pages/HomePage.scss @@ -1,6 +1,5 @@ .home-page { height: 100%; - background: var(--bg-primary); display: flex; align-items: center; justify-content: center; @@ -8,105 +7,94 @@ position: relative; } -.home-bg-blobs { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - filter: blur(80px); - z-index: 0; - opacity: 0.6; - pointer-events: none; -} - -.blob { - position: absolute; - border-radius: 50%; - animation: moveBlob 20s infinite alternate ease-in-out; -} - -.blob-1 { - width: 400px; - height: 400px; - background: rgba(var(--primary-rgb), 0.25); - top: -100px; - left: -50px; - animation-duration: 25s; -} - -.blob-2 { - width: 350px; - height: 350px; - background: rgba(var(--primary-rgb), 0.15); - bottom: -50px; - right: -50px; - animation-duration: 30s; - animation-delay: -5s; -} - -.blob-3 { - width: 300px; - height: 300px; - background: rgba(255, 255, 255, 0.1); - top: 40%; - left: 30%; - animation-duration: 22s; - animation-delay: -10s; -} - -[data-mode="dark"] .blob-3 { - background: rgba(255, 255, 255, 0.03); -} - .home-content { - z-index: 1; - animation: fadeScaleUp 1s cubic-bezier(0.2, 0.8, 0.2, 1); -} - -.hero { text-align: center; + max-width: 640px; + width: 100%; + padding: 0 24px; + animation: homeFadeIn 0.5s ease-out; } -.hero-title { - font-size: 64px; - font-weight: 800; - margin: 0 0 16px; +.home-title { + font-size: 48px; + font-weight: 700; + margin: 0 0 12px; color: var(--text-primary); - letter-spacing: -2px; - background: linear-gradient(135deg, var(--primary) 0%, rgba(var(--primary-rgb), 0.6) 100%); - background-clip: text; - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; + letter-spacing: -1.5px; } -.hero-subtitle { - font-size: 18px; +.home-subtitle { + font-size: 16px; color: var(--text-secondary); - max-width: 520px; - margin: 0 auto; + margin: 0 0 48px; line-height: 1.6; - opacity: 0.8; } -@keyframes moveBlob { - from { - transform: translate(0, 0) scale(1); +.home-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +.home-feature-card { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 18px; + border: 1px solid var(--border-color); + border-radius: 12px; + background: var(--card-bg); + cursor: pointer; + text-align: left; + transition: background 0.15s ease, border-color 0.15s ease; + color: var(--text-secondary); + + &:hover { + background: var(--bg-hover); + border-color: var(--text-tertiary); + color: var(--text-primary); } - to { - transform: translate(100px, 50px) scale(1.1); + svg { + flex-shrink: 0; } } -@keyframes fadeScaleUp { +.home-feature-text { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.home-feature-label { + font-size: 14px; + font-weight: 600; + color: var(--text-primary); +} + +.home-feature-desc { + font-size: 12px; + color: var(--text-tertiary); +} + +@keyframes homeFadeIn { from { opacity: 0; - transform: scale(0.95) translateY(20px); + transform: translateY(12px); } - to { opacity: 1; - transform: scale(1) translateY(0); + transform: translateY(0); + } +} + +@media (max-width: 480px) { + .home-grid { + grid-template-columns: 1fr; + } + + .home-title { + font-size: 36px; } } \ No newline at end of file diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index edca946..0423bad 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,20 +1,40 @@ -import { FolderOpen, ShieldCheck, Sparkles, Waves } from 'lucide-react' -import { useAppStore } from '../stores/appStore' +import { MessageSquare, BarChart3, Download, Aperture, Footprints, FolderClosed } from 'lucide-react' +import { useNavigate } from 'react-router-dom' import './HomePage.scss' function HomePage() { + const navigate = useNavigate() + + const features = [ + { icon: MessageSquare, label: '聊天', desc: '浏览聊天记录', path: '/chat' }, + { icon: Aperture, label: '朋友圈', desc: '查看朋友圈动态', path: '/sns' }, + { icon: BarChart3, label: '聊天分析', desc: '分析聊天统计数据', path: '/analytics' }, + { icon: FolderClosed, label: '资源浏览', desc: '管理媒体文件', path: '/resources' }, + { icon: Footprints, label: '我的足迹', desc: '回顾你的轨迹', path: '/footprint' }, + { icon: Download, label: '导出', desc: '导出聊天记录', path: '/export' }, + ] + return (
-
-
-
-
-
-
-
-

WeFlow

-

每一条消息的背后,都藏着一段温暖的时光

+

WeFlow

+

每一条消息的背后,都藏着一段温暖的时光

+ +
+ {features.map((f) => ( + + ))}
diff --git a/src/styles/main.scss b/src/styles/main.scss index 0de9c35..8b19126 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -1,400 +1,106 @@ -// CSS 变量 - 主题 +// WeFlow — ChatGPT-inspired design token system +// Only light/dark/system modes. No per-theme variants. +// All legacy variable names preserved for backward compatibility. @use './chat-patterns.scss'; -:root { - // 颜色 - --primary: #8B7355; - --primary-hover: #7A6548; - --primary-light: rgba(139, 115, 85, 0.1); - --danger: #dc3545; - --warning: #ffc107; +// ============================================= +// Light mode (default) +// ============================================= +:root, +[data-mode="light"] { + // ---- Accent / Brand ---- + --primary: #10a37f; + --primary-rgb: 16, 163, 127; + --primary-hover: #0d8a6c; + --primary-light: rgba(16, 163, 127, 0.1); + --primary-gradient: linear-gradient(135deg, #10a37f 0%, #1ab893 100%); - // 背景 - --bg-primary: #F0EEE9; - --bg-secondary: rgba(255, 255, 255, 0.7); - --bg-tertiary: rgba(0, 0, 0, 0.03); - --bg-hover: rgba(0, 0, 0, 0.05); + --danger: #ef4444; + --warning: #f59e0b; - // 文字 - --text-primary: #3d3d3d; - --text-secondary: #666666; - --text-tertiary: #999999; - - // 边框 - --border-color: rgba(0, 0, 0, 0.08); - --border-radius: 9999px; - - // 阴影 - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08); - - // 侧边栏 - --sidebar-width: 220px; - - // 主题渐变 - --bg-gradient: linear-gradient(135deg, #F0EEE9 0%, #E8E6E1 100%); - --primary-gradient: linear-gradient(135deg, #8B7355 0%, #A68B5B 100%); - - // 卡片背景 - --card-bg: rgba(255, 255, 255, 0.7); - --card-inner-bg: #FAFAF7; - --sent-card-bg: var(--primary); - - // primary 色上方的前景文字色(大多数主题为白色) - --on-primary: white; -} - -// ==================== 浅色主题 ==================== - -// 云上舞白主题 (默认) -[data-theme="cloud-dancer"][data-mode="light"], -[data-theme="cloud-dancer"]:not([data-mode]) { - --primary: #8B7355; - --primary-rgb: 139, 115, 85; - --primary-hover: #7A6548; - --primary-light: rgba(139, 115, 85, 0.1); - --bg-primary: #F0EEE9; - --bg-secondary: rgba(255, 255, 255, 0.7); - --bg-tertiary: rgba(0, 0, 0, 0.03); - --bg-hover: rgba(0, 0, 0, 0.05); - --text-primary: #3d3d3d; - --text-secondary: #666666; - --text-tertiary: #999999; - --border-color: rgba(0, 0, 0, 0.08); - --bg-gradient: linear-gradient(135deg, #F0EEE9 0%, #E8E6E1 100%); - --primary-gradient: linear-gradient(135deg, #8B7355 0%, #A68B5B 100%); - --card-bg: rgba(255, 255, 255, 0.7); - --card-inner-bg: #FAFAF7; - --sent-card-bg: var(--primary); -} - -// 刚玉蓝主题 -[data-theme="corundum-blue"][data-mode="light"], -[data-theme="corundum-blue"]:not([data-mode]) { - --primary: #4A6670; - --primary-rgb: 74, 102, 112; - --primary-hover: #3D565E; - --primary-light: rgba(74, 102, 112, 0.1); - --bg-primary: #E8EEF0; - --bg-secondary: rgba(255, 255, 255, 0.7); - --bg-tertiary: rgba(0, 0, 0, 0.03); - --bg-hover: rgba(0, 0, 0, 0.05); - --text-primary: #3d3d3d; - --text-secondary: #666666; - --text-tertiary: #999999; - --border-color: rgba(0, 0, 0, 0.08); - --bg-gradient: linear-gradient(135deg, #E8EEF0 0%, #D8E4E8 100%); - --primary-gradient: linear-gradient(135deg, #4A6670 0%, #5A7A86 100%); - --card-bg: rgba(255, 255, 255, 0.7); - --card-inner-bg: #F8FAFB; - --sent-card-bg: var(--primary); -} - -// 冰猕猴桃汁绿主题 -[data-theme="kiwi-green"][data-mode="light"], -[data-theme="kiwi-green"]:not([data-mode]) { - --primary: #7A9A5C; - --primary-rgb: 122, 154, 92; - --primary-hover: #6A8A4C; - --primary-light: rgba(122, 154, 92, 0.1); - --bg-primary: #E8F0E4; - --bg-secondary: rgba(255, 255, 255, 0.7); - --bg-tertiary: rgba(0, 0, 0, 0.03); - --bg-hover: rgba(0, 0, 0, 0.05); - --text-primary: #3d3d3d; - --text-secondary: #666666; - --text-tertiary: #999999; - --border-color: rgba(0, 0, 0, 0.08); - --bg-gradient: linear-gradient(135deg, #E8F0E4 0%, #D8E8D0 100%); - --primary-gradient: linear-gradient(135deg, #7A9A5C 0%, #8AAA6C 100%); - --card-bg: rgba(255, 255, 255, 0.7); - --card-inner-bg: #F8FBF6; - --sent-card-bg: var(--primary); -} - -// 辛辣红主题 -[data-theme="spicy-red"][data-mode="light"], -[data-theme="spicy-red"]:not([data-mode]) { - --primary: #8B4049; - --primary-rgb: 139, 64, 73; - --primary-hover: #7A3540; - --primary-light: rgba(139, 64, 73, 0.1); - --bg-primary: #F0E8E8; - --bg-secondary: rgba(255, 255, 255, 0.7); - --bg-tertiary: rgba(0, 0, 0, 0.03); - --bg-hover: rgba(0, 0, 0, 0.05); - --text-primary: #3d3d3d; - --text-secondary: #666666; - --text-tertiary: #999999; - --border-color: rgba(0, 0, 0, 0.08); - --bg-gradient: linear-gradient(135deg, #F0E8E8 0%, #E8D8D8 100%); - --primary-gradient: linear-gradient(135deg, #8B4049 0%, #A05058 100%); - --card-bg: rgba(255, 255, 255, 0.7); - --card-inner-bg: #FAF8F8; - --sent-card-bg: var(--primary); -} - -// 明水鸭色主题 -[data-theme="teal-water"][data-mode="light"], -[data-theme="teal-water"]:not([data-mode]) { - --primary: #5A8A8A; - --primary-rgb: 90, 138, 138; - --primary-hover: #4A7A7A; - --primary-light: rgba(90, 138, 138, 0.1); - --bg-primary: #E4F0F0; - --bg-secondary: rgba(255, 255, 255, 0.7); - --bg-tertiary: rgba(0, 0, 0, 0.03); - --bg-hover: rgba(0, 0, 0, 0.05); - --text-primary: #3d3d3d; - --text-secondary: #666666; - --text-tertiary: #999999; - --border-color: rgba(0, 0, 0, 0.08); - --bg-gradient: linear-gradient(135deg, #E4F0F0 0%, #D4E8E8 100%); - --primary-gradient: linear-gradient(135deg, #5A8A8A 0%, #6A9A9A 100%); - --card-bg: rgba(255, 255, 255, 0.7); - --card-inner-bg: #F6FBFB; - --sent-card-bg: var(--primary); -} - -// 繁花如梦 - 浅色(晨曦花境) -[data-theme="blossom-dream"][data-mode="light"], -[data-theme="blossom-dream"]:not([data-mode]) { - // 三色定义(供伪元素光晕使用,饱和度提高以便在底色上可见) - --blossom-pink: #F0A0B8; - --blossom-peach: #FFB07A; - --blossom-blue: #90B8E0; - - // 主品牌色:Pantone 粉晶 Rose Quartz - --primary: #D4849A; - --primary-rgb: 212, 132, 154; - --primary-hover: #C4748A; - --primary-light: rgba(212, 132, 154, 0.12); - - // 背景三层:主背景最深(相对),面板次之,卡片最白 - --bg-primary: #F5EDF2; - --bg-secondary: rgba(255, 255, 255, 0.82); - --bg-tertiary: rgba(212, 132, 154, 0.06); - --bg-hover: rgba(212, 132, 154, 0.09); - - // 文字:提高对比度,主色接近纯黑只带微弱紫调 - --text-primary: #1E1A22; - --text-secondary: #6B5F70; - --text-tertiary: #9A8A9E; - // 边框:粉色半透明,有存在感但不强硬 - --border-color: rgba(212, 132, 154, 0.18); - - --bg-gradient: linear-gradient(150deg, #F5EDF2 0%, #F0EAF6 50%, #EAF0F8 100%); - --primary-gradient: linear-gradient(135deg, #D4849A 0%, #E8A8B8 100%); - - // 卡片:高不透明度白,与背景形成明显层次 - --card-bg: rgba(255, 255, 255, 0.88); - --card-inner-bg: rgba(255, 255, 255, 0.95); - - --sent-card-bg: var(--primary); -} - -// Geist · 极简黑白 - 浅色 -[data-theme="geist"][data-mode="light"], -[data-theme="geist"]:not([data-mode]) { - --primary: #444444; - --primary-rgb: 68, 68, 68; - --primary-hover: #333333; - --primary-light: rgba(68, 68, 68, 0.08); + // ---- Backgrounds ---- --bg-primary: #ffffff; - --bg-secondary: rgba(250, 250, 250, 0.95); - --bg-tertiary: rgba(0, 0, 0, 0.03); - --bg-hover: rgba(0, 0, 0, 0.05); - --text-primary: #111111; - --text-secondary: #666666; - --text-tertiary: #999999; - --border-color: #eaeaea; - --border-radius: 6px; - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.08); - --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12); - --bg-gradient: linear-gradient(135deg, #ffffff 0%, #fafafa 100%); - --primary-gradient: linear-gradient(135deg, #444444 0%, #666666 100%); - --card-bg: rgba(250, 250, 250, 0.95); - --card-inner-bg: #f5f5f5; - --sent-card-bg: #444444; -} + --bg-secondary: #f9f9f9; + --bg-secondary-solid: #f9f9f9; + --bg-tertiary: #f0f0f0; + --bg-hover: #ececec; + --bg-sidebar: #f9f9f9; + --bg-gradient: linear-gradient(180deg, #ffffff 0%, #f7f7f8 100%); -// ==================== 深色主题 ==================== + // ---- Text ---- + --text-primary: #0d0d0d; + --text-secondary: #6e6e80; + --text-tertiary: #8e8ea0; -// 云上舞白 - 深色 -[data-theme="cloud-dancer"][data-mode="dark"] { - --primary: #C9A86C; - --primary-rgb: 201, 168, 108; - --primary-hover: #D9B87C; - --primary-light: rgba(201, 168, 108, 0.15); - --bg-primary: #1a1816; - --bg-secondary: rgba(40, 36, 32, 0.9); - --bg-secondary-solid: #282420; - --bg-tertiary: rgba(255, 255, 255, 0.05); - --bg-hover: rgba(255, 255, 255, 0.08); - --text-primary: #F0EEE9; - --text-secondary: #b3b0aa; - --text-tertiary: #807d78; - --border-color: rgba(255, 255, 255, 0.1); - --bg-gradient: linear-gradient(135deg, #1a1816 0%, #252220 100%); - --primary-gradient: linear-gradient(135deg, #8B7355 0%, #C9A86C 100%); - --card-bg: rgba(40, 36, 32, 0.9); - --card-inner-bg: #27231F; + // ---- Borders ---- + --border-color: rgba(0, 0, 0, 0.08); + --border-radius: 12px; + + // ---- Shadows ---- + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.04); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.06); + + // ---- Cards ---- + --card-bg: #f7f7f8; + --card-inner-bg: #ffffff; --sent-card-bg: var(--primary); + + // ---- On-primary foreground ---- + --on-primary: #ffffff; + + // ---- Layout ---- + --sidebar-width: 260px; } -// 刚玉蓝 - 深色 -[data-theme="corundum-blue"][data-mode="dark"] { - --primary: #6A9AAA; - --primary-rgb: 106, 154, 170; - --primary-hover: #7AAABA; - --primary-light: rgba(106, 154, 170, 0.15); - --bg-primary: #141a1c; - --bg-secondary: rgba(30, 40, 44, 0.9); - --bg-secondary-solid: #1e282c; - --bg-tertiary: rgba(255, 255, 255, 0.05); - --bg-hover: rgba(255, 255, 255, 0.08); - --text-primary: #E8EEF0; - --text-secondary: #a8b4b8; - --text-tertiary: #6a7a80; - --border-color: rgba(255, 255, 255, 0.1); - --bg-gradient: linear-gradient(135deg, #141a1c 0%, #1e282c 100%); - --primary-gradient: linear-gradient(135deg, #4A6670 0%, #6A9AAA 100%); - --card-bg: rgba(30, 40, 44, 0.9); - --card-inner-bg: #1D272A; +// ============================================= +// Dark mode +// ============================================= +[data-mode="dark"] { + // ---- Accent / Brand ---- + --primary: #10a37f; + --primary-rgb: 16, 163, 127; + --primary-hover: #1ab893; + --primary-light: rgba(16, 163, 127, 0.15); + --primary-gradient: linear-gradient(135deg, #10a37f 0%, #1ab893 100%); + + --danger: #f87171; + --warning: #fbbf24; + + // ---- Backgrounds ---- + --bg-primary: #212121; + --bg-secondary: #2f2f2f; + --bg-secondary-solid: #2f2f2f; + --bg-tertiary: #383838; + --bg-hover: #383838; + --bg-sidebar: #171717; + --bg-gradient: linear-gradient(180deg, #212121 0%, #1a1a1a 100%); + + // ---- Text ---- + --text-primary: #ececec; + --text-secondary: #b4b4b4; + --text-tertiary: #676767; + + // ---- Borders ---- + --border-color: rgba(255, 255, 255, 0.08); + + // ---- Shadows ---- + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); + --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); + + // ---- Cards ---- + --card-bg: #2f2f2f; + --card-inner-bg: #353535; --sent-card-bg: var(--primary); + + // ---- On-primary foreground ---- + --on-primary: #ffffff; + + --primary-gradient: linear-gradient(135deg, #10a37f 0%, #1ab893 100%); } -// 冰猕猴桃汁绿 - 深色 -[data-theme="kiwi-green"][data-mode="dark"] { - --primary: #9ABA7C; - --primary-rgb: 154, 186, 124; - --primary-hover: #AACA8C; - --primary-light: rgba(154, 186, 124, 0.15); - --bg-primary: #161a14; - --bg-secondary: rgba(34, 42, 30, 0.9); - --bg-secondary-solid: #222a1e; - --bg-tertiary: rgba(255, 255, 255, 0.05); - --bg-hover: rgba(255, 255, 255, 0.08); - --text-primary: #E8F0E4; - --text-secondary: #a8b4a0; - --text-tertiary: #6a7a60; - --border-color: rgba(255, 255, 255, 0.1); - --bg-gradient: linear-gradient(135deg, #161a14 0%, #222a1e 100%); - --primary-gradient: linear-gradient(135deg, #7A9A5C 0%, #9ABA7C 100%); - --card-bg: rgba(34, 42, 30, 0.9); - --card-inner-bg: #21281D; - --sent-card-bg: var(--primary); -} - -// 辛辣红 - 深色 -[data-theme="spicy-red"][data-mode="dark"] { - --primary: #C06068; - --primary-rgb: 192, 96, 104; - --primary-hover: #D07078; - --primary-light: rgba(192, 96, 104, 0.15); - --bg-primary: #1a1416; - --bg-secondary: rgba(42, 32, 34, 0.9); - --bg-secondary-solid: #2a2022; - --bg-tertiary: rgba(255, 255, 255, 0.05); - --bg-hover: rgba(255, 255, 255, 0.08); - --text-primary: #F0E8E8; - --text-secondary: #b4a8aa; - --text-tertiary: #7a6a6c; - --border-color: rgba(255, 255, 255, 0.1); - --bg-gradient: linear-gradient(135deg, #1a1416 0%, #2a2022 100%); - --primary-gradient: linear-gradient(135deg, #8B4049 0%, #C06068 100%); - --card-bg: rgba(42, 32, 34, 0.9); - --card-inner-bg: #281F21; - --sent-card-bg: var(--primary); -} - -// 明水鸭色 - 深色 -[data-theme="teal-water"][data-mode="dark"] { - --primary: #7ABAAA; - --primary-rgb: 122, 186, 170; - --primary-hover: #8ACABA; - --primary-light: rgba(122, 186, 170, 0.15); - --bg-primary: #121a1a; - --bg-secondary: rgba(28, 42, 42, 0.9); - --bg-secondary-solid: #1c2a2a; - --bg-tertiary: rgba(255, 255, 255, 0.05); - --bg-hover: rgba(255, 255, 255, 0.08); - --text-primary: #E4F0F0; - --text-secondary: #a0b4b4; - --text-tertiary: #607a7a; - --border-color: rgba(255, 255, 255, 0.1); - --bg-gradient: linear-gradient(135deg, #121a1a 0%, #1c2a2a 100%); - --primary-gradient: linear-gradient(135deg, #5A8A8A 0%, #7ABAAA 100%); - --card-bg: rgba(28, 42, 42, 0.9); - --card-inner-bg: #1B2828; - --sent-card-bg: var(--primary); -} - -// 繁花如梦 - 深色(夜阑幽梦) -[data-theme="blossom-dream"][data-mode="dark"] { - // 光晕色(供伪元素使用,降低饱和度避免刺眼) - --blossom-pink: #C670C3; - --blossom-purple: #5F4B8B; - --blossom-blue: #3A2A50; - - // 主品牌色:藕粉/烟紫粉,降饱和度不刺眼 - --primary: #D19EBB; - --primary-rgb: 209, 158, 187; - --primary-hover: #DDB0C8; - --primary-light: rgba(209, 158, 187, 0.15); - - // 背景三层:极深黑灰底(去掉紫薯色),面板略浅,卡片再浅一级 - --bg-primary: #151316; - --bg-secondary: rgba(34, 30, 36, 0.92); - --bg-secondary-solid: #221E24; - --bg-tertiary: rgba(255, 255, 255, 0.04); - --bg-hover: rgba(209, 158, 187, 0.1); - - // 文字 - --text-primary: #F0EAF4; - --text-secondary: #A898AE; - --text-tertiary: #6A5870; - // 边框:极细白色内发光,剥离层级 - --border-color: rgba(255, 255, 255, 0.07); - - --bg-gradient: linear-gradient(150deg, #151316 0%, #1A1620 50%, #131018 100%); - --primary-gradient: linear-gradient(135deg, #D19EBB 0%, #A878A8 100%); - - // 卡片:比面板更亮一档,用深灰而非紫色 - --card-bg: rgba(42, 38, 46, 0.92); - --card-inner-bg: rgba(52, 48, 56, 0.96); - - --sent-card-bg: var(--primary); -} - -// Geist · 极简黑白 - 深色 -[data-theme="geist"][data-mode="dark"] { - --primary: #ededed; - --primary-rgb: 237, 237, 237; - --primary-hover: #d5d5d5; - --primary-light: rgba(237, 237, 237, 0.1); - --bg-primary: #1a1a1a; - --bg-secondary: rgba(34, 34, 34, 0.95); - --bg-secondary-solid: #222222; - --bg-tertiary: rgba(255, 255, 255, 0.04); - --bg-hover: rgba(255, 255, 255, 0.07); - --text-primary: #ededed; - --text-secondary: #999999; - --text-tertiary: #666666; - --border-color: #2e2e2e; - --border-radius: 6px; - --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4); - --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.5); - --bg-gradient: linear-gradient(135deg, #1a1a1a 0%, #222222 100%); - --primary-gradient: linear-gradient(135deg, #ededed 0%, #cccccc 100%); - --card-bg: rgba(34, 34, 34, 0.95); - --card-inner-bg: #2a2a2a; - --sent-card-bg: #3a3a3a; - // primary 是浅灰色,上方文字需要用深色 - --on-primary: #111111; -} - -// 重置样式 +// ============================================= +// Global reset +// ============================================= * { margin: 0; padding: 0; @@ -412,12 +118,13 @@ body { user-select: none; } - #app { height: 100%; } -// 滚动条样式 +// ============================================= +// Scrollbar +// ============================================= ::-webkit-scrollbar { width: 6px; height: 6px; @@ -436,17 +143,21 @@ body { } } -// 按钮基础样式 +// ============================================= +// Button base +// ============================================= .btn { display: inline-flex; align-items: center; justify-content: center; + gap: 6px; padding: 8px 16px; border: none; - border-radius: var(--border-radius); + border-radius: 8px; font-size: 14px; + font-weight: 500; cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease, opacity 0.15s ease; &-primary { background: var(--primary); @@ -455,6 +166,11 @@ body { &:hover { background: var(--primary-hover); } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } } &-secondary { @@ -462,20 +178,28 @@ body { color: var(--text-primary); &:hover { - background: var(--border-color); + background: var(--bg-hover); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; } } } -// 卡片样式 +// ============================================= +// Card base +// ============================================= .card { - background: var(--bg-secondary); - border-radius: 16px; - box-shadow: var(--shadow-sm); + background: var(--card-bg); + border-radius: 12px; padding: 16px; } -// 全局 Switch 开关样式 +// ============================================= +// Switch toggle +// ============================================= .switch { position: relative; display: inline-block; @@ -525,7 +249,6 @@ body { } } - // 禁用状态 input:disabled+.switch-slider { opacity: 0.5; cursor: not-allowed; From 72beca65bb608914290caf2859ffd1cf5ff04c76 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 4 May 2026 13:29:07 +0800 Subject: [PATCH 02/23] fix(theme): restore accent color system, redesign sidebar and report pages --- src/App.tsx | 26 +++-- src/components/Sidebar.scss | 75 ++++++++++--- src/pages/AnnualReportPage.scss | 165 +++++++++++++++------------- src/pages/ChatAnalyticsHubPage.scss | 119 ++++++++++---------- src/pages/ChatAnalyticsHubPage.tsx | 46 ++++---- src/pages/DualReportPage.scss | 88 ++++++++------- src/styles/main.scss | 133 +++++++++++++++++++--- 7 files changed, 417 insertions(+), 235 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index f544cdf..5166c70 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,7 @@ import AccountManagementPage from './pages/AccountManagementPage' import BackupPage from './pages/BackupPage' import { useAppStore } from './stores/appStore' -import { useThemeStore, type ThemeMode } from './stores/themeStore' +import { themes, useThemeStore, type ThemeId, type ThemeMode } from './stores/themeStore' import * as configService from './services/config' import * as cloudControl from './services/cloudControl' import { Download, X, Shield } from 'lucide-react' @@ -74,7 +74,7 @@ function App() { setLocked } = useAppStore() - const { themeMode, setThemeMode } = useThemeStore() + const { currentTheme, themeMode, setTheme, setThemeMode } = useThemeStore() const isAgreementWindow = location.pathname === '/agreement-window' const isOnboardingWindow = location.pathname === '/onboarding-window' const isVideoPlayerWindow = location.pathname === '/video-player-window' @@ -149,11 +149,12 @@ function App() { } }, [isOnboardingWindow, isNotificationWindow, isAnnualReportWindow, isDualReportWindow]) - // 应用主题模式 (light / dark / system) + // 应用主题 (accent color + light/dark mode) useEffect(() => { const mq = window.matchMedia('(prefers-color-scheme: dark)') const applyMode = (mode: ThemeMode, systemDark?: boolean) => { const effectiveMode = mode === 'system' ? (systemDark ?? mq.matches ? 'dark' : 'light') : mode + document.documentElement.setAttribute('data-theme', currentTheme) document.documentElement.setAttribute('data-mode', effectiveMode) } @@ -167,13 +168,19 @@ function App() { } mq.addEventListener('change', handler) return () => mq.removeEventListener('change', handler) - }, [themeMode, isOnboardingWindow, isNotificationWindow, isAnnualReportWindow, isDualReportWindow]) + }, [currentTheme, themeMode, isOnboardingWindow, isNotificationWindow, isAnnualReportWindow, isDualReportWindow]) // 读取已保存的主题设置 useEffect(() => { const loadTheme = async () => { try { - const savedThemeMode = await configService.getTheme() + const [savedThemeId, savedThemeMode] = await Promise.all([ + configService.getThemeId(), + configService.getTheme() + ]) + if (savedThemeId && themes.some((theme) => theme.id === savedThemeId)) { + setTheme(savedThemeId as ThemeId) + } if (savedThemeMode === 'light' || savedThemeMode === 'dark' || savedThemeMode === 'system') { setThemeMode(savedThemeMode) } @@ -184,20 +191,23 @@ function App() { } } loadTheme() - }, [setThemeMode]) + }, [setTheme, setThemeMode]) // 保存主题设置 useEffect(() => { if (!themeHydrated) return const saveTheme = async () => { try { - await configService.setTheme(themeMode) + await Promise.all([ + configService.setThemeId(currentTheme), + configService.setTheme(themeMode) + ]) } catch (e) { console.error('保存主题配置失败:', e) } } saveTheme() - }, [themeMode, themeHydrated]) + }, [currentTheme, themeMode, themeHydrated]) // 检查是否已同意协议 useEffect(() => { diff --git a/src/components/Sidebar.scss b/src/components/Sidebar.scss index d082bd4..0c1d31e 100644 --- a/src/components/Sidebar.scss +++ b/src/components/Sidebar.scss @@ -1,13 +1,14 @@ -// ChatGPT-style sidebar +// Redesigned sidebar — premium feel with left accent bar, refined spacing .sidebar { width: var(--sidebar-width, 260px); background: var(--bg-sidebar, var(--bg-secondary)); display: flex; flex-direction: column; - padding: 8px 0; - transition: width 0.2s ease; + padding: 0; + transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1); flex-shrink: 0; overflow: hidden; + border-right: 1px solid var(--border-color); &.collapsed { width: 68px; @@ -23,21 +24,37 @@ .user-meta { display: none; } + + .user-menu-caret { + display: none; + } } - .nav-menu, - .sidebar-footer { + .nav-menu { padding: 0 8px; } + .sidebar-footer { + padding: 0 8px; + padding-top: 8px; + } + .nav-label { display: none; } + .nav-badge:not(.icon-badge) { + display: none; + } + .nav-item { justify-content: center; padding: 10px; gap: 0; + + &::before { + display: none; + } } } } @@ -47,18 +64,19 @@ flex: 1; display: flex; flex-direction: column; - gap: 2px; - padding: 0 12px; + gap: 1px; + padding: 12px 10px; overflow-y: auto; overflow-x: hidden; } .nav-item { + position: relative; display: flex; align-items: center; gap: 12px; - padding: 10px 12px; - border-radius: 8px; + padding: 9px 14px; + border-radius: 10px; color: var(--text-secondary); text-decoration: none; transition: background 0.15s ease, color 0.15s ease; @@ -68,6 +86,21 @@ cursor: pointer; font-family: inherit; font-size: 14px; + margin: 1px 0; + + // Left accent bar for active state + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%) scaleY(0); + width: 3px; + height: 16px; + border-radius: 0 2px 2px 0; + background: var(--primary); + transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); + } &:hover { background: var(--bg-hover); @@ -78,6 +111,14 @@ background: var(--bg-hover); color: var(--text-primary); font-weight: 600; + + &::before { + transform: translateY(-50%) scaleY(1); + } + + .nav-icon { + color: var(--primary); + } } } @@ -88,6 +129,7 @@ width: 20px; height: 20px; flex-shrink: 0; + transition: color 0.15s ease; } .nav-icon-with-badge { @@ -129,19 +171,19 @@ // ---- Footer ---- .sidebar-footer { - padding: 0 12px; + padding: 4px 10px; border-top: 1px solid var(--border-color); padding-top: 8px; margin-top: 4px; display: flex; flex-direction: column; - gap: 2px; + gap: 1px; } // ---- User card ---- .sidebar-user-card-wrap { position: relative; - margin: 0 12px 8px; + margin: 0 10px 10px; --sidebar-user-menu-width: 172px; } @@ -153,7 +195,7 @@ width: max(100%, var(--sidebar-user-menu-width)); z-index: 12; border: 1px solid var(--border-color); - border-radius: 10px; + border-radius: 12px; background: var(--bg-secondary-solid, var(--bg-secondary)); display: flex; flex-direction: column; @@ -203,7 +245,7 @@ .sidebar-user-card { width: 100%; - padding: 10px; + padding: 10px 12px; border-radius: 10px; background: transparent; display: flex; @@ -223,8 +265,8 @@ } .user-avatar { - width: 32px; - height: 32px; + width: 34px; + height: 34px; border-radius: 50%; overflow: hidden; background: var(--primary); @@ -232,6 +274,7 @@ align-items: center; justify-content: center; flex-shrink: 0; + box-shadow: 0 0 0 2px var(--bg-sidebar, var(--bg-secondary)); img { width: 100%; diff --git a/src/pages/AnnualReportPage.scss b/src/pages/AnnualReportPage.scss index 396441c..bdee64f 100644 --- a/src/pages/AnnualReportPage.scss +++ b/src/pages/AnnualReportPage.scss @@ -7,6 +7,7 @@ min-height: 100%; text-align: center; padding: 40px 24px; + animation: reportFadeIn 0.35s ease-out; } .annual-report-page.report-route-transitioning > :not(.report-launch-overlay) { @@ -20,40 +21,43 @@ } .page-title { - font-size: 32px; + font-size: 28px; font-weight: 700; color: var(--text-primary); - margin: 0 0 12px; + margin: 0 0 10px; + letter-spacing: -0.5px; } .page-desc { font-size: 15px; color: var(--text-secondary); - margin: 0 0 48px; + margin: 0 0 40px; + line-height: 1.7; } .page-desc.load-summary { - margin: 0 0 28px; + margin: 0 0 24px; } .page-desc.load-summary.complete { color: var(--text-secondary); } +// ---- Load telemetry ---- .load-telemetry { - width: min(760px, 100%); - padding: 12px 14px; - margin: 0 0 28px; - border-radius: 12px; - border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); - background: color-mix(in srgb, var(--card-bg) 92%, transparent); + width: min(620px, 100%); + padding: 12px 16px; + margin: 0 0 24px; + border-radius: 10px; + border: 1px solid var(--border-color); + background: var(--card-bg); text-align: left; font-size: 13px; color: var(--text-secondary); line-height: 1.5; p { - margin: 4px 0; + margin: 3px 0; } .label { @@ -62,31 +66,32 @@ } .load-telemetry.loading { - border-color: color-mix(in srgb, var(--primary) 30%, var(--border-color)); + border-color: color-mix(in srgb, var(--primary) 25%, var(--border-color)); } .load-telemetry.complete { - border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color)); + border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color)); } .load-telemetry.compact { margin: 12px 0 0; - width: min(560px, 100%); + width: min(500px, 100%); } +// ---- Report sections ---- .report-sections { display: flex; flex-direction: column; - gap: 32px; - width: min(760px, 100%); + gap: 20px; + width: min(620px, 100%); } .report-section { width: 100%; background: var(--card-bg); border: 1px solid var(--border-color); - border-radius: 20px; - padding: 28px; + border-radius: 16px; + padding: 24px; text-align: left; } @@ -95,57 +100,57 @@ align-items: flex-start; justify-content: space-between; gap: 16px; - margin-bottom: 20px; + margin-bottom: 16px; } .section-title { margin: 0; - font-size: 20px; + font-size: 17px; font-weight: 700; color: var(--text-primary); } .section-desc { - margin: 8px 0 0; - font-size: 14px; + margin: 6px 0 0; + font-size: 13px; color: var(--text-tertiary); } .section-badge { display: inline-flex; align-items: center; - gap: 6px; - padding: 6px 10px; + gap: 5px; + padding: 4px 10px; border-radius: 999px; - background: color-mix(in srgb, var(--primary) 12%, transparent); + background: var(--primary-light); color: var(--primary); - border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent); font-size: 12px; font-weight: 600; white-space: nowrap; } .section-hint { - margin: 12px 0 0; + margin: 10px 0 0; font-size: 12px; color: var(--text-tertiary); } +// ---- Year cards ---- .year-grid-with-status { display: flex; align-items: flex-start; justify-content: space-between; - gap: 16px; - margin-bottom: 24px; + gap: 12px; + margin-bottom: 20px; } .year-grid { display: flex; flex-wrap: wrap; - gap: 16px; + gap: 10px; justify-content: center; max-width: 600px; - margin-bottom: 48px; + margin-bottom: 40px; } .report-section .year-grid { @@ -169,7 +174,7 @@ } .year-load-status.complete { - color: color-mix(in srgb, var(--primary) 80%, var(--text-secondary)); + color: var(--primary); } .dot-ellipsis { @@ -187,32 +192,33 @@ } .year-card { - width: 120px; - height: 100px; + width: 88px; + height: 64px; display: flex; flex-direction: column; align-items: center; justify-content: center; - background: var(--card-bg); - border: 2px solid var(--border-color); - border-radius: 16px; + background: transparent; + border: 1px solid var(--border-color); + border-radius: 10px; cursor: pointer; - transition: all 0.2s; + transition: all 0.15s ease; + gap: 2px; &:hover { - border-color: var(--primary); - transform: translateY(-2px); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.1); + border-color: var(--text-tertiary); + background: var(--bg-hover); } &.disabled { pointer-events: none; - opacity: 0.72; + opacity: 0.6; } &.selected { border-color: var(--primary); background: var(--primary-light); + box-shadow: 0 0 0 1px var(--primary); .year-number { color: var(--primary); @@ -220,45 +226,41 @@ } .year-number { - font-size: 32px; + font-size: 22px; font-weight: 700; color: var(--text-primary); line-height: 1; } .year-label { - font-size: 14px; + font-size: 11px; color: var(--text-tertiary); - margin-top: 4px; } } +// ---- Generate button ---- .generate-btn { display: flex; align-items: center; - gap: 10px; - padding: 16px 40px; - background: linear-gradient(135deg, var(--primary) 0%, color-mix(in srgb, var(--primary) 80%, #000) 100%); + justify-content: center; + gap: 8px; + width: 100%; + padding: 12px 24px; + background: var(--primary); border: none; - border-radius: 50px; - color: #fff; - font-size: 16px; + border-radius: 10px; + color: var(--on-primary); + font-size: 15px; font-weight: 600; cursor: pointer; - transition: all 0.2s; - box-shadow: 0 4px 16px color-mix(in srgb, var(--primary) 30%, transparent); + transition: opacity 0.15s ease; &:hover:not(:disabled) { - transform: translateY(-2px); - box-shadow: 0 8px 24px color-mix(in srgb, var(--primary) 40%, transparent); - } - - &:active:not(:disabled) { - transform: translateY(0); + opacity: 0.9; } &:disabled { - opacity: 0.6; + opacity: 0.5; cursor: not-allowed; } @@ -267,13 +269,18 @@ } &.secondary { - background: var(--card-bg); + background: var(--bg-tertiary); color: var(--text-primary); border: 1px solid var(--border-color); - box-shadow: none; + + &:hover:not(:disabled) { + background: var(--bg-hover); + opacity: 1; + } } } +// ---- Launch overlay ---- .report-launch-overlay { position: fixed; inset: 0; @@ -281,9 +288,9 @@ display: flex; align-items: center; justify-content: center; - background: color-mix(in srgb, var(--bg-primary) 78%, transparent); + background: color-mix(in srgb, var(--bg-primary) 80%, transparent); backdrop-filter: blur(8px); - animation: report-launch-overlay-in 420ms ease-out both; + animation: report-launch-overlay-in 350ms ease-out both; } .launch-core { @@ -293,13 +300,13 @@ gap: 10px; text-align: center; color: var(--text-primary); - animation: report-launch-core-in 420ms cubic-bezier(0.2, 0.8, 0.2, 1) both; + animation: report-launch-core-in 350ms cubic-bezier(0.2, 0.8, 0.2, 1) both; } .launch-title { margin: 4px 0 0; - font-size: 18px; - font-weight: 650; + font-size: 17px; + font-weight: 600; } .launch-subtitle { @@ -312,6 +319,7 @@ animation: spin 1s linear infinite; } +// ---- Animations ---- @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } @@ -329,27 +337,34 @@ } to { opacity: 0; - filter: blur(8px); + filter: blur(6px); transform: scale(0.985); } } @keyframes report-launch-overlay-in { - from { - opacity: 0; - } - to { - opacity: 1; - } + from { opacity: 0; } + to { opacity: 1; } } @keyframes report-launch-core-in { from { opacity: 0; - transform: translateY(18px) scale(0.96); + transform: translateY(14px) scale(0.97); } to { opacity: 1; transform: translateY(0) scale(1); } } + +@keyframes reportFadeIn { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/src/pages/ChatAnalyticsHubPage.scss b/src/pages/ChatAnalyticsHubPage.scss index f6621a7..e84a14d 100644 --- a/src/pages/ChatAnalyticsHubPage.scss +++ b/src/pages/ChatAnalyticsHubPage.scss @@ -7,68 +7,79 @@ } .analytics-hub-inner { - width: min(720px, 100%); - display: flex; - flex-direction: column; - align-items: center; - text-align: center; - animation: analyticsHubFadeIn 0.4s ease-out; + width: min(560px, 100%); + animation: analyticsHubFadeIn 0.35s ease-out; +} + +// ---- Hero ---- +.analytics-hub-hero { + margin-bottom: 40px; } .analytics-hub-title { font-size: 28px; font-weight: 700; color: var(--text-primary); - margin: 0 0 12px; + margin: 0 0 10px; letter-spacing: -0.5px; } .analytics-hub-desc { - max-width: 520px; - margin: 0 0 36px; + margin: 0; color: var(--text-secondary); font-size: 15px; line-height: 1.7; } -.analytics-hub-grid { - width: 100%; - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; -} - -.analytics-hub-card { +// ---- Perspectives list ---- +.analytics-hub-perspectives { display: flex; flex-direction: column; - align-items: flex-start; - text-align: left; - gap: 16px; - padding: 24px; - border: 1px solid var(--border-color); + gap: 4px; +} + +.analytics-hub-perspectives-label { + font-size: 12px; + font-weight: 600; + color: var(--text-tertiary); + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 0 4px; + margin-bottom: 8px; +} + +.analytics-hub-row { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + border: none; border-radius: 12px; - background: var(--card-bg); + background: transparent; color: var(--text-primary); cursor: pointer; - transition: background 0.15s ease, border-color 0.15s ease; + text-align: left; + transition: background 0.15s ease; + width: 100%; &:hover { background: var(--bg-hover); - border-color: var(--text-tertiary); - .analytics-hub-card-arrow { + .analytics-hub-row-arrow { transform: translateX(3px); + color: var(--text-secondary); } } } -.analytics-hub-card-icon { - width: 44px; - height: 44px; - border-radius: 10px; +.analytics-hub-row-icon { + width: 40px; + height: 40px; + border-radius: 50%; display: flex; align-items: center; justify-content: center; + flex-shrink: 0; background: var(--primary-light); color: var(--primary); @@ -78,53 +89,39 @@ } } -[data-mode="dark"] .analytics-hub-card-icon--group { +[data-mode="dark"] .analytics-hub-row-icon--group { background: rgba(96, 165, 250, 0.12); color: #60a5fa; } -.analytics-hub-card-body { - display: flex; - flex-direction: column; - gap: 8px; - width: 100%; +.analytics-hub-row-body { + flex: 1; + min-width: 0; } -.analytics-hub-card-header { - display: flex; - align-items: center; - justify-content: space-between; - - h2 { - margin: 0; - font-size: 18px; - font-weight: 600; - line-height: 1.2; - } +.analytics-hub-row-title { + font-size: 15px; + font-weight: 600; + color: var(--text-primary); + margin-bottom: 2px; } -.analytics-hub-card-arrow { - color: var(--text-tertiary); - transition: transform 0.15s ease; -} - -.analytics-hub-card-body p { - margin: 0; - color: var(--text-secondary); +.analytics-hub-row-desc { font-size: 13px; - line-height: 1.6; + color: var(--text-tertiary); + line-height: 1.5; } -@media (max-width: 640px) { - .analytics-hub-grid { - grid-template-columns: 1fr; - } +.analytics-hub-row-arrow { + color: var(--text-tertiary); + flex-shrink: 0; + transition: transform 0.15s ease, color 0.15s ease; } @keyframes analyticsHubFadeIn { from { opacity: 0; - transform: translateY(10px); + transform: translateY(8px); } to { opacity: 1; diff --git a/src/pages/ChatAnalyticsHubPage.tsx b/src/pages/ChatAnalyticsHubPage.tsx index 3e21c73..4bc514e 100644 --- a/src/pages/ChatAnalyticsHubPage.tsx +++ b/src/pages/ChatAnalyticsHubPage.tsx @@ -8,44 +8,44 @@ function ChatAnalyticsHubPage() { return (
-

聊天分析

-

- 选择你要进入的分析视角。私聊分析适合查看好友聊天统计,群聊分析则用于查看群成员活跃度。 -

+
+

聊天分析

+

+ 选择你要进入的分析视角,深入了解关系网络、活跃时段与消息趋势。 +

+
+ +
+
视角
-
diff --git a/src/pages/DualReportPage.scss b/src/pages/DualReportPage.scss index 802f0bb..61944a0 100644 --- a/src/pages/DualReportPage.scss +++ b/src/pages/DualReportPage.scss @@ -1,6 +1,7 @@ .dual-report-page { padding: 32px 28px; color: var(--text-primary); + animation: dualFadeIn 0.35s ease-out; } .dual-report-page.loading { @@ -22,25 +23,26 @@ h1 { margin: 0; - font-size: 24px; + font-size: 22px; font-weight: 700; + letter-spacing: -0.3px; } p { - margin: 8px 0 0; + margin: 6px 0 0; color: var(--text-secondary); + font-size: 14px; } } .year-badge { display: inline-flex; align-items: center; - gap: 6px; - padding: 6px 10px; + gap: 5px; + padding: 4px 10px; border-radius: 999px; - background: color-mix(in srgb, var(--primary) 12%, transparent); + background: var(--primary-light); color: var(--primary); - border: 1px solid color-mix(in srgb, var(--primary) 30%, transparent); font-size: 12px; font-weight: 600; white-space: nowrap; @@ -50,11 +52,12 @@ display: flex; align-items: center; gap: 8px; - padding: 10px 12px; + padding: 9px 12px; background: var(--card-bg); border: 1px solid var(--border-color); - border-radius: 12px; - margin-bottom: 20px; + border-radius: 10px; + margin-bottom: 16px; + color: var(--text-tertiary); input { flex: 1; @@ -69,7 +72,7 @@ .ranking-list { display: flex; flex-direction: column; - gap: 12px; + gap: 4px; } .ranking-item { @@ -77,42 +80,41 @@ grid-template-columns: auto auto 1fr auto; align-items: center; gap: 12px; - padding: 12px 16px; - background: var(--card-bg); - border: 1px solid var(--border-color); - border-radius: 14px; + padding: 10px 14px; + background: transparent; + border: none; + border-radius: 10px; text-align: left; cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease; + width: 100%; &:hover { - border-color: var(--primary); - box-shadow: 0 8px 20px rgba(0, 0, 0, 0.08); - transform: translateY(-1px); + background: var(--bg-hover); } } .rank-badge { - width: 28px; - height: 28px; + width: 26px; + height: 26px; border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; - background: var(--border-color); + background: var(--bg-tertiary); color: var(--text-secondary); font-size: 12px; font-weight: 700; &.top { - background: color-mix(in srgb, var(--primary) 18%, transparent); - color: var(--primary); + background: var(--primary); + color: var(--on-primary); } } .avatar { - width: 40px; - height: 40px; + width: 36px; + height: 36px; border-radius: 50%; overflow: hidden; background: var(--primary-light); @@ -121,6 +123,7 @@ justify-content: center; color: var(--primary); font-weight: 700; + font-size: 14px; img { width: 100%; @@ -132,11 +135,11 @@ .info { display: flex; flex-direction: column; - gap: 2px; - min-width: 0; // 允许 flex 子项缩小,配合 ellipsis + gap: 1px; + min-width: 0; .name { - font-size: 15px; + font-size: 14px; font-weight: 600; color: var(--text-primary); white-space: nowrap; @@ -146,29 +149,29 @@ .sub { font-size: 12px; - color: var(--text-secondary); // 从 tertiary 改为 secondary 以增强对比度 + color: var(--text-tertiary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - opacity: 0.8; } } .meta { text-align: right; font-size: 12px; - color: var(--text-secondary); // 改为 secondary + color: var(--text-secondary); flex-shrink: 0; .count { - font-size: 14px; + font-size: 13px; font-weight: 700; - color: var(--primary); // 使用主题色更醒目 - margin-bottom: 2px; + color: var(--primary); + margin-bottom: 1px; } .hint { - opacity: 0.7; + color: var(--text-tertiary); + font-size: 11px; } } @@ -176,6 +179,7 @@ text-align: center; color: var(--text-tertiary); padding: 40px 0; + font-size: 14px; } .spin { @@ -183,11 +187,17 @@ } @keyframes spin { - from { - transform: rotate(0deg); - } + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} +@keyframes dualFadeIn { + from { + opacity: 0; + transform: translateY(8px); + } to { - transform: rotate(360deg); + opacity: 1; + transform: translateY(0); } } \ No newline at end of file diff --git a/src/styles/main.scss b/src/styles/main.scss index 8b19126..5d0b7d4 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -1,6 +1,5 @@ // WeFlow — ChatGPT-inspired design token system -// Only light/dark/system modes. No per-theme variants. -// All legacy variable names preserved for backward compatibility. +// Accent colors per theme, backgrounds unified across themes. @use './chat-patterns.scss'; // ============================================= @@ -8,7 +7,7 @@ // ============================================= :root, [data-mode="light"] { - // ---- Accent / Brand ---- + // ---- Accent / Brand (default = ChatGPT green) ---- --primary: #10a37f; --primary-rgb: 16, 163, 127; --primary-hover: #0d8a6c; @@ -18,7 +17,7 @@ --danger: #ef4444; --warning: #f59e0b; - // ---- Backgrounds ---- + // ---- Backgrounds (same for all themes) ---- --bg-primary: #ffffff; --bg-secondary: #f9f9f9; --bg-secondary-solid: #f9f9f9; @@ -56,7 +55,6 @@ // Dark mode // ============================================= [data-mode="dark"] { - // ---- Accent / Brand ---- --primary: #10a37f; --primary-rgb: 16, 163, 127; --primary-hover: #1ab893; @@ -66,7 +64,6 @@ --danger: #f87171; --warning: #fbbf24; - // ---- Backgrounds ---- --bg-primary: #212121; --bg-secondary: #2f2f2f; --bg-secondary-solid: #2f2f2f; @@ -75,27 +72,137 @@ --bg-sidebar: #171717; --bg-gradient: linear-gradient(180deg, #212121 0%, #1a1a1a 100%); - // ---- Text ---- --text-primary: #ececec; --text-secondary: #b4b4b4; --text-tertiary: #676767; - // ---- Borders ---- --border-color: rgba(255, 255, 255, 0.08); - // ---- Shadows ---- --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3); --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.4); - // ---- Cards ---- --card-bg: #2f2f2f; --card-inner-bg: #353535; --sent-card-bg: var(--primary); - - // ---- On-primary foreground ---- --on-primary: #ffffff; +} - --primary-gradient: linear-gradient(135deg, #10a37f 0%, #1ab893 100%); +// ============================================= +// Accent color themes (override --primary only) +// Backgrounds/text stay the same — like ChatGPT's accent system. +// ============================================= + +// 云上舞白 — warm brown +[data-theme="cloud-dancer"] { + --primary: #8B7355; + --primary-rgb: 139, 115, 85; + --primary-hover: #7A6548; + --primary-light: rgba(139, 115, 85, 0.1); + --primary-gradient: linear-gradient(135deg, #8B7355 0%, #A68B5B 100%); +} +[data-theme="cloud-dancer"][data-mode="dark"] { + --primary: #C9A86C; + --primary-rgb: 201, 168, 108; + --primary-hover: #D9B87C; + --primary-light: rgba(201, 168, 108, 0.15); + --primary-gradient: linear-gradient(135deg, #8B7355 0%, #C9A86C 100%); +} + +// 繁花如梦 — rose pink +[data-theme="blossom-dream"] { + --primary: #D4849A; + --primary-rgb: 212, 132, 154; + --primary-hover: #C4748A; + --primary-light: rgba(212, 132, 154, 0.12); + --primary-gradient: linear-gradient(135deg, #D4849A 0%, #E8A8B8 100%); +} +[data-theme="blossom-dream"][data-mode="dark"] { + --primary: #D19EBB; + --primary-rgb: 209, 158, 187; + --primary-hover: #DDB0C8; + --primary-light: rgba(209, 158, 187, 0.15); + --primary-gradient: linear-gradient(135deg, #D19EBB 0%, #A878A8 100%); +} + +// 刚玉蓝 — steel blue +[data-theme="corundum-blue"] { + --primary: #4A6670; + --primary-rgb: 74, 102, 112; + --primary-hover: #3D565E; + --primary-light: rgba(74, 102, 112, 0.1); + --primary-gradient: linear-gradient(135deg, #4A6670 0%, #5A7A86 100%); +} +[data-theme="corundum-blue"][data-mode="dark"] { + --primary: #6A9AAA; + --primary-rgb: 106, 154, 170; + --primary-hover: #7AAABA; + --primary-light: rgba(106, 154, 170, 0.15); + --primary-gradient: linear-gradient(135deg, #4A6670 0%, #6A9AAA 100%); +} + +// 冰猕猴桃汁绿 — kiwi green +[data-theme="kiwi-green"] { + --primary: #7A9A5C; + --primary-rgb: 122, 154, 92; + --primary-hover: #6A8A4C; + --primary-light: rgba(122, 154, 92, 0.1); + --primary-gradient: linear-gradient(135deg, #7A9A5C 0%, #8AAA6C 100%); +} +[data-theme="kiwi-green"][data-mode="dark"] { + --primary: #9ABA7C; + --primary-rgb: 154, 186, 124; + --primary-hover: #AACA8C; + --primary-light: rgba(154, 186, 124, 0.15); + --primary-gradient: linear-gradient(135deg, #7A9A5C 0%, #9ABA7C 100%); +} + +// 辛辣红 — spicy red +[data-theme="spicy-red"] { + --primary: #8B4049; + --primary-rgb: 139, 64, 73; + --primary-hover: #7A3540; + --primary-light: rgba(139, 64, 73, 0.1); + --primary-gradient: linear-gradient(135deg, #8B4049 0%, #A05058 100%); +} +[data-theme="spicy-red"][data-mode="dark"] { + --primary: #C06068; + --primary-rgb: 192, 96, 104; + --primary-hover: #D07078; + --primary-light: rgba(192, 96, 104, 0.15); + --primary-gradient: linear-gradient(135deg, #8B4049 0%, #C06068 100%); +} + +// 明水鸭色 — teal +[data-theme="teal-water"] { + --primary: #5A8A8A; + --primary-rgb: 90, 138, 138; + --primary-hover: #4A7A7A; + --primary-light: rgba(90, 138, 138, 0.1); + --primary-gradient: linear-gradient(135deg, #5A8A8A 0%, #6A9A9A 100%); +} +[data-theme="teal-water"][data-mode="dark"] { + --primary: #7ABAAA; + --primary-rgb: 122, 186, 170; + --primary-hover: #8ACABA; + --primary-light: rgba(122, 186, 170, 0.15); + --primary-gradient: linear-gradient(135deg, #5A8A8A 0%, #7ABAAA 100%); +} + +// Geist — monochrome +[data-theme="geist"] { + --primary: #444444; + --primary-rgb: 68, 68, 68; + --primary-hover: #333333; + --primary-light: rgba(68, 68, 68, 0.08); + --primary-gradient: linear-gradient(135deg, #444444 0%, #666666 100%); +} +[data-theme="geist"][data-mode="dark"] { + --primary: #ededed; + --primary-rgb: 237, 237, 237; + --primary-hover: #d5d5d5; + --primary-light: rgba(237, 237, 237, 0.1); + --primary-gradient: linear-gradient(135deg, #ededed 0%, #cccccc 100%); + --on-primary: #111111; } // ============================================= From 715718c3e5e3fbfe6ce47ef72387a96f1499daa3 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 4 May 2026 15:58:31 +0800 Subject: [PATCH 03/23] style(sidebar): narrow width from 260px to 200px --- src/styles/main.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/main.scss b/src/styles/main.scss index 5d0b7d4..4054f31 100644 --- a/src/styles/main.scss +++ b/src/styles/main.scss @@ -48,7 +48,7 @@ --on-primary: #ffffff; // ---- Layout ---- - --sidebar-width: 260px; + --sidebar-width: 200px; } // ============================================= From b314fc55f950c859e8fcd1270abfdc77ec088e04 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 4 May 2026 16:25:33 +0800 Subject: [PATCH 04/23] refactor(ui): apply ChatGPT minimalist styling to SettingsPage --- src/pages/SettingsPage.scss | 854 ++++++++++++------------------------ 1 file changed, 272 insertions(+), 582 deletions(-) diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index ecf1d8d..273e9ed 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -1,100 +1,65 @@ -.settings-modal-overlay { +.settings-modal-overlay { position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; z-index: 2050; display: flex; align-items: center; justify-content: center; - padding: 28px 32px; - background: rgba(15, 23, 42, 0.28); - backdrop-filter: blur(10px); - animation: settingsFadeIn 0.2s ease; + background: rgba(0, 0, 0, 0.4); + animation: settingsFadeIn 0.2s ease-out; &.closing { - animation: settingsFadeOut 0.2s ease forwards; + animation: settingsFadeOut 0.15s ease-in forwards; } } @keyframes settingsFadeIn { - from { - opacity: 0; - backdrop-filter: blur(0); - } - to { - opacity: 1; - backdrop-filter: blur(10px); - } + from { opacity: 0; } + to { opacity: 1; } } @keyframes settingsFadeOut { - from { - opacity: 1; - backdrop-filter: blur(10px); - } - to { - opacity: 0; - backdrop-filter: blur(0); - } + from { opacity: 1; } + to { opacity: 0; } } .settings-page { position: relative; display: flex; - flex-direction: column; - width: min(1160px, calc(100vw - 96px)); - height: min(820px, calc(100vh - 120px)); + width: min(1000px, calc(100vw - 40px)); + height: min(760px, calc(100vh - 40px)); max-height: 100%; - padding: 24px; background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: 24px; - box-shadow: 0 28px 80px rgba(15, 23, 42, 0.22); + border-radius: 16px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px var(--border-color); overflow: hidden; - animation: settingsSlideUp 0.3s ease; + animation: settingsScaleIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); &.closing { - animation: settingsSlideDown 0.2s ease forwards; + animation: settingsScaleOut 0.15s ease-in forwards; } } -@keyframes settingsSlideUp { - from { - opacity: 0; - transform: translateY(30px) scale(0.96); - } - to { - opacity: 1; - transform: translateY(0) scale(1); - } +@keyframes settingsScaleIn { + from { opacity: 0; transform: scale(0.97) translateY(10px); } + to { opacity: 1; transform: scale(1) translateY(0); } } -@keyframes settingsSlideDown { - from { - opacity: 1; - transform: translateY(0) scale(1); - } - to { - opacity: 0; - transform: translateY(20px) scale(0.98); - } +@keyframes settingsScaleOut { + from { opacity: 1; transform: scale(1) translateY(0); } + to { opacity: 0; transform: scale(0.98) translateY(5px); } } .settings-header { + position: absolute; + top: 16px; + right: 16px; + z-index: 10; display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 20px; - margin-bottom: 14px; - flex-shrink: 0; - - h1 { - font-size: 24px; - font-weight: 600; - color: var(--text-primary); - margin: 0; + align-items: center; + + .settings-title-block h1 { + display: none; } } @@ -110,78 +75,76 @@ } .settings-close-btn { - width: 36px; - height: 36px; - padding: 0; - border: 1px solid var(--border-color); - border-radius: 10px; - background: var(--bg-secondary); + width: 32px; + height: 32px; + border: none; + border-radius: 8px; + background: transparent; color: var(--text-secondary); - display: inline-flex; + display: flex; align-items: center; justify-content: center; cursor: pointer; - transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease; + transition: all 0.15s ease; &:hover { - background: var(--bg-tertiary); + background: var(--bg-hover); color: var(--text-primary); - border-color: rgba(139, 115, 85, 0.28); } } .settings-layout { - flex: 1; - min-height: 0; display: flex; - gap: 20px; - overflow: hidden; + width: 100%; + height: 100%; } .settings-tabs { - display: flex; - flex-direction: column; - gap: 6px; - padding: 12px; width: 220px; flex-shrink: 0; - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 20px; + background: var(--bg-sidebar, var(--bg-secondary)); + padding: 24px 12px; + display: flex; + flex-direction: column; + gap: 2px; overflow-y: auto; + border-right: 1px solid var(--border-color); .tab-btn { display: flex; align-items: center; - gap: 6px; + gap: 10px; width: 100%; - justify-content: flex-start; - padding: 11px 14px; + padding: 10px 12px; border: none; - border-radius: 12px; + border-radius: 8px; font-size: 14px; font-weight: 500; - cursor: pointer; - transition: all 0.2s; - background: transparent; color: var(--text-secondary); + background: transparent; + cursor: pointer; + transition: all 0.15s ease; + text-align: left; &:hover { + background: var(--bg-hover); color: var(--text-primary); - background: var(--bg-secondary); } &.active { - background: var(--card-bg); - color: var(--primary); - box-shadow: var(--shadow-sm); + background: var(--bg-hover); + color: var(--text-primary); + font-weight: 600; } } .tab-group { display: flex; flex-direction: column; - gap: 4px; + gap: 2px; + margin-top: 8px; + border-top: 1px solid var(--border-color); + padding-top: 8px; } .tab-group-trigger { @@ -190,73 +153,44 @@ .tab-group-arrow { margin-left: auto; - color: var(--text-tertiary); transition: transform 0.2s ease; - - &.expanded { - transform: rotate(180deg); - } - } - - .tab-sublist { - display: flex; - flex-direction: column; - gap: 4px; - padding-left: 8px; + &.expanded { transform: rotate(180deg); } } .tab-sublist-wrap { display: grid; grid-template-rows: 0fr; - opacity: 0; - transition: grid-template-rows 0.22s ease, opacity 0.18s ease; - - &.expanded { - grid-template-rows: 1fr; - opacity: 1; - } - - &.collapsed { - pointer-events: none; - } + transition: grid-template-rows 0.2s ease; + &.expanded { grid-template-rows: 1fr; } + &.collapsed { pointer-events: none; } } .tab-sublist { - min-height: 0; overflow: hidden; + display: flex; + flex-direction: column; + gap: 2px; + padding-left: 12px; + margin-top: 2px; } .tab-sub-btn { - padding-left: 24px; font-size: 13px; + padding: 8px 12px; } - .tab-sub-dot { - width: 5px; - height: 5px; - border-radius: 999px; - background: color-mix(in srgb, var(--text-tertiary) 70%, transparent); - } + .tab-sub-dot { display: none; } } .settings-body { flex: 1; + padding: 32px 40px; overflow-y: auto; min-width: 0; - padding-right: 8px; - &::-webkit-scrollbar { - width: 6px; - } - - &::-webkit-scrollbar-track { - background: transparent; - } - - &::-webkit-scrollbar-thumb { - background: var(--border-color); - border-radius: 3px; - } + &::-webkit-scrollbar { width: 6px; } + &::-webkit-scrollbar-track { background: transparent; } + &::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 3px; } } .ai-prompt-textarea { @@ -266,16 +200,19 @@ } .tab-content { - background: var(--bg-secondary); - border: 1px solid var(--border-color); - border-radius: 20px; - padding: 24px; - min-height: 100%; + background: transparent; + border: none; + padding: 0; + min-height: auto; + max-width: 680px; + margin: 0 auto; + padding-bottom: 40px; .section-desc { - font-size: 13px; + font-size: 14px; color: var(--text-tertiary); - margin: 0 0 20px; + margin: 0 0 24px; + line-height: 1.6; } } @@ -307,7 +244,7 @@ } .form-group { - margin-bottom: 20px; + margin-bottom: 24px; &:last-child { margin-bottom: 0; @@ -316,21 +253,23 @@ label { display: block; font-size: 14px; - font-weight: 500; + font-weight: 600; color: var(--text-primary); - margin-bottom: 2px; + margin-bottom: 6px; .optional { font-weight: 400; color: var(--text-tertiary); + margin-left: 4px; } } .form-hint { display: block; - font-size: 12px; + font-size: 13px; color: var(--text-tertiary); - margin-bottom: 8px; + margin-bottom: 12px; + line-height: 1.5; } .status-text { @@ -341,16 +280,16 @@ .mac-key-faq-link { border: none; background: transparent; - color: #0f62fe; + color: var(--text-primary); text-decoration: underline; cursor: pointer; - font-size: 12px; + font-size: 13px; padding: 0; margin-top: 6px; display: inline-block; &:hover { - opacity: 0.9; + opacity: 0.8; } } @@ -382,22 +321,21 @@ input:not(.filter-search-box input) { width: 100%; - padding: 10px 16px; + padding: 10px 14px; border: 1px solid var(--border-color); - border-radius: 9999px; + border-radius: 10px; font-size: 14px; - background: var(--bg-primary); + background: transparent; color: var(--text-primary); + transition: border-color 0.15s ease, box-shadow 0.15s ease; margin-bottom: 10px; &:focus { outline: none; border-color: var(--primary); + box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 15%, transparent); } - - &::placeholder { - color: var(--text-tertiary); - } + &::placeholder { color: var(--text-tertiary); } &:read-only { cursor: pointer; @@ -409,9 +347,9 @@ padding: 10px 16px; padding-right: 36px; border: 1px solid var(--border-color); - border-radius: 9999px; + border-radius: 10px; font-size: 14px; - background: var(--bg-primary); + background: transparent; color: var(--text-primary); margin-bottom: 10px; cursor: pointer; @@ -449,28 +387,22 @@ margin-bottom: 10px; } - .custom-select-trigger { + .custom-select-trigger, .select-trigger, .settings-time-range-trigger { display: flex; align-items: center; justify-content: space-between; width: 100%; - padding: 10px 16px; + padding: 10px 14px; border: 1px solid var(--border-color); - border-radius: 9999px; + border-radius: 10px; font-size: 14px; - background: var(--bg-primary); + background: transparent; color: var(--text-primary); cursor: pointer; - transition: all 0.2s; + transition: border-color 0.15s ease; - &:hover { - border-color: var(--text-tertiary); - } - - &.open { - border-color: var(--primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); - } + &:hover { border-color: var(--text-tertiary); } + &.open { border-color: var(--primary); } } .custom-select-value { @@ -486,63 +418,52 @@ } } - .custom-select-dropdown { + .custom-select-dropdown, .select-dropdown { position: absolute; - top: calc(100% + 6px); + top: calc(100% + 4px); left: 0; right: 0; - background: color-mix(in srgb, var(--bg-primary) 90%, var(--bg-secondary)); + background: var(--bg-secondary); border: 1px solid var(--border-color); - border-radius: 12px; - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12); - overflow: hidden; + border-radius: 10px; + box-shadow: 0 4px 16px rgba(0,0,0,0.1); + padding: 4px; z-index: 100; - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); - - // 展开收起动画 opacity: 0; visibility: hidden; - transform: translateY(-8px) scaleY(0.95); - transform-origin: top center; - transition: all 0.2s cubic-bezier(0.2, 0, 0.2, 1); + transform: translateY(-4px); + transition: all 0.15s ease; &.open { opacity: 1; visibility: visible; - transform: translateY(0) scaleY(1); - } - } - - .custom-select-option { - display: flex; - align-items: center; - justify-content: space-between; - padding: 12px 16px; - font-size: 14px; - color: var(--text-primary); - cursor: pointer; - transition: background 0.15s; - - &:hover { - background: var(--bg-tertiary); + transform: translateY(0); } - &.selected { - color: var(--primary); - font-weight: 500; - - svg { + .custom-select-option, .select-option { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-radius: 6px; + font-size: 13px; + color: var(--text-primary); + cursor: pointer; + border: none; + background: transparent; + + &:hover { background: var(--bg-hover); } + &.selected, &.active { + background: var(--bg-hover); color: var(--primary); + font-weight: 500; + svg { color: var(--primary); } } - } - svg { - flex-shrink: 0; + svg { flex-shrink: 0; } } } - .select-field { position: relative; margin-bottom: 10px; @@ -552,33 +473,7 @@ margin-bottom: 10px; } - .settings-time-range-trigger { - width: 100%; - padding: 10px 16px; - border: 1px solid var(--border-color); - border-radius: 9999px; - font-size: 14px; - background: var(--bg-primary); - color: var(--text-primary); - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - cursor: pointer; - transition: all 0.2s; - - &:hover { - border-color: rgba(var(--primary-rgb), 0.45); - color: var(--primary); - } - - &.open { - border-color: var(--primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); - } - } - - .settings-time-range-value { + .settings-time-range-value, .select-value { flex: 1; min-width: 0; text-align: left; @@ -593,82 +488,6 @@ line-height: 1; } - .select-trigger { - width: 100%; - padding: 10px 16px; - border: 1px solid var(--border-color); - border-radius: 9999px; - font-size: 14px; - background: var(--bg-primary); - color: var(--text-primary); - display: flex; - align-items: center; - justify-content: space-between; - gap: 8px; - cursor: pointer; - transition: all 0.2s; - - &:hover { - border-color: var(--text-tertiary); - } - - &.open { - border-color: var(--primary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 15%, transparent); - } - } - - .select-value { - flex: 1; - min-width: 0; - text-align: left; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .select-dropdown { - position: absolute; - top: calc(100% + 6px); - left: 0; - right: 0; - background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary)); - border: 1px solid var(--border-color); - border-radius: 12px; - padding: 6px; - box-shadow: var(--shadow-md); - z-index: 20; - max-height: 320px; - overflow-y: auto; - backdrop-filter: blur(14px); - -webkit-backdrop-filter: blur(14px); - } - - .select-option { - width: 100%; - text-align: left; - display: flex; - flex-direction: column; - gap: 4px; - padding: 10px 12px; - border: none; - border-radius: 10px; - background: transparent; - cursor: pointer; - transition: all 0.15s; - color: var(--text-primary); - font-size: 14px; - - &:hover { - background: var(--bg-tertiary); - } - - &.active { - background: color-mix(in srgb, var(--primary) 12%, transparent); - color: var(--primary); - } - } - .option-label { font-weight: 500; } @@ -940,57 +759,45 @@ backface-visibility: hidden; } -/* Premium Switch Style */ .switch { position: relative; display: inline-block; - width: 44px; - height: 24px; + width: 36px; + height: 20px; flex-shrink: 0; input { - opacity: 0; - width: 0; - height: 0; - - &:checked+.switch-slider { + opacity: 0; width: 0; height: 0; + &:checked + .switch-slider { background-color: var(--primary); - box-shadow: 0 0 8px color-mix(in srgb, var(--primary) 30%, transparent); - + border-color: var(--primary); &::before { - transform: translateX(18px); - background-color: white; + transform: translateX(16px); + background-color: #fff; } } - - &:focus+.switch-slider { - box-shadow: 0 0 1px var(--primary); - } } .switch-slider { position: absolute; cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; background-color: var(--bg-tertiary); - transition: .4s cubic-bezier(0.4, 0, 0.2, 1); - border-radius: 24px; border: 1px solid var(--border-color); + border-radius: 20px; + transition: 0.2s ease; &::before { position: absolute; content: ""; - height: 18px; - width: 18px; + height: 14px; + width: 14px; left: 2px; bottom: 2px; background-color: var(--text-tertiary); - transition: .4s cubic-bezier(0.4, 0, 0.2, 1); border-radius: 50%; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + transition: 0.2s ease; + box-shadow: 0 1px 2px rgba(0,0,0,0.1); } } } @@ -1077,71 +884,53 @@ } .btn { - display: flex; + display: inline-flex; align-items: center; + justify-content: center; gap: 6px; - padding: 8px 20px; + padding: 8px 16px; border: none; - border-radius: 9999px; + border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.2s; + transition: all 0.15s ease; &:disabled { - opacity: 0.6; + opacity: 0.5; cursor: not-allowed; } } .btn-primary { - background: var(--primary); - color: white; - box-shadow: 0 2px 6px color-mix(in srgb, var(--primary) 15%, transparent); + background: var(--text-primary); + color: var(--bg-primary); &:hover:not(:disabled) { - background: var(--primary-hover); - box-shadow: 0 4px 12px color-mix(in srgb, var(--primary) 25%, transparent); - transform: translateY(-1px); - } - - &:active:not(:disabled) { - transform: translateY(0); + opacity: 0.85; } } .btn-secondary { - background: var(--bg-tertiary); + background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); &:hover:not(:disabled) { - background: var(--bg-primary); - border-color: var(--text-tertiary); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); - transform: translateY(-1px); - } - - &:active:not(:disabled) { - transform: translateY(0); + background: var(--bg-tertiary); } } .btn-danger { - background: var(--danger); - color: white; - - &:hover:not(:disabled) { - opacity: 0.9; - } + background: #ef4444; + color: #fff; + &:hover:not(:disabled) { background: #dc2626; } } .btn-sm { - display: flex; - align-items: center; - gap: 4px; padding: 6px 12px; font-size: 13px; + border-radius: 6px; } .btn-row { @@ -1218,36 +1007,34 @@ // 主题选择器 .theme-mode-toggle { - display: flex; - gap: 8px; - margin-bottom: 16px; + display: inline-flex; + background: var(--bg-secondary); padding: 4px; - background: var(--bg-tertiary); - border-radius: 12px; - width: fit-content; + border-radius: 10px; + margin-bottom: 24px; + border: 1px solid var(--border-color); .mode-btn { display: flex; align-items: center; gap: 6px; - padding: 8px 16px; + padding: 6px 16px; border: none; - border-radius: 8px; + background: transparent; + border-radius: 6px; font-size: 13px; font-weight: 500; - cursor: pointer; - transition: all 0.2s; - background: transparent; color: var(--text-secondary); + cursor: pointer; + transition: all 0.15s ease; - &:hover { - color: var(--text-primary); - } + &:hover { color: var(--text-primary); } &.active { - background: var(--card-bg); - color: var(--primary); - box-shadow: var(--shadow-sm); + background: var(--bg-primary); + color: var(--text-primary); + box-shadow: 0 1px 3px rgba(0,0,0,0.08); + border: 1px solid var(--border-color); } } } @@ -1258,171 +1045,104 @@ .quote-layout-picker { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - gap: 12px; - margin-top: 10px; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-top: 12px; } .quote-layout-card { - position: relative; border: 1px solid var(--border-color); - border-radius: 14px; - padding: 14px 14px 12px; - background: color-mix(in srgb, var(--primary) 6%, var(--bg-primary)); - color: inherit; - cursor: pointer; + border-radius: 12px; + padding: 16px; + background: var(--bg-primary); text-align: left; - transition: border-color 0.18s ease, box-shadow 0.18s ease, background 0.18s ease; + cursor: pointer; + transition: all 0.15s ease; + display: flex; + flex-direction: column; + gap: 16px; &:hover { - border-color: color-mix(in srgb, var(--primary) 32%, var(--border-color)); + background: var(--bg-hover); } &.active { - border-color: color-mix(in srgb, var(--primary) 68%, var(--border-color)); - box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 12%, transparent); - background: color-mix(in srgb, var(--primary) 10%, var(--bg-primary)); + border-color: var(--text-primary); + background: var(--bg-secondary); } } +.quote-layout-card-footer { + display: flex; + align-items: center; + gap: 8px; +} + .quote-layout-card-check { - position: absolute; - top: 12px; - left: 12px; - width: 18px; - height: 18px; + width: 16px; + height: 16px; border-radius: 50%; - border: 1.5px solid color-mix(in srgb, var(--primary) 46%, var(--border-color)); - background: transparent; - display: inline-flex; + border: 1px solid var(--border-color); + display: flex; align-items: center; justify-content: center; - flex-shrink: 0; - transition: all 0.2s ease; - - &::after { - content: ''; - width: 7px; - height: 7px; - border-radius: 50%; - background: color-mix(in srgb, var(--primary) 78%, #6f8653); - transform: scale(0); - transition: transform 0.2s ease; - } &.active { - border-color: color-mix(in srgb, var(--primary) 78%, var(--border-color)); - } - - &.active::after { - transform: scale(1); + border-color: var(--text-primary); + &::after { + content: ''; + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-primary); + } } } +.quote-layout-card-title { + font-size: 14px; + font-weight: 500; + color: var(--text-primary); +} + .quote-layout-preview-shell { - padding-left: 22px; - min-height: 72px; + background: var(--bg-secondary); + border-radius: 8px; + padding: 12px; + border: 1px solid var(--border-color); } .quote-layout-preview-chat { display: flex; - align-items: flex-start; + justify-content: flex-end; } -.quote-layout-preview-chat .message-bubble { - display: flex; - gap: 10px; - max-width: 70%; - align-items: flex-start; +.message-bubble { + max-width: 80%; } -.quote-layout-preview-chat .message-bubble.sent { - flex-direction: row-reverse; -} - -.quote-layout-preview-chat .message-bubble.sent .bubble-content { +.bubble-content { background: var(--primary); color: var(--on-primary); - border-radius: 18px 18px 4px 18px; -} - -.quote-layout-preview-chat .bubble-content { - padding: 10px 14px; - font-size: 14px; - line-height: 1.6; - word-break: break-word; - white-space: pre-wrap; -} - -.quote-layout-preview-chat .message-text { - font-size: 14px; - line-height: 1.6; -} - -.quote-layout-preview-chat .quoted-message { - background: rgba(0, 0, 0, 0.04); - border-left: 2px solid var(--primary); - padding: 6px 10px; - border-radius: 4px; + border-radius: 12px; + padding: 10px 12px; font-size: 13px; -} - -.quote-layout-preview-chat .quoted-sender { - color: var(--primary); - font-weight: 500; - margin-right: 4px; -} - -.quote-layout-preview-chat .quoted-sender::after { - content: ':'; -} - -.quote-layout-preview-chat .quoted-text { - color: var(--text-secondary); - white-space: pre-wrap; -} - -.quote-layout-preview-chat .message-bubble.sent .quoted-message { - background: color-mix(in srgb, var(--on-primary) 12%, var(--primary)); - border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary)); -} - -.quote-layout-preview-chat .message-bubble.sent .quoted-sender { - color: color-mix(in srgb, var(--on-primary) 92%, var(--primary)); -} - -.quote-layout-preview-chat .message-bubble.sent .quoted-text { - color: color-mix(in srgb, var(--on-primary) 80%, var(--primary)); -} - -.quote-layout-preview-chat .bubble-content.quote-layout-top .quoted-message { - margin-bottom: 8px; -} - -.quote-layout-preview-chat .bubble-content.quote-layout-bottom .quoted-message { - margin-top: 8px; -} - -.quote-layout-card-footer { - margin-top: 8px; - padding-left: 22px; -} - -.quote-layout-card-title-group { display: flex; flex-direction: column; - gap: 2px; } -.quote-layout-card-title { - font-size: 13px; - font-weight: 600; - color: var(--text-primary); -} +.quote-layout-top .quoted-message { margin-bottom: 6px; } +.quote-layout-bottom .quoted-message { margin-top: 6px; } -.quote-layout-card-desc { - font-size: 11px; - color: var(--text-tertiary); +.quoted-message { + background: rgba(0,0,0,0.15); + border-left: 2px solid rgba(255,255,255,0.4); + padding: 4px 8px; + border-radius: 4px; + font-size: 12px; + + .quoted-sender { font-weight: 500; margin-right: 4px; &::after { content: ':'; } } + .quoted-text { opacity: 0.9; } } @media (max-width: 760px) { @@ -1432,79 +1152,49 @@ } .theme-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 12px; + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-bottom: 32px; } .theme-card { position: relative; - border: 2px solid var(--border-color); - border-radius: 12px; - padding: 8px; + width: 32px; + height: 32px; + border-radius: 50%; cursor: pointer; - transition: all 0.2s; - background: var(--bg-primary); + transition: transform 0.15s ease; + display: flex; + align-items: center; + justify-content: center; - &:hover { - border-color: var(--text-tertiary); - } + &:hover { transform: scale(1.1); } &.active { - border-color: var(--primary); - - .theme-preview { - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); - } + box-shadow: 0 0 0 2px var(--bg-primary), 0 0 0 4px var(--text-primary); } .theme-preview { - height: 60px; - border-radius: 8px; - margin-bottom: 8px; - position: relative; - overflow: hidden; - - .theme-accent { - position: absolute; - bottom: 8px; - right: 8px; - width: 24px; - height: 24px; - border-radius: 50%; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); - } - } - - .theme-info { - display: flex; - flex-direction: column; - gap: 2px; - - .theme-name { - font-size: 13px; - font-weight: 500; - color: var(--text-primary); - } - - .theme-desc { - font-size: 11px; - color: var(--text-tertiary); - } - } - - .theme-check { - position: absolute; - top: 8px; - right: 8px; - width: 20px; - height: 20px; + width: 100%; + height: 100%; border-radius: 50%; - background: var(--primary); - color: white; - display: flex; - align-items: center; - justify-content: center; + overflow: hidden; + margin: 0; + background: transparent !important; + border: none !important; + } + + .theme-info, .theme-check { display: none; } + + .theme-accent { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + border-radius: 50%; + box-shadow: none; + z-index: 2; } } From a7fa088470b85c4289620b5246561521ab08d245 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 4 May 2026 18:13:42 +0800 Subject: [PATCH 05/23] fix(ui): address settings layout issues and tighten industrial design --- src/pages/SettingsPage.scss | 65 +++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 273e9ed..f18308d 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -30,7 +30,7 @@ height: min(760px, calc(100vh - 40px)); max-height: 100%; background: var(--bg-primary); - border-radius: 16px; + border-radius: 12px; box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15), 0 0 0 1px var(--border-color); overflow: hidden; animation: settingsScaleIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); @@ -132,9 +132,21 @@ } &.active { + position: relative; background: var(--bg-hover); color: var(--text-primary); - font-weight: 600; + font-weight: 500; + + &::before { + content: ''; + position: absolute; + left: -8px; + top: 20%; + bottom: 20%; + width: 3px; + background: var(--primary); + border-radius: 0 4px 4px 0; + } } } @@ -142,9 +154,6 @@ display: flex; flex-direction: column; gap: 2px; - margin-top: 8px; - border-top: 1px solid var(--border-color); - padding-top: 8px; } .tab-group-trigger { @@ -184,7 +193,7 @@ .settings-body { flex: 1; - padding: 32px 40px; + padding: 24px 32px; overflow-y: auto; min-width: 0; @@ -245,6 +254,10 @@ .form-group { margin-bottom: 24px; + padding: 16px 20px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: transparent; &:last-child { margin-bottom: 0; @@ -321,10 +334,10 @@ input:not(.filter-search-box input) { width: 100%; - padding: 10px 14px; + padding: 8px 12px; border: 1px solid var(--border-color); - border-radius: 10px; - font-size: 14px; + border-radius: 6px; + font-size: 13px; background: transparent; color: var(--text-primary); transition: border-color 0.15s ease, box-shadow 0.15s ease; @@ -344,11 +357,11 @@ select { width: 100%; - padding: 10px 16px; + padding: 8px 12px; padding-right: 36px; border: 1px solid var(--border-color); - border-radius: 10px; - font-size: 14px; + border-radius: 6px; + font-size: 13px; background: transparent; color: var(--text-primary); margin-bottom: 10px; @@ -392,10 +405,10 @@ align-items: center; justify-content: space-between; width: 100%; - padding: 10px 14px; + padding: 8px 12px; border: 1px solid var(--border-color); - border-radius: 10px; - font-size: 14px; + border-radius: 6px; + font-size: 13px; background: transparent; color: var(--text-primary); cursor: pointer; @@ -887,11 +900,11 @@ display: inline-flex; align-items: center; justify-content: center; - gap: 6px; + gap: 8px; padding: 8px 16px; border: none; - border-radius: 8px; - font-size: 14px; + border-radius: 6px; + font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s ease; @@ -929,8 +942,8 @@ .btn-sm { padding: 6px 12px; - font-size: 13px; - border-radius: 6px; + font-size: 12px; + border-radius: 4px; } .btn-row { @@ -2558,12 +2571,14 @@ .path-selector { display: flex; gap: 8px; - flex-wrap: wrap; + flex-wrap: nowrap; + align-items: center; input { margin-bottom: 0 !important; flex: 1; - min-width: 220px; + min-width: 0; + width: auto !important; font-family: monospace; font-size: 12px; } @@ -2893,8 +2908,8 @@ align-items: flex-start; justify-content: space-between; gap: 18px; - padding: 18px; - border-radius: 18px; + padding: 16px; + border-radius: 10px; border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); background: linear-gradient( 180deg, @@ -2990,7 +3005,7 @@ .anti-revoke-control-card { border: 1px solid color-mix(in srgb, var(--border-color) 90%, transparent); - border-radius: 16px; + border-radius: 10px; padding: 14px; background: color-mix(in srgb, var(--bg-secondary) 95%, var(--bg-primary) 5%); } From 6eb304ef940a12439f0de3a91bf72c31c68a7f4a Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 4 May 2026 19:22:18 +0800 Subject: [PATCH 06/23] fix(ui): apply missing flex wrapping and fix sidebar active indicator --- src/pages/SettingsPage.scss | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index f18308d..6cf1c34 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -111,6 +111,7 @@ border-right: 1px solid var(--border-color); .tab-btn { + position: relative; display: flex; align-items: center; gap: 10px; @@ -123,29 +124,35 @@ color: var(--text-secondary); background: transparent; cursor: pointer; - transition: all 0.15s ease; + transition: background 0.15s ease, color 0.15s ease; text-align: left; + // Left accent bar for active state + &::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%) scaleY(0); + width: 3px; + height: 16px; + border-radius: 0 2px 2px 0; + background: var(--primary); + transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1); + } + &:hover { background: var(--bg-hover); color: var(--text-primary); } &.active { - position: relative; background: var(--bg-hover); color: var(--text-primary); - font-weight: 500; + font-weight: 600; &::before { - content: ''; - position: absolute; - left: -8px; - top: 20%; - bottom: 20%; - width: 3px; - background: var(--primary); - border-radius: 0 4px 4px 0; + transform: translateY(-50%) scaleY(1); } } } @@ -2911,6 +2918,7 @@ padding: 16px; border-radius: 10px; border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); + flex-wrap: wrap; background: linear-gradient( 180deg, color-mix(in srgb, var(--bg-secondary) 94%, var(--primary) 6%) 0%, @@ -2941,9 +2949,9 @@ .anti-revoke-metrics { flex: 1; display: grid; - grid-template-columns: repeat(4, minmax(112px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: 10px; - min-width: 460px; + min-width: 0; } .anti-revoke-metric { From 404b06ff16bbc5b6a8d0c84d75b250baf22cac02 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 4 May 2026 21:19:44 +0800 Subject: [PATCH 07/23] Polish settings anti-revoke and model directory UI --- src/pages/SettingsPage.scss | 248 +++++++++++++++++------------------- src/pages/SettingsPage.tsx | 70 +++++----- 2 files changed, 149 insertions(+), 169 deletions(-) diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 6cf1c34..2645f2e 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -2339,7 +2339,7 @@ border: 1px solid var(--border-color); border-radius: 12px; padding: 16px; - background: var(--bg-primary); + background: color-mix(in srgb, var(--bg-primary) 96%, var(--bg-secondary) 4%); } .model-status-card { @@ -2381,8 +2381,9 @@ .model-meta { display: flex; - flex-direction: column; + flex-direction: row; align-items: flex-start; + flex-wrap: wrap; gap: 10px; .status-indicator { @@ -2408,32 +2409,6 @@ } } - .model-path-block { - width: 100%; - display: flex; - flex-direction: column; - gap: 6px; - padding: 10px 12px; - border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--bg-secondary); - } - - .path-label { - font-size: 11px; - font-weight: 600; - color: var(--text-tertiary); - letter-spacing: 0.02em; - } - - .path-text { - font-size: 12px; - color: var(--text-tertiary); - font-family: monospace; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } } } @@ -2553,6 +2528,46 @@ } } +.model-directory-control { + display: grid; + grid-template-columns: minmax(0, 1fr) auto auto; + align-items: center; + gap: 8px; + padding-top: 14px; + border-top: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + + input { + min-width: 0; + height: 38px; + margin-bottom: 0 !important; + font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace); + font-size: 12px; + color: var(--text-secondary); + background: color-mix(in srgb, var(--bg-secondary) 86%, var(--bg-primary) 14%); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + cursor: default !important; + } + + .btn { + height: 38px; + padding-inline: 12px; + border-radius: 8px; + white-space: nowrap; + } +} + +@media (max-width: 720px) { + .model-directory-control { + grid-template-columns: minmax(0, 1fr); + + .btn { + width: 100%; + } + } +} + @keyframes progress-shimmer { 0% { transform: translateX(-100%); @@ -2563,59 +2578,6 @@ } } -.sub-setting { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid var(--border-color); - - .sub-label { - font-size: 13px; - color: var(--text-secondary); - margin-bottom: 8px; - } -} - -.path-selector { - display: flex; - gap: 8px; - flex-wrap: nowrap; - align-items: center; - - input { - margin-bottom: 0 !important; - flex: 1; - min-width: 0; - width: auto !important; - font-family: monospace; - font-size: 12px; - } - - .btn-icon { - width: 38px; - height: 38px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 999px; // Circle - border: 1px solid var(--border-color); - background: var(--bg-primary); - color: var(--text-secondary); - cursor: pointer; - transition: all 0.2s; - - &:hover { - color: var(--text-primary); - background: var(--bg-tertiary); - } - - &.danger:hover { - color: var(--danger); - background: rgba(220, 38, 38, 0.1); - border-color: rgba(220, 38, 38, 0.2); - } - } -} - @keyframes spin { from { transform: rotate(0deg); @@ -2912,45 +2874,41 @@ .anti-revoke-hero { display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 18px; - padding: 16px; - border-radius: 10px; + flex-direction: column; + align-items: stretch; + gap: 14px; + padding: 16px 18px; + border-radius: 12px; border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); - flex-wrap: wrap; - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--bg-secondary) 94%, var(--primary) 6%) 0%, - color-mix(in srgb, var(--bg-secondary) 96%, var(--bg-primary) 4%) 100% - ); + background: color-mix(in srgb, var(--bg-primary) 94%, var(--bg-secondary) 6%); } .anti-revoke-hero-main { - min-width: 240px; + min-width: 0; h3 { margin: 0; - font-size: 19px; + font-size: 18px; font-weight: 600; line-height: 1.3; color: var(--text-primary); - letter-spacing: 0.3px; + letter-spacing: 0; } p { - margin: 8px 0 0; + margin: 7px 0 0; font-size: 13px; color: var(--text-secondary); line-height: 1.6; + max-width: 560px; } } .anti-revoke-metrics { - flex: 1; display: grid; - grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); - gap: 10px; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 8px; + width: 100%; min-width: 0; } @@ -2958,21 +2916,22 @@ display: flex; flex-direction: column; justify-content: center; - gap: 6px; - padding: 12px 14px; - border-radius: 12px; + gap: 5px; + min-height: 74px; + padding: 11px 12px; + border-radius: 10px; border: 1px solid color-mix(in srgb, var(--border-color) 90%, transparent); - background: color-mix(in srgb, var(--bg-primary) 93%, var(--bg-secondary) 7%); + background: color-mix(in srgb, var(--bg-secondary) 82%, var(--bg-primary) 18%); .label { font-size: 12px; color: var(--text-secondary); line-height: 1.2; - letter-spacing: 0.2px; + letter-spacing: 0; } .value { - font-size: 30px; + font-size: 26px; font-weight: 700; color: var(--text-primary); line-height: 1; @@ -3013,25 +2972,24 @@ .anti-revoke-control-card { border: 1px solid color-mix(in srgb, var(--border-color) 90%, transparent); - border-radius: 10px; + border-radius: 12px; padding: 14px; background: color-mix(in srgb, var(--bg-secondary) 95%, var(--bg-primary) 5%); } .anti-revoke-toolbar { display: flex; - gap: 14px; + gap: 10px; align-items: center; justify-content: space-between; - margin-bottom: 12px; flex-wrap: wrap; } .anti-revoke-search { min-width: 280px; flex: 1; - max-width: 420px; - border-radius: 10px; + max-width: none; + border-radius: 8px; background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary) 15%); input { @@ -3044,7 +3002,7 @@ display: flex; align-items: stretch; justify-content: flex-end; - gap: 10px; + gap: 8px; flex-wrap: wrap; margin-left: auto; } @@ -3056,14 +3014,50 @@ flex-wrap: wrap; } + .anti-revoke-selection-strip { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 12px; + padding-top: 12px; + border-top: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + } + + .anti-revoke-selection-actions { + display: inline-flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + .anti-revoke-batch-actions { display: flex; - align-items: flex-start; + align-items: center; gap: 12px; flex-wrap: wrap; justify-content: space-between; - border-top: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); - padding-top: 12px; + margin-top: 12px; + padding: 12px; + border-radius: 10px; + border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + background: color-mix(in srgb, var(--bg-primary) 88%, var(--bg-secondary) 12%); + } + + .anti-revoke-batch-copy { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + font-size: 12px; + line-height: 1.4; + color: var(--text-tertiary); + } + + .anti-revoke-section-label { + font-size: 13px; + font-weight: 600; + color: var(--text-primary); } .anti-revoke-selected-count { @@ -3072,11 +3066,7 @@ gap: 14px; font-size: 12px; color: var(--text-secondary); - margin-left: auto; - padding: 8px 12px; - border-radius: 10px; - border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); - background: color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary) 8%); + min-width: 0; span { position: relative; @@ -3104,12 +3094,14 @@ } .anti-revoke-toolbar-actions .btn, + .anti-revoke-selection-actions .btn, .anti-revoke-batch-actions .btn { - border-radius: 10px; + border-radius: 8px; padding-inline: 14px; border-width: 1px; - min-height: 36px; + min-height: 34px; justify-content: center; + white-space: nowrap; } .anti-revoke-summary { @@ -3136,7 +3128,7 @@ .anti-revoke-list { border: 1px solid color-mix(in srgb, var(--border-color) 90%, transparent); - border-radius: 16px; + border-radius: 12px; background: var(--bg-primary); max-height: 460px; overflow-y: auto; @@ -3531,14 +3523,13 @@ } @media (max-width: 980px) { - .anti-revoke-hero { - flex-direction: column; + .anti-revoke-metrics { + grid-template-columns: repeat(2, minmax(0, 1fr)); } - .anti-revoke-metrics { - width: 100%; - min-width: 0; - grid-template-columns: repeat(2, minmax(130px, 1fr)); + .anti-revoke-selection-strip { + align-items: flex-start; + flex-direction: column; } .anti-revoke-batch-actions { @@ -3547,7 +3538,6 @@ } .anti-revoke-selected-count { - margin-left: 0; width: 100%; justify-content: flex-start; overflow-x: auto; diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index fb53979..1111534 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -2189,23 +2189,32 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { />
-
- -
-
- - -
+ +
+
+ +
+
+ 已选 {selectedCount} 个会话 + 筛选命中 {selectedInFilteredCount} / {filteredSessionIds.length} +
+
+ +
+
+ 批量部署 + 对已选会话执行防撤回安装或卸载 +
-
- 已选 {selectedCount} 个会话 - 筛选命中 {selectedInFilteredCount} / {filteredSessionIds.length} -
@@ -2516,11 +2521,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { const renderModelsTab = () => (
-
- - 管理语音识别模型 -
-
用于语音消息转文字功能 @@ -2538,12 +2538,6 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { ) : ( 未安装 )} - {resolvedWhisperModelPath && ( -
- 模型目录 -
{resolvedWhisperModelPath}
-
- )}
{(!whisperModelStatus?.exists || isWhisperDownloading) && ( @@ -2574,25 +2568,21 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { )}
)} -
-
-
自定义模型目录
-
+
- + - {whisperModelDir && ( - - )}
From 4f13b609d452683a913c08c837f1ed7c15be8464 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 5 May 2026 19:27:56 +0800 Subject: [PATCH 08/23] refactor: simplify home page --- src/pages/HomePage.scss | 69 ++--------------------------------------- src/pages/HomePage.tsx | 30 ------------------ 2 files changed, 2 insertions(+), 97 deletions(-) diff --git a/src/pages/HomePage.scss b/src/pages/HomePage.scss index 40a4ee8..fcc6976 100644 --- a/src/pages/HomePage.scss +++ b/src/pages/HomePage.scss @@ -12,7 +12,6 @@ max-width: 640px; width: 100%; padding: 0 24px; - animation: homeFadeIn 0.5s ease-out; } .home-title { @@ -20,81 +19,17 @@ font-weight: 700; margin: 0 0 12px; color: var(--text-primary); - letter-spacing: -1.5px; } .home-subtitle { font-size: 16px; color: var(--text-secondary); - margin: 0 0 48px; + margin: 0; line-height: 1.6; } -.home-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 12px; -} - -.home-feature-card { - display: flex; - align-items: center; - gap: 14px; - padding: 16px 18px; - border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--card-bg); - cursor: pointer; - text-align: left; - transition: background 0.15s ease, border-color 0.15s ease; - color: var(--text-secondary); - - &:hover { - background: var(--bg-hover); - border-color: var(--text-tertiary); - color: var(--text-primary); - } - - svg { - flex-shrink: 0; - } -} - -.home-feature-text { - display: flex; - flex-direction: column; - gap: 2px; - min-width: 0; -} - -.home-feature-label { - font-size: 14px; - font-weight: 600; - color: var(--text-primary); -} - -.home-feature-desc { - font-size: 12px; - color: var(--text-tertiary); -} - -@keyframes homeFadeIn { - from { - opacity: 0; - transform: translateY(12px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - @media (max-width: 480px) { - .home-grid { - grid-template-columns: 1fr; - } - .home-title { font-size: 36px; } -} \ No newline at end of file +} diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 0423bad..8ddbb84 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -1,41 +1,11 @@ -import { MessageSquare, BarChart3, Download, Aperture, Footprints, FolderClosed } from 'lucide-react' -import { useNavigate } from 'react-router-dom' import './HomePage.scss' function HomePage() { - const navigate = useNavigate() - - const features = [ - { icon: MessageSquare, label: '聊天', desc: '浏览聊天记录', path: '/chat' }, - { icon: Aperture, label: '朋友圈', desc: '查看朋友圈动态', path: '/sns' }, - { icon: BarChart3, label: '聊天分析', desc: '分析聊天统计数据', path: '/analytics' }, - { icon: FolderClosed, label: '资源浏览', desc: '管理媒体文件', path: '/resources' }, - { icon: Footprints, label: '我的足迹', desc: '回顾你的轨迹', path: '/footprint' }, - { icon: Download, label: '导出', desc: '导出聊天记录', path: '/export' }, - ] - return (

WeFlow

每一条消息的背后,都藏着一段温暖的时光

- -
- {features.map((f) => ( - - ))} -
) From 531915387921af0dc0ce790d417f6e8121c94312 Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 5 May 2026 20:24:58 +0800 Subject: [PATCH 09/23] refactor: streamline sns page --- .../Sns/ContactSnsTimelineDialog.scss | 67 ++- .../Sns/ContactSnsTimelineDialog.tsx | 4 - src/components/Sns/SnsFilterPanel.tsx | 119 ++--- src/components/Sns/SnsPostItem.tsx | 2 +- src/pages/SnsPage.scss | 423 +++++++++--------- src/pages/SnsPage.tsx | 142 +++--- 6 files changed, 396 insertions(+), 361 deletions(-) diff --git a/src/components/Sns/ContactSnsTimelineDialog.scss b/src/components/Sns/ContactSnsTimelineDialog.scss index dd45cd0..b8e1c20 100644 --- a/src/components/Sns/ContactSnsTimelineDialog.scss +++ b/src/components/Sns/ContactSnsTimelineDialog.scss @@ -6,16 +6,16 @@ align-items: center; justify-content: center; padding: 24px 16px; - background: rgba(15, 23, 42, 0.38); + background: rgba(15, 23, 42, 0.28); } .contact-sns-dialog { - width: min(760px, 100%); - max-height: min(86vh, 860px); - border-radius: 14px; + width: min(720px, 100%); + max-height: min(84vh, 820px); + border-radius: 10px; border: 1px solid var(--border-color); background: var(--bg-secondary-solid, #ffffff); - box-shadow: 0 22px 46px rgba(0, 0, 0, 0.24); + box-shadow: 0 18px 34px rgba(0, 0, 0, 0.18); display: flex; flex-direction: column; overflow: hidden; @@ -29,7 +29,7 @@ align-items: center; justify-content: space-between; gap: 10px; - padding: 14px 16px; + padding: 12px 14px; border-bottom: 1px solid var(--border-color); } @@ -41,9 +41,9 @@ } .contact-sns-dialog-avatar { - width: 42px; - height: 42px; - border-radius: 10px; + width: 36px; + height: 36px; + border-radius: 8px; background: linear-gradient(135deg, var(--primary), var(--primary-hover)); overflow: hidden; flex-shrink: 0; @@ -69,7 +69,7 @@ h4 { margin: 0; - font-size: 15px; + font-size: 14px; color: var(--text-primary); overflow: hidden; text-overflow: ellipsis; @@ -79,7 +79,7 @@ .contact-sns-dialog-username { margin-top: 2px; - font-size: 12px; + font-size: 11px; color: var(--text-tertiary); overflow: hidden; text-overflow: ellipsis; @@ -88,7 +88,7 @@ .contact-sns-dialog-stats { margin-top: 4px; - font-size: 12px; + font-size: 11px; color: var(--text-secondary); } @@ -111,9 +111,9 @@ border-radius: 8px; background: var(--bg-primary); color: var(--text-secondary); - height: 28px; - padding: 0 10px; - font-size: 12px; + height: 30px; + padding: 0 9px; + font-size: 11px; line-height: 1; cursor: pointer; white-space: nowrap; @@ -134,8 +134,8 @@ position: absolute; top: calc(100% + 8px); right: 0; - width: 248px; - max-height: calc((28px * 15) + 16px); + width: 228px; + max-height: calc((26px * 15) + 16px); overflow-y: auto; border: 1px solid color-mix(in srgb, var(--primary) 30%, var(--border-color)); border-radius: 10px; @@ -220,26 +220,20 @@ } .contact-sns-dialog-tip { - padding: 10px 16px; - border-bottom: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); - background: color-mix(in srgb, var(--bg-primary) 78%, var(--bg-secondary)); - font-size: 12px; - line-height: 1.6; - color: var(--text-secondary); - word-break: break-word; + display: none; } .contact-sns-dialog-body { flex: 1; min-height: 0; overflow-y: auto; - padding: 12px 16px 14px; + padding: 10px 12px 12px; } .contact-sns-dialog-posts-list { display: flex; flex-direction: column; - gap: 14px; + gap: 10px; } .contact-sns-dialog-posts-list .post-header-actions { @@ -247,9 +241,9 @@ } .contact-sns-dialog-status { - padding: 20px 12px; + padding: 16px 10px; text-align: center; - font-size: 13px; + font-size: 12px; color: var(--text-secondary); &.empty { @@ -264,8 +258,8 @@ background: var(--bg-primary); color: var(--text-primary); border-radius: 10px; - padding: 9px 18px; - font-size: 13px; + padding: 8px 14px; + font-size: 12px; cursor: pointer; &:hover:not(:disabled) { @@ -282,15 +276,15 @@ @media (max-width: 768px) { .contact-sns-dialog-overlay { - padding: 12px 8px; + padding: 10px 8px; } .contact-sns-dialog { - width: min(100vw - 16px, 760px); + width: min(100vw - 16px, 720px); max-height: calc(100vh - 24px); .contact-sns-dialog-header { - padding: 12px; + padding: 10px 12px; } .contact-sns-dialog-header-actions { @@ -300,18 +294,13 @@ .contact-sns-dialog-rank-btn { height: 26px; padding: 0 8px; - font-size: 11px; + font-size: 10px; } .contact-sns-dialog-rank-panel { width: min(78vw, 232px); } - .contact-sns-dialog-tip { - padding: 10px 12px; - line-height: 1.55; - } - .contact-sns-dialog-body { padding: 10px 10px 12px; } diff --git a/src/components/Sns/ContactSnsTimelineDialog.tsx b/src/components/Sns/ContactSnsTimelineDialog.tsx index 3547954..3ab1280 100644 --- a/src/components/Sns/ContactSnsTimelineDialog.tsx +++ b/src/components/Sns/ContactSnsTimelineDialog.tsx @@ -538,10 +538,6 @@ export function ContactSnsTimelineDialog({
-
- 在微信桌面客户端中打开这个人的朋友圈浏览,可快速把其朋友圈同步到这里。若你在乎这个人,一定要试试~ -
-
= ({ onClearSelectedContacts, onExportSelectedContacts }) => { - const filteredContacts = contacts.filter(c => - (c.displayName || '').toLowerCase().includes(contactSearch.toLowerCase()) || - c.username.toLowerCase().includes(contactSearch.toLowerCase()) - ) + const filteredContacts = React.useMemo(() => { + const keyword = contactSearch.trim().toLowerCase() + if (!keyword) return contacts + return contacts.filter(c => + (c.displayName || '').toLowerCase().includes(keyword) || + c.username.toLowerCase().includes(keyword) + ) + }, [contacts, contactSearch]) const selectedContactLookup = React.useMemo( () => new Set(selectedContactUsernames), [selectedContactUsernames] @@ -85,10 +90,52 @@ export const SnsFilterPanel: React.FC = ({ return '没有找到联系人' } + const renderContactRow = React.useCallback((_: number, contact: Contact) => { + const isPostCountReady = contact.postCountStatus === 'ready' + const isSelected = selectedContactLookup.has(contact.username) + const isActive = activeContactUsername === contact.username + + return ( +
+ + +
+ ) + }, [activeContactUsername, onOpenContactTimeline, onToggleContactSelected, selectedContactLookup]) + return (
diff --git a/src/components/Sns/SnsPostItem.tsx b/src/components/Sns/SnsPostItem.tsx index adb7be1..e448b3f 100644 --- a/src/components/Sns/SnsPostItem.tsx +++ b/src/components/Sns/SnsPostItem.tsx @@ -493,7 +493,7 @@ export const SnsPostItem: React.FC = ({ post, onPreview, onDeb diff --git a/src/pages/SnsPage.scss b/src/pages/SnsPage.scss index cc1b0bc..fa66895 100644 --- a/src/pages/SnsPage.scss +++ b/src/pages/SnsPage.scss @@ -1,12 +1,13 @@ /* Global Variables */ :root { - --sns-max-width: 800px; - --sns-panel-width: 380px; + --sns-max-width: 920px; + --sns-panel-width: 320px; --sns-bg-color: var(--bg-primary); - --sns-card-bg: var(--bg-secondary); - --sns-border-radius-lg: 16px; - --sns-border-radius-md: 12px; - --sns-border-radius-sm: 8px; + --sns-card-bg: transparent; + --sns-border-radius-lg: 10px; + --sns-border-radius-md: 8px; + --sns-border-radius-sm: 6px; + --sns-media-cell: 88px; } .sns-page-layout { @@ -33,7 +34,7 @@ .sns-feed-container { width: 100%; max-width: var(--sns-max-width); - padding: 10px 24px 12px 24px; + padding: 14px 20px 0; display: flex; flex-direction: column; gap: 0; @@ -45,13 +46,12 @@ display: flex; align-items: center; justify-content: space-between; - margin-bottom: 4px; - padding: 0 4px; + gap: 16px; + margin-bottom: 0; + padding: 2px 2px 10px; z-index: 2; background: var(--sns-bg-color); border-bottom: 1px solid var(--border-color); - padding-top: 4px; - padding-bottom: 6px; .feed-header-main { display: flex; @@ -70,9 +70,9 @@ .feed-stats-line { display: flex; align-items: center; - gap: 8px; + gap: 6px; flex-wrap: wrap; - font-size: 13px; + font-size: 12px; color: var(--text-secondary); line-height: 1.4; @@ -170,7 +170,7 @@ .header-actions { display: flex; align-items: center; - gap: 10px; + gap: 6px; } .jump-calendar-anchor { @@ -186,24 +186,28 @@ } .icon-btn { - background: var(--bg-tertiary); + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + background: transparent; border: 1px solid var(--border-color); border-radius: var(--sns-border-radius-sm); - padding: 8px; + padding: 0; color: var(--text-secondary); cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; &:hover { - background: var(--hover-bg); - color: var(--primary); - transform: scale(1.05); + background: var(--bg-hover); + border-color: color-mix(in srgb, var(--text-tertiary) 34%, var(--border-color)); + color: var(--text-primary); } &:disabled { opacity: 0.5; cursor: not-allowed; - transform: none; } .spinning { @@ -217,20 +221,21 @@ gap: 8px; border: 1px solid var(--border-color); border-radius: var(--sns-border-radius-sm); - background: var(--bg-tertiary); + background: transparent; color: var(--text-secondary); - padding: 8px 10px; + min-height: 32px; + padding: 0 9px; cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; &:hover { - background: var(--hover-bg); - color: var(--primary); + background: var(--bg-hover); + color: var(--text-primary); } &.active { border-color: var(--primary); - background: rgba(var(--primary-rgb), 0.08); + background: rgba(var(--primary-rgb), 0.07); color: var(--primary); } } @@ -256,19 +261,28 @@ } } -.sns-posts-scroll { +.sns-posts-stage { flex: 1; min-height: 0; + position: relative; +} + +.sns-posts-scroll { + height: 100%; overflow-y: auto; - padding-top: 16px; + padding-top: 8px; +} + +.sns-post-row { + padding-bottom: 10px; } .feed-contact-filter-bar { - margin: 10px 4px 0; - padding: 10px 12px; - border: 1px solid color-mix(in srgb, var(--primary) 28%, var(--border-color)); - border-radius: 12px; - background: rgba(var(--primary-rgb), 0.08); + margin: 8px 0 0; + padding: 7px 9px; + border: 1px solid color-mix(in srgb, var(--primary) 20%, var(--border-color)); + border-radius: var(--sns-border-radius-md); + background: rgba(var(--primary-rgb), 0.06); display: flex; align-items: center; gap: 8px; @@ -281,7 +295,7 @@ } .feed-contact-filter-summary { - font-size: 13px; + font-size: 12px; color: var(--text-primary); font-weight: 600; min-width: 0; @@ -293,7 +307,7 @@ background: transparent; color: var(--primary); font-size: 12px; - font-weight: 600; + font-weight: 500; cursor: pointer; padding: 0; white-space: nowrap; @@ -308,7 +322,7 @@ .posts-list { display: flex; flex-direction: column; - gap: 24px; + gap: 10px; } /* ========================================= @@ -316,19 +330,21 @@ ========================================= */ .sns-post-item { background: var(--sns-card-bg); - border-radius: var(--sns-border-radius-lg); - border: 1px solid var(--border-color); - padding: 20px; + border-radius: 0; + border: none; + border-bottom: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); + padding: 10px 6px 12px; display: flex; - gap: 16px; - transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02); + gap: 12px; + transition: background 0.15s ease; + box-shadow: none; position: relative; - overflow: hidden; + overflow: visible; &:hover { - transform: translateY(-2px); - box-shadow: 0 8px 16px rgba(0, 0, 0, 0.06); + background: color-mix(in srgb, var(--bg-secondary) 72%, transparent); + box-shadow: none; + transform: none; } &.post-deleted { @@ -341,9 +357,9 @@ gap: 4px; background: rgba(255, 77, 79, 0.1); color: #ff4d4f; - font-size: 12px; + font-size: 11px; font-weight: 500; - padding: 3px 8px; + padding: 2px 6px; border-radius: 6px; } } @@ -362,12 +378,12 @@ } .avatar-trigger { - border-radius: 12px; - transition: transform 0.15s ease, box-shadow 0.15s ease; + border-radius: var(--sns-border-radius-md); + transition: opacity 0.15s ease; &:hover { - transform: translateY(-1px); - box-shadow: 0 6px 14px rgba(0, 0, 0, 0.1); + opacity: 0.86; + box-shadow: none; } &:focus-visible { @@ -386,7 +402,8 @@ display: flex; justify-content: space-between; align-items: flex-start; - margin-bottom: 8px; + gap: 10px; + margin-bottom: 5px; .post-author-info { display: flex; @@ -395,12 +412,10 @@ overflow: hidden; .author-name { - font-size: 15px; - font-weight: 700; - color: var(--text-primary); - /* Changed to primary from accent for cleaner look, or keep accent */ + font-size: 14px; + font-weight: 600; color: var(--primary); - margin-bottom: 2px; + margin-bottom: 1px; } .author-name-trigger { @@ -430,13 +445,14 @@ .post-time { font-size: 12px; color: var(--text-tertiary); + line-height: 1.25; } } .post-header-actions { display: flex; align-items: center; - gap: 6px; + gap: 4px; flex-shrink: 0; } @@ -451,9 +467,11 @@ opacity: 0; transition: opacity 0.2s; background: none; - border: 1px solid var(--border-color); + border: none; border-radius: 6px; - padding: 6px; + width: 24px; + height: 24px; + padding: 0; color: var(--text-tertiary); cursor: pointer; display: flex; @@ -462,8 +480,7 @@ &:hover { color: var(--text-primary); - background: var(--bg-tertiary); - border-color: var(--text-secondary); + background: var(--bg-hover); } &.delete-btn:hover { @@ -751,20 +768,20 @@ } .post-text { - font-size: 15px; - line-height: 1.6; + font-size: 14px; + line-height: 1.48; color: var(--text-primary); white-space: pre-wrap; word-break: break-word; - margin-bottom: 12px; + margin: 0 0 8px; } .post-location { display: flex; align-items: flex-start; gap: 6px; - margin: -4px 0 12px; - font-size: 13px; + margin: -2px 0 8px; + font-size: 12px; line-height: 1.45; color: var(--text-secondary); @@ -780,7 +797,7 @@ } .post-media-container { - margin-bottom: 12px; + margin-bottom: 8px; } .post-link-card { @@ -788,14 +805,14 @@ background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: var(--sns-border-radius-md); - padding: 10px; + padding: 8px; display: flex; align-items: center; - gap: 12px; + gap: 10px; cursor: pointer; text-align: left; - transition: all 0.2s; - margin-bottom: 12px; + transition: background 0.15s ease, border-color 0.15s ease; + margin-bottom: 8px; &:hover { background: rgba(var(--primary-rgb), 0.08); @@ -803,8 +820,8 @@ } .link-thumb { - width: 60px; - height: 60px; + width: 48px; + height: 48px; border-radius: var(--sns-border-radius-sm); overflow: hidden; flex-shrink: 0; @@ -830,7 +847,7 @@ min-width: 0; .link-title { - font-size: 14px; + font-size: 13px; font-weight: 600; color: var(--text-primary); overflow: hidden; @@ -839,7 +856,7 @@ } .link-url { - font-size: 12px; + font-size: 11px; color: var(--text-tertiary); margin-top: 4px; } @@ -851,15 +868,15 @@ } .post-interactions { - margin-top: 12px; - padding-top: 12px; - border-top: 1px dashed var(--border-color); - font-size: 13px; + margin-top: 8px; + padding-top: 0; + border-top: none; + font-size: 12px; .likes-block { display: flex; - gap: 8px; - margin-bottom: 8px; + gap: 6px; + margin-bottom: 6px; color: var(--primary); font-weight: 500; @@ -871,11 +888,11 @@ .comments-block { background: var(--bg-tertiary); border-radius: var(--sns-border-radius-sm); - padding: 8px 12px; + padding: 6px 8px; .comment-row { - margin-bottom: 4px; - line-height: 1.4; + margin-bottom: 3px; + line-height: 1.45; color: var(--text-secondary); &:last-child { @@ -912,13 +929,13 @@ ========================================= */ .sns-media-grid { display: grid; - gap: 6px; + gap: 4px; &.grid-1 .sns-media-item { width: fit-content; height: auto; - max-width: 300px; - max-height: 480px; + max-width: 270px; + max-height: 340px; aspect-ratio: auto; border-radius: var(--sns-border-radius-md); @@ -930,7 +947,7 @@ img, video { max-width: 100%; - max-height: 480px; + max-height: 340px; width: auto; height: auto; object-fit: contain; @@ -941,26 +958,25 @@ &.grid-2, &.grid-4 { - grid-template-columns: repeat(2, 1fr); - max-width: 320px; + grid-template-columns: repeat(2, var(--sns-media-cell)); + max-width: calc((var(--sns-media-cell) * 2) + 4px); } &.grid-3, &.grid-6, &.grid-9 { - grid-template-columns: repeat(3, 1fr); - max-width: 320px; + grid-template-columns: repeat(3, var(--sns-media-cell)); + max-width: calc((var(--sns-media-cell) * 3) + 8px); } .sns-media-item { position: relative; aspect-ratio: 1; - border-radius: var(--sns-border-radius-md); - /* Consistent radius for grid items */ + border-radius: var(--sns-border-radius-sm); overflow: hidden; cursor: zoom-in; background: var(--bg-tertiary); - transition: opacity 0.2s; + transition: opacity 0.15s ease; &:hover { opacity: 0.9; @@ -972,7 +988,7 @@ height: 100%; object-fit: cover; display: block; - animation: fade-in 0.3s ease; + animation: fade-in 0.2s ease; } .media-badge { @@ -984,7 +1000,6 @@ height: 32px; background: rgba(0, 0, 0, 0.4); border-radius: 50%; - backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; @@ -993,8 +1008,8 @@ /* Let clicks pass through badge to item */ &.live { - top: 8px; - right: 8px; + top: 6px; + right: 6px; left: auto; transform: none; width: 24px; @@ -1015,30 +1030,27 @@ gap: 8px; z-index: 10; font-size: 12px; - backdrop-filter: blur(2px); } .media-download-btn { position: absolute; - bottom: 6px; - right: 6px; - width: 28px; - height: 28px; + bottom: 5px; + right: 5px; + width: 24px; + height: 24px; background: rgba(0, 0, 0, 0.5); border-radius: 50%; - backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; color: white; cursor: pointer; opacity: 0; - transition: all 0.2s; + transition: opacity 0.15s ease, background 0.15s ease; z-index: 5; &:hover { background: var(--primary); - transform: scale(1.1); } /* Increase click area */ @@ -1092,8 +1104,8 @@ background: var(--bg-secondary); display: flex; flex-direction: column; - padding: 24px; - gap: 24px; + padding: 16px 14px; + gap: 14px; z-index: 10; .filter-header { @@ -1102,8 +1114,8 @@ align-items: center; h3 { - font-size: 16px; - font-weight: 700; + font-size: 15px; + font-weight: 650; margin: 0; } @@ -1112,10 +1124,16 @@ border: none; cursor: pointer; color: var(--text-tertiary); + width: 28px; + height: 28px; + border-radius: var(--sns-border-radius-sm); + display: inline-flex; + align-items: center; + justify-content: center; &:hover { - color: var(--primary); - animation: spin 0.5s ease; + background: var(--bg-hover); + color: var(--text-primary); } } } @@ -1123,30 +1141,30 @@ .filter-widgets { display: flex; flex-direction: column; - gap: 20px; + gap: 14px; flex: 1; min-height: 0; } .filter-widget { - background: var(--bg-primary); - border: 1px solid var(--border-color); - border-radius: var(--sns-border-radius-md); - padding: 16px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02); + background: transparent; + border: none; + border-radius: 0; + padding: 0; + box-shadow: none; .widget-header { display: flex; align-items: center; - gap: 8px; - margin-bottom: 12px; - font-size: 13px; + gap: 6px; + margin-bottom: 8px; + font-size: 12px; font-weight: 600; color: var(--text-secondary); svg { - color: var(--primary); - opacity: 0.8; + color: var(--text-tertiary); + opacity: 1; } .badge { @@ -1161,7 +1179,7 @@ .widget-header-summary { margin-left: auto; font-size: 12px; - font-weight: 500; + font-weight: 400; color: var(--text-tertiary); white-space: nowrap; } @@ -1183,17 +1201,16 @@ background: var(--bg-tertiary); border: 1px solid transparent; border-radius: var(--sns-border-radius-sm); - padding: 10px 30px 10px 12px; + height: 34px; + padding: 0 30px 0 10px; font-size: 13px; color: var(--text-primary); - transition: all 0.2s; + transition: background 0.15s ease, border-color 0.15s ease; &:focus { - background: var(--bg-primary); + background: var(--bg-secondary); border-color: transparent; - /* Explicitly transparent */ outline: none; - /* Ensure no outline */ box-shadow: none; } } @@ -1218,14 +1235,16 @@ min-height: 300px; overflow: hidden; padding: 0; + border-top: 1px solid var(--border-color); + padding-top: 12px; .widget-header { - padding: 16px 16px 12px 16px; - margin-bottom: 0; + padding: 0; + margin-bottom: 8px; } .contact-search-bar { - padding: 0 16px 12px 16px; + padding: 0 0 10px; position: relative; border-bottom: 1px solid var(--border-color); @@ -1233,22 +1252,23 @@ width: 100%; background: var(--bg-tertiary); border: 1px solid transparent; - border-radius: 8px; - padding: 8px 32px 8px 12px; + border-radius: var(--sns-border-radius-sm); + height: 32px; + padding: 0 32px 0 10px; font-size: 12px; color: var(--text-primary); &:focus { outline: none; - background: var(--bg-primary); - border-color: var(--primary); + background: var(--bg-secondary); + border-color: transparent; } } .search-icon { position: absolute; - right: 28px; - top: 8px; + right: 10px; + top: 9px; color: var(--text-tertiary); pointer-events: none; display: none; @@ -1256,8 +1276,8 @@ .clear-icon { position: absolute; - right: 28px; - top: 8px; + right: 10px; + top: 9px; color: var(--text-tertiary); cursor: pointer; display: flex; @@ -1270,7 +1290,7 @@ align-items: center; justify-content: space-between; gap: 10px; - padding: 10px 16px; + padding: 8px 0; border-bottom: 1px dashed color-mix(in srgb, var(--border-color) 72%, transparent); } @@ -1287,9 +1307,9 @@ background: color-mix(in srgb, var(--bg-primary) 84%, rgba(var(--primary-rgb), 0.08)); color: var(--text-secondary); border-radius: 999px; - padding: 5px 10px; + padding: 4px 9px; font-size: 12px; - font-weight: 600; + font-weight: 500; line-height: 1.2; cursor: pointer; transition: border-color 0.2s ease, background 0.2s ease, color 0.2s ease; @@ -1312,7 +1332,7 @@ } .contact-count-progress { - padding: 8px 16px 10px; + padding: 7px 0 8px; font-size: 11px; color: var(--text-tertiary); border-bottom: 1px dashed color-mix(in srgb, var(--border-color) 70%, transparent); @@ -1320,7 +1340,7 @@ } .contact-interaction-hint { - padding: 10px 16px 0; + padding: 8px 0 0; font-size: 11px; line-height: 1.5; color: var(--text-tertiary); @@ -1328,23 +1348,20 @@ .contact-list-scroll { flex: 1; - overflow-y: auto; - padding: 8px 12px; - display: flex; - flex-direction: column; - gap: 0; + min-height: 0; + padding: 8px 0 0; + + .contact-list-virtuoso { + height: 100%; + } .contact-row { display: flex; align-items: center; - gap: 8px; - margin-bottom: 4px; - border-radius: var(--sns-border-radius-md); - transition: transform 0.2s ease; - - &:hover { - transform: translateX(2px); - } + gap: 4px; + min-height: 40px; + border-radius: var(--sns-border-radius-sm); + transition: background 0.15s ease; &.is-selected .contact-main-btn { background: rgba(var(--primary-rgb), 0.06); @@ -1362,7 +1379,7 @@ } .contact-select-btn { - width: 32px; + width: 28px; height: 32px; flex-shrink: 0; border: none; @@ -1373,7 +1390,7 @@ align-items: center; justify-content: center; cursor: pointer; - transition: background 0.2s ease, color 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { background: rgba(var(--primary-rgb), 0.1); @@ -1390,17 +1407,17 @@ min-width: 0; display: flex; align-items: center; - gap: 12px; - padding: 10px 12px; - border-radius: var(--sns-border-radius-md); + gap: 8px; + padding: 5px 6px; + border-radius: var(--sns-border-radius-sm); border: 1px solid transparent; background: transparent; cursor: pointer; - transition: background 0.2s ease, border-color 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease; text-align: left; &:hover { - background: var(--hover-bg); + background: var(--bg-hover); } } @@ -1412,7 +1429,7 @@ gap: 2px; .contact-name { - font-size: 14px; + font-size: 13px; color: var(--text-secondary); white-space: nowrap; overflow: hidden; @@ -1422,7 +1439,7 @@ .contact-post-count-wrap { margin-left: 8px; - min-width: 46px; + min-width: 42px; display: flex; justify-content: flex-end; align-items: center; @@ -1430,7 +1447,7 @@ } .contact-post-count { - font-size: 12px; + font-size: 11px; color: var(--text-tertiary); font-variant-numeric: tabular-nums; white-space: nowrap; @@ -1453,9 +1470,9 @@ display: flex; align-items: center; gap: 8px; - padding: 12px 16px 14px; + padding: 10px 0 0; border-top: 1px solid var(--border-color); - background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary)); + background: transparent; } .contact-batch-summary { @@ -1470,8 +1487,8 @@ border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-secondary); - border-radius: var(--sns-border-radius-md); - height: 32px; + border-radius: var(--sns-border-radius-sm); + height: 30px; padding: 0 10px; display: inline-flex; align-items: center; @@ -1483,7 +1500,7 @@ &:hover { border-color: var(--text-tertiary); - background: var(--hover-bg); + background: var(--bg-hover); color: var(--text-primary); } @@ -1508,9 +1525,9 @@ ========================================= */ .status-indicator { text-align: center; - padding: 16px; + padding: 10px 8px; color: var(--text-tertiary); - font-size: 13px; + font-size: 12px; display: flex; align-items: center; justify-content: center; @@ -1520,8 +1537,10 @@ background: rgba(var(--primary-rgb), 0.1); color: var(--primary); cursor: pointer; + border: 1px solid color-mix(in srgb, var(--primary) 18%, var(--border-color)); border-radius: var(--sns-border-radius-sm); - margin: 0 24px; + margin: 0 0 8px; + width: 100%; } } @@ -1530,45 +1549,44 @@ display: flex; align-items: center; justify-content: center; - padding: 120px 0; + padding: 72px 0; .loading-pulse { display: flex; flex-direction: column; align-items: center; - gap: 20px; + gap: 12px; .pulse-circle { - width: 48px; - height: 48px; + width: 12px; + height: 12px; border-radius: 50%; background: var(--primary, #576b95); - opacity: 0.25; - animation: pulse-ring 1.4s ease-in-out infinite; + opacity: 0.4; + animation: pulse-ring 1.2s ease-in-out infinite; } span { - font-size: 14px; + font-size: 12px; color: var(--text-tertiary); - letter-spacing: 0.5px; } } } @keyframes pulse-ring { 0% { - transform: scale(0.6); - opacity: 0.15; + transform: scale(0.85); + opacity: 0.2; } 50% { - transform: scale(1.0); - opacity: 0.35; + transform: scale(1); + opacity: 0.45; } 100% { - transform: scale(0.6); - opacity: 0.15; + transform: scale(0.85); + opacity: 0.2; } } @@ -1576,20 +1594,25 @@ display: flex; flex-direction: column; align-items: center; - padding: 60px 0; + padding: 56px 0; color: var(--text-tertiary); .no-results-icon { - margin-bottom: 16px; + margin-bottom: 10px; opacity: 0.5; } + p { + margin: 0; + font-size: 13px; + } + .reset-inline { - margin-top: 16px; + margin-top: 12px; background: none; border: 1px solid var(--border-color); - padding: 6px 16px; - border-radius: 20px; + padding: 6px 12px; + border-radius: 8px; cursor: pointer; color: var(--text-secondary); @@ -2760,7 +2783,7 @@ transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease; &:hover:not(:disabled) { - background: var(--hover-bg); + background: var(--bg-hover); color: var(--text-primary); } @@ -2902,7 +2925,7 @@ color: var(--text-secondary); &:hover { - background: var(--hover-bg); + background: var(--bg-hover); color: var(--text-primary); } diff --git a/src/pages/SnsPage.tsx b/src/pages/SnsPage.tsx index 019f73b..e347005 100644 --- a/src/pages/SnsPage.tsx +++ b/src/pages/SnsPage.tsx @@ -1,5 +1,6 @@ import { useEffect, useLayoutEffect, useState, useRef, useCallback, useMemo } from 'react' import { RefreshCw, Search, X, Download, FolderOpen, FileJson, FileText, Image, CheckCircle, AlertCircle, Calendar, Info, Shield, ShieldOff, Loader2, Pause, Play, Square } from 'lucide-react' +import { Virtuoso, type VirtuosoHandle } from 'react-virtuoso' import './SnsPage.scss' import { SnsPost } from '../types/sns' import { SnsPostItem } from '../components/Sns/SnsPostItem' @@ -229,7 +230,8 @@ export default function SnsPage() { const [cacheMigrationDone, setCacheMigrationDone] = useState(false) const [cacheMigrationError, setCacheMigrationError] = useState(null) - const postsContainerRef = useRef(null) + const postsContainerRef = useRef(null) + const postsVirtuosoRef = useRef(null) const jumpCalendarWrapRef = useRef(null) const [hasNewer, setHasNewer] = useState(false) const [loadingNewer, setLoadingNewer] = useState(false) @@ -1110,6 +1112,7 @@ export default function SnsPage() { setHasNewer(false); } + postsVirtuosoRef.current?.scrollToIndex({ index: 0, align: 'start', behavior: 'auto' }) if (postsContainerRef.current) { postsContainerRef.current.scrollTop = 0 } @@ -1764,23 +1767,61 @@ export default function SnsPage() { loadPosts({ reset: true }) }, [loadPosts, selectedContactUsernamesKey]) - const handleScroll = (e: React.UIEvent) => { - const { scrollTop, clientHeight, scrollHeight } = e.currentTarget - if (scrollHeight - scrollTop - clientHeight < 400 && hasMore && !loading && !loadingNewer) { - loadPosts({ direction: 'older' }) - } - if (scrollTop < 10 && hasNewer && !loading && !loadingNewer) { - loadPosts({ direction: 'newer' }) - } - } + const handlePostsEndReached = useCallback(() => { + if (!hasMore || loading || loadingNewer) return + void loadPosts({ direction: 'older' }) + }, [hasMore, loadPosts, loading, loadingNewer]) - const handleWheel = (e: React.WheelEvent) => { - const container = postsContainerRef.current - if (!container) return - if (e.deltaY < -20 && container.scrollTop <= 0 && hasNewer && !loading && !loadingNewer) { - loadPosts({ direction: 'newer' }) - } - } + const renderPostItem = useCallback((_: number, post: SnsPost) => ( +
+ { + if (isVideo) { + void window.electronAPI.window.openVideoPlayerWindow(src) + } else { + void window.electronAPI.window.openImageViewerWindow(src, liveVideoPath || undefined) + } + }} + onDebug={(p) => setDebugPost(p)} + onDelete={handlePostDelete} + onOpenAuthorPosts={openAuthorTimeline} + /> +
+ ), [handlePostDelete, openAuthorTimeline, triggerInstalled]) + + const snsVirtuosoComponents = useMemo(() => ({ + Header: () => ( + <> + {loadingNewer && ( +
+ + 正在检查更新动态 +
+ )} + + {!loadingNewer && hasNewer && ( + + )} + + ), + Footer: () => ( + <> + {loading && visiblePosts.length > 0 && ( +
+ + 正在加载更多 +
+ )} + + {!hasMore && visiblePosts.length > 0 && ( +
已加载全部动态
+ )} + + ) + }), [hasMore, hasNewer, loadPosts, loading, loadingNewer, visiblePosts.length]) return (
@@ -1940,62 +1981,19 @@ export default function SnsPage() {
)} -
- {loadingNewer && ( -
- - 正在检查更新的动态... -
- )} - - {!loadingNewer && hasNewer && ( -
loadPosts({ direction: 'newer' })}> - 有新动态,点击查看 -
- )} - -
- {visiblePosts.map(post => ( - { - if (isVideo) { - void window.electronAPI.window.openVideoPlayerWindow(src) - } else { - void window.electronAPI.window.openImageViewerWindow(src, liveVideoPath || undefined) - } - }} - onDebug={(p) => setDebugPost(p)} - onDelete={handlePostDelete} - onOpenAuthorPosts={openAuthorTimeline} - /> - ))} -
- +
{loading && visiblePosts.length === 0 && (
- 正在加载朋友圈... + 正在加载动态
)} - {loading && visiblePosts.length > 0 && ( -
- - 正在加载更多... -
- )} - - {!hasMore && visiblePosts.length > 0 && ( -
或许过往已无可溯洄,但好在还有可以与你相遇的明天
- )} - {!loading && visiblePosts.length === 0 && (
-
+

未找到相关动态

{(searchKeyword || jumpTargetDate || selectedContactUsernames.length > 0) && (
)} + + {visiblePosts.length > 0 && ( + post.id} + itemContent={renderPostItem} + components={snsVirtuosoComponents} + endReached={handlePostsEndReached} + scrollerRef={(ref) => { + postsContainerRef.current = ref instanceof HTMLElement ? ref : null + }} + defaultItemHeight={220} + increaseViewportBy={{ top: 260, bottom: 520 }} + overscan={{ main: 900, reverse: 480 }} + /> + )}
From 24ab0239df354dfef9baa585d1c49ee65efd588b Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 5 May 2026 22:17:30 +0800 Subject: [PATCH 10/23] refactor: BackupPage & ContactsPage & ExportPage & InsightInboxPage & MyFootprintPage & ResourcesPage UI rebuild --- src/pages/BackupPage.scss | 58 +++--- src/pages/ContactsPage.scss | 127 ++++++------ src/pages/ContactsPage.tsx | 2 +- src/pages/ExportPage.scss | 336 +++++++++++++++----------------- src/pages/ExportPage.tsx | 2 +- src/pages/InsightInboxPage.scss | 98 +++++----- src/pages/MyFootprintPage.scss | 47 +++-- src/pages/ResourcesPage.scss | 86 ++++---- 8 files changed, 358 insertions(+), 398 deletions(-) diff --git a/src/pages/BackupPage.scss b/src/pages/BackupPage.scss index f05e82e..2f2c76e 100644 --- a/src/pages/BackupPage.scss +++ b/src/pages/BackupPage.scss @@ -1,7 +1,7 @@ .backup-page { height: 100%; overflow: auto; - padding: 24px; + padding: 20px; color: var(--text-primary); background: var(--bg-primary); } @@ -10,13 +10,13 @@ display: flex; align-items: flex-start; justify-content: space-between; - gap: 20px; - margin-bottom: 20px; + gap: 16px; + margin-bottom: 16px; h1 { margin: 0; - font-size: 26px; - font-weight: 700; + font-size: 22px; + font-weight: 650; letter-spacing: 0; } @@ -30,7 +30,7 @@ .backup-actions { display: flex; align-items: center; - gap: 10px; + gap: 8px; flex-wrap: wrap; justify-content: flex-end; } @@ -38,17 +38,17 @@ .resource-options { display: flex; align-items: center; - gap: 10px; + gap: 8px; flex-wrap: wrap; - margin: -8px 0 18px; + margin: -6px 0 14px; label { border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-secondary); color: var(--text-primary); - min-height: 36px; - padding: 8px 10px; + min-height: 34px; + padding: 7px 10px; display: inline-flex; align-items: center; gap: 8px; @@ -69,7 +69,7 @@ .secondary-btn { border: 1px solid var(--border-color); border-radius: 8px; - padding: 9px 12px; + padding: 8px 11px; display: inline-flex; align-items: center; gap: 8px; @@ -100,19 +100,19 @@ } .backup-status-band { - min-height: 88px; + min-height: 76px; border-top: 1px solid var(--border-color); border-bottom: 1px solid var(--border-color); display: flex; align-items: center; - gap: 14px; - margin-bottom: 18px; - padding: 16px 0; + gap: 12px; + margin-bottom: 14px; + padding: 12px 0; } .status-icon { - width: 42px; - height: 42px; + width: 36px; + height: 36px; border-radius: 8px; background: var(--bg-secondary); color: var(--primary); @@ -142,8 +142,8 @@ } .progress-track { - margin-top: 12px; - height: 6px; + margin-top: 10px; + height: 5px; background: var(--bg-tertiary); border-radius: 999px; overflow: hidden; @@ -160,7 +160,7 @@ display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px; - margin-bottom: 18px; + margin-bottom: 14px; } .summary-item, @@ -168,8 +168,8 @@ border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-secondary); - padding: 14px; - min-height: 74px; + padding: 12px; + min-height: 66px; display: flex; flex-direction: column; gap: 6px; @@ -185,14 +185,14 @@ strong { color: var(--text-primary); - font-size: 20px; + font-size: 18px; line-height: 1.1; } } .backup-detail { border-top: 1px solid var(--border-color); - padding-top: 18px; + padding-top: 14px; } .detail-heading { @@ -200,11 +200,11 @@ justify-content: space-between; align-items: center; gap: 12px; - margin-bottom: 12px; + margin-bottom: 10px; h2 { margin: 0; - font-size: 18px; + font-size: 17px; } span { @@ -217,7 +217,7 @@ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; - margin-bottom: 14px; + margin-bottom: 12px; div { border: 1px solid var(--border-color); @@ -246,7 +246,7 @@ .db-list { display: flex; flex-direction: column; - gap: 8px; + gap: 6px; } .db-row { @@ -255,7 +255,7 @@ gap: 10px; align-items: center; border-bottom: 1px solid var(--border-color); - padding: 9px 0; + padding: 8px 0; font-size: 13px; span { diff --git a/src/pages/ContactsPage.scss b/src/pages/ContactsPage.scss index ed71ec3..d4a300e 100644 --- a/src/pages/ContactsPage.scss +++ b/src/pages/ContactsPage.scss @@ -7,8 +7,8 @@ // 左侧联系人面板 .contacts-panel { - width: 350px; - min-width: 350px; + width: 324px; + min-width: 324px; display: flex; flex-direction: column; border-right: 1px solid var(--border-color); @@ -19,7 +19,7 @@ display: flex; align-items: center; justify-content: space-between; - padding: 20px 24px; + padding: 14px 18px; border-bottom: 1px solid var(--border-color); h2 { @@ -40,7 +40,7 @@ align-items: center; justify-content: center; color: var(--text-secondary); - transition: all 0.2s; + transition: background 0.15s ease, color 0.15s ease; &:hover { background: var(--bg-hover); @@ -67,10 +67,10 @@ display: flex; align-items: center; gap: 10px; - margin: 16px 20px; - padding: 10px 14px; + margin: 12px 16px; + padding: 8px 10px; background: var(--bg-secondary); - border-radius: 10px; + border-radius: 8px; border: 1px solid var(--border-color); transition: border-color 0.2s; @@ -117,9 +117,9 @@ .type-filters { display: grid; grid-template-columns: 1fr 1fr; - gap: 8px; - padding: 0 20px 16px; - max-width: 300px; + gap: 6px; + padding: 0 16px 12px; + max-width: 292px; &::-webkit-scrollbar { display: none; @@ -129,16 +129,16 @@ display: flex; align-items: center; gap: 6px; - padding: 8px 14px; + padding: 7px 10px; background: var(--bg-secondary); border: 1px solid var(--border-color); - border-radius: 10px; + border-radius: 8px; cursor: pointer; user-select: none; font-size: 13px; font-weight: 500; color: var(--text-secondary); - transition: all 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; white-space: nowrap; input[type="checkbox"] { @@ -167,7 +167,7 @@ color: var(--text-primary); svg { - transform: translateY(-1px); + transform: none; } } @@ -185,7 +185,7 @@ } .contacts-count { - padding: 0 20px 12px; + padding: 0 16px 10px; font-size: 13px; color: var(--text-secondary); @@ -211,7 +211,7 @@ align-items: center; justify-content: space-between; gap: 10px; - padding: 0 20px 12px; + padding: 0 16px 10px; .checkbox-item { font-size: 13px; @@ -249,7 +249,7 @@ .issue-card { border: 1px solid color-mix(in srgb, var(--danger, #ef4444) 45%, var(--border-color)); background: color-mix(in srgb, var(--danger, #ef4444) 8%, var(--card-bg)); - border-radius: 12px; + border-radius: 8px; padding: 14px; color: var(--text-primary); @@ -303,7 +303,7 @@ align-items: center; gap: 6px; cursor: pointer; - transition: all 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; &:hover { color: var(--text-primary); @@ -335,7 +335,7 @@ .contacts-list { flex: 1; overflow-y: auto; - padding: 0 12px 12px; + padding: 0 10px 10px; position: relative; &::-webkit-scrollbar { @@ -358,7 +358,7 @@ position: absolute; left: 0; right: 0; - height: 76px; + height: 64px; padding-bottom: 4px; will-change: transform; } @@ -366,12 +366,12 @@ .contact-item { display: flex; align-items: center; - gap: 12px; - padding: 12px; - height: 72px; + gap: 10px; + padding: 8px 10px; + height: 60px; box-sizing: border-box; - border-radius: 10px; - transition: all 0.2s; + border-radius: 8px; + transition: background 0.15s ease; cursor: pointer; margin-bottom: 0; @@ -402,10 +402,10 @@ } .contact-avatar { - width: 44px; - height: 44px; - border-radius: 10px; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + width: 38px; + height: 38px; + border-radius: 8px; + background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; @@ -419,8 +419,8 @@ } span { - color: #fff; - font-size: 16px; + color: var(--text-secondary); + font-size: 14px; font-weight: 600; } } @@ -484,23 +484,23 @@ display: flex; flex-direction: column; align-items: center; - gap: 12px; - padding-bottom: 24px; + gap: 10px; + padding-bottom: 18px; border-bottom: 1px solid var(--border-color); - margin-bottom: 20px; + margin-bottom: 16px; .detail-avatar { - width: 80px; - height: 80px; - border-radius: 16px; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + width: 68px; + height: 68px; + border-radius: 8px; + background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; overflow: hidden; img { width: 100%; height: 100%; object-fit: cover; } - span { color: #fff; font-size: 28px; font-weight: 600; } + span { color: var(--text-secondary); font-size: 24px; font-weight: 600; } } .detail-name { @@ -511,7 +511,7 @@ } .detail-info-list { - margin-bottom: 24px; + margin-bottom: 18px; .detail-row { display: flex; @@ -565,15 +565,15 @@ align-items: center; justify-content: center; gap: 8px; - padding: 12px; + padding: 10px 12px; background: var(--primary); color: #fff; border: none; - border-radius: 10px; + border-radius: 8px; font-size: 14px; font-weight: 500; cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease; &:hover { background: var(--primary-hover); } } @@ -600,7 +600,7 @@ .settings-content { flex: 1; overflow-y: auto; - padding: 20px 24px; + padding: 16px 18px; &::-webkit-scrollbar { width: 6px; @@ -613,7 +613,7 @@ } .setting-section { - margin-bottom: 28px; + margin-bottom: 22px; h3 { font-size: 13px; @@ -621,7 +621,7 @@ color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; - margin: 0 0 14px; + margin: 0 0 10px; } } @@ -631,10 +631,9 @@ .select-trigger { width: 100%; - padding: 10px 16px; + padding: 9px 12px; border: 1px solid var(--border-color); - border-radius: 9999px; - /* Rounded pill shape */ + border-radius: 8px; font-size: 14px; background: var(--bg-primary); color: var(--text-primary); @@ -643,7 +642,7 @@ justify-content: space-between; gap: 8px; cursor: pointer; - transition: all 0.2s; + transition: border-color 0.15s ease, box-shadow 0.15s ease; &:hover { border-color: var(--text-tertiary); @@ -671,14 +670,12 @@ right: 0; background: color-mix(in srgb, var(--bg-primary) 85%, var(--bg-secondary)); border: 1px solid var(--border-color); - border-radius: 12px; + border-radius: 8px; padding: 6px; - box-shadow: var(--shadow-md); + box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12); z-index: 20; max-height: 260px; overflow-y: auto; - backdrop-filter: blur(14px); - -webkit-backdrop-filter: blur(14px); } .select-option { @@ -689,10 +686,10 @@ gap: 4px; padding: 10px 12px; border: none; - border-radius: 10px; + border-radius: 8px; background: transparent; cursor: pointer; - transition: all 0.15s; + transition: background 0.15s ease, color 0.15s ease; color: var(--text-primary); font-size: 14px; @@ -740,9 +737,9 @@ display: flex; align-items: center; gap: 10px; - padding: 12px 16px; + padding: 10px 12px; background: var(--bg-secondary); - border-radius: 10px; + border-radius: 8px; font-size: 13px; color: var(--text-primary); margin-bottom: 12px; @@ -774,7 +771,7 @@ font-weight: 500; color: var(--text-primary); cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; &:hover { background: var(--bg-hover); @@ -786,10 +783,6 @@ } } - &:active { - transform: scale(0.98); - } - svg { color: var(--text-secondary); transition: color 0.2s; @@ -797,7 +790,7 @@ } .export-action { - padding: 20px 24px; + padding: 16px 18px; border-top: 1px solid var(--border-color); } @@ -807,15 +800,15 @@ align-items: center; justify-content: center; gap: 10px; - padding: 14px 24px; + padding: 12px 16px; background: var(--primary); color: #fff; border: none; - border-radius: 12px; + border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; - transition: all 0.2s; + transition: background 0.15s ease; &:hover:not(:disabled) { background: var(--primary-hover); diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx index e2d04a3..ff90aff 100644 --- a/src/pages/ContactsPage.tsx +++ b/src/pages/ContactsPage.tsx @@ -16,7 +16,7 @@ interface ContactEnrichInfo { const AVATAR_ENRICH_BATCH_SIZE = 80 const SEARCH_DEBOUNCE_MS = 120 -const VIRTUAL_ROW_HEIGHT = 76 +const VIRTUAL_ROW_HEIGHT = 64 const VIRTUAL_OVERSCAN = 10 const DEFAULT_CONTACTS_LOAD_TIMEOUT_MS = 10000 const AVATAR_RECHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 0224b90..6616b9a 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -2,15 +2,14 @@ min-height: 100%; height: 100%; margin: -24px -24px 0; - padding: 18px 22px 12px; + padding: 16px 18px 12px; background: var(--bg-primary); /* Minimal background matching Footprint */ display: flex; flex-direction: column; - gap: 16px; + gap: 12px; overflow-x: hidden; overflow-y: hidden; - animation: exportPageEnter 0.34s ease-out; .spin { animation: exportSpin 1s linear infinite; @@ -22,7 +21,7 @@ flex-shrink: 0; position: relative; z-index: 55; - animation: exportSectionReveal 0.34s ease both; + animation: none; } .export-top-bar { @@ -38,8 +37,8 @@ justify-content: flex-start; gap: 12px; flex-wrap: wrap; - margin-bottom: 8px; - animation: exportSectionReveal 0.38s ease both; + margin-bottom: 6px; + animation: none; } .session-load-detail-entry { @@ -137,7 +136,7 @@ font-size: 18px; font-weight: 700; color: var(--text-primary); - letter-spacing: 0.2px; + letter-spacing: 0; } .section-info-tooltip { @@ -209,10 +208,10 @@ width: min(820px, 100%); max-height: min(78vh, 860px); overflow: hidden; - border-radius: 14px; + border-radius: 8px; border: 1px solid var(--border-color); background: var(--bg-primary); - box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18); display: flex; flex-direction: column; animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; @@ -530,10 +529,10 @@ width: min(760px, 100%); max-height: min(82vh, 900px); overflow: hidden; - border-radius: 16px; + border-radius: 10px; border: 1px solid var(--border-color); background: var(--bg-primary); - box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18); display: flex; flex-direction: column; animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; @@ -559,7 +558,7 @@ width: 44px; height: 44px; border-radius: 12px; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + background: var(--bg-tertiary); display: inline-flex; align-items: center; justify-content: center; @@ -754,29 +753,28 @@ --top-inline-control-height: 34px; flex: 0 1 980px; width: min(980px, 100%); - background: - linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 75%, var(--bg-primary)) 0%, var(--card-bg) 100%); + background: var(--bg-secondary); border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); - border-radius: 14px; - box-shadow: 0 14px 28px rgba(15, 23, 42, 0.08); - padding: 13px; + border-radius: 8px; + box-shadow: none; + padding: 10px; display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(240px, 1fr) auto; gap: 10px; align-items: stretch; - animation: exportSectionReveal 0.4s ease both; - transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; + animation: none; + transition: border-color 0.15s ease, background 0.15s ease; &:hover { border-color: color-mix(in srgb, var(--primary) 30%, var(--border-color)); - box-shadow: 0 16px 30px rgba(15, 23, 42, 0.1); + box-shadow: none; } .control-label { font-size: 11px; color: var(--text-secondary); font-weight: 600; - letter-spacing: 0.2px; + letter-spacing: 0; width: 78px; flex: 0 0 78px; line-height: 1.2; @@ -879,7 +877,7 @@ .more-export-settings-btn { min-height: var(--top-inline-control-height); - border-radius: 10px; + border-radius: 8px; border: 1px solid var(--border-color); background: var(--bg-secondary); color: var(--text-primary); @@ -889,13 +887,12 @@ line-height: 1; white-space: nowrap; cursor: pointer; - transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease, transform 0.12s ease; + transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease; &:hover { border-color: var(--primary); color: var(--primary); background: color-mix(in srgb, var(--primary) 6%, var(--bg-secondary)); - transform: translateY(-1px); } } @@ -934,8 +931,8 @@ max-width: calc(100vw - 40px); background: var(--bg-secondary-solid, #ffffff); border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); - border-radius: 14px; - box-shadow: 0 22px 38px rgba(15, 23, 42, 0.2); + border-radius: 8px; + box-shadow: 0 14px 30px rgba(15, 23, 42, 0.16); padding: 6px; z-index: 3000; max-height: 260px; @@ -1070,14 +1067,13 @@ .task-center-card { min-width: 92px; - min-height: 42px; + min-height: 38px; margin-left: auto; border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); - border-radius: 14px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 80%, var(--bg-primary)) 0%, var(--card-bg) 100%); + border-radius: 8px; + background: var(--bg-secondary); color: var(--text-primary); - padding: 10px 12px; + padding: 8px 11px; display: inline-flex; align-items: center; justify-content: center; @@ -1087,15 +1083,14 @@ cursor: pointer; flex-shrink: 0; align-self: stretch; - transition: border-color 0.14s ease, color 0.14s ease, box-shadow 0.14s ease, transform 0.14s ease, background 0.14s ease; - animation: exportSectionReveal 0.46s ease both; + transition: border-color 0.14s ease, color 0.14s ease, background 0.14s ease; + animation: none; &:hover { border-color: color-mix(in srgb, var(--primary) 48%, var(--border-color)); color: var(--primary); - transform: translateY(-2px); background: color-mix(in srgb, var(--primary) 7%, var(--card-bg)); - box-shadow: 0 10px 18px rgba(15, 23, 42, 0.12); + box-shadow: none; } &.has-alert { @@ -1120,10 +1115,10 @@ width: min(720px, 100%); max-height: min(80vh, 860px); overflow: hidden; - border-radius: 14px; + border-radius: 8px; border: 1px solid var(--border-color); background: var(--bg-primary); - box-shadow: 0 22px 46px rgba(0, 0, 0, 0.28); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18); display: flex; flex-direction: column; animation: exportModalPopIn 0.24s cubic-bezier(0.2, 0.78, 0.26, 1) both; @@ -1180,35 +1175,32 @@ justify-content: center; padding: 0 5px; line-height: 1; - box-shadow: 0 0 0 2px rgba(255, 77, 79, 0.16); - animation: exportTaskBadgePulse 1.2s ease-in-out infinite; + box-shadow: none; + animation: none; } .content-card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(142px, 1fr)); - gap: 10px; + gap: 8px; flex-shrink: 0; - animation: exportSectionReveal 0.52s ease both; + animation: none; } .content-card { border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); - border-radius: 13px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 82%, var(--bg-primary)) 0%, var(--card-bg) 100%); - box-shadow: 0 10px 20px rgba(15, 23, 42, 0.06); - padding: 11px; + border-radius: 8px; + background: var(--bg-secondary); + box-shadow: none; + padding: 10px; display: flex; flex-direction: column; gap: 8px; - transition: border-color 0.16s ease, box-shadow 0.16s ease, transform 0.16s ease, background 0.16s ease; - animation: exportCardReveal 0.42s ease both; + transition: border-color 0.15s ease, background 0.15s ease; + animation: none; &:hover { border-color: color-mix(in srgb, var(--primary) 38%, var(--border-color)); - box-shadow: 0 16px 24px rgba(15, 23, 42, 0.11); - transform: translateY(-2px); background: color-mix(in srgb, var(--primary) 4%, var(--card-bg)); } @@ -1263,8 +1255,8 @@ .card-export-btn { margin-top: auto; border: 1px solid transparent; - border-radius: 9px; - padding: 8px 10px; + border-radius: 8px; + padding: 7px 10px; cursor: pointer; font-size: 13px; font-weight: 600; @@ -1281,8 +1273,7 @@ &.primary:hover { background: var(--primary-hover); - transform: translateY(-1px); - box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 24%, transparent); + box-shadow: none; } &.secondary { @@ -1295,7 +1286,6 @@ border-color: color-mix(in srgb, var(--primary) 28%, transparent); color: var(--text-primary); background: color-mix(in srgb, var(--bg-primary) 94%, var(--primary) 6%); - transform: translateY(-1px); } &:disabled { @@ -1372,10 +1362,10 @@ .task-center-modal { width: min(980px, calc(100vw - 40px)); max-height: calc(100vh - 72px); - border-radius: 14px; + border-radius: 10px; border: 1px solid var(--border-color); background: var(--bg-secondary-solid, #ffffff); - box-shadow: 0 20px 48px rgba(0, 0, 0, 0.24); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18); display: flex; flex-direction: column; overflow: hidden; @@ -1677,16 +1667,15 @@ flex-direction: column; overflow: visible; border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); - border-radius: 14px; - background: - linear-gradient(180deg, color-mix(in srgb, var(--card-bg) 72%, var(--bg-primary)) 0%, color-mix(in srgb, var(--card-bg) 90%, var(--bg-primary)) 100%); - box-shadow: 0 14px 30px rgba(15, 23, 42, 0.07); - animation: exportSectionReveal 0.58s ease both; - transition: border-color 0.18s ease, box-shadow 0.18s ease, transform 0.18s ease; + border-radius: 8px; + background: var(--bg-secondary); + box-shadow: none; + animation: none; + transition: border-color 0.15s ease, background 0.15s ease; &:hover { border-color: color-mix(in srgb, var(--primary) 20%, var(--border-color)); - box-shadow: 0 18px 36px rgba(15, 23, 42, 0.09); + box-shadow: none; } } @@ -1696,24 +1685,24 @@ gap: 6px; margin: 8px 12px 0; padding: 6px 10px; - border-radius: 999px; + border-radius: 8px; background: rgba(var(--primary-rgb), 0.1); border: 1px solid rgba(var(--primary-rgb), 0.2); color: var(--primary); font-size: 12px; width: fit-content; - animation: exportSectionReveal 0.35s ease both; + animation: none; } .table-toolbar { display: flex; justify-content: space-between; align-items: flex-start; - gap: 12px; + gap: 10px; flex-wrap: wrap; - padding: 12px 14px; + padding: 10px 12px; border-bottom: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); - background: linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 88%, var(--card-bg)) 0%, color-mix(in srgb, var(--bg-primary) 92%, var(--bg-secondary)) 100%); + background: var(--bg-secondary); transition: border-color 0.16s ease, background 0.16s ease; } @@ -1751,7 +1740,7 @@ color: var(--text-secondary); min-height: 32px; padding: 7px 12px; - border-radius: 999px; + border-radius: 8px; cursor: pointer; font-size: 13px; font-weight: 500; @@ -1759,7 +1748,7 @@ display: inline-flex; align-items: center; justify-content: center; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; .tab-btn-content { display: inline-flex; @@ -1791,7 +1780,7 @@ &.active { color: var(--primary); background: color-mix(in srgb, var(--primary) 12%, transparent); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02); + box-shadow: none; .tab-btn-content span:last-child { background: color-mix(in srgb, var(--primary) 16%, transparent); @@ -1822,11 +1811,10 @@ .secondary-btn { min-height: 34px; - border-radius: 10px; - transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease, transform 0.14s ease; + border-radius: 8px; + transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease; &:hover:not(:disabled) { - transform: translateY(-1px); border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color)); color: var(--primary); background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); @@ -1842,14 +1830,14 @@ border-radius: 10px; border: none; background: color-mix(in srgb, var(--text-tertiary) 4%, transparent); - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.03) inset; + box-shadow: none; flex: 1; min-width: 180px; max-width: 320px; - transition: all 0.2s ease; + transition: background 0.15s ease, box-shadow 0.15s ease; &:focus-within { - box-shadow: 0 0 0 2px color-mix(in srgb, var(--primary) 20%, transparent), 0 1px 3px rgba(0, 0, 0, 0.02) inset; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 24%, transparent); background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); } @@ -1874,7 +1862,7 @@ width: 18px; height: 18px; border-radius: 999px; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { color: var(--primary); @@ -1909,31 +1897,31 @@ .table-wrap { --contacts-native-scrollbar-compensation: 18px; - --contacts-row-height: 76px; - --contacts-default-visible-rows: 8; + --contacts-row-height: 64px; + --contacts-default-visible-rows: 9; --contacts-default-list-height: calc(var(--contacts-row-height) * var(--contacts-default-visible-rows)); - --contacts-select-col-width: 34px; - --contacts-avatar-col-width: 44px; - --contacts-inline-padding: 12px; - --contacts-column-gap: 10px; + --contacts-select-col-width: 30px; + --contacts-avatar-col-width: 38px; + --contacts-inline-padding: 10px; + --contacts-column-gap: 8px; --contacts-name-text-width: 9.5em; --contacts-main-col-width: calc(var(--contacts-avatar-col-width) + var(--contacts-column-gap) + var(--contacts-name-text-width)); --contacts-left-sticky-width: calc(var(--contacts-select-col-width) + var(--contacts-main-col-width) + var(--contacts-column-gap)); - --contacts-message-col-width: 104px; - --contacts-media-col-width: 66px; - --contacts-action-col-width: 140px; - --contacts-actions-sticky-width: 180px; - --contacts-table-min-width: 1240px; + --contacts-message-col-width: 94px; + --contacts-media-col-width: 58px; + --contacts-action-col-width: 126px; + --contacts-actions-sticky-width: 160px; + --contacts-table-min-width: 1120px; overflow: hidden; border: none; - border-radius: 12px; + border-radius: 8px; min-height: 320px; height: auto; flex: 1; display: flex; flex-direction: column; background: var(--bg-secondary); - transition: all 0.2s ease; + transition: background 0.15s ease; &:hover { background: color-mix(in srgb, var(--text-tertiary) 2%, var(--bg-secondary)); @@ -2012,7 +2000,7 @@ .issue-card { border: 1px solid color-mix(in srgb, var(--danger, #ef4444) 45%, var(--border-color)); background: color-mix(in srgb, var(--danger, #ef4444) 8%, var(--bg-secondary)); - border-radius: 12px; + border-radius: 8px; padding: 14px; color: var(--text-primary); } @@ -2068,12 +2056,11 @@ align-items: center; gap: 6px; cursor: pointer; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { color: var(--text-primary); background: color-mix(in srgb, var(--text-tertiary) 16%, transparent); - transform: translateY(-1px); } &.primary { @@ -2104,7 +2091,7 @@ display: flex; align-items: center; gap: var(--contacts-column-gap); - padding: 10px var(--contacts-inline-padding) 8px; + padding: 8px var(--contacts-inline-padding) 6px; min-width: max(100%, var(--contacts-table-min-width)); border-bottom: 1px solid color-mix(in srgb, var(--text-tertiary) 6%, transparent); background: var(--contacts-header-bg); @@ -2113,7 +2100,6 @@ font-weight: 600; letter-spacing: 0.01em; flex-shrink: 0; - backdrop-filter: blur(10px); &.is-draggable { cursor: grab; @@ -2273,12 +2259,11 @@ padding: 6px 10px; cursor: pointer; white-space: nowrap; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover:not(:disabled) { color: var(--text-primary); background: color-mix(in srgb, var(--text-tertiary) 16%, transparent); - transform: translateY(-1px); } &:disabled { @@ -2301,13 +2286,12 @@ gap: 6px; white-space: nowrap; flex-shrink: 0; - transition: all 0.2s ease; - box-shadow: 0 2px 6px color-mix(in srgb, var(--primary) 20%, transparent); + transition: background 0.15s ease; + box-shadow: none; &:hover:not(:disabled) { background: color-mix(in srgb, var(--primary) 85%, #fff); - transform: translateY(-1px); - box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 30%, transparent); + box-shadow: none; } .selection-export-count { @@ -2382,19 +2366,18 @@ display: flex; align-items: center; gap: var(--contacts-column-gap); - padding: 12px var(--contacts-inline-padding); + padding: 8px var(--contacts-inline-padding); min-width: max(100%, var(--contacts-table-min-width)); - height: 72px; + height: 60px; box-sizing: border-box; - border-radius: 10px; - transition: all 0.2s ease; + border-radius: 8px; + transition: background 0.15s ease; cursor: default; background: var(--contacts-row-bg); box-shadow: none; &:hover { background: color-mix(in srgb, var(--text-tertiary) 6%, transparent); - transform: translateX(2px); } } @@ -2421,10 +2404,10 @@ } .contact-avatar { - width: 44px; - height: 44px; - border-radius: 10px; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + width: var(--contacts-avatar-col-width); + height: var(--contacts-avatar-col-width); + border-radius: 8px; + background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; @@ -2438,8 +2421,8 @@ } span { - color: #fff; - font-size: 16px; + color: var(--text-secondary); + font-size: 14px; font-weight: 600; } } @@ -2740,7 +2723,7 @@ border-radius: 8px; overflow: hidden; flex-shrink: 0; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; @@ -2752,7 +2735,7 @@ } span { - color: #fff; + color: var(--text-secondary); font-size: 14px; font-weight: 600; } @@ -2811,12 +2794,11 @@ font-weight: 500; cursor: pointer; white-space: nowrap; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { color: var(--text-primary); background: color-mix(in srgb, var(--text-tertiary) 14%, transparent); - transform: translateY(-1px); } &.active { @@ -2923,7 +2905,7 @@ height: 100%; max-height: calc(100vh - 24px); border: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent); - border-radius: 20px; + border-radius: 10px; background: var(--bg-primary); display: flex; flex-direction: column; @@ -2949,8 +2931,8 @@ .detail-header-avatar { width: 36px; height: 36px; - border-radius: 10px; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + border-radius: 8px; + background: var(--bg-tertiary); display: flex; align-items: center; justify-content: center; @@ -3248,13 +3230,12 @@ align-items: center; justify-content: center; cursor: pointer; - transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease, transform 0.14s ease; + transition: border-color 0.14s ease, background 0.14s ease, color 0.14s ease; &:hover:not(:disabled) { border-color: color-mix(in srgb, var(--primary) 38%, var(--border-color)); color: var(--primary); background: color-mix(in srgb, var(--primary) 9%, var(--bg-secondary)); - transform: translateY(-1px); } &:focus-visible { @@ -3321,10 +3302,10 @@ .export-session-sns-dialog { width: min(760px, 100%); max-height: min(86vh, 860px); - border-radius: 20px; + border-radius: 10px; border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent); background: var(--bg-primary); - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.16); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16); display: flex; flex-direction: column; overflow: hidden; @@ -3349,8 +3330,8 @@ .sns-dialog-avatar { width: 42px; height: 42px; - border-radius: 10px; - background: linear-gradient(135deg, var(--primary), var(--primary-hover)); + border-radius: 8px; + background: var(--bg-tertiary); overflow: hidden; flex-shrink: 0; display: flex; @@ -3739,7 +3720,7 @@ position: fixed; inset: 0; background: rgba(15, 18, 28, 0.48); - backdrop-filter: blur(7px); + backdrop-filter: blur(2px); display: flex; align-items: center; justify-content: center; @@ -3752,9 +3733,9 @@ max-height: calc(100vh - 40px); background: var(--card-bg); border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); - border-radius: 16px; - box-shadow: 0 26px 52px rgba(0, 0, 0, 0.3); - padding: 16px 16px 14px; + border-radius: 10px; + box-shadow: 0 18px 42px rgba(0, 0, 0, 0.22); + padding: 14px 14px 12px; display: flex; flex-direction: column; overflow: hidden; @@ -3789,9 +3770,9 @@ h3 { margin: 0; color: var(--text-primary); - font-size: 22px; + font-size: 20px; line-height: 1.2; - letter-spacing: 0.2px; + letter-spacing: 0; } } @@ -3811,7 +3792,7 @@ .close-icon-btn { border: 1px solid color-mix(in srgb, var(--border-color) 84%, transparent); background: color-mix(in srgb, var(--bg-secondary) 86%, var(--bg-primary)); - border-radius: 10px; + border-radius: 8px; width: 34px; height: 34px; display: flex; @@ -3819,20 +3800,19 @@ justify-content: center; cursor: pointer; color: var(--text-secondary); - transition: border-color 0.16s ease, color 0.16s ease, transform 0.16s ease, background 0.16s ease; + transition: border-color 0.16s ease, color 0.16s ease, background 0.16s ease; &:hover { border-color: color-mix(in srgb, var(--primary) 42%, var(--border-color)); color: var(--primary); background: color-mix(in srgb, var(--primary) 7%, var(--bg-primary)); - transform: translateY(-1px); } } .dialog-section { border: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); - border-radius: 12px; - padding: 13px 14px; + border-radius: 8px; + padding: 12px; background: color-mix(in srgb, var(--bg-secondary) 78%, var(--bg-primary)); h4 { @@ -3840,7 +3820,7 @@ font-size: 15px; color: var(--text-primary); font-weight: 700; - letter-spacing: 0.2px; + letter-spacing: 0; line-height: 1.3; } } @@ -3924,7 +3904,7 @@ border-radius: 10px; background: transparent; cursor: pointer; - transition: all 0.15s; + transition: background 0.15s ease, color 0.15s ease; color: var(--text-primary); font-size: 14px; @@ -4007,8 +3987,8 @@ width: 100%; min-height: 0; border: 1px solid color-mix(in srgb, var(--border-color) 88%, transparent); - border-radius: 11px; - padding: 10px 11px; + border-radius: 8px; + padding: 9px 10px; text-align: left; background: color-mix(in srgb, var(--bg-primary) 70%, var(--bg-secondary)); cursor: pointer; @@ -4016,7 +3996,7 @@ flex-direction: column; align-items: flex-start; justify-content: flex-start; - transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; + transition: border-color 0.15s ease, background 0.15s ease; .format-label { font-size: 14px; @@ -4034,7 +4014,6 @@ &:hover { border-color: color-mix(in srgb, var(--primary) 36%, var(--border-color)); - transform: translateY(-1px); } &.active { @@ -4102,20 +4081,19 @@ .media-option-card { position: relative; border: 1px solid color-mix(in srgb, var(--border-color) 86%, transparent); - border-radius: 12px; - background: color-mix(in srgb, var(--bg-primary) 72%, var(--bg-secondary)); - min-height: 74px; - padding: 10px 11px; + border-radius: 8px; + background: color-mix(in srgb, var(--bg-primary) 76%, var(--bg-secondary)); + min-height: 68px; + padding: 9px 10px; display: flex; align-items: center; justify-content: space-between; gap: 9px; cursor: pointer; - transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; + transition: border-color 0.15s ease, background 0.15s ease, box-shadow 0.15s ease; &:hover { border-color: color-mix(in srgb, var(--primary) 46%, var(--border-color)); - transform: translateY(-1px); } &:has(.media-option-input:focus-visible) { @@ -4126,7 +4104,7 @@ &.active { border-color: color-mix(in srgb, var(--primary) 76%, var(--border-color)); background: color-mix(in srgb, var(--primary) 12%, var(--bg-secondary)); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary) 28%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 26%, transparent); } } @@ -4147,7 +4125,7 @@ .media-option-icon { width: 30px; height: 30px; - border-radius: 9px; + border-radius: 8px; border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); background: color-mix(in srgb, var(--bg-primary) 86%, var(--bg-secondary)); color: var(--text-secondary); @@ -4428,7 +4406,7 @@ font: inherit; appearance: none; -webkit-appearance: none; - transition: border-color 0.15s ease, background 0.15s ease, transform 0.15s ease; + transition: border-color 0.15s ease, background 0.15s ease; &:focus-visible { outline: 2px solid rgba(var(--primary-rgb), 0.35); @@ -4437,7 +4415,6 @@ &:hover { border-color: color-mix(in srgb, var(--primary) 44%, var(--border-color)); - transform: translateY(-1px); } span { @@ -4467,14 +4444,12 @@ justify-content: flex-end; gap: 10px; flex-shrink: 0; - background: linear-gradient(180deg, - transparent, - var(--card-bg) 38%); + background: var(--card-bg); } .primary-btn, .secondary-btn { - border-radius: 10px; + border-radius: 8px; min-height: 38px; padding: 8px 14px; font-size: 13px; @@ -4484,7 +4459,7 @@ align-items: center; gap: 6px; cursor: pointer; - transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease, transform 0.15s ease; + transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease; } .primary-btn { @@ -4495,7 +4470,6 @@ &:hover { background: var(--primary-hover); - transform: translateY(-1px); } &:disabled { @@ -4513,7 +4487,6 @@ border-color: var(--primary); color: var(--primary); background: color-mix(in srgb, var(--primary) 7%, var(--bg-secondary)); - transform: translateY(-1px); } } @@ -5028,10 +5001,10 @@ .table-wrap { --contacts-inline-padding: 10px; --contacts-name-text-width: 10em; - --contacts-main-col-width: calc(44px + 10px + var(--contacts-name-text-width)); - --contacts-message-col-width: 104px; - --contacts-media-col-width: 62px; - --contacts-action-col-width: 140px; + --contacts-main-col-width: calc(var(--contacts-avatar-col-width) + var(--contacts-column-gap) + var(--contacts-name-text-width)); + --contacts-message-col-width: 94px; + --contacts-media-col-width: 56px; + --contacts-action-col-width: 126px; } .table-wrap .contacts-list-header { @@ -5094,7 +5067,7 @@ width: calc(100vw - 20px); max-height: calc(100vh - 20px); padding: 12px 10px 10px; - border-radius: 14px; + border-radius: 8px; } .dialog-header { @@ -5266,10 +5239,10 @@ .automation-modal { width: min(680px, 100%); max-height: min(80vh, 820px); - border-radius: 20px; + border-radius: 10px; border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent); background: var(--bg-primary); - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.16); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16); display: flex; flex-direction: column; overflow: hidden; @@ -5320,9 +5293,9 @@ } .automation-task-card { - border-radius: 14px; + border-radius: 8px; background: color-mix(in srgb, var(--text-tertiary) 5%, transparent); - padding: 14px 16px; + padding: 12px 14px; display: flex; align-items: flex-start; gap: 12px; @@ -5411,10 +5384,10 @@ .automation-editor-modal { width: min(560px, 100%); max-height: min(88vh, 900px); - border-radius: 20px; + border-radius: 10px; border: 1px solid color-mix(in srgb, var(--text-tertiary) 8%, transparent); background: var(--bg-primary); - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.18); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.16); display: flex; flex-direction: column; overflow: hidden; @@ -5683,7 +5656,7 @@ border-radius: 4px; border: 1px solid color-mix(in srgb, var(--text-tertiary) 30%, transparent); background: transparent; - transition: all 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease; position: relative; &:hover { @@ -5724,7 +5697,7 @@ font-size: 12px; font-weight: 500; cursor: pointer; - transition: all 0.18s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { background: color-mix(in srgb, var(--text-tertiary) 14%, transparent); @@ -5749,7 +5722,7 @@ .automation-draft-summary { padding: 10px 12px; - border-radius: 10px; + border-radius: 8px; background: color-mix(in srgb, var(--text-tertiary) 5%, transparent); font-size: 12px; color: var(--text-secondary); @@ -5768,7 +5741,7 @@ justify-content: center; cursor: pointer; flex-shrink: 0; - transition: all 0.18s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { background: color-mix(in srgb, var(--text-tertiary) 16%, transparent); @@ -5778,20 +5751,19 @@ .primary-btn { border: none; - border-radius: 9px; + border-radius: 8px; padding: 8px 18px; background: var(--primary); color: #fff; font-size: 13px; font-weight: 600; cursor: pointer; - transition: all 0.2s ease; - box-shadow: 0 2px 6px color-mix(in srgb, var(--primary) 20%, transparent); + transition: background 0.15s ease; + box-shadow: none; &:hover:not(:disabled) { background: color-mix(in srgb, var(--primary) 85%, #fff); - transform: translateY(-1px); - box-shadow: 0 8px 14px color-mix(in srgb, var(--primary) 30%, transparent); + box-shadow: none; } &:disabled { @@ -5819,7 +5791,7 @@ padding: 0 10px; font-size: 13px; outline: none; - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + transition: background 0.15s ease, box-shadow 0.15s ease; font-variant-numeric: tabular-nums; &:hover { diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 758a1ca..02e1d8e 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -9552,7 +9552,7 @@ function ExportPage() { customScrollParent={contactsListScrollParent ?? undefined} data={filteredContacts} computeItemKey={(_, contact) => contact.username} - fixedItemHeight={76} + fixedItemHeight={64} itemContent={renderContactRow} rangeChanged={handleContactsRangeChanged} atTopStateChange={setIsContactsListAtTop} diff --git a/src/pages/InsightInboxPage.scss b/src/pages/InsightInboxPage.scss index 84b8b85..b2c5484 100644 --- a/src/pages/InsightInboxPage.scss +++ b/src/pages/InsightInboxPage.scss @@ -1,5 +1,5 @@ .insight-inbox-page { - --insight-panel-width: 360px; + --insight-panel-width: 328px; --insight-card-bg: var(--bg-secondary); display: flex; height: calc(100% + 48px); @@ -14,15 +14,15 @@ min-width: 0; display: flex; flex-direction: column; - padding: 18px 24px 14px; + padding: 16px 20px 12px; } .insight-inbox-header { display: flex; align-items: center; justify-content: space-between; - gap: 16px; - padding: 0 4px 12px; + gap: 14px; + padding: 0 2px 10px; border-bottom: 1px solid var(--border-color); } @@ -30,7 +30,7 @@ min-width: 0; display: flex; flex-direction: column; - gap: 7px; + gap: 6px; } .insight-inbox-title-line { @@ -40,7 +40,7 @@ h2 { margin: 0; - font-size: 22px; + font-size: 21px; font-weight: 700; color: var(--text-primary); } @@ -82,7 +82,7 @@ align-items: center; justify-content: center; cursor: pointer; - transition: color 0.2s ease, background 0.2s ease, border-color 0.2s ease; + transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease; &:hover { color: var(--text-primary); @@ -92,15 +92,15 @@ } .insight-icon-btn { - width: 40px; - height: 40px; - border-radius: 10px; + width: 34px; + height: 34px; + border-radius: 8px; } .insight-action-btn { - width: 30px; - height: 30px; - border-radius: 8px; + width: 28px; + height: 28px; + border-radius: 6px; &.code { color: var(--primary); @@ -118,10 +118,10 @@ } .insight-focus-bar { - margin: 12px 4px 0; - padding: 9px 12px; + margin: 10px 2px 0; + padding: 8px 10px; border: 1px solid rgba(91, 147, 144, 0.22); - border-radius: 10px; + border-radius: 8px; background: rgba(91, 147, 144, 0.08); color: var(--text-secondary); display: flex; @@ -143,14 +143,14 @@ flex: 1; min-height: 0; overflow-y: auto; - padding: 16px 4px 22px; + padding: 12px 2px 18px; } .insight-date-group { display: flex; flex-direction: column; - gap: 12px; - margin-bottom: 18px; + gap: 10px; + margin-bottom: 16px; } .insight-date-label { @@ -158,27 +158,26 @@ top: 0; z-index: 1; width: fit-content; - padding: 5px 10px; - border-radius: 999px; - background: color-mix(in srgb, var(--bg-primary) 86%, transparent); + padding: 4px 8px; + border-radius: 6px; + background: var(--bg-primary); color: var(--text-tertiary); font-size: 12px; - backdrop-filter: blur(10px); } .insight-card { display: flex; - gap: 14px; - padding: 18px; + gap: 12px; + padding: 14px; border: 1px solid var(--border-color); - border-radius: 14px; + border-radius: 8px; background: var(--insight-card-bg); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.04); - transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease; + box-shadow: none; + transition: border-color 0.15s ease, background 0.15s ease; &:hover { border-color: rgba(91, 147, 144, 0.28); - box-shadow: 0 10px 28px rgba(0, 0, 0, 0.07); + background: color-mix(in srgb, var(--bg-secondary) 86%, var(--bg-primary)); } &.unread { @@ -187,7 +186,7 @@ &.focused { border-color: var(--primary); - box-shadow: 0 0 0 3px rgba(91, 147, 144, 0.14), 0 12px 32px rgba(0, 0, 0, 0.08); + box-shadow: inset 0 0 0 1px rgba(91, 147, 144, 0.28); } } @@ -200,7 +199,7 @@ flex: 1; display: flex; flex-direction: column; - gap: 12px; + gap: 10px; } .insight-card-header { @@ -277,8 +276,8 @@ .insight-body { margin: 0; color: var(--text-primary); - font-size: 15px; - line-height: 1.72; + font-size: 14px; + line-height: 1.66; white-space: pre-wrap; word-break: break-word; } @@ -286,14 +285,14 @@ .insight-filter-panel { width: var(--insight-panel-width); flex-shrink: 0; - padding: 24px 24px 18px; + padding: 18px 18px 14px; border-left: 1px solid var(--border-color); - background: color-mix(in srgb, var(--bg-secondary) 70%, var(--bg-primary)); + background: var(--bg-secondary); overflow-y: auto; } .insight-filter-header { - margin-bottom: 18px; + margin-bottom: 14px; h3 { margin: 0; @@ -304,10 +303,10 @@ .insight-filter-widget { border: 1px solid var(--border-color); - border-radius: 12px; - background: var(--bg-secondary); - padding: 14px; - margin-bottom: 14px; + border-radius: 8px; + background: var(--bg-primary); + padding: 12px; + margin-bottom: 12px; } .insight-widget-title { @@ -330,14 +329,14 @@ display: flex; align-items: center; gap: 6px; - border-radius: 9px; + border-radius: 8px; background: var(--bg-tertiary); padding: 0 9px; input { min-width: 0; flex: 1; - height: 38px; + height: 34px; border: none; outline: none; background: transparent; @@ -410,12 +409,12 @@ .insight-contact-row { width: 100%; - min-height: 42px; + min-height: 38px; display: flex; align-items: center; gap: 9px; border: none; - border-radius: 9px; + border-radius: 8px; background: transparent; color: var(--text-secondary); padding: 7px 8px; @@ -478,7 +477,6 @@ inset: 0; z-index: 1000; background: rgba(0, 0, 0, 0.45); - backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center; @@ -490,9 +488,9 @@ display: flex; flex-direction: column; border: 1px solid var(--border-color); - border-radius: 14px; + border-radius: 10px; background: var(--bg-secondary); - box-shadow: 0 18px 48px rgba(0, 0, 0, 0.18); + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.14); overflow: hidden; } @@ -501,7 +499,7 @@ justify-content: space-between; align-items: center; gap: 16px; - padding: 16px 18px; + padding: 14px 16px; border-bottom: 1px solid var(--border-color); background: var(--bg-tertiary); @@ -549,7 +547,7 @@ .insight-log-body { flex: 1; overflow-y: auto; - padding: 18px; + padding: 16px; background: var(--bg-primary); section { @@ -567,7 +565,7 @@ margin: 0; padding: 12px; border: 1px solid var(--border-color); - border-radius: 10px; + border-radius: 8px; background: var(--bg-secondary); color: var(--text-primary); font-family: Consolas, Monaco, 'Courier New', monospace; diff --git a/src/pages/MyFootprintPage.scss b/src/pages/MyFootprintPage.scss index af1dd69..96e1928 100644 --- a/src/pages/MyFootprintPage.scss +++ b/src/pages/MyFootprintPage.scss @@ -11,7 +11,6 @@ gap: 24px; overflow-y: auto; overflow-x: hidden; - animation: footprintPageEnter 0.4s ease-out; .card-surface { /* Removing border and strong shadows, just subtle background if any */ @@ -31,7 +30,7 @@ gap: 20px; padding: 10px 0 20px 0; border-bottom: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent); - animation: footprintFadeSlideUp 0.3s ease both; + animation: footprintFadeSlideUp 0.22s ease both; } .footprint-title-wrap { @@ -41,10 +40,10 @@ h1 { margin: 0; - font-size: 26px; + font-size: 24px; font-weight: 600; line-height: 1.3; - letter-spacing: -0.3px; + letter-spacing: 0; color: var(--text-primary); } @@ -84,7 +83,7 @@ font-size: 13px; font-weight: 500; cursor: pointer; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { color: var(--text-primary); @@ -93,7 +92,7 @@ &.active { color: var(--text-primary); background: var(--bg-primary); - box-shadow: 0 1px 3px rgba(0,0,0,0.05); + box-shadow: none; } } @@ -117,9 +116,9 @@ padding: 6px 10px; font-size: 13px; font-weight: 500; - transition: all 0.2s ease; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; cursor: pointer; - box-shadow: 0 1px 2px rgba(0,0,0,0.02) inset; + box-shadow: none; &:hover { background: color-mix(in srgb, var(--text-tertiary) 12%, transparent); @@ -135,7 +134,7 @@ &::-webkit-calendar-picker-indicator { cursor: pointer; opacity: 0.5; - transition: all 0.2s ease; + transition: opacity 0.15s ease, background 0.15s ease; padding: 4px; margin-left: 4px; margin-right: -4px; @@ -165,7 +164,7 @@ border-radius: 8px; padding: 8px 12px; color: var(--text-tertiary); - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:focus-within { background: color-mix(in srgb, var(--primary) 8%, transparent); @@ -201,7 +200,7 @@ align-items: center; gap: 6px; cursor: pointer; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { background: color-mix(in srgb, var(--text-tertiary) 15%, transparent); @@ -217,8 +216,8 @@ .kpi-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 20px; - padding: 20px 0; + gap: 16px; + padding: 18px 0; border-bottom: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent); } @@ -241,11 +240,11 @@ } strong { - font-size: 32px; + font-size: 30px; font-weight: 300; line-height: 1; color: var(--text-primary); - letter-spacing: -0.5px; + letter-spacing: 0; } small { @@ -259,7 +258,7 @@ } .footprint-ai-result { - border-radius: 10px; + border-radius: 8px; padding: 14px 16px; background: color-mix(in srgb, var(--text-tertiary) 8%, transparent); border: 1px solid color-mix(in srgb, var(--border-color) 40%, transparent); @@ -360,7 +359,7 @@ padding: 6px 12px; border-radius: 6px; cursor: pointer; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &:hover { color: var(--text-primary); @@ -688,11 +687,11 @@ .footprint-export-modal { width: min(520px, 100%); - border-radius: 16px; + border-radius: 10px; background: var(--bg-primary); border: 1px solid color-mix(in srgb, var(--border-color) 60%, transparent); - box-shadow: 0 18px 60px rgba(0, 0, 0, 0.2); - padding: 22px 22px 18px; + box-shadow: 0 14px 38px rgba(0, 0, 0, 0.16); + padding: 18px 18px 16px; display: flex; flex-direction: column; gap: 10px; @@ -700,7 +699,7 @@ h3 { margin: 0; - font-size: 18px; + font-size: 17px; font-weight: 600; color: var(--text-primary); } @@ -716,7 +715,7 @@ .export-modal-icon { width: 34px; height: 34px; - border-radius: 10px; + border-radius: 8px; display: inline-flex; align-items: center; justify-content: center; @@ -740,8 +739,8 @@ .export-modal-path { display: block; margin-top: 2px; - padding: 10px 12px; - border-radius: 10px; + padding: 9px 11px; + border-radius: 8px; font-family: inherit; font-weight: 500; font-size: 12px; diff --git a/src/pages/ResourcesPage.scss b/src/pages/ResourcesPage.scss index 920fe3f..d4a15d5 100644 --- a/src/pages/ResourcesPage.scss +++ b/src/pages/ResourcesPage.scss @@ -1,37 +1,37 @@ .resources-page.stream-rebuild { --stream-columns: 4; - --stream-grid-gap: 12px; - --stream-card-width: 272px; - --stream-card-height: 356px; - --stream-visual-height: 236px; + --stream-grid-gap: 10px; + --stream-card-width: 248px; + --stream-card-height: 318px; + --stream-visual-height: 210px; --stream-slot-width: calc(var(--stream-card-width) + var(--stream-grid-gap)); --stream-slot-height: calc(var(--stream-card-height) + var(--stream-grid-gap)); --stream-grid-width: calc(var(--stream-slot-width) * var(--stream-columns)); height: calc(100% + 48px); margin: -24px; - padding: 16px 18px; + padding: 14px 16px; position: relative; background: var(--bg-primary); display: flex; flex-direction: column; - gap: 12px; + gap: 10px; overflow: hidden; .stream-toolbar { border: 1px solid color-mix(in srgb, var(--border-color) 78%, transparent); background: var(--card-bg, #f8f9fb); - border-radius: 16px; - padding: 12px; + border-radius: 8px; + padding: 10px; display: flex; justify-content: space-between; - gap: 12px; + gap: 10px; align-items: flex-start; } .toolbar-left { display: flex; flex-direction: column; - gap: 10px; + gap: 8px; min-width: 0; flex: 1; } @@ -41,7 +41,7 @@ align-items: center; width: fit-content; padding: 4px; - border-radius: 12px; + border-radius: 8px; background: color-mix(in srgb, var(--bg-secondary) 85%, transparent); border: 1px solid var(--border-color); @@ -49,16 +49,16 @@ border: none; background: transparent; color: var(--text-secondary, #5f6674); - border-radius: 9px; - padding: 7px 14px; + border-radius: 6px; + padding: 6px 12px; font-size: 13px; cursor: pointer; - transition: all 0.2s ease; + transition: background 0.15s ease, color 0.15s ease; &.active { background: color-mix(in srgb, var(--primary) 18%, var(--card-bg)); color: var(--text-primary, #1c2230); - box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--primary) 45%, transparent); + box-shadow: none; } } } @@ -76,9 +76,9 @@ border: 1px solid color-mix(in srgb, var(--border-color, #d2d7df) 95%, transparent); background: var(--bg-secondary, #f3f5f8); color: var(--text-secondary, #566074); - border-radius: 10px; + border-radius: 8px; padding: 0 10px; - min-height: 36px; + min-height: 34px; box-sizing: border-box; svg { @@ -103,8 +103,8 @@ color: var(--text-primary, #1c2230); font-size: 13px; min-width: 0; - height: 34px; - line-height: 34px; + height: 32px; + line-height: 32px; font-family: "PingFang SC", "Noto Sans SC", "Microsoft YaHei", sans-serif; appearance: none; } @@ -129,8 +129,8 @@ border: 1px solid var(--border-color); background: transparent; color: var(--text-secondary, #5f6674); - border-radius: 10px; - height: 36px; + border-radius: 8px; + height: 34px; padding: 0 12px; cursor: pointer; } @@ -152,8 +152,8 @@ border: 1px solid var(--border-color); background: var(--bg-secondary, #f3f5f8); color: var(--text-secondary, #5f6674); - border-radius: 10px; - height: 34px; + border-radius: 8px; + height: 32px; padding: 0 12px; font-size: 13px; display: inline-flex; @@ -184,9 +184,9 @@ } .stream-state { - height: 120px; + height: 104px; border: 1px dashed var(--border-color); - border-radius: 12px; + border-radius: 8px; background: var(--card-bg); color: var(--text-secondary); display: flex; @@ -204,7 +204,7 @@ flex: 1; min-height: 0; border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); - border-radius: 16px; + border-radius: 8px; background: color-mix(in srgb, var(--card-bg) 94%, transparent); overflow: hidden; display: flex; @@ -226,7 +226,7 @@ justify-content: flex-start; align-items: flex-start; align-content: flex-start; - padding: 10px 0 2px; + padding: 8px 0 2px; width: var(--stream-grid-width); min-width: var(--stream-grid-width); max-width: var(--stream-grid-width); @@ -257,7 +257,7 @@ height: 100%; border: 1px solid var(--border-color); background: var(--bg-secondary); - border-radius: 14px; + border-radius: 8px; overflow: hidden; transition: border-color 0.16s ease; position: relative; @@ -288,7 +288,7 @@ z-index: 4; width: 28px; height: 28px; - border-radius: 9px; + border-radius: 6px; border: 1px solid color-mix(in srgb, var(--danger) 48%, var(--border-color)); color: var(--danger); background: color-mix(in srgb, var(--bg-secondary) 90%, transparent); @@ -297,9 +297,9 @@ justify-content: center; cursor: pointer; opacity: 0; - transform: translateY(-2px) scale(0.96); + transform: none; pointer-events: none; - transition: opacity 0.16s ease, transform 0.16s ease; + transition: opacity 0.15s ease; } .floating-info { @@ -317,7 +317,7 @@ .media-card:hover .floating-delete, .media-card:focus-within .floating-delete { opacity: 1; - transform: translateY(0) scale(1); + transform: none; pointer-events: auto; } @@ -329,7 +329,7 @@ border: 1px solid color-mix(in srgb, var(--primary) 45%, var(--border-color)); background: color-mix(in srgb, var(--bg-secondary) 90%, transparent); color: var(--text-primary); - border-radius: 9px; + border-radius: 6px; height: 28px; padding: 0 8px; font-size: 11px; @@ -408,15 +408,15 @@ align-items: center; justify-content: center; pointer-events: none; - background: linear-gradient(140deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.04)); + background: rgba(255, 255, 255, 0.08); overflow: hidden; &::before { content: ''; position: absolute; inset: -40%; - background: linear-gradient(105deg, transparent 35%, rgba(255, 255, 255, 0.35) 50%, transparent 65%); - animation: decrypt-sheen 1.6s linear infinite; + background: none; + animation: none; pointer-events: none; } } @@ -429,16 +429,14 @@ border-radius: 999px; border: 2px solid rgba(15, 23, 42, 0.2); border-top-color: color-mix(in srgb, var(--primary) 78%, #ffffff); - animation: decrypt-spin 0.85s linear infinite, decrypt-pulse 1.2s ease-in-out infinite; - box-shadow: - 0 0 0 8px rgba(255, 255, 255, 0.26), - 0 10px 24px rgba(15, 23, 42, 0.12); + animation: decrypt-spin 0.85s linear infinite; + box-shadow: none; } } .card-meta { - padding: 9px 10px 8px; - min-height: 66px; + padding: 8px 10px; + min-height: 58px; margin-top: auto; cursor: pointer; border-top: 1px solid color-mix(in srgb, var(--border-color) 70%, transparent); @@ -517,8 +515,8 @@ width: min(420px, calc(100% - 32px)); background: var(--dialog-surface); border: 1px solid color-mix(in srgb, var(--border-color) 90%, transparent); - border-radius: 14px; - box-shadow: 0 24px 64px rgba(0, 0, 0, 0.28); + border-radius: 10px; + box-shadow: 0 18px 40px rgba(0, 0, 0, 0.18); overflow: hidden; } From 0f0f5abb2a72fed4e1b9484c82aab36063d270da Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 6 May 2026 21:45:55 +0800 Subject: [PATCH 11/23] refactor: modernize chat page --- src/pages/Chat/ChatHeader.tsx | 234 +++++++++++ src/pages/Chat/ChatInputArea.tsx | 42 ++ src/pages/Chat/ChatMessageBubble.tsx | 136 +++++++ src/pages/ChatPage.scss | 578 +++++++++++++++++++++++++++ src/pages/ChatPage.tsx | 359 +++++------------ 5 files changed, 1096 insertions(+), 253 deletions(-) create mode 100644 src/pages/Chat/ChatHeader.tsx create mode 100644 src/pages/Chat/ChatInputArea.tsx create mode 100644 src/pages/Chat/ChatMessageBubble.tsx diff --git a/src/pages/Chat/ChatHeader.tsx b/src/pages/Chat/ChatHeader.tsx new file mode 100644 index 0000000..1293b7f --- /dev/null +++ b/src/pages/Chat/ChatHeader.tsx @@ -0,0 +1,234 @@ +import React from 'react' +import { + Aperture, + BarChart3, + Calendar, + Download, + Image as ImageIcon, + Info, + Loader2, + Mic, + RefreshCw, + Search, + Users +} from 'lucide-react' +import { Avatar } from '../../components/Avatar' +import type { ChatSession } from '../../types/models' +import type { BatchVoiceTaskType } from '../../stores/batchTranscribeStore' + +export interface ChatHeaderProps { + session: ChatSession + isGroupChat: boolean + standaloneSessionWindow: boolean + showGroupMembersPanel: boolean + showJumpPopover: boolean + showInSessionSearch: boolean + showDetailPanel: boolean + shouldHideStandaloneDetailButton: boolean + isPrivateSnsSupported: boolean + isExportActionBusy: boolean + isCurrentSessionExporting: boolean + isPreparingExportDialog: boolean + isBatchTranscribing: boolean + runningBatchVoiceTaskType?: BatchVoiceTaskType + isBatchDecrypting: boolean + isRefreshingMessages: boolean + isLoadingMessages: boolean + currentSessionId?: string | null + jumpCalendarWrapRef: React.RefObject + onGroupAnalytics: () => void + onToggleGroupMembersPanel: () => void + onExportCurrentSession: () => void + onOpenSnsTimeline: () => void + onBatchTranscribe: () => void + onBatchDecrypt: () => void + onToggleJumpPopover: () => void + onToggleInSessionSearch: () => void + onRefreshMessages: () => void + onToggleDetailPanel: () => void +} + +function ChatHeader({ + session, + isGroupChat, + standaloneSessionWindow, + showGroupMembersPanel, + showJumpPopover, + showInSessionSearch, + showDetailPanel, + shouldHideStandaloneDetailButton, + isPrivateSnsSupported, + isExportActionBusy, + isCurrentSessionExporting, + isPreparingExportDialog, + isBatchTranscribing, + runningBatchVoiceTaskType, + isBatchDecrypting, + isRefreshingMessages, + isLoadingMessages, + currentSessionId, + jumpCalendarWrapRef, + onGroupAnalytics, + onToggleGroupMembersPanel, + onExportCurrentSession, + onOpenSnsTimeline, + onBatchTranscribe, + onBatchDecrypt, + onToggleJumpPopover, + onToggleInSessionSearch, + onRefreshMessages, + onToggleDetailPanel +}: ChatHeaderProps) { + const sessionName = session.displayName || session.username + const exportTitle = isCurrentSessionExporting + ? '导出中' + : isPreparingExportDialog + ? '正在准备导出模块' + : '导出当前会话' + const batchVoiceTitle = isBatchTranscribing + ? `${runningBatchVoiceTaskType === 'decrypt' ? '批量语音解密' : '批量转写'}中,可在导出页任务中心查看进度` + : '批量语音处理' + + return ( +
+ +
+

{sessionName}

+ {isGroupChat &&
群聊
} +
+
+ {!standaloneSessionWindow && isGroupChat && ( + + )} + {isGroupChat && ( + + )} + {!standaloneSessionWindow && ( + + )} + {!standaloneSessionWindow && isPrivateSnsSupported && ( + + )} + {!standaloneSessionWindow && ( + + )} + {!standaloneSessionWindow && ( + + )} +
+ +
+ + + {!shouldHideStandaloneDetailButton && ( + + )} +
+
+ ) +} + +function areEqual(prev: ChatHeaderProps, next: ChatHeaderProps) { + return ( + prev.session.username === next.session.username && + prev.session.displayName === next.session.displayName && + prev.session.avatarUrl === next.session.avatarUrl && + prev.isGroupChat === next.isGroupChat && + prev.standaloneSessionWindow === next.standaloneSessionWindow && + prev.showGroupMembersPanel === next.showGroupMembersPanel && + prev.showJumpPopover === next.showJumpPopover && + prev.showInSessionSearch === next.showInSessionSearch && + prev.showDetailPanel === next.showDetailPanel && + prev.shouldHideStandaloneDetailButton === next.shouldHideStandaloneDetailButton && + prev.isPrivateSnsSupported === next.isPrivateSnsSupported && + prev.isExportActionBusy === next.isExportActionBusy && + prev.isCurrentSessionExporting === next.isCurrentSessionExporting && + prev.isPreparingExportDialog === next.isPreparingExportDialog && + prev.isBatchTranscribing === next.isBatchTranscribing && + prev.runningBatchVoiceTaskType === next.runningBatchVoiceTaskType && + prev.isBatchDecrypting === next.isBatchDecrypting && + prev.isRefreshingMessages === next.isRefreshingMessages && + prev.isLoadingMessages === next.isLoadingMessages && + prev.currentSessionId === next.currentSessionId && + prev.jumpCalendarWrapRef === next.jumpCalendarWrapRef && + prev.onGroupAnalytics === next.onGroupAnalytics && + prev.onToggleGroupMembersPanel === next.onToggleGroupMembersPanel && + prev.onExportCurrentSession === next.onExportCurrentSession && + prev.onOpenSnsTimeline === next.onOpenSnsTimeline && + prev.onBatchTranscribe === next.onBatchTranscribe && + prev.onBatchDecrypt === next.onBatchDecrypt && + prev.onToggleJumpPopover === next.onToggleJumpPopover && + prev.onToggleInSessionSearch === next.onToggleInSessionSearch && + prev.onRefreshMessages === next.onRefreshMessages && + prev.onToggleDetailPanel === next.onToggleDetailPanel + ) +} + +export default React.memo(ChatHeader, areEqual) diff --git a/src/pages/Chat/ChatInputArea.tsx b/src/pages/Chat/ChatInputArea.tsx new file mode 100644 index 0000000..c8303d5 --- /dev/null +++ b/src/pages/Chat/ChatInputArea.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { Lock, Search } from 'lucide-react' + +export interface ChatInputAreaProps { + disabled?: boolean + placeholder?: string + onFocusSearch?: () => void +} + +function ChatInputArea({ + disabled = true, + placeholder = '聊天记录', + onFocusSearch +}: ChatInputAreaProps) { + return ( +
+ +
+ + {placeholder} +
+
+ ) +} + +function areEqual(prev: ChatInputAreaProps, next: ChatInputAreaProps) { + return ( + prev.disabled === next.disabled && + prev.placeholder === next.placeholder && + prev.onFocusSearch === next.onFocusSearch + ) +} + +export default React.memo(ChatInputArea, areEqual) diff --git a/src/pages/Chat/ChatMessageBubble.tsx b/src/pages/Chat/ChatMessageBubble.tsx new file mode 100644 index 0000000..d88c6fd --- /dev/null +++ b/src/pages/Chat/ChatMessageBubble.tsx @@ -0,0 +1,136 @@ +import React from 'react' +import { Check } from 'lucide-react' +import { Avatar } from '../../components/Avatar' +import type { ChatSession, Message } from '../../types/models' + +export interface ChatMessageBubbleProps { + message: Message + messageKey: string + session: ChatSession + showTime?: boolean + timeText?: string + isSent: boolean + isSystem: boolean + isEmoji?: boolean + isImage?: boolean + isVoice?: boolean + emojiHasAsset?: boolean + emojiError?: boolean + avatarUrl?: string + isGroupChat?: boolean + resolvedSenderName?: string + isSelectionMode?: boolean + isSelected?: boolean + onContextMenu?: (event: React.MouseEvent, message: Message) => void + onToggleSelection?: (messageKey: string, isShiftKey?: boolean) => void + children: React.ReactNode + portal?: React.ReactNode +} + +function SelectionCheckbox({ checked, side }: { checked?: boolean; side: 'left' | 'right' }) { + return ( +
+ {checked && } +
+ ) +} + +function ChatMessageBubble({ + message, + messageKey, + session, + showTime, + timeText, + isSent, + isSystem, + isEmoji, + isImage, + isVoice, + emojiHasAsset, + emojiError, + avatarUrl, + isGroupChat, + resolvedSenderName, + isSelectionMode, + isSelected, + onContextMenu, + onToggleSelection, + children, + portal +}: ChatMessageBubbleProps) { + const bubbleClass = isSystem ? 'system' : (isSent ? 'sent' : 'received') + const avatarName = !isSent + ? (isGroupChat ? (resolvedSenderName || '?') : (session.displayName || session.username)) + : '我' + + return ( + <> + {showTime && timeText && ( +
+ {timeText} +
+ )} +
{ + if (!isSelectionMode) return + event.stopPropagation() + onToggleSelection?.(messageKey, event.shiftKey) + }} + > + {isSelectionMode && !isSent && } + +
onContextMenu?.(event, message)} + > +
+ +
+
+ {isGroupChat && !isSent && ( +
+ {resolvedSenderName || '群成员'} +
+ )} + {children} +
+
+ + {isSelectionMode && isSent && } + {portal} +
+ + ) +} + +function areEqual(prev: ChatMessageBubbleProps, next: ChatMessageBubbleProps) { + return ( + prev.message === next.message && + prev.messageKey === next.messageKey && + prev.session.username === next.session.username && + prev.session.displayName === next.session.displayName && + prev.session.avatarUrl === next.session.avatarUrl && + prev.showTime === next.showTime && + prev.timeText === next.timeText && + prev.isSent === next.isSent && + prev.isSystem === next.isSystem && + prev.isEmoji === next.isEmoji && + prev.isImage === next.isImage && + prev.isVoice === next.isVoice && + prev.emojiHasAsset === next.emojiHasAsset && + prev.emojiError === next.emojiError && + prev.avatarUrl === next.avatarUrl && + prev.isGroupChat === next.isGroupChat && + prev.resolvedSenderName === next.resolvedSenderName && + prev.isSelectionMode === next.isSelectionMode && + prev.isSelected === next.isSelected && + prev.onContextMenu === next.onContextMenu && + prev.onToggleSelection === next.onToggleSelection && + prev.children === next.children && + prev.portal === next.portal + ) +} + +export default React.memo(ChatMessageBubble, areEqual) diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index e998744..39ad989 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -5301,3 +5301,581 @@ .in-session-search-btn.active { color: var(--accent-color, #07c160); } + +// Modern chat surface overrides for the refactored Chat components. +.chat-page { + gap: 0; + background: var(--bg-sidebar); + + &:not(.standalone) { + .message-area { + margin-left: 0; + } + } +} + +.session-sidebar { + background: var(--bg-sidebar); + border-right: 0; + border-radius: 0; +} + +.resize-handle { + background: transparent; + + &:hover { + background: color-mix(in srgb, var(--text-tertiary) 18%, transparent); + } +} + +.message-area { + background: var(--bg-primary); + border-radius: 0; + box-shadow: none; +} + +.chat-page.standalone { + background: var(--bg-primary); + + .session-sidebar { + background: var(--bg-sidebar); + border-right: 0; + backdrop-filter: none; + } + + .message-area { + background: var(--bg-primary); + + .message-header { + min-height: 64px; + padding: 12px 22px; + border-bottom: 0; + background: color-mix(in srgb, var(--bg-primary) 88%, transparent); + backdrop-filter: blur(14px); + box-shadow: 0 1px 0 color-mix(in srgb, var(--border-color) 70%, transparent); + + .session-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + } + + .header-actions .icon-btn { + width: 34px; + height: 34px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--text-secondary); + box-shadow: none; + + &:hover { + background: var(--bg-hover); + color: var(--text-primary); + box-shadow: none; + } + + &.active { + background: var(--primary-light); + color: var(--primary); + } + } + } + + .message-list { + background: var(--bg-primary); + padding: 18px clamp(16px, 3vw, 48px) 104px; + padding-bottom: calc(104px + env(safe-area-inset-bottom)); + } + } +} + +.message-header { + min-height: 64px; + padding: 12px 22px; + border-bottom: 0; + background: color-mix(in srgb, var(--bg-primary) 88%, transparent); + backdrop-filter: blur(14px); + box-shadow: 0 1px 0 color-mix(in srgb, var(--border-color) 70%, transparent); + + .session-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + } + + .header-info { + min-width: 0; + + h3 { + font-size: 15px; + font-weight: 600; + letter-spacing: 0; + line-height: 1.25; + } + } + + .header-actions { + gap: 2px; + } + + .icon-btn { + width: 34px; + height: 34px; + border: 0; + border-radius: 8px; + background: transparent; + color: var(--text-secondary); + box-shadow: none; + + &:hover { + background: var(--bg-hover); + color: var(--text-primary); + box-shadow: none; + } + + &.active { + background: var(--primary-light); + color: var(--primary); + } + } +} + +.message-content-wrapper { + background: var(--bg-primary); +} + +.message-list { + background: var(--bg-primary); + padding: 18px clamp(16px, 3vw, 48px) 104px; + padding-bottom: calc(104px + env(safe-area-inset-bottom)); + gap: 0; +} + +.message-wrapper { + padding-bottom: 14px; + + &.sent { + align-items: flex-end; + } + + &.received { + align-items: flex-start; + } +} + +.message-wrapper-with-selection { + display: flex; + align-items: flex-start; + width: 100%; + cursor: default; + + &[data-sent="true"] { + justify-content: flex-end; + } + + &[data-sent="false"] { + justify-content: flex-start; + } + + &.selectable { + cursor: pointer; + } +} + +.chat-selection-checkbox { + width: 20px; + height: 20px; + border-radius: 6px; + border: 1.5px solid color-mix(in srgb, var(--text-tertiary) 55%, transparent); + background: transparent; + color: var(--on-primary); + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + margin-top: 9px; + + &.left { + margin-right: 12px; + } + + &.right { + margin-left: 12px; + } + + &.checked { + background: var(--primary); + border-color: var(--primary); + } +} + +.message-bubble { + gap: 10px; + max-width: min(76%, 760px); + + .bubble-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--bg-tertiary); + } + + .bubble-body { + min-width: 0; + width: fit-content; + } + + .bubble-content { + border-radius: 18px; + padding: 10px 14px; + line-height: 1.56; + box-shadow: none; + border: 0; + } + + &.sent { + .bubble-content { + background: #3b82f6; + color: #fff; + border-radius: 18px; + } + + .bubble-body { + align-items: flex-end; + } + } + + &.received { + .bubble-content { + background: var(--bg-tertiary); + color: var(--text-primary); + border-radius: 18px; + backdrop-filter: none; + } + + .bubble-body { + align-items: flex-start; + } + } + + &.system { + max-width: min(86%, 680px); + + .bubble-avatar { + display: none; + } + + .bubble-content { + background: color-mix(in srgb, var(--bg-tertiary) 70%, transparent); + color: var(--text-tertiary); + border-radius: 999px; + padding: 6px 12px; + text-align: center; + } + } + + &.emoji, + &.image { + max-width: min(82%, 760px); + + .bubble-content { + background: transparent; + padding: 0; + } + } + + &.voice.sent .bubble-content { + background: var(--bg-tertiary); + color: var(--text-primary); + } +} + +.message-bubble .bubble-content:has(> .link-message), +.message-bubble .bubble-content:has(> .card-message), +.message-bubble .bubble-content:has(> .chat-record-message), +.message-bubble .bubble-content:has(> .solitaire-message), +.message-bubble .bubble-content:has(> .official-message), +.message-bubble .bubble-content:has(> .channel-video-card), +.message-bubble .bubble-content:has(> .location-message), +.message-bubble .bubble-content:has(> .hongbao-message), +.message-bubble .bubble-content:has(> .transfer-message), +.message-bubble .bubble-content:has(> .gift-message), +.message-bubble .bubble-content:has(> .miniapp-message), +.message-bubble .bubble-content:has(> .file-message) { + background: transparent !important; + padding: 0 !important; + border: 0 !important; + box-shadow: none !important; +} + +.sender-name { + color: var(--text-tertiary); + font-size: 12px; + margin-bottom: 5px; +} + +.quoted-message { + background: transparent; + border-left: 2px solid color-mix(in srgb, var(--primary) 62%, var(--text-tertiary)); + border-radius: 0; + padding: 2px 0 2px 10px; + color: var(--text-secondary); +} + +.message-bubble.sent .quoted-message { + background: color-mix(in srgb, #fff 12%, transparent); + border-left-color: color-mix(in srgb, #fff 62%, #3b82f6); + + .quoted-sender, + .quoted-text { + color: color-mix(in srgb, #fff 82%, #3b82f6); + } +} + +.link-message, +.card-message, +.chat-record-message, +.solitaire-message, +.official-message, +.channel-video-card, +.location-message, +.miniapp-message, +.file-message { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + box-shadow: var(--shadow-sm); +} + +.link-message, +.appmsg-rich-card { + width: min(320px, calc(100vw - 112px)); + overflow: hidden; + + &:hover { + background: var(--bg-hover); + border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color)); + } + + .link-thumb, + .link-thumb-placeholder { + border-radius: 8px; + } +} + +.message-bubble.sent .link-message, +.message-bubble.sent .card-message, +.message-bubble.sent .miniapp-message, +.message-bubble.sent .appmsg-rich-card { + background: var(--card-bg); + border-color: var(--border-color); + + .card-name, + .miniapp-title, + .link-title { + color: var(--text-primary); + } + + .card-label, + .miniapp-label, + .link-desc, + .appmsg-url-line { + color: var(--text-secondary); + } +} + +.hongbao-message, +.transfer-message, +.gift-message { + width: min(280px, calc(100vw - 112px)); + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 14px; + box-shadow: var(--shadow-sm); + color: var(--text-primary); +} + +.hongbao-message, +.transfer-message { + padding: 14px; + + .hongbao-icon, + .transfer-icon { + width: 38px; + height: 38px; + border-radius: 12px; + display: grid; + place-items: center; + flex-shrink: 0; + } + + .hongbao-icon { + background: #ef4444; + } + + .transfer-icon { + background: #f59e0b; + } + + .hongbao-info, + .transfer-info { + color: var(--text-primary); + } + + .hongbao-greeting, + .transfer-amount { + color: var(--text-primary); + text-shadow: none; + } + + .hongbao-label, + .transfer-desc, + .transfer-memo, + .transfer-label { + color: var(--text-secondary); + opacity: 1; + text-shadow: none; + } +} + +.transfer-message.received .transfer-icon { + background: #64748b; +} + +.gift-message { + padding: 12px; + + .gift-info { + color: var(--text-primary); + } + + .gift-wish, + .gift-price { + color: var(--text-primary); + } + + .gift-label { + color: var(--text-secondary); + opacity: 1; + } +} + +[data-mode="dark"] { + .hongbao-message, + .transfer-message, + .gift-message { + background: #2b2b2b; + border-color: rgba(255, 255, 255, 0.08); + } + + .message-bubble.received .bubble-content, + .message-bubble.voice.sent .bubble-content { + background: #2f2f2f; + } +} + +.chat-input-area { + position: absolute; + left: max(18px, env(safe-area-inset-left)); + right: max(18px, env(safe-area-inset-right)); + bottom: calc(14px + env(safe-area-inset-bottom)); + z-index: 5; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + pointer-events: none; + + &::before { + content: ''; + position: absolute; + left: -18px; + right: -18px; + bottom: -14px; + height: 86px; + background: linear-gradient(to top, var(--bg-primary) 62%, transparent); + pointer-events: none; + z-index: -1; + } +} + +.chat-input-search-btn, +.chat-input-shell { + height: 44px; + border: 1px solid var(--border-color); + background: color-mix(in srgb, var(--bg-primary) 92%, transparent); + color: var(--text-secondary); + box-shadow: var(--shadow-sm); + backdrop-filter: blur(14px); +} + +.chat-input-search-btn { + width: 44px; + border-radius: 12px; + display: grid; + place-items: center; + cursor: pointer; + pointer-events: auto; + + &:hover:not(:disabled) { + background: var(--bg-hover); + color: var(--text-primary); + } + + &:disabled { + cursor: default; + opacity: 0.72; + } +} + +.chat-input-shell { + width: min(680px, 100%); + border-radius: 14px; + padding: 0 14px; + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; +} + +.scroll-to-bottom { + bottom: 74px; + background: color-mix(in srgb, var(--bg-primary) 92%, transparent); + color: var(--text-primary); + border: 1px solid var(--border-color); + box-shadow: var(--shadow-sm); + + &:hover { + background: var(--bg-hover); + color: var(--text-primary); + } +} + +@media (max-width: 720px) { + .message-header { + padding-inline: 14px; + + .header-actions { + gap: 0; + } + + .icon-btn { + width: 32px; + height: 32px; + } + } + + .message-list { + padding-inline: 14px; + } + + .message-bubble { + max-width: 88%; + } + + .chat-input-area { + left: 12px; + right: 12px; + } +} diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 6194a5f..e8b320f 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -29,6 +29,9 @@ import { onSingleExportDialogStatus, requestExportSessionStatus } from '../services/exportBridge' +import ChatHeader from './Chat/ChatHeader' +import ChatInputArea from './Chat/ChatInputArea' +import ChatMessageBubble from './Chat/ChatMessageBubble' import '../styles/batchTranscribe.scss' import './ChatPage.scss' @@ -6988,155 +6991,62 @@ function ChatPage(props: ChatPageProps) { ) : currentSession ? ( <> -
- -
-

{currentSession.displayName || currentSession.username}

- {isCurrentSessionGroup && ( -
群聊
- )} -
-
- {!standaloneSessionWindow && isCurrentSessionGroup && ( - - )} - {isCurrentSessionGroup && ( - - )} - {!standaloneSessionWindow && ( - - )} - {!standaloneSessionWindow && isCurrentSessionPrivateSnsSupported && ( - - )} - {!standaloneSessionWindow && ( - - )} - {!standaloneSessionWindow && ( - - )} -
- -
- {showJumpPopover && createPortal( -
- setShowJumpPopover(false)} - onSelect={handleJumpDateSelect} - messageDates={messageDates} - hasLoadedMessageDates={hasLoadedMessageDates} - messageDateCounts={messageDateCounts} - loadingDates={loadingDates} - loadingDateCounts={loadingDateCounts} - style={{ position: 'static', top: 'auto', right: 'auto' }} - /> -
, - document.body - )} - - - {!shouldHideStandaloneDetailButton && ( - - )} -
-
+ setShowJumpPopover(false)} + onSelect={handleJumpDateSelect} + messageDates={messageDates} + hasLoadedMessageDates={hasLoadedMessageDates} + messageDateCounts={messageDateCounts} + loadingDates={loadingDates} + loadingDateCounts={loadingDateCounts} + style={{ position: 'static', top: 'auto', right: 'auto' }} + /> +
, + document.body + )} {isPreparingExportDialog && exportPrepareHint && (
@@ -7292,6 +7202,7 @@ function ChatPage(props: ChatPageProps) { 回到底部
+ {/* 群成员面板 */} {showGroupMembersPanel && isCurrentSessionGroup && ( @@ -10696,115 +10607,57 @@ function MessageBubble({ return
{renderTextWithEmoji(cleanedParsedContent)}
} - return ( - <> - {showTime && ( -
- {formatTime(message.createTime)} + const systemAlertPortal = systemAlert ? createPortal( +
setSystemAlert(null)} style={{ zIndex: 99999 }}> +
e.stopPropagation()} style={{ maxWidth: '400px' }}> +
+
- )} -
{ - if (isSelectionMode) { - e.stopPropagation() - onToggleSelection?.(messageKey, e.shiftKey) - } - }} - > - {isSelectionMode && !isSent && ( -
- {isSelected && } -
- )} - -
onContextMenu?.(e, message)} - > -
- -
-
- {/* 群聊中显示发送者名称 */} - {isGroupChat && !isSent && ( -
- {resolvedSenderName || '群成员'} -
- )} - {renderContent()} -
+
+

{systemAlert.title}

+

+ {systemAlert.message} +

+
+
+
- - {isSelectionMode && isSent && ( -
- {isSelected && } -
- )} - {systemAlert && createPortal( -
setSystemAlert(null)} style={{ zIndex: 99999 }}> -
e.stopPropagation()} style={{ maxWidth: '400px' }}> -
- -
-
-

{systemAlert.title}

-

- {systemAlert.message} -

-
-
- -
-
-
, - document.body - )}
- +
, + document.body + ) : null + + return ( + + {renderContent()} + ) } From ff15dc6e9f81e66a9db6a415ee08b7d0bef28f9d Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 6 May 2026 22:36:11 +0800 Subject: [PATCH 12/23] fix: polish chat page refactor --- electron/main.ts | 25 +++++-- src/components/MessageBubble.tsx | 36 ---------- src/pages/Chat/ChatInputArea.tsx | 42 ------------ src/pages/ChatHistoryPage.scss | 6 +- src/pages/ChatPage.scss | 111 ++++++------------------------- src/pages/ChatPage.tsx | 2 - 6 files changed, 42 insertions(+), 180 deletions(-) delete mode 100644 src/components/MessageBubble.tsx delete mode 100644 src/pages/Chat/ChatInputArea.tsx diff --git a/electron/main.ts b/electron/main.ts index 76688c3..59815fe 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1309,9 +1309,6 @@ function createChatHistoryRouteWindow(route: string) { ? join(process.resourcesPath, 'icon.icns') : join(process.resourcesPath, 'icon.ico')) - // 根据系统主题设置窗口背景色 - const isDark = nativeTheme.shouldUseDarkColors - const win = new BrowserWindow({ width: 600, height: 800, @@ -1326,13 +1323,31 @@ function createChatHistoryRouteWindow(route: string) { titleBarStyle: 'hidden', titleBarOverlay: false, show: false, - backgroundColor: isDark ? '#1A1A1A' : '#F0F0F0', + backgroundColor: '#FFFFFF', autoHideMenuBar: true }) setupCustomTitleBarWindow(win) - win.once('ready-to-show', () => { + let hasShown = false + let isReadyToShow = false + let hasLoadedRoute = false + const showChatHistoryWindow = () => { + if (hasShown || !isReadyToShow || !hasLoadedRoute || win.isDestroyed()) return + hasShown = true win.show() + } + + win.webContents.once('did-finish-load', () => { + hasLoadedRoute = true + setTimeout(showChatHistoryWindow, 30) + }) + win.webContents.once('did-fail-load', () => { + hasLoadedRoute = true + showChatHistoryWindow() + }) + win.once('ready-to-show', () => { + isReadyToShow = true + showChatHistoryWindow() }) if (process.env.VITE_DEV_SERVER_URL) { diff --git a/src/components/MessageBubble.tsx b/src/components/MessageBubble.tsx deleted file mode 100644 index c807584..0000000 --- a/src/components/MessageBubble.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import React from 'react' -import { Bot, User } from 'lucide-react' - -interface ChatMessage { - id: string; - role: 'user' | 'ai'; - content: string; - timestamp: number; -} - -interface MessageBubbleProps { - message: ChatMessage; -} - -/** - * 优化后的消息气泡组件 - * 使用 React.memo 避免不必要的重新渲染 - */ -export const MessageBubble = React.memo(({ message }) => { - return ( -
-
- {message.role === 'ai' ? : } -
-
-
{message.content}
-
-
- ) -}, (prevProps, nextProps) => { - // 自定义比较函数:只有内容或ID变化时才重新渲染 - return prevProps.message.content === nextProps.message.content && - prevProps.message.id === nextProps.message.id -}) - -MessageBubble.displayName = 'MessageBubble' diff --git a/src/pages/Chat/ChatInputArea.tsx b/src/pages/Chat/ChatInputArea.tsx deleted file mode 100644 index c8303d5..0000000 --- a/src/pages/Chat/ChatInputArea.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react' -import { Lock, Search } from 'lucide-react' - -export interface ChatInputAreaProps { - disabled?: boolean - placeholder?: string - onFocusSearch?: () => void -} - -function ChatInputArea({ - disabled = true, - placeholder = '聊天记录', - onFocusSearch -}: ChatInputAreaProps) { - return ( -
- -
- - {placeholder} -
-
- ) -} - -function areEqual(prev: ChatInputAreaProps, next: ChatInputAreaProps) { - return ( - prev.disabled === next.disabled && - prev.placeholder === next.placeholder && - prev.onFocusSearch === next.onFocusSearch - ) -} - -export default React.memo(ChatInputArea, areEqual) diff --git a/src/pages/ChatHistoryPage.scss b/src/pages/ChatHistoryPage.scss index 7465fae..3bc96b5 100644 --- a/src/pages/ChatHistoryPage.scss +++ b/src/pages/ChatHistoryPage.scss @@ -2,8 +2,8 @@ display: flex; flex-direction: column; height: 100vh; - background: - linear-gradient(180deg, color-mix(in srgb, var(--bg-primary) 96%, white) 0%, var(--bg-primary) 100%); + color: var(--text-primary); + background: var(--bg-primary); .history-list { flex: 1; @@ -149,7 +149,7 @@ .nested-chat-record-card { min-width: 220px; max-width: 320px; - background: color-mix(in srgb, var(--bg-secondary) 97%, #f5f7fb); + background: var(--card-bg); border: 1px solid var(--border-color); border-radius: 14px; overflow: hidden; diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 39ad989..9b79cd2 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -2730,13 +2730,9 @@ width: clamp(280px, 25vw, 360px); min-width: 280px; max-width: 360px; - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--card-bg) 94%, #fff 6%) 0%, - var(--card-bg) 100% - ); + background: var(--bg-primary); border-left: 1px solid color-mix(in srgb, var(--border-color) 82%, transparent); - box-shadow: -14px 0 28px rgba(0, 0, 0, 0.07); + box-shadow: var(--shadow-sm); display: flex; flex-direction: column; overflow: hidden; @@ -2749,7 +2745,7 @@ justify-content: space-between; gap: 8px; padding: 14px 14px 12px; - background: color-mix(in srgb, var(--card-bg) 92%, #fff 8%); + background: var(--bg-primary); border-bottom: 1px solid var(--border-color); position: sticky; top: 0; @@ -2850,7 +2846,7 @@ padding: 12px; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); - background: color-mix(in srgb, var(--bg-secondary) 84%, transparent); + background: var(--bg-secondary); animation: detailCardEnter 0.24s ease both; .detail-overview-avatar { @@ -2891,7 +2887,7 @@ padding: 12px; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); - background: color-mix(in srgb, var(--bg-secondary) 86%, transparent); + background: var(--bg-secondary); animation: detailCardEnter 0.24s ease both; .section-title { @@ -2917,7 +2913,7 @@ border-radius: 8px; font-size: 12px; color: var(--text-tertiary); - background: color-mix(in srgb, var(--card-bg) 84%, transparent); + background: var(--card-bg); } } @@ -5384,8 +5380,8 @@ .message-list { background: var(--bg-primary); - padding: 18px clamp(16px, 3vw, 48px) 104px; - padding-bottom: calc(104px + env(safe-area-inset-bottom)); + padding: 18px clamp(16px, 3vw, 48px) 28px; + padding-bottom: calc(28px + env(safe-area-inset-bottom)); } } } @@ -5447,8 +5443,8 @@ .message-list { background: var(--bg-primary); - padding: 18px clamp(16px, 3vw, 48px) 104px; - padding-bottom: calc(104px + env(safe-area-inset-bottom)); + padding: 18px clamp(16px, 3vw, 48px) 28px; + padding-bottom: calc(28px + env(safe-area-inset-bottom)); gap: 0; } @@ -5536,8 +5532,8 @@ &.sent { .bubble-content { - background: #3b82f6; - color: #fff; + background: var(--primary); + color: var(--on-primary); border-radius: 18px; } @@ -5624,12 +5620,12 @@ } .message-bubble.sent .quoted-message { - background: color-mix(in srgb, #fff 12%, transparent); - border-left-color: color-mix(in srgb, #fff 62%, #3b82f6); + background: color-mix(in srgb, var(--on-primary) 12%, transparent); + border-left-color: color-mix(in srgb, var(--on-primary) 62%, var(--primary)); .quoted-sender, .quoted-text { - color: color-mix(in srgb, #fff 82%, #3b82f6); + color: color-mix(in srgb, var(--on-primary) 82%, var(--primary)); } } @@ -5765,82 +5761,18 @@ .hongbao-message, .transfer-message, .gift-message { - background: #2b2b2b; - border-color: rgba(255, 255, 255, 0.08); + background: var(--card-bg); + border-color: var(--border-color); } .message-bubble.received .bubble-content, .message-bubble.voice.sent .bubble-content { - background: #2f2f2f; + background: var(--bg-secondary); } } -.chat-input-area { - position: absolute; - left: max(18px, env(safe-area-inset-left)); - right: max(18px, env(safe-area-inset-right)); - bottom: calc(14px + env(safe-area-inset-bottom)); - z-index: 5; - display: flex; - align-items: center; - justify-content: center; - gap: 8px; - pointer-events: none; - - &::before { - content: ''; - position: absolute; - left: -18px; - right: -18px; - bottom: -14px; - height: 86px; - background: linear-gradient(to top, var(--bg-primary) 62%, transparent); - pointer-events: none; - z-index: -1; - } -} - -.chat-input-search-btn, -.chat-input-shell { - height: 44px; - border: 1px solid var(--border-color); - background: color-mix(in srgb, var(--bg-primary) 92%, transparent); - color: var(--text-secondary); - box-shadow: var(--shadow-sm); - backdrop-filter: blur(14px); -} - -.chat-input-search-btn { - width: 44px; - border-radius: 12px; - display: grid; - place-items: center; - cursor: pointer; - pointer-events: auto; - - &:hover:not(:disabled) { - background: var(--bg-hover); - color: var(--text-primary); - } - - &:disabled { - cursor: default; - opacity: 0.72; - } -} - -.chat-input-shell { - width: min(680px, 100%); - border-radius: 14px; - padding: 0 14px; - display: flex; - align-items: center; - gap: 8px; - font-size: 13px; -} - .scroll-to-bottom { - bottom: 74px; + bottom: 20px; background: color-mix(in srgb, var(--bg-primary) 92%, transparent); color: var(--text-primary); border: 1px solid var(--border-color); @@ -5873,9 +5805,4 @@ .message-bubble { max-width: 88%; } - - .chat-input-area { - left: 12px; - right: 12px; - } } diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index e8b320f..714c2a2 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -30,7 +30,6 @@ import { requestExportSessionStatus } from '../services/exportBridge' import ChatHeader from './Chat/ChatHeader' -import ChatInputArea from './Chat/ChatInputArea' import ChatMessageBubble from './Chat/ChatMessageBubble' import '../styles/batchTranscribe.scss' import './ChatPage.scss' @@ -7202,7 +7201,6 @@ function ChatPage(props: ChatPageProps) { 回到底部
- {/* 群成员面板 */} {showGroupMembersPanel && isCurrentSessionGroup && ( From 45a42475638d969aa768bc383b44e8ee07b50c69 Mon Sep 17 00:00:00 2001 From: Jason Date: Wed, 6 May 2026 23:20:14 +0800 Subject: [PATCH 13/23] ci: harden release workflow --- .github/workflows/release.yml | 92 +++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 30 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 917a8b4..6627afa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,7 +28,23 @@ jobs: node-version: 24 cache: "npm" - name: Install Dependencies - run: npm install + run: npm install --ignore-scripts + + - name: Ensure mac key helpers are executable + shell: bash + run: | + set -euo pipefail + for file in \ + resources/key/macos/universal/xkey_helper \ + resources/key/macos/universal/image_scan_helper \ + resources/key/macos/universal/xkey_helper_macos \ + resources/key/macos/universal/libwx_key.dylib + do + if [ -f "$file" ]; then + chmod +x "$file" + ls -l "$file" + fi + done - name: Sync version with tag shell: bash @@ -52,9 +68,9 @@ jobs: set -euo pipefail export ELECTRON_BUILDER_BINARIES_MIRROR="https://github.com/electron-userland/electron-builder-binaries/releases/download/" echo "Using ELECTRON_BUILDER_BINARIES_MIRROR=$ELECTRON_BUILDER_BINARIES_MIRROR" - if ! npx electron-builder --mac dmg zip --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'; then + if ! npx electron-builder --mac dmg zip --arm64 --publish always '--config.npmRebuild=false' '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}'; then echo "::warning::DMG packaging failed (hdiutil instability on runner). Retrying with ZIP only." - npx electron-builder --mac zip --arm64 --publish always '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' + npx electron-builder --mac zip --arm64 --publish always '--config.npmRebuild=false' '--config.publish.owner=${{ github.repository_owner }}' '--config.publish.repo=${{ github.event.repository.name }}' fi - name: Inject minimumVersion into latest yml @@ -327,33 +343,49 @@ jobs: retry_cmd 5 3 gh release edit "$TAG" --repo "$REPO" --notes-file release_notes.md deploy-aur: - runs-on: ubuntu-latest - needs: [release-linux] - if: startsWith(github.ref, 'refs/tags/v') - steps: - - name: Checkout code - uses: actions/checkout@v5 - with: - fetch-depth: 0 + runs-on: ubuntu-latest + needs: [release-linux] + if: startsWith(github.ref, 'refs/tags/v') + steps: + - name: Check AUR credentials + id: aur-credentials + shell: bash + env: + AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + run: | + if [ -z "${AUR_SSH_PRIVATE_KEY}" ]; then + echo "::notice::AUR_SSH_PRIVATE_KEY is not configured; skipping AUR publish." + echo "enabled=false" >> "$GITHUB_OUTPUT" + else + echo "enabled=true" >> "$GITHUB_OUTPUT" + fi - - name: Update PKGBUILD version - run: | - NEW_VER=$(echo "${{ github.ref_name }}" | sed 's/^v//') - sed -i "s/^pkgver=.*/pkgver=${NEW_VER}/" resources/installer/linux/PKGBUILD - sed -i "s/^pkgrel=.*/pkgrel=1/" resources/installer/linux/PKGBUILD + - name: Checkout code + if: steps.aur-credentials.outputs.enabled == 'true' + uses: actions/checkout@v5 + with: + fetch-depth: 0 - - name: Publish AUR package - uses: KSXGitHub/github-actions-deploy-aur@master - with: - pkgname: weflow - pkgbuild: resources/installer/linux/PKGBUILD - updpkgsums: true - assets: | - resources/installer/linux/weflow.desktop - resources/installer/linux/icon.png - resources/installer/linux/.gitignore + - name: Update PKGBUILD version + if: steps.aur-credentials.outputs.enabled == 'true' + run: | + NEW_VER=$(echo "${{ github.ref_name }}" | sed 's/^v//') + sed -i "s/^pkgver=.*/pkgver=${NEW_VER}/" resources/installer/linux/PKGBUILD + sed -i "s/^pkgrel=.*/pkgrel=1/" resources/installer/linux/PKGBUILD - ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} - commit_username: H3CoF6 - commit_email: h3cof6@gmail.com - ssh_keyscan_types: ed25519 + - name: Publish AUR package + if: steps.aur-credentials.outputs.enabled == 'true' + uses: KSXGitHub/github-actions-deploy-aur@master + with: + pkgname: weflow + pkgbuild: resources/installer/linux/PKGBUILD + updpkgsums: true + assets: | + resources/installer/linux/weflow.desktop + resources/installer/linux/icon.png + resources/installer/linux/.gitignore + + ssh_private_key: ${{ secrets.AUR_SSH_PRIVATE_KEY }} + commit_username: H3CoF6 + commit_email: h3cof6@gmail.com + ssh_keyscan_types: ed25519 From 0bd5610cf069cdb1747187f21e9c7a3366930d79 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 7 May 2026 19:46:37 +0800 Subject: [PATCH 14/23] refactor: modernize splash screen --- electron/main.ts | 4 +- public/splash.html | 506 +++++++++++++++++++++++++++++---------------- 2 files changed, 330 insertions(+), 180 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 59815fe..1a6809a 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1008,8 +1008,8 @@ function createSplashWindow(): BrowserWindow { : join(process.resourcesPath, 'icon.ico')) splashWindow = new BrowserWindow({ - width: 760, - height: 460, + width: 856, + height: 540, resizable: false, frame: false, transparent: true, diff --git a/public/splash.html b/public/splash.html index d71c241..7105ba7 100644 --- a/public/splash.html +++ b/public/splash.html @@ -5,245 +5,395 @@ WeFlow -
-
- -
-
WeFlow
-
微信聊天记录管理工具
+
+
+
+ +

WeFlow

+

微信聊天记录管理工具

+
+ +
+
正在启动...
+
-
- -
-
-
-
-
-
正在启动...
-
-
+ -
+
From ae5d1d95ab9c5078cc3d9758aaface0102cb654a Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 7 May 2026 23:00:24 +0800 Subject: [PATCH 15/23] refactor: polish UI for Export and Contacts pages --- .../Export/ExportDefaultsSettingsForm.scss | 122 ++++++++ src/pages/ContactsPage.scss | 189 ++++++++++++ src/pages/ContactsPage.tsx | 129 +++++---- src/pages/ExportPage.scss | 23 ++ src/pages/GroupAnalyticsPage.scss | 273 ++++++++++++++++++ 5 files changed, 685 insertions(+), 51 deletions(-) diff --git a/src/components/Export/ExportDefaultsSettingsForm.scss b/src/components/Export/ExportDefaultsSettingsForm.scss index c24b44f..8381c87 100644 --- a/src/components/Export/ExportDefaultsSettingsForm.scss +++ b/src/components/Export/ExportDefaultsSettingsForm.scss @@ -457,3 +457,125 @@ } } } + +// UI rebuild polish for the modal variant used by ExportPage. +.export-defaults-settings-form.layout-split { + display: grid; + gap: 12px; + + .form-group { + grid-template-columns: minmax(180px, 0.85fr) minmax(0, 1.15fr); + gap: 14px; + align-items: start; + padding: 14px; + border: 1px solid color-mix(in srgb, var(--border-color) 68%, transparent); + border-radius: 14px; + background: var(--bg-secondary); + } + + .form-group:first-child, + .form-group:last-child { + padding: 14px; + } + + .form-copy { + padding-top: 2px; + } + + label { + margin-bottom: 3px; + line-height: 1.35; + } + + .form-hint { + line-height: 1.45; + } + + .form-control { + width: 100%; + min-width: 0; + justify-content: stretch; + } + + .select-field, + .settings-time-range-field, + .log-toggle-line, + .media-default-grid, + .concurrency-inline-options { + max-width: none; + width: 100%; + } + + .select-trigger, + .settings-time-range-trigger { + border-radius: 12px; + background: var(--bg-primary); + } + + .log-toggle-line { + border-radius: 12px; + background: var(--bg-primary); + } + + .concurrency-inline-options { + grid-template-columns: repeat(6, minmax(42px, 1fr)); + gap: 6px; + } + + .concurrency-option { + min-width: 0; + min-height: 36px; + } + + .format-setting-group { + grid-template-columns: 1fr; + gap: 12px; + } + + .format-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 8px; + width: 100%; + } + + .format-card { + min-height: 74px; + padding: 10px 12px; + border-radius: 12px; + background: var(--bg-primary); + } + + .format-label, + .format-desc { + max-width: 100%; + overflow-wrap: anywhere; + } + + .media-default-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(78px, 1fr)); + gap: 8px; + + label { + min-height: 36px; + padding: 8px 10px; + border: 1px solid var(--border-color); + border-radius: 12px; + background: var(--bg-primary); + } + } +} + +@media (max-width: 980px) { + .export-defaults-settings-form.layout-split { + .form-group { + grid-template-columns: 1fr; + gap: 10px; + } + + .format-grid { + grid-template-columns: repeat(auto-fit, minmax(156px, 1fr)); + } + } +} diff --git a/src/pages/ContactsPage.scss b/src/pages/ContactsPage.scss index d4a300e..e5bf4a6 100644 --- a/src/pages/ContactsPage.scss +++ b/src/pages/ContactsPage.scss @@ -834,3 +834,192 @@ transform: rotate(360deg); } } + +// UI rebuild polish for the contact detail surface. +.contacts-page { + background: var(--bg-primary); + + .contact-detail-panel { + background: var(--bg-primary); + } + + .contact-detail-scroll { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 24px; + display: flex; + flex-direction: column; + gap: 16px; + + &::-webkit-scrollbar { + width: 6px; + } + + &::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--text-tertiary) 55%, transparent); + border-radius: 3px; + } + } + + .contact-detail-hero { + display: flex; + align-items: center; + gap: 18px; + padding: 22px; + border-radius: 16px; + background: var(--bg-secondary); + border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); + } + + .contact-detail-hero .detail-avatar { + width: 84px; + height: 84px; + border-radius: 16px; + background: var(--bg-tertiary); + overflow: hidden; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + + img { + width: 100%; + height: 100%; + object-fit: cover; + } + + span { + color: var(--text-secondary); + font-size: 28px; + font-weight: 650; + } + } + + .contact-detail-heading { + min-width: 0; + display: flex; + flex-direction: column; + gap: 8px; + + h2 { + margin: 0; + color: var(--text-primary); + font-size: 24px; + line-height: 1.2; + font-weight: 650; + overflow-wrap: anywhere; + } + + p { + margin: 0; + color: var(--text-secondary); + font-size: 13px; + line-height: 1.5; + overflow-wrap: anywhere; + } + } + + .detail-type { + width: fit-content; + border-radius: 999px; + padding: 5px 10px; + } + + .contact-action-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 10px; + } + + .contact-action-row .goto-chat-btn, + .contact-action-row .detail-entry-btn { + min-height: 44px; + width: 100%; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 12px; + padding: 10px 14px; + font-size: 14px; + font-weight: 600; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; + } + + .contact-action-row .goto-chat-btn { + border: 1px solid var(--primary); + background: var(--primary); + color: var(--on-primary, #fff); + + &:hover { + background: var(--primary-hover); + border-color: var(--primary-hover); + } + } + + .contact-action-row .detail-entry-btn { + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-primary); + + &:hover { + color: var(--primary); + border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 7%, var(--bg-secondary)); + } + } + + .contact-detail-section { + padding: 18px; + border-radius: 16px; + background: var(--bg-secondary); + border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); + } + + .section-title { + margin-bottom: 12px; + color: var(--text-secondary); + font-size: 12px; + font-weight: 650; + } + + .contact-detail-section .detail-info-list { + margin: 0; + display: grid; + gap: 8px; + } + + .contact-detail-section .detail-row { + display: grid; + grid-template-columns: 76px minmax(0, 1fr); + align-items: start; + gap: 12px; + padding: 12px 14px; + border: none; + border-radius: 12px; + background: var(--bg-primary); + font-size: 13px; + } + + .contact-detail-section .detail-label { + min-width: 0; + color: var(--text-tertiary); + } + + .contact-detail-section .detail-value { + color: var(--text-primary); + line-height: 1.55; + word-break: break-word; + overflow-wrap: anywhere; + user-select: text; + } + + @media (max-width: 860px) { + .contact-detail-hero { + align-items: flex-start; + flex-direction: column; + } + } +} diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx index ff90aff..0af23b4 100644 --- a/src/pages/ContactsPage.tsx +++ b/src/pages/ContactsPage.tsx @@ -635,6 +635,39 @@ function ContactsPage() { return '朋友圈:统计中...' }, [selectedContactSupportsSns, selectedContactSnsCount, snsUserPostCountsStatus]) + const selectedContactTitle = useMemo(() => { + if (!selectedContact) return '' + return selectedContact.displayName || selectedContact.remark || selectedContact.nickname || selectedContact.username + }, [selectedContact]) + + const selectedContactSubtitle = useMemo(() => { + if (!selectedContact) return '' + const parts = [ + selectedContact.remark && selectedContact.remark !== selectedContactTitle ? `备注 ${selectedContact.remark}` : '', + selectedContact.alias ? `微信号 ${selectedContact.alias}` : '', + selectedContact.region || '' + ].filter(Boolean) + return parts.join(' · ') + }, [selectedContact, selectedContactTitle]) + + const selectedContactDetailRows = useMemo(() => { + if (!selectedContact) return [] + return [ + { key: 'username', label: '用户名', value: selectedContact.username }, + { key: 'nickname', label: '昵称', value: selectedContact.nickname || selectedContact.displayName }, + selectedContact.remark ? { key: 'remark', label: '备注', value: selectedContact.remark } : null, + selectedContact.alias ? { key: 'alias', label: '微信号', value: selectedContact.alias } : null, + selectedContact.labels && selectedContact.labels.length > 0 + ? { key: 'labels', label: '标签', value: selectedContact.labels.join('、') } + : null, + selectedContact.detailDescription + ? { key: 'signature', label: '个性签名', value: selectedContact.detailDescription } + : null, + selectedContact.region ? { key: 'region', label: '地区', value: selectedContact.region } : null, + { key: 'type', label: '类型', value: getContactTypeName(selectedContact.type) } + ].filter((row): row is { key: string; label: string; value: string } => Boolean(row && row.value)) + }, [selectedContact]) + const openSelectedContactSnsTimeline = useCallback(() => { if (!selectedContact || !selectedContactSupportsSns) return if (snsUserPostCountsStatus === 'idle') { @@ -1090,12 +1123,9 @@ function ContactsPage() {
) : selectedContact ? ( -
-
-

联系人详情

-
-
-
+
+
+
{selectedContact.avatarUrl ? ( @@ -1103,53 +1133,50 @@ function ContactsPage() { {getAvatarLetter(selectedContact.displayName)} )}
-
{selectedContact.displayName}
-
- {getContactTypeIcon(selectedContact.type)} - {getContactTypeName(selectedContact.type)} -
-
- -
-
用户名{selectedContact.username}
-
昵称{selectedContact.nickname || selectedContact.displayName}
- {selectedContact.remark &&
备注{selectedContact.remark}
} - {selectedContact.alias &&
微信号{selectedContact.alias}
} - {selectedContact.labels && selectedContact.labels.length > 0 && ( -
标签{selectedContact.labels.join('、')}
- )} - {selectedContact.detailDescription && ( -
个性签名{selectedContact.detailDescription}
- )} - {selectedContact.region && ( -
地区{selectedContact.region}
- )} -
类型{getContactTypeName(selectedContact.type)}
- {selectedContactSupportsSns && ( -
- 朋友圈 - +
+
+ {getContactTypeIcon(selectedContact.type)} + {getContactTypeName(selectedContact.type)}
- )} -
+

{selectedContactTitle}

+ {selectedContactSubtitle &&

{selectedContactSubtitle}

} +
+ - +
+ + {selectedContactSupportsSns && ( + + )} +
+ +
+
基础资料
+
+ {selectedContactDetailRows.map(row => ( +
+ {row.label} + {row.value} +
+ ))} +
+
) : ( diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 6616b9a..50a5953 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -1157,6 +1157,29 @@ padding: 0 16px 16px; } +.export-defaults-modal { + width: min(760px, calc(100vw - 40px)); + max-height: min(82vh, 820px); + border-radius: 16px; + background: var(--bg-primary); + border-color: color-mix(in srgb, var(--border-color) 80%, transparent); + box-shadow: var(--shadow-md); +} + +.export-defaults-modal-header { + padding: 16px 18px 12px; +} + +.export-defaults-modal-body { + padding: 14px 18px 16px; + overflow-x: hidden; +} + +.export-defaults-modal-actions { + padding: 0 18px 16px; + background: var(--bg-primary); +} + .task-center-card-label { line-height: 1; white-space: nowrap; diff --git a/src/pages/GroupAnalyticsPage.scss b/src/pages/GroupAnalyticsPage.scss index e55c30f..31afc9e 100644 --- a/src/pages/GroupAnalyticsPage.scss +++ b/src/pages/GroupAnalyticsPage.scss @@ -1955,3 +1955,276 @@ opacity: 0.92; } } + +// UI rebuild polish: align group analysis with the shared ChatGPT-style tokens. +.group-analytics-shell { + gap: 14px; + background: transparent; +} + +.group-analytics-page { + gap: 14px; +} + +.group-sidebar, +.detail-area { + border-radius: 16px; + background: var(--bg-secondary); + border: 1px solid color-mix(in srgb, var(--border-color) 72%, transparent); + box-shadow: var(--shadow-sm); +} + +.group-sidebar { + .sidebar-header { + min-height: 58px; + padding: 14px; + } + + .search-box { + border-radius: 12px; + border: 1px solid transparent; + background: var(--bg-primary); + + &:focus-within { + border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color)); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary) 12%, transparent); + } + } + + .refresh-btn { + border-radius: 10px; + background: var(--bg-primary); + } +} + +.group-item { + margin: 0 8px 4px; + border-bottom: none; + border-radius: 12px; + + &.active { + background: color-mix(in srgb, var(--primary) 12%, var(--bg-secondary)); + color: var(--text-primary); + } + + .group-avatar { + border-radius: 10px; + } +} + +.detail-drag-region { + height: 14px; +} + +.function-menu { + padding: 18px 22px 24px; + + .selected-group-info { + border-radius: 16px; + border-color: color-mix(in srgb, var(--border-color) 70%, transparent); + background: var(--bg-primary); + box-shadow: none; + } + + .function-grid { + grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); + gap: 14px; + } + + .function-card { + min-height: 136px; + border-radius: 16px; + border-color: color-mix(in srgb, var(--border-color) 76%, transparent); + background: var(--bg-primary); + box-shadow: none; + transform: none; + + &:hover { + transform: none; + border-color: color-mix(in srgb, var(--primary) 35%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 5%, var(--bg-primary)); + box-shadow: var(--shadow-sm); + } + } +} + +.function-content { + .content-header { + padding: 16px 22px 12px; + background: var(--bg-secondary); + border-bottom-color: color-mix(in srgb, var(--border-color) 70%, transparent); + + .back-btn, + .refresh-btn, + .export-btn { + border-radius: 10px; + background: var(--bg-primary); + } + } + + .content-body { + padding: 18px 22px 22px; + } +} + +.members-grid { + grid-template-columns: repeat(auto-fill, minmax(108px, 1fr)); + gap: 10px; + + .member-card { + border-radius: 12px; + background: var(--bg-primary); + border: 1px solid transparent; + + &:hover { + background: color-mix(in srgb, var(--primary) 5%, var(--bg-primary)); + border-color: color-mix(in srgb, var(--primary) 28%, var(--border-color)); + } + } +} + +.member-export-panel, +.member-messages-panel, +.member-analytics-panel { + .select-trigger { + border-radius: 12px; + background: var(--bg-primary); + } + + .member-export-options, + .member-message-item, + .member-message-summary-card { + border-radius: 12px; + background: var(--bg-primary); + box-shadow: none; + } +} + +.rankings-list { + .ranking-item { + border-radius: 12px; + background: var(--bg-primary); + border: 1px solid transparent; + + &:hover { + border-color: var(--border-color); + background: color-mix(in srgb, var(--primary) 4%, var(--bg-primary)); + } + } +} + +.media-stats { + .media-layout .media-legend, + .stat-card, + .charts-grid .chart-card { + border-radius: 12px; + border: 1px solid color-mix(in srgb, var(--border-color) 76%, transparent); + background: var(--bg-primary); + box-shadow: none; + } +} + +.member-modal-overlay { + background: rgba(0, 0, 0, 0.42); + backdrop-filter: blur(8px); +} + +.member-modal, +.member-export-modal, +.member-result-modal { + border-radius: 16px; + border: 1px solid color-mix(in srgb, var(--border-color) 80%, transparent); + background: color-mix(in srgb, var(--bg-primary) 96%, transparent); + box-shadow: var(--shadow-md); +} + +.member-modal { + width: min(380px, calc(100vw - 32px)); + padding: 28px 32px 32px; + + .member-display-name { + margin-bottom: 20px; + } + + .detail-row { + border-radius: 12px; + background: var(--bg-secondary); + } + + .member-modal-actions { + margin-top: 18px; + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 8px; + } + + .member-modal-primary-btn, + .member-modal-secondary-btn { + width: 100%; + min-width: 0; + min-height: 44px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + padding: 10px 12px; + border-radius: 12px; + font-size: 13px; + font-weight: 600; + line-height: 1.25; + cursor: pointer; + transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease; + + span { + min-width: 0; + white-space: normal; + text-align: center; + } + + svg { + flex-shrink: 0; + } + } + + .member-modal-primary-btn { + border: 1px solid var(--primary); + background: var(--primary); + color: var(--on-primary, #fff); + + &:hover { + background: var(--primary-hover); + border-color: var(--primary-hover); + opacity: 1; + } + } + + .member-modal-secondary-btn { + border: 1px solid var(--border-color); + background: var(--bg-secondary); + color: var(--text-primary); + + &:hover { + border-color: color-mix(in srgb, var(--primary) 40%, var(--border-color)); + background: color-mix(in srgb, var(--primary) 8%, var(--bg-secondary)); + color: var(--primary); + } + } +} + +[data-mode="dark"] { + .member-modal, + .member-export-modal, + .member-result-modal { + background: color-mix(in srgb, var(--bg-secondary) 94%, transparent); + border-color: color-mix(in srgb, var(--border-color) 85%, transparent); + } + + .members-grid .member-card, + .rankings-list .ranking-item, + .member-export-panel .member-message-item, + .media-stats .media-layout .media-legend, + .media-stats .stat-card, + .media-stats .charts-grid .chart-card { + background: var(--bg-secondary); + } +} From e41a1197cb01dab90bdb9032167faedfaa422ffe Mon Sep 17 00:00:00 2001 From: Jason Date: Fri, 8 May 2026 17:33:57 +0800 Subject: [PATCH 16/23] fix: polish splash and export settings --- public/splash.html | 18 +++++------ .../Export/ExportDefaultsSettingsForm.scss | 31 +++++++++++-------- src/pages/ExportPage.scss | 9 +++--- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/public/splash.html b/public/splash.html index 7105ba7..56141e5 100644 --- a/public/splash.html +++ b/public/splash.html @@ -20,7 +20,7 @@ --logo-surface: #f4f4f5; --card-gloss-start: rgba(255, 255, 255, 0.62); --card-gloss-end: rgba(255, 255, 255, 0); - --shadow: 0 30px 80px rgba(0, 0, 0, 0.18), 0 2px 10px rgba(0, 0, 0, 0.06); + --shadow: none; } * { @@ -48,14 +48,14 @@ } .splash-shell { - width: calc(100% - 76px); - height: calc(100% - 72px); - min-width: 640px; - min-height: 360px; + width: 100%; + height: 100%; + min-width: 0; + min-height: 0; position: relative; overflow: hidden; - border-radius: 8px; - border: 1px solid var(--card-border); + border-radius: 0; + border: none; background: linear-gradient(180deg, var(--card-gloss-start) 0%, var(--card-gloss-end) 48%), var(--card-bg); @@ -350,7 +350,7 @@ setVar("--logo-surface", "#2b2b2b"); setVar("--card-gloss-start", "rgba(255, 255, 255, 0.035)"); setVar("--card-gloss-end", "rgba(255, 255, 255, 0)"); - setVar("--shadow", "0 30px 80px rgba(0, 0, 0, 0.42), 0 2px 10px rgba(0, 0, 0, 0.24)"); + setVar("--shadow", "none"); } else { setVar("--card-bg", "rgba(255, 255, 255, 0.96)"); setVar("--card-bg-solid", "#ffffff"); @@ -362,7 +362,7 @@ setVar("--logo-surface", "#f4f4f5"); setVar("--card-gloss-start", "rgba(255, 255, 255, 0.62)"); setVar("--card-gloss-end", "rgba(255, 255, 255, 0)"); - setVar("--shadow", "0 30px 80px rgba(0, 0, 0, 0.18), 0 2px 10px rgba(0, 0, 0, 0.06)"); + setVar("--shadow", "none"); } } diff --git a/src/components/Export/ExportDefaultsSettingsForm.scss b/src/components/Export/ExportDefaultsSettingsForm.scss index 8381c87..e237673 100644 --- a/src/components/Export/ExportDefaultsSettingsForm.scss +++ b/src/components/Export/ExportDefaultsSettingsForm.scss @@ -461,21 +461,21 @@ // UI rebuild polish for the modal variant used by ExportPage. .export-defaults-settings-form.layout-split { display: grid; - gap: 12px; + gap: 10px; .form-group { - grid-template-columns: minmax(180px, 0.85fr) minmax(0, 1.15fr); - gap: 14px; + grid-template-columns: minmax(176px, 0.82fr) minmax(0, 1.18fr); + gap: 12px; align-items: start; - padding: 14px; - border: 1px solid color-mix(in srgb, var(--border-color) 68%, transparent); - border-radius: 14px; - background: var(--bg-secondary); + padding: 12px; + border: none; + border-radius: 12px; + background: color-mix(in srgb, var(--bg-secondary) 82%, var(--bg-primary)); } .form-group:first-child, .form-group:last-child { - padding: 14px; + padding: 12px; } .form-copy { @@ -510,26 +510,31 @@ .settings-time-range-trigger { border-radius: 12px; background: var(--bg-primary); + min-height: 42px; + padding: 9px 12px; } .log-toggle-line { border-radius: 12px; background: var(--bg-primary); + min-height: 42px; + padding: 8px 12px; } .concurrency-inline-options { - grid-template-columns: repeat(6, minmax(42px, 1fr)); + grid-template-columns: repeat(6, minmax(38px, 1fr)); gap: 6px; } .concurrency-option { min-width: 0; min-height: 36px; + border-radius: 8px; } .format-setting-group { grid-template-columns: 1fr; - gap: 12px; + gap: 10px; } .format-grid { @@ -540,9 +545,9 @@ } .format-card { - min-height: 74px; + min-height: 68px; padding: 10px 12px; - border-radius: 12px; + border-radius: 8px; background: var(--bg-primary); } @@ -561,7 +566,7 @@ min-height: 36px; padding: 8px 10px; border: 1px solid var(--border-color); - border-radius: 12px; + border-radius: 8px; background: var(--bg-primary); } } diff --git a/src/pages/ExportPage.scss b/src/pages/ExportPage.scss index 50a5953..29e34e7 100644 --- a/src/pages/ExportPage.scss +++ b/src/pages/ExportPage.scss @@ -1158,20 +1158,21 @@ } .export-defaults-modal { - width: min(760px, calc(100vw - 40px)); + width: min(740px, calc(100vw - 40px)); max-height: min(82vh, 820px); border-radius: 16px; background: var(--bg-primary); - border-color: color-mix(in srgb, var(--border-color) 80%, transparent); + border-color: color-mix(in srgb, var(--border-color) 68%, transparent); box-shadow: var(--shadow-md); } .export-defaults-modal-header { - padding: 16px 18px 12px; + padding: 16px 18px 10px; + border-bottom-color: color-mix(in srgb, var(--border-color) 64%, transparent); } .export-defaults-modal-body { - padding: 14px 18px 16px; + padding: 12px 18px 14px; overflow-x: hidden; } From 762a2ec8324239f67c77cabd72d72aa0a8e3db5f Mon Sep 17 00:00:00 2001 From: Jason Date: Sat, 9 May 2026 23:48:10 +0800 Subject: [PATCH 17/23] fix: Reply UI --- src/pages/ChatPage.scss | 247 ++++++++++++++++++++++++++++++---------- src/pages/ChatPage.tsx | 60 +++++----- 2 files changed, 215 insertions(+), 92 deletions(-) diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 9b79cd2..b7c5039 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -1,4 +1,4 @@ -.chat-page { +.chat-page { position: relative; display: flex; height: 100%; @@ -2448,13 +2448,197 @@ white-space: nowrap; } -// 引用消息样式 +// ═══════════════════════════════════════════ +// Ambient Reply System — "Spectral Thread" +// ═══════════════════════════════════════════ + +// Wrapper for the entire ambient reply UI +.ambient-reply-wrapper { + position: relative; + cursor: pointer; + margin-bottom: 4px; + + // Reply anchor — always visible, minimal + .reply-anchor { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 6px 2px 4px; + border-radius: 8px; + background: transparent; + opacity: 0.42; + transition: opacity 0.35s ease, background-color 0.35s ease; + user-select: none; + max-width: 100%; + overflow: hidden; + } + + &:hover .reply-anchor { + opacity: 1; + background: color-mix(in srgb, var(--primary) 8%, transparent); + } + + .reply-anchor-icon { + width: 13px; + height: 13px; + color: var(--primary); + opacity: 0.7; + flex-shrink: 0; + transition: opacity 0.3s ease; + } + + &:hover .reply-anchor-icon { + opacity: 1; + } + + .reply-anchor-name { + font-size: 12px; + font-weight: 500; + color: var(--primary); + letter-spacing: 0.01em; + flex-shrink: 0; + } + + .reply-anchor-sep { + font-size: 10px; + color: var(--text-tertiary); + margin: 0 1px; + flex-shrink: 0; + } + + .reply-anchor-excerpt { + font-size: 12px; + color: var(--text-secondary); + max-width: 200px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.7; + + // Hide inline emoji images in anchor excerpt + .inline-emoji { + width: 14px !important; + height: 14px !important; + vertical-align: -2px; + } + } + + // Ghost preview — appears on hover, frosted glass + .reply-ghost { + position: absolute; + bottom: calc(100% + 6px); + max-width: 320px; + min-width: 160px; + padding: 8px 12px 10px 12px; + border-radius: 12px; + background: color-mix(in srgb, var(--bg-primary) 75%, transparent); + backdrop-filter: blur(20px) saturate(1.3); + -webkit-backdrop-filter: blur(20px) saturate(1.3); + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04); + opacity: 0; + transform: translateY(4px) scale(0.98); + pointer-events: none; + transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1), + transform 0.35s cubic-bezier(0.16, 1, 0.3, 1); + z-index: 50; + + // Gradient accent bar on left edge + &::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + bottom: 8px; + width: 2.5px; + border-radius: 2px; + background: linear-gradient(to bottom, color-mix(in srgb, var(--primary) 40%, transparent), transparent); + } + } + + &:hover .reply-ghost { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; + } + + .reply-ghost-sender { + font-size: 11px; + font-weight: 500; + color: var(--primary); + opacity: 0.75; + margin-bottom: 3px; + padding-left: 6px; + } + + .reply-ghost-text { + font-size: 12.5px; + line-height: 1.5; + color: var(--text-secondary); + opacity: 0.82; + padding-left: 6px; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; + white-space: pre-wrap; + word-break: break-word; + + // Edge fade effect + -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent 100%); + mask-image: linear-gradient(to bottom, #000 55%, transparent 100%); + + .inline-emoji { + width: 16px !important; + height: 16px !important; + vertical-align: -2px; + } + } +} + +// Sent message — adjust ghost position to right +.message-bubble.sent .ambient-reply-wrapper { + .reply-ghost { + right: 0; + } + + .reply-anchor-name { + color: color-mix(in srgb, var(--on-primary) 92%, var(--primary)); + } + + .reply-anchor-excerpt { + color: color-mix(in srgb, var(--on-primary) 72%, var(--primary)); + } + + .reply-anchor-sep { + color: color-mix(in srgb, var(--on-primary) 50%, var(--primary)); + } + + .reply-anchor-icon { + color: color-mix(in srgb, var(--on-primary) 80%, var(--primary)); + } + + &:hover .reply-anchor { + background: color-mix(in srgb, var(--on-primary) 10%, transparent); + } + + .reply-ghost { + background: color-mix(in srgb, var(--bg-primary) 80%, transparent); + } +} + +// Received message — ghost to left +.message-bubble.received .ambient-reply-wrapper { + .reply-ghost { + left: 0; + } +} +// Legacy .quoted-message — used by SettingsPage quote-layout preview widget .quoted-message { background: rgba(0, 0, 0, 0.04); border-left: 2px solid var(--primary); - padding: 6px 10px; + padding: 4px 8px; border-radius: 4px; - font-size: 13px; + font-size: 12px; .quoted-sender { color: var(--primary); @@ -2468,38 +2652,9 @@ .quoted-text { color: var(--text-secondary); - white-space: pre-wrap; - - .quoted-type-label { - font-style: italic; - opacity: 0.8; - } - - .quoted-emoji-image { - width: 40px; - height: 40px; - vertical-align: middle; - object-fit: contain; - } } } -// 自己发送的消息中的引用样式 -.message-bubble.sent .quoted-message { - background: color-mix(in srgb, var(--on-primary) 12%, var(--primary)); - border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary)); - - .quoted-sender { - color: color-mix(in srgb, var(--on-primary) 92%, var(--primary)); - } - - .quoted-text { - color: color-mix(in srgb, var(--on-primary) 80%, var(--primary)); - } -} - - - // 气泡内容区域(包含名字和内容) .bubble-body { display: flex; @@ -2514,14 +2669,6 @@ .bubble-content { -webkit-app-region: no-drag; - - &.quote-layout-top .quoted-message { - margin-bottom: 8px; - } - - &.quote-layout-bottom .quoted-message { - margin-top: 8px; - } } // 时间分隔 @@ -5611,23 +5758,7 @@ margin-bottom: 5px; } -.quoted-message { - background: transparent; - border-left: 2px solid color-mix(in srgb, var(--primary) 62%, var(--text-tertiary)); - border-radius: 0; - padding: 2px 0 2px 10px; - color: var(--text-secondary); -} - -.message-bubble.sent .quoted-message { - background: color-mix(in srgb, var(--on-primary) 12%, transparent); - border-left-color: color-mix(in srgb, var(--on-primary) 62%, var(--primary)); - - .quoted-sender, - .quoted-text { - color: color-mix(in srgb, var(--on-primary) 82%, var(--primary)); - } -} +// Ambient Reply dark mode / alternate adjustments handled via CSS variables .link-message, .card-message, diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 714c2a2..d8c63bc 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -64,7 +64,7 @@ interface GlobalMsgPrefixCacheEntry { completed: boolean } -type QuoteLayout = configService.QuoteLayout + const GLOBAL_MSG_PER_SESSION_LIMIT = 10 const GLOBAL_MSG_SEED_LIMIT = 120 @@ -8252,7 +8252,6 @@ function MessageBubble({ const [senderAvatarUrl, setSenderAvatarUrl] = useState(undefined) const [senderName, setSenderName] = useState(undefined) const [quotedSenderName, setQuotedSenderName] = useState(undefined) - const [quoteLayout, setQuoteLayout] = useState('quote-top') const [solitaireExpanded, setSolitaireExpanded] = useState(false) const senderProfileRequestSeqRef = useRef(0) const [emojiError, setEmojiError] = useState(false) @@ -9401,17 +9400,7 @@ function MessageBubble({ myWxid ]) - useEffect(() => { - let cancelled = false - void configService.getQuoteLayout().then((layout) => { - if (!cancelled) setQuoteLayout(layout) - }).catch(() => { - if (!cancelled) setQuoteLayout('quote-top') - }) - return () => { - cancelled = true - } - }, []) + // quoteLayout config removed - Ambient Reply uses a single fixed layout const locationMessageMeta = useMemo(() => { if (message.localType !== 48) return null @@ -9448,29 +9437,32 @@ function MessageBubble({ // 是否有引用消息 const hasQuote = quotedContent.length > 0 const displayQuotedSenderName = quotedSenderName || quotedSenderFallbackName - const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => { - const quoteFirst = quoteLayout !== 'quote-bottom' - return ( -
- {quoteFirst ? ( - <> - {quotedNode} - {messageNode} - - ) : ( - <> - {messageNode} - {quotedNode} - - )} -
- ) - }, [quoteLayout]) + // Ambient Reply: single fixed layout (anchor above, message below) + const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => ( +
+ {quotedNode} + {messageNode} +
+ ), []) + // Ambient Reply: render reply-anchor + ghost preview const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => ( -
- {displayQuotedSenderName && {displayQuotedSenderName}} - {contentNode} +
+ {/* Reply anchor - always visible, subtle */} +
+ + + + + {displayQuotedSenderName && {displayQuotedSenderName}} + · + {contentNode} +
+ {/* Ghost preview - appears on hover */} +
+ {displayQuotedSenderName &&
{displayQuotedSenderName}
} +
{contentNode}
+
), [displayQuotedSenderName]) From 796515d3e8c7d081b74549c41828ae121d8cc0a7 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 10 May 2026 21:50:46 +0800 Subject: [PATCH 18/23] fix: Updated Chat Page UI, & Fixed Address Book White Screen Issue & Optimized Launch Page UI --- public/splash.html | 10 +- src/pages/Chat/ChatMessageBubble.tsx | 5 +- src/pages/ChatPage.scss | 52 +++++- src/pages/ChatPage.tsx | 240 ++++++++++++++++++++++++++- src/pages/ContactsPage.tsx | 2 +- 5 files changed, 291 insertions(+), 18 deletions(-) diff --git a/public/splash.html b/public/splash.html index 56141e5..8295a56 100644 --- a/public/splash.html +++ b/public/splash.html @@ -10,7 +10,7 @@ --primary-rgb: 139, 115, 85; --on-primary: #ffffff; --window-bg: transparent; - --card-bg: rgba(255, 255, 255, 0.96); + --card-bg: #ffffff; --card-bg-solid: #ffffff; --card-border: rgba(0, 0, 0, 0.06); --text-primary: #171717; @@ -54,11 +54,11 @@ min-height: 0; position: relative; overflow: hidden; - border-radius: 0; + border-radius: 18px; border: none; background: linear-gradient(180deg, var(--card-gloss-start) 0%, var(--card-gloss-end) 48%), - var(--card-bg); + var(--card-bg-solid); box-shadow: var(--shadow); isolation: isolate; animation: shellIn 420ms cubic-bezier(0.22, 1, 0.36, 1) both; @@ -340,7 +340,7 @@ setVar("--window-bg", "transparent"); if (isDark) { - setVar("--card-bg", "rgba(31, 31, 31, 0.96)"); + setVar("--card-bg", "#1f1f1f"); setVar("--card-bg-solid", "#1f1f1f"); setVar("--card-border", "rgba(255, 255, 255, 0.08)"); setVar("--text-primary", "#f1f1f1"); @@ -352,7 +352,7 @@ setVar("--card-gloss-end", "rgba(255, 255, 255, 0)"); setVar("--shadow", "none"); } else { - setVar("--card-bg", "rgba(255, 255, 255, 0.96)"); + setVar("--card-bg", "#ffffff"); setVar("--card-bg-solid", "#ffffff"); setVar("--card-border", "rgba(0, 0, 0, 0.06)"); setVar("--text-primary", "#171717"); diff --git a/src/pages/Chat/ChatMessageBubble.tsx b/src/pages/Chat/ChatMessageBubble.tsx index d88c6fd..e413979 100644 --- a/src/pages/Chat/ChatMessageBubble.tsx +++ b/src/pages/Chat/ChatMessageBubble.tsx @@ -13,6 +13,7 @@ export interface ChatMessageBubbleProps { isSystem: boolean isEmoji?: boolean isImage?: boolean + isVideo?: boolean isVoice?: boolean emojiHasAsset?: boolean emojiError?: boolean @@ -45,6 +46,7 @@ function ChatMessageBubble({ isSystem, isEmoji, isImage, + isVideo, isVoice, emojiHasAsset, emojiError, @@ -82,7 +84,7 @@ function ChatMessageBubble({ {isSelectionMode && !isSent && }
onContextMenu?.(event, message)} >
@@ -118,6 +120,7 @@ function areEqual(prev: ChatMessageBubbleProps, next: ChatMessageBubbleProps) { prev.isSystem === next.isSystem && prev.isEmoji === next.isEmoji && prev.isImage === next.isImage && + prev.isVideo === next.isVideo && prev.isVoice === next.isVoice && prev.emojiHasAsset === next.emojiHasAsset && prev.emojiError === next.emojiError && diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index b7c5039..9730e6b 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -666,7 +666,9 @@ } } - &.emoji { + &.emoji, + &.image, + &.video { .bubble-content { background: transparent !important; padding: 0; @@ -2012,6 +2014,15 @@ color: var(--text-primary); } } + + &.voice { + .bubble-content { + background: transparent !important; + padding: 0 !important; + border: 0 !important; + box-shadow: none !important; + } + } } .bubble-avatar { @@ -2067,7 +2078,12 @@ .message-bubble .bubble-content:has(> .solitaire-message), .message-bubble .bubble-content:has(> .official-message), .message-bubble .bubble-content:has(> .channel-video-card), -.message-bubble .bubble-content:has(> .location-message) { +.message-bubble .bubble-content:has(> .location-message), +.message-bubble .bubble-content:has(> .voice-stack), +.message-bubble .bubble-content:has(> .video-thumb-wrapper), +.message-bubble .bubble-content:has(> .video-placeholder), +.message-bubble .bubble-content:has(> .video-loading), +.message-bubble .bubble-content:has(> .video-unavailable) { background: transparent !important; padding: 0 !important; border: none !important; @@ -2471,6 +2487,15 @@ user-select: none; max-width: 100%; overflow: hidden; + + &.jumpable { + cursor: pointer; + } + + &:focus-visible { + outline: 2px solid color-mix(in srgb, var(--primary) 45%, transparent); + outline-offset: 2px; + } } &:hover .reply-anchor { @@ -2529,7 +2554,7 @@ bottom: calc(100% + 6px); max-width: 320px; min-width: 160px; - padding: 8px 12px 10px 12px; + padding: 8px 12px 12px 12px; border-radius: 12px; background: color-mix(in srgb, var(--bg-primary) 75%, transparent); backdrop-filter: blur(20px) saturate(1.3); @@ -2583,10 +2608,6 @@ white-space: pre-wrap; word-break: break-word; - // Edge fade effect - -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent 100%); - mask-image: linear-gradient(to bottom, #000 55%, transparent 100%); - .inline-emoji { width: 16px !important; height: 16px !important; @@ -5719,7 +5740,8 @@ } &.emoji, - &.image { + &.image, + &.video { max-width: min(82%, 760px); .bubble-content { @@ -5732,6 +5754,13 @@ background: var(--bg-tertiary); color: var(--text-primary); } + + &.voice .bubble-content { + background: transparent !important; + padding: 0 !important; + border: 0 !important; + box-shadow: none !important; + } } .message-bubble .bubble-content:has(> .link-message), @@ -5745,7 +5774,12 @@ .message-bubble .bubble-content:has(> .transfer-message), .message-bubble .bubble-content:has(> .gift-message), .message-bubble .bubble-content:has(> .miniapp-message), -.message-bubble .bubble-content:has(> .file-message) { +.message-bubble .bubble-content:has(> .file-message), +.message-bubble .bubble-content:has(> .voice-stack), +.message-bubble .bubble-content:has(> .video-thumb-wrapper), +.message-bubble .bubble-content:has(> .video-placeholder), +.message-bubble .bubble-content:has(> .video-loading), +.message-bubble .bubble-content:has(> .video-unavailable) { background: transparent !important; padding: 0 !important; border: 0 !important; diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index d8c63bc..37acf41 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -55,6 +55,17 @@ interface PendingFootprintJumpPayload { createTime: number } +interface QuotedMessageJumpTarget { + sourceMessageKey: string + sourceCreateTime: number + sessionId: string + localId?: number + serverId?: string + createTime?: number + senderUsername?: string + content?: string +} + type GlobalMsgSearchPhase = 'idle' | 'seed' | 'backfill' | 'done' type GlobalMsgSearchResult = Message & { sessionId: string } @@ -676,6 +687,26 @@ function cleanMessageContent(content: string): string { return content.trim() } +function normalizeMessageIdToken(value: unknown): string { + const raw = String(value ?? '').trim() + if (!raw) return '' + if (!/^\d+$/.test(raw)) return raw + return raw.replace(/^0+(?=\d)/, '') +} + +function parsePositiveInteger(value: unknown): number | undefined { + const raw = String(value ?? '').trim() + if (!raw) return undefined + const parsed = Number(raw) + if (!Number.isFinite(parsed) || parsed <= 0) return undefined + return Math.floor(parsed) +} + +function normalizeQuotedComparableText(value: unknown): string { + const text = cleanMessageContent(String(value ?? '')).replace(/\s+/g, ' ').trim() + return text.length > 160 ? text.slice(0, 160) : text +} + const CHAT_SESSION_LIST_CACHE_TTL_MS = 24 * 60 * 60 * 1000 const CHAT_SESSION_PREVIEW_CACHE_TTL_MS = 24 * 60 * 60 * 1000 const CHAT_SESSION_PREVIEW_LIMIT_PER_SESSION = 30 @@ -1114,6 +1145,15 @@ interface LoadMessagesOptions { inSessionJumpRequestSeq?: number } +type LoadMessagesFn = ( + sessionId: string, + offset?: number, + startTime?: number, + endTime?: number, + ascending?: boolean, + options?: LoadMessagesOptions +) => Promise + // 全局头像加载队列管理器已移至 src/utils/AvatarLoadQueue.ts import { avatarLoadQueue } from '../utils/AvatarLoadQueue' import { Avatar } from '../components/Avatar' @@ -1571,6 +1611,8 @@ function ChatPage(props: ChatPageProps) { const [globalMsgSearchError, setGlobalMsgSearchError] = useState(null) const pendingInSessionSearchRef = useRef(null) const pendingFootprintJumpRef = useRef(null) + const pendingQuotedMessageJumpRef = useRef(null) + const loadMessagesRef = useRef(null) const pendingGlobalMsgSearchReplayRef = useRef(null) const globalMsgPrefixCacheRef = useRef(null) @@ -3742,6 +3784,8 @@ function ChatPage(props: ChatPageProps) { } } + loadMessagesRef.current = loadMessages + const handleJumpDateSelect = useCallback((date: Date, options: { sessionId?: string; switchRequestSeq?: number } = {}) => { const targetSessionId = String(options.sessionId || currentSessionRef.current || currentSessionId || '').trim() if (!targetSessionId) return @@ -4851,6 +4895,136 @@ function ChatPage(props: ChatPageProps) { }, 2500) }, []) + const findQuotedTargetInMessages = useCallback((target: QuotedMessageJumpTarget): { index: number; message: Message } | null => { + if (messages.length === 0) return null + + const targetServerId = normalizeMessageIdToken(target.serverId) + const targetLocalId = typeof target.localId === 'number' && target.localId > 0 ? target.localId : undefined + const targetCreateTime = typeof target.createTime === 'number' && target.createTime > 0 ? target.createTime : undefined + const targetSender = String(target.senderUsername || '').trim() + const targetContent = normalizeQuotedComparableText(target.content) + const sourceIndex = target.sourceMessageKey + ? messages.findIndex((item) => getMessageKey(item) === target.sourceMessageKey) + : -1 + + const orderedIndices: number[] = [] + const usedIndices = new Set() + const pushIndex = (index: number) => { + if (index < 0 || index >= messages.length || usedIndices.has(index)) return + usedIndices.add(index) + orderedIndices.push(index) + } + + if (sourceIndex > 0) { + for (let index = sourceIndex - 1; index >= 0; index--) { + pushIndex(index) + } + } + for (let index = 0; index < messages.length; index++) { + pushIndex(index) + } + + let best: { index: number; message: Message; score: number } | null = null + for (const index of orderedIndices) { + const item = messages[index] + const itemKey = getMessageKey(item) + if (itemKey === target.sourceMessageKey) continue + + const itemServerId = normalizeMessageIdToken(item.serverIdRaw ?? item.serverId) + const serverMatch = Boolean(targetServerId && itemServerId && itemServerId === targetServerId) + const localIdMatch = Boolean(targetLocalId && Number(item.localId || 0) === targetLocalId) + const itemCreateTime = Number(item.createTime || 0) + const timeDelta = targetCreateTime ? Math.abs(itemCreateTime - targetCreateTime) : Number.POSITIVE_INFINITY + const exactTimeMatch = Boolean(targetCreateTime && timeDelta <= 1) + const nearTimeMatch = Boolean(targetCreateTime && timeDelta <= 300) + const senderMatch = Boolean(targetSender && String(item.senderUsername || '').trim() === targetSender) + const itemText = targetContent + ? normalizeQuotedComparableText(item.parsedContent || item.rawContent || item.content || '') + : '' + const contentMatch = Boolean( + targetContent && + itemText && + (itemText.includes(targetContent) || targetContent.includes(itemText)) + ) + + const strongMatch = Boolean( + serverMatch || + localIdMatch || + (exactTimeMatch && (senderMatch || contentMatch)) || + (exactTimeMatch && !targetSender && !targetContent) + ) + if (!strongMatch) continue + + const score = + (localIdMatch ? 100 : 0) + + (serverMatch ? 90 : 0) + + (exactTimeMatch ? 35 : (nearTimeMatch ? 8 : 0)) + + (senderMatch ? 12 : 0) + + (contentMatch ? 12 : 0) + + if (!best || score > best.score) { + best = { index, message: item, score } + if (score >= 125) break + } + } + + return best ? { index: best.index, message: best.message } : null + }, [messages, getMessageKey]) + + const scrollToResolvedMessage = useCallback((resolved: { index: number; message: Message }, behavior: 'auto' | 'smooth' = 'smooth') => { + const key = getMessageKey(resolved.message) + flashNewMessages([key]) + requestAnimationFrame(() => { + if (messageVirtuosoRef.current) { + messageVirtuosoRef.current.scrollToIndex({ + index: resolved.index, + align: 'center', + behavior + }) + } + }) + }, [flashNewMessages, getMessageKey]) + + const handleJumpToQuotedMessage = useCallback((target: QuotedMessageJumpTarget) => { + const targetSessionId = String(currentSessionRef.current || currentSessionId || target.sessionId || '').trim() + if (!targetSessionId) return + + const normalizedTarget: QuotedMessageJumpTarget = { + ...target, + sessionId: targetSessionId + } + const resolved = findQuotedTargetInMessages(normalizedTarget) + if (resolved) { + pendingQuotedMessageJumpRef.current = null + scrollToResolvedMessage(resolved) + return + } + + pendingQuotedMessageJumpRef.current = normalizedTarget + const targetTime = Number(normalizedTarget.createTime || 0) + if (!targetTime) return + + const requestSeq = inSessionResultJumpRequestSeqRef.current + 1 + inSessionResultJumpRequestSeqRef.current = requestSeq + setCurrentOffset(0) + setJumpStartTime(0) + setJumpEndTime(targetTime + 1) + suppressAutoLoadLaterRef.current = true + void loadMessagesRef.current?.(targetSessionId, 0, 0, targetTime + 1, false, { + forceInitialLimit: 120, + inSessionJumpRequestSeq: requestSeq + }) + }, [currentSessionId, findQuotedTargetInMessages, scrollToResolvedMessage]) + + useEffect(() => { + const pending = pendingQuotedMessageJumpRef.current + if (!pending) return + const resolved = findQuotedTargetInMessages(pending) + if (!resolved) return + pendingQuotedMessageJumpRef.current = null + scrollToResolvedMessage(resolved, 'auto') + }, [messages, findQuotedTargetInMessages, scrollToResolvedMessage]) + const handleInSessionResultJump = useCallback((msg: Message) => { const targetTime = Number(msg.createTime || 0) const targetSessionId = String(currentSessionRef.current || currentSessionId || '').trim() @@ -6649,6 +6823,7 @@ function ChatPage(props: ChatPageProps) { autoTranscribeVoiceEnabled={autoTranscribeVoiceEnabled} onRequireModelDownload={handleRequireModelDownload} onContextMenu={handleContextMenu} + onJumpToQuotedMessage={handleJumpToQuotedMessage} isSelectionMode={isSelectionMode} messageKey={messageKey} isSelected={selectedMessages.has(messageKey)} @@ -6668,6 +6843,7 @@ function ChatPage(props: ChatPageProps) { autoTranscribeVoiceEnabled, handleRequireModelDownload, handleContextMenu, + handleJumpToQuotedMessage, isSelectionMode, selectedMessages, handleToggleSelection @@ -8222,6 +8398,7 @@ function MessageBubble({ autoTranscribeVoiceEnabled, onRequireModelDownload, onContextMenu, + onJumpToQuotedMessage, isSelectionMode, isSelected, onToggleSelection @@ -8236,6 +8413,7 @@ function MessageBubble({ autoTranscribeVoiceEnabled?: boolean; onRequireModelDownload?: (sessionId: string, messageId: string) => void; onContextMenu?: (e: React.MouseEvent, message: Message) => void; + onJumpToQuotedMessage?: (target: QuotedMessageJumpTarget) => void; isSelectionMode?: boolean; isSelected?: boolean; onToggleSelection?: (messageKey: string, isShiftKey?: boolean) => void; @@ -9437,6 +9615,56 @@ function MessageBubble({ // 是否有引用消息 const hasQuote = quotedContent.length > 0 const displayQuotedSenderName = quotedSenderName || quotedSenderFallbackName + const quotedJumpTarget = useMemo(() => { + if (!hasQuote) return null + + const quotedServerId = normalizeMessageIdToken( + queryAppMsgText('refermsg > svrid') || + queryAppMsgText('refermsg > msgsvrid') || + queryAppMsgText('refermsg > newmsgid') || + queryAppMsgText('refermsg > msgid') + ) + const quotedCreateTime = parsePositiveInteger( + queryAppMsgText('refermsg > createtime') || + queryAppMsgText('refermsg > create_time') || + queryAppMsgText('refermsg > createTime') + ) + const quotedLocalId = parsePositiveInteger( + queryAppMsgText('refermsg > localid') || + queryAppMsgText('refermsg > local_id') || + queryAppMsgText('refermsg > localId') + ) + const normalizedQuotedContent = normalizeQuotedComparableText(quotedContent) + + if (!quotedServerId && !quotedCreateTime && !quotedLocalId && !normalizedQuotedContent) { + return null + } + + return { + sourceMessageKey: messageKey, + sourceCreateTime: Number(message.createTime || 0), + sessionId: session.username, + localId: quotedLocalId, + serverId: quotedServerId || undefined, + createTime: quotedCreateTime, + senderUsername: quotedSenderUsername || undefined, + content: normalizedQuotedContent || undefined + } + }, [hasQuote, message.createTime, messageKey, queryAppMsgText, quotedContent, quotedSenderUsername, session.username]) + const handleQuotedJumpClick = useCallback((event: React.MouseEvent) => { + if (isSelectionMode) return + if (!quotedJumpTarget || !onJumpToQuotedMessage) return + event.stopPropagation() + onJumpToQuotedMessage(quotedJumpTarget) + }, [isSelectionMode, onJumpToQuotedMessage, quotedJumpTarget]) + const handleQuotedJumpKeyDown = useCallback((event: React.KeyboardEvent) => { + if (event.key !== 'Enter' && event.key !== ' ') return + if (isSelectionMode) return + if (!quotedJumpTarget || !onJumpToQuotedMessage) return + event.preventDefault() + event.stopPropagation() + onJumpToQuotedMessage(quotedJumpTarget) + }, [isSelectionMode, onJumpToQuotedMessage, quotedJumpTarget]) // Ambient Reply: single fixed layout (anchor above, message below) const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => (
@@ -9449,7 +9677,13 @@ function MessageBubble({ const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => (
{/* Reply anchor - always visible, subtle */} -
+
@@ -9464,7 +9698,7 @@ function MessageBubble({
{contentNode}
- ), [displayQuotedSenderName]) + ), [displayQuotedSenderName, handleQuotedJumpClick, handleQuotedJumpKeyDown, isSelectionMode, quotedJumpTarget]) const handlePlayVideo = useCallback(async () => { if (!videoInfo?.videoUrl) return @@ -10634,6 +10868,7 @@ function MessageBubble({ isSystem={isSystem} isEmoji={isEmoji} isImage={isImage} + isVideo={isVideo} isVoice={isVoice} emojiHasAsset={Boolean(message.emojiCdnUrl || message.emojiLocalPath)} emojiError={emojiError} @@ -10663,6 +10898,7 @@ const MemoMessageBubble = React.memo(MessageBubble, (prevProps, nextProps) => { if (prevProps.isSelected !== nextProps.isSelected) return false if (prevProps.onRequireModelDownload !== nextProps.onRequireModelDownload) return false if (prevProps.onContextMenu !== nextProps.onContextMenu) return false + if (prevProps.onJumpToQuotedMessage !== nextProps.onJumpToQuotedMessage) return false if (prevProps.onToggleSelection !== nextProps.onToggleSelection) return false return ( diff --git a/src/pages/ContactsPage.tsx b/src/pages/ContactsPage.tsx index 0af23b4..909b4f7 100644 --- a/src/pages/ContactsPage.tsx +++ b/src/pages/ContactsPage.tsx @@ -793,7 +793,7 @@ function ContactsPage() { } } - const getContactTypeName = (type: string) => { + function getContactTypeName(type: string) { switch (type) { case 'friend': return '好友' case 'group': return '群聊' From b6b930ebb9e196e784f01a1d2ed42ca282021dda Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 10 May 2026 22:29:39 +0800 Subject: [PATCH 19/23] feat: enhance splash screen with dynamic theme support and improved styling --- electron/main.ts | 18 +- public/splash.html | 523 ++++++++++++++++++++++++++++----------------- 2 files changed, 343 insertions(+), 198 deletions(-) diff --git a/electron/main.ts b/electron/main.ts index 1a6809a..e9bc512 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1001,6 +1001,8 @@ function createAgreementWindow() { */ function createSplashWindow(): BrowserWindow { const isDev = !!process.env.VITE_DEV_SERVER_URL + const splashThemeId = configService?.get('themeId') || 'cloud-dancer' + const splashThemeMode = configService?.get('theme') || 'system' const iconPath = isDev ? join(__dirname, '../public/icon.ico') : (process.platform === 'darwin' @@ -1008,8 +1010,8 @@ function createSplashWindow(): BrowserWindow { : join(process.resourcesPath, 'icon.ico')) splashWindow = new BrowserWindow({ - width: 856, - height: 540, + width: 680, + height: 460, resizable: false, frame: false, transparent: true, @@ -1027,9 +1029,17 @@ function createSplashWindow(): BrowserWindow { }) if (isDev) { - splashWindow.loadURL(`${process.env.VITE_DEV_SERVER_URL}splash.html`) + const splashUrl = new URL('splash.html', process.env.VITE_DEV_SERVER_URL) + splashUrl.searchParams.set('themeId', splashThemeId) + splashUrl.searchParams.set('themeMode', splashThemeMode) + splashWindow.loadURL(splashUrl.toString()) } else { - splashWindow.loadFile(join(__dirname, '../dist/splash.html')) + splashWindow.loadFile(join(__dirname, '../dist/splash.html'), { + query: { + themeId: splashThemeId, + themeMode: splashThemeMode + } + }) } splashWindow.once('ready-to-show', () => { diff --git a/public/splash.html b/public/splash.html index 8295a56..aa32404 100644 --- a/public/splash.html +++ b/public/splash.html @@ -4,23 +4,65 @@ WeFlow +
-
-
- -

WeFlow

-

微信聊天记录管理工具

+
+ + +

WeFlow

+

微信聊天记录管理工具

-
正在启动...
+
+ +
正在预加载会话逻辑...
+
@@ -288,82 +456,45 @@
From fea00a6e36658a8e201ca322ef327f3cf8d45841 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 10 May 2026 22:50:19 +0800 Subject: [PATCH 20/23] fix: Splash Page --- public/splash.html | 116 ++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 54 deletions(-) diff --git a/public/splash.html b/public/splash.html index aa32404..f16f995 100644 --- a/public/splash.html +++ b/public/splash.html @@ -26,7 +26,6 @@ --accent: #5b6abf; --accent-rgb: 91, 106, 191; --accent-hot: #364491; - --logo-highlight: #5b6abf; --ambient-glow: rgba(91, 106, 191, 0.08); --core-glow: rgba(91, 106, 191, 0.16); --text: #1a1b1e; @@ -51,7 +50,6 @@ --accent: #7c8deb; --accent-rgb: 124, 141, 235; --accent-hot: #ffffff; - --logo-highlight: #ffffff; --ambient-glow: rgba(124, 141, 235, 0.08); --core-glow: rgba(124, 141, 235, 0.28); --text: #f0f0f0; @@ -153,22 +151,13 @@ border-color: rgba(255, 255, 255, 0.10); } - .logo-mark { - width: 32px; - height: 32px; - color: var(--accent); + .logo-image { + width: 52px; + height: 52px; display: block; - filter: drop-shadow(0 0 8px rgba(var(--accent-rgb), 0.18)); - } - - [data-mode="light"] .logo-mark .mark-base { - color: var(--text-faint); - opacity: 0.42; - } - - [data-mode="dark"] .logo-mark .mark-base { - color: var(--accent); - opacity: 1; + object-fit: contain; + border-radius: 16px; + filter: drop-shadow(0 10px 22px rgba(var(--accent-rgb), 0.18)); } .app-name { @@ -276,18 +265,17 @@ .progress-fill { position: absolute; - left: 0; + left: -18%; bottom: 0; - width: 0%; + width: 8%; height: 100%; min-width: 0; - border-radius: 0 999px 999px 0; + border-radius: 999px; background: linear-gradient(90deg, transparent 0%, var(--accent) 54%, var(--accent-hot) 100%); box-shadow: 0 0 10px 1px rgba(var(--accent-rgb), 0.30); - transition: - width 680ms var(--ease-ambient), - opacity 260ms ease, - left 680ms var(--ease-ambient); + transform-origin: center; + will-change: left, width, transform, opacity; + animation: beamSweep 2680ms var(--ease-ambient) infinite; } .progress-fill::before { @@ -315,12 +303,6 @@ animation-delay: 180ms; } - .progress-fill.waiting { - border-radius: 999px; - transition: none; - animation: beamSweep 2500ms var(--ease-ambient) infinite; - } - @media (prefers-reduced-motion: reduce) { .splash-shell, .brand-stage, @@ -334,9 +316,10 @@ transition: none !important; } - .progress-fill.waiting { + .progress-fill { left: 0 !important; - width: 70% !important; + width: 46% !important; + opacity: 1 !important; } } @@ -414,16 +397,52 @@ @keyframes beamSweep { 0% { - left: -30%; - width: 0%; + left: -18%; + width: 8%; + opacity: 0; + transform: scaleX(0.72); } - 50% { - left: 30%; - width: 40%; + 11% { + left: -2%; + width: 18%; + opacity: 0.72; + transform: scaleX(0.92); + } + 34% { + left: 27%; + width: 38%; + opacity: 1; + transform: scaleX(1.08); + } + 58% { + left: 60%; + width: 35%; + opacity: 1; + transform: scaleX(1); + } + 73% { + left: 86%; + width: 18%; + opacity: 0.94; + transform: scaleX(0.68); + } + 82% { + left: 96%; + width: 8%; + opacity: 0.58; + transform: scaleX(0.30); + } + 91% { + left: 92%; + width: 11%; + opacity: 0.18; + transform: scaleX(0.22); } 100% { - left: 100%; - width: 10%; + left: 112%; + width: 0%; + opacity: 0; + transform: scaleX(0.18); } } @@ -432,10 +451,7 @@

WeFlow

@@ -497,21 +513,13 @@ syncSystemModeListener(safeMode); } - function updateProgress(percent, text, waiting) { + function updateProgress(_percent, text, _waiting) { var fill = document.getElementById("progressFill"); var label = document.getElementById("progressText"); - var safePercent = Math.max(0, Math.min(100, Number(percent) || 0)); if (fill) { - if (waiting) { - fill.classList.add("waiting"); - fill.style.left = ""; - fill.style.width = ""; - } else { - fill.classList.remove("waiting"); - fill.style.left = "0"; - fill.style.width = safePercent + "%"; - } + fill.style.left = ""; + fill.style.width = ""; } if (label && text) label.textContent = text; From 0dc5efb63557dd3581719a2808783c5a06d9d388 Mon Sep 17 00:00:00 2001 From: Jason Date: Sun, 10 May 2026 23:07:26 +0800 Subject: [PATCH 21/23] fix: Splash Page UI --- public/splash.html | 139 +++++++++++++-------------------------------- 1 file changed, 39 insertions(+), 100 deletions(-) diff --git a/public/splash.html b/public/splash.html index f16f995..0c50382 100644 --- a/public/splash.html +++ b/public/splash.html @@ -21,18 +21,14 @@ :root { --surface-start: #ffffff; --surface-end: #f8f9fc; - --surface-glass: rgba(255, 255, 255, 0.78); - --surface-glass-end: rgba(255, 255, 255, 0.52); --accent: #5b6abf; --accent-rgb: 91, 106, 191; - --accent-hot: #364491; --ambient-glow: rgba(91, 106, 191, 0.08); - --core-glow: rgba(91, 106, 191, 0.16); --text: #1a1b1e; --text-muted: #5f6368; --text-faint: #9aa0a6; --border-subtle: rgba(0, 0, 0, 0.05); - --loader-track: rgba(0, 0, 0, 0.03); + --loader-track: rgba(0, 0, 0, 0.06); --shadow-window: 0 24px 60px rgba(23, 27, 38, 0.10), 0 4px 12px rgba(23, 27, 38, 0.04), @@ -45,18 +41,14 @@ [data-mode="dark"] { --surface-start: #14171d; --surface-end: #0b0d10; - --surface-glass: rgba(255, 255, 255, 0.10); - --surface-glass-end: rgba(255, 255, 255, 0.02); --accent: #7c8deb; --accent-rgb: 124, 141, 235; - --accent-hot: #ffffff; --ambient-glow: rgba(124, 141, 235, 0.08); - --core-glow: rgba(124, 141, 235, 0.28); --text: #f0f0f0; --text-muted: #8b92a5; --text-faint: #4e5569; --border-subtle: rgba(255, 255, 255, 0.06); - --loader-track: rgba(255, 255, 255, 0.03); + --loader-track: rgba(255, 255, 255, 0.09); --shadow-window: 0 24px 80px rgba(0, 0, 0, 0.60), inset 0 1px 0 rgba(255, 255, 255, 0.05); @@ -132,32 +124,20 @@ .logo-core { width: 64px; height: 64px; - border-radius: 20px; display: grid; place-items: center; margin-bottom: 24px; - background: linear-gradient(135deg, var(--surface-glass), var(--surface-glass-end)); - border: 1px solid rgba(255, 255, 255, 0.18); - backdrop-filter: blur(12px) saturate(1.16); - -webkit-backdrop-filter: blur(12px) saturate(1.16); - animation: coreBreathe 3200ms ease-in-out infinite alternate; - } - - [data-mode="light"] .logo-core { - border-color: rgba(255, 255, 255, 0.82); - } - - [data-mode="dark"] .logo-core { - border-color: rgba(255, 255, 255, 0.10); + background: transparent; + border: 0; } .logo-image { - width: 52px; - height: 52px; + width: 64px; + height: 64px; display: block; object-fit: contain; - border-radius: 16px; - filter: drop-shadow(0 10px 22px rgba(var(--accent-rgb), 0.18)); + border-radius: 20px; + animation: logoBreathe 3200ms ease-in-out infinite alternate; } .app-name { @@ -254,28 +234,27 @@ right: 0; bottom: 0; z-index: 3; - height: 1.5px; + height: 3px; background: var(--loader-track); overflow: hidden; } [data-mode="dark"] .progress-track { - height: 1px; + height: 3px; } .progress-fill { position: absolute; - left: -18%; + left: 0; bottom: 0; - width: 8%; + width: 0%; height: 100%; min-width: 0; - border-radius: 999px; - background: linear-gradient(90deg, transparent 0%, var(--accent) 54%, var(--accent-hot) 100%); - box-shadow: 0 0 10px 1px rgba(var(--accent-rgb), 0.30); - transform-origin: center; - will-change: left, width, transform, opacity; - animation: beamSweep 2680ms var(--ease-ambient) infinite; + border-radius: 0 999px 999px 0; + background: var(--accent); + box-shadow: 0 0 18px rgba(var(--accent-rgb), 0.34); + overflow: hidden; + transition: width 440ms var(--ease-ambient); } .progress-fill::before { @@ -288,8 +267,7 @@ border-radius: 999px; background: rgba(var(--accent-rgb), 0.34); filter: blur(8px); - opacity: 0.65; - animation: leadingGlow 1800ms ease-in-out infinite; + opacity: 0; } .progress-fill::after { @@ -299,15 +277,19 @@ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.54), transparent); opacity: 0; transform: translateX(-100%); - animation: spectralGlide 1500ms ease-out infinite; - animation-delay: 180ms; + animation: spectralGlide 1200ms ease-out; + } + + .progress-fill.waiting::before { + opacity: 0.65; + animation: leadingGlow 1300ms ease-in-out infinite; } @media (prefers-reduced-motion: reduce) { .splash-shell, .brand-stage, .status-row, - .logo-core, + .logo-image, .status-dot, .progress-fill, .progress-fill::before, @@ -318,7 +300,6 @@ .progress-fill { left: 0 !important; - width: 46% !important; opacity: 1 !important; } } @@ -345,13 +326,13 @@ } } - @keyframes coreBreathe { + @keyframes logoBreathe { 0% { - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.03), 0 0 0 1px rgba(var(--accent-rgb), 0.08); + opacity: 0.94; transform: translateY(0); } 100% { - box-shadow: 0 8px 30px var(--core-glow), 0 0 0 1px rgba(var(--accent-rgb), 0.20); + opacity: 1; transform: translateY(-3px); } } @@ -395,56 +376,6 @@ } } - @keyframes beamSweep { - 0% { - left: -18%; - width: 8%; - opacity: 0; - transform: scaleX(0.72); - } - 11% { - left: -2%; - width: 18%; - opacity: 0.72; - transform: scaleX(0.92); - } - 34% { - left: 27%; - width: 38%; - opacity: 1; - transform: scaleX(1.08); - } - 58% { - left: 60%; - width: 35%; - opacity: 1; - transform: scaleX(1); - } - 73% { - left: 86%; - width: 18%; - opacity: 0.94; - transform: scaleX(0.68); - } - 82% { - left: 96%; - width: 8%; - opacity: 0.58; - transform: scaleX(0.30); - } - 91% { - left: 92%; - width: 11%; - opacity: 0.18; - transform: scaleX(0.22); - } - 100% { - left: 112%; - width: 0%; - opacity: 0; - transform: scaleX(0.18); - } - } @@ -513,13 +444,21 @@ syncSystemModeListener(safeMode); } - function updateProgress(_percent, text, _waiting) { + function updateProgress(percent, text, waiting) { var fill = document.getElementById("progressFill"); var label = document.getElementById("progressText"); + var safePercent = Math.max(0, Math.min(100, Number(percent) || 0)); if (fill) { - fill.style.left = ""; - fill.style.width = ""; + fill.style.width = safePercent + "%"; + if (waiting) { + fill.classList.add("waiting"); + } else { + fill.classList.remove("waiting"); + fill.style.animation = "none"; + fill.offsetHeight; + fill.style.animation = ""; + } } if (label && text) label.textContent = text; From d5d64b2b50dd53e3bbeb99ecbf6b86be65e0ae38 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 11 May 2026 22:57:25 +0800 Subject: [PATCH 22/23] fix: Reply Style --- src/pages/ChatPage.scss | 16 ++++---- src/pages/SettingsPage.scss | 74 +++++++++++++++++++++++++++++-------- src/pages/SettingsPage.tsx | 26 ++++++++++--- 3 files changed, 87 insertions(+), 29 deletions(-) diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index 9730e6b..ba8c70b 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -2482,7 +2482,7 @@ padding: 2px 6px 2px 4px; border-radius: 8px; background: transparent; - opacity: 0.42; + opacity: 0.68; transition: opacity 0.35s ease, background-color 0.35s ease; user-select: none; max-width: 100%; @@ -2507,7 +2507,7 @@ width: 13px; height: 13px; color: var(--primary); - opacity: 0.7; + opacity: 0.86; flex-shrink: 0; transition: opacity 0.3s ease; } @@ -2538,7 +2538,7 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - opacity: 0.7; + opacity: 0.92; // Hide inline emoji images in anchor excerpt .inline-emoji { @@ -2599,7 +2599,7 @@ font-size: 12.5px; line-height: 1.5; color: var(--text-secondary); - opacity: 0.82; + opacity: 0.94; padding-left: 6px; display: -webkit-box; -webkit-line-clamp: 3; @@ -2623,19 +2623,19 @@ } .reply-anchor-name { - color: color-mix(in srgb, var(--on-primary) 92%, var(--primary)); + color: color-mix(in srgb, var(--on-primary) 96%, var(--primary)); } .reply-anchor-excerpt { - color: color-mix(in srgb, var(--on-primary) 72%, var(--primary)); + color: color-mix(in srgb, var(--on-primary) 86%, var(--primary)); } .reply-anchor-sep { - color: color-mix(in srgb, var(--on-primary) 50%, var(--primary)); + color: color-mix(in srgb, var(--on-primary) 68%, var(--primary)); } .reply-anchor-icon { - color: color-mix(in srgb, var(--on-primary) 80%, var(--primary)); + color: color-mix(in srgb, var(--on-primary) 90%, var(--primary)); } &:hover .reply-anchor { diff --git a/src/pages/SettingsPage.scss b/src/pages/SettingsPage.scss index 2645f2e..363b04a 100644 --- a/src/pages/SettingsPage.scss +++ b/src/pages/SettingsPage.scss @@ -1128,8 +1128,9 @@ .quote-layout-preview-shell { background: var(--bg-secondary); border-radius: 8px; - padding: 12px; + padding: 16px; border: 1px solid var(--border-color); + overflow: hidden; } .quote-layout-preview-chat { @@ -1137,32 +1138,75 @@ justify-content: flex-end; } -.message-bubble { - max-width: 80%; +.quote-layout-preview-chat .message-bubble { + max-width: 82%; } -.bubble-content { +.quote-layout-preview-chat .bubble-content { background: var(--primary); color: var(--on-primary); - border-radius: 12px; + border-radius: 18px; padding: 10px 12px; font-size: 13px; + line-height: 1.5; display: flex; flex-direction: column; + min-width: 0; + max-width: 100%; } -.quote-layout-top .quoted-message { margin-bottom: 6px; } -.quote-layout-bottom .quoted-message { margin-top: 6px; } +.quote-layout-top .ambient-reply-wrapper { margin-bottom: 4px; } +.quote-layout-bottom .ambient-reply-wrapper { margin-top: 4px; margin-bottom: 0; } -.quoted-message { - background: rgba(0,0,0,0.15); - border-left: 2px solid rgba(255,255,255,0.4); - padding: 4px 8px; - border-radius: 4px; +.quote-layout-preview-chat .ambient-reply-wrapper { + position: relative; + cursor: default; + min-width: 0; + max-width: 100%; +} + +.quote-layout-preview-chat .reply-anchor { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 100%; + min-width: 0; + padding: 2px 6px 2px 4px; + border-radius: 8px; + background: transparent; + user-select: none; + overflow: hidden; +} + +.quote-layout-preview-chat .reply-anchor-icon { + width: 13px; + height: 13px; + color: color-mix(in srgb, var(--on-primary) 84%, var(--primary)); + opacity: 0.78; + flex-shrink: 0; +} + +.quote-layout-preview-chat .reply-anchor-name { font-size: 12px; - - .quoted-sender { font-weight: 500; margin-right: 4px; &::after { content: ':'; } } - .quoted-text { opacity: 0.9; } + font-weight: 500; + color: color-mix(in srgb, var(--on-primary) 92%, var(--primary)); + flex-shrink: 0; +} + +.quote-layout-preview-chat .reply-anchor-sep { + font-size: 10px; + color: color-mix(in srgb, var(--on-primary) 58%, var(--primary)); + flex-shrink: 0; +} + +.quote-layout-preview-chat .reply-anchor-excerpt { + min-width: 0; + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 12px; + color: color-mix(in srgb, var(--on-primary) 78%, var(--primary)); } @media (max-width: 760px) { diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index b15ad52..ddd274b 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1711,16 +1711,30 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { {isQuoteBottom ? ( <>
拍得真不错!
-
- 张三 - 那天去爬山的照片... +
+
+ + + + + 张三 + · + 那天去爬山的照片... +
) : ( <> -
- 张三 - 那天去爬山的照片... +
+
+ + + + + 张三 + · + 那天去爬山的照片... +
拍得真不错!
From 16608b2c8e6b25aa55d63e4b21b28bf1159a470b Mon Sep 17 00:00:00 2001 From: Jason Date: Tue, 12 May 2026 23:08:34 +0800 Subject: [PATCH 23/23] fix: Reply Setting --- src/pages/ChatPage.scss | 19 +++++++++++++ src/pages/ChatPage.tsx | 56 +++++++++++++++++++++++++++++++++----- src/pages/SettingsPage.tsx | 1 + 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/pages/ChatPage.scss b/src/pages/ChatPage.scss index ba8c70b..b8bdf73 100644 --- a/src/pages/ChatPage.scss +++ b/src/pages/ChatPage.scss @@ -2549,6 +2549,25 @@ } // Ghost preview — appears on hover, frosted glass + &.preview-below { + margin-top: 4px; + margin-bottom: 0; + + .reply-ghost { + top: calc(100% + 6px); + bottom: auto; + transform: translateY(-4px) scale(0.98); + } + + &:hover .reply-ghost { + transform: translateY(0) scale(1); + } + } + + &.preview-above { + margin-bottom: 4px; + } + .reply-ghost { position: absolute; bottom: calc(100% + 6px); diff --git a/src/pages/ChatPage.tsx b/src/pages/ChatPage.tsx index 37acf41..a840199 100644 --- a/src/pages/ChatPage.tsx +++ b/src/pages/ChatPage.tsx @@ -1510,6 +1510,7 @@ function ChatPage(props: ChatPageProps) { const [groupMemberSearchKeyword, setGroupMemberSearchKeyword] = useState('') const [copiedField, setCopiedField] = useState(null) const [highlightedMessageKeys, setHighlightedMessageKeys] = useState([]) + const [quoteLayout, setQuoteLayout] = useState('quote-top') const [isRefreshingSessions, setIsRefreshingSessions] = useState(false) const [foldedView, setFoldedView] = useState(false) // 是否在"折叠的群聊"视图 const [bizView, setBizView] = useState(false) // 是否在"公众号"视图 @@ -3066,6 +3067,33 @@ function ChatPage(props: ChatPageProps) { } }, []) + useEffect(() => { + let canceled = false + const loadQuoteLayout = () => { + void configService.getQuoteLayout() + .then((layout) => { + if (!canceled) setQuoteLayout(layout) + }) + .catch(() => { + if (!canceled) setQuoteLayout('quote-top') + }) + } + + loadQuoteLayout() + const handleFocus = () => loadQuoteLayout() + const handleQuoteLayoutChanged = (event: Event) => { + const layout = (event as CustomEvent).detail + setQuoteLayout(layout === 'quote-bottom' ? 'quote-bottom' : 'quote-top') + } + window.addEventListener('focus', handleFocus) + window.addEventListener('quote-layout-changed', handleQuoteLayoutChanged) + return () => { + canceled = true + window.removeEventListener('focus', handleFocus) + window.removeEventListener('quote-layout-changed', handleQuoteLayoutChanged) + } + }, []) + useEffect(() => { let cancelled = false void (async () => { @@ -6820,6 +6848,7 @@ function ChatPage(props: ChatPageProps) { myAvatarUrl={myAvatarUrl} myWxid={myWxid} isGroupChat={isCurrentSessionGroup} + quoteLayout={quoteLayout} autoTranscribeVoiceEnabled={autoTranscribeVoiceEnabled} onRequireModelDownload={handleRequireModelDownload} onContextMenu={handleContextMenu} @@ -6840,6 +6869,7 @@ function ChatPage(props: ChatPageProps) { myAvatarUrl, myWxid, isCurrentSessionGroup, + quoteLayout, autoTranscribeVoiceEnabled, handleRequireModelDownload, handleContextMenu, @@ -8395,6 +8425,7 @@ function MessageBubble({ myAvatarUrl, myWxid, isGroupChat, + quoteLayout, autoTranscribeVoiceEnabled, onRequireModelDownload, onContextMenu, @@ -8410,6 +8441,7 @@ function MessageBubble({ myAvatarUrl?: string; myWxid?: string; isGroupChat?: boolean; + quoteLayout: configService.QuoteLayout; autoTranscribeVoiceEnabled?: boolean; onRequireModelDownload?: (sessionId: string, messageId: string) => void; onContextMenu?: (e: React.MouseEvent, message: Message) => void; @@ -9665,17 +9697,26 @@ function MessageBubble({ event.stopPropagation() onJumpToQuotedMessage(quotedJumpTarget) }, [isSelectionMode, onJumpToQuotedMessage, quotedJumpTarget]) - // Ambient Reply: single fixed layout (anchor above, message below) + const isQuoteBelow = quoteLayout === 'quote-bottom' const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => ( -
- {quotedNode} - {messageNode} +
+ {isQuoteBelow ? ( + <> + {messageNode} + {quotedNode} + + ) : ( + <> + {quotedNode} + {messageNode} + + )}
- ), []) + ), [isQuoteBelow]) // Ambient Reply: render reply-anchor + ghost preview const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => ( -
+
{/* Reply anchor - always visible, subtle */}
{contentNode}
- ), [displayQuotedSenderName, handleQuotedJumpClick, handleQuotedJumpKeyDown, isSelectionMode, quotedJumpTarget]) + ), [displayQuotedSenderName, handleQuotedJumpClick, handleQuotedJumpKeyDown, isQuoteBelow, isSelectionMode, quotedJumpTarget]) const handlePlayVideo = useCallback(async () => { if (!videoInfo?.videoUrl) return @@ -10893,6 +10934,7 @@ const MemoMessageBubble = React.memo(MessageBubble, (prevProps, nextProps) => { if (prevProps.myAvatarUrl !== nextProps.myAvatarUrl) return false if (prevProps.myWxid !== nextProps.myWxid) return false if (prevProps.isGroupChat !== nextProps.isGroupChat) return false + if (prevProps.quoteLayout !== nextProps.quoteLayout) return false if (prevProps.autoTranscribeVoiceEnabled !== nextProps.autoTranscribeVoiceEnabled) return false if (prevProps.isSelectionMode !== nextProps.isSelectionMode) return false if (prevProps.isSelected !== nextProps.isSelected) return false diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index ddd274b..dc53ef3 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -1698,6 +1698,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) { if (selected) return setQuoteLayout(option.value) await configService.setQuoteLayout(option.value) + window.dispatchEvent(new CustomEvent('quote-layout-changed', { detail: option.value })) showMessage(option.successMessage, true) }} role="radio"