feat(P1): 护理专项评估升级 — 压疮/跌倒/营养/疼痛/导管完整业务逻辑

- NursingAssessmentEnhancedController: 5种评估量表(Braden/Morse/NRS2002/NRS/TUBE)
- 风险等级自动判定+干预措施自动生成
- NursingAssessmentIntervention: 干预措施管理(待执行/已执行)
- V26 Flyway: 干预措施表+评估统计视图
- 前端assessment.vue: Tab式5种评估+实时评分+统计卡片
- 后端编译通过,前端构建通过
This commit is contained in:
2026-06-06 20:40:35 +08:00
parent ec39c8b13b
commit cf26554f60
8 changed files with 913 additions and 0 deletions

View File

@@ -0,0 +1,512 @@
package com.healthlink.his.web.nursing.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.core.common.core.domain.R;
import com.healthlink.his.nursing.domain.NursingAssessment;
import com.healthlink.his.nursing.domain.NursingAssessmentIntervention;
import com.healthlink.his.nursing.service.INursingAssessmentInterventionService;
import com.healthlink.his.nursing.service.INursingAssessmentService;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import java.util.*;
/**
* 护理评估增强 Controller — 压疮/跌倒/管道/营养/疼痛专项评估
* 使用已有 nursing_assessment 表,通过 assessment_tool 区分量表类型
*/
@RestController
@RequestMapping("/nursing-assessment-enhanced")
@Slf4j
@AllArgsConstructor
public class NursingAssessmentEnhancedController {
private final INursingAssessmentService assessmentService;
private final INursingAssessmentInterventionService interventionService;
// ==================== 通用查询 ====================
@GetMapping("/page")
public R<?> getPage(
@RequestParam(value = "assessmentTool", required = false) String assessmentTool,
@RequestParam(value = "riskLevel", required = false) String riskLevel,
@RequestParam(value = "encounterId", required = false) Long encounterId,
@RequestParam(value = "patientName", required = false) String patientName,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<NursingAssessment> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(assessmentTool), NursingAssessment::getAssessmentTool, assessmentTool)
.eq(StringUtils.hasText(riskLevel), NursingAssessment::getRiskLevel, riskLevel)
.eq(encounterId != null, NursingAssessment::getEncounterId, encounterId)
.like(StringUtils.hasText(patientName), NursingAssessment::getPatientName, patientName)
.eq(NursingAssessment::getDelFlag, "0")
.orderByDesc(NursingAssessment::getAssessmentTime);
return R.ok(assessmentService.page(new Page<>(pageNo, pageSize), w));
}
@GetMapping("/list")
public R<?> getList(
@RequestParam("encounterId") Long encounterId,
@RequestParam(value = "assessmentTool", required = false) String assessmentTool) {
LambdaQueryWrapper<NursingAssessment> w = new LambdaQueryWrapper<>();
w.eq(NursingAssessment::getEncounterId, encounterId)
.eq(StringUtils.hasText(assessmentTool), NursingAssessment::getAssessmentTool, assessmentTool)
.eq(NursingAssessment::getDelFlag, "0")
.orderByDesc(NursingAssessment::getAssessmentTime);
return R.ok(assessmentService.list(w));
}
// ==================== 压疮评估(Braden) ====================
@PostMapping("/braden/assess")
@Transactional(rollbackFor = Exception.class)
public R<?> bradenAssess(@RequestBody NursingAssessment assessment) {
Map<String, Integer> scores = parseItemScores(assessment.getItemScores());
// Braden量表6项: 感觉(1-4) 潮湿(1-4) 活动(1-4) 移动(1-4) 营养(1-4) 摩擦力(1-3)
int total = scores.values().stream().mapToInt(Integer::intValue).sum();
String riskLevel = bradenRiskLevel(total);
assessment.setAssessmentTool("BRADEN");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
// 自动生成干预措施
List<String> interventions = bradenInterventions(riskLevel);
for (String content : interventions) {
NursingAssessmentIntervention iv = new NursingAssessmentIntervention();
iv.setAssessmentId(assessment.getId());
iv.setEncounterId(assessment.getEncounterId());
iv.setPatientId(assessment.getPatientId());
iv.setRiskLevel(riskLevel);
iv.setInterventionType("PRESSURE_ULCER");
iv.setInterventionContent(content);
iv.setNurseName(assessment.getAssessorName());
iv.setStatus("PENDING");
iv.setCreateTime(new Date());
interventionService.save(iv);
}
Map<String, Object> result = new HashMap<>();
result.put("assessment", assessment);
result.put("interventions", interventions);
result.put("riskDescription", bradenRiskDescription(riskLevel));
return R.ok(result);
}
// ==================== 跌倒评估(Morse) ====================
@PostMapping("/morse/assess")
@Transactional(rollbackFor = Exception.class)
public R<?> morseAssess(@RequestBody NursingAssessment assessment) {
Map<String, Integer> scores = parseItemScores(assessment.getItemScores());
// Morse量表5项: 跌倒史(0-25) 医学诊断(0-15) 步行辅助(0-15) 静脉输液(0-20) 步态(0-20)
int total = scores.values().stream().mapToInt(Integer::intValue).sum();
String riskLevel = morseRiskLevel(total);
assessment.setAssessmentTool("MORSE");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
List<String> interventions = morseInterventions(riskLevel);
for (String content : interventions) {
NursingAssessmentIntervention iv = new NursingAssessmentIntervention();
iv.setAssessmentId(assessment.getId());
iv.setEncounterId(assessment.getEncounterId());
iv.setPatientId(assessment.getPatientId());
iv.setRiskLevel(riskLevel);
iv.setInterventionType("FALL_RISK");
iv.setInterventionContent(content);
iv.setNurseName(assessment.getAssessorName());
iv.setStatus("PENDING");
iv.setCreateTime(new Date());
interventionService.save(iv);
}
Map<String, Object> result = new HashMap<>();
result.put("assessment", assessment);
result.put("interventions", interventions);
result.put("riskDescription", morseRiskDescription(riskLevel));
return R.ok(result);
}
// ==================== 营养筛查(NRS 2002) ====================
@PostMapping("/nrs2002/assess")
@Transactional(rollbackFor = Exception.class)
public R<?> nrs2002Assess(@RequestBody NursingAssessment assessment) {
Map<String, Integer> scores = parseItemScores(assessment.getItemScores());
// NRS2002: 疾病严重(0-3) 营养状况(0-3) 年龄(0-1) 总分0-7
int total = scores.values().stream().mapToInt(Integer::intValue).sum();
String riskLevel = nrs2002RiskLevel(total);
assessment.setAssessmentTool("NRS2002");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
List<String> interventions = nrs2002Interventions(riskLevel);
for (String content : interventions) {
NursingAssessmentIntervention iv = new NursingAssessmentIntervention();
iv.setAssessmentId(assessment.getId());
iv.setEncounterId(assessment.getEncounterId());
iv.setPatientId(assessment.getPatientId());
iv.setRiskLevel(riskLevel);
iv.setInterventionType("NUTRITION");
iv.setInterventionContent(content);
iv.setNurseName(assessment.getAssessorName());
iv.setStatus("PENDING");
iv.setCreateTime(new Date());
interventionService.save(iv);
}
Map<String, Object> result = new HashMap<>();
result.put("assessment", assessment);
result.put("interventions", interventions);
result.put("riskDescription", nrs2002RiskDescription(riskLevel));
return R.ok(result);
}
// ==================== 疼痛评估(NRS) ====================
@PostMapping("/pain/assess")
@Transactional(rollbackFor = Exception.class)
public R<?> painAssess(@RequestBody NursingAssessment assessment) {
Map<String, Integer> scores = parseItemScores(assessment.getItemScores());
// NRS疼痛评分: 0-10
int total = scores.getOrDefault("pain_score", 0);
String riskLevel = painRiskLevel(total);
assessment.setAssessmentTool("NRS_PAIN");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
List<String> interventions = painInterventions(riskLevel, total);
for (String content : interventions) {
NursingAssessmentIntervention iv = new NursingAssessmentIntervention();
iv.setAssessmentId(assessment.getId());
iv.setEncounterId(assessment.getEncounterId());
iv.setPatientId(assessment.getPatientId());
iv.setRiskLevel(riskLevel);
iv.setInterventionType("PAIN");
iv.setInterventionContent(content);
iv.setNurseName(assessment.getAssessorName());
iv.setStatus("PENDING");
iv.setCreateTime(new Date());
interventionService.save(iv);
}
Map<String, Object> result = new HashMap<>();
result.put("assessment", assessment);
result.put("interventions", interventions);
result.put("riskDescription", painRiskDescription(riskLevel, total));
return R.ok(result);
}
// ==================== 导管评估 ====================
@PostMapping("/tube/assess")
@Transactional(rollbackFor = Exception.class)
public R<?> tubeAssess(@RequestBody NursingAssessment assessment) {
Map<String, Integer> scores = parseItemScores(assessment.getItemScores());
// 导管评估: 导管类型(1-5) 固定情况(1-3) 通畅情况(1-3) 周围皮肤(1-3) 感染风险(1-3)
int total = scores.values().stream().mapToInt(Integer::intValue).sum();
String riskLevel = tubeRiskLevel(total);
assessment.setAssessmentTool("TUBE");
assessment.setTotalScore(total);
assessment.setRiskLevel(riskLevel);
assessment.setDelFlag("0");
assessment.setAssessmentTime(new Date());
assessmentService.save(assessment);
List<String> interventions = tubeInterventions(riskLevel);
for (String content : interventions) {
NursingAssessmentIntervention iv = new NursingAssessmentIntervention();
iv.setAssessmentId(assessment.getId());
iv.setEncounterId(assessment.getEncounterId());
iv.setPatientId(assessment.getPatientId());
iv.setRiskLevel(riskLevel);
iv.setInterventionType("TUBE");
iv.setInterventionContent(content);
iv.setNurseName(assessment.getAssessorName());
iv.setStatus("PENDING");
iv.setCreateTime(new Date());
interventionService.save(iv);
}
Map<String, Object> result = new HashMap<>();
result.put("assessment", assessment);
result.put("interventions", interventions);
result.put("riskDescription", tubeRiskDescription(riskLevel));
return R.ok(result);
}
// ==================== 干预措施管理 ====================
@GetMapping("/intervention/page")
public R<?> getInterventionPage(
@RequestParam(value = "interventionType", required = false) String interventionType,
@RequestParam(value = "status", required = false) String status,
@RequestParam(value = "encounterId", required = false) Long encounterId,
@RequestParam(value = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(value = "pageSize", defaultValue = "20") Integer pageSize) {
LambdaQueryWrapper<NursingAssessmentIntervention> w = new LambdaQueryWrapper<>();
w.eq(StringUtils.hasText(interventionType), NursingAssessmentIntervention::getInterventionType, interventionType)
.eq(StringUtils.hasText(status), NursingAssessmentIntervention::getStatus, status)
.eq(encounterId != null, NursingAssessmentIntervention::getEncounterId, encounterId)
.orderByDesc(NursingAssessmentIntervention::getCreateTime);
return R.ok(interventionService.page(new Page<>(pageNo, pageSize), w));
}
@PutMapping("/intervention/execute/{id}")
@Transactional(rollbackFor = Exception.class)
public R<?> executeIntervention(@PathVariable Long id) {
NursingAssessmentIntervention iv = interventionService.getById(id);
if (iv == null) return R.fail("干预记录不存在");
iv.setStatus("EXECUTED");
iv.setExecuteTime(new Date());
interventionService.updateById(iv);
return R.ok();
}
// ==================== 统计 ====================
@GetMapping("/stats")
public R<?> getStats(
@RequestParam(value = "startDate", required = false) String startDate,
@RequestParam(value = "endDate", required = false) String endDate) {
Map<String, Object> stats = new HashMap<>();
String[] tools = {"BRADEN", "MORSE", "NRS2002", "NRS_PAIN", "TUBE"};
for (String tool : tools) {
LambdaQueryWrapper<NursingAssessment> w = new LambdaQueryWrapper<>();
w.eq(NursingAssessment::getAssessmentTool, tool)
.eq(NursingAssessment::getDelFlag, "0");
long total = assessmentService.count(w);
LambdaQueryWrapper<NursingAssessment> hw = new LambdaQueryWrapper<>();
hw.eq(NursingAssessment::getAssessmentTool, tool)
.eq(NursingAssessment::getRiskLevel, "HIGH")
.eq(NursingAssessment::getDelFlag, "0");
long highRisk = assessmentService.count(hw);
stats.put(tool + "_total", total);
stats.put(tool + "_high_risk", highRisk);
}
LambdaQueryWrapper<NursingAssessmentIntervention> iw = new LambdaQueryWrapper<>();
iw.eq(NursingAssessmentIntervention::getStatus, "PENDING");
stats.put("pending_interventions", interventionService.count(iw));
return R.ok(stats);
}
// ==================== 业务逻辑: 风险等级判定 ====================
private String bradenRiskLevel(int score) {
if (score <= 12) return "HIGH";
if (score <= 14) return "MEDIUM";
if (score <= 18) return "LOW";
return "NORMAL";
}
private String morseRiskLevel(int score) {
if (score >= 45) return "HIGH";
if (score >= 25) return "MEDIUM";
return "LOW";
}
private String nrs2002RiskLevel(int score) {
if (score >= 3) return "HIGH";
if (score >= 2) return "MEDIUM";
return "NORMAL";
}
private String painRiskLevel(int score) {
if (score >= 7) return "HIGH";
if (score >= 4) return "MEDIUM";
if (score >= 1) return "LOW";
return "NORMAL";
}
private String tubeRiskLevel(int score) {
if (score >= 12) return "HIGH";
if (score >= 8) return "MEDIUM";
return "LOW";
}
// ==================== 业务逻辑: 干预措施 ====================
private List<String> bradenInterventions(String riskLevel) {
List<String> list = new ArrayList<>();
if ("HIGH".equals(riskLevel)) {
list.add("每2小时翻身一次使用气垫床");
list.add("保持皮肤清洁干燥,使用皮肤保护膜");
list.add("加强营养支持,高蛋白饮食");
list.add("使用减压敷料保护骨隆突处");
list.add("填写压疮风险评估单,上报护士长");
} else if ("MEDIUM".equals(riskLevel)) {
list.add("每3小时翻身一次");
list.add("保持皮肤清洁,使用润肤露");
list.add("加强营养,增加蛋白质摄入");
list.add("使用减压床垫");
} else if ("LOW".equals(riskLevel)) {
list.add("定时翻身,避免局部长期受压");
list.add("保持皮肤清洁");
}
return list;
}
private List<String> morseInterventions(String riskLevel) {
List<String> list = new ArrayList<>();
if ("HIGH".equals(riskLevel)) {
list.add("悬挂高危跌倒标识");
list.add("床栏上抬,必要时使用约束带");
list.add("协助如厕,防滑鞋");
list.add("床头铃置于易取处,呼叫器放在枕边");
list.add("每1小时巡视评估意识状态");
list.add("填写跌倒风险评估单,上报护士长");
} else if ("MEDIUM".equals(riskLevel)) {
list.add("悬挂中危跌倒标识");
list.add("床栏上抬,走廊扶手");
list.add("协助下床活动");
list.add("每2小时巡视");
} else {
list.add("做好防跌倒宣教");
list.add("保持地面干燥");
}
return list;
}
private List<String> nrs2002Interventions(String riskLevel) {
List<String> list = new ArrayList<>();
if ("HIGH".equals(riskLevel)) {
list.add("请营养科会诊,制定营养支持方案");
list.add("给予肠内/肠外营养支持");
list.add("每日监测体重、白蛋白、前白蛋白");
list.add("记录24小时出入量");
} else if ("MEDIUM".equals(riskLevel)) {
list.add("增加蛋白质摄入,口服营养补充剂");
list.add("每周监测体重");
list.add("营养宣教");
}
return list;
}
private List<String> painInterventions(String riskLevel, int score) {
List<String> list = new ArrayList<>();
if ("HIGH".equals(riskLevel)) {
list.add("立即通知医生,遵医嘱给予镇痛药");
list.add("评估疼痛性质、部位、持续时间");
list.add("每4小时评估疼痛评分");
list.add("非药物镇痛:体位调整、冷敷/热敷");
} else if ("MEDIUM".equals(riskLevel)) {
list.add("遵医嘱给予口服镇痛药");
list.add("每8小时评估疼痛评分");
list.add("指导患者使用疼痛日记");
} else if ("LOW".equals(riskLevel)) {
list.add("继续观察,每日评估");
list.add("非药物缓解:音乐疗法、放松训练");
}
return list;
}
private List<String> tubeInterventions(String riskLevel) {
List<String> list = new ArrayList<>();
if ("HIGH".equals(riskLevel)) {
list.add("每日评估导管必要性,尽早拔管");
list.add("加强导管固定,使用专业固定装置");
list.add("每日消毒导管周围皮肤");
list.add("监测感染指标,必要时做培养");
list.add("每班评估导管通畅性");
} else if ("MEDIUM".equals(riskLevel)) {
list.add("加强导管固定,防止脱出");
list.add("每日评估导管功能");
list.add("保持导管周围皮肤清洁");
} else {
list.add("常规导管护理");
list.add("每班评估导管状态");
}
return list;
}
// ==================== 业务逻辑: 风险描述 ====================
private String bradenRiskDescription(String level) {
switch (level) {
case "HIGH": return "压疮高危(≤12分): 需立即采取综合预防措施";
case "MEDIUM": return "压疮中危(13-14分): 需加强预防";
case "LOW": return "压疮低危(15-18分): 常规预防";
default: return "压疮无风险(>18分)";
}
}
private String morseRiskDescription(String level) {
switch (level) {
case "HIGH": return "跌倒高危(≥45分): 需专人看护+环境改造";
case "MEDIUM": return "跌倒中危(25-44分): 需加强防护";
default: return "跌倒低危(<25分): 常规宣教";
}
}
private String nrs2002RiskDescription(String level) {
switch (level) {
case "HIGH": return "营养高危(≥3分): 需营养科会诊+营养支持";
case "MEDIUM": return "营养中危(2分): 需营养干预";
default: return "营养正常(<2分): 常规饮食";
}
}
private String painRiskDescription(String level, int score) {
if (score >= 7) return "重度疼痛(" + score + "分): 立即镇痛";
if (score >= 4) return "中度疼痛(" + score + "分): 遵医嘱镇痛";
if (score >= 1) return "轻度疼痛(" + score + "分): 观察+非药物缓解";
return "无疼痛(0分)";
}
private String tubeRiskDescription(String level) {
switch (level) {
case "HIGH": return "导管高危: 需专人管理+感染防控";
case "MEDIUM": return "导管中危: 加强固定+定期评估";
default: return "导管低危: 常规护理";
}
}
// ==================== 工具方法 ====================
private Map<String, Integer> parseItemScores(String itemScoresJson) {
Map<String, Integer> map = new HashMap<>();
if (!StringUtils.hasText(itemScoresJson)) return map;
try {
String json = itemScoresJson.trim();
if (json.startsWith("{")) {
json = json.substring(1, json.length() - 1);
for (String pair : json.split(",")) {
String[] kv = pair.split(":");
if (kv.length == 2) {
String key = kv[0].trim().replace("\"", "");
String val = kv[1].trim().replace("\"", "");
map.put(key, Integer.parseInt(val));
}
}
}
} catch (Exception e) {
log.warn("解析评估分数失败: {}", itemScoresJson, e);
}
return map;
}
}

