feat: 修复部分功能(健康信息、VIP、医生端健康信息导入)
This commit is contained in:
49
.cursor/rules/Api-Response.mdc
Normal file
49
.cursor/rules/Api-Response.mdc
Normal file
@@ -0,0 +1,49 @@
|
||||
---
|
||||
description: Yii 与 xk-api 接口响应取值规范(按后端固定写死,禁止混取)
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# 接口响应取值(Yii vs xk-api)
|
||||
|
||||
写请求代码时**先确认该接口走哪个后端**,再按对应字段固定取值。
|
||||
**禁止**运行时猜测(如「有 code 就取 result」),**禁止** `result || data` / `code ?? errcode` 混写兼容。
|
||||
|
||||
## 形态对照
|
||||
|
||||
| | Yii(oldApi) | xk-api(Laravel jok) |
|
||||
|---|---|---|
|
||||
| 成功码 | `errcode: 0` | `code: 0` |
|
||||
| 业务数据 | `data` | `result` |
|
||||
| 提示文案 | `msg` | `message` |
|
||||
|
||||
## 强制用法
|
||||
|
||||
统一用 `@/utils/api-response.js`,按后端选方法:
|
||||
|
||||
```js
|
||||
// 该接口是 xk-api(医生端拦截器多已返回 body)
|
||||
import { unwrapXkApi } from '@/utils/api-response.js'
|
||||
const { ok, payload, message } = unwrapXkApi(res)
|
||||
|
||||
// 该接口是 Yii
|
||||
import { unwrapYiiApi } from '@/utils/api-response.js'
|
||||
const { ok, payload, message } = unwrapYiiApi(res)
|
||||
```
|
||||
|
||||
也可直接写死:
|
||||
|
||||
```js
|
||||
// xk-api(拦截器已解包时)
|
||||
if (res.code == 0) { const data = res.result }
|
||||
|
||||
// Yii
|
||||
if (res.errcode == 0) { const data = res.data }
|
||||
```
|
||||
|
||||
API 封装文件注释里标明后端。
|
||||
|
||||
## 禁止
|
||||
|
||||
- 禁止 `unwrapApi` 一类「自动识别后端再取值」
|
||||
- 禁止 `res.result || res.data`
|
||||
- 禁止同一处同时判断 `code` 与 `errcode` 做兼容分支
|
||||
@@ -10,3 +10,4 @@ alwaysApply: true
|
||||
5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化
|
||||
6. 微信小程序 `.vue` 模板(会编译成 WXML)禁止使用 `??`、`?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.`
|
||||
7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定
|
||||
8. 接口取值:写代码时先确认接口是 xk-api 还是 Yii,再固定用 `code/result/message` 或 `errcode/data/msg`(可用 `unwrapXkApi` / `unwrapYiiApi`);禁止运行时猜测、禁止 `result || data`(详见 Api-Response.mdc)
|
||||
|
||||
@@ -10,3 +10,5 @@ alwaysApply: true
|
||||
5. 数据库的(created_at、updated_at、deleted_at)统一使用时间戳,不要使用字符串,并且我在查询器中一级格式化成字符串了,无需再次格式化
|
||||
6. 微信小程序 `.vue` 模板(会编译成 WXML)禁止使用 `??`、`?.` 等现代运算符,否则会报 `unexpected token '?'`;模板里用 `||` / 显式判断,脚本里可用 `??`/`?.`
|
||||
7. 微信小程序模板里 `:class` / `:style` 禁止写 `fn(arg)` 方法调用(如 `:class="sexClass(p)"` 会编译失败);用对象/数组字面量(如 `:class="{ male: p.sex == 1 }"`),或把结果先算进 data/computed 再绑定
|
||||
8. 接口取值:写代码时先确认接口是 xk-api 还是 Yii,再固定用 `code/result/message` 或 `errcode/data/msg`(可用 `unwrapXkApi` / `unwrapYiiApi`);禁止运行时猜测、禁止 `result || data`(详见 Api-Response.mdc)
|
||||
9. VIP 功能判断:统一用 `@/utils/vip.js` 的 `hasVipPermission` / `fetchStoreVipPermissions` / `VIP_FEATURE`;模板显隐用 `<vip-gate code="medical_record" :permissions="storeVipPermissions">`;禁止各页手写 `permissions.indexOf` 或重复解析 get-current-store-type
|
||||
|
||||
40
components/vip-gate/vip-gate.vue
Normal file
40
components/vip-gate/vip-gate.vue
Normal file
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<!-- VIP 功能显隐:有权限才渲染默认插槽 -->
|
||||
<view v-if="allowed" class="vip-gate">
|
||||
<slot />
|
||||
</view>
|
||||
<view v-else-if="showFallback" class="vip-gate vip-gate--fallback">
|
||||
<slot name="fallback" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 全局 VIP 显隐组件
|
||||
* 用法:
|
||||
* <vip-gate code="medical_record" :permissions="storeVipPermissions">...</vip-gate>
|
||||
* code 也可用 VIP_FEATURE 常量;permissions 必须传履约诊所权限列表
|
||||
*/
|
||||
import { hasVipPermission } from '@/utils/vip.js'
|
||||
|
||||
export default {
|
||||
name: 'VipGate',
|
||||
props: {
|
||||
/** 功能码,如 medical_record */
|
||||
code: { type: String, default: '' },
|
||||
/** 履约诊所 permissions 数组 */
|
||||
permissions: { type: Array, default: null },
|
||||
/** 无权限时是否渲染 fallback 插槽 */
|
||||
fallback: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
allowed() {
|
||||
if (!this.code) return true
|
||||
return hasVipPermission(this.code, this.permissions)
|
||||
},
|
||||
showFallback() {
|
||||
return !!this.fallback && !this.allowed
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -89,6 +89,10 @@
|
||||
<view v-if="t.key === 'info'" class="health-card">
|
||||
<text class="section-title">健康信息</text>
|
||||
<template v-if="health">
|
||||
<view class="info-row">
|
||||
<text class="info-label">现病史</text>
|
||||
<text class="info-value">{{ historyText(health.present_status, health.present_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">既往史</text>
|
||||
<text class="info-value">{{ historyText(health.person_status, health.person_history) }}</text>
|
||||
@@ -104,6 +108,22 @@
|
||||
<text class="info-label">家族遗传史</text>
|
||||
<text class="info-value">{{ historyText(health.family_status, health.family_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">流行病学史</text>
|
||||
<text class="info-value">{{ historyText(health.epidemic_status, health.epidemic_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">个人史</text>
|
||||
<text class="info-value">{{ historyText(health.personal_status, health.personal_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="showMenstrualHistory">
|
||||
<text class="info-label">月经史</text>
|
||||
<text class="info-value">{{ historyText(health.menstrual_status, health.menstrual_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="showMaritalHistory">
|
||||
<text class="info-label">婚育史</text>
|
||||
<text class="info-value">{{ historyText(health.marital_status, health.marital_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">肝功能异常</text>
|
||||
<text
|
||||
@@ -171,7 +191,7 @@
|
||||
<template v-else-if="t.key === 'prescription'">
|
||||
<text class="r-title">{{ item.prescription_no || '—' }}</text>
|
||||
<text class="r-sub">{{ item.store || '—' }} · {{ item.doctor || '—' }}</text>
|
||||
<text class="r-sub">{{ item.clinical_diagnose || '—' }} · {{ item.created_at || '' }}</text>
|
||||
<text class="r-sub">{{ item.is_pay == 1 ? '已支付' : '未支付' }} · {{ item.clinical_diagnose || '—' }} · {{ item.created_at || '' }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="r-title">{{ item.order_no || '—' }}</text>
|
||||
@@ -298,6 +318,16 @@ export default {
|
||||
if (Number(sex) === 2) return 'female';
|
||||
return '';
|
||||
},
|
||||
/** 月经史仅女 */
|
||||
showMenstrualHistory() {
|
||||
return Number((this.profile.patient && this.profile.patient.sex) || 0) === 2;
|
||||
},
|
||||
/** 婚育史:男>22 或 女>20 */
|
||||
showMaritalHistory() {
|
||||
const sex = Number((this.profile.patient && this.profile.patient.sex) || 0);
|
||||
const age = Number((this.profile.patient && this.profile.patient.age) || 0);
|
||||
return (sex === 1 && age > 22) || (sex === 2 && age > 20);
|
||||
},
|
||||
/**
|
||||
* 回访拨号:只用就诊人 call_mobile(拦截器只解密不脱敏)
|
||||
* 不用脱敏后的 mobile,也不回退微信用户号
|
||||
|
||||
@@ -69,6 +69,10 @@
|
||||
<view v-if="t.key === 'info'" class="health-card">
|
||||
<text class="section-title">健康信息</text>
|
||||
<template v-if="health">
|
||||
<view class="info-row">
|
||||
<text class="info-label">现病史</text>
|
||||
<text class="info-value">{{ historyText(health.present_status, health.present_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">既往史</text>
|
||||
<text class="info-value">{{ historyText(health.person_status, health.person_history) }}</text>
|
||||
@@ -84,6 +88,22 @@
|
||||
<text class="info-label">家族遗传史</text>
|
||||
<text class="info-value">{{ historyText(health.family_status, health.family_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">流行病学史</text>
|
||||
<text class="info-value">{{ historyText(health.epidemic_status, health.epidemic_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">个人史</text>
|
||||
<text class="info-value">{{ historyText(health.personal_status, health.personal_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="showMenstrualHistory">
|
||||
<text class="info-label">月经史</text>
|
||||
<text class="info-value">{{ historyText(health.menstrual_status, health.menstrual_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="showMaritalHistory">
|
||||
<text class="info-label">婚育史</text>
|
||||
<text class="info-value">{{ historyText(health.marital_status, health.marital_history) }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">肝功能异常</text>
|
||||
<text
|
||||
@@ -152,7 +172,7 @@
|
||||
<template v-else-if="t.key === 'prescription'">
|
||||
<text class="r-title">{{ item.prescription_no || '—' }}</text>
|
||||
<text class="r-sub">{{ item.store || '—' }} · {{ item.doctor || '—' }}</text>
|
||||
<text class="r-sub">{{ item.clinical_diagnose || '—' }} · {{ item.created_at || '' }}</text>
|
||||
<text class="r-sub">{{ item.is_pay == 1 ? '已支付' : '未支付' }} · {{ item.clinical_diagnose || '—' }} · {{ item.created_at || '' }}</text>
|
||||
</template>
|
||||
<template v-else>
|
||||
<text class="r-title">{{ item.order_no || '—' }}</text>
|
||||
@@ -264,6 +284,16 @@ export default {
|
||||
if (Number(sex) === 2) return 'female';
|
||||
return '';
|
||||
},
|
||||
/** 月经史仅女 */
|
||||
showMenstrualHistory() {
|
||||
return Number((this.profile.patient && this.profile.patient.sex) || 0) === 2;
|
||||
},
|
||||
/** 婚育史:男>22 或 女>20 */
|
||||
showMaritalHistory() {
|
||||
const sex = Number((this.profile.patient && this.profile.patient.sex) || 0);
|
||||
const age = Number((this.profile.patient && this.profile.patient.age) || 0);
|
||||
return (sex === 1 && age > 22) || (sex === 2 && age > 20);
|
||||
},
|
||||
/**
|
||||
* 回访拨号:只用就诊人 call_mobile(拦截器只解密不脱敏)
|
||||
* 不用脱敏后的 mobile,也不回退微信用户号
|
||||
|
||||
@@ -15,7 +15,13 @@
|
||||
<u-icon margin-left="8" label-size="32" label-color="#6C7380"
|
||||
:name="require('@/static/image/ys.png')" :label="item.prescription_no"
|
||||
color="#6ACDBB" size="32"></u-icon>
|
||||
<d-text :text="statusName[item.status]"></d-text>
|
||||
<view class="flex-row flex-ali-center">
|
||||
<text
|
||||
class="pay-tag"
|
||||
:class="{ 'pay-tag--paid': item.is_pay == 1 }"
|
||||
>{{ item.is_pay == 1 ? '已支付' : '未支付' }}</text>
|
||||
<d-text :text="statusName[item.status]"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="flex-row flex-ali-center flex-jus-sp m-t-2">
|
||||
<d-text className="content-c fs-32" :text="getInfo(item.patient)"></d-text>
|
||||
@@ -189,4 +195,12 @@
|
||||
.search {
|
||||
padding: 16rpx 32rpx;
|
||||
}
|
||||
.pay-tag {
|
||||
margin-right: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #F59E0B;
|
||||
}
|
||||
.pay-tag--paid {
|
||||
color: #10B981;
|
||||
}
|
||||
</style>
|
||||
@@ -404,6 +404,41 @@
|
||||
@overlay-change="onAiOverlayChange"
|
||||
@import="onAiConfirmImport"
|
||||
/>
|
||||
|
||||
<!-- 档案史与病历史不一致:自定义预览确认(不用原生 showModal,文案会超长) -->
|
||||
<u-modal
|
||||
v-model="historyImportShow"
|
||||
:show-cancel-button="true"
|
||||
:mask-close-able="false"
|
||||
title="导入患者档案病史?"
|
||||
confirm-text="确认导入"
|
||||
cancel-text="暂不导入"
|
||||
@confirm="onConfirmHistoryImport"
|
||||
@cancel="onCancelHistoryImport"
|
||||
>
|
||||
<view class="hist-import-body">
|
||||
<text class="hist-import-tip">就诊人档案病史与当前病历不一致,确认后用档案内容覆盖对应字段(仍需保存病历)。</text>
|
||||
<scroll-view scroll-y class="hist-import-scroll">
|
||||
<view
|
||||
v-for="row in historyImportRows"
|
||||
:key="row.field"
|
||||
class="hist-import-card"
|
||||
>
|
||||
<text class="hist-import-label">{{ row.label }}</text>
|
||||
<view class="hist-import-cols">
|
||||
<view class="hist-import-col">
|
||||
<text class="hist-import-tag">当前病历</text>
|
||||
<text class="hist-import-val">{{ row.current || '无' }}</text>
|
||||
</view>
|
||||
<view class="hist-import-col hist-import-col--import">
|
||||
<text class="hist-import-tag hist-import-tag--import">将导入</text>
|
||||
<text class="hist-import-val">{{ row.import || '无' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-modal>
|
||||
</view>
|
||||
</template>
|
||||
<script>
|
||||
@@ -558,6 +593,11 @@ export default {
|
||||
hydrating: false,
|
||||
// AI写病历抽屉显隐
|
||||
aiShow: false,
|
||||
/** 档案史导入确认弹窗 */
|
||||
historyImportShow: false,
|
||||
historyImportRows: [],
|
||||
/** 本面板实例内已提示过的挂号 id,避免切 Tab 反复弹 */
|
||||
_historyImportAskedMap: {},
|
||||
_draftTimer: null,
|
||||
areaStyle: { background: '#f4f6f8', borderRadius: '12rpx', padding: '18rpx', minHeight: '80rpx', fontSize: '28rpx', color: '#1a2530', lineHeight: '1.6' },
|
||||
leftFields: [
|
||||
@@ -660,6 +700,9 @@ export default {
|
||||
aiShow() {
|
||||
this.emitOverlayChange()
|
||||
},
|
||||
historyImportShow() {
|
||||
this.emitOverlayChange()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
@@ -675,7 +718,7 @@ export default {
|
||||
* 通知父页:病历侧是否有弹层打开(与处方弹层共用返回拦截)
|
||||
*/
|
||||
emitOverlayChange() {
|
||||
this.$emit('overlay-change', !!(this.citeShow || this.entryShow || this.aiShow))
|
||||
this.$emit('overlay-change', !!(this.citeShow || this.entryShow || this.aiShow || this.historyImportShow))
|
||||
},
|
||||
/**
|
||||
* 供父页返回键先关抽屉(不退开方页)
|
||||
@@ -684,6 +727,7 @@ export default {
|
||||
this.citeShow = false
|
||||
this.entryShow = false
|
||||
this.entryTcmType = ''
|
||||
this.historyImportShow = false
|
||||
this.closeAiModal()
|
||||
},
|
||||
closeAiModal() {
|
||||
@@ -794,6 +838,7 @@ export default {
|
||||
},
|
||||
async load() {
|
||||
this.hydrating = true
|
||||
let historyDiff = []
|
||||
try {
|
||||
const mrParams = { register_id: this.registerId }
|
||||
const mrStoreId = Number(this.storeId || 0)
|
||||
@@ -801,7 +846,11 @@ export default {
|
||||
const res = await getMedicalRecordApi(mrParams)
|
||||
// Laravel jok 在 result;兼容 data / 已解包
|
||||
const data = (res && (res.result != null ? res.result : res.data)) || res || {}
|
||||
historyDiff = Array.isArray(data.history_import_diff) ? data.history_import_diff : []
|
||||
this.form = Object.assign(emptyForm(), data)
|
||||
// 提示字段不进表单/草稿,避免脏数据
|
||||
delete this.form.history_import_diff
|
||||
delete this.form.patient_history_defaults
|
||||
const draft = PrescriptionStorage.loadMedicalRecordDraft(this.registerId)
|
||||
if (draft) {
|
||||
this.applyDraft(draft)
|
||||
@@ -827,9 +876,68 @@ export default {
|
||||
this.hydrating = false
|
||||
this.$nextTick(() => {
|
||||
this.emitSyncToPrescription()
|
||||
this.maybeAskHistoryImport(historyDiff)
|
||||
})
|
||||
}
|
||||
},
|
||||
/** 空串与「无」视为同一默认态 */
|
||||
normalizeHistoryCompare(value) {
|
||||
const t = value == null ? '' : String(value).trim()
|
||||
return t === '' || t === '无' ? '无' : t
|
||||
},
|
||||
/**
|
||||
* 草稿合并后对比档案史与当前表单;有差异则弹自定义预览确认
|
||||
*/
|
||||
maybeAskHistoryImport(rawDiff) {
|
||||
const rid = Number(this.registerId || 0)
|
||||
if (!rid || !Array.isArray(rawDiff) || !rawDiff.length) return
|
||||
if (this._historyImportAskedMap && this._historyImportAskedMap[rid]) return
|
||||
const rows = []
|
||||
for (let i = 0; i < rawDiff.length; i++) {
|
||||
const item = rawDiff[i]
|
||||
if (!item || !item.field) continue
|
||||
const current = this.normalizeHistoryCompare(this.form[item.field])
|
||||
const importVal = this.normalizeHistoryCompare(item.import)
|
||||
if (current === importVal) continue
|
||||
rows.push({
|
||||
field: item.field,
|
||||
label: item.label || item.field,
|
||||
current: current,
|
||||
import: importVal,
|
||||
})
|
||||
}
|
||||
if (!rows.length) return
|
||||
this.historyImportRows = rows
|
||||
this.historyImportShow = true
|
||||
},
|
||||
/** 确认导入:只改表单草稿,不自动落库 */
|
||||
onConfirmHistoryImport() {
|
||||
const rows = this.historyImportRows || []
|
||||
const patch = {}
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i]
|
||||
if (row && row.field) {
|
||||
patch[row.field] = row.import || '无'
|
||||
}
|
||||
}
|
||||
this.patchFields(patch)
|
||||
const rid = Number(this.registerId || 0)
|
||||
if (rid) {
|
||||
this._historyImportAskedMap[rid] = 1
|
||||
}
|
||||
this.historyImportShow = false
|
||||
this.historyImportRows = []
|
||||
uni.showToast({ title: '已导入档案病史', icon: 'none' })
|
||||
},
|
||||
/** 暂不导入:本会话不再提示 */
|
||||
onCancelHistoryImport() {
|
||||
const rid = Number(this.registerId || 0)
|
||||
if (rid) {
|
||||
this._historyImportAskedMap[rid] = 1
|
||||
}
|
||||
this.historyImportShow = false
|
||||
this.historyImportRows = []
|
||||
},
|
||||
getPayload() {
|
||||
return Object.assign({}, this.form, {
|
||||
register_id: Number(this.registerId),
|
||||
@@ -1834,4 +1942,65 @@ export default {
|
||||
.ai-btn--disabled {
|
||||
opacity: 0.45;
|
||||
}
|
||||
/* 档案史导入确认预览 */
|
||||
.hist-import-body {
|
||||
padding: 8rpx 24rpx 16rpx;
|
||||
}
|
||||
.hist-import-tip {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.hist-import-scroll {
|
||||
max-height: 520rpx;
|
||||
}
|
||||
.hist-import-card {
|
||||
background: #f8fafc;
|
||||
border-radius: 12rpx;
|
||||
padding: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.hist-import-label {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.hist-import-cols {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.hist-import-col {
|
||||
flex: 1;
|
||||
background: #fff;
|
||||
border-radius: 8rpx;
|
||||
padding: 12rpx;
|
||||
border: 1rpx solid #e2e8f0;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
.hist-import-col--import {
|
||||
margin-right: 0;
|
||||
border-color: #6acdbb;
|
||||
background: #f0faf8;
|
||||
}
|
||||
.hist-import-tag {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.hist-import-tag--import {
|
||||
color: #0d9488;
|
||||
font-weight: 600;
|
||||
}
|
||||
.hist-import-val {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #0f172a;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -805,6 +805,11 @@ import { getChatRoomByRegisterApi, sendToUserHttpApi } from '@/api/chat.js';
|
||||
import { PrescriptionStorage } from './utils/prescriptionStorage.js';
|
||||
import { PrescriptionCalculator } from './utils/prescriptionCalculator.js';
|
||||
import { PrescriptionValidator } from './utils/prescriptionValidator.js';
|
||||
import {
|
||||
hasVipPermission,
|
||||
parseVipPermissionsFromRes,
|
||||
VIP_FEATURE,
|
||||
} from '@/utils/vip.js';
|
||||
import DiagnosisModal from './components/modals/DiagnosisModal.vue';
|
||||
import DoctorOrderModal from './components/modals/DoctorOrderModal.vue';
|
||||
import MedicalRecordPanel from './components/MedicalRecordPanel.vue';
|
||||
@@ -978,6 +983,8 @@ export default {
|
||||
specialPrescriptionRecord: null,
|
||||
applyingSpecialPrescription: false,
|
||||
pendingImportSpecial: false,
|
||||
/** 外部指定初始 Tab(如导入健康史后进病历) */
|
||||
pendingMainTab: '',
|
||||
/** 医生已导入的特色方 ID,仅 >0 时提交走特色方计价 */
|
||||
appliedSpecialPrescriptionId: 0,
|
||||
appliedSpecialPrescriptionFreeShipping: false,
|
||||
@@ -1214,15 +1221,15 @@ export default {
|
||||
},
|
||||
/** 门店 VIP:AI辅助出方 */
|
||||
canUseAiPrescription() {
|
||||
return this.hasStoreVipPermission('ai_prescription');
|
||||
return this.hasStoreVipPermission(VIP_FEATURE.AI_PRESCRIPTION);
|
||||
},
|
||||
/** 门店 VIP:AI写病历 */
|
||||
canUseAiMedicalRecord() {
|
||||
return this.hasStoreVipPermission('ai_medical_record');
|
||||
return this.hasStoreVipPermission(VIP_FEATURE.AI_MEDICAL_RECORD);
|
||||
},
|
||||
/** 门店 VIP:金方导入 */
|
||||
canUseGoldenFormula() {
|
||||
return this.hasStoreVipPermission('golden_formula');
|
||||
return this.hasStoreVipPermission(VIP_FEATURE.GOLDEN_FORMULA);
|
||||
},
|
||||
navbarTitle() {
|
||||
if (this.isSalespersonTransferMode) {
|
||||
@@ -1317,6 +1324,8 @@ export default {
|
||||
this.patientId = options.patient_id || '';
|
||||
this.pendingReusePrescriptionId = options.reuse_prescription_id ? String(options.reuse_prescription_id) : '';
|
||||
this.pendingImportSpecial = String(options.import_special) === '1';
|
||||
// 从患者详情「导入到病历」进入时强制切病历 Tab
|
||||
this.pendingMainTab = String(options.tab || '') === 'mr' ? 'mr' : '';
|
||||
this.isCommonPrescription = String(options.save_common) === '1';
|
||||
if (!this.isCommonPrescription) {
|
||||
uni.removeStorageSync('add');
|
||||
@@ -1770,22 +1779,18 @@ export default {
|
||||
this.closePreSendPriceAdjust();
|
||||
},
|
||||
/**
|
||||
* 判断当前诊所 VIP 是否含指定权益码
|
||||
* 判断当前诊所 VIP 是否含指定权益码(统一走全局 hasVipPermission)
|
||||
*/
|
||||
hasStoreVipPermission(code) {
|
||||
const list = this.storeVipPermissions;
|
||||
return Array.isArray(list) && list.indexOf(code) >= 0;
|
||||
return hasVipPermission(code, this.storeVipPermissions);
|
||||
},
|
||||
/**
|
||||
* 从门店类型接口结果写入 VIP permissions
|
||||
* 有 medical_record 才显示病历 Tab;无则静默留在处方(不打 get-medical-record 避免弹 VIP 错)
|
||||
*/
|
||||
applyStoreVipFromRes(res) {
|
||||
const data = (res && (res.result != null ? res.result : res.data != null ? res.data : res)) || {};
|
||||
const vip = data.vip || {};
|
||||
const perms = vip.permissions;
|
||||
this.storeVipPermissions = Array.isArray(perms) ? perms.slice() : [];
|
||||
const allowMr = this.hasStoreVipPermission('medical_record');
|
||||
this.storeVipPermissions = parseVipPermissionsFromRes(res);
|
||||
const allowMr = hasVipPermission(VIP_FEATURE.MEDICAL_RECORD, this.storeVipPermissions);
|
||||
this.canUseMedicalRecord = allowMr;
|
||||
if (allowMr) {
|
||||
this.restoreMainTab();
|
||||
@@ -1965,12 +1970,18 @@ export default {
|
||||
});
|
||||
}
|
||||
},
|
||||
/** 从本地恢复外层 Tab */
|
||||
/** 从本地恢复外层 Tab;URL tab=mr 优先一次 */
|
||||
restoreMainTab() {
|
||||
if (!this.registerId || !this.canUseMedicalRecord) {
|
||||
this.mainTab = 'rx';
|
||||
return;
|
||||
}
|
||||
if (this.pendingMainTab === 'mr') {
|
||||
this.mainTab = 'mr';
|
||||
this.pendingMainTab = '';
|
||||
PrescriptionStorage.saveRxMrTab('mr', this.registerId);
|
||||
return;
|
||||
}
|
||||
const saved = PrescriptionStorage.loadRxMrTab(this.registerId);
|
||||
this.mainTab = saved || 'rx';
|
||||
},
|
||||
@@ -3623,7 +3634,7 @@ export default {
|
||||
}
|
||||
/* 整页纵向 flex:顶部 chrome 不收缩,swiper 吃满剩余高度 */
|
||||
.page-flex {
|
||||
height: 110vh;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* 就诊人健康问诊 → 病历「史」字段
|
||||
* 与 PC / 后端 resolveHistoryDefaultsFromPatient 映射一致
|
||||
* present→现病史,person→既往史,allergic→过敏史,family→家族史,其余同名
|
||||
*/
|
||||
|
||||
const HISTORY_MAP = [
|
||||
{ statusKey: 'present_status', historyKey: 'present_history', mrKey: 'present_illness', visibility: 'always' },
|
||||
{ statusKey: 'person_status', historyKey: 'person_history', mrKey: 'past_history', visibility: 'always' },
|
||||
{ statusKey: 'allergic_status', historyKey: 'allergic_history', mrKey: 'allergy_history', visibility: 'always' },
|
||||
{ statusKey: 'family_status', historyKey: 'family_history', mrKey: 'family_history', visibility: 'always' },
|
||||
{ statusKey: 'epidemic_status', historyKey: 'epidemic_history', mrKey: 'epidemic_history', visibility: 'always' },
|
||||
{ statusKey: 'personal_status', historyKey: 'personal_history', mrKey: 'personal_history', visibility: 'always' },
|
||||
{ statusKey: 'menstrual_status', historyKey: 'menstrual_history', mrKey: 'menstrual_history', visibility: 'menstrual' },
|
||||
{ statusKey: 'marital_status', historyKey: 'marital_history', mrKey: 'marital_history', visibility: 'marital' },
|
||||
]
|
||||
|
||||
function isFieldVisible(visibility, sex, age) {
|
||||
if (visibility === 'menstrual') return Number(sex) === 2
|
||||
if (visibility === 'marital') {
|
||||
const s = Number(sex)
|
||||
const a = Number(age) || 0
|
||||
return (s === 1 && a > 22) || (s === 2 && a > 20)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function resolveHistoryText(status, raw) {
|
||||
if (Number(status) !== 1) return '无'
|
||||
if (Array.isArray(raw)) {
|
||||
const parts = raw.map((v) => String(v == null ? '' : v).trim()).filter(Boolean)
|
||||
return parts.length ? parts.join('、') : '无'
|
||||
}
|
||||
const str = String(raw == null ? '' : raw).trim()
|
||||
if (!str || str === '[]' || str === 'null') return '无'
|
||||
try {
|
||||
const decoded = JSON.parse(str)
|
||||
if (Array.isArray(decoded)) {
|
||||
const parts = decoded.map((v) => String(v == null ? '' : v).trim()).filter(Boolean)
|
||||
return parts.length ? parts.join('、') : '无'
|
||||
}
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
const parts = str.split(/[,,、]+/).map((s) => s.trim()).filter(Boolean)
|
||||
return parts.length ? parts.join('、') : '无'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Record<string, any>|null|undefined} health
|
||||
* @param {{ sex?: number, age?: number }} [opts]
|
||||
* @returns {Record<string, string>}
|
||||
*/
|
||||
export function mapHealthInquiryToMrHistory(health, opts) {
|
||||
const hi = health || {}
|
||||
const sex = Number((opts && opts.sex) || 0)
|
||||
const age = Number((opts && opts.age) || 0)
|
||||
const out = {}
|
||||
for (let i = 0; i < HISTORY_MAP.length; i++) {
|
||||
const row = HISTORY_MAP[i]
|
||||
if (!isFieldVisible(row.visibility, sex, age)) continue
|
||||
out[row.mrKey] = resolveHistoryText(hi[row.statusKey], hi[row.historyKey])
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -120,20 +120,48 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 健康信息 -->
|
||||
<!-- 健康信息:与就诊人档案七类史 + 肝肾对齐;有病历 VIP 可导入 -->
|
||||
<view class="modern-card">
|
||||
<view class="card-title">健康信息</view>
|
||||
<view class="card-title-row flex-row flex-jus-sp flex-ali-center">
|
||||
<view class="card-title">健康信息</view>
|
||||
<vip-gate :code="vipFeatureMedicalRecord" :permissions="storeVipPermissions">
|
||||
<view
|
||||
class="health-import-btn"
|
||||
@click="onImportHealthToMedicalRecord"
|
||||
>导入到病历</view>
|
||||
</vip-gate>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">现病史</text>
|
||||
<text class="info-value">{{ healthHistoryText(infoList.healthInquery, 'present') }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">既往史</text>
|
||||
<text class="info-value">{{infoList.healthInquery.person_status==0?'无':infoList.healthInquery.person_history}}</text>
|
||||
<text class="info-value">{{ healthHistoryText(infoList.healthInquery, 'person') }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">过敏史</text>
|
||||
<text class="info-value" :class="{'error-text font-bold': infoList.healthInquery.allergic_status!=0}">{{infoList.healthInquery.allergic_status==0?'无':infoList.healthInquery.allergic_history}}</text>
|
||||
<text class="info-value" :class="{'error-text font-bold': infoList.healthInquery.allergic_status!=0}">{{ healthHistoryText(infoList.healthInquery, 'allergic') }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">家族遗传史</text>
|
||||
<text class="info-value">{{infoList.healthInquery.family_status==0?'无':infoList.healthInquery.family_history}}</text>
|
||||
<text class="info-value">{{ healthHistoryText(infoList.healthInquery, 'family') }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">流行病学史</text>
|
||||
<text class="info-value">{{ healthHistoryText(infoList.healthInquery, 'epidemic') }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">个人史</text>
|
||||
<text class="info-value">{{ healthHistoryText(infoList.healthInquery, 'personal') }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="showMenstrualHistory">
|
||||
<text class="info-label">月经史</text>
|
||||
<text class="info-value">{{ healthHistoryText(infoList.healthInquery, 'menstrual') }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="showMaritalHistory">
|
||||
<text class="info-label">婚育史</text>
|
||||
<text class="info-value">{{ healthHistoryText(infoList.healthInquery, 'marital') }}</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">肝功能异常</text>
|
||||
@@ -173,6 +201,14 @@
|
||||
<u-icon name="file-text" color="#00A88A" size="32" class="m-r-8"></u-icon>
|
||||
<text class="rx-no font-bold color-title">{{item.prescription_no}}</text>
|
||||
</view>
|
||||
<text
|
||||
class="rx-pay-tag"
|
||||
:class="{
|
||||
'rx-pay-tag--paid': item.order_is_pay == 1,
|
||||
'rx-pay-tag--wait': item.order_pay_text == '待支付',
|
||||
'rx-pay-tag--muted': item.order_pay_text == '无订单' || item.order_pay_text == '已取消'
|
||||
}"
|
||||
>{{ item.order_pay_text || payTextByIsPay(item.is_pay) }}</text>
|
||||
</view>
|
||||
<view class="rx-actions flex-row flex-jus-end m-t-16">
|
||||
<view v-if="item.can_withdraw" class="action-btn-ghost danger" @click="withdrawRxByIndex('current', rxIdx)">撤回</view>
|
||||
@@ -202,6 +238,14 @@
|
||||
<u-icon name="file-text" color="#8A92A3" size="32" class="m-r-8"></u-icon>
|
||||
<text class="rx-no color-title">{{item.prescription_no}}</text>
|
||||
</view>
|
||||
<text
|
||||
class="rx-pay-tag"
|
||||
:class="{
|
||||
'rx-pay-tag--paid': item.order_is_pay == 1,
|
||||
'rx-pay-tag--wait': item.order_pay_text == '待支付',
|
||||
'rx-pay-tag--muted': item.order_pay_text == '无订单' || item.order_pay_text == '已取消'
|
||||
}"
|
||||
>{{ item.order_pay_text || payTextByIsPay(item.is_pay) }}</text>
|
||||
</view>
|
||||
<view class="rx-actions flex-row flex-jus-end m-t-16">
|
||||
<view v-if="item.can_withdraw" class="action-btn-ghost danger" @click="withdrawRxByIndex('history', idx)">撤回</view>
|
||||
@@ -252,18 +296,36 @@ import {
|
||||
getPatientPrescriptionHistory,
|
||||
withdrawPrescriptionWx,
|
||||
receptionApi,
|
||||
endOfDiagnosisApi
|
||||
endOfDiagnosisApi,
|
||||
} from '@/api/reception.js'
|
||||
import { pickSpecialPrescriptionRecord, resolveSpecialPrescriptionPurchaseLabel } from '@/subPackages/sub_workbench/prescription_v2/utils/specialPrescription.js'
|
||||
import { mapHealthInquiryToMrHistory } from '@/subPackages/sub_workbench/prescription_v2/utils/mapHealthInquiryToMrHistory.js'
|
||||
import { PrescriptionStorage } from '@/subPackages/sub_workbench/prescription_v2/utils/prescriptionStorage.js'
|
||||
import {
|
||||
fetchStoreVipPermissions,
|
||||
hasVipPermission,
|
||||
VIP_FEATURE,
|
||||
} from '@/utils/vip.js'
|
||||
import VipGate from '@/components/vip-gate/vip-gate.vue'
|
||||
|
||||
function emptyHealthInquiry() {
|
||||
return {
|
||||
present_status: 0,
|
||||
present_history: '',
|
||||
person_status: 0,
|
||||
person_history: '',
|
||||
allergic_status: 0,
|
||||
allergic_history: '',
|
||||
family_status: 0,
|
||||
family_history: '',
|
||||
epidemic_status: 0,
|
||||
epidemic_history: '',
|
||||
personal_status: 0,
|
||||
personal_history: '',
|
||||
menstrual_status: 0,
|
||||
menstrual_history: '',
|
||||
marital_status: 0,
|
||||
marital_history: '',
|
||||
liver_function: 0,
|
||||
renal_function: 0
|
||||
}
|
||||
@@ -295,8 +357,13 @@ function normalizeRegisterDetail(raw) {
|
||||
}
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VipGate,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
/** 给模板 vip-gate 用的功能码(模板禁止直接写导入常量较别扭) */
|
||||
vipFeatureMedicalRecord: VIP_FEATURE.MEDICAL_RECORD,
|
||||
flag: true,
|
||||
item: {},
|
||||
status: "loading",
|
||||
@@ -318,14 +385,31 @@ export default {
|
||||
rxHistoryNextPage: 1,
|
||||
rxCurrentHasMore: true,
|
||||
rxHistoryHasMore: true,
|
||||
rxLoadingMore: false
|
||||
rxLoadingMore: false,
|
||||
/** 履约诊所 VIP permissions(全局判断用) */
|
||||
storeVipPermissions: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 是否开通病历 VIP(统一 hasVipPermission) */
|
||||
canUseMedicalRecord() {
|
||||
return hasVipPermission(VIP_FEATURE.MEDICAL_RECORD, this.storeVipPermissions)
|
||||
},
|
||||
/** 特色方卡片规格:SKU 名或单剂 */
|
||||
specialPrescriptionPurchaseLabel() {
|
||||
return resolveSpecialPrescriptionPurchaseLabel(this.infoList?.special_prescription_patient_record)
|
||||
},
|
||||
/** 月经史仅女 */
|
||||
showMenstrualHistory() {
|
||||
const sex = Number((this.infoList.patient && this.infoList.patient.sex) || 0)
|
||||
return sex === 2
|
||||
},
|
||||
/** 婚育史:男>22 或 女>20 */
|
||||
showMaritalHistory() {
|
||||
const sex = Number((this.infoList.patient && this.infoList.patient.sex) || 0)
|
||||
const age = Number((this.infoList.patient && this.infoList.patient.age) || 0)
|
||||
return (sex === 1 && age > 22) || (sex === 2 && age > 20)
|
||||
},
|
||||
},
|
||||
onLoad(op) {
|
||||
this.id = op["id"]
|
||||
@@ -341,6 +425,21 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 健康史展示:status=0 无;=1 展示内容
|
||||
* prefix: person / allergic / family / epidemic / personal / menstrual / marital
|
||||
*/
|
||||
healthHistoryText(hi, prefix) {
|
||||
const h = hi || {}
|
||||
const status = Number(h[prefix + '_status'] || 0)
|
||||
if (status !== 1) return '无'
|
||||
const text = h[prefix + '_history']
|
||||
return text != null && String(text).trim() !== '' ? String(text) : '有'
|
||||
},
|
||||
/** 处方表 is_pay 兜底文案(接口无 order_pay_text 时) */
|
||||
payTextByIsPay(isPay) {
|
||||
return Number(isPay) === 1 ? '已支付' : '未支付'
|
||||
},
|
||||
apiOk(res) {
|
||||
return res && (res.code === 0 || res.errcode === 0)
|
||||
},
|
||||
@@ -495,7 +594,7 @@ export default {
|
||||
* @param {number|string} patientId 就诊人 ID
|
||||
* @param {boolean} importSpecial 是否自动导入特色方
|
||||
*/
|
||||
buildPrescriptionV2Url(registerId, patientId, importSpecial = false) {
|
||||
buildPrescriptionV2Url(registerId, patientId, importSpecial = false, tab = '') {
|
||||
let url = `/subPackages/sub_workbench/prescription_v2/index?register_id=${registerId}`;
|
||||
if (patientId) {
|
||||
url += `&patient_id=${patientId}`;
|
||||
@@ -503,8 +602,56 @@ export default {
|
||||
if (importSpecial) {
|
||||
url += '&import_special=1';
|
||||
}
|
||||
if (tab === 'mr') {
|
||||
url += '&tab=mr';
|
||||
}
|
||||
return url;
|
||||
},
|
||||
/**
|
||||
* 拉取履约诊所 VIP permissions(全局 fetchStoreVipPermissions)
|
||||
*/
|
||||
async refreshMedicalRecordVip() {
|
||||
const storeId = Number((this.infoList && this.infoList.store_id) || 0)
|
||||
const registerId = Number((this.infoList && this.infoList.id) || 0)
|
||||
this.storeVipPermissions = await fetchStoreVipPermissions({ storeId, registerId })
|
||||
},
|
||||
/**
|
||||
* 健康档案导入病历:写入本地草稿后跳转开方页病历 Tab
|
||||
*/
|
||||
onImportHealthToMedicalRecord() {
|
||||
if (!this.canUseMedicalRecord) {
|
||||
this.$toast('未开通病历功能')
|
||||
return
|
||||
}
|
||||
const registerId = Number((this.infoList && this.infoList.id) || 0)
|
||||
const patientId = Number((this.infoList && this.infoList.user_patient_id) || 0)
|
||||
if (!registerId) {
|
||||
this.$toast('挂号信息无效')
|
||||
return
|
||||
}
|
||||
if (Number(this.infoList.status) !== 2) {
|
||||
this.$toast('请先接诊后再导入病历')
|
||||
return
|
||||
}
|
||||
uni.showModal({
|
||||
title: '导入到病历',
|
||||
content: '将用健康档案病史覆盖病历对应字段,导入后请检查并保存病历。',
|
||||
confirmColor: '#00A88A',
|
||||
success: (r) => {
|
||||
if (!r.confirm) return
|
||||
const patient = (this.infoList && this.infoList.patient) || {}
|
||||
const fields = mapHealthInquiryToMrHistory(this.infoList.healthInquery, {
|
||||
sex: patient.sex,
|
||||
age: patient.age,
|
||||
})
|
||||
const draft = PrescriptionStorage.loadMedicalRecordDraft(registerId) || {}
|
||||
PrescriptionStorage.saveMedicalRecordDraft(registerId, Object.assign({}, draft, fields))
|
||||
PrescriptionStorage.saveRxMrTab('mr', registerId)
|
||||
this.$toast('已写入病历草稿')
|
||||
this.$go(this.buildPrescriptionV2Url(registerId, patientId, false, 'mr'))
|
||||
},
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 延迟跳转开方页(接诊成功后统一入口)
|
||||
*/
|
||||
@@ -567,6 +714,7 @@ export default {
|
||||
}
|
||||
const raw = res.result || res.data || res
|
||||
this.infoList = normalizeRegisterDetail(raw)
|
||||
this.refreshMedicalRecordVip()
|
||||
if (this.infoList.status !== 7) {
|
||||
if (this.rxTabIndex === 0) {
|
||||
this.loadRxCurrent(true)
|
||||
@@ -679,11 +827,14 @@ page {
|
||||
margin-bottom: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(42, 46, 53, 0.04);
|
||||
|
||||
.card-title-row {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.card-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #2A2E35;
|
||||
margin-bottom: 24rpx;
|
||||
margin-bottom: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
@@ -699,6 +850,14 @@ page {
|
||||
border-radius: 4rpx;
|
||||
}
|
||||
}
|
||||
.health-import-btn {
|
||||
font-size: 24rpx;
|
||||
color: #00A88A;
|
||||
padding: 8rpx 20rpx;
|
||||
border: 1rpx solid #00A88A;
|
||||
border-radius: 28rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Info Rows */
|
||||
@@ -764,6 +923,21 @@ page {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
}
|
||||
.rx-pay-tag {
|
||||
font-size: 24rpx;
|
||||
color: #F59E0B;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
.rx-pay-tag--paid {
|
||||
color: #10B981;
|
||||
}
|
||||
.rx-pay-tag--wait {
|
||||
color: #F59E0B;
|
||||
}
|
||||
.rx-pay-tag--muted {
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
/* Modern Ghost Buttons */
|
||||
.action-btn-ghost {
|
||||
|
||||
78
utils/api-response.js
Normal file
78
utils/api-response.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* 按「接口所属后端」固定取值,禁止运行时猜测 / result||data 混取
|
||||
*
|
||||
* 写代码时先确认该请求打的是 xk-api 还是 Yii,再选用对应方法:
|
||||
* - xk-api:unwrapXkApi / xkOk / xkPayload / xkMessage → code / result / message
|
||||
* - Yii: unwrapYiiApi / yiiOk / yiiPayload / yiiMessage → errcode / data / msg
|
||||
*
|
||||
* 医生端拦截器多数已返回 body;也兼容完整 uni 响应(业务在 res.data)
|
||||
*/
|
||||
|
||||
/** 取出业务 body */
|
||||
export function getApiBody(res) {
|
||||
if (res == null || typeof res !== 'object') {
|
||||
return res
|
||||
}
|
||||
if ('statusCode' in res || 'errMsg' in res) {
|
||||
return res.data
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/** xk-api 是否成功(code === 0) */
|
||||
export function xkOk(res) {
|
||||
const body = getApiBody(res)
|
||||
return !!(body && Number(body.code) === 0)
|
||||
}
|
||||
|
||||
/** xk-api 业务数据(固定取 result) */
|
||||
export function xkPayload(res) {
|
||||
const body = getApiBody(res)
|
||||
return body ? body.result : undefined
|
||||
}
|
||||
|
||||
/** xk-api 提示文案(固定取 message) */
|
||||
export function xkMessage(res) {
|
||||
const body = getApiBody(res)
|
||||
return (body && body.message) || ''
|
||||
}
|
||||
|
||||
/** xk-api 一次解包 */
|
||||
export function unwrapXkApi(res) {
|
||||
const body = getApiBody(res)
|
||||
return {
|
||||
ok: xkOk(res),
|
||||
payload: xkPayload(res),
|
||||
message: xkMessage(res),
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
|
||||
/** Yii 是否成功(errcode === 0) */
|
||||
export function yiiOk(res) {
|
||||
const body = getApiBody(res)
|
||||
return !!(body && Number(body.errcode) === 0)
|
||||
}
|
||||
|
||||
/** Yii 业务数据(固定取 data) */
|
||||
export function yiiPayload(res) {
|
||||
const body = getApiBody(res)
|
||||
return body ? body.data : undefined
|
||||
}
|
||||
|
||||
/** Yii 提示文案(固定取 msg) */
|
||||
export function yiiMessage(res) {
|
||||
const body = getApiBody(res)
|
||||
return (body && body.msg) || ''
|
||||
}
|
||||
|
||||
/** Yii 一次解包 */
|
||||
export function unwrapYiiApi(res) {
|
||||
const body = getApiBody(res)
|
||||
return {
|
||||
ok: yiiOk(res),
|
||||
payload: yiiPayload(res),
|
||||
message: yiiMessage(res),
|
||||
body: body,
|
||||
}
|
||||
}
|
||||
87
utils/vip.js
Normal file
87
utils/vip.js
Normal file
@@ -0,0 +1,87 @@
|
||||
/**
|
||||
* VIP 权限判断(医生小程序全局方法)
|
||||
* - 履约诊所 permissions 来自 get-current-store-type 的 vip.permissions
|
||||
* - 业务里用 hasVipPermission(code, permissions) / fetchStoreVipPermissions
|
||||
* - 模板显隐可用 components/vip-gate/vip-gate.vue
|
||||
*/
|
||||
import { getCurrentStoreTypeApi } from '@/api/reception.js'
|
||||
|
||||
/** 与后台 xk_vip_feature.code 对齐 */
|
||||
export const VIP_FEATURE = {
|
||||
MEDICAL_RECORD: 'medical_record',
|
||||
AI_MEDICAL_RECORD: 'ai_medical_record',
|
||||
AI_PRESCRIPTION: 'ai_prescription',
|
||||
GOLDEN_FORMULA: 'golden_formula',
|
||||
}
|
||||
|
||||
/**
|
||||
* 从接口响应解出业务体(兼容 result / data / 已解包)
|
||||
*/
|
||||
function unwrapPayload(res) {
|
||||
if (!res || typeof res !== 'object') return {}
|
||||
if (res.result != null && typeof res.result === 'object') return res.result
|
||||
if (res.data != null && typeof res.data === 'object') return res.data
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 get-current-store-type 响应取出 permissions 数组
|
||||
* @param {any} res
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function parseVipPermissionsFromRes(res) {
|
||||
const data = unwrapPayload(res)
|
||||
const vip = data && data.vip
|
||||
const list = vip && Array.isArray(vip.permissions) ? vip.permissions : []
|
||||
return list.filter((x) => typeof x === 'string' && x)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应取出完整 vip 对象
|
||||
* @param {any} res
|
||||
* @returns {object|null}
|
||||
*/
|
||||
export function pickVipFromRes(res) {
|
||||
const data = unwrapPayload(res)
|
||||
const vip = data && data.vip
|
||||
return vip && typeof vip === 'object' ? vip : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否拥有指定 VIP 功能码
|
||||
* @param {string} code
|
||||
* @param {string[]|object|null|undefined} vipOrPermissions vip 对象或 permissions 数组(医生端必须传履约诊所)
|
||||
*/
|
||||
export function hasVipPermission(code, vipOrPermissions) {
|
||||
if (!code) return false
|
||||
if (vipOrPermissions == null) return false
|
||||
let list = []
|
||||
if (Array.isArray(vipOrPermissions)) {
|
||||
list = vipOrPermissions
|
||||
} else if (typeof vipOrPermissions === 'object') {
|
||||
list = Array.isArray(vipOrPermissions.permissions) ? vipOrPermissions.permissions : []
|
||||
}
|
||||
return list.indexOf(code) >= 0
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取履约诊所 VIP permissions(统一入口,避免各页复制 getCurrentStoreType)
|
||||
* @param {{ storeId?: number|string, registerId?: number|string }} opts
|
||||
* @returns {Promise<string[]>}
|
||||
*/
|
||||
export async function fetchStoreVipPermissions(opts) {
|
||||
const storeId = Number((opts && opts.storeId) || 0)
|
||||
const registerId = Number((opts && opts.registerId) || 0)
|
||||
const params = {}
|
||||
if (storeId > 0) params.store_id = storeId
|
||||
if (registerId > 0) params.register_id = registerId
|
||||
if (!params.store_id && !params.register_id) {
|
||||
return []
|
||||
}
|
||||
try {
|
||||
const res = await getCurrentStoreTypeApi(params)
|
||||
return parseVipPermissionsFromRes(res)
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user