The store's can()/useCan() helpers existed but were unused. Now: - src/auth/permissions.ts PERM catalog mirrors backend keys - every nav item carries a permission; sidebar filter reuses can() - requirePermission() beforeLoad guard on all child routes (direct URLs redirect home when the session lacks the key) - useCan() hides/disables mutation buttons: moderation actions, retranslate, robot-author edit/pause, category + locale CRUD, and the reserved-SID assign path (admin:sid:override hint) - 403 responses toast 无权限执行此操作; can() unit tests added Co-Authored-By: Claude Fable 5 <[email protected]>
81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
import { createFileRoute } from '@tanstack/react-router'
|
|
import { PERM, requirePermission } from '@/auth/permissions'
|
|
import { useState } from 'react'
|
|
import type { ColumnDef } from '@tanstack/react-table'
|
|
import type { StudioSeries } from '@/api/types'
|
|
import { fetchSeries } from '@/api/modules/studio'
|
|
import { useListPage } from '@/lib/list-page'
|
|
import { DetailSheet } from '@/features/studio/detail-sheet'
|
|
import { DataTable } from '@/components/data-table/data-table'
|
|
import { PageHeader } from '@/components/page-header'
|
|
import { StatusPill } from '@/components/status-pill'
|
|
import { Button } from '@/components/ui/button'
|
|
|
|
export const Route = createFileRoute('/_authed/studio/series')({
|
|
beforeLoad: () => requirePermission(PERM.STUDIO_READ),
|
|
component: SeriesPage,
|
|
})
|
|
|
|
function SeriesPage() {
|
|
const [detail, setDetail] = useState<StudioSeries | null>(null)
|
|
const list = useListPage('studio-series', fetchSeries)
|
|
|
|
const columns: ColumnDef<StudioSeries>[] = [
|
|
{
|
|
header: '系列目标',
|
|
cell: ({ row }) => (
|
|
<div className="max-w-96 truncate font-medium">{row.original.goal ?? '(未命名系列)'}</div>
|
|
),
|
|
},
|
|
{
|
|
header: '计划篇数',
|
|
cell: ({ row }) => row.original.target_article_count ?? '—',
|
|
},
|
|
{ header: '状态', cell: ({ row }) => <StatusPill status={row.original.status} /> },
|
|
{
|
|
header: '更新时间',
|
|
cell: ({ row }) => new Date(row.original.updated_at).toLocaleString('zh-CN'),
|
|
},
|
|
{
|
|
header: '操作',
|
|
cell: ({ row }) => (
|
|
<Button size="sm" variant="ghost" onClick={() => setDetail(row.original)}>
|
|
详情
|
|
</Button>
|
|
),
|
|
},
|
|
]
|
|
|
|
return (
|
|
<div>
|
|
<PageHeader title="文章系列" description="机器人作者的系列规划(目标 / 读者旅程 / 计划列表)" />
|
|
<DataTable
|
|
columns={columns}
|
|
data={list.rows}
|
|
pagination={list.pagination}
|
|
loading={list.loading}
|
|
onPageChange={list.setPage}
|
|
onPageSizeChange={list.setPageSize}
|
|
/>
|
|
<DetailSheet
|
|
open={detail !== null}
|
|
onOpenChange={(open) => !open && setDetail(null)}
|
|
title={detail?.goal ?? '系列详情'}
|
|
fields={[
|
|
{ label: 'ID', value: <code className="font-mono text-xs">{detail?.id}</code> },
|
|
{ label: '状态', value: detail && <StatusPill status={detail.status} /> },
|
|
{ label: '计划篇数', value: detail?.target_article_count },
|
|
{ label: '内容定位', value: detail?.positioning },
|
|
{ label: '目标读者', value: detail?.target_readers },
|
|
{ label: '读者旅程', value: detail?.reader_journey },
|
|
{
|
|
label: '作者',
|
|
value: <code className="font-mono text-xs">{detail?.robot_author_id}</code>,
|
|
},
|
|
]}
|
|
jsonBlocks={[{ label: '计划文章列表', value: detail?.planned_articles }]}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|