- Question.stem 从 string 改为 { text: string } 对象,匹配 API 多语言题干格式
- 新增 contentType 字段(text/image/video/audio)
- knowledgeCard 从扁平字段重组为嵌套对象 { summary, deepDive?, sourceRef? }
- source/stats 改为可选字段,UI 添加空值回退
- 查询参数对齐:search→keyword, sort→sortBy, order→sortOrder
- QuestionForm 接入 createQuestion/updateQuestion API
- KnowledgeCardFields 组件重命名 props 匹配新结构
179 lines
5.9 KiB
TypeScript
179 lines
5.9 KiB
TypeScript
import { useState } from "react"
|
||
import type { UseFormRegisterReturn } from "react-hook-form"
|
||
import { Button } from "@/components/ui/button"
|
||
import { Input } from "@/components/ui/input"
|
||
import { Label } from "@/components/ui/label"
|
||
import { Textarea } from "@/components/ui/textarea"
|
||
import {
|
||
Card,
|
||
CardContent,
|
||
CardDescription,
|
||
CardHeader,
|
||
CardTitle,
|
||
} from "@/components/ui/card"
|
||
import { Badge } from "@/components/ui/badge"
|
||
|
||
const SUMMARY_MAX = 2000
|
||
const DEEP_DIVE_MAX = 300
|
||
|
||
interface KnowledgeCardFieldsProps {
|
||
summaryRegister: UseFormRegisterReturn
|
||
deepDiveRegister: UseFormRegisterReturn
|
||
sourceRefRegister: UseFormRegisterReturn
|
||
summaryError?: string
|
||
deepDiveError?: string
|
||
watchSummary: string
|
||
watchDeepDive: string
|
||
}
|
||
|
||
export function KnowledgeCardFields({
|
||
summaryRegister,
|
||
deepDiveRegister,
|
||
sourceRefRegister,
|
||
summaryError,
|
||
deepDiveError,
|
||
watchSummary,
|
||
watchDeepDive,
|
||
}: KnowledgeCardFieldsProps) {
|
||
const [showPreview, setShowPreview] = useState(false)
|
||
const [deepExpanded, setDeepExpanded] = useState(!!watchDeepDive)
|
||
|
||
const summaryCount = watchSummary.length
|
||
const deepCount = watchDeepDive.length
|
||
const summaryOver = summaryCount > SUMMARY_MAX
|
||
const deepOver = deepCount > DEEP_DIVE_MAX
|
||
|
||
return (
|
||
<div className="space-y-4">
|
||
{/* 摘要 */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<Label htmlFor="cardSummary">知识卡摘要</Label>
|
||
<Badge variant="secondary" className="text-xs">
|
||
所有用户可见
|
||
</Badge>
|
||
</div>
|
||
<p className="text-sm text-muted-foreground">
|
||
2-3 句趣味解读,让用户答完题后学到新知识
|
||
</p>
|
||
<Textarea
|
||
id="cardSummary"
|
||
placeholder="例:唐太宗李世民不仅是千古一帝,还是书法发烧友。他最爱王羲之的字,据说《兰亭集序》真迹被他带进了昭陵。"
|
||
rows={3}
|
||
{...summaryRegister}
|
||
/>
|
||
<div className="flex items-center justify-between">
|
||
{summaryError && (
|
||
<p className="text-sm text-destructive">{summaryError}</p>
|
||
)}
|
||
<span
|
||
className={`ml-auto text-xs ${summaryOver ? "text-destructive font-medium" : "text-muted-foreground"}`}
|
||
>
|
||
{summaryCount}/{SUMMARY_MAX}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 深度解析 */}
|
||
<div className="space-y-2">
|
||
<button
|
||
type="button"
|
||
className="flex items-center gap-2"
|
||
onClick={() => setDeepExpanded((prev) => !prev)}
|
||
>
|
||
<Label className="cursor-pointer">深度解析</Label>
|
||
<Badge variant="outline" className="text-xs">
|
||
Pro 用户可见
|
||
</Badge>
|
||
<span className="text-xs text-muted-foreground">
|
||
{deepExpanded ? "收起" : "展开"}
|
||
</span>
|
||
</button>
|
||
|
||
{deepExpanded && (
|
||
<>
|
||
<p className="text-sm text-muted-foreground">
|
||
扩展背景故事、趣味延伸,给 Pro 用户更深层的内容体验
|
||
</p>
|
||
<Textarea
|
||
placeholder="例:王羲之写《兰亭集序》时喝了点酒,一气呵成。后来他多次重写都不满意,感叹「此神助耳,何吾能力致」。唐太宗派萧翼用计从辩才和尚手中骗得真迹,临终遗命将真迹陪葬昭陵。不过近年有学者认为,真迹可能并未入昭陵,而是另有下落……"
|
||
rows={5}
|
||
{...deepDiveRegister}
|
||
/>
|
||
<div className="flex items-center justify-between">
|
||
{deepDiveError && (
|
||
<p className="text-sm text-destructive">{deepDiveError}</p>
|
||
)}
|
||
<span
|
||
className={`ml-auto text-xs ${deepOver ? "text-destructive font-medium" : "text-muted-foreground"}`}
|
||
>
|
||
{deepCount}/{DEEP_DIVE_MAX}
|
||
</span>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
|
||
{/* 来源参考 */}
|
||
<div className="space-y-2">
|
||
<Label htmlFor="cardSourceRef">
|
||
来源参考
|
||
<span className="ml-1 text-muted-foreground font-normal">选填</span>
|
||
</Label>
|
||
<Input
|
||
id="cardSourceRef"
|
||
placeholder="如:《旧唐书·太宗本纪》"
|
||
{...sourceRefRegister}
|
||
/>
|
||
</div>
|
||
|
||
{/* 预览切换 */}
|
||
<Button
|
||
type="button"
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={() => setShowPreview((prev) => !prev)}
|
||
>
|
||
{showPreview ? "关闭预览" : "预览效果"}
|
||
</Button>
|
||
|
||
{showPreview && (
|
||
<Card className="border-dashed">
|
||
<CardHeader>
|
||
<CardTitle className="text-base">知识卡预览</CardTitle>
|
||
<CardDescription>
|
||
用户答完题后看到的知识卡效果
|
||
</CardDescription>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{watchSummary ? (
|
||
<p className="text-sm leading-relaxed">{watchSummary}</p>
|
||
) : (
|
||
<p className="text-sm text-muted-foreground italic">
|
||
(摘要内容为空)
|
||
</p>
|
||
)}
|
||
{watchDeepDive && (
|
||
<>
|
||
<div className="border-t pt-3">
|
||
<div className="flex items-center gap-1 mb-1">
|
||
<Badge variant="outline" className="text-xs">
|
||
Pro
|
||
</Badge>
|
||
<span className="text-xs text-muted-foreground">
|
||
深度解读
|
||
</span>
|
||
</div>
|
||
<p className="text-sm leading-relaxed text-muted-foreground">
|
||
{watchDeepDive}
|
||
</p>
|
||
</div>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|