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:
+2
-64
@@ -298,70 +298,9 @@
|
|||||||
background-color: var(--su-color-primary);
|
background-color: var(--su-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 应用条目 ---------- */
|
/* ---------- 应用条目 / 图文白卡 ---------- */
|
||||||
|
|
||||||
.app-card {
|
/* 条目行的版式在 @stash/ui 的 EntryRow,图文卡在 EntryTile,这里不再有手写卡片外壳 */
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--su-space-4);
|
|
||||||
padding: var(--su-space-2) 0;
|
|
||||||
border-radius: var(--su-radius-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-card:hover .app-card-name {
|
|
||||||
color: var(--su-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-card-info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--su-space-1);
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 旧版应用头像是 48px 圆形,右下角是 10px 小认证标 */
|
|
||||||
.app-card-avatar {
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
font-size: var(--su-font-size-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-card-avatar > span + span {
|
|
||||||
width: 10px;
|
|
||||||
height: 10px;
|
|
||||||
border-width: 1px;
|
|
||||||
/* 6px 不是文字尺寸:这是 10px 圆标里的一枚「✓」字形,按图标对待;
|
|
||||||
设计系统的字阶(12 起)不覆盖这种装饰字形 */
|
|
||||||
font-size: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 图文白卡 ---------- */
|
|
||||||
|
|
||||||
.ces-card {
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform var(--su-duration-fast) var(--su-ease-in-out);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ces-card:hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ces-body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: var(--su-space-1);
|
|
||||||
padding: var(--su-space-3);
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ces-body--button {
|
|
||||||
width: 100%;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ces-body--button:hover {
|
|
||||||
background-color: var(--su-bg-subtle);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 详情 ---------- */
|
/* ---------- 详情 ---------- */
|
||||||
|
|
||||||
@@ -860,7 +799,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.app-card,
|
|
||||||
.related-item,
|
.related-item,
|
||||||
.link-button {
|
.link-button {
|
||||||
transition: none;
|
transition: none;
|
||||||
|
|||||||
@@ -178,6 +178,28 @@ export function AdminNext() {
|
|||||||
toast.success('侧边栏已保存')
|
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 () => {
|
const confirmDelete = async () => {
|
||||||
if (pending === null) return
|
if (pending === null) return
|
||||||
setDeleting(true)
|
setDeleting(true)
|
||||||
@@ -209,6 +231,12 @@ export function AdminNext() {
|
|||||||
<Brand href="/" name={brandName} mark={brandMark} logo={brandLogo} />
|
<Brand href="/" name={brandName} mark={brandMark} logo={brandLogo} />
|
||||||
|
|
||||||
<div className="header-actions">
|
<div className="header-actions">
|
||||||
|
<IconButton
|
||||||
|
label={isDark ? '切换到浅色' : '切换到深色'}
|
||||||
|
icon={<Icon name={isDark ? 'sun_line' : 'moon_line'} />}
|
||||||
|
onClick={() => void toggleTheme()}
|
||||||
|
disabled={!authenticated || saving}
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
label="返回首页"
|
label="返回首页"
|
||||||
icon={<Icon name="arrow_left_line" />}
|
icon={<Icon name="arrow_left_line" />}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Avatar, ItemBadge, Space, Text } from '@stash/ui'
|
import { EntryRow, ItemBadge } from '@stash/ui'
|
||||||
import type { AppItem } from '../../data/types'
|
import type { AppItem } from '../../data/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用卡片:头像 + 名称(热门带 HOT 角标)+ 单行描述。
|
* 应用卡片:头像 + 名称(热门带 HOT 角标)+ 单行描述。
|
||||||
* 点击进入条目详情;按住修饰键时保持浏览器默认行为(新标签打开原始链接)。
|
* 版式与点击语义都在库里的 EntryRow,这里只做数据映射。
|
||||||
*/
|
*/
|
||||||
type AppCardProps = {
|
type AppCardProps = {
|
||||||
item: AppItem
|
item: AppItem
|
||||||
@@ -12,37 +12,15 @@ type AppCardProps = {
|
|||||||
|
|
||||||
export function AppCard({ item, onOpen }: AppCardProps) {
|
export function AppCard({ item, onOpen }: AppCardProps) {
|
||||||
return (
|
return (
|
||||||
<a
|
<EntryRow
|
||||||
className="app-card"
|
|
||||||
href={item.href}
|
|
||||||
title={item.name}
|
title={item.name}
|
||||||
onClick={(event) => {
|
description={item.desc}
|
||||||
if (onOpen === undefined) return
|
href={item.href}
|
||||||
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return
|
avatar={item.cover}
|
||||||
event.preventDefault()
|
fallback={item.icon}
|
||||||
onOpen(item.id)
|
verified={item.hot === true}
|
||||||
}}
|
badge={item.hot === true ? <ItemBadge tone="hot">HOT</ItemBadge> : undefined}
|
||||||
>
|
onOpen={onOpen === undefined ? undefined : () => 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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { Card, Cover, Space, Text } from '@stash/ui'
|
import { EntryTile } from '@stash/ui'
|
||||||
import type { ArticleItem } from '../../data/types'
|
import type { ArticleItem } from '../../data/types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 明细页图文卡:上图下文的白卡。
|
* 明细页图文卡:上图下文的白卡。
|
||||||
* 传入 onOpen 时整张卡变成按钮,点击进入文章详情。
|
* 版式与点击语义都在库里的 EntryTile,这里只做数据映射。
|
||||||
*/
|
*/
|
||||||
type CesCardProps = {
|
type CesCardProps = {
|
||||||
item: ArticleItem
|
item: ArticleItem
|
||||||
@@ -11,38 +11,12 @@ type CesCardProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CesCard({ item, onOpen }: 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 (
|
return (
|
||||||
<Card padding="none" hoverable className="ces-card">
|
<EntryTile
|
||||||
<Cover
|
title={item.title}
|
||||||
src={item.cover}
|
description={item.desc}
|
||||||
alt={item.title}
|
cover={item.cover}
|
||||||
ratio={16 / 10}
|
onOpen={onOpen === undefined ? undefined : () => onOpen(item.id)}
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user