feat(admin): 分区增删、资料设置、操作条与交互收尾
- POST/DELETE /api/categories(需登录,级联,local 保护) - profile 设置(头像/名称/角色/简介/位置)应用到资料卡 - 详情操作条:浏览 +1、点赞切换、分享复制、跳转外链 - 编辑按钮垂直居中且留 12px;设置页按钮上间距、保存 Toast、Tabs 去滚动条 - 登录密码可见切换 - 测试 48 passed
This commit is contained in:
@@ -8,8 +8,10 @@ import {
|
||||
Tag,
|
||||
Text,
|
||||
Title,
|
||||
toast,
|
||||
type BreadcrumbItem,
|
||||
} from '@stash/ui'
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { ItemDetailModel } from '../../data/detail'
|
||||
import type { RelatedItem } from '../../data/types'
|
||||
import { RelatedList } from './RelatedList'
|
||||
@@ -22,6 +24,8 @@ import { RelatedList } from './RelatedList'
|
||||
* 具体形态由 ItemDetailModel 决定,这里不判断条目类型。
|
||||
*/
|
||||
type ItemDetailViewProps = {
|
||||
/** 条目业务键:浏览 / 点赞要写回数据库 */
|
||||
itemKey: string
|
||||
crumbs: BreadcrumbItem[]
|
||||
model: ItemDetailModel
|
||||
related: RelatedItem[]
|
||||
@@ -34,6 +38,7 @@ type ItemDetailViewProps = {
|
||||
}
|
||||
|
||||
export function ItemDetailView({
|
||||
itemKey,
|
||||
crumbs,
|
||||
model,
|
||||
related,
|
||||
@@ -42,6 +47,74 @@ export function ItemDetailView({
|
||||
onEdit,
|
||||
canEdit,
|
||||
}: ItemDetailViewProps) {
|
||||
const [views, setViews] = useState(model.views)
|
||||
const [likes, setLikes] = useState(model.likes)
|
||||
const [liked, setLiked] = useState(() => {
|
||||
try {
|
||||
return window.localStorage.getItem(`stash-liked:${itemKey}`) === '1'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// 进入详情页记一次浏览(每个页面打开一次)
|
||||
useEffect(() => {
|
||||
let alive = true
|
||||
fetch(`/api/items/${encodeURIComponent(itemKey)}/view`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((payload: { views?: number; likes?: number } | null) => {
|
||||
if (!alive || payload === null) return
|
||||
if (typeof payload.views === 'number') setViews(payload.views)
|
||||
if (typeof payload.likes === 'number') setLikes(payload.likes)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
|
||||
return () => {
|
||||
alive = false
|
||||
}
|
||||
}, [itemKey])
|
||||
|
||||
const toggleLike = () => {
|
||||
const next = !liked
|
||||
setLiked(next)
|
||||
setLikes((current) => Math.max(0, current + (next ? 1 : -1)))
|
||||
|
||||
try {
|
||||
window.localStorage.setItem(`stash-liked:${itemKey}`, next ? '1' : '0')
|
||||
} catch {
|
||||
// 隐私模式下 localStorage 不可用,忽略即可
|
||||
}
|
||||
|
||||
fetch(`/api/items/${encodeURIComponent(itemKey)}/like`, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ liked: next }),
|
||||
})
|
||||
.then((response) => (response.ok ? response.json() : null))
|
||||
.then((payload: { views?: number; likes?: number } | null) => {
|
||||
if (payload === null) return
|
||||
if (typeof payload.views === 'number') setViews(payload.views)
|
||||
if (typeof payload.likes === 'number') setLikes(payload.likes)
|
||||
})
|
||||
.catch(() => undefined)
|
||||
}
|
||||
|
||||
const share = () => {
|
||||
const url = window.location.href
|
||||
const clipboard = navigator.clipboard
|
||||
if (clipboard === undefined) {
|
||||
toast.error('当前浏览器不支持复制,请手动复制地址栏')
|
||||
return
|
||||
}
|
||||
void clipboard
|
||||
.writeText(url)
|
||||
.then(() => toast.success('链接已复制'))
|
||||
.catch(() => toast.error('复制失败,请手动复制地址栏'))
|
||||
}
|
||||
return (
|
||||
<div className="detail-view item-view">
|
||||
<Card padding="none" className="detail-nav">
|
||||
@@ -98,19 +171,35 @@ export function ItemDetailView({
|
||||
|
||||
<div className="item-actions">
|
||||
<Space gap="sm">
|
||||
<Space gap="xs" className="item-stat">
|
||||
<button
|
||||
type="button"
|
||||
className="item-stat"
|
||||
title="浏览量"
|
||||
onClick={() => toast.success(`浏览量 ${views}`)}
|
||||
>
|
||||
<Icon name="eye_line" />
|
||||
<Text size="xs" tone="sub">
|
||||
{model.views}
|
||||
{views}
|
||||
</Text>
|
||||
</Space>
|
||||
<Space gap="xs" className="item-stat">
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`item-stat${liked ? ' is-liked' : ''}`}
|
||||
title={liked ? '取消点赞' : '点赞'}
|
||||
aria-pressed={liked}
|
||||
onClick={toggleLike}
|
||||
>
|
||||
<Icon name="thumb_up_2_line" />
|
||||
<Text size="xs" tone="sub">
|
||||
{model.likes}
|
||||
{likes}
|
||||
</Text>
|
||||
</Space>
|
||||
<IconButton label="分享(待接入)" size="sm" icon={<Icon name="share_3_line" />} />
|
||||
</button>
|
||||
<IconButton
|
||||
label="分享链接"
|
||||
size="sm"
|
||||
icon={<Icon name="share_3_line" />}
|
||||
onClick={share}
|
||||
/>
|
||||
</Space>
|
||||
|
||||
{/* 占位数据没有真实地址时,按钮置灰而不是打开空白页 */}
|
||||
|
||||
Reference in New Issue
Block a user