chore: 初始化项目结构并添加基础配置文件
添加后端API和前端项目的初始目录结构,包括应用层模块、工具模块和模型定义 配置前后端的.dockerignore文件以排除开发环境文件 添加前端样式文件,包括字体、Tailwind CSS和主题配置 初始化Python虚拟环境中的类型标记文件
This commit is contained in:
284
前端-孕期报告解读-figma/src/app/pages/DoctorQuestionsPage.tsx
Normal file
284
前端-孕期报告解读-figma/src/app/pages/DoctorQuestionsPage.tsx
Normal file
@@ -0,0 +1,284 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { motion } from "motion/react";
|
||||
import {
|
||||
ChevronLeft,
|
||||
Copy,
|
||||
Check,
|
||||
MessageCircle,
|
||||
Clipboard,
|
||||
Share2,
|
||||
Download,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
import { Button } from "../components/ui/button";
|
||||
import { Card } from "../components/ui/card";
|
||||
import { Badge } from "../components/ui/badge";
|
||||
import { getDoctorQuestions } from "../services/api";
|
||||
|
||||
interface Question {
|
||||
id: string;
|
||||
category: string;
|
||||
question: string;
|
||||
reason: string;
|
||||
priority: "high" | "medium" | "low";
|
||||
}
|
||||
|
||||
export function DoctorQuestionsPage() {
|
||||
const navigate = useNavigate();
|
||||
const { reportId } = useParams();
|
||||
const [copiedId, setCopiedId] = useState(null);
|
||||
const [questions, setQuestions] = useState<Question[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!reportId) return;
|
||||
getDoctorQuestions(reportId)
|
||||
.then((data) => {
|
||||
setQuestions(
|
||||
data.questions.map((q, i) => ({
|
||||
id: String(i),
|
||||
category: q.category,
|
||||
question: q.question,
|
||||
reason: q.reason,
|
||||
priority: (i < 2 ? "high" : i < 4 ? "medium" : "low") as Question["priority"],
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch(() => {/* keep empty */})
|
||||
.finally(() => setLoading(false));
|
||||
}, [reportId]);
|
||||
|
||||
const handleCopy = (text: string, id: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopiedId(id);
|
||||
setTimeout(() => setCopiedId(null), 2000);
|
||||
};
|
||||
|
||||
const handleCopyAll = () => {
|
||||
const allQuestions = questions
|
||||
.map((q, index) => `${index + 1}. ${q.question}`)
|
||||
.join("\n\n");
|
||||
navigator.clipboard.writeText(allQuestions);
|
||||
setCopiedId("all");
|
||||
setTimeout(() => setCopiedId(null), 2000);
|
||||
};
|
||||
|
||||
const priorityLabels = {
|
||||
high: { text: "重要", color: "bg-red-100 text-red-700" },
|
||||
medium: { text: "建议", color: "bg-morandi-rose-100 text-morandi-rose-400" },
|
||||
low: { text: "了解", color: "bg-morandi-50 text-morandi-500" },
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-dvh bg-morandi-50 flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<Loader2 className="w-8 h-8 text-morandi-400 animate-spin mx-auto mb-3" />
|
||||
<p className="text-sm text-gray-500">正在生成问诊清单...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-dvh bg-morandi-50">
|
||||
{/* 顶部导航 */}
|
||||
<div className="bg-white border-b border-gray-100 sticky top-0 z-10">
|
||||
<div className="mx-auto px-4 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
className="w-10 h-10 rounded-full hover:bg-gray-100 flex items-center justify-center"
|
||||
>
|
||||
<ChevronLeft className="w-6 h-6 text-gray-600" />
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 bg-morandi-400 rounded-full flex items-center justify-center">
|
||||
<MessageCircle className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="font-semibold text-gray-800">问诊清单</h1>
|
||||
<p className="text-xs text-gray-500">
|
||||
智能生成{questions.length}个问题
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
onClick={handleCopyAll}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full"
|
||||
>
|
||||
{copiedId === "all" ? (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
已复制
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Clipboard className="w-4 h-4 mr-2" />
|
||||
全部复制
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto p-4 space-y-6">
|
||||
{/* 使用说明 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
>
|
||||
<Card className="bg-gradient-to-br from-morandi-400 to-morandi-500 border-0 text-white p-6 rounded-3xl shadow-sm">
|
||||
<h2 className="text-xl font-semibold mb-3">💬 如何使用问诊清单</h2>
|
||||
<div className="space-y-2 text-sm text-white/90">
|
||||
<p>✓ 基于您的报告异常项,AI智能生成针对性问题</p>
|
||||
<p>✓ 点击单个问题可复制,方便向医生提问</p>
|
||||
<p>✓ 可一键复制全部问题,就诊时查看</p>
|
||||
<p>✓ 按优先级排序,重要问题优先询问</p>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* 问题列表 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<h2 className="text-lg font-semibold text-gray-800 mb-4">
|
||||
📋 建议咨询的问题
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{questions.map((question, index) => (
|
||||
<motion.div
|
||||
key={question.id}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
transition={{ delay: 0.2 + index * 0.05 }}
|
||||
>
|
||||
<Card className="bg-white rounded-2xl p-5 shadow-sm border-0 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="flex-shrink-0 w-8 h-8 bg-morandi-100 text-morandi-500 rounded-full flex items-center justify-center font-semibold text-sm">
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Badge
|
||||
className={`${
|
||||
priorityLabels[question.priority].color
|
||||
} hover:bg-current`}
|
||||
>
|
||||
{priorityLabels[question.priority].text}
|
||||
</Badge>
|
||||
<span className="text-xs text-gray-500">
|
||||
{question.category}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p className="text-gray-800 leading-relaxed mb-3 font-medium">
|
||||
{question.question}
|
||||
</p>
|
||||
|
||||
<div className="bg-morandi-slate-50 rounded-xl p-3 mb-3">
|
||||
<p className="text-sm text-morandi-500">
|
||||
💡 <strong>为什么问:</strong>
|
||||
{question.reason}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={() =>
|
||||
handleCopy(question.question, question.id)
|
||||
}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
{copiedId === question.id ? (
|
||||
<>
|
||||
<Check className="w-4 h-4 mr-2" />
|
||||
已复制
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Copy className="w-4 h-4 mr-2" />
|
||||
复制此问题
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
|
||||
{/* 其他建议 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<Card className="bg-white rounded-2xl p-5 shadow-sm border-0">
|
||||
<h3 className="font-semibold text-gray-800 mb-3 flex items-center gap-2">
|
||||
<MessageCircle className="w-5 h-5 text-morandi-400" />
|
||||
💡 就诊小贴士
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-600">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="flex-shrink-0 w-1.5 h-1.5 bg-morandi-400 rounded-full mt-2"></span>
|
||||
<span>
|
||||
问诊时可以拿出手机查看问题清单,避免紧张时忘记要问的内容
|
||||
</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="flex-shrink-0 w-1.5 h-1.5 bg-morandi-400 rounded-full mt-2"></span>
|
||||
<span>听从医生建议,记录重要的用药和注意事项</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="flex-shrink-0 w-1.5 h-1.5 bg-morandi-400 rounded-full mt-2"></span>
|
||||
<span>如医生时间有限,优先询问标记为"重要"的问题</span>
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="flex-shrink-0 w-1.5 h-1.5 bg-morandi-400 rounded-full mt-2"></span>
|
||||
<span>
|
||||
就诊后可返回APP查看AI建议,结合医生意见调整生活方式
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</Card>
|
||||
</motion.div>
|
||||
|
||||
{/* 底部操作 */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ delay: 0.6 }}
|
||||
>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-12"
|
||||
onClick={() => navigate(`/share/${reportId}`)}
|
||||
>
|
||||
<Share2 className="w-4 h-4 mr-2" />
|
||||
分享清单
|
||||
</Button>
|
||||
<Button className="h-12 bg-morandi-400">
|
||||
<Download className="w-4 h-4 mr-2" />
|
||||
导出PDF
|
||||
</Button>
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user