Files
his/openhis-ui-vue3/src/views/surgicalschedule/temporaryMedical.vue
刘备 0f52327b8b Fix Bug #446: 【手术管理-门诊手术安排】临时医嘱生成后界面非法关闭且按钮名称/功能显示不一致
移除签名成功后自动关闭弹窗的 setTimeout,改为保留弹窗让用户查看已签发的医嘱状态。
新增 isSignedProp 传递给子组件,使重新打开弹窗时按钮名称保持为"提交医嘱"而非重置为"一键签名并生成医嘱"。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-09 20:18:41 +08:00

1196 lines
40 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="temporary-medical-container">
<!-- 顶部区域标题 + 患者信息 + 操作按钮 -->
<div class="top-container">
<!-- 标题栏 -->
<div class="title-bar">
<h1 class="title-text">门诊术中临时医嘱</h1>
</div>
<!-- 患者信息卡 -->
<div class="patient-info-card">
<div class="info-item">
<span class="info-label">患者</span>
<span class="info-value">{{ patientInfo.patientName || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">就诊卡号</span>
<span class="info-value">{{ patientInfo.identifierNo || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">手术单号</span>
<span class="info-value">{{ patientInfo.operCode || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">科室</span>
<span class="info-value">{{ patientInfo.roomCode || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">医师</span>
<span class="info-value">{{ patientInfo.doctorName || '-' }}</span>
</div>
<div class="info-item">
<span class="info-label">角色</span>
<span class="info-value">{{ patientInfo.role || '-' }}</span>
</div>
</div>
<!-- 操作按钮组 -->
<div class="action-buttons">
<el-button class="refresh-btn" @click="handleRefresh">
<span></span> 刷新
</el-button>
<el-button class="quote-btn" @click="handleQuoteBilling">
引用计费
</el-button>
</div>
</div>
<!-- 已引用计费药品区域 -->
<div class="medicine-section">
<div class="section-title">
已引用计费药品待生成医嘱
</div>
<el-table
:data="billingMedicines"
stripe
border
style="width: 100%;"
fit
>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="药品名称" prop="medicineName" min-width="150" show-overflow-tooltip />
<el-table-column label="规格" prop="specification" min-width="100" align="center" />
<el-table-column label="数量" prop="quantity" width="80" align="center">
<template #default="{ row }">
{{ row.quantity || 0 }}
</template>
</el-table-column>
<el-table-column label="批号" prop="batchNumber" min-width="120" align="center" />
<el-table-column label="单价" prop="unitPrice" min-width="80" align="center">
<template #default="{ row }">
{{ row.unitPrice ? `${row.unitPrice}` : '-' }}
</template>
</el-table-column>
<el-table-column label="小计" prop="subtotal" min-width="100" align="center">
<template #default="{ row }">
{{ row.subtotal ? `${(row.unitPrice * row.quantity).toFixed(2)}` : '-' }}
</template>
</el-table-column>
<el-table-column label="医保" prop="insuranceType" width="80" align="center">
<template #default="{ row }">
<el-tag
:size="'small'"
:class="getInsuranceClass(row.insuranceType)"
>
{{ row.insuranceType || '-' }}
</el-tag>
</template>
</el-table-column>
</el-table>
<div class="medicine-summary">
<div class="summary-item insurance">医保内{{ insuranceAmount.toFixed(2) }}</div>
<div class="summary-item self-pay">自费{{ selfPayAmount.toFixed(2) }}</div>
<div class="summary-item total">总计{{ totalAmount.toFixed(2) }}</div>
</div>
</div>
<!-- 临时医嘱预览区域 -->
<div class="advice-section">
<div class="section-title">
临时医嘱预览已生成
</div>
<el-table
:data="displayAdvices"
stripe
border
style="width: 100%;"
fit
>
<el-table-column label="序号" type="index" width="60" align="center" />
<el-table-column label="医嘱名称" prop="adviceName" min-width="150" show-overflow-tooltip />
<el-table-column label="剂量" prop="dosage" min-width="80" align="center" />
<el-table-column label="单位" prop="unit" min-width="80" align="center" />
<el-table-column label="用法" prop="usage" min-width="100" show-overflow-tooltip>
<template #default="{ row }">
{{ row.usageLabel || getUsageLabel(row.usage) }}
</template>
</el-table-column>
<el-table-column label="频次" prop="frequency" min-width="80" align="center" />
<el-table-column label="执行时间" prop="executeTime" min-width="160" align="center" />
<el-table-column label="操作" min-width="140" align="center">
<template #default="{ $index }">
<el-button link type="primary" @click="handleEditAdvice($index)">编辑</el-button>
<el-button link type="danger" @click="handleDeleteAdvice($index)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 医师电子签名区域 -->
<div class="signature-section">
<div class="section-title">
医师电子签名
</div>
<!-- 签名信息卡 -->
<div class="signature-card">
<div class="signature-info">
<span class="info-label">签名医师</span>
<span class="info-value" :class="{ 'unsigned': !isSigned }">{{ isSigned ? currentUser.name : '未签名' }}</span>
</div>
<div class="signature-info">
<span class="info-label">签名时间</span>
<span class="info-value">{{ signatureTime || '-' }}</span>
</div>
</div>
<!-- 底部操作按钮 -->
<div class="signature-actions">
<el-button class="cancel-btn" @click="handleCancel">取消</el-button>
<el-button
class="sign-btn"
@click="handleSignAndSubmit"
>
{{ isSigned ? '提交医嘱' : '一键签名并生成医嘱' }}
</el-button>
</div>
</div>
<!-- 签名密码弹窗 -->
<el-dialog v-model="showSignDialog" title="弹窗-签名密码" width="400px" append-to-body>
<div class="sign-dialog-content">
<p>请输入账户密码</p>
<el-input
v-model="signPassword"
type="password"
placeholder="请输入密码"
style="margin-bottom: 16px"
@keyup.enter="confirmSign"
/>
<div class="dialog-actions">
<el-button @click="showSignDialog = false">取消</el-button>
<el-button type="primary" @click="confirmSign">确认</el-button>
</div>
</div>
</el-dialog>
<!-- 编辑医嘱弹窗 -->
<el-dialog v-model="showEditDialog" title="编辑医嘱" width="500px" append-to-body>
<div class="edit-dialog-content">
<el-form :model="editForm" label-width="80px">
<el-form-item label="药品名称">
<el-input v-model="editForm.adviceName" disabled />
</el-form-item>
<el-form-item label="剂量" required>
<el-input v-model.number="editForm.dosage" placeholder="请输入剂量" />
</el-form-item>
<el-form-item label="单位">
<el-input v-model="editForm.unit" placeholder="请输入单位" />
</el-form-item>
<el-form-item label="用法" required>
<el-select
v-if="getMethodCodeDict.length > 0"
v-model="editForm.usage"
placeholder="请选择用法"
style="width: 100%"
filterable
>
<el-option
v-for="dict in getMethodCodeDict"
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select>
<el-input v-else disabled placeholder="加载中..." />
</el-form-item>
<el-form-item label="频次">
<el-input v-model="editForm.frequency" disabled />
</el-form-item>
</el-form>
<div class="dialog-actions">
<el-button @click="handleCancelEdit">取消</el-button>
<el-button type="primary" @click="handleSaveEdit">保存</el-button>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup>
import { ref, computed, getCurrentInstance, onMounted, watch, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import useUserStore from '@/store/modules/user'
import { checkPassword } from '@/api/surgicalschedule'
import { savePrescription } from '@/views/clinicmanagement/bargain/component/api.js'
// 定义props
const props = defineProps({
// 患者和医生信息
patientInfo: {
type: Object,
default: () => ({
patientName: '',
visitId: '',
operCode: '',
roomCode: '',
doctorName: '',
role: '',
patientId: null,
organizationId: null,
})
},
// 一区域:已引用计费药品(待生成医嘱)
billingMedicines: {
type: Array,
default: () => []
},
// 二区域:临时医嘱预览(已生成医嘱)- v-model:temporary-advices 对应 prop 名
temporaryAdvices: {
type: Array,
default: () => []
},
// 签名状态:用于保持按钮名称一致性
isSignedProp: {
type: Boolean,
default: false
}
})
// 定义emit事件
const emit = defineEmits(['submit', 'cancel', 'refresh', 'quote-billing', 'update:temporary-advices'])
// 用户store
const userStore = useUserStore()
// 获取当前实例
const { proxy } = getCurrentInstance()
// 获取字典数据(与门诊医生站保持一致)
const { method_code } = proxy.useDict('method_code')
// 🔧 新增:用法编码映射表(将通用编码映射到后端字典的正确编码)
const usageCodeMapping = {
'iv': '404', // 静脉注射
'po': '1', // 口服
'im': '403', // 肌肉注射
'sc': '401', // 皮下注射
'ivgtt': '405', // 静脉滴注
'ih': '5', // 吸入给药
'it': '610', // 含化
'ip': '604', // 腹腔用药
'top': '6', // 局部用药
'pr': '2' // 直肠给药
}
// 🔧 新增:将用法编码映射到后端字典的正确编码
const mapUsageCode = (advice) => {
const mappedAdvice = { ...advice }
if (usageCodeMapping[advice.usage]) {
mappedAdvice.usage = usageCodeMapping[advice.usage]
}
return mappedAdvice
}
// 🔧 新增:本地备用用法字典,确保字典未加载时也能显示中文
const localMethodCode = [
{ value: '404', label: '静脉注射' },
{ value: '1', label: '口服' },
{ value: '403', label: '肌肉注射' },
{ value: '401', label: '皮下注射' },
{ value: '405', label: '静脉滴注' },
{ value: '5', label: '吸入给药' },
{ value: '610', label: '含化' },
{ value: '604', label: '腹腔用药' },
{ value: '6', label: '局部用药' },
{ value: '2', label: '直肠给药' }
]
// 🔧 新增:获取用法字典(优先使用从后端加载的,否则使用本地备用)
const getMethodCodeDict = computed(() => {
let dict = method_code.value && method_code.value.length > 0 ? method_code.value : localMethodCode
return dict
})
// 响应式数据 - isSigned 从父组件传入的 prop 初始化
const isSigned = ref(props.isSignedProp)
const signatureTime = ref('')
const showSignDialog = ref(false)
const signPassword = ref('')
const showEditDialog = ref(false)
const currentEditIndex = ref(-1)
const editForm = ref({
adviceName: '',
dosage: '',
unit: '',
usage: '',
frequency: '临时'
})
// 计算属性
const currentUser = computed(() => ({
name: userStore.name || '未知用户',
id: userStore.id
}))
const insuranceAmount = computed(() => {
return (props.billingMedicines || [])
.filter(med => med.insuranceType === '甲' || med.insuranceType === '乙' || med.insuranceType === '医保')
.reduce((sum, med) => sum + (Number(med.subtotal) || 0), 0)
})
const selfPayAmount = computed(() => {
return (props.billingMedicines || [])
.filter(med => !med.insuranceType || med.insuranceType === '自费')
.reduce((sum, med) => sum + (Number(med.subtotal) || 0), 0)
})
const totalAmount = computed(() => {
return Number(insuranceAmount.value || 0) + Number(selfPayAmount.value || 0)
})
// 将计费药品转换为临时医嘱数据
const convertedAdvices = computed(() => {
return props.billingMedicines.map((medicine, index) => {
// 解析规格中的数值和单位
const specMatch = medicine.specification ? medicine.specification.match(/(\d+)(\D+)/) : null
const specValue = specMatch ? parseInt(specMatch[1]) : 1
const specUnit = specMatch ? specMatch[2] : 'ml'
// 计算剂量 = 规格数值 × 数量
const dosage = specValue * (medicine.quantity || 1)
// 🔧 修复:根据药品名称判断用法,使用后端字典的正确编码
let usageCode = '404' // 默认静脉注射编码
let usageLabel = '静脉注射' // 默认显示名称
if (medicine.medicineName && medicine.medicineName.includes('注射液')) {
usageCode = '404'
usageLabel = '静脉注射'
} else if (medicine.medicineName && medicine.medicineName.includes('片')) {
usageCode = '1'
usageLabel = '口服'
} else if (medicine.medicineName && medicine.medicineName.includes('胶囊')) {
usageCode = '1'
usageLabel = '口服'
}
return {
id: index + 1,
adviceName: medicine.medicineName || '',
dosage: dosage,
unit: specUnit,
usage: usageCode, // 🔧 修复:使用后端字典的正确编码
usageLabel: usageLabel, // 🔧 新增:保存显示名称
frequency: '临时',
executeTime: new Date().toLocaleString('zh-CN'),
originalMedicine: medicine
}
})
})
// 🔧 修复:使用 ref 而不是 computed避免响应性问题
// 直接引用父组件传入的数据,确保更新能立即反映
const displayAdvices = ref([])
// 初始化 displayAdvices
const initDisplayAdvices = () => {
// 只要有用户修改过的数据,就优先使用用户修改后的数据
// 避免自动转换覆盖用户的修改
if (props.temporaryAdvices && props.temporaryAdvices.length > 0) {
// 🔧 修复:将旧编码映射到新编码
displayAdvices.value = props.temporaryAdvices.map(mapUsageCode)
} else {
// 否则自动转换第一区域的已引用计费药品
displayAdvices.value = convertedAdvices.value
}
}
// 初始化
initDisplayAdvices()
// 组件挂载时,如果没有传入临时医嘱数据,将自动转换后的数据同步到父组件
// 确保修改可以被父组件保存,下次打开仍然能看到
onMounted(() => {
// 初始化 displayAdvices
initDisplayAdvices()
if ((!props.temporaryAdvices || props.temporaryAdvices.length === 0) && convertedAdvices.value.length > 0) {
console.log('=== onMounted 触发 emit ===')
emit('update:temporary-advices', convertedAdvices.value)
}
})
// 🔧 新增:监听 temporary-advices prop 的变化,同步更新 displayAdvices
watch(() => props.temporaryAdvices, (newVal, oldVal) => {
console.log('=== watch props.temporaryAdvices 被调用 ===')
console.log('=== newVal ===', newVal)
// 如果 props 有新数据,更新 displayAdvices
if (newVal && newVal.length > 0) {
// 🔧 修复:将旧编码映射到新编码
displayAdvices.value = newVal.map(mapUsageCode)
}
}, { deep: true, immediate: true })
// 监听第一区域的计费药品变化,如果第一区域更新了,并且用户还没修改过第二区域数据,自动更新第二区域
watch(() => props.billingMedicines, (newVal) => {
console.log('=== watch billingMedicines 被调用 ===')
console.log('=== newVal ===', newVal)
console.log('=== props.temporaryAdvices ===', props.temporaryAdvices)
console.log('=== props.temporaryAdvices.length ===', props.temporaryAdvices?.length)
// 🔧 修复:只有当 temporary-advices 为空时才自动更新,避免覆盖用户的修改
if (!props.temporaryAdvices || props.temporaryAdvices.length === 0) {
if (newVal.length > 0) {
console.log('=== watch 触发 emit ===')
displayAdvices.value = convertedAdvices.value
emit('update:temporary-advices', convertedAdvices.value)
}
} else {
console.log('=== watch 不触发 emit因为 props.temporaryAdvices 已有数据 ===')
}
}, { deep: true })
// 🔧 新增:监听字典加载,确保字典加载后编辑弹窗能正确显示
watch(() => method_code.value, (newVal) => {
console.log('=== watch method_code 被调用 ===')
console.log('字典数据已更新,长度:', newVal?.length || 0)
}, { immediate: true })
// 方法
const handleSign = () => {
showSignDialog.value = true
}
// 编辑医嘱
const handleEditAdvice = (index) => {
const advice = displayAdvices.value[index]
// 🔧 修复:如果用法是旧编码,映射到新编码
let usageCode = advice.usage
if (usageCodeMapping[usageCode]) {
usageCode = usageCodeMapping[usageCode]
}
currentEditIndex.value = index
editForm.value = {
adviceName: advice.adviceName,
dosage: advice.dosage,
unit: advice.unit,
usage: usageCode, // 使用映射后的正确编码
frequency: advice.frequency
}
showEditDialog.value = true
}
// 保存编辑
const handleSaveEdit = () => {
if (!editForm.value.dosage && editForm.value.dosage !== 0) {
ElMessage.warning('请填写剂量')
return
}
// 🔧 新增:验证剂量必须为数字
if (isNaN(editForm.value.dosage) || editForm.value.dosage === '') {
ElMessage.warning('剂量必须为数字')
return
}
if (!editForm.value.usage) {
ElMessage.warning('请填写用法')
return
}
console.log('=== 保存编辑 ===')
console.log('editForm.value.usage:', editForm.value.usage)
console.log('editForm.value.usage 类型:', typeof editForm.value.usage)
// 构建更新后的数据
const updatedAdvice = {
...displayAdvices.value[currentEditIndex.value],
...editForm.value
}
// 🔧 修复:确保 usage 字段保存的是编码,而不是中文名称
// 如果用户选择的是编码,直接使用;如果用户选择的是中文名称,需要转换为编码
if (editForm.value.usage && typeof editForm.value.usage === 'string') {
// 检查是否是编码(通常编码较短,如 'iv', 'po'
const isCode = editForm.value.usage.length <= 10 && getMethodCodeDict.value.some(item => item.value === editForm.value.usage)
if (!isCode) {
// 如果是中文名称,转换为编码
const dictItem = getMethodCodeDict.value.find(item => item.label === editForm.value.usage)
if (dictItem) {
updatedAdvice.usage = dictItem.value
updatedAdvice.usageLabel = dictItem.label
}
} else {
// 如果是编码,从字典中获取对应的标签
const dictItem = getMethodCodeDict.value.find(item => item.value === editForm.value.usage)
if (dictItem) {
updatedAdvice.usageLabel = dictItem.label
}
}
}
// 🔧 新增:同时更新 originalMedicine 中的数据,确保提交时使用最新的用户修改
const originalMedicine = displayAdvices.value[currentEditIndex.value].originalMedicine
if (originalMedicine && originalMedicine.contentJson) {
try {
const contentData = JSON.parse(originalMedicine.contentJson)
// 更新用户修改的字段
contentData.dose = editForm.value.dosage
contentData.doseUnitCode = editForm.value.unit
contentData.methodCode = updatedAdvice.usage
// 如果用户修改了剂量,重新计算数量
if (originalMedicine.specification) {
const specMatch = originalMedicine.specification.match(/(\d+)(\D+)/)
const specValue = specMatch ? parseInt(specMatch[1]) : 1
if (specValue > 0) {
const newQuantity = editForm.value.dosage / specValue
contentData.quantity = newQuantity
contentData.totalPrice = contentData.unitPrice * newQuantity
// 🔧 关键修复:同时更新显示字段,确保表格正确显示修改后的值
updatedAdvice.quantity = newQuantity
}
}
// 更新 contentJson
updatedAdvice.originalMedicine = {
...originalMedicine,
contentJson: JSON.stringify(contentData)
}
} catch (e) {
console.error('解析 originalMedicine.contentJson 失败', e)
}
}
// 构建完整的更新后列表
const updatedAdvices = [...displayAdvices.value]
updatedAdvices[currentEditIndex.value] = updatedAdvice
// 通知父组件更新数据
emit('update:temporary-advices', updatedAdvices)
showEditDialog.value = false
ElMessage.success('编辑成功(已暂存本地,请点击"一键签名并生成医嘱"按钮提交到服务器)')
}
// 取消编辑
const handleCancelEdit = () => {
showEditDialog.value = false
currentEditIndex.value = -1
editForm.value = {
adviceName: '',
dosage: '',
unit: '',
usage: '',
frequency: '临时'
}
}
const confirmSign = async () => {
if (!signPassword.value) {
ElMessage.warning('请输入密码')
return
}
// 调用后端接口验证密码
try {
const response = await checkPassword({
password: signPassword.value
})
if (response.code === 200 && response.data) {
isSigned.value = true
signatureTime.value = new Date().toLocaleString('zh-CN')
showSignDialog.value = false
signPassword.value = ''
ElMessage.success('签名成功')
// 签名成功后自动提交,用 setTimeout 确保响应式状态更新完成
setTimeout(() => {
handleSubmit()
}, 100)
} else {
// 🔧 修改:当密码验证失败时,显示更清晰的错误提示
signPassword.value = ''
}
} catch (error) {
}
}
const handleSignAndSubmit = () => {
if (isSigned.value) {
// 如果已经签名,直接提交
handleSubmit()
} else {
// 如果未签名,打开签名弹窗
handleSign()
}
}
const handleDeleteAdvice = (index) => {
ElMessageBox.confirm('确定要删除这条医嘱吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
// 构建新的临时医嘱数据
const updatedAdvices = [...displayAdvices.value]
updatedAdvices.splice(index, 1)
// 通知父组件更新数据
emit('update:temporary-advices', updatedAdvices)
ElMessage.success('删除成功')
}).catch(() => {
// 用户取消删除
})
}
// 🔧 防重复提交标志位
const isSubmitting = ref(false)
const handleSubmit = async () => {
if (!isSigned.value) {
ElMessage.warning('请先进行电子签名')
return
}
// 🔧 防重复提交:如果正在提交,直接返回
if (isSubmitting.value) {
ElMessage.warning('正在提交医嘱,请稍候...')
return
}
// 检查是否有医嘱数据
if (displayAdvices.value.length === 0) {
ElMessage.warning('没有可保存的医嘱数据')
return
}
// 🔧 设置提交标志位
isSubmitting.value = true
try {
// 构建保存医嘱的请求参数
const saveData = {
organizationId: props.patientInfo.orgId || props.patientInfo.organizationId || 1, // 使用计费时的orgId
adviceSaveList: displayAdvices.value.map((advice, index) => {
// 获取原始药品数据
const originalMedicine = advice.originalMedicine
// 解析contentJson
let contentJsonData = {}
if (originalMedicine && originalMedicine.contentJson) {
try {
contentJsonData = JSON.parse(originalMedicine.contentJson)
} catch (e) {
// 解析失败继续使用originalMedicine中的数据
}
}
// 根据修改后的剂量重新计算数量和总价
let quantity = originalMedicine?.quantity || 1;
const unitPrice = originalMedicine?.unitPrice || contentJsonData.unitPrice || 0;
// 如果用户修改了剂量,重新计算数量 = 剂量 / 规格数值
if (advice.dosage !== undefined && originalMedicine?.specification) {
const specMatch = originalMedicine.specification.match(/(\d+)(\D+)/);
const specValue = specMatch ? parseInt(specMatch[1]) : 1;
if (specValue > 0) {
quantity = advice.dosage / specValue;
}
}
const totalPrice = unitPrice * quantity;
// ✅ 关键修复把修改后的数据更新到contentJson中后端会优先读取这里的值
contentJsonData.adviceName = advice.adviceName;
contentJsonData.quantity = quantity;
contentJsonData.volume = advice.dosage + (advice.unit || '');
contentJsonData.totalPrice = totalPrice;
contentJsonData.unitPrice = unitPrice;
// 用法、剂量、单位也更新到contentJson
// 🔧 修复:确保 methodCode 使用的是编码,而不是中文名称
let methodCode = advice.usage;
if (methodCode && typeof methodCode === 'string') {
// 检查是否是编码(通常编码较短,如 'iv', 'po'
const isCode = methodCode.length <= 10 && getMethodCodeDict.value.some(item => item.value === methodCode)
if (!isCode) {
// 如果是中文名称,转换为编码
const dictItem = getMethodCodeDict.value.find(item => item.label === methodCode)
if (dictItem) {
methodCode = dictItem.value
}
}
}
contentJsonData.methodCode = methodCode;
contentJsonData.dose = advice.dosage;
contentJsonData.doseUnitCode = advice.unit;
contentJsonData.rateCode = advice.frequency;
// 重新序列化contentJson
const updatedContentJson = JSON.stringify(contentJsonData);
// 构造请求参数(与门诊医生工作站完全一致)
return {
// 基础信息
// 🔧 修复dbOpType 的判断逻辑 - 如果有 requestId 则为修改,否则为新增
// 但对于从计费药品转换来的数据,即使没有 requestId也应该先更新 chargeItemId
dbOpType: originalMedicine?.requestId ? '2' : (originalMedicine?.chargeItemId ? '2' : '1'),
adviceType: originalMedicine?.adviceType || 1, // 使用原始类型或默认1
requestId: originalMedicine?.requestId,
chargeItemId: originalMedicine?.chargeItemId,
contentJson: updatedContentJson,
categoryCode: contentJsonData.categoryCode || originalMedicine?.categoryCode,
pharmacologyCategoryCode: contentJsonData.pharmacologyCategoryCode || originalMedicine?.pharmacologyCategoryCode,
partPercent: originalMedicine?.partPercent || contentJsonData.partPercent || 1,
partAttributeEnum: originalMedicine?.partAttributeEnum || contentJsonData.partAttributeEnum,
executeNum: contentJsonData.executeNum || originalMedicine?.executeNum || 1,
prescriptionNo: originalMedicine?.prescriptionNo,
// 数量和单位:使用重新计算后的数量
quantity: quantity,
dispensePerDuration: originalMedicine?.dispensePerDuration || contentJsonData.dispensePerDuration,
unitCode: contentJsonData.unitCode || originalMedicine?.unitCode || advice.unit,
unitPrice: unitPrice,
// 总价根据新的数量重新计算
totalPrice: totalPrice,
definitionId: originalMedicine?.definitionId ? String(originalMedicine.definitionId) : contentJsonData.definitionId ? String(contentJsonData.definitionId) : advice.definitionId ? String(advice.definitionId) : null,
definitionDetailId: originalMedicine?.definitionDetailId ? String(originalMedicine.definitionDetailId) : contentJsonData.definitionDetailId ? String(contentJsonData.definitionDetailId) : advice.definitionDetailId ? String(advice.definitionDetailId) : null,
lotNumber: originalMedicine?.lotNumber || contentJsonData.lotNumber,
// 状态和类型
statusEnum: originalMedicine?.statusEnum || 2, // 默认状态:已发送
categoryEnum: originalMedicine?.categoryEnum || contentJsonData.categoryEnum || 1,
// 药品/诊疗信息 - 🔧 修复优先使用originalMedicine中的adviceDefinitionId和adviceTableName
// 🔧 关键修复确保adviceDefinitionId不为null使用definitionId作为后备
adviceDefinitionId: originalMedicine?.adviceDefinitionId || contentJsonData.adviceDefinitionId || advice.definitionId || contentJsonData.definitionId || definitionId,
adviceTableName: originalMedicine?.adviceTableName || contentJsonData.adviceTableName || 'med_medication_definition',
adviceName: advice.adviceName,
minUnitQuantity: originalMedicine?.minUnitQuantity || contentJsonData.minUnitQuantity,
// 患者和就诊信息
patientId: props.patientInfo.patientId,
practitionerId: currentUser.value.id || originalMedicine?.practitionerId, // 开方医生
locationId: originalMedicine?.positionId || contentJsonData.locationId || props.patientInfo.orgId || props.patientInfo.positionId,
positionId: originalMedicine?.positionId || contentJsonData.positionId,
orgId: originalMedicine?.orgId || props.patientInfo.orgId || props.patientInfo.positionId,
performLocation: originalMedicine?.performLocation || contentJsonData.performLocation,
founderOrgId: currentUser.value.id || originalMedicine?.founderOrgId, // 开方人科室
encounterId: props.patientInfo.visitId || contentJsonData.encounterId || originalMedicine?.encounterId,
accountId: contentJsonData.accountId || originalMedicine?.accountId,
conditionId: contentJsonData.conditionId || originalMedicine?.conditionId,
encounterDiagnosisId: originalMedicine?.encounterDiagnosisId || contentJsonData.encounterDiagnosisId,
conditionDefinitionId: originalMedicine?.conditionDefinitionId || contentJsonData.conditionDefinitionId,
// 治疗信息
therapyEnum: originalMedicine?.therapyEnum || contentJsonData.therapyEnum || 1, // 默认临时医嘱
// 🔧 修复methodCode 使用编码,而不是中文名称
methodCode: methodCode, // 用法(使用编码)
rateCode: advice.frequency, // 频次
dose: advice.dosage,
firstDose: originalMedicine?.firstDose || contentJsonData.firstDose,
doseUnitCode: contentJsonData.doseUnitCode || originalMedicine?.doseUnitCode || advice.unit,
skinTestFlag: originalMedicine?.skinTestFlag || contentJsonData.skinTestFlag,
injectFlag: originalMedicine?.injectFlag || contentJsonData.injectFlag,
// 分组信息
groupId: originalMedicine?.groupId,
packageId: originalMedicine?.packageId || contentJsonData.packageId,
// 诊疗活动信息
activityId: contentJsonData.adviceDefinitionId || originalMedicine?.adviceDefinitionId, // 对于诊疗类型,使用 adviceDefinitionId 作为 activity_id
// 医保信息
ybClassEnum: originalMedicine?.ybClassEnum || contentJsonData.ybClassEnum,
// 中药信息
chineseHerbsDoseQuantity: originalMedicine?.chineseHerbsDoseQuantity || contentJsonData.chineseHerbsDoseQuantity || 1
}
})
}
// 调用保存医嘱接口
// 如果已签名,则执行签发操作('1'),否则执行保存操作('0'
const adviceOpType = isSigned.value ? '1' : '0'
const response = await savePrescription(saveData, adviceOpType)
if (response.code === 200) {
ElMessage.success('临时医嘱保存成功')
// 🔧 关键修复:处理后端返回的医嘱 ID
// 后端返回的 data 数组只包含新创建的医嘱记录的 ID不包含已更新的记录
// 我们需要正确地映射这些 ID 到对应的临时医嘱数据
if (response.data && Array.isArray(response.data) && response.data.length > 0) {
// 创建一个索引,记录哪些记录是新创建的
let newDataIndex = 0
displayAdvices.value.forEach((advice, index) => {
const originalMedicine = advice.originalMedicine || {}
// 如果这个记录没有 requestId说明是新创建的
if (!originalMedicine.requestId) {
if (newDataIndex < response.data.length) {
// 更新 originalMedicine 中的 requestId
if (!displayAdvices.value[index].originalMedicine) {
displayAdvices.value[index].originalMedicine = {}
}
displayAdvices.value[index].originalMedicine.requestId = response.data[newDataIndex]
newDataIndex++
}
}
})
}
// 🔧 修复:将保存后的医嘱数据(包含用户的修改和后端返回的 requestId传递给父组件
// 这样父组件可以使用用户修改后的数据,而不是重新加载数据
const submitData = {
patientInfo: props.patientInfo,
billingMedicines: props.billingMedicines,
temporaryAdvices: displayAdvices.value, // 使用用户修改后的数据,包含后端返回的 requestId
signature: {
doctorName: currentUser.value.name,
signatureTime: signatureTime.value
}
}
// 通知父组件
emit('submit', submitData)
} else {
ElMessage.error('保存医嘱失败:' + (response.msg || response.message || '未知错误'))
}
} catch (error) {
ElMessage.error('保存医嘱失败,请重试')
} finally {
// 🔧 重置防重复提交标志位
isSubmitting.value = false
}
}
const handleCancel = () => {
// 通知父组件关闭弹窗
emit('cancel')
}
// 刷新按钮点击
const handleRefresh = () => {
emit('refresh')
}
// 引用计费按钮点击
const handleQuoteBilling = () => {
emit('quote-billing')
}
// 数量改变时重新计算小计
const handleQuantityChange = () => {
// 数量变化会自动更新,因为是响应式
}
// 获取医保标签样式类
const getInsuranceClass = (insuranceType) => {
if (!insuranceType) return ''
if (insuranceType === '甲' || insuranceType === '乙' || insuranceType === '医保') {
return 'insurance-tag'
}
if (insuranceType === '自费') {
return 'selfpay-tag'
}
return ''
}
// 🔧 新增:根据用法编码获取对应的显示名称
const getUsageLabel = (usageCode) => {
if (!usageCode) return '-'
const dictItem = getMethodCodeDict.value.find(item => item.value === usageCode)
return dictItem ? dictItem.label : usageCode
}
// 🔧 新增:获取编辑表单中当前选中的用法标签
const editFormUsageLabel = computed(() => {
if (!editForm.value.usage) return ''
return getUsageLabel(editForm.value.usage)
})
</script>
<style scoped>
.temporary-medical-container {
padding: 0;
max-height: 80vh;
overflow-y: auto;
}
/* 顶部容器 */
.top-container {
height: 180px;
padding: 0 0 20px 0;
background: linear-gradient(135deg, #63A3E7 0%, #4a8bc9 100%);
display: flex;
flex-direction: column;
}
/* 标题栏 */
.title-bar {
padding: 16px 20px;
text-align: center;
}
.title-text {
color: white;
font-size: 1.5rem;
font-weight: 600;
margin: 0;
line-height: 1.2;
}
/* 患者信息卡 */
.patient-info-card {
flex: 1;
margin: 0 20px 12px 20px;
padding: 16px 20px;
background: rgba(255, 255, 255, 0.9);
border-radius: 8px;
display: flex;
flex-wrap: wrap;
gap: 16px 40px;
align-items: center;
}
.info-item {
display: flex;
align-items: center;
gap: 8px;
}
.info-label {
font-weight: bold;
color: #333;
font-size: 14px;
}
.info-value {
color: #333;
font-size: 14px;
}
/* 操作按钮组 */
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 16px;
padding: 0 20px;
}
/* 刷新按钮 */
.refresh-btn {
background: linear-gradient(135deg, #63A3E7 0%, #4a8bc9 100%);
border-color: #63A3E7;
color: white;
font-size: 14px;
padding: 8px 20px;
transition: all 0.3s ease;
}
.refresh-btn:hover {
background: linear-gradient(135deg, #5391d1 0%, #3a7bb5 100%);
border-color: #5391d1;
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(99, 163, 231, 0.4);
}
/* 引用计费按钮 */
.quote-btn {
background: rgba(255, 255, 255, 0.9);
border-color: #63A3E7;
color: #63A3E7;
font-size: 14px;
padding: 8px 20px;
transition: all 0.3s ease;
}
.quote-btn:hover {
background: rgba(255, 255, 255, 1);
border-color: #5391d1;
color: #5391d1;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(99, 163, 231, 0.3);
}
.medicine-section,
.advice-section,
.signature-section {
margin: 24px 20px 30px 20px;
}
/* 区域标题 */
.section-title {
font-size: 1.2rem;
color: #333;
font-weight: 600;
padding-bottom: 12px;
margin-bottom: 16px;
border-bottom: 2px solid #e4e7ed;
}
.medicine-summary {
display: flex;
justify-content: flex-end;
gap: 30px;
padding: 16px;
background-color: #f5f7fa;
border-radius: 4px;
margin-top: 12px;
}
.summary-item {
font-size: 15px;
font-weight: 500;
}
.summary-item.insurance {
color: #63A3E7;
}
.summary-item.self-pay {
color: #67c23a;
}
.summary-item.total {
font-weight: bold;
color: #f56c6c;
font-size: 16px;
}
/* 医保标签样式 */
:deep(.insurance-tag) {
background-color: #63A3E7;
color: white;
}
:deep(.selfpay-tag) {
background-color: #67c23a;
color: white;
}
/* 计费药品表格表头浅灰色背景 */
.medicine-section :deep(.el-table th) {
background-color: #f5f7fa !important;
color: #333 !important;
}
.medicine-section :deep(.el-table th.el-table__cell > .cell) {
color: #333 !important;
font-weight: 600;
}
/* 临时医嘱表格表头浅灰色背景(与计费药品表格保持一致) */
.advice-section :deep(.el-table th) {
background-color: #f5f7fa !important;
color: #333 !important;
}
.advice-section :deep(.el-table th.el-table__cell > .cell) {
color: #333 !important;
font-weight: 600;
}
/* 签名信息卡 */
.signature-card {
padding: 20px;
background-color: #f5f7fa;
border-radius: 8px;
margin-bottom: 20px;
}
.signature-info {
margin-bottom: 16px;
font-size: 15px;
display: flex;
align-items: center;
}
.signature-info:last-child {
margin-bottom: 0;
}
.signature-info .info-label {
font-weight: bold;
color: #333;
width: 100px;
}
.signature-info .info-value {
color: #333;
}
.unsigned {
color: #f56c6c;
font-weight: 500;
}
/* 签名操作按钮区 */
.signature-actions {
display: flex;
justify-content: center;
align-items: center;
gap: 30px;
}
.cancel-btn {
width: 140px;
height: 48px;
font-size: 16px;
border: 1px solid #dcdfe6;
transition: all 0.3s ease;
}
.cancel-btn:hover {
border-color: #409eff;
color: #409eff;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(64, 158, 255, 0.2);
}
/* 一键签名按钮 */
.sign-btn {
width: 220px;
height: 48px;
font-size: 16px;
font-weight: bold;
background: linear-gradient(135deg, #67c23a 0%, #85ce61 100%);
border: none;
color: white;
transition: all 0.3s ease;
box-shadow: 0 4px 8px rgba(103, 194, 58, 0.3);
}
.sign-btn:hover {
transform: translateY(-3px);
box-shadow: 0 6px 12px rgba(103, 194, 58, 0.4);
}
.sign-btn:active {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(103, 194, 58, 0.4);
}
.sign-dialog-content p {
margin-bottom: 16px;
color: #666;
}
.dialog-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
}
:deep(.el-descriptions) {
margin-top: 8px;
}
:deep(.el-descriptions__label) {
font-weight: bold;
}
</style>