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.petId?.name || '未知宠物'}} + {{item.userId?.nickName || '未知宠主'}} + + {{item.severity || '待评估'}} + + + {{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.petId?.name || '未知宠物'}} + {{caseData.petId?.species || ''}} {{caseData.petId?.breed || ''}} + + + {{caseData.severity || '待评估'}} + + + + 宠主 + {{caseData.userId?.nickName || '未知'}} + + + 联系电话 + {{caseData.userId?.phoneNumber || '未提供'}} + + + 建档日期 + {{formatTime(caseData.createdAt)}} + + + 体重 + {{caseData.weight}}kg + + + + + + 诊断记录 + + + + {{item.diseaseName}} + + {{item.isCured === '是' ? '已痊愈' : '治疗中'}} + + + + 症状: + {{item.symptoms.join('、')}} + + + 部位: + {{item.affectedAreas.join('、')}} + + + 用药: + {{item.medication}} + + {{formatTime(item.startDate)}} + + + + 暂无诊断记录 + + + + + + 检查图片 + + + + + + + + 补充信息 + + 过敏史 + {{caseData.allergies}} + + + 近期接触 + {{caseData.recentContact}} + + + 绝育状态 + {{caseData.sterilized}} + + + + + + + AI 病例摘要 + + + + {{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 @@ + + + + + + + + {{doctorName || '合作医生'}} + PetDerma 机构端工作台 + + + + + + + {{stats.total}} + 全部病例 + + + {{stats.pendingReview}} + 待处理 + + + {{stats.followUpNeeded}} + 需复诊 + + + + + + 快捷操作 + + + 📋 + 病例列表 + 查看授权病例 + + + 📤 + 发送AI报告 + 发送诊断报告给机构 + + + 🔄 + 刷新数据 + 同步最新病例 + + + 📊 + 导入测试数据 + 生成演示病例 + + + + + + + + 最近病例 + 查看全部 > + + + + + + + {{item.petId?.name || '未知宠物'}} + {{item.userId?.nickName || '未知宠主'}} + + {{item.severity || '待评估'}} + + + {{item.skinDiseaseHistory?.[item.skinDiseaseHistory.length - 1]?.diseaseName || '暂无诊断'}} + 建档:{{formatTime(item.createdAt)}} + + + 已生成摘要 + 待复诊 + + + + 暂无授权病例 + 等待宠主授权后,病例将显示在此处 + + + + + + + + + + + 选择机构发送报告 + × + + + + + + {{item.name}} + {{item.address}} + {{item.distance}} + + + {{item}} + + + + + 暂无可选机构 + 附近暂无合作的宠物医院或护理店 + + + 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.petName}} + {{item.statusText}} + + {{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 @@ + + + + + + + + + + {{instInfo.name || '未认证机构'}} + {{instInfo.statusText || '待认证'}} + + + + + + + {{stats.pendingPreconsult}} + 待预问诊 + + + {{stats.todayAppointments}} + 今日预约 + + + {{stats.totalReports}} + AI报告 + + + + + + + 🤖 + AI预问诊 + 查看AI诊断报告 + + + 📅 + 预约管理 + 处理预约请求 + + + 🏪 + 机构入驻 + 完善机构信息 + + + + + + + 最新AI预问诊报告 + 查看全部 > + + + + + {{item.petName}} + {{item.time}} + + {{item.diseaseName}} + + {{item.ownerName}} · {{item.petType}} + {{item.statusText}} + + + + 暂无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.petName}} + {{report.petType}} · {{report.petBreed || '未知品种'}} + + + + 宠主 + {{report.ownerName}} + {{report.ownerPhone}} + + + + + + + 🤖 AI诊断摘要 + {{report.statusText}} + + + + 诊断疾病 + {{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 宠医辅助台 + + + + +
+ + + + + + + + + + + + + + + +
加载中...
+ + + + 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 ? `
📈 病情趋势
${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)}
+
+
+
诊断记录
+ ${diseases} +
+
+
🤖 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分析皮肤问题 +
+
+
💬
+ 社区交流 + 分享经验和案例 +
+
+
📖
+ 知识科普 + 学习防治知识 +
+
+
👤
+ 个人中心 + 宠物健康管理 +
+
+ + +
+
+
🏥
+
+ 机构服务 + 宠物医院 / 护理店入驻 +
+
+
+ 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报告 +
+
+ +
+
+
🤖
+ AI预问诊 + 查看AI诊断报告 +
+
+
📅
+ 预约管理 + 处理预约请求 +
+
+
🏪
+ 机构入驻 + 完善机构信息 +
+
+ +
+
最新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 附近机构 - 地图导航 + + + +
+ + + + +
+
+
+
我的位置
+
+
+
+
+
爱宠宠物医院 1.2km
+
+
+
+
毛毛护理店 0.8km
+
+
+
+
美联宠物诊所 2.3km
+
+
+
+
瑞鹏宠物医院 1.8km
+
+
+
📍 共 4 家合作机构在附近
+
+ +
+ +
+ 📋 本次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 诊断结果 + + + +
+ + + +
+ +
+
+
📷
+
📷
+
📷
+
+
⚠️ 已标记异常区域
+
+ + +
+
+ 诊断结果 + 奶茶 +
+
真菌感染(猫癣)
+ +
+ 可信度: +
+
+ 72% +
+
+ +
+ 严重程度: +
+
+
+
+
+
+
+ 中度 +
+ +
+ 猫癣是由真菌引起的常见皮肤病,主要表现为皮肤局部脱毛、发红、皮屑增多等症状。建议尽快到宠物医院进行伍德灯检查确认真菌类型。 +
+ + +
+ 详细预测结果(前三名) +
+ 🦠 真菌感染 +
+
+ 72.0% +
+
+
+ 🤧 过敏性皮炎 +
+
+ 58.0% +
+
+
+ 🐛 螨虫感染 +
+
+ 41.0% +
+
+
+
+ + +
+ 基本诊疗建议 +
+
居家护理
+
就医建议
+
预防措施
+
+
+
保持患处清洁干燥,避免宠物抓挠
+
使用兽医推荐的抗真菌洗液给宠物洗澡
+
定期更换宠物的床垫和玩具
+
使用伊丽莎白圈防止宠物舔舐患处
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + + + + + + +
+ +
+ + + + 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 端来说,是复诊管理和用户留存。 +