feat: VIP功能绑定、VIP病历模块、字典模块
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
:duration="200"
|
||||
@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-y
|
||||
class="tab-scroll"
|
||||
@@ -94,7 +94,7 @@
|
||||
</view>
|
||||
<view v-else-if="t.key === 'answers'" class="card">
|
||||
<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="value">{{ a.answer || '—' }}</text>
|
||||
</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 }"
|
||||
@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">
|
||||
<u-icon name="plus" size="20"></u-icon>
|
||||
</view>
|
||||
@@ -140,6 +149,7 @@
|
||||
// 保留所有原有逻辑代码与注释
|
||||
import { getDiseaseList, getMyDiseaseList, addMyDisease, deleteMyDisease } from '@/api/reception.js';
|
||||
import { getClinicSalespersonDiseaseList } from '@/api/clinicSalesperson.js';
|
||||
import { applyDictRankAndHighlight, splitHighlight } from '../../utils/dictSearchRank.js';
|
||||
|
||||
export default {
|
||||
name: 'DiagnosisModal',
|
||||
@@ -228,29 +238,37 @@ export default {
|
||||
}
|
||||
let list = [];
|
||||
let hasMore = false;
|
||||
let rankMode = 'backend';
|
||||
|
||||
// 正确解析响应数据
|
||||
// 正确解析响应数据(分页结构为 data.list)
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
// 优先使用 data,然后是 result
|
||||
if (res.data && Array.isArray(res.data)) {
|
||||
list = res.data;
|
||||
} else if (res.result && Array.isArray(res.result)) {
|
||||
list = res.result;
|
||||
const payload = res.data !== undefined ? res.data : res.result;
|
||||
if (payload && Array.isArray(payload.list)) {
|
||||
list = payload.list;
|
||||
hasMore = !!payload.has_more;
|
||||
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)) {
|
||||
list = res;
|
||||
}
|
||||
|
||||
// 从响应中获取分页信息
|
||||
if (res.pagination) {
|
||||
hasMore = list.length >= this.pageSize;
|
||||
} else if (res.pagination) {
|
||||
hasMore = res.pagination.has_more || false;
|
||||
} else if (res.has_more !== undefined) {
|
||||
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) => {
|
||||
item.isSelect = 0;
|
||||
item.isInMyList = this.doctorMyDiseaseList.some(
|
||||
@@ -264,6 +282,7 @@ export default {
|
||||
}
|
||||
});
|
||||
}
|
||||
item._hlParts = item._hlParts || splitHighlight(item.name || '', searchKey);
|
||||
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);
|
||||
this.searchTimer = setTimeout(() => {
|
||||
if (this.searchKey && this.searchKey.trim()) {
|
||||
@@ -348,7 +371,7 @@ export default {
|
||||
this.currentPage = 1;
|
||||
this.hasMore = false;
|
||||
}
|
||||
}, 300);
|
||||
}, 500);
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -546,6 +569,13 @@ export default {
|
||||
color: #4b5563;
|
||||
}
|
||||
|
||||
.tag-score {
|
||||
margin-left: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #d4380d;
|
||||
}
|
||||
|
||||
.tag-action {
|
||||
margin-left: 12rpx;
|
||||
display: flex;
|
||||
@@ -554,6 +584,11 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
.hit-text {
|
||||
color: #cf1322;
|
||||
background: rgba(255, 214, 102, 0.55);
|
||||
}
|
||||
|
||||
.load-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -15,6 +15,18 @@
|
||||
</view>
|
||||
|
||||
<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>
|
||||
<view class="order-scroll-inner">
|
||||
<!-- 我的医嘱 -->
|
||||
@@ -30,13 +42,19 @@
|
||||
<!-- 我的医嘱列表 -->
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in doctorOrderMyList"
|
||||
v-for="item in filteredMyList"
|
||||
:key="item.id"
|
||||
class="modern-tag-item"
|
||||
:class="{ 'is-selected': item.isSelect === 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)">
|
||||
<u-icon name="close" size="20"></u-icon>
|
||||
</view>
|
||||
@@ -45,19 +63,25 @@
|
||||
</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">
|
||||
<d-text text="系统公共医嘱" className="fs-30 font-bold color-title"></d-text>
|
||||
</view>
|
||||
<view class="modern-tag-container">
|
||||
<view
|
||||
v-for="item in doctorOrderCommonList"
|
||||
v-for="item in filteredCommonList"
|
||||
:key="item.id"
|
||||
class="modern-tag-item"
|
||||
:class="{ 'is-selected': item.isSelect === 1 }"
|
||||
@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>
|
||||
@@ -124,6 +148,7 @@
|
||||
<script>
|
||||
// 保留所有原有逻辑代码与注释
|
||||
import { getDoctorOrderList, getDoctorOrderCommonList, createDoctorOrder, deleteDoctorOrder } from '@/api/reception.js';
|
||||
import { applyDictRankAndHighlight, splitHighlight } from '../../utils/dictSearchRank.js';
|
||||
|
||||
export default {
|
||||
name: 'DoctorOrderModal',
|
||||
@@ -150,14 +175,41 @@ export default {
|
||||
doctorOrderMyList: [],
|
||||
selectDoctorOrderList: '',
|
||||
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: {
|
||||
value(newVal) {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.selectDoctorOrderList = this.selectedDoctorOrder || '';
|
||||
this.searchKey = '';
|
||||
this.debouncedSearchKey = '';
|
||||
this.loadDoctorOrderListData();
|
||||
}
|
||||
},
|
||||
@@ -168,6 +220,42 @@ export default {
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
||||
.tag-score {
|
||||
margin-left: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #d4380d;
|
||||
}
|
||||
|
||||
.tag-action {
|
||||
margin-left: 12rpx;
|
||||
display: flex;
|
||||
@@ -462,4 +557,13 @@ export default {
|
||||
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 0 32rpx 8rpx;
|
||||
}
|
||||
|
||||
.hit-text {
|
||||
color: #cf1322;
|
||||
background: rgba(255, 214, 102, 0.55);
|
||||
}
|
||||
</style>
|
||||
@@ -99,6 +99,8 @@ export default {
|
||||
searchTimer: null,
|
||||
pendingId: null,
|
||||
scrollTop: 0,
|
||||
/** 防抖后的过滤关键字(输入框仍用 searchKey 即时回显) */
|
||||
filterKey: '',
|
||||
searchStyle: {
|
||||
fontSize: '28rpx',
|
||||
backgroundColor: '#fff',
|
||||
@@ -133,7 +135,7 @@ export default {
|
||||
return this.title || '请选择';
|
||||
},
|
||||
searchKeyTrim() {
|
||||
return (this.searchKey || '').trim();
|
||||
return (this.filterKey || '').trim();
|
||||
},
|
||||
displayedList() {
|
||||
const key = this.searchKeyTrim.toLowerCase();
|
||||
@@ -146,7 +148,7 @@ export default {
|
||||
if (!it) continue;
|
||||
const name = (it.name || '').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)) {
|
||||
out.push(it);
|
||||
}
|
||||
@@ -159,6 +161,7 @@ export default {
|
||||
this.show = newVal;
|
||||
if (newVal) {
|
||||
this.searchKey = '';
|
||||
this.filterKey = '';
|
||||
this.pendingId = this.selectedId != null && this.selectedId !== '' ? Number(this.selectedId) : null;
|
||||
this.scrollTop = 0;
|
||||
}
|
||||
@@ -170,14 +173,19 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSearchInput() {
|
||||
onSearchInput(val) {
|
||||
if (typeof val === 'string') {
|
||||
this.searchKey = val;
|
||||
}
|
||||
if (this.searchTimer) clearTimeout(this.searchTimer);
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.filterKey = this.searchKey || '';
|
||||
this.scrollTop = 0;
|
||||
}, 80);
|
||||
}, 500);
|
||||
},
|
||||
onSearchClear() {
|
||||
this.searchKey = '';
|
||||
this.filterKey = '';
|
||||
this.scrollTop = 0;
|
||||
},
|
||||
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>
|
||||
|
||||
<!-- 外层:处方 | 病历(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 双向同步 -->
|
||||
<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
|
||||
:list="prescriptionTabsList"
|
||||
:is-scroll="true"
|
||||
@@ -25,11 +44,12 @@
|
||||
></u-tabs>
|
||||
</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>
|
||||
</view>
|
||||
|
||||
<view
|
||||
v-show="isRxMainVisible"
|
||||
v-if="!isSalespersonTransferMode && hasSpecialPrescriptionRecord"
|
||||
class="special-rx-card mx-32 m-t-12 rx-flex-fixed"
|
||||
>
|
||||
@@ -81,17 +101,17 @@
|
||||
</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" class="m-l-16" :type="feeCategory === 2 ? 'success' : 'default'" @click="feeCategory = 2">医保</u-button>
|
||||
</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>
|
||||
</view>
|
||||
|
||||
<!-- 诊断输入区域(传方模式不展示) -->
|
||||
<view v-if="!isSalespersonTransferMode" class="top white p-32 flex-col m-t-16 radius-12 mx-32 rx-flex-fixed">
|
||||
<!-- 诊断:swiper 外固定(与改 VIP 前一致),线下可手输也可点选;在线复诊必须点选 -->
|
||||
<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">
|
||||
<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>
|
||||
@@ -109,17 +129,24 @@
|
||||
@close="handleRemoveDiagnosis(i)" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="m-t-24">
|
||||
<view class="m-t-24 flex-row flex-ali-center">
|
||||
<u-input
|
||||
disabled
|
||||
placeholder="请选择或输入疾病诊断"
|
||||
class="flex-1"
|
||||
:disabled="isOnlineRevisit"
|
||||
:value="diagnosisText"
|
||||
:placeholder="isOnlineRevisit ? '请选择疾病诊断' : '请选择或输入疾病诊断'"
|
||||
: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>
|
||||
|
||||
<!-- 滚动内容:swiper flex:1 吃满剩余高度(对齐订单列表 order-swiper,不算死高度) -->
|
||||
<!-- 滚动内容:swiper 必须是 page-flex 的直接子项,flex:1 + height:0 才能吃满剩余高度 -->
|
||||
<swiper
|
||||
v-show="isRxMainVisible"
|
||||
class="rx-order-swiper"
|
||||
:current="swiperCurrent"
|
||||
:disable-touch="isSalespersonTransferMode || swiperCategories.length < 2"
|
||||
@@ -494,8 +521,8 @@
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="bottom flex-jus-sp flex-ali-center safe-area-inset-bottom">
|
||||
<!-- 底部操作栏(fixed,不占 flex;须与 swiper 同为 page-flex 直接子项) -->
|
||||
<view v-show="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"
|
||||
@@ -517,6 +544,34 @@
|
||||
</u-button>
|
||||
</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
|
||||
v-model="showTcmPicker"
|
||||
:title="tcmPickerTitle"
|
||||
@@ -655,6 +710,8 @@ import { PrescriptionCalculator } from './utils/prescriptionCalculator.js';
|
||||
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 { saveMedicalRecordApi } from '@/api/reception.js';
|
||||
import CommonPrescriptionModal from './components/modals/CommonPrescriptionModal.vue';
|
||||
import WesternMedicineModal from './components/modals/WesternMedicineModal.vue';
|
||||
import ChineseMedicineModal from './components/modals/ChineseMedicineModal.vue';
|
||||
@@ -704,6 +761,7 @@ export default {
|
||||
components: {
|
||||
DiagnosisModal,
|
||||
DoctorOrderModal,
|
||||
MedicalRecordPanel,
|
||||
CommonPrescriptionModal,
|
||||
WesternMedicineModal,
|
||||
ChineseMedicineModal,
|
||||
@@ -719,6 +777,10 @@ export default {
|
||||
return {
|
||||
registerId: '',
|
||||
patientId: '',
|
||||
/** 外层处方|病历 */
|
||||
mainTab: 'rx',
|
||||
/** 是否开通病历 VIP(拉取病历成功后为 true) */
|
||||
canUseMedicalRecord: false,
|
||||
pendingReusePrescriptionId: '',
|
||||
patientInfo: null,
|
||||
prescriptionCategories: [
|
||||
@@ -880,6 +942,14 @@ export default {
|
||||
}
|
||||
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 非数组警告 */
|
||||
prescriptionTabsList() {
|
||||
const list = this.visiblePrescriptionCategories || [];
|
||||
@@ -915,6 +985,20 @@ export default {
|
||||
|| uni.getStorageSync('store_id')
|
||||
|| 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() {
|
||||
if (this.currentDrugs.length === 0) return 0;
|
||||
if (this.activeCategory === 1) {
|
||||
@@ -1647,6 +1731,95 @@ export default {
|
||||
handleOpenDiagnosisModal() {
|
||||
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() {
|
||||
if (this.traditionalTcmData) return this.traditionalTcmData;
|
||||
if (this.traditionalTcmLoadPromise) return this.traditionalTcmLoadPromise;
|
||||
@@ -1689,7 +1862,11 @@ export default {
|
||||
this.diagnosisText = diagnosisText;
|
||||
if (diagnosisText) {
|
||||
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 {
|
||||
this.diagnoses = [];
|
||||
}
|
||||
@@ -2303,6 +2480,7 @@ export default {
|
||||
category: this.activeCategory,
|
||||
drugs: this.currentDrugs,
|
||||
diagnoses: this.diagnoses,
|
||||
diagnosisText: this.diagnosisText,
|
||||
medicalAdvice: this.medicalAdvice,
|
||||
chineseConfig: this.activeCategory === 1 ? this.chineseConfig : null,
|
||||
requireOnlineTcm: this.activeCategory === 1 && this.isOnlineRevisit,
|
||||
@@ -2310,6 +2488,7 @@ export default {
|
||||
onlineTcmMethodId: this.onlineTcmMethodId,
|
||||
onlineTcmSyndromeId: this.onlineTcmSyndromeId,
|
||||
skipDiagnosisAndAdvice: this.isSalespersonTransferMode,
|
||||
isOnlineRevisit: this.isOnlineRevisit,
|
||||
});
|
||||
},
|
||||
async checkChineseMedicineConflict() {
|
||||
@@ -2570,6 +2749,7 @@ export default {
|
||||
const res = await addWestPrescription(params);
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
this.$toast('发送成功');
|
||||
await this.syncMedicalRecordAfterSend();
|
||||
const needTransfer = res?.result?.need_transfer || res?.data?.need_transfer || res?.need_transfer;
|
||||
if (needTransfer) {
|
||||
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 吃满剩余高度 */
|
||||
.page-flex {
|
||||
height: 100vh;
|
||||
height: 110vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -2877,6 +3057,57 @@ export default {
|
||||
.rx-flex-fixed {
|
||||
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); }
|
||||
.mx-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: 1rpx solid #d4efe8;
|
||||
}
|
||||
.diagnosis-pick-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 12rpx 20rpx;
|
||||
background: #e8f6f4;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
.register-mode-hint__text {
|
||||
font-size: 24rpx;
|
||||
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.getRxMrTabKey(registerId));
|
||||
this.clearMedicalRecordDraft(registerId);
|
||||
} catch (error) {
|
||||
console.error('清除所有处方数据失败:', error);
|
||||
}
|
||||
@@ -141,4 +143,90 @@ export class PrescriptionStorage {
|
||||
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,
|
||||
drugs,
|
||||
diagnoses,
|
||||
diagnosisText,
|
||||
medicalAdvice,
|
||||
chineseConfig,
|
||||
requireOnlineTcm,
|
||||
@@ -25,6 +26,7 @@ export class PrescriptionValidator {
|
||||
onlineTcmMethodId,
|
||||
onlineTcmSyndromeId,
|
||||
skipDiagnosisAndAdvice,
|
||||
isOnlineRevisit,
|
||||
} = params;
|
||||
|
||||
// 验证药品
|
||||
@@ -37,11 +39,31 @@ export class PrescriptionValidator {
|
||||
|
||||
// 传方模式跳过诊断与医嘱校验
|
||||
if (!skipDiagnosisAndAdvice) {
|
||||
if (!diagnoses || diagnoses.length === 0) {
|
||||
return {
|
||||
valid: false,
|
||||
message: '请添加诊断'
|
||||
};
|
||||
const hasDiagnoses = diagnoses && diagnoses.length > 0;
|
||||
const hasDiagnosisText = diagnosisText && String(diagnosisText).trim() !== '';
|
||||
if (isOnlineRevisit) {
|
||||
// 在线复诊:必须从常用诊断弹窗选择(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() === '') {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/** 中医术语表本地缓存(与 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 默认一致 */
|
||||
export const DEFAULT_TTL_MS = 259200 * 1000;
|
||||
|
||||
Reference in New Issue
Block a user