- 迁移 v17:entry.updated_at(乐观锁)+ audit 表
- GET/PATCH /api/items/{key}:字段编辑、标签替换、409 冲突、400 校验
- 详情页 [编辑] → Drawer 表单(标题/简介/标签/评价/正文/封面/跳转),保存后局部刷新
- GET /api/backup:VACUUM INTO 一致性快照,附件下载
- /admin 系统 Tab:修改密码(清空会话)、下载备份
- 审计:item.update / settings.update
- 测试 46 passed(entries 4 例)
- 文档:ADMIN-PLAN P2/P3 记录、README
134 lines
3.9 KiB
TypeScript
134 lines
3.9 KiB
TypeScript
import {
|
|
Breadcrumb,
|
|
Card,
|
|
Icon,
|
|
IconButton,
|
|
Paragraph,
|
|
Space,
|
|
Tag,
|
|
Text,
|
|
Title,
|
|
type BreadcrumbItem,
|
|
} from '@stash/ui'
|
|
import type { ItemDetailModel } from '../../data/detail'
|
|
import type { RelatedItem } from '../../data/types'
|
|
import { RelatedList } from './RelatedList'
|
|
|
|
/**
|
|
* 条目详情页:应用与电影共用。
|
|
*
|
|
* 版式:面包屑 + 标题 + 主图(方形头像 / 竖版海报)+ 标签行 + 一句话评价 + 正文
|
|
* + 操作条(浏览 / 点赞 / 分享 / 跳转)+ 可选横幅 + 「推荐阅读」。
|
|
* 具体形态由 ItemDetailModel 决定,这里不判断条目类型。
|
|
*/
|
|
type ItemDetailViewProps = {
|
|
crumbs: BreadcrumbItem[]
|
|
model: ItemDetailModel
|
|
related: RelatedItem[]
|
|
onNavigate: (href: string) => void
|
|
onOpenItem: (itemId: string) => void
|
|
/** 打开编辑 Drawer */
|
|
onEdit: () => void
|
|
}
|
|
|
|
export function ItemDetailView({
|
|
crumbs,
|
|
model,
|
|
related,
|
|
onNavigate,
|
|
onOpenItem,
|
|
onEdit,
|
|
}: ItemDetailViewProps) {
|
|
return (
|
|
<div className="detail-view item-view">
|
|
<Card padding="none" className="detail-nav">
|
|
<Breadcrumb className="detail-breadcrumb" items={crumbs} onNavigate={onNavigate} />
|
|
</Card>
|
|
|
|
<Card padding="none" className="item-card">
|
|
<Title level={1} as="h2" weight="semibold" className="item-title">
|
|
{model.title}
|
|
<button type="button" className="item-edit" title="编辑条目" onClick={onEdit}>
|
|
[编辑]
|
|
</button>
|
|
</Title>
|
|
|
|
<div className={`item-cover item-cover--${model.shape}`}>
|
|
{model.cover !== undefined ? (
|
|
<img src={model.cover} alt={model.title} loading="lazy" />
|
|
) : (
|
|
<span className="item-cover-letter">{model.fallbackIcon ?? '•'}</span>
|
|
)}
|
|
</div>
|
|
|
|
{model.tags.length > 0 && (
|
|
<Space gap="sm" wrap>
|
|
{model.tags.map((tag) => (
|
|
<Tag key={tag} tone="primary" className="item-tag">
|
|
{tag}
|
|
</Tag>
|
|
))}
|
|
</Space>
|
|
)}
|
|
|
|
{model.comment !== undefined && (
|
|
<p className="item-quote">
|
|
<span className="item-quote-mark" aria-hidden="true">
|
|
“
|
|
</span>
|
|
<span className="item-quote-text">{model.comment}</span>
|
|
</p>
|
|
)}
|
|
|
|
{model.paragraphs.map((paragraph, index) => (
|
|
<Paragraph
|
|
key={`${index}-${paragraph.slice(0, 8)}`}
|
|
tone="sub"
|
|
size="md"
|
|
className="item-desc"
|
|
>
|
|
{paragraph}
|
|
</Paragraph>
|
|
))}
|
|
|
|
<div className="item-actions">
|
|
<Space gap="sm">
|
|
<Space gap="xs" className="item-stat">
|
|
<Icon name="eye_line" />
|
|
<Text size="xs" tone="sub">
|
|
{model.views}
|
|
</Text>
|
|
</Space>
|
|
<Space gap="xs" className="item-stat">
|
|
<Icon name="thumb_up_2_line" />
|
|
<Text size="xs" tone="sub">
|
|
{model.likes}
|
|
</Text>
|
|
</Space>
|
|
<IconButton label="分享(待接入)" size="sm" icon={<Icon name="share_3_line" />} />
|
|
</Space>
|
|
|
|
{/* 占位数据没有真实地址时,按钮置灰而不是打开空白页 */}
|
|
{model.href === '#' ? (
|
|
<button type="button" className="item-jump" disabled title="暂无跳转地址">
|
|
跳转到
|
|
<Icon name="a-external_link_line1" />
|
|
</button>
|
|
) : (
|
|
<a className="item-jump" href={model.href} target="_blank" rel="noreferrer">
|
|
跳转到
|
|
<Icon name="a-external_link_line1" />
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{model.banner !== undefined && (
|
|
<img className="item-banner" src={model.banner} alt="" loading="lazy" />
|
|
)}
|
|
</Card>
|
|
|
|
<RelatedList items={related} onOpen={onOpenItem} />
|
|
</div>
|
|
)
|
|
}
|