feat(nav): 侧栏高对比度 + 顶栏已打开页面标签 + 折叠态只留日夜切换 + 换明月图标

1) 侧边栏对比度
   - 新增 --sb-* 专属色板(固定深色底,不随日夜主题翻转),文字 #c3d7e3、
     hover 纯白、选中纯白 + 品牌色强调条 + 内描边,参考 GoodJob 的 Command Deck
   - 原过渡曲线(--transition-fast/base)保持不变
   - luxury.css 里把侧栏刷回浅底的覆盖一并改为同一套色板

2) 顶栏「已打开页面」标签
   - 新增 src/navigation.js:NAV_GROUPS 与 PAGE_LABELS 单点维护,App 与顶栏共用
   - GlobalHeader 用 useLocation 自动登记访问过的页面,工作台恒定置顶且不可关闭,
     其余按最近使用排序,可单独关闭,最多 10 个,落 localStorage(happy.openTabs)
   - 关掉当前标签会退到剩下的最近一个,没有则回工作台

3) 折叠态页脚
   - .sidebar.collapsed .settings-btn 隐藏,76px 宽度下只留一个日夜切换按钮

4) 明月图标
   - MoonIcon → MoonStarsIcon,造型取自 igoutu.cn/icon/101343/moon-and-stars,
     重绘为矢量(月牙 = 两段圆弧相减,两枚凹边圆角四角星),走 currentColor 随主题变色
   - 源图存 design-refs/(已忽略,不入库)

5) 顶栏排布修正
   - 标签条加入后头部变挤,收窄品牌区/搜索框,按钮加 nowrap 防止文字折行
   - ≤1280px 隐藏标签条
This commit is contained in:
Drsakura
2026-09-10 17:25:38 +08:00
parent d650ff9c4d
commit 1edf5ac995
7 changed files with 195 additions and 83 deletions
+58 -1
View File
@@ -1,7 +1,20 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useLocation, useNavigate } from 'react-router-dom'
import axios from 'axios'
import { BotIcon, ClockIcon, CloudDbIcon, PlusCircleIcon, SearchIcon } from './Icons'
import { HOME_PATH, PAGE_LABELS } from '../navigation'
const TABS_STORAGE_KEY = 'happy.openTabs'
const MAX_TABS = 10
/* 从本地缓存读取上次打开的页面(只保留仍然存在的路由,工作台恒定在首位) */
function readStoredTabs() {
try {
const raw = JSON.parse(localStorage.getItem(TABS_STORAGE_KEY) || '[]')
const list = Array.isArray(raw) ? raw.filter(path => PAGE_LABELS[path]) : []
return [HOME_PATH, ...list.filter(path => path !== HOME_PATH)]
} catch { return [HOME_PATH] }
}
function GlobalHeader({ onOpenAssistant }) {
const [now, setNow] = useState(new Date())
@@ -9,14 +22,44 @@ function GlobalHeader({ onOpenAssistant }) {
const [query, setQuery] = useState('')
const [results, setResults] = useState([])
const [searching, setSearching] = useState(false)
const [openTabs, setOpenTabs] = useState(readStoredTabs)
const searchRef = useRef(null)
const tabsRef = useRef(null)
const navigate = useNavigate()
const location = useLocation()
useEffect(() => {
const timer = window.setInterval(() => setNow(new Date()), 1000)
return () => window.clearInterval(timer)
}, [])
/* 访问过的页面自动登记成标签:工作台恒在首位,其余按最近使用排序,新页插在第二位 */
useEffect(() => {
const path = location.pathname
if (!PAGE_LABELS[path]) return
setOpenTabs(current => {
if (current[0] === path) return current
if (current[1] === path) return current
const rest = current.filter(item => item !== path && item !== HOME_PATH)
return [HOME_PATH, path, ...rest].slice(0, MAX_TABS)
})
}, [location.pathname])
useEffect(() => {
try { localStorage.setItem(TABS_STORAGE_KEY, JSON.stringify(openTabs)) } catch { /* 存储不可用时仅本次会话有效 */ }
const active = tabsRef.current?.querySelector('.page-tab.active')
active?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
}, [openTabs, location.pathname])
/* 关闭标签:关掉的若是当前页,就退到剩下的最近一个,没有则回工作台 */
const closeTab = (event, path) => {
event.stopPropagation()
const next = openTabs.filter(item => item !== path)
const result = next.length ? next : [HOME_PATH]
setOpenTabs(result)
if (path === location.pathname) navigate(result[0])
}
useEffect(() => {
const keyword = query.trim()
if (!keyword) { setResults([]); setSearching(false); return }
@@ -59,6 +102,20 @@ function GlobalHeader({ onOpenAssistant }) {
<div className="global-search-footnote">已检索客户、供应商、产品、订单、商机、待办和活动记录</div>
</div>}
</div>
{/* 已打开页面:搜索框后的快速跳转标签,工作台常驻且不可关闭 */}
<nav className="page-tabs" ref={tabsRef} aria-label="已打开页面">
{openTabs.map(path => {
const label = PAGE_LABELS[path] || path
const isHome = path === HOME_PATH
const active = location.pathname === path
return (
<div key={path} className={`page-tab${active ? ' active' : ''}`}>
<button type="button" className="page-tab-label" title={label} aria-current={active ? 'page' : undefined} onClick={() => navigate(path)}>{label}</button>
{!isHome && <button type="button" className="page-tab-close" aria-label={`关闭${label}`} title={`关闭${label}`} onClick={event => closeTab(event, path)}>×</button>}
</div>
)
})}
</nav>
<div className="global-header-actions"><div className="header-clock"><ClockIcon size={16} /><span>{dateLabel}</span><b>{timeLabel}</b></div><button className="quick-create-btn" title="快速新建"><PlusCircleIcon size={19} /><span>快速新建</span></button><button className="ai-entry-btn" onClick={onOpenAssistant} title="打开 AI 助手"><BotIcon size={19} /><span>AI 助手</span><i>β</i></button><button className="header-avatar" title="当前用户">W</button><button className="mobile-search-btn" onClick={() => { setSearchOpen(v => !v); window.setTimeout(() => searchRef.current?.focus(), 0) }} title="搜索"><SearchIcon size={18} /></button></div>
</header>
)