refactor: 对接 duoqi-api 文档规范
- API 路径前缀改为 /v1/admin - 分类管理改用服务端分页(page/limit),移除未定义的 search/status 筛选 - 知识卡字段重命名:basic→summary、deep→deepDive - 各页面移除不必要的 limit 参数
This commit is contained in:
parent
b6dc6848af
commit
2c2fc952f9
@ -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) {
|
||||||
|
|||||||
@ -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 }) => {
|
||||||
|
|||||||
@ -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 })
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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="扩展背景故事、趣味延伸..."
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user