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(() => {
fetchCategories({ limit: 100 }).then((res) => setCategories(res.data))
fetchCategories({}).then((res) => setCategories(res.data))
}, [])
async function onSubmit(data: FormValues) {

View File

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

View File

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

View File

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

View File

@ -4,16 +4,8 @@ import {
getCoreRowModel,
flexRender,
} from "@tanstack/react-table"
import { Plus, Search } from "lucide-react"
import { Plus } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
@ -31,8 +23,7 @@ import {
updateCategory,
deleteCategory,
} from "@/lib/api/category-api"
import { CATEGORY_STATUS_LABELS } from "@/lib/constants"
import type { Category, CategoryFormData, CategoryStatus } from "@/types/category"
import type { Category, CategoryFormData } from "@/types/category"
const PAGE_SIZE = 20
@ -41,8 +32,6 @@ export default function CategoriesPage() {
const [loading, setLoading] = useState(true)
const [total, setTotal] = useState(0)
const [page, setPage] = useState(1)
const [search, setSearch] = useState("")
const [statusFilter, setStatusFilter] = useState<CategoryStatus | "all">("all")
// 对话框状态
const [formOpen, setFormOpen] = useState(false)
@ -58,17 +47,16 @@ export default function CategoriesPage() {
const res = await fetchCategories({
page,
limit: PAGE_SIZE,
search: search || undefined,
status: statusFilter !== "all" ? statusFilter : undefined,
})
setCategories(res.data)
setTotal(res.pagination.total)
} catch {
setCategories([])
setTotal(0)
} finally {
setLoading(false)
}
}, [page, search, statusFilter])
}, [page])
useEffect(() => {
loadCategories()
@ -133,41 +121,6 @@ export default function CategoriesPage() {
</Button>
</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">
<Table>

View File

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

View File

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

View File

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