feat: 病历模块优化、AI辅助问诊
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
<template>
|
||||
<!--
|
||||
AI写病历:主弹层=历史+预览;二级=确认主诉生成
|
||||
无历史自动打开生成框;点生成先关二级,主层等待
|
||||
模板禁止 ?? ?. ;:class 禁止方法调用
|
||||
-->
|
||||
<u-popup
|
||||
v-model="visible"
|
||||
mode="bottom"
|
||||
border-radius="24"
|
||||
:safe-area-inset-bottom="true"
|
||||
:mask-close-able="true"
|
||||
height="82%"
|
||||
z-index="10090"
|
||||
@close="onClose"
|
||||
>
|
||||
<view class="ai-mr-drawer">
|
||||
<view class="ai-mr-title-row">
|
||||
<text class="ai-mr-title">AI写病历</text>
|
||||
<text class="ai-mr-sub">{{ patientName || '—' }} · {{ sexLabel }} · {{ patientAge || 0 }}岁</text>
|
||||
</view>
|
||||
<view class="ai-mr-body flex-row">
|
||||
<scroll-view scroll-y class="ai-mr-hist">
|
||||
<view class="ai-hist-hd">
|
||||
<text class="fs-24 font-bold">历史</text>
|
||||
</view>
|
||||
<view v-if="histLoading" class="p-24 color-sub fs-24">加载中…</view>
|
||||
<view v-else-if="historyEmpty" class="p-24 color-sub fs-22">暂无记录,可生成病历</view>
|
||||
<view
|
||||
v-for="row in historyView"
|
||||
:key="row.id"
|
||||
class="ai-hist-item"
|
||||
:class="{ 'ai-hist-item--active': activeId == row.id }"
|
||||
:data-id="row.id"
|
||||
@click="onSelectHistoryTap"
|
||||
>
|
||||
<text class="fs-26 font-bold ai-hist-name">{{ row.nameText }}</text>
|
||||
<text class="fs-22 color-sub">{{ row.timeText }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<scroll-view scroll-y class="ai-mr-preview">
|
||||
<view v-if="generating || detailLoading" class="p-32 color-sub">AI 处理中…</view>
|
||||
<block v-else>
|
||||
<view v-if="!previewChief && !previewRows.length" class="p-32 color-sub fs-24">
|
||||
请选择历史或生成病历
|
||||
</view>
|
||||
<block v-else>
|
||||
<view v-if="previewChief" class="ai-chief-box">
|
||||
<text class="fs-20 color-sub">主诉</text>
|
||||
<text class="fs-24 m-t-4">{{ previewChief }}</text>
|
||||
</view>
|
||||
<view v-if="previewRows.length" class="ai-preview-grid">
|
||||
<view
|
||||
v-for="row in previewRows"
|
||||
:key="row.key"
|
||||
class="ai-preview-card"
|
||||
:class="{
|
||||
'ai-preview-card--full':
|
||||
row.key == 'present_illness' ||
|
||||
row.key == 'tcm_case' ||
|
||||
row.key == 'treatment_advice' ||
|
||||
row.key == 'auxiliary_exam',
|
||||
}"
|
||||
>
|
||||
<text class="fs-20 color-sub">{{ row.label }}</text>
|
||||
<text class="fs-24 m-t-4">{{ row.value }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="ai-mr-footer safe-area-inset-bottom">
|
||||
<view class="ai-actions">
|
||||
<view class="ai-btn ai-btn--ghost" @click="onClose">关闭</view>
|
||||
<view class="ai-btn ai-btn--ghost" @click="openGenModal">生成病历</view>
|
||||
<view class="ai-btn ai-btn--primary" @click="onConfirmImport">确认导入</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<page-container
|
||||
v-if="genModalVisible"
|
||||
:show="genModalVisible"
|
||||
overlay
|
||||
round
|
||||
position="bottom"
|
||||
@afterleave="onGenModalLeave"
|
||||
@clickoverlay="closeGenModal"
|
||||
>
|
||||
<view class="ai-gen-sheet">
|
||||
<view class="ai-gen-hd flex-row flex-jus-sp flex-ali-center">
|
||||
<text class="fs-30 font-bold">生成病历</text>
|
||||
<text class="ai-mr-sub" @click="closeGenModal">关闭</text>
|
||||
</view>
|
||||
<text class="ai-gen-tip">确认主诉后开始生成,将关闭本弹窗并在主层等待</text>
|
||||
<view class="ai-gen-patient">
|
||||
<text class="fs-24 color-sub">患者</text>
|
||||
<text class="fs-26 m-t-8">{{ patientName || '—' }} · {{ sexLabel }} · {{ patientAge || 0 }}岁</text>
|
||||
</view>
|
||||
<view class="ai-gen-field">
|
||||
<text class="fs-24 color-sub">主诉(可编辑)</text>
|
||||
<textarea
|
||||
class="ai-gen-textarea"
|
||||
:value="aiChief"
|
||||
maxlength="500"
|
||||
placeholder="请确认或补充主诉"
|
||||
@input="onAiChiefInput"
|
||||
/>
|
||||
</view>
|
||||
<view class="ai-mr-footer">
|
||||
<view class="ai-btn ai-btn--primary ai-btn--block" @click="onGenerate">开始生成</view>
|
||||
</view>
|
||||
</view>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
aiGenerateMedicalRecordApi,
|
||||
aiGenerationDetailApi,
|
||||
aiListGenerationsApi,
|
||||
} from '@/api/reception.js'
|
||||
|
||||
const PREVIEW_DEFS = [
|
||||
{ key: 'present_illness', label: '现病史' },
|
||||
{ key: 'tongue', label: '舌象' },
|
||||
{ key: 'pulse', label: '脉象' },
|
||||
{ key: 'tcm_case', label: '中医病案' },
|
||||
{ key: 'tcm_syndrome', label: '中医证候' },
|
||||
{ key: 'tcm_disease', label: '中医疾病' },
|
||||
{ key: 'tcm_method', label: '中医治法' },
|
||||
{ key: 'diagnosis', label: '临床诊断' },
|
||||
{ key: 'treatment_advice', label: '治疗意见' },
|
||||
{ key: 'family_history', label: '家族史' },
|
||||
{ key: 'epidemic_history', label: '流行病学史' },
|
||||
{ key: 'past_history', label: '既往史' },
|
||||
{ key: 'menstrual_history', label: '月经史' },
|
||||
{ key: 'marital_history', label: '婚育史' },
|
||||
{ key: 'personal_history', label: '个人史' },
|
||||
{ key: 'allergy_history', label: '过敏史' },
|
||||
{ key: 'physical_exam', label: '体征检查' },
|
||||
{ key: 'auxiliary_exam', label: '辅助检查' },
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'AiMedicalRecordDrawer',
|
||||
props: {
|
||||
value: { type: Boolean, default: false },
|
||||
registerId: { type: [Number, String], default: 0 },
|
||||
storeId: { type: [Number, String], default: 0 },
|
||||
patientName: { type: String, default: '' },
|
||||
patientSex: { type: [Number, String], default: 0 },
|
||||
patientAge: { type: [Number, String], default: 0 },
|
||||
chiefComplaint: { type: String, default: '' },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
genModalVisible: false,
|
||||
aiChief: '',
|
||||
histLoading: false,
|
||||
generating: false,
|
||||
detailLoading: false,
|
||||
historyList: [],
|
||||
historyView: [],
|
||||
historyEmpty: true,
|
||||
activeId: 0,
|
||||
previewFields: {},
|
||||
previewChief: '',
|
||||
previewRows: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sexLabel() {
|
||||
const s = Number(this.patientSex)
|
||||
if (s === 1) return '男'
|
||||
if (s === 2) return '女'
|
||||
return '未知'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(v) {
|
||||
this.visible = !!v
|
||||
if (v) this.bootstrapOpen()
|
||||
},
|
||||
},
|
||||
visible(v) {
|
||||
this.$emit('input', v)
|
||||
this.$emit('overlay-change', !!v)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
bootstrapOpen() {
|
||||
this.aiChief = String(this.chiefComplaint || '').trim()
|
||||
this.genModalVisible = false
|
||||
this.resetPreview()
|
||||
this.loadHistory().then(() => {
|
||||
if (this.historyList.length) {
|
||||
this.selectHistoryById(Number(this.historyList[0].id))
|
||||
} else {
|
||||
this.openGenModal()
|
||||
}
|
||||
})
|
||||
},
|
||||
resetPreview() {
|
||||
this.activeId = 0
|
||||
this.previewFields = {}
|
||||
this.previewChief = ''
|
||||
this.previewRows = []
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
this.genModalVisible = false
|
||||
},
|
||||
formatTime(ts) {
|
||||
const n = Number(ts || 0)
|
||||
if (!n) return '—'
|
||||
const d = new Date(n * 1000)
|
||||
const pad = (x) => (x < 10 ? '0' + x : '' + x)
|
||||
return pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
|
||||
},
|
||||
rebuildHistoryView() {
|
||||
const list = Array.isArray(this.historyList) ? this.historyList : []
|
||||
this.historyEmpty = list.length === 0
|
||||
this.historyView = list.map((row) => ({
|
||||
id: Number(row.id || 0),
|
||||
nameText: String(row.name || '').trim() || ('病历 #' + row.id),
|
||||
timeText: this.formatTime(row.created_at),
|
||||
}))
|
||||
},
|
||||
rebuildPreviewRows() {
|
||||
const fields = this.previewFields || {}
|
||||
const rows = []
|
||||
for (let i = 0; i < PREVIEW_DEFS.length; i++) {
|
||||
const d = PREVIEW_DEFS[i]
|
||||
const val = String(fields[d.key] || '').trim()
|
||||
if (val) rows.push({ key: d.key, label: d.label, value: val })
|
||||
}
|
||||
this.previewRows = rows
|
||||
},
|
||||
async loadHistory() {
|
||||
this.histLoading = true
|
||||
try {
|
||||
const res = await aiListGenerationsApi({
|
||||
register_id: Number(this.registerId),
|
||||
scene: 'medical_record',
|
||||
limit: 30,
|
||||
})
|
||||
const list = (res && res.data) || res || []
|
||||
this.historyList = Array.isArray(list) ? list : []
|
||||
this.rebuildHistoryView()
|
||||
} catch (e) {
|
||||
this.historyList = []
|
||||
this.rebuildHistoryView()
|
||||
} finally {
|
||||
this.histLoading = false
|
||||
}
|
||||
},
|
||||
openGenModal() {
|
||||
this.genModalVisible = true
|
||||
},
|
||||
closeGenModal() {
|
||||
this.genModalVisible = false
|
||||
},
|
||||
onGenModalLeave() {
|
||||
this.genModalVisible = false
|
||||
},
|
||||
onAiChiefInput(e) {
|
||||
this.aiChief = (e && e.detail && e.detail.value) || ''
|
||||
},
|
||||
async onGenerate() {
|
||||
if (this.generating) return
|
||||
const chief = String(this.aiChief || '').trim()
|
||||
if (!chief) {
|
||||
uni.showToast({ title: '请填写主诉后再生成', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!Number(this.registerId)) {
|
||||
uni.showToast({ title: '挂号无效', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.closeGenModal()
|
||||
this.generating = true
|
||||
this.previewFields = {}
|
||||
this.previewChief = chief
|
||||
this.rebuildPreviewRows()
|
||||
try {
|
||||
const res = await aiGenerateMedicalRecordApi({
|
||||
register_id: Number(this.registerId),
|
||||
store_id: Number(this.storeId || 0) || undefined,
|
||||
chief_complaint: chief,
|
||||
})
|
||||
const data = (res && res.data) || res || {}
|
||||
this.previewFields = data.fields || {}
|
||||
this.rebuildPreviewRows()
|
||||
await this.loadHistory()
|
||||
this.activeId = Number(data.generation_id || 0)
|
||||
uni.showToast({ title: '已生成', icon: 'success' })
|
||||
} catch (e) {
|
||||
const msg = (e && e.message) || (e && e.msg) || '生成失败'
|
||||
uni.showToast({ title: String(msg), icon: 'none' })
|
||||
} finally {
|
||||
this.generating = false
|
||||
}
|
||||
},
|
||||
onSelectHistoryTap(e) {
|
||||
const raw = e && e.currentTarget && e.currentTarget.dataset
|
||||
? e.currentTarget.dataset.id
|
||||
: 0
|
||||
const id = Number(raw || 0)
|
||||
if (!id) return
|
||||
this.selectHistoryById(id)
|
||||
},
|
||||
async selectHistoryById(id) {
|
||||
this.activeId = id
|
||||
this.detailLoading = true
|
||||
try {
|
||||
const res = await aiGenerationDetailApi({ id: id })
|
||||
const data = (res && res.data) || res || {}
|
||||
this.previewFields = data.fields || {}
|
||||
const snap = data.input_snapshot || {}
|
||||
this.previewChief = String(snap.chief_complaint || this.aiChief || '').trim()
|
||||
this.rebuildPreviewRows()
|
||||
} catch (e) {
|
||||
this.previewFields = {}
|
||||
this.previewChief = ''
|
||||
this.rebuildPreviewRows()
|
||||
const msg = (e && e.message) || (e && e.msg) || '加载失败'
|
||||
uni.showToast({ title: String(msg), icon: 'none' })
|
||||
} finally {
|
||||
this.detailLoading = false
|
||||
}
|
||||
},
|
||||
onConfirmImport() {
|
||||
if (this.generating || this.detailLoading) return
|
||||
const hasFields = this.previewRows && this.previewRows.length
|
||||
const chief = String(this.previewChief || this.aiChief || '').trim()
|
||||
if (!hasFields && !chief) {
|
||||
uni.showToast({ title: '暂无可导入内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.$emit('import', {
|
||||
chief_complaint: chief,
|
||||
fields: Object.assign({}, this.previewFields || {}),
|
||||
})
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-mr-drawer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
.ai-mr-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.ai-mr-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.ai-mr-sub {
|
||||
font-size: 24rpx;
|
||||
color: #6acdbb;
|
||||
}
|
||||
.ai-mr-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ai-mr-hist {
|
||||
width: 220rpx;
|
||||
background: #fff;
|
||||
border-right: 1px solid #eee;
|
||||
height: 100%;
|
||||
}
|
||||
.ai-hist-hd {
|
||||
padding: 16rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.ai-hist-item {
|
||||
padding: 16rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ai-hist-item--active {
|
||||
background: #e8f8f4;
|
||||
}
|
||||
.ai-hist-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-mr-preview {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 16rpx 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ai-chief-box {
|
||||
margin-bottom: 12rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 10rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ai-preview-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -6rpx;
|
||||
}
|
||||
.ai-preview-card {
|
||||
width: calc(50% - 12rpx);
|
||||
margin: 0 6rpx 12rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 10rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ai-preview-card--full {
|
||||
width: calc(100% - 12rpx);
|
||||
}
|
||||
.m-t-4 {
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.ai-mr-footer {
|
||||
padding: 16rpx 24rpx 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.ai-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ai-btn {
|
||||
padding: 16rpx 28rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.ai-btn--ghost {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
.ai-btn--primary {
|
||||
background: #6acdbb;
|
||||
color: #fff;
|
||||
}
|
||||
.ai-btn--block {
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-left: 0;
|
||||
}
|
||||
.ai-gen-sheet {
|
||||
background: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
max-height: 78vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.ai-gen-hd {
|
||||
padding: 24rpx 32rpx 8rpx;
|
||||
}
|
||||
.ai-gen-tip {
|
||||
padding: 0 32rpx 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
.ai-gen-patient,
|
||||
.ai-gen-field {
|
||||
margin: 0 24rpx 16rpx;
|
||||
padding: 16rpx 20rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 12rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ai-gen-textarea {
|
||||
width: 100%;
|
||||
min-height: 160rpx;
|
||||
margin-top: 8rpx;
|
||||
font-size: 26rpx;
|
||||
color: #2a2e35;
|
||||
background: #fff;
|
||||
padding: 12rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.color-sub {
|
||||
color: #999;
|
||||
}
|
||||
.p-24 {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.p-32 {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.m-t-8 {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.fs-22 {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.fs-24 {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.fs-26 {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.fs-30 {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.flex-ali-center {
|
||||
align-items: center;
|
||||
}
|
||||
.flex-jus-sp {
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,997 @@
|
||||
<template>
|
||||
<!--
|
||||
AI辅助出方:主弹层=历史+对照;二级弹层=可编辑病历预览+生成
|
||||
模板禁止 ?? ?. ;:class 禁止方法调用
|
||||
-->
|
||||
<u-popup
|
||||
v-model="visible"
|
||||
mode="bottom"
|
||||
border-radius="24"
|
||||
:safe-area-inset-bottom="true"
|
||||
:mask-close-able="true"
|
||||
height="82%"
|
||||
z-index="10090"
|
||||
@close="onClose"
|
||||
>
|
||||
<view class="ai-rx-drawer">
|
||||
<view class="ai-rx-title-row">
|
||||
<text class="ai-rx-title">AI辅助出方</text>
|
||||
<text class="ai-rx-sub">{{ patientName || '—' }} · {{ sexLabel }} · {{ patientAge || 0 }}岁</text>
|
||||
</view>
|
||||
<view v-if="conflictView.length" class="ai-conflict">
|
||||
<text class="ai-conflict-title">配伍禁忌提示</text>
|
||||
<view v-for="(line, ci) in conflictView" :key="ci" class="ai-conflict-line">· {{ line }}</view>
|
||||
</view>
|
||||
<view class="ai-rx-body flex-row">
|
||||
<scroll-view scroll-y class="ai-rx-hist">
|
||||
<view class="ai-hist-hd">
|
||||
<text class="fs-24 font-bold">历史</text>
|
||||
</view>
|
||||
<view v-if="histLoading" class="p-24 color-sub fs-24">加载中…</view>
|
||||
<view v-else-if="historyEmpty" class="p-24 color-sub fs-22">暂无记录,可生成处方</view>
|
||||
<view
|
||||
v-for="row in historyView"
|
||||
:key="row.id"
|
||||
class="ai-hist-item"
|
||||
:class="{ 'ai-hist-item--active': activeId == row.id }"
|
||||
:data-id="row.id"
|
||||
@click="onSelectHistoryTap"
|
||||
>
|
||||
<text class="fs-26 font-bold ai-hist-name">{{ row.nameText }}</text>
|
||||
<text class="fs-22 color-sub">{{ row.typeText }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<scroll-view scroll-y class="ai-rx-match">
|
||||
<view v-if="generating || matchLoading" class="p-32 color-sub">AI 处理中…</view>
|
||||
<block v-else>
|
||||
<view v-if="!matchResult" class="p-32 color-sub fs-24">请选择历史或生成处方</view>
|
||||
<block v-else>
|
||||
<view class="ai-sec-title">已对照({{ matchedCount }})</view>
|
||||
<!-- 中药一行多味 -->
|
||||
<view v-if="isTcmRx" class="ai-tcm-grid">
|
||||
<view v-for="m in matchedView" :key="m.mi" class="ai-tcm-cell">
|
||||
<view class="flex-row flex-ali-center flex-jus-sp">
|
||||
<text class="fs-24 font-bold ai-match-name">{{ m.ai_name }}</text>
|
||||
<switch
|
||||
:checked="m.importOn"
|
||||
color="#6ACDBB"
|
||||
style="transform: scale(0.6)"
|
||||
:data-mi="m.mi"
|
||||
@change="onToggleImportTap"
|
||||
/>
|
||||
</view>
|
||||
<text class="fs-20 color-sub">{{ m.doseUnit }}{{ m.usageText }}</text>
|
||||
<view class="flex-row flex-wrap m-t-8">
|
||||
<view
|
||||
v-for="c in m.candidates"
|
||||
:key="c.drug_id"
|
||||
class="ai-cand ai-cand--sm"
|
||||
:class="{ 'ai-cand--on': m.selected_drug_id == c.drug_id }"
|
||||
:data-mi="m.mi"
|
||||
:data-drug-id="c.drug_id"
|
||||
@click="onPickCandidateTap"
|
||||
>
|
||||
<text class="fs-18">{{ c.drug_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<block v-else>
|
||||
<view v-for="m in matchedView" :key="m.mi" class="ai-match-row ai-match-row--card">
|
||||
<view class="flex-row flex-ali-center flex-jus-sp">
|
||||
<text class="fs-26 font-bold ai-match-name">{{ m.ai_name }}</text>
|
||||
<text class="fs-22 color-sub">{{ m.doseUnit }}{{ m.usageText }}</text>
|
||||
<switch
|
||||
:checked="m.importOn"
|
||||
color="#6ACDBB"
|
||||
style="transform: scale(0.65)"
|
||||
:data-mi="m.mi"
|
||||
@change="onToggleImportTap"
|
||||
/>
|
||||
</view>
|
||||
<view class="flex-row flex-wrap m-t-8">
|
||||
<view
|
||||
v-for="c in m.candidates"
|
||||
:key="c.drug_id"
|
||||
class="ai-cand ai-cand--sm"
|
||||
:class="{ 'ai-cand--on': m.selected_drug_id == c.drug_id }"
|
||||
:data-mi="m.mi"
|
||||
:data-drug-id="c.drug_id"
|
||||
@click="onPickCandidateTap"
|
||||
>
|
||||
<text class="fs-20">{{ c.drug_name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="ai-sec-title m-t-16">未对照({{ unmatchedCount }})</view>
|
||||
<view
|
||||
v-for="u in unmatchedView"
|
||||
:key="u.ui"
|
||||
class="ai-match-row ai-match-row--warn"
|
||||
>
|
||||
<text class="fs-26">{{ u.ai_name }}</text>
|
||||
<text class="ai-link m-t-8" :data-ui="u.ui" @click="onSearchUnmatchedTap">去手动搜索</text>
|
||||
</view>
|
||||
<!-- 药方摘要:仿 Descriptions 双列紧凑 -->
|
||||
<view
|
||||
v-if="rxPrescriptionName || rxReason || rxBasis || rxDosage || rxDayDosage"
|
||||
class="ai-rx-meta m-t-16"
|
||||
>
|
||||
<view class="ai-desc-row ai-desc-row--full">
|
||||
<text class="ai-desc-label">药方名</text>
|
||||
<text class="ai-desc-val">{{ rxPrescriptionName || '未命名药方' }}</text>
|
||||
</view>
|
||||
<view class="ai-desc-row">
|
||||
<text class="ai-desc-label">推荐剂数</text>
|
||||
<text class="ai-desc-val">{{ rxDosage || '—' }}剂</text>
|
||||
</view>
|
||||
<view class="ai-desc-row">
|
||||
<text class="ai-desc-label">每天几次</text>
|
||||
<text class="ai-desc-val">{{ rxDayDosage || '—' }}次</text>
|
||||
</view>
|
||||
<view v-if="rxReason" class="ai-desc-row ai-desc-row--full">
|
||||
<text class="ai-desc-label">为什么出这个方</text>
|
||||
<text class="ai-desc-val">{{ rxReason }}</text>
|
||||
</view>
|
||||
<view v-if="rxBasis" class="ai-desc-row ai-desc-row--full">
|
||||
<text class="ai-desc-label">根据哪些</text>
|
||||
<text class="ai-desc-val">{{ rxBasis }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="ai-rx-footer safe-area-inset-bottom">
|
||||
<view class="ai-actions">
|
||||
<view class="ai-btn ai-btn--ghost" @click="onClose">关闭</view>
|
||||
<view class="ai-btn ai-btn--ghost" @click="openGenModal">生成处方</view>
|
||||
<view class="ai-btn ai-btn--primary" @click="onConfirmImport">确认导入</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</u-popup>
|
||||
|
||||
<!-- 二级:可编辑病历预览 + 生成(用 page-container 防误退) -->
|
||||
<page-container
|
||||
v-if="genModalVisible"
|
||||
:show="genModalVisible"
|
||||
overlay
|
||||
round
|
||||
position="bottom"
|
||||
@afterleave="onGenModalLeave"
|
||||
@clickoverlay="closeGenModal"
|
||||
>
|
||||
<view class="ai-gen-sheet">
|
||||
<view class="ai-gen-hd flex-row flex-jus-sp flex-ali-center">
|
||||
<text class="fs-30 font-bold">生成处方</text>
|
||||
<text class="ai-rx-sub" @click="closeGenModal">关闭</text>
|
||||
</view>
|
||||
<text class="ai-gen-tip">点击文字可编辑,将同步到病历 · {{ typeLabel }}</text>
|
||||
<scroll-view scroll-y class="ai-gen-scroll">
|
||||
<view class="ai-gen-grid">
|
||||
<view
|
||||
v-for="row in genPreviewView"
|
||||
:key="row.key"
|
||||
class="ai-gen-row"
|
||||
:class="{ 'ai-gen-row--full': row.key == 'chief_complaint' }"
|
||||
>
|
||||
<text class="ai-gen-label">{{ row.label }}</text>
|
||||
<textarea
|
||||
v-if="editingKey == row.key"
|
||||
class="ai-gen-textarea"
|
||||
:value="row.value"
|
||||
:data-key="row.key"
|
||||
auto-height
|
||||
maxlength="2000"
|
||||
@input="onGenFieldInput"
|
||||
@blur="onGenFieldBlur"
|
||||
/>
|
||||
<text
|
||||
v-else
|
||||
class="ai-gen-val"
|
||||
:data-key="row.key"
|
||||
@click="onStartEditTap"
|
||||
>{{ row.value || '(空,点击填写)' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="ai-rx-footer">
|
||||
<view class="ai-btn ai-btn--primary ai-btn--block" @click="onGenerate">
|
||||
{{ generating ? '生成中…' : '开始生成' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</page-container>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {
|
||||
aiGeneratePrescriptionApi,
|
||||
aiListGenerationsApi,
|
||||
aiMatchPrescriptionDrugsApi,
|
||||
} from '@/api/reception.js'
|
||||
|
||||
const MR_EDIT_FIELDS = [
|
||||
{ key: 'present_illness', label: '现病史' },
|
||||
{ key: 'tongue', label: '舌象' },
|
||||
{ key: 'pulse', label: '脉象' },
|
||||
{ key: 'tcm_case', label: '中医病案' },
|
||||
{ key: 'tcm_disease', label: '中医疾病' },
|
||||
{ key: 'tcm_syndrome', label: '中医证候' },
|
||||
{ key: 'tcm_method', label: '中医治法' },
|
||||
{ key: 'diagnosis', label: '临床诊断' },
|
||||
{ key: 'treatment_advice', label: '治疗意见' },
|
||||
{ key: 'doctor_order', label: '医嘱' },
|
||||
{ key: 'allergy_history', label: '过敏史' },
|
||||
{ key: 'past_history', label: '既往史' },
|
||||
{ key: 'family_history', label: '家族史' },
|
||||
{ key: 'epidemic_history', label: '流行病学史' },
|
||||
{ key: 'personal_history', label: '个人史' },
|
||||
{ key: 'menstrual_history', label: '月经史' },
|
||||
{ key: 'marital_history', label: '婚育史' },
|
||||
{ key: 'physical_exam', label: '体征检查' },
|
||||
{ key: 'auxiliary_exam', label: '辅助检查' },
|
||||
]
|
||||
|
||||
export default {
|
||||
name: 'AiPrescriptionDrawer',
|
||||
props: {
|
||||
value: { type: Boolean, default: false },
|
||||
registerId: { type: [Number, String], default: 0 },
|
||||
storeId: { type: [Number, String], default: 0 },
|
||||
prescriptionType: { type: [Number, String], default: 0 },
|
||||
patientName: { type: String, default: '' },
|
||||
patientSex: { type: [Number, String], default: 0 },
|
||||
patientAge: { type: [Number, String], default: 0 },
|
||||
chiefComplaint: { type: String, default: '' },
|
||||
medicalRecord: { type: Object, default: () => ({}) },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
genModalVisible: false,
|
||||
aiChief: '',
|
||||
localMr: {},
|
||||
editingKey: '',
|
||||
histLoading: false,
|
||||
generating: false,
|
||||
matchLoading: false,
|
||||
historyList: [],
|
||||
historyView: [],
|
||||
historyEmpty: true,
|
||||
activeId: 0,
|
||||
matchResult: null,
|
||||
matchedList: [],
|
||||
unmatchedList: [],
|
||||
matchedView: [],
|
||||
unmatchedView: [],
|
||||
matchedCount: 0,
|
||||
unmatchedCount: 0,
|
||||
conflictView: [],
|
||||
rxDosage: 0,
|
||||
rxDayDosage: 0,
|
||||
rxReason: '',
|
||||
rxBasis: '',
|
||||
rxPrescriptionName: '',
|
||||
genPreviewView: [],
|
||||
importBtnStyle: {
|
||||
backgroundColor: '#6ACDBB',
|
||||
color: '#fff',
|
||||
border: 'none',
|
||||
width: '100%',
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
sexLabel() {
|
||||
const s = Number(this.patientSex)
|
||||
if (s === 1) return '男'
|
||||
if (s === 2) return '女'
|
||||
return '未知'
|
||||
},
|
||||
typeLabel() {
|
||||
const t = Number(this.prescriptionType)
|
||||
if (t === 1) return '中药'
|
||||
if (t === 2) return '西(中成)药'
|
||||
return t ? '类型' + t : '—'
|
||||
},
|
||||
isTcmRx() {
|
||||
return Number(this.prescriptionType) === 1
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(v) {
|
||||
this.visible = !!v
|
||||
if (v) this.bootstrapOpen()
|
||||
},
|
||||
},
|
||||
visible(v) {
|
||||
this.$emit('input', v)
|
||||
this.$emit('overlay-change', !!v)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
bootstrapOpen() {
|
||||
this.aiChief = String(this.chiefComplaint || '').trim()
|
||||
this.localMr = Object.assign({}, this.medicalRecord || {})
|
||||
this.genModalVisible = false
|
||||
this.editingKey = ''
|
||||
this.resetPreview()
|
||||
this.loadHistory().then(() => {
|
||||
if (this.historyList.length) {
|
||||
this.selectHistoryById(Number(this.historyList[0].id))
|
||||
} else {
|
||||
// 无历史:自动打开生成框
|
||||
this.openGenModal()
|
||||
}
|
||||
})
|
||||
},
|
||||
resetPreview() {
|
||||
this.matchResult = null
|
||||
this.matchedList = []
|
||||
this.unmatchedList = []
|
||||
this.conflictView = []
|
||||
this.rxDosage = 0
|
||||
this.rxDayDosage = 0
|
||||
this.rxReason = ''
|
||||
this.rxBasis = ''
|
||||
this.rxPrescriptionName = ''
|
||||
this.activeId = 0
|
||||
this.rebuildMatchView()
|
||||
},
|
||||
onClose() {
|
||||
this.visible = false
|
||||
this.genModalVisible = false
|
||||
},
|
||||
formatTime(ts) {
|
||||
const n = Number(ts || 0)
|
||||
if (!n) return '—'
|
||||
const d = new Date(n * 1000)
|
||||
const pad = (x) => (x < 10 ? '0' + x : '' + x)
|
||||
return pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
|
||||
},
|
||||
rebuildHistoryView() {
|
||||
const list = Array.isArray(this.historyList) ? this.historyList : []
|
||||
this.historyEmpty = list.length === 0
|
||||
this.historyView = list.map((row) => {
|
||||
const id = Number(row.id || 0)
|
||||
const cnt = Number(row.item_count || 0)
|
||||
const pt = Number(row.prescription_type || 0)
|
||||
const name = String(row.name || row.prescription_name || '').trim()
|
||||
let typeLabel = String(row.prescription_type_label || '').trim()
|
||||
if (!typeLabel) {
|
||||
if (pt === 1) typeLabel = '中药'
|
||||
else if (pt === 2) typeLabel = '西药(中成药)'
|
||||
else typeLabel = pt ? '类型' + pt : '—'
|
||||
}
|
||||
return {
|
||||
id: id,
|
||||
nameText: name || ('方案 #' + id),
|
||||
typeText: typeLabel + ' · ' + cnt + '味 · ' + this.formatTime(row.created_at),
|
||||
prescription_type: pt,
|
||||
}
|
||||
})
|
||||
},
|
||||
rebuildMatchView() {
|
||||
const matched = Array.isArray(this.matchedList) ? this.matchedList : []
|
||||
this.matchedCount = matched.length
|
||||
this.matchedView = matched.map((m, mi) => {
|
||||
const cands = Array.isArray(m.candidates) ? m.candidates : []
|
||||
return {
|
||||
mi: mi,
|
||||
ai_name: m.ai_name || '',
|
||||
doseUnit: String(m.dose || '') + String(m.unit || ''),
|
||||
usageText: m.usage ? ('·' + m.usage) : '',
|
||||
selected_drug_id: Number(m.selected_drug_id || 0),
|
||||
importOn: m._import !== false,
|
||||
candidates: cands.map((c) => ({
|
||||
drug_id: Number(c.drug_id || 0),
|
||||
drug_name: c.drug_name || '',
|
||||
})),
|
||||
}
|
||||
})
|
||||
const unmatched = Array.isArray(this.unmatchedList) ? this.unmatchedList : []
|
||||
this.unmatchedCount = unmatched.length
|
||||
this.unmatchedView = unmatched.map((u, ui) => ({
|
||||
ui: ui,
|
||||
ai_name: u.ai_name || '',
|
||||
tip: String(u.dose || '') + String(u.unit || '') + ' · 未匹配',
|
||||
}))
|
||||
},
|
||||
rebuildGenPreview() {
|
||||
const mr = this.localMr || {}
|
||||
const rows = [
|
||||
{
|
||||
key: 'chief_complaint',
|
||||
label: '主诉',
|
||||
value: String(this.aiChief || mr.chief_complaint || '').trim(),
|
||||
},
|
||||
]
|
||||
for (let i = 0; i < MR_EDIT_FIELDS.length; i++) {
|
||||
const f = MR_EDIT_FIELDS[i]
|
||||
const val = String(mr[f.key] || '').trim()
|
||||
if (val) rows.push({ key: f.key, label: f.label, value: val })
|
||||
}
|
||||
this.genPreviewView = rows
|
||||
},
|
||||
applyRxMeta(data) {
|
||||
this.rxDosage = Number((data && data.dosage) || 0)
|
||||
this.rxDayDosage = Number((data && data.day_dosage) || 0)
|
||||
this.rxReason = String((data && data.reason) || '').trim()
|
||||
this.rxBasis = String((data && data.basis) || '').trim()
|
||||
this.rxPrescriptionName = String((data && data.prescription_name) || '').trim()
|
||||
},
|
||||
applyConflict(conflict) {
|
||||
const msgs = (conflict && conflict.messages) || []
|
||||
this.conflictView = (Array.isArray(msgs) ? msgs : [])
|
||||
.map((m) => String(m || '').trim())
|
||||
.filter((m) => !!m)
|
||||
},
|
||||
applyMatch(match) {
|
||||
this.matchResult = match || null
|
||||
const matched = (match && match.matched) || []
|
||||
this.matchedList = (Array.isArray(matched) ? matched : []).map((m) =>
|
||||
Object.assign({}, m, { _import: true }),
|
||||
)
|
||||
this.unmatchedList = Array.isArray(match && match.unmatched) ? match.unmatched : []
|
||||
this.rebuildMatchView()
|
||||
},
|
||||
async loadHistory() {
|
||||
this.histLoading = true
|
||||
try {
|
||||
const res = await aiListGenerationsApi({
|
||||
register_id: Number(this.registerId),
|
||||
scene: 'prescription',
|
||||
limit: 30,
|
||||
})
|
||||
const list = (res && res.data) || res || []
|
||||
this.historyList = Array.isArray(list) ? list : []
|
||||
this.rebuildHistoryView()
|
||||
} catch (e) {
|
||||
this.historyList = []
|
||||
this.rebuildHistoryView()
|
||||
} finally {
|
||||
this.histLoading = false
|
||||
}
|
||||
},
|
||||
openGenModal() {
|
||||
if (!Number(this.prescriptionType || 0)) {
|
||||
uni.showToast({ title: '请先选择处方类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.editingKey = ''
|
||||
this.rebuildGenPreview()
|
||||
this.genModalVisible = true
|
||||
},
|
||||
closeGenModal() {
|
||||
this.genModalVisible = false
|
||||
this.editingKey = ''
|
||||
},
|
||||
onGenModalLeave() {
|
||||
this.genModalVisible = false
|
||||
},
|
||||
onStartEditTap(e) {
|
||||
const key = e && e.currentTarget && e.currentTarget.dataset
|
||||
? e.currentTarget.dataset.key
|
||||
: ''
|
||||
if (!key) return
|
||||
this.editingKey = key
|
||||
},
|
||||
onGenFieldInput(e) {
|
||||
const key = e && e.currentTarget && e.currentTarget.dataset
|
||||
? e.currentTarget.dataset.key
|
||||
: this.editingKey
|
||||
const val = (e && e.detail && e.detail.value) || ''
|
||||
if (!key) return
|
||||
if (key === 'chief_complaint') {
|
||||
this.aiChief = val
|
||||
this.localMr = Object.assign({}, this.localMr, { chief_complaint: val })
|
||||
} else {
|
||||
const next = Object.assign({}, this.localMr)
|
||||
next[key] = val
|
||||
this.localMr = next
|
||||
}
|
||||
// 同步到父页病历
|
||||
const payload = {}
|
||||
payload[key] = val
|
||||
this.$emit('sync-medical-record', payload)
|
||||
// 更新预览行展示值
|
||||
for (let i = 0; i < this.genPreviewView.length; i++) {
|
||||
if (this.genPreviewView[i].key === key) {
|
||||
this.$set(this.genPreviewView[i], 'value', val)
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
onGenFieldBlur() {
|
||||
this.editingKey = ''
|
||||
},
|
||||
async onGenerate() {
|
||||
if (this.generating) return
|
||||
const chief = String(this.aiChief || '').trim()
|
||||
if (!chief) {
|
||||
uni.showToast({ title: '请填写主诉后再生成', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const type = Number(this.prescriptionType || 0)
|
||||
if (!type) {
|
||||
uni.showToast({ title: '请先选择处方类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 先关二级弹窗,回主抽屉等待生成
|
||||
this.closeGenModal()
|
||||
this.generating = true
|
||||
try {
|
||||
const res = await aiGeneratePrescriptionApi({
|
||||
register_id: Number(this.registerId),
|
||||
store_id: Number(this.storeId || 0) || undefined,
|
||||
prescription_type: type,
|
||||
chief_complaint: chief,
|
||||
medical_record: Object.assign({}, this.localMr || {}, {
|
||||
chief_complaint: chief,
|
||||
}),
|
||||
})
|
||||
const data = (res && res.data) || res || {}
|
||||
await this.loadHistory()
|
||||
this.activeId = Number(data.generation_id || 0)
|
||||
this.applyMatch(data.match || null)
|
||||
this.applyConflict(data.conflict || null)
|
||||
this.applyRxMeta(data)
|
||||
uni.showToast({ title: '已生成', icon: 'success' })
|
||||
} catch (e) {
|
||||
const msg = (e && e.message) || (e && e.msg) || '生成失败'
|
||||
uni.showToast({ title: String(msg), icon: 'none' })
|
||||
} finally {
|
||||
this.generating = false
|
||||
}
|
||||
},
|
||||
onSelectHistoryTap(e) {
|
||||
const raw = e && e.currentTarget && e.currentTarget.dataset
|
||||
? e.currentTarget.dataset.id
|
||||
: 0
|
||||
const id = Number(raw || 0)
|
||||
if (!id) return
|
||||
this.selectHistoryById(id)
|
||||
},
|
||||
async selectHistoryById(id) {
|
||||
this.activeId = id
|
||||
this.matchLoading = true
|
||||
try {
|
||||
let pt = Number(this.prescriptionType || 0)
|
||||
let histName = ''
|
||||
for (let i = 0; i < this.historyList.length; i++) {
|
||||
if (Number(this.historyList[i].id) === id) {
|
||||
pt = Number(this.historyList[i].prescription_type || pt)
|
||||
histName = String(
|
||||
this.historyList[i].name || this.historyList[i].prescription_name || '',
|
||||
).trim()
|
||||
break
|
||||
}
|
||||
}
|
||||
const res = await aiMatchPrescriptionDrugsApi({
|
||||
generation_id: id,
|
||||
register_id: Number(this.registerId),
|
||||
store_id: Number(this.storeId || 0) || undefined,
|
||||
prescription_type: pt,
|
||||
})
|
||||
const data = (res && res.data) || res || {}
|
||||
this.applyMatch(data)
|
||||
this.applyConflict(data.conflict || null)
|
||||
this.applyRxMeta({
|
||||
dosage: data.dosage,
|
||||
day_dosage: data.day_dosage,
|
||||
reason: data.reason,
|
||||
basis: data.basis,
|
||||
prescription_name: data.prescription_name || histName,
|
||||
})
|
||||
} catch (e) {
|
||||
this.matchResult = null
|
||||
this.matchedList = []
|
||||
this.unmatchedList = []
|
||||
this.conflictView = []
|
||||
this.rebuildMatchView()
|
||||
const msg = (e && e.message) || (e && e.msg) || '对照失败'
|
||||
uni.showToast({ title: String(msg), icon: 'none' })
|
||||
} finally {
|
||||
this.matchLoading = false
|
||||
}
|
||||
},
|
||||
onPickCandidateTap(e) {
|
||||
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {}
|
||||
const mi = Number(ds.mi || 0)
|
||||
const drugId = Number(ds.drugId || ds['drug-id'] || 0)
|
||||
if (!this.matchedList[mi]) return
|
||||
this.$set(this.matchedList[mi], 'selected_drug_id', drugId)
|
||||
this.rebuildMatchView()
|
||||
},
|
||||
onToggleImportTap(e) {
|
||||
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {}
|
||||
const mi = Number(ds.mi || 0)
|
||||
const checked = !!(e && e.detail && e.detail.value)
|
||||
if (!this.matchedList[mi]) return
|
||||
this.$set(this.matchedList[mi], '_import', checked)
|
||||
this.rebuildMatchView()
|
||||
},
|
||||
onSearchUnmatchedTap(e) {
|
||||
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {}
|
||||
const ui = Number(ds.ui || 0)
|
||||
const row = this.unmatchedList[ui]
|
||||
this.$emit('search-drug', (row && row.ai_name) || '')
|
||||
this.visible = false
|
||||
},
|
||||
onConfirmImport() {
|
||||
const drugs = []
|
||||
for (let i = 0; i < this.matchedList.length; i++) {
|
||||
const m = this.matchedList[i]
|
||||
if (!m || m._import === false) continue
|
||||
const sid = Number(m.selected_drug_id || 0)
|
||||
if (!sid) continue
|
||||
const cands = m.candidates || []
|
||||
let picked = null
|
||||
for (let j = 0; j < cands.length; j++) {
|
||||
if (Number(cands[j].drug_id) === sid) {
|
||||
picked = cands[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!picked) continue
|
||||
drugs.push({
|
||||
candidate: picked,
|
||||
dose: m.dose || '',
|
||||
unit: m.unit || '',
|
||||
usage: m.usage || '',
|
||||
ai_name: m.ai_name || '',
|
||||
})
|
||||
}
|
||||
if (!drugs.length) {
|
||||
uni.showToast({ title: '请先勾选已对照药品', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.$emit('import', {
|
||||
rows: drugs,
|
||||
dosage: this.rxDosage || undefined,
|
||||
day_dosage: this.rxDayDosage || undefined,
|
||||
})
|
||||
this.visible = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.ai-rx-drawer {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f7f8fa;
|
||||
}
|
||||
.ai-rx-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 24rpx 32rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.ai-rx-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.ai-rx-sub {
|
||||
font-size: 24rpx;
|
||||
color: #6acdbb;
|
||||
}
|
||||
.ai-rx-meta {
|
||||
margin: 0 0 12rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid #eee;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ai-desc-row {
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12rpx 16rpx;
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
}
|
||||
.ai-desc-row--full {
|
||||
width: 100%;
|
||||
border-right: none;
|
||||
}
|
||||
.ai-desc-label {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.ai-desc-val {
|
||||
font-size: 24rpx;
|
||||
color: #2a2e35;
|
||||
line-height: 1.45;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.ai-conflict {
|
||||
margin: 0 24rpx 12rpx;
|
||||
padding: 16rpx;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffe0b2;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
.ai-conflict-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #ad6800;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.ai-conflict-line {
|
||||
font-size: 22rpx;
|
||||
color: #874d00;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.ai-rx-body {
|
||||
flex: 1;
|
||||
/* 微信 scroll-view 在 flex 下需 height:0 才能吃满剩余高度 */
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ai-rx-hist {
|
||||
width: 220rpx;
|
||||
background: #fff;
|
||||
border-right: 1px solid #eee;
|
||||
height: 100%;
|
||||
}
|
||||
.ai-rx-match {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
padding: 16rpx 20rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ai-rx-dose-row {
|
||||
margin-top: 10rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ai-rx-dose-item {
|
||||
margin-right: 32rpx;
|
||||
margin-bottom: 4rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
.ai-rx-dose-val {
|
||||
font-weight: bold;
|
||||
color: #2a2e35;
|
||||
}
|
||||
.ai-hist-hd {
|
||||
padding: 16rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.ai-hist-item {
|
||||
padding: 16rpx;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.ai-hist-item--active {
|
||||
background: #e8f8f4;
|
||||
}
|
||||
.ai-hist-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ai-sec-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: bold;
|
||||
color: #666;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.ai-tcm-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -6rpx;
|
||||
}
|
||||
.ai-tcm-cell {
|
||||
width: calc(50% - 12rpx);
|
||||
margin: 0 6rpx 12rpx;
|
||||
padding: 12rpx;
|
||||
background: #fff;
|
||||
border-radius: 10rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ai-match-row {
|
||||
padding: 12rpx 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
.ai-match-row--card {
|
||||
margin-bottom: 12rpx;
|
||||
padding: 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 12rpx;
|
||||
border-bottom: none;
|
||||
}
|
||||
.ai-match-row--warn {
|
||||
margin-bottom: 12rpx;
|
||||
padding: 16rpx;
|
||||
background: #fff7e6;
|
||||
border-radius: 12rpx;
|
||||
border-bottom: none;
|
||||
}
|
||||
.ai-match-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
.ai-cand {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
margin-right: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.ai-cand--sm {
|
||||
padding: 4rpx 10rpx;
|
||||
margin-right: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.ai-cand--on {
|
||||
border-color: #6acdbb;
|
||||
background: #e8f8f4;
|
||||
}
|
||||
.ai-link {
|
||||
color: #6acdbb;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.ai-rx-footer {
|
||||
padding: 16rpx 24rpx 24rpx;
|
||||
background: #fff;
|
||||
}
|
||||
.ai-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.ai-btn {
|
||||
padding: 16rpx 28rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.ai-btn--ghost {
|
||||
background: #f5f5f5;
|
||||
color: #666;
|
||||
}
|
||||
.ai-btn--primary {
|
||||
background: #6acdbb;
|
||||
color: #fff;
|
||||
}
|
||||
.ai-btn--block {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
.ai-gen-sheet {
|
||||
background: #fff;
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
max-height: 78vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
.ai-gen-hd {
|
||||
padding: 24rpx 32rpx 8rpx;
|
||||
}
|
||||
.ai-gen-tip {
|
||||
padding: 0 32rpx 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
.ai-gen-scroll {
|
||||
max-height: 52vh;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ai-gen-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
margin: 0 -6rpx;
|
||||
}
|
||||
.ai-gen-row {
|
||||
width: calc(50% - 12rpx);
|
||||
margin: 0 6rpx 8rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 10rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ai-gen-row--full {
|
||||
width: calc(100% - 12rpx);
|
||||
}
|
||||
.ai-gen-label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.ai-gen-val {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #2a2e35;
|
||||
line-height: 1.5;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.ai-gen-textarea {
|
||||
width: 100%;
|
||||
min-height: 80rpx;
|
||||
font-size: 24rpx;
|
||||
color: #2a2e35;
|
||||
background: #fff;
|
||||
padding: 8rpx;
|
||||
box-sizing: border-box;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
.color-sub {
|
||||
color: #999;
|
||||
}
|
||||
.p-24 {
|
||||
padding: 24rpx;
|
||||
}
|
||||
.p-32 {
|
||||
padding: 32rpx;
|
||||
}
|
||||
.m-t-8 {
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.m-t-16 {
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.fs-18 {
|
||||
font-size: 18rpx;
|
||||
}
|
||||
.fs-20 {
|
||||
font-size: 20rpx;
|
||||
}
|
||||
.fs-22 {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.fs-24 {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.fs-26 {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.fs-30 {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
.font-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.flex-wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.flex-ali-center {
|
||||
align-items: center;
|
||||
}
|
||||
.flex-jus-sp {
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -237,6 +237,17 @@
|
||||
:ripple="true">
|
||||
常用方
|
||||
</u-button>
|
||||
<view v-if="!isSpecialPrescriptionCartLocked" class="m-l-16">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
shape="circle"
|
||||
size="mini"
|
||||
:custom-style="{backgroundColor: 'transparent !important',border:'1px solid #6ACDBB',color:'#6ACDBB'}"
|
||||
plain
|
||||
@click="handleOpenAiPrescriptionDrawer">
|
||||
AI辅助出方
|
||||
</u-button>
|
||||
</view>
|
||||
<view v-if="!isSpecialPrescriptionCartLocked" class="m-l-16">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
@@ -316,6 +327,17 @@
|
||||
:ripple="true">
|
||||
常用方
|
||||
</u-button>
|
||||
<view v-if="!isSpecialPrescriptionCartLocked" class="m-l-16">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
shape="circle"
|
||||
size="mini"
|
||||
:custom-style="{backgroundColor: 'transparent !important',border:'1px solid #6ACDBB',color:'#6ACDBB'}"
|
||||
plain
|
||||
@click="handleOpenAiPrescriptionDrawer">
|
||||
AI辅助出方
|
||||
</u-button>
|
||||
</view>
|
||||
<view v-if="!isSpecialPrescriptionCartLocked" class="m-l-16">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
@@ -385,6 +407,17 @@
|
||||
<text class="fs-28 text-gray m-l-8">{{ activeCategoryLabel }}</text>
|
||||
</view>
|
||||
<view class="flex-row m-l-163" style="height: 50rpx;">
|
||||
<view v-if="!isSpecialPrescriptionCartLocked" class="m-l-16">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
shape="circle"
|
||||
size="mini"
|
||||
:custom-style="{backgroundColor: 'transparent !important',border:'1px solid #6ACDBB',color:'#6ACDBB'}"
|
||||
plain
|
||||
@click="handleOpenAiPrescriptionDrawer">
|
||||
AI辅助出方
|
||||
</u-button>
|
||||
</view>
|
||||
<view v-if="!isSpecialPrescriptionCartLocked" class="m-l-16">
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
@@ -544,11 +577,18 @@
|
||||
|
||||
<!-- 底部操作栏:fixed;病历 Tab 必须 v-if 卸载,微信 v-show 对 fixed 无效会串到病历 -->
|
||||
<view v-if="isRxMainVisible" class="bottom flex-jus-sp flex-ali-center safe-area-inset-bottom">
|
||||
<view
|
||||
v-if="!isSalespersonTransferMode && !isCommonPrescription && supportsCommonPrescriptionTemplate"
|
||||
class="flex-row flex-ali-center"
|
||||
>
|
||||
<view @click="handleSaveAsCommonPrescription" class="btnText">另存常用方</view>
|
||||
<view class="flex-row flex-ali-center bottom-left-actions">
|
||||
<view
|
||||
v-if="!isSalespersonTransferMode && !isCommonPrescription && supportsCommonPrescriptionTemplate"
|
||||
@click="handleSaveAsCommonPrescription"
|
||||
class="btnText"
|
||||
>另存常用方</view>
|
||||
<!-- AI入口放底部,避免顶部三按钮挤出屏幕 -->
|
||||
<view
|
||||
v-if="!isSpecialPrescriptionCartLocked && !isSalespersonTransferMode && !isCommonPrescription"
|
||||
@click="handleOpenAiPrescriptionDrawer"
|
||||
class="btnText"
|
||||
>AI辅助出方</view>
|
||||
</view>
|
||||
<u-button
|
||||
:throttle-time="0"
|
||||
@@ -584,6 +624,7 @@
|
||||
:medical-advice="medicalAdvice"
|
||||
:diagnosis-readonly="isOnlineRevisit"
|
||||
:active="isMrMainVisible"
|
||||
:patient-name="(patientInfo && patientInfo.name) || ''"
|
||||
:patient-sex="(patientInfo && patientInfo.sex) || 0"
|
||||
:patient-age="(patientInfo && patientInfo.age) || 0"
|
||||
@update:diagnosis="onMrDiagnosisUpdate"
|
||||
@@ -702,6 +743,23 @@
|
||||
@close="closePreSendPriceAdjust"
|
||||
@confirm="onPreSendPriceAdjustConfirm"
|
||||
/>
|
||||
|
||||
<!-- AI 给处方:确认主诉 → 生成 → 预览对照 → 确认导入 -->
|
||||
<AiPrescriptionDrawer
|
||||
v-model="showAiPrescriptionDrawer"
|
||||
:register-id="registerId"
|
||||
:store-id="currentStoreId"
|
||||
:prescription-type="activeCategory"
|
||||
:patient-name="(patientInfo && patientInfo.name) || ''"
|
||||
:patient-sex="(patientInfo && patientInfo.sex) || 0"
|
||||
:patient-age="(patientInfo && patientInfo.age) || 0"
|
||||
:chief-complaint="aiRxChiefComplaint"
|
||||
:medical-record="aiRxMedicalRecord"
|
||||
@overlay-change="onAiPrescriptionOverlayChange"
|
||||
@import="handleImportAiPrescription"
|
||||
@search-drug="handleAiSearchUnmatchedDrug"
|
||||
@sync-medical-record="handleSyncAiMedicalRecord"
|
||||
/>
|
||||
</block>
|
||||
</view>
|
||||
</template>
|
||||
@@ -737,6 +795,7 @@ import { PrescriptionValidator } from './utils/prescriptionValidator.js';
|
||||
import DiagnosisModal from './components/modals/DiagnosisModal.vue';
|
||||
import DoctorOrderModal from './components/modals/DoctorOrderModal.vue';
|
||||
import MedicalRecordPanel from './components/MedicalRecordPanel.vue';
|
||||
import AiPrescriptionDrawer from './components/AiPrescriptionDrawer.vue';
|
||||
import { saveMedicalRecordApi } from '@/api/reception.js';
|
||||
import CommonPrescriptionModal from './components/modals/CommonPrescriptionModal.vue';
|
||||
import WesternMedicineModal from './components/modals/WesternMedicineModal.vue';
|
||||
@@ -781,6 +840,7 @@ const PRESCRIPTION_OVERLAY_KEYS = [
|
||||
'showTransferPatientDrawer',
|
||||
'showWarehouseSelectDrawer',
|
||||
'showMedicalRecordOverlay',
|
||||
'showAiPrescriptionDrawer',
|
||||
];
|
||||
|
||||
export default {
|
||||
@@ -789,6 +849,7 @@ export default {
|
||||
DiagnosisModal,
|
||||
DoctorOrderModal,
|
||||
MedicalRecordPanel,
|
||||
AiPrescriptionDrawer,
|
||||
CommonPrescriptionModal,
|
||||
WesternMedicineModal,
|
||||
ChineseMedicineModal,
|
||||
@@ -860,6 +921,11 @@ export default {
|
||||
showDiagnosisModal: false,
|
||||
showDoctorOrderModal: false,
|
||||
showCommonPrescriptionModal: false,
|
||||
showAiPrescriptionDrawer: false,
|
||||
/** 打开 AI 出方时带入的主诉(可在抽屉内再编辑) */
|
||||
aiRxChiefComplaint: '',
|
||||
/** 打开 AI 出方时带入的完整病历快照 */
|
||||
aiRxMedicalRecord: {},
|
||||
showWesternMedicineModal: false,
|
||||
westernModalInitialKeyword: '',
|
||||
showChineseMedicineModal: false,
|
||||
@@ -1113,15 +1179,16 @@ export default {
|
||||
isSpecialPrescriptionCartLocked() {
|
||||
return isSpecialPrescriptionCartLocked(this);
|
||||
},
|
||||
/** 底部发送按钮宽度(左侧仅「另存常用方」时需留出空间) */
|
||||
/** 底部发送按钮宽度(左侧有「另存常用方 / AI辅助出方」时收窄) */
|
||||
bottomSendBtnWidth() {
|
||||
if (this.isSalespersonTransferMode || this.isCommonPrescription) {
|
||||
return '686rpx';
|
||||
}
|
||||
// 左侧至少有 AI辅助出方;有常用方模板时再多一个入口
|
||||
if (this.supportsCommonPrescriptionTemplate) {
|
||||
return '400rpx';
|
||||
return '320rpx';
|
||||
}
|
||||
return '686rpx';
|
||||
return '480rpx';
|
||||
},
|
||||
navbarTitle() {
|
||||
if (this.isSalespersonTransferMode) {
|
||||
@@ -1975,6 +2042,175 @@ export default {
|
||||
this.saveToLocalStorage();
|
||||
},
|
||||
handleOpenCommonPrescriptionModal() { this.showCommonPrescriptionModal = true; },
|
||||
/**
|
||||
* 打开 AI 给处方抽屉:带入完整病历(含未保存),先确认再生成
|
||||
*/
|
||||
handleOpenAiPrescriptionDrawer() {
|
||||
if (guardSpecialPrescriptionCartEdit(this)) return;
|
||||
if (!this.registerId) {
|
||||
this.$toast('挂号无效');
|
||||
return;
|
||||
}
|
||||
if (!this.activeCategory) {
|
||||
this.$toast('请先选择处方类型');
|
||||
return;
|
||||
}
|
||||
const panel = this.$refs.medicalRecordPanel;
|
||||
const payload = panel && panel.getPayload ? panel.getPayload() : {};
|
||||
this.aiRxChiefComplaint = String((payload && payload.chief_complaint) || '');
|
||||
this.aiRxMedicalRecord = Object.assign({}, payload || {});
|
||||
this.showAiPrescriptionDrawer = true;
|
||||
},
|
||||
onAiPrescriptionOverlayChange(open) {
|
||||
this.showAiPrescriptionDrawer = !!open;
|
||||
},
|
||||
/**
|
||||
* AI 出方预览改病历字段时回写病历面板
|
||||
*/
|
||||
handleSyncAiMedicalRecord(partial) {
|
||||
const panel = this.$refs.medicalRecordPanel;
|
||||
if (panel && panel.patchFields) {
|
||||
panel.patchFields(partial || {});
|
||||
}
|
||||
// 本地快照也更新,避免再次打开仍是旧值
|
||||
this.aiRxMedicalRecord = Object.assign({}, this.aiRxMedicalRecord || {}, partial || {});
|
||||
if (partial && partial.chief_complaint != null) {
|
||||
this.aiRxChiefComplaint = String(partial.chief_complaint || '');
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 未对照药名:打开对应选药弹窗并带入关键词
|
||||
*/
|
||||
handleAiSearchUnmatchedDrug(keyword) {
|
||||
const key = String(keyword || '');
|
||||
if (this.activeCategory === 1) {
|
||||
this.chineseModalInitialKeyword = key;
|
||||
this.showChineseMedicineModal = true;
|
||||
return;
|
||||
}
|
||||
if (this.activeCategory === 2) {
|
||||
this.westernModalInitialKeyword = key;
|
||||
this.showWesternMedicineModal = true;
|
||||
return;
|
||||
}
|
||||
this.showSimpleProductModal = true;
|
||||
},
|
||||
/**
|
||||
* 将 AI 对照结果覆盖导入当前处方(非追加)
|
||||
* 中药同步剂数/每日次数,usage 映射先煎后下 way_id
|
||||
* @param {Object} payload { rows, dosage, day_dosage }
|
||||
*/
|
||||
handleImportAiPrescription(payload) {
|
||||
if (guardSpecialPrescriptionCartEdit(this)) return;
|
||||
const list = Array.isArray(payload && payload.rows) ? payload.rows : (Array.isArray(payload) ? payload : []);
|
||||
if (!list.length) {
|
||||
this.$toast('没有可导入的药品');
|
||||
return;
|
||||
}
|
||||
const wayList = (this.drugUseList && this.drugUseList.drug_use_way) || [];
|
||||
const resolveWayId = (usage) => {
|
||||
const u = String(usage || '').trim();
|
||||
if (!u) return 0;
|
||||
const found = wayList.find((w) => String(w.name || '') === u || String(w.name || '').indexOf(u) >= 0);
|
||||
return found ? Number(found.id || 0) : 0;
|
||||
};
|
||||
const doOverwrite = () => {
|
||||
clearAppliedSpecialPrescription(this);
|
||||
const next = [];
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const row = list[i] || {};
|
||||
const c = row.candidate || {};
|
||||
const drugId = Number(c.drug_id || 0);
|
||||
if (!drugId) continue;
|
||||
if (next.find((item) => Number(item.id) === drugId || Number(item.drug_id) === drugId)) {
|
||||
continue;
|
||||
}
|
||||
const doseNum = parseFloat(row.dose);
|
||||
const qty = !isNaN(doseNum) && doseNum > 0 ? doseNum : (c.number || 1);
|
||||
if (this.activeCategory === 1) {
|
||||
const wayId = resolveWayId(row.usage || '');
|
||||
const newDrug = {
|
||||
index_id: c.id || drugId,
|
||||
id: drugId,
|
||||
drug_id: drugId,
|
||||
drug_name: c.drug_name || row.ai_name,
|
||||
name: c.drug_name || row.ai_name,
|
||||
number: qty,
|
||||
price: parseFloat(c.price) || 0,
|
||||
specification: c.specification || '',
|
||||
image: c.image || '',
|
||||
unit: c.unit || { id: 0, name: row.unit || 'g' },
|
||||
way_id: wayId,
|
||||
use_ways: wayList.find((item) => item.id === wayId),
|
||||
};
|
||||
next.push(this.prepareDrugForCart(newDrug));
|
||||
continue;
|
||||
}
|
||||
if (this.activeCategory === 2) {
|
||||
const d = c.drug || {};
|
||||
const newDrug = {
|
||||
index_id: c.id || drugId,
|
||||
id: drugId,
|
||||
drug_id: drugId,
|
||||
drug_name: c.drug_name || d.drug_name || row.ai_name,
|
||||
number: qty,
|
||||
select_number: qty,
|
||||
price: parseFloat(c.price) || 0,
|
||||
specification: c.specification || d.specification || '',
|
||||
image: c.image || d.image || '',
|
||||
time_id: c.time_id || d.time_id || 0,
|
||||
type_id: c.type_id || d.type_id || 0,
|
||||
frequency_id: c.frequency_id || d.frequency_id || 0,
|
||||
unit_id: c.unit_id || d.unit_id || 0,
|
||||
use_num: this.drugUseList.drug_time && this.drugUseList.drug_time.find((item) => item.id === (c.time_id || d.time_id || 0)),
|
||||
use_type: this.drugUseList.drug_use_type && this.drugUseList.drug_use_type.find((item) => item.id === (c.type_id || d.type_id || 0)),
|
||||
use_frequency: this.drugUseList.drug_use_frequency && this.drugUseList.drug_use_frequency.find((item) => item.id === (c.frequency_id || d.frequency_id || 0)),
|
||||
unit: this.drugUseList.drug_unit && this.drugUseList.drug_unit.find((item) => item.id === (c.unit_id || d.unit_id || 0)),
|
||||
};
|
||||
next.push(this.prepareDrugForCart(newDrug));
|
||||
continue;
|
||||
}
|
||||
const newProduct = {
|
||||
index_id: c.id || drugId,
|
||||
id: drugId,
|
||||
drug_id: drugId,
|
||||
drug_name: c.drug_name || row.ai_name,
|
||||
number: 1,
|
||||
select_number: 1,
|
||||
price: parseFloat(c.price) || 0,
|
||||
image: c.image || '',
|
||||
type: this.activeCategory,
|
||||
};
|
||||
next.push(this.prepareDrugForCart(newProduct));
|
||||
}
|
||||
if (!next.length) {
|
||||
this.$toast('没有可导入的药品');
|
||||
return;
|
||||
}
|
||||
this.currentDrugs = next;
|
||||
if (this.activeCategory === 1) {
|
||||
if (payload && Number(payload.dosage) > 0) {
|
||||
this.chineseConfig.dosage = Number(payload.dosage);
|
||||
}
|
||||
if (payload && Number(payload.day_dosage) > 0) {
|
||||
this.chineseConfig.dayDosage = Number(payload.day_dosage);
|
||||
}
|
||||
}
|
||||
this.saveToLocalStorage();
|
||||
this.$toast(`已覆盖导入 ${next.length} 个药品`);
|
||||
};
|
||||
if (this.currentDrugs && this.currentDrugs.length > 0) {
|
||||
uni.showModal({
|
||||
title: '覆盖当前处方',
|
||||
content: '导入将清空并覆盖现有处方药品,是否继续?',
|
||||
success: ({ confirm }) => {
|
||||
if (confirm) doOverwrite();
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
doOverwrite();
|
||||
},
|
||||
async handleApplySpecialPrescription() {
|
||||
if (!this.registerId || this.applyingSpecialPrescription) return;
|
||||
this.applyingSpecialPrescription = true;
|
||||
|
||||
Reference in New Issue
Block a user