feat: VIP功能绑定、VIP病历模块、字典模块
This commit is contained in:
@@ -493,3 +493,48 @@ export function getDrugsByRegisterIdApi(data) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 获取病历 */
|
||||||
|
export function getMedicalRecordApi(data) {
|
||||||
|
return req.request({
|
||||||
|
url: '/newApi/doctor-reception-wx/get-medical-record',
|
||||||
|
method: 'GET',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存病历 */
|
||||||
|
export function saveMedicalRecordApi(data) {
|
||||||
|
return req.request({
|
||||||
|
url: '/newApi/doctor-reception-wx/save-medical-record',
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清空病历 */
|
||||||
|
export function clearMedicalRecordApi(data) {
|
||||||
|
return req.request({
|
||||||
|
url: '/newApi/doctor-reception-wx/clear-medical-record',
|
||||||
|
method: 'POST',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 引用最近病历 */
|
||||||
|
export function latestMedicalRecordApi(data) {
|
||||||
|
return req.request({
|
||||||
|
url: '/newApi/doctor-reception-wx/latest-medical-record',
|
||||||
|
method: 'GET',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 病历词条检索 */
|
||||||
|
export function searchMedicalRecordEntryApi(data) {
|
||||||
|
return req.request({
|
||||||
|
url: '/newApi/doctor-reception-wx/search-medical-record-entry',
|
||||||
|
method: 'GET',
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
:duration="200"
|
:duration="200"
|
||||||
@change="onTabSwiperChange"
|
@change="onTabSwiperChange"
|
||||||
>
|
>
|
||||||
<swiper-item v-for="t in visibleTabs" :key="'sw-' + t.key">
|
<swiper-item v-for="t in visibleTabs" :key="t.key">
|
||||||
<scroll-view
|
<scroll-view
|
||||||
scroll-y
|
scroll-y
|
||||||
class="tab-scroll"
|
class="tab-scroll"
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
</view>
|
</view>
|
||||||
<view v-else-if="t.key === 'answers'" class="card">
|
<view v-else-if="t.key === 'answers'" class="card">
|
||||||
<view v-if="!answerRecords.length" class="empty-inline">暂无回答</view>
|
<view v-if="!answerRecords.length" class="empty-inline">暂无回答</view>
|
||||||
<view v-for="(a, aidx) in answerRecords" :key="'a-' + aidx" class="row answer-row">
|
<view v-for="(a, aidx) in answerRecords" :key="aidx" class="row answer-row">
|
||||||
<text class="label">{{ a.question || '问题' }}</text>
|
<text class="label">{{ a.question || '问题' }}</text>
|
||||||
<text class="value">{{ a.answer || '—' }}</text>
|
<text class="value">{{ a.answer || '—' }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -0,0 +1,771 @@
|
|||||||
|
<template>
|
||||||
|
<!--
|
||||||
|
医生小程序病历面板
|
||||||
|
诊断/医嘱与处方同源:emit 打开父页常用诊断/常用医嘱弹窗
|
||||||
|
其余字段走病历词条抽屉
|
||||||
|
模板禁止 ?? ?. 与 :class 方法调用
|
||||||
|
-->
|
||||||
|
<view class="mr-panel">
|
||||||
|
<!-- 三个操作按钮吸顶,滚动字段时始终可见 -->
|
||||||
|
<view class="mr-actions flex-row flex-wrap">
|
||||||
|
<view class="mr-btn danger" @click="onClear">清空病历</view>
|
||||||
|
<view class="mr-btn" @click="onCite">引用历史</view>
|
||||||
|
<view class="mr-btn primary" @click="onSave">保存病历</view>
|
||||||
|
</view>
|
||||||
|
<scroll-view scroll-y class="mr-scroll" :enable-back-to-top="true">
|
||||||
|
<view class="mr-scroll-inner">
|
||||||
|
<view class="mr-field">
|
||||||
|
<d-text text="诊断(与处方同步)" className="fs-28 font-bold" color="#333" />
|
||||||
|
<view class="mr-entry-row" @click="onOpenDiagnosis">
|
||||||
|
<text class="color-sub fs-24">{{ diagnosisReadonly ? '去选择常用诊断' : '选择常用诊断' }}</text>
|
||||||
|
</view>
|
||||||
|
<u-input
|
||||||
|
v-model="localDiagnosis"
|
||||||
|
type="textarea"
|
||||||
|
:auto-height="true"
|
||||||
|
:disabled="diagnosisReadonly"
|
||||||
|
:placeholder="diagnosisReadonly ? '请通过常用诊断选择' : '临床诊断'"
|
||||||
|
:custom-style="areaStyle"
|
||||||
|
@input="emitDiagnosis"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view v-for="item in leftFields" :key="item.code" class="mr-field">
|
||||||
|
<d-text :text="item.label" className="fs-28 font-bold" color="#333" />
|
||||||
|
<!-- 中医病案:舌脉 → 辨证 → 证候|疾病 → 治法 -->
|
||||||
|
<view v-if="item.code === 'tcm_case'" class="m-b-8">
|
||||||
|
<view class="flex-row m-b-8">
|
||||||
|
<view class="mr-entry-row flex-1 m-r-8" @click="openEntry('tongue')">
|
||||||
|
<text class="color-sub fs-24">舌象词条</text>
|
||||||
|
</view>
|
||||||
|
<view class="mr-entry-row flex-1" @click="openEntry('pulse')">
|
||||||
|
<text class="color-sub fs-24">脉象词条</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view class="flex-row m-b-8">
|
||||||
|
<u-input v-model="form.tongue" placeholder="舌象" class="flex-1 m-r-8" />
|
||||||
|
<u-input v-model="form.pulse" placeholder="脉象" class="flex-1" />
|
||||||
|
</view>
|
||||||
|
<view class="mr-entry-row m-b-8" @click="openEntry('tcm_case')">
|
||||||
|
<text class="color-sub fs-24">选择辨证/病案词条</text>
|
||||||
|
</view>
|
||||||
|
<u-input
|
||||||
|
v-model="form.tcm_case"
|
||||||
|
type="textarea"
|
||||||
|
:auto-height="true"
|
||||||
|
placeholder="辨证/病案"
|
||||||
|
:custom-style="areaStyle"
|
||||||
|
class="m-b-8"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-for="row in tcmCaseSubRows"
|
||||||
|
:key="row.key"
|
||||||
|
class="flex-row m-b-8"
|
||||||
|
>
|
||||||
|
<view
|
||||||
|
v-for="(sub, colIdx) in row.cols"
|
||||||
|
:key="sub._key"
|
||||||
|
class="flex-1"
|
||||||
|
:class="{ 'm-r-8': colIdx === 0 }"
|
||||||
|
>
|
||||||
|
<view v-if="!sub._empty">
|
||||||
|
<d-text :text="sub.label" className="fs-24" color="#666" />
|
||||||
|
<view class="mr-entry-row" @click="openTcmEntry(sub)">
|
||||||
|
<text class="color-sub fs-24">选择{{ sub.label }}</text>
|
||||||
|
</view>
|
||||||
|
<u-input v-model="form[sub.code]" :placeholder="sub.label" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-else-if="item.code === 'doctor_order'"
|
||||||
|
class="mr-entry-row"
|
||||||
|
@click="onOpenDoctorOrder"
|
||||||
|
>
|
||||||
|
<text class="color-sub fs-24">选择常用医嘱</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
v-else
|
||||||
|
class="mr-entry-row"
|
||||||
|
@click="openEntry(item.code)"
|
||||||
|
>
|
||||||
|
<text class="color-sub fs-24">选择{{ item.label }}词条</text>
|
||||||
|
</view>
|
||||||
|
<u-input
|
||||||
|
v-if="item.code === 'doctor_order'"
|
||||||
|
v-model="localAdvice"
|
||||||
|
type="textarea"
|
||||||
|
:auto-height="true"
|
||||||
|
placeholder="医嘱(与处方同步)"
|
||||||
|
:custom-style="areaStyle"
|
||||||
|
@input="emitAdvice"
|
||||||
|
/>
|
||||||
|
<u-input
|
||||||
|
v-else-if="item.code !== 'tcm_case'"
|
||||||
|
v-model="form[item.code]"
|
||||||
|
type="textarea"
|
||||||
|
:auto-height="true"
|
||||||
|
:placeholder="item.label"
|
||||||
|
:custom-style="areaStyle"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view v-for="item in rightFields" :key="item.code" class="mr-field">
|
||||||
|
<d-text :text="item.label" className="fs-28 font-bold" color="#333" />
|
||||||
|
<view class="mr-entry-row" @click="openEntry(item.code)">
|
||||||
|
<text class="color-sub fs-24">选择{{ item.label }}词条</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="item.code === 'physical_exam'" class="vitals m-b-8">
|
||||||
|
<view class="vital-item">
|
||||||
|
<text>体温</text>
|
||||||
|
<u-input v-model="form.temperature" type="digit" placeholder="℃" />
|
||||||
|
</view>
|
||||||
|
<view class="vital-item">
|
||||||
|
<text>身高</text>
|
||||||
|
<u-input v-model="form.height" type="digit" placeholder="cm" />
|
||||||
|
</view>
|
||||||
|
<view class="vital-item">
|
||||||
|
<text>体重</text>
|
||||||
|
<u-input v-model="form.weight" type="digit" placeholder="KG" />
|
||||||
|
</view>
|
||||||
|
<view class="vital-item">
|
||||||
|
<text>呼吸</text>
|
||||||
|
<u-input v-model="form.respiratory_rate" type="number" placeholder="次/分" />
|
||||||
|
</view>
|
||||||
|
<view class="vital-item">
|
||||||
|
<text>血压</text>
|
||||||
|
<u-input v-model="form.bp_systolic" type="number" placeholder="高" />
|
||||||
|
<text>/</text>
|
||||||
|
<u-input v-model="form.bp_diastolic" type="number" placeholder="低" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<u-input
|
||||||
|
v-model="form[item.code]"
|
||||||
|
type="textarea"
|
||||||
|
:auto-height="true"
|
||||||
|
:placeholder="item.label"
|
||||||
|
:custom-style="areaStyle"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
<!-- 词条选择抽屉:page-container + v-if -->
|
||||||
|
<page-container
|
||||||
|
v-if="entryShow"
|
||||||
|
:show="entryShow"
|
||||||
|
position="bottom"
|
||||||
|
round
|
||||||
|
@clickoverlay="entryShow = false"
|
||||||
|
@afterleave="entryShow = false"
|
||||||
|
>
|
||||||
|
<view class="entry-drawer">
|
||||||
|
<view class="entry-title-row">
|
||||||
|
<text class="entry-title">选择词条</text>
|
||||||
|
<text class="entry-count">{{ entryKeyword ? ('搜到 ' + entryList.length + ' 条') : ('共 ' + entryList.length + ' 条') }}</text>
|
||||||
|
</view>
|
||||||
|
<u-input
|
||||||
|
v-model="entryKeyword"
|
||||||
|
placeholder="搜索词条名称/内容/首拼"
|
||||||
|
:custom-style="entrySearchStyle"
|
||||||
|
@input="onEntryKeywordInput"
|
||||||
|
/>
|
||||||
|
<view
|
||||||
|
v-for="row in entryList"
|
||||||
|
:key="row.id"
|
||||||
|
class="entry-row"
|
||||||
|
@click="pickEntry(row)"
|
||||||
|
>
|
||||||
|
<view class="entry-row-top">
|
||||||
|
<view class="fs-28 font-bold entry-row-name">
|
||||||
|
<block v-for="(p, pi) in row._titleParts" :key="pi">
|
||||||
|
<text v-if="p.hit" class="hit-text">{{ p.text }}</text>
|
||||||
|
<text v-else>{{ p.text }}</text>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<text v-if="entryKeyword" class="entry-score">{{ row.match_score || 0 }}%</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="row.pinyin_initials" class="fs-22 color-sub m-t-4">
|
||||||
|
<text>首拼(全拼):</text>
|
||||||
|
<block v-for="(p, pi) in row._pyParts" :key="pi">
|
||||||
|
<text v-if="p.hit" class="hit-text">{{ p.text }}</text>
|
||||||
|
<text v-else>{{ p.text }}</text>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<view class="fs-24 color-sub m-t-8">
|
||||||
|
<block v-for="(p, pi) in row._contentParts" :key="pi">
|
||||||
|
<text v-if="p.hit" class="hit-text">{{ p.text }}</text>
|
||||||
|
<text v-else>{{ p.text }}</text>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<text v-if="row.remark" class="fs-22 color-sub">{{ row.remark }}</text>
|
||||||
|
</view>
|
||||||
|
<view v-if="!entryList.length" class="p-32 color-sub">暂无词条</view>
|
||||||
|
</view>
|
||||||
|
</page-container>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* 病历面板:与处方诊断/医嘱双向同步
|
||||||
|
* 诊断/医嘱选用走父页常用诊断、常用医嘱弹窗
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
clearMedicalRecordApi,
|
||||||
|
getMedicalRecordApi,
|
||||||
|
getTraditionalChineseMedicineJson,
|
||||||
|
latestMedicalRecordApi,
|
||||||
|
saveMedicalRecordApi,
|
||||||
|
searchMedicalRecordEntryApi,
|
||||||
|
} from '@/api/reception.js'
|
||||||
|
import { PrescriptionStorage } from '../utils/prescriptionStorage.js'
|
||||||
|
import {
|
||||||
|
applyDictRankAndHighlight,
|
||||||
|
splitHighlight,
|
||||||
|
} from '../utils/dictSearchRank.js'
|
||||||
|
|
||||||
|
function emptyForm() {
|
||||||
|
return {
|
||||||
|
chief_complaint: '',
|
||||||
|
present_illness: '',
|
||||||
|
tcm_disease: '',
|
||||||
|
tcm_syndrome: '',
|
||||||
|
tcm_method: '',
|
||||||
|
tongue: '',
|
||||||
|
pulse: '',
|
||||||
|
tcm_case: '',
|
||||||
|
treatment_advice: '',
|
||||||
|
doctor_order: '',
|
||||||
|
diagnosis: '',
|
||||||
|
family_history: '',
|
||||||
|
epidemic_history: '',
|
||||||
|
past_history: '',
|
||||||
|
allergy_history: '',
|
||||||
|
physical_exam: '',
|
||||||
|
auxiliary_exam: '',
|
||||||
|
temperature: '',
|
||||||
|
height: '',
|
||||||
|
weight: '',
|
||||||
|
respiratory_rate: '',
|
||||||
|
bp_systolic: '',
|
||||||
|
bp_diastolic: '',
|
||||||
|
user_patient_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拼出 首拼(全拼) 展示(与 PC 病历气泡一致)
|
||||||
|
*/
|
||||||
|
function formatPyDisplayOf(item) {
|
||||||
|
if (!item) return ''
|
||||||
|
const display = String(item.pinyin_display || '').trim()
|
||||||
|
if (display) return display
|
||||||
|
const initials = String(item.pinyin_initials || '').trim()
|
||||||
|
if (initials.indexOf('(') >= 0 && initials.slice(-1) === ')') return initials
|
||||||
|
const abbr = String(item.pinyin || initials || item.initials_of_pinyin || '').trim()
|
||||||
|
const full = String(item.pinyin_full || '').trim()
|
||||||
|
if (!abbr && !full) return ''
|
||||||
|
if (!full || abbr.toLowerCase() === full.toLowerCase()) return abbr || full
|
||||||
|
if (!abbr) return '(' + full + ')'
|
||||||
|
return abbr + '(' + full + ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MedicalRecordPanel',
|
||||||
|
props: {
|
||||||
|
registerId: { type: [Number, String], default: 0 },
|
||||||
|
storeId: { type: [Number, String], default: 0 },
|
||||||
|
userPatientId: { type: [Number, String], default: 0 },
|
||||||
|
diagnosis: { type: String, default: '' },
|
||||||
|
medicalAdvice: { type: String, default: '' },
|
||||||
|
/** 在线复诊:诊断只读,必须点选 */
|
||||||
|
diagnosisReadonly: { type: Boolean, default: false },
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const tcmCaseSubFields = [
|
||||||
|
{ code: 'tcm_syndrome', label: '中医证候', tcmType: 'syndrome', _key: 'tcm_syndrome' },
|
||||||
|
{ code: 'tcm_disease', label: '中医疾病', tcmType: 'diseases', _key: 'tcm_disease' },
|
||||||
|
{ code: 'tcm_method', label: '中医治法', tcmType: 'method', _key: 'tcm_method' },
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
form: emptyForm(),
|
||||||
|
localDiagnosis: '',
|
||||||
|
localAdvice: '',
|
||||||
|
entryShow: false,
|
||||||
|
entryField: '',
|
||||||
|
entryList: [],
|
||||||
|
entryKeyword: '',
|
||||||
|
entrySearchTimer: null,
|
||||||
|
entrySearchStyle: { background: '#f7f8fa', padding: '12rpx 16rpx', marginBottom: '16rpx' },
|
||||||
|
hydrating: false,
|
||||||
|
_draftTimer: null,
|
||||||
|
areaStyle: { background: '#f7f8fa', padding: '16rpx', minHeight: '120rpx' },
|
||||||
|
leftFields: [
|
||||||
|
{ code: 'chief_complaint', label: '主诉' },
|
||||||
|
{ code: 'present_illness', label: '现病史' },
|
||||||
|
{ code: 'tcm_case', label: '中医病案' },
|
||||||
|
{ code: 'treatment_advice', label: '治疗意见' },
|
||||||
|
{ code: 'doctor_order', label: '医嘱' },
|
||||||
|
],
|
||||||
|
/** 中医病案内嵌:证候→疾病→治法(一行两列;_key 供小程序 :key 绑定) */
|
||||||
|
tcmCaseSubFields,
|
||||||
|
tcmCaseSubRows: [
|
||||||
|
{
|
||||||
|
key: 'r0',
|
||||||
|
cols: [tcmCaseSubFields[0], tcmCaseSubFields[1]],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'r1',
|
||||||
|
cols: [tcmCaseSubFields[2], { _key: 'empty', _empty: true }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
/** 中医三典当前抽屉类型 diseases|syndrome|method,空=病历词条 */
|
||||||
|
entryTcmType: '',
|
||||||
|
rightFields: [
|
||||||
|
{ code: 'family_history', label: '家族史' },
|
||||||
|
{ code: 'epidemic_history', label: '流行病学史' },
|
||||||
|
{ code: 'past_history', label: '既往史' },
|
||||||
|
{ code: 'allergy_history', label: '过敏史' },
|
||||||
|
{ code: 'physical_exam', label: '体征检查' },
|
||||||
|
{ code: 'auxiliary_exam', label: '辅助检查' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/** props 优先,其次病历回填 */
|
||||||
|
effectivePatientId() {
|
||||||
|
return Number(this.userPatientId || this.form.user_patient_id || 0)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
registerId: {
|
||||||
|
immediate: true,
|
||||||
|
handler(v) {
|
||||||
|
if (v) this.load()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
diagnosis(v) {
|
||||||
|
this.localDiagnosis = v || ''
|
||||||
|
this.schedulePersistDraft()
|
||||||
|
},
|
||||||
|
medicalAdvice(v) {
|
||||||
|
this.localAdvice = v || ''
|
||||||
|
this.schedulePersistDraft()
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
deep: true,
|
||||||
|
handler() {
|
||||||
|
this.schedulePersistDraft()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
emitDiagnosis() {
|
||||||
|
if (this.diagnosisReadonly) return
|
||||||
|
this.$emit('update:diagnosis', this.localDiagnosis || '')
|
||||||
|
this.schedulePersistDraft()
|
||||||
|
},
|
||||||
|
emitAdvice() {
|
||||||
|
this.$emit('update:medicalAdvice', this.localAdvice || '')
|
||||||
|
this.schedulePersistDraft()
|
||||||
|
},
|
||||||
|
onOpenDiagnosis() {
|
||||||
|
this.$emit('open-diagnosis')
|
||||||
|
},
|
||||||
|
onOpenDoctorOrder() {
|
||||||
|
this.$emit('open-doctor-order')
|
||||||
|
},
|
||||||
|
schedulePersistDraft() {
|
||||||
|
if (this.hydrating || !this.registerId) return
|
||||||
|
if (this._draftTimer) clearTimeout(this._draftTimer)
|
||||||
|
this._draftTimer = setTimeout(() => {
|
||||||
|
this.persistDraft()
|
||||||
|
}, 300)
|
||||||
|
},
|
||||||
|
persistDraft() {
|
||||||
|
if (!this.registerId || this.hydrating) return
|
||||||
|
PrescriptionStorage.saveMedicalRecordDraft(this.registerId, this.getPayload())
|
||||||
|
},
|
||||||
|
applyDraft(draft) {
|
||||||
|
if (!draft) return
|
||||||
|
this.form = Object.assign(emptyForm(), draft, {
|
||||||
|
user_patient_id: this.userPatientId || draft.user_patient_id || 0,
|
||||||
|
})
|
||||||
|
if (draft.diagnosis) {
|
||||||
|
this.localDiagnosis = draft.diagnosis
|
||||||
|
this.$emit('update:diagnosis', draft.diagnosis)
|
||||||
|
}
|
||||||
|
if (draft.doctor_order || draft.medicalAdvice) {
|
||||||
|
this.localAdvice = draft.doctor_order || draft.medicalAdvice || ''
|
||||||
|
this.$emit('update:medicalAdvice', this.localAdvice)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async load() {
|
||||||
|
this.hydrating = true
|
||||||
|
try {
|
||||||
|
const res = await getMedicalRecordApi({
|
||||||
|
register_id: this.registerId,
|
||||||
|
store_id: this.storeId || undefined,
|
||||||
|
})
|
||||||
|
const data = (res && res.data) || res || {}
|
||||||
|
this.form = Object.assign(emptyForm(), data)
|
||||||
|
const draft = PrescriptionStorage.loadMedicalRecordDraft(this.registerId)
|
||||||
|
if (draft) {
|
||||||
|
this.applyDraft(draft)
|
||||||
|
} else {
|
||||||
|
if (!this.diagnosis && data.diagnosis) {
|
||||||
|
this.$emit('update:diagnosis', data.diagnosis)
|
||||||
|
}
|
||||||
|
if (!this.medicalAdvice && data.doctor_order) {
|
||||||
|
this.$emit('update:medicalAdvice', data.doctor_order)
|
||||||
|
}
|
||||||
|
this.localDiagnosis = this.diagnosis || data.diagnosis || ''
|
||||||
|
this.localAdvice = this.medicalAdvice || data.doctor_order || ''
|
||||||
|
}
|
||||||
|
if (this.diagnosis) this.localDiagnosis = this.diagnosis
|
||||||
|
if (this.medicalAdvice) this.localAdvice = this.medicalAdvice
|
||||||
|
this.$emit('vip-ok')
|
||||||
|
} catch (e) {
|
||||||
|
const draft = PrescriptionStorage.loadMedicalRecordDraft(this.registerId)
|
||||||
|
if (draft) {
|
||||||
|
this.applyDraft(draft)
|
||||||
|
this.$emit('vip-ok')
|
||||||
|
} else {
|
||||||
|
const msg = (e && e.message) || (e && e.msg) || ''
|
||||||
|
if (String(msg).indexOf('VIP') > -1 || String(msg).indexOf('vip') > -1 || String(msg).indexOf('病历') > -1) {
|
||||||
|
this.$emit('vip-denied')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.hydrating = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getPayload() {
|
||||||
|
return Object.assign({}, this.form, {
|
||||||
|
register_id: Number(this.registerId),
|
||||||
|
store_id: Number(this.storeId || 0),
|
||||||
|
user_patient_id: this.effectivePatientId,
|
||||||
|
diagnosis: this.localDiagnosis,
|
||||||
|
doctor_order: this.localAdvice,
|
||||||
|
medicalAdvice: this.localAdvice,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
async onSave() {
|
||||||
|
await saveMedicalRecordApi(this.getPayload())
|
||||||
|
this.persistDraft()
|
||||||
|
uni.showToast({ title: '病历已保存', icon: 'success' })
|
||||||
|
},
|
||||||
|
async onClear() {
|
||||||
|
await clearMedicalRecordApi({
|
||||||
|
register_id: this.registerId,
|
||||||
|
store_id: this.storeId || undefined,
|
||||||
|
})
|
||||||
|
const keepPatient = this.effectivePatientId
|
||||||
|
this.form = emptyForm()
|
||||||
|
this.form.user_patient_id = keepPatient
|
||||||
|
this.localDiagnosis = ''
|
||||||
|
this.localAdvice = ''
|
||||||
|
this.$emit('update:diagnosis', '')
|
||||||
|
this.$emit('update:medicalAdvice', '')
|
||||||
|
PrescriptionStorage.clearMedicalRecordDraft(this.registerId)
|
||||||
|
uni.showToast({ title: '已清空', icon: 'none' })
|
||||||
|
},
|
||||||
|
async onCite() {
|
||||||
|
const patientId = this.effectivePatientId
|
||||||
|
if (!patientId) {
|
||||||
|
uni.showToast({ title: '无就诊人', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const res = await latestMedicalRecordApi({
|
||||||
|
user_patient_id: patientId,
|
||||||
|
register_id: this.registerId,
|
||||||
|
store_id: this.storeId || undefined,
|
||||||
|
})
|
||||||
|
const data = (res && res.data) || res
|
||||||
|
if (!data) {
|
||||||
|
uni.showToast({ title: '暂无历史病历', icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.form = Object.assign(emptyForm(), data, {
|
||||||
|
user_patient_id: patientId,
|
||||||
|
})
|
||||||
|
if (data.diagnosis) {
|
||||||
|
this.localDiagnosis = data.diagnosis
|
||||||
|
this.$emit('update:diagnosis', data.diagnosis)
|
||||||
|
}
|
||||||
|
if (data.doctor_order) {
|
||||||
|
this.localAdvice = data.doctor_order
|
||||||
|
this.$emit('update:medicalAdvice', data.doctor_order)
|
||||||
|
}
|
||||||
|
this.persistDraft()
|
||||||
|
uni.showToast({ title: '已引用', icon: 'success' })
|
||||||
|
},
|
||||||
|
async openEntry(fieldCode) {
|
||||||
|
// 诊断/医嘱不走病历词条
|
||||||
|
if (fieldCode === 'diagnosis' || fieldCode === 'doctor_order') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.entryField = fieldCode
|
||||||
|
this.entryTcmType = ''
|
||||||
|
this.entryKeyword = ''
|
||||||
|
this.entryShow = true
|
||||||
|
await this.loadEntryList()
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 打开中医三典选择(疾病/证候/治法)
|
||||||
|
*/
|
||||||
|
async openTcmEntry(item) {
|
||||||
|
if (!item || !item.tcmType) return
|
||||||
|
this.entryField = item.code
|
||||||
|
this.entryTcmType = item.tcmType
|
||||||
|
this.entryKeyword = ''
|
||||||
|
this.entryShow = true
|
||||||
|
await this.loadEntryList()
|
||||||
|
},
|
||||||
|
/** 词条关键字输入防抖(u-input 的 @input 可能先于 v-model,以事件值为准) */
|
||||||
|
onEntryKeywordInput(val) {
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
this.entryKeyword = val
|
||||||
|
}
|
||||||
|
if (this.entrySearchTimer) clearTimeout(this.entrySearchTimer)
|
||||||
|
this.entrySearchTimer = setTimeout(() => {
|
||||||
|
this.loadEntryList()
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 解析病历词条接口列表(Laravel jok 在 result,兼容 data)
|
||||||
|
*/
|
||||||
|
unwrapEntryList(res) {
|
||||||
|
if (Array.isArray(res)) return res
|
||||||
|
if (!res || typeof res !== 'object') return []
|
||||||
|
if (Array.isArray(res.result)) return res.result
|
||||||
|
if (Array.isArray(res.data)) return res.data
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 拉取词条或中医字典,并做匹配度排序 + 高亮拆分
|
||||||
|
*/
|
||||||
|
async loadEntryList() {
|
||||||
|
try {
|
||||||
|
const kw = (this.entryKeyword || '').trim()
|
||||||
|
let list = []
|
||||||
|
// 小程序侧统一前端打分,保证有匹配度排序;后端若已打分则 apply 内会优先用
|
||||||
|
let mode = 'frontend'
|
||||||
|
if (this.entryTcmType) {
|
||||||
|
list = await this.fetchTcmDictList(this.entryTcmType, kw)
|
||||||
|
} else {
|
||||||
|
const res = await searchMedicalRecordEntryApi({
|
||||||
|
field_code: this.entryField,
|
||||||
|
keyword: kw || undefined,
|
||||||
|
store_id: this.storeId || undefined,
|
||||||
|
})
|
||||||
|
list = this.unwrapEntryList(res)
|
||||||
|
if (list[0] && list[0].rank_mode) {
|
||||||
|
mode = String(list[0].rank_mode)
|
||||||
|
}
|
||||||
|
list = list.map((row) =>
|
||||||
|
Object.assign({}, row, {
|
||||||
|
pinyin_initials: formatPyDisplayOf(row),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const ranked = applyDictRankAndHighlight(
|
||||||
|
list,
|
||||||
|
kw,
|
||||||
|
(row) => String((row.title || '') + ' ' + (row.content || '')),
|
||||||
|
mode,
|
||||||
|
(row) => String(row.pinyin_initials || row.pinyin || ''),
|
||||||
|
)
|
||||||
|
this.entryList = ranked.map((row) =>
|
||||||
|
Object.assign({}, row, {
|
||||||
|
_titleParts: splitHighlight(row.title || '', kw),
|
||||||
|
_contentParts: splitHighlight(row.content || '', kw),
|
||||||
|
_pyParts: splitHighlight(row.pinyin_initials || '', kw),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
this.entryList = []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 从本地/接口中医术语表过滤出候选列表
|
||||||
|
*/
|
||||||
|
async fetchTcmDictList(tcmType, kw) {
|
||||||
|
const all = await getTraditionalChineseMedicineJson()
|
||||||
|
const rows = (all && all[tcmType]) || []
|
||||||
|
const k = (kw || '').toLowerCase()
|
||||||
|
const result = []
|
||||||
|
for (let i = 0; i < rows.length; i++) {
|
||||||
|
const item = rows[i] || {}
|
||||||
|
const name = String(item.name || '').trim()
|
||||||
|
if (!name) continue
|
||||||
|
const py = formatPyDisplayOf(item)
|
||||||
|
const pyRaw = [
|
||||||
|
item.pinyin_display,
|
||||||
|
item.pinyin_initials,
|
||||||
|
item.initials_of_pinyin,
|
||||||
|
item.pinyin_full,
|
||||||
|
]
|
||||||
|
.map((v) => String(v || '').toLowerCase())
|
||||||
|
.join(' ')
|
||||||
|
const alias = String(item.alias || '')
|
||||||
|
if (
|
||||||
|
kw &&
|
||||||
|
name.indexOf(kw) < 0 &&
|
||||||
|
name.toLowerCase().indexOf(k) < 0 &&
|
||||||
|
pyRaw.indexOf(k) < 0 &&
|
||||||
|
alias.indexOf(kw) < 0
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result.push({
|
||||||
|
id: item.id || name,
|
||||||
|
title: name,
|
||||||
|
content: name,
|
||||||
|
remark: '中医字典',
|
||||||
|
pinyin_initials: py,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return kw ? result : result.slice(0, 80)
|
||||||
|
},
|
||||||
|
pickEntry(row) {
|
||||||
|
const text = row.content || row.title || ''
|
||||||
|
const field = this.entryField
|
||||||
|
if (field === 'tongue' || field === 'pulse') {
|
||||||
|
this.form[field] = text
|
||||||
|
} else if (
|
||||||
|
field === 'tcm_disease' ||
|
||||||
|
field === 'tcm_syndrome' ||
|
||||||
|
field === 'tcm_method'
|
||||||
|
) {
|
||||||
|
this.form[field] = this.appendCommaText(this.form[field] || '', text)
|
||||||
|
} else {
|
||||||
|
this.form[field] = this.appendText(this.form[field] || '', text)
|
||||||
|
}
|
||||||
|
this.entryShow = false
|
||||||
|
this.entryTcmType = ''
|
||||||
|
this.schedulePersistDraft()
|
||||||
|
},
|
||||||
|
appendText(base, add) {
|
||||||
|
if (!add) return base || ''
|
||||||
|
if (!base) return add
|
||||||
|
return base + (base.endsWith('\n') ? '' : '\n') + add
|
||||||
|
},
|
||||||
|
/** 中文逗号拼接,去重 */
|
||||||
|
appendCommaText(base, add) {
|
||||||
|
if (!add) return base || ''
|
||||||
|
const parts = String(base || '')
|
||||||
|
.split(/[,,]/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
if (parts.indexOf(add) >= 0) return parts.join(',')
|
||||||
|
parts.push(add)
|
||||||
|
return parts.join(',')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style scoped>
|
||||||
|
.mr-panel {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.mr-actions {
|
||||||
|
flex-shrink: 0;
|
||||||
|
gap: 16rpx;
|
||||||
|
padding: 20rpx 32rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.mr-scroll {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
.mr-scroll-inner {
|
||||||
|
padding: 24rpx 32rpx 48rpx;
|
||||||
|
}
|
||||||
|
.mr-btn {
|
||||||
|
padding: 12rpx 24rpx;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
.mr-btn.primary {
|
||||||
|
background: #6acdbb;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.mr-btn.danger {
|
||||||
|
background: #ffecec;
|
||||||
|
color: #e54d42;
|
||||||
|
}
|
||||||
|
.mr-field {
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.mr-entry-row {
|
||||||
|
margin: 8rpx 0;
|
||||||
|
padding: 12rpx 16rpx;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
.vitals {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12rpx;
|
||||||
|
}
|
||||||
|
.vital-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
.entry-drawer {
|
||||||
|
background: #fff;
|
||||||
|
max-height: 70vh;
|
||||||
|
padding: 32rpx;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.entry-title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 24rpx;
|
||||||
|
}
|
||||||
|
.entry-title {
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.entry-count {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #666;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.entry-row {
|
||||||
|
padding: 20rpx 0;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.entry-row-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
.entry-row-name {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.entry-score {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #d4380d;
|
||||||
|
}
|
||||||
|
.hit-text {
|
||||||
|
color: #cf1322;
|
||||||
|
background: rgba(255, 214, 102, 0.55);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -73,7 +73,16 @@
|
|||||||
:class="{ 'is-selected': item.isSelect === 1 }"
|
:class="{ 'is-selected': item.isSelect === 1 }"
|
||||||
@click="selectDiagnosis(item)"
|
@click="selectDiagnosis(item)"
|
||||||
>
|
>
|
||||||
<text class="tag-text">{{ item.name }}</text>
|
<text class="tag-text">
|
||||||
|
<block
|
||||||
|
v-for="(p, pi) in (item._hlParts || [{ text: item.name, hit: false }])"
|
||||||
|
:key="pi"
|
||||||
|
>
|
||||||
|
<text v-if="p.hit" class="hit-text">{{ p.text }}</text>
|
||||||
|
<text v-else>{{ p.text }}</text>
|
||||||
|
</block>
|
||||||
|
</text>
|
||||||
|
<text v-if="searchKey" class="tag-score">{{ item.match_score || 0 }}%</text>
|
||||||
<view class="tag-action" @click.stop="addToMyDiagnosis(item)" v-if="!item.isInMyList">
|
<view class="tag-action" @click.stop="addToMyDiagnosis(item)" v-if="!item.isInMyList">
|
||||||
<u-icon name="plus" size="20"></u-icon>
|
<u-icon name="plus" size="20"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
@@ -140,6 +149,7 @@
|
|||||||
// 保留所有原有逻辑代码与注释
|
// 保留所有原有逻辑代码与注释
|
||||||
import { getDiseaseList, getMyDiseaseList, addMyDisease, deleteMyDisease } from '@/api/reception.js';
|
import { getDiseaseList, getMyDiseaseList, addMyDisease, deleteMyDisease } from '@/api/reception.js';
|
||||||
import { getClinicSalespersonDiseaseList } from '@/api/clinicSalesperson.js';
|
import { getClinicSalespersonDiseaseList } from '@/api/clinicSalesperson.js';
|
||||||
|
import { applyDictRankAndHighlight, splitHighlight } from '../../utils/dictSearchRank.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DiagnosisModal',
|
name: 'DiagnosisModal',
|
||||||
@@ -228,29 +238,37 @@ export default {
|
|||||||
}
|
}
|
||||||
let list = [];
|
let list = [];
|
||||||
let hasMore = false;
|
let hasMore = false;
|
||||||
|
let rankMode = 'backend';
|
||||||
|
|
||||||
// 正确解析响应数据
|
// 正确解析响应数据(分页结构为 data.list)
|
||||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||||
// 优先使用 data,然后是 result
|
const payload = res.data !== undefined ? res.data : res.result;
|
||||||
if (res.data && Array.isArray(res.data)) {
|
if (payload && Array.isArray(payload.list)) {
|
||||||
list = res.data;
|
list = payload.list;
|
||||||
} else if (res.result && Array.isArray(res.result)) {
|
hasMore = !!payload.has_more;
|
||||||
list = res.result;
|
if (payload.rank_mode) rankMode = String(payload.rank_mode);
|
||||||
|
} else if (Array.isArray(payload)) {
|
||||||
|
list = payload;
|
||||||
|
hasMore = list.length >= this.pageSize;
|
||||||
} else if (Array.isArray(res)) {
|
} else if (Array.isArray(res)) {
|
||||||
list = res;
|
list = res;
|
||||||
}
|
hasMore = list.length >= this.pageSize;
|
||||||
|
} else if (res.pagination) {
|
||||||
// 从响应中获取分页信息
|
|
||||||
if (res.pagination) {
|
|
||||||
hasMore = res.pagination.has_more || false;
|
hasMore = res.pagination.has_more || false;
|
||||||
} else if (res.has_more !== undefined) {
|
} else if (res.has_more !== undefined) {
|
||||||
hasMore = res.has_more;
|
hasMore = res.has_more;
|
||||||
} else {
|
|
||||||
// 如果没有分页信息,根据返回数量判断
|
|
||||||
hasMore = list.length >= this.pageSize;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 前端模式时本地按匹配度重排;并拆高亮片段
|
||||||
|
list = applyDictRankAndHighlight(
|
||||||
|
list,
|
||||||
|
searchKey,
|
||||||
|
(item) => String(item.name || ''),
|
||||||
|
rankMode,
|
||||||
|
(item) => String(item.pinyin || item.pinyin_initials || ''),
|
||||||
|
);
|
||||||
|
|
||||||
const processedList = list.map((item) => {
|
const processedList = list.map((item) => {
|
||||||
item.isSelect = 0;
|
item.isSelect = 0;
|
||||||
item.isInMyList = this.doctorMyDiseaseList.some(
|
item.isInMyList = this.doctorMyDiseaseList.some(
|
||||||
@@ -264,6 +282,7 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
item._hlParts = item._hlParts || splitHighlight(item.name || '', searchKey);
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -334,7 +353,11 @@ export default {
|
|||||||
* 搜索诊断
|
* 搜索诊断
|
||||||
* 职责:防抖搜索,只在有输入时调用加载诊断列表
|
* 职责:防抖搜索,只在有输入时调用加载诊断列表
|
||||||
*/
|
*/
|
||||||
handleSearch() {
|
handleSearch(val) {
|
||||||
|
// u-input @input 传入最新值,避免 v-model 滞后导致搜空/不排序
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
this.searchKey = val
|
||||||
|
}
|
||||||
clearTimeout(this.searchTimer);
|
clearTimeout(this.searchTimer);
|
||||||
this.searchTimer = setTimeout(() => {
|
this.searchTimer = setTimeout(() => {
|
||||||
if (this.searchKey && this.searchKey.trim()) {
|
if (this.searchKey && this.searchKey.trim()) {
|
||||||
@@ -348,7 +371,7 @@ export default {
|
|||||||
this.currentPage = 1;
|
this.currentPage = 1;
|
||||||
this.hasMore = false;
|
this.hasMore = false;
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 500);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -546,6 +569,13 @@ export default {
|
|||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-score {
|
||||||
|
margin-left: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #d4380d;
|
||||||
|
}
|
||||||
|
|
||||||
.tag-action {
|
.tag-action {
|
||||||
margin-left: 12rpx;
|
margin-left: 12rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -554,6 +584,11 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hit-text {
|
||||||
|
color: #cf1322;
|
||||||
|
background: rgba(255, 214, 102, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
.load-more {
|
.load-more {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -15,6 +15,18 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="modal-content">
|
<view class="modal-content">
|
||||||
|
<view class="search-box">
|
||||||
|
<u-input
|
||||||
|
v-model="searchKey"
|
||||||
|
placeholder="搜索医嘱内容..."
|
||||||
|
:custom-style="searchStyle"
|
||||||
|
@input="onSearchInput"
|
||||||
|
>
|
||||||
|
<template slot="suffix">
|
||||||
|
<u-icon name="search" size="32" color="#B0B6C2"></u-icon>
|
||||||
|
</template>
|
||||||
|
</u-input>
|
||||||
|
</view>
|
||||||
<scroll-view class="order-scroll" scroll-y>
|
<scroll-view class="order-scroll" scroll-y>
|
||||||
<view class="order-scroll-inner">
|
<view class="order-scroll-inner">
|
||||||
<!-- 我的医嘱 -->
|
<!-- 我的医嘱 -->
|
||||||
@@ -30,13 +42,19 @@
|
|||||||
<!-- 我的医嘱列表 -->
|
<!-- 我的医嘱列表 -->
|
||||||
<view class="modern-tag-container">
|
<view class="modern-tag-container">
|
||||||
<view
|
<view
|
||||||
v-for="item in doctorOrderMyList"
|
v-for="item in filteredMyList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="modern-tag-item"
|
class="modern-tag-item"
|
||||||
:class="{ 'is-selected': item.isSelect === 1 }"
|
:class="{ 'is-selected': item.isSelect === 1 }"
|
||||||
@click="selectDoctorOrder(item, 1)"
|
@click="selectDoctorOrder(item, 1)"
|
||||||
>
|
>
|
||||||
<text class="tag-text">{{ item.content }}</text>
|
<text class="tag-text">
|
||||||
|
<block v-for="(p, pi) in item._hlParts" :key="pi">
|
||||||
|
<text v-if="p.hit" class="hit-text">{{ p.text }}</text>
|
||||||
|
<text v-else>{{ p.text }}</text>
|
||||||
|
</block>
|
||||||
|
</text>
|
||||||
|
<text v-if="debouncedSearchKey" class="tag-score">{{ item.match_score || 0 }}%</text>
|
||||||
<view class="tag-action" @click.stop="deleteMyDoctorOrder(item)">
|
<view class="tag-action" @click.stop="deleteMyDoctorOrder(item)">
|
||||||
<u-icon name="close" size="20"></u-icon>
|
<u-icon name="close" size="20"></u-icon>
|
||||||
</view>
|
</view>
|
||||||
@@ -45,19 +63,25 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 公共医嘱 -->
|
<!-- 公共医嘱 -->
|
||||||
<view class="section white radius-16 p-32 shadow-sm" v-if="doctorOrderCommonList.length > 0">
|
<view class="section white radius-16 p-32 shadow-sm" v-if="filteredCommonList.length > 0">
|
||||||
<view class="section-header m-b-24">
|
<view class="section-header m-b-24">
|
||||||
<d-text text="系统公共医嘱" className="fs-30 font-bold color-title"></d-text>
|
<d-text text="系统公共医嘱" className="fs-30 font-bold color-title"></d-text>
|
||||||
</view>
|
</view>
|
||||||
<view class="modern-tag-container">
|
<view class="modern-tag-container">
|
||||||
<view
|
<view
|
||||||
v-for="item in doctorOrderCommonList"
|
v-for="item in filteredCommonList"
|
||||||
:key="item.id"
|
:key="item.id"
|
||||||
class="modern-tag-item"
|
class="modern-tag-item"
|
||||||
:class="{ 'is-selected': item.isSelect === 1 }"
|
:class="{ 'is-selected': item.isSelect === 1 }"
|
||||||
@click="selectDoctorOrder(item, 2)"
|
@click="selectDoctorOrder(item, 2)"
|
||||||
>
|
>
|
||||||
<text class="tag-text">{{ item.content }}</text>
|
<text class="tag-text">
|
||||||
|
<block v-for="(p, pi) in item._hlParts" :key="pi">
|
||||||
|
<text v-if="p.hit" class="hit-text">{{ p.text }}</text>
|
||||||
|
<text v-else>{{ p.text }}</text>
|
||||||
|
</block>
|
||||||
|
</text>
|
||||||
|
<text v-if="debouncedSearchKey" class="tag-score">{{ item.match_score || 0 }}%</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -124,6 +148,7 @@
|
|||||||
<script>
|
<script>
|
||||||
// 保留所有原有逻辑代码与注释
|
// 保留所有原有逻辑代码与注释
|
||||||
import { getDoctorOrderList, getDoctorOrderCommonList, createDoctorOrder, deleteDoctorOrder } from '@/api/reception.js';
|
import { getDoctorOrderList, getDoctorOrderCommonList, createDoctorOrder, deleteDoctorOrder } from '@/api/reception.js';
|
||||||
|
import { applyDictRankAndHighlight, splitHighlight } from '../../utils/dictSearchRank.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'DoctorOrderModal',
|
name: 'DoctorOrderModal',
|
||||||
@@ -150,14 +175,41 @@ export default {
|
|||||||
doctorOrderMyList: [],
|
doctorOrderMyList: [],
|
||||||
selectDoctorOrderList: '',
|
selectDoctorOrderList: '',
|
||||||
showAddModal: false,
|
showAddModal: false,
|
||||||
newOrderContent: ''
|
newOrderContent: '',
|
||||||
|
searchKey: '',
|
||||||
|
/** 防抖后的关键字,用于过滤/排序/高亮 */
|
||||||
|
debouncedSearchKey: '',
|
||||||
|
searchTimer: null,
|
||||||
|
searchStyle: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
borderRadius: '16rpx',
|
||||||
|
height: '80rpx',
|
||||||
|
padding: '0 32rpx',
|
||||||
|
marginBottom: '24rpx',
|
||||||
|
boxShadow: '0 4rpx 16rpx rgba(0,0,0,0.02)'
|
||||||
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
/** 我的医嘱:本地过滤 + 匹配度排序 + 高亮 */
|
||||||
|
filteredMyList() {
|
||||||
|
return this.filterOrderList(this.doctorOrderMyList, (item) => item.content || '')
|
||||||
|
},
|
||||||
|
/** 公共医嘱 */
|
||||||
|
filteredCommonList() {
|
||||||
|
return this.filterOrderList(
|
||||||
|
this.doctorOrderCommonList,
|
||||||
|
(item) => item.content || item.name || '',
|
||||||
|
)
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
value(newVal) {
|
value(newVal) {
|
||||||
this.show = newVal;
|
this.show = newVal;
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.selectDoctorOrderList = this.selectedDoctorOrder || '';
|
this.selectDoctorOrderList = this.selectedDoctorOrder || '';
|
||||||
|
this.searchKey = '';
|
||||||
|
this.debouncedSearchKey = '';
|
||||||
this.loadDoctorOrderListData();
|
this.loadDoctorOrderListData();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -168,6 +220,42 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onSearchInput(val) {
|
||||||
|
// 输入框即时回显,过滤走防抖关键字
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
this.searchKey = val
|
||||||
|
}
|
||||||
|
if (this.searchTimer) clearTimeout(this.searchTimer)
|
||||||
|
this.searchTimer = setTimeout(() => {
|
||||||
|
this.debouncedSearchKey = this.searchKey || ''
|
||||||
|
}, 500)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 医嘱列表本地过滤排序(接口无 keyword,统一前端打分)
|
||||||
|
*/
|
||||||
|
filterOrderList(list, getText) {
|
||||||
|
const kw = (this.debouncedSearchKey || '').trim()
|
||||||
|
let rows = list || []
|
||||||
|
if (kw) {
|
||||||
|
const lower = kw.toLowerCase()
|
||||||
|
rows = rows.filter((item) => {
|
||||||
|
const text = String(getText(item) || '')
|
||||||
|
const py = String(item.pinyin_initials || item.pinyin || '').toLowerCase()
|
||||||
|
return text.indexOf(kw) > -1 || py.indexOf(lower) > -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return applyDictRankAndHighlight(
|
||||||
|
rows,
|
||||||
|
kw,
|
||||||
|
getText,
|
||||||
|
'frontend',
|
||||||
|
(item) => String(item.pinyin_initials || item.pinyin || ''),
|
||||||
|
).map((item) =>
|
||||||
|
Object.assign({}, item, {
|
||||||
|
_hlParts: splitHighlight(getText(item), kw),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
},
|
||||||
/**
|
/**
|
||||||
* 加载医嘱列表数据
|
* 加载医嘱列表数据
|
||||||
* 职责:调用接口获取我的医嘱和公共医嘱列表
|
* 职责:调用接口获取我的医嘱和公共医嘱列表
|
||||||
@@ -446,6 +534,13 @@ export default {
|
|||||||
color: #4b5563;
|
color: #4b5563;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tag-score {
|
||||||
|
margin-left: 8rpx;
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #d4380d;
|
||||||
|
}
|
||||||
|
|
||||||
.tag-action {
|
.tag-action {
|
||||||
margin-left: 12rpx;
|
margin-left: 12rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -462,4 +557,13 @@ export default {
|
|||||||
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
|
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
padding: 0 32rpx 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hit-text {
|
||||||
|
color: #cf1322;
|
||||||
|
background: rgba(255, 214, 102, 0.55);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -99,6 +99,8 @@ export default {
|
|||||||
searchTimer: null,
|
searchTimer: null,
|
||||||
pendingId: null,
|
pendingId: null,
|
||||||
scrollTop: 0,
|
scrollTop: 0,
|
||||||
|
/** 防抖后的过滤关键字(输入框仍用 searchKey 即时回显) */
|
||||||
|
filterKey: '',
|
||||||
searchStyle: {
|
searchStyle: {
|
||||||
fontSize: '28rpx',
|
fontSize: '28rpx',
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
@@ -133,7 +135,7 @@ export default {
|
|||||||
return this.title || '请选择';
|
return this.title || '请选择';
|
||||||
},
|
},
|
||||||
searchKeyTrim() {
|
searchKeyTrim() {
|
||||||
return (this.searchKey || '').trim();
|
return (this.filterKey || '').trim();
|
||||||
},
|
},
|
||||||
displayedList() {
|
displayedList() {
|
||||||
const key = this.searchKeyTrim.toLowerCase();
|
const key = this.searchKeyTrim.toLowerCase();
|
||||||
@@ -146,7 +148,7 @@ export default {
|
|||||||
if (!it) continue;
|
if (!it) continue;
|
||||||
const name = (it.name || '').toLowerCase();
|
const name = (it.name || '').toLowerCase();
|
||||||
const alias = (it.alias || '').toLowerCase();
|
const alias = (it.alias || '').toLowerCase();
|
||||||
const py = (it.initials_of_pinyin || '').toLowerCase();
|
const py = (it.initials_of_pinyin || it.pinyin_initials || it.pinyin_full || '').toLowerCase();
|
||||||
if (name.includes(key) || alias.includes(key) || py.includes(key)) {
|
if (name.includes(key) || alias.includes(key) || py.includes(key)) {
|
||||||
out.push(it);
|
out.push(it);
|
||||||
}
|
}
|
||||||
@@ -159,6 +161,7 @@ export default {
|
|||||||
this.show = newVal;
|
this.show = newVal;
|
||||||
if (newVal) {
|
if (newVal) {
|
||||||
this.searchKey = '';
|
this.searchKey = '';
|
||||||
|
this.filterKey = '';
|
||||||
this.pendingId = this.selectedId != null && this.selectedId !== '' ? Number(this.selectedId) : null;
|
this.pendingId = this.selectedId != null && this.selectedId !== '' ? Number(this.selectedId) : null;
|
||||||
this.scrollTop = 0;
|
this.scrollTop = 0;
|
||||||
}
|
}
|
||||||
@@ -170,14 +173,19 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSearchInput() {
|
onSearchInput(val) {
|
||||||
|
if (typeof val === 'string') {
|
||||||
|
this.searchKey = val;
|
||||||
|
}
|
||||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||||
this.searchTimer = setTimeout(() => {
|
this.searchTimer = setTimeout(() => {
|
||||||
|
this.filterKey = this.searchKey || '';
|
||||||
this.scrollTop = 0;
|
this.scrollTop = 0;
|
||||||
}, 80);
|
}, 500);
|
||||||
},
|
},
|
||||||
onSearchClear() {
|
onSearchClear() {
|
||||||
this.searchKey = '';
|
this.searchKey = '';
|
||||||
|
this.filterKey = '';
|
||||||
this.scrollTop = 0;
|
this.scrollTop = 0;
|
||||||
},
|
},
|
||||||
handlePick(item) {
|
handlePick(item) {
|
||||||
|
|||||||
@@ -12,8 +12,27 @@
|
|||||||
<u-navbar class="navbar rx-flex-fixed" :is-back="true" :title="navbarTitle" :custom-back="boundCustomBack" title-color="#000" background="{ background: '#fff' }">
|
<u-navbar class="navbar rx-flex-fixed" :is-back="true" :title="navbarTitle" :custom-back="boundCustomBack" title-color="#000" background="{ background: '#fff' }">
|
||||||
</u-navbar>
|
</u-navbar>
|
||||||
|
|
||||||
|
<!-- 外层:处方 | 病历(VIP) -->
|
||||||
|
<view v-if="canUseMedicalRecord" class="outer-tabs rx-flex-fixed flex-row">
|
||||||
|
<view
|
||||||
|
class="outer-tab"
|
||||||
|
:class="{ 'outer-tab--active': mainTab === 'rx' }"
|
||||||
|
@click="switchMainTab('rx')"
|
||||||
|
>
|
||||||
|
<text>处方</text>
|
||||||
|
</view>
|
||||||
|
<view
|
||||||
|
class="outer-tab"
|
||||||
|
:class="{ 'outer-tab--active': mainTab === 'mr' }"
|
||||||
|
@click="switchMainTab('mr')"
|
||||||
|
>
|
||||||
|
<text>病历</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 以下处方区与 HEAD 一致:直接挂在 page-flex 下,禁止再包一层 flex 列(会压矮 swiper) -->
|
||||||
<!-- 处方类型 Tab:与用户端订单列表一致,u-tabs + 下方 swiper 双向同步 -->
|
<!-- 处方类型 Tab:与用户端订单列表一致,u-tabs + 下方 swiper 双向同步 -->
|
||||||
<view v-if="!isSalespersonTransferMode" class="tabs-wrap shadow-sm rx-flex-fixed">
|
<view v-show="isRxMainVisible" v-if="!isSalespersonTransferMode" class="tabs-wrap shadow-sm rx-flex-fixed">
|
||||||
<u-tabs
|
<u-tabs
|
||||||
:list="prescriptionTabsList"
|
:list="prescriptionTabsList"
|
||||||
:is-scroll="true"
|
:is-scroll="true"
|
||||||
@@ -25,11 +44,12 @@
|
|||||||
></u-tabs>
|
></u-tabs>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="!isSalespersonTransferMode && registerModeHint" class="register-mode-hint mx-32 m-t-12 rx-flex-fixed">
|
<view v-show="isRxMainVisible" v-if="!isSalespersonTransferMode && registerModeHint" class="register-mode-hint mx-32 m-t-12 rx-flex-fixed">
|
||||||
<text class="register-mode-hint__text">{{ registerModeHint }}</text>
|
<text class="register-mode-hint__text">{{ registerModeHint }}</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
|
v-show="isRxMainVisible"
|
||||||
v-if="!isSalespersonTransferMode && hasSpecialPrescriptionRecord"
|
v-if="!isSalespersonTransferMode && hasSpecialPrescriptionRecord"
|
||||||
class="special-rx-card mx-32 m-t-12 rx-flex-fixed"
|
class="special-rx-card mx-32 m-t-12 rx-flex-fixed"
|
||||||
>
|
>
|
||||||
@@ -81,17 +101,17 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="!isSalespersonTransferMode && allowInsuranceCategory" class="mx-32 m-t-12 flex flex-ali-center rx-flex-fixed">
|
<view v-show="isRxMainVisible" v-if="!isSalespersonTransferMode && allowInsuranceCategory" class="mx-32 m-t-12 flex flex-ali-center rx-flex-fixed">
|
||||||
<u-button size="mini" :type="feeCategory === 1 ? 'success' : 'default'" @click="feeCategory = 1">自费</u-button>
|
<u-button size="mini" :type="feeCategory === 1 ? 'success' : 'default'" @click="feeCategory = 1">自费</u-button>
|
||||||
<u-button size="mini" class="m-l-16" :type="feeCategory === 2 ? 'success' : 'default'" @click="feeCategory = 2">医保</u-button>
|
<u-button size="mini" class="m-l-16" :type="feeCategory === 2 ? 'success' : 'default'" @click="feeCategory = 2">医保</u-button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view v-if="!isSalespersonTransferMode && activeCategory === 1 && hasSalespersonTransfer" class="mx-32 m-t-12 rx-flex-fixed">
|
<view v-show="isRxMainVisible" v-if="!isSalespersonTransferMode && activeCategory === 1 && hasSalespersonTransfer" class="mx-32 m-t-12 rx-flex-fixed">
|
||||||
<u-button size="mini" type="primary" plain @click="openSalespersonTransfer">查看传方</u-button>
|
<u-button size="mini" type="primary" plain @click="openSalespersonTransfer">查看传方</u-button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 诊断输入区域(传方模式不展示) -->
|
<!-- 诊断:swiper 外固定(与改 VIP 前一致),线下可手输也可点选;在线复诊必须点选 -->
|
||||||
<view v-if="!isSalespersonTransferMode" class="top white p-32 flex-col m-t-16 radius-12 mx-32 rx-flex-fixed">
|
<view v-show="isRxMainVisible" v-if="!isSalespersonTransferMode" class="top white p-32 flex-col m-t-16 radius-12 mx-32 rx-flex-fixed">
|
||||||
<view class="top_title flex-row flex-ali-center">
|
<view class="top_title flex-row flex-ali-center">
|
||||||
<image :src="require('@/static/image/js.png')" class="size-32"></image>
|
<image :src="require('@/static/image/js.png')" class="size-32"></image>
|
||||||
<d-text text="临床诊断" className="fs-32 m-l-16 font-bold" color="#333"></d-text>
|
<d-text text="临床诊断" className="fs-32 m-l-16 font-bold" color="#333"></d-text>
|
||||||
@@ -109,17 +129,24 @@
|
|||||||
@close="handleRemoveDiagnosis(i)" />
|
@close="handleRemoveDiagnosis(i)" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view class="m-t-24">
|
<view class="m-t-24 flex-row flex-ali-center">
|
||||||
<u-input
|
<u-input
|
||||||
disabled
|
class="flex-1"
|
||||||
placeholder="请选择或输入疾病诊断"
|
:disabled="isOnlineRevisit"
|
||||||
|
:value="diagnosisText"
|
||||||
|
:placeholder="isOnlineRevisit ? '请选择疾病诊断' : '请选择或输入疾病诊断'"
|
||||||
:custom-style="illnessStyle"
|
:custom-style="illnessStyle"
|
||||||
@click="handleOpenDiagnosisModal" />
|
@input="onDiagnosisTextInput"
|
||||||
|
@click="onDiagnosisInputClick" />
|
||||||
|
<view class="m-l-16 diagnosis-pick-btn" @click="handleOpenDiagnosisModal">
|
||||||
|
<d-text text="选择" className="fs-26" color="#6ACDBB"></d-text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 滚动内容:swiper flex:1 吃满剩余高度(对齐订单列表 order-swiper,不算死高度) -->
|
<!-- 滚动内容:swiper 必须是 page-flex 的直接子项,flex:1 + height:0 才能吃满剩余高度 -->
|
||||||
<swiper
|
<swiper
|
||||||
|
v-show="isRxMainVisible"
|
||||||
class="rx-order-swiper"
|
class="rx-order-swiper"
|
||||||
:current="swiperCurrent"
|
:current="swiperCurrent"
|
||||||
:disable-touch="isSalespersonTransferMode || swiperCategories.length < 2"
|
:disable-touch="isSalespersonTransferMode || swiperCategories.length < 2"
|
||||||
@@ -494,8 +521,8 @@
|
|||||||
</swiper-item>
|
</swiper-item>
|
||||||
</swiper>
|
</swiper>
|
||||||
|
|
||||||
<!-- 底部操作栏 -->
|
<!-- 底部操作栏(fixed,不占 flex;须与 swiper 同为 page-flex 直接子项) -->
|
||||||
<view class="bottom flex-jus-sp flex-ali-center safe-area-inset-bottom">
|
<view v-show="isRxMainVisible" class="bottom flex-jus-sp flex-ali-center safe-area-inset-bottom">
|
||||||
<view
|
<view
|
||||||
v-if="!isSalespersonTransferMode && !isCommonPrescription && supportsCommonPrescriptionTemplate"
|
v-if="!isSalespersonTransferMode && !isCommonPrescription && supportsCommonPrescriptionTemplate"
|
||||||
class="flex-row flex-ali-center"
|
class="flex-row flex-ali-center"
|
||||||
@@ -517,6 +544,34 @@
|
|||||||
</u-button>
|
</u-button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
病历:常挂载保草稿 / 探测 VIP。
|
||||||
|
处方 Tab:fixed 离屏探测,绝不参与 page-flex 占高。
|
||||||
|
病历 Tab:flex:1 吃满剩余高度。
|
||||||
|
-->
|
||||||
|
<view
|
||||||
|
class="mr-host"
|
||||||
|
:class="isMrMainVisible ? 'mr-host--fill' : 'mr-host--probe'"
|
||||||
|
>
|
||||||
|
<view class="mr-panel-fill">
|
||||||
|
<MedicalRecordPanel
|
||||||
|
ref="medicalRecordPanel"
|
||||||
|
:register-id="registerId"
|
||||||
|
:store-id="currentStoreId"
|
||||||
|
:user-patient-id="patientUserPatientId"
|
||||||
|
:diagnosis="diagnosisText"
|
||||||
|
:medical-advice="medicalAdvice"
|
||||||
|
:diagnosis-readonly="isOnlineRevisit"
|
||||||
|
@update:diagnosis="onMrDiagnosisUpdate"
|
||||||
|
@update:medicalAdvice="onMrAdviceUpdate"
|
||||||
|
@open-diagnosis="handleOpenDiagnosisModal"
|
||||||
|
@open-doctor-order="handleOpenDoctorOrderModal"
|
||||||
|
@vip-ok="onMedicalRecordVipOk"
|
||||||
|
@vip-denied="onMedicalRecordVipDenied"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<TraditionalTermPickerModal
|
<TraditionalTermPickerModal
|
||||||
v-model="showTcmPicker"
|
v-model="showTcmPicker"
|
||||||
:title="tcmPickerTitle"
|
:title="tcmPickerTitle"
|
||||||
@@ -655,6 +710,8 @@ import { PrescriptionCalculator } from './utils/prescriptionCalculator.js';
|
|||||||
import { PrescriptionValidator } from './utils/prescriptionValidator.js';
|
import { PrescriptionValidator } from './utils/prescriptionValidator.js';
|
||||||
import DiagnosisModal from './components/modals/DiagnosisModal.vue';
|
import DiagnosisModal from './components/modals/DiagnosisModal.vue';
|
||||||
import DoctorOrderModal from './components/modals/DoctorOrderModal.vue';
|
import DoctorOrderModal from './components/modals/DoctorOrderModal.vue';
|
||||||
|
import MedicalRecordPanel from './components/MedicalRecordPanel.vue';
|
||||||
|
import { saveMedicalRecordApi } from '@/api/reception.js';
|
||||||
import CommonPrescriptionModal from './components/modals/CommonPrescriptionModal.vue';
|
import CommonPrescriptionModal from './components/modals/CommonPrescriptionModal.vue';
|
||||||
import WesternMedicineModal from './components/modals/WesternMedicineModal.vue';
|
import WesternMedicineModal from './components/modals/WesternMedicineModal.vue';
|
||||||
import ChineseMedicineModal from './components/modals/ChineseMedicineModal.vue';
|
import ChineseMedicineModal from './components/modals/ChineseMedicineModal.vue';
|
||||||
@@ -704,6 +761,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
DiagnosisModal,
|
DiagnosisModal,
|
||||||
DoctorOrderModal,
|
DoctorOrderModal,
|
||||||
|
MedicalRecordPanel,
|
||||||
CommonPrescriptionModal,
|
CommonPrescriptionModal,
|
||||||
WesternMedicineModal,
|
WesternMedicineModal,
|
||||||
ChineseMedicineModal,
|
ChineseMedicineModal,
|
||||||
@@ -719,6 +777,10 @@ export default {
|
|||||||
return {
|
return {
|
||||||
registerId: '',
|
registerId: '',
|
||||||
patientId: '',
|
patientId: '',
|
||||||
|
/** 外层处方|病历 */
|
||||||
|
mainTab: 'rx',
|
||||||
|
/** 是否开通病历 VIP(拉取病历成功后为 true) */
|
||||||
|
canUseMedicalRecord: false,
|
||||||
pendingReusePrescriptionId: '',
|
pendingReusePrescriptionId: '',
|
||||||
patientInfo: null,
|
patientInfo: null,
|
||||||
prescriptionCategories: [
|
prescriptionCategories: [
|
||||||
@@ -880,6 +942,14 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.visiblePrescriptionCategories;
|
return this.visiblePrescriptionCategories;
|
||||||
},
|
},
|
||||||
|
/** 处方主界面可见:无 VIP 或当前在处方 Tab(病历 Tab 时整段隐藏,把高度让给病历) */
|
||||||
|
isRxMainVisible() {
|
||||||
|
return !this.canUseMedicalRecord || this.mainTab === 'rx';
|
||||||
|
},
|
||||||
|
/** 病历主界面:VIP 且当前在病历 Tab */
|
||||||
|
isMrMainVisible() {
|
||||||
|
return this.canUseMedicalRecord && this.mainTab === 'mr';
|
||||||
|
},
|
||||||
/** u-tabs 需要 { name };有 icon_text 时拼到 name 后便于看见角标;始终返回数组避免 list 非数组警告 */
|
/** u-tabs 需要 { name };有 icon_text 时拼到 name 后便于看见角标;始终返回数组避免 list 非数组警告 */
|
||||||
prescriptionTabsList() {
|
prescriptionTabsList() {
|
||||||
const list = this.visiblePrescriptionCategories || [];
|
const list = this.visiblePrescriptionCategories || [];
|
||||||
@@ -915,6 +985,20 @@ export default {
|
|||||||
|| uni.getStorageSync('store_id')
|
|| uni.getStorageSync('store_id')
|
||||||
|| null;
|
|| null;
|
||||||
},
|
},
|
||||||
|
/** 病历用门店 ID */
|
||||||
|
currentStoreId() {
|
||||||
|
return Number(
|
||||||
|
this.selectedStoreId
|
||||||
|
|| (this.registerStoreInfo && this.registerStoreInfo.store_id)
|
||||||
|
|| uni.getStorageSync('store_id')
|
||||||
|
|| 0
|
||||||
|
);
|
||||||
|
},
|
||||||
|
/** 病历用就诊人 ID */
|
||||||
|
patientUserPatientId() {
|
||||||
|
const p = this.patientInfo || {};
|
||||||
|
return Number(p.id || p.user_patient_id || this.patientId || 0);
|
||||||
|
},
|
||||||
totalProductCost() {
|
totalProductCost() {
|
||||||
if (this.currentDrugs.length === 0) return 0;
|
if (this.currentDrugs.length === 0) return 0;
|
||||||
if (this.activeCategory === 1) {
|
if (this.activeCategory === 1) {
|
||||||
@@ -1647,6 +1731,95 @@ export default {
|
|||||||
handleOpenDiagnosisModal() {
|
handleOpenDiagnosisModal() {
|
||||||
this.showDiagnosisModal = true;
|
this.showDiagnosisModal = true;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 切换外层处方|病历 Tab,并写入本地
|
||||||
|
* @param {'rx'|'mr'} tab
|
||||||
|
*/
|
||||||
|
switchMainTab(tab) {
|
||||||
|
if (tab === 'mr' && !this.canUseMedicalRecord) {
|
||||||
|
uni.showToast({ title: '未开通病历功能', icon: 'none' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.mainTab = tab;
|
||||||
|
if (this.registerId) {
|
||||||
|
PrescriptionStorage.saveRxMrTab(tab, this.registerId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/** 从本地恢复外层 Tab */
|
||||||
|
restoreMainTab() {
|
||||||
|
if (!this.registerId || !this.canUseMedicalRecord) {
|
||||||
|
this.mainTab = 'rx';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const saved = PrescriptionStorage.loadRxMrTab(this.registerId);
|
||||||
|
this.mainTab = saved || 'rx';
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 线下:输入框可编辑,按中文/英文逗号拆成诊断标签
|
||||||
|
* 在线复诊:输入框 disabled,不走此逻辑
|
||||||
|
*/
|
||||||
|
onDiagnosisTextInput(val) {
|
||||||
|
if (this.isOnlineRevisit) return;
|
||||||
|
const text = typeof val === 'string' ? val : (val && val.detail && val.detail.value) || '';
|
||||||
|
this.diagnosisText = text;
|
||||||
|
const names = String(text)
|
||||||
|
.split(/[,,]/)
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
this.diagnoses = names.map((name, index) => ({ id: `typed_${index}`, name, fromSelect: false }));
|
||||||
|
this.saveToLocalStorage();
|
||||||
|
},
|
||||||
|
/** 在线复诊点击输入框时打开常用诊断;线下不拦截手输 */
|
||||||
|
onDiagnosisInputClick() {
|
||||||
|
if (this.isOnlineRevisit) {
|
||||||
|
this.handleOpenDiagnosisModal();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMrDiagnosisUpdate(v) {
|
||||||
|
this.diagnosisText = v || '';
|
||||||
|
if (this.diagnosisText) {
|
||||||
|
const names = String(this.diagnosisText).split(/[,,]/).map((s) => s.trim()).filter(Boolean);
|
||||||
|
// 病历引用回填不算「已选择」,在线复诊仍需点常用诊断确认
|
||||||
|
this.diagnoses = names.map((name, index) => ({
|
||||||
|
id: `mr_${index}`,
|
||||||
|
name,
|
||||||
|
fromSelect: false,
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
this.diagnoses = [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onMrAdviceUpdate(v) {
|
||||||
|
this.medicalAdvice = v || '';
|
||||||
|
},
|
||||||
|
onMedicalRecordVipDenied() {
|
||||||
|
this.canUseMedicalRecord = false;
|
||||||
|
if (this.mainTab === 'mr') this.mainTab = 'rx';
|
||||||
|
},
|
||||||
|
onMedicalRecordVipOk() {
|
||||||
|
this.canUseMedicalRecord = true;
|
||||||
|
this.restoreMainTab();
|
||||||
|
},
|
||||||
|
/** 发送处方成功后同步病历(有 VIP 时) */
|
||||||
|
async syncMedicalRecordAfterSend() {
|
||||||
|
if (!this.canUseMedicalRecord) return;
|
||||||
|
try {
|
||||||
|
const panel = this.$refs.medicalRecordPanel;
|
||||||
|
const payload = panel && panel.getPayload
|
||||||
|
? panel.getPayload()
|
||||||
|
: {
|
||||||
|
diagnosis: this.diagnosisText,
|
||||||
|
doctor_order: this.medicalAdvice,
|
||||||
|
medicalAdvice: this.medicalAdvice,
|
||||||
|
register_id: Number(this.registerId),
|
||||||
|
store_id: this.currentStoreId,
|
||||||
|
user_patient_id: this.patientUserPatientId,
|
||||||
|
};
|
||||||
|
await saveMedicalRecordApi(payload);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('同步保存病历失败', e);
|
||||||
|
}
|
||||||
|
},
|
||||||
async ensureTraditionalTcmData() {
|
async ensureTraditionalTcmData() {
|
||||||
if (this.traditionalTcmData) return this.traditionalTcmData;
|
if (this.traditionalTcmData) return this.traditionalTcmData;
|
||||||
if (this.traditionalTcmLoadPromise) return this.traditionalTcmLoadPromise;
|
if (this.traditionalTcmLoadPromise) return this.traditionalTcmLoadPromise;
|
||||||
@@ -1689,7 +1862,11 @@ export default {
|
|||||||
this.diagnosisText = diagnosisText;
|
this.diagnosisText = diagnosisText;
|
||||||
if (diagnosisText) {
|
if (diagnosisText) {
|
||||||
const diagnosisNames = diagnosisText.split(',').filter(Boolean);
|
const diagnosisNames = diagnosisText.split(',').filter(Boolean);
|
||||||
this.diagnoses = diagnosisNames.map((name, index) => ({ id: `temp_${index}`, name: name }));
|
this.diagnoses = diagnosisNames.map((name, index) => ({
|
||||||
|
id: `temp_${index}`,
|
||||||
|
name: name,
|
||||||
|
fromSelect: true,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
this.diagnoses = [];
|
this.diagnoses = [];
|
||||||
}
|
}
|
||||||
@@ -2303,6 +2480,7 @@ export default {
|
|||||||
category: this.activeCategory,
|
category: this.activeCategory,
|
||||||
drugs: this.currentDrugs,
|
drugs: this.currentDrugs,
|
||||||
diagnoses: this.diagnoses,
|
diagnoses: this.diagnoses,
|
||||||
|
diagnosisText: this.diagnosisText,
|
||||||
medicalAdvice: this.medicalAdvice,
|
medicalAdvice: this.medicalAdvice,
|
||||||
chineseConfig: this.activeCategory === 1 ? this.chineseConfig : null,
|
chineseConfig: this.activeCategory === 1 ? this.chineseConfig : null,
|
||||||
requireOnlineTcm: this.activeCategory === 1 && this.isOnlineRevisit,
|
requireOnlineTcm: this.activeCategory === 1 && this.isOnlineRevisit,
|
||||||
@@ -2310,6 +2488,7 @@ export default {
|
|||||||
onlineTcmMethodId: this.onlineTcmMethodId,
|
onlineTcmMethodId: this.onlineTcmMethodId,
|
||||||
onlineTcmSyndromeId: this.onlineTcmSyndromeId,
|
onlineTcmSyndromeId: this.onlineTcmSyndromeId,
|
||||||
skipDiagnosisAndAdvice: this.isSalespersonTransferMode,
|
skipDiagnosisAndAdvice: this.isSalespersonTransferMode,
|
||||||
|
isOnlineRevisit: this.isOnlineRevisit,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
async checkChineseMedicineConflict() {
|
async checkChineseMedicineConflict() {
|
||||||
@@ -2570,6 +2749,7 @@ export default {
|
|||||||
const res = await addWestPrescription(params);
|
const res = await addWestPrescription(params);
|
||||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||||
this.$toast('发送成功');
|
this.$toast('发送成功');
|
||||||
|
await this.syncMedicalRecordAfterSend();
|
||||||
const needTransfer = res?.result?.need_transfer || res?.data?.need_transfer || res?.need_transfer;
|
const needTransfer = res?.result?.need_transfer || res?.data?.need_transfer || res?.need_transfer;
|
||||||
if (needTransfer) {
|
if (needTransfer) {
|
||||||
const transferId = res?.result?.transfer_prescription_id || res?.data?.transfer_prescription_id || res?.transfer_prescription_id;
|
const transferId = res?.result?.transfer_prescription_id || res?.data?.transfer_prescription_id || res?.transfer_prescription_id;
|
||||||
@@ -2868,7 +3048,7 @@ export default {
|
|||||||
}
|
}
|
||||||
/* 整页纵向 flex:顶部 chrome 不收缩,swiper 吃满剩余高度 */
|
/* 整页纵向 flex:顶部 chrome 不收缩,swiper 吃满剩余高度 */
|
||||||
.page-flex {
|
.page-flex {
|
||||||
height: 100vh;
|
height: 110vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -2877,6 +3057,57 @@ export default {
|
|||||||
.rx-flex-fixed {
|
.rx-flex-fixed {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.outer-tabs {
|
||||||
|
background: #fff;
|
||||||
|
padding: 0 24rpx;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.outer-tab {
|
||||||
|
padding: 20rpx 32rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
.outer-tab--active {
|
||||||
|
color: #6acdbb;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: 4rpx solid #6acdbb;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 病历宿主:
|
||||||
|
* - fill:病历 Tab,作为 page-flex 子项 flex:1 吃满
|
||||||
|
* - probe:处方 Tab / 未开通,fixed 离屏探测 VIP,不占 flex 高度(这是之前高度被压矮的根因)
|
||||||
|
*/
|
||||||
|
.mr-host--fill {
|
||||||
|
flex: 1 1 0%;
|
||||||
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.mr-host--probe {
|
||||||
|
position: fixed !important;
|
||||||
|
left: -200vw !important;
|
||||||
|
top: 0 !important;
|
||||||
|
width: 2px !important;
|
||||||
|
height: 2px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
z-index: -1 !important;
|
||||||
|
flex: 0 0 auto !important;
|
||||||
|
max-height: 0 !important;
|
||||||
|
visibility: hidden !important;
|
||||||
|
}
|
||||||
|
.mr-panel-fill {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
.safe-area-inset-bottom { padding-bottom: env(safe-area-inset-bottom); }
|
.safe-area-inset-bottom { padding-bottom: env(safe-area-inset-bottom); }
|
||||||
.mx-32 { margin-left: 32rpx; margin-right: 32rpx; }
|
.mx-32 { margin-left: 32rpx; margin-right: 32rpx; }
|
||||||
.m-x-32 { margin-left: 32rpx; margin-right: 32rpx; }
|
.m-x-32 { margin-left: 32rpx; margin-right: 32rpx; }
|
||||||
@@ -3096,6 +3327,12 @@ export default {
|
|||||||
border-radius: 12rpx;
|
border-radius: 12rpx;
|
||||||
border: 1rpx solid #d4efe8;
|
border: 1rpx solid #d4efe8;
|
||||||
}
|
}
|
||||||
|
.diagnosis-pick-btn {
|
||||||
|
flex-shrink: 0;
|
||||||
|
padding: 12rpx 20rpx;
|
||||||
|
background: #e8f6f4;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
}
|
||||||
.register-mode-hint__text {
|
.register-mode-hint__text {
|
||||||
font-size: 24rpx;
|
font-size: 24rpx;
|
||||||
color: #008771;
|
color: #008771;
|
||||||
|
|||||||
@@ -0,0 +1,116 @@
|
|||||||
|
/**
|
||||||
|
* 字典搜索匹配度 / 高亮(与 PC dictSearchRank、后端 DictSearchRankService 对齐)
|
||||||
|
* 小程序侧:按 rank_mode 决定是否本地重排;高亮始终本地拆分(禁止 v-html)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {string} keyword
|
||||||
|
* @param {string} [extra]
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export function scoreDictMatch(text, keyword, extra) {
|
||||||
|
const kw = String(keyword || '').trim()
|
||||||
|
if (!kw) return 0
|
||||||
|
const main = String(text || '').trim()
|
||||||
|
if (!main) return 0
|
||||||
|
let s = scoreOne(main, kw)
|
||||||
|
if (extra) {
|
||||||
|
s = Math.max(s, Math.floor(scoreOne(String(extra), kw) * 0.9))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
function scoreOne(haystack, needle) {
|
||||||
|
if (haystack === needle) return 100
|
||||||
|
const lowerH = haystack.toLowerCase()
|
||||||
|
const lowerN = needle.toLowerCase()
|
||||||
|
const pos = lowerH.indexOf(lowerN)
|
||||||
|
if (pos < 0) return 0
|
||||||
|
if (pos === 0) {
|
||||||
|
const lenBonus = Math.max(0, 10 - Math.abs(haystack.length - needle.length))
|
||||||
|
return 80 + Math.min(15, lenBonus)
|
||||||
|
}
|
||||||
|
return Math.max(20, 60 - Math.min(40, pos))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @param {T[]} items
|
||||||
|
* @param {string} keyword
|
||||||
|
* @param {(item: T) => string} getText
|
||||||
|
* @param {(item: T) => string} [getExtra]
|
||||||
|
*/
|
||||||
|
export function rankByMatchScore(items, keyword, getText, getExtra) {
|
||||||
|
const kw = String(keyword || '').trim()
|
||||||
|
const scored = (items || []).map((item) => {
|
||||||
|
const match_score = kw
|
||||||
|
? scoreDictMatch(getText(item), kw, getExtra ? getExtra(item) : '')
|
||||||
|
: 0
|
||||||
|
return Object.assign({}, item, { match_score })
|
||||||
|
})
|
||||||
|
if (!kw) return scored
|
||||||
|
return scored.sort((a, b) => b.match_score - a.match_score)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} text
|
||||||
|
* @param {string} keyword
|
||||||
|
* @returns {{ text: string, hit: boolean }[]}
|
||||||
|
*/
|
||||||
|
export function splitHighlight(text, keyword) {
|
||||||
|
const raw = String(text == null ? '' : text)
|
||||||
|
const kw = String(keyword || '').trim()
|
||||||
|
if (!raw || !kw) return [{ text: raw, hit: false }]
|
||||||
|
const lower = raw.toLowerCase()
|
||||||
|
const needle = kw.toLowerCase()
|
||||||
|
const parts = []
|
||||||
|
let start = 0
|
||||||
|
let idx = lower.indexOf(needle, start)
|
||||||
|
while (idx >= 0) {
|
||||||
|
if (idx > start) {
|
||||||
|
parts.push({ text: raw.slice(start, idx), hit: false })
|
||||||
|
}
|
||||||
|
parts.push({ text: raw.slice(idx, idx + kw.length), hit: true })
|
||||||
|
start = idx + kw.length
|
||||||
|
idx = lower.indexOf(needle, start)
|
||||||
|
}
|
||||||
|
if (start < raw.length) {
|
||||||
|
parts.push({ text: raw.slice(start), hit: false })
|
||||||
|
}
|
||||||
|
return parts.length ? parts : [{ text: raw, hit: false }]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按接口 rank_mode 决定是否前端重排;有关键字时保证匹配度降序并可展示分数
|
||||||
|
* @param {any[]} items
|
||||||
|
* @param {string} keyword
|
||||||
|
* @param {(item: any) => string} getText
|
||||||
|
* @param {string} [rankMode]
|
||||||
|
* @param {(item: any) => string} [getExtra] 拼音等附加匹配字段
|
||||||
|
*/
|
||||||
|
export function applyDictRankAndHighlight(items, keyword, getText, rankMode, getExtra) {
|
||||||
|
const mode = rankMode === 'frontend' ? 'frontend' : 'backend'
|
||||||
|
const kw = String(keyword || '').trim()
|
||||||
|
let list = items || []
|
||||||
|
if (mode === 'frontend') {
|
||||||
|
list = rankByMatchScore(list, keyword, getText, getExtra)
|
||||||
|
} else if (kw) {
|
||||||
|
const mapped = list.map((item) =>
|
||||||
|
Object.assign({}, item, { match_score: Number(item.match_score || 0) }),
|
||||||
|
)
|
||||||
|
const hasPositive = mapped.some((i) => Number(i.match_score) > 0)
|
||||||
|
if (!hasPositive) {
|
||||||
|
list = rankByMatchScore(list, keyword, getText, getExtra)
|
||||||
|
} else {
|
||||||
|
list = mapped.sort((a, b) => b.match_score - a.match_score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list.map((item) => {
|
||||||
|
const text = getText(item) || ''
|
||||||
|
return Object.assign({}, item, {
|
||||||
|
match_score: Number(item.match_score || 0),
|
||||||
|
_hlParts: splitHighlight(text, kw),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -88,6 +88,8 @@ export class PrescriptionStorage {
|
|||||||
});
|
});
|
||||||
// 清除激活分类
|
// 清除激活分类
|
||||||
uni.removeStorageSync(this.getActiveCategoryKey(registerId));
|
uni.removeStorageSync(this.getActiveCategoryKey(registerId));
|
||||||
|
uni.removeStorageSync(this.getRxMrTabKey(registerId));
|
||||||
|
this.clearMedicalRecordDraft(registerId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('清除所有处方数据失败:', error);
|
console.error('清除所有处方数据失败:', error);
|
||||||
}
|
}
|
||||||
@@ -141,4 +143,90 @@ export class PrescriptionStorage {
|
|||||||
return data && Array.isArray(data.drugs) && data.drugs.length > 0;
|
return data && Array.isArray(data.drugs) && data.drugs.length > 0;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 处方|病历 Tab 存储键 */
|
||||||
|
static getRxMrTabKey(registerId) {
|
||||||
|
return `${this.STORAGE_PREFIX}rxMrTab_${registerId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存外层处方|病历 Tab
|
||||||
|
* @param {'rx'|'mr'} tab
|
||||||
|
* @param {string|number} registerId
|
||||||
|
*/
|
||||||
|
static saveRxMrTab(tab, registerId) {
|
||||||
|
if (!registerId) return;
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(this.getRxMrTabKey(registerId), tab);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存处方病历Tab失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取外层处方|病历 Tab
|
||||||
|
* @param {string|number} registerId
|
||||||
|
* @returns {'rx'|'mr'|null}
|
||||||
|
*/
|
||||||
|
static loadRxMrTab(registerId) {
|
||||||
|
if (!registerId) return null;
|
||||||
|
try {
|
||||||
|
const v = uni.getStorageSync(this.getRxMrTabKey(registerId));
|
||||||
|
if (v === 'rx' || v === 'mr') return v;
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 病历草稿存储键 */
|
||||||
|
static getMedicalRecordKey(registerId) {
|
||||||
|
return `${this.STORAGE_PREFIX}medicalRecord_${registerId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存病历草稿到本地(刷新不丢)
|
||||||
|
* @param {string|number} registerId
|
||||||
|
* @param {Object} data
|
||||||
|
*/
|
||||||
|
static saveMedicalRecordDraft(registerId, data) {
|
||||||
|
if (!registerId) return;
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(
|
||||||
|
this.getMedicalRecordKey(registerId),
|
||||||
|
JSON.stringify({ ...data, _savedAt: Date.now() }),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('保存病历草稿失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取病历草稿
|
||||||
|
* @param {string|number} registerId
|
||||||
|
* @returns {Object|null}
|
||||||
|
*/
|
||||||
|
static loadMedicalRecordDraft(registerId) {
|
||||||
|
if (!registerId) return null;
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(this.getMedicalRecordKey(registerId));
|
||||||
|
if (!raw) return null;
|
||||||
|
return typeof raw === 'string' ? JSON.parse(raw) : raw;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除病历草稿
|
||||||
|
* @param {string|number} registerId
|
||||||
|
*/
|
||||||
|
static clearMedicalRecordDraft(registerId) {
|
||||||
|
if (!registerId) return;
|
||||||
|
try {
|
||||||
|
uni.removeStorageSync(this.getMedicalRecordKey(registerId));
|
||||||
|
} catch (e) {
|
||||||
|
console.error('清除病历草稿失败', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export class PrescriptionValidator {
|
|||||||
category,
|
category,
|
||||||
drugs,
|
drugs,
|
||||||
diagnoses,
|
diagnoses,
|
||||||
|
diagnosisText,
|
||||||
medicalAdvice,
|
medicalAdvice,
|
||||||
chineseConfig,
|
chineseConfig,
|
||||||
requireOnlineTcm,
|
requireOnlineTcm,
|
||||||
@@ -25,6 +26,7 @@ export class PrescriptionValidator {
|
|||||||
onlineTcmMethodId,
|
onlineTcmMethodId,
|
||||||
onlineTcmSyndromeId,
|
onlineTcmSyndromeId,
|
||||||
skipDiagnosisAndAdvice,
|
skipDiagnosisAndAdvice,
|
||||||
|
isOnlineRevisit,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
// 验证药品
|
// 验证药品
|
||||||
@@ -37,11 +39,31 @@ export class PrescriptionValidator {
|
|||||||
|
|
||||||
// 传方模式跳过诊断与医嘱校验
|
// 传方模式跳过诊断与医嘱校验
|
||||||
if (!skipDiagnosisAndAdvice) {
|
if (!skipDiagnosisAndAdvice) {
|
||||||
if (!diagnoses || diagnoses.length === 0) {
|
const hasDiagnoses = diagnoses && diagnoses.length > 0;
|
||||||
return {
|
const hasDiagnosisText = diagnosisText && String(diagnosisText).trim() !== '';
|
||||||
valid: false,
|
if (isOnlineRevisit) {
|
||||||
message: '请添加诊断'
|
// 在线复诊:必须从常用诊断弹窗选择(fromSelect=true)
|
||||||
};
|
if (!hasDiagnoses) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: '请选择诊断'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const allSelected = diagnoses.every((d) => d && d.fromSelect === true);
|
||||||
|
if (!allSelected) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: '在线复诊请从常用诊断中选择疾病诊断'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 线下:手输或选择均可
|
||||||
|
if (!hasDiagnoses && !hasDiagnosisText) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: '请填写或选择诊断'
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!medicalAdvice || medicalAdvice.trim() === '') {
|
if (!medicalAdvice || medicalAdvice.trim() === '') {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/** 中医术语表本地缓存(与 doctor-reception-wx traditional-chinese-medicine-all 配合) */
|
/** 中医术语表本地缓存(与 doctor-reception-wx traditional-chinese-medicine-all 配合) */
|
||||||
|
|
||||||
export const STORAGE_KEY = 'xk_traditional_tcm_v1';
|
/** v2:条目含 pinyin_full / pinyin_initials 展示串 */
|
||||||
|
export const STORAGE_KEY = 'xk_traditional_tcm_v2';
|
||||||
|
|
||||||
/** 默认 3 天(秒),与后端 TRADITIONAL_TCM_CACHE_TTL_SECONDS 默认一致 */
|
/** 默认 3 天(秒),与后端 TRADITIONAL_TCM_CACHE_TTL_SECONDS 默认一致 */
|
||||||
export const DEFAULT_TTL_MS = 259200 * 1000;
|
export const DEFAULT_TTL_MS = 259200 * 1000;
|
||||||
|
|||||||
Reference in New Issue
Block a user