Files
CollegeAIcenter/web/src/app/student/academic/page.tsx
T

215 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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>
);
}