feat(anesthesia): ASA麻醉评估

This commit is contained in:
2026-06-17 12:35:45 +08:00
parent 0ec77ab89c
commit f444584908
10 changed files with 519 additions and 0 deletions

View File

@@ -1,5 +1,6 @@
package com.healthlink.his.web.anesthesia.appservice;
import com.healthlink.his.anesthesia.domain.AnesAsaAssessment;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
@@ -31,4 +32,8 @@ public interface IAnesthesiaAppService {
AnesthesiaIoSummaryDto getIoSummary(Long recordId);
void completeRecord(Long recordId);
AnesAsaAssessment saveAsaAssessment(AnesAsaAssessment assessment);
List<AnesAsaAssessment> getAsaAssessments(Long recordId);
}

View File

@@ -1,5 +1,6 @@
package com.healthlink.his.web.anesthesia.appservice.impl;
import com.healthlink.his.anesthesia.domain.AnesAsaAssessment;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
@@ -7,6 +8,7 @@ import com.healthlink.his.anesthesia.domain.AnesthesiaRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaVitalSign;
import com.healthlink.his.anesthesia.dto.AnesthesiaIoSummaryDto;
import com.healthlink.his.anesthesia.dto.AnesthesiaRecordDetailDto;
import com.healthlink.his.anesthesia.service.IAnesAsaAssessmentService;
import com.healthlink.his.anesthesia.service.IAnesthesiaFollowupService;
import com.healthlink.his.anesthesia.service.IAnesthesiaIoRecordService;
import com.healthlink.his.anesthesia.service.IAnesthesiaMedicationService;
@@ -39,6 +41,9 @@ public class AnesthesiaAppServiceImpl implements IAnesthesiaAppService {
@Resource
private IAnesthesiaFollowupService anesthesiaFollowupService;
@Resource
private IAnesAsaAssessmentService anesAsaAssessmentService;
@Override
@Transactional
public AnesthesiaRecord createRecord(AnesthesiaRecord record) {
@@ -125,4 +130,20 @@ public class AnesthesiaAppServiceImpl implements IAnesthesiaAppService {
record.setEndTime(new Date());
anesthesiaRecordService.updateById(record);
}
@Override
@Transactional
public AnesAsaAssessment saveAsaAssessment(AnesAsaAssessment assessment) {
if (assessment.getId() != null) {
anesAsaAssessmentService.updateById(assessment);
} else {
anesAsaAssessmentService.save(assessment);
}
return assessment;
}
@Override
public List<AnesAsaAssessment> getAsaAssessments(Long recordId) {
return anesAsaAssessmentService.selectByRecordId(recordId);
}
}

View File

@@ -1,6 +1,7 @@
package com.healthlink.his.web.anesthesia.controller;
import com.core.common.core.domain.R;
import com.healthlink.his.anesthesia.domain.AnesAsaAssessment;
import com.healthlink.his.anesthesia.domain.AnesthesiaFollowup;
import com.healthlink.his.anesthesia.domain.AnesthesiaIoRecord;
import com.healthlink.his.anesthesia.domain.AnesthesiaMedication;
@@ -17,6 +18,7 @@ import com.healthlink.his.web.anesthesia.appservice.IAnesthesiaAppService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@@ -134,4 +136,18 @@ public class AnesthesiaController {
anesthesiaAppService.completeRecord(id);
return R.ok();
}
@PostMapping("/asa-assessment")
@Operation(summary = "保存ASA评估")
@PreAuthorize("@ss.hasPermi('inpatient:anesthesia:edit')")
public R<AnesAsaAssessment> saveAsaAssessment(@RequestBody AnesAsaAssessment assessment) {
return R.ok(anesthesiaAppService.saveAsaAssessment(assessment));
}
@GetMapping("/asa-assessment/{recordId}")
@Operation(summary = "查询ASA评估列表")
@PreAuthorize("@ss.hasPermi('inpatient:anesthesia:list')")
public R<List<AnesAsaAssessment>> getAsaAssessments(@PathVariable Long recordId) {
return R.ok(anesthesiaAppService.getAsaAssessments(recordId));
}
}

View File

