feat(admin): 页头拆成三个子页签,导航顺序与命名调整

- 首页管理下顺序改为 页头 / 侧边栏 / 内容卡片,默认展开
- 资料 -> 资料管理、系统 -> 系统管理
- 页头面板内用水平页签分为 站点信息 / 操作工具 / 滚动信息
- 滚动信息页列出可用通配符 {count} / {project} / {status} 及含义
This commit is contained in:
2026-11-01 20:36:11 +08:00
parent 10b653e30f
commit 0375669a6e
2 changed files with 260 additions and 195 deletions
+22
View File
@@ -53,6 +53,28 @@
padding: 0 var(--su-space-5); padding: 0 var(--su-space-5);
} }
/* 通配符说明:两列小表 */
.ticker-vars {
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: var(--su-space-2) var(--su-space-4);
align-items: center;
padding: var(--su-space-3);
border: 1px solid var(--su-border);
border-radius: var(--su-radius-md);
background-color: var(--su-bg-subtle);
font-size: var(--su-font-size-sm);
}
.ticker-vars code {
padding: 0 var(--su-space-2);
border-radius: var(--su-radius-xs);
background-color: var(--su-bg-card);
color: var(--su-color-primary);
font-family: var(--su-font-mono);
justify-self: start;
}
/* 后台:左侧导航 + 右侧内容 */ /* 后台:左侧导航 + 右侧内容 */
.admin-shell { .admin-shell {
display: grid; display: grid;
+238 -195
View File
@@ -14,6 +14,7 @@ import {
Select, Select,
Space, Space,
Switch, Switch,
Tabs,
Tag, Tag,
Text, Text,
Textarea, Textarea,
@@ -81,13 +82,13 @@ const ADMIN_NAV: MenuItem[] = [
key: 'home', key: 'home',
label: '首页管理', label: '首页管理',
children: [ children: [
{ key: 'home-header', label: '页头' },
{ key: 'home-sidebar', label: '侧边栏' }, { key: 'home-sidebar', label: '侧边栏' },
{ key: 'home-cards', label: '内容卡片' }, { key: 'home-cards', label: '内容卡片' },
{ key: 'home-header', label: '页头' },
], ],
}, },
{ key: 'profile', label: '资料' }, { key: 'profile', label: '资料管理' },
{ key: 'system', label: '系统' }, { key: 'system', label: '系统管理' },
] ]
const FORMAT_OPTIONS = [ const FORMAT_OPTIONS = [
@@ -308,6 +309,7 @@ export function AdminNext() {
activeKey={tab} activeKey={tab}
onSelect={(key) => setTab(key as TabKey)} onSelect={(key) => setTab(key as TabKey)}
itemHeight={40} itemHeight={40}
defaultOpenKeys={['home']}
/> />
</nav> </nav>
@@ -1900,206 +1902,247 @@ function HeaderPanel({
site: SiteSettings['site'] site: SiteSettings['site']
onChangeSite: (site: SiteSettings['site']) => void onChangeSite: (site: SiteSettings['site']) => void
}) { }) {
const [tab, setTab] = useState<'site' | 'actions' | 'ticker'>('site')
return ( return (
<Space direction="column" gap="lg" align="stretch" className="admin-form"> <Space direction="column" gap="lg" align="stretch" className="admin-form">
<Text size="sm" strong> <Tabs
站点信息 className="admin-tabs"
</Text> items={[
<FormField label="页面标题" htmlFor="site-title" hint="浏览器标签页上的文字"> { key: 'site', label: '站点信息' },
<Input { key: 'actions', label: '操作工具' },
id="site-title" { key: 'ticker', label: '滚动信息' },
value={site.title} ]}
placeholder="Stash · 本地应用工作区" activeKey={tab}
onChange={(event) => onChangeSite({ ...site, title: event.target.value })} onChange={(key) => setTab(key as 'site' | 'actions' | 'ticker')}
/> />
</FormField>
<FormField label="Favicon 地址" htmlFor="site-favicon" hint="留空使用内置图标;支持 .svg / .png 或完整 URL">
<Input
id="site-favicon"
value={site.favicon}
placeholder="/favicon.svg"
onChange={(event) => onChangeSite({ ...site, favicon: event.target.value })}
/>
</FormField>
<FormField label="配色" hint="深色由组件库的令牌块接管,页面与组件都不用改">
<Select
options={THEME_SELECT}
value={site.theme}
onChange={(theme) => onChangeSite({ ...site, theme: theme as SiteTheme })}
/>
</FormField>
<Text size="sm" strong> {tab === 'site' && (
首页 Logo <Space direction="column" gap="lg" align="stretch">
</Text> <FormField label="页面标题" htmlFor="site-title" hint="浏览器标签页上的文字">
<div className="brand-preview"> <Input
<Brand id="site-title"
name={site.brand.name} value={site.title}
mark={site.brand.mark} placeholder="Stash · 本地应用工作区"
logo={site.brand.logo} onChange={(event) => onChangeSite({ ...site, title: event.target.value })}
/> />
</div> </FormField>
<FormField label="Logo 图片地址" htmlFor="site-logo" hint="留空则用右侧的首字圆形标记"> <FormField
<Input label="Favicon 地址"
id="site-logo" htmlFor="site-favicon"
value={site.brand.logo} hint="留空使用内置图标;支持 .svg / .png 或完整 URL"
placeholder="https://… 或 /logo.svg" >
onChange={(event) => <Input
onChangeSite({ ...site, brand: { ...site.brand, logo: event.target.value } }) id="site-favicon"
} value={site.favicon}
/> placeholder="/favicon.svg"
</FormField> onChange={(event) => onChangeSite({ ...site, favicon: event.target.value })}
<div className="admin-row"> />
<FormField label="品牌文字" htmlFor="site-brand-name" hint="可留空只显示 logo"> </FormField>
<Input <FormField label="配色" hint="深色由组件库的令牌块接管,页面与组件都不用改">
id="site-brand-name" <Select
value={site.brand.name} options={THEME_SELECT}
placeholder="STASH" value={site.theme}
onChange={(event) => onChange={(theme) => onChangeSite({ ...site, theme: theme as SiteTheme })}
onChangeSite({ ...site, brand: { ...site.brand, name: event.target.value } }) />
} </FormField>
/>
</FormField>
<FormField label="首字标记" htmlFor="site-brand-mark" hint="1–4 个字">
<Input
id="site-brand-mark"
value={site.brand.mark}
placeholder="S"
onChange={(event) =>
onChangeSite({ ...site, brand: { ...site.brand, mark: event.target.value } })
}
/>
</FormField>
</div>
<Text size="sm" strong> <Text size="sm" strong>
操作工具 首页 Logo
</Text> </Text>
<div className="sidebar-toggle-row"> <div className="brand-preview">
<Text>明暗切换</Text> <Brand name={site.brand.name} mark={site.brand.mark} logo={site.brand.logo} />
<Switch </div>
checked={site.actions.theme} <FormField label="Logo 图片地址" htmlFor="site-logo" hint="留空则用右侧的首字圆形标记">
onChange={(next) => <Input
onChangeSite({ ...site, actions: { ...site.actions, theme: next } }) id="site-logo"
} value={site.brand.logo}
label="页头显示明暗切换按钮" placeholder="https://… 或 /logo.svg"
/> onChange={(event) =>
</div> onChangeSite({ ...site, brand: { ...site.brand, logo: event.target.value } })
<div className="sidebar-toggle-row"> }
<Text>刷新</Text> />
<Switch </FormField>
checked={site.actions.refresh} <div className="admin-row">
onChange={(next) => <FormField label="品牌文字" htmlFor="site-brand-name" hint="可留空只显示 logo">
onChangeSite({ ...site, actions: { ...site.actions, refresh: next } }) <Input
} id="site-brand-name"
label="页头显示刷新按钮" value={site.brand.name}
/> placeholder="STASH"
</div> onChange={(event) =>
<div className="sidebar-toggle-row"> onChangeSite({ ...site, brand: { ...site.brand, name: event.target.value } })
<Text>站点设置</Text> }
<Switch />
checked={site.actions.settings} </FormField>
onChange={(next) => <FormField label="首字标记" htmlFor="site-brand-mark" hint="1–4 个字">
onChangeSite({ ...site, actions: { ...site.actions, settings: next } }) <Input
} id="site-brand-mark"
label="页头显示站点设置按钮" value={site.brand.mark}
/> placeholder="S"
</div> onChange={(event) =>
<div className="sidebar-toggle-row"> onChangeSite({ ...site, brand: { ...site.brand, mark: event.target.value } })
<Text>登录 / 退出</Text> }
<Switch />
checked={site.actions.session} </FormField>
onChange={(next) => </div>
onChangeSite({ ...site, actions: { ...site.actions, session: next } }) </Space>
} )}
label="页头显示登录或退出按钮"
/>
</div>
<Text size="sm" strong> {tab === 'actions' && (
滚动信息 <Space direction="column" gap="md" align="stretch">
</Text> <Text size="sm" tone="muted">
<div className="sidebar-toggle-row"> 说明:这里控制页头右侧显示哪些图标按钮;关掉的按钮不再出现,也不影响其它页面。
<Text>显示页头滚动信息</Text> </Text>
<Switch <div className="sidebar-toggle-row">
checked={value.enabled} <Text>明暗切换</Text>
onChange={(enabled) => onChange({ ...value, enabled })} <Switch
label="显示页头滚动信息" checked={site.actions.theme}
/> onChange={(next) =>
</div> onChangeSite({ ...site, actions: { ...site.actions, theme: next } })
<FormField label="滚动速度" htmlFor="header-speed" hint="秒数越大越慢,范围 8–120"> }
<Input label="页头显示明暗切换按钮"
id="header-speed" />
type="number" </div>
value={String(value.speed)} <div className="sidebar-toggle-row">
onChange={(event) => { <Text>刷新</Text>
const speed = Number(event.target.value) <Switch
if (Number.isFinite(speed)) onChange({ ...value, speed }) checked={site.actions.refresh}
}} onChange={(next) =>
/> onChangeSite({ ...site, actions: { ...site.actions, refresh: next } })
</FormField> }
{value.items.map((item, index) => ( label="页头显示刷新按钮"
<div className="admin-item-row" key={index}> />
<Input </div>
value={item.text} <div className="sidebar-toggle-row">
placeholder="信息文本" <Text>站点设置</Text>
aria-label="信息文本" <Switch
onChange={(event) => checked={site.actions.settings}
onChange({ onChange={(next) =>
...value, onChangeSite({ ...site, actions: { ...site.actions, settings: next } })
items: value.items.map((current, currentIndex) => }
currentIndex === index ? { ...current, text: event.target.value } : current, label="页头显示站点设置按钮"
), />
}) </div>
} <div className="sidebar-toggle-row">
/> <Text>登录 / 退出</Text>
<Input <Switch
value={item.tag ?? ''} checked={site.actions.session}
placeholder="标签" onChange={(next) =>
aria-label="标签" onChangeSite({ ...site, actions: { ...site.actions, session: next } })
onChange={(event) => }
onChange({ label="页头显示登录或退出按钮"
...value, />
items: value.items.map((current, currentIndex) => </div>
currentIndex === index ? { ...current, tag: event.target.value } : current, </Space>
), )}
})
} {tab === 'ticker' && (
/> <Space direction="column" gap="md" align="stretch">
<Select <div className="sidebar-toggle-row">
options={TONE_SELECT} <Text>显示页头滚动信息</Text>
value={item.tone} <Switch
onChange={(tone) => checked={value.enabled}
onChange({ onChange={(enabled) => onChange({ ...value, enabled })}
...value, label="显示页头滚动信息"
items: value.items.map((current, currentIndex) => />
currentIndex === index </div>
? { ...current, tone: tone as SettingsTone } <FormField label="滚动速度" htmlFor="header-speed" hint="秒数越大越慢,范围 8–120">
: current, <Input
), id="header-speed"
}) type="number"
} value={String(value.speed)}
/> onChange={(event) => {
const speed = Number(event.target.value)
if (Number.isFinite(speed)) onChange({ ...value, speed })
}}
/>
</FormField>
<Text size="sm" strong>
可用通配符
</Text>
<Text size="sm" tone="muted">
说明:下面这些占位符写在「信息文本」里,渲染时会替换成实时值;写别的花括号不会替换。
</Text>
<div className="ticker-vars">
<code>{'{count}'}</code>
<span>本地应用数量</span>
<code>{'{project}'}</code>
<span>第一个本地项目的名字(没有项目时显示 —)</span>
<code>{'{status}'}</code>
<span>实时同步状态:实时同步已连接 / 等待服务连接</span>
</div>
<Text size="sm" strong>
信息条目
</Text>
{value.items.map((item, index) => (
<div className="admin-item-row" key={index}>
<Input
value={item.text}
placeholder="信息文本"
aria-label="信息文本"
onChange={(event) =>
onChange({
...value,
items: value.items.map((current, currentIndex) =>
currentIndex === index ? { ...current, text: event.target.value } : current,
),
})
}
/>
<Input
value={item.tag ?? ''}
placeholder="标签"
aria-label="标签"
onChange={(event) =>
onChange({
...value,
items: value.items.map((current, currentIndex) =>
currentIndex === index ? { ...current, tag: event.target.value } : current,
),
})
}
/>
<Select
options={TONE_SELECT}
value={item.tone}
onChange={(tone) =>
onChange({
...value,
items: value.items.map((current, currentIndex) =>
currentIndex === index
? { ...current, tone: tone as SettingsTone }
: current,
),
})
}
/>
<Button
variant="ghost"
size="sm"
icon={<Icon name="delete_2_line" />}
aria-label="删除这一条"
onClick={() =>
onChange({
...value,
items: value.items.filter((_, current) => current !== index),
})
}
/>
</div>
))}
<Button <Button
variant="ghost" variant="secondary"
size="sm" icon={<Icon name="add_line" />}
icon={<Icon name="delete_2_line" />}
aria-label="删除这一条"
onClick={() => onClick={() =>
onChange({ ...value, items: value.items.filter((_, current) => current !== index) }) onChange({ ...value, items: [...value.items, { text: '', tag: '', tone: 'plain' }] })
} }
/> >
</div> 添加一条
))} </Button>
<Button </Space>
variant="secondary" )}
icon={<Icon name="add_line" />}
onClick={() =>
onChange({ ...value, items: [...value.items, { text: '', tag: '', tone: 'plain' }] })
}
>
添加一条
</Button>
</Space> </Space>
) )
} }