Files
his/openhis-ui-vue3/src/template/inHospitalSurgicalRecord.vue
zhaoyun 11618e3d6c fix(#591): 请修复 Bug #591:【住院医生站-临床医嘱】长期医嘱点击停嘱未弹出时间录入弹窗
根因:
- Bug #请修复 Bug #591 存在的问题

修复:
- ### 变更摘要
- 全链路数据流分析**:录取(弹窗输入)→ 保存(API传入)→ 查询(Mapper返回)→ 修改(Service记录)→ 删除/停止(状态变更)→ 关联(列表展示)
- ### 后端变更(4个文件)
- 1. `AdviceBatchOpParam.java`** — 停嘱参数添加 `stopTime` 字段
- 新增 `@JsonFormat Date stopTime`,支持前端传入停嘱时间
- 2. `RequestBaseDto.java`** — 查询DTO添加 `stopUserName`、`stopTime` 字段
- 新增 `String stopUserName`(停嘱医生姓名)
- 新增 `Date stopTime`(停嘱时间)
- 3. `AdviceManageAppServiceImpl.java`** — 停嘱Service增强
- 优先使用前端传入的 `stopTime`,兜底用当前时间
- 通过 `SecurityUtils.getNickName()` 获取当前操作用户昵称,记录到 `updateBy`
- 药品和诊疗两个更新入口均已同步修改
- 4. `AdviceManageAppMapper.xml`** — 三个UNION ALL子查询添加字段
- 药品子查询:`T1.effective_dose_end AS stop_time` + `T1.update_by AS stop_user_name`
- 耗材子查询:`NULL AS stop_time` + `'' AS stop_user_name`
- 诊疗子查询:`T1.occurrence_end_time AS stop_time` + `T1.update_by AS stop_user_name`
- ### 前端变更(1个文件)
- `order/index.vue`**:
- 1. **停嘱时间弹窗** — 点击「停嘱」后弹出 `el-dialog`,内含 `el-date-picker`(datetime类型,默认当前时间),确定后才调用API
- 2. **表格列** — 在「皮试」列后面、「诊断」列前面新增两列:
- 「停嘱医生」`prop="stopUserName"`,宽度120px
- 「停嘱时间」`prop="stopTime"`,宽度170px
- 3. **`handleStopAdvice`** — 保留原有校验(未保存/未签发/已停止检查),校验通过后弹出时间选择弹窗而非直接调API
- 4. **`confirmStopAdvice`** — 新增确认函数,将 `stopTime` 拼入请求参数后调用 `stopAdvice` API
- ### 验证结果
-  前端 Lint 检查通过(仅1个预存的 `vue/no-dupe-keys` 警告)
-  后端 Maven 编译通过(BUILD SUCCESS)
2026-05-29 00:39:26 +08:00

851 lines
22 KiB
Vue
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="medical-document">
<!-- 标题区域 -->
<div class="doc-header">
<h1 class="doc-title">
{{ hospitalName }} 住院手术记录单
</h1>
<div class="doc-subtitle">
住院号: {{ formData.busNo || '待填写' }}
</div>
</div>
<!-- 内容区域 -->
<el-form
ref="formRef"
:model="formData"
:rules="rules"
label-width="120px"
label-align="left"
class="doc-content"
style="height: 60vh; overflow: scroll"
>
<!-- 患者与手术基础信息 -->
<section class="doc-section">
<h2 class="section-title">
患者与手术基本信息
</h2>
<div class="adaptive-grid">
<el-form-item
label="患者姓名"
prop="patientName"
class="grid-item required"
>
<el-input
v-model="formData.patientName"
placeholder="请输入患者姓名"
clearable
/>
</el-form-item>
<el-form-item
label="性别"
prop="gender"
class="grid-item required"
>
<el-select
v-model="formData.gender"
placeholder="请选择性别"
>
<el-option
label="男"
value="男"
/>
<el-option
label="女"
value="女"
/>
</el-select>
</el-form-item>
<el-form-item
label="年龄"
prop="age"
class="grid-item required"
>
<div class="input-with-unit">
<el-input
v-model.number="formData.age"
placeholder="请输入年龄"
/>
<span class="unit"></span>
</div>
</el-form-item>
<el-form-item
label="科室"
prop="department"
class="grid-item required"
>
<el-input
v-model="formData.department"
placeholder="如:普外科"
clearable
/>
</el-form-item>
<el-form-item
label="病房/床号"
prop="bedNo"
class="grid-item required"
>
<el-input
v-model="formData.bedNo"
placeholder="如502-03"
clearable
/>
</el-form-item>
<el-form-item
label="手术日期/时间"
prop="operationDateTime"
class="grid-item required"
>
<el-date-picker
v-model="formData.operationDateTime"
type="datetime"
placeholder="选择手术日期时间"
value-format="YYYY-MM-DD HH:mm"
/>
</el-form-item>
</div>
</section>
<!-- 手术团队信息 -->
<section class="doc-section">
<h2 class="section-title">
手术团队信息
</h2>
<div class="adaptive-grid">
<el-form-item
label="手术者"
prop="surgeon"
class="grid-item required"
>
<el-input
v-model="formData.surgeon"
placeholder="主刀医师姓名"
clearable
/>
</el-form-item>
<el-form-item
label="第一助手"
prop="firstAssistant"
class="grid-item required"
>
<el-input
v-model="formData.firstAssistant"
placeholder="第一助手姓名"
clearable
/>
</el-form-item>
<el-form-item
label="第二助手"
prop="secondAssistant"
class="grid-item"
>
<el-input
v-model="formData.secondAssistant"
placeholder="第二助手姓名"
clearable
/>
</el-form-item>
<el-form-item
label="麻醉医师"
prop="anesthesiologist"
class="grid-item required"
>
<el-input
v-model="formData.anesthesiologist"
placeholder="麻醉医师姓名"
clearable
/>
</el-form-item>
<el-form-item
label="巡回护士"
prop="circulatingNurse"
class="grid-item required"
>
<el-input
v-model="formData.circulatingNurse"
placeholder="巡回护士姓名"
clearable
/>
</el-form-item>
<el-form-item
label="器械护士"
prop="scrubNurse"
class="grid-item required"
>
<el-input
v-model="formData.scrubNurse"
placeholder="器械护士姓名"
clearable
/>
</el-form-item>
</div>
</section>
<!-- 手术详情 -->
<section class="doc-section">
<h2 class="section-title">
手术详情
</h2>
<el-form-item
label="手术名称"
prop="operationName"
class="full-width-item required"
>
<el-input
v-model="formData.operationName"
placeholder="规范手术名称(如:腹腔镜下胆囊切除术)"
clearable
/>
</el-form-item>
<el-form-item
label="手术方式"
prop="operationMethod"
class="full-width-item required"
>
<el-select
v-model="formData.operationMethod"
placeholder="选择手术方式"
>
<el-option
label="开放手术"
value="开放手术"
/>
<el-option
label="微创手术"
value="微创手术"
/>
<el-option
label="介入手术"
value="介入手术"
/>
</el-select>
</el-form-item>
<el-form-item
label="手术入路"
prop="surgicalApproach"
class="full-width-item required"
>
<el-input
v-model="formData.surgicalApproach"
placeholder="如:右上腹经腹直肌切口"
clearable
/>
</el-form-item>
<el-form-item
label="术中发现"
prop="intraoperativeFindings"
class="full-width-item required"
>
<el-input
v-model="formData.intraoperativeFindings"
type="textarea"
placeholder="详细描述术中所见器官、病变情况"
autosize
maxlength="1000"
show-word-limit
/>
</el-form-item>
<el-form-item
label="手术过程"
prop="operationProcess"
class="full-width-item required"
>
<el-input
v-model="formData.operationProcess"
type="textarea"
placeholder="按操作顺序描述手术步骤(如:游离胆囊三角→结扎胆囊管→切除胆囊..."
autosize
maxlength="1500"
show-word-limit
/>
</el-form-item>
</section>
<!-- 术后情况 -->
<section class="doc-section">
<h2 class="section-title">
术后情况
</h2>
<div class="adaptive-grid">
<el-form-item
label="术中出血量"
prop="bloodLoss"
class="grid-item required"
>
<div class="input-with-unit">
<el-input
v-model.number="formData.bloodLoss"
type="number"
placeholder="请输入出血量"
/>
<span class="unit">ml</span>
</div>
</el-form-item>
<el-form-item
label="输血情况"
prop="bloodTransfusion"
class="grid-item"
>
<el-select
v-model="formData.bloodTransfusion"
placeholder="是否输血"
>
<el-option
label="是"
value="是"
/>
<el-option
label="否"
value="否"
/>
</el-select>
</el-form-item>
<el-form-item
label="引流管放置"
prop="drainageTube"
class="grid-item"
>
<el-input
v-model="formData.drainageTube"
placeholder="如腹腔引流管1根"
clearable
/>
</el-form-item>
<el-form-item
label="标本处理"
prop="specimenDisposal"
class="grid-item required"
>
<el-input
v-model="formData.specimenDisposal"
placeholder="如:胆囊标本送病理检查"
clearable
/>
</el-form-item>
<el-form-item
label="手术结束时间"
prop="operationEndTime"
class="grid-item required"
>
<el-date-picker
v-model="formData.operationEndTime"
type="datetime"
placeholder="选择手术结束时间"
value-format="YYYY-MM-DD HH:mm"
/>
</el-form-item>
<el-form-item
label="患者去向"
prop="patientDestination"
class="grid-item required"
>
<el-select
v-model="formData.patientDestination"
placeholder="选择去向"
>
<el-option
label="ICU"
value="ICU"
/>
<el-option
label="普通病房"
value="普通病房"
/>
</el-select>
</el-form-item>
</div>
</section>
<!-- 签署区域 -->
<section class="doc-section">
<h2 class="section-title">
签署确认
</h2>
<div
class="adaptive-grid signature-area"
style="grid-template-columns: repeat(auto-fit, minmax(240px, 1fr))"
>
<el-form-item
label="手术者签名"
prop="surgeonSignature"
class="grid-item required"
>
<el-input
v-model="formData.surgeonSignature"
placeholder="主刀医师签字"
clearable
/>
<div class="signature-tip">
请手术者亲笔签名
</div>
</el-form-item>
<el-form-item
label="记录者签名"
prop="recorderSignature"
class="grid-item required"
>
<el-input
v-model="formData.recorderSignature"
placeholder="记录者签字"
clearable
/>
<div class="signature-tip">
请记录者如第一助手签字
</div>
</el-form-item>
<el-form-item
label="记录日期"
prop="recordDate"
class="grid-item required"
>
<el-date-picker
v-model="formData.recordDate"
type="date"
placeholder="选择记录日期"
value-format="YYYY-MM-DD"
style="width: 100%"
/>
</el-form-item>
</div>
</section>
</el-form>
<!-- 操作按钮 -->
<div class="btn-group">
<el-button
type="primary"
@click="submit"
>
保存记录
</el-button>
<el-button
type="success"
@click="handlePrint"
>
打印记录
</el-button>
<el-button
type="warning"
@click="handleReset"
>
重置表单
</el-button>
</div>
</div>
<intOperRecordSheet
v-if="isShowprintDom"
ref="recordPrintRef"
/>
</template>
<script setup>
import {onMounted, reactive, ref} from 'vue';
import intOperRecordSheet from '../views/hospitalRecord/components/intOperRecordSheet.vue';
import useUserStore from '@/store/modules/user';
// 迁移到 hiprint
import { previewPrint } from '@/utils/printUtils.js';
const userStore = useUserStore();
const isShowprintDom = ref(false);
const recordPrintRef = ref();
// 医院名称
const hospitalName = userStore.hospitalName;
defineOptions({
name: 'IInHospitalSurgicalRecord',
});
// 表单引用
const formRef = ref(null);
// 表单数据
const formData = reactive({
// 患者与手术基础信息
busNo: '',
patientName: '',
gender: '',
age: '',
department: '',
bedNo: '',
operationDateTime: '', // 手术日期时间
// 手术团队信息
surgeon: '', // 主刀医师
firstAssistant: '', // 第一助手
secondAssistant: '', // 第二助手
anesthesiologist: '', // 麻醉医师
circulatingNurse: '', // 巡回护士
scrubNurse: '', // 器械护士
// 手术详情
operationName: '', // 规范手术名称
operationMethod: '', // 手术方式
surgicalApproach: '', // 手术入路
intraoperativeFindings: '', // 术中发现
operationProcess: '', // 手术过程
// 术后情况
bloodLoss: '', // 术中出血量
bloodTransfusion: '', // 输血情况
drainageTube: '', // 引流管放置
specimenDisposal: '', // 标本处理
operationEndTime: '', // 手术结束时间
patientDestination: '', // 患者去向
// 签署信息
surgeonSignature: '', // 手术者签名
recorderSignature: '', // 记录者签名
recordDate: '', // 记录日期
});
// Props与事件
const props = defineProps({
patientInfo: {
type: Object,
required: true,
},
});
const patient = props.patientInfo;
// 表单验证规则
const rules = reactive({
busNo: [{ required: true, message: '请填写住院号', trigger: ['blur', 'submit'] }],
patientName: [{ required: true, message: '请填写患者姓名', trigger: ['blur', 'submit'] }],
gender: [{ required: true, message: '请选择性别', trigger: ['change', 'submit'] }],
age: [
{ required: true, message: '请填写年龄', trigger: ['blur', 'submit'] },
{ type: 'number', min: 0, max: 150, message: '年龄需在0-150之间', trigger: ['blur', 'submit'] },
],
department: [{ required: true, message: '请填写科室', trigger: ['blur', 'submit'] }],
bedNo: [{ required: true, message: '请填写病房/床号', trigger: ['blur', 'submit'] }],
operationDateTime: [
{ required: true, message: '请选择手术日期时间', trigger: ['change', 'submit'] },
],
surgeon: [{ required: true, message: '请填写手术者姓名', trigger: ['blur', 'submit'] }],
firstAssistant: [{ required: true, message: '请填写第一助手姓名', trigger: ['blur', 'submit'] }],
anesthesiologist: [
{ required: true, message: '请填写麻醉医师姓名', trigger: ['blur', 'submit'] },
],
circulatingNurse: [
{ required: true, message: '请填写巡回护士姓名', trigger: ['blur', 'submit'] },
],
scrubNurse: [{ required: true, message: '请填写器械护士姓名', trigger: ['blur', 'submit'] }],
operationName: [{ required: true, message: '请填写手术名称', trigger: ['blur', 'submit'] }],
operationMethod: [{ required: true, message: '请选择手术方式', trigger: ['change', 'submit'] }],
surgicalApproach: [{ required: true, message: '请填写手术入路', trigger: ['blur', 'submit'] }],
intraoperativeFindings: [
{ required: true, message: '请描述术中发现', trigger: ['blur', 'submit'] },
],
operationProcess: [{ required: true, message: '请描述手术过程', trigger: ['blur', 'submit'] }],
bloodLoss: [
{ required: true, message: '请填写术中出血量', trigger: ['blur', 'submit'] },
{ type: 'number', min: 0, message: '出血量不能为负数', trigger: ['blur', 'submit'] },
],
specimenDisposal: [
{ required: true, message: '请填写标本处理方式', trigger: ['blur', 'submit'] },
],
operationEndTime: [
{ required: true, message: '请选择手术结束时间', trigger: ['change', 'submit'] },
],
patientDestination: [
{ required: true, message: '请选择患者去向', trigger: ['change', 'submit'] },
],
surgeonSignature: [{ required: true, message: '请手术者签名', trigger: ['blur', 'submit'] }],
recorderSignature: [{ required: true, message: '请记录者签名', trigger: ['blur', 'submit'] }],
recordDate: [{ required: true, message: '请选择记录日期', trigger: ['change', 'submit'] }],
});
// 生命周期
onMounted(() => {
// 初始化日期为当前日期时间
const today = new Date();
formData.operationDateTime = formatDateTime(today);
formData.operationEndTime = formatDateTime(today);
formData.recordDate = formatDate(today);
if (!formData.patientName) {
formData.patientName = patient?.patientName || '';
}
if (!formData.gender) {
formData.gender = patient?.genderEnum_enumText || '';
}
if (!formData.age) {
formData.age = patient?.age || '';
}
if (!formData.department) {
formData.department = patient?.inHospitalOrgName || '';
}
if (!formData.bedNo) {
formData.bedNo = patient?.houseName + '-' + patient?.bedName;
}
if (!formData.busNo) {
formData.busNo = patient?.busNo || '';
}
});
const emits = defineEmits(['submitOk']);
// 提交表单
const submit = () => {
formRef.value.validate((valid) => {
if (valid) {
ElMessage.success('手术记录保存成功');
console.log('手术记录数据:', formData);
emits('submitOk', formData);
}
});
};
// 表单数据赋值
const setFormData = (data) => {
if (data) {
Object.assign(formData, data);
if (!formData.busNo) {
formData.busNo = patient?.busNo || '';
}
}
};
// 打印功能 - 使用 hiprint
const handlePrint = () => {
formRef.value.validate((valid) => {
if (valid) {
const printDom = document.querySelector('.form-container');
if (printDom) {
previewPrint(printDom);
} else {
window.print();
}
} else {
ElMessageBox.warning('请先完善表单信息再打印');
}
});
};
// 重置表单
const handleReset = () => {
ElMessageBox.confirm('确定要重置表单吗?所有已填写内容将被清空', '确认重置', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}).then(() => {
formRef.value.resetFields();
const today = new Date();
formData.operationDateTime = formatDateTime(today);
formData.operationEndTime = formatDateTime(today);
formData.recordDate = formatDate(today);
ElMessage.success('表单已重置');
});
};
// 日期格式化工具
const formatDate = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
const formatDateTime = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}`;
};
const printFun = () => {
console.log('入院记录打印');
isShowprintDom.value = true;
nextTick(() => {
recordPrintRef?.value.setData(formData);
nextTick(() => {
previewPrint(recordPrintRef?.value.getDom());
isShowprintDom.value = false;
});
});
};
defineExpose({ submit, setFormData, printFun });
</script>
<style scoped>
/* 核心容器PC端限制合理最大宽度避免超宽屏内容过散 */
.medical-document {
max-width: 1440px; /* PC端最优宽度兼顾大屏和常规屏 */
width: 98%; /* 占满父容器98%,保留少量边距 */
margin: 20px auto;
padding: 30px;
background: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
font-family: 'SimSun', '宋体', serif;
box-sizing: border-box; /* 确保内边距不撑大容器 */
}
.doc-header {
text-align: center;
margin-bottom: 30px;
}
.doc-title {
font-size: 22px;
margin: 0 0 10px;
font-weight: bold;
}
.doc-subtitle {
font-size: 16px;
color: #666;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #333;
}
.doc-content {
width: 100%;
}
.doc-section {
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px dashed #ccc;
}
.section-title {
font-size: 18px;
margin: 0 0 15px;
color: #333;
font-weight: bold;
}
/* 自适应网格PC端优先展示多列优化列宽比例 */
.adaptive-grid {
display: grid;
/* PC端最小列宽220px保证每列内容不拥挤自动适配列数 */
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 15px 20px;
margin-bottom: 15px;
width: 100%;
}
.grid-item {
margin-bottom: 0;
display: flex;
flex-direction: column;
}
.grid-item .el-form-item__content {
flex: 1;
min-width: 0;
}
.full-width-item {
width: 100%;
margin-bottom: 15px;
}
.input-with-unit {
display: flex;
align-items: center;
gap: 8px;
}
.unit {
white-space: nowrap;
color: #666;
}
.signature-area {
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.signature-tip {
font-size: 12px;
color: #f56c6c;
margin-top: 4px;
}
.btn-group {
display: flex;
justify-content: center;
gap: 15px;
margin-top: 30px;
padding-top: 20px;
border-top: 2px solid #333;
}
.required .el-form-item__label::before {
content: '*';
color: #ff4d4f;
margin-right: 4px;
}
/* 仅针对小屏设备做基础适配优先保证PC端体验 */
@media (max-width: 768px) {
.medical-document {
max-width: 100%;
padding: 15px;
}
.adaptive-grid {
grid-template-columns: 1fr; /* 移动端强制单列 */
}
.doc-title {
font-size: 18px;
}
.section-title {
font-size: 16px;
}
}
/* 超宽屏≥1920px优化适度增大间距提升视觉体验 */
@media (min-width: 1920px) {
.medical-document {
max-width: 1600px;
padding: 40px;
}
.adaptive-grid {
gap: 20px 25px;
}
}
/* 打印样式保留 */
@media print {
.btn-group {
display: none;
}
.medical-document {
box-shadow: none;
margin: 0;
padding: 0;
max-width: 100%;
}
.el-input__inner,
.el-select__input,
.el-textarea__inner {
border: none !important;
box-shadow: none !important;
background: transparent !important;
}
.el-form-item__label {
font-weight: bold !important;
}
}
</style>