chore: 初始化项目结构并添加基础配置文件

添加后端API和前端项目的初始目录结构,包括应用层模块、工具模块和模型定义
配置前后端的.dockerignore文件以排除开发环境文件
添加前端样式文件,包括字体、Tailwind CSS和主题配置
初始化Python虚拟环境中的类型标记文件
This commit is contained in:
2026-04-01 16:55:24 +08:00
commit cfdeeb5e41
4149 changed files with 837587 additions and 0 deletions

View 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>
);
}