feat(admin): 页头拆成三个子页签,导航顺序与命名调整
- 首页管理下顺序改为 页头 / 侧边栏 / 内容卡片,默认展开
- 资料 -> 资料管理、系统 -> 系统管理
- 页头面板内用水平页签分为 站点信息 / 操作工具 / 滚动信息
- 滚动信息页列出可用通配符 {count} / {project} / {status} 及含义
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user