From 4f0b4884b635e9e5321762588d073e883d04501a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B5=E6=91=87=E5=B0=8F=E5=AD=90?= <969409112@qq.com> Date: Sat, 11 Jul 2026 23:30:18 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=20UI-01=20=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=A4=96=E5=A3=B3=E4=B8=8E=E8=AE=BE=E8=AE=A1=E4=BB=A4=E7=89=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/src/App.tsx | 64 ++-- src/renderer/src/assets/main.css | 278 +++++++++++++++++- .../src/components/account/AccountSummary.tsx | 50 ++++ .../src/components/layout/AppShell.tsx | 73 +++++ .../components/layout/PrimaryNavigation.tsx | 95 ++++++ .../src/components/layout/navigation.ts | 15 + src/renderer/src/main.tsx | 1 + src/renderer/src/styles/tokens.css | 26 ++ 8 files changed, 574 insertions(+), 28 deletions(-) create mode 100644 src/renderer/src/components/account/AccountSummary.tsx create mode 100644 src/renderer/src/components/layout/AppShell.tsx create mode 100644 src/renderer/src/components/layout/PrimaryNavigation.tsx create mode 100644 src/renderer/src/components/layout/navigation.ts create mode 100644 src/renderer/src/styles/tokens.css diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 1faec1d..e18fc9b 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -2,6 +2,8 @@ import { Sidebar } from './components/Sidebar' import ChatWindow from './components/ChatWindow' import { SettingsPanel } from './components/SettingsPanel' +import { AppShell } from './components/layout/AppShell' +import { AppPage } from './components/layout/navigation' import { Contact, Message } from '../../shared/types' function EyeIcon({ hidden }: { hidden: boolean }): React.ReactElement { @@ -149,6 +151,7 @@ function App(): React.ReactElement { const [showDbKey, setShowDbKey] = useState(false) const [showMacKeyFaq, setShowMacKeyFaq] = useState(false) const [showSettings, setShowSettings] = useState(false) + const [activePage, setActivePage] = useState('archive') const [selfInfo, setSelfInfo] = useState(null) const [isNativeMonitorActive, setIsNativeMonitorActive] = useState(false) const [bootState, setBootState] = useState<'loading' | 'connecting' | 'login'>('loading') @@ -830,30 +833,41 @@ function App(): React.ReactElement { } return ( -
- setShowSettings(true)} - /> -
- selectedContact && handleSelectContact(selectedContact)} - onRefreshData={loadContacts} - /> + { + setActivePage('settings') + setShowSettings(true) + }} + > +
+ setShowSettings(true)} + /> +
+ selectedContact && handleSelectContact(selectedContact)} + onRefreshData={loadContacts} + /> +
-
+
) } diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index f1ea6a0..4744b60 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -13,9 +13,9 @@ body { margin: 0; padding: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; - background-color: var(--bg-color); - color: var(--text-color); + font-family: var(--wxex-font); + background-color: var(--wxex-bg-app); + color: var(--wxex-text-primary); height: 100vh; overflow: hidden; } @@ -25,10 +25,277 @@ body { display: flex; } +.app-shell { + box-sizing: border-box; + display: flex; + width: 100%; + height: 100%; + min-height: 0; + min-width: 0; + overflow: hidden; + background: var(--wxex-bg-app); +} + +.app-primary-rail { + box-sizing: border-box; + width: var(--wxex-nav-width); + flex: 0 0 var(--wxex-nav-width); + height: 100%; + min-height: 0; + display: flex; + flex-direction: column; + align-items: center; + border-right: 1px solid var(--wxex-border); + background: var(--wxex-bg-sidebar); + padding: 12px 8px; + overflow: hidden; + -webkit-app-region: drag; +} + +.app-brand { + box-sizing: border-box; + width: 42px; + height: 42px; + display: grid; + place-items: center; + border: 0; + border-radius: var(--wxex-radius-md); + background: var(--wxex-brand); + color: #ffffff; + -webkit-app-region: no-drag; +} + +.app-brand svg { + width: 34px; + height: 34px; +} + +.app-brand path { + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; +} + +.app-brand circle { + fill: currentColor; +} + +.primary-navigation { + box-sizing: border-box; + display: flex; + flex: 1; + flex-direction: column; + gap: 6px; + width: 100%; + margin-top: 18px; + -webkit-app-region: no-drag; +} + +.primary-nav-item { + width: 100%; + height: 58px; + flex: 0 0 58px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 4px; + border: 0; + border-radius: var(--wxex-radius-md); + background: transparent; + color: var(--wxex-text-secondary); + cursor: pointer; + font-family: var(--wxex-font); +} + +.primary-nav-item:hover { + background: rgba(255, 255, 255, 0.46); + color: var(--wxex-text-primary); +} + +.primary-nav-item.active { + background: var(--wxex-brand-soft); + color: var(--wxex-brand); +} + +.primary-nav-mark { + width: 24px; + height: 24px; + display: grid; + place-items: center; +} + +.primary-nav-mark svg { + width: 22px; + height: 22px; +} + +.primary-nav-mark path, +.primary-nav-mark circle { + fill: none; + stroke: currentColor; + stroke-width: 1.7; + stroke-linecap: round; + stroke-linejoin: round; +} + +.primary-nav-label { + font-size: 12px; + line-height: 16px; + font-weight: 600; + white-space: nowrap; +} + +.app-rail-account { + box-sizing: border-box; + width: 100%; + flex: 0 0 auto; + -webkit-app-region: no-drag; +} + +.account-summary { + box-sizing: border-box; + width: 100%; + display: flex; + align-items: center; + gap: 10px; + border: 0; + border-radius: var(--wxex-radius-md); + background: transparent; + color: var(--wxex-text-primary); + cursor: pointer; + font-family: var(--wxex-font); + text-align: left; +} + +.account-summary:hover { + background: rgba(255, 255, 255, 0.72); +} + +.account-summary.compact { + justify-content: center; + padding: 8px 0; +} + +.account-summary-avatar { + box-sizing: border-box; + position: relative; + width: 38px; + height: 38px; + display: grid; + place-items: center; + flex: 0 0 auto; + border-radius: var(--wxex-radius-md); + background: var(--wxex-bg-elevated); + color: var(--wxex-brand); + font-size: 14px; + font-weight: 700; + overflow: hidden; +} + +.account-summary-avatar img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.account-summary-status { + position: absolute; + right: 2px; + bottom: 2px; + width: 9px; + height: 9px; + border: 2px solid var(--wxex-bg-elevated); + border-radius: 999px; + background: var(--wxex-text-muted); +} + +.account-summary-status.ready { + background: var(--wxex-success); +} + +.account-summary-text { + min-width: 0; + display: flex; + flex-direction: column; +} + +.account-summary-name { + overflow: hidden; + color: var(--wxex-text-primary); + font-size: 13px; + font-weight: 600; + line-height: 18px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.account-summary-meta { + overflow: hidden; + color: var(--wxex-text-muted); + font-size: 12px; + line-height: 16px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.app-shell-main { + box-sizing: border-box; + flex: 1; + min-width: 0; + min-height: 0; + height: 100%; + display: flex; + align-items: stretch; + overflow: hidden; + padding-top: var(--wxex-shell-content-top); + background: var(--wxex-bg-main); +} + +.app-page-placeholder { + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 24px; + color: var(--wxex-text-secondary); + text-align: center; +} + +.app-page-placeholder-eyebrow { + margin-bottom: 8px; + color: var(--wxex-brand); + font-size: 11px; + font-weight: 700; + letter-spacing: 0; +} + +.app-page-placeholder h2 { + margin: 0 0 8px; + color: var(--wxex-text-primary); + font-size: 20px; + font-weight: 600; + line-height: 28px; +} + +.app-page-placeholder p { + margin: 0; + font-size: 13px; + line-height: 20px; +} + .app-container { display: flex; width: 100%; height: 100%; + min-height: 0; + min-width: 0; + align-items: stretch; + overflow: hidden; } .sidebar { @@ -37,11 +304,13 @@ body { display: flex; flex-direction: column; height: 100%; + min-height: 0; flex-shrink: 0; /* Prevent shrinking */ } .sidebar-header { + flex: 0 0 auto; padding: 10px; border-bottom: 1px solid var(--border-color); -webkit-app-region: drag; @@ -208,12 +477,15 @@ body { display: flex; flex-direction: column; background-color: var(--chat-bg); + height: 100%; + min-height: 0; min-width: 0; /* Allow flex item to shrink below content size */ } .chat-header { height: 50px; + flex: 0 0 50px; border-bottom: 1px solid var(--border-color); display: flex; align-items: center; diff --git a/src/renderer/src/components/account/AccountSummary.tsx b/src/renderer/src/components/account/AccountSummary.tsx new file mode 100644 index 0000000..58293a2 --- /dev/null +++ b/src/renderer/src/components/account/AccountSummary.tsx @@ -0,0 +1,50 @@ +import React from 'react' + +interface SelfInfo { + wxid: string + nickname: string + avatar?: string + accountRoot: string +} + +interface AccountSummaryProps { + selfInfo: SelfInfo | null + dbReady: boolean + compact?: boolean + onClick?: () => void +} + +export function AccountSummary({ + selfInfo, + dbReady, + compact = false, + onClick +}: AccountSummaryProps): React.ReactElement { + const displayName = dbReady && selfInfo ? selfInfo.nickname || selfInfo.wxid || '当前账号' : '未连接' + const subtitle = dbReady && selfInfo ? selfInfo.wxid : '打开设置' + const initial = (displayName || '?').charAt(0) + + return ( + + ) +} diff --git a/src/renderer/src/components/layout/AppShell.tsx b/src/renderer/src/components/layout/AppShell.tsx new file mode 100644 index 0000000..87db084 --- /dev/null +++ b/src/renderer/src/components/layout/AppShell.tsx @@ -0,0 +1,73 @@ +import React from 'react' +import { AccountSummary } from '../account/AccountSummary' +import { PrimaryNavigation } from './PrimaryNavigation' +import { AppPage, PRIMARY_NAV_ITEMS } from './navigation' + +interface SelfInfo { + wxid: string + nickname: string + avatar?: string + accountRoot: string +} + +interface AppShellProps { + activePage: AppPage + selfInfo: SelfInfo | null + dbReady: boolean + onPageChange: (page: AppPage) => void + onOpenSettings: () => void + children: React.ReactNode +} + +function BrandLogo(): React.ReactElement { + return ( +
+ +
+ ) +} + +export function AppShell({ + activePage, + selfInfo, + dbReady, + onPageChange, + onOpenSettings, + children +}: AppShellProps): React.ReactElement { + const activeItem = PRIMARY_NAV_ITEMS.find((item) => item.id === activePage) + + return ( +
+ +
+ {activePage === 'archive' ? ( + children + ) : ( +
+
WechatExplorer
+

{activeItem?.label || '工作区'}

+

这个工作区会在后续 UI 重构阶段接入真实功能。

+
+ )} +
+
+ ) +} diff --git a/src/renderer/src/components/layout/PrimaryNavigation.tsx b/src/renderer/src/components/layout/PrimaryNavigation.tsx new file mode 100644 index 0000000..665868d --- /dev/null +++ b/src/renderer/src/components/layout/PrimaryNavigation.tsx @@ -0,0 +1,95 @@ +import React from 'react' +import { AppPage, PRIMARY_NAV_ITEMS } from './navigation' + +interface NavIconProps { + page: AppPage +} + +interface PrimaryNavigationProps { + activePage: AppPage + onPageChange: (page: AppPage) => void +} + +function NavIcon({ page }: NavIconProps): React.ReactElement { + switch (page) { + case 'archive': + return ( + + ) + case 'search': + return ( + + ) + case 'report': + return ( + + ) + case 'export': + return ( + + ) + case 'api': + return ( + + ) + case 'settings': + return ( + + ) + } +} + +export function PrimaryNavigation({ + activePage, + onPageChange +}: PrimaryNavigationProps): React.ReactElement { + return ( + + ) +} diff --git a/src/renderer/src/components/layout/navigation.ts b/src/renderer/src/components/layout/navigation.ts new file mode 100644 index 0000000..75102a7 --- /dev/null +++ b/src/renderer/src/components/layout/navigation.ts @@ -0,0 +1,15 @@ +export type AppPage = 'archive' | 'search' | 'report' | 'export' | 'api' | 'settings' + +export interface NavigationItem { + id: AppPage + label: string +} + +export const PRIMARY_NAV_ITEMS: NavigationItem[] = [ + { id: 'archive', label: '档案' }, + { id: 'search', label: '检索' }, + { id: 'report', label: '日报' }, + { id: 'export', label: '导出' }, + { id: 'api', label: 'API' }, + { id: 'settings', label: '设置' } +] diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 0ec3f40..0b85533 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -1,6 +1,7 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' +import './styles/tokens.css' import './assets/main.css' ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( diff --git a/src/renderer/src/styles/tokens.css b/src/renderer/src/styles/tokens.css new file mode 100644 index 0000000..8bd462b --- /dev/null +++ b/src/renderer/src/styles/tokens.css @@ -0,0 +1,26 @@ +:root { + --wxex-bg-app: #f4f6f5; + --wxex-bg-main: #fafbfa; + --wxex-bg-sidebar: #eef2f0; + --wxex-bg-elevated: #ffffff; + --wxex-text-primary: #202724; + --wxex-text-secondary: #66706b; + --wxex-text-muted: #929a96; + --wxex-border: #dde3e0; + --wxex-brand: #247a63; + --wxex-brand-hover: #1d6754; + --wxex-brand-soft: #ddede7; + --wxex-ai: #686edc; + --wxex-ai-soft: #eeeffd; + --wxex-success: #2e8b68; + --wxex-warning: #c68635; + --wxex-danger: #c85a5a; + --wxex-font: + -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; + --wxex-nav-width: 76px; + --wxex-shell-content-top: 12px; + --wxex-radius-sm: 4px; + --wxex-radius-md: 8px; + --wxex-radius-lg: 12px; + --wxex-shadow-popover: 0 4px 12px rgba(0, 0, 0, 0.08); +}