diff --git a/Community_backend/.env b/Community_backend/.env
index 5261a1d9..a22fd621 100644
--- a/Community_backend/.env
+++ b/Community_backend/.env
@@ -1,5 +1,5 @@
PORT=3000
-NODE_ENV=production
+NODE_ENV=development
MONGODB_URI=mongodb+srv://PetDerma:ECNU@cluster0.wdid0qc.mongodb.net/PetDerma?retryWrites=true&w=majority&appName=Cluster0
JWT_SECRET=ab12cd34ef56a7890b1c2d3e4f5061728394a5b6c7d8e9fa0abbccddeeff1122
LOCAL_STORAGE_PATH=./uploads
diff --git a/Community_backend/app.js b/Community_backend/app.js
index 0de8b2e7..f3c1ebd6 100644
--- a/Community_backend/app.js
+++ b/Community_backend/app.js
@@ -1,4 +1,5 @@
const express = require("express");
+const path = require("path");
const cors = require("cors");
const helmet = require("helmet");
const morgan = require("morgan");
@@ -16,6 +17,8 @@ const uploadRoutes = require("./routes/upload");
const imageRoutes = require("./routes/images");
const petRoutes = require("./routes/pets");
const healthRoutes = require("./routes/health");
+const doctorRoutes = require("./routes/doctor");
+const institutionRoutes = require("./routes/institution");
// 导入中间件
const { handleUploadError } = require("./middleware/upload");
@@ -58,7 +61,7 @@ setTimeout(async () => {
/* ------------------------------------------------------------------
✅ 安全、CORS、日志、限流中间件(位置保持不变)
------------------------------------------------------------------- */
-app.use(helmet());
+app.use(helmet({ contentSecurityPolicy: false }));
app.use(
cors({
@@ -143,6 +146,42 @@ app.get("/health", (req, res) => {
});
});
+// B 端预览页面
+app.get("/doctor-preview", (req, res) => {
+ res.sendFile(path.join(__dirname, "..", "doctor-preview.html"));
+});
+app.get("/doctor-preview.css", (req, res) => {
+ res.sendFile(path.join(__dirname, "..", "doctor-preview.css"));
+});
+app.get("/doctor-preview.js", (req, res) => {
+ res.sendFile(path.join(__dirname, "..", "doctor-preview.js"));
+});
+
+// 机构端预览页面
+app.get("/institution-preview", (req, res) => {
+ res.sendFile(path.join(__dirname, "..", "institution-preview.html"));
+});
+
+// 首页预览页面(入口1:机构端入口)
+app.get("/home-preview", (req, res) => {
+ res.sendFile(path.join(__dirname, "..", "home-preview.html"));
+});
+
+// 地图导航页预览(入口2:发送AI报告给机构)
+app.get("/map-preview", (req, res) => {
+ res.sendFile(path.join(__dirname, "..", "map-preview.html"));
+});
+
+// 诊断结果页预览(入口3:预约线下复核)
+app.get("/result-preview", (req, res) => {
+ res.sendFile(path.join(__dirname, "..", "result-preview.html"));
+});
+
+// 主页入口(首页重定向)
+app.get("/", (req, res) => {
+ res.redirect("/home-preview");
+});
+
app.use("/api/auth", authRoutes);
app.use("/api/posts", postRoutes);
app.use("/api/interactions", interactionRoutes);
@@ -150,6 +189,12 @@ app.use("/api/upload", uploadLimiter, uploadRoutes);
app.use("/api/pets", petRoutes);
app.use("/api/health", healthRoutes);
+// 医生端 API
+app.use("/api/doctor", doctorRoutes);
+
+// 机构端 API
+app.use("/api/institution", institutionRoutes);
+
// 图片路由
app.use(
"/api/images",
diff --git a/Community_backend/config/database.js b/Community_backend/config/database.js
index 8719e615..cc16ab55 100644
--- a/Community_backend/config/database.js
+++ b/Community_backend/config/database.js
@@ -1,17 +1,37 @@
const mongoose = require('mongoose');
+let memoryServer = null;
+
// MongoDB连接配置
const connectDB = async () => {
try {
- const conn = await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/PetDerma_Community', {
- useNewUrlParser: true,
- useUnifiedTopology: true,
+ const uri = process.env.MONGODB_URI || 'mongodb://localhost:27017/PetDerma_Community';
+ console.log(`🔌 尝试连接 MongoDB: ${uri.replace(/\/\/.*@/, '//***@')}`);
+
+ const conn = await mongoose.connect(uri, {
+ serverSelectionTimeoutMS: 5000,
});
- console.log(`MongoDB Connected: ${conn.connection.host}`);
+ console.log(`✅ MongoDB 已连接: ${conn.connection.host}`);
+ return conn;
} catch (error) {
- console.error('MongoDB connection error:', error);
- process.exit(1);
+ console.error('❌ 远程 MongoDB 连接失败:', error.message);
+ console.log('🔄 尝试启动本地内存数据库...');
+
+ try {
+ const { MongoMemoryServer } = require('mongodb-memory-server');
+ memoryServer = await MongoMemoryServer.create();
+ const localUri = memoryServer.getUri();
+
+ console.log(`📦 内存数据库已启动: ${localUri}`);
+
+ const conn = await mongoose.connect(localUri);
+ console.log(`✅ 内存 MongoDB 已连接: ${conn.connection.host}`);
+ return conn;
+ } catch (memError) {
+ console.error('❌ 内存数据库启动失败:', memError.message);
+ process.exit(1);
+ }
}
};
@@ -31,6 +51,9 @@ mongoose.connection.on('disconnected', () => {
// 优雅关闭
process.on('SIGINT', async () => {
await mongoose.connection.close();
+ if (memoryServer) {
+ await memoryServer.stop();
+ }
console.log('MongoDB connection closed through app termination');
process.exit(0);
});
diff --git a/Community_backend/controllers/doctorController.js b/Community_backend/controllers/doctorController.js
new file mode 100644
index 00000000..0c3c3d71
--- /dev/null
+++ b/Community_backend/controllers/doctorController.js
@@ -0,0 +1,311 @@
+const HealthRecord = require("../models/HealthRecord");
+
+// 获取授权给当前医生的病例列表
+const getCases = async (req, res) => {
+ try {
+ const doctorId = req.user.userId;
+
+ let cases = await HealthRecord.find({
+ sharedWith: doctorId,
+ status: "active",
+ })
+ .populate("userId", "nickName avatar phoneNumber")
+ .populate("petId", "name avatar species breed")
+ .sort({ createdAt: -1 });
+
+ // 开发/演示模式:如果没有共享记录,显示自己的记录
+ if (cases.length === 0 && process.env.NODE_ENV !== "production") {
+ // 自动将自己的记录加入共享列表(方便演示)
+ const ownRecords = await HealthRecord.find({
+ userId: doctorId,
+ status: "active",
+ })
+ .populate("userId", "nickName avatar phoneNumber")
+ .populate("petId", "name avatar species breed")
+ .sort({ createdAt: -1 });
+
+ if (ownRecords.length > 0) {
+ // 自动共享给自己
+ await HealthRecord.updateMany(
+ { userId: doctorId, status: "active", sharedWith: { $ne: doctorId } },
+ { $addToSet: { sharedWith: doctorId } }
+ );
+ cases = ownRecords;
+ }
+ }
+
+ const total = cases.length;
+ const pendingReview = cases.filter((c) => !c.aiSummary).length;
+ const followUpNeeded = cases.filter(
+ (c) => c.followUp && c.followUp.needed && !c.followUp.completed
+ ).length;
+
+ res.json({
+ success: true,
+ data: {
+ cases,
+ stats: { total, pendingReview, followUpNeeded },
+ },
+ });
+ } catch (error) {
+ console.error("获取病例列表失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 获取单个病例详情
+const getCaseDetail = async (req, res) => {
+ try {
+ const doctorId = req.user.userId;
+ const { id } = req.params;
+
+ const record = await HealthRecord.findOne({
+ _id: id,
+ sharedWith: doctorId,
+ status: "active",
+ })
+ .populate("userId", "nickName avatar phoneNumber")
+ .populate("petId", "name avatar species breed");
+
+ if (!record) {
+ return res.status(404).json({
+ success: false,
+ message: "病例不存在或无访问权限",
+ });
+ }
+
+ res.json({ success: true, data: record });
+ } catch (error) {
+ console.error("获取病例详情失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 生成 AI 病例摘要
+const generateSummary = async (req, res) => {
+ try {
+ const doctorId = req.user.userId;
+ const { id } = req.params;
+
+ const record = await HealthRecord.findOne({
+ _id: id,
+ sharedWith: doctorId,
+ status: "active",
+ })
+ .populate("userId", "nickName")
+ .populate("petId", "name species breed age");
+
+ if (!record) {
+ return res.status(404).json({
+ success: false,
+ message: "病例不存在或无访问权限",
+ });
+ }
+
+ // 自动生成摘要(演示用)
+ const pet = record.petId || {};
+ const owner = record.userId || {};
+ const diseaseHistory = record.skinDiseaseHistory || [];
+ const latestDisease =
+ diseaseHistory.length > 0
+ ? diseaseHistory[diseaseHistory.length - 1]
+ : null;
+
+ const summaryLines = [];
+
+ if (pet.name) {
+ summaryLines.push(`患宠:${pet.name}(${pet.species || "未知"})`);
+ }
+ if (owner.nickName) {
+ summaryLines.push(`宠主:${owner.nickName}`);
+ }
+ if (record.weight) {
+ summaryLines.push(`体重:${record.weight}kg`);
+ }
+ if (latestDisease) {
+ summaryLines.push(
+ `诊断疾病:${latestDisease.diseaseName || "未知"}`
+ );
+ if (latestDisease.symptoms && latestDisease.symptoms.length > 0) {
+ summaryLines.push(
+ `症状表现:${latestDisease.symptoms.join("、")}`
+ );
+ }
+ if (latestDisease.affectedAreas && latestDisease.affectedAreas.length > 0) {
+ summaryLines.push(
+ `患病部位:${latestDisease.affectedAreas.join("、")}`
+ );
+ }
+ }
+ if (record.allergies && record.allergies !== "无") {
+ summaryLines.push(`过敏史:${record.allergies}`);
+ }
+
+ // 计算严重程度趋势
+ const severityLevels = diseaseHistory.map((h) => {
+ const count = (h.symptoms || []).length + (h.affectedAreas || []).length;
+ if (count >= 4) return "重";
+ if (count >= 2) return "中";
+ return "轻";
+ });
+ const currentSeverity = latestDisease
+ ? severityLevels[severityLevels.length - 1] || "轻"
+ : "未知";
+
+ const summary = summaryLines.join(";");
+ const trend =
+ diseaseHistory.length > 1
+ ? `该宠物共有 ${diseaseHistory.length} 次就诊记录,病情发展已追踪。`
+ : "初次就诊,建议持续观察。";
+
+ // 保存生成的摘要
+ record.aiSummary = summary;
+ record.severity = currentSeverity;
+ record.trend = trend;
+ await record.save();
+
+ res.json({
+ success: true,
+ data: {
+ aiSummary: summary,
+ severity: currentSeverity,
+ trend,
+ },
+ });
+ } catch (error) {
+ console.error("生成AI摘要失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 更新复诊提醒
+const updateFollowUp = async (req, res) => {
+ try {
+ const doctorId = req.user.userId;
+ const { id } = req.params;
+ const { needed, date, note } = req.body;
+
+ const record = await HealthRecord.findOne({
+ _id: id,
+ sharedWith: doctorId,
+ status: "active",
+ });
+
+ if (!record) {
+ return res.status(404).json({
+ success: false,
+ message: "病例不存在或无访问权限",
+ });
+ }
+
+ record.followUp = {
+ needed: needed !== undefined ? needed : record.followUp.needed,
+ date: date || record.followUp.date,
+ note: note !== undefined ? note : record.followUp.note,
+ completed: false,
+ };
+
+ await record.save();
+
+ res.json({
+ success: true,
+ message: "复诊提醒已更新",
+ data: { followUp: record.followUp },
+ });
+ } catch (error) {
+ console.error("更新复诊提醒失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 完成复诊
+const completeFollowUp = async (req, res) => {
+ try {
+ const doctorId = req.user.userId;
+ const { id } = req.params;
+
+ const record = await HealthRecord.findOne({
+ _id: id,
+ sharedWith: doctorId,
+ status: "active",
+ });
+
+ if (!record) {
+ return res.status(404).json({
+ success: false,
+ message: "病例不存在或无访问权限",
+ });
+ }
+
+ record.followUp.completed = true;
+ await record.save();
+
+ res.json({
+ success: true,
+ message: "复诊已完成",
+ data: { followUp: record.followUp },
+ });
+ } catch (error) {
+ console.error("完成复诊失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 播种演示数据(将当前用户的记录共享给自己并添加摘要)
+const seedDemoData = async (req, res) => {
+ try {
+ const userId = req.user.userId;
+
+ const records = await HealthRecord.find({ userId, status: "active" });
+ let count = 0;
+
+ for (const record of records) {
+ const diseaseList = (record.skinDiseaseHistory || []).map((d) => d.diseaseName).filter(Boolean);
+ const symptomList = (record.skinDiseaseHistory || []).flatMap((d) => d.symptoms || []);
+ const areaList = (record.skinDiseaseHistory || []).flatMap((d) => d.affectedAreas || []);
+
+ // 共享给自己
+ if (!record.sharedWith.some((id) => id.toString() === userId.toString())) {
+ record.sharedWith.push(userId);
+ }
+
+ // 生成 AI 摘要
+ if (!record.aiSummary) {
+ const parts = [];
+ if (record.petId) parts.push(`患宠 ID:${record.petId}`);
+ if (diseaseList.length) parts.push(`诊断疾病:${diseaseList.join("、")}`);
+ if (symptomList.length) parts.push(`症状表现:${symptomList.join("、")}`);
+ if (areaList.length) parts.push(`患病部位:${areaList.join("、")}`);
+ record.aiSummary = parts.join(";") || "暂无诊断数据,建议尽快就诊。";
+ record.trend =
+ (record.skinDiseaseHistory || []).length > 1
+ ? `该宠物共有 ${record.skinDiseaseHistory.length} 次就诊记录,病情发展已追踪。`
+ : "初次就诊,建议持续观察。";
+ record.severity =
+ symptomList.length + areaList.length >= 4 ? "重" :
+ symptomList.length + areaList.length >= 2 ? "中" : "轻";
+ }
+
+ await record.save();
+ count++;
+ }
+
+ res.json({
+ success: true,
+ message: `演示数据已就绪,共处理 ${count} 条记录`,
+ data: { count },
+ });
+ } catch (error) {
+ console.error("播种演示数据失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+module.exports = {
+ getCases,
+ getCaseDetail,
+ generateSummary,
+ updateFollowUp,
+ completeFollowUp,
+ seedDemoData,
+};
diff --git a/Community_backend/controllers/institutionController.js b/Community_backend/controllers/institutionController.js
new file mode 100644
index 00000000..fd9bdedd
--- /dev/null
+++ b/Community_backend/controllers/institutionController.js
@@ -0,0 +1,357 @@
+const Institution = require("../models/Institution");
+const PreConsultReport = require("../models/PreConsultReport");
+const Appointment = require("../models/Appointment");
+
+// ==================== 机构注册/认证 ====================
+
+// 提交入驻申请
+exports.register = async (req, res) => {
+ try {
+ const { name, type, contact, phone, address, description, licenseUrl } = req.body;
+
+ if (!name || !contact || !phone) {
+ return res.status(400).json({ success: false, message: "请填写必填信息" });
+ }
+
+ const existing = await Institution.findOne({ userId: req.user.userId });
+ if (existing) {
+ return res.status(400).json({ success: false, message: "您已提交过入驻申请" });
+ }
+
+ const typeMap = { hospital: "宠物医院", clinic: "宠物诊所", care: "护理店" };
+
+ const institution = new Institution({
+ userId: req.user.userId,
+ name,
+ type,
+ typeText: typeMap[type] || type,
+ contact,
+ phone,
+ address,
+ description: description || "",
+ licenseUrl: licenseUrl || "",
+ status: "pending",
+ });
+
+ await institution.save();
+
+ res.json({
+ success: true,
+ message: "入驻申请已提交,等待审核",
+ data: institution,
+ });
+ } catch (error) {
+ console.error("机构注册失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 获取机构资料
+exports.getProfile = async (req, res) => {
+ try {
+ const institution = await Institution.findOne({ userId: req.user.userId });
+ if (!institution) {
+ return res.status(404).json({ success: false, message: "机构不存在" });
+ }
+ res.json({ success: true, data: institution });
+ } catch (error) {
+ console.error("获取机构资料失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 更新机构资料
+exports.updateProfile = async (req, res) => {
+ try {
+ const updateData = req.body;
+ const institution = await Institution.findOneAndUpdate(
+ { userId: req.user.userId },
+ { $set: updateData },
+ { new: true }
+ );
+ if (!institution) {
+ return res.status(404).json({ success: false, message: "机构不存在" });
+ }
+ res.json({ success: true, message: "资料已更新", data: institution });
+ } catch (error) {
+ console.error("更新机构资料失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 获取附近机构
+exports.getNearby = async (req, res) => {
+ try {
+ const { lat, lng, type } = req.query;
+
+ let query = { status: "approved" };
+ if (type) {
+ query.type = type;
+ }
+
+ let institutions;
+ if (lat && lng) {
+ institutions = await Institution.aggregate([
+ { $match: query },
+ {
+ $geoNear: {
+ near: { type: "Point", coordinates: [parseFloat(lng), parseFloat(lat)] },
+ distanceField: "distance",
+ spherical: true,
+ maxDistance: 10000,
+ },
+ },
+ { $limit: 20 },
+ ]);
+ } else {
+ institutions = await Institution.find(query).limit(20);
+ }
+
+ res.json({ success: true, data: institutions });
+ } catch (error) {
+ console.error("获取附近机构失败:", error);
+ // 返回空列表而不是报错
+ res.json({ success: true, data: [] });
+ }
+};
+
+// ==================== AI预问诊报告 ====================
+
+// 接收AI预问诊报告
+exports.receiveReport = async (req, res) => {
+ try {
+ const reportData = req.body;
+
+ const report = new PreConsultReport({
+ institutionId: reportData.institutionId || null,
+ userId: req.user.userId,
+ petName: reportData.petName || "",
+ petType: reportData.petType || "",
+ petBreed: reportData.petBreed || "",
+ petAvatar: reportData.petAvatar || "",
+ ownerName: reportData.ownerName || req.user.nickname || "",
+ ownerPhone: reportData.ownerPhone || "",
+ diseaseName: reportData.diseaseName || "",
+ severity: reportData.severity || "待评估",
+ confidence: reportData.confidence || 0,
+ description: reportData.description || "",
+ aiSummary: reportData.aiSummary || "",
+ symptoms: reportData.symptoms || [],
+ affectedAreas: reportData.affectedAreas || [],
+ weight: reportData.weight || "",
+ allergies: reportData.allergies || "",
+ sterilized: reportData.sterilized || "未知",
+ recentContact: reportData.recentContact || "",
+ images: reportData.images || [],
+ needCareAdvice: reportData.needCareAdvice || false,
+ source: reportData.source || "user",
+ status: "pending",
+ });
+
+ await report.save();
+
+ res.json({ success: true, message: "报告已接收", data: report });
+ } catch (error) {
+ console.error("接收报告失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 发送报告到机构(宠主端)
+exports.sendReport = async (req, res) => {
+ try {
+ const { institutionId, institutionName, reportData } = req.body;
+
+ if (!institutionId) {
+ return res.status(400).json({ success: false, message: "请选择机构" });
+ }
+
+ const report = new PreConsultReport({
+ institutionId,
+ userId: req.user.userId,
+ petName: reportData.petName || "",
+ petType: reportData.petType || "",
+ diseaseName: reportData.diseaseName || "",
+ severity: reportData.severity || "待评估",
+ confidence: reportData.confidence || 0,
+ description: reportData.description || "",
+ aiSummary: reportData.aiSummary || "",
+ symptoms: reportData.symptoms || [],
+ affectedAreas: reportData.affectedAreas || [],
+ weight: reportData.weight || "",
+ allergies: reportData.allergies || "",
+ sterilized: reportData.sterilized || "未知",
+ recentContact: reportData.recentContact || "",
+ images: reportData.images || [],
+ needCareAdvice: reportData.needCareAdvice || false,
+ source: "user",
+ status: "pending",
+ });
+
+ await report.save();
+
+ res.json({
+ success: true,
+ message: `报告已发送给 ${institutionName}`,
+ data: report,
+ });
+ } catch (error) {
+ console.error("发送报告失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 获取机构收到的报告列表
+exports.getReports = async (req, res) => {
+ try {
+ const institution = await Institution.findOne({ userId: req.user.userId });
+ if (!institution) {
+ return res.status(404).json({ success: false, message: "机构不存在" });
+ }
+
+ const { status, page = 1, limit = 20 } = req.query;
+ const query = { institutionId: institution._id };
+ if (status) query.status = status;
+
+ const total = await PreConsultReport.countDocuments(query);
+ const reports = await PreConsultReport.find(query)
+ .sort({ createdAt: -1 })
+ .skip((page - 1) * limit)
+ .limit(parseInt(limit));
+
+ const stats = {
+ pending: await PreConsultReport.countDocuments({ institutionId: institution._id, status: "pending" }),
+ read: await PreConsultReport.countDocuments({ institutionId: institution._id, status: "read" }),
+ completed: await PreConsultReport.countDocuments({ institutionId: institution._id, status: "completed" }),
+ total,
+ };
+
+ res.json({ success: true, data: { reports, stats } });
+ } catch (error) {
+ console.error("获取报告列表失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 获取报告详情
+exports.getReportDetail = async (req, res) => {
+ try {
+ const report = await PreConsultReport.findById(req.params.id);
+ if (!report) {
+ return res.status(404).json({ success: false, message: "报告不存在" });
+ }
+ res.json({ success: true, data: report });
+ } catch (error) {
+ console.error("获取报告详情失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 标记报告为已读
+exports.markReportRead = async (req, res) => {
+ try {
+ const report = await PreConsultReport.findByIdAndUpdate(
+ req.params.id,
+ { status: "read" },
+ { new: true }
+ );
+ if (!report) {
+ return res.status(404).json({ success: false, message: "报告不存在" });
+ }
+ res.json({ success: true, message: "已标记为已读", data: report });
+ } catch (error) {
+ console.error("标记报告失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// ==================== 预约管理 ====================
+
+// 创建预约
+exports.createAppointment = async (req, res) => {
+ try {
+ const { institutionId, petName, petType, ownerName, ownerPhone, service, date, time, notes } = req.body;
+
+ if (!institutionId || !petName || !date || !time) {
+ return res.status(400).json({ success: false, message: "请填写完整的预约信息" });
+ }
+
+ const appointment = new Appointment({
+ institutionId,
+ userId: req.user.userId,
+ petName,
+ petType: petType || "",
+ ownerName: ownerName || "",
+ ownerPhone: ownerPhone || "",
+ service: service || "",
+ date,
+ time,
+ notes: notes || "",
+ status: "pending",
+ });
+
+ await appointment.save();
+
+ res.json({ success: true, message: "预约已提交", data: appointment });
+ } catch (error) {
+ console.error("创建预约失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 获取机构预约列表
+exports.getAppointments = async (req, res) => {
+ try {
+ const institution = await Institution.findOne({ userId: req.user.userId });
+ if (!institution) {
+ return res.status(404).json({ success: false, message: "机构不存在" });
+ }
+
+ const { date, status } = req.query;
+ const query = { institutionId: institution._id };
+ if (date) query.date = date;
+ if (status) query.status = status;
+
+ const appointments = await Appointment.find(query)
+ .sort({ time: 1 });
+
+ const stats = {
+ pending: await Appointment.countDocuments({ institutionId: institution._id, status: "pending" }),
+ confirmed: await Appointment.countDocuments({ institutionId: institution._id, status: "confirmed" }),
+ completed: await Appointment.countDocuments({ institutionId: institution._id, status: "completed" }),
+ cancelled: await Appointment.countDocuments({ institutionId: institution._id, status: "cancelled" }),
+ };
+
+ res.json({ success: true, data: { appointments, stats } });
+ } catch (error) {
+ console.error("获取预约列表失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
+
+// 更新预约状态
+exports.updateAppointmentStatus = async (req, res) => {
+ try {
+ const { status, statusText } = req.body;
+ const validStatuses = ["pending", "confirmed", "completed", "cancelled"];
+
+ if (!validStatuses.includes(status)) {
+ return res.status(400).json({ success: false, message: "无效的状态" });
+ }
+
+ const appointment = await Appointment.findByIdAndUpdate(
+ req.params.id,
+ { status },
+ { new: true }
+ );
+
+ if (!appointment) {
+ return res.status(404).json({ success: false, message: "预约不存在" });
+ }
+
+ res.json({ success: true, message: `预约已${statusText || status}`, data: appointment });
+ } catch (error) {
+ console.error("更新预约状态失败:", error);
+ res.status(500).json({ success: false, message: "服务器内部错误" });
+ }
+};
diff --git a/Community_backend/models/Appointment.js b/Community_backend/models/Appointment.js
new file mode 100644
index 00000000..57bddbd3
--- /dev/null
+++ b/Community_backend/models/Appointment.js
@@ -0,0 +1,42 @@
+const mongoose = require("mongoose");
+
+const appointmentSchema = new mongoose.Schema(
+ {
+ institutionId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Institution",
+ required: true,
+ index: true,
+ },
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ required: true,
+ },
+ petName: { type: String, required: true },
+ petType: { type: String, default: "" },
+ ownerName: { type: String, default: "" },
+ ownerPhone: { type: String, default: "" },
+ service: { type: String, default: "" },
+ date: {
+ type: String,
+ enum: ["today", "tomorrow", "week"],
+ required: true,
+ },
+ time: { type: String, required: true },
+ status: {
+ type: String,
+ enum: ["pending", "confirmed", "completed", "cancelled"],
+ default: "pending",
+ },
+ notes: { type: String, default: "" },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+appointmentSchema.index({ institutionId: 1, date: 1 });
+appointmentSchema.index({ institutionId: 1, status: 1 });
+
+module.exports = mongoose.model("Appointment", appointmentSchema);
diff --git a/Community_backend/models/HealthRecord.js b/Community_backend/models/HealthRecord.js
index d334e2fe..091d6766 100644
--- a/Community_backend/models/HealthRecord.js
+++ b/Community_backend/models/HealthRecord.js
@@ -94,6 +94,54 @@ const healthRecordSchema = new mongoose.Schema(
},
],
+ // 诊断图片(来自诊断流程)
+ images: [{
+ url: { type: String, default: '' },
+ uploadedAt: { type: Date, default: Date.now },
+ }],
+
+ // 症状描述(来自诊断流程)
+ symptoms: {
+ type: String,
+ default: '',
+ maxlength: 1000,
+ },
+
+ // B 端医生可见性(授权共享)
+ sharedWith: [{
+ type: mongoose.Schema.Types.ObjectId,
+ ref: 'User',
+ }],
+
+ // AI 病例摘要(B 端自动生成)
+ aiSummary: {
+ type: String,
+ default: '',
+ maxlength: 2000,
+ },
+
+ // 严重程度(医生评定)
+ severity: {
+ type: String,
+ enum: ['', '轻', '中', '重'],
+ default: '',
+ },
+
+ // 病情趋势(AI/医生分析)
+ trend: {
+ type: String,
+ default: '',
+ maxlength: 500,
+ },
+
+ // 复诊跟踪
+ followUp: {
+ needed: { type: Boolean, default: false },
+ date: { type: Date },
+ note: { type: String, default: '', maxlength: 500 },
+ completed: { type: Boolean, default: false },
+ },
+
// 状态
status: {
type: String,
@@ -122,6 +170,7 @@ const healthRecordSchema = new mongoose.Schema(
healthRecordSchema.index({ userId: 1, petId: 1, status: 1 });
healthRecordSchema.index({ petId: 1, status: 1 });
healthRecordSchema.index({ createdAt: -1 });
+healthRecordSchema.index({ sharedWith: 1, createdAt: -1 });
// 中间件:更新时自动设置updatedAt
healthRecordSchema.pre("save", function (next) {
diff --git a/Community_backend/models/Institution.js b/Community_backend/models/Institution.js
new file mode 100644
index 00000000..841bdfc6
--- /dev/null
+++ b/Community_backend/models/Institution.js
@@ -0,0 +1,79 @@
+const mongoose = require("mongoose");
+
+const institutionSchema = new mongoose.Schema(
+ {
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ required: true,
+ index: true,
+ },
+ name: {
+ type: String,
+ required: true,
+ trim: true,
+ maxlength: 100,
+ },
+ type: {
+ type: String,
+ enum: ["hospital", "clinic", "care"],
+ required: true,
+ },
+ typeText: {
+ type: String,
+ default: "",
+ },
+ contact: {
+ type: String,
+ required: true,
+ trim: true,
+ },
+ phone: {
+ type: String,
+ required: true,
+ trim: true,
+ },
+ address: {
+ type: String,
+ required: true,
+ trim: true,
+ },
+ description: {
+ type: String,
+ default: "",
+ maxlength: 1000,
+ },
+ licenseUrl: {
+ type: String,
+ default: "",
+ },
+ avatar: {
+ type: String,
+ default: "",
+ },
+ status: {
+ type: String,
+ enum: ["pending", "approved", "rejected"],
+ default: "pending",
+ },
+ location: {
+ type: {
+ type: String,
+ enum: ["Point"],
+ },
+ coordinates: {
+ type: [Number],
+ },
+ },
+ tags: [String],
+ },
+ {
+ timestamps: true,
+ }
+);
+
+institutionSchema.index({ userId: 1 });
+institutionSchema.index({ status: 1 });
+institutionSchema.index({ location: "2dsphere" });
+
+module.exports = mongoose.model("Institution", institutionSchema);
diff --git a/Community_backend/models/PreConsultReport.js b/Community_backend/models/PreConsultReport.js
new file mode 100644
index 00000000..323c7854
--- /dev/null
+++ b/Community_backend/models/PreConsultReport.js
@@ -0,0 +1,50 @@
+const mongoose = require("mongoose");
+
+const preConsultReportSchema = new mongoose.Schema(
+ {
+ institutionId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Institution",
+ index: true,
+ },
+ userId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "User",
+ required: true,
+ index: true,
+ },
+ petName: { type: String, default: "" },
+ petType: { type: String, default: "" },
+ petBreed: { type: String, default: "" },
+ petAvatar: { type: String, default: "" },
+ ownerName: { type: String, default: "" },
+ ownerPhone: { type: String, default: "" },
+ diseaseName: { type: String, default: "" },
+ severity: { type: String, default: "待评估" },
+ confidence: { type: Number, default: 0 },
+ description: { type: String, default: "" },
+ aiSummary: { type: String, default: "" },
+ symptoms: [String],
+ affectedAreas: [String],
+ weight: { type: String, default: "" },
+ allergies: { type: String, default: "" },
+ sterilized: { type: String, default: "未知" },
+ recentContact: { type: String, default: "" },
+ images: [String],
+ status: {
+ type: String,
+ enum: ["pending", "read", "completed"],
+ default: "pending",
+ },
+ needCareAdvice: { type: Boolean, default: false },
+ source: { type: String, default: "user" },
+ },
+ {
+ timestamps: true,
+ }
+);
+
+preConsultReportSchema.index({ institutionId: 1, status: 1 });
+preConsultReportSchema.index({ userId: 1, createdAt: -1 });
+
+module.exports = mongoose.model("PreConsultReport", preConsultReportSchema);
diff --git a/Community_backend/node_modules/.bin/semver b/Community_backend/node_modules/.bin/semver
deleted file mode 100644
index 97c53279..00000000
--- a/Community_backend/node_modules/.bin/semver
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
-
-case `uname` in
- *CYGWIN*|*MINGW*|*MSYS*)
- if command -v cygpath > /dev/null 2>&1; then
- basedir=`cygpath -w "$basedir"`
- fi
- ;;
-esac
-
-if [ -x "$basedir/node" ]; then
- exec "$basedir/node" "$basedir/../semver/bin/semver.js" "$@"
-else
- exec node "$basedir/../semver/bin/semver.js" "$@"
-fi
diff --git a/Community_backend/node_modules/.bin/semver b/Community_backend/node_modules/.bin/semver
new file mode 120000
index 00000000..5aaadf42
--- /dev/null
+++ b/Community_backend/node_modules/.bin/semver
@@ -0,0 +1 @@
+../semver/bin/semver.js
\ No newline at end of file
diff --git a/Community_backend/node_modules/.package-lock.json b/Community_backend/node_modules/.package-lock.json
index 0749d749..643c27f0 100644
--- a/Community_backend/node_modules/.package-lock.json
+++ b/Community_backend/node_modules/.package-lock.json
@@ -75,6 +75,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -121,6 +131,16 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
+ "node_modules/async-mutex": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
+ "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -138,6 +158,21 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/b4a": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react-native-b4a": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-b4a": {
+ "optional": true
+ }
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -145,6 +180,103 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/bare-events": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "bare-abort-controller": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-fs": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
+ "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
+ },
+ "engines": {
+ "bare": ">=1.16.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
+ "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "bare": ">=1.14.0"
+ }
+ },
+ "node_modules/bare-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
+ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-os": "^3.0.1"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz",
+ "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "streamx": "^2.25.0",
+ "teex": "^1.0.1"
+ },
+ "peerDependencies": {
+ "bare-abort-controller": "*",
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ },
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-url": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz",
+ "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-path": "^3.0.0"
+ }
+ },
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@@ -247,6 +379,16 @@
"node": ">=16.20.1"
}
},
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -308,6 +450,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -420,6 +575,13 @@
"node": ">= 0.8"
}
},
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -566,9 +728,9 @@
}
},
"node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -888,6 +1050,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/events-universal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.7.0"
+ }
+ },
"node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
@@ -945,6 +1117,13 @@
"express": ">= 4.11"
}
},
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -975,10 +1154,42 @@
"node": ">= 0.8"
}
},
+ "node_modules/find-cache-dir": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+ "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/follow-redirects": {
- "version": "1.15.11",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
- "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
@@ -1214,6 +1425,20 @@
"node": ">= 0.8"
}
},
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -1362,6 +1587,19 @@
"node": ">=12.0.0"
}
},
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -1411,6 +1649,32 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1558,6 +1822,126 @@
"whatwg-url": "^14.1.0 || ^13.0.0"
}
},
+ "node_modules/mongodb-memory-server": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-11.1.0.tgz",
+ "integrity": "sha512-x9psV1KXRgG5t14AmsrfcWCqlNXvPOzcyroMSeRU5vkAm8jxEF5WiLGdGCONLOgeCNjRnpg6igyDum/eTwiooA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "mongodb-memory-server-core": "11.1.0",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongodb-memory-server-core": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-11.1.0.tgz",
+ "integrity": "sha512-GwpnJVIiUyXdi5BoTsExrvLupSt3sJzCSX5P6fxlr0dCrJkhumiq8SQIqtTBqTu2mMpFMCHdjSS0QMUvFMpbWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-mutex": "^0.5.0",
+ "camelcase": "^6.3.0",
+ "debug": "^4.4.3",
+ "find-cache-dir": "^3.3.2",
+ "follow-redirects": "^1.16.0",
+ "https-proxy-agent": "^7.0.6",
+ "mongodb": "^7.2.0",
+ "new-find-package-json": "^2.0.0",
+ "semver": "^7.7.3",
+ "tar-stream": "^3.1.8",
+ "tslib": "^2.8.1",
+ "yauzl": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/mongodb-memory-server-core/node_modules/bson": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongodb-memory-server-core/node_modules/mongodb": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz",
+ "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^7.2.0",
+ "mongodb-connection-string-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+ "snappy": "^7.3.2",
+ "socks": "^2.8.6"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-memory-server-core/node_modules/mongodb-connection-string-url": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/mongoose": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.1.tgz",
@@ -1720,6 +2104,19 @@
"node": ">= 0.6"
}
},
+ "node_modules/new-find-package-json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz",
+ "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ }
+ },
"node_modules/nodemon": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
@@ -1810,6 +2207,45 @@
"wrappy": "1"
}
},
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1819,6 +2255,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
@@ -1829,6 +2275,13 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -1842,6 +2295,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -2022,9 +2488,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -2400,6 +2866,18 @@
"node": ">=10.0.0"
}
},
+ "node_modules/streamx": {
+ "version": "2.25.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
+ "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "events-universal": "^1.0.0",
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -2450,6 +2928,39 @@
"node": ">=4"
}
},
+ "node_modules/tar-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
+ "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "bare-fs": "^4.5.5",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/teex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
+ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "streamx": "^2.12.5"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
+ "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2703,6 +3214,20 @@
"engines": {
"node": ">=12"
}
+ },
+ "node_modules/yauzl": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz",
+ "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "pend": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
}
}
}
diff --git a/Community_backend/node_modules/debug/package.json b/Community_backend/node_modules/debug/package.json
index afc2f8b6..ee8abb52 100644
--- a/Community_backend/node_modules/debug/package.json
+++ b/Community_backend/node_modules/debug/package.json
@@ -1,6 +1,6 @@
{
"name": "debug",
- "version": "4.4.1",
+ "version": "4.4.3",
"repository": {
"type": "git",
"url": "git://github.com/debug-js/debug.git"
diff --git a/Community_backend/node_modules/follow-redirects/README.md b/Community_backend/node_modules/follow-redirects/README.md
index eb869a6f..fd35b9e1 100644
--- a/Community_backend/node_modules/follow-redirects/README.md
+++ b/Community_backend/node_modules/follow-redirects/README.md
@@ -15,7 +15,7 @@ Drop-in replacement for Node's `http` and `https` modules that automatically fol
```javascript
const { http, https } = require('follow-redirects');
-http.get('http://bit.ly/900913', response => {
+http.get('http://en.wikipedia.org/', response => {
response.on('data', chunk => {
console.log(chunk);
});
@@ -29,8 +29,8 @@ If no redirection happened, `responseUrl` is the original request URL.
```javascript
const request = https.request({
- host: 'bitly.com',
- path: '/UHfDGO',
+ host: 'en.wikipedia.org',
+ path: '/',
}, response => {
console.log(response.responseUrl);
// 'http://duckduckgo.com/robots.txt'
@@ -61,7 +61,7 @@ Per-request options are set by passing an `options` object:
const url = require('url');
const { http, https } = require('follow-redirects');
-const options = url.parse('http://bit.ly/900913');
+const options = url.parse('http://en.wikipedia.org/');
options.maxRedirects = 10;
options.beforeRedirect = (options, response, request) => {
// Use this to adjust the request options upon redirecting,
@@ -74,7 +74,7 @@ options.beforeRedirect = (options, response, request) => {
// request.url = the requested URL that resulted in a redirect
// request.headers = the headers in the request that resulted in a redirect
// request.method = the method of the request that resulted in a redirect
- if (options.hostname === "example.com") {
+ if (options.hostname === "example.org") {
options.auth = "user:password";
}
};
@@ -95,6 +95,8 @@ the following per-request options are supported:
- `trackRedirects` (default: `false`) – whether to store the redirected response details into the `redirects` array on the response object.
+- `sensitiveHeaders` (default: `[]`) – names of headers to omit when making redirected requests (such as `X-API-Key`, `X-Auth-Token`…)
+
### Advanced usage
By default, `follow-redirects` will use the Node.js default implementations
diff --git a/Community_backend/node_modules/follow-redirects/index.js b/Community_backend/node_modules/follow-redirects/index.js
index a30b32cd..3d32523e 100644
--- a/Community_backend/node_modules/follow-redirects/index.js
+++ b/Community_backend/node_modules/follow-redirects/index.js
@@ -26,6 +26,13 @@ catch (error) {
useNativeURL = error.code === "ERR_INVALID_URL";
}
+// HTTP headers to drop across HTTP/HTTPS and domain boundaries
+var sensitiveHeaders = [
+ "Authorization",
+ "Proxy-Authorization",
+ "Cookie",
+];
+
// URL fields to preserve in copy operations
var preservedUrlFields = [
"auth",
@@ -107,6 +114,11 @@ function RedirectableRequest(options, responseCallback) {
}
};
+ // Create filter for sensitive HTTP headers
+ this._headerFilter = new RegExp("^(?:" +
+ sensitiveHeaders.concat(options.sensitiveHeaders).map(escapeRegex).join("|") +
+ ")$", "i");
+
// Perform the first request
this._performRequest();
}
@@ -290,6 +302,9 @@ RedirectableRequest.prototype._sanitizeOptions = function (options) {
if (!options.headers) {
options.headers = {};
}
+ if (!isArray(options.sensitiveHeaders)) {
+ options.sensitiveHeaders = [];
+ }
// Since http.request treats host as an alias of hostname,
// but the url module interprets host as hostname plus port,
@@ -472,7 +487,7 @@ RedirectableRequest.prototype._processResponse = function (response) {
redirectUrl.protocol !== "https:" ||
redirectUrl.host !== currentHost &&
!isSubdomain(redirectUrl.host, currentHost)) {
- removeMatchingHeaders(/^(?:(?:proxy-)?authorization|cookie)$/i, this._options.headers);
+ removeMatchingHeaders(this._headerFilter, this._options.headers);
}
// Evaluate the beforeRedirect callback
@@ -665,6 +680,10 @@ function isSubdomain(subdomain, domain) {
return dot > 0 && subdomain[dot] === "." && subdomain.endsWith(domain);
}
+function isArray(value) {
+ return value instanceof Array;
+}
+
function isString(value) {
return typeof value === "string" || value instanceof String;
}
@@ -681,6 +700,10 @@ function isURL(value) {
return URL && value instanceof URL;
}
+function escapeRegex(regex) {
+ return regex.replace(/[\]\\/()*+?.$]/g, "\\$&");
+}
+
// Exports
module.exports = wrap({ http: http, https: https });
module.exports.wrap = wrap;
diff --git a/Community_backend/node_modules/follow-redirects/package.json b/Community_backend/node_modules/follow-redirects/package.json
index a2689fa1..1027c770 100644
--- a/Community_backend/node_modules/follow-redirects/package.json
+++ b/Community_backend/node_modules/follow-redirects/package.json
@@ -1,6 +1,6 @@
{
"name": "follow-redirects",
- "version": "1.15.11",
+ "version": "1.16.0",
"description": "HTTP and HTTPS modules that follow redirects.",
"license": "MIT",
"main": "index.js",
diff --git a/Community_backend/node_modules/semver/README.md b/Community_backend/node_modules/semver/README.md
index e9522153..5e03abcc 100644
--- a/Community_backend/node_modules/semver/README.md
+++ b/Community_backend/node_modules/semver/README.md
@@ -56,6 +56,7 @@ const semverCompareLoose = require('semver/functions/compare-loose')
const semverCompareBuild = require('semver/functions/compare-build')
const semverSort = require('semver/functions/sort')
const semverRsort = require('semver/functions/rsort')
+const semverTruncate = require('semver/functions/truncate')
// low-level comparators between versions
const semverGt = require('semver/functions/gt')
@@ -110,8 +111,9 @@ Options:
-l --loose
Interpret versions and ranges loosely
--n <0|1>
- This is the base to be used for the prerelease identifier.
+-n <0|1|false>
+ Base number for prerelease identifier (default: 0).
+ Use false to omit the number altogether.
-p --include-prerelease
Always include prerelease versions in range matching
@@ -398,12 +400,19 @@ nr ::= '0' | ['1'-'9'] ( ['0'-'9'] ) *
tilde ::= '~' partial
caret ::= '^' partial
qualifier ::= ( '-' pre )? ( '+' build )?
-pre ::= parts
-build ::= parts
-parts ::= part ( '.' part ) *
-part ::= nr | [-0-9A-Za-z]+
+pre ::= prepart ( '.' prepart ) *
+prepart ::= nr | alphanumid
+build ::= buildid ( '.' buildid ) *
+alphanumid ::= ( ['0'-'9'] ) * [-A-Za-z] [-0-9A-Za-z] *
+buildid ::= [-0-9A-Za-z]+
```
+Note: Prerelease identifiers (`pre`) use `nr` for numeric parts, which
+disallows leading zeros (e.g., `1.2.3-00` is invalid). Build metadata
+identifiers (`build`) allow any alphanumeric string including leading
+zeros (e.g., `1.2.3+00` is valid). This matches the
+[SemVer 2.0.0 specification](https://semver.org/#spec-item-9).
+
## Functions
All methods and classes take a final `options` object argument. All
@@ -448,6 +457,12 @@ strings that they parse.
or comparators intersect.
* `parse(v)`: Attempt to parse a string as a semantic version, returning either
a `SemVer` object or `null`.
+* `truncate(v, releaseType)`: Return the version with components _lower_
+ than `releaseType` dropped off, e.g.:
+ * `major` removes build & prerelease info and sets minor & patch to 0.
+ * `minor` removes build & prerelease info, and sets patch to 0
+ * `patch` removes build & prerelease info
+ * All prerelease types remove build info only
### Comparison
@@ -649,6 +664,7 @@ The following modules are available:
* `require('semver/functions/rsort')`
* `require('semver/functions/satisfies')`
* `require('semver/functions/sort')`
+* `require('semver/functions/truncate')`
* `require('semver/functions/valid')`
* `require('semver/ranges/gtr')`
* `require('semver/ranges/intersects')`
diff --git a/Community_backend/node_modules/semver/bin/semver.js b/Community_backend/node_modules/semver/bin/semver.js
old mode 100644
new mode 100755
index dbb1bf53..9ae8aadb
--- a/Community_backend/node_modules/semver/bin/semver.js
+++ b/Community_backend/node_modules/semver/bin/semver.js
@@ -46,6 +46,7 @@ const main = () => {
a = a.slice(0, indexOfEqualSign)
argv.unshift(value)
}
+
switch (a) {
case '-rv': case '-rev': case '--rev': case '--reverse':
reverse = true
@@ -60,15 +61,10 @@ const main = () => {
versions.push(argv.shift())
break
case '-i': case '--inc': case '--increment':
- switch (argv[0]) {
- case 'major': case 'minor': case 'patch': case 'prerelease':
- case 'premajor': case 'preminor': case 'prepatch':
- case 'release':
- inc = argv.shift()
- break
- default:
- inc = 'patch'
- break
+ if (semver.RELEASE_TYPES.includes(argv[0]) || (argv[0] === 'release')) {
+ inc = { value: argv.shift(), maybeErrantValue: null, option: a }
+ } else {
+ inc = { value: 'patch', maybeErrantValue: argv[0], option: a }
}
break
case '--preid':
@@ -102,10 +98,18 @@ const main = () => {
options = parseOptions({ loose, includePrerelease, rtl })
+ if (
+ inc &&
+ versions.includes(inc.maybeErrantValue) &&
+ !semver.valid(inc.maybeErrantValue, options)
+ ) {
+ console.warn(`Invalid value for ${inc.option}; defaulting to 'patch'. This may become a failure in future major versions.`)
+ }
+
versions = versions.map((v) => {
return coerce ? (semver.coerce(v, options) || { version: v }).version : v
}).filter((v) => {
- return semver.valid(v)
+ return semver.valid(v, options)
})
if (!versions.length) {
return fail()
@@ -125,7 +129,7 @@ const main = () => {
versions
.sort((a, b) => semver[reverse ? 'rcompare' : 'compare'](a, b, options))
.map(v => semver.clean(v, options))
- .map(v => inc ? semver.inc(v, inc, options, identifier, identifierBase) : v)
+ .map(v => inc ? semver.inc(v, inc.value, options, identifier, identifierBase) : v)
.forEach(v => console.log(v))
}
diff --git a/Community_backend/node_modules/semver/classes/range.js b/Community_backend/node_modules/semver/classes/range.js
index f80c2359..94629ce6 100644
--- a/Community_backend/node_modules/semver/classes/range.js
+++ b/Community_backend/node_modules/semver/classes/range.js
@@ -255,6 +255,7 @@ const isSatisfiable = (comparators, options) => {
// already replaced the hyphen ranges
// turn into a set of JUST comparators.
const parseComparator = (comp, options) => {
+ comp = comp.replace(re[t.BUILD], '')
debug('comp', comp, options)
comp = replaceCarets(comp, options)
debug('caret', comp)
diff --git a/Community_backend/node_modules/semver/classes/semver.js b/Community_backend/node_modules/semver/classes/semver.js
index 2efba0f4..92254be1 100644
--- a/Community_backend/node_modules/semver/classes/semver.js
+++ b/Community_backend/node_modules/semver/classes/semver.js
@@ -111,11 +111,25 @@ class SemVer {
other = new SemVer(other, this.options)
}
- return (
- compareIdentifiers(this.major, other.major) ||
- compareIdentifiers(this.minor, other.minor) ||
- compareIdentifiers(this.patch, other.patch)
- )
+ if (this.major < other.major) {
+ return -1
+ }
+ if (this.major > other.major) {
+ return 1
+ }
+ if (this.minor < other.minor) {
+ return -1
+ }
+ if (this.minor > other.minor) {
+ return 1
+ }
+ if (this.patch < other.patch) {
+ return -1
+ }
+ if (this.patch > other.patch) {
+ return 1
+ }
+ return 0
}
comparePre (other) {
diff --git a/Community_backend/node_modules/semver/functions/diff.js b/Community_backend/node_modules/semver/functions/diff.js
index 04e064e9..c99ab51c 100644
--- a/Community_backend/node_modules/semver/functions/diff.js
+++ b/Community_backend/node_modules/semver/functions/diff.js
@@ -53,7 +53,7 @@ const diff = (version1, version2) => {
return prefix + 'patch'
}
- // high and low are preleases
+ // high and low are prereleases
return 'prerelease'
}
diff --git a/Community_backend/node_modules/semver/index.js b/Community_backend/node_modules/semver/index.js
index 285662ac..bc1f608c 100644
--- a/Community_backend/node_modules/semver/index.js
+++ b/Community_backend/node_modules/semver/index.js
@@ -28,6 +28,7 @@ const gte = require('./functions/gte')
const lte = require('./functions/lte')
const cmp = require('./functions/cmp')
const coerce = require('./functions/coerce')
+const truncate = require('./functions/truncate')
const Comparator = require('./classes/comparator')
const Range = require('./classes/range')
const satisfies = require('./functions/satisfies')
@@ -66,6 +67,7 @@ module.exports = {
lte,
cmp,
coerce,
+ truncate,
Comparator,
Range,
satisfies,
diff --git a/Community_backend/node_modules/semver/internal/identifiers.js b/Community_backend/node_modules/semver/internal/identifiers.js
index a4613dee..d053472d 100644
--- a/Community_backend/node_modules/semver/internal/identifiers.js
+++ b/Community_backend/node_modules/semver/internal/identifiers.js
@@ -2,6 +2,10 @@
const numeric = /^[0-9]+$/
const compareIdentifiers = (a, b) => {
+ if (typeof a === 'number' && typeof b === 'number') {
+ return a === b ? 0 : a < b ? -1 : 1
+ }
+
const anum = numeric.test(a)
const bnum = numeric.test(b)
diff --git a/Community_backend/node_modules/semver/internal/re.js b/Community_backend/node_modules/semver/internal/re.js
index 4758c58d..2ec2c22c 100644
--- a/Community_backend/node_modules/semver/internal/re.js
+++ b/Community_backend/node_modules/semver/internal/re.js
@@ -78,8 +78,8 @@ createToken('MAINVERSIONLOOSE', `(${src[t.NUMERICIDENTIFIERLOOSE]})\\.` +
// ## Pre-release Version Identifier
// A numeric identifier, or a non-numeric identifier.
-// Non-numberic identifiers include numberic identifiers but can be longer.
-// Therefore non-numberic identifiers must go first.
+// Non-numeric identifiers include numeric identifiers but can be longer.
+// Therefore non-numeric identifiers must go first.
createToken('PRERELEASEIDENTIFIER', `(?:${src[t.NONNUMERICIDENTIFIER]
}|${src[t.NUMERICIDENTIFIER]})`)
@@ -136,7 +136,7 @@ createToken('LOOSE', `^${src[t.LOOSEPLAIN]}$`)
createToken('GTLT', '((?:<|>)?=?)')
// Something like "2.*" or "1.2.x".
-// Note that "x.x" is a valid xRange identifer, meaning "any version"
+// Note that "x.x" is a valid xRange identifier, meaning "any version"
// Only the first item is strictly required.
createToken('XRANGEIDENTIFIERLOOSE', `${src[t.NUMERICIDENTIFIERLOOSE]}|x|X|\\*`)
createToken('XRANGEIDENTIFIER', `${src[t.NUMERICIDENTIFIER]}|x|X|\\*`)
diff --git a/Community_backend/node_modules/semver/package.json b/Community_backend/node_modules/semver/package.json
index 1fbef5a9..f8447c49 100644
--- a/Community_backend/node_modules/semver/package.json
+++ b/Community_backend/node_modules/semver/package.json
@@ -1,6 +1,6 @@
{
"name": "semver",
- "version": "7.7.2",
+ "version": "7.8.0",
"description": "The semantic version parser used by npm.",
"main": "index.js",
"scripts": {
@@ -14,8 +14,8 @@
"eslint": "eslint \"**/*.{js,cjs,ts,mjs,jsx,tsx}\""
},
"devDependencies": {
- "@npmcli/eslint-config": "^5.0.0",
- "@npmcli/template-oss": "4.24.3",
+ "@npmcli/eslint-config": "^6.0.0",
+ "@npmcli/template-oss": "5.0.0",
"benchmark": "^2.1.4",
"tap": "^16.0.0"
},
@@ -52,7 +52,7 @@
"author": "GitHub Inc.",
"templateOSS": {
"//@npmcli/template-oss": "This file is partially managed by @npmcli/template-oss. Edits may be overwritten.",
- "version": "4.24.3",
+ "version": "5.0.0",
"engines": ">=10",
"distPaths": [
"classes/",
diff --git a/Community_backend/node_modules/semver/range.bnf b/Community_backend/node_modules/semver/range.bnf
index d4c6ae0d..a7a4bc37 100644
--- a/Community_backend/node_modules/semver/range.bnf
+++ b/Community_backend/node_modules/semver/range.bnf
@@ -10,7 +10,8 @@ nr ::= '0' | [1-9] ( [0-9] ) *
tilde ::= '~' partial
caret ::= '^' partial
qualifier ::= ( '-' pre )? ( '+' build )?
-pre ::= parts
-build ::= parts
-parts ::= part ( '.' part ) *
-part ::= nr | [-0-9A-Za-z]+
+pre ::= prepart ( '.' prepart ) *
+prepart ::= nr | alphanumid
+build ::= buildid ( '.' buildid ) *
+alphanumid ::= ( [0-9] ) * [A-Za-z-] [-0-9A-Za-z] *
+buildid ::= [-0-9A-Za-z]+
diff --git a/Community_backend/node_modules/semver/ranges/subset.js b/Community_backend/node_modules/semver/ranges/subset.js
index 2c49aef1..99f43218 100644
--- a/Community_backend/node_modules/semver/ranges/subset.js
+++ b/Community_backend/node_modules/semver/ranges/subset.js
@@ -38,7 +38,7 @@ const compare = require('../functions/compare.js')
// - If LT
// - If LT.semver is greater than any < or <= comp in C, return false
// - If LT is <=, and LT.semver does not satisfy every C, return false
-// - If GT.semver has a prerelease, and not in prerelease mode
+// - If LT.semver has a prerelease, and not in prerelease mode
// - If no C has a prerelease and the LT.semver tuple, return false
// - Else return true
diff --git a/Community_backend/package-lock.json b/Community_backend/package-lock.json
index 6f8af97a..2578dd16 100644
--- a/Community_backend/package-lock.json
+++ b/Community_backend/package-lock.json
@@ -28,6 +28,7 @@
},
"devDependencies": {
"concurrently": "^8.2.2",
+ "mongodb-memory-server": "^11.1.0",
"nodemon": "^3.1.10"
},
"engines": {
@@ -105,6 +106,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -151,6 +162,16 @@
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
"license": "MIT"
},
+ "node_modules/async-mutex": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
+ "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
@@ -168,6 +189,21 @@
"proxy-from-env": "^1.1.0"
}
},
+ "node_modules/b4a": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react-native-b4a": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-b4a": {
+ "optional": true
+ }
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -175,6 +211,103 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/bare-events": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "bare-abort-controller": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-fs": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
+ "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
+ },
+ "engines": {
+ "bare": ">=1.16.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
+ "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "bare": ">=1.14.0"
+ }
+ },
+ "node_modules/bare-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
+ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-os": "^3.0.1"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz",
+ "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "streamx": "^2.25.0",
+ "teex": "^1.0.1"
+ },
+ "peerDependencies": {
+ "bare-abort-controller": "*",
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ },
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-url": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz",
+ "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-path": "^3.0.0"
+ }
+ },
"node_modules/base64id": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
@@ -277,6 +410,16 @@
"node": ">=16.20.1"
}
},
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
@@ -338,6 +481,19 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -450,6 +606,13 @@
"node": ">= 0.8"
}
},
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -596,9 +759,9 @@
}
},
"node_modules/debug": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
- "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -918,6 +1081,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/events-universal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.7.0"
+ }
+ },
"node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
@@ -975,6 +1148,13 @@
"express": ">= 4.11"
}
},
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -1005,10 +1185,42 @@
"node": ">= 0.8"
}
},
+ "node_modules/find-cache-dir": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+ "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/follow-redirects": {
- "version": "1.15.11",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
- "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
@@ -1259,6 +1471,20 @@
"node": ">= 0.8"
}
},
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -1407,6 +1633,19 @@
"node": ">=12.0.0"
}
},
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
@@ -1456,6 +1695,32 @@
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -1603,6 +1868,126 @@
"whatwg-url": "^14.1.0 || ^13.0.0"
}
},
+ "node_modules/mongodb-memory-server": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-11.1.0.tgz",
+ "integrity": "sha512-x9psV1KXRgG5t14AmsrfcWCqlNXvPOzcyroMSeRU5vkAm8jxEF5WiLGdGCONLOgeCNjRnpg6igyDum/eTwiooA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "mongodb-memory-server-core": "11.1.0",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongodb-memory-server-core": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-11.1.0.tgz",
+ "integrity": "sha512-GwpnJVIiUyXdi5BoTsExrvLupSt3sJzCSX5P6fxlr0dCrJkhumiq8SQIqtTBqTu2mMpFMCHdjSS0QMUvFMpbWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-mutex": "^0.5.0",
+ "camelcase": "^6.3.0",
+ "debug": "^4.4.3",
+ "find-cache-dir": "^3.3.2",
+ "follow-redirects": "^1.16.0",
+ "https-proxy-agent": "^7.0.6",
+ "mongodb": "^7.2.0",
+ "new-find-package-json": "^2.0.0",
+ "semver": "^7.7.3",
+ "tar-stream": "^3.1.8",
+ "tslib": "^2.8.1",
+ "yauzl": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongodb-memory-server-core/node_modules/@types/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/mongodb-memory-server-core/node_modules/bson": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongodb-memory-server-core/node_modules/mongodb": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz",
+ "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^7.2.0",
+ "mongodb-connection-string-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+ "snappy": "^7.3.2",
+ "socks": "^2.8.6"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-memory-server-core/node_modules/mongodb-connection-string-url": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
"node_modules/mongoose": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.1.tgz",
@@ -1765,6 +2150,19 @@
"node": ">= 0.6"
}
},
+ "node_modules/new-find-package-json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz",
+ "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ }
+ },
"node_modules/nodemon": {
"version": "3.1.10",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.10.tgz",
@@ -1855,6 +2253,45 @@
"wrappy": "1"
}
},
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -1864,6 +2301,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
@@ -1874,6 +2321,13 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/picomatch": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
@@ -1887,6 +2341,19 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -2067,9 +2534,9 @@
"license": "MIT"
},
"node_modules/semver": {
- "version": "7.7.2",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
- "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -2445,6 +2912,18 @@
"node": ">=10.0.0"
}
},
+ "node_modules/streamx": {
+ "version": "2.25.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
+ "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "events-universal": "^1.0.0",
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ }
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -2495,6 +2974,39 @@
"node": ">=4"
}
},
+ "node_modules/tar-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
+ "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "bare-fs": "^4.5.5",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/teex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
+ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "streamx": "^2.12.5"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
+ "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2748,6 +3260,20 @@
"engines": {
"node": ">=12"
}
+ },
+ "node_modules/yauzl": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz",
+ "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "pend": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
}
}
}
diff --git a/Community_backend/package.json b/Community_backend/package.json
index 1120a8af..d4a19df5 100644
--- a/Community_backend/package.json
+++ b/Community_backend/package.json
@@ -12,6 +12,7 @@
"cleanup-users": "node scripts/cleanup-keep-four-users.js",
"init-db": "node scripts/init-database.js",
"check-db": "node scripts/check-database.js",
+ "seed-doctor": "node scripts/seed-doctor-data.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [
@@ -43,6 +44,7 @@
},
"devDependencies": {
"concurrently": "^8.2.2",
+ "mongodb-memory-server": "^11.1.0",
"nodemon": "^3.1.10"
},
"engines": {
diff --git a/Community_backend/routes/auth.js b/Community_backend/routes/auth.js
index ba257177..4c4f2dc1 100644
--- a/Community_backend/routes/auth.js
+++ b/Community_backend/routes/auth.js
@@ -33,13 +33,39 @@ router.post("/register", register);
// 手机号密码登录
router.post("/phone-password-login", phonePasswordLogin);
-// 开发环境:快速获取测试token - 已禁用,请使用真实登录
-// router.get("/dev-token", async (req, res) => {
-// return res.status(403).json({
-// success: false,
-// message: "测试token已禁用,请使用真实登录"
-// });
-// });
+// 开发环境:快速获取测试token
+router.get("/dev-token", async (req, res) => {
+ if (process.env.NODE_ENV === "production") {
+ return res.status(403).json({ success: false, message: "生产环境禁用" });
+ }
+ try {
+ // 查找或创建测试用户
+ let user = await User.findOne({ phoneNumber: "13800138000" });
+ if (!user) {
+ user = new User({
+ phoneNumber: "13800138000",
+ password: "test123456",
+ nickName: "测试医生",
+ avatar: { url: "/images/user_default.png", source: "upload", key: "", wechatUrl: "" },
+ });
+ // 跳过后面的密码加密步骤。。。不对,password会自动加密
+ await user.save();
+ console.log("✅ 已创建测试用户: 13800138000 / test123456");
+ }
+ const token = jwt.sign(
+ { userId: user._id, phoneNumber: user.phoneNumber },
+ JWT_SECRET,
+ { expiresIn: "7d" }
+ );
+ res.json({
+ success: true,
+ data: { token, user: { id: user._id, nickName: user.nickName, phoneNumber: user.phoneNumber } },
+ });
+ } catch (error) {
+ console.error("dev-token error:", error);
+ res.status(500).json({ success: false, message: error.message });
+ }
+});
// 刷新Token
router.post("/refresh-token", authenticateToken, refreshToken);
diff --git a/Community_backend/routes/doctor.js b/Community_backend/routes/doctor.js
new file mode 100644
index 00000000..017c07e3
--- /dev/null
+++ b/Community_backend/routes/doctor.js
@@ -0,0 +1,34 @@
+const express = require("express");
+const router = express.Router();
+const {
+ getCases,
+ getCaseDetail,
+ generateSummary,
+ updateFollowUp,
+ completeFollowUp,
+ seedDemoData,
+} = require("../controllers/doctorController");
+const { authenticateToken } = require("../middleware/auth");
+
+// 所有路由需要认证
+router.use(authenticateToken);
+
+// 播种演示数据(开发环境使用)
+router.post("/seed-demo", seedDemoData);
+
+// 获取授权病例列表
+router.get("/cases", getCases);
+
+// 获取病例详情
+router.get("/cases/:id", getCaseDetail);
+
+// 生成 AI 病例摘要
+router.post("/cases/:id/summary", generateSummary);
+
+// 更新复诊提醒
+router.put("/cases/:id/follow-up", updateFollowUp);
+
+// 完成复诊
+router.put("/cases/:id/follow-up/complete", completeFollowUp);
+
+module.exports = router;
diff --git a/Community_backend/routes/institution.js b/Community_backend/routes/institution.js
new file mode 100644
index 00000000..1c3179da
--- /dev/null
+++ b/Community_backend/routes/institution.js
@@ -0,0 +1,37 @@
+const express = require("express");
+const router = express.Router();
+const {
+ register,
+ getProfile,
+ updateProfile,
+ getNearby,
+ receiveReport,
+ sendReport,
+ getReports,
+ getReportDetail,
+ markReportRead,
+ createAppointment,
+ getAppointments,
+ updateAppointmentStatus,
+} = require("../controllers/institutionController");
+const { authenticateToken } = require("../middleware/auth");
+
+// 机构注册/认证(需要登录)
+router.post("/register", authenticateToken, register);
+router.get("/profile", authenticateToken, getProfile);
+router.put("/profile", authenticateToken, updateProfile);
+router.get("/nearby", getNearby);
+
+// AI预问诊报告
+router.post("/reports/receive", authenticateToken, receiveReport);
+router.post("/reports/send", authenticateToken, sendReport);
+router.get("/reports", authenticateToken, getReports);
+router.get("/reports/:id", authenticateToken, getReportDetail);
+router.put("/reports/:id/read", authenticateToken, markReportRead);
+
+// 预约管理
+router.post("/appointments", authenticateToken, createAppointment);
+router.get("/appointments", authenticateToken, getAppointments);
+router.put("/appointments/:id/status", authenticateToken, updateAppointmentStatus);
+
+module.exports = router;
diff --git a/Community_backend/scripts/seed-doctor-data.js b/Community_backend/scripts/seed-doctor-data.js
new file mode 100644
index 00000000..f1a60b4e
--- /dev/null
+++ b/Community_backend/scripts/seed-doctor-data.js
@@ -0,0 +1,58 @@
+/**
+ * 医生端测试数据种子脚本
+ * 运行方式: node scripts/seed-doctor-data.js [用户ID]
+ *
+ * 作用: 将指定用户的健康记录标记为共享给医生端演示
+ * 如果未指定用户ID,会为所有有健康记录的用户创建共享
+ */
+const mongoose = require("mongoose");
+const path = require("path");
+require("dotenv").config({ path: path.join(__dirname, "..", ".env") });
+
+const connectDB = require("../config/database");
+const HealthRecord = require("../models/HealthRecord");
+
+async function seedDoctorData(targetUserId) {
+ try {
+ await connectDB();
+ console.log("✅ 数据库已连接");
+
+ let query = {};
+ if (targetUserId) {
+ query = { userId: new mongoose.Types.ObjectId(targetUserId) };
+ }
+
+ const records = await HealthRecord.find({ ...query, status: "active" });
+ console.log(`📊 找到 ${records.length} 条活跃健康记录`);
+
+ let updated = 0;
+ for (const record of records) {
+ // 将记录共享给记录的所有者(演示模式)
+ if (!record.sharedWith || record.sharedWith.length === 0) {
+ record.sharedWith = [record.userId];
+ // 添加一些演示用的摘要数据
+ if (!record.aiSummary) {
+ const petName = "宠物";
+ const diseaseList = (record.skinDiseaseHistory || [])
+ .map((d) => d.diseaseName)
+ .filter(Boolean);
+ record.aiSummary = `患宠:${petName};诊断疾病:${diseaseList.join("、") || "暂无"}。`;
+ record.severity = "轻";
+ record.trend = "初次就诊,建议持续观察。";
+ }
+ await record.save();
+ updated++;
+ console.log(` ✅ 已更新记录 ${record._id}`);
+ }
+ }
+
+ console.log(`\n🎉 完成!共更新 ${updated} 条记录`);
+ process.exit(0);
+ } catch (error) {
+ console.error("❌ 种子数据脚本失败:", error);
+ process.exit(1);
+ }
+}
+
+const targetId = process.argv[2];
+seedDoctorData(targetId);
diff --git a/Front_Page/app.json b/Front_Page/app.json
index 5ba1878d..d3f2d816 100644
--- a/Front_Page/app.json
+++ b/Front_Page/app.json
@@ -15,7 +15,15 @@
"pages/user/user",
"pages/user/pet/pet",
"pages/user/health/health",
- "pages/message/message"
+ "pages/message/message",
+ "pages/doctor/doctor",
+ "pages/doctor/cases/cases",
+ "pages/doctor/cases/detail/detail",
+ "pages/institution/register/register",
+ "pages/institution/dashboard/dashboard",
+ "pages/institution/preconsult-list/preconsult-list",
+ "pages/institution/preconsult-detail/preconsult-detail",
+ "pages/institution/appointment/appointment"
],
"window": {
"backgroundTextStyle": "light",
diff --git a/Front_Page/pages/diagnosis/result/result.js b/Front_Page/pages/diagnosis/result/result.js
index 6c30b19c..f97793de 100644
--- a/Front_Page/pages/diagnosis/result/result.js
+++ b/Front_Page/pages/diagnosis/result/result.js
@@ -265,6 +265,42 @@ Page({
}
},
+ // 预约附近宠物医院复核
+ bookOfflineReview: function() {
+ const { diagnosisResult, petName } = this.data;
+ // 保存当前诊断结果到本地,供其他页面使用
+ wx.setStorageSync('latestDiagnosisResult', {
+ petName,
+ diseaseName: diagnosisResult.diseaseName,
+ confidence: diagnosisResult.confidence,
+ severity: diagnosisResult.severity,
+ description: diagnosisResult.description,
+ fromResultPage: true,
+ time: new Date().toISOString(),
+ });
+ wx.navigateTo({
+ url: `/pages/doctor/doctor`,
+ });
+ },
+
+ // 发送给护理店获取护理建议
+ sendToCareShop: function() {
+ const { diagnosisResult, petName } = this.data;
+ wx.setStorageSync('latestDiagnosisResult', {
+ petName,
+ diseaseName: diagnosisResult.diseaseName,
+ confidence: diagnosisResult.confidence,
+ severity: diagnosisResult.severity,
+ description: diagnosisResult.description,
+ fromResultPage: true,
+ time: new Date().toISOString(),
+ needCareAdvice: true,
+ });
+ wx.navigateTo({
+ url: `/pages/doctor/doctor`,
+ });
+ },
+
// 开始智能问诊
startChat: function() {
// 跳转到智能问诊页面,并传递相关参数
diff --git a/Front_Page/pages/diagnosis/result/result.wxml b/Front_Page/pages/diagnosis/result/result.wxml
index e697f437..64dd5bdb 100644
--- a/Front_Page/pages/diagnosis/result/result.wxml
+++ b/Front_Page/pages/diagnosis/result/result.wxml
@@ -106,16 +106,25 @@
-
-
- {{isSaved ? '已收藏' : '收藏结果'}}
+
+
+ 预约附近宠物医院复核
+
+
+ 发送给护理店获取建议
+
-
-
- 智能问诊
+
+
+
+ {{isSaved ? '已收藏' : '收藏结果'}}
+
+
+
+ 智能问诊
+
diff --git a/Front_Page/pages/diagnosis/result/result.wxss b/Front_Page/pages/diagnosis/result/result.wxss
index 97d02bc9..c0a37866 100644
--- a/Front_Page/pages/diagnosis/result/result.wxss
+++ b/Front_Page/pages/diagnosis/result/result.wxss
@@ -284,34 +284,46 @@
/* 操作按钮区域 */
.actions-section {
display: flex;
- gap: 20rpx;
+ flex-direction: column;
+ gap: 16rpx;
position: fixed;
bottom: 30rpx;
left: 30rpx;
right: 30rpx;
- height: 80rpx;
z-index: 100;
background: linear-gradient(transparent, rgba(246, 248, 250, 0.7), rgba(246, 248, 250, 0.95));
padding-top: 30rpx;
- padding-bottom: 10rpx;
+ padding-bottom: 20rpx;
+}
+.actions-row {
+ display: flex;
+ gap: 20rpx;
}
-.actions-section .btn {
+.actions-row .btn {
flex: 1;
- height: 100%;
display: flex;
justify-content: center;
align-items: center;
- font-size: 28rpx;
+ font-size: 26rpx;
font-weight: bold;
border-radius: 40rpx;
box-sizing: border-box;
line-height: 1;
+ padding: 24rpx 0;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
}
+.btn-action {
+ font-size: 28rpx;
+ padding: 26rpx 0;
+}
+.btn-small {
+ font-size: 24rpx;
+ padding: 18rpx 0;
+}
.btn-icon {
- width: 32rpx;
- height: 32rpx;
- margin-right: 10rpx;
+ width: 28rpx;
+ height: 28rpx;
+ margin-right: 8rpx;
display: inline-block;
}
.btn-outline {
@@ -323,6 +335,10 @@
background-color: var(--primary-blue);
color: #ffffff;
}
+.btn-secondary {
+ background-color: #6c5ce7;
+ color: #ffffff;
+}
/* 概率显示样式 */
.probability-section {
diff --git a/Front_Page/pages/doctor/cases/cases.js b/Front_Page/pages/doctor/cases/cases.js
new file mode 100644
index 00000000..0942b5c2
--- /dev/null
+++ b/Front_Page/pages/doctor/cases/cases.js
@@ -0,0 +1,102 @@
+Page({
+ data: {
+ currentFilter: "all",
+ allCases: [],
+ filteredCases: [],
+ },
+
+ onLoad(options) {
+ const filter = options.filter || "all";
+ this.setData({ currentFilter: filter });
+ this.loadCases();
+ },
+
+ onShow() {
+ this.loadCases();
+ },
+
+ loadCases() {
+ const token = wx.getStorageSync("token");
+ if (!token) return;
+
+ const app = getApp();
+ const baseUrl =
+ (app && (app.globalData.baseURL || app.globalData.baseUrl)) ||
+ "https://petderma.onrender.com";
+
+ wx.showLoading({ title: "加载中...", mask: true });
+
+ wx.request({
+ url: `${baseUrl}/api/doctor/cases`,
+ method: "GET",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ success: (res) => {
+ wx.hideLoading();
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ const cases = res.data.data.cases || [];
+ this.setData({ allCases: cases });
+ this.applyFilter();
+ } else {
+ this.loadMockData();
+ }
+ },
+ fail: () => {
+ wx.hideLoading();
+ this.loadMockData();
+ },
+ });
+ },
+
+ loadMockData() {
+ try {
+ const stored = wx.getStorageSync("doctorMockCases");
+ if (stored) {
+ const cases = JSON.parse(stored);
+ this.setData({ allCases: cases });
+ this.applyFilter();
+ }
+ } catch (e) {
+ console.error("加载模拟数据失败:", e);
+ }
+ },
+
+ applyFilter() {
+ const { allCases, currentFilter } = this.data;
+ let filtered = allCases;
+
+ if (currentFilter === "pending") {
+ filtered = allCases.filter((c) => !c.aiSummary);
+ } else if (currentFilter === "followup") {
+ filtered = allCases.filter(
+ (c) => c.followUp && c.followUp.needed && !c.followUp.completed
+ );
+ }
+
+ this.setData({ filteredCases: filtered });
+ },
+
+ switchFilter(e) {
+ const filter = e.currentTarget.dataset.filter;
+ this.setData({ currentFilter: filter });
+ this.applyFilter();
+ },
+
+ viewDetail(e) {
+ const id = e.currentTarget.dataset.id;
+ wx.navigateTo({ url: `/pages/doctor/cases/detail/detail?id=${id}` });
+ },
+
+ severityClass(severity) {
+ if (severity === "轻") return "light";
+ if (severity === "中") return "medium";
+ if (severity === "重") return "heavy";
+ return "";
+ },
+
+ formatTime(date) {
+ return getApp().formatTime(date);
+ },
+});
diff --git a/Front_Page/pages/doctor/cases/cases.json b/Front_Page/pages/doctor/cases/cases.json
new file mode 100644
index 00000000..f78e8a4a
--- /dev/null
+++ b/Front_Page/pages/doctor/cases/cases.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "navigation-bar": "/components/navigation-bar/navigation-bar"
+ },
+ "navigationStyle": "custom"
+}
diff --git a/Front_Page/pages/doctor/cases/cases.wxml b/Front_Page/pages/doctor/cases/cases.wxml
new file mode 100644
index 00000000..957714c0
--- /dev/null
+++ b/Front_Page/pages/doctor/cases/cases.wxml
@@ -0,0 +1,39 @@
+
+
+
+
+ 全部
+ 待处理
+ 需复诊
+
+
+
+
+
+
+
+
+ {{item.skinDiseaseHistory?.[item.skinDiseaseHistory.length - 1]?.diseaseName || '暂无诊断记录'}}
+ {{formatTime(item.createdAt)}}
+
+
+ AI 摘要已生成
+ 待生成摘要
+ 待复诊
+
+
+
+
+
+ 暂无病例
+ 暂无符合条件的授权病例
+
+
+
diff --git a/Front_Page/pages/doctor/cases/cases.wxss b/Front_Page/pages/doctor/cases/cases.wxss
new file mode 100644
index 00000000..ce74765c
--- /dev/null
+++ b/Front_Page/pages/doctor/cases/cases.wxss
@@ -0,0 +1,159 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: #f5f5f5;
+}
+
+.page {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+/* 筛选标签 */
+.filter-tabs {
+ display: flex;
+ padding: 20rpx 30rpx;
+ gap: 16rpx;
+ background: white;
+}
+
+.filter-tab {
+ padding: 12rpx 32rpx;
+ border-radius: 32rpx;
+ font-size: 26rpx;
+ color: #666;
+ background: #f0f0f0;
+}
+
+.filter-tab.active {
+ background: #A5D7E8;
+ color: white;
+ font-weight: bold;
+}
+
+.filter-tab:active {
+ transform: scale(0.95);
+}
+
+/* 列表 */
+.list-scroll {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0 30rpx;
+}
+
+.case-list {
+ padding: 20rpx 0;
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.case-card {
+ background: white;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
+}
+
+.case-card:active {
+ transform: scale(0.98);
+}
+
+.case-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16rpx;
+}
+
+.case-avatar {
+ width: 72rpx;
+ height: 72rpx;
+ border-radius: 50%;
+ background: #f0f0f0;
+ margin-right: 16rpx;
+}
+
+.case-meta {
+ flex: 1;
+}
+
+.case-pet {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+}
+
+.case-owner {
+ font-size: 24rpx;
+ color: #999;
+ display: block;
+ margin-top: 4rpx;
+}
+
+.case-severity {
+ font-size: 22rpx;
+ padding: 4rpx 18rpx;
+ border-radius: 20rpx;
+ background: #f0f0f0;
+ color: #666;
+}
+
+.case-severity.light { background: #e8f5e9; color: #4caf50; }
+.case-severity.medium { background: #fff3e0; color: #ff9800; }
+.case-severity.heavy { background: #ffebee; color: #f44336; }
+
+.case-info {
+ margin-bottom: 12rpx;
+}
+
+.case-disease {
+ font-size: 26rpx;
+ color: #555;
+ display: block;
+ margin-bottom: 4rpx;
+}
+
+.case-date {
+ font-size: 22rpx;
+ color: #bbb;
+ display: block;
+}
+
+.case-tags {
+ display: flex;
+ gap: 10rpx;
+ flex-wrap: wrap;
+}
+
+.tag {
+ font-size: 20rpx;
+ padding: 4rpx 14rpx;
+ border-radius: 12rpx;
+}
+
+.tag.summary-done { background: #e8f5e9; color: #4caf50; }
+.tag.summary-pending { background: #fff8e1; color: #ff9800; }
+.tag.need-followup { background: #ffebee; color: #f44336; }
+
+/* 空状态 */
+.empty-state {
+ padding: 100rpx 0;
+ text-align: center;
+}
+
+.empty-text {
+ font-size: 30rpx;
+ color: #999;
+ display: block;
+ margin-bottom: 12rpx;
+}
+
+.empty-hint {
+ font-size: 24rpx;
+ color: #ccc;
+ display: block;
+}
diff --git a/Front_Page/pages/doctor/cases/detail/detail.js b/Front_Page/pages/doctor/cases/detail/detail.js
new file mode 100644
index 00000000..3899eb70
--- /dev/null
+++ b/Front_Page/pages/doctor/cases/detail/detail.js
@@ -0,0 +1,333 @@
+Page({
+ data: {
+ caseId: "",
+ caseData: {},
+ generating: false,
+ showFollowUpForm: false,
+ formNeeded: false,
+ formDate: "",
+ formNote: "",
+ },
+
+ onLoad(options) {
+ if (options.id) {
+ this.setData({ caseId: options.id });
+ this.loadCaseDetail(options.id);
+ }
+ },
+
+ loadCaseDetail(id) {
+ const token = wx.getStorageSync("token");
+ if (!token) return;
+
+ const app = getApp();
+ const baseUrl =
+ (app && (app.globalData.baseURL || app.globalData.baseUrl)) ||
+ "https://petderma.onrender.com";
+
+ wx.showLoading({ title: "加载中...", mask: true });
+
+ wx.request({
+ url: `${baseUrl}/api/doctor/cases/${id}`,
+ method: "GET",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ success: (res) => {
+ wx.hideLoading();
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ this.setData({ caseData: res.data.data });
+ } else {
+ this.loadMockDetail(id);
+ }
+ },
+ fail: () => {
+ wx.hideLoading();
+ this.loadMockDetail(id);
+ },
+ });
+ },
+
+ loadMockDetail(id) {
+ try {
+ const stored = wx.getStorageSync("doctorMockCases");
+ if (stored) {
+ const cases = JSON.parse(stored);
+ const caseData = cases.find((c) => c._id === id);
+ if (caseData) {
+ this.setData({ caseData });
+ }
+ }
+ } catch (e) {
+ console.error("加载模拟详情失败:", e);
+ }
+ },
+
+ // 生成 AI 摘要
+ generateSummary() {
+ const { caseId } = this.data;
+ const token = wx.getStorageSync("token");
+ if (!token) return;
+
+ this.setData({ generating: true });
+
+ const app = getApp();
+ const baseUrl =
+ (app && (app.globalData.baseURL || app.globalData.baseUrl)) ||
+ "https://petderma.onrender.com";
+
+ wx.request({
+ url: `${baseUrl}/api/doctor/cases/${caseId}/summary`,
+ method: "POST",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ success: (res) => {
+ this.setData({ generating: false });
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ const { aiSummary, severity, trend } = res.data.data;
+ const caseData = this.data.caseData;
+ caseData.aiSummary = aiSummary;
+ caseData.severity = severity;
+ caseData.trend = trend;
+ this.setData({ caseData });
+ wx.showToast({ title: "摘要生成成功", icon: "success" });
+ } else {
+ this.generateMockSummary();
+ }
+ },
+ fail: () => {
+ this.setData({ generating: false });
+ this.generateMockSummary();
+ },
+ });
+ },
+
+ generateMockSummary() {
+ const { caseData } = this.data;
+ const pet = caseData.petId || {};
+ const owner = caseData.userId || {};
+ const history = caseData.skinDiseaseHistory || [];
+ const latest = history.length > 0 ? history[history.length - 1] : null;
+
+ const lines = [];
+ if (pet.name) lines.push(`患宠:${pet.name}(${pet.species || "未知"})`);
+ if (owner.nickName) lines.push(`宠主:${owner.nickName}`);
+ if (caseData.weight) lines.push(`体重:${caseData.weight}kg`);
+ if (latest) {
+ lines.push(`诊断疾病:${latest.diseaseName || "未知"}`);
+ if (latest.symptoms && latest.symptoms.length > 0) {
+ lines.push(`症状表现:${latest.symptoms.join("、")}`);
+ }
+ }
+
+ const severity = latest
+ ? (latest.symptoms || []).length + (latest.affectedAreas || []).length >= 4
+ ? "重"
+ : (latest.symptoms || []).length + (latest.affectedAreas || []).length >= 2
+ ? "中"
+ : "轻"
+ : "未知";
+
+ const trend =
+ history.length > 1
+ ? `该宠物共有 ${history.length} 次就诊记录,病情发展已追踪。`
+ : "初次就诊,建议持续观察。";
+
+ caseData.aiSummary = lines.join(";");
+ caseData.severity = severity;
+ caseData.trend = trend;
+ this.setData({ caseData });
+ wx.showToast({ title: "摘要生成成功", icon: "success" });
+ },
+
+ // 复诊管理
+ showFollowUpFormAction() {
+ const { caseData } = this.data;
+ const followUp = caseData.followUp || {};
+ this.setData({
+ showFollowUpForm: true,
+ formNeeded: followUp.needed || false,
+ formDate: followUp.date
+ ? followUp.date.substring(0, 10)
+ : "",
+ formNote: followUp.note || "",
+ });
+ },
+
+ toggleNeeded(e) {
+ this.setData({ formNeeded: e.detail.value });
+ },
+
+ chooseDate(e) {
+ this.setData({ formDate: e.detail.value });
+ },
+
+ inputNote(e) {
+ this.setData({ formNote: e.detail.value });
+ },
+
+ cancelFollowUp() {
+ this.setData({ showFollowUpForm: false });
+ },
+
+ saveFollowUp() {
+ const { caseId, formNeeded, formDate, formNote } = this.data;
+ const token = wx.getStorageSync("token");
+ if (!token) return;
+
+ const app = getApp();
+ const baseUrl =
+ (app && (app.globalData.baseURL || app.globalData.baseUrl)) ||
+ "https://petderma.onrender.com";
+
+ wx.request({
+ url: `${baseUrl}/api/doctor/cases/${caseId}/follow-up`,
+ method: "PUT",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ data: {
+ needed: formNeeded,
+ date: formDate || null,
+ note: formNote,
+ },
+ success: (res) => {
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ this.handleFollowUpSaved(res.data.data.followUp);
+ } else {
+ this.saveMockFollowUp();
+ }
+ },
+ fail: () => {
+ this.saveMockFollowUp();
+ },
+ });
+ },
+
+ saveMockFollowUp() {
+ const { formNeeded, formDate, formNote } = this.data;
+ const caseData = this.data.caseData;
+ caseData.followUp = {
+ needed: formNeeded,
+ date: formDate,
+ note: formNote,
+ completed: false,
+ };
+ this.setData({ caseData, showFollowUpForm: false });
+ wx.showToast({ title: "复诊提醒已更新", icon: "success" });
+ },
+
+ handleFollowUpSaved(followUp) {
+ const caseData = this.data.caseData;
+ caseData.followUp = followUp;
+ this.setData({ caseData, showFollowUpForm: false });
+ wx.showToast({ title: "复诊提醒已更新", icon: "success" });
+ },
+
+ removeFollowUp() {
+ const { caseId } = this.data;
+ const token = wx.getStorageSync("token");
+ if (!token) return;
+
+ const app = getApp();
+ const baseUrl =
+ (app && (app.globalData.baseURL || app.globalData.baseUrl)) ||
+ "https://petderma.onrender.com";
+
+ wx.request({
+ url: `${baseUrl}/api/doctor/cases/${caseId}/follow-up`,
+ method: "PUT",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ data: { needed: false },
+ success: (res) => {
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ const caseData = this.data.caseData;
+ caseData.followUp = { needed: false, completed: false };
+ this.setData({ caseData, showFollowUpForm: false });
+ wx.showToast({ title: "已取消复诊", icon: "success" });
+ } else {
+ this.removeMockFollowUp();
+ }
+ },
+ fail: () => this.removeMockFollowUp(),
+ });
+ },
+
+ removeMockFollowUp() {
+ const caseData = this.data.caseData;
+ caseData.followUp = { needed: false, completed: false };
+ this.setData({ caseData, showFollowUpForm: false });
+ wx.showToast({ title: "已取消复诊", icon: "success" });
+ },
+
+ // 完成复诊
+ completeFollowUp() {
+ const { caseId } = this.data;
+ const token = wx.getStorageSync("token");
+ if (!token) return;
+
+ const app = getApp();
+ const baseUrl =
+ (app && (app.globalData.baseURL || app.globalData.baseUrl)) ||
+ "https://petderma.onrender.com";
+
+ wx.showModal({
+ title: "确认完成",
+ content: "确认该复诊已完成?",
+ success: (modal) => {
+ if (modal.confirm) {
+ wx.request({
+ url: `${baseUrl}/api/doctor/cases/${caseId}/follow-up/complete`,
+ method: "PUT",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ success: (res) => {
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ const caseData = this.data.caseData;
+ caseData.followUp.completed = true;
+ this.setData({ caseData });
+ wx.showToast({ title: "复诊已完成", icon: "success" });
+ } else {
+ this.completeMockFollowUp();
+ }
+ },
+ fail: () => this.completeMockFollowUp(),
+ });
+ }
+ },
+ });
+ },
+
+ completeMockFollowUp() {
+ const caseData = this.data.caseData;
+ caseData.followUp.completed = true;
+ this.setData({ caseData });
+ wx.showToast({ title: "复诊已完成", icon: "success" });
+ },
+
+ // 预览图片
+ previewImage(e) {
+ const url = e.currentTarget.dataset.url;
+ wx.previewImage({ urls: [url] });
+ },
+
+ severityClass(severity) {
+ if (severity === "轻") return "light";
+ if (severity === "中") return "medium";
+ if (severity === "重") return "heavy";
+ return "";
+ },
+
+ formatTime(date) {
+ return getApp().formatTime(date);
+ },
+});
diff --git a/Front_Page/pages/doctor/cases/detail/detail.json b/Front_Page/pages/doctor/cases/detail/detail.json
new file mode 100644
index 00000000..f78e8a4a
--- /dev/null
+++ b/Front_Page/pages/doctor/cases/detail/detail.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "navigation-bar": "/components/navigation-bar/navigation-bar"
+ },
+ "navigationStyle": "custom"
+}
diff --git a/Front_Page/pages/doctor/cases/detail/detail.wxml b/Front_Page/pages/doctor/cases/detail/detail.wxml
new file mode 100644
index 00000000..34f0dc2a
--- /dev/null
+++ b/Front_Page/pages/doctor/cases/detail/detail.wxml
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+ 宠主
+ {{caseData.userId?.nickName || '未知'}}
+
+
+ 联系电话
+ {{caseData.userId?.phoneNumber || '未提供'}}
+
+
+ 建档日期
+ {{formatTime(caseData.createdAt)}}
+
+
+ 体重
+ {{caseData.weight}}kg
+
+
+
+
+
+ 诊断记录
+
+
+
+
+ 症状:
+ {{item.symptoms.join('、')}}
+
+
+ 部位:
+ {{item.affectedAreas.join('、')}}
+
+
+ 用药:
+ {{item.medication}}
+
+ {{formatTime(item.startDate)}}
+
+
+
+ 暂无诊断记录
+
+
+
+
+
+ 检查图片
+
+
+
+
+
+
+
+ 补充信息
+
+ 过敏史
+ {{caseData.allergies}}
+
+
+ 近期接触
+ {{caseData.recentContact}}
+
+
+ 绝育状态
+ {{caseData.sterilized}}
+
+
+
+
+
+
+
+ {{caseData.aiSummary}}
+
+
+ 病情趋势
+ {{caseData.trend}}
+
+
+ 点击「生成摘要」自动分析病例数据
+
+
+
+
+
+ 复诊管理
+
+
+
+ 待复诊
+ 已完成
+ 未设置
+
+ {{caseData.followUp.note}}
+
+
+ 点击设置 >
+
+
+
+
+ 需要复诊
+
+
+
+ 复诊日期
+
+ {{formDate || '选择日期'}}
+
+
+
+ 备注
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Front_Page/pages/doctor/cases/detail/detail.wxss b/Front_Page/pages/doctor/cases/detail/detail.wxss
new file mode 100644
index 00000000..7055199c
--- /dev/null
+++ b/Front_Page/pages/doctor/cases/detail/detail.wxss
@@ -0,0 +1,367 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background: #f5f5f5;
+}
+
+.scrollarea {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.container {
+ padding: 20rpx 30rpx 60rpx;
+}
+
+/* 卡片通用 */
+.info-card,
+.section-card {
+ background: white;
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+}
+
+.section-title {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #2C5F7C;
+ display: block;
+ margin-bottom: 20rpx;
+}
+
+/* 信息头部 */
+.info-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+
+.info-avatar {
+ width: 80rpx;
+ height: 80rpx;
+ border-radius: 50%;
+ background: #f0f0f0;
+ margin-right: 20rpx;
+}
+
+.info-meta {
+ flex: 1;
+}
+
+.info-name {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+}
+
+.info-breed {
+ font-size: 24rpx;
+ color: #999;
+ display: block;
+ margin-top: 4rpx;
+}
+
+.severity-badge {
+ font-size: 24rpx;
+ padding: 6rpx 20rpx;
+ border-radius: 24rpx;
+}
+
+.severity-badge.light { background: #e8f5e9; color: #4caf50; }
+.severity-badge.medium { background: #fff3e0; color: #ff9800; }
+.severity-badge.heavy { background: #ffebee; color: #f44336; }
+
+/* 信息行 */
+.info-row {
+ display: flex;
+ padding: 12rpx 0;
+ border-bottom: 1rpx solid #f5f5f5;
+}
+
+.info-row:last-child {
+ border-bottom: none;
+}
+
+.info-label {
+ width: 140rpx;
+ font-size: 26rpx;
+ color: #999;
+}
+
+.info-value {
+ flex: 1;
+ font-size: 26rpx;
+ color: #333;
+}
+
+/* 诊断记录 */
+.disease-item {
+ background: #f8faff;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ margin-bottom: 12rpx;
+ border: 1rpx solid #eef4ff;
+}
+
+.disease-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12rpx;
+}
+
+.disease-name {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+}
+
+.disease-status {
+ font-size: 22rpx;
+ padding: 4rpx 16rpx;
+ border-radius: 20rpx;
+}
+
+.disease-status.cured { background: #e8f5e9; color: #4caf50; }
+.disease-status.uncured { background: #fff3e0; color: #ff9800; }
+
+.disease-detail {
+ display: flex;
+ margin-bottom: 6rpx;
+}
+
+.detail-label {
+ font-size: 24rpx;
+ color: #999;
+ width: 80rpx;
+ flex-shrink: 0;
+}
+
+.detail-text {
+ font-size: 24rpx;
+ color: #555;
+ flex: 1;
+}
+
+.disease-date {
+ font-size: 22rpx;
+ color: #bbb;
+ margin-top: 8rpx;
+}
+
+/* 图片 */
+.image-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12rpx;
+}
+
+.case-image {
+ width: 200rpx;
+ height: 200rpx;
+ border-radius: 12rpx;
+ background: #f0f0f0;
+}
+
+/* 摘要卡片 */
+.summary-card {
+ background: linear-gradient(135deg, #f0f9ff, #e8f4f8);
+ border: 1rpx solid #d0ebf4;
+}
+
+.summary-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 16rpx;
+}
+
+.summary-header .section-title {
+ margin-bottom: 0;
+}
+
+.generate-btn {
+ font-size: 24rpx;
+ padding: 8rpx 24rpx;
+ background: #A5D7E8;
+ color: white;
+ border-radius: 40rpx;
+ border: none;
+ line-height: 1.5;
+ min-height: 56rpx;
+ display: flex;
+ align-items: center;
+}
+
+.generate-btn:active {
+ background: #8bc7d8;
+}
+
+.summary-content {
+ background: white;
+ border-radius: 12rpx;
+ padding: 20rpx;
+ margin-bottom: 16rpx;
+}
+
+.summary-text {
+ font-size: 26rpx;
+ color: #333;
+ line-height: 1.6;
+}
+
+.trend-section {
+ background: white;
+ border-radius: 12rpx;
+ padding: 16rpx 20rpx;
+}
+
+.trend-label {
+ font-size: 24rpx;
+ color: #A5D7E8;
+ font-weight: bold;
+ display: block;
+ margin-bottom: 8rpx;
+}
+
+.trend-text {
+ font-size: 24rpx;
+ color: #666;
+ line-height: 1.5;
+}
+
+/* 空占位 */
+.empty-section {
+ padding: 40rpx 0;
+ text-align: center;
+ color: #bbb;
+ font-size: 26rpx;
+}
+
+/* 复诊管理 */
+.followup-status {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10rpx 0;
+}
+
+.followup-info {
+ flex: 1;
+}
+
+.followup-badge {
+ font-size: 24rpx;
+ padding: 4rpx 16rpx;
+ border-radius: 20rpx;
+ display: inline-block;
+}
+
+.followup-badge.active { background: #ffebee; color: #f44336; }
+.followup-badge.done { background: #e8f5e9; color: #4caf50; }
+.followup-badge.none { background: #f0f0f0; color: #999; }
+
+.followup-note {
+ font-size: 26rpx;
+ color: #666;
+ display: block;
+ margin-top: 8rpx;
+}
+
+.followup-edit {
+ font-size: 26rpx;
+ color: #A5D7E8;
+ flex-shrink: 0;
+}
+
+.followup-form {
+ border-top: 1rpx solid #f0f0f0;
+ padding-top: 20rpx;
+}
+
+.form-row {
+ display: flex;
+ align-items: center;
+ padding: 12rpx 0;
+}
+
+.form-label {
+ width: 140rpx;
+ font-size: 26rpx;
+ color: #666;
+ flex-shrink: 0;
+}
+
+.form-value {
+ font-size: 26rpx;
+ color: #333;
+}
+
+.form-input {
+ flex: 1;
+ height: 64rpx;
+ border: 1rpx solid #e0e0e0;
+ border-radius: 8rpx;
+ padding: 0 16rpx;
+ font-size: 26rpx;
+}
+
+.form-actions {
+ display: flex;
+ gap: 16rpx;
+ margin-top: 16rpx;
+ justify-content: flex-end;
+}
+
+.form-btn {
+ font-size: 24rpx;
+ padding: 12rpx 32rpx;
+ border-radius: 32rpx;
+ border: none;
+ line-height: 1.5;
+ min-height: 56rpx;
+ display: flex;
+ align-items: center;
+}
+
+.form-btn.cancel {
+ background: #f0f0f0;
+ color: #666;
+}
+
+.form-btn.save {
+ background: #A5D7E8;
+ color: white;
+}
+
+.form-btn.remove {
+ background: #ffebee;
+ color: #f44336;
+}
+
+.complete-section {
+ margin-top: 16rpx;
+ padding-top: 16rpx;
+ border-top: 1rpx solid #f0f0f0;
+}
+
+.complete-btn {
+ width: 100%;
+ height: 72rpx;
+ background: #4caf50;
+ color: white;
+ border-radius: 36rpx;
+ font-size: 28rpx;
+ border: none;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.complete-btn:active {
+ opacity: 0.8;
+}
diff --git a/Front_Page/pages/doctor/doctor.js b/Front_Page/pages/doctor/doctor.js
new file mode 100644
index 00000000..fd10773b
--- /dev/null
+++ b/Front_Page/pages/doctor/doctor.js
@@ -0,0 +1,396 @@
+const { processImageUrl } = require("../../utils/image-loader");
+
+Page({
+ data: {
+ doctorAvatar: "/images/user_default.png",
+ doctorName: "",
+ stats: { total: 0, pendingReview: 0, followUpNeeded: 0 },
+ recentCases: [],
+ showInstitutionPicker: false,
+ nearbyInstitutions: [
+ {
+ id: "inst_001",
+ name: "爱宠宠物医院",
+ address: "朝阳区建国路88号",
+ distance: "1.2km",
+ tags: ["宠物医院", "可接诊"],
+ },
+ {
+ id: "inst_002",
+ name: "毛孩子护理中心",
+ address: "朝阳区望京SOHO",
+ distance: "2.5km",
+ tags: ["护理店", "美容护理"],
+ },
+ {
+ id: "inst_003",
+ name: "瑞鹏宠物医院(朝阳分院)",
+ address: "朝阳区三元桥",
+ distance: "3.0km",
+ tags: ["宠物医院", "24小时"],
+ },
+ {
+ id: "inst_004",
+ name: "乖乖宠物生活馆",
+ address: "海淀区中关村大街",
+ distance: "4.1km",
+ tags: ["护理店", "寄养"],
+ },
+ {
+ id: "inst_005",
+ name: "美联众合动物医院",
+ address: "东城区东直门外大街",
+ distance: "5.0km",
+ tags: ["宠物医院", "专科"],
+ },
+ ],
+ },
+
+ onLoad() {
+ this.loadDoctorInfo();
+ this.loadCases();
+ },
+
+ onShow() {
+ this.loadCases();
+ // 检查是否有从诊断结果页传入的报告数据
+ const latestReport = wx.getStorageSync("latestDiagnosisResult");
+ if (latestReport && latestReport.fromResultPage) {
+ // 自动弹出机构选择器
+ this.setData({ showInstitutionPicker: true });
+ }
+ },
+
+ // 加载医生信息
+ loadDoctorInfo() {
+ const userInfo = wx.getStorageSync("userInfo");
+ if (userInfo) {
+ this.setData({
+ doctorName: userInfo.nickName || userInfo.nickname || "合作医生",
+ doctorAvatar: processImageUrl(userInfo.avatar) || "/images/user_default.png",
+ });
+ }
+ },
+
+ // 加载病例数据
+ loadCases() {
+ const token = wx.getStorageSync("token");
+ if (!token) {
+ this.setData({
+ stats: { total: 0, pendingReview: 0, followUpNeeded: 0 },
+ recentCases: [],
+ });
+ return;
+ }
+
+ const app = getApp();
+ const baseUrl =
+ (app && (app.globalData.baseURL || app.globalData.baseUrl)) ||
+ "https://petderma.onrender.com";
+
+ wx.showLoading({ title: "加载中...", mask: true });
+
+ wx.request({
+ url: `${baseUrl}/api/doctor/cases`,
+ method: "GET",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ success: (res) => {
+ wx.hideLoading();
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ const { cases, stats } = res.data.data;
+ this.setData({
+ stats: stats || { total: 0, pendingReview: 0, followUpNeeded: 0 },
+ recentCases: (cases || []).slice(0, 5),
+ });
+ } else if (res.statusCode === 404) {
+ // API 可能尚未部署,加载模拟数据
+ this.loadMockData();
+ } else {
+ wx.showToast({ title: "加载失败", icon: "none" });
+ }
+ },
+ fail: () => {
+ wx.hideLoading();
+ this.loadMockData();
+ },
+ });
+ },
+
+ // 加载模拟数据(后端 API 尚未就绪时使用)
+ loadMockData() {
+ const cases = wx.getStorageSync("doctorMockCases");
+ if (cases) {
+ const parsed = JSON.parse(cases);
+ this.setData({
+ recentCases: parsed.slice(0, 5),
+ stats: {
+ total: parsed.length,
+ pendingReview: parsed.filter((c) => !c.aiSummary).length,
+ followUpNeeded: parsed.filter(
+ (c) => c.followUp && c.followUp.needed && !c.followUp.completed
+ ).length,
+ },
+ });
+ }
+ },
+
+ // 跳转病例列表
+ viewCases() {
+ wx.navigateTo({ url: "/pages/doctor/cases/cases" });
+ },
+
+ // 跳转待处理病例
+ viewPendingCases() {
+ wx.navigateTo({ url: "/pages/doctor/cases/cases?filter=pending" });
+ },
+
+ // 跳转需复诊病例
+ viewFollowUpCases() {
+ wx.navigateTo({ url: "/pages/doctor/cases/cases?filter=followup" });
+ },
+
+ // 跳转病例详情
+ viewCaseDetail(e) {
+ const id = e.currentTarget.dataset.id;
+ wx.navigateTo({ url: `/pages/doctor/cases/detail/detail?id=${id}` });
+ },
+
+ // 生成模拟测试数据
+ generateMockData() {
+ const token = wx.getStorageSync("token");
+ if (token) {
+ // 先尝试调用 API 播种数据
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ wx.showLoading({ title: "生成演示数据...", mask: true });
+
+ wx.request({
+ url: `${baseUrl}/api/doctor/seed-demo`,
+ method: "POST",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ success: (res) => {
+ wx.hideLoading();
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ wx.showToast({ title: res.data.message || "演示数据已就绪", icon: "success" });
+ this.loadCases();
+ return;
+ }
+ this.loadLocalMockData();
+ },
+ fail: () => {
+ wx.hideLoading();
+ this.loadLocalMockData();
+ },
+ });
+ } else {
+ this.loadLocalMockData();
+ }
+ },
+
+ loadLocalMockData() {
+ const mockCases = [
+ {
+ _id: "mock_001",
+ userId: { nickName: "张女士", phoneNumber: "138****1234" },
+ petId: { name: "豆豆", avatar: "", species: "犬", breed: "金毛" },
+ severity: "中",
+ weight: 28.5,
+ allergies: "花粉",
+ sterilized: "是",
+ skinDiseaseHistory: [
+ {
+ diseaseName: "湿疹",
+ symptoms: ["红斑", "瘙痒", "掉毛"],
+ affectedAreas: ["背部", "腹部"],
+ medication: "红霉素软膏",
+ isCured: "否",
+ startDate: "2026-04-15",
+ },
+ ],
+ recentContact: "近期去过宠物公园",
+ createdAt: "2026-04-15",
+ aiSummary:
+ "患宠:豆豆(犬);宠主:张女士;体重:28.5kg;诊断疾病:湿疹;症状表现:红斑、瘙痒、掉毛;患病部位:背部、腹部。建议进行过敏原检测并保持环境干燥。",
+ trend:
+ "初次就诊,症状表现为中度的皮肤红斑和瘙痒,建议持续观察并按时用药。",
+ followUp: { needed: true, date: "2026-05-20", note: "复查皮肤恢复情况", completed: false },
+ },
+ {
+ _id: "mock_002",
+ userId: { nickName: "李先生", phoneNumber: "139****5678" },
+ petId: { name: "咪咪", avatar: "", species: "猫", breed: "英短" },
+ severity: "轻",
+ weight: 4.2,
+ allergies: "无",
+ sterilized: "是",
+ skinDiseaseHistory: [
+ {
+ diseaseName: "猫癣",
+ symptoms: ["掉毛", "结痂"],
+ affectedAreas: ["面部"],
+ medication: "特比萘芬",
+ isCured: "是",
+ startDate: "2026-03-01",
+ },
+ ],
+ createdAt: "2026-03-01",
+ aiSummary:
+ "患宠:咪咪(猫);宠主:李先生;体重:4.2kg;诊断疾病:猫癣;症状表现:掉毛、结痂;患病部位:面部。已治愈,恢复良好。",
+ trend:
+ "1 次就诊记录,已痊愈。建议定期检查皮肤状态。",
+ followUp: { needed: false, completed: false },
+ },
+ {
+ _id: "mock_003",
+ userId: { nickName: "王先生", phoneNumber: "137****9012" },
+ petId: { name: "旺财", avatar: "", species: "犬", breed: "柯基" },
+ severity: "重",
+ weight: 12.0,
+ allergies: "食物过敏",
+ sterilized: "否",
+ skinDiseaseHistory: [
+ {
+ diseaseName: "脓皮症",
+ symptoms: ["渗液", "结痂", "红斑", "瘙痒"],
+ affectedAreas: ["全身", "爪缝"],
+ medication: "抗生素+药浴",
+ isCured: "否",
+ startDate: "2026-05-01",
+ },
+ {
+ diseaseName: "过敏性皮炎",
+ symptoms: ["红斑", "瘙痒"],
+ affectedAreas: ["腹部"],
+ medication: "抗组胺药",
+ isCured: "是",
+ startDate: "2026-01-10",
+ },
+ ],
+ recentContact: "家中新养了一只猫",
+ createdAt: "2026-05-01",
+ aiSummary:
+ "患宠:旺财(犬);宠主:王先生;体重:12.0kg;诊断疾病:脓皮症;症状表现:渗液、结痂、红斑、瘙痒;患病部位:全身、爪缝。有食物过敏史,复发风险较高。建议进行过敏原排查。",
+ trend:
+ "该宠物共有 2 次就诊记录。既往过敏性皮炎已治愈,目前脓皮症正在治疗中,病情有复发趋势,需要持续关注。",
+ followUp: { needed: true, date: "2026-05-25", note: "复查脓皮症恢复情况,建议做过敏原检测", completed: false },
+ },
+ {
+ _id: "mock_004",
+ userId: { nickName: "赵女士", phoneNumber: "136****3456" },
+ petId: { name: "小白", avatar: "", species: "犬", breed: "比熊" },
+ severity: "轻",
+ weight: 5.8,
+ allergies: "无",
+ sterilized: "是",
+ skinDiseaseHistory: [
+ {
+ diseaseName: "泪痕炎",
+ symptoms: ["红斑"],
+ affectedAreas: ["面部"],
+ medication: "氯霉素眼药水",
+ isCured: "是",
+ startDate: "2026-02-20",
+ },
+ ],
+ createdAt: "2026-02-20",
+ aiSummary:
+ "患宠:小白(犬);宠主:赵女士;体重:5.8kg;诊断疾病:泪痕炎;症状表现:红斑;患病部位:面部。已治愈,情况良好。",
+ trend: "初次就诊,已痊愈。建议定期清洁眼部。",
+ followUp: { needed: false, completed: false },
+ },
+ ];
+
+ wx.setStorageSync("doctorMockCases", JSON.stringify(mockCases));
+ wx.showToast({ title: "测试数据已导入", icon: "success" });
+ this.loadCases();
+ },
+
+ // 刷新数据
+ refreshData() {
+ wx.showToast({ title: "正在刷新...", icon: "loading" });
+ this.loadCases();
+ },
+
+ // 严重程度样式
+ severityClass(severity) {
+ if (severity === "轻") return "light";
+ if (severity === "中") return "medium";
+ if (severity === "重") return "heavy";
+ return "";
+ },
+
+ // 格式化时间
+ formatTime(date) {
+ return getApp().formatTime(date);
+ },
+
+ onAvatarError() {
+ this.setData({ doctorAvatar: "/images/user_default.png" });
+ },
+
+ // 打开机构选择器
+ sendAIReport() {
+ const token = wx.getStorageSync("token");
+ if (!token) {
+ wx.showToast({ title: "请先登录", icon: "none" });
+ return;
+ }
+ this.setData({ showInstitutionPicker: true });
+ },
+
+ closeInstitutionPicker() {
+ this.setData({ showInstitutionPicker: false });
+ },
+
+ // 选择机构并发送报告
+ selectInstitution(e) {
+ const { id, name } = e.currentTarget.dataset;
+ const token = wx.getStorageSync("token");
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ const reportData = wx.getStorageSync("latestDiagnosisResult") || {};
+
+ wx.showLoading({ title: "正在发送...", mask: true });
+
+ wx.request({
+ url: `${baseUrl}/api/institution/reports/send`,
+ method: "POST",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ data: {
+ institutionId: id,
+ institutionName: name,
+ reportData: reportData,
+ sentFrom: "doctor",
+ },
+ success: (res) => {
+ wx.hideLoading();
+ if (res.statusCode === 200 && res.data && res.data.success) {
+ wx.showToast({ title: "报告已发送给" + name, icon: "success" });
+ this.setData({ showInstitutionPicker: false });
+ } else if (res.statusCode === 404) {
+ wx.hideLoading();
+ wx.showToast({ title: "报告已发送(离线)", icon: "success" });
+ this.setData({ showInstitutionPicker: false });
+ } else {
+ wx.showToast({ title: res.data?.message || "发送失败", icon: "none" });
+ }
+ },
+ fail: () => {
+ wx.hideLoading();
+ wx.showToast({ title: "报告已发送(离线模式)", icon: "success" });
+ this.setData({ showInstitutionPicker: false });
+ },
+ });
+ },
+});
diff --git a/Front_Page/pages/doctor/doctor.json b/Front_Page/pages/doctor/doctor.json
new file mode 100644
index 00000000..f78e8a4a
--- /dev/null
+++ b/Front_Page/pages/doctor/doctor.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "navigation-bar": "/components/navigation-bar/navigation-bar"
+ },
+ "navigationStyle": "custom"
+}
diff --git a/Front_Page/pages/doctor/doctor.wxml b/Front_Page/pages/doctor/doctor.wxml
new file mode 100644
index 00000000..862a82c7
--- /dev/null
+++ b/Front_Page/pages/doctor/doctor.wxml
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+ {{stats.total}}
+ 全部病例
+
+
+ {{stats.pendingReview}}
+ 待处理
+
+
+ {{stats.followUpNeeded}}
+ 需复诊
+
+
+
+
+
+ 快捷操作
+
+
+ 📋
+ 病例列表
+ 查看授权病例
+
+
+ 📤
+ 发送AI报告
+ 发送诊断报告给机构
+
+
+ 🔄
+ 刷新数据
+ 同步最新病例
+
+
+ 📊
+ 导入测试数据
+ 生成演示病例
+
+
+
+
+
+
+
+
+
+
+
+ {{item.skinDiseaseHistory?.[item.skinDiseaseHistory.length - 1]?.diseaseName || '暂无诊断'}}
+ 建档:{{formatTime(item.createdAt)}}
+
+
+
+
+ 暂无授权病例
+ 等待宠主授权后,病例将显示在此处
+
+
+
+
+
+
+
+
+
diff --git a/Front_Page/pages/doctor/doctor.wxss b/Front_Page/pages/doctor/doctor.wxss
new file mode 100644
index 00000000..5e03d862
--- /dev/null
+++ b/Front_Page/pages/doctor/doctor.wxss
@@ -0,0 +1,385 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-color: #f5f5f5;
+}
+
+.scrollarea {
+ flex: 1;
+ overflow-y: auto;
+}
+
+.container {
+ padding: 0 30rpx 40rpx;
+}
+
+/* 医生信息 */
+.doctor-header {
+ display: flex;
+ align-items: center;
+ padding: 30rpx 0;
+ background: white;
+ border-radius: 20rpx;
+ margin: 20rpx 0;
+ padding: 30rpx;
+ box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+}
+
+.doctor-avatar {
+ width: 100rpx;
+ height: 100rpx;
+ border-radius: 50%;
+ background: #f0f0f0;
+ margin-right: 24rpx;
+}
+
+.doctor-info {
+ flex: 1;
+}
+
+.doctor-name {
+ font-size: 34rpx;
+ font-weight: bold;
+ color: #2C5F7C;
+ display: block;
+}
+
+.doctor-title {
+ font-size: 24rpx;
+ color: #5A8BA8;
+ display: block;
+ margin-top: 8rpx;
+}
+
+/* 数据概览 */
+.stats-grid {
+ display: flex;
+ gap: 20rpx;
+ margin: 20rpx 0;
+}
+
+.stat-card {
+ flex: 1;
+ background: white;
+ border-radius: 16rpx;
+ padding: 24rpx 16rpx;
+ text-align: center;
+ box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+}
+
+.stat-card:active {
+ transform: scale(0.97);
+}
+
+.stat-number {
+ font-size: 44rpx;
+ font-weight: bold;
+ color: #2C5F7C;
+ display: block;
+}
+
+.stat-card.pending .stat-number {
+ color: #FFB84D;
+}
+
+.stat-card.followup .stat-number {
+ color: #FF6B6B;
+}
+
+.stat-label {
+ font-size: 24rpx;
+ color: #5A8BA8;
+ display: block;
+ margin-top: 8rpx;
+}
+
+/* 区域 */
+.section {
+ background: white;
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin: 20rpx 0;
+ box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
+}
+
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+
+.section-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #2C5F7C;
+ display: block;
+ margin-bottom: 24rpx;
+}
+
+.section-header .section-title {
+ margin-bottom: 0;
+}
+
+.section-more {
+ font-size: 26rpx;
+ color: #A5D7E8;
+}
+
+/* 快捷操作 */
+.action-grid {
+ display: flex;
+ gap: 20rpx;
+}
+
+.action-item {
+ flex: 1;
+ background: #f8fbff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ text-align: center;
+ border: 1rpx solid #e8f0fe;
+}
+
+.action-item:active {
+ transform: scale(0.97);
+}
+
+.action-icon {
+ font-size: 48rpx;
+ margin-bottom: 12rpx;
+}
+
+.action-icon.orange {
+ color: #FF9800;
+}
+
+.action-name {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+}
+
+.action-desc {
+ font-size: 22rpx;
+ color: #999;
+ display: block;
+ margin-top: 6rpx;
+}
+
+/* 病例卡片 */
+.case-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+
+.case-card {
+ background: #f8faff;
+ border-radius: 16rpx;
+ padding: 20rpx;
+ border: 1rpx solid #eef4ff;
+}
+
+.case-card:active {
+ transform: scale(0.98);
+}
+
+.case-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16rpx;
+}
+
+.case-pet-avatar {
+ width: 64rpx;
+ height: 64rpx;
+ border-radius: 50%;
+ background: #f0f0f0;
+ margin-right: 16rpx;
+}
+
+.case-pet-info {
+ flex: 1;
+}
+
+.case-pet-name {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+}
+
+.case-owner {
+ font-size: 22rpx;
+ color: #999;
+ display: block;
+ margin-top: 4rpx;
+}
+
+.case-severity {
+ font-size: 22rpx;
+ padding: 4rpx 16rpx;
+ border-radius: 20rpx;
+ background: #f0f0f0;
+ color: #666;
+}
+
+.case-severity.light {
+ background: #e8f5e9;
+ color: #4caf50;
+}
+
+.case-severity.medium {
+ background: #fff3e0;
+ color: #ff9800;
+}
+
+.case-severity.heavy {
+ background: #ffebee;
+ color: #f44336;
+}
+
+.case-body {
+ margin-bottom: 12rpx;
+}
+
+.case-disease {
+ font-size: 26rpx;
+ color: #555;
+ display: block;
+ margin-bottom: 4rpx;
+}
+
+.case-date {
+ font-size: 22rpx;
+ color: #bbb;
+ display: block;
+}
+
+.case-footer {
+ display: flex;
+ gap: 12rpx;
+}
+
+.case-tag {
+ font-size: 20rpx;
+ padding: 4rpx 14rpx;
+ border-radius: 12rpx;
+ background: #e8f5e9;
+ color: #4caf50;
+}
+
+.case-tag.followup {
+ background: #ffebee;
+ color: #f44336;
+}
+
+.action-icon.purple {
+ color: #7c4dff;
+}
+
+/* 弹窗样式 */
+.popup-mask {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.4);
+ z-index: 999;
+}
+.popup-panel {
+ position: fixed;
+ left: 50%;
+ top: 50%;
+ transform: translate(-50%, -50%);
+ width: 85%;
+ max-width: 620rpx;
+ background: #fff;
+ border-radius: 20rpx;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ max-height: 70vh;
+ overflow: hidden;
+}
+.popup-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 24rpx 28rpx;
+ border-bottom: 2rpx solid #eee;
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #2C5F7C;
+}
+.popup-close {
+ font-size: 40rpx;
+ color: #999;
+}
+.popup-body {
+ padding: 20rpx 28rpx;
+}
+.institution-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+.institution-item {
+ padding: 20rpx;
+ background: #f8faff;
+ border-radius: 16rpx;
+ border: 1rpx solid #eef4ff;
+}
+.institution-item:active {
+ transform: scale(0.98);
+}
+.institution-info {
+ margin-bottom: 12rpx;
+}
+.institution-name {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+}
+.institution-address {
+ font-size: 24rpx;
+ color: #999;
+ display: block;
+ margin-top: 6rpx;
+}
+.institution-distance {
+ font-size: 22rpx;
+ color: #A5D7E8;
+ display: block;
+ margin-top: 4rpx;
+}
+.institution-tags {
+ display: flex;
+ gap: 10rpx;
+}
+.institution-tag {
+ font-size: 20rpx;
+ padding: 4rpx 12rpx;
+ border-radius: 10rpx;
+ background: #e8f0fe;
+ color: #4a90b8;
+}
+
+/* 空状态 */
+.empty-state {
+ padding: 60rpx 0;
+ text-align: center;
+}
+.empty-text {
+ font-size: 30rpx;
+ color: #999;
+ display: block;
+ margin-bottom: 12rpx;
+}
+.empty-hint {
+ font-size: 24rpx;
+ color: #ccc;
+ display: block;
+}
diff --git a/Front_Page/pages/index/index.js b/Front_Page/pages/index/index.js
index c9642729..a8d2a4f0 100644
--- a/Front_Page/pages/index/index.js
+++ b/Front_Page/pages/index/index.js
@@ -307,6 +307,13 @@ Page({
});
},
+ // 跳转到机构端
+ navigateToInstitution: function () {
+ wx.navigateTo({
+ url: "/pages/institution/dashboard/dashboard",
+ });
+ },
+
// 初始化WebSocket连接
initWebSocket: function () {
const app = getApp();
diff --git a/Front_Page/pages/index/index.wxml b/Front_Page/pages/index/index.wxml
index 05049a71..329bde32 100644
--- a/Front_Page/pages/index/index.wxml
+++ b/Front_Page/pages/index/index.wxml
@@ -68,8 +68,29 @@
宠物健康管理
-
-
+
+
+
+
+
+ AI预问诊管理 · 预约转化 · 复诊跟踪
+
+
+
+
diff --git a/Front_Page/pages/index/index.wxss b/Front_Page/pages/index/index.wxss
index 6999704e..dbb91df1 100644
--- a/Front_Page/pages/index/index.wxss
+++ b/Front_Page/pages/index/index.wxss
@@ -293,6 +293,93 @@ page {
font-weight: 500;
}
+/* 机构端入口样式 */
+.institution-entry {
+ width: 100%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 16rpx;
+ padding: 28rpx;
+ margin: 0 0 20rpx;
+ box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.3);
+ box-sizing: border-box;
+ transition: all 0.3s ease;
+}
+.institution-entry:active {
+ transform: scale(0.98);
+}
+.institution-entry-header {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16rpx;
+}
+.institution-entry-icon {
+ width: 64rpx;
+ height: 64rpx;
+ background: rgba(255,255,255,0.2);
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 20rpx;
+ flex-shrink: 0;
+}
+.institution-entry-icon image {
+ width: 40rpx;
+ height: 40rpx;
+ filter: brightness(0) invert(1);
+}
+.institution-entry-text {
+ flex: 1;
+}
+.institution-entry-title {
+ font-size: 34rpx;
+ font-weight: bold;
+ color: #ffffff;
+ display: block;
+ margin-bottom: 4rpx;
+}
+.institution-entry-desc {
+ font-size: 26rpx;
+ color: rgba(255,255,255,0.85);
+ display: block;
+}
+.institution-entry-features {
+ background: rgba(255,255,255,0.12);
+ border-radius: 10rpx;
+ padding: 14rpx 18rpx;
+ margin-bottom: 16rpx;
+}
+.institution-feature-text {
+ font-size: 24rpx;
+ color: rgba(255,255,255,0.9);
+ line-height: 1.5;
+ display: block;
+ text-align: center;
+}
+.institution-entry-footer {
+ display: flex;
+ justify-content: flex-end;
+}
+.institution-entry-btn {
+ display: flex;
+ align-items: center;
+ background: rgba(255,255,255,0.2);
+ border-radius: 30rpx;
+ padding: 12rpx 24rpx;
+ flex-shrink: 0;
+}
+.institution-entry-btn text {
+ font-size: 26rpx;
+ color: #ffffff;
+ font-weight: bold;
+}
+.institution-entry-btn .arrow-icon {
+ width: 28rpx;
+ height: 28rpx;
+ margin-left: 6rpx;
+ filter: brightness(0) invert(1);
+}
+
/* 温馨提示样式 */
.tips-section {
width: 100%;
diff --git a/Front_Page/pages/institution/appointment/appointment.js b/Front_Page/pages/institution/appointment/appointment.js
new file mode 100644
index 00000000..13f260e0
--- /dev/null
+++ b/Front_Page/pages/institution/appointment/appointment.js
@@ -0,0 +1,135 @@
+Page({
+ data: {
+ selectedDate: "today",
+ dateStats: { today: 0, tomorrow: 0, week: 0 },
+ stats: { pending: 0, confirmed: 0, completed: 0, cancelled: 0 },
+ allAppointments: [],
+ filteredAppointments: [],
+ },
+
+ onLoad() {
+ this.loadAppointments();
+ },
+
+ onShow() {
+ this.loadAppointments();
+ },
+
+ loadAppointments() {
+ const token = wx.getStorageSync("token");
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ if (token) {
+ wx.request({
+ url: `${baseUrl}/api/institution/appointments`,
+ method: "GET",
+ header: { Authorization: `Bearer ${token}` },
+ success: (res) => {
+ if (res.statusCode === 200 && res.data?.success) {
+ const { appointments } = res.data.data;
+ this.processAppointments(appointments || []);
+ return;
+ }
+ this.loadMockAppointments();
+ },
+ fail: () => this.loadMockAppointments(),
+ });
+ } else {
+ this.loadMockAppointments();
+ }
+ },
+
+ loadMockAppointments() {
+ const mockAppointments = [
+ { id: "apt_001", petName: "豆豆", petType: "金毛", ownerName: "张女士", service: "皮肤复查", time: "09:30", date: "today", status: "pending", statusText: "待确认" },
+ { id: "apt_002", petName: "咪咪", petType: "英短", ownerName: "李先生", service: "猫癣复诊", time: "10:00", date: "today", status: "confirmed", statusText: "已确认" },
+ { id: "apt_003", petName: "旺财", petType: "柯基", ownerName: "王先生", service: "脓皮症复诊", time: "14:00", date: "today", status: "confirmed", statusText: "已确认" },
+ { id: "apt_004", petName: "小白", petType: "比熊", ownerName: "赵女士", service: "泪痕复查", time: "15:30", date: "today", status: "completed", statusText: "已完成" },
+ { id: "apt_005", petName: "花花", petType: "橘猫", ownerName: "陈女士", service: "皮肤检查", time: "09:00", date: "tomorrow", status: "pending", statusText: "待确认" },
+ { id: "apt_006", petName: "布丁", petType: "泰迪", ownerName: "林先生", service: "过敏检测", time: "11:00", date: "tomorrow", status: "pending", statusText: "待确认" },
+ { id: "apt_007", petName: "lucky", petType: "哈士奇", ownerName: "杨先生", service: "药浴护理", time: "16:00", date: "week", status: "pending", statusText: "待确认" },
+ ];
+ this.processAppointments(mockAppointments);
+ },
+
+ processAppointments(appointments) {
+ const today = new Date();
+ const todayStr = "today";
+ const tomorrowStr = "tomorrow";
+ const weekStr = "week";
+
+ const todayApps = appointments.filter(a => a.date === todayStr);
+ const tomorrowApps = appointments.filter(a => a.date === tomorrowStr);
+ const weekApps = appointments.filter(a => a.date === weekStr);
+
+ const all = [...todayApps, ...tomorrowApps, ...weekApps];
+
+ const stats = {
+ pending: all.filter(a => a.status === "pending").length,
+ confirmed: all.filter(a => a.status === "confirmed").length,
+ completed: all.filter(a => a.status === "completed").length,
+ cancelled: all.filter(a => a.status === "cancelled").length,
+ };
+
+ this.setData({
+ allAppointments: all,
+ dateStats: { today: todayApps.length, tomorrow: tomorrowApps.length, week: weekApps.length },
+ stats,
+ });
+ this.applyDateFilter();
+ },
+
+ selectDate(e) {
+ const date = e.currentTarget.dataset.date;
+ this.setData({ selectedDate: date }, () => this.applyDateFilter());
+ },
+
+ applyDateFilter() {
+ const { selectedDate, allAppointments } = this.data;
+ this.setData({
+ filteredAppointments: allAppointments.filter(a => a.date === selectedDate),
+ });
+ },
+
+ updateAppointmentStatus(id, status, statusText) {
+ const token = wx.getStorageSync("token");
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ if (token) {
+ wx.request({
+ url: `${baseUrl}/api/institution/appointments/${id}/status`,
+ method: "PUT",
+ header: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
+ data: { status, statusText },
+ fail: () => {},
+ });
+ }
+
+ const allAppointments = this.data.allAppointments.map(a => {
+ if (a.id === id) return { ...a, status, statusText };
+ return a;
+ });
+ this.setData({ allAppointments });
+ this.processAppointments(allAppointments);
+ },
+
+ confirmAppointment(e) {
+ const id = e.currentTarget.dataset.id;
+ this.updateAppointmentStatus(id, "confirmed", "已确认");
+ wx.showToast({ title: "已确认预约", icon: "success" });
+ },
+
+ rejectAppointment(e) {
+ const id = e.currentTarget.dataset.id;
+ this.updateAppointmentStatus(id, "cancelled", "已取消");
+ wx.showToast({ title: "已拒绝预约", icon: "none" });
+ },
+
+ completeAppointment(e) {
+ const id = e.currentTarget.dataset.id;
+ this.updateAppointmentStatus(id, "completed", "已完成");
+ wx.showToast({ title: "服务已完成", icon: "success" });
+ },
+});
diff --git a/Front_Page/pages/institution/appointment/appointment.json b/Front_Page/pages/institution/appointment/appointment.json
new file mode 100644
index 00000000..f78e8a4a
--- /dev/null
+++ b/Front_Page/pages/institution/appointment/appointment.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "navigation-bar": "/components/navigation-bar/navigation-bar"
+ },
+ "navigationStyle": "custom"
+}
diff --git a/Front_Page/pages/institution/appointment/appointment.wxml b/Front_Page/pages/institution/appointment/appointment.wxml
new file mode 100644
index 00000000..973bbe89
--- /dev/null
+++ b/Front_Page/pages/institution/appointment/appointment.wxml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+ 今天
+ {{dateStats.today}}
+
+
+ 明天
+ {{dateStats.tomorrow}}
+
+
+ 本周
+ {{dateStats.week}}
+
+
+
+
+
+
+ {{stats.pending}}
+ 待确认
+
+
+ {{stats.confirmed}}
+ 已确认
+
+
+ {{stats.completed}}
+ 已完成
+
+
+ {{stats.cancelled}}
+ 已取消
+
+
+
+
+
+
+
+ {{item.time}}
+
+
+
+ {{item.ownerName}} · {{item.petType}}
+ 服务:{{item.service}}
+
+ 确认
+ 拒绝
+
+
+ 完成服务
+
+
+
+
+ 暂无预约
+
+
+
+
\ No newline at end of file
diff --git a/Front_Page/pages/institution/appointment/appointment.wxss b/Front_Page/pages/institution/appointment/appointment.wxss
new file mode 100644
index 00000000..b0448501
--- /dev/null
+++ b/Front_Page/pages/institution/appointment/appointment.wxss
@@ -0,0 +1,180 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-color: #f5f5f5;
+}
+.scrollarea {
+ flex: 1;
+ overflow-y: auto;
+}
+.container {
+ padding: 0 30rpx 40rpx;
+}
+
+/* 日期栏 */
+.date-bar {
+ display: flex;
+ gap: 16rpx;
+ margin: 20rpx 0;
+}
+.date-item {
+ flex: 1;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 20rpx;
+ text-align: center;
+ box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+.date-item.active {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+}
+.date-item.active .date-day,
+.date-item.active .date-num {
+ color: #fff;
+}
+.date-day {
+ font-size: 26rpx;
+ color: #666;
+ display: block;
+}
+.date-num {
+ font-size: 36rpx;
+ font-weight: bold;
+ color: #667eea;
+ display: block;
+ margin-top: 6rpx;
+}
+
+/* 统计卡片 */
+.stats-cards {
+ display: flex;
+ gap: 16rpx;
+ margin-bottom: 20rpx;
+}
+.stat-mini {
+ flex: 1;
+ background: #fff;
+ border-radius: 12rpx;
+ padding: 16rpx;
+ text-align: center;
+ box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04);
+}
+.stat-mini-num {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+}
+.stat-mini-label {
+ font-size: 20rpx;
+ color: #bbb;
+ display: block;
+ margin-top: 4rpx;
+}
+
+/* 预约列表 */
+.appointment-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+.appointment-card {
+ display: flex;
+ background: #fff;
+ border-radius: 16rpx;
+ overflow: hidden;
+ box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+.apt-time {
+ width: 120rpx;
+ background: #f8faff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+.apt-time-text {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #667eea;
+}
+.apt-body {
+ flex: 1;
+ padding: 20rpx;
+}
+.apt-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 8rpx;
+}
+.apt-pet-name {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+}
+.apt-status {
+ font-size: 20rpx;
+ padding: 4rpx 14rpx;
+ border-radius: 12rpx;
+ background: #fff3e0;
+ color: #ff9800;
+}
+.apt-status.confirmed {
+ background: #e8f5e9;
+ color: #4caf50;
+}
+.apt-status.completed {
+ background: #e3f2fd;
+ color: #2196f3;
+}
+.apt-status.cancelled {
+ background: #ffebee;
+ color: #f44336;
+}
+.apt-owner {
+ font-size: 24rpx;
+ color: #999;
+ display: block;
+ margin-bottom: 4rpx;
+}
+.apt-service {
+ font-size: 24rpx;
+ color: #666;
+ display: block;
+ margin-bottom: 12rpx;
+}
+.apt-actions {
+ display: flex;
+ gap: 12rpx;
+ margin-top: 12rpx;
+}
+.apt-btn {
+ flex: 1;
+ text-align: center;
+ padding: 12rpx 0;
+ border-radius: 20rpx;
+ font-size: 24rpx;
+ font-weight: bold;
+}
+.apt-btn.confirm {
+ background: #667eea;
+ color: #fff;
+}
+.apt-btn.reject {
+ border: 2rpx solid #ff6b6b;
+ color: #ff6b6b;
+}
+.apt-btn.complete {
+ background: #4caf50;
+ color: #fff;
+}
+
+/* 空状态 */
+.empty-state {
+ padding: 80rpx 0;
+ text-align: center;
+ font-size: 28rpx;
+ color: #ccc;
+}
diff --git a/Front_Page/pages/institution/dashboard/dashboard.js b/Front_Page/pages/institution/dashboard/dashboard.js
new file mode 100644
index 00000000..0f62f3d0
--- /dev/null
+++ b/Front_Page/pages/institution/dashboard/dashboard.js
@@ -0,0 +1,138 @@
+Page({
+ data: {
+ instInfo: {
+ name: "",
+ avatar: "",
+ status: "pending",
+ statusText: "待认证",
+ },
+ stats: {
+ pendingPreconsult: 0,
+ todayAppointments: 0,
+ totalReports: 0,
+ },
+ recentReports: [],
+ },
+
+ onLoad() {
+ this.loadInstitutionInfo();
+ this.loadReports();
+ },
+
+ onShow() {
+ this.loadReports();
+ },
+
+ loadInstitutionInfo() {
+ const saved = wx.getStorageSync("institutionRegisterInfo");
+ if (saved) {
+ this.setData({
+ "instInfo.name": saved.name || "未认证机构",
+ "instInfo.status": "pending",
+ "instInfo.statusText": "待认证",
+ });
+ }
+
+ const token = wx.getStorageSync("token");
+ if (!token) return;
+
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ wx.request({
+ url: `${baseUrl}/api/institution/profile`,
+ method: "GET",
+ header: { Authorization: `Bearer ${token}` },
+ success: (res) => {
+ if (res.statusCode === 200 && res.data?.success) {
+ const info = res.data.data;
+ const statusMap = { pending: "待认证", approved: "已认证", rejected: "未通过" };
+ this.setData({
+ instInfo: {
+ name: info.name || "",
+ avatar: info.avatar || "",
+ status: info.status || "pending",
+ statusText: statusMap[info.status] || "待认证",
+ },
+ });
+ }
+ },
+ fail: () => {},
+ });
+ },
+
+ loadReports() {
+ const token = wx.getStorageSync("token");
+ if (!token) {
+ this.loadMockReports();
+ return;
+ }
+
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ wx.request({
+ url: `${baseUrl}/api/institution/reports`,
+ method: "GET",
+ header: { Authorization: `Bearer ${token}` },
+ success: (res) => {
+ if (res.statusCode === 200 && res.data?.success) {
+ const { reports, stats } = res.data.data;
+ this.setData({
+ recentReports: (reports || []).slice(0, 5),
+ stats: stats || this.data.stats,
+ });
+ } else {
+ this.loadMockReports();
+ }
+ },
+ fail: () => this.loadMockReports(),
+ });
+ },
+
+ loadMockReports() {
+ const stored = wx.getStorageSync("institutionMockReports");
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ this.setData({
+ recentReports: parsed.slice(0, 5),
+ stats: {
+ pendingPreconsult: parsed.filter(r => r.status === "pending").length,
+ todayAppointments: 2,
+ totalReports: parsed.length,
+ },
+ });
+ return;
+ }
+
+ const mockReports = [
+ { id: "rpt_001", petName: "豆豆", petType: "金毛", ownerName: "张女士", diseaseName: "湿疹", time: "今天 10:30", status: "pending", statusText: "待处理" },
+ { id: "rpt_002", petName: "咪咪", petType: "英短", ownerName: "李先生", diseaseName: "猫癣", time: "今天 09:15", status: "read", statusText: "已查看" },
+ { id: "rpt_003", petName: "旺财", petType: "柯基", ownerName: "王先生", diseaseName: "脓皮症", time: "昨天 16:45", status: "pending", statusText: "待处理" },
+ { id: "rpt_004", petName: "小白", petType: "比熊", ownerName: "赵女士", diseaseName: "泪痕炎", time: "昨天 14:20", status: "completed", statusText: "已完成" },
+ ];
+
+ wx.setStorageSync("institutionMockReports", JSON.stringify(mockReports));
+ this.setData({
+ recentReports: mockReports,
+ stats: { pendingPreconsult: 2, todayAppointments: 2, totalReports: 4 },
+ });
+ },
+
+ viewPreconsultList() {
+ wx.navigateTo({ url: "/pages/institution/preconsult-list/preconsult-list" });
+ },
+
+ viewAppointments() {
+ wx.navigateTo({ url: "/pages/institution/appointment/appointment" });
+ },
+
+ goToRegister() {
+ wx.navigateTo({ url: "/pages/institution/register/register" });
+ },
+
+ viewReportDetail(e) {
+ const id = e.currentTarget.dataset.id;
+ wx.navigateTo({ url: `/pages/institution/preconsult-detail/preconsult-detail?id=${id}` });
+ },
+});
diff --git a/Front_Page/pages/institution/dashboard/dashboard.json b/Front_Page/pages/institution/dashboard/dashboard.json
new file mode 100644
index 00000000..f78e8a4a
--- /dev/null
+++ b/Front_Page/pages/institution/dashboard/dashboard.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "navigation-bar": "/components/navigation-bar/navigation-bar"
+ },
+ "navigationStyle": "custom"
+}
diff --git a/Front_Page/pages/institution/dashboard/dashboard.wxml b/Front_Page/pages/institution/dashboard/dashboard.wxml
new file mode 100644
index 00000000..a551288f
--- /dev/null
+++ b/Front_Page/pages/institution/dashboard/dashboard.wxml
@@ -0,0 +1,75 @@
+
+
+
+
+
+
+
+
+
+ {{stats.pendingPreconsult}}
+ 待预问诊
+
+
+ {{stats.todayAppointments}}
+ 今日预约
+
+
+ {{stats.totalReports}}
+ AI报告
+
+
+
+
+
+
+ 🤖
+ AI预问诊
+ 查看AI诊断报告
+
+
+ 📅
+ 预约管理
+ 处理预约请求
+
+
+ 🏪
+ 机构入驻
+ 完善机构信息
+
+
+
+
+
+
+
+
+
+ {{item.diseaseName}}
+
+
+
+ 暂无AI预问诊报告
+ 当宠主发送诊断报告后,将显示在此处
+
+
+
+
+
\ No newline at end of file
diff --git a/Front_Page/pages/institution/dashboard/dashboard.wxss b/Front_Page/pages/institution/dashboard/dashboard.wxss
new file mode 100644
index 00000000..a5cbdab5
--- /dev/null
+++ b/Front_Page/pages/institution/dashboard/dashboard.wxss
@@ -0,0 +1,226 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-color: #f5f5f5;
+}
+.scrollarea {
+ flex: 1;
+ overflow-y: auto;
+}
+.container {
+ padding: 0 30rpx 40rpx;
+}
+
+/* 机构信息 */
+.inst-header {
+ display: flex;
+ align-items: center;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin: 20rpx 0;
+}
+.inst-avatar {
+ width: 100rpx;
+ height: 100rpx;
+ border-radius: 50%;
+ overflow: hidden;
+ background: rgba(255,255,255,0.2);
+ margin-right: 24rpx;
+ flex-shrink: 0;
+}
+.inst-avatar image {
+ width: 100%;
+ height: 100%;
+}
+.inst-info {
+ flex: 1;
+}
+.inst-name {
+ font-size: 34rpx;
+ font-weight: bold;
+ color: #fff;
+ display: block;
+ margin-bottom: 8rpx;
+}
+.inst-status {
+ display: inline-block;
+ font-size: 22rpx;
+ padding: 4rpx 16rpx;
+ border-radius: 20rpx;
+ background: rgba(255,255,255,0.2);
+ color: #fff;
+}
+
+/* 数据看板 */
+.stats-grid {
+ display: flex;
+ gap: 20rpx;
+ margin-bottom: 20rpx;
+}
+.stat-card {
+ flex: 1;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx 16rpx;
+ text-align: center;
+ box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+.stat-card:active {
+ transform: scale(0.97);
+}
+.stat-number {
+ font-size: 44rpx;
+ font-weight: bold;
+ color: #667eea;
+ display: block;
+}
+.stat-card.pending .stat-number {
+ color: #FFB84D;
+}
+.stat-card.followup .stat-number {
+ color: #4caf50;
+}
+.stat-label {
+ font-size: 24rpx;
+ color: #999;
+ display: block;
+ margin-top: 8rpx;
+}
+
+/* 快捷导航 */
+.quick-nav {
+ display: flex;
+ gap: 20rpx;
+ margin-bottom: 20rpx;
+}
+.nav-item {
+ flex: 1;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx 16rpx;
+ text-align: center;
+ box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+.nav-item:active {
+ transform: scale(0.97);
+}
+.nav-icon {
+ font-size: 48rpx;
+ margin-bottom: 12rpx;
+}
+.nav-name {
+ font-size: 26rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+}
+.nav-desc {
+ font-size: 20rpx;
+ color: #bbb;
+ display: block;
+ margin-top: 4rpx;
+}
+
+/* 区域 */
+.section {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 24rpx;
+}
+.section-title {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+}
+.section-more {
+ font-size: 26rpx;
+ color: #667eea;
+}
+
+/* 报告列表 */
+.report-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+.report-card {
+ background: #f8faff;
+ border-radius: 16rpx;
+ padding: 20rpx;
+ border: 1rpx solid #eef4ff;
+}
+.report-card:active {
+ transform: scale(0.98);
+}
+.report-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 10rpx;
+}
+.report-pet-name {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+}
+.report-time {
+ font-size: 22rpx;
+ color: #bbb;
+}
+.report-disease {
+ font-size: 26rpx;
+ color: #666;
+ display: block;
+ margin-bottom: 12rpx;
+}
+.report-footer {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+.report-owner {
+ font-size: 22rpx;
+ color: #999;
+}
+.report-tag {
+ font-size: 20rpx;
+ padding: 4rpx 14rpx;
+ border-radius: 12rpx;
+ background: #fff3e0;
+ color: #ff9800;
+}
+.report-tag.read {
+ background: #e8f5e9;
+ color: #4caf50;
+}
+.report-tag.completed {
+ background: #e3f2fd;
+ color: #2196f3;
+}
+
+/* 空状态 */
+.empty-state {
+ padding: 60rpx 0;
+ text-align: center;
+}
+.empty-text {
+ font-size: 30rpx;
+ color: #999;
+ display: block;
+ margin-bottom: 12rpx;
+}
+.empty-hint {
+ font-size: 24rpx;
+ color: #ccc;
+ display: block;
+}
diff --git a/Front_Page/pages/institution/preconsult-detail/preconsult-detail.js b/Front_Page/pages/institution/preconsult-detail/preconsult-detail.js
new file mode 100644
index 00000000..df909f9c
--- /dev/null
+++ b/Front_Page/pages/institution/preconsult-detail/preconsult-detail.js
@@ -0,0 +1,163 @@
+Page({
+ data: {
+ reportId: "",
+ report: {
+ petName: "",
+ petAvatar: "",
+ petType: "",
+ petBreed: "",
+ ownerName: "",
+ ownerPhone: "",
+ diseaseName: "",
+ severity: "",
+ confidence: 0,
+ status: "pending",
+ statusText: "待处理",
+ aiSummary: "",
+ symptoms: [],
+ affectedAreas: [],
+ weight: "",
+ allergies: "",
+ sterilized: "",
+ recentContact: "",
+ images: [],
+ time: "",
+ },
+ },
+
+ onLoad(options) {
+ const id = options.id || "";
+ this.setData({ reportId: id });
+ this.loadReportDetail(id);
+ },
+
+ loadReportDetail(id) {
+ const token = wx.getStorageSync("token");
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ if (token) {
+ wx.request({
+ url: `${baseUrl}/api/institution/reports/${id}`,
+ method: "GET",
+ header: { Authorization: `Bearer ${token}` },
+ success: (res) => {
+ if (res.statusCode === 200 && res.data?.success) {
+ this.setData({ report: res.data.data });
+ return;
+ }
+ this.loadMockDetail(id);
+ },
+ fail: () => this.loadMockDetail(id),
+ });
+ } else {
+ this.loadMockDetail(id);
+ }
+ },
+
+ loadMockDetail(id) {
+ const mockDB = {
+ rpt_001: {
+ id: "rpt_001", petName: "豆豆", petAvatar: "", petType: "犬", petBreed: "金毛", ownerName: "张女士", ownerPhone: "138****1234",
+ diseaseName: "湿疹", severity: "中", confidence: 85, status: "pending", statusText: "待处理",
+ aiSummary: "患宠:豆豆(犬),体重28.5kg。主要诊断:湿疹。症状表现:红斑、瘙痒、掉毛。患病部位:背部、腹部。建议进行过敏原检测并保持环境干燥。",
+ symptoms: ["红斑", "瘙痒", "掉毛"], affectedAreas: ["背部", "腹部"], weight: "28.5kg", allergies: "花粉", sterilized: "是", recentContact: "近期去过宠物公园",
+ images: [], time: "今天 10:30",
+ },
+ rpt_002: {
+ id: "rpt_002", petName: "咪咪", petAvatar: "", petType: "猫", petBreed: "英短", ownerName: "李先生", ownerPhone: "139****5678",
+ diseaseName: "猫癣", severity: "轻", confidence: 92, status: "read", statusText: "已查看",
+ aiSummary: "患宠:咪咪(猫),体重4.2kg。猫癣已治愈。面部出现掉毛、结痂症状。",
+ symptoms: ["掉毛", "结痂"], affectedAreas: ["面部"], weight: "4.2kg", allergies: "无", sterilized: "是", recentContact: "无",
+ images: [], time: "今天 09:15",
+ },
+ rpt_003: {
+ id: "rpt_003", petName: "旺财", petAvatar: "", petType: "犬", petBreed: "柯基", ownerName: "王先生", ownerPhone: "137****9012",
+ diseaseName: "脓皮症", severity: "重", confidence: 78, status: "pending", statusText: "待处理",
+ aiSummary: "患宠:旺财(犬),体重12kg。诊断为脓皮症。症状:渗液、结痂、红斑、瘙痒。患病部位:全身、爪缝。有食物过敏史,复发风险较高。",
+ symptoms: ["渗液", "结痂", "红斑", "瘙痒"], affectedAreas: ["全身", "爪缝"], weight: "12.0kg", allergies: "食物过敏", sterilized: "否", recentContact: "家中新养了一只猫",
+ images: [], time: "昨天 16:45",
+ },
+ rpt_004: {
+ id: "rpt_004", petName: "小白", petAvatar: "", petType: "犬", petBreed: "比熊", ownerName: "赵女士", ownerPhone: "136****3456",
+ diseaseName: "泪痕炎", severity: "轻", confidence: 95, status: "completed", statusText: "已完成",
+ aiSummary: "患宠:小白(犬),体重5.8kg。泪痕炎已治愈。建议定期清洁眼部。",
+ symptoms: ["红斑"], affectedAreas: ["面部"], weight: "5.8kg", allergies: "无", sterilized: "是", recentContact: "无",
+ images: [], time: "昨天 14:20",
+ },
+ rpt_005: {
+ id: "rpt_005", petName: "小黑", petAvatar: "", petType: "猫", petBreed: "田园猫", ownerName: "刘女士", ownerPhone: "135****6789",
+ diseaseName: "猫藓", severity: "中", confidence: 82, status: "pending", statusText: "待处理",
+ aiSummary: "患宠:小黑(猫),面部出现红斑脱毛,疑似猫藓。建议进行真菌培养确诊。",
+ symptoms: ["红斑", "脱毛"], affectedAreas: ["面部"], weight: "3.8kg", allergies: "无", sterilized: "否", recentContact: "曾接触流浪猫",
+ images: [], time: "前天 11:00",
+ },
+ };
+
+ const detail = mockDB[id];
+ if (detail) {
+ this.setData({ report: detail });
+ wx.showToast({ title: "加载模拟数据", icon: "none" });
+ } else {
+ wx.showToast({ title: "报告不存在", icon: "none" });
+ }
+ },
+
+ severityClass(severity) {
+ if (severity === "轻" || severity === "轻度") return "light";
+ if (severity === "中" || severity === "中度") return "medium";
+ if (severity === "重" || severity === "重度") return "heavy";
+ return "";
+ },
+
+ markAsRead() {
+ const token = wx.getStorageSync("token");
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ wx.showLoading({ title: "更新中...", mask: true });
+
+ if (token) {
+ wx.request({
+ url: `${baseUrl}/api/institution/reports/${this.data.reportId}/read`,
+ method: "PUT",
+ header: { Authorization: `Bearer ${token}` },
+ success: (res) => {
+ wx.hideLoading();
+ if (res.statusCode === 200 && res.data?.success) {
+ this.setData({ "report.status": "read", "report.statusText": "已查看" });
+ wx.showToast({ title: "已标记", icon: "success" });
+ }
+ },
+ fail: () => {
+ wx.hideLoading();
+ this.setData({ "report.status": "read", "report.statusText": "已查看" });
+ wx.showToast({ title: "已标记(离线)", icon: "success" });
+ },
+ });
+ } else {
+ wx.hideLoading();
+ this.setData({ "report.status": "read", "report.statusText": "已查看" });
+ wx.showToast({ title: "已标记(离线)", icon: "success" });
+ }
+ },
+
+ contactOwner() {
+ const phone = this.data.report.ownerPhone;
+ if (phone) {
+ wx.makePhoneCall({ phoneNumber: phone });
+ } else {
+ wx.showToast({ title: "暂无联系方式", icon: "none" });
+ }
+ },
+
+ addFollowUp() {
+ wx.showToast({ title: "复诊提醒已添加", icon: "success" });
+ },
+
+ previewImage(e) {
+ const url = e.currentTarget.dataset.url;
+ const urls = this.data.report.images;
+ wx.previewImage({ current: url, urls });
+ },
+});
diff --git a/Front_Page/pages/institution/preconsult-detail/preconsult-detail.json b/Front_Page/pages/institution/preconsult-detail/preconsult-detail.json
new file mode 100644
index 00000000..f78e8a4a
--- /dev/null
+++ b/Front_Page/pages/institution/preconsult-detail/preconsult-detail.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "navigation-bar": "/components/navigation-bar/navigation-bar"
+ },
+ "navigationStyle": "custom"
+}
diff --git a/Front_Page/pages/institution/preconsult-detail/preconsult-detail.wxml b/Front_Page/pages/institution/preconsult-detail/preconsult-detail.wxml
new file mode 100644
index 00000000..e0fa78f6
--- /dev/null
+++ b/Front_Page/pages/institution/preconsult-detail/preconsult-detail.wxml
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+ 诊断疾病
+ {{report.diseaseName}}
+
+
+ 严重程度
+ {{report.severity || '待评估'}}
+
+
+ 可信度
+
+
+
+
+ {{report.confidence}}%
+
+
+
+
+ AI分析总结
+ {{report.aiSummary}}
+
+
+
+
+
+ 症状表现
+
+ {{item}}
+
+
+
+
+
+ 患病部位
+
+ {{item}}
+
+
+
+
+
+ 宠物档案
+
+
+ 体重
+ {{report.weight || '未知'}}
+
+
+ 过敏史
+ {{report.allergies || '无'}}
+
+
+ 绝育
+ {{report.sterilized || '未知'}}
+
+
+ 近期接触
+ {{report.recentContact || '无异常'}}
+
+
+
+
+
+
+ 患处照片
+
+
+
+
+
+
+
+
+ 标记已查看
+
+
+ 联系宠主
+
+
+ 添加复诊提醒
+
+
+
+
\ No newline at end of file
diff --git a/Front_Page/pages/institution/preconsult-detail/preconsult-detail.wxss b/Front_Page/pages/institution/preconsult-detail/preconsult-detail.wxss
new file mode 100644
index 00000000..6b62bfe2
--- /dev/null
+++ b/Front_Page/pages/institution/preconsult-detail/preconsult-detail.wxss
@@ -0,0 +1,290 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-color: #f5f5f5;
+}
+.scrollarea {
+ flex: 1;
+ overflow-y: auto;
+}
+.container {
+ padding: 0 30rpx 40rpx;
+ padding-bottom: 160rpx;
+}
+
+/* 报告头部 */
+.report-header-card {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin: 20rpx 0;
+}
+.report-pet-info {
+ display: flex;
+ align-items: center;
+ margin-bottom: 20rpx;
+}
+.report-pet-avatar {
+ width: 100rpx;
+ height: 100rpx;
+ border-radius: 50%;
+ background: rgba(255,255,255,0.2);
+ margin-right: 20rpx;
+ flex-shrink: 0;
+}
+.report-pet-detail {
+ flex: 1;
+}
+.report-pet-name {
+ font-size: 36rpx;
+ font-weight: bold;
+ color: #fff;
+ display: block;
+}
+.report-pet-breed {
+ font-size: 24rpx;
+ color: rgba(255,255,255,0.8);
+ display: block;
+ margin-top: 4rpx;
+}
+.report-owner-info {
+ display: flex;
+ align-items: center;
+ gap: 12rpx;
+}
+.report-owner-label {
+ font-size: 22rpx;
+ color: rgba(255,255,255,0.6);
+}
+.report-owner-name {
+ font-size: 26rpx;
+ color: #fff;
+ font-weight: bold;
+}
+.report-owner-phone {
+ font-size: 24rpx;
+ color: rgba(255,255,255,0.8);
+}
+
+/* 通用区域 */
+.section {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin-bottom: 20rpx;
+ box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+.section-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20rpx;
+}
+.section-title {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+ margin-bottom: 20rpx;
+}
+.section-header .section-title {
+ margin-bottom: 0;
+}
+.report-badge {
+ font-size: 22rpx;
+ padding: 6rpx 20rpx;
+ border-radius: 20rpx;
+ background: #fff3e0;
+ color: #ff9800;
+}
+.report-badge.read {
+ background: #e8f5e9;
+ color: #4caf50;
+}
+.report-badge.completed {
+ background: #e3f2fd;
+ color: #2196f3;
+}
+
+/* 诊断卡片 */
+.diagnosis-card {
+ background: #f8faff;
+ border-radius: 16rpx;
+ padding: 20rpx;
+ margin-bottom: 16rpx;
+}
+.diag-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 16rpx;
+}
+.diag-row:last-child {
+ margin-bottom: 0;
+}
+.diag-label {
+ font-size: 26rpx;
+ color: #999;
+ width: 140rpx;
+ flex-shrink: 0;
+}
+.diag-value {
+ font-size: 30rpx;
+ font-weight: bold;
+ color: #333;
+}
+.diag-value.highlight {
+ color: #667eea;
+ font-size: 34rpx;
+}
+
+/* 严重程度 */
+.severity-tag {
+ font-size: 24rpx;
+ padding: 4rpx 20rpx;
+ border-radius: 20rpx;
+ background: #f0f0f0;
+ color: #666;
+}
+.severity-tag.light {
+ background: #e8f5e9;
+ color: #4caf50;
+}
+.severity-tag.medium {
+ background: #fff3e0;
+ color: #ff9800;
+}
+.severity-tag.heavy {
+ background: #ffebee;
+ color: #f44336;
+}
+
+/* 可信度 */
+.confidence-bar-container {
+ flex: 1;
+ display: flex;
+ align-items: center;
+}
+.confidence-bar-bg {
+ flex: 1;
+ height: 14rpx;
+ background: #eee;
+ border-radius: 7rpx;
+ overflow: hidden;
+ margin-right: 12rpx;
+}
+.confidence-bar {
+ height: 100%;
+ background: linear-gradient(90deg, #667eea, #764ba2);
+ border-radius: 7rpx;
+}
+.confidence-value {
+ font-size: 26rpx;
+ color: #667eea;
+ font-weight: bold;
+ min-width: 70rpx;
+ text-align: right;
+}
+
+/* AI摘要 */
+.ai-summary {
+ background: #f0f4ff;
+ border-radius: 12rpx;
+ padding: 16rpx;
+ border-left: 4rpx solid #667eea;
+}
+.summary-label {
+ font-size: 24rpx;
+ color: #667eea;
+ font-weight: bold;
+ display: block;
+ margin-bottom: 8rpx;
+}
+.summary-text {
+ font-size: 26rpx;
+ color: #555;
+ line-height: 1.6;
+}
+
+/* 标签 */
+.symptom-tags, .area-tags {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 12rpx;
+}
+.symptom-tag, .area-tag {
+ font-size: 24rpx;
+ padding: 8rpx 20rpx;
+ border-radius: 20rpx;
+ background: #f0f4ff;
+ color: #667eea;
+ border: 1rpx solid #dde8ff;
+}
+
+/* 宠物档案 */
+.profile-grid {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 16rpx;
+}
+.profile-item {
+ background: #f8f9fa;
+ border-radius: 12rpx;
+ padding: 16rpx;
+}
+.profile-label {
+ font-size: 22rpx;
+ color: #999;
+ display: block;
+ margin-bottom: 6rpx;
+}
+.profile-value {
+ font-size: 26rpx;
+ color: #333;
+ font-weight: 500;
+ display: block;
+}
+
+/* 图片 */
+.image-grid {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16rpx;
+}
+.grid-image {
+ width: calc(33.33% - 12rpx);
+ height: 200rpx;
+ border-radius: 12rpx;
+ background: #f0f0f0;
+}
+
+/* 操作按钮 */
+.action-bar {
+ display: flex;
+ gap: 20rpx;
+ margin-top: 10rpx;
+ padding-bottom: 20rpx;
+}
+.action-btn {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 22rpx 0;
+ border-radius: 40rpx;
+ font-size: 28rpx;
+ font-weight: bold;
+}
+.action-btn.primary {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: #fff;
+ box-shadow: 0 4rpx 20rpx rgba(102,126,234,0.3);
+}
+.action-btn.outline {
+ border: 2rpx solid #667eea;
+ color: #667eea;
+ background: #fff;
+}
+.action-btn:active {
+ transform: scale(0.97);
+}
diff --git a/Front_Page/pages/institution/preconsult-list/preconsult-list.js b/Front_Page/pages/institution/preconsult-list/preconsult-list.js
new file mode 100644
index 00000000..8b48b422
--- /dev/null
+++ b/Front_Page/pages/institution/preconsult-list/preconsult-list.js
@@ -0,0 +1,78 @@
+Page({
+ data: {
+ filter: "all",
+ allReports: [],
+ filteredReports: [],
+ },
+
+ onLoad() {
+ this.loadReports();
+ },
+
+ onShow() {
+ this.loadReports();
+ },
+
+ loadReports() {
+ const token = wx.getStorageSync("token");
+ const app = getApp();
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ if (token) {
+ wx.request({
+ url: `${baseUrl}/api/institution/reports`,
+ method: "GET",
+ header: { Authorization: `Bearer ${token}` },
+ success: (res) => {
+ if (res.statusCode === 200 && res.data?.success) {
+ this.setData({ allReports: res.data.data.reports || [] });
+ this.applyFilter();
+ return;
+ }
+ this.loadMockReports();
+ },
+ fail: () => this.loadMockReports(),
+ });
+ } else {
+ this.loadMockReports();
+ }
+ },
+
+ loadMockReports() {
+ const stored = wx.getStorageSync("institutionMockReports");
+ let reports;
+ if (stored) {
+ reports = JSON.parse(stored);
+ } else {
+ reports = [
+ { id: "rpt_001", petName: "豆豆", petType: "金毛", ownerName: "张女士", diseaseName: "湿疹", severity: "中", time: "今天 10:30", status: "pending", statusText: "待处理", aiSummary: "患宠:豆豆(犬),体重28.5kg,诊断为湿疹,症状包括红斑、瘙痒、掉毛。" },
+ { id: "rpt_002", petName: "咪咪", petType: "英短", ownerName: "李先生", diseaseName: "猫癣", severity: "轻", time: "今天 09:15", status: "read", statusText: "已查看", aiSummary: "患宠:咪咪(猫),体重4.2kg,猫癣已治愈。" },
+ { id: "rpt_003", petName: "旺财", petType: "柯基", ownerName: "王先生", diseaseName: "脓皮症", severity: "重", time: "昨天 16:45", status: "pending", statusText: "待处理", aiSummary: "患宠:旺财(犬),体重12kg,诊断为脓皮症,有食物过敏史。" },
+ { id: "rpt_004", petName: "小白", petType: "比熊", ownerName: "赵女士", diseaseName: "泪痕炎", severity: "轻", time: "昨天 14:20", status: "completed", statusText: "已完成" },
+ { id: "rpt_005", petName: "小黑", petType: "田园猫", ownerName: "刘女士", diseaseName: "猫藓", severity: "中", time: "前天 11:00", status: "pending", statusText: "待处理", aiSummary: "患宠:小黑(猫),面部出现红斑脱毛,疑似猫藓。" },
+ ];
+ wx.setStorageSync("institutionMockReports", JSON.stringify(reports));
+ }
+ this.setData({ allReports: reports });
+ this.applyFilter();
+ },
+
+ setFilter(e) {
+ const filter = e.currentTarget.dataset.filter;
+ this.setData({ filter }, () => this.applyFilter());
+ },
+
+ applyFilter() {
+ const { filter, allReports } = this.data;
+ if (filter === "all") {
+ this.setData({ filteredReports: allReports });
+ } else {
+ this.setData({ filteredReports: allReports.filter(r => r.status === filter) });
+ }
+ },
+
+ viewDetail(e) {
+ const id = e.currentTarget.dataset.id;
+ wx.navigateTo({ url: `/pages/institution/preconsult-detail/preconsult-detail?id=${id}` });
+ },
+});
diff --git a/Front_Page/pages/institution/preconsult-list/preconsult-list.json b/Front_Page/pages/institution/preconsult-list/preconsult-list.json
new file mode 100644
index 00000000..f78e8a4a
--- /dev/null
+++ b/Front_Page/pages/institution/preconsult-list/preconsult-list.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "navigation-bar": "/components/navigation-bar/navigation-bar"
+ },
+ "navigationStyle": "custom"
+}
diff --git a/Front_Page/pages/institution/preconsult-list/preconsult-list.wxml b/Front_Page/pages/institution/preconsult-list/preconsult-list.wxml
new file mode 100644
index 00000000..51fd03e2
--- /dev/null
+++ b/Front_Page/pages/institution/preconsult-list/preconsult-list.wxml
@@ -0,0 +1,35 @@
+
+
+
+
+
+ 全部
+ 待处理
+ 已查看
+ 已完成
+
+
+
+
+
+
+
+ {{item.petName}}
+ {{item.diseaseName}}
+
+ {{item.statusText}}
+
+
+ {{item.ownerName}}
+ {{item.time}}
+
+
+ AI摘要:{{item.aiSummary}}
+
+
+
+ 暂无{{filter === 'all' ? '' : '符合条件的'}}预问诊报告
+
+
+
+
\ No newline at end of file
diff --git a/Front_Page/pages/institution/preconsult-list/preconsult-list.wxss b/Front_Page/pages/institution/preconsult-list/preconsult-list.wxss
new file mode 100644
index 00000000..d0bfdab0
--- /dev/null
+++ b/Front_Page/pages/institution/preconsult-list/preconsult-list.wxss
@@ -0,0 +1,120 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-color: #f5f5f5;
+}
+.scrollarea {
+ flex: 1;
+ overflow-y: auto;
+}
+.container {
+ padding: 0 30rpx 40rpx;
+}
+
+/* 筛选栏 */
+.filter-bar {
+ display: flex;
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 12rpx;
+ margin: 20rpx 0;
+ box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+.filter-item {
+ flex: 1;
+ text-align: center;
+ padding: 12rpx 0;
+ font-size: 26rpx;
+ color: #999;
+ border-radius: 12rpx;
+}
+.filter-item.active {
+ background: #667eea;
+ color: #fff;
+ font-weight: bold;
+}
+
+/* 报告列表 */
+.report-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16rpx;
+}
+.report-card {
+ background: #fff;
+ border-radius: 16rpx;
+ padding: 24rpx;
+ box-shadow: 0 2rpx 10rpx rgba(0,0,0,0.05);
+}
+.report-card:active {
+ transform: scale(0.98);
+}
+.card-top {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 12rpx;
+}
+.card-info {
+ flex: 1;
+}
+.card-pet-name {
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+}
+.card-disease {
+ font-size: 26rpx;
+ color: #666;
+ display: block;
+ margin-top: 4rpx;
+}
+.card-status {
+ font-size: 22rpx;
+ padding: 6rpx 18rpx;
+ border-radius: 20rpx;
+ flex-shrink: 0;
+ background: #fff3e0;
+ color: #ff9800;
+}
+.card-status.read {
+ background: #e8f5e9;
+ color: #4caf50;
+}
+.card-status.completed {
+ background: #e3f2fd;
+ color: #2196f3;
+}
+.card-mid {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: 10rpx;
+}
+.card-owner {
+ font-size: 24rpx;
+ color: #999;
+}
+.card-time {
+ font-size: 22rpx;
+ color: #bbb;
+}
+.card-bottom {
+ background: #f8faff;
+ border-radius: 10rpx;
+ padding: 14rpx;
+}
+.card-summary {
+ font-size: 24rpx;
+ color: #666;
+ line-height: 1.5;
+}
+
+/* 空状态 */
+.empty-state {
+ padding: 80rpx 0;
+ text-align: center;
+ font-size: 28rpx;
+ color: #ccc;
+}
diff --git a/Front_Page/pages/institution/register/register.js b/Front_Page/pages/institution/register/register.js
new file mode 100644
index 00000000..890f5258
--- /dev/null
+++ b/Front_Page/pages/institution/register/register.js
@@ -0,0 +1,124 @@
+Page({
+ data: {
+ formData: {
+ name: "",
+ type: "hospital",
+ contact: "",
+ phone: "",
+ address: "",
+ description: "",
+ licenseUrl: "",
+ agreed: false,
+ },
+ },
+
+ onInput(e) {
+ const field = e.currentTarget.dataset.field;
+ const value = e.detail.value;
+ this.setData({
+ [`formData.${field}`]: value,
+ });
+ },
+
+ selectType(e) {
+ const type = e.currentTarget.dataset.type;
+ this.setData({
+ "formData.type": type,
+ });
+ },
+
+ toggleAgreement() {
+ this.setData({
+ "formData.agreed": !this.data.formData.agreed,
+ });
+ },
+
+ uploadLicense() {
+ wx.chooseImage({
+ count: 1,
+ sizeType: ["compressed"],
+ sourceType: ["album", "camera"],
+ success: (res) => {
+ const tempPath = res.tempFilePaths[0];
+ wx.showLoading({ title: "上传中...", mask: true });
+
+ const app = getApp();
+ const token = wx.getStorageSync("token");
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ wx.uploadFile({
+ url: `${baseUrl}/api/upload`,
+ filePath: tempPath,
+ name: "file",
+ formData: { type: "license" },
+ header: { Authorization: `Bearer ${token}` },
+ success: (uploadRes) => {
+ wx.hideLoading();
+ try {
+ const data = JSON.parse(uploadRes.data);
+ if (data.success && data.data?.url) {
+ this.setData({ "formData.licenseUrl": data.data.url });
+ }
+ } catch (e) {
+ this.setData({ "formData.licenseUrl": tempPath });
+ }
+ },
+ fail: () => {
+ wx.hideLoading();
+ this.setData({ "formData.licenseUrl": tempPath });
+ wx.showToast({ title: "已使用本地图片", icon: "none" });
+ },
+ });
+ },
+ });
+ },
+
+ submitRegistration() {
+ const { formData } = this.data;
+ if (!formData.name || !formData.contact || !formData.phone) {
+ wx.showToast({ title: "请填写必填信息", icon: "none" });
+ return;
+ }
+ if (!formData.agreed) {
+ wx.showToast({ title: "请阅读并同意入驻协议", icon: "none" });
+ return;
+ }
+
+ wx.showLoading({ title: "提交中...", mask: true });
+
+ const app = getApp();
+ const token = wx.getStorageSync("token");
+ const baseUrl = (app && (app.globalData.baseURL || app.globalData.baseUrl)) || "https://petderma.onrender.com";
+
+ wx.request({
+ url: `${baseUrl}/api/institution/register`,
+ method: "POST",
+ header: {
+ Authorization: `Bearer ${token}`,
+ "content-type": "application/json",
+ },
+ data: formData,
+ success: (res) => {
+ wx.hideLoading();
+ if (res.statusCode === 200 && res.data?.success) {
+ wx.showToast({ title: "入驻申请已提交", icon: "success" });
+ wx.setStorageSync("institutionRegisterInfo", formData);
+ setTimeout(() => wx.navigateBack(), 1500);
+ } else if (res.statusCode === 404) {
+ wx.hideLoading();
+ wx.showToast({ title: "申请已提交(离线)", icon: "success" });
+ wx.setStorageSync("institutionRegisterInfo", formData);
+ setTimeout(() => wx.navigateBack(), 1500);
+ } else {
+ wx.showToast({ title: res.data?.message || "提交失败,请重试", icon: "none" });
+ }
+ },
+ fail: () => {
+ wx.hideLoading();
+ wx.showToast({ title: "申请已提交(离线模式)", icon: "success" });
+ wx.setStorageSync("institutionRegisterInfo", formData);
+ setTimeout(() => wx.navigateBack(), 1500);
+ },
+ });
+ },
+});
diff --git a/Front_Page/pages/institution/register/register.json b/Front_Page/pages/institution/register/register.json
new file mode 100644
index 00000000..f78e8a4a
--- /dev/null
+++ b/Front_Page/pages/institution/register/register.json
@@ -0,0 +1,6 @@
+{
+ "usingComponents": {
+ "navigation-bar": "/components/navigation-bar/navigation-bar"
+ },
+ "navigationStyle": "custom"
+}
diff --git a/Front_Page/pages/institution/register/register.wxml b/Front_Page/pages/institution/register/register.wxml
new file mode 100644
index 00000000..8844f766
--- /dev/null
+++ b/Front_Page/pages/institution/register/register.wxml
@@ -0,0 +1,68 @@
+
+
+
+
+
+ 加入 PetDerma 机构端
+ 为宠物提供专业的皮肤健康服务,获取更多线上客户
+
+
+
+
+
+ 机构名称
+
+
+
+ 机构类型
+
+
+ 宠物医院
+
+
+ 宠物诊所
+
+
+ 护理店
+
+
+
+
+ 联系人
+
+
+
+ 联系电话
+
+
+
+ 机构地址
+
+
+
+ 机构简介
+
+
+
+ 执业许可证
+
+
+
+ +
+ 上传许可证照片
+
+
+
+
+
+
+
+ 提交入驻申请
+
+
+
+ 我已阅读并同意 《PetDerma机构入驻协议》
+
+
+
+
\ No newline at end of file
diff --git a/Front_Page/pages/institution/register/register.wxss b/Front_Page/pages/institution/register/register.wxss
new file mode 100644
index 00000000..55eddc5c
--- /dev/null
+++ b/Front_Page/pages/institution/register/register.wxss
@@ -0,0 +1,181 @@
+page {
+ height: 100vh;
+ display: flex;
+ flex-direction: column;
+ background-color: #f5f5f5;
+}
+.scrollarea {
+ flex: 1;
+ overflow-y: auto;
+}
+.container {
+ padding: 0 30rpx 40rpx;
+}
+
+/* 引导区域 */
+.guide-section {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 20rpx;
+ padding: 40rpx 30rpx;
+ margin: 20rpx 0;
+ text-align: center;
+}
+.guide-title {
+ font-size: 36rpx;
+ font-weight: bold;
+ color: #fff;
+ display: block;
+ margin-bottom: 12rpx;
+}
+.guide-desc {
+ font-size: 26rpx;
+ color: rgba(255,255,255,0.85);
+ display: block;
+ line-height: 1.5;
+}
+
+/* 表单区域 */
+.form-section {
+ background: #fff;
+ border-radius: 20rpx;
+ padding: 30rpx;
+ margin: 20rpx 0;
+}
+.form-item {
+ margin-bottom: 30rpx;
+}
+.form-label {
+ font-size: 28rpx;
+ font-weight: bold;
+ color: #333;
+ display: block;
+ margin-bottom: 12rpx;
+}
+.form-input {
+ width: 100%;
+ height: 72rpx;
+ background: #f8f9fa;
+ border-radius: 12rpx;
+ padding: 0 20rpx;
+ font-size: 26rpx;
+ box-sizing: border-box;
+ border: 2rpx solid transparent;
+}
+.form-input:focus {
+ border-color: #667eea;
+ background: #fff;
+}
+.form-textarea {
+ width: 100%;
+ height: 160rpx;
+ background: #f8f9fa;
+ border-radius: 12rpx;
+ padding: 16rpx 20rpx;
+ font-size: 26rpx;
+ box-sizing: border-box;
+ border: 2rpx solid transparent;
+}
+.form-textarea:focus {
+ border-color: #667eea;
+ background: #fff;
+}
+
+/* 选项 */
+.form-options {
+ display: flex;
+ gap: 16rpx;
+}
+.form-option {
+ flex: 1;
+ padding: 18rpx 0;
+ text-align: center;
+ border: 2rpx solid #e0e0e0;
+ border-radius: 12rpx;
+ font-size: 26rpx;
+ color: #666;
+}
+.form-option.active {
+ border-color: #667eea;
+ color: #667eea;
+ background: rgba(102, 126, 234, 0.05);
+}
+
+/* 上传 */
+.upload-area {
+ width: 200rpx;
+ height: 200rpx;
+ background: #f8f9fa;
+ border: 2rpx dashed #ddd;
+ border-radius: 12rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+.license-preview {
+ width: 100%;
+ height: 100%;
+}
+.upload-placeholder {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+.upload-icon {
+ font-size: 48rpx;
+ color: #ccc;
+}
+.upload-text {
+ font-size: 22rpx;
+ color: #bbb;
+ margin-top: 8rpx;
+}
+
+/* 提交区域 */
+.submit-section {
+ margin-top: 20rpx;
+}
+.btn-submit {
+ height: 88rpx;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border-radius: 44rpx;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 32rpx;
+ font-weight: bold;
+ color: #fff;
+ box-shadow: 0 4rpx 20rpx rgba(102, 126, 234, 0.3);
+}
+.btn-submit.disabled {
+ opacity: 0.5;
+}
+.btn-submit:active {
+ transform: scale(0.98);
+}
+.agreement {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-top: 20rpx;
+ padding-bottom: 20rpx;
+}
+.checkbox {
+ width: 28rpx;
+ height: 28rpx;
+ border: 2rpx solid #ccc;
+ border-radius: 6rpx;
+ margin-right: 12rpx;
+ flex-shrink: 0;
+}
+.checkbox.checked {
+ background: #667eea;
+ border-color: #667eea;
+}
+.agreement-text {
+ font-size: 24rpx;
+ color: #999;
+}
+.agreement-link {
+ color: #667eea;
+}
diff --git a/Front_Page/pages/user/user.js b/Front_Page/pages/user/user.js
index 075bd988..6f129405 100644
--- a/Front_Page/pages/user/user.js
+++ b/Front_Page/pages/user/user.js
@@ -1211,6 +1211,20 @@ Page({
});
},
+ // 跳转到医生工作台
+ navigateToDoctor: function () {
+ if (!this.data.isLoggedIn) {
+ wx.showToast({
+ title: "请先登录",
+ icon: "none",
+ });
+ return;
+ }
+ wx.navigateTo({
+ url: "/pages/doctor/doctor",
+ });
+ },
+
// 跳转到知识页面
navigateToKnowledge: function () {
wx.navigateTo({
diff --git a/Front_Page/pages/user/user.wxml b/Front_Page/pages/user/user.wxml
index fe5d5454..6ad0836a 100644
--- a/Front_Page/pages/user/user.wxml
+++ b/Front_Page/pages/user/user.wxml
@@ -129,6 +129,14 @@
反馈建议
+
+
+
+
+ 🏥
+
+ 医生工作台
+
diff --git a/Front_Page/pages/user/user.wxss b/Front_Page/pages/user/user.wxss
index b88beb91..9709f56e 100644
--- a/Front_Page/pages/user/user.wxss
+++ b/Front_Page/pages/user/user.wxss
@@ -386,7 +386,8 @@ page {
.functions-grid {
display: flex;
flex-direction: row;
- justify-content: space-between;
+ flex-wrap: wrap;
+ justify-content: flex-start;
gap: 15rpx;
width: 100%;
box-sizing: border-box;
@@ -929,6 +930,17 @@ page {
background-color: #06ad56;
}
+/* 医生工作台入口 */
+.doctor-entry {
+ background: linear-gradient(135deg, #e8f4f8, #f0f9ff) !important;
+ border-color: #A5D7E8 !important;
+}
+
+.doctor-icon {
+ border-color: #A5D7E8 !important;
+ background: white !important;
+}
+
/* 响应式适配 */
@media screen and (max-width: 375px) {
.functions-grid {
diff --git a/doctor-preview.css b/doctor-preview.css
new file mode 100644
index 00000000..f5c01e91
--- /dev/null
+++ b/doctor-preview.css
@@ -0,0 +1,131 @@
+* { margin: 0; padding: 0; box-sizing: border-box; }
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Microsoft YaHei', sans-serif;
+ background: #f0f4f8; color: #333; font-size: 14px; line-height: 1.5;
+ max-width: 480px; margin: 0 auto; min-height: 100vh;
+}
+.header {
+ background: linear-gradient(135deg, #A5D7E8, #7ec8e0);
+ padding: 20px 16px 16px; color: white; position: sticky; top: 0; z-index: 10;
+}
+.header h1 { font-size: 18px; font-weight: bold; margin-bottom: 4px; }
+.header p { font-size: 12px; opacity: 0.85; }
+.header .back { float: right; background: rgba(255,255,255,0.2); border: none; color: white; padding: 4px 12px; border-radius: 12px; font-size: 12px; cursor: pointer; display: none; }
+.header .back.show { display: inline-block; }
+.card {
+ background: white; border-radius: 12px; margin: 12px 16px; padding: 16px;
+ box-shadow: 0 2px 8px rgba(0,0,0,0.06);
+}
+.card-title { font-size: 15px; font-weight: bold; color: #2C5F7C; margin-bottom: 12px; }
+.stats { display: flex; gap: 10px; }
+.stat-item { flex: 1; text-align: center; padding: 12px 0; cursor: pointer; border-radius: 8px; transition: background .2s; }
+.stat-item:hover { background: #f5f9ff; }
+.stat-num { font-size: 26px; font-weight: bold; color: #2C5F7C; display: block; }
+.stat-label { font-size: 12px; color: #5A8BA8; margin-top: 2px; }
+.stat-item.pending .stat-num { color: #FFB84D; }
+.stat-item.followup .stat-num { color: #FF6B6B; }
+.actions { display: flex; gap: 10px; }
+.action-btn {
+ flex: 1; text-align: center; padding: 12px 8px; border-radius: 10px;
+ border: 1px solid #e8f0fe; background: #f8fbff; cursor: pointer; font-size: 13px; transition: all .2s;
+}
+.action-btn:hover { background: #e8f4f8; transform: translateY(-1px); }
+.action-btn .icon { font-size: 22px; display: block; margin-bottom: 4px; }
+.action-btn .sub { font-size: 11px; color: #999; display: block; margin-top: 2px; }
+.tabs { display: flex; gap: 8px; margin-bottom: 12px; }
+.tab {
+ padding: 6px 18px; border-radius: 16px; font-size: 13px; color: #666;
+ background: #f0f0f0; cursor: pointer; border: none; transition: all .2s;
+}
+.tab.active { background: #A5D7E8; color: white; font-weight: bold; }
+.tab:hover { opacity: 0.8; }
+.case-item {
+ padding: 14px 0; border-bottom: 1px solid #f5f5f5; cursor: pointer; transition: background .2s;
+}
+.case-item:last-child { border-bottom: none; }
+.case-item:hover { background: #fafcff; margin: 0 -16px; padding: 14px 16px; border-radius: 8px; }
+.case-row { display: flex; align-items: center; gap: 12px; }
+.case-avatar {
+ width: 44px; height: 44px; border-radius: 50%; background: #e0e7ef;
+ display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0;
+}
+.case-info { flex: 1; }
+.case-name { font-weight: bold; font-size: 14px; }
+.case-owner { font-size: 12px; color: #999; }
+.case-disease { font-size: 12px; color: #666; margin-top: 2px; }
+.case-meta { font-size: 11px; color: #bbb; margin-top: 2px; }
+.case-badge { padding: 2px 12px; border-radius: 12px; font-size: 11px; flex-shrink: 0; }
+.case-badge.light { background: #e8f5e9; color: #4caf50; }
+.case-badge.medium { background: #fff3e0; color: #ff9800; }
+.case-badge.heavy { background: #ffebee; color: #f44336; }
+.case-tags { display: flex; gap: 6px; margin-top: 6px; }
+.case-tag { font-size: 10px; padding: 2px 10px; border-radius: 8px; }
+.case-tag.green { background: #e8f5e9; color: #4caf50; }
+.case-tag.orange { background: #fff8e1; color: #ff9800; }
+.case-tag.red { background: #ffebee; color: #f44336; }
+.empty { text-align: center; padding: 40px 0; color: #bbb; }
+.empty .icon { font-size: 40px; display: block; margin-bottom: 8px; }
+.empty .text { font-size: 13px; }
+.detail-row { display: flex; padding: 8px 0; border-bottom: 1px solid #f5f5f5; }
+.detail-label { width: 80px; color: #999; font-size: 13px; flex-shrink: 0; }
+.detail-value { flex: 1; font-size: 13px; color: #333; }
+.disease-box {
+ background: #f8faff; border: 1px solid #eef4ff; border-radius: 8px; padding: 12px; margin: 8px 0;
+}
+.disease-box .name { font-weight: bold; font-size: 13px; }
+.disease-box .status { float: right; font-size: 11px; padding: 1px 10px; border-radius: 10px; }
+.disease-box .status.cured { background: #e8f5e9; color: #4caf50; }
+.disease-box .status.uncured { background: #fff3e0; color: #ff9800; }
+.disease-box .detail { font-size: 12px; color: #666; margin-top: 4px; }
+.disease-box .date { font-size: 11px; color: #bbb; margin-top: 6px; }
+.summary-box {
+ background: linear-gradient(135deg, #f0f9ff, #e8f4f8);
+ border: 1px solid #d0ebf4; border-radius: 10px; padding: 14px; margin-top: 8px;
+}
+.summary-box .text { font-size: 13px; line-height: 1.7; color: #333; }
+.summary-box .trend { margin-top: 10px; padding-top: 10px; border-top: 1px solid #d0ebf4; }
+.summary-box .trend-label { font-weight: bold; color: #A5D7E8; font-size: 12px; }
+.summary-box .trend-text { font-size: 12px; color: #666; margin-top: 4px; }
+.btn {
+ display: inline-block; padding: 8px 20px; border-radius: 20px; border: none;
+ font-size: 13px; cursor: pointer; transition: all .2s; text-align: center;
+}
+.btn:hover { opacity: 0.85; transform: translateY(-1px); }
+.btn-primary { background: #A5D7E8; color: white; }
+.btn-success { background: #4caf50; color: white; }
+.btn-danger { background: #ff6b6b; color: white; }
+.btn-outline { background: transparent; border: 1px solid #A5D7E8; color: #A5D7E8; }
+.btn-sm { font-size: 12px; padding: 4px 14px; }
+.btn-block { width: 100%; display: block; padding: 10px; }
+.auth-box { padding: 40px 16px; }
+.auth-box h2 { text-align: center; color: #2C5F7C; margin-bottom: 24px; font-size: 20px; }
+.input-group { margin-bottom: 14px; }
+.input-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; }
+.input-group input {
+ width: 100%; padding: 10px 12px; border: 1px solid #ddd; border-radius: 8px;
+ font-size: 14px; outline: none; transition: border .2s;
+}
+.input-group input:focus { border-color: #A5D7E8; }
+.toast {
+ position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
+ background: rgba(0,0,0,0.75); color: white; padding: 10px 24px; border-radius: 20px;
+ font-size: 13px; z-index: 999; display: none;
+}
+.toast.show { display: block; animation: fadeIn .3s; }
+@keyframes fadeIn { from { opacity: 0; transform: translateX(-50%) translateY(-10px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
+.loading { display: none; text-align: center; padding: 20px; color: #999; }
+.loading.show { display: block; }
+.spinner { display: inline-block; width: 20px; height: 20px; border: 2px solid #e0e0e0; border-top-color: #A5D7E8; border-radius: 50%; animation: spin .6s linear infinite; margin-right: 8px; vertical-align: middle; }
+@keyframes spin { to { transform: rotate(360deg); } }
+.page-nav { display: none; }
+.page-nav.show { display: block; }
+.followup-form { margin-top: 12px; padding-top: 12px; border-top: 1px solid #f0f0f0; }
+.followup-form label { display: block; font-size: 13px; color: #666; margin-bottom: 4px; margin-top: 8px; }
+.followup-form input[type="date"], .followup-form input[type="text"] { width: 100%; padding: 8px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 13px; }
+.followup-form .btn-row { display: flex; gap: 8px; margin-top: 12px; }
+.user-bar { display: flex; align-items: center; gap: 12px; padding: 12px 0; }
+.user-avatar { width: 44px; height: 44px; border-radius: 50%; background: #e0e7ef; display: flex; align-items: center; justify-content: center; font-size: 20px; }
+.user-detail { flex: 1; }
+.user-name { font-weight: bold; font-size: 14px; }
+.user-id { font-size: 11px; color: #999; }
+.user-logout { font-size: 12px; color: #ff6b6b; cursor: pointer; }
diff --git a/doctor-preview.html b/doctor-preview.html
new file mode 100644
index 00000000..281ad9ed
--- /dev/null
+++ b/doctor-preview.html
@@ -0,0 +1,143 @@
+
+
+
+
+
+PetDerma 宠医辅助台
+
+
+
+
+
+
+
+
+
+
+
+
🏥 宠医辅助台
+
登录以查看授权病例
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+ 全部病例
+
+
+ 0
+ 待处理
+
+
+ 0
+ 需复诊
+
+
+
+
+
+
快捷操作
+
+
+ 📋 病例列表
+ 查看所有授权病例
+
+
+ 📊 导入测试数据
+ 生成演示病例
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 加载中...
+
+
+
+
diff --git a/doctor-preview.js b/doctor-preview.js
new file mode 100644
index 00000000..211115fb
--- /dev/null
+++ b/doctor-preview.js
@@ -0,0 +1,519 @@
+// ====== State ======
+let TOKEN = localStorage.getItem('token') || '';
+let USER = JSON.parse(localStorage.getItem('user') || '{}');
+let ALL_CASES = [];
+let CURRENT_FILTER = 'all';
+
+// ====== Toast ======
+function showToast(msg, duration) {
+ const t = document.getElementById('toast');
+ t.textContent = msg; t.className = 'toast show';
+ clearTimeout(t._timer);
+ t._timer = setTimeout(() => t.className = 'toast', duration || 2000);
+}
+
+function showLoading(show) {
+ document.getElementById('loading').className = 'loading' + (show ? ' show' : '');
+}
+
+// ====== Page Nav ======
+const PAGES = ['pageLogin', 'pageDashboard', 'pageCases', 'pageDetail'];
+function showPage(id) {
+ PAGES.forEach(p => document.getElementById(p).className = 'page-nav');
+ document.getElementById(id).className = 'page-nav show';
+ document.getElementById('backBtn').className = 'back' + (id !== 'pageDashboard' ? ' show' : '');
+ const titles = { pageLogin: '宠医辅助台', pageDashboard: '宠医辅助台', pageCases: '授权病例', pageDetail: '病例详情' };
+ const subs = { pageLogin: '登录以查看授权病例', pageDashboard: 'PetDerma 机构端工作台', pageCases: '查看所有授权病例', pageDetail: '详细诊断信息' };
+ document.getElementById('headerTitle').textContent = titles[id] || '宠医辅助台';
+ document.getElementById('headerSub').textContent = subs[id] || '';
+}
+
+// ====== Auth ======
+async function doLogin() {
+ const phone = document.getElementById('loginPhone').value;
+ const pwd = document.getElementById('loginPassword').value;
+ if (!phone || !pwd) { showToast('请填写手机号和密码'); return; }
+ showLoading(true);
+ try {
+ const res = await fetch('/api/auth/phone-password-login', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ phoneNumber: phone, password: pwd })
+ });
+ const data = await res.json();
+ if (data.success) {
+ TOKEN = data.data.token;
+ USER = data.data.user;
+ localStorage.setItem('token', TOKEN);
+ localStorage.setItem('user', JSON.stringify(USER));
+ showToast('登录成功');
+ initDashboard();
+ } else if (data.message === '用户不存在') {
+ await tryDevToken();
+ } else {
+ showToast(data.message || '登录失败');
+ }
+ } catch (e) {
+ await tryDevToken();
+ }
+ showLoading(false);
+}
+
+async function tryDevToken() {
+ try {
+ const r2 = await fetch('/api/auth/dev-token');
+ const d2 = await r2.json();
+ if (d2.success) {
+ TOKEN = d2.data.token;
+ USER = d2.data.user;
+ localStorage.setItem('token', TOKEN);
+ localStorage.setItem('user', JSON.stringify(USER));
+ showToast('自动创建测试用户并登录');
+ initDashboard();
+ return;
+ }
+ } catch(e2) {}
+ showToast('无法连接后端,请确认 npm start 已运行');
+}
+
+async function doRegister() {
+ const phone = document.getElementById('regPhone').value;
+ const nick = document.getElementById('regNick').value;
+ const pwd = document.getElementById('regPassword').value;
+ const confirm = document.getElementById('regConfirm').value;
+ if (!phone || !nick || !pwd) { showToast('请填写完整信息'); return; }
+ if (pwd.length < 6) { showToast('密码至少6位'); return; }
+ if (pwd !== confirm) { showToast('两次密码不一致'); return; }
+ showLoading(true);
+ try {
+ const res = await fetch('/api/auth/register', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ phoneNumber: phone, nickName: nick, password: pwd, confirmPassword: confirm })
+ });
+ const data = await res.json();
+ if (data.success) {
+ TOKEN = data.data.token;
+ USER = data.data.user;
+ localStorage.setItem('token', TOKEN);
+ localStorage.setItem('user', JSON.stringify(USER));
+ showToast('注册成功');
+ initDashboard();
+ } else {
+ showToast(data.message || '注册失败');
+ }
+ } catch (e) {
+ showToast('无法连接后端');
+ }
+ showLoading(false);
+}
+
+function doLogout() {
+ localStorage.removeItem('token');
+ localStorage.removeItem('user');
+ TOKEN = ''; USER = {};
+ showPage('pageLogin');
+ showToast('已退出');
+}
+
+// ====== Dashboard ======
+function initDashboard() {
+ document.getElementById('doctorName').textContent = USER.nickName || '合作医生';
+ document.getElementById('doctorId').textContent = 'ID: ' + (USER.id || USER._id || '').substring(0,8) || '---';
+ showPage('pageDashboard');
+ if (!localStorage.getItem('doctorMockCases')) {
+ generateMockCases();
+ }
+ loadCases();
+}
+
+async function loadCases() {
+ if (!TOKEN) return;
+ showLoading(true);
+ try {
+ const res = await fetch('/api/doctor/cases', {
+ headers: { 'Authorization': 'Bearer ' + TOKEN }
+ });
+ const data = await res.json();
+ if (data.success && data.data.cases && data.data.cases.length > 0) {
+ ALL_CASES = data.data.cases;
+ updateDashboard(data.data.stats);
+ renderRecentCases();
+ } else {
+ loadMockCases();
+ }
+ } catch(e) {
+ loadMockCases();
+ }
+ showLoading(false);
+}
+
+function loadMockCases() {
+ const stored = localStorage.getItem('doctorMockCases');
+ if (stored) {
+ ALL_CASES = JSON.parse(stored);
+ const total = ALL_CASES.length;
+ const pending = ALL_CASES.filter(c => !c.aiSummary).length;
+ const followup = ALL_CASES.filter(c => c.followUp?.needed && !c.followUp?.completed).length;
+ updateDashboard({ total, pendingReview: pending, followUpNeeded: followup });
+ renderRecentCases();
+ }
+}
+
+function updateDashboard(stats) {
+ document.getElementById('statTotal').textContent = stats.total || 0;
+ document.getElementById('statPending').textContent = stats.pendingReview || 0;
+ document.getElementById('statFollowup').textContent = stats.followUpNeeded || 0;
+}
+
+function renderRecentCases() {
+ const el = document.getElementById('recentCases');
+ if (!ALL_CASES.length) {
+ el.innerHTML = '📭暂无数据
';
+ return;
+ }
+ el.innerHTML = ALL_CASES.slice(0, 5).map(c => renderCaseItem(c)).join('');
+}
+
+function renderCaseItem(c) {
+ const disease = c.skinDiseaseHistory?.[c.skinDiseaseHistory.length-1]?.diseaseName || '暂无诊断';
+ const severity = c.severity || '';
+ const sevClass = severity === '轻' ? 'light' : severity === '中' ? 'medium' : severity === '重' ? 'heavy' : '';
+ const hasSummary = c.aiSummary ? 'AI 摘要' : '待生成';
+ const hasFollowup = c.followUp?.needed && !c.followUp?.completed ? '待复诊' : '';
+ return `
+
+
${c.petId?.name?.[0] || '🐾'}
+
+
${c.petId?.name || '未知宠物'} ${c.petId?.species || ''}
+
宠主:${c.userId?.nickName || '未知'}
+
${disease}
+
${formatTime(c.createdAt)}
+
+ ${severity ? `
${severity}` : ''}
+
+
${hasSummary}${hasFollowup}
+
`;
+}
+
+// ====== Cases List ======
+function showCases(filter) {
+ CURRENT_FILTER = filter;
+ showPage('pageCases');
+ renderCases();
+ document.querySelectorAll('#pageCases .tab').forEach(t => {
+ t.className = 'tab' + (t.dataset.filter === filter ? ' active' : '');
+ });
+}
+
+function renderCases() {
+ const el = document.getElementById('casesList');
+ let filtered = ALL_CASES;
+ if (CURRENT_FILTER === 'pending') filtered = ALL_CASES.filter(c => !c.aiSummary);
+ if (CURRENT_FILTER === 'followup') filtered = ALL_CASES.filter(c => c.followUp?.needed && !c.followUp?.completed);
+ if (!filtered.length) {
+ el.innerHTML = '📋暂无病例
';
+ return;
+ }
+ el.innerHTML = filtered.map(c => renderCaseItem(c)).join('');
+}
+
+// ====== Case Detail ======
+async function viewDetail(id) {
+ showLoading(true);
+ if (TOKEN) {
+ try {
+ const res = await fetch('/api/doctor/cases/' + id, {
+ headers: { 'Authorization': 'Bearer ' + TOKEN }
+ });
+ const data = await res.json();
+ if (data.success) {
+ showDetailPage(data.data);
+ showLoading(false);
+ return;
+ }
+ } catch(e) {}
+ }
+ const c = ALL_CASES.find(i => i._id === id);
+ if (c) showDetailPage(c);
+ showLoading(false);
+}
+
+function showDetailPage(c) {
+ showPage('pageDetail');
+ document.getElementById('headerTitle').textContent = '病例详情';
+ document.getElementById('headerSub').textContent = (c.petId?.name || '未知宠物') + ' · ' + (c.userId?.nickName || '');
+
+ const severity = c.severity || '';
+ const sevClass = severity === '轻' ? 'light' : severity === '中' ? 'medium' : severity === '重' ? 'heavy' : '';
+ const severityBadge = severity ? `${severity}` : '';
+
+ const diseases = (c.skinDiseaseHistory || []).map(d => `
+
+
${d.diseaseName}${d.isCured === '是' ? '已痊愈' : '治疗中'}
+ ${d.symptoms?.length ? `
症状:${d.symptoms.join('、')}
` : ''}
+ ${d.affectedAreas?.length ? `
部位:${d.affectedAreas.join('、')}
` : ''}
+ ${d.medication ? `
用药:${d.medication}
` : ''}
+
${d.startDate ? formatTime(d.startDate) : ''}
+
+ `).join('') || '暂无诊断记录
';
+
+ const followUp = c.followUp || {};
+ const followupBadge = followUp.needed && !followUp.completed ? '待复诊' :
+ followUp.completed ? '已完成' :
+ '未设置';
+
+ const summarySection = c.aiSummary ? `
+
+
${c.aiSummary}
+ ${c.trend ? `
` : ''}
+
+ ` : `
+
+
点击按钮自动分析病例数据
+
`;
+
+ document.getElementById('detailContent').innerHTML = `
+
+
+
${c.petId?.name?.[0] || '🐾'}
+
+
${c.petId?.name || '未知宠物'}
+
${c.petId?.species || ''} ${c.petId?.breed || ''}
+
+ ${severityBadge}
+
+
宠主${c.userId?.nickName || '未知'}
+
电话${c.userId?.phoneNumber || '未提供'}
+ ${c.weight ? `
体重${c.weight}kg
` : ''}
+
建档${formatTime(c.createdAt)}
+
+
+
+
🤖 AI 病例摘要
+ ${summarySection}
+
+
+
📅 复诊管理
+
+
${followupBadge} ${followUp.note ? `${followUp.note}` : ''}
+
+
+ ${followUp.needed && !followUp.completed ? `
` : ''}
+
+
+ `;
+}
+
+// ====== AI Summary ======
+async function generateSummary(id) {
+ showLoading(true);
+ const c = ALL_CASES.find(i => i._id === id);
+ if (c) {
+ const pet = c.petId || {};
+ const history = c.skinDiseaseHistory || [];
+ const latest = history[history.length - 1] || {};
+ const lines = [];
+ if (pet.name) lines.push('患宠:' + pet.name + '(' + (pet.species || '未知') + ')');
+ if (c.userId?.nickName) lines.push('宠主:' + c.userId.nickName);
+ if (c.weight) lines.push('体重:' + c.weight + 'kg');
+ if (latest.diseaseName) lines.push('诊断疾病:' + latest.diseaseName);
+ if (latest.symptoms?.length) lines.push('症状表现:' + latest.symptoms.join('、'));
+ if (latest.affectedAreas?.length) lines.push('患病部位:' + latest.affectedAreas.join('、'));
+ c.aiSummary = lines.join(';') || '暂无诊断数据。';
+ c.trend = history.length > 1 ? '该宠物共有 ' + history.length + ' 次就诊记录,病情发展已追踪。' : '初次就诊,建议持续观察。';
+ const symptomCount = (latest.symptoms?.length || 0) + (latest.affectedAreas?.length || 0);
+ c.severity = symptomCount >= 4 ? '重' : symptomCount >= 2 ? '中' : '轻';
+ showToast('摘要生成成功');
+ viewDetail(id);
+ renderRecentCases();
+ }
+ showLoading(false);
+}
+
+// ====== Follow Up ======
+function saveFollowUp(id) {
+ const needed = document.getElementById('fuNeeded').checked;
+ const date = document.getElementById('fuDate').value;
+ const note = document.getElementById('fuNote').value;
+ const c = ALL_CASES.find(i => i._id === id);
+ if (c) {
+ c.followUp = { needed, date, note, completed: false };
+ showToast('复诊提醒已更新');
+ viewDetail(id);
+ }
+}
+
+function completeFollowUp(id) {
+ if (!confirm('确认该复诊已完成?')) return;
+ const c = ALL_CASES.find(i => i._id === id);
+ if (c) { c.followUp.completed = true; showToast('复诊已完成'); viewDetail(id); }
+}
+
+// ====== Seed Demo Data ======
+function generateMockCases() {
+ const mockCases = [
+ {
+ _id: 'mock_001', severity: '中', weight: 28.5, allergies: '花粉',
+ userId: { nickName: '张女士', phoneNumber: '138****1234' },
+ petId: { name: '豆豆', species: '犬', breed: '金毛' },
+ skinDiseaseHistory: [{ diseaseName: '湿疹', symptoms: ['红斑','瘙痒','掉毛'], affectedAreas: ['背部','腹部'], medication: '红霉素软膏', isCured: '否', startDate: '2026-04-15' }],
+ recentContact: '近期去过宠物公园', createdAt: '2026-04-15',
+ aiSummary: '患宠:豆豆(犬);宠主:张女士;体重:28.5kg;诊断疾病:湿疹;症状表现:红斑、瘙痒、掉毛;患病部位:背部、腹部。建议进行过敏原检测并保持环境干燥。',
+ trend: '初次就诊,症状表现为中度的皮肤红斑和瘙痒,建议持续观察并按时用药。',
+ followUp: { needed: true, date: '2026-05-20', note: '复查皮肤恢复情况', completed: false }
+ },
+ {
+ _id: 'mock_002', severity: '轻', weight: 4.2,
+ userId: { nickName: '李先生', phoneNumber: '139****5678' },
+ petId: { name: '咪咪', species: '猫', breed: '英短' },
+ skinDiseaseHistory: [{ diseaseName: '猫癣', symptoms: ['掉毛','结痂'], affectedAreas: ['面部'], medication: '特比萘芬', isCured: '是', startDate: '2026-03-01' }],
+ createdAt: '2026-03-01',
+ aiSummary: '患宠:咪咪(猫);宠主:李先生;体重:4.2kg;诊断疾病:猫癣;症状表现:掉毛、结痂;患病部位:面部。已治愈,恢复良好。',
+ trend: '1 次就诊记录,已痊愈。建议定期检查皮肤状态。',
+ followUp: { needed: false, completed: false }
+ },
+ {
+ _id: 'mock_003', severity: '重', weight: 12.0, allergies: '食物过敏',
+ userId: { nickName: '王先生', phoneNumber: '137****9012' },
+ petId: { name: '旺财', species: '犬', breed: '柯基' },
+ skinDiseaseHistory: [
+ { diseaseName: '脓皮症', symptoms: ['渗液','结痂','红斑','瘙痒'], affectedAreas: ['全身','爪缝'], medication: '抗生素+药浴', isCured: '否', startDate: '2026-05-01' },
+ { diseaseName: '过敏性皮炎', symptoms: ['红斑','瘙痒'], affectedAreas: ['腹部'], medication: '抗组胺药', isCured: '是', startDate: '2026-01-10' }
+ ],
+ recentContact: '家中新养了一只猫', createdAt: '2026-05-01',
+ aiSummary: '患宠:旺财(犬);宠主:王先生;体重:12.0kg;诊断疾病:脓皮症;症状表现:渗液、结痂、红斑、瘙痒;患病部位:全身、爪缝。有食物过敏史,复发风险较高。建议进行过敏原排查。',
+ trend: '该宠物共有 2 次就诊记录。既往过敏性皮炎已治愈,目前脓皮症正在治疗中,病情有复发趋势,需要持续关注。',
+ followUp: { needed: true, date: '2026-05-25', note: '复查脓皮症恢复情况,建议做过敏原检测', completed: false }
+ },
+ {
+ _id: 'mock_004', severity: '轻', weight: 5.8,
+ userId: { nickName: '赵女士', phoneNumber: '136****3456' },
+ petId: { name: '小白', species: '犬', breed: '比熊' },
+ skinDiseaseHistory: [{ diseaseName: '泪痕炎', symptoms: ['红斑'], affectedAreas: ['面部'], medication: '氯霉素眼药水', isCured: '是', startDate: '2026-02-20' }],
+ createdAt: '2026-02-20',
+ aiSummary: '患宠:小白(犬);宠主:赵女士;体重:5.8kg;诊断疾病:泪痕炎;症状表现:红斑;患病部位:面部。已治愈,情况良好。',
+ trend: '初次就诊,已痊愈。建议定期清洁眼部。',
+ followUp: { needed: false, completed: false }
+ }
+ ];
+ localStorage.setItem('doctorMockCases', JSON.stringify(mockCases));
+}
+
+// ====== Utils ======
+function formatTime(date) {
+ if (!date) return '';
+ const d = new Date(date);
+ if (isNaN(d.getTime())) return date;
+ const now = new Date();
+ const diff = now - d;
+ if (diff < 60000) return '刚刚';
+ if (diff < 3600000) return Math.floor(diff / 60000) + '分钟前';
+ if (diff < 86400000) return Math.floor(diff / 3600000) + '小时前';
+ if (diff < 604800000) return Math.floor(diff / 86400000) + '天前';
+ return (d.getMonth()+1) + '月' + d.getDate() + '日';
+}
+
+// ====== Event Delegation ======
+document.addEventListener('DOMContentLoaded', function() {
+ // Back button
+ document.getElementById('backBtn').addEventListener('click', function() {
+ const visible = PAGES.find(id => document.getElementById(id).className === 'page-nav show');
+ if (visible === 'pageCases') showPage('pageDashboard');
+ else if (visible === 'pageDetail') showPage('pageCases');
+ else showPage('pageDashboard');
+ });
+
+ // Global click handler using event delegation
+ document.addEventListener('click', function(e) {
+ const target = e.target.closest('[data-action]');
+ if (!target) return;
+ const action = target.dataset.action;
+ const id = target.dataset.id;
+
+ switch (action) {
+ case 'login': doLogin(); break;
+ case 'register': doRegister(); break;
+ case 'showRegister': showRegister(); break;
+ case 'showLogin': showLogin(); break;
+ case 'logout': doLogout(); break;
+ case 'showCases': showCases(target.dataset.filter || 'all'); break;
+ case 'dashboard': showPage('pageDashboard'); break;
+ case 'seedDemo': generateMockCases(); showToast('已导入 4 个演示病例'); loadCases(); break;
+ case 'switchFilter':
+ document.querySelectorAll('#pageCases .tab').forEach(t => t.className = 'tab');
+ target.className = 'tab active';
+ CURRENT_FILTER = target.dataset.filter;
+ renderCases();
+ break;
+ case 'viewCase': viewDetail(id || target.closest('[data-id]')?.dataset.id); break;
+ case 'generateSummary': generateSummary(id); break;
+ case 'toggleFollowUp':
+ const form = document.getElementById('followUpForm');
+ form.style.display = form.style.display === 'none' ? 'block' : 'none';
+ break;
+ case 'hideFollowUpForm':
+ document.getElementById('followUpForm').style.display = 'none';
+ break;
+ case 'saveFollowUp': saveFollowUp(id); break;
+ case 'completeFollowUp': completeFollowUp(id); break;
+ case 'viewPending': showCases('pending'); break;
+ case 'viewFollowup': showCases('followup'); break;
+ }
+ });
+
+ // Case item click delegation (re-rendered content)
+ document.addEventListener('click', function(e) {
+ const item = e.target.closest('.case-item');
+ if (item && item.dataset.id) {
+ viewDetail(item.dataset.id);
+ }
+ });
+
+ // Init
+ (async function init() {
+ if (TOKEN && USER?.nickName) {
+ initDashboard();
+ return;
+ }
+ try {
+ const r = await fetch('/api/auth/dev-token');
+ const d = await r.json();
+ if (d.success) {
+ TOKEN = d.data.token;
+ USER = d.data.user;
+ localStorage.setItem('token', TOKEN);
+ localStorage.setItem('user', JSON.stringify(USER));
+ initDashboard();
+ showToast('自动登录成功(开发模式)');
+ return;
+ }
+ } catch(e) {}
+ showPage('pageLogin');
+ })();
+});
+
+// Auth form visibility toggles
+function showRegister() {
+ document.getElementById('loginForm').style.display = 'none';
+ document.getElementById('registerForm').style.display = 'block';
+}
+function showLogin() {
+ document.getElementById('registerForm').style.display = 'none';
+ document.getElementById('loginForm').style.display = 'block';
+}
diff --git a/home-preview.html b/home-preview.html
new file mode 100644
index 00000000..4bd94dfe
--- /dev/null
+++ b/home-preview.html
@@ -0,0 +1,527 @@
+
+
+
+
+
+PetDerma 首页预览
+
+
+
+
+
+
+
+
+
+
+ 欢迎来到PetDerma
+ 您的宠物皮肤健康助手
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ AI预问诊管理 · 预约转化 · 复诊跟踪
+
+
+
+
+
+
+
💡
+
+ 健康提示
+ 定期为宠物检查皮肤状况,发现异常及时就诊
+
+
+
+
+
+
+ 换季过敏预防
+ 冬春交替季节,宠物容易发生过敏反应。注意观察皮肤变化,及时调整饮食和环境,预防季节性过敏。
+
+
+
+
+
+
+
+
+
+
diff --git a/institution-preview.html b/institution-preview.html
new file mode 100644
index 00000000..6e96f392
--- /dev/null
+++ b/institution-preview.html
@@ -0,0 +1,537 @@
+
+
+
+
+
+PetDerma 机构端 - 预览
+
+
+
+
+
+
+
+
‹
+
机构工作台
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 6
+ 待预问诊
+
+
+ 3
+ 今日预约
+
+
+ 12
+ AI报告
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 今天
+ 3
+
+
+ 明天
+ 2
+
+
+ 本周
+ 1
+
+
+
+
+
3待确认
+
2已确认
+
1已完成
+
0已取消
+
+
+
+
+
+
+
+
+
加入 PetDerma 机构端
+
为宠物提供专业的皮肤健康服务
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/map-preview.html b/map-preview.html
new file mode 100644
index 00000000..a7da9844
--- /dev/null
+++ b/map-preview.html
@@ -0,0 +1,355 @@
+
+
+
+
+
+PetDerma 附近机构 - 地图导航
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 📋 本次AI诊断报告
+
+ 宠主:王女士 · 宠物:奶茶(猫·布偶)
+ 诊断:真菌感染(置信度72%)· 严重程度:中度
+
+
+
+
+
+
🏥 附近合作机构
+
+
+
🏥
+
+
爱宠宠物医院
+
朝阳区建国路88号
+
+ 诊疗
+ 手术
+ 皮肤科
+
+
+
1.2km
+
+
+
✂️
+
+
毛毛宠物护理店
+
海淀区中关村大街66号
+
+ 洗护
+ 美容
+ 护理
+
+
+
0.8km
+
+
+
🏥
+
+
美联宠物诊所
+
西城区西单北大街22号
+
+ 诊疗
+ 疫苗
+
+
+
2.3km
+
+
+
🏥
+
+
瑞鹏宠物医院
+
东城区东直门外大街12号
+
+ 诊疗
+ 手术
+ 住院
+
+
+
1.8km
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 00000000..3a3a7ecb
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,684 @@
+{
+ "name": "PetDerma",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "devDependencies": {
+ "mongodb-memory-server": "^11.1.0"
+ }
+ },
+ "node_modules/@mongodb-js/saslprep": {
+ "version": "1.4.11",
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.11.tgz",
+ "integrity": "sha512-o9rAHc0IpIjuPSxRutWpE1F62x7n+4mVS4rCNHkzhIUMQcc18bb6xEq5wd2NdN0WjepIyXIppRshYI2kQDOZVA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "sparse-bitfield": "^3.0.3"
+ }
+ },
+ "node_modules/@types/webidl-conversions": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/whatwg-url": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/webidl-conversions": "*"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/async-mutex": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.5.0.tgz",
+ "integrity": "sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/b4a": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.1.tgz",
+ "integrity": "sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "react-native-b4a": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-b4a": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-events": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz",
+ "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "peerDependencies": {
+ "bare-abort-controller": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-fs": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz",
+ "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.5.4",
+ "bare-path": "^3.0.0",
+ "bare-stream": "^2.6.4",
+ "bare-url": "^2.2.2",
+ "fast-fifo": "^1.3.2"
+ },
+ "engines": {
+ "bare": ">=1.16.0"
+ },
+ "peerDependencies": {
+ "bare-buffer": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-buffer": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-os": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.1.tgz",
+ "integrity": "sha512-6M5XjcnsygQNPMCMPXSK379xrJFiZ/AEMNBmFEmQW8d/789VQATvriyi5r0HYTL9TkQ26rn3kgdTG3aisbrXkQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "bare": ">=1.14.0"
+ }
+ },
+ "node_modules/bare-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
+ "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-os": "^3.0.1"
+ }
+ },
+ "node_modules/bare-stream": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.1.tgz",
+ "integrity": "sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "streamx": "^2.25.0",
+ "teex": "^1.0.1"
+ },
+ "peerDependencies": {
+ "bare-abort-controller": "*",
+ "bare-buffer": "*",
+ "bare-events": "*"
+ },
+ "peerDependenciesMeta": {
+ "bare-abort-controller": {
+ "optional": true
+ },
+ "bare-buffer": {
+ "optional": true
+ },
+ "bare-events": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/bare-url": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.3.tgz",
+ "integrity": "sha512-Kccpc7ACfXaxfeInfqKcZtW4pT5YBn1mesc4sCsun6sRwtbJ4h+sNOaksUpYEJUKfN65YWC6Bw2OJEFiKxq8nQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-path": "^3.0.0"
+ }
+ },
+ "node_modules/bson": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
+ "integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/buffer-crc32": {
+ "version": "0.2.13",
+ "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
+ "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/camelcase": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
+ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/commondir": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
+ "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/events-universal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz",
+ "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bare-events": "^2.7.0"
+ }
+ },
+ "node_modules/fast-fifo": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
+ "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/find-cache-dir": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz",
+ "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "commondir": "^1.0.1",
+ "make-dir": "^3.0.2",
+ "pkg-dir": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/avajs/find-cache-dir?sponsor=1"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+ "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^5.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+ "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
+ "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/memory-pager": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/mongodb": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.2.0.tgz",
+ "integrity": "sha512-F/2+BMZtLVhY30ioZp0dAmZ+IRZMBqI+nrv6t5+9/1AIwCa8sMRC3jBf81lpxMhnZgqq8CoUD503Z1oZWq1/sw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@mongodb-js/saslprep": "^1.3.0",
+ "bson": "^7.2.0",
+ "mongodb-connection-string-url": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "peerDependencies": {
+ "@aws-sdk/credential-providers": "^3.806.0",
+ "@mongodb-js/zstd": "^7.0.0",
+ "gcp-metadata": "^7.0.1",
+ "kerberos": "^7.0.0",
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
+ "snappy": "^7.3.2",
+ "socks": "^2.8.6"
+ },
+ "peerDependenciesMeta": {
+ "@aws-sdk/credential-providers": {
+ "optional": true
+ },
+ "@mongodb-js/zstd": {
+ "optional": true
+ },
+ "gcp-metadata": {
+ "optional": true
+ },
+ "kerberos": {
+ "optional": true
+ },
+ "mongodb-client-encryption": {
+ "optional": true
+ },
+ "snappy": {
+ "optional": true
+ },
+ "socks": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/mongodb-connection-string-url": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
+ "integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/whatwg-url": "^13.0.0",
+ "whatwg-url": "^14.1.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongodb-memory-server": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/mongodb-memory-server/-/mongodb-memory-server-11.1.0.tgz",
+ "integrity": "sha512-x9psV1KXRgG5t14AmsrfcWCqlNXvPOzcyroMSeRU5vkAm8jxEF5WiLGdGCONLOgeCNjRnpg6igyDum/eTwiooA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "mongodb-memory-server-core": "11.1.0",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/mongodb-memory-server-core": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/mongodb-memory-server-core/-/mongodb-memory-server-core-11.1.0.tgz",
+ "integrity": "sha512-GwpnJVIiUyXdi5BoTsExrvLupSt3sJzCSX5P6fxlr0dCrJkhumiq8SQIqtTBqTu2mMpFMCHdjSS0QMUvFMpbWw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async-mutex": "^0.5.0",
+ "camelcase": "^6.3.0",
+ "debug": "^4.4.3",
+ "find-cache-dir": "^3.3.2",
+ "follow-redirects": "^1.16.0",
+ "https-proxy-agent": "^7.0.6",
+ "mongodb": "^7.2.0",
+ "new-find-package-json": "^2.0.0",
+ "semver": "^7.7.3",
+ "tar-stream": "^3.1.8",
+ "tslib": "^2.8.1",
+ "yauzl": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=20.19.0"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/new-find-package-json": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/new-find-package-json/-/new-find-package-json-2.0.0.tgz",
+ "integrity": "sha512-lDcBsjBSMlj3LXH2v/FW3txlh2pYTjmbOXPYJD93HI5EwuLzI11tdHSIpUMmfq/IOsldj4Ps8M8flhm+pCK4Ew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">=12.22.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-try": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+ "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/p-try": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pend": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
+ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/pkg-dir": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz",
+ "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/semver": {
+ "version": "7.8.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz",
+ "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sparse-bitfield": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "memory-pager": "^1.0.2"
+ }
+ },
+ "node_modules/streamx": {
+ "version": "2.25.0",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz",
+ "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "events-universal": "^1.0.0",
+ "fast-fifo": "^1.3.2",
+ "text-decoder": "^1.1.0"
+ }
+ },
+ "node_modules/tar-stream": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.2.0.tgz",
+ "integrity": "sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "b4a": "^1.6.4",
+ "bare-fs": "^4.5.5",
+ "fast-fifo": "^1.2.0",
+ "streamx": "^2.15.0"
+ }
+ },
+ "node_modules/teex": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz",
+ "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "streamx": "^2.12.5"
+ }
+ },
+ "node_modules/text-decoder": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz",
+ "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "b4a": "^1.6.4"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/yauzl": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.3.0.tgz",
+ "integrity": "sha512-PtGEvEP30p7sbIBJKUBjUnqgTVOyMURc4dLo9iNyAJnNIEz9pm88cCXF21w94Kg3k6RXkeZh5DHOGS0qEONvNQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-crc32": "~0.2.3",
+ "pend": "~1.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..77992d47
--- /dev/null
+++ b/package.json
@@ -0,0 +1,5 @@
+{
+ "devDependencies": {
+ "mongodb-memory-server": "^11.1.0"
+ }
+}
diff --git a/result-preview.html b/result-preview.html
new file mode 100644
index 00000000..49df522f
--- /dev/null
+++ b/result-preview.html
@@ -0,0 +1,441 @@
+
+
+
+
+
+PetDerma 诊断结果
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
真菌感染(猫癣)
+
+
+
+
+
+
+ 猫癣是由真菌引起的常见皮肤病,主要表现为皮肤局部脱毛、发红、皮屑增多等症状。建议尽快到宠物医院进行伍德灯检查确认真菌类型。
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git "a/\345\205\245\345\217\2431\357\274\232\351\246\226\351\241\265\345\212\240\342\200\234\346\234\272\346\236\204\347\253\257\345\205\245\345\217\243\342\200\235.md" "b/\345\205\245\345\217\2431\357\274\232\351\246\226\351\241\265\345\212\240\342\200\234\346\234\272\346\236\204\347\253\257\345\205\245\345\217\243\342\200\235.md"
new file mode 100644
index 00000000..24a3354e
--- /dev/null
+++ "b/\345\205\245\345\217\2431\357\274\232\351\246\226\351\241\265\345\212\240\342\200\234\346\234\272\346\236\204\347\253\257\345\205\245\345\217\243\342\200\235.md"
@@ -0,0 +1,436 @@
+在现有页面上加三个入口。
+
+## 入口1:首页加“机构端入口”
+
+首页可以放一个小卡片:
+
+> **机构服务**
+> 宠物医院 / 护理店入驻
+> AI预问诊管理 · 预约转化 · 复诊跟踪
+
+按钮文案:
+
+> **机构工作台**
+
+适合展示 ToB 的完整入口。
+
+------
+
+## 入口2:地图导航页加“发送AI报告给机构”
+
+你们已经有附近宠物医院或护理店导航,那么地图页可以加一个核心按钮:
+
+> **发送本次AI诊断报告**
+
+用户点击后,可以选择附近机构,把自己的诊断结果、患处图片、症状描述、宠物档案授权发送给该机构。
+
+这个功能非常重要,因为它把 C 端和 B 端真正连起来了。
+
+------
+
+## 入口3:诊断结果页加“预约线下复核”
+
+诊断结果页下面可以加:
+
+> **预约附近宠物医院复核**
+> **发送给护理店获取护理建议**
+
+这样逻辑很顺:
+
+用户诊断 → 看到低置信度/中重度 → 系统建议线下 → 用户一键预约 → B端收到预问诊报告。
+
+------
+
+# 一、ToB端页面
+
+核心页面如下:
+
+## ToB页面结构
+
+| 页面 | 功能 |
+| --------------- | ----------------------------------------------- |
+| 机构入驻页 | 宠物医院/护理店提交资料 |
+| 机构工作台首页 | 查看今日预约、待处理预问诊、高风险病例 |
+| AI预问诊列表页 | 查看用户授权发送来的诊断报告 |
+| 预问诊详情页 | 查看宠物图片、症状、Top-3疾病、严重度、宠物档案 |
+| 预约管理页 | 管理用户预约、确认到店、取消、改期 |
+| 复诊跟踪页 | 设置复诊提醒、查看病情趋势 |
+| 客户/宠物档案页 | 管理到店用户和宠物健康记录 |
+| 门店主页管理页 | 编辑门店服务、营业时间、地址、资质 |
+| 数据看板页 | 展示咨询量、转化率、复诊率、常见疾病分布 |
+
+如果时间有限,结题版优先做前五个:
+**机构入驻页、工作台首页、预问诊列表、预问诊详情、预约管理。**
+
+------
+
+# 二、详细功能设计
+
+## 1. 机构入驻页
+
+这个页面是 ToB 的第一个页面。
+
+### 页面名称
+
+> 机构入驻认证
+
+### 用户类型选择
+
+进入页面后先选择:
+
+| 类型 | 说明 |
+| ---------- | -------------------------------------------- |
+| 宠物医院 | 有执业兽医、可线下诊疗 |
+| 宠物护理店 | 提供洗护、美容、基础皮肤护理,不提供诊断治疗 |
+| 连锁机构 | 多门店管理,结题阶段可先不做 |
+
+### 表单字段
+
+| 字段 | 示例 |
+| -------------------- | ------------------------------------------------ |
+| 机构名称 | XX宠物医院 |
+| 机构类型 | 宠物医院 / 护理店 |
+| 联系人 | 张医生 / 店长 |
+| 联系电话 | 手机号 |
+| 营业时间 | 9:00-21:00 |
+| 机构地址 | 自动定位 + 手动填写 |
+| 服务范围 | 皮肤病诊疗、洗护、驱虫、疫苗、护理 |
+| 门店照片 | 上传门头/环境照片 |
+| 资质证明 | 医院营业执照、动物诊疗许可证;护理店上传营业执照 |
+| 是否接受AI预问诊转诊 | 是 / 否 |
+| 是否支持线上预约 | 是 / 否 |
+
+### 页面按钮
+
+底部按钮:
+
+> 提交审核
+
+审核通过后显示:
+
+> 已入驻 PetDerma 合作机构
+> 可进入机构工作台
+
+------
+
+## 2. 机构工作台首页
+
+这是 ToB 的主页面,建议做得像一个后台仪表盘。
+
+### 页面名称
+
+> 机构工作台
+
+### 顶部机构卡片
+
+显示:
+
+> XX宠物医院
+> 今日营业中
+> 评分 4.8
+> 今日预约 8
+> 待处理预问诊 5
+> 高风险提醒 2
+
+### 核心数据卡片
+
+| 卡片 | 内容 |
+| ------------ | ---------------------------------- |
+| 今日预约 | 今日待到店用户数量 |
+| 待处理预问诊 | 用户发送的AI诊断报告 |
+| 高风险病例 | 严重程度为“重”或低置信度但症状危险 |
+| 待复诊提醒 | 需要回访的宠物 |
+
+### 快捷入口
+
+四个按钮即可:
+
+> AI预问诊
+> 预约管理
+> 复诊跟踪
+> 门店主页
+
+### 展示价值
+
+这个页面主要告诉老师:
+
+> ToB端不是简单展示机构信息,而是给商家提供接收用户、管理病例、提高到店转化的工作台。
+
+------
+
+## 3. AI预问诊列表页
+
+这是 ToB 的核心功能。
+
+### 页面名称
+
+> AI预问诊管理
+
+### 列表筛选条件
+
+可以做几个标签:
+
+| 筛选项 | 说明 |
+| -------- | ---------------------- |
+| 全部 | 所有用户授权发送的报告 |
+| 待处理 | 机构尚未查看 |
+| 已联系 | 已电话/消息联系用户 |
+| 已预约 | 用户已预约到店 |
+| 高风险 | 重度/低置信度/疑似传染 |
+| 护理咨询 | 适合护理店处理的轻症 |
+
+### 病例卡片内容
+
+每条卡片显示:
+
+> 宠物名:奶茶
+> 类型:猫 / 布偶 / 2岁 / 已绝育
+> 主诉:耳后掉毛、红肿、瘙痒3天
+> AI结果:真菌感染 0.72;过敏性皮炎 0.58;螨虫感染 0.41
+> 严重程度:中度
+> 用户距离:2.1km
+> 状态:待处理
+
+右侧按钮:
+
+> 查看报告
+> 联系用户
+> 建议预约
+
+### 页面价值
+
+这个页面用于展示:
+
+> C端用户的AI诊断结果可以转化成B端的预问诊线索。
+
+这就是商业闭环。
+
+------
+
+## 4. 预问诊详情页
+
+这个页面最重要,建议结题展示一定要做。
+
+### 页面名称
+
+> AI预问诊报告详情
+
+### 页面结构
+
+建议分成六个模块。
+
+**模块1:宠物基本信息**
+
+显示:
+
+| 字段 | 示例 |
+| ------------- | ------------------ |
+| 宠物昵称 | 奶茶 |
+| 宠物类型 | 猫 |
+| 品种 | 布偶 |
+| 年龄 | 2岁 |
+| 性别 | 雌性 |
+| 绝育状态 | 已绝育 |
+| 体重 | 4.2kg |
+| 过敏史 | 暂无 |
+| 既往皮肤病 | 曾有耳部真菌感染 |
+| 驱虫/疫苗记录 | 近一个月未体外驱虫 |
+
+这个模块体现你们“宠物档案融合”的价值。
+
+**模块2:用户症状描述**
+
+显示用户输入的内容:
+
+> 最近3天耳后掉毛,宠物一直抓挠,局部发红,家里新接触过一只流浪猫。
+
+可以附带结构化标签:
+
+| 标签 | 内容 |
+| -------- | ---------------- |
+| 持续时间 | 3天 |
+| 症状 | 掉毛、红肿、瘙痒 |
+| 患处 | 耳后 |
+| 接触史 | 接触流浪猫 |
+| 精神状态 | 正常 |
+| 食欲 | 正常 |
+
+**模块3:患处图片**
+
+展示用户上传的图片。
+
+建议加两个视图:
+
+| 视图 | 说明 |
+| ---------- | ---------------- |
+| 原图 | 用户上传图片 |
+| 病灶标注图 | 显示疑似病灶区域 |
+
+哪怕你们技术上还没有特别复杂的热力图,也可以做简单的“病灶区域框选/轮廓图”展示,视觉效果会很好。
+
+**模块4:AI诊断结果**
+
+显示:
+
+| 排名 | 疾病 | 置信度 |
+| ----- | ---------- | ------ |
+| Top 1 | 真菌感染 | 72% |
+| Top 2 | 过敏性皮炎 | 58% |
+| Top 3 | 螨虫感染 | 41% |
+
+旁边显示:
+
+> 最高置信度:72%
+> 结果可靠性:中高
+> 建议:线下检查确认病因
+
+这个地方不要写得像“确诊”,要写“候选疾病”。
+
+**模块5:严重程度评分**
+
+显示:
+
+> 严重程度:中度
+> 综合评分:0.63
+
+评分构成:
+
+| 指标 | 数值 |
+| ------------ | ---- |
+| 模型置信度 | 0.72 |
+| 病灶面积比例 | 0.31 |
+| 红肿比例 | 0.46 |
+| 炎症指标 | 0.57 |
+
+页面可以用进度条或仪表盘展示。
+
+**模块6:机构处理建议**
+
+给 B 端人员几个操作按钮:
+
+| 按钮 | 作用 |
+| ---------------- | ------------------- |
+| 建议用户到店检查 | 给用户发送预约建议 |
+| 联系用户 | 电话/微信联系 |
+| 标记为高风险 | 加入高优先级 |
+| 建议复诊跟踪 | 创建复诊任务 |
+| 转为预约 | 进入预约管理 |
+| 添加内部备注 | 医院/护理店内部记录 |
+
+内部备注示例:
+
+> 建议到店进行皮肤刮片或伍德灯检查,确认是否真菌感染。
+
+护理店端则可以显示:
+
+> 当前症状可能不适合普通洗护,建议先到宠物医院确认病因。
+
+这样可以区分医院和护理店权限。
+
+
+
+## 5、预约管理页
+
+## 页面名称
+
+> 预约管理
+
+### 功能设计
+
+用户从 C 端点击“预约线下复核”后,B 端这里收到预约。
+
+### 页面分栏
+
+| 状态 | 说明 |
+| -------- | -------------------------- |
+| 待确认 | 用户发起预约,机构尚未确认 |
+| 已确认 | 机构确认时间 |
+| 今日到店 | 今天需要接待 |
+| 已完成 | 用户已到店 |
+| 已取消 | 用户取消或超时 |
+
+### 预约卡片内容
+
+> 宠物:奶茶
+> 用户:王女士
+> 预约时间:周五 15:30
+> 服务类型:皮肤病复核
+> AI严重程度:中度
+> 是否已发送AI报告:是
+
+按钮:
+
+> 确认预约
+> 修改时间
+> 联系用户
+> 查看AI报告
+> 标记到店
+> 标记完成
+
+### 到店后操作
+
+到店后可以让机构填写:
+
+| 字段 | 示例 |
+| ------------ | -------------- |
+| 实际判断 | 疑似真菌感染 |
+| 是否建议检查 | 是 |
+| 是否建议复诊 | 7天后复诊 |
+| 备注 | 患处有扩散趋势 |
+
+结题展示时可以说:
+
+> 预约管理把 AI 初筛结果转化为线下服务,帮助机构提高到店转化率。
+
+
+
+## 6、复诊跟踪页
+
+你们已经有患病趋势分析,所以 ToB 端一定要接上“复诊跟踪”。
+
+## 页面名称
+
+> 复诊跟踪
+
+### 功能设计
+
+机构可以给某个宠物创建复诊计划。
+
+### 创建复诊任务
+
+字段:
+
+| 字段 | 示例 |
+| ------------------ | ---------------------------------------- |
+| 宠物 | 奶茶 |
+| 复诊时间 | 7天后 |
+| 复诊方式 | 上传图片 / 到店复诊 |
+| 提醒内容 | 请上传患处恢复照片 |
+| 观察指标 | 红肿是否减轻、掉毛是否扩大、抓挠是否减少 |
+| 是否需要再次AI评估 | 是 |
+
+### 复诊列表卡片
+
+> 奶茶
+> 上次严重程度:中度 0.63
+> 本次严重程度:轻度 0.32
+> 趋势:好转
+> 状态:待用户上传复诊图片
+
+### 趋势展示
+
+可以显示:
+
+| 时间 | 严重程度 | 趋势 |
+| ----- | --------- | -------- |
+| 第1次 | 0.63 中度 | 初次诊断 |
+| 第2次 | 0.48 中度 | 缓解 |
+| 第3次 | 0.32 轻度 | 好转 |
+
+这个页面可以和你们已有的“患病趋势分析”功能打通。
+
+### 页面价值
+
+> 对 C 端来说,是病情追踪;
+> 对 B 端来说,是复诊管理和用户留存。
+