Files
gig-admin/src/routes/_authed/studio/series.tsx
T
kongkxandClaude Fable 5 46056ae4b0 feat(auth): wire permission gating through menu, routes and actions
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]>
2026-07-12 06:42:31 +08:00

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>
)
}