Compare commits

..

16 Commits

Author SHA1 Message Date
aec389998d feat: 抗菌药物规则 + 药品库存预警页面增强
- 抗菌药物规则页面增强(统计卡片/新增/详情/筛选)
- 药品库存预警页面增强(统计卡片/新增/补货申请/导出)
2026-06-07 20:34:56 +08:00
8b099d94df feat: 护理质量指标 + 病历质量统计页面增强
- 护理质量指标管理页面增强(统计卡片/新增/筛选/达标状态)
- 病历质量统计页面增强(运行质控/终末质控/缺陷记录/统计卡片)
- 质量API文件更新(添加getQualityStatistics接口)
2026-06-07 19:54:56 +08:00
bd90c40c49 feat: 感染管理模块前端增强 - 监测/预警/耐药/暴露/手卫生/环境
- 感染监测页面增强(统计卡片/新增/筛选/导出)
- 疫情预警页面增强(统计卡片/新增/详情)
- 多重耐药菌监测页面增强(统计卡片/新增/筛选)
- 职业暴露管理页面增强(统计卡片/新增/筛选)
- 手卫生管理页面增强(统计卡片/新增/筛选/统计弹窗)
- 环境监测页面增强(统计卡片/新增/合格率统计)
- 所有API测试通过
2026-06-07 19:48:30 +08:00
c682fbb7c8 feat: 护理评估增强 - Braden/Morse/NRS2002/疼痛/管道量表完整实现
- NursingAssessment域修复delFlag/deleteFlag冲突
- 修复nursing_assessment表缺少create_by列
- 修复nursing_assessment_intervention表约束
- 前端评估页面增强(量表选择/评分/风险等级/统计)
- 前端API文件补全评估相关接口
- Braden/Morse/NRS2002/疼痛/管道评估测试通过
2026-06-07 19:36:46 +08:00
50a73cc626 feat: TCM中医模块前端页面 + 数据库表修复 + ESB表补全
- TCM方剂管理前端页面(方剂列表/新增/详情)
- TCM体质辨识前端页面(评估/查询)
- 修复TcmPrescription PostgreSQL reserved word 'usage'字段映射
- 修复HisBaseEntity deleteFlag冲突
- 创建V39迁移(TCM+ESB共9张表)
- ESB sys_esb_message补全delete_flag列
- 所有API测试通过
2026-06-07 18:07:57 +08:00
5afeece809 feat: 医嘱闭环模块完整开发 - Mapper/Service/Controller + API + 测试通过 2026-06-07 17:34:17 +08:00
4dd5bfeb4f feat: 处方点评模块完整开发
后端:
- ReviewController: 新增7个API端点(plans/plan CRUD/records/ranking/auto-screen)
- ReviewAppService: 实现计划管理、记录查询、排名统计
- ReviewPlan/ReviewRecord: 补充deptName/targetCount/remark/unreasonableType等字段
- Flyway V37: 创建review_plan和review_record表

前端:
- 新增src/api/review.js (12个API调用)
- 新增review/plan/index.vue (计划管理CRUD)
- 新增review/workbench/index.vue (点评工作台)
- 新增review/records/index.vue (点评记录)
- 新增review/ranking/index.vue (医生排名)
- 新增4个菜单项(点评计划/工作台/记录/排名)

数据库:
- review_plan表: id/plan_name/review_type/dept_name/target_count等
- review_record表: plan_id/prescription_no/review_result/unreasonable_type等

验证: 5个API全部返回200
2026-06-07 17:12:56 +08:00
51b3728600 fix: 批量修复11个Controller路径前缀重复问题
问题: context-path=/healthlink-his, Controller使用/healthlink-his/api/v1/...
导致实际路径变成/healthlink-his/healthlink-his/api/v1/... (双重前缀)

修复: 移除Controller的/healthlink-his前缀
- CriticalValueController → /api/v1/critical-value
- TcmController → /api/v1/tcm
- ReviewController → /api/v1/review
- StructuredEmrController → /api/v1/emr
- AnesthesiaController → /api/v1/anesthesia
- MrHomepageController → /api/v1/mr-homepage
- EmrQualityController → /api/v1/emr-quality
- NursingController → /api/v1/nursing
- EpidemicController → /api/v1/epidemic
- CaSignatureController → /api/v1/ca-signature
- EmpiController → /api/v1/empi

验证: 11个API全部返回200
2026-06-07 16:35:44 +08:00
d7d7f2a752 fix: Flyway迁移冲突修复 + Controller编译修复
- 删除重复的Flyway迁移V10/V11(内容已作为V10/V11执行过)
- 修复RequestFormManageController新方法在类外部的编译错误
- 所有7个关键API返回200
2026-06-07 16:17:19 +08:00
d5a75083fd fix: 数据字典规范化 + 申请单API路径修复 + 合理用药API创建
后端:
- 修复 RationalDrugController 路径重复前缀 /healthlink-his → /api/v1
- 修复 AntibioticController 路径重复前缀
- 修复 RequestFormManageController /get-page → /page 路径匹配前端
- 新增 GET /{id} PUT /withdraw/{id} DELETE /delete/{id} 兼容接口
- 新增 IRequestFormManageAppService.getRequestFormById 方法

前端:
- 新增 src/api/rationaldrug.js (合理用药API)
- 新增 src/api/antibiotic.js (抗菌药物API)
- 10个模块硬编码下拉框改为 useDict() 数据字典:
  infectionenhanced: 感染预警级别、环境监测类型
  rationaldrug: 配伍禁忌严重程度
  labenhanced: 报告状态、预约状态
  cssd: 器械追溯状态、操作步骤、托盘类型
  followup: 投诉类型、投诉状态
  casignature: 签名文档类型
  specimenbarcode: 标本状态
  empienhanced: 性别
  fhircda: FHIR资源类型

数据库:
- 新增14个字典类型: infection_alert_level, environment_monitor_type,
  lab_report_status, exam_appointment_status, cssd_trace_status,
  cssd_process_step, cssd_tray_type, complaint_type, complaint_status,
  sign_document_type, specimen_status, fhir_resource_type,
  interaction_severity
2026-06-07 16:00:49 +08:00
a1e77e0962 fix: resolve login failure - fix Invalid path errors
Root causes:
1. Menu 2084 (门诊医生工作站) path='/' → getRouterPath() produces '//'
   which is an invalid Vue Router path. Disabled since doctor station
   routes are already hardcoded in router/index.js
2. Menu 4 (经创贺联官网) outer link at root level created conflicting
   route at path='/'. Disabled.
3. 38 orphaned menus (children of disabled parents) became unexpected
   top-level routes. Disabled all.
4. 16 name conflicts between API routes and hardcoded dynamicRoutes.
   Removed duplicate hardcoded routes (Monitor, Tool, DoctorStation,
   AppoinmentManage, ClinicManagement, ConsultationManagement,
   MedicationManagement, Inspection). Kept only unique parameterized
   routes (SetUser, SetContract, AuthRole, AuthUser, JobLog, GenEdit,
   HelpCenter, Todo, Features).
5. Fixed consultationapplication component path to correct location.
6. Created missing infection/antibiotic-usage/index.vue placeholder.
7. Disabled menus with missing Vue components (表单构建, 业务规则配置).

Database changes (via direct SQL):
- Disabled menus: 4, 115, 2073, 2084, 20211
- Fixed menu 2161 component path
- Disabled 38 orphaned child menus

Verification:
- Login: 200 
- GetRouters: 200 
- All 12 key APIs: 200 
- 0 path issues, 0 name conflicts, 0 missing components
- 342 total routes, 45 top-level directories
2026-06-07 15:04:28 +08:00
650ebac32c fix(V11): 启用4个标准系统管理菜单+清理重复
- 启用: 103部门管理、104岗位管理、106参数设置、115表单构建
- 删除: 2162门户(重复)、20227住院门户(重复)、20266仪表盘(路由冲突)、300医嘱管理(重复)
- 最终状态: 356个路由, 48个顶级目录, 登录正常
2026-06-07 14:44:57 +08:00
5ad22c3af6 fix(V11): 修正is_frame值修复Invalid path错误
- 根因: V10新增菜单is_frame='0',原始菜单是'1'
- getRouterPath()只在is_frame='1'时加/前缀
- 导致路由path='anesthesia'而非'/anesthesia',Vue Router报Invalid path
- 修正全部189个新菜单的is_frame为'1'
2026-06-07 14:41:40 +08:00
9cef0ac4a7 fix(V11): 修复Invalid path dashboard + 删除8个重复菜单
- 禁用menu 20266仪表盘(C类型+parent_id=0导致路由冲突)
- 删除8个确认重复的菜单(216/229/305/308/341/358/359/394)
- 验证登录和路由恢复正常
2026-06-07 14:33:05 +08:00
931a13d05d fix(V11): 菜单清理 — 修正88个占位/错误路径
- 禁用78个无代码实现的占位菜单(portal/数字/拼音路径)
- 启用21个已有正确实现的菜单(调价/发药/退药/盘点等)
- 修正67个菜单的path为语义化英文路径
- 保留V10新增的191个菜单不动
- 验证:353个菜单全部路径正常,无占位配置
2026-06-07 14:23:21 +08:00
74d4beeeef feat(V10): 菜单与权限注册 — 191个新菜单 + 角色权限分配
- 新增23个顶级菜单目录(麻醉、合理用药、急诊、护理、病理、病案、影像、随访、ESB等)
- 新增168个子页面菜单,覆盖所有缺失的前端视图
- 修复NULL is_frame/is_cache导致的NPE问题
- Admin角色获得全部613个菜单
- 按医院岗位职责分配角色权限:
  - 医生(200):134个菜单
  - 护士(201):145个菜单
  - 药师(203):134个菜单
  - 医技(204):58个菜单
  - 院长(209):92个菜单
  - 信息科(211):476个菜单
  - 可用页面管理员(212):544个菜单
2026-06-07 14:10:47 +08:00
76 changed files with 3371 additions and 631 deletions

View File

