Files
his/openhis-ui-vue3/src/views/surgicalschedule/temporaryMedical.vue
2026-05-25 15:49:49 +08:00

1185 lines
38 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">
已引用计费药品待生成医嘱
<span v-if="(billingMedicines || []).length >= PAGE_SIZE" style="margin-left:auto;font-size:14px;color:#4a8bc9;cursor:pointer;white-space:nowrap;" @click="billingExpanded = !billingExpanded">
{{ billingExpanded ? '收起' : `展开全部(${(billingMedicines || []).length})` }}
</span>
</div>
<el-table
:data="displayBillingMedicines"
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">
临时医嘱预览已生成
<span v-if="(displayAdvices || []).length >= PAGE_SIZE" style="margin-left:auto;font-size:14px;color:#4a8bc9;cursor:pointer;white-space:nowrap;" @click="advicesExpanded = !advicesExpanded">
{{ advicesExpanded ? '收起' : `展开全部(${(displayAdvices || []).length})` }}
</span>
</div>
<el-table
:data="displayAdvicesList"
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"
:disabled="allItemsSubmitted"
@click="handleSignAndSubmit"
>
{{ allItemsSubmitted ? '已签发' : '一键签名并生成医嘱' }}
</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
})
// 🔧 检查是否已全部签发statusEnum=2用于控制"一键签名"按钮是否禁用
// 注意:不能依赖 requestId因为草稿记录也有 requestId
const allItemsSubmitted = computed(() => {
const meds = props.billingMedicines || []
return meds.length > 0 && meds.every(m => m._signed)
})
// 展开/收起控制
const PAGE_SIZE = 3
const billingExpanded = ref(false)
const advicesExpanded = ref(false)
const displayBillingMedicines = computed(() => {
const all = props.billingMedicines || []
return billingExpanded.value ? all : all.slice(0, PAGE_SIZE)
})
const displayAdvicesList = computed(() => {
const all = displayAdvices.value || []
return advicesExpanded.value ? all : all.slice(0, PAGE_SIZE)
})
// 响应式数据 - isSigned 从父组件传入的 prop 初始化
const isSigned = ref(props.isSignedProp)
// 🔧 修复 Bug #446: 同步父组件 isSignedProp 的变化到本地 isSigned
// ref(props.isSignedProp) 只在初始化时读取一次,父组件后续更新不会自动同步
watch(() => props.isSignedProp, (newVal) => {
isSigned.value = newVal
})
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 = []
}
}
// 初始化
initDisplayAdvices()
onMounted(() => {
initDisplayAdvices()
// 🔧 修复:不再自动把区域一草稿转为区域二数据
})
// 🔧 新增:监听 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) => {
// 空 watcher仅保留结构避免破坏其他逻辑
}, { 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
}
// 检查是否有医嘱数据
// 🔧 修复签发应该处理区域一中所有待签发的药品billingMedicines而非区域二中已有数据displayAdvices
const itemsToSign = convertedAdvices.value
if (itemsToSign.length === 0) {
ElMessage.warning('没有可保存的医嘱数据')
return
}
// 🔧 设置提交标志位
isSubmitting.value = true
try {
// 构建保存医嘱的请求参数
const saveData = {
organizationId: props.patientInfo.orgId || props.patientInfo.organizationId || 1, // 使用计费时的orgId
adviceSaveList: itemsToSign.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);
// 🔧 构造请求参数:与计费弹窗 handleSave 保持完全一致的模式
// 先展开原始 contentJson 的所有字段,然后用当前值覆盖(保证不丢字段)
return {
...contentJsonData,
// 基础信息
dbOpType: originalMedicine?.requestId ? '2' : (originalMedicine?.chargeItemId ? '2' : '1'),
adviceType: originalMedicine?.adviceType || 1,
requestId: originalMedicine?.requestId,
chargeItemId: originalMedicine?.chargeItemId,
contentJson: updatedContentJson,
categoryCode: contentJsonData.categoryCode || originalMedicine?.categoryCode,
partPercent: originalMedicine?.partPercent || contentJsonData.partPercent || 1,
executeNum: contentJsonData.executeNum || originalMedicine?.executeNum || 1,
// 数量和单位:使用重新计算后的数量
quantity: quantity,
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,
// 状态和类型
categoryEnum: originalMedicine?.categoryEnum || contentJsonData.categoryEnum || 1,
// 药品/诊疗信息
adviceDefinitionId: originalMedicine?.adviceDefinitionId || contentJsonData.adviceDefinitionId || advice.definitionId || contentJsonData.definitionId,
adviceTableName: originalMedicine?.adviceTableName || contentJsonData.adviceTableName || 'med_medication_definition',
adviceName: advice.adviceName,
// 患者和就诊信息
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,
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,
rateCode: advice.frequency,
dose: advice.dosage,
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,
// 医保信息
ybClassEnum: originalMedicine?.ybClassEnum || contentJsonData.ybClassEnum,
// 🔧 补充:手术计费上下文
generateSourceEnum: 6,
sourceBillNo: props.patientInfo?.operCode || originalMedicine?.sourceBillNo || ''
}
})
}
// 🔧 修复:使用 savePrescription + adviceOpType='2'(后端 AdviceOpType: '2'=SIGN_ADVICE 签发)
const response = await savePrescription(saveData, '2')
if (response.code === 200) {
ElMessage.success('临时医嘱保存成功')
// 🔧 签名成功,后端已更新已有记录的 statusEnum 为 2
// 不需要手动更新 requestId签名操作是 UPDATE不是 INSERT后端不返回新 ID
// 🔧 修复:将保存后的医嘱数据传递给父组件
const submitData = {
patientInfo: props.patientInfo,
billingMedicines: props.billingMedicines,
temporaryAdvices: itemsToSign,
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;
display: flex;
align-items: center;
gap: 12px;
}
.expand-btn {
font-size: 0.85rem;
color: #4a8bc9;
cursor: pointer;
font-weight: 400;
margin-left: auto;
}
.expand-btn:hover {
color: #2a6ba9;
text-decoration: underline;
}
.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>