style: 统一前后台宽度与数值标尺,站点外壳改用 @stash/ui
对应风格审计的 1-4 条: 1. 宽度:后台内容区改为与页头内层同宽同左缘(1380 - 40 = 1340), 修掉品牌 x=50、卡片 x=280 的错位 2. 阴影:--su-shadow-card / hover 的覆盖从 .app 挪到 :root,前后台同一套 3. 死代码:删除未被引用的 App.tsx(436 行)、AdminPage.tsx(54KB)与 .content-veil 样式 4. 标尺:37 处脱离 4/8/12/16/20/24/32/40 与字阶的间距/字号/圆角/行高改为令牌 (含新增的 --su-font-size-2xl);6px 的认证角标按图标保留并注明原因 顺带把重复实现收进库: - 页头品牌 -> Brand - 页头滚动信息 -> Ticker(含实测宽度的无缝循环) - 搜索框 -> SearchField(variant=card),删除 components/search/SearchBar.tsx - 后台「首页 Logo」预览与后台页头同样复用 Brand
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
Brand,
|
||||
Button,
|
||||
Card,
|
||||
FormField,
|
||||
@@ -202,16 +203,7 @@ export function AdminNext() {
|
||||
{/* 与前台页头同一套结构:品牌在左、图标操作在右,整条吸顶 */}
|
||||
<header className="admin-topbar">
|
||||
<div className="site-header-inner">
|
||||
<a className="site-brand" href="/" aria-label={`${brandName || '首页'} 首页`}>
|
||||
{brandLogo !== '' ? (
|
||||
<img className="site-brand-logo" src={brandLogo} alt="" />
|
||||
) : (
|
||||
<span className="site-brand-mark" aria-hidden="true">
|
||||
{brandMark}
|
||||
</span>
|
||||
)}
|
||||
{brandName !== '' && <span className="site-brand-name">{brandName}</span>}
|
||||
</a>
|
||||
<Brand href="/" name={brandName} mark={brandMark} logo={brandLogo} />
|
||||
|
||||
<div className="header-actions">
|
||||
<IconButton
|
||||
@@ -1806,17 +1798,12 @@ function HeaderPanel({
|
||||
<Text size="sm" strong>
|
||||
首页 Logo
|
||||
</Text>
|
||||
<div className="brand-preview" aria-hidden="true">
|
||||
{site.brand.logo.trim() !== '' ? (
|
||||
<img className="site-brand-logo" src={site.brand.logo} alt="" />
|
||||
) : (
|
||||
<span className="site-brand-mark">
|
||||
{(site.brand.mark.trim() || site.brand.name.trim().slice(0, 1) || 'S').slice(0, 4)}
|
||||
</span>
|
||||
)}
|
||||
{site.brand.name.trim() !== '' && (
|
||||
<span className="site-brand-name">{site.brand.name}</span>
|
||||
)}
|
||||
<div className="brand-preview">
|
||||
<Brand
|
||||
name={site.brand.name}
|
||||
mark={site.brand.mark}
|
||||
logo={site.brand.logo}
|
||||
/>
|
||||
</div>
|
||||
<FormField label="Logo 图片地址" htmlFor="site-logo" hint="留空则用右侧的首字圆形标记">
|
||||
<Input
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { Icon, IconButton, Tag } from '@stash/ui'
|
||||
import { Brand, Icon, IconButton, Tag, Ticker } from '@stash/ui'
|
||||
import {
|
||||
fillHeaderText,
|
||||
type HeaderItem,
|
||||
@@ -9,8 +8,8 @@ import {
|
||||
|
||||
/**
|
||||
* 顶部栏:左品牌、中间滚动信息(可在 /admin 配置)、右侧操作图标。
|
||||
* 品牌(logo / 文字)与操作图标是否出现,都来自「页头」Tab 的站点设置。
|
||||
* 外层 <header> 由 Layout 提供,这里只渲染栏内的内容。
|
||||
* 品牌与操作图标是否出现,都来自「页头」Tab 的站点设置。
|
||||
* 外壳(品牌、跑马灯)都用组件库,这里只拼内容。
|
||||
*/
|
||||
type SiteHeaderProps = {
|
||||
projectCount: number
|
||||
@@ -42,40 +41,6 @@ const tagTone: Record<SettingsTone, 'neutral' | 'primary' | 'success'> = {
|
||||
success: 'success',
|
||||
}
|
||||
|
||||
type InfoGroupProps = {
|
||||
items: HeaderItem[]
|
||||
projectCount: number
|
||||
sampleProject?: string
|
||||
live: boolean
|
||||
'aria-hidden'?: boolean
|
||||
}
|
||||
|
||||
/** 运行信息:同一份内容渲染三遍,轨道平移 1/3 形成无缝滚动 */
|
||||
function InfoGroup({
|
||||
items,
|
||||
projectCount,
|
||||
sampleProject,
|
||||
live,
|
||||
'aria-hidden': ariaHidden,
|
||||
}: InfoGroupProps) {
|
||||
const context = { count: projectCount, project: sampleProject, live }
|
||||
|
||||
return (
|
||||
<span className="header-ticker-group" aria-hidden={ariaHidden}>
|
||||
{items.map((item, index) => (
|
||||
<span className="header-ticker-item" key={`${index}-${item.text}`}>
|
||||
{item.tag !== undefined && item.tag !== null && item.tag !== '' && (
|
||||
<Tag tone={tagTone[item.tone]} shape="rounded">
|
||||
{item.tag}
|
||||
</Tag>
|
||||
)}
|
||||
{fillHeaderText(item.text, context)}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function SiteHeader({
|
||||
projectCount,
|
||||
sampleProject,
|
||||
@@ -92,79 +57,30 @@ export function SiteHeader({
|
||||
onLogout,
|
||||
onSettings,
|
||||
}: SiteHeaderProps) {
|
||||
const brandName = site.brand.name.trim()
|
||||
const brandMark = site.brand.mark.trim() || brandName.slice(0, 1) || 'S'
|
||||
const brandLogo = site.brand.logo.trim()
|
||||
|
||||
// 跑马灯的位移要正好等于「一组」的宽度,循环才接得上。
|
||||
// CSS 里的 -100%/3 会按轨道的 border-box(只有一组宽)算,导致提前 2/3 回跳,
|
||||
// 所以这里量一次真实宽度写进 --ticker-shift。
|
||||
const trackRef = useRef<HTMLDivElement>(null)
|
||||
const tickerKey = `${headerItems.map((item) => `${item.tag ?? ''}:${item.text}`).join('|')}@${headerSpeed}`
|
||||
|
||||
useEffect(() => {
|
||||
const track = trackRef.current
|
||||
if (track === null) return
|
||||
|
||||
const sync = () => {
|
||||
const group = track.firstElementChild
|
||||
if (!(group instanceof HTMLElement)) return
|
||||
track.style.setProperty('--ticker-shift', `${-group.getBoundingClientRect().width}px`)
|
||||
}
|
||||
|
||||
sync()
|
||||
const observer = new ResizeObserver(sync)
|
||||
observer.observe(track)
|
||||
window.addEventListener('resize', sync)
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
window.removeEventListener('resize', sync)
|
||||
}
|
||||
}, [tickerKey])
|
||||
const context = { count: projectCount, project: sampleProject, live }
|
||||
|
||||
return (
|
||||
<div className="site-header-inner">
|
||||
<a className="site-brand" href="/" aria-label={`${brandName || '首页'} 首页`}>
|
||||
{brandLogo !== '' ? (
|
||||
<img className="site-brand-logo" src={brandLogo} alt="" />
|
||||
) : (
|
||||
<span className="site-brand-mark" aria-hidden="true">
|
||||
{brandMark}
|
||||
</span>
|
||||
)}
|
||||
{brandName !== '' && <span className="site-brand-name">{brandName}</span>}
|
||||
</a>
|
||||
<Brand
|
||||
href="/"
|
||||
name={site.brand.name}
|
||||
mark={site.brand.mark}
|
||||
logo={site.brand.logo}
|
||||
/>
|
||||
|
||||
{headerEnabled && (
|
||||
<div className="header-ticker" role="status" aria-label="运行状态">
|
||||
<div
|
||||
ref={trackRef}
|
||||
className="header-ticker-track"
|
||||
style={{ animationDuration: `${headerSpeed}s` }}
|
||||
>
|
||||
<InfoGroup
|
||||
items={headerItems}
|
||||
projectCount={projectCount}
|
||||
sampleProject={sampleProject}
|
||||
live={live}
|
||||
/>
|
||||
<InfoGroup
|
||||
items={headerItems}
|
||||
projectCount={projectCount}
|
||||
sampleProject={sampleProject}
|
||||
live={live}
|
||||
// 后两份只用于视觉无缝,读屏忽略
|
||||
aria-hidden
|
||||
/>
|
||||
<InfoGroup
|
||||
items={headerItems}
|
||||
projectCount={projectCount}
|
||||
sampleProject={sampleProject}
|
||||
live={live}
|
||||
aria-hidden
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Ticker speed={headerSpeed} label="运行状态">
|
||||
{headerItems.map((item, index) => (
|
||||
<span className="header-ticker-item" key={`${index}-${item.text}`}>
|
||||
{item.tag !== undefined && item.tag !== null && item.tag !== '' && (
|
||||
<Tag tone={tagTone[item.tone]} shape="rounded">
|
||||
{item.tag}
|
||||
</Tag>
|
||||
)}
|
||||
{fillHeaderText(item.text, context)}
|
||||
</span>
|
||||
))}
|
||||
</Ticker>
|
||||
)}
|
||||
|
||||
<div className="header-actions">
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Input } from '@stash/ui'
|
||||
import { useEffect, type RefObject } from 'react'
|
||||
|
||||
/**
|
||||
* 全局搜索框:吸在内容区顶部。
|
||||
* 支持 Ctrl/Cmd+K 聚焦、Esc 清空,输入内容由父组件接管过滤逻辑。
|
||||
*/
|
||||
type SearchBarProps = {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
inputRef: RefObject<HTMLInputElement | null>
|
||||
}
|
||||
|
||||
export function SearchBar({ value, onChange, inputRef }: SearchBarProps) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'k') {
|
||||
event.preventDefault()
|
||||
inputRef.current?.focus()
|
||||
inputRef.current?.select()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown)
|
||||
}, [inputRef])
|
||||
|
||||
return (
|
||||
<div className="search-row">
|
||||
<Input
|
||||
inputRef={inputRef}
|
||||
size="lg"
|
||||
icon="search_3_line"
|
||||
value={value}
|
||||
placeholder="搜索本地项目…"
|
||||
aria-label="搜索本地项目"
|
||||
clearable
|
||||
onClear={() => onChange('')}
|
||||
spellCheck={false}
|
||||
onChange={(event) => onChange(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Escape') onChange('')
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user