mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 11:37:06 +08:00
完成 UI-01 应用外壳与设计令牌
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
import { Sidebar } from './components/Sidebar'
|
import { Sidebar } from './components/Sidebar'
|
||||||
import ChatWindow from './components/ChatWindow'
|
import ChatWindow from './components/ChatWindow'
|
||||||
import { SettingsPanel } from './components/SettingsPanel'
|
import { SettingsPanel } from './components/SettingsPanel'
|
||||||
|
import { AppShell } from './components/layout/AppShell'
|
||||||
|
import { AppPage } from './components/layout/navigation'
|
||||||
import { Contact, Message } from '../../shared/types'
|
import { Contact, Message } from '../../shared/types'
|
||||||
|
|
||||||
function EyeIcon({ hidden }: { hidden: boolean }): React.ReactElement {
|
function EyeIcon({ hidden }: { hidden: boolean }): React.ReactElement {
|
||||||
@@ -149,6 +151,7 @@ function App(): React.ReactElement {
|
|||||||
const [showDbKey, setShowDbKey] = useState(false)
|
const [showDbKey, setShowDbKey] = useState(false)
|
||||||
const [showMacKeyFaq, setShowMacKeyFaq] = useState(false)
|
const [showMacKeyFaq, setShowMacKeyFaq] = useState(false)
|
||||||
const [showSettings, setShowSettings] = useState(false)
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
|
const [activePage, setActivePage] = useState<AppPage>('archive')
|
||||||
const [selfInfo, setSelfInfo] = useState<SelfInfo | null>(null)
|
const [selfInfo, setSelfInfo] = useState<SelfInfo | null>(null)
|
||||||
const [isNativeMonitorActive, setIsNativeMonitorActive] = useState(false)
|
const [isNativeMonitorActive, setIsNativeMonitorActive] = useState(false)
|
||||||
const [bootState, setBootState] = useState<'loading' | 'connecting' | 'login'>('loading')
|
const [bootState, setBootState] = useState<'loading' | 'connecting' | 'login'>('loading')
|
||||||
@@ -830,6 +833,16 @@ function App(): React.ReactElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<AppShell
|
||||||
|
activePage={activePage}
|
||||||
|
selfInfo={selfInfo}
|
||||||
|
dbReady={isAuthenticated}
|
||||||
|
onPageChange={setActivePage}
|
||||||
|
onOpenSettings={() => {
|
||||||
|
setActivePage('settings')
|
||||||
|
setShowSettings(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div className="app-container">
|
<div className="app-container">
|
||||||
<Sidebar
|
<Sidebar
|
||||||
contacts={filteredContacts}
|
contacts={filteredContacts}
|
||||||
@@ -854,6 +867,7 @@ function App(): React.ReactElement {
|
|||||||
onRefresh={() => selectedContact && handleSelectContact(selectedContact)}
|
onRefresh={() => selectedContact && handleSelectContact(selectedContact)}
|
||||||
onRefreshData={loadContacts}
|
onRefreshData={loadContacts}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
<SettingsPanel
|
<SettingsPanel
|
||||||
open={showSettings}
|
open={showSettings}
|
||||||
selfInfo={selfInfo}
|
selfInfo={selfInfo}
|
||||||
@@ -866,7 +880,7 @@ function App(): React.ReactElement {
|
|||||||
void loadContacts()
|
void loadContacts()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</AppShell>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,9 @@
|
|||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
font-family: var(--wxex-font);
|
||||||
background-color: var(--bg-color);
|
background-color: var(--wxex-bg-app);
|
||||||
color: var(--text-color);
|
color: var(--wxex-text-primary);
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -25,10 +25,277 @@ body {
|
|||||||
display: flex;
|
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 {
|
.app-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
align-items: stretch;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
@@ -37,11 +304,13 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
/* Prevent shrinking */
|
/* Prevent shrinking */
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-header {
|
.sidebar-header {
|
||||||
|
flex: 0 0 auto;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
@@ -208,12 +477,15 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--chat-bg);
|
background-color: var(--chat-bg);
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
/* Allow flex item to shrink below content size */
|
/* Allow flex item to shrink below content size */
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-header {
|
.chat-header {
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
flex: 0 0 50px;
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`account-summary ${compact ? 'compact' : ''}`}
|
||||||
|
onClick={onClick}
|
||||||
|
title={compact ? `${displayName}\n${subtitle}` : undefined}
|
||||||
|
>
|
||||||
|
<span className="account-summary-avatar">
|
||||||
|
{selfInfo?.avatar ? (
|
||||||
|
<img src={selfInfo.avatar} alt={displayName} referrerPolicy="no-referrer" />
|
||||||
|
) : (
|
||||||
|
initial
|
||||||
|
)}
|
||||||
|
<span className={`account-summary-status ${dbReady ? 'ready' : ''}`} aria-hidden />
|
||||||
|
</span>
|
||||||
|
{!compact && (
|
||||||
|
<span className="account-summary-text">
|
||||||
|
<span className="account-summary-name">{displayName}</span>
|
||||||
|
<span className="account-summary-meta">{subtitle}</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="app-brand" title="WechatExplorer" aria-label="WechatExplorer">
|
||||||
|
<svg viewBox="0 0 40 40" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M13 15.5 20 10l7 5.5" />
|
||||||
|
<path d="M13 24.5 20 30l7-5.5" />
|
||||||
|
<path d="M13 15.5v9" />
|
||||||
|
<path d="M27 15.5v9" />
|
||||||
|
<circle cx="20" cy="10" r="2.6" />
|
||||||
|
<circle cx="13" cy="15.5" r="2.6" />
|
||||||
|
<circle cx="27" cy="15.5" r="2.6" />
|
||||||
|
<circle cx="13" cy="24.5" r="2.6" />
|
||||||
|
<circle cx="27" cy="24.5" r="2.6" />
|
||||||
|
<circle cx="20" cy="30" r="2.6" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AppShell({
|
||||||
|
activePage,
|
||||||
|
selfInfo,
|
||||||
|
dbReady,
|
||||||
|
onPageChange,
|
||||||
|
onOpenSettings,
|
||||||
|
children
|
||||||
|
}: AppShellProps): React.ReactElement {
|
||||||
|
const activeItem = PRIMARY_NAV_ITEMS.find((item) => item.id === activePage)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-shell">
|
||||||
|
<aside className="app-primary-rail">
|
||||||
|
<BrandLogo />
|
||||||
|
<PrimaryNavigation activePage={activePage} onPageChange={onPageChange} />
|
||||||
|
<div className="app-rail-account">
|
||||||
|
<AccountSummary selfInfo={selfInfo} dbReady={dbReady} compact onClick={onOpenSettings} />
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main className="app-shell-main">
|
||||||
|
{activePage === 'archive' ? (
|
||||||
|
children
|
||||||
|
) : (
|
||||||
|
<div className="app-page-placeholder">
|
||||||
|
<div className="app-page-placeholder-eyebrow">WechatExplorer</div>
|
||||||
|
<h2>{activeItem?.label || '工作区'}</h2>
|
||||||
|
<p>这个工作区会在后续 UI 重构阶段接入真实功能。</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M5 5.5h14v14H5z" />
|
||||||
|
<path d="M8 8.5h8" />
|
||||||
|
<path d="M8 12h8" />
|
||||||
|
<path d="M8 15.5h5" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'search':
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<circle cx="10.5" cy="10.5" r="5.5" />
|
||||||
|
<path d="m15 15 4 4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'report':
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M7 4.5h7l3 3v12H7z" />
|
||||||
|
<path d="M14 4.5v3h3" />
|
||||||
|
<path d="M9.5 13.5h5" />
|
||||||
|
<path d="M9.5 16.5h4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'export':
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M12 4.5v9" />
|
||||||
|
<path d="m8.5 10 3.5 3.5 3.5-3.5" />
|
||||||
|
<path d="M5.5 15.5v3h13v-3" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'api':
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<path d="M8 8.5 4.5 12 8 15.5" />
|
||||||
|
<path d="m16 8.5 3.5 3.5-3.5 3.5" />
|
||||||
|
<path d="m13.5 6.5-3 11" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
case 'settings':
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M12 4.5v2" />
|
||||||
|
<path d="M12 17.5v2" />
|
||||||
|
<path d="M4.5 12h2" />
|
||||||
|
<path d="M17.5 12h2" />
|
||||||
|
<path d="m6.8 6.8 1.4 1.4" />
|
||||||
|
<path d="m15.8 15.8 1.4 1.4" />
|
||||||
|
<path d="m17.2 6.8-1.4 1.4" />
|
||||||
|
<path d="m8.2 15.8-1.4 1.4" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PrimaryNavigation({
|
||||||
|
activePage,
|
||||||
|
onPageChange
|
||||||
|
}: PrimaryNavigationProps): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<nav className="primary-navigation" aria-label="一级导航">
|
||||||
|
{PRIMARY_NAV_ITEMS.map((item) => (
|
||||||
|
<button
|
||||||
|
key={item.id}
|
||||||
|
type="button"
|
||||||
|
className={`primary-nav-item ${activePage === item.id ? 'active' : ''}`}
|
||||||
|
onClick={() => onPageChange(item.id)}
|
||||||
|
title={item.label}
|
||||||
|
>
|
||||||
|
<span className="primary-nav-mark" aria-hidden>
|
||||||
|
<NavIcon page={item.id} />
|
||||||
|
</span>
|
||||||
|
<span className="primary-nav-label">{item.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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: '设置' }
|
||||||
|
]
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import './styles/tokens.css'
|
||||||
import './assets/main.css'
|
import './assets/main.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user