feat: 后台页头加明暗切换;内容卡改用库里的 EntryRow / EntryTile

- 后台页头新增月亮/太阳按钮:写回 site.theme 并即时切换,前后台一起变,
  忽略未登录与保存中
- AppCard -> EntryRow,CesCard -> EntryTile(PosterCard 本来就用 Cover)
- 删除 .app-card / .app-card-* / .ces-card / .ces-body* 手写样式,
  两套卡片的点击语义与悬停反馈统一由库提供

视觉变化:头像认证勾回到库默认尺寸(20px,之前应用里压到 10px)
This commit is contained in:
2026-11-01 16:08:44 +08:00
parent 18e9543607
commit 5c170f8819
4 changed files with 49 additions and 131 deletions
@@ -178,6 +178,28 @@ export function AdminNext() {
toast.success('侧边栏已保存')
}
/** 页头的明暗切换:写回 site.theme,前后台一起变 */
const themeSetting = draft?.site.theme ?? document?.settings.site.theme ?? 'auto'
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
const isDark = themeSetting === 'dark' || (themeSetting === 'auto' && prefersDark)
const toggleTheme = async () => {
if (document === null || draft === null) return
const next = isDark ? 'light' : 'dark'
setSaving(true)
const result = await saveSettings(document.version, {
...draft,
site: { ...draft.site, theme: next },
})
setSaving(false)
if (!result.ok) {
toast.error(result.error)
return
}
setDocument(result.document)
setDraft(clone(result.document.settings))
}
const confirmDelete = async () => {
if (pending === null) return
setDeleting(true)
@@ -209,6 +231,12 @@ export function AdminNext() {
<Brand href="/" name={brandName} mark={brandMark} logo={brandLogo} />
<div className="header-actions">
<IconButton
label={isDark ? '切换到浅色' : '切换到深色'}
icon={<Icon name={isDark ? 'sun_line' : 'moon_line'} />}
onClick={() => void toggleTheme()}
disabled={!authenticated || saving}
/>
<IconButton
label="返回首页"
icon={<Icon name="arrow_left_line" />}
+11 -33
View File
@@ -1,9 +1,9 @@
import { Avatar, ItemBadge, Space, Text } from '@stash/ui'
import { EntryRow, ItemBadge } from '@stash/ui'
import type { AppItem } from '../../data/types'
/**
* 应用卡片:头像 + 名称(热门带 HOT 角标)+ 单行描述。
* 点击进入条目详情;按住修饰键时保持浏览器默认行为(新标签打开原始链接)。
* 版式与点击语义都在库里的 EntryRow,这里只做数据映射。
*/
type AppCardProps = {
item: AppItem
@@ -12,37 +12,15 @@ type AppCardProps = {
export function AppCard({ item, onOpen }: AppCardProps) {
return (
<a
className="app-card"
href={item.href}
<EntryRow
title={item.name}
onClick={(event) => {
if (onOpen === undefined) return
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
event.preventDefault()
onOpen(item.id)
}}
>
<Avatar
size="lg"
tone="primary"
className="app-card-avatar"
src={item.cover}
fallback={item.icon}
verified={item.hot === true}
/>
<div className="app-card-info">
<Space gap="xs">
<Text strong truncate className="app-card-name">
{item.name}
</Text>
{item.hot === true && <ItemBadge tone="hot">HOT</ItemBadge>}
</Space>
<Text size="xs" tone="muted" truncate>
{item.desc}
</Text>
</div>
</a>
description={item.desc}
href={item.href}
avatar={item.cover}
fallback={item.icon}
verified={item.hot === true}
badge={item.hot === true ? <ItemBadge tone="hot">HOT</ItemBadge> : undefined}
onOpen={onOpen === undefined ? undefined : () => onOpen(item.id)}
/>
)
}
+8 -34
View File
@@ -1,9 +1,9 @@
import { Card, Cover, Space, Text } from '@stash/ui'
import { EntryTile } from '@stash/ui'
import type { ArticleItem } from '../../data/types'
/**
* 明细页图文卡:上图下文的白卡。
* 传入 onOpen 时整张卡变成按钮,点击进入文章详情。
* 版式与点击语义都在库里的 EntryTile,这里只做数据映射。
*/
type CesCardProps = {
item: ArticleItem
@@ -11,38 +11,12 @@ type CesCardProps = {
}
export function CesCard({ item, onOpen }: CesCardProps) {
const body = (
<Space direction="column" gap="xs" align="stretch">
<Text strong truncate>
{item.title}
</Text>
<Text size="xs" tone="muted" truncate>
{item.desc}
</Text>
</Space>
)
return (
<Card padding="none" hoverable className="ces-card">
<Cover
src={item.cover}
alt={item.title}
ratio={16 / 10}
radius="none"
onClick={onOpen === undefined ? undefined : () => onOpen(item.id)}
/>
{onOpen === undefined ? (
<div className="ces-body">{body}</div>
) : (
<button
type="button"
className="ces-body ces-body--button"
title={item.title}
onClick={() => onOpen(item.id)}
>
{body}
</button>
)}
</Card>
<EntryTile
title={item.title}
description={item.desc}
cover={item.cover}
onOpen={onOpen === undefined ? undefined : () => onOpen(item.id)}
/>
)
}