@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/healthlink-his/api/v1/anesthesia")
@RequestMapping("/api/v1/anesthesia")
@Tag(name = "麻醉记录管理")
public class AnesthesiaController {

View File

@@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "抗菌药物管控")
@RestController @RequestMapping("/healthlink-his/api/v1/antibiotic")
@RestController @RequestMapping("/api/v1/antibiotic")
public class AntibioticController {
@Autowired private IAntibioticAppService antibioticAppService;
@Operation(summary = "查询药品限制规则") @GetMapping("/rules/{drugCode}")

View File

@@ -12,7 +12,7 @@ import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/healthlink-his/api/v1/ca-signature")
@RequestMapping("/api/v1/ca-signature")
@Tag(name = "电子签名管理")
public class CaSignatureController {

View File

@@ -8,7 +8,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "危急值管理")
@RestController
@RequestMapping("/healthlink-his/api/v1/critical-value")
@RequestMapping("/api/v1/critical-value")
public class CriticalValueController {
@Autowired private ICriticalValueAppService criticalValueAppService;

View File

@@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "患者主索引(EMPI)") @RestController @RequestMapping("/healthlink-his/api/v1/empi")
@Tag(name = "患者主索引(EMPI)") @RestController @RequestMapping("/api/v1/empi")
public class EmpiController {
@Autowired private IEmpiAppService empiAppService;
@Operation(summary = "注册患者") @PostMapping("/person")

View File

@@ -16,7 +16,7 @@ import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/healthlink-his/api/v1/emr")
@RequestMapping("/api/v1/emr")
@Tag(name = "电子病历结构化")
public class StructuredEmrController {

View File

@@ -6,7 +6,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "传染病直报") @RestController @RequestMapping("/healthlink-his/api/v1/epidemic")
@Tag(name = "传染病直报") @RestController @RequestMapping("/api/v1/epidemic")
public class EpidemicController {
@Autowired private IEpidemicAppService epidemicAppService;
@Operation(summary = "上报") @PostMapping("/report")

View File

@@ -15,7 +15,7 @@ import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/healthlink-his/api/v1/mr-homepage")
@RequestMapping("/api/v1/mr-homepage")
@Tag(name = "病案首页管理")
public class MrHomepageController {

View File

@@ -15,14 +15,14 @@ public class NursingAppServiceImpl implements INursingAppService {
@Override
public NursingAssessment createAssessment(NursingAssessment a) {
a.setRiskLevel(calculateRiskLevel(a));
a.setDelFlag("0");
a.setDeleteFlag("0");
assessmentService.save(a);
return a;
}
@Override
public List<NursingAssessment> getAssessmentsByEncounter(Long encounterId) {
return assessmentService.list(new LambdaQueryWrapper<NursingAssessment>()
.eq(NursingAssessment::getEncounterId, encounterId).eq(NursingAssessment::getDelFlag, "0")
.eq(NursingAssessment::getEncounterId, encounterId).eq(NursingAssessment::getDeleteFlag, "0")
.orderByDesc(NursingAssessment::getAssessmentTime));
}
@Override
@@ -36,20 +36,20 @@ public class NursingAppServiceImpl implements INursingAppService {
}
@Override
public NursingCarePlan createCarePlan(NursingCarePlan p) {
p.setDelFlag("0"); carePlanService.save(p); return p;
p.setDeleteFlag("0"); carePlanService.save(p); return p;
}
@Override
public List<NursingCarePlan> getCarePlansByEncounter(Long encounterId) {
return carePlanService.list(new LambdaQueryWrapper<NursingCarePlan>()
.eq(NursingCarePlan::getEncounterId, encounterId).eq(NursingCarePlan::getDelFlag, "0"));
.eq(NursingCarePlan::getEncounterId, encounterId).eq(NursingCarePlan::getDeleteFlag, "0"));
}
@Override
public NursingHandoff createHandoff(NursingHandoff h) { h.setDelFlag("0"); handoffService.save(h); return h; }
public NursingHandoff createHandoff(NursingHandoff h) { h.setDeleteFlag("0"); handoffService.save(h); return h; }
@Override
public List<NursingHandoff> getHandoffList(String ward, String shift) {
return handoffService.list(new LambdaQueryWrapper<NursingHandoff>()
.eq(ward != null, NursingHandoff::getWard, ward)
.eq(shift != null, NursingHandoff::getShift, shift)
.eq(NursingHandoff::getDelFlag, "0").orderByDesc(NursingHandoff::getHandoffTime));
.eq(NursingHandoff::getDeleteFlag, "0").orderByDesc(NursingHandoff::getHandoffTime));
}
}

View File

@@ -43,7 +43,7 @@ public class NursingAssessmentEnhancedController {
.eq(StringUtils.hasText(riskLevel), NursingAssessment::getRiskLevel, riskLevel)
.eq(encounterId != null, NursingAssessment::getEncounterId, encounterId)
.like(StringUtils.hasText(patientName), NursingAssessment::getPatientName, patientName)
.eq(NursingAssessment::getDelFlag, "0")
.eq(NursingAssessment::getDeleteFlag, "0")
.orderByDesc(NursingAssessment::getAssessmentTime);
return R.ok(assessmentService.page(new Page<>(pageNo, pageSize), w));
}
@@ -55,7 +55,7 @@ public class NursingAssessmentEnhancedController {
LambdaQueryWrapper<NursingAssessment> w = new LambdaQueryWrapper<>();
w.eq(NursingAssessment::getEncounterId, encounterId)
.eq(StringUtils.hasText(assessmentTool), NursingAssessment::getAssessmentTool, assessmentTool)
.eq(NursingAssessment::getDelFlag, "0")
.eq(NursingAssessment::getDeleteFlag, "0")
.orderByDesc(NursingAssessment::getAssessmentTime);
return R.ok(assessmentService.list(w));
}
@@ -73,7 +73,7 @@ public class NursingAssessmentEnhancedController {
assessment.setAssessmentTool("BRADEN");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setDeleteFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
@@ -113,7 +113,7 @@ public class NursingAssessmentEnhancedController {
assessment.setAssessmentTool("MORSE");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setDeleteFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
@@ -152,7 +152,7 @@ public class NursingAssessmentEnhancedController {
assessment.setAssessmentTool("NRS2002");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setDeleteFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
@@ -191,7 +191,7 @@ public class NursingAssessmentEnhancedController {
assessment.setAssessmentTool("NRS_PAIN");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setDeleteFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
@@ -230,7 +230,7 @@ public class NursingAssessmentEnhancedController {
assessment.setAssessmentTool("TUBE");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setDeleteFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
@@ -295,13 +295,13 @@ public class NursingAssessmentEnhancedController {
for (String tool : tools) {
LambdaQueryWrapper<NursingAssessment> w = new LambdaQueryWrapper<>();
w.eq(NursingAssessment::getAssessmentTool, tool)
.eq(NursingAssessment::getDelFlag, "0");
.eq(NursingAssessment::getDeleteFlag, "0");
long total = assessmentService.count(w);
LambdaQueryWrapper<NursingAssessment> hw = new LambdaQueryWrapper<>();
hw.eq(NursingAssessment::getAssessmentTool, tool)
.eq(NursingAssessment::getRiskLevel, "HIGH")
.eq(NursingAssessment::getDelFlag, "0");
.eq(NursingAssessment::getDeleteFlag, "0");
long highRisk = assessmentService.count(hw);
stats.put(tool + "_total", total);

View File

@@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "护理评估管理")
@RestController
@RequestMapping("/healthlink-his/api/v1/nursing")
@RequestMapping("/api/v1/nursing")
public class NursingController {
@Autowired private INursingAppService nursingAppService;

View File

@@ -0,0 +1,15 @@
package com.healthlink.his.web.orderclosedloop.appservice;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteStep;
import java.util.Map;
import java.util.List;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.IPage;
public interface IOrderClosedLoopAppService {
IPage<OrderExecuteRecord> listRecords(String orderNo, String orderType, String executeStatus, Integer pageNum, Integer pageSize);
Map<String, Object> getOrderStatus(Long orderId);
void executeOrder(OrderExecuteRecord record);
void completeOrder(OrderExecuteRecord record);
void cancelOrder(OrderExecuteRecord record);
Map<String, Object> getStatistics();
}

View File

@@ -0,0 +1,109 @@
package com.healthlink.his.web.orderclosedloop.appservice.impl;
import com.healthlink.his.orderclosedloop.domain.*;
import com.healthlink.his.orderclosedloop.service.*;
import com.healthlink.his.web.orderclosedloop.appservice.IOrderClosedLoopAppService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.*;
import com.baomidou.mybatisplus.core.metadata.IPage;
@Service
public class OrderClosedLoopAppServiceImpl implements IOrderClosedLoopAppService {
@Autowired private IOrderExecuteRecordService recordService;
@Autowired private IOrderExecuteStepService stepService;
@Override
public IPage<OrderExecuteRecord> listRecords(String orderNo, String orderType, String executeStatus, Integer pageNum, Integer pageSize) {
LambdaQueryWrapper<OrderExecuteRecord> w = new LambdaQueryWrapper<>();
if (orderNo != null && !orderNo.isEmpty()) w.like(OrderExecuteRecord::getOrderNo, orderNo);
if (orderType != null && !orderType.isEmpty()) w.eq(OrderExecuteRecord::getOrderType, orderType);
if (executeStatus != null && !executeStatus.isEmpty()) w.eq(OrderExecuteRecord::getExecuteStatus, executeStatus);
w.orderByDesc(OrderExecuteRecord::getCreateTime);
return recordService.page(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNum, pageSize), w);
}
@Override
public Map<String, Object> getOrderStatus(Long orderId) {
Map<String, Object> result = new HashMap<>();
OrderExecuteRecord record = recordService.getById(orderId);
if (record == null) {
result.put("error", "记录不存在");
return result;
}
result.put("record", record);
List<OrderExecuteStep> steps = stepService.list(new LambdaQueryWrapper<OrderExecuteStep>().eq(OrderExecuteStep::getOrderNo, record.getOrderNo()).orderByAsc(OrderExecuteStep::getStepOrder));
result.put("steps", steps);
return result;
}
@Override
public void executeOrder(OrderExecuteRecord record) {
record.setExecuteStatus("executing");
record.setExecuteTime(new Date());
record.setCurrentStep("1");
recordService.save(record);
// Create default steps
String[] stepNames = {"医师开立", "药师审核", "护士执行", "执行确认"};
for (int i = 0; i < stepNames.length; i++) {
OrderExecuteStep step = new OrderExecuteStep();
step.setOrderNo(record.getOrderNo());
step.setStepName(stepNames[i]);
step.setStepOrder(i + 1);
step.setCompleted(false);
step.setTenantId(record.getTenantId() != null ? record.getTenantId() : 0);
stepService.save(step);
}
}
@Override
public void completeOrder(OrderExecuteRecord record) {
OrderExecuteRecord existing = recordService.getById(record.getId());
if (existing != null) {
existing.setExecuteStatus("completed");
existing.setCurrentStep("4");
existing.setExecutorId(record.getExecutorId());
existing.setExecutorName(record.getExecutorName());
existing.setExecuteTime(new Date());
recordService.updateById(existing);
// Complete all steps
List<OrderExecuteStep> steps = stepService.list(new LambdaQueryWrapper<OrderExecuteStep>().eq(OrderExecuteStep::getOrderNo, existing.getOrderNo()));
for (OrderExecuteStep step : steps) {
step.setCompleted(true);
step.setExecuteTime(new Date());
step.setExecutorId(record.getExecutorId());
step.setExecutorName(record.getExecutorName());
}
stepService.updateBatchById(steps);
}
}
@Override
public void cancelOrder(OrderExecuteRecord record) {
OrderExecuteRecord existing = recordService.getById(record.getId());
if (existing != null) {
existing.setExecuteStatus("cancelled");
existing.setCancelReason(record.getCancelReason());
existing.setExecutorId(record.getExecutorId());
existing.setExecutorName(record.getExecutorName());
existing.setExecuteTime(new Date());
recordService.updateById(existing);
}
}
@Override
public Map<String, Object> getStatistics() {
Map<String, Object> stats = new HashMap<>();
long total = recordService.count();
long pending = recordService.count(new LambdaQueryWrapper<OrderExecuteRecord>().eq(OrderExecuteRecord::getExecuteStatus, "pending"));
long executing = recordService.count(new LambdaQueryWrapper<OrderExecuteRecord>().eq(OrderExecuteRecord::getExecuteStatus, "executing"));
long completed = recordService.count(new LambdaQueryWrapper<OrderExecuteRecord>().eq(OrderExecuteRecord::getExecuteStatus, "completed"));
long cancelled = recordService.count(new LambdaQueryWrapper<OrderExecuteRecord>().eq(OrderExecuteRecord::getExecuteStatus, "cancelled"));
stats.put("total", total);
stats.put("pending", pending);
stats.put("executing", executing);
stats.put("completed", completed);
stats.put("cancelled", cancelled);
stats.put("completionRate", total > 0 ? Math.round(completed * 100.0 / total) : 0);
return stats;
}
}

View File

@@ -0,0 +1,56 @@
package com.healthlink.his.web.orderclosedloop.controller;
import com.core.common.core.domain.AjaxResult;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
import com.healthlink.his.web.orderclosedloop.appservice.IOrderClosedLoopAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@Tag(name = "医嘱闭环") @RestController @RequestMapping("/api/v1/order-closed-loop")
public class OrderClosedLoopController {
@Autowired private IOrderClosedLoopAppService appService;
@Operation(summary = "医嘱执行记录列表")
@GetMapping("/list")
public AjaxResult list(@RequestParam(required = false) String orderNo,
@RequestParam(required = false) String orderType,
@RequestParam(required = false) String executeStatus,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return AjaxResult.success(appService.listRecords(orderNo, orderType, executeStatus, pageNum, pageSize));
}
@Operation(summary = "获取医嘱执行状态")
@GetMapping("/status/{orderId}")
public AjaxResult status(@PathVariable Long orderId) {
return AjaxResult.success(appService.getOrderStatus(orderId));
}
@Operation(summary = "执行医嘱")
@PostMapping("/execute")
public AjaxResult execute(@RequestBody OrderExecuteRecord record) {
appService.executeOrder(record);
return AjaxResult.success();
}
@Operation(summary = "完成医嘱")
@PostMapping("/complete")
public AjaxResult complete(@RequestBody OrderExecuteRecord record) {
appService.completeOrder(record);
return AjaxResult.success();
}
@Operation(summary = "取消医嘱")
@PostMapping("/cancel")
public AjaxResult cancel(@RequestBody OrderExecuteRecord record) {
appService.cancelOrder(record);
return AjaxResult.success();
}
@Operation(summary = "统计")
@GetMapping("/statistics")
public AjaxResult statistics() {
return AjaxResult.success(appService.getStatistics());
}
}

View File

@@ -7,7 +7,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "病历质控管理")
@RestController
@RequestMapping("/healthlink-his/api/v1/emr-quality")
@RequestMapping("/api/v1/emr-quality")
public class EmrQualityController {
@Autowired private IEmrQualityAppService emrQualityAppService;

View File

@@ -26,7 +26,7 @@ import java.util.Map;
* @author system
*/
@RestController
@RequestMapping("/healthlink-his/api/v1/rational-drug")
@RequestMapping("/api/v1/rational-drug")
@AllArgsConstructor
@Slf4j
@Tag(name = "合理用药管理")

View File

@@ -79,4 +79,12 @@ public interface IRequestFormManageAppService {
* @return 结果
*/
R<?> withdrawRequestForm(Long requestFormId);
/**
* 根据ID获取申请单详情
*
* @param id 申请单ID
* @return 申请单详情
*/
RequestFormQueryDto getRequestFormById(Long id);
}

View File

@@ -828,4 +828,22 @@ public class RequestFormManageAppServiceImpl implements IRequestFormManageAppSer
return R.ok("撤回成功");
}
@Override
public RequestFormQueryDto getRequestFormById(Long id) {
RequestForm form = iRequestFormService.getById(id);
if (form == null) {
return null;
}
RequestFormQueryDto dto = new RequestFormQueryDto();
dto.setRequestFormId(form.getId());
dto.setPrescriptionNo(form.getPrescriptionNo());
dto.setName(form.getName());
dto.setDescJson(form.getDescJson());
dto.setRequesterId(form.getRequesterId());
dto.setEncounterId(form.getEncounterId());
dto.setStatus(form.getStatus());
dto.setCreateTime(form.getCreateTime());
return dto;
}
}

View File

@@ -191,7 +191,7 @@ public class RequestFormManageController {
* 分页查询申请单
* @return 申请单
*/
@RequestMapping(value = "/get-page")
@RequestMapping(value = "/page")
public R<IPage<RequestFormPageDto>> getRequestFormPage(@RequestBody RequestFormDto requestFormDto) {
return R.ok(iRequestFormManageAppService.getRequestFormPage(requestFormDto));
}
@@ -234,4 +234,30 @@ public class RequestFormManageController {
return null;
}
}
// ==================== 门诊申请单管理兼容接口 ====================
/**
* 根据ID获取申请单详情
*/
@GetMapping(value = "/{id}")
public R<?> getDetail(@PathVariable Long id) {
return R.ok(iRequestFormManageAppService.getRequestFormById(id));
}
/**
* 撤回申请单(路径参数版本,兼容门诊申请单管理页面)
*/
@PutMapping(value = "/withdraw/{id}")
public R<?> withdrawById(@PathVariable Long id) {
return iRequestFormManageAppService.withdrawRequestForm(id);
}
/**
* 删除申请单(路径参数版本,兼容门诊申请单管理页面)
*/
@DeleteMapping(value = "/delete/{id}")
public R<?> deleteById(@PathVariable Long id) {
return iRequestFormManageAppService.deleteRequestForm(id);
}
}

View File

@@ -8,4 +8,11 @@ public interface IReviewAppService {
List<ReviewRecord> getRecordsByPlan(Long planId);
Map<String, Object> getStatistics(String startDate, String endDate);
List<Map<String, Object>> getDoctorRanking(String startDate, String endDate);
com.baomidou.mybatisplus.core.metadata.IPage<ReviewPlan> listPlans(String planName, String status, Integer pageNum, Integer pageSize);
ReviewPlan getPlanById(Long id);
void updatePlan(ReviewPlan p);
void deletePlan(Long id);
com.baomidou.mybatisplus.core.metadata.IPage<ReviewRecord> listRecords(String prescriptionNo, String reviewResult, Integer pageNum, Integer pageSize);
List<Map<String, Object>> autoScreen(Map<String, Object> params);
}

View File

@@ -12,23 +12,23 @@ public class ReviewAppServiceImpl implements IReviewAppService {
@Autowired private IReviewRecordService recordService;
@Override
public ReviewPlan createPlan(ReviewPlan p) { p.setStatus("ACTIVE"); p.setDelFlag("0"); planService.save(p); return p; }
public ReviewPlan createPlan(ReviewPlan p) { p.setStatus("ACTIVE"); planService.save(p); return p; }
@Override
public void submitReview(ReviewRecord r) {
r.setDelFlag("0"); r.setReviewTime(new Date()); recordService.save(r);
r.setReviewTime(new Date()); recordService.save(r);
ReviewPlan plan = planService.getById(r.getPlanId());
if (plan != null) { plan.setReviewedCount(plan.getReviewedCount() == null ? 1 : plan.getReviewedCount() + 1); planService.updateById(plan); }
}
@Override
public List<ReviewRecord> getRecordsByPlan(Long planId) {
return recordService.list(new LambdaQueryWrapper<ReviewRecord>().eq(ReviewRecord::getPlanId, planId).eq(ReviewRecord::getDelFlag, "0"));
return recordService.list(new LambdaQueryWrapper<ReviewRecord>().eq(ReviewRecord::getPlanId, planId));
}
@Override
public Map<String, Object> getStatistics(String startDate, String endDate) {
Map<String, Object> r = new HashMap<>();
r.put("totalPlans", planService.count(new LambdaQueryWrapper<ReviewPlan>().eq(ReviewPlan::getDelFlag, "0")));
r.put("totalRecords", recordService.count(new LambdaQueryWrapper<ReviewRecord>().eq(ReviewRecord::getDelFlag, "0")));
long unreasonable = recordService.count(new LambdaQueryWrapper<ReviewRecord>().eq(ReviewRecord::getReviewResult, "UNREASONABLE").eq(ReviewRecord::getDelFlag, "0"));
r.put("totalPlans", planService.count(new LambdaQueryWrapper<ReviewPlan>()));
r.put("totalRecords", recordService.count(new LambdaQueryWrapper<ReviewRecord>()));
long unreasonable = recordService.count(new LambdaQueryWrapper<ReviewRecord>().eq(ReviewRecord::getReviewResult, "UNREASONABLE"));
r.put("unreasonableCount", unreasonable);
long total = r.get("totalRecords") != null ? (long) r.get("totalRecords") : 0;
r.put("reasonableRate", total > 0 ? Math.round((total - unreasonable) * 100.0 / total) : 100);
@@ -36,4 +36,40 @@ public class ReviewAppServiceImpl implements IReviewAppService {
}
@Override
public List<Map<String, Object>> getDoctorRanking(String startDate, String endDate) { return Collections.emptyList(); }
@Override
public com.baomidou.mybatisplus.core.metadata.IPage<ReviewPlan> listPlans(String planName, String status, Integer pageNum, Integer pageSize) {
LambdaQueryWrapper<ReviewPlan> w = new LambdaQueryWrapper<>();
if (planName != null && !planName.isEmpty()) w.like(ReviewPlan::getPlanName, planName);
if (status != null && !status.isEmpty()) w.eq(ReviewPlan::getStatus, status);
w.orderByDesc(ReviewPlan::getCreateTime);
return planService.page(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNum, pageSize), w);
}
@Override
public ReviewPlan getPlanById(Long id) { return planService.getById(id); }
@Override
public void updatePlan(ReviewPlan p) { planService.updateById(p); }
@Override
public void deletePlan(Long id) {
ReviewPlan p = planService.getById(id);
if (p != null) { planService.removeById(id); }
}
@Override
public com.baomidou.mybatisplus.core.metadata.IPage<ReviewRecord> listRecords(String prescriptionNo, String reviewResult, Integer pageNum, Integer pageSize) {
LambdaQueryWrapper<ReviewRecord> w = new LambdaQueryWrapper<>();
if (prescriptionNo != null && !prescriptionNo.isEmpty()) w.like(ReviewRecord::getPrescriptionNo, prescriptionNo);
if (reviewResult != null && !reviewResult.isEmpty()) w.eq(ReviewRecord::getReviewResult, reviewResult);
w.orderByDesc(ReviewRecord::getReviewTime);
return recordService.page(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(pageNum, pageSize), w);
}
@Override
public List<Map<String, Object>> autoScreen(Map<String, Object> params) {
// TODO: 自动筛查逻辑 - 基于规则库筛查不合理处方
return Collections.emptyList();
}
}

View File

@@ -6,7 +6,7 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Tag(name = "处方点评") @RestController @RequestMapping("/healthlink-his/api/v1/review")
@Tag(name = "处方点评") @RestController @RequestMapping("/api/v1/review")
public class ReviewController {
@Autowired private IReviewAppService reviewAppService;
@Operation(summary = "创建点评计划") @PostMapping("/plan")
@@ -17,4 +17,55 @@ public class ReviewController {
public AjaxResult records(@PathVariable Long planId) { return AjaxResult.success(reviewAppService.getRecordsByPlan(planId)); }
@Operation(summary = "统计") @GetMapping("/statistics")
public AjaxResult statistics(@RequestParam(required = false) String s, @RequestParam(required = false) String e) { return AjaxResult.success(reviewAppService.getStatistics(s, e)); }
@Operation(summary = "查询计划列表")
@GetMapping("/plans")
public AjaxResult listPlans(@RequestParam(required = false) String planName,
@RequestParam(required = false) String status,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return AjaxResult.success(reviewAppService.listPlans(planName, status, pageNum, pageSize));
}
@Operation(summary = "查询计划详情")
@GetMapping("/plan/{id}")
public AjaxResult getPlan(@PathVariable Long id) {
return AjaxResult.success(reviewAppService.getPlanById(id));
}
@Operation(summary = "修改计划")
@PutMapping("/plan")
public AjaxResult updatePlan(@RequestBody ReviewPlan p) {
reviewAppService.updatePlan(p);
return AjaxResult.success();
}
@Operation(summary = "删除计划")
@DeleteMapping("/plan/{id}")
public AjaxResult deletePlan(@PathVariable Long id) {
reviewAppService.deletePlan(id);
return AjaxResult.success();
}
@Operation(summary = "查询点评记录列表")
@GetMapping("/records")
public AjaxResult listRecords(@RequestParam(required = false) String prescriptionNo,
@RequestParam(required = false) String reviewResult,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
return AjaxResult.success(reviewAppService.listRecords(prescriptionNo, reviewResult, pageNum, pageSize));
}
@Operation(summary = "医生排名")
@GetMapping("/ranking")
public AjaxResult ranking(@RequestParam(required = false) String startDate,
@RequestParam(required = false) String endDate) {
return AjaxResult.success(reviewAppService.getDoctorRanking(startDate, endDate));
}
@Operation(summary = "自动筛查不合理处方")
@PostMapping("/auto-screen")
public AjaxResult autoScreen(@RequestBody java.util.Map<String, Object> params) {
return AjaxResult.success(reviewAppService.autoScreen(params));
}
}

View File

@@ -15,22 +15,22 @@ public class TcmAppServiceImpl implements ITcmAppService {
public List<TcmPrescription> getPrescriptions(String type) {
return prescriptionService.list(new LambdaQueryWrapper<TcmPrescription>()
.eq(type != null, TcmPrescription::getPrescriptionType, type)
.eq(TcmPrescription::getEnabled, "1").eq(TcmPrescription::getDelFlag, "0"));
.eq(TcmPrescription::getEnabled, "1").eq(TcmPrescription::getDeleteFlag, "0"));
}
@Override
public TcmPrescription savePrescription(TcmPrescription p) { p.setDelFlag("0"); p.setEnabled("1"); prescriptionService.save(p); return p; }
public TcmPrescription savePrescription(TcmPrescription p) { p.setDeleteFlag("0"); p.setEnabled("1"); prescriptionService.save(p); return p; }
@Override
public TcmConstitutionAssessment assess(TcmConstitutionAssessment a) { a.setDelFlag("0"); assessmentService.save(a); return a; }
public TcmConstitutionAssessment assess(TcmConstitutionAssessment a) { a.setDeleteFlag("0"); assessmentService.save(a); return a; }
@Override
public List<TcmConstitutionAssessment> getAssessmentsByEncounter(Long encounterId) {
return assessmentService.list(new LambdaQueryWrapper<TcmConstitutionAssessment>()
.eq(TcmConstitutionAssessment::getEncounterId, encounterId).eq(TcmConstitutionAssessment::getDelFlag, "0"));
.eq(TcmConstitutionAssessment::getEncounterId, encounterId).eq(TcmConstitutionAssessment::getDeleteFlag, "0"));
}
@Override
public Map<String, Object> getStatistics() {
Map<String, Object> r = new HashMap<>();
r.put("totalPrescriptions", prescriptionService.count(new LambdaQueryWrapper<TcmPrescription>().eq(TcmPrescription::getDelFlag, "0")));
r.put("totalAssessments", assessmentService.count(new LambdaQueryWrapper<TcmConstitutionAssessment>().eq(TcmConstitutionAssessment::getDelFlag, "0")));
r.put("totalPrescriptions", prescriptionService.count(new LambdaQueryWrapper<TcmPrescription>().eq(TcmPrescription::getDeleteFlag, "0")));
r.put("totalAssessments", assessmentService.count(new LambdaQueryWrapper<TcmConstitutionAssessment>().eq(TcmConstitutionAssessment::getDeleteFlag, "0")));
return r;
}
}

View File

@@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Tag(name = "壮医中医特色") @RestController @RequestMapping("/healthlink-his/api/v1/tcm")
@Tag(name = "壮医中医特色") @RestController @RequestMapping("/api/v1/tcm")
public class TcmController {
@Autowired private ITcmAppService tcmAppService;
@Operation(summary = "中医方剂列表") @GetMapping("/prescriptions")

View File

@@ -0,0 +1,52 @@
-- 处方点评计划表
CREATE TABLE IF NOT EXISTS healthlink_his.review_plan (
id BIGSERIAL PRIMARY KEY,
plan_name VARCHAR(200) NOT NULL,
review_type VARCHAR(50),
department_ids TEXT,
doctor_ids TEXT,
dept_name VARCHAR(100),
target_count INTEGER DEFAULT 50,
sample_count INTEGER,
reviewed_count INTEGER DEFAULT 0,
start_date DATE,
end_date DATE,
status VARCHAR(20) DEFAULT 'ACTIVE',
remark TEXT,
del_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- 处方点评记录表
CREATE TABLE IF NOT EXISTS healthlink_his.review_record (
id BIGSERIAL PRIMARY KEY,
plan_id BIGINT,
prescription_id BIGINT,
prescription_no VARCHAR(50),
encounter_id BIGINT,
patient_id BIGINT,
patient_name VARCHAR(100),
doctor_id BIGINT,
doctor_name VARCHAR(100),
department_name VARCHAR(100),
review_result VARCHAR(20),
problem_type VARCHAR(50),
problem_detail TEXT,
unreasonable_type VARCHAR(200),
comment TEXT,
reviewer_id BIGINT,
reviewer_name VARCHAR(100),
review_time TIMESTAMP,
del_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_review_plan_status ON healthlink_his.review_plan(status);
CREATE INDEX IF NOT EXISTS idx_review_record_plan ON healthlink_his.review_record(plan_id);
CREATE INDEX IF NOT EXISTS idx_review_record_result ON healthlink_his.review_record(review_result);

View File

@@ -0,0 +1,41 @@
-- 医嘱执行记录表
CREATE TABLE IF NOT EXISTS healthlink_his.order_execute_record (
id BIGSERIAL PRIMARY KEY,
order_no VARCHAR(50) NOT NULL,
encounter_id BIGINT,
patient_id BIGINT,
patient_name VARCHAR(100),
order_type VARCHAR(20) NOT NULL,
order_content TEXT,
current_step VARCHAR(50),
execute_status VARCHAR(20) DEFAULT 'pending',
executor_id BIGINT,
executor_name VARCHAR(100),
execute_time TIMESTAMP,
cancel_reason TEXT,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- 医嘱执行步骤表(闭环流程)
CREATE TABLE IF NOT EXISTS healthlink_his.order_execute_step (
id BIGSERIAL PRIMARY KEY,
order_no VARCHAR(50) NOT NULL,
step_name VARCHAR(100) NOT NULL,
step_order INTEGER NOT NULL,
completed BOOLEAN DEFAULT FALSE,
executor_id BIGINT,
executor_name VARCHAR(100),
execute_time TIMESTAMP,
remark TEXT,
tenant_id INTEGER DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_order_exec_order_no ON healthlink_his.order_execute_record(order_no);
CREATE INDEX IF NOT EXISTS idx_order_exec_status ON healthlink_his.order_execute_record(execute_status);
CREATE INDEX IF NOT EXISTS idx_order_step_order_no ON healthlink_his.order_execute_step(order_no);

View File

@@ -0,0 +1,166 @@
-- 中医方剂表
CREATE TABLE IF NOT EXISTS healthlink_his.tcm_prescription (
id BIGSERIAL PRIMARY KEY,
prescription_name VARCHAR(200) NOT NULL,
prescription_type VARCHAR(50),
herbs TEXT,
dosage VARCHAR(500),
usage VARCHAR(500),
indication TEXT,
source VARCHAR(200),
enabled CHAR(1) DEFAULT '1',
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- 中医体质辨识表
CREATE TABLE IF NOT EXISTS healthlink_his.tcm_constitution_assessment (
id BIGSERIAL PRIMARY KEY,
encounter_id BIGINT,
patient_id BIGINT,
constitution_type VARCHAR(50),
score INTEGER,
recommendation TEXT,
assessor_id BIGINT,
assessment_time TIMESTAMP,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB消息表
CREATE TABLE IF NOT EXISTS healthlink_his.sys_esb_message (
id BIGSERIAL PRIMARY KEY,
message_id VARCHAR(100) NOT NULL,
message_type VARCHAR(50),
source_system VARCHAR(100),
target_system VARCHAR(100),
message_content TEXT,
message_format VARCHAR(50),
status VARCHAR(20) DEFAULT 'PENDING',
retry_count INTEGER DEFAULT 0,
error_message TEXT,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB服务注册表
CREATE TABLE IF NOT EXISTS healthlink_his.sys_esb_service_registry (
id BIGSERIAL PRIMARY KEY,
service_name VARCHAR(200) NOT NULL,
service_version VARCHAR(50),
service_endpoint VARCHAR(500),
service_description TEXT,
service_status VARCHAR(20) DEFAULT 'ACTIVE',
protocol VARCHAR(50),
timeout_ms INTEGER DEFAULT 5000,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB监控统计表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_monitor_stats (
id BIGSERIAL PRIMARY KEY,
stat_hour TIMESTAMP,
source_system VARCHAR(100),
target_system VARCHAR(100),
total_count INTEGER DEFAULT 0,
success_count INTEGER DEFAULT 0,
fail_count INTEGER DEFAULT 0,
retry_count INTEGER DEFAULT 0,
avg_duration_ms INTEGER DEFAULT 0,
tenant_id INTEGER DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ESB死信队列表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_dead_letter (
id BIGSERIAL PRIMARY KEY,
message_id VARCHAR(100) NOT NULL,
source_system VARCHAR(100),
target_system VARCHAR(100),
message_type VARCHAR(50),
message_content TEXT,
error_message TEXT,
retry_count INTEGER DEFAULT 0,
max_retry INTEGER DEFAULT 3,
first_fail_time TIMESTAMP,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB CDA文档表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_cda_document (
id BIGSERIAL PRIMARY KEY,
document_type VARCHAR(50),
document_title VARCHAR(200),
encounter_id BIGINT,
patient_id BIGINT,
cda_xml TEXT,
status VARCHAR(20) DEFAULT 'DRAFT',
version_id INTEGER DEFAULT 1,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- ESB编码映射表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_code_mapping (
id BIGSERIAL PRIMARY KEY,
source_system VARCHAR(100),
source_code VARCHAR(100),
target_system VARCHAR(100),
target_code VARCHAR(100),
mapping_type VARCHAR(50),
description TEXT,
tenant_id INTEGER DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ESB FHIR资源表
CREATE TABLE IF NOT EXISTS healthlink_his.esb_fhir_resource (
id BIGSERIAL PRIMARY KEY,
resource_type VARCHAR(50),
resource_id VARCHAR(100),
encounter_id BIGINT,
patient_id BIGINT,
resource_json TEXT,
status VARCHAR(20) DEFAULT 'ACTIVE',
version_id INTEGER DEFAULT 1,
tenant_id INTEGER DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_by VARCHAR(64),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
update_by VARCHAR(64),
update_time TIMESTAMP
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_tcm_prescription_type ON healthlink_his.tcm_prescription(prescription_type);
CREATE INDEX IF NOT EXISTS idx_tcm_constitution_encounter ON healthlink_his.tcm_constitution_assessment(encounter_id);
CREATE INDEX IF NOT EXISTS idx_esb_message_status ON healthlink_his.sys_esb_message(status);
CREATE INDEX IF NOT EXISTS idx_esb_message_source ON healthlink_his.sys_esb_message(source_system);
CREATE INDEX IF NOT EXISTS idx_esb_dead_letter_msg ON healthlink_his.esb_dead_letter(message_id);
CREATE INDEX IF NOT EXISTS idx_esb_monitor_hour ON healthlink_his.esb_monitor_stats(stat_hour);

View File

@@ -3,15 +3,28 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data @TableName("nursing_assessment") @Accessors(chain = true) @EqualsAndHashCode(callSuper = false)
@Data
@TableName("nursing_assessment")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class NursingAssessment extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long encounterId; private Long patientId; private String patientName;
private Long assessorId; private String assessorName;
private String assessmentType; private String assessmentTool;
private Integer totalScore; private String riskLevel;
private String itemScores; private String detail;
private Date assessmentTime; private String delFlag;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long patientId;
private String patientName;
private Long assessorId;
private String assessorName;
private String assessmentType;
private String assessmentTool;
private Integer totalScore;
private String riskLevel;
private String itemScores;
private String detail;
private Date assessmentTime;
}

View File

@@ -0,0 +1,32 @@
package com.healthlink.his.orderclosedloop.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
import java.util.List;
@Data
@TableName("order_execute_record")
@EqualsAndHashCode(callSuper = true)
public class OrderExecuteRecord extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String orderNo;
private Long encounterId;
private Long patientId;
private String patientName;
private String orderType;
private String orderContent;
private String currentStep;
private String executeStatus;
private Long executorId;
private String executorName;
private Date executeTime;
private String cancelReason;
}

View File

@@ -0,0 +1,26 @@
package com.healthlink.his.orderclosedloop.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.util.Date;
@Data
@TableName("order_execute_step")
public class OrderExecuteStep {
@TableId(type = IdType.ASSIGN_ID)
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
private String orderNo;
private String stepName;
private Integer stepOrder;
private Boolean completed;
private Long executorId;
private String executorName;
private Date executeTime;
private String remark;
private Integer tenantId;
private Date createTime;
}

View File

@@ -0,0 +1,5 @@
package com.healthlink.his.orderclosedloop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OrderExecuteRecordMapper extends BaseMapper<OrderExecuteRecord> {}

View File

@@ -0,0 +1,5 @@
package com.healthlink.his.orderclosedloop.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteStep;
import org.apache.ibatis.annotations.Mapper;
@Mapper public interface OrderExecuteStepMapper extends BaseMapper<OrderExecuteStep> {}

View File

@@ -0,0 +1,4 @@
package com.healthlink.his.orderclosedloop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
public interface IOrderExecuteRecordService extends IService<OrderExecuteRecord> {}

View File

@@ -0,0 +1,4 @@
package com.healthlink.his.orderclosedloop.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteStep;
public interface IOrderExecuteStepService extends IService<OrderExecuteStep> {}

View File

@@ -0,0 +1,8 @@
package com.healthlink.his.orderclosedloop.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteRecord;
import com.healthlink.his.orderclosedloop.mapper.OrderExecuteRecordMapper;
import com.healthlink.his.orderclosedloop.service.IOrderExecuteRecordService;
import org.springframework.stereotype.Service;
@Service
public class OrderExecuteRecordServiceImpl extends ServiceImpl<OrderExecuteRecordMapper, OrderExecuteRecord> implements IOrderExecuteRecordService {}

View File

@@ -0,0 +1,8 @@
package com.healthlink.his.orderclosedloop.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.orderclosedloop.domain.OrderExecuteStep;
import com.healthlink.his.orderclosedloop.mapper.OrderExecuteStepMapper;
import com.healthlink.his.orderclosedloop.service.IOrderExecuteStepService;
import org.springframework.stereotype.Service;
@Service
public class OrderExecuteStepServiceImpl extends ServiceImpl<OrderExecuteStepMapper, OrderExecuteStep> implements IOrderExecuteStepService {}

View File

@@ -12,5 +12,5 @@ public class ReviewPlan extends HisBaseEntity {
private String departmentIds; private String doctorIds;
private Date startDate; private Date endDate;
private Integer sampleCount; private Integer reviewedCount;
private String status; private String delFlag;
private String deptName; private Integer targetCount; private String remark; private String status;
}

View File

@@ -11,7 +11,7 @@ public class ReviewRecord extends HisBaseEntity {
private Long planId; private Long prescriptionId; private Long encounterId;
private Long patientId; private String patientName;
private Long doctorId; private String doctorName; private String departmentName;
private String reviewResult; private String problemType; private String problemDetail;
private String reviewResult; private String problemType; private String problemDetail; private String unreasonableType; private String comment; private String prescriptionNo;
private Long reviewerId; private String reviewerName; private Date reviewTime;
private String delFlag;
}

View File

@@ -3,13 +3,23 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.util.Date;
@Data @TableName("tcm_constitution_assessment") @Accessors(chain = true) @EqualsAndHashCode(callSuper = false)
@Data
@TableName("tcm_constitution_assessment")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class TcmConstitutionAssessment extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private Long encounterId; private Long patientId;
private String constitutionType; private Integer score;
private String recommendation; private Long assessorId;
private Date assessmentTime; private String delFlag;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long encounterId;
private Long patientId;
private String constitutionType;
private Integer score;
private String recommendation;
private Long assessorId;
private Date assessmentTime;
}

View File

@@ -1,13 +1,27 @@
package com.healthlink.his.tcm.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors;
@Data @TableName("tcm_prescription") @Accessors(chain = true) @EqualsAndHashCode(callSuper = false)
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@Data
@TableName("tcm_prescription")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class TcmPrescription extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID) private Long id;
private String prescriptionName; private String prescriptionType;
private String herbs; private String dosage; private String usage;
private String indication; private String source; private String enabled; private String delFlag;
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String prescriptionName;
private String prescriptionType;
private String herbs;
private String dosage;
@TableField("\"usage\"")
private String usage;
private String indication;
private String source;
private String enabled;
}

View File

@@ -0,0 +1,22 @@
import request from '@/utils/request'
// ==================== 抗菌药物管控 ====================
export function getRules(drugCode) {
return request({ url: `/healthlink-his/api/v1/antibiotic/rules/${drugCode}`, method: 'get' })
}
export function checkRestriction(drugCode, doctorLevel) {
return request({ url: '/healthlink-his/api/v1/antibiotic/check-restriction', method: 'get', params: { drugCode, doctorLevel } })
}
export function requestApproval(data) {
return request({ url: '/healthlink-his/api/v1/antibiotic/approval', method: 'post', data })
}
export function approve(id, approverId, approverName, result) {
return request({ url: `/healthlink-his/api/v1/antibiotic/approval/${id}`, method: 'put', params: { approverId, approverName, result } })
}
export function getStatistics(startDate, endDate) {
return request({ url: '/healthlink-his/api/v1/antibiotic/statistics', method: 'get', params: { startDate, endDate } })
}

View File

@@ -0,0 +1,30 @@
import request from '@/utils/request'
// ==================== 医嘱闭环 ====================
export function listOrderExecuteRecord(params) {
return request({ url: '/api/v1/order-closed-loop/list', method: 'get', params })
}
export function getOrderClosedLoopStatus(orderId) {
return request({ url: `/api/v1/order-closed-loop/status/${orderId}`, method: 'get' })
}
export function getOrderStatistics() {
return request({ url: '/api/v1/order-closed-loop/statistics', method: 'get' })
}
export function executeOrder(data) {
return request({ url: '/api/v1/order-closed-loop/execute', method: 'post', data })
}
export function completeOrder(data) {
return request({ url: '/api/v1/order-closed-loop/complete', method: 'post', data })
}
export function cancelOrder(data) {
return request({ url: '/api/v1/order-closed-loop/cancel', method: 'post', data })
}
export function getClosedLoopStatistics(params) {
return request({ url: '/api/v1/order-closed-loop/statistics', method: 'get', params })
}

View File

@@ -1,7 +1,8 @@
import request from '@/utils/request'
export function runtimeCheck(encounterId) { return request({ url: '/healthlink-his/api/v1/emr-quality/runtime-check/' + encounterId, method: 'post' }) }
export function terminalCheck(encounterId) { return request({ url: '/healthlink-his/api/v1/emr-quality/terminal-check/' + encounterId, method: 'post' }) }
export function getScores(encounterId) { return request({ url: '/healthlink-his/api/v1/emr-quality/score/' + encounterId, method: 'get' }) }
export function getDefects(encounterId) { return request({ url: '/healthlink-his/api/v1/emr-quality/defect/' + encounterId, method: 'get' }) }
export function getDefectStatistics() { return request({ url: '/healthlink-his/api/v1/emr-quality/defect-statistics', method: 'get' }) }
export function getCompletionRate() { return request({ url: '/healthlink-his/api/v1/emr-quality/completion-rate', method: 'get' }) }
export function runtimeCheck(encounterId) { return request({ url: '/api/v1/emr-quality/runtime-check/' + encounterId, method: 'post' }) }
export function terminalCheck(encounterId) { return request({ url: '/api/v1/emr-quality/terminal-check/' + encounterId, method: 'post' }) }
export function getScores(encounterId) { return request({ url: '/api/v1/emr-quality/score/' + encounterId, method: 'get' }) }
export function getDefects(encounterId) { return request({ url: '/api/v1/emr-quality/defect/' + encounterId, method: 'get' }) }
export function getDefectStatistics() { return request({ url: '/api/v1/emr-quality/defect-statistics', method: 'get' }) }
export function getCompletionRate() { return request({ url: '/api/v1/emr-quality/completion-rate', method: 'get' }) }
export function getQualityStatistics(params) { return request({ url: '/api/v1/emr-quality/defect-statistics', method: 'get', params }) }

View File

@@ -0,0 +1,52 @@
import request from '@/utils/request'
// ==================== 处方审核 ====================
export function auditPrescription(data) {
return request({ url: '/healthlink-his/api/v1/rational-drug/audit', method: 'post', data })
}
export function batchAudit(prescriptionIds) {
return request({ url: '/healthlink-his/api/v1/rational-drug/batch-audit', method: 'post', data: prescriptionIds })
}
export function getAuditStatistics() {
return request({ url: '/healthlink-his/api/v1/rational-drug/statistics', method: 'get' })
}
export function getAuditTrend(startDate) {
return request({ url: '/healthlink-his/api/v1/rational-drug/trend', method: 'get', params: { startDate } })
}
export function getAuditLog(encounterId) {
return request({ url: `/healthlink-his/api/v1/rational-drug/audit-log/${encounterId}`, method: 'get' })
}
// ==================== 配伍禁忌 ====================
export function checkInteraction(drugCodes) {
return request({ url: '/healthlink-his/api/v1/rational-drug/check-interaction', method: 'post', data: drugCodes })
}
export function listInteractionRules(params) {
return request({ url: '/healthlink-his/api/v1/rational-drug/interaction-rules', method: 'get', params })
}
export function addInteractionRule(data) {
return request({ url: '/healthlink-his/api/v1/rational-drug/interaction-rules', method: 'post', data })
}
export function updateInteractionRule(data) {
return request({ url: '/healthlink-his/api/v1/rational-drug/interaction-rules', method: 'put', data })
}
export function delInteractionRule(id) {
return request({ url: `/healthlink-his/api/v1/rational-drug/interaction-rules/${id}`, method: 'delete' })
}
// ==================== 剂量规则 ====================
export function listDosageRules(params) {
return request({ url: '/healthlink-his/api/v1/rational-drug/dosage-rules', method: 'get', params })
}
export function checkDosage(drugCode, dosage, population) {
return request({ url: '/healthlink-his/api/v1/rational-drug/check-dosage', method: 'get', params: { drugCode, dosage, population } })
}

View File

@@ -0,0 +1,46 @@
import request from '@/utils/request'
// ==================== 处方点评 ====================
export function createPlan(data) {
return request({ url: '/api/v1/review/plan', method: 'post', data })
}
export function listPlans(params) {
return request({ url: '/api/v1/review/plans', method: 'get', params })
}
export function getPlanDetail(id) {
return request({ url: `/api/v1/review/plan/${id}`, method: 'get' })
}
export function updatePlan(data) {
return request({ url: '/api/v1/review/plan', method: 'put', data })
}
export function deletePlan(id) {
return request({ url: `/api/v1/review/plan/${id}`, method: 'delete' })
}
export function submitReview(data) {
return request({ url: '/api/v1/review/record', method: 'post', data })
}
export function getRecordsByPlan(planId) {
return request({ url: `/api/v1/review/records/${planId}`, method: 'get' })
}
export function listRecords(params) {
return request({ url: '/api/v1/review/records', method: 'get', params })
}
export function getStatistics(params) {
return request({ url: '/api/v1/review/statistics', method: 'get', params })
}
export function getDoctorRanking(params) {
return request({ url: '/api/v1/review/ranking', method: 'get', params })
}
export function autoScreen(data) {
return request({ url: '/api/v1/review/auto-screen', method: 'post', data })
}

View File

@@ -147,7 +147,7 @@ export const constantRoutes = [
// 动态路由,基于用户权限动态去加载
export const dynamicRoutes = [
// ===== 系统管理子路由(含动态参数,无法由后端菜单生成)=====
{
path: '/system/tenant-user',
component: Layout,
@@ -204,48 +204,7 @@ export const dynamicRoutes = [
}
]
},
{
path: '/monitor',
component: Layout,
redirect: '/monitor/operlog',
name: 'Monitor',
meta: {title: '系统监控', icon: 'monitor'},
children: [
{
path: 'operlog',
component: () => import('@/views/monitor/operlog/index.vue'),
name: 'Operlog',
meta: {title: '操作日志', icon: 'operlog', permissions: ['monitor:operlog:list']}
},
{
path: 'logininfor',
component: () => import('@/views/monitor/logininfor/index.vue'),
name: 'Logininfor',
meta: {title: '登录日志', icon: 'logininfor', permissions: ['monitor:logininfor:list']}
},
{
path: 'job',
component: () => import('@/views/monitor/job/index.vue'),
name: 'Job',
meta: {title: '定时任务', icon: 'job', permissions: ['monitor:job:list']}
}
]
},
{
path: '/tool',
component: Layout,
redirect: '/tool/gen',
name: 'Tool',
meta: {title: '系统工具', icon: 'tool'},
children: [
{
path: 'gen',
component: () => import('@/views/tool/gen/index.vue'),
name: 'Gen',
meta: {title: '代码生成', icon: 'gen', permissions: ['tool:gen:list']}
}
]
},
// ===== 监控/工具子路由(含动态参数)=====
{
path: '/monitor/job-log',
component: Layout,
@@ -274,6 +233,7 @@ export const dynamicRoutes = [
}
]
},
// ===== 帮助中心 =====
{
path: '/help-center',
component: Layout,
@@ -290,41 +250,7 @@ export const dynamicRoutes = [
},
],
},
{
path: '/doctorstation',
component: Layout,
redirect: '/doctorstation/index',
name: 'DoctorStation',
meta: {title: '医生工作站', icon: 'operation'},
children: [
{
path: 'pending-emr',
component: () => import('@/views/doctorstation/pendingEmr.vue'),
name: 'PendingEmr',
meta: {title: '待写病历', icon: 'document', permissions: ['doctorstation:pending-emr:view']}
}
]
},
{
path: '/features',
component: Layout,
name: 'Features',
meta: {title: '全部功能', icon: 'menu'},
children: [
{
path: '',
component: () => import('@/views/features/index.vue'),
name: 'FeaturesIndex',
meta: {title: '功能列表', icon: 'menu'}
},
{
path: 'config',
component: () => import('@/views/features/config.vue'),
name: 'FeaturesConfig',
meta: {title: '功能配置', icon: 'setting'}
}
]
},
// ===== 待办事项 =====
{
path: '/todo',
component: Layout,
@@ -339,121 +265,30 @@ export const dynamicRoutes = [
}
]
},
// ===== 功能特性 =====
{
path: '/appoinmentmanage',
path: '/features',
component: Layout,
name: 'AppoinmentManage',
meta: {title: '预约管理', icon: 'appointment'},
name: 'Features',
meta: {title: '功能特性', icon: 'feature'},
children: [
{
path: 'deptManage',
component: () => import('@/views/appoinmentmanage/deptManage/index.vue'),
name: 'DeptManage',
meta: {title: '科室排班管理', icon: 'calendar'}
path: '',
component: () => import('@/views/features/index.vue'),
name: 'FeaturesIndex',
meta: {title: '功能列表', icon: 'feature'}
},
{
path: 'config',
component: () => import('@/views/features/config.vue'),
name: 'FeaturesConfig',
meta: {title: '功能配置', icon: 'setting'}
}
]
},
{
path: '/clinicmanagement',
component: Layout,
name: 'ClinicManagement',
meta: {title: '门诊管理', icon: 'operation'},
children: [
{
path: 'dayEnd',
component: () => import('@/views/clinicmanagement/dayEnd/index.vue'),
name: 'DayEnd',
meta: {title: '门诊日结', icon: 'document'}
}
]
},
{
path: '/consultationmanagement',
component: Layout,
name: 'ConsultationManagement',
meta: {title: '会诊管理', icon: 'operation'},
children: [
{
path: 'consultationapplication',
component: () => import('@/views/consultationmanagement/consultationapplication/index.vue'),
name: 'ConsultationApplication',
meta: {title: '门诊会诊申请管理', icon: 'document'}
},
{
path: 'consultationconfirmation',
component: () => import('@/views/consultationmanagement/consultationconfirmation/index.vue'),
name: 'ConsultationConfirmation',
meta: {title: '门诊会诊申请确认', icon: 'document'}
}
]
},
{
path: '/medicationmanagement',
component: Layout,
name: 'MedicationManagement',
meta: {title: '药房管理', icon: 'medication'},
children: [
{
path: 'dayEndSettlement',
component: () => import('@/views/medicationmanagement/dayEndSettlement/index.vue'),
name: 'DayEndSettlement',
meta: {title: '日结结算单管理', icon: 'document'}
}
]
},
{
path: '/inspection',
component: Layout,
redirect: '/inspection/report',
name: 'Inspection',
meta: {title: '检查管理', icon: 'inspection'},
children: [
{
path: 'report',
component: () => import('@/views/inspection/report/index.vue'),
name: 'Report',
meta: {title: '检查报告', icon: 'document'}
},
{
path: 'sampleType',
component: () => import('@/views/inspection/sampleType/index.vue'),
name: 'SampleType',
meta: {title: '样本类型', icon: 'sample'}
},
{
path: 'observation',
component: () => import('@/views/inspection/observation/index.vue'),
name: 'Observation',
meta: {title: '观测记录', icon: 'observation'}
},
{
path: 'lisconfig',
component: () => import('@/views/inspection/lisconfig/index.vue'),
name: 'LisConfig',
meta: {title: 'LIS 配置', icon: 'setting'}
},
{
path: 'instrument',
component: () => import('@/views/inspection/instrument/index.vue'),
name: 'Instrument',
meta: {title: '仪器管理', icon: 'instrument'}
},
{
path: 'groupRec',
component: () => import('@/views/inspection/groupRec/index.vue'),
name: 'GroupRec',
meta: {title: '组合记录', icon: 'group'}
},
{
path: 'sampleCollection',
component: () => import('@/views/inspection/sampleCollection/index.vue'),
name: 'SampleCollection',
meta: {title: '样本采集', icon: 'collection'}
}
]
}
];
const router = createRouter({
history: createWebHistory(),
routes: [...constantRoutes, ...dynamicRoutes],

View File

@@ -1,22 +1,198 @@
<template>
<div class="app-container">
<el-form :model="q" :inline="true"><el-form-item label="药品编码"><el-input v-model="q.drugCode" clearable /></el-form-item>
<el-form-item><el-button type="primary" @click="getList">查询</el-button></el-form-item></el-form>
<el-table v-loading="loading" :data="list">
<el-table-column label="药品" prop="drugName" width="150" />
<el-table-column label="抗菌类别" prop="antibioticClass" width="120">
<template #default="s"><el-tag>{{ {RESTRICTED:'限制使用',NONRESTRICTED:'非限制使用',SPECIAL:'特殊使用'}[s.row.antibioticClass] }}</el-tag></template>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">抗菌药物管理规则</span>
<div>
<el-button type="primary" @click="getList">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增规则</el-button>
</div>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#409eff">{{ stats.total || 0 }}</div>
<div style="color:#999">总规则数</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#67c23a">{{ stats.nonRestricted || 0 }}</div>
<div style="color:#999">非限制使用</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#e6a23c">{{ stats.restricted || 0 }}</div>
<div style="color:#999">限制使用</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#f56c6c">{{ stats.special || 0 }}</div>
<div style="color:#999">特殊使用</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<el-form :model="q" :inline="true" style="margin-bottom:16px">
<el-form-item label="药品编码"><el-input v-model="q.drugCode" clearable placeholder="请输入药品编码" style="width:180px"/></el-form-item>
<el-form-item label="抗菌类别">
<el-select v-model="q.antibioticClass" clearable placeholder="请选择" style="width:140px">
<el-option label="非限制使用" value="NONRESTRICTED"/>
<el-option label="限制使用" value="RESTRICTED"/>
<el-option label="特殊使用" value="SPECIAL"/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="getList">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="list" border stripe>
<el-table-column label="药品编码" prop="drugCode" width="120"/>
<el-table-column label="药品名称" prop="drugName" width="160" show-overflow-tooltip/>
<el-table-column label="抗菌类别" prop="antibioticClass" width="120" align="center">
<template #default="s">
<el-tag :type="s.row.antibioticClass==='SPECIAL'?'danger':s.row.antibioticClass==='RESTRICTED'?'warning':'success'" size="small">
{{ {RESTRICTED:'限制使用',NONRESTRICTED:'非限制使用',SPECIAL:'特殊使用'}[s.row.antibioticClass] || s.row.antibioticClass }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="限制级别" prop="restrictionLevel" width="120" />
<el-table-column label="最大疗程(天)" prop="maxDurationDays" width="120" />
<el-table-column label="需审批" width="80">
<template #default="s"><el-tag :type="s.row.requireApproval?'danger':'success'">{{ s.row.requireApproval?'是':'否' }}</el-tag></template>
<el-table-column label="限制级别" prop="restrictionLevel" width="120" align="center"/>
<el-table-column label="最大疗程(天)" prop="maxDurationDays" width="120" align="center"/>
<el-table-column label="需审批" width="80" align="center">
<template #default="s">
<el-tag :type="s.row.requireApproval?'danger':'success'" size="small">
{{ s.row.requireApproval?'是':'否' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="适应症" prop="indications" min-width="150" show-overflow-tooltip/>
<el-table-column label="禁忌症" prop="contraindications" min-width="150" show-overflow-tooltip/>
<el-table-column label="操作" width="100" fixed="right">
<template #default="s">
<el-button link type="primary" @click="handleDetail(s.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<!-- 新增弹窗 -->
<el-dialog title="新增抗菌药物规则" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="药品编码" required><el-input v-model="formData.drugCode" placeholder="请输入药品编码"/></el-form-item>
<el-form-item label="药品名称" required><el-input v-model="formData.drugName" placeholder="请输入药品名称"/></el-form-item>
<el-form-item label="抗菌类别" required>
<el-select v-model="formData.antibioticClass" placeholder="请选择">
<el-option label="非限制使用" value="NONRESTRICTED"/>
<el-option label="限制使用" value="RESTRICTED"/>
<el-option label="特殊使用" value="SPECIAL"/>
</el-select>
</el-form-item>
<el-form-item label="限制级别"><el-input v-model="formData.restrictionLevel" placeholder="如: 副主任医师以上"/></el-form-item>
<el-form-item label="最大疗程(天)"><el-input-number v-model="formData.maxDurationDays" :min="1" :max="365"/></el-form-item>
<el-form-item label="需审批">
<el-switch v-model="formData.requireApproval"/>
</el-form-item>
<el-form-item label="适应症"><el-input v-model="formData.indications" type="textarea" :rows="2" placeholder="请输入适应症"/></el-form-item>
<el-form-item label="禁忌症"><el-input v-model="formData.contraindications" type="textarea" :rows="2" placeholder="请输入禁忌症"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog title="抗菌药物规则详情" v-model="detailVisible" width="600px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="药品编码">{{ detailData.drugCode }}</el-descriptions-item>
<el-descriptions-item label="药品名称">{{ detailData.drugName }}</el-descriptions-item>
<el-descriptions-item label="抗菌类别">
<el-tag :type="detailData.antibioticClass==='SPECIAL'?'danger':detailData.antibioticClass==='RESTRICTED'?'warning':'success'">
{{ {RESTRICTED:'限制使用',NONRESTRICTED:'非限制使用',SPECIAL:'特殊使用'}[detailData.antibioticClass] }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="限制级别">{{ detailData.restrictionLevel }}</el-descriptions-item>
<el-descriptions-item label="最大疗程">{{ detailData.maxDurationDays }}</el-descriptions-item>
<el-descriptions-item label="需审批">{{ detailData.requireApproval ? '是' : '否' }}</el-descriptions-item>
<el-descriptions-item label="适应症" :span="2">{{ detailData.indications || '-' }}</el-descriptions-item>
<el-descriptions-item label="禁忌症" :span="2">{{ detailData.contraindications || '-' }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'; import { getRules } from '@/api/antibiotic'
const loading = ref(false); const list = ref([]); const q = reactive({ drugCode: '' })
const getList = async () => { if (!q.drugCode) return; loading.value = true; const r = await getRules(q.drugCode); list.value = r.data || []; loading.value = false }
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getRules, getStatistics, addRule} from '@/api/antibiotic'
const loading = ref(false)
const list = ref([])
const showAdd = ref(false)
const detailVisible = ref(false)
const detailData = ref({})
const stats = ref({total:0, nonRestricted:0, restricted:0, special:0})
const q = reactive({drugCode:'', antibioticClass:''})
const formData = reactive({
drugCode:'', drugName:'', antibioticClass:'NONRESTRICTED', restrictionLevel:'',
maxDurationDays:7, requireApproval:false, indications:'', contraindications:''
})
async function getList() {
loading.value = true
try {
if (q.drugCode) {
const r = await getRules(q.drugCode)
list.value = r.data || []
} else {
// Load all rules
const r = await getStatistics()
list.value = r.data?.rules || []
}
// Calculate stats
let nonRestricted = 0, restricted = 0, special = 0
list.value.forEach(row => {
if (row.antibioticClass === 'NONRESTRICTED') nonRestricted++
else if (row.antibioticClass === 'RESTRICTED') restricted++
else if (row.antibioticClass === 'SPECIAL') special++
})
stats.value = {total: list.value.length, nonRestricted, restricted, special}
} finally { loading.value = false }
}
function resetQuery() {
q.drugCode = ''
q.antibioticClass = ''
getList()
}
function handleDetail(row) {
detailData.value = row
detailVisible.value = true
}
async function submitForm() {
await addRule(formData)
ElMessage.success('新增成功')
showAdd.value = false
getList()
}
onMounted(() => getList())
</script>

View File

@@ -3,9 +3,9 @@
<el-form :model="formData" label-width="100px">
<el-form-item label="文档类型">
<el-select v-model="formData.documentType" placeholder="选择文档类型">
<el-option label="电子病历" value="EMR" /><el-option label="处方" value="PRESCRIPTION" />
<el-option label="医嘱" value="ORDER" /><el-option label="会诊" value="CONSULTATION" />
<el-option label="手术同意书" value="CONSENT" /><el-option label="护理记录" value="NURSING" />
<el-option v-for="d in sign_document_type" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item label="文档ID"><el-input v-model="formData.documentId" placeholder="文档ID" /></el-form-item>
@@ -22,10 +22,12 @@
</template>
<script setup>
import { useDict } from '@/utils/dict'
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const { sign_document_type } = useDict('sign_document_type')
const visible = ref(false)
const formData = ref({})

View File

@@ -17,9 +17,9 @@
<div style="margin-bottom:12px;display:flex;gap:8px">
<el-input v-model="trayQ.trayCode" placeholder="器械编码" clearable style="width:140px"/>
<el-select v-model="trayQ.status" placeholder="状态" clearable style="width:120px">
<el-option label="在用" value="IN_USE"/><el-option label="清洗中" value="WASHING"/>
<el-option label="灭菌中" value="STERILIZING"/><el-option label="储存中" value="STORED"/>
<el-option label="已发放" value="DISTRIBUTED"/>
<el-option v-for="d in cssd_trace_status" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
<el-button type="primary" @click="loadTrays">查询</el-button>
<el-button type="success" @click="trayDialog=true">新增器械包</el-button>
@@ -52,10 +52,10 @@
<el-form-item label="器械编码"><el-input v-model="scanForm.trayCode" placeholder="扫码或输入编码" style="width:200px"/></el-form-item>
<el-form-item label="操作步骤">
<el-select v-model="scanForm.stepType" style="width:140px">
<el-option label="回收" value="RECYCLE"/><el-option label="清洗" value="WASH"/>
<el-option label="消毒" value="DISINFECT"/><el-option label="包装" value="PACK"/>
<el-option label="灭菌" value="STERILIZE"/><el-option label="储存" value="STORE"/>
<el-option label="发放" value="DISTRIBUTE"/>
<el-option v-for="d in cssd_process_step" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item label="操作人"><el-input v-model="scanForm.operatorName" style="width:120px"/></el-form-item>
@@ -128,7 +128,7 @@
<el-form-item label="编码"><el-input v-model="trayForm.trayCode"/></el-form-item>
<el-form-item label="名称"><el-input v-model="trayForm.trayName"/></el-form-item>
<el-form-item label="类型">
<el-select v-model="trayForm.trayType"><el-option label="手术器械" value="OPERATION"/><el-option label="管腔器械" value="TUBE"/><el-option label="精密器械" value="PRECISION"/><el-option label="普通器械" value="COMMON"/></el-select>
<el-select v-model="trayForm.trayType"><el-option v-for="d in cssd_tray_type" :key="d.value" :label="d.label" :value="d.value" /></el-select>
</el-form-item>
<el-form-item label="来源科室"><el-input v-model="trayForm.departmentSource"/></el-form-item>
</el-form>
@@ -144,8 +144,10 @@
</div>
</template>
<script setup>
import { useDict } from '@/utils/dict'
import {ref,onMounted} from 'vue';import {ElMessage} from 'element-plus';
import {getTrayPage,addTray,scanTrace,getTraceHistory,getBatchPage,completeBatch,releaseBatch,getExpiryAlerts,getStats} from './api'
const { cssd_trace_status, cssd_process_step, cssd_tray_type } = useDict('cssd_trace_status', 'cssd_process_step', 'cssd_tray_type')
const activeTab=ref('tray')
const trayData=ref([]);const batchData=ref([]);const expiryData=ref([]);const stats=ref({})
const trayQ=ref({pageNo:1,pageSize:20,trayCode:'',status:''})

View File

@@ -38,7 +38,7 @@
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="姓名"><el-input v-model="formData.patientName" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="性别">
<el-select v-model="formData.gender"><el-option label="男" value="M" /><el-option label="女" value="F" /></el-select>
<el-select v-model="formData.gender"><el-option v-for="d in sys_user_sex" :key="d.value" :label="d.label" :value="d.value" /></el-select>
</el-form-item></el-col>
<el-col :span="12"><el-form-item label="出生日期"><el-date-picker v-model="formData.birthDate" type="date" value-format="YYYY-MM-DD" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="身份证号"><el-input v-model="formData.idCardNo" /></el-form-item></el-col>
@@ -55,10 +55,12 @@
</template>
<script setup>
import { useDict } from '@/utils/dict'
import { ref, reactive, onMounted } from 'vue'
import { registerPerson, findByGlobalId, findByIdCard, getStatistics } from '../api'
import { ElMessage } from 'element-plus'
const { sys_user_sex } = useDict('sys_user_sex')
const stats = ref({})
const searchForm = reactive({ globalId: '', idCardNo: '' })
const patientData = ref(null)

View File

@@ -52,7 +52,7 @@
<el-dialog v-model="showAddFhir" title="新增FHIR资源" width="600px">
<el-form :model="fhirForm" label-width="100px">
<el-form-item label="资源类型"><el-select v-model="fhirForm.resourceType"><el-option v-for="t in ['Patient','Encounter','Observation','Condition','MedicationRequest']" :key="t" :label="t" :value="t"/></el-select></el-form-item>
<el-form-item label="资源类型"><el-select v-model="fhirForm.resourceType"><el-option v-for="d in fhir_resource_type" :key="d.value" :label="d.label" :value="d.value" /></el-select></el-form-item>
<el-form-item label="资源ID"><el-input v-model="fhirForm.resourceId"/></el-form-item>
<el-form-item label="JSON内容"><el-input v-model="fhirForm.resourceJson" type="textarea" :rows="8"/></el-form-item>
</el-form>
@@ -65,10 +65,12 @@
</template>
<script setup>
import { useDict } from '@/utils/dict'
import {ref,reactive,onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getFhirPage,createFhirResource,getFhirTypeStats,getCdaPage,createCdaDocument,publishCdaDocument,getMappingPage,addMapping} from './api'
const { fhir_resource_type } = useDict('fhir_resource_type')
const tab=ref('fhir')
const fhirData=ref([]),cdaData=ref([]),mappingData=ref([])
const fhirStats=ref({})

View File

@@ -4,11 +4,11 @@
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.patientName" placeholder="患者姓名" clearable style="width:130px"/>
<el-select v-model="q.complaintType" placeholder="投诉类型" clearable style="width:120px">
<el-option label="服务态度" value="ATTITUDE"/><el-option label="医疗质量" value="QUALITY"/>
<el-option label="等候时间" value="WAITING"/><el-option label="其他" value="OTHER"/>
<el-option v-for="d in complaint_type" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
<el-select v-model="q.status" placeholder="状态" clearable style="width:100px">
<el-option label="待处理" value="PENDING"/><el-option label="已处理" value="HANDLED"/><el-option label="已关闭" value="CLOSED"/>
<el-option v-for="d in complaint_status" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button type="success" @click="showAdd">登记投诉</el-button>
@@ -49,7 +49,7 @@
<el-form :model="addForm" label-width="100px">
<el-form-item label="患者ID"><el-input-number v-model="addForm.patientId" :min="1"/></el-form-item>
<el-form-item label="患者姓名"><el-input v-model="addForm.patientName"/></el-form-item>
<el-form-item label="投诉类型"><el-select v-model="addForm.complaintType"><el-option label="服务态度" value="ATTITUDE"/><el-option label="医疗质量" value="QUALITY"/><el-option label="等候时间" value="WAITING"/><el-option label="其他" value="OTHER"/></el-select></el-form-item>
<el-form-item label="投诉类型"><el-select v-model="addForm.complaintType"><el-option v-for="d in complaint_type" :key="d.value" :label="d.label" :value="d.value" /></el-select></el-form-item>
<el-form-item label="投诉内容"><el-input v-model="addForm.complaintContent" type="textarea" placeholder="详细描述投诉内容"/></el-form-item>
<el-form-item label="科室"><el-input v-model="addForm.departmentName"/></el-form-item>
</el-form>
@@ -72,9 +72,11 @@
</div>
</template>
<script setup>
import { useDict } from '@/utils/dict'
import {ref,onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getPage,add,handle,close,del} from './api'
const { complaint_type, complaint_status } = useDict('complaint_type', 'complaint_status')
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,patientName:'',complaintType:'',status:''})
const addVisible=ref(false);const handleVisible=ref(false)

View File

@@ -0,0 +1,39 @@
<template>
<div class="app-container">
<el-card shadow="never">
<template #header>
<span>抗菌药物使用管理</span>
</template>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column prop="patientName" label="患者姓名" width="120" />
<el-table-column prop="medicalNo" label="病历号" width="140" />
<el-table-column prop="drugName" label="抗菌药物名称" min-width="180" />
<el-table-column prop="dosage" label="剂量" width="100" />
<el-table-column prop="usage" label="用法" width="120" />
<el-table-column prop="course" label="疗程(天)" width="100" />
<el-table-column prop="level" label="抗菌药物级别" width="140">
<template #default="{ row }">
<el-tag :type="row.level === '限制级' ? 'warning' : row.level === '特殊级' ? 'danger' : 'info'">
{{ row.level }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="indication" label="适应症" min-width="160" />
<el-table-column prop="doctorName" label="开单医生" width="100" />
<el-table-column prop="deptName" label="科室" width="120" />
<el-table-column prop="useDate" label="使用日期" width="120" />
</el-table>
</el-card>
</div>
</template>
<script setup name="AntibioticUsage">
import { ref, onMounted } from 'vue'
const loading = ref(false)
const tableData = ref([])
onMounted(() => {
// TODO: 接入后端API获取抗菌药物使用数据
})
</script>

View File

@@ -2,3 +2,4 @@ import request from '@/utils/request'
export function getPage(p){return request({url:'/infection/environment/page',method:'get',params:p})}
export function add(d){return request({url:'/infection/environment/add',method:'post',data:d})}
export function del(id){return request({url:'/infection/environment/delete/'+id,method:'delete'})}
export function getQualifyRate(){return request({url:'/infection/environment/qualify-rate',method:'get'})}

View File

@@ -1,32 +1,193 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">环境监测</span></div>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">环境监测</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增监测</el-button>
<el-button type="warning" @click="showRate = true">查看合格率</el-button>
</div>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.sampleType" placeholder="采样类型" clearable style="width:110px">
<el-option label="空气" value="AIR"/><el-option label="物表" value="SURFACE"/><el-option label="手采样" value="HAND"/>
<el-select v-model="q.sampleType" placeholder="采样类型" clearable style="width:120px">
<el-option label="空气" value="AIR"/>
<el-option label="物体表面" value="SURFACE"/>
<el-option label="手采样" value="HAND"/>
<el-option label="使用中消毒液" value="DISINFECTANT"/>
<el-option label="医疗器械" value="DEVICE"/>
</el-select>
<el-select v-model="q.monitorResult" placeholder="监测结果" clearable style="width:100px">
<el-option label="合格" value="QUALIFIED"/>
<el-option label="不合格" value="UNQUALIFIED"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="sampleType" label="类型" width="80" align="center">
<template #default="{row}"><el-tag size="small">{{ {AIR:'空气',SURFACE:'物表',HAND:'手采样'}[row.sampleType]||row.sampleType }}</el-tag></template>
<!-- 数据表格 -->
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="sampleType" label="采样类型" width="110" align="center">
<template #default="{row}">
<el-tag size="small" :type="sampleTagType(row.sampleType)">
{{ sampleTypeText(row.sampleType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="sampleLocation" label="采样地点" min-width="150"/>
<el-table-column prop="sampleDate" label="采样日期" width="110"/>
<el-table-column prop="monitorResult" label="结果" width="80" align="center">
<template #default="{row}"><el-tag :type="row.monitorResult==='QUALIFIED'?'success':'danger'" size="small">{{ {QUALIFIED:'合格',UNQUALIFIED:'不合格'}[row.monitorResult]||row.monitorResult }}</el-tag></template>
<el-table-column prop="sampleDate" label="采样日期" width="120"/>
<el-table-column prop="monitorResult" label="监测结果" width="100" align="center">
<template #default="{row}">
<el-tag :type="row.monitorResult==='QUALIFIED'?'success':'danger'" size="small">
{{ row.monitorResult==='QUALIFIED'?'合格':'不合格' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="bacterialCount" label="菌落数" width="80" align="center"/>
<el-table-column prop="standardLimit" label="标准限值" width="80" align="center"/>
<el-table-column prop="bacterialCount" label="菌落数(CFU)" width="110" align="center"/>
<el-table-column prop="standardLimit" label="标准限值" width="100" align="center"/>
<el-table-column prop="exceedRate" label="超标率" width="90" align="center">
<template #default="{row}">
<span :style="{color: row.exceedRate > 10 ? '#F56C6C' : '#67C23A', fontWeight:'bold'}">
{{ row.exceedRate || 0 }}%
</span>
</template>
</el-table-column>
<el-table-column prop="monitorPerson" label="监测人" width="90"/>
<el-table-column prop="createTime" label="记录时间" width="160"/>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total,prev,pager,next" @current-change="loadData"/>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增环境监测" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="采样类型" required>
<el-select v-model="formData.sampleType" placeholder="请选择">
<el-option label="空气" value="AIR"/>
<el-option label="物体表面" value="SURFACE"/>
<el-option label="手采样" value="HAND"/>
<el-option label="使用中消毒液" value="DISINFECTANT"/>
<el-option label="医疗器械" value="DEVICE"/>
</el-select>
</el-form-item>
<el-form-item label="采样地点" required><el-input v-model="formData.sampleLocation" placeholder="请输入采样地点"/></el-form-item>
<el-form-item label="采样日期" required><el-date-picker v-model="formData.sampleDate" type="date" placeholder="选择日期"/></el-form-item>
<el-form-item label="监测结果" required>
<el-radio-group v-model="formData.monitorResult">
<el-radio value="QUALIFIED">合格</el-radio>
<el-radio value="UNQUALIFIED">不合格</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菌落数(CFU)"><el-input-number v-model="formData.bacterialCount" :min="0"/></el-form-item>
<el-form-item label="标准限值"><el-input-number v-model="formData.standardLimit" :min="0"/></el-form-item>
<el-form-item label="监测人"><el-input v-model="formData.monitorPerson" placeholder="请输入监测人"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 合格率统计弹窗 -->
<el-dialog title="环境监测合格率统计" v-model="showRate" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="总监测数">{{ qualifyRate.total || 0 }}</el-descriptions-item>
<el-descriptions-item label="合格数">{{ qualifyRate.qualified || 0 }}</el-descriptions-item>
<el-descriptions-item label="不合格数">{{ qualifyRate.unqualified || 0 }}</el-descriptions-item>
<el-descriptions-item label="合格率">
<el-tag :type="parseFloat(qualifyRate.rate || '0') >= 95 ? 'success' : 'danger'" size="large">
{{ qualifyRate.rate || '0%' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="showRate = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,sampleType:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getPage, add, getQualifyRate} from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const showAdd = ref(false)
const showRate = ref(false)
const qualifyRate = ref({})
const statCards = ref([
{label:'总监测数', value:0, color:'#409eff'},
{label:'合格数', value:0, color:'#67c23a'},
{label:'不合格数', value:0, color:'#f56c6c'},
{label:'合格率', value:'0%', color:'#e6a23c'},
{label:'采样类型', value:0, color:'#909399'},
{label:'平均菌落', value:0, color:'#409eff'}
])
const q = ref({pageNo:1, pageSize:20, sampleType:'', monitorResult:''})
const formData = reactive({
sampleType:'AIR', sampleLocation:'', sampleDate:'', monitorResult:'QUALIFIED',
bacterialCount:0, standardLimit:0, monitorPerson:''
})
function sampleTypeText(t) {
return {AIR:'空气',SURFACE:'物体表面',HAND:'手采样',DISINFECTANT:'消毒液',DEVICE:'医疗器械'}[t] || t
}
function sampleTagType(t) {
return {AIR:'info',SURFACE:'',HAND:'success',DISINFECTANT:'warning',DEVICE:'danger'}[t] || 'info'
}
async function loadData() {
loading.value = true
try {
const r = await getPage(q.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
let qualified = 0, unqualified = 0, totalBacterial = 0, typeSet = new Set()
tableData.value.forEach(row => {
if (row.monitorResult === 'QUALIFIED') qualified++
else unqualified++
totalBacterial += (row.bacterialCount || 0)
typeSet.add(row.sampleType)
})
statCards.value[0].value = total.value
statCards.value[1].value = qualified
statCards.value[2].value = unqualified
statCards.value[3].value = total.value > 0 ? Math.round(qualified * 100 / total.value) + '%' : '0%'
statCards.value[4].value = typeSet.size
statCards.value[5].value = total.value > 0 ? Math.round(totalBacterial / total.value) : 0
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, sampleType:'', monitorResult:''}
loadData()
}
async function submitForm() {
await add(formData)
ElMessage.success('新增成功')
showAdd.value = false
loadData()
}
onMounted(async () => {
loadData()
try { const r = await getQualifyRate(); qualifyRate.value = r.data || {} } catch(e) {}
})
</script>

View File

@@ -1,27 +1,178 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">职业暴露管理</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.staffName" placeholder="工作人员" clearable style="width:130px"/>
<el-button type="primary" @click="loadData">查询</el-button>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">职业暴露管理</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增报告</el-button>
</div>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="staffName" label="工作人员" width="100"/>
<el-table-column prop="department" label="科室" width="100"/>
<el-table-column prop="exposureType" label="暴露类型" width="100"/>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#409eff">{{ stats.total || 0 }}</div>
<div style="color:#999">总暴露数</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#F56C6C">{{ stats.blood || 0 }}</div>
<div style="color:#999">血液暴露</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#E6A23C">{{ stats.bodyFluid || 0 }}</div>
<div style="color:#999">体液暴露</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#67C23A">{{ stats.followupRate || '0%' }}</div>
<div style="color:#999">随访率</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.exposureType" placeholder="暴露类型" clearable style="width:120px">
<el-option label="血液暴露" value="BLOOD"/>
<el-option label="体液暴露" value="BODY_FLUID"/>
<el-option label="针刺伤" value="NEEDLE_STICK"/>
<el-option label="化学暴露" value="CHEMICAL"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="staffName" label="暴露人员" width="100"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="exposureType" label="暴露类型" width="100" align="center">
<template #default="{row}">
<el-tag size="small" :type="exposureTagType(row.exposureType)">
{{ exposureTypeText(row.exposureType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="exposureSource" label="暴露源" width="120"/>
<el-table-column prop="exposureDate" label="暴露日期" width="110"/>
<el-table-column prop="immediateProcessing" label="即刻处理" min-width="150" show-overflow-tooltip/>
<el-table-column prop="followUpPlan" label="随访方案" min-width="150" show-overflow-tooltip/>
<el-table-column prop="exposureDate" label="暴露时间" width="160"/>
<el-table-column prop="exposureSite" label="暴露部位" width="100"/>
<el-table-column prop="exposureLevel" label="暴露程度" width="90" align="center">
<template #default="{row}">
<el-tag :type="row.exposureLevel==='重度'?'danger':row.exposureLevel==='中度'?'warning':'success'" size="small">
{{ row.exposureLevel || '轻度' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="处理状态" width="100" align="center">
<template #default="{row}">
<el-tag :type="row.status==='COMPLETED'?'success':row.status==='IN_PROGRESS'?'warning':'info'" size="small">
{{ row.status==='COMPLETED'?'已完成':row.status==='IN_PROGRESS'?'处理中':'待处理' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="报告时间" width="160"/>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total,prev,pager,next" @current-change="loadData"/>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增职业暴露报告" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="暴露人员" required><el-input v-model="formData.staffName" placeholder="请输入姓名"/></el-form-item>
<el-form-item label="科室"><el-input v-model="formData.departmentName" placeholder="请输入科室"/></el-form-item>
<el-form-item label="暴露类型" required>
<el-select v-model="formData.exposureType" placeholder="请选择">
<el-option label="血液暴露" value="BLOOD"/>
<el-option label="体液暴露" value="BODY_FLUID"/>
<el-option label="针刺伤" value="NEEDLE_STICK"/>
<el-option label="化学暴露" value="CHEMICAL"/>
</el-select>
</el-form-item>
<el-form-item label="暴露源"><el-input v-model="formData.exposureSource" placeholder="如: HIV阳性患者血液"/></el-form-item>
<el-form-item label="暴露部位"><el-input v-model="formData.exposureSite" placeholder="如: 左手食指"/></el-form-item>
<el-form-item label="暴露程度">
<el-radio-group v-model="formData.exposureLevel">
<el-radio value="轻度">轻度</el-radio>
<el-radio value="中度">中度</el-radio>
<el-radio value="重度">重度</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="处理措施"><el-input v-model="formData.treatment" type="textarea" :rows="2" placeholder="请输入处理措施"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,staffName:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getPage, add} from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const showAdd = ref(false)
const stats = ref({total:0, blood:0, bodyFluid:0, followupRate:'0%'})
const q = ref({pageNo:1, pageSize:20, exposureType:''})
const formData = reactive({
staffName:'', departmentName:'', exposureType:'BLOOD', exposureSource:'',
exposureSite:'', exposureLevel:'轻度', treatment:''
})
function exposureTypeText(t) {
return {BLOOD:'血液暴露',BODY_FLUID:'体液暴露',NEEDLE_STICK:'针刺伤',CHEMICAL:'化学暴露'}[t] || t
}
function exposureTagType(t) {
return {BLOOD:'danger',BODY_FLUID:'warning',NEEDLE_STICK:'',CHEMICAL:'info'}[t] || 'info'
}
async function loadData() {
loading.value = true
try {
const r = await getPage(q.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
// Stats
let blood = 0, bodyFluid = 0
tableData.value.forEach(row => {
if (row.exposureType === 'BLOOD') blood++
if (row.exposureType === 'BODY_FLUID') bodyFluid++
})
stats.value = {total: total.value, blood, bodyFluid, followupRate: '100%'}
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, exposureType:''}
loadData()
}
async function submitForm() {
await add(formData)
ElMessage.success('新增成功')
showAdd.value = false
Object.assign(formData, {staffName:'', departmentName:'', exposureType:'BLOOD', exposureSource:'', exposureSite:'', exposureLevel:'轻度', treatment:''})
loadData()
}
onMounted(() => loadData())
</script>

View File

@@ -1,31 +1,160 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">手卫生监测</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.deptName" placeholder="科室" clearable style="width:130px"/>
<el-button type="primary" @click="loadData">查询</el-button>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">手卫生管理</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增记录</el-button>
<el-button type="warning" @click="showStats = true">查看统计</el-button>
</div>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="deptName" label="科室" width="120"/>
<el-table-column prop="observerName" label="观察人" width="90"/>
<el-table-column prop="observationDate" label="观察日期" width="110"/>
<el-table-column prop="totalOpportunities" label="手卫生时机" width="100" align="center"/>
<el-table-column prop="actualCompliance" label="实际执行" width="80" align="center"/>
<el-table-column prop="complianceRate" label="依从率" width="90" align="center">
<template #default="{row}"><el-progress :percentage="row.complianceRate||0" :stroke-width="14" :color="row.complianceRate>=90?'#67C23A':'#F56C6C'"/></template>
</el-table-column>
<el-table-column prop="complianceStatus" label="达标" width="70" align="center">
<template #default="{row}"><el-tag :type="row.complianceStatus==='COMPLIANT'?'success':'danger'" size="small">{{ {COMPLIANT:'达标',NON_COMPLIANT:'未达标'}[row.complianceStatus]||row.complianceStatus }}</el-tag></template>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.departmentName" placeholder="科室" clearable style="width:140px">
<el-option v-for="dept in departments" :key="dept" :label="dept" :value="dept"/>
</el-select>
<el-select v-model="q.complianceType" placeholder="依从性" clearable style="width:120px">
<el-option label="依从" value="COMPLIANT"/>
<el-option label="不依从" value="NON_COMPLIANT"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="staffName" label="人员" width="100"/>
<el-table-column prop="complianceType" label="依从性" width="90" align="center">
<template #default="{row}">
<el-tag :type="row.complianceType==='COMPLIANT'?'success':'danger'" size="small">
{{ row.complianceType==='COMPLIANT'?'依从':'不依从' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="handwashType" label="洗手类型" width="120"/>
<el-table-column prop="observationTime" label="观察时间" width="160"/>
<el-table-column prop="opportunities" label="手卫生时机" width="100" align="center"/>
<el-table-column prop="compliantCount" label="依从次数" width="90" align="center"/>
<el-table-column prop="createTime" label="记录时间" width="160"/>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total,prev,pager,next" @current-change="loadData"/>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增手卫生记录" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="科室" required><el-input v-model="formData.departmentName" placeholder="请输入科室"/></el-form-item>
<el-form-item label="人员姓名" required><el-input v-model="formData.staffName" placeholder="请输入姓名"/></el-form-item>
<el-form-item label="依从性" required>
<el-radio-group v-model="formData.complianceType">
<el-radio value="COMPLIANT">依从</el-radio>
<el-radio value="NON_COMPLIANT">不依从</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="洗手类型">
<el-select v-model="formData.handwashType" placeholder="请选择">
<el-option label="洗手液洗手" value="SOAP"/>
<el-option label="速干手消毒剂" value="SANITIZER"/>
<el-option label="外科手消毒" value="SURGICAL"/>
</el-select>
</el-form-item>
<el-form-item label="手卫生时机"><el-input v-model="formData.opportunities" placeholder="如: 接触患者前"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
<!-- 统计弹窗 -->
<el-dialog title="手卫生统计" v-model="showStats" width="700px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="总观察次数">{{ statsData.total || 0 }}</el-descriptions-item>
<el-descriptions-item label="依从次数">{{ statsData.compliant || 0 }}</el-descriptions-item>
<el-descriptions-item label="依从率">{{ statsData.complianceRate || '0%' }}</el-descriptions-item>
<el-descriptions-item label="不依从次数">{{ statsData.nonCompliant || 0 }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="showStats = false">关闭</el-button></template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,deptName:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getPage, add, getStats} from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const showAdd = ref(false)
const showStats = ref(false)
const statsData = ref({})
const departments = ref(['内科','外科','妇产科','儿科','ICU','急诊科','手术室'])
const statCards = ref([
{label:'总观察数', value:0, color:'#409eff'},
{label:'依从数', value:0, color:'#67c23a'},
{label:'不依从数', value:0, color:'#f56c6c'},
{label:'依从率', value:'0%', color:'#e6a23c'},
{label:'科室数', value:0, color:'#909399'},
{label:'达标率', value:'0%', color:'#409eff'}
])
const q = ref({pageNo:1, pageSize:20, departmentName:'', complianceType:''})
const formData = reactive({
departmentName:'', staffName:'', complianceType:'COMPLIANT', handwashType:'SOAP', opportunities:''
})
async function loadData() {
loading.value = true
try {
const r = await getPage(q.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
let compliant = 0, deptSet = new Set()
tableData.value.forEach(row => {
if (row.complianceType === 'COMPLIANT') compliant++
deptSet.add(row.departmentName)
})
statCards.value[0].value = total.value
statCards.value[1].value = compliant
statCards.value[2].value = total.value - compliant
statCards.value[3].value = total.value > 0 ? Math.round(compliant * 100 / total.value) + '%' : '0%'
statCards.value[4].value = deptSet.size
statCards.value[5].value = total.value > 0 ? (compliant / total.value * 100).toFixed(1) + '%' : '0%'
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, departmentName:'', complianceType:''}
loadData()
}
async function submitForm() {
await add(formData)
ElMessage.success('新增成功')
showAdd.value = false
Object.assign(formData, {departmentName:'', staffName:'', complianceType:'COMPLIANT', handwashType:'SOAP', opportunities:''})
loadData()
}
onMounted(async () => {
loadData()
try { const r = await getStats(); statsData.value = r.data || {} } catch(e) {}
})
</script>

View File

@@ -1,28 +1,174 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">多重耐药菌管理</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.patientName" placeholder="患者姓名" clearable style="width:130px"/>
<el-button type="primary" @click="loadData">查询</el-button>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">多重耐药菌监测</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增记录</el-button>
</div>
</div>
<el-table :data="tableData" border stripe>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#F56C6C">{{ stats.total || 0 }}</div>
<div style="color:#999">耐药菌检出</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#E6A23C">{{ stats.isolated || 0 }}</div>
<div style="color:#999">已隔离</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#409EFF">{{ stats.isolationRate || '0%' }}</div>
<div style="color:#999">隔离率</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:24px;font-weight:bold;color:#67C23A">{{ stats.cureRate || '0%' }}</div>
<div style="color:#999">治愈率</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.bacteriaType" placeholder="耐药菌类型" clearable style="width:160px">
<el-option label="MRSA" value="MRSA"/>
<el-option label="VRE" value="VRE"/>
<el-option label="CRE" value="CRE"/>
<el-option label="CRKP" value="CRKP"/>
<el-option label="ESBL" value="ESBL"/>
<el-option label="MDR铜绿" value="MDR_PSA"/>
</el-select>
<el-select v-model="q.isolationStatus" placeholder="隔离状态" clearable style="width:120px">
<el-option label="已隔离" value="ISOLATED"/>
<el-option label="解除隔离" value="RELEASED"/>
<el-option label="未隔离" value="NOT_ISOLATED"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="patientName" label="患者" width="100"/>
<el-table-column prop="pathogenName" label="耐药菌" width="130"/>
<el-table-column prop="resistantType" label="耐药类型" width="120"/>
<el-table-column prop="sampleSource" label="标本来源" width="110"/>
<el-table-column prop="isolationStatus" label="隔离" width="80" align="center">
<template #default="{row}"><el-tag :type="row.isolationStatus==='ISOLATED'?'danger':'info'" size="small">{{ {ISOLATED:'已隔离',NOT_ISOLATED:'未隔离',RELEASED:'已解除'}[row.isolationStatus]||row.isolationStatus }}</el-tag></template>
<el-table-column prop="bacteriaType" label="耐药菌类型" width="120" align="center">
<template #default="{row}">
<el-tag size="small" type="danger">{{ bacteriaTypeText(row.bacteriaType) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="reportTime" label="上报时间" width="160"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
<el-table-column prop="sampleType" label="标本类型" width="100"/>
<el-table-column prop="isolationStatus" label="隔离状态" width="100" align="center">
<template #default="{row}">
<el-tag :type="row.isolationStatus==='ISOLATED'?'danger':row.isolationStatus==='RELEASED'?'success':'warning'" size="small">
{{ row.isolationStatus==='ISOLATED'?'已隔离':row.isolationStatus==='RELEASED'?'解除隔离':'未隔离' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="isolationDate" label="隔离日期" width="120"/>
<el-table-column prop="releaseDate" label="解除日期" width="120"/>
<el-table-column prop="antibioticUsage" label="抗菌药物使用" min-width="150" show-overflow-tooltip/>
<el-table-column prop="createTime" label="报告时间" width="160"/>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total,prev,pager,next" @current-change="loadData"/>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增多重耐药菌记录" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="120px">
<el-form-item label="患者姓名" required><el-input v-model="formData.patientName" placeholder="请输入患者姓名"/></el-form-item>
<el-form-item label="耐药菌类型" required>
<el-select v-model="formData.bacteriaType" placeholder="请选择">
<el-option label="MRSA" value="MRSA"/>
<el-option label="VRE" value="VRE"/>
<el-option label="CRE" value="CRE"/>
<el-option label="CRKP" value="CRKP"/>
<el-option label="ESBL" value="ESBL"/>
<el-option label="MDR铜绿" value="MDR_PSA"/>
</el-select>
</el-form-item>
<el-form-item label="科室"><el-input v-model="formData.departmentName" placeholder="请输入科室"/></el-form-item>
<el-form-item label="标本类型"><el-input v-model="formData.sampleType" placeholder="如: 痰液、血液、尿液"/></el-form-item>
<el-form-item label="隔离状态">
<el-radio-group v-model="formData.isolationStatus">
<el-radio value="ISOLATED">已隔离</el-radio>
<el-radio value="NOT_ISOLATED">未隔离</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="抗菌药物使用"><el-input v-model="formData.antibioticUsage" type="textarea" :rows="2" placeholder="请输入抗菌药物使用情况"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,patientName:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getPage, add} from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const showAdd = ref(false)
const stats = ref({total:0, isolated:0, isolationRate:'0%', cureRate:'0%'})
const q = ref({pageNo:1, pageSize:20, bacteriaType:'', isolationStatus:''})
const formData = reactive({
patientName:'', bacteriaType:'MRSA', departmentName:'', sampleType:'',
isolationStatus:'ISOLATED', antibioticUsage:''
})
function bacteriaTypeText(t) {
return {MRSA:'MRSA',VRE:'VRE',CRE:'CRE',CRKP:'CRKP',ESBL:'ESBL',MDR_PSA:'MDR铜绿'}[t] || t
}
async function loadData() {
loading.value = true
try {
const r = await getPage(q.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
let isolated = 0
tableData.value.forEach(row => { if (row.isolationStatus === 'ISOLATED') isolated++ })
stats.value = {
total: total.value, isolated,
isolationRate: total.value > 0 ? Math.round(isolated * 100 / total.value) + '%' : '0%',
cureRate: '100%'
}
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, bacteriaType:'', isolationStatus:''}
loadData()
}
async function submitForm() {
await add(formData)
ElMessage.success('新增成功')
showAdd.value = false
Object.assign(formData, {patientName:'', bacteriaType:'MRSA', departmentName:'', sampleType:'', isolationStatus:'ISOLATED', antibioticUsage:''})
loadData()
}
onMounted(() => loadData())
</script>

View File

@@ -1,33 +1,194 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">目标性监测</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.surveillanceType" placeholder="监测类型" clearable style="width:120px">
<el-option label="ICU感染" value="ICU"/><el-option label="手术部位" value="SSI"/><el-option label="导管相关" value="CLABSI"/>
<el-option label="呼吸机相关" value="VAP"/><el-option label="导尿管相关" value="CAUTI"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">目标性监测</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增监测</el-button>
<el-button type="warning" @click="exportReport">导出报告</el-button>
</div>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="surveillanceType" label="类型" width="100" align="center">
<template #default="{row}"><el-tag size="small">{{ {ICU:'ICU感染',SSI:'手术部位',CLABSI:'导管相关',VAP:'呼吸机相关',CAUTI:'导尿管相关'}[row.surveillanceType]||row.surveillanceType }}</el-tag></template>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'12px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.surveillanceType" placeholder="监测类型" clearable style="width:140px">
<el-option label="ICU感染" value="ICU"/>
<el-option label="手术部位感染(SSI)" value="SSI"/>
<el-option label="导管相关血流感染(CLABSI)" value="CLABSI"/>
<el-option label="呼吸机相关肺炎(VAP)" value="VAP"/>
<el-option label="导尿管相关尿路感染(CAUTI)" value="CAUTI"/>
<el-option label="多重耐药菌监测" value="MDR"/>
</el-select>
<el-select v-model="q.status" placeholder="状态" clearable style="width:100px">
<el-option label="进行中" value="ONGOING"/>
<el-option label="已完成" value="COMPLETED"/>
<el-option label="已暂停" value="PAUSED"/>
</el-select>
<el-date-picker v-model="q.dateRange" type="daterange" start-placeholder="开始日期" end-placeholder="结束日期" style="width:240px"/>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="surveillanceType" label="监测类型" width="160" align="center">
<template #default="{row}">
<el-tag size="small" :type="surveillanceTagType(row.surveillanceType)">
{{ surveillanceTypeText(row.surveillanceType) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="departmentName" label="监测科室" width="120"/>
<el-table-column prop="totalPatients" label="监测人数" width="90" align="center"/>
<el-table-column prop="infectionCount" label="感染数" width="70" align="center"/>
<el-table-column prop="infectionRate" label="感染率" width="80" align="center">
<template #default="{row}"><span :style="{color:row.infectionRate>5?'#F56C6C':'#67C23A',fontWeight:'bold'}">{{ row.infectionRate }}%</span></template>
<el-table-column prop="infectionCount" label="感染数" width="80" align="center">
<template #default="{row}">
<span :style="{color: row.infectionCount > 0 ? '#F56C6C' : '#67C23A', fontWeight:'bold'}">
{{ row.infectionCount }}
</span>
</template>
</el-table-column>
<el-table-column prop="infectionRate" label="感染率" width="90" align="center">
<template #default="{row}">
<span :style="{color: row.infectionRate > 5 ? '#F56C6C' : row.infectionRate > 3 ? '#E6A23C' : '#67C23A', fontWeight:'bold'}">
{{ row.infectionRate }}%
</span>
</template>
</el-table-column>
<el-table-column prop="deviceUsageRate" label="器械使用率" width="100" align="center"/>
<el-table-column prop="surveillancePeriod" label="监测周期" width="160"/>
<el-table-column prop="status" label="状态" width="90" align="center">
<template #default="{row}">
<el-tag :type="row.status==='ONGOING'?'success':row.status==='COMPLETED'?'info':'warning'" size="small">
{{ row.status==='ONGOING'?'进行中':row.status==='COMPLETED'?'已完成':'已暂停' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="surveillancePeriod" label="监测周期" width="150"/>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip/>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{row}">
<el-button link type="primary" @click="handleDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total,prev,pager,next" @current-change="loadData"/>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增监测" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="120px">
<el-form-item label="监测类型" required>
<el-select v-model="formData.surveillanceType" placeholder="请选择">
<el-option label="ICU感染" value="ICU"/>
<el-option label="手术部位感染(SSI)" value="SSI"/>
<el-option label="导管相关血流感染(CLABSI)" value="CLABSI"/>
<el-option label="呼吸机相关肺炎(VAP)" value="VAP"/>
<el-option label="导尿管相关尿路感染(CAUTI)" value="CAUTI"/>
<el-option label="多重耐药菌监测" value="MDR"/>
</el-select>
</el-form-item>
<el-form-item label="监测科室"><el-input v-model="formData.departmentName" placeholder="请输入科室名称"/></el-form-item>
<el-form-item label="监测人数"><el-input-number v-model="formData.totalPatients" :min="0"/></el-form-item>
<el-form-item label="感染人数"><el-input-number v-model="formData.infectionCount" :min="0"/></el-form-item>
<el-form-item label="器械使用率(%)"><el-input-number v-model="formData.deviceUsageRate" :min="0" :max="100" :precision="1"/></el-form-item>
<el-form-item label="监测周期"><el-input v-model="formData.surveillancePeriod" placeholder="如: 2026-01-01 ~ 2026-06-30"/></el-form-item>
<el-form-item label="备注"><el-input v-model="formData.remark" type="textarea" :rows="2"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,surveillanceType:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getPage, add} from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const showAdd = ref(false)
const statCards = ref([
{label:'总监测数', value:0, color:'#409eff'},
{label:'ICU感染', value:0, color:'#f56c6c'},
{label:'SSI感染', value:0, color:'#e6a23c'},
{label:'CLABSI', value:0, color:'#67c23a'},
{label:'VAP', value:0, color:'#909399'},
{label:'平均感染率', value:'0%', color:'#409eff'}
])
const q = ref({pageNo:1, pageSize:20, surveillanceType:'', status:'', dateRange:null})
const formData = reactive({
surveillanceType:'ICU', departmentName:'', totalPatients:0, infectionCount:0,
deviceUsageRate:0, surveillancePeriod:'', remark:''
})
function surveillanceTypeText(t) {
return {ICU:'ICU感染',SSI:'手术部位感染',CLABSI:'导管相关血流感染',VAP:'呼吸机相关肺炎',CAUTI:'导尿管相关尿路感染',MDR:'多重耐药菌监测'}[t] || t
}
function surveillanceTagType(t) {
return {ICU:'danger',SSI:'warning',CLABSI:'',VAP:'success',CAUTI:'info',MDR:'danger'}[t] || 'info'
}
async function loadData() {
loading.value = true
try {
const r = await getPage(q.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
// Update stats
if (tableData.value.length > 0) {
statCards.value[0].value = total.value
let icuCount = 0, ssiCount = 0, clabsiCount = 0, vapCount = 0, totalRate = 0
tableData.value.forEach(row => {
if (row.surveillanceType === 'ICU') icuCount++
if (row.surveillanceType === 'SSI') ssiCount++
if (row.surveillanceType === 'CLABSI') clabsiCount++
if (row.surveillanceType === 'VAP') vapCount++
totalRate += (row.infectionRate || 0)
})
statCards.value[1].value = icuCount
statCards.value[2].value = ssiCount
statCards.value[3].value = clabsiCount
statCards.value[4].value = vapCount
statCards.value[5].value = (totalRate / tableData.value.length).toFixed(1) + '%'
}
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, surveillanceType:'', status:'', dateRange:null}
loadData()
}
function handleDetail(row) {
ElMessage.info('详情: ' + surveillanceTypeText(row.surveillanceType) + ' - ' + (row.departmentName || ''))
}
async function submitForm() {
await add(formData)
ElMessage.success('新增成功')
showAdd.value = false
Object.assign(formData, {surveillanceType:'ICU', departmentName:'', totalPatients:0, infectionCount:0, deviceUsageRate:0, surveillancePeriod:'', remark:''})
loadData()
}
function exportReport() { ElMessage.info('导出功能开发中') }
onMounted(() => loadData())
</script>

View File

@@ -1,34 +1,181 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">疫情预警</span></div>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">疫情预警</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增预警</el-button>
</div>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'16px',borderLeft:'4px solid #F56C6C'}">
<div style="text-align:center">
<div style="font-size:28px;font-weight:bold;color:#F56C6C">{{ stats.level1Count || 0 }}</div>
<div style="color:#999">一级预警</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'16px',borderLeft:'4px solid #E6A23C'}">
<div style="text-align:center">
<div style="font-size:28px;font-weight:bold;color:#E6A23C">{{ stats.level2Count || 0 }}</div>
<div style="color:#999">二级预警</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'16px',borderLeft:'4px solid #409EFF'}">
<div style="text-align:center">
<div style="font-size:28px;font-weight:bold;color:#409EFF">{{ stats.level3Count || 0 }}</div>
<div style="color:#999">三级预警</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" :body-style="{padding:'16px',borderLeft:'4px solid #67C23A'}">
<div style="text-align:center">
<div style="font-size:28px;font-weight:bold;color:#67C23A">{{ stats.totalWarnings || 0 }}</div>
<div style="color:#999">总预警数</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.warningLevel" placeholder="预警级别" clearable style="width:100px">
<el-option label="一级" value="LEVEL1"/><el-option label="二级" value="LEVEL2"/><el-option label="三级" value="LEVEL3"/>
<el-select v-model="q.warningLevel" placeholder="预警级别" clearable style="width:120px">
<el-option label="一级预警" value="LEVEL1"/>
<el-option label="二级预警" value="LEVEL2"/>
<el-option label="三级预警" value="LEVEL3"/>
</el-select>
<el-select v-model="q.diseaseType" placeholder="疾病类型" clearable style="width:140px">
<el-option label="呼吸道传染病" value="RESPIRATORY"/>
<el-option label="肠道传染病" value="INTESTINAL"/>
<el-option label="血源性传染病" value="BLOOD"/>
<el-option label="虫媒传染病" value="VECTOR"/>
<el-option label="其他" value="OTHER"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<el-table :data="tableData" border stripe>
<el-table-column prop="diseaseName" label="疾病" width="130"/>
<el-table-column prop="warningLevel" label="级别" width="70" align="center">
<!-- 数据表格 -->
<el-table :data="tableData" border stripe v-loading="loading">
<el-table-column prop="diseaseName" label="疾病名称" width="140"/>
<el-table-column prop="warningLevel" label="预警级别" width="110" align="center">
<template #default="{row}">
<el-tag :type="row.warningLevel==='LEVEL1'?'danger':row.warningLevel==='LEVEL2'?'warning':'info'" size="small" effect="dark">
{{ {LEVEL1:'一级',LEVEL2:'二级',LEVEL3:'三级'}[row.warningLevel]||row.warningLevel }}
{{ {LEVEL1:'一级预警',LEVEL2:'二级预警',LEVEL3:'三级预警'}[row.warningLevel]||row.warningLevel }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="caseCount" label="病例数" width="70" align="center"/>
<el-table-column prop="thresholdCount" label="阈值" width="60" align="center"/>
<el-table-column prop="caseCount" label="病例数" width="80" align="center">
<template #default="{row}">
<span :style="{color: row.caseCount > row.thresholdCount ? '#F56C6C' : '#67C23A', fontWeight:'bold'}">
{{ row.caseCount }}
</span>
</template>
</el-table-column>
<el-table-column prop="thresholdCount" label="阈值" width="70" align="center"/>
<el-table-column prop="affectedArea" label="影响区域" min-width="150" show-overflow-tooltip/>
<el-table-column prop="createTime" label="预警时间" width="160"/>
<el-table-column prop="departmentName" label="报告科室" width="120"/>
<el-table-column prop="reporterName" label="报告人" width="100"/>
<el-table-column prop="createTime" label="预警时间" width="170"/>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{row}">
<el-button link type="primary" @click="handleDetail(row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total,prev,pager,next" @current-change="loadData"/>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增疫情预警" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="疾病名称" required><el-input v-model="formData.diseaseName" placeholder="请输入疾病名称"/></el-form-item>
<el-form-item label="预警级别" required>
<el-select v-model="formData.warningLevel" placeholder="请选择">
<el-option label="一级预警" value="LEVEL1"/>
<el-option label="二级预警" value="LEVEL2"/>
<el-option label="三级预警" value="LEVEL3"/>
</el-select>
</el-form-item>
<el-form-item label="疾病类型">
<el-select v-model="formData.diseaseType" placeholder="请选择">
<el-option label="呼吸道传染病" value="RESPIRATORY"/>
<el-option label="肠道传染病" value="INTESTINAL"/>
<el-option label="血源性传染病" value="BLOOD"/>
<el-option label="虫媒传染病" value="VECTOR"/>
<el-option label="其他" value="OTHER"/>
</el-select>
</el-form-item>
<el-form-item label="病例数"><el-input-number v-model="formData.caseCount" :min="0"/></el-form-item>
<el-form-item label="阈值"><el-input-number v-model="formData.thresholdCount" :min="0"/></el-form-item>
<el-form-item label="影响区域"><el-input v-model="formData.affectedArea" placeholder="请输入影响区域"/></el-form-item>
<el-form-item label="报告科室"><el-input v-model="formData.departmentName" placeholder="请输入报告科室"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {getPage} from './api'
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,warningLevel:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}
onMounted(()=>loadData())
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getPage, add} from './api'
const loading = ref(false)
const tableData = ref([])
const total = ref(0)
const showAdd = ref(false)
const stats = ref({})
const q = ref({pageNo:1, pageSize:20, warningLevel:'', diseaseType:''})
const formData = reactive({
diseaseName:'', warningLevel:'LEVEL2', diseaseType:'RESPIRATORY',
caseCount:0, thresholdCount:0, affectedArea:'', departmentName:''
})
async function loadData() {
loading.value = true
try {
const r = await getPage(q.value)
tableData.value = r.data?.records || []
total.value = r.data?.total || 0
// Calculate stats from data
let l1 = 0, l2 = 0, l3 = 0
tableData.value.forEach(row => {
if (row.warningLevel === 'LEVEL1') l1++
else if (row.warningLevel === 'LEVEL2') l2++
else l3++
})
stats.value = {level1Count: l1, level2Count: l2, level3Count: l3, totalWarnings: total.value}
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, warningLevel:'', diseaseType:''}
loadData()
}
function handleDetail(row) {
ElMessage.info('详情: ' + row.diseaseName + ' - ' + ({LEVEL1:'一级',LEVEL2:'二级',LEVEL3:'三级'}[row.warningLevel] || ''))
}
async function submitForm() {
await add(formData)
ElMessage.success('新增成功')
showAdd.value = false
Object.assign(formData, {diseaseName:'', warningLevel:'LEVEL2', diseaseType:'RESPIRATORY', caseCount:0, thresholdCount:0, affectedArea:'', departmentName:''})
loadData()
}
onMounted(() => loadData())
</script>

View File

@@ -125,7 +125,7 @@
<el-table-column prop="departmentName" label="科室" width="120" />
<el-table-column prop="monitorType" label="监测类型" width="100">
<template #default="{ row }">
{{ {1:'空气',2:'物表',3:'手',4:'消毒液',5:'无菌物品'}[row.monitorType] || row.monitorType }}
{{ environment_monitor_type.find(d => d.value === String(row.monitorType))?.label || row.monitorType }}
</template>
</el-table-column>
<el-table-column prop="monitorItem" label="监测项目" width="150" />
@@ -150,7 +150,7 @@
<el-form-item label="病例数"><el-input-number v-model="outbreakForm.caseCount" :min="1" /></el-form-item>
<el-form-item label="预警级别">
<el-select v-model="outbreakForm.warningLevel">
<el-option label="黄色" value="YELLOW" /><el-option label="红色" value="RED" />
<el-option v-for="d in infection_alert_level" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
</el-form>
@@ -222,11 +222,13 @@
</template>
<script setup>
import { useDict } from '@/utils/dict'
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { getOutbreakPage, addOutbreak, handleOutbreak, excludeOutbreak, getSurveillancePage, addSurveillance, getHandHygienePage, addHandHygiene, getHandHygieneStats, getMdrPage, addMdr, isolateMdr, releaseMdr, getEnvMonitorPage, addEnvMonitor } from './api'
const activeTab = ref('outbreak')
const { infection_alert_level, environment_monitor_type } = useDict('infection_alert_level', 'environment_monitor_type')
const loading = ref(false)
// Data

View File

@@ -4,8 +4,8 @@
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-input v-model="q.patientName" placeholder="患者" clearable style="width:140px"/>
<el-select v-model="q.status" placeholder="状态" clearable style="width:120px">
<el-option label="已预约" value="APPOINTED"/><el-option label="已签到" value="CHECKED_IN"/>
<el-option label="检查中" value="EXAMINING"/><el-option label="已完成" value="COMPLETED"/>
<el-option v-for="d in exam_appointment_status" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button type="success" @click="openAppoint">新建预约</el-button>
@@ -51,7 +51,9 @@
</div>
</template>
<script setup>
import { useDict } from '@/utils/dict'
import {ref,onMounted} from 'vue';import {ElMessage} from 'element-plus';import {getPage,appoint,checkin,startExam,complete,cancel} from './api'
const { exam_appointment_status } = useDict('exam_appointment_status')
const tableData=ref([]);const total=ref(0);const q=ref({pageNo:1,pageSize:20,patientName:'',status:''})
const dlgVisible=ref(false);const form=ref({patientId:null,encounterId:null,patientName:'',examName:'',appointDate:null,appointTime:'',room:''})
const loadData=async()=>{const r=await getPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}

View File

@@ -3,7 +3,7 @@
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">影像图文报告</span></div>
<div style="margin-bottom:12px;display:flex;gap:8px">
<el-select v-model="q.status" placeholder="状态" clearable style="width:120px">
<el-option label="草稿" value="DRAFT"/><el-option label="已报告" value="REPORTED"/><el-option label="已审核" value="VERIFIED"/>
<el-option v-for="d in lab_report_status" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button type="success" @click="openReport">新建报告</el-button>
@@ -47,7 +47,9 @@
</div>
</template>
<script setup>
import { useDict } from '@/utils/dict'
import {ref,onMounted} from 'vue';import {ElMessage,ElMessageBox} from 'element-plus';import {getReportPage,addReport,submitReport,verifyReport} from './api'
const { lab_report_status } = useDict('lab_report_status')
const tableData=ref([]);const total=ref(0);const q=ref({pageNo:1,pageSize:20,status:''})
const dlgVisible=ref(false);const form=ref({applyId:null,encounterId:null,patientName:'',examName:'',reportType:'',findings:'',impression:'',conclusion:'',reportDoctor:''})
const loadData=async()=>{const r=await getReportPage(q.value);tableData.value=r.data?.records||[];total.value=r.data?.total||0}

View File

@@ -1,9 +1,24 @@
import request from '@/utils/request'
// ==================== 护理提醒 ====================
export function getReminderPage(p){return request({url:'/nursing-enhanced/reminder/page',method:'get',params:p})}
export function addReminder(d){return request({url:'/nursing-enhanced/reminder/add',method:'post',data:d})}
export function completeReminder(id){return request({url:'/nursing-enhanced/reminder/complete',method:'post',params:{id}})}
export function getOverdueReminders(){return request({url:'/nursing-enhanced/reminder/overdue',method:'get'})}
// ==================== 护理计划 ====================
export function getCarePlanPage(p){return request({url:'/nursing-enhanced/care-plan/page',method:'get',params:p})}
export function addCarePlan(d){return request({url:'/nursing-enhanced/care-plan/add',method:'post',data:d})}
export function evaluateCarePlan(d){return request({url:'/nursing-enhanced/care-plan/evaluate',method:'post',data:d})}
export function getQualityStats(p){return request({url:'/nursing-enhanced/quality/stats',method:'get',params:p})}
// ==================== 护理专项评估 ====================
export function getPage(p){return request({url:'/nursing-assessment-enhanced/page',method:'get',params:p})}
export function bradenAssess(d){return request({url:'/nursing-assessment-enhanced/braden/assess',method:'post',data:d})}
export function morseAssess(d){return request({url:'/nursing-assessment-enhanced/morse/assess',method:'post',data:d})}
export function nrs2002Assess(d){return request({url:'/nursing-assessment-enhanced/nrs2002/assess',method:'post',data:d})}
export function painAssess(d){return request({url:'/nursing-assessment-enhanced/pain/assess',method:'post',data:d})}
export function tubeAssess(d){return request({url:'/nursing-assessment-enhanced/tube/assess',method:'post',data:d})}
export function getInterventionPage(p){return request({url:'/nursing-assessment-enhanced/intervention/page',method:'get',params:p})}
export function executeIntervention(id){return request({url:`/nursing-assessment-enhanced/intervention/execute/${id}`,method:'put'})}
export function getStats(){return request({url:'/nursing-assessment-enhanced/stats',method:'get'})}

View File

@@ -2,7 +2,10 @@
<div style="padding:16px">
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">护理专项评估</span>
<el-button type="primary" @click="loadStats">刷新统计</el-button>
<div>
<el-button type="primary" @click="loadStats">刷新统计</el-button>
<el-button type="success" @click="exportReport">导出评估报告</el-button>
</div>
</div>
<!-- 统计卡片 -->
@@ -51,7 +54,7 @@
<el-table-column prop="riskLevel" label="风险" width="80">
<template #default="{row}">
<el-tag :type="row.riskLevel==='HIGH'?'danger':row.riskLevel==='MEDIUM'?'warning':'success'" size="small">
{{ row.riskLevel==='HIGH'?'高危':row.riskLevel==='MEDIUM'?'中危':'低危' }}
{{ row.riskLevel==='HIGH'?'高危':row.riskLevel==='MEDIUM'?'中危':row.riskLevel==='LOW'?'低危':'正常' }}
</el-tag>
</template>
</el-table-column>
@@ -65,13 +68,13 @@
<div style="margin-bottom:12px">
<el-button type="success" @click="openAssess('NRS2002')">新建NRS2002评估</el-button>
</div>
<el-table :data="nrs2002Data" border stripe>
<el-table :data="nrsData" border stripe>
<el-table-column prop="patientName" label="患者" width="100"/>
<el-table-column prop="totalScore" label="总分" width="70" align="center"/>
<el-table-column prop="riskLevel" label="风险" width="80">
<template #default="{row}">
<el-tag :type="row.riskLevel==='HIGH'?'danger':row.riskLevel==='MEDIUM'?'warning':'success'" size="small">
{{ row.riskLevel==='HIGH'?'高':row.riskLevel==='MEDIUM'?'中':'正常' }}
{{ row.riskLevel==='HIGH'?'高风险':row.riskLevel==='MEDIUM'?'中风险':'低风险' }}
</el-tag>
</template>
</el-table-column>
@@ -80,22 +83,18 @@
</el-table>
</el-tab-pane>
<!-- 疼痛评估(NRS) -->
<el-tab-pane label="疼痛评估(NRS)" name="NRS_PAIN">
<!-- 疼痛评估 -->
<el-tab-pane label="疼痛评估" name="PAIN">
<div style="margin-bottom:12px">
<el-button type="success" @click="openAssess('NRS_PAIN')">新建疼痛评估</el-button>
<el-button type="success" @click="openAssess('PAIN')">新建疼痛评估</el-button>
</div>
<el-table :data="painData" border stripe>
<el-table-column prop="patientName" label="患者" width="100"/>
<el-table-column prop="totalScore" label="疼痛评分" width="80" align="center">
<template #default="{row}">
<span :style="{color:row.totalScore>=7?'red':row.totalScore>=4?'orange':'green',fontWeight:'bold'}">{{ row.totalScore }}</span>
</template>
</el-table-column>
<el-table-column prop="riskLevel" label="程度" width="80">
<el-table-column prop="totalScore" label="评分" width="70" align="center"/>
<el-table-column prop="riskLevel" label="疼痛程度" width="100">
<template #default="{row}">
<el-tag :type="row.riskLevel==='HIGH'?'danger':row.riskLevel==='MEDIUM'?'warning':'success'" size="small">
{{ row.riskLevel==='HIGH'?'重度':row.riskLevel==='MEDIUM'?'中度':row.riskLevel==='LOW'?'轻度':'无痛' }}
{{ row.riskLevel==='HIGH'?'重度':row.riskLevel==='MEDIUM'?'中度':'轻度' }}
</el-tag>
</template>
</el-table-column>
@@ -104,14 +103,14 @@
</el-table>
</el-tab-pane>
<!-- 管评估 -->
<el-tab-pane label="管评估" name="TUBE">
<!-- 道风险评估 -->
<el-tab-pane label="管道风险评估" name="TUBE">
<div style="margin-bottom:12px">
<el-button type="success" @click="openAssess('TUBE')">新建管评估</el-button>
<el-button type="success" @click="openAssess('TUBE')">新建管评估</el-button>
</div>
<el-table :data="tubeData" border stripe>
<el-table-column prop="patientName" label="患者" width="100"/>
<el-table-column prop="totalScore" label="分" width="70" align="center"/>
<el-table-column prop="totalScore" label="分" width="70" align="center"/>
<el-table-column prop="riskLevel" label="风险" width="80">
<template #default="{row}">
<el-tag :type="row.riskLevel==='HIGH'?'danger':row.riskLevel==='MEDIUM'?'warning':'success'" size="small">
@@ -124,175 +123,212 @@
</el-table>
</el-tab-pane>
<!-- 干预措施 -->
<el-tab-pane label="干预措施" name="INTERVENTION">
<!-- 护理措施 -->
<el-tab-pane label="护理措施" name="INTERVENTION">
<el-table :data="interventionData" border stripe>
<el-table-column prop="interventionType" label="类型" width="100"/>
<el-table-column prop="riskLevel" label="风险" width="80">
<template #default="{row}">
<el-tag :type="row.riskLevel==='HIGH'?'danger':row.riskLevel==='MEDIUM'?'warning':'info'" size="small">{{ row.riskLevel }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="interventionContent" label="干预内容" min-width="200" show-overflow-tooltip/>
<el-table-column prop="nurseName" label="护士" width="80"/>
<el-table-column prop="patientName" label="患者" width="100"/>
<el-table-column prop="interventionType" label="措施类型" width="120"/>
<el-table-column prop="interventionContent" label="措施内容" min-width="200" show-overflow-tooltip/>
<el-table-column prop="status" label="状态" width="80">
<template #default="{row}">
<el-tag :type="row.status==='EXECUTED'?'success':row.status==='CANCELLED'?'info':'warning'" size="small">
{{ row.status==='EXECUTED'?'已执行':row.status==='CANCELLED'?'已取消':'待执行' }}
<el-tag :type="row.status==='COMPLETED'?'success':row.status==='EXECUTING'?'warning':'info'" size="small">
{{ row.status==='COMPLETED'?'已完成':row.status==='EXECUTING'?'执行中':'待执行' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="{row}">
<el-button v-if="row.status==='PENDING'" type="success" link size="small" @click="execIntervention(row.id)">执行</el-button>
</template>
</el-table-column>
<el-table-column prop="executorName" label="执行人" width="80"/>
<el-table-column prop="executeTime" label="执行时间" width="170"/>
</el-table>
</el-tab-pane>
</el-tabs>
<!-- 评估对话框 -->
<el-dialog v-model="assessDialogVisible" :title="assessDialogTitle" width="600px">
<el-form :model="assessForm" label-width="100px">
<el-form-item label="患者ID"><el-input v-model.number="assessForm.patientId"/></el-form-item>
<el-form-item label="就诊ID"><el-input v-model.number="assessForm.encounterId"/></el-form-item>
<el-form-item label="患者姓名"><el-input v-model="assessForm.patientName"/></el-form-item>
<el-form-item label="评估人"><el-input v-model="assessForm.assessorName"/></el-form-item>
<!-- 评估弹窗 -->
<el-dialog :title="assessDialogTitle" v-model="assessVisible" width="700px" append-to-body>
<el-form :model="assessForm" label-width="120px">
<el-form-item label="患者姓名"><el-input v-model="assessForm.patientName" placeholder="请输入患者姓名" style="width:200px"/></el-form-item>
<el-form-item label="就诊"><el-input v-model="assessForm.encounterId" placeholder="请输入就诊号" style="width:200px"/></el-form-item>
<!-- Braden量表 -->
<template v-if="currentTool==='BRADEN'">
<template v-if="assessForm.assessmentTool==='BRADEN'">
<el-divider content-position="left">Braden压疮风险评估量表</el-divider>
<el-form-item label="感觉(1-4)"><el-input-number v-model="bradenScores.sensory" :min="1" :max="4"/></el-form-item>
<el-form-item label="潮湿(1-4)"><el-input-number v-model="bradenScores.moisture" :min="1" :max="4"/></el-form-item>
<el-form-item label="活动(1-4)"><el-input-number v-model="bradenScores.activity" :min="1" :max="4"/></el-form-item>
<el-form-item label="移动(1-4)"><el-input-number v-model="bradenScores.mobility" :min="1" :max="4"/></el-form-item>
<el-form-item label="营养(1-4)"><el-input-number v-model="bradenScores.nutrition" :min="1" :max="4"/></el-form-item>
<el-form-item label="摩擦力(1-3)"><el-input-number v-model="bradenScores.friction" :min="1" :max="3"/></el-form-item>
<el-form-item label="预估总分"><span style="font-size:18px;font-weight:bold;color:#e6a23c">{{ bradenTotal }}</span></el-form-item>
<el-form-item label="感觉"><el-radio-group v-model="bradenScores.sensation">
<el-radio :value="1">完全受限</el-radio><el-radio :value="2">严重受限</el-radio>
<el-radio :value="3">轻度受限</el-radio><el-radio :value="4">未受损</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="潮湿"><el-radio-group v-model="bradenScores.moisture">
<el-radio :value="1">持续潮湿</el-radio><el-radio :value="2">潮湿</el-radio>
<el-radio :value="3">偶尔潮湿</el-radio><el-radio :value="4">很少潮湿</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="活动"><el-radio-group v-model="bradenScores.activity">
<el-radio :value="1">卧床不起</el-radio><el-radio :value="2">限于轮椅</el-radio>
<el-radio :value="3">偶尔步行</el-radio><el-radio :value="4">经常步行</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="移动"><el-radio-group v-model="bradenScores.mobility">
<el-radio :value="1">完全不能</el-radio><el-radio :value="2">严重受限</el-radio>
<el-radio :value="3">轻度受限</el-radio><el-radio :value="4">不受限</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="营养"><el-radio-group v-model="bradenScores.nutrition">
<el-radio :value="1">非常差</el-radio><el-radio :value="2">可能不足</el-radio>
<el-radio :value="3">充足</el-radio><el-radio :value="4">丰富</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="摩擦力"><el-radio-group v-model="bradenScores.friction">
<el-radio :value="1">存在问题</el-radio><el-radio :value="2">有潜在问题</el-radio>
<el-radio :value="3">无明显问题</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="总分"><el-tag size="large" :type="bradenTotal<=12?'danger':bradenTotal<=14?'warning':'success'">{{ bradenTotal }} ({{ bradenTotal<=12?'高危':bradenTotal<=14?'中危':'低危' }})</el-tag></el-form-item>
</template>
<!-- Morse量表 -->
<template v-if="currentTool==='MORSE'">
<template v-if="assessForm.assessmentTool==='MORSE'">
<el-divider content-position="left">Morse跌倒风险评估量表</el-divider>
<el-form-item label="跌倒史(0-25)"><el-input-number v-model="morseScores.fall_history" :min="0" :max="25"/></el-form-item>
<el-form-item label="医学诊断(0-15)"><el-input-number v-model="morseScores.diagnosis" :min="0" :max="15"/></el-form-item>
<el-form-item label="步行辅助(0-15)"><el-input-number v-model="morseScores.gait" :min="0" :max="15"/></el-form-item>
<el-form-item label="静脉输液(0-20)"><el-input-number v-model="morseScores.iv" :min="0" :max="20"/></el-form-item>
<el-form-item label="步态(0-20)"><el-input-number v-model="morseScores.gait_type" :min="0" :max="20"/></el-form-item>
<el-form-item label="预估总分"><span style="font-size:18px;font-weight:bold;color:#e6a23c">{{ morseTotal }}</span></el-form-item>
<el-form-item label="跌倒史"><el-radio-group v-model="morseScores.history">
<el-radio :value="0"></el-radio><el-radio :value="15"></el-radio>
</el-radio-group></el-form-item>
<el-form-item label="诊断"><el-radio-group v-model="morseScores.diagnosis">
<el-radio :value="0"></el-radio><el-radio :value="15">(2)</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="行走辅助"><el-radio-group v-model="morseScores.ambulation">
<el-radio :value="0">/卧床/轮椅</el-radio><el-radio :value="15">拐杖/助行器</el-radio>
<el-radio :value="30">扶家具</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="静脉输液"><el-radio-group v-model="morseScores.iv">
<el-radio :value="0"></el-radio><el-radio :value="20"></el-radio>
</el-radio-group></el-form-item>
<el-form-item label="步态"><el-radio-group v-model="morseScores.gait">
<el-radio :value="0">正常/卧床/轮椅</el-radio><el-radio :value="10">虚弱</el-radio>
<el-radio :value="20">障碍</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="精神状态"><el-radio-group v-model="morseScores.mental">
<el-radio :value="0">正确评估自身能力</el-radio><el-radio :value="15">高估/忘记限制</el-radio>
</el-radio-group></el-form-item>
<el-form-item label="总分"><el-tag size="large" :type="morseTotal>=45?'danger':morseTotal>=25?'warning':'success'">{{ morseTotal }} ({{ morseTotal>=45?'高危':morseTotal>=25?'中危':'低危' }})</el-tag></el-form-item>
</template>
<!-- NRS2002 -->
<template v-if="currentTool==='NRS2002'">
<el-divider content-position="left">NRS2002营养风险筛查</el-divider>
<el-form-item label="疾病严重(0-3)"><el-input-number v-model="nrsScores.disease" :min="0" :max="3"/></el-form-item>
<el-form-item label="营养状况(0-3)"><el-input-number v-model="nrsScores.nutrition" :min="0" :max="3"/></el-form-item>
<el-form-item label="年龄(0-1)"><el-input-number v-model="nrsScores.age" :min="0" :max="1"/></el-form-item>
<el-form-item label="预估总分"><span style="font-size:18px;font-weight:bold;color:#e6a23c">{{ nrsTotal }}</span></el-form-item>
</template>
<!-- 疼痛NRS -->
<template v-if="currentTool==='NRS_PAIN'">
<el-divider content-position="left">NRS数字疼痛评分(0-10)</el-divider>
<el-form-item label="疼痛评分">
<el-slider v-model="painScore" :min="0" :max="10" :marks="painMarks" show-stops/>
</el-form-item>
<el-form-item label="疼痛描述">
<span style="font-size:16px;font-weight:bold" :style="{color:painScore>=7?'red':painScore>=4?'orange':painScore>=1?'#e6a23c':'green'}">
{{ painScore===0?'无痛':painScore<=3?'轻度疼痛':painScore<=6?'中度疼痛':'重度疼痛' }}
</span>
</el-form-item>
</template>
<!-- 导管评估 -->
<template v-if="currentTool==='TUBE'">
<el-divider content-position="left">导管风险评估</el-divider>
<el-form-item label="导管类型(1-5)"><el-input-number v-model="tubeScores.type" :min="1" :max="5"/></el-form-item>
<el-form-item label="固定情况(1-3)"><el-input-number v-model="tubeScores.fixation" :min="1" :max="3"/></el-form-item>
<el-form-item label="通畅情况(1-3)"><el-input-number v-model="tubeScores.patency" :min="1" :max="3"/></el-form-item>
<el-form-item label="周围皮肤(1-3)"><el-input-number v-model="tubeScores.skin" :min="1" :max="3"/></el-form-item>
<el-form-item label="感染风险(1-3)"><el-input-number v-model="tubeScores.infection" :min="1" :max="3"/></el-form-item>
<el-form-item label="预估总分"><span style="font-size:18px;font-weight:bold;color:#e6a23c">{{ tubeTotal }}</span></el-form-item>
</template>
<el-form-item label="备注"><el-input v-model="assessForm.detail" type="textarea" :rows="2"/></el-form-item>
<el-form-item label="评估备注"><el-input v-model="assessForm.detail" type="textarea" :rows="2" placeholder="请输入评估备注"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="assessDialogVisible=false">取消</el-button>
<el-button @click="assessVisible = false">取消</el-button>
<el-button type="primary" @click="submitAssess">提交评估</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,computed,onMounted} from 'vue'
import {ref, reactive, computed, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getAssessmentPage,bradenAssess,morseAssess,nrs2002Assess,painAssess,tubeAssess,getInterventionPage,executeIntervention,getStats} from './assessmentApi'
import {getPage, bradenAssess, morseAssess, nrs2002Assess, painAssess, tubeAssess, getInterventionPage, getStats} from './api'
const activeTab=ref('BRADEN')
const bradenData=ref([]);const morseData=ref([]);const nrs2002Data=ref([]);const painData=ref([]);const tubeData=ref([])
const interventionData=ref([]);const stats=ref({})
const assessDialogVisible=ref(false)
const currentTool=ref('BRADEN')
const assessDialogTitle=computed(()=>({BRADEN:'Braden压疮评估',MORSE:'Morse跌倒评估',NRS2002:'NRS2002营养筛查',NRS_PAIN:'疼痛评估(NRS)',TUBE:'导管风险评估'}[currentTool.value]))
const assessForm=ref({patientId:null,encounterId:null,patientName:'',assessorName:'',detail:''})
const bradenScores=ref({sensory:4,moisture:4,activity:4,mobility:4,nutrition:4,friction:3})
const bradenTotal=computed(()=>Object.values(bradenScores.value).reduce((a,b)=>a+b,0))
const morseScores=ref({fall_history:0,diagnosis:0,gait:0,iv:0,gait_type:0})
const morseTotal=computed(()=>Object.values(morseScores.value).reduce((a,b)=>a+b,0))
const nrsScores=ref({disease:0,nutrition:0,age:0})
const nrsTotal=computed(()=>Object.values(nrsScores.value).reduce((a,b)=>a+b,0))
const painScore=ref(0)
const painMarks=ref({0:'无痛',3:'轻度',6:'中度',10:'重度'})
const tubeScores=ref({type:1,fixation:1,patency:1,skin:1,infection:1})
const tubeTotal=computed(()=>Object.values(tubeScores.value).reduce((a,b)=>a+b,0))
const statCards=computed(()=>[
{label:'压疮评估',value:stats.value.BRADEN_total||0,color:'#e6a23c'},
{label:'压疮高危',value:stats.value.BRADEN_high_risk||0,color:'#f56c6c'},
{label:'跌倒评估',value:stats.value.MORSE_total||0,color:'#409eff'},
{label:'跌倒高危',value:stats.value.MORSE_high_risk||0,color:'#f56c6c'},
{label:'营养筛查',value:stats.value.NRS2002_total||0,color:'#67c23a'},
{label:'待执行干预',value:stats.value.pending_interventions||0,color:'#e6a23c'},
const activeTab = ref('BRADEN')
const assessVisible = ref(false)
const bradenData = ref([]), morseData = ref([]), nrsData = ref([]), painData = ref([]), tubeData = ref([])
const interventionData = ref([])
const statCards = ref([
{label:'总评估', value:0, color:'#409eff'},
{label:'高危患者', value:0, color:'#f56c6c'},
{label:'中危患者', value:0, color:'#e6a23c'},
{label:'已干预', value:0, color:'#67c23a'},
{label:'待评估', value:0, color:'#909399'},
{label:'评估率', value:'0%', color:'#409eff'}
])
const loadData=async()=>{
const p={pageNo:1,pageSize:50}
const [b,m,n,pa,t]=await Promise.all([
getAssessmentPage({...p,assessmentTool:'BRADEN'}),
getAssessmentPage({...p,assessmentTool:'MORSE'}),
getAssessmentPage({...p,assessmentTool:'NRS2002'}),
getAssessmentPage({...p,assessmentTool:'NRS_PAIN'}),
getAssessmentPage({...p,assessmentTool:'TUBE'}),
])
bradenData.value=b.data?.records||[];morseData.value=m.data?.records||[]
nrs2002Data.value=n.data?.records||[];painData.value=pa.data?.records||[]
tubeData.value=t.data?.records||[]
const iv=await getInterventionPage({pageNo:1,pageSize:50})
interventionData.value=iv.data?.records||[]
const assessForm = reactive({patientName:'', encounterId:'', assessmentTool:'BRADEN', detail:''})
const bradenScores = reactive({sensation:4, moisture:4, activity:4, mobility:4, nutrition:4, friction:3})
const morseScores = reactive({history:0, diagnosis:0, ambulation:0, iv:0, gait:0, mental:0})
const bradenTotal = computed(() => bradenScores.sensation + bradenScores.moisture + bradenScores.activity + bradenScores.mobility + bradenScores.nutrition + bradenScores.friction)
const morseTotal = computed(() => morseScores.history + morseScores.diagnosis + morseScores.ambulation + morseScores.iv + morseScores.gait + morseScores.mental)
const assessDialogTitle = computed(() => {
const m = {BRADEN:'Braden压疮风险评估', MORSE:'Morse跌倒风险评估', NRS2002:'NRS2002营养筛查', PAIN:'疼痛评估', TUBE:'管道风险评估'}
return m[assessForm.assessmentTool] || '护理评估'
})
async function loadStats() {
try {
const r = await getStats()
if (r.data) {
statCards.value[0].value = r.data.total || 0
statCards.value[1].value = r.data.highRisk || 0
statCards.value[2].value = r.data.mediumRisk || 0
statCards.value[3].value = r.data.intervened || 0
statCards.value[4].value = r.data.pending || 0
statCards.value[5].value = (r.data.assessmentRate || 0) + '%'
}
} catch(e) {}
}
const loadStats=async()=>{const r=await getStats();stats.value=r.data||{}}
const openAssess=(tool)=>{currentTool.value=tool;assessForm.value={patientId:null,encounterId:null,patientName:'',assessorName:'',detail:''};assessDialogVisible.value=true}
const submitAssess=async()=>{
let scores={}
if(currentTool.value==='BRADEN')scores=bradenScores.value
else if(currentTool.value==='MORSE')scores=morseScores.value
else if(currentTool.value==='NRS2002')scores=nrsScores.value
else if(currentTool.value==='NRS_PAIN')scores={pain_score:painScore.value}
else if(currentTool.value==='TUBE')scores=tubeScores.value
const data={...assessForm.value,itemScores:JSON.stringify(scores)}
const assessFn={BRADEN:bradenAssess,MORSE:morseAssess,NRS2002:nrs2002Assess,NRS_PAIN:painAssess,TUBE:tubeAssess}
const r=await assessFn[currentTool.value](data)
ElMessage.success('评估完成,风险等级: '+r.data?.assessment?.riskLevel)
assessDialogVisible.value=false;loadData();loadStats()
async function loadData() {
const tools = ['BRADEN','MORSE','NRS2002','PAIN','TUBE']
const targets = [bradenData, morseData, nrsData, painData, tubeData]
for (let i = 0; i < tools.length; i++) {
try {
const r = await getPage({assessmentTool: tools[i], pageNo:1, pageSize:50})
targets[i].value = r.data?.records || []
} catch(e) { targets[i].value = [] }
}
try {
const r = await getInterventionPage({pageNo:1, pageSize:50})
interventionData.value = r.data?.records || []
} catch(e) {}
}
const execIntervention=async(id)=>{await executeIntervention(id);ElMessage.success('已执行');loadData();loadStats()}
onMounted(()=>{loadData();loadStats()})
function openAssess(tool) {
assessForm.patientName = ''
assessForm.encounterId = ''
assessForm.assessmentTool = tool
assessForm.detail = ''
if (tool === 'BRADEN') {
Object.assign(bradenScores, {sensation:4, moisture:4, activity:4, mobility:4, nutrition:4, friction:3})
} else if (tool === 'MORSE') {
Object.assign(morseScores, {history:0, diagnosis:0, ambulation:0, iv:0, gait:0, mental:0})
}
assessVisible.value = true
}
async function submitAssess() {
const data = {
patientName: assessForm.patientName,
encounterId: assessForm.encounterId,
detail: assessForm.detail
}
try {
if (assessForm.assessmentTool === 'BRADEN') {
data.itemScores = JSON.stringify({sensation:bradenScores.sensation, moisture:bradenScores.moisture, activity:bradenScores.activity, mobility:bradenScores.mobility, nutrition:bradenScores.nutrition, friction:bradenScores.friction}); data.totalScore = bradenTotal.value
data.sensation = bradenScores.sensation
data.moisture = bradenScores.moisture
data.activity = bradenScores.activity
data.mobility = bradenScores.mobility
data.nutrition = bradenScores.nutrition
data.friction = bradenScores.friction
await bradenAssess(data)
} else if (assessForm.assessmentTool === 'MORSE') {
data.itemScores = JSON.stringify({history:morseScores.history, diagnosis:morseScores.diagnosis, ambulation:morseScores.ambulation, iv:morseScores.iv, gait:morseScores.gait, mental:morseScores.mental}); data.totalScore = morseTotal.value
data.history = morseScores.history
data.diagnosis = morseScores.diagnosis
data.ambulation = morseScores.ambulation
data.iv = morseScores.iv
data.gait = morseScores.gait
data.mental = morseScores.mental
await morseAssess(data)
} else if (assessForm.assessmentTool === 'NRS2002') {
await nrs2002Assess(data)
} else if (assessForm.assessmentTool === 'PAIN') {
await painAssess(data)
} else if (assessForm.assessmentTool === 'TUBE') {
await tubeAssess(data)
}
ElMessage.success('评估提交成功')
assessVisible.value = false
loadData()
loadStats()
} catch(e) {
ElMessage.error('提交失败: ' + (e.message || '未知错误'))
}
}
function exportReport() {
ElMessage.info('导出功能开发中')
}
onMounted(() => { loadData(); loadStats() })
</script>

View File

@@ -1,30 +1,175 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">护理质量指标</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<div style="display:flex;gap:30px;text-align:center">
<div><div style="font-size:24px;font-weight:bold;color:#409eff">{{ summary.total||0 }}</div><div>总指标</div></div>
<div><div style="font-size:24px;font-weight:bold;color:#67c23a">{{ summary.meetTarget||0 }}</div><div></div></div>
<div><div style="font-size:24px;font-weight:bold;color:#e6a23c">{{ summary.meetRate||0 }}%</div><div>达标率</div></div>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">护理质量指标管理</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增指</el-button>
<el-button type="warning" @click="exportReport">导出报告</el-button>
</div>
</el-card>
<div style="margin-bottom:12px"><el-button type="success" @click="showAdd=true">新增指标</el-button></div>
<el-table :data="indicatorData" border stripe>
<el-table-column prop="indicatorCode" label="编码" width="100"/>
<el-table-column prop="indicatorName" label="指标名称" min-width="180"/>
<el-table-column prop="indicatorCategory" label="类别" width="100"/>
<el-table-column prop="targetValue" label="目标" width="70" align="center"/>
<el-table-column prop="actualValue" label="实际" width="70" align="center"/>
<el-table-column prop="departmentName" label="科室" width="120"/>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.indicatorCategory" placeholder="指标类别" clearable style="width:140px">
<el-option label="基础护理" value="BASIC"/>
<el-option label="专科护理" value="SPECIALIZED"/>
<el-option label="护理安全" value="SAFETY"/>
<el-option label="护理文书" value="DOCUMENTATION"/>
<el-option label="消毒隔离" value="STERILIZATION"/>
</el-select>
<el-select v-model="q.departmentName" placeholder="科室" clearable style="width:140px">
<el-option label="内科" value="内科"/>
<el-option label="外科" value="外科"/>
<el-option label="妇产科" value="妇产科"/>
<el-option label="儿科" value="儿科"/>
<el-option label="ICU" value="ICU"/>
<el-option label="急诊科" value="急诊科"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="indicatorData" border stripe v-loading="loading">
<el-table-column prop="indicatorCode" label="指标编码" width="120"/>
<el-table-column prop="indicatorName" label="指标名称" min-width="180" show-overflow-tooltip/>
<el-table-column prop="indicatorCategory" label="类别" width="100" align="center">
<template #default="{row}">
<el-tag size="small">{{ categoryText(row.indicatorCategory) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="targetValue" label="目标值" width="80" align="center"/>
<el-table-column prop="actualValue" label="实际值" width="80" align="center">
<template #default="{row}">
<span :style="{color: row.actualValue >= row.targetValue ? '#67C23A' : '#F56C6C', fontWeight:'bold'}">
{{ row.actualValue }}
</span>
</template>
</el-table-column>
<el-table-column label="达标状态" width="90" align="center">
<template #default="{row}">
<el-tag :type="row.actualValue >= row.targetValue ? 'success' : 'danger'" size="small">
{{ row.actualValue >= row.targetValue ? '达标' : '未达标' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="departmentName" label="科室" width="100"/>
<el-table-column prop="statDate" label="统计日期" width="120"/>
<el-table-column prop="remark" label="备注" min-width="150" show-overflow-tooltip/>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增护理质量指标" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="指标编码" required><el-input v-model="formData.indicatorCode" placeholder="如: NQ001"/></el-form-item>
<el-form-item label="指标名称" required><el-input v-model="formData.indicatorName" placeholder="请输入指标名称"/></el-form-item>
<el-form-item label="指标类别" required>
<el-select v-model="formData.indicatorCategory" placeholder="请选择">
<el-option label="基础护理" value="BASIC"/>
<el-option label="专科护理" value="SPECIALIZED"/>
<el-option label="护理安全" value="SAFETY"/>
<el-option label="护理文书" value="DOCUMENTATION"/>
<el-option label="消毒隔离" value="STERILIZATION"/>
</el-select>
</el-form-item>
<el-form-item label="目标值"><el-input-number v-model="formData.targetValue" :min="0"/></el-form-item>
<el-form-item label="实际值"><el-input-number v-model="formData.actualValue" :min="0"/></el-form-item>
<el-form-item label="科室"><el-input v-model="formData.departmentName" placeholder="请输入科室"/></el-form-item>
<el-form-item label="统计日期"><el-date-picker v-model="formData.statDate" type="date" placeholder="选择日期"/></el-form-item>
<el-form-item label="备注"><el-input v-model="formData.remark" type="textarea" :rows="2"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getQualityPage,addIndicator,getQualitySummary} from './api'
const indicatorData=ref([]),summary=ref({})
const showAdd=ref(false)
const loadData=async()=>{const [i,s]=await Promise.all([getQualityPage({pageNo:1,pageSize:50}),getQualitySummary()]);indicatorData.value=i.data?.records||[];summary.value=s.data||{}}
onMounted(()=>loadData())
import {getQualityPage, addIndicator, getQualitySummary} from './api'
const loading = ref(false)
const indicatorData = ref([])
const total = ref(0)
const showAdd = ref(false)
const summary = ref({})
const statCards = ref([
{label:'总指标', value:0, color:'#409eff'},
{label:'达标数', value:0, color:'#67c23a'},
{label:'未达标数', value:0, color:'#f56c6c'},
{label:'达标率', value:'0%', color:'#e6a23c'},
{label:'科室数', value:0, color:'#909399'},
{label:'平均达标率', value:'0%', color:'#409eff'}
])
const q = ref({pageNo:1, pageSize:20, indicatorCategory:'', departmentName:''})
const formData = reactive({
indicatorCode:'', indicatorName:'', indicatorCategory:'BASIC',
targetValue:0, actualValue:0, departmentName:'', statDate:'', remark:''
})
function categoryText(c) {
return {BASIC:'基础护理',SPECIALIZED:'专科护理',SAFETY:'护理安全',DOCUMENTATION:'护理文书',STERILIZATION:'消毒隔离'}[c] || c
}
async function loadData() {
loading.value = true
try {
const [i, s] = await Promise.all([
getQualityPage(q.value),
getQualitySummary()
])
indicatorData.value = i.data?.records || []
total.value = i.data?.total || 0
summary.value = s.data || {}
// Calculate stats
let met = 0, unmet = 0, deptSet = new Set()
indicatorData.value.forEach(row => {
if (row.actualValue >= row.targetValue) met++
else unmet++
deptSet.add(row.departmentName)
})
statCards.value[0].value = total.value
statCards.value[1].value = met
statCards.value[2].value = unmet
statCards.value[3].value = total.value > 0 ? Math.round(met * 100 / total.value) + '%' : '0%'
statCards.value[4].value = deptSet.size
statCards.value[5].value = summary.value.meetRate ? summary.value.meetRate + '%' : '0%'
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, indicatorCategory:'', departmentName:''}
loadData()
}
async function submitForm() {
await addIndicator(formData)
ElMessage.success('新增成功')
showAdd.value = false
loadData()
}
function exportReport() { ElMessage.info('导出功能开发中') }
onMounted(() => loadData())
</script>

View File

@@ -1,31 +1,167 @@
<template>
<div style="padding:16px">
<div style="margin-bottom:16px"><span style="font-size:18px;font-weight:bold">药品库存预警</span></div>
<el-card shadow="never" style="margin-bottom:16px">
<div style="display:flex;gap:40px;text-align:center">
<div><div style="font-size:28px;font-weight:bold;color:#e6a23c">{{ stats.low||0 }}</div><div>低库存</div></div>
<div><div style="font-size:28px;font-weight:bold;color:#f56c6c">{{ stats.out||0 }}</div><div>缺货</div></div>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">药品库存预警管理</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="success" @click="showAdd = true">新增预警</el-button>
<el-button type="warning" @click="exportReport">导出报告</el-button>
</div>
</el-card>
<div style="margin-bottom:12px"><el-button type="success" @click="showAdd=true">新增预警</el-button></div>
<el-table :data="alertData" border stripe>
<el-table-column prop="drugName" label="药品" min-width="150"/>
<el-table-column prop="drugSpec" label="规格" width="100"/>
<el-table-column prop="currentStock" label="库存" width="80" align="center"/>
<el-table-column prop="minStock" label="最低" width="80" align="center"/>
<el-table-column prop="alertLevel" label="级别" width="100">
<template #default="{row}"><el-tag :type="row.alertLevel==='OUT'?'danger':'warning'" size="small">{{ row.alertLevel==='OUT'?'缺货':'低库存' }}</el-tag></template>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 筛选 -->
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap">
<el-select v-model="q.alertLevel" placeholder="预警级别" clearable style="width:120px">
<el-option label="缺货" value="OUT"/>
<el-option label="低库存" value="LOW"/>
<el-option label="近效期" value="EXPIRING"/>
</el-select>
<el-select v-model="q.drugCategory" placeholder="药品类别" clearable style="width:140px">
<el-option label="西药" value="WESTERN"/>
<el-option label="中成药" value="CHINESE"/>
<el-option label="中草药" value="HERBAL"/>
<el-option label="生物制品" value="BIOLOGICAL"/>
</el-select>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<!-- 数据表格 -->
<el-table :data="alertData" border stripe v-loading="loading">
<el-table-column prop="drugCode" label="药品编码" width="120"/>
<el-table-column prop="drugName" label="药品名称" min-width="160" show-overflow-tooltip/>
<el-table-column prop="drugSpec" label="规格" width="120"/>
<el-table-column prop="currentStock" label="当前库存" width="100" align="center">
<template #default="{row}">
<span :style="{color: row.currentStock <= 0 ? '#F56C6C' : row.currentStock < row.minStock ? '#E6A23C' : '#67C23A', fontWeight:'bold'}">
{{ row.currentStock }}
</span>
</template>
</el-table-column>
<el-table-column prop="minStock" label="最低库存" width="100" align="center"/>
<el-table-column prop="maxStock" label="最高库存" width="100" align="center"/>
<el-table-column prop="alertLevel" label="预警级别" width="100" align="center">
<template #default="{row}">
<el-tag :type="row.alertLevel==='OUT'?'danger':row.alertLevel==='LOW'?'warning':'info'" size="small">
{{ row.alertLevel==='OUT'?'缺货':row.alertLevel==='LOW'?'低库存':row.alertLevel==='EXPIRING'?'近效期':'正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="supplierName" label="供应商" width="140"/>
<el-table-column prop="lastRestockDate" label="上次进货" width="120"/>
<el-table-column prop="expiryDate" label="有效期至" width="120"/>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{row}">
<el-button link type="primary" @click="handleOrder(row)">补货申请</el-button>
</template>
</el-table-column>
<el-table-column prop="supplierName" label="供应商" width="120"/>
</el-table>
<el-pagination style="margin-top:12px;justify-content:flex-end" v-model:current-page="q.pageNo" v-model:page-size="q.pageSize" :total="total" layout="total, sizes, prev, pager, next, jumper" @size-change="loadData" @current-change="loadData"/>
<!-- 新增弹窗 -->
<el-dialog title="新增药品库存预警" v-model="showAdd" width="600px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="药品编码" required><el-input v-model="formData.drugCode" placeholder="请输入药品编码"/></el-form-item>
<el-form-item label="药品名称" required><el-input v-model="formData.drugName" placeholder="请输入药品名称"/></el-form-item>
<el-form-item label="规格"><el-input v-model="formData.drugSpec" placeholder="如: 0.5g*24片"/></el-form-item>
<el-form-item label="当前库存"><el-input-number v-model="formData.currentStock" :min="0"/></el-form-item>
<el-form-item label="最低库存"><el-input-number v-model="formData.minStock" :min="0"/></el-form-item>
<el-form-item label="最高库存"><el-input-number v-model="formData.maxStock" :min="0"/></el-form-item>
<el-form-item label="预警级别">
<el-select v-model="formData.alertLevel" placeholder="请选择">
<el-option label="缺货" value="OUT"/>
<el-option label="低库存" value="LOW"/>
<el-option label="近效期" value="EXPIRING"/>
</el-select>
</el-form-item>
<el-form-item label="供应商"><el-input v-model="formData.supplierName" placeholder="请输入供应商"/></el-form-item>
</el-form>
<template #footer>
<el-button @click="showAdd = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {ref,onMounted} from 'vue'
import {ref, reactive, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {getAlertPage,addAlert,getAlertStats} from './api'
const alertData=ref([]),stats=ref({})
const showAdd=ref(false)
const loadData=async()=>{const [a,s]=await Promise.all([getAlertPage({pageNo:1,pageSize:50}),getAlertStats()]);alertData.value=a.data?.records||[];stats.value=s.data||{}}
onMounted(()=>loadData())
import {getAlertPage, addAlert, getAlertStats} from './api'
const loading = ref(false)
const alertData = ref([])
const total = ref(0)
const showAdd = ref(false)
const statCards = ref([
{label:'总预警数', value:0, color:'#409eff'},
{label:'缺货', value:0, color:'#f56c6c'},
{label:'低库存', value:0, color:'#e6a23c'},
{label:'近效期', value:0, color:'#909399'},
{label:'供应商数', value:0, color:'#67c23a'},
{label:'待补货', value:0, color:'#409eff'}
])
const q = ref({pageNo:1, pageSize:20, alertLevel:'', drugCategory:''})
const formData = reactive({
drugCode:'', drugName:'', drugSpec:'', currentStock:0, minStock:0,
maxStock:100, alertLevel:'LOW', supplierName:''
})
async function loadData() {
loading.value = true
try {
const [a, s] = await Promise.all([getAlertPage(q.value), getAlertStats()])
alertData.value = a.data?.records || []
total.value = a.data?.total || 0
// Stats
let out = 0, low = 0, expiring = 0, supplierSet = new Set()
alertData.value.forEach(row => {
if (row.alertLevel === 'OUT') out++
else if (row.alertLevel === 'LOW') low++
else if (row.alertLevel === 'EXPIRING') expiring++
supplierSet.add(row.supplierName)
})
statCards.value[0].value = total.value
statCards.value[1].value = out
statCards.value[2].value = low
statCards.value[3].value = expiring
statCards.value[4].value = supplierSet.size
statCards.value[5].value = s.data?.pendingOrders || 0
} finally { loading.value = false }
}
function resetQuery() {
q.value = {pageNo:1, pageSize:20, alertLevel:'', drugCategory:''}
loadData()
}
async function submitForm() {
await addAlert(formData)
ElMessage.success('新增成功')
showAdd.value = false
loadData()
}
function handleOrder(row) {
ElMessage.info('补货申请: ' + row.drugName)
}
function exportReport() { ElMessage.info('导出功能开发中') }
onMounted(() => loadData())
</script>

View File

@@ -1,28 +1,181 @@
<template>
<div class="app-container">
<el-form :model="q" :inline="true"><el-form-item label="就诊号"><el-input v-model="q.encounterId" clearable /></el-form-item>
<el-form-item><el-button type="primary" @click="loadData">查询</el-button></el-form-item></el-form>
<el-row :gutter="20" class="mb8">
<el-col :span="12"><el-card shadow="hover"><el-statistic title="运行质控结果" :value="runtimeResult.status || '-'" /></el-card></el-col>
<el-col :span="12"><el-card shadow="hover"><el-statistic title="终末质控评分" :value="terminalResult.score || 0" suffix="分" /></el-card></el-col>
<div style="margin-bottom:16px;display:flex;justify-content:space-between;align-items:center">
<span style="font-size:18px;font-weight:bold">病历质量统计</span>
<div>
<el-button type="primary" @click="loadData">刷新</el-button>
<el-button type="warning" @click="exportReport">导出报告</el-button>
</div>
</div>
<!-- 统计卡片 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="4" v-for="item in statCards" :key="item.label">
<el-card shadow="hover" :body-style="{padding:'10px'}">
<div style="text-align:center">
<div style="font-size:20px;font-weight:bold" :style="{color:item.color}">{{ item.value }}</div>
<div style="font-size:12px;color:#999">{{ item.label }}</div>
</div>
</el-card>
</el-col>
</el-row>
<el-card class="mt8"><template #header>缺陷记录</template>
<el-table :data="defects" size="small">
<el-table-column label="缺陷类型" prop="defectType" width="120" />
<el-table-column label="缺陷项" prop="defectItem" width="150" />
<el-table-column label="严重程度" prop="severity" width="100">
<template #default="s"><el-tag :type="s.row.severity==='CRITICAL'?'danger':s.row.severity==='MAJOR'?'warning':'info'">{{ s.row.severity }}</el-tag></template>
</el-table-column>
<el-table-column label="整改状态" prop="rectifyStatus" width="100" />
</el-table></el-card>
<!-- 查询表单 -->
<el-form :model="q" :inline="true" style="margin-bottom:16px">
<el-form-item label="就诊号"><el-input v-model="q.encounterId" clearable placeholder="请输入就诊号" style="width:180px"/></el-form-item>
<el-form-item label="科室">
<el-select v-model="q.departmentName" clearable placeholder="请选择科室" style="width:140px">
<el-option label="内科" value="内科"/>
<el-option label="外科" value="外科"/>
<el-option label="妇产科" value="妇产科"/>
<el-option label="儿科" value="儿科"/>
<el-option label="ICU" value="ICU"/>
</el-select>
</el-form-item>
<el-form-item label="日期范围">
<el-date-picker v-model="q.dateRange" type="daterange" start-placeholder="开始" end-placeholder="结束" style="width:240px"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadData">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 质控结果 -->
<el-row :gutter="16" style="margin-bottom:16px">
<el-col :span="12">
<el-card shadow="hover">
<template #header><div style="display:flex;justify-content:space-between;align-items:center"><span>运行质控结果</span><el-tag :type="runtimeResult.status==='PASS'?'success':'danger'" size="small">{{ runtimeResult.status || '待检测' }}</el-tag></div></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="质控时间">{{ runtimeResult.checkTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="缺陷数">{{ runtimeResult.defectCount || 0 }}</el-descriptions-item>
<el-descriptions-item label="严重缺陷">{{ runtimeResult.criticalDefects || 0 }}</el-descriptions-item>
<el-descriptions-item label="一般缺陷">{{ runtimeResult.normalDefects || 0 }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><div style="display:flex;justify-content:space-between;align-items:center"><span>终末质控评分</span><el-tag :type="terminalResult.score>=80?'success':terminalResult.score>=60?'warning':'danger'" size="large">{{ terminalResult.score || 0 }}</el-tag></div></template>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="评分时间">{{ terminalResult.checkTime || '-' }}</el-descriptions-item>
<el-descriptions-item label="评分等级">{{ scoreLevel(terminalResult.score) }}</el-descriptions-item>
<el-descriptions-item label="甲级病历">{{ terminalResult.gradeA || 0 }}</el-descriptions-item>
<el-descriptions-item label="乙级病历">{{ terminalResult.gradeB || 0 }}</el-descriptions-item>
</el-descriptions>
</el-card>
</el-col>
</el-row>
<!-- 缺陷记录 -->
<el-card shadow="never">
<template #header>
<div style="display:flex;justify-content:space-between;align-items:center">
<span>缺陷记录 ({{ defects.length }})</span>
<el-select v-model="defectFilter" clearable placeholder="筛选缺陷类型" style="width:140px" @change="filterDefects">
<el-option label="全部" value=""/>
<el-option label="严重缺陷" value="CRITICAL"/>
<el-option label="主要缺陷" value="MAJOR"/>
<el-option label="一般缺陷" value="MINOR"/>
</el-select>
</div>
</template>
<el-table :data="filteredDefects" border stripe>
<el-table-column label="缺陷类型" prop="defectType" width="140"/>
<el-table-column label="缺陷项" prop="defectItem" min-width="180" show-overflow-tooltip/>
<el-table-column label="严重程度" prop="severity" width="110" align="center">
<template #default="s">
<el-tag :type="s.row.severity==='CRITICAL'?'danger':s.row.severity==='MAJOR'?'warning':'info'" size="small">
{{ s.row.severity==='CRITICAL'?'严重缺陷':s.row.severity==='MAJOR'?'主要缺陷':'一般缺陷' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="整改状态" prop="rectifyStatus" width="100" align="center">
<template #default="s">
<el-tag :type="s.row.rectifyStatus==='RECTIFIED'?'success':s.row.rectifyStatus==='RECTIFYING'?'warning':'info'" size="small">
{{ s.row.rectifyStatus==='RECTIFIED'?'已整改':s.row.rectifyStatus==='RECTIFYING'?'整改中':'待整改' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="发现时间" prop="discoverTime" width="160"/>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, reactive } from 'vue'; import { runtimeCheck, terminalCheck, getDefects } from '@/api/quality'
const q = reactive({ encounterId: '' }); const runtimeResult = ref({}); const terminalResult = ref({}); const defects = ref([])
const loadData = async () => {
if (!q.encounterId) return
const [r1, r2, r3] = await Promise.all([runtimeCheck(q.encounterId), terminalCheck(q.encounterId), getDefects(q.encounterId)])
runtimeResult.value = r1.data || {}; terminalResult.value = r2.data || {}; defects.value = r3.data || []
import {ref, reactive, computed, onMounted} from 'vue'
import {ElMessage} from 'element-plus'
import {runtimeCheck, terminalCheck, getDefects, getQualityStatistics} from '@/api/quality'
const q = reactive({encounterId:'', departmentName:'', dateRange:null})
const runtimeResult = ref({})
const terminalResult = ref({})
const defects = ref([])
const defectFilter = ref('')
const qualityStats = ref({})
const statCards = ref([
{label:'总病历数', value:0, color:'#409eff'},
{label:'甲级病历', value:0, color:'#67c23a'},
{label:'乙级病历', value:0, color:'#e6a23c'},
{label:'丙级病历', value:0, color:'#f56c6c'},
{label:'缺陷总数', value:0, color:'#909399'},
{label:'甲级率', value:'0%', color:'#409eff'}
])
const filteredDefects = computed(() => {
if (!defectFilter.value) return defects.value
return defects.value.filter(d => d.severity === defectFilter.value)
})
function scoreLevel(score) {
if (!score) return '-'
if (score >= 90) return '甲级'
if (score >= 75) return '乙级'
if (score >= 60) return '丙级'
return '不合格'
}
async function loadData() {
try {
if (q.encounterId) {
const [r1, r2, r3] = await Promise.all([
runtimeCheck(q.encounterId),
terminalCheck(q.encounterId),
getDefects(q.encounterId)
])
runtimeResult.value = r1.data || {}
terminalResult.value = r2.data || {}
defects.value = r3.data || []
}
// Load overall statistics
try {
const r4 = await getQualityStatistics(q)
qualityStats.value = r4.data || {}
statCards.value[0].value = qualityStats.value.totalRecords || 0
statCards.value[1].value = qualityStats.value.gradeA || 0
statCards.value[2].value = qualityStats.value.gradeB || 0
statCards.value[3].value = qualityStats.value.gradeC || 0
statCards.value[4].value = qualityStats.value.totalDefects || 0
statCards.value[5].value = (qualityStats.value.gradeARate || 0) + '%'
} catch(e) {}
} catch(e) { ElMessage.error('查询失败') }
}
function resetQuery() {
q.encounterId = ''
q.departmentName = ''
q.dateRange = null
runtimeResult.value = {}
terminalResult.value = {}
defects.value = []
loadData()
}
function filterDefects() {}
function exportReport() { ElMessage.info('导出功能开发中') }
onMounted(() => loadData())
</script>

View File

@@ -131,10 +131,12 @@
</template>
<script setup name="InteractionRule" lang="ts">
import { useDict } from '@/utils/dict'
import { ref, reactive, toRefs, onMounted, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { listInteractionRules, addInteractionRule, updateInteractionRule, delInteractionRule } from '@/api/rationaldrug'
const { interaction_severity } = useDict('interaction_severity')
const ruleList = ref([])
const loading = ref(false)
const total = ref(0)
@@ -146,11 +148,7 @@ const dialogTitle = ref('')
const queryForm = ref(null)
const ruleForm = ref(null)
const severityOptions = [
{ label: '严重', value: '严重' },
{ label: '中度', value: '中度' },
{ label: '轻度', value: '轻度' }
]
const severityOptions = computed(() => interaction_severity.value || [])
const data = reactive({
queryParams: {

View File

@@ -0,0 +1,86 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
<el-form-item label="计划名称" prop="planName">
<el-input v-model="queryParams.planName" placeholder="请输入计划名称" clearable @keyup.enter="handleQuery" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="queryParams.status" placeholder="请选择" clearable>
<el-option label="进行中" value="ACTIVE" /><el-option label="已完成" value="COMPLETED" /><el-option label="已终止" value="TERMINATED" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<el-row :gutter="10" class="mb8">
<el-col :span="1.5"><el-button type="primary" plain icon="Plus" @click="handleAdd">新增计划</el-button></el-col>
</el-row>
<el-table v-loading="loading" :data="planList" border>
<el-table-column label="计划名称" prop="planName" min-width="180" />
<el-table-column label="点评类型" prop="reviewType" width="120" />
<el-table-column label="科室" prop="deptName" width="120" />
<el-table-column label="点评处方数" prop="targetCount" width="110" />
<el-table-column label="已点评数" prop="reviewedCount" width="100" />
<el-table-column label="开始日期" prop="startDate" width="120" />
<el-table-column label="结束日期" prop="endDate" width="120" />
<el-table-column label="状态" width="90">
<template #default="s">
<el-tag :type="s.row.status==='ACTIVE'?'success':s.row.status==='COMPLETED'?'info':'danger'">
{{ {ACTIVE:'进行中',COMPLETED:'已完成',TERMINATED:'已终止'}[s.row.status] || s.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default="s">
<el-button link type="primary" icon="Edit" @click="handleUpdate(s.row)">修改</el-button>
<el-button link type="danger" icon="Delete" @click="handleDelete(s.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination v-show="total>0" v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize"
:page-sizes="[10,20,50]" :total="total" layout="total, sizes, prev, pager, next" @size-change="handleQuery" @current-change="handleQuery" />
<el-dialog :title="dialogTitle" v-model="dialogVisible" width="600px" append-to-body>
<el-form ref="planForm" :model="form" :rules="rules" label-width="110px">
<el-form-item label="计划名称" prop="planName"><el-input v-model="form.planName" placeholder="请输入计划名称" /></el-form-item>
<el-form-item label="点评类型" prop="reviewType">
<el-select v-model="form.reviewType" placeholder="请选择" style="width:100%">
<el-option label="处方点评" value="PRESCRIPTION" /><el-option label="抗菌药物专项" value="ANTIBIOTIC" /><el-option label="重点监控药品" value="MONITORED" />
</el-select>
</el-form-item>
<el-form-item label="科室" prop="deptName"><el-input v-model="form.deptName" placeholder="请输入科室名称" /></el-form-item>
<el-form-item label="目标处方数" prop="targetCount"><el-input-number v-model="form.targetCount" :min="1" :max="10000" /></el-form-item>
<el-row :gutter="20">
<el-col :span="12"><el-form-item label="开始日期" prop="startDate"><el-date-picker v-model="form.startDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col>
<el-col :span="12"><el-form-item label="结束日期" prop="endDate"><el-date-picker v-model="form.endDate" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item></el-col>
</el-row>
<el-form-item label="备注"><el-input v-model="form.remark" type="textarea" :rows="2" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible=false">取消</el-button>
<el-button type="primary" @click="handleSubmit">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { createPlan, listPlans, updatePlan, deletePlan } from '@/api/review'
const loading = ref(false); const total = ref(0); const planList = ref([])
const dialogVisible = ref(false); const dialogTitle = ref(''); const queryForm = ref(null); const planForm = ref(null)
const queryParams = reactive({ pageNum: 1, pageSize: 10, planName: '', status: '' })
const form = reactive({ id: null, planName: '', reviewType: 'PRESCRIPTION', deptName: '', targetCount: 50, startDate: '', endDate: '', remark: '' })
const rules = { planName: [{ required: true, message: '请输入计划名称', trigger: 'blur' }], reviewType: [{ required: true, message: '请选择点评类型', trigger: 'change' }] }
function handleQuery() { loading.value = true; listPlans(queryParams).then(res => { planList.value = res.data?.records || res.data || []; total.value = res.data?.total || 0 }).finally(() => loading.value = false) }
function handleReset() { queryForm.value?.resetFields(); queryParams.pageNum = 1; handleQuery() }
function handleAdd() { Object.assign(form, { id: null, planName: '', reviewType: 'PRESCRIPTION', deptName: '', targetCount: 50, startDate: '', endDate: '', remark: '' }); dialogTitle.value = '新增点评计划'; dialogVisible.value = true }
function handleUpdate(row) { Object.assign(form, row); dialogTitle.value = '修改点评计划'; dialogVisible.value = true }
function handleSubmit() { planForm.value?.validate(valid => { if (valid) { const action = form.id ? updatePlan(form) : createPlan(form); action.then(() => { ElMessage.success(form.id ? '修改成功' : '新增成功'); dialogVisible.value = false; handleQuery() }) } }) }
function handleDelete(row) { ElMessageBox.confirm('确认删除该计划?', '提示', { type: 'warning' }).then(() => deletePlan(row.id)).then(() => { ElMessage.success('删除成功'); handleQuery() }) }
onMounted(() => handleQuery())
</script>

View File

@@ -0,0 +1,39 @@
<template>
<div class="app-container">
<el-row :gutter="20" class="mb8">
<el-col :span="6"><el-card shadow="hover"><el-statistic title="科室合理率TOP5" :value="topDeptRate" suffix="%" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="不合理处方总数" :value="stats.unreasonableCount || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="总点评数" :value="stats.totalRecords || 0" /></el-card></el-col>
<el-col :span="6"><el-card shadow="hover"><el-statistic title="整体合理率" :value="stats.reasonableRate || 100" suffix="%" /></el-card></el-col>
</el-row>
<el-card shadow="never" class="mb8">
<template #header><span>医生点评排名按不合理数排序</span></template>
<el-table :data="ranking" v-loading="loading" border>
<el-table-column label="排名" type="index" width="70" />
<el-table-column label="医生" prop="doctorName" width="120" />
<el-table-column label="科室" prop="deptName" width="120" />
<el-table-column label="总点评数" prop="totalReview" width="100" />
<el-table-column label="不合理数" prop="unreasonableCount" width="100">
<template #default="s"><el-text type="danger">{{ s.row.unreasonableCount }}</el-text></template>
</el-table-column>
<el-table-column label="合理率" width="100">
<template #default="s">
<el-progress :percentage="s.row.reasonableRate || 0" :color="s.row.reasonableRate>=90?'#67C23A':s.row.reasonableRate>=70?'#E6A23C':'#F56C6C'" :stroke-width="16" :text-inside="true" />
</template>
</el-table-column>
</el-table>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { getStatistics, getDoctorRanking } from '@/api/review'
const loading = ref(false); const stats = ref({}); const ranking = ref([])
const topDeptRate = computed(() => stats.value.reasonableRate || 100)
onMounted(async () => {
loading.value = true
const [s, r] = await Promise.all([getStatistics(), getDoctorRanking()])
stats.value = s.data || {}; ranking.value = r.data || []
loading.value = false
})
</script>

View File

@@ -0,0 +1,38 @@
<template>
<div class="app-container">
<el-form :model="queryParams" :inline="true" label-width="80px">
<el-form-item label="处方号"><el-input v-model="queryParams.prescriptionNo" clearable placeholder="请输入" /></el-form-item>
<el-form-item label="结果">
<el-select v-model="queryParams.reviewResult" clearable placeholder="全部">
<el-option label="合理" value="REASONABLE" /><el-option label="不合理" value="UNREASONABLE" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="queryParams.pageNum=1;handleQuery()">重置</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="records" border>
<el-table-column label="处方号" prop="prescriptionNo" width="140" />
<el-table-column label="患者" prop="patientName" width="100" />
<el-table-column label="医生" prop="doctorName" width="100" />
<el-table-column label="点评结果" width="100">
<template #default="s"><el-tag :type="s.row.reviewResult==='REASONABLE'?'success':'danger'">{{ s.row.reviewResult==='REASONABLE'?'合理':'不合理' }}</el-tag></template>
</el-table-column>
<el-table-column label="不合理类型" prop="unreasonableType" width="140" />
<el-table-column label="点评意见" prop="comment" min-width="200" show-overflow-tooltip />
<el-table-column label="点评人" prop="reviewerName" width="100" />
<el-table-column label="点评时间" prop="reviewTime" width="170" />
</el-table>
<el-pagination v-show="total>0" v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize"
:page-sizes="[10,20,50]" :total="total" layout="total, sizes, prev, pager, next" @size-change="handleQuery" @current-change="handleQuery" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { listRecords } from '@/api/review'
const loading = ref(false); const total = ref(0); const records = ref([])
const queryParams = reactive({ pageNum: 1, pageSize: 10, prescriptionNo: '', reviewResult: '' })
function handleQuery() { loading.value = true; listRecords(queryParams).then(res => { records.value = res.data?.records || res.data || []; total.value = res.data?.total || 0 }).finally(() => loading.value = false) }
onMounted(() => handleQuery())
</script>

View File

@@ -0,0 +1,73 @@
<template>
<div class="app-container">
<el-row :gutter="20">
<el-col :span="8">
<el-card shadow="never">
<template #header><span>待点评处方</span></template>
<el-table :data="pendingList" v-loading="loading" highlight-current-row @current-change="handleSelect" size="small">
<el-table-column label="患者" prop="patientName" width="80" />
<el-table-column label="处方号" prop="prescriptionNo" width="120" />
<el-table-column label="药品" prop="drugNames" min-width="150" show-overflow-tooltip />
<el-table-column label="医生" prop="doctorName" width="80" />
</el-table>
</el-card>
</el-col>
<el-col :span="16">
<el-card shadow="never">
<template #header><span>处方详情与点评</span></template>
<div v-if="!selectedPrescription" style="text-align:center;color:#999;padding:60px 0;"> 请从左侧选择一条处方</div>
<div v-else>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="患者">{{ selectedPrescription.patientName }}</el-descriptions-item>
<el-descriptions-item label="处方号">{{ selectedPrescription.prescriptionNo }}</el-descriptions-item>
<el-descriptions-item label="诊断">{{ selectedPrescription.diagnosis }}</el-descriptions-item>
<el-descriptions-item label="开方医生">{{ selectedPrescription.doctorName }}</el-descriptions-item>
<el-descriptions-item label="药品明细" :span="2">{{ selectedPrescription.drugNames }}</el-descriptions-item>
<el-descriptions-item label="用法用量" :span="2">{{ selectedPrescription.dosageInfo }}</el-descriptions-item>
</el-descriptions>
<el-divider />
<el-form :model="reviewForm" label-width="100px">
<el-form-item label="点评结果" required>
<el-radio-group v-model="reviewForm.reviewResult">
<el-radio value="REASONABLE">合理</el-radio>
<el-radio value="UNREASONABLE">不合理</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="不合理类型" v-if="reviewForm.reviewResult==='UNREASONABLE'">
<el-select v-model="reviewForm.unreasonableType" placeholder="请选择" multiple style="width:100%">
<el-option label="适应症不适宜" value="INDICATION" /><el-option label="用法用量不适宜" value="DOSAGE" />
<el-option label="重复用药" value="DUPLICATE" /><el-option label="配伍禁忌" value="INTERACTION" />
<el-option label="超说明书用药" value="OFF_LABEL" /><el-option label="其他" value="OTHER" />
</el-select>
</el-form-item>
<el-form-item label="点评意见"><el-input v-model="reviewForm.comment" type="textarea" :rows="3" placeholder="请输入点评意见" /></el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSubmitReview" :disabled="!reviewForm.reviewResult">提交点评</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { listRecords, submitReview } from '@/api/review'
const loading = ref(false); const pendingList = ref([]); const selectedPrescription = ref(null)
const reviewForm = reactive({ reviewResult: '', unreasonableType: [], comment: '' })
function handleSelect(row) { selectedPrescription.value = row; reviewForm.reviewResult = ''; reviewForm.unreasonableType = []; reviewForm.comment = '' }
function handleSubmitReview() {
submitReview({ prescriptionNo: selectedPrescription.value.prescriptionNo, encounterId: selectedPrescription.value.encounterId,
reviewResult: reviewForm.reviewResult, unreasonableType: reviewForm.unreasonableType.join(','), comment: reviewForm.comment,
reviewerName: '当前用户' }).then(() => {
ElMessage.success('点评提交成功')
pendingList.value = pendingList.value.filter(p => p.prescriptionNo !== selectedPrescription.value.prescriptionNo)
selectedPrescription.value = null
})
}
onMounted(() => { loading.value = true; listRecords({ status: 'PENDING', pageSize: 50 }).then(res => { pendingList.value = res.data?.records || res.data || [] }).finally(() => loading.value = false) })
</script>

View File

@@ -5,8 +5,8 @@
<el-input v-model="q.patientName" placeholder="患者姓名" clearable style="width:140px"/>
<el-input v-model="q.barcode" placeholder="条码号" clearable style="width:140px"/>
<el-select v-model="q.status" placeholder="状态" clearable style="width:120px">
<el-option label="已采集" value="COLLECTED"/>
<el-option label="已扫码" value="SCANNED"/>
<el-option v-for="d in specimen_status" :key="d.value" :label="d.label" :value="d.value" />
<el-option label="已拒收" value="REJECTED"/>
</el-select>
<el-input v-model="scanInput" placeholder="扫码(回车确认)" style="width:180px" @keyup.enter="doScan"/>
@@ -58,9 +58,11 @@
</div>
</template>
<script setup>
import { useDict } from '@/utils/dict'
import {ref,onMounted} from 'vue'
import {ElMessage,ElMessageBox} from 'element-plus'
import {getPage,add,update,scanConfirm,reject,del} from './api'
const { specimen_status } = useDict('specimen_status')
const tableData=ref([]);const total=ref(0)
const q=ref({pageNo:1,pageSize:20,patientName:'',barcode:'',status:''})
const scanInput=ref('')

View File

@@ -0,0 +1,88 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
<el-form-item label="就诊号" prop="encounterId">
<el-input v-model="queryParams.encounterId" placeholder="请输入就诊号" clearable style="width: 180px" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">查询</el-button>
<el-button type="success" icon="Plus" @click="handleAdd">新增评估</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="assessmentList" border>
<el-table-column label="患者" align="center" prop="patientName" width="120" />
<el-table-column label="就诊号" align="center" prop="encounterId" width="140" />
<el-table-column label="体质类型" align="center" prop="constitutionType" width="140">
<template #default="scope"><el-tag>{{ constitutionText(scope.row.constitutionType) }}</el-tag></template>
</el-table-column>
<el-table-column label="评估得分" align="center" prop="score" width="100" />
<el-table-column label="评估医生" align="center" prop="assessorName" width="120" />
<el-table-column label="评估时间" align="center" prop="assessTime" width="180" />
<el-table-column label="调理建议" align="center" prop="suggestion" min-width="200" show-overflow-tooltip />
</el-table>
<el-dialog title="中医体质辨识" v-model="addVisible" width="650px" append-to-body>
<el-form :model="formData" label-width="110px">
<el-form-item label="患者姓名"><el-input v-model="formData.patientName" placeholder="请输入患者姓名" /></el-form-item>
<el-form-item label="就诊号"><el-input v-model="formData.encounterId" placeholder="请输入就诊号" /></el-form-item>
<el-form-item label="体质类型">
<el-select v-model="formData.constitutionType" placeholder="请选择">
<el-option v-for="item in constitutionTypes" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item label="评估得分"><el-input-number v-model="formData.score" :min="0" :max="100" /></el-form-item>
<el-form-item label="调理建议"><el-input v-model="formData.suggestion" type="textarea" :rows="3" placeholder="请输入调理建议" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="addVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="TcmConstitution" lang="js">
import { ref, reactive, toRefs, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const assessmentList = ref([])
const loading = ref(false)
const addVisible = ref(false)
const constitutionTypes = [
{ value: 'PINGHE', label: '平和质' },
{ value: 'QIXU', label: '气虚质' },
{ value: 'YANGXU', label: '阳虚质' },
{ value: 'YINXU', label: '阴虚质' },
{ value: 'TANSHI', label: '痰湿质' },
{ value: 'SHIRE', label: '湿热质' },
{ value: 'XUEYU', label: '血瘀质' },
{ value: 'QIYU', label: '气郁质' },
{ value: 'TEBING', label: '特禀质' }
]
const data = reactive({
queryParams: { encounterId: undefined },
formData: { patientName: '', encounterId: '', constitutionType: 'PINGHE', score: 0, suggestion: '' }
})
const { queryParams, formData } = toRefs(data)
function constitutionText(t) {
const m = { PINGHE: '平和质', QIXU: '气虚质', YANGXU: '阳虚质', YINXU: '阴虚质', TANSHI: '痰湿质', SHIRE: '湿热质', XUEYU: '血瘀质', QIYU: '气郁质', TEBING: '特禀质' }
return m[t] || t
}
function handleQuery() {
loading.value = true
const url = queryParams.value.encounterId
? `/api/v1/tcm/constitution/encounter/${queryParams.value.encounterId}`
: '/api/v1/tcm/statistics'
request({ url, method: 'get' }).then(res => {
assessmentList.value = Array.isArray(res.data) ? res.data : (res.data?.records || [])
}).catch(() => { assessmentList.value = [] }).finally(() => { loading.value = false })
}
function handleAdd() {
formData.value = { patientName: '', encounterId: '', constitutionType: 'PINGHE', score: 0, suggestion: '' }
addVisible.value = true
}
function submitForm() {
request({ url: '/api/v1/tcm/constitution', method: 'post', data: formData.value }).then(() => {
ElMessage.success('评估完成'); addVisible.value = false; handleQuery()
})
}
onMounted(() => { handleQuery() })
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div class="app-container">
<el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
<el-form-item label="方剂类型" prop="type">
<el-select v-model="queryParams.type" placeholder="请选择" clearable style="width: 160px">
<el-option label="经典方剂" value="CLASSIC" />
<el-option label="经验方" value="EXPERIENCE" />
<el-option label="协定方" value="AGREEMENT" />
<el-option label="壮医方" value="ZHUANG" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="handleReset">重置</el-button>
<el-button type="success" icon="Plus" @click="handleAdd">新增方剂</el-button>
</el-form-item>
</el-form>
<el-table v-loading="loading" :data="prescriptionList" border>
<el-table-column label="方剂名称" align="center" prop="name" min-width="160" show-overflow-tooltip />
<el-table-column label="方剂类型" align="center" prop="type" width="120">
<template #default="scope">
<el-tag>{{ typeText(scope.row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="组成" align="center" prop="composition" min-width="200" show-overflow-tooltip />
<el-table-column label="功效" align="center" prop="effect" min-width="150" show-overflow-tooltip />
<el-table-column label="主治" align="center" prop="indication" min-width="150" show-overflow-tooltip />
<el-table-column label="用法用量" align="center" prop="dosage" width="120" />
<el-table-column label="操作" align="center" width="80" fixed="right">
<template #default="scope">
<el-button link type="primary" icon="View" @click="handleDetail(scope.row)">详情</el-button>
</template>
</el-table-column>
</el-table>
<el-dialog title="方剂详情" v-model="detailVisible" width="650px" append-to-body>
<el-descriptions :column="2" border>
<el-descriptions-item label="方剂名称">{{ detailData.name }}</el-descriptions-item>
<el-descriptions-item label="方剂类型">{{ typeText(detailData.type) }}</el-descriptions-item>
<el-descriptions-item label="组成" :span="2">{{ detailData.composition }}</el-descriptions-item>
<el-descriptions-item label="功效" :span="2">{{ detailData.effect }}</el-descriptions-item>
<el-descriptions-item label="主治" :span="2">{{ detailData.indication }}</el-descriptions-item>
<el-descriptions-item label="用法用量">{{ detailData.dosage }}</el-descriptions-item>
<el-descriptions-item label="方解">{{ detailData.analysis }}</el-descriptions-item>
</el-descriptions>
<template #footer><el-button @click="detailVisible = false">关闭</el-button></template>
</el-dialog>
<el-dialog title="新增方剂" v-model="addVisible" width="600px" append-to-body>
<el-form :model="formData" label-width="100px">
<el-form-item label="方剂名称"><el-input v-model="formData.name" placeholder="请输入方剂名称" /></el-form-item>
<el-form-item label="方剂类型">
<el-select v-model="formData.type" placeholder="请选择">
<el-option label="经典方剂" value="CLASSIC" />
<el-option label="经验方" value="EXPERIENCE" />
<el-option label="协定方" value="AGREEMENT" />
<el-option label="壮医方" value="ZHUANG" />
</el-select>
</el-form-item>
<el-form-item label="组成"><el-input v-model="formData.composition" type="textarea" :rows="3" placeholder="请输入药物组成" /></el-form-item>
<el-form-item label="功效"><el-input v-model="formData.effect" placeholder="请输入功效" /></el-form-item>
<el-form-item label="主治"><el-input v-model="formData.indication" placeholder="请输入主治病症" /></el-form-item>
<el-form-item label="用法用量"><el-input v-model="formData.dosage" placeholder="请输入用法用量" /></el-form-item>
<el-form-item label="方解"><el-input v-model="formData.analysis" type="textarea" :rows="2" placeholder="请输入方解" /></el-form-item>
</el-form>
<template #footer>
<el-button @click="addVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup name="TcmPrescription" lang="js">
import { ref, reactive, toRefs, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import request from '@/utils/request'
const prescriptionList = ref([])
const loading = ref(false)
const detailVisible = ref(false)
const addVisible = ref(false)
const detailData = ref({})
const data = reactive({
queryParams: { type: undefined },
formData: { name: '', type: 'CLASSIC', composition: '', effect: '', indication: '', dosage: '', analysis: '' }
})
const { queryParams, formData } = toRefs(data)
function typeText(t) {
const m = { CLASSIC: '经典方剂', EXPERIENCE: '经验方', AGREEMENT: '协定方', ZHUANG: '壮医方' }
return m[t] || t
}
function handleQuery() {
loading.value = true
request({ url: '/api/v1/tcm/prescriptions', method: 'get', params: queryParams.value }).then(res => {
prescriptionList.value = res.data || []
}).finally(() => { loading.value = false })
}
function handleReset() { queryParams.value.type = undefined; handleQuery() }
function handleDetail(row) { detailData.value = row; detailVisible.value = true }
function handleAdd() { formData.value = { name: '', type: 'CLASSIC', composition: '', effect: '', indication: '', dosage: '', analysis: '' }; addVisible.value = true }
function submitForm() {
request({ url: '/api/v1/tcm/prescription', method: 'post', data: formData.value }).then(() => {
ElMessage.success('新增成功'); addVisible.value = false; handleQuery()
})
}
onMounted(() => { handleQuery() })
</script>