View File

@@ -0,0 +1,35 @@
-- V26: 护理评估增强 — 干预措施+趋势统计
-- 评估干预措施表
CREATE TABLE IF NOT EXISTS nursing_assessment_intervention (
id BIGSERIAL PRIMARY KEY,
assessment_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
risk_level VARCHAR(16) NOT NULL,
intervention_type VARCHAR(32) NOT NULL,
intervention_content TEXT NOT NULL,
nurse_name VARCHAR(64),
execute_time TIMESTAMP,
status VARCHAR(20) DEFAULT 'PENDING',
tenant_id BIGINT DEFAULT 0,
is_deleted INT NOT NULL DEFAULT 0,
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
COMMENT ON TABLE nursing_assessment_intervention IS '评估干预措施';
COMMENT ON COLUMN nursing_assessment_intervention.intervention_type IS '干预类型(PRESSURE_ULCER/FALL_RISK/NUTRITION/PAIN/TUBE)';
COMMENT ON COLUMN nursing_assessment_intervention.status IS '状态(PENDING/EXECUTED/CANCELLED)';
CREATE INDEX idx_nai_encounter ON nursing_assessment_intervention(encounter_id);
CREATE INDEX idx_nai_status ON nursing_assessment_intervention(status);
-- 评估统计视图
CREATE OR REPLACE VIEW v_assessment_stats AS
SELECT
assessment_tool,
risk_level,
COUNT(*) as count,
DATE(assessment_time) as assess_date
FROM nursing_assessment
WHERE del_flag = '0'
GROUP BY assessment_tool, risk_level, DATE(assessment_time);
COMMENT ON VIEW v_assessment_stats IS '评估统计视图';

View File

@@ -0,0 +1,28 @@
package com.healthlink.his.nursing.domain;
import com.baomidou.mybatisplus.annotation.*;
import com.core.common.core.domain.HisBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.Date;
/**
* 评估干预措施
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("nursing_assessment_intervention")
public class NursingAssessmentIntervention extends HisBaseEntity {
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
private Long assessmentId;
private Long encounterId;
private Long patientId;
private String riskLevel;
private String interventionType;
private String interventionContent;
private String nurseName;
private Date executeTime;
private String status;
}

View File

@@ -0,0 +1,9 @@
package com.healthlink.his.nursing.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.nursing.domain.NursingAssessmentIntervention;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NursingAssessmentInterventionMapper extends BaseMapper<NursingAssessmentIntervention> {
}

View File

@@ -0,0 +1,7 @@
package com.healthlink.his.nursing.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.nursing.domain.NursingAssessmentIntervention;
public interface INursingAssessmentInterventionService extends IService<NursingAssessmentIntervention> {
}

View File

@@ -0,0 +1,13 @@
package com.healthlink.his.nursing.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.nursing.domain.NursingAssessmentIntervention;
import com.healthlink.his.nursing.mapper.NursingAssessmentInterventionMapper;
import com.healthlink.his.nursing.service.INursingAssessmentInterventionService;
import org.springframework.stereotype.Service;
@Service
public class NursingAssessmentInterventionServiceImpl
extends ServiceImpl<NursingAssessmentInterventionMapper, NursingAssessmentIntervention>
implements INursingAssessmentInterventionService {
}

View File

@@ -0,0 +1,298 @@
<template>
<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-row :gutter="12" 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>
<!-- 评估类型Tab -->
<el-tabs v-model="activeTab" type="border-card">
<!-- 压疮评估(Braden) -->
<el-tab-pane label="压疮评估(Braden)" name="BRADEN">
<div style="margin-bottom:12px">
<el-button type="success" @click="openAssess('BRADEN')">新建Braden评估</el-button>
</div>
<el-table :data="bradenData" 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==='LOW'?'低危':'正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assessorName" label="评估人" width="80"/>
<el-table-column prop="assessmentTime" label="评估时间" width="170"/>
<el-table-column prop="detail" label="备注" show-overflow-tooltip/>
</el-table>
</el-tab-pane>
<!-- 跌倒评估(Morse) -->
<el-tab-pane label="跌倒评估(Morse)" name="MORSE">
<div style="margin-bottom:12px">
<el-button type="success" @click="openAssess('MORSE')">新建Morse评估</el-button>
</div>
<el-table :data="morseData" 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'?'中危':'低危' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assessorName" label="评估人" width="80"/>
<el-table-column prop="assessmentTime" label="评估时间" width="170"/>
</el-table>
</el-tab-pane>
<!-- 营养筛查(NRS2002) -->
<el-tab-pane label="营养筛查(NRS2002)" name="NRS2002">
<div style="margin-bottom:12px">
<el-button type="success" @click="openAssess('NRS2002')">新建NRS2002评估</el-button>
</div>
<el-table :data="nrs2002Data" 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'?'中危':'正常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assessorName" label="评估人" width="80"/>
<el-table-column prop="assessmentTime" label="评估时间" width="170"/>
</el-table>
</el-tab-pane>
<!-- 疼痛评估(NRS) -->
<el-tab-pane label="疼痛评估(NRS)" name="NRS_PAIN">
<div style="margin-bottom:12px">
<el-button type="success" @click="openAssess('NRS_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">
<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'?'轻度':'无痛' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assessorName" label="评估人" width="80"/>
<el-table-column prop="assessmentTime" label="评估时间" width="170"/>
</el-table>
</el-tab-pane>
<!-- 导管评估 -->
<el-tab-pane label="导管评估" name="TUBE">
<div style="margin-bottom:12px">
<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="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'?'中危':'低危' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="assessorName" label="评估人" width="80"/>
<el-table-column prop="assessmentTime" label="评估时间" width="170"/>
</el-table>
</el-tab-pane>
<!-- 干预措施 -->
<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="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>
</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>
</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>
<!-- Braden量表 -->
<template v-if="currentTool==='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>
</template>
<!-- Morse量表 -->
<template v-if="currentTool==='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>
</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>
<template #footer>
<el-button @click="assessDialogVisible=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 {ElMessage} from 'element-plus'
import {getAssessmentPage,bradenAssess,morseAssess,nrs2002Assess,painAssess,tubeAssess,getInterventionPage,executeIntervention,getStats} from './assessmentApi'
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 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 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()
}
const execIntervention=async(id)=>{await executeIntervention(id);ElMessage.success('已执行');loadData();loadStats()}
onMounted(()=>{loadData();loadStats()})
</script>

View File

@@ -0,0 +1,11 @@
import request from '@/utils/request'
export function getAssessmentPage(p){return request({url:'/nursing-assessment-enhanced/page',method:'get',params:p})}
export function getAssessmentList(p){return request({url:'/nursing-assessment-enhanced/list',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(p){return request({url:'/nursing-assessment-enhanced/stats',method:'get',params:p})}