refactor: 对接 duoqi-api 文档规范

- API 路径前缀改为 /v1/admin
- 分类管理改用服务端分页(page/limit),移除未定义的 search/status 筛选
- 知识卡字段重命名:basic→summary、deep→deepDive
- 各页面移除不必要的 limit 参数
This commit is contained in:
Wang Zhuoxuan 2026-04-11 15:10:44 +08:00
parent b6dc6848af
commit 2c2fc952f9
8 changed files with 34 additions and 85 deletions

View File

@ -83,7 +83,7 @@ export function QuestionForm({ question }: QuestionFormProps) {
}) })
useEffect(() => { useEffect(() => {
fetchCategories({ limit: 100 }).then((res) => setCategories(res.data)) fetchCategories({}).then((res) => setCategories(res.data))
}, []) }, [])
async function onSubmit(data: FormValues) { async function onSubmit(data: FormValues) {

View File

@ -4,7 +4,7 @@ import { getStoredToken, removeStoredToken } from "./auth"
export const apiClient = ky.create({ export const apiClient = ky.create({
baseUrl: API_BASE_URL, baseUrl: API_BASE_URL,
prefix: "/admin", prefix: "/v1/admin",
hooks: { hooks: {
beforeRequest: [ beforeRequest: [
({ request }) => { ({ request }) => {

View File

@ -1,12 +1,10 @@
import { apiClient } from "@/lib/api-client" import { apiClient } from "@/lib/api-client"
import type { PaginatedResponse, ApiResponse } from "@/types/api" import type { ApiResponse, PaginatedResponse } from "@/types/api"
import type { Category, CategoryFormData, CategoryStatus } from "@/types/category" import type { Category, CategoryFormData } from "@/types/category"
export interface FetchCategoriesParams { export interface FetchCategoriesParams {
page?: number page?: number
limit?: number limit?: number
search?: string
status?: CategoryStatus
} }
export async function fetchCategories( export async function fetchCategories(
@ -15,8 +13,6 @@ export async function fetchCategories(
const searchParams = new URLSearchParams() const searchParams = new URLSearchParams()
if (params.page) searchParams.set("page", String(params.page)) if (params.page) searchParams.set("page", String(params.page))
if (params.limit) searchParams.set("limit", String(params.limit)) if (params.limit) searchParams.set("limit", String(params.limit))
if (params.search) searchParams.set("search", params.search)
if (params.status) searchParams.set("status", params.status)
return apiClient return apiClient
.get("categories", { searchParams }) .get("categories", { searchParams })

View File

@ -6,8 +6,8 @@ export interface KnowledgeCardItem {
questionId: string questionId: string
questionStem: string questionStem: string
categoryId: string categoryId: string
basic: string summary: string
deep?: string deepDive?: string
sourceRef?: string sourceRef?: string
updatedAt: string updatedAt: string
} }
@ -34,8 +34,8 @@ export async function fetchKnowledgeCards(
} }
export interface UpdateKnowledgeCardData { export interface UpdateKnowledgeCardData {
basic: string summary: string
deep?: string deepDive?: string
sourceRef?: string sourceRef?: string
} }

View File

@ -4,16 +4,8 @@ import {
getCoreRowModel, getCoreRowModel,
flexRender, flexRender,
} from "@tanstack/react-table" } from "@tanstack/react-table"
import { Plus, Search } from "lucide-react" import { Plus } from "lucide-react"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { import {
Table, Table,
TableBody, TableBody,
@ -31,8 +23,7 @@ import {
updateCategory, updateCategory,
deleteCategory, deleteCategory,
} from "@/lib/api/category-api" } from "@/lib/api/category-api"
import { CATEGORY_STATUS_LABELS } from "@/lib/constants" import type { Category, CategoryFormData } from "@/types/category"
import type { Category, CategoryFormData, CategoryStatus } from "@/types/category"
const PAGE_SIZE = 20 const PAGE_SIZE = 20
@ -41,8 +32,6 @@ export default function CategoriesPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0) const [total, setTotal] = useState(0)
const [page, setPage] = useState(1) const [page, setPage] = useState(1)
const [search, setSearch] = useState("")
const [statusFilter, setStatusFilter] = useState<CategoryStatus | "all">("all")
// 对话框状态 // 对话框状态
const [formOpen, setFormOpen] = useState(false) const [formOpen, setFormOpen] = useState(false)
@ -58,17 +47,16 @@ export default function CategoriesPage() {
const res = await fetchCategories({ const res = await fetchCategories({
page, page,
limit: PAGE_SIZE, limit: PAGE_SIZE,
search: search || undefined,
status: statusFilter !== "all" ? statusFilter : undefined,
}) })
setCategories(res.data) setCategories(res.data)
setTotal(res.pagination.total) setTotal(res.pagination.total)
} catch { } catch {
setCategories([]) setCategories([])
setTotal(0)
} finally { } finally {
setLoading(false) setLoading(false)
} }
}, [page, search, statusFilter]) }, [page])
useEffect(() => { useEffect(() => {
loadCategories() loadCategories()
@ -133,41 +121,6 @@ export default function CategoriesPage() {
</Button> </Button>
</div> </div>
{/* 筛选栏 */}
<div className="flex items-center gap-3">
<div className="relative max-w-xs flex-1">
<Search className="absolute left-2.5 top-2.5 size-4 text-muted-foreground" />
<Input
placeholder="搜索分类名称..."
className="pl-9"
value={search}
onChange={(e) => {
setSearch(e.target.value)
setPage(1)
}}
/>
</div>
<Select
value={statusFilter}
onValueChange={(val: CategoryStatus | "all") => {
setStatusFilter(val)
setPage(1)
}}
>
<SelectTrigger className="w-28">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
{Object.entries(CATEGORY_STATUS_LABELS).map(([value, label]) => (
<SelectItem key={value} value={value}>
{label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 表格 */} {/* 表格 */}
<div className="rounded-lg border"> <div className="rounded-lg border">
<Table> <Table>

View File

@ -45,7 +45,7 @@ const DEEP_MAX = 300
type CardStatus = "all" | "complete" | "incomplete" type CardStatus = "all" | "complete" | "incomplete"
function getCardStatus(item: KnowledgeCardItem): "complete" | "incomplete" { function getCardStatus(item: KnowledgeCardItem): "complete" | "incomplete" {
return item.basic && item.basic.trim().length > 0 ? "complete" : "incomplete" return item.summary && item.summary.trim().length > 0 ? "complete" : "incomplete"
} }
export default function KnowledgeCardsPage() { export default function KnowledgeCardsPage() {
@ -58,7 +58,7 @@ export default function KnowledgeCardsPage() {
// 编辑对话框 // 编辑对话框
const [editOpen, setEditOpen] = useState(false) const [editOpen, setEditOpen] = useState(false)
const [editingCard, setEditingCard] = useState<KnowledgeCardItem | null>(null) const [editingCard, setEditingCard] = useState<KnowledgeCardItem | null>(null)
const [editForm, setEditForm] = useState<UpdateKnowledgeCardData>({ basic: "", deep: "", sourceRef: "" }) const [editForm, setEditForm] = useState<UpdateKnowledgeCardData>({ summary: "", deepDive: "", sourceRef: "" })
const [submitting, setSubmitting] = useState(false) const [submitting, setSubmitting] = useState(false)
// 详情对话框 // 详情对话框
@ -66,7 +66,7 @@ export default function KnowledgeCardsPage() {
const [detailCard, setDetailCard] = useState<KnowledgeCardItem | null>(null) const [detailCard, setDetailCard] = useState<KnowledgeCardItem | null>(null)
useEffect(() => { useEffect(() => {
fetchCategories({ limit: 100 }).then((res) => setCategories(res.data)) fetchCategories({}).then((res) => setCategories(res.data))
}, []) }, [])
const loadCards = useCallback(async () => { const loadCards = useCallback(async () => {
@ -95,8 +95,8 @@ export default function KnowledgeCardsPage() {
function openEdit(card: KnowledgeCardItem) { function openEdit(card: KnowledgeCardItem) {
setEditingCard(card) setEditingCard(card)
setEditForm({ setEditForm({
basic: card.basic, summary: card.summary,
deep: card.deep || "", deepDive: card.deepDive || "",
sourceRef: card.sourceRef || "", sourceRef: card.sourceRef || "",
}) })
setEditOpen(true) setEditOpen(true)
@ -145,10 +145,10 @@ export default function KnowledgeCardsPage() {
id: "basicStatus", id: "basicStatus",
header: "基础卡", header: "基础卡",
cell: ({ row }) => { cell: ({ row }) => {
const hasBasic = row.original.basic?.trim().length > 0 const hasBasic = row.original.summary?.trim().length > 0
return ( return (
<Badge variant={hasBasic ? "default" : "outline"}> <Badge variant={hasBasic ? "default" : "outline"}>
{hasBasic ? `${row.original.basic.length}` : "未填写"} {hasBasic ? `${row.original.summary.length}` : "未填写"}
</Badge> </Badge>
) )
}, },
@ -157,10 +157,10 @@ export default function KnowledgeCardsPage() {
id: "deepStatus", id: "deepStatus",
header: "深度卡", header: "深度卡",
cell: ({ row }) => { cell: ({ row }) => {
const hasDeep = (row.original.deep?.trim().length ?? 0) > 0 const hasDeep = (row.original.deepDive?.trim().length ?? 0) > 0
return ( return (
<Badge variant={hasDeep ? "secondary" : "outline"}> <Badge variant={hasDeep ? "secondary" : "outline"}>
{hasDeep ? `${row.original.deep!.length}` : "未填写"} {hasDeep ? `${row.original.deepDive!.length}` : "未填写"}
</Badge> </Badge>
) )
}, },
@ -312,17 +312,17 @@ export default function KnowledgeCardsPage() {
<Badge variant="secondary" className="text-xs"></Badge> <Badge variant="secondary" className="text-xs"></Badge>
</div> </div>
<p className="text-sm leading-relaxed bg-muted/30 rounded-md p-3"> <p className="text-sm leading-relaxed bg-muted/30 rounded-md p-3">
{detailCard.basic || <span className="italic text-muted-foreground"></span>} {detailCard.summary || <span className="italic text-muted-foreground"></span>}
</p> </p>
</div> </div>
{detailCard.deep && ( {detailCard.deepDive && (
<div> <div>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<Label></Label> <Label></Label>
<Badge variant="outline" className="text-xs">Pro </Badge> <Badge variant="outline" className="text-xs">Pro </Badge>
</div> </div>
<p className="text-sm leading-relaxed bg-muted/30 rounded-md p-3 text-muted-foreground"> <p className="text-sm leading-relaxed bg-muted/30 rounded-md p-3 text-muted-foreground">
{detailCard.deep} {detailCard.deepDive}
</p> </p>
</div> </div>
)} )}
@ -364,14 +364,14 @@ export default function KnowledgeCardsPage() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Label htmlFor="edit-basic"></Label> <Label htmlFor="edit-basic"></Label>
<span className={`text-xs ${editForm.basic.length > BASIC_MAX ? "text-destructive font-medium" : "text-muted-foreground"}`}> <span className={`text-xs ${editForm.summary.length > BASIC_MAX ? "text-destructive font-medium" : "text-muted-foreground"}`}>
{editForm.basic.length}/{BASIC_MAX} {editForm.summary.length}/{BASIC_MAX}
</span> </span>
</div> </div>
<Textarea <Textarea
id="edit-basic" id="edit-basic"
value={editForm.basic} value={editForm.summary}
onChange={(e) => setEditForm({ ...editForm, basic: e.target.value })} onChange={(e) => setEditForm({ ...editForm, summary: e.target.value })}
rows={3} rows={3}
placeholder="2-3 句趣味解读..." placeholder="2-3 句趣味解读..."
/> />
@ -380,14 +380,14 @@ export default function KnowledgeCardsPage() {
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<Label htmlFor="edit-deep">Pro</Label> <Label htmlFor="edit-deep">Pro</Label>
<span className={`text-xs ${editForm.deep && editForm.deep.length > DEEP_MAX ? "text-destructive font-medium" : "text-muted-foreground"}`}> <span className={`text-xs ${editForm.deepDive && editForm.deepDive.length > DEEP_MAX ? "text-destructive font-medium" : "text-muted-foreground"}`}>
{(editForm.deep?.length ?? 0)}/{DEEP_MAX} {(editForm.deepDive?.length ?? 0)}/{DEEP_MAX}
</span> </span>
</div> </div>
<Textarea <Textarea
id="edit-deep" id="edit-deep"
value={editForm.deep ?? ""} value={editForm.deepDive ?? ""}
onChange={(e) => setEditForm({ ...editForm, deep: e.target.value })} onChange={(e) => setEditForm({ ...editForm, deepDive: e.target.value })}
rows={5} rows={5}
placeholder="扩展背景故事、趣味延伸..." placeholder="扩展背景故事、趣味延伸..."
/> />

View File

@ -113,7 +113,7 @@ export default function QuestionsPage() {
// 加载分类列表(用于筛选和列显示) // 加载分类列表(用于筛选和列显示)
useEffect(() => { useEffect(() => {
fetchCategories({ limit: 100 }).then((res) => setCategories(res.data)) fetchCategories({}).then((res) => setCategories(res.data))
}, []) }, [])
const loadQuestions = useCallback(async () => { const loadQuestions = useCallback(async () => {

View File

@ -55,7 +55,7 @@ export default function SkillTreePage() {
// 加载分类列表 // 加载分类列表
useEffect(() => { useEffect(() => {
fetchCategories({ limit: 100 }).then((res) => setCategories(res.data)) fetchCategories({}).then((res) => setCategories(res.data))
}, []) }, [])
const loadChapters = useCallback(async () => { const loadChapters = useCallback(async () => {