duoqi-admin/src/routes/categories/index.tsx
Wang Zhuoxuan 2c2fc952f9 refactor: 对接 duoqi-api 文档规范
- API 路径前缀改为 /v1/admin
- 分类管理改用服务端分页(page/limit),移除未定义的 search/status 筛选
- 知识卡字段重命名:basic→summary、deep→deepDive
- 各页面移除不必要的 limit 参数
2026-04-11 15:10:44 +08:00

224 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useCallback, useEffect, useState } from "react"
import {
useReactTable,
getCoreRowModel,
flexRender,
} from "@tanstack/react-table"
import { Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import { CategoryFormDialog } from "@/components/category/CategoryFormDialog"
import { DeleteCategoryDialog } from "@/components/category/DeleteCategoryDialog"
import { getColumns } from "@/components/category/columns"
import {
fetchCategories,
createCategory,
updateCategory,
deleteCategory,
} from "@/lib/api/category-api"
import type { Category, CategoryFormData } from "@/types/category"
const PAGE_SIZE = 20
export default function CategoriesPage() {
const [categories, setCategories] = useState<Category[]>([])
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
// 对话框状态
const [formOpen, setFormOpen] = useState(false)
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
const [deleteOpen, setDeleteOpen] = useState(false)
const [deletingCategory, setDeletingCategory] = useState<Category | null>(null)
const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE))
const loadCategories = useCallback(async () => {
setLoading(true)
try {
const res = await fetchCategories({
page,
limit: PAGE_SIZE,
})
setCategories(res.data)
setTotal(res.pagination.total)
} catch {
setCategories([])
setTotal(0)
} finally {
setLoading(false)
}
}, [page])
useEffect(() => {
loadCategories()
}, [loadCategories])
// 新建 / 编辑提交
async function handleFormSubmit(data: CategoryFormData) {
if (editingCategory) {
await updateCategory(editingCategory.id, data)
} else {
await createCategory(data)
}
await loadCategories()
}
// 删除提交
async function handleDelete() {
if (!deletingCategory) return
await deleteCategory(deletingCategory.id)
setDeleteOpen(false)
setDeletingCategory(null)
await loadCategories()
}
// 打开编辑对话框
function openEdit(category: Category) {
setEditingCategory(category)
setFormOpen(true)
}
// 打开新建对话框
function openCreate() {
setEditingCategory(null)
setFormOpen(true)
}
// 打开删除对话框
function openDelete(category: Category) {
setDeletingCategory(category)
setDeleteOpen(true)
}
const columns = getColumns({
onEdit: openEdit,
onDelete: openDelete,
})
const table = useReactTable({
data: categories,
columns,
getCoreRowModel: getCoreRowModel(),
})
return (
<div className="space-y-6">
{/* 页面头部 */}
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold"></h1>
<Button onClick={openCreate}>
<Plus className="size-4" />
</Button>
</div>
{/* 表格 */}
<div className="rounded-lg border">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-muted-foreground"
>
...
</TableCell>
</TableRow>
) : categories.length === 0 ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center text-muted-foreground"
>
</TableCell>
</TableRow>
) : (
table.getRowModel().rows.map((row) => (
<TableRow key={row.id}>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="flex items-center justify-between text-sm text-muted-foreground">
<span>
{total} {page}/{totalPages}
</span>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
>
</Button>
<Button
variant="outline"
size="sm"
disabled={page >= totalPages}
onClick={() => setPage((p) => p + 1)}
>
</Button>
</div>
</div>
)}
{/* 对话框 */}
<CategoryFormDialog
open={formOpen}
onOpenChange={setFormOpen}
category={editingCategory}
onSubmit={handleFormSubmit}
/>
<DeleteCategoryDialog
open={deleteOpen}
onOpenChange={setDeleteOpen}
category={deletingCategory}
onConfirm={handleDelete}
/>
</div>
)
}