feat: add four new student modules (rotation, skill-video, exam-prep, academic) with backend APIs and frontend pages; add exam-prep question history with DB persistence
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
BookOpenText,
|
||||
MessageSquareText,
|
||||
Send,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
|
||||
import { RawDetails } from "@/components/display";
|
||||
import {
|
||||
EmptyState,
|
||||
ErrorBanner,
|
||||
Loading,
|
||||
PageHeading,
|
||||
} from "@/components/feedback";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Select } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { UsageGuide } from "@/components/usage-guide";
|
||||
import { ApiError } from "@/lib/api";
|
||||
import { academicApi } from "@/lib/services";
|
||||
|
||||
const TYPES = [
|
||||
{ value: "journal-club", label: "文献汇报" },
|
||||
{ value: "case-discussion", label: "病例讨论" },
|
||||
{ value: "seminar", label: "专题讲座" },
|
||||
{ value: "rounds", label: "教学查房" },
|
||||
];
|
||||
|
||||
export default function AcademicPage() {
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const [type, setType] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [mentorFeedback, setMentorFeedback] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
|
||||
async function loadRecords() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await academicApi.listRecords();
|
||||
setRecords(Array.isArray(res) ? res : []);
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "加载失败");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
try {
|
||||
await academicApi.addRecord({
|
||||
type: type || undefined,
|
||||
title,
|
||||
content,
|
||||
mentorFeedback: mentorFeedback || undefined,
|
||||
});
|
||||
setType("");
|
||||
setTitle("");
|
||||
setContent("");
|
||||
setMentorFeedback("");
|
||||
await loadRecords();
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "提交失败");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeading
|
||||
icon={<Users className="size-5" />}
|
||||
title="学术交流"
|
||||
description="文献汇报、病例讨论记录与导师反馈,沉淀学术交流轨迹。"
|
||||
/>
|
||||
|
||||
<UsageGuide
|
||||
steps={[
|
||||
{ title: "选择交流类型", detail: "文献汇报、病例讨论、专题讲座或教学查房。" },
|
||||
{ title: "填写主题与内容", detail: "记录汇报主题、核心观点与个人收获。" },
|
||||
{ title: "记录导师反馈", detail: "将导师的点评与建议同步记录,便于后续回顾。" },
|
||||
{ title: "查看交流轨迹", detail: "按时间线查看所有学术交流记录与成长脉络。" },
|
||||
]}
|
||||
tip="高质量的学术交流记录是胜任力画像中「学术与科研」维度的重要数据来源,建议每次活动后 24 小时内完成记录。"
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Send className="size-4" />
|
||||
新增交流记录
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<div>
|
||||
<Label>交流类型</Label>
|
||||
<Select value={type} onChange={(e) => setType(e.target.value)} required>
|
||||
<option value="">请选择</option>
|
||||
{TYPES.map((t) => (
|
||||
<option key={t.value} value={t.value}>
|
||||
{t.label}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>主题</Label>
|
||||
<Input
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="如:糖尿病肾病最新诊疗进展"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Label>内容摘要</Label>
|
||||
<Textarea
|
||||
value={content}
|
||||
onChange={(e) => setContent(e.target.value)}
|
||||
placeholder="记录汇报内容、核心观点、个人收获…"
|
||||
rows={4}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="sm:col-span-2">
|
||||
<Label>导师反馈(可选)</Label>
|
||||
<Textarea
|
||||
value={mentorFeedback}
|
||||
onChange={(e) => setMentorFeedback(e.target.value)}
|
||||
placeholder="导师点评、建议与改进方向…"
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 sm:col-span-2">
|
||||
<Button type="submit" disabled={submitting}>
|
||||
{submitting ? "提交中…" : "提交记录"}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={loadRecords} disabled={loading}>
|
||||
{loading ? "加载中…" : "刷新记录"}
|
||||
</Button>
|
||||
</div>
|
||||
{error && (
|
||||
<div className="sm:col-span-2">
|
||||
<ErrorBanner message={error} />
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<BookOpenText className="size-4" />
|
||||
交流记录
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loading ? (
|
||||
<Loading />
|
||||
) : records.length === 0 ? (
|
||||
<EmptyState message="暂无学术交流记录,请添加第一条记录。" />
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{records.map((r: any, i: number) => (
|
||||
<div
|
||||
key={r.id ?? i}
|
||||
className="rounded-lg border border-border p-4"
|
||||
>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline">
|
||||
{TYPES.find((t) => t.value === r.type)?.label ?? r.type}
|
||||
</Badge>
|
||||
<span className="text-sm font-semibold text-foreground">{r.title}</span>
|
||||
</div>
|
||||
{r.date && (
|
||||
<span className="text-xs text-muted-foreground">{r.date}</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm leading-relaxed text-foreground/80">{r.content}</p>
|
||||
{r.mentorFeedback && (
|
||||
<div className="mt-3 rounded-lg bg-muted/50 p-3">
|
||||
<p className="mb-1 flex items-center gap-1 text-xs font-medium text-muted-foreground">
|
||||
<MessageSquareText className="size-3" />
|
||||
导师反馈
|
||||
</p>
|
||||
<p className="text-sm text-foreground/80">{r.mentorFeedback}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user