@@ -0,0 +1,26 @@
CREATE TABLE anes_asa_assessment (
id BIGSERIAL PRIMARY KEY,
record_id BIGINT NOT NULL,
encounter_id BIGINT NOT NULL,
patient_id BIGINT NOT NULL,
asa_grade VARCHAR(5) NOT NULL,
assessment_time TIMESTAMP NOT NULL,
assessor_id BIGINT NOT NULL,
assessor_name VARCHAR(50),
airway_assessment TEXT,
mallampati_grade VARCHAR(5),
neck_mobility VARCHAR(50),
mouth_opening_cm DECIMAL(4,1),
weight_kg DECIMAL(6,2),
height_cm DECIMAL(5,1),
bmi DECIMAL(5,1),
npo_hours INTEGER,
asa_factors TEXT,
risk_level VARCHAR(20),
tenant_id BIGINT DEFAULT 0,
delete_flag CHAR(1) DEFAULT '0',
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
create_by VARCHAR(64),
update_time TIMESTAMP,
update_by VARCHAR(64)
);

View File

@@ -0,0 +1,58 @@
package com.healthlink.his.anesthesia.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.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.util.Date;
@Data
@TableName("anes_asa_assessment")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
public class AnesAsaAssessment extends HisBaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long recordId;
private Long encounterId;
private Long patientId;
private String asaGrade;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date assessmentTime;
private Long assessorId;
private String assessorName;
private String airwayAssessment;
private String mallampatiGrade;
private String neckMobility;
private BigDecimal mouthOpeningCm;
private BigDecimal weightKg;
private BigDecimal heightCm;
private BigDecimal bmi;
private Integer npoHours;
private String asaFactors;
private String riskLevel;
}

View File

@@ -0,0 +1,14 @@
package com.healthlink.his.anesthesia.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.healthlink.his.anesthesia.domain.AnesAsaAssessment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface AnesAsaAssessmentMapper extends BaseMapper<AnesAsaAssessment> {
List<AnesAsaAssessment> selectByRecordId(@Param("recordId") Long recordId);
}

View File

@@ -0,0 +1,11 @@
package com.healthlink.his.anesthesia.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.healthlink.his.anesthesia.domain.AnesAsaAssessment;
import java.util.List;
public interface IAnesAsaAssessmentService extends IService<AnesAsaAssessment> {
List<AnesAsaAssessment> selectByRecordId(Long recordId);
}

View File

@@ -0,0 +1,20 @@
package com.healthlink.his.anesthesia.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.healthlink.his.anesthesia.domain.AnesAsaAssessment;
import com.healthlink.his.anesthesia.mapper.AnesAsaAssessmentMapper;
import com.healthlink.his.anesthesia.service.IAnesAsaAssessmentService;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class AnesAsaAssessmentServiceImpl
extends ServiceImpl<AnesAsaAssessmentMapper, AnesAsaAssessment>
implements IAnesAsaAssessmentService {
@Override
public List<AnesAsaAssessment> selectByRecordId(Long recordId) {
return baseMapper.selectByRecordId(recordId);
}
}

View File

@@ -69,3 +69,11 @@ export function getFollowup(recordId) {
export function addFollowup(data) {
return request({ url: '/api/v1/anesthesia/followup', method: 'post', data })
}
export function saveAsaAssessment(data) {
return request({ url: '/api/v1/anesthesia/asa-assessment', method: 'post', data })
}
export function getAsaAssessments(recordId) {
return request({ url: '/api/v1/anesthesia/asa-assessment/' + recordId, method: 'get' })
}

View File

@@ -0,0 +1,340 @@
<template>
<div class="asa-assessment-container">
<el-card v-loading="loading">
<template #header>
<div class="card-header">
<span>ASA麻醉评估</span>
<el-button type="primary" size="small" @click="handleNew">新建评估</el-button>
</div>
</template>
<el-table :data="tableData" border style="width: 100%">
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="asaGrade" label="ASA分级" width="100" align="center">
<template #default="scope">
<el-tag :type="getGradeType(scope.row.asaGrade)">{{ scope.row.asaGrade }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="assessmentTime" label="评估时间" width="170" />
<el-table-column prop="assessorName" label="评估医师" width="120" />
<el-table-column prop="mallampatiGrade" label="Mallampati" width="100" align="center" />
<el-table-column prop="weightKg" label="体重(kg)" width="90" align="center" />
<el-table-column prop="heightCm" label="身高(cm)" width="90" align="center" />
<el-table-column prop="bmi" label="BMI" width="80" align="center" />
<el-table-column prop="npoHours" label="禁食(h)" width="90" align="center" />
<el-table-column prop="riskLevel" label="风险等级" width="100" align="center">
<template #default="scope">
<el-tag v-if="scope.row.riskLevel === 'LOW'" type="success">低风险</el-tag>
<el-tag v-else-if="scope.row.riskLevel === 'MEDIUM'" type="warning">中风险</el-tag>
<el-tag v-else-if="scope.row.riskLevel === 'HIGH'" type="danger">高风险</el-tag>
<el-tag v-else-if="scope.row.riskLevel === 'CRITICAL'" type="danger">极高风险</el-tag>
<el-tag v-else type="info">{{ scope.row.riskLevel || '-' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center" fixed="right">
<template #default="scope">
<el-button link type="primary" @click="handleEdit(scope.row)">编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑ASA评估' : '新建ASA评估'" width="800px" destroy-on-close :close-on-click-modal="false">
<el-form ref="formRef" :model="form" :rules="rules" label-width="110px">
<el-divider content-position="left">基本信息</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="ASA分级" prop="asaGrade">
<el-select v-model="form.asaGrade" placeholder="请选择ASA分级" style="width: 100%">
<el-option label="Ⅰ级 - 健康患者" value="" />
<el-option label="Ⅱ级 - 轻度系统性疾病" value="Ⅱ" />
<el-option label="Ⅲ级 - 严重系统性疾病" value="Ⅲ" />
<el-option label="Ⅳ级 - 持续威胁生命的疾病" value="Ⅳ" />
<el-option label="Ⅴ级 - 不手术则无法存活" value="" />
<el-option label="Ⅵ级 - 脑死亡,器官捐献" value="Ⅵ" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="评估时间" prop="assessmentTime">
<el-date-picker v-model="form.assessmentTime" type="datetime" placeholder="选择评估时间" value-format="YYYY-MM-DD HH:mm:ss" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="评估医师ID" prop="assessorId">
<el-input-number v-model="form.assessorId" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="评估医师姓名">
<el-input v-model="form.assessorName" placeholder="请输入评估医师姓名" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="就诊ID" prop="encounterId">
<el-input-number v-model="form.encounterId" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="患者ID" prop="patientId">
<el-input-number v-model="form.patientId" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="麻醉记录ID" prop="recordId">
<el-input-number v-model="form.recordId" :min="1" style="width: 100%" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">气道评估</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="Mallampati分级">
<el-select v-model="form.mallampatiGrade" placeholder="请选择" style="width: 100%">
<el-option label="Ⅰ级 - 可见咽腭弓、软腭、悬雍垂" value="" />
<el-option label="Ⅱ级 - 可见咽腭弓、软腭" value="Ⅱ" />
<el-option label="Ⅲ级 - 只能见软腭" value="Ⅲ" />
<el-option label="Ⅳ级 - 只能见硬腭" value="Ⅳ" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="颈部活动度">
<el-select v-model="form.neckMobility" placeholder="请选择" style="width: 100%">
<el-option label="正常" value="正常" />
<el-option label="轻度受限" value="轻度受限" />
<el-option label="明显受限" value="明显受限" />
<el-option label="完全受限" value="完全受限" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="张口度(cm)">
<el-input-number v-model="form.mouthOpeningCm" :min="0" :max="10" :step="0.1" :precision="1" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="气道评估描述">
<el-input v-model="form.airwayAssessment" type="textarea" :rows="2" placeholder="请输入气道评估描述" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">体格信息</el-divider>
<el-row :gutter="20">
<el-col :span="8">
<el-form-item label="体重(kg)">
<el-input-number v-model="form.weightKg" :min="0" :max="300" :step="0.1" :precision="2" style="width: 100%" @change="calcBmi" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="身高(cm)">
<el-input-number v-model="form.heightCm" :min="0" :max="300" :step="0.1" :precision="1" style="width: 100%" @change="calcBmi" />
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="BMI">
<el-input v-model="form.bmi" disabled placeholder="自动计算" />
</el-form-item>
</el-col>
</el-row>
<el-divider content-position="left">禁食与风险</el-divider>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="禁食时间(h)">
<el-input-number v-model="form.npoHours" :min="0" :max="72" style="width: 100%" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="风险等级">
<el-select v-model="form.riskLevel" placeholder="请选择" style="width: 100%">
<el-option label="低风险" value="LOW" />
<el-option label="中风险" value="MEDIUM" />
<el-option label="高风险" value="HIGH" />
<el-option label="极高风险" value="CRITICAL" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="风险因素">
<el-checkbox-group v-model="selectedFactors">
<el-checkbox label="高血压" value="高血压" />
<el-checkbox label="糖尿病" value="糖尿病" />
<el-checkbox label="冠心病" value="冠心病" />
<el-checkbox label="COPD" value="COPD" />
<el-checkbox label="哮喘" value="哮喘" />
<el-checkbox label="肝硬化" value="肝硬化" />
<el-checkbox label="肾功能不全" value="肾功能不全" />
<el-checkbox label="凝血障碍" value="凝血障碍" />
<el-checkbox label="肥胖" value="肥胖" />
<el-checkbox label="困难气道" value="困难气道" />
<el-checkbox label="过敏史" value="过敏史" />
<el-checkbox label="吸烟" value="吸烟" />
</el-checkbox-group>
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="其他评估">
<el-input v-model="form.asaFactors" type="textarea" :rows="2" placeholder="请输入其他风险因素或补充说明" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" :loading="submitLoading" @click="handleSave">保存</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { saveAsaAssessment, getAsaAssessments } from '@/api/anesthesia'
const loading = ref(false)
const submitLoading = ref(false)
const tableData = ref([])
const dialogVisible = ref(false)
const isEdit = ref(false)
const formRef = ref(null)
const selectedFactors = ref([])
const form = reactive({
id: null,
recordId: null,
encounterId: null,
patientId: null,
asaGrade: '',
assessmentTime: '',
assessorId: null,
assessorName: '',
airwayAssessment: '',
mallampatiGrade: '',
neckMobility: '',
mouthOpeningCm: null,
weightKg: null,
heightCm: null,
bmi: null,
npoHours: null,
asaFactors: '',
riskLevel: ''
})
const rules = {
asaGrade: [{ required: true, message: '请选择ASA分级', trigger: 'change' }],
assessmentTime: [{ required: true, message: '请选择评估时间', trigger: 'change' }],
assessorId: [{ required: true, message: '请输入评估医师ID', trigger: 'blur' }],
encounterId: [{ required: true, message: '请输入就诊ID', trigger: 'blur' }],
patientId: [{ required: true, message: '请输入患者ID', trigger: 'blur' }],
recordId: [{ required: true, message: '请输入麻醉记录ID', trigger: 'blur' }]
}
function getGradeType(grade) {
const map = { '': 'success', 'Ⅱ': 'success', 'Ⅲ': 'warning', 'Ⅳ': 'danger', '': 'danger', 'Ⅵ': 'danger' }
return map[grade] || 'info'
}
function calcBmi() {
if (form.weightKg && form.heightCm && form.heightCm > 0) {
const heightM = form.heightCm / 100
form.bmi = Math.round((form.weightKg / (heightM * heightM)) * 10) / 10
} else {
form.bmi = null
}
}
function resetForm() {
form.id = null
form.recordId = null
form.encounterId = null
form.patientId = null
form.asaGrade = ''
form.assessmentTime = ''
form.assessorId = null
form.assessorName = ''
form.airwayAssessment = ''
form.mallampatiGrade = ''
form.neckMobility = ''
form.mouthOpeningCm = null
form.weightKg = null
form.heightCm = null
form.bmi = null
form.npoHours = null
form.asaFactors = ''
form.riskLevel = ''
selectedFactors.value = []
}
function handleNew() {
resetForm()
isEdit.value = false
dialogVisible.value = true
}
function handleEdit(row) {
Object.assign(form, row)
if (row.asaFactors) {
try {
selectedFactors.value = row.asaFactors.split(',').filter(Boolean)
} catch {
selectedFactors.value = []
}
} else {
selectedFactors.value = []
}
isEdit.value = true
dialogVisible.value = true
}
function handleSave() {
formRef.value?.validate(valid => {
if (!valid) return
submitLoading.value = true
const data = { ...form }
data.asaFactors = selectedFactors.value.join(',')
saveAsaAssessment(data).then(res => {
if (res.code === 200) {
ElMessage.success('保存成功')
dialogVisible.value = false
loadData()
} else {
ElMessage.error(res.msg || '保存失败')
}
}).catch(() => {
ElMessage.error('保存失败')
}).finally(() => {
submitLoading.value = false
})
})
}
function loadData() {
loading.value = true
getAsaAssessments(1).then(res => {
tableData.value = res.data || []
}).catch(() => {
ElMessage.error('查询失败')
}).finally(() => {
loading.value = false
})
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.asa-assessment-container {
padding: 12px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
</style>