feat: 语音开方、修复分账问题、识图成方、识图传方、优化订单卡片展示
This commit is contained in:
@@ -6,7 +6,7 @@ alwaysApply: true
|
||||
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
|
||||
2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于2行
|
||||
3. 有封装好的方法、组件需要复用,不要重复造轮子
|
||||
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show)
|
||||
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show);**禁止 v-if 与 :show 同绑**,开/关拆分与 350ms 卸挂载对齐切换账号,详见 `Page-Container-Drawer.mdc`
|
||||
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 再绑定
|
||||
|
||||
@@ -6,7 +6,7 @@ alwaysApply: true
|
||||
1. 写代码的时候要补充详细的中文注释(每个方法是干嘛的,为什么要这样写),如果是工作区,则所有项目都适用该规则
|
||||
2. 注意不要生成太多的空行,上一部分代码和下一部分代码中间的空行不要大于2行
|
||||
3. 有封装好的方法、组件需要复用,不要重复造轮子
|
||||
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show)
|
||||
4. 小程序端的抽屉全部需要用page-container来防止用户意外退出页面(记得使用v-if而不是v-show);**禁止 v-if 与 :show 同绑**,开/关拆分与 350ms 卸挂载对齐切换账号,详见 `Page-Container-Drawer.mdc`
|
||||
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 再绑定
|
||||
|
||||
95
.cursor/rules/Page-Container-Drawer.mdc
Normal file
95
.cursor/rules/Page-Container-Drawer.mdc
Normal file
@@ -0,0 +1,95 @@
|
||||
---
|
||||
description: 小程序抽屉 page-container 返回拦截(对齐切换账号 / 工作台切换诊所)
|
||||
globs: **/*.{vue,js}
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 小程序抽屉返回拦截(强制)
|
||||
|
||||
标杆实现:
|
||||
|
||||
- **切换账号**:`components/account-switch/account-switch-panel.vue`
|
||||
- **工作台切换诊所**:`pages/workbench/index.vue`(门店选择 `storePageGuard` + `openShow`)
|
||||
|
||||
新增/改抽屉、底部弹层、认领码等凡需拦返回的,**必须**按本规则,禁止再 `v-if` 与 `:show` 绑同一个变量。
|
||||
|
||||
## 核心原则
|
||||
|
||||
1. 微信**同一页只能有一个** `page-container`;多抽屉必须互斥(先卸再挂,或等约 350ms)。
|
||||
2. **挂载**与**可见**拆开:
|
||||
- `pageGuard`(或 `storePageGuard`)→ `v-if`(控制是否挂载)
|
||||
- `visible` / `panelShow` / `openShow` → `:show`(控制是否展示)
|
||||
3. 必须用 `v-if`,禁止 `v-show` 控制 `page-container`。
|
||||
4. 关抽屉:先 `:show = false` 播离场,再 **约 350ms**(或 `@afterleave`)后才 `pageGuard = false` 卸节点。
|
||||
5. 开抽屉:先 `pageGuard = true`,`$nextTick` 后再 `:show = true`。
|
||||
|
||||
## 标准写法(对齐切换账号)
|
||||
|
||||
```vue
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container
|
||||
v-if="pageGuard"
|
||||
:show="visible"
|
||||
:overlay="false"
|
||||
@leave="close"
|
||||
@afterleave="releasePageGuard"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
```
|
||||
|
||||
```js
|
||||
// 打开
|
||||
open() {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = true;
|
||||
this.$nextTick(() => {
|
||||
this.visible = true;
|
||||
});
|
||||
},
|
||||
// 关闭
|
||||
close() {
|
||||
this.visible = false;
|
||||
this.scheduleReleaseGuard(); // 350ms 后再 pageGuard=false
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = false;
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false;
|
||||
this.releaseTimer = null;
|
||||
}, 350);
|
||||
},
|
||||
```
|
||||
|
||||
## 禁止
|
||||
|
||||
```vue
|
||||
<!-- ❌ v-if 与 :show 同绑 → 二次侧滑/返回不再拦截 -->
|
||||
<page-container v-if="show" :show="show" />
|
||||
|
||||
<!-- ❌ 关抽屉立刻 v-if=false → 页面滑动锁残留 / 二次打开失败 -->
|
||||
<page-container v-if="show" :show="true" />
|
||||
```
|
||||
|
||||
## 同页多抽屉互斥(工作台切换诊所已落地)
|
||||
|
||||
打开 A 前必须先关掉 B 的 `page-container`:
|
||||
|
||||
- 关本层:可先 `show=false` 再 `scheduleReleaseGuard(350)`。
|
||||
- **立刻要挂另一层**:必须 **同步** `pageGuard = false`(不要等 350ms),或等对方卸完再挂(约 360ms)。
|
||||
- 参考:`openStoreSelect` / `openApptDrawer` / `onBriefPopupChange`(工作台)。
|
||||
|
||||
## 组件内嵌套两层抽屉(如患者详情 + 回访)
|
||||
|
||||
同一组件内也只能保留一个 `page-container`:
|
||||
|
||||
1. 开内层前:外层 `panelShow=false` 且 **立刻** `pageGuard=false`(勿只靠 afterleave,晚到会冲掉恢复后的 guard)。
|
||||
2. 关内层后:再 `pageGuard=true` → `$nextTick` → `panelShow=true`。
|
||||
3. 恢复逻辑加防重入,避免 `update:visible` + `close` 双事件恢复两次。
|
||||
|
||||
## 与 Code-Standards 的关系
|
||||
|
||||
`Code-Standards.mdc` 第 4 条要求抽屉必须配 `page-container`;**具体拆分与 350ms 卸挂载以本规则 + 切换账号组件为准**。
|
||||
@@ -73,6 +73,10 @@ export function createBusinessApi(prefix, headerFlag) {
|
||||
getWithdrawableOrders(params) {
|
||||
return bizRequest({ url: `${prefix}-withdrawal/withdrawable-orders`, method: 'GET', data: params || {} });
|
||||
},
|
||||
/** 提现规则文案 xk-api GET /{prefix}-withdrawal/rules */
|
||||
getWithdrawalRules() {
|
||||
return bizRequest({ url: `${prefix}-withdrawal/rules`, method: 'GET', data: {} });
|
||||
},
|
||||
passWithdrawal(data) {
|
||||
return bizRequest({ url: `${prefix}-withdrawal/pass-application`, method: 'POST', data });
|
||||
},
|
||||
|
||||
@@ -183,6 +183,11 @@ export function getWithdrawableOrders(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-withdrawal/withdrawable-orders`, method: 'GET', data: params || {} });
|
||||
}
|
||||
|
||||
/** 提现规则文案 xk-api GET /clinic-admin-withdrawal/rules */
|
||||
export function getWithdrawalRules() {
|
||||
return clinicRequest({ url: `${PREFIX}-withdrawal/rules`, method: 'GET', data: {} });
|
||||
}
|
||||
|
||||
export function getReconciliationList(params) {
|
||||
return clinicRequest({ url: `${PREFIX}-reconciliation/list`, method: 'GET', data: params });
|
||||
}
|
||||
|
||||
220
api/imageTransferOrder.js
Normal file
220
api/imageTransferOrder.js
Normal file
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* 识图传方订单(xk-api)
|
||||
* 医生:/image-transfer-order-wx/* 取值 unwrapXkApi
|
||||
* 诊所管理员:/clinic-admin-image-transfer/* 请求头 _clinicAdmin
|
||||
*/
|
||||
import { req } from '@/common/js/index.js';
|
||||
|
||||
function clinicReq(options) {
|
||||
return req.request({
|
||||
...options,
|
||||
header: {
|
||||
...(options.header || {}),
|
||||
_clinicAdmin: '1',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 平台管理员请求:带 _platformAdmin 头走平台传方接口 */
|
||||
function platformReq(options) {
|
||||
return req.request({
|
||||
...options,
|
||||
header: {
|
||||
...(options.header || {}),
|
||||
_platformAdmin: '1',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 医生我的传方 xk-api GET /image-transfer-order-wx/list */
|
||||
export function listMyImageTransferOrders(data) {
|
||||
return req.request({
|
||||
url: '/newApi/image-transfer-order-wx/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 医生传方详情 xk-api GET /image-transfer-order-wx/detail */
|
||||
export function getMyImageTransferOrder(data) {
|
||||
return req.request({
|
||||
url: '/newApi/image-transfer-order-wx/detail',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 医生驳回后再交 xk-api POST /image-transfer-order-wx/resubmit */
|
||||
export function resubmitMyImageTransferOrder(data) {
|
||||
return req.request({
|
||||
url: '/newApi/image-transfer-order-wx/resubmit',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所识图 OCR xk-api POST /clinic-admin-image-transfer/ocr-prescription-image */
|
||||
export function clinicOcrPrescriptionImage(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/ocr-prescription-image',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所药库搜索 xk-api GET /clinic-admin-image-transfer/product-list */
|
||||
export function clinicSearchTransferDrugs(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/product-list',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所提交识图传方 xk-api POST /clinic-admin-image-transfer/confirm */
|
||||
export function clinicConfirmImageTransfer(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/confirm',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所识图传方匹配就诊人 xk-api POST /clinic-admin-image-transfer/match-transfer-patient */
|
||||
export function clinicMatchTransferPatient(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/match-transfer-patient',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所识图历史列表 xk-api GET /clinic-admin-image-transfer/ai-list-generations */
|
||||
export function clinicAiListGenerations(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/ai-list-generations',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所识图历史详情 xk-api GET /clinic-admin-image-transfer/ai-generation-detail */
|
||||
export function clinicAiGenerationDetail(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/ai-generation-detail',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所传方列表 xk-api GET /clinic-admin-image-transfer/list */
|
||||
export function listStoreImageTransferOrders(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所传方详情 xk-api GET /clinic-admin-image-transfer/detail */
|
||||
export function getStoreImageTransferOrder(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/detail',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所驳回后再交 xk-api POST /clinic-admin-image-transfer/resubmit */
|
||||
export function resubmitStoreImageTransferOrder(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/resubmit',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 医生传方报价预览 xk-api POST /image-transfer-order-wx/quote */
|
||||
export function quoteMyImageTransferOrder(data) {
|
||||
return req.request({
|
||||
url: '/newApi/image-transfer-order-wx/quote',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所传方报价预览 xk-api POST /clinic-admin-image-transfer/quote */
|
||||
export function quoteStoreImageTransferOrder(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/quote',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 诊所委托调剂规则级联列表
|
||||
* xk-api GET /clinic-admin-image-transfer/process-rule-list
|
||||
* 入参与医生端一致:pid 拉下级,rule_id 拉备注
|
||||
*/
|
||||
export function clinicProcessRuleList(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/process-rule-list',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 平台传方列表 xk-api GET /platform-admin-image-transfer/list */
|
||||
export function listPlatformImageTransferOrders(data) {
|
||||
return platformReq({
|
||||
url: '/newApi/platform-admin-image-transfer/list',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 平台传方详情 xk-api GET /platform-admin-image-transfer/detail */
|
||||
export function getPlatformImageTransferOrder(data) {
|
||||
return platformReq({
|
||||
url: '/newApi/platform-admin-image-transfer/detail',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 平台审方通过 xk-api POST /platform-admin-image-transfer/approve */
|
||||
export function approvePlatformImageTransferOrder(data) {
|
||||
return platformReq({
|
||||
url: '/newApi/platform-admin-image-transfer/approve',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 平台审方驳回 xk-api POST /platform-admin-image-transfer/reject */
|
||||
export function rejectPlatformImageTransferOrder(data) {
|
||||
return platformReq({
|
||||
url: '/newApi/platform-admin-image-transfer/reject',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 平台失败重试推送 xk-api POST /platform-admin-image-transfer/retry */
|
||||
export function retryPlatformImageTransferOrder(data) {
|
||||
return platformReq({
|
||||
url: '/newApi/platform-admin-image-transfer/retry',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 诊所本店医生选项 xk-api GET /clinic-admin-image-transfer/doctor-option */
|
||||
export function clinicTransferDoctorOption(data) {
|
||||
return clinicReq({
|
||||
url: '/newApi/clinic-admin-image-transfer/doctor-option',
|
||||
method: 'GET',
|
||||
data,
|
||||
});
|
||||
}
|
||||
41
api/prescriptionImageOcr.js
Normal file
41
api/prescriptionImageOcr.js
Normal file
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 处方图 OCR / 识图传方(xk-api)
|
||||
* 取值:unwrapXkApi → code / result / message
|
||||
*/
|
||||
import { req } from '@/common/js/index.js';
|
||||
|
||||
/** 处方图 OCR xk-api POST /doctor-reception-wx/ocr-prescription-image */
|
||||
export function ocrPrescriptionImageApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/ocr-prescription-image',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 确认识图传方 xk-api POST /doctor-reception-wx/confirm-transfer-image-ocr */
|
||||
export function confirmTransferImageOcrApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/confirm-transfer-image-ocr',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 识图传方匹配就诊人 xk-api POST /doctor-reception-wx/match-transfer-patient */
|
||||
export function matchTransferPatientApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/match-transfer-patient',
|
||||
method: 'POST',
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
||||
/** 医生导入传方准备 xk-api GET /transfer-prescription/prepare-doctor-import */
|
||||
export function prepareDoctorImportApi(transferId, registerId) {
|
||||
return req.request({
|
||||
url: '/newApi/transfer-prescription/prepare-doctor-import',
|
||||
method: 'GET',
|
||||
data: { id: transferId, register_id: registerId },
|
||||
});
|
||||
}
|
||||
@@ -189,6 +189,19 @@ export function getProductListDoctorReception(data) {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复用/常用方填方前对比中药快照价与当前门店价
|
||||
* 为什么:供货比例变更后关系表已更新,价格不一致时要让医生确认是否按最新价计算
|
||||
* xk-api POST /doctor-reception-wx/compare-chinese-drug-prices
|
||||
*/
|
||||
export function compareChineseDrugPricesApi(data) {
|
||||
return req.request({
|
||||
url: '/newApi/doctor-reception-wx/compare-chinese-drug-prices',
|
||||
method: 'POST',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
/** 中药全局快捷克数(与药品无关,每次打开中药抽屉拉取) */
|
||||
export function getChineseMedicineQuickGramsApi() {
|
||||
return req.request({
|
||||
@@ -622,7 +635,7 @@ export function aiGeneratePrescriptionApi(data) {
|
||||
|
||||
/**
|
||||
* 语音转写(multipart:字段名 audio)
|
||||
* @param {{ filePath: string, duration_ms?: number }} data
|
||||
* @param {{ filePath: string, duration_ms?: number, audio_format?: string }} data
|
||||
*/
|
||||
export function aiVoiceAsrApi(data) {
|
||||
return req.request({
|
||||
@@ -632,7 +645,9 @@ export function aiVoiceAsrApi(data) {
|
||||
filePath: data.filePath,
|
||||
name: 'audio',
|
||||
formData: {
|
||||
duration_ms: String(data.duration_ms || 0)
|
||||
duration_ms: String(data.duration_ms || 0),
|
||||
// 显式声明:服务端优先采信,避免 MIME/扩展名误杀
|
||||
audio_format: String(data.audio_format || 'wav')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -204,6 +204,7 @@ const LEGACY_ADMIN_UPLOAD_URL = 'https://api.xiaokang88.com/open-api/upload/old-
|
||||
|
||||
/**
|
||||
* 录入/管理端旧版图片上传(与 d-upload、select-avatar-image 一致)
|
||||
* 注意:只认医生态 token;诊所管理员不要走这条
|
||||
*/
|
||||
export function uploadLegacyAdminImage(params) {
|
||||
const { filePath, name = 'file', formData = { store_id: '11001' } } = params || {};
|
||||
@@ -238,3 +239,45 @@ export function uploadLegacyAdminImage(params) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 识图传方选图上传:按 loginMode 选接口
|
||||
* 诊所管理员必须走 clinic-admin-upload(带 _clinicAdmin + clinic_admin_token),
|
||||
* 不能用旧 Yii + 医生态 token,否则会一直「上传失败」
|
||||
* @param {{ filePath: string }} params
|
||||
* @returns {Promise<string>} 可访问的图片 URL
|
||||
*/
|
||||
export async function uploadOcrImageByLoginMode(params) {
|
||||
const filePath = params && params.filePath;
|
||||
if (!filePath) {
|
||||
throw new Error('缺少文件路径');
|
||||
}
|
||||
const mode = uni.getStorageSync('loginMode');
|
||||
if (mode === 'clinic_admin') {
|
||||
const res = await req.request({
|
||||
url: `${CLINIC_ADMIN_PREFIX}image`,
|
||||
type: 'upload',
|
||||
method: 'POST',
|
||||
filePath,
|
||||
name: 'file',
|
||||
formData: {},
|
||||
header: { _clinicAdmin: '1' },
|
||||
});
|
||||
let body = res;
|
||||
if (typeof body === 'string') {
|
||||
try {
|
||||
body = JSON.parse(body);
|
||||
} catch (e) {
|
||||
body = null;
|
||||
}
|
||||
}
|
||||
// xk-api:code=0,url 在 result
|
||||
if (body && Number(body.code) === 0 && body.result && body.result.url) {
|
||||
return String(body.result.url);
|
||||
}
|
||||
const viaExtract = extractUploadUrl(body);
|
||||
if (viaExtract) return String(viaExtract);
|
||||
throw new Error((body && body.message) || '上传失败');
|
||||
}
|
||||
return uploadLegacyAdminImage({ filePath });
|
||||
}
|
||||
|
||||
48
pages.json
48
pages.json
@@ -4,8 +4,8 @@
|
||||
"^u-(.*)": "uview-ui/components/u-$1/u-$1.vue",
|
||||
"^d-(.*)": "@/components/d-$1/d-$1.vue",
|
||||
"^ai-generation-loading$": "@/components/ai-generation-loading/ai-generation-loading.vue",
|
||||
"^proxy-pay-qrcode-modal$": "@/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue",
|
||||
"^claim-qrcode-modal$": "@/components/claim-qrcode-modal/claim-qrcode-modal.vue"
|
||||
"^proxy-pay-qrcode-modal$": "@/subPackages/sub_workbench/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue",
|
||||
"^claim-qrcode-modal$": "@/subPackages/sub_workbench/components/claim-qrcode-modal/claim-qrcode-modal.vue"
|
||||
},
|
||||
"pages": [{
|
||||
"path": "pages/login/index",
|
||||
@@ -69,8 +69,8 @@
|
||||
"path": "workbench_upInfo/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "上传资料",
|
||||
"enablePullDownRefresh ": false, //此设置 是否开启当前页面下拉刷新。
|
||||
"disableScroll": false //此设置 是否允许页面整体上下滚动。
|
||||
"enablePullDownRefresh": false,
|
||||
"disableScroll": false
|
||||
|
||||
}
|
||||
},
|
||||
@@ -141,6 +141,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "image-transfer-prescription/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "识图传方"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "workbench_code",
|
||||
"style": {
|
||||
@@ -512,6 +518,40 @@
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
"root": "subPackages/sub_image_transfer",
|
||||
"pages": [{
|
||||
"path": "upload",
|
||||
"style": {
|
||||
"navigationBarTitleText": "识图传方",
|
||||
"usingComponents": {
|
||||
"image-ocr-drawer": "/subPackages/sub_workbench/prescription_v2/components/ImageOcrDrawer"
|
||||
},
|
||||
"componentPlaceholder": {
|
||||
"image-ocr-drawer": "view"
|
||||
}
|
||||
}
|
||||
}, {
|
||||
"path": "order-list",
|
||||
"style": {
|
||||
"navigationBarTitleText": "传方订单",
|
||||
"backgroundColor": "#E8F6F4",
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
}, {
|
||||
"path": "order-detail",
|
||||
"style": {
|
||||
"navigationBarTitleText": "传方详情",
|
||||
"usingComponents": {
|
||||
"chinese-medicine-config": "/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig",
|
||||
"chinese-medicine-modal": "/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal"
|
||||
},
|
||||
"componentPlaceholder": {
|
||||
"chinese-medicine-config": "view",
|
||||
"chinese-medicine-modal": "view"
|
||||
}
|
||||
}
|
||||
}]
|
||||
}
|
||||
],
|
||||
"tabBar": {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</view>
|
||||
<view class="header-wrap" :style="{ paddingTop: statusBarHeight + navbarHeight + 'px' }">
|
||||
<view class="box">
|
||||
<view class="store_" @click="openShow=true">
|
||||
<view class="store_" @click="openStoreSelect">
|
||||
{{doctorInfo.store.store.name || '默认门店'}}
|
||||
<u-icon name="map-fill" color="#fff" size="28" margin-left="12"></u-icon>
|
||||
</view>
|
||||
@@ -164,6 +164,28 @@
|
||||
></u-badge>
|
||||
<u-icon name="arrow-right" color="#6acdbb" size="28"></u-icon>
|
||||
</view>
|
||||
<view
|
||||
class="offline-entry-row flex-row flex-ali-center m-t-24"
|
||||
@click="goReceptionEntry(imageTransferEntry.url)"
|
||||
>
|
||||
<image class="offline-entry-icon" :src="imageTransferEntry.icon"></image>
|
||||
<view class="offline-entry-body flex-col flex-1">
|
||||
<text class="offline-entry-title">{{ imageTransferEntry.name }}</text>
|
||||
<text class="offline-entry-desc">拍照识方,按配置进接诊或提交传方</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#6acdbb" size="28"></u-icon>
|
||||
</view>
|
||||
<view
|
||||
class="offline-entry-row flex-row flex-ali-center m-t-24"
|
||||
@click="goReceptionEntry(imageTransferOrderEntry.url)"
|
||||
>
|
||||
<image class="offline-entry-icon" :src="imageTransferOrderEntry.icon"></image>
|
||||
<view class="offline-entry-body flex-col flex-1">
|
||||
<text class="offline-entry-title">{{ imageTransferOrderEntry.name }}</text>
|
||||
<text class="offline-entry-desc">查看自己上传的传方订单</text>
|
||||
</view>
|
||||
<u-icon name="arrow-right" color="#6acdbb" size="28"></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
<block>
|
||||
<!-- 正在接诊模块 -->
|
||||
@@ -392,8 +414,17 @@
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 切换门店精简 -->
|
||||
<u-popup v-model="openShow" mode="bottom" border-radius="24" length="65%" :safe-area-inset-bottom="true">
|
||||
<!-- 切换门店:page-container 拦截返回;与预约抽屉/简报互斥(同页只能一个 page-container) -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container
|
||||
v-if="storePageGuard"
|
||||
:show="openShow"
|
||||
:overlay="false"
|
||||
@leave="closeStoreSelect"
|
||||
@afterleave="releaseStorePageGuard"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<u-popup v-model="openShow" mode="bottom" border-radius="24" length="65%" :safe-area-inset-bottom="true" @close="closeStoreSelect">
|
||||
<view class="content_popup">
|
||||
<view class="top">
|
||||
<view class="title">选择诊所</view>
|
||||
@@ -413,7 +444,7 @@
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="confrim-btn">
|
||||
<view class="u-btn qx" @click="openShow=false">取消</view>
|
||||
<view class="u-btn qx" @click="closeStoreSelect">取消</view>
|
||||
<view class="u-btn qd" @click="sure">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -489,7 +520,18 @@ const ONLINE_RECEPTION_ENTRY = {
|
||||
icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/xsfz.png',
|
||||
url: '../../subPackages/sub_online_reception/pages/list',
|
||||
};
|
||||
const RECEPTION_GRID_ENTRIES = [OFFLINE_RECEPTION_ENTRY, ONLINE_RECEPTION_ENTRY];
|
||||
/** 医生识图传方:上传处方图 → OCR → 待导入传方 → 接诊中药回填 */
|
||||
const IMAGE_TRANSFER_ENTRY = {
|
||||
name: '识图传方',
|
||||
icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/zxzx.png',
|
||||
url: '../../subPackages/sub_workbench/image-transfer-prescription/index',
|
||||
};
|
||||
const IMAGE_TRANSFER_ORDER_ENTRY = {
|
||||
name: '我的传方',
|
||||
icon: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/image/zxzx.png',
|
||||
url: '../../subPackages/sub_image_transfer/order-list?scope=mine',
|
||||
};
|
||||
const RECEPTION_GRID_ENTRIES = [OFFLINE_RECEPTION_ENTRY, ONLINE_RECEPTION_ENTRY, IMAGE_TRANSFER_ENTRY, IMAGE_TRANSFER_ORDER_ENTRY];
|
||||
const EMPTY_WORKBENCH_STATS = {
|
||||
prescription_count: 0,
|
||||
wait_accept: 0,
|
||||
@@ -514,6 +556,8 @@ export default {
|
||||
},
|
||||
receptionGridEntries: RECEPTION_GRID_ENTRIES,
|
||||
offlineReceptionEntry: OFFLINE_RECEPTION_ENTRY,
|
||||
imageTransferEntry: IMAGE_TRANSFER_ENTRY,
|
||||
imageTransferOrderEntry: IMAGE_TRANSFER_ORDER_ENTRY,
|
||||
canUseOnlineConsultation: false,
|
||||
onlineConsultationConfigReady: false,
|
||||
states: {
|
||||
@@ -543,6 +587,9 @@ export default {
|
||||
page: 1,
|
||||
totalPage: 1,
|
||||
openShow: false,
|
||||
/** 门店选择 page-container 挂载态,与 openShow 拆开 */
|
||||
storePageGuard: false,
|
||||
storeReleaseTimer: null,
|
||||
keyword: "",
|
||||
store: "",
|
||||
storeList: [],
|
||||
@@ -625,6 +672,7 @@ export default {
|
||||
},
|
||||
onUnload() {
|
||||
this.stopRefreshTimer();
|
||||
this.clearStoreReleaseTimer();
|
||||
if (this.__doctorImRoomUpdateHandler) {
|
||||
uni.$off('doctor-im-room-update', this.__doctorImRoomUpdateHandler);
|
||||
this.__doctorImRoomUpdateHandler = null;
|
||||
@@ -831,7 +879,7 @@ export default {
|
||||
this.store_id = uni.getStorageSync('store_id')
|
||||
}
|
||||
|
||||
this.openShow = false
|
||||
this.closeStoreSelect()
|
||||
this.onlineConsultationConfigReady = false
|
||||
this.refreshWorkbenchData()
|
||||
})
|
||||
@@ -892,13 +940,76 @@ export default {
|
||||
if (Number.isNaN(num)) return '0.000'
|
||||
return num.toFixed(3)
|
||||
},
|
||||
/** AI 简报弹层开合:打开时隐藏自定义 tabbar,避免抽屉底部「知道了」被挡住 */
|
||||
/** AI 简报弹层开合:打开时隐藏自定义 tabbar;与门店/预约抽屉互斥 */
|
||||
onBriefPopupChange(show) {
|
||||
this.briefPopupShow = !!show;
|
||||
if (show) {
|
||||
// 简报自带 page-container:开之前立刻卸门店拦截,不能等 350ms
|
||||
this.openShow = false;
|
||||
this.clearStoreReleaseTimer();
|
||||
this.storePageGuard = false;
|
||||
this.apptDrawerShow = false;
|
||||
}
|
||||
},
|
||||
/** 打开未来7天预约明细抽屉;date 为空看全部,点某天卡格预筛选该天 */
|
||||
/**
|
||||
* 打开门店选择:先关预约抽屉/简报,再挂 page-container
|
||||
* 为什么:微信同页只能一个 page-container,且门店弹层原先无返回拦截
|
||||
*/
|
||||
openStoreSelect() {
|
||||
// 预约/简报关后仍留约 350ms pageGuard,需等卸完再挂门店,否则同页双 page-container
|
||||
const waitMs = (this.apptDrawerShow || this.briefPopupShow) ? 360 : 0;
|
||||
this.apptDrawerShow = false;
|
||||
if (this.briefPopupShow && this.$refs.aiBrief && this.$refs.aiBrief.closePopup) {
|
||||
this.$refs.aiBrief.closePopup();
|
||||
}
|
||||
const open = () => {
|
||||
this.clearStoreReleaseTimer();
|
||||
this.storePageGuard = true;
|
||||
this.$nextTick(() => {
|
||||
this.openShow = true;
|
||||
});
|
||||
};
|
||||
if (waitMs > 0) {
|
||||
setTimeout(open, waitMs);
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
},
|
||||
/** 关闭门店选择:先卸 show,再延迟卸挂载 */
|
||||
closeStoreSelect() {
|
||||
this.openShow = false;
|
||||
this.scheduleStoreReleaseGuard();
|
||||
},
|
||||
releaseStorePageGuard() {
|
||||
this.clearStoreReleaseTimer();
|
||||
this.storePageGuard = false;
|
||||
},
|
||||
scheduleStoreReleaseGuard() {
|
||||
this.clearStoreReleaseTimer();
|
||||
this.storeReleaseTimer = setTimeout(() => {
|
||||
this.storePageGuard = false;
|
||||
this.storeReleaseTimer = null;
|
||||
}, 350);
|
||||
},
|
||||
clearStoreReleaseTimer() {
|
||||
if (this.storeReleaseTimer) {
|
||||
clearTimeout(this.storeReleaseTimer);
|
||||
this.storeReleaseTimer = null;
|
||||
}
|
||||
},
|
||||
/** 打开预约抽屉:立刻卸门店 pageGuard;若简报开着则等其卸完再开 */
|
||||
openApptDrawer(date) {
|
||||
this.openShow = false;
|
||||
this.clearStoreReleaseTimer();
|
||||
this.storePageGuard = false;
|
||||
this.apptDefaultDate = date || '';
|
||||
if (this.briefPopupShow && this.$refs.aiBrief && this.$refs.aiBrief.closePopup) {
|
||||
this.$refs.aiBrief.closePopup();
|
||||
setTimeout(() => {
|
||||
this.apptDrawerShow = true;
|
||||
}, 360);
|
||||
return;
|
||||
}
|
||||
this.apptDrawerShow = true;
|
||||
},
|
||||
/** 抽屉内改期成功:预约分布/待接诊数已变化,静默刷新看板(不打断用户继续操作抽屉) */
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 回访记录表单抽屉:使用 page-container 防止用户误退出页面;用 v-if 控制挂载/卸载 -->
|
||||
<!-- 对齐切换账号:pageGuard 与 panelShow 拆开;visible 为父级 v-model -->
|
||||
<page-container
|
||||
v-if="visible"
|
||||
:show="visible"
|
||||
v-if="pageGuard"
|
||||
:show="panelShow"
|
||||
:overlay="true"
|
||||
position="bottom"
|
||||
:round="true"
|
||||
@beforeleave="onClose"
|
||||
@leave="onClose"
|
||||
@afterleave="releasePageGuard"
|
||||
@clickoverlay="onClose"
|
||||
>
|
||||
<view class="cl-drawer">
|
||||
@@ -169,6 +170,9 @@ export default {
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageGuard: false,
|
||||
panelShow: false,
|
||||
releaseTimer: null,
|
||||
submitting: false,
|
||||
form: {
|
||||
phone: '',
|
||||
@@ -255,18 +259,29 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* v-if 挂载时 visible 已是 true,用 immediate 同步初始化表单 + 加载 dict
|
||||
* visible 打开:先挂 pageGuard 再 panelShow;关闭:先卸 show 再延迟卸挂载
|
||||
*/
|
||||
visible: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = true;
|
||||
this.$nextTick(() => {
|
||||
this.panelShow = true;
|
||||
});
|
||||
this.initForm();
|
||||
this.loadDict();
|
||||
} else {
|
||||
this.panelShow = false;
|
||||
this.scheduleReleaseGuard();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearReleaseTimer();
|
||||
},
|
||||
methods: {
|
||||
/** 打开抽屉时根据 prefill 初始化表单 */
|
||||
initForm() {
|
||||
@@ -301,12 +316,31 @@ export default {
|
||||
// 静默兜底,使用 FALLBACK 数据保证 UI 可用
|
||||
}
|
||||
},
|
||||
/** 关闭抽屉 */
|
||||
/** 关闭抽屉:父级靠 update:visible;保留 close 兼容就诊人详情页监听 */
|
||||
onClose() {
|
||||
if (this.submitting) return;
|
||||
this.panelShow = false;
|
||||
this.scheduleReleaseGuard();
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('close');
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = false;
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false;
|
||||
this.releaseTimer = null;
|
||||
}, 350);
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer);
|
||||
this.releaseTimer = null;
|
||||
}
|
||||
},
|
||||
/** 判断标签是否已选中 */
|
||||
isTagChecked(value) {
|
||||
return this.form.tags.indexOf(value) >= 0;
|
||||
@@ -425,6 +459,8 @@ export default {
|
||||
}
|
||||
uni.showToast({ title: '已记录', icon: 'success' });
|
||||
this.$emit('success');
|
||||
this.panelShow = false;
|
||||
this.scheduleReleaseGuard();
|
||||
this.$emit('update:visible', false);
|
||||
this.$emit('close');
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- 患者视角详情:page-container 抽屉;用 v-if 控制挂载 -->
|
||||
<!-- 患者视角详情:pageGuard(v-if) / panelShow(:show) 拆开,对齐切换账号,避免二次侧滑失效 -->
|
||||
<!-- 微信同一页只能有一个 page-container:打开回访表单前必须先卸掉本实例,再挂 CallLogDrawer -->
|
||||
<page-container
|
||||
v-if="visible && !patientSheetHiddenForCallLog"
|
||||
:show="true"
|
||||
v-if="pageGuard && !patientSheetHiddenForCallLog"
|
||||
:show="panelShow"
|
||||
:overlay="true"
|
||||
position="bottom"
|
||||
:round="true"
|
||||
@@ -303,6 +303,11 @@ export default {
|
||||
* 与 callLogDrawerVisible 配合两阶段互斥,保证页面上最多一个 page-container
|
||||
*/
|
||||
patientSheetHiddenForCallLog: false,
|
||||
/** page-container 挂载态(v-if),与 panelShow 拆开 */
|
||||
pageGuard: false,
|
||||
/** page-container 可见态(:show) */
|
||||
panelShow: false,
|
||||
releaseTimer: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -339,18 +344,25 @@ export default {
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* v-if 挂载时 visible 已是 true,必须 immediate,否则 loadDetail 永不触发
|
||||
* visible 打开:先挂 pageGuard 再 panelShow;关闭:先卸 show 再延迟卸挂载
|
||||
* immediate:父级 v-if 挂载时 visible 已是 true,否则 loadDetail 永不触发
|
||||
*/
|
||||
visible: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val && this.upId) {
|
||||
this.openAndLoad();
|
||||
}
|
||||
// 抽屉挂载时注册"拨号返回"监听;卸载时取消
|
||||
if (val) {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = true;
|
||||
this.$nextTick(() => {
|
||||
this.panelShow = true;
|
||||
});
|
||||
if (this.upId) {
|
||||
this.openAndLoad();
|
||||
}
|
||||
uni.$on(CALLBACK_RETURNED_EVENT, this.handleCallbackReturned);
|
||||
} else {
|
||||
this.panelShow = false;
|
||||
this.scheduleReleaseGuard();
|
||||
uni.$off(CALLBACK_RETURNED_EVENT, this.handleCallbackReturned);
|
||||
}
|
||||
},
|
||||
@@ -365,11 +377,32 @@ export default {
|
||||
/** 兜底:组件销毁前移除事件监听,避免重复触发 */
|
||||
beforeDestroy() {
|
||||
uni.$off(CALLBACK_RETURNED_EVENT, this.handleCallbackReturned);
|
||||
this.clearReleaseTimer();
|
||||
},
|
||||
methods: {
|
||||
/** 关抽屉:先卸 show,再延迟卸挂载,并通知父级 */
|
||||
close() {
|
||||
this.panelShow = false;
|
||||
this.scheduleReleaseGuard();
|
||||
this.$emit('close');
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = false;
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false;
|
||||
this.releaseTimer = null;
|
||||
}, 350);
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer);
|
||||
this.releaseTimer = null;
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 遮罩点击:为打开回访而卸载本抽屉时勿关
|
||||
*/
|
||||
@@ -489,26 +522,37 @@ export default {
|
||||
this.beginOpenCallLogDrawer();
|
||||
},
|
||||
/**
|
||||
* 两阶段打开回访表单:先卸外层 page-container,nextTick 后再挂 CallLogDrawer
|
||||
* 微信同一页只能存在一个 page-container 实例
|
||||
* 两阶段打开回访表单:先同步卸外层 page-container,再挂 CallLogDrawer
|
||||
* 必须立刻 pageGuard=false,否则 afterleave 晚到会把恢复后的抽屉又卸掉
|
||||
*/
|
||||
beginOpenCallLogDrawer() {
|
||||
this.clearReleaseTimer();
|
||||
this.panelShow = false;
|
||||
this.pageGuard = false;
|
||||
this.patientSheetHiddenForCallLog = true;
|
||||
this.$nextTick(() => {
|
||||
this.callLogDrawerVisible = true;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 关闭回访表单后恢复就诊人抽屉:先卸 CallLogDrawer,nextTick 再挂外层
|
||||
* 关闭回访表单后恢复就诊人抽屉:先卸 CallLogDrawer,再挂外层并 nextTick 开 show
|
||||
*/
|
||||
restorePatientSheetAfterCallLog() {
|
||||
if (this._restoringPatientSheet) return;
|
||||
this._restoringPatientSheet = true;
|
||||
this.callLogDrawerVisible = false;
|
||||
this.$nextTick(() => {
|
||||
this.clearReleaseTimer();
|
||||
this.patientSheetHiddenForCallLog = false;
|
||||
this.suppressPatientClose = false;
|
||||
this.pageGuard = true;
|
||||
this.$nextTick(() => {
|
||||
this.panelShow = true;
|
||||
this.suppressPatientClose = false;
|
||||
this._restoringPatientSheet = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
/** CallLogDrawer 通过 .sync 更新 visible */
|
||||
/** CallLogDrawer 通过 .sync 更新 visible;关时与 @close 只走一次恢复 */
|
||||
onCallLogDrawerVisibleChange(val) {
|
||||
if (val) {
|
||||
this.callLogDrawerVisible = true;
|
||||
@@ -516,7 +560,7 @@ export default {
|
||||
}
|
||||
this.restorePatientSheetAfterCallLog();
|
||||
},
|
||||
/** 抽屉关闭 */
|
||||
/** @close 与 update:visible=false 会连发,恢复逻辑已防重入 */
|
||||
onCallLogDrawerClose() {
|
||||
this.restorePatientSheetAfterCallLog();
|
||||
},
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<!-- 小程序抽屉须用 page-container + v-if,避免 u-popup 在订单详情页底内联 -->
|
||||
<!-- 对齐切换账号:pageGuard(v-if) 与 visible(:show) 拆开,关抽屉先离场再卸 -->
|
||||
<view>
|
||||
<page-container
|
||||
v-if="show"
|
||||
:show="show"
|
||||
v-if="pageGuard"
|
||||
:show="visible"
|
||||
:overlay="true"
|
||||
position="bottom"
|
||||
:round="true"
|
||||
@beforeleave="onClose"
|
||||
@leave="onClose"
|
||||
@afterleave="releasePageGuard"
|
||||
@clickoverlay="onClose"
|
||||
>
|
||||
<view class="rx-drawer">
|
||||
@@ -170,11 +172,13 @@
|
||||
</view>
|
||||
</view>
|
||||
</page-container>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 处方详情抽屉:page-container 底部浮层(管理端看方)
|
||||
* 返回拦截对齐切换账号:pageGuard / visible 拆分
|
||||
*/
|
||||
import { mapPrescriptionDetailView } from '@/subPackages/sub_business_shared/common/display.js'
|
||||
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||
@@ -185,11 +189,16 @@ export default {
|
||||
name: 'PrescriptionDetailPopup',
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
pageGuard: false,
|
||||
visible: false,
|
||||
releaseTimer: null,
|
||||
loading: false,
|
||||
view: null,
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearReleaseTimer()
|
||||
},
|
||||
methods: {
|
||||
/** 解析签名图片,自动兼容 base64 和 http 链接 */
|
||||
checkImageUrl(url) {
|
||||
@@ -198,7 +207,11 @@ export default {
|
||||
},
|
||||
open(prescriptionId) {
|
||||
if (!prescriptionId) return
|
||||
this.show = true
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = true
|
||||
this.$nextTick(() => {
|
||||
this.visible = true
|
||||
})
|
||||
this.view = null
|
||||
this.loading = true
|
||||
this.bizApi.getPrescriptionDetail(prescriptionId).then(w => {
|
||||
@@ -236,7 +249,25 @@ export default {
|
||||
},
|
||||
onClose() {
|
||||
this.view = null
|
||||
this.show = false
|
||||
this.visible = false
|
||||
this.scheduleReleaseGuard()
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = false
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false
|
||||
this.releaseTimer = null
|
||||
}, 350)
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer)
|
||||
this.releaseTimer = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<business-page-layout title="申请提现" :show-back="true">
|
||||
<view class="tip">可提现余额:{{ balanceText }}</view>
|
||||
<view class="tip tip-row">
|
||||
<text class="tip-balance">可提现余额:{{ balanceText }}</text>
|
||||
<view class="rules-entry" hover-class="rules-entry-hover" @click="openRules">
|
||||
<text class="rules-entry-txt">提现规则</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="gapText" class="gap">
|
||||
<text>{{ gapText }}</text>
|
||||
<view
|
||||
@@ -58,17 +63,19 @@
|
||||
<view v-if="orderList.length === 0" class="empty">暂无可提订单</view>
|
||||
</view>
|
||||
<button class="submit" @click="submit" :loading="loading">提交申请</button>
|
||||
<withdrawal-rules-drawer ref="rulesDrawer" :fetch-rules="fetchWithdrawalRules" />
|
||||
</business-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||
import WithdrawalRulesDrawer from '@/subPackages/sub_business_shared/withdrawal/components/WithdrawalRulesDrawer.vue'
|
||||
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
|
||||
|
||||
export default {
|
||||
mixins: [businessMixin],
|
||||
components: { BusinessPageLayout },
|
||||
components: { BusinessPageLayout, WithdrawalRulesDrawer },
|
||||
data() {
|
||||
return {
|
||||
amount: '',
|
||||
@@ -140,6 +147,17 @@ export default {
|
||||
this.loadOrders()
|
||||
},
|
||||
methods: {
|
||||
/** 打开提现规则抽屉 */
|
||||
openRules() {
|
||||
this.$refs.rulesDrawer && this.$refs.rulesDrawer.open()
|
||||
},
|
||||
/** 拉系统配置提现规则;失败由抽屉回落默认文案 */
|
||||
fetchWithdrawalRules() {
|
||||
if (!this.bizApi || !this.bizApi.getWithdrawalRules) {
|
||||
return Promise.resolve({ ok: false, data: null })
|
||||
}
|
||||
return this.bizApi.getWithdrawalRules()
|
||||
},
|
||||
/**
|
||||
* 切换提现方式:清已选和旧列表,避免按时间窄集残留到按订单导致漏提
|
||||
* 按订单立刻重拉全量;按时间等用户点预览;自定义不拉单
|
||||
@@ -267,6 +285,30 @@ export default {
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.tip-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.tip-balance {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.rules-entry {
|
||||
flex-shrink: 0;
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(238, 248, 246, 0.95);
|
||||
border: 1rpx solid rgba(15, 118, 110, 0.22);
|
||||
box-shadow: 0 0 12rpx rgba(15, 118, 110, 0.1);
|
||||
}
|
||||
.rules-entry-hover { opacity: 0.75; }
|
||||
.rules-entry-txt {
|
||||
font-size: 24rpx;
|
||||
color: #0f766e;
|
||||
font-weight: 500;
|
||||
}
|
||||
.gap { color: #B45309; background: #FFF7ED; }
|
||||
.gap-order { display: block; margin-top: 12rpx; color: #0F766E; }
|
||||
.label { display: block; font-size: 26rpx; color: #999; margin-bottom: 12rpx; }
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* 提现规则默认文案(接口不存在 / 失败时兜底)
|
||||
* 与后端 SystemConfigService::defaultWithdrawalRules 对齐
|
||||
*/
|
||||
export const DEFAULT_WITHDRAWAL_RULES = {
|
||||
lead: '提现前请仔细阅读',
|
||||
foot: '实际到账时间以银行处理进度为准',
|
||||
flow_steps: [
|
||||
{ key: 'apply', label: '提交申请' },
|
||||
{ key: 'audit', label: '平台审核' },
|
||||
{ key: 'pay', label: '发起打款' },
|
||||
{ key: 'arrive', label: '到账' },
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
key: 'amount',
|
||||
title: '可提现额度',
|
||||
desc: '以账户「可提现余额」为准,单笔最低 50 元。',
|
||||
},
|
||||
{
|
||||
key: 'time',
|
||||
title: '提现时间',
|
||||
desc: '全天可提交申请,不限制时段。',
|
||||
},
|
||||
{
|
||||
key: 'daily',
|
||||
title: '每日次数',
|
||||
desc: '不限制每日提现次数。',
|
||||
},
|
||||
{
|
||||
key: 'arrive',
|
||||
title: '到账时间',
|
||||
desc: '工作日审核通过后约 10 分钟内到账;节假日顺延至次日到账。',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
/** @deprecated 请用 DEFAULT_WITHDRAWAL_RULES */
|
||||
export const WITHDRAWAL_RULES = DEFAULT_WITHDRAWAL_RULES.rules
|
||||
/** @deprecated 请用 DEFAULT_WITHDRAWAL_RULES */
|
||||
export const WITHDRAWAL_FLOW_STEPS = DEFAULT_WITHDRAWAL_RULES.flow_steps
|
||||
|
||||
/**
|
||||
* 规范化接口结果;残缺则补默认
|
||||
*/
|
||||
export function normalizeWithdrawalRules(raw) {
|
||||
const base = DEFAULT_WITHDRAWAL_RULES
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
return {
|
||||
lead: base.lead,
|
||||
foot: base.foot,
|
||||
flow_steps: base.flow_steps.slice(),
|
||||
rules: base.rules.slice(),
|
||||
}
|
||||
}
|
||||
const flow =
|
||||
Array.isArray(raw.flow_steps) && raw.flow_steps.length > 0
|
||||
? raw.flow_steps
|
||||
.map((s, i) => ({
|
||||
key: String((s && s.key) || `step_${i + 1}`),
|
||||
label: String((s && s.label) || '').trim(),
|
||||
}))
|
||||
.filter((s) => s.label)
|
||||
: base.flow_steps
|
||||
const rules =
|
||||
Array.isArray(raw.rules) && raw.rules.length > 0
|
||||
? raw.rules
|
||||
.map((r, i) => ({
|
||||
key: String((r && r.key) || `rule_${i + 1}`),
|
||||
title: String((r && r.title) || '').trim(),
|
||||
desc: String((r && r.desc) || '').trim(),
|
||||
}))
|
||||
.filter((r) => r.title || r.desc)
|
||||
: base.rules
|
||||
return {
|
||||
lead: String(raw.lead || '').trim() || base.lead,
|
||||
foot: String(raw.foot || '').trim() || base.foot,
|
||||
flow_steps: flow.length ? flow : base.flow_steps.slice(),
|
||||
rules: rules.length ? rules : base.rules.slice(),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,404 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- pageGuard 与 visible 拆开,对齐 Page-Container-Drawer -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container
|
||||
v-if="pageGuard"
|
||||
:show="visible"
|
||||
:overlay="false"
|
||||
@leave="close"
|
||||
@afterleave="releasePageGuard"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<u-popup
|
||||
v-model="visible"
|
||||
mode="bottom"
|
||||
border-radius="28"
|
||||
height="78%"
|
||||
z-index="10080"
|
||||
:safe-area-inset-bottom="true"
|
||||
@close="close"
|
||||
>
|
||||
<view class="rules-sheet">
|
||||
<view class="sheet-glow"></view>
|
||||
<view class="sheet-handle"></view>
|
||||
<view class="sheet-hd">
|
||||
<view class="hd-text">
|
||||
<text class="hd-title">提现规则</text>
|
||||
<text class="hd-sub">{{ leadText }}</text>
|
||||
</view>
|
||||
<view class="hd-close" hover-class="hd-close-hover" @click="close">
|
||||
<text class="hd-close-x">×</text>
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-y class="sheet-body">
|
||||
<view class="glass-card flow-card">
|
||||
<text class="card-label">审核流程</text>
|
||||
<view class="flow-row">
|
||||
<view
|
||||
v-for="(step, idx) in flowSteps"
|
||||
:key="step.key"
|
||||
class="flow-item"
|
||||
>
|
||||
<view class="flow-dot">
|
||||
<text class="flow-num">{{ idx + 1 }}</text>
|
||||
</view>
|
||||
<text class="flow-label">{{ step.label }}</text>
|
||||
<view v-if="idx < flowSteps.length - 1" class="flow-line"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
v-for="(rule, idx) in rules"
|
||||
:key="rule.key"
|
||||
class="glass-card rule-card"
|
||||
>
|
||||
<view class="rule-index">
|
||||
<text class="rule-index-txt">{{ idx + 1 }}</text>
|
||||
</view>
|
||||
<view class="rule-main">
|
||||
<text class="rule-title">{{ rule.title }}</text>
|
||||
<text class="rule-desc">{{ rule.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sheet-foot">
|
||||
<text class="foot-tip">{{ footText }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</u-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 提现规则抽屉(液态玻璃风格)
|
||||
* 优先走 fetchRules 拉系统配置;失败/未上线用本地默认文案
|
||||
*/
|
||||
import {
|
||||
normalizeWithdrawalRules,
|
||||
} from '@/subPackages/sub_business_shared/withdrawal/common/withdrawalRules.js'
|
||||
|
||||
export default {
|
||||
name: 'WithdrawalRulesDrawer',
|
||||
props: {
|
||||
/** 拉取规则的函数,返回 Promise<{ok,data}|data>;不传则只用默认文案 */
|
||||
fetchRules: {
|
||||
type: Function,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
const fallback = normalizeWithdrawalRules(null)
|
||||
return {
|
||||
visible: false,
|
||||
pageGuard: false,
|
||||
releaseTimer: null,
|
||||
leadText: fallback.lead,
|
||||
footText: fallback.foot,
|
||||
rules: fallback.rules,
|
||||
flowSteps: fallback.flow_steps,
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearReleaseTimer()
|
||||
},
|
||||
methods: {
|
||||
applyPayload(raw) {
|
||||
const payload = normalizeWithdrawalRules(raw)
|
||||
this.leadText = payload.lead
|
||||
this.footText = payload.foot
|
||||
this.rules = payload.rules
|
||||
this.flowSteps = payload.flow_steps
|
||||
},
|
||||
/** 打开前拉配置;接口失败保持默认,保证审核页有内容 */
|
||||
loadRules() {
|
||||
if (typeof this.fetchRules !== 'function') {
|
||||
this.applyPayload(null)
|
||||
return Promise.resolve()
|
||||
}
|
||||
return Promise.resolve()
|
||||
.then(() => this.fetchRules())
|
||||
.then((w) => {
|
||||
// 兼容 {ok,data} 与直接 result
|
||||
if (w && w.ok === false) {
|
||||
this.applyPayload(null)
|
||||
return
|
||||
}
|
||||
const data = w && w.data !== undefined ? w.data : w
|
||||
this.applyPayload(data)
|
||||
})
|
||||
.catch(() => {
|
||||
this.applyPayload(null)
|
||||
})
|
||||
},
|
||||
/** 打开抽屉:先挂 page-container,再显示弹层 */
|
||||
open() {
|
||||
this.clearReleaseTimer()
|
||||
this.loadRules()
|
||||
this.pageGuard = true
|
||||
this.$nextTick(() => {
|
||||
this.visible = true
|
||||
})
|
||||
},
|
||||
/** 关闭:先播离场,再卸 page-container */
|
||||
close() {
|
||||
if (!this.visible && !this.pageGuard) return
|
||||
this.visible = false
|
||||
this.scheduleReleaseGuard()
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = false
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false
|
||||
this.releaseTimer = null
|
||||
}, 350)
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer)
|
||||
this.releaseTimer = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 液态玻璃:半透明 + 轻模糊 + 细边,避免紫霓虹 / 粗阴影 */
|
||||
$glass-bg: rgba(255, 255, 255, 0.72);
|
||||
$glass-border: rgba(255, 255, 255, 0.55);
|
||||
$ink: #1d2129;
|
||||
$muted: #86909c;
|
||||
$accent: #0f766e;
|
||||
|
||||
.rules-sheet {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(245, 250, 248, 0.96) 0%,
|
||||
rgba(236, 244, 242, 0.98) 100%
|
||||
);
|
||||
border-radius: 28rpx 28rpx 0 0;
|
||||
}
|
||||
|
||||
.sheet-glow {
|
||||
position: absolute;
|
||||
top: -80rpx;
|
||||
left: 50%;
|
||||
width: 420rpx;
|
||||
height: 220rpx;
|
||||
margin-left: -210rpx;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(
|
||||
ellipse,
|
||||
rgba(106, 205, 187, 0.28) 0%,
|
||||
rgba(106, 205, 187, 0) 70%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sheet-handle {
|
||||
width: 64rpx;
|
||||
height: 8rpx;
|
||||
margin: 16rpx auto 0;
|
||||
border-radius: 8rpx;
|
||||
background: rgba(29, 33, 41, 0.12);
|
||||
}
|
||||
|
||||
.sheet-hd {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx 36rpx 12rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hd-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: $ink;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.hd-sub {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
.hd-close {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.55);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.7);
|
||||
/* #ifdef H5 || APP-PLUS */
|
||||
backdrop-filter: blur(12px);
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.hd-close-hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.hd-close-x {
|
||||
font-size: 40rpx;
|
||||
line-height: 1;
|
||||
color: $muted;
|
||||
margin-top: -4rpx;
|
||||
}
|
||||
|
||||
.sheet-body {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
padding: 8rpx 28rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
position: relative;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 24rpx;
|
||||
background: $glass-bg;
|
||||
border: 1rpx solid $glass-border;
|
||||
box-shadow:
|
||||
0 8rpx 28rpx rgba(15, 23, 32, 0.06),
|
||||
inset 0 1rpx 0 rgba(255, 255, 255, 0.85);
|
||||
/* #ifdef H5 || APP-PLUS */
|
||||
backdrop-filter: blur(18px);
|
||||
/* #endif */
|
||||
}
|
||||
|
||||
.card-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: $muted;
|
||||
margin-bottom: 24rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.flow-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flow-item {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flow-dot {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(15, 118, 110, 0.1);
|
||||
border: 1rpx solid rgba(15, 118, 110, 0.22);
|
||||
box-shadow: 0 0 12rpx rgba(15, 118, 110, 0.12);
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.flow-num {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
.flow-label {
|
||||
font-size: 22rpx;
|
||||
color: $ink;
|
||||
text-align: center;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.flow-line {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
left: calc(50% + 28rpx);
|
||||
right: calc(-50% + 28rpx);
|
||||
height: 2rpx;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(15, 118, 110, 0.35),
|
||||
rgba(15, 118, 110, 0.08)
|
||||
);
|
||||
}
|
||||
|
||||
.rule-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.rule-index {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 14rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(15, 118, 110, 0.08);
|
||||
border: 1rpx solid rgba(15, 118, 110, 0.16);
|
||||
}
|
||||
|
||||
.rule-index-txt {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: $accent;
|
||||
}
|
||||
|
||||
.rule-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rule-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: $ink;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.rule-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #4e5969;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.sheet-foot {
|
||||
padding: 12rpx 8rpx 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.foot-tip {
|
||||
font-size: 22rpx;
|
||||
color: $muted;
|
||||
}
|
||||
</style>
|
||||
@@ -59,6 +59,9 @@
|
||||
<view class="list-section">
|
||||
<view class="section-hd">
|
||||
<text class="section-title">提现记录</text>
|
||||
<view class="rules-entry" hover-class="rules-entry-hover" @click="openRules">
|
||||
<text class="rules-entry-txt">提现规则</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-list">
|
||||
@@ -91,11 +94,13 @@
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<withdrawal-rules-drawer ref="rulesDrawer" :fetch-rules="fetchWithdrawalRules" />
|
||||
</business-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
|
||||
import WithdrawalRulesDrawer from '@/subPackages/sub_business_shared/withdrawal/components/WithdrawalRulesDrawer.vue'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
|
||||
import { withdrawalCheckStatusText, formatDateTime } from '@/subPackages/sub_business_shared/common/display.js'
|
||||
@@ -105,7 +110,7 @@ import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefre
|
||||
|
||||
export default {
|
||||
mixins: [businessMixin, pullRefreshMixin],
|
||||
components: { BusinessPageLayout },
|
||||
components: { BusinessPageLayout, WithdrawalRulesDrawer },
|
||||
data() {
|
||||
return {
|
||||
cardTitle: '我的账户',
|
||||
@@ -171,6 +176,17 @@ export default {
|
||||
uni.navigateTo({ url: `${this.bizRoot}/withdrawal/account-change/index` });
|
||||
}
|
||||
},
|
||||
/** 打开提现规则抽屉(微信审核要求页面内清晰展示) */
|
||||
openRules() {
|
||||
this.$refs.rulesDrawer && this.$refs.rulesDrawer.open()
|
||||
},
|
||||
/** 拉系统配置提现规则;抽屉内部失败会回落默认文案 */
|
||||
fetchWithdrawalRules() {
|
||||
if (!this.bizApi || !this.bizApi.getWithdrawalRules) {
|
||||
return Promise.resolve({ ok: false, data: null })
|
||||
}
|
||||
return this.bizApi.getWithdrawalRules()
|
||||
},
|
||||
|
||||
// 提现详情跳转
|
||||
goWithdrawalDetail(item) {
|
||||
@@ -436,6 +452,10 @@ $color-text-muted: #86909C;
|
||||
.section-hd {
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 8rpx;
|
||||
padding-right: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@@ -444,6 +464,24 @@ $color-text-muted: #86909C;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.rules-entry {
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1rpx solid rgba(15, 118, 110, 0.22);
|
||||
box-shadow: 0 0 12rpx rgba(15, 118, 110, 0.1);
|
||||
}
|
||||
|
||||
.rules-entry-hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.rules-entry-txt {
|
||||
font-size: 24rpx;
|
||||
color: #0f766e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
padding: 24rpx 8rpx;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<template>
|
||||
<!-- 小程序抽屉须用 page-container + v-if,避免 u-popup 在订单详情页底内联 -->
|
||||
<!-- 对齐切换账号:pageGuard(v-if) 与 visible(:show) 拆开,关抽屉先离场再卸 -->
|
||||
<view>
|
||||
<page-container
|
||||
v-if="show"
|
||||
:show="show"
|
||||
v-if="pageGuard"
|
||||
:show="visible"
|
||||
:overlay="true"
|
||||
position="bottom"
|
||||
:round="true"
|
||||
@beforeleave="onClose"
|
||||
@leave="onClose"
|
||||
@afterleave="releasePageGuard"
|
||||
@clickoverlay="onClose"
|
||||
>
|
||||
<view class="rx-drawer">
|
||||
@@ -160,11 +162,13 @@
|
||||
</view>
|
||||
</view>
|
||||
</page-container>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 处方详情抽屉:page-container 底部浮层(诊所管理端)
|
||||
* 返回拦截对齐切换账号:pageGuard / visible 拆分
|
||||
*/
|
||||
import { getPrescriptionDetail } from '@/api/clinicAdmin.js'
|
||||
import { mapPrescriptionDetailView } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||
@@ -174,11 +178,16 @@ export default {
|
||||
name: 'PrescriptionDetailPopup',
|
||||
data() {
|
||||
return {
|
||||
show: false,
|
||||
pageGuard: false,
|
||||
visible: false,
|
||||
releaseTimer: null,
|
||||
loading: false,
|
||||
view: null,
|
||||
}
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearReleaseTimer()
|
||||
},
|
||||
methods: {
|
||||
/** 解析签名图片,自动兼容 base64 和 http 链接 */
|
||||
checkImageUrl(url) {
|
||||
@@ -187,7 +196,11 @@ export default {
|
||||
},
|
||||
open(prescriptionId) {
|
||||
if (!prescriptionId) return
|
||||
this.show = true
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = true
|
||||
this.$nextTick(() => {
|
||||
this.visible = true
|
||||
})
|
||||
this.view = null
|
||||
this.loading = true
|
||||
getPrescriptionDetail(prescriptionId).then(w => {
|
||||
@@ -225,7 +238,25 @@ export default {
|
||||
},
|
||||
onClose() {
|
||||
this.view = null
|
||||
this.show = false
|
||||
this.visible = false
|
||||
this.scheduleReleaseGuard()
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = false
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false
|
||||
this.releaseTimer = null
|
||||
}, 350)
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer)
|
||||
this.releaseTimer = null
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
<template>
|
||||
<clinic-page-layout title="申请提现" :show-back="true">
|
||||
<view class="tip">可提现余额:{{ balanceText }}</view>
|
||||
<view class="tip tip-row">
|
||||
<text class="tip-balance">可提现余额:{{ balanceText }}</text>
|
||||
<view class="rules-entry" hover-class="rules-entry-hover" @click="openRules">
|
||||
<text class="rules-entry-txt">提现规则</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="gapText" class="gap">
|
||||
<text>{{ gapText }}</text>
|
||||
<view
|
||||
@@ -58,16 +63,18 @@
|
||||
<view v-if="orderList.length === 0" class="empty">暂无可提订单</view>
|
||||
</view>
|
||||
<button class="submit" @click="submit" :loading="loading">提交申请</button>
|
||||
<withdrawal-rules-drawer ref="rulesDrawer" :fetch-rules="fetchWithdrawalRules" />
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||
import WithdrawalRulesDrawer from '@/subPackages/sub_business_shared/withdrawal/components/WithdrawalRulesDrawer.vue'
|
||||
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||
import { getClinicAdminBalance, getWithdrawableOrders, submitWithdrawal } from '@/api/clinicAdmin.js'
|
||||
import { getClinicAdminBalance, getWithdrawableOrders, submitWithdrawal, getWithdrawalRules } from '@/api/clinicAdmin.js'
|
||||
|
||||
export default {
|
||||
components: { ClinicPageLayout },
|
||||
components: { ClinicPageLayout, WithdrawalRulesDrawer },
|
||||
data() {
|
||||
return {
|
||||
amount: '',
|
||||
@@ -138,6 +145,14 @@
|
||||
this.loadOrders()
|
||||
},
|
||||
methods: {
|
||||
/** 打开提现规则抽屉 */
|
||||
openRules() {
|
||||
this.$refs.rulesDrawer && this.$refs.rulesDrawer.open()
|
||||
},
|
||||
/** 拉系统配置提现规则;失败由抽屉回落默认文案 */
|
||||
fetchWithdrawalRules() {
|
||||
return getWithdrawalRules()
|
||||
},
|
||||
onStartDate(e) { this.startDate = e.detail.value },
|
||||
onEndDate(e) { this.endDate = e.detail.value },
|
||||
onCategory(e) {
|
||||
@@ -251,6 +266,30 @@
|
||||
padding: 24rpx;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.tip-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.tip-balance {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.rules-entry {
|
||||
flex-shrink: 0;
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(238, 248, 246, 0.95);
|
||||
border: 1rpx solid rgba(15, 118, 110, 0.22);
|
||||
box-shadow: 0 0 12rpx rgba(15, 118, 110, 0.1);
|
||||
}
|
||||
.rules-entry-hover { opacity: 0.75; }
|
||||
.rules-entry-txt {
|
||||
font-size: 24rpx;
|
||||
color: #0f766e;
|
||||
font-weight: 500;
|
||||
}
|
||||
.gap { color: #B45309; background: #FFF7ED; }
|
||||
.gap-order { display: block; margin-top: 12rpx; color: #0F766E; }
|
||||
.label { display: block; font-size: 26rpx; color: #999; margin-bottom: 12rpx; }
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
<view class="list-section">
|
||||
<view class="section-hd">
|
||||
<text class="section-title">提现记录</text>
|
||||
<view class="rules-entry" hover-class="rules-entry-hover" @click="openRules">
|
||||
<text class="rules-entry-txt">提现规则</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="record-list">
|
||||
@@ -76,12 +79,14 @@
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<withdrawal-rules-drawer ref="rulesDrawer" :fetch-rules="fetchWithdrawalRules" />
|
||||
</clinic-page-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
|
||||
import { getClinicAdminCard, getClinicAdminBalance, getWithdrawalList } from '@/api/clinicAdmin.js'
|
||||
import WithdrawalRulesDrawer from '@/subPackages/sub_business_shared/withdrawal/components/WithdrawalRulesDrawer.vue'
|
||||
import { getClinicAdminCard, getClinicAdminBalance, getWithdrawalList, getWithdrawalRules } from '@/api/clinicAdmin.js'
|
||||
import { withWxKey } from '@/utils/wxListKey.js'
|
||||
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
|
||||
import { withdrawalCheckStatusText, formatDateTime } from '@/subPackages/sub_clinic_admin/common/display.js'
|
||||
@@ -89,7 +94,7 @@ import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefre
|
||||
|
||||
export default {
|
||||
mixins: [pullRefreshMixin],
|
||||
components: { ClinicPageLayout },
|
||||
components: { ClinicPageLayout, WithdrawalRulesDrawer },
|
||||
data() {
|
||||
return {
|
||||
cardTitle: '我的账户',
|
||||
@@ -155,6 +160,14 @@ export default {
|
||||
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/withdrawal/account-change/index' });
|
||||
}
|
||||
},
|
||||
/** 打开提现规则抽屉(微信审核要求页面内清晰展示) */
|
||||
openRules() {
|
||||
this.$refs.rulesDrawer && this.$refs.rulesDrawer.open()
|
||||
},
|
||||
/** 拉系统配置提现规则;失败由抽屉回落默认文案 */
|
||||
fetchWithdrawalRules() {
|
||||
return getWithdrawalRules()
|
||||
},
|
||||
|
||||
// 提现详情跳转
|
||||
goWithdrawalDetail(item) {
|
||||
@@ -384,6 +397,10 @@ $color-text-muted: #86909C;
|
||||
.section-hd {
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 8rpx;
|
||||
padding-right: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
@@ -392,6 +409,24 @@ $color-text-muted: #86909C;
|
||||
color: $color-text-main;
|
||||
}
|
||||
|
||||
.rules-entry {
|
||||
padding: 10rpx 22rpx;
|
||||
border-radius: 999rpx;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1rpx solid rgba(15, 118, 110, 0.22);
|
||||
box-shadow: 0 0 12rpx rgba(15, 118, 110, 0.1);
|
||||
}
|
||||
|
||||
.rules-entry-hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.rules-entry-txt {
|
||||
font-size: 24rpx;
|
||||
color: #0f766e;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.record-item {
|
||||
padding: 24rpx 8rpx;
|
||||
border-bottom: 1rpx solid #F2F3F5;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- pageGuard / show 拆开,对齐 DrawerPageContainer -->
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container
|
||||
v-if="visible"
|
||||
v-if="pageGuard"
|
||||
:show="visible"
|
||||
:overlay="false"
|
||||
@leave="close"
|
||||
@afterleave="releasePageGuard"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<u-popup
|
||||
@@ -65,13 +67,16 @@
|
||||
import { getClinicSalespersonTransferPrescriptionDetail } from '@/api/clinicSalesperson.js'
|
||||
|
||||
/**
|
||||
* 传方详情抽屉
|
||||
* 传方详情抽屉(诊所业务员)
|
||||
* 返回拦截:pageGuard 与 visible 拆开,leave 关抽屉,afterleave 再卸 page-container
|
||||
*/
|
||||
export default {
|
||||
name: 'TransferDetailDrawer',
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
pageGuard: false,
|
||||
releaseTimer: null,
|
||||
loading: false,
|
||||
detail: null,
|
||||
}
|
||||
@@ -79,12 +84,30 @@ export default {
|
||||
computed: {
|
||||
statusType() {
|
||||
const map = { 0: 'warning', 1: 'primary', 2: 'success', 3: 'error' }
|
||||
return map[Number(this.detail?.status)] || 'info'
|
||||
const st = this.detail ? Number(this.detail.status) : -1
|
||||
return map[st] || 'info'
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
visible(v) {
|
||||
if (v) {
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = true
|
||||
} else {
|
||||
this.scheduleReleaseGuard()
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearReleaseTimer()
|
||||
},
|
||||
methods: {
|
||||
async open(id) {
|
||||
this.visible = true
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = true
|
||||
this.$nextTick(() => {
|
||||
this.visible = true
|
||||
})
|
||||
this.loading = true
|
||||
this.detail = null
|
||||
try {
|
||||
@@ -99,8 +122,25 @@ export default {
|
||||
close() {
|
||||
this.visible = false
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = false
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false
|
||||
this.releaseTimer = null
|
||||
}, 350)
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer)
|
||||
this.releaseTimer = null
|
||||
}
|
||||
},
|
||||
preview(url) {
|
||||
const urls = this.detail?.prescription_images || []
|
||||
const urls = (this.detail && this.detail.prescription_images) || []
|
||||
uni.previewImage({ urls, current: url })
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<clinic-salesperson-page-layout title="我的" :show-tabbar="true" :tab-index="1">
|
||||
<!-- tab 顺序是工作台/功能/我的,我的必须是 2;写成 1 时点「功能」会被当成当前页直接 return -->
|
||||
<clinic-salesperson-page-layout title="我的" :show-tabbar="true" :tab-index="2">
|
||||
<!-- 页面主体统一收进 admin-my-panel(资料区 + 个人中心入口 + 菜单卡 + 退出) -->
|
||||
<admin-my-panel
|
||||
ref="myPanel"
|
||||
|
||||
50
subPackages/sub_image_transfer/common/display.js
Normal file
50
subPackages/sub_image_transfer/common/display.js
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* 传方订单状态展示口径(列表 / 详情共用)
|
||||
*
|
||||
* 状态枚举来自后端 app/Enum/imageTransfer/ImageTransferOrderStatusEnum.php:
|
||||
* 1 待审 / 2 驳回 / 3 已传 / 4 推送失败
|
||||
* 小程序端可编辑:status∈{1,2}(审核通过前可改后再交)
|
||||
*
|
||||
* 为什么单独抽一份:列表的状态药丸和详情的状态标签必须同色同文案,
|
||||
* 两页各写一套迟早会漂移;后台 xk-admin 传方订单卡片也用同一套语义。
|
||||
*/
|
||||
|
||||
/** 状态 → 展示元数据:color 用于文字与圆点,bg 用于药丸底色 */
|
||||
const STATUS_META = {
|
||||
1: { text: '待审核', color: '#FF9900', bg: '#FFF7E6' },
|
||||
2: { text: '已驳回', color: '#FA3534', bg: '#FEF0F0' },
|
||||
3: { text: '已传方', color: '#12B886', bg: '#E8F8F2' },
|
||||
4: { text: '传方失败', color: '#E4572E', bg: '#FDF1EC' },
|
||||
};
|
||||
|
||||
/** 未知状态兜底:后端新增状态时不应出现白字或错色 */
|
||||
const STATUS_FALLBACK = { text: '未知状态', color: '#8A94A6', bg: '#F1F3F5' };
|
||||
|
||||
/**
|
||||
* 取状态展示元数据
|
||||
* @param {number|string} status 接口返回的 status(可能是字符串,统一 Number 化)
|
||||
*/
|
||||
export function transferStatusMeta(status) {
|
||||
return STATUS_META[Number(status)] || STATUS_FALLBACK;
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态色药丸的内联样式(背景 + 描边由状态色推导)
|
||||
* @param {number|string} status
|
||||
*/
|
||||
export function transferPillStyle(status) {
|
||||
const meta = transferStatusMeta(status);
|
||||
return {
|
||||
backgroundColor: meta.bg,
|
||||
borderColor: meta.color + '33',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 状态文案:优先用后端的 status_text,缺省时回落本地文案
|
||||
* @param {{status: number|string, status_text?: string}} row
|
||||
*/
|
||||
export function transferStatusText(row) {
|
||||
const meta = transferStatusMeta(row && row.status);
|
||||
return (row && row.status_text) || meta.text;
|
||||
}
|
||||
144
subPackages/sub_image_transfer/common/processRuleCascade.js
Normal file
144
subPackages/sub_image_transfer/common/processRuleCascade.js
Normal file
@@ -0,0 +1,144 @@
|
||||
/**
|
||||
* 委托调剂三级级联(制剂 / 煎法 / 备注)
|
||||
* 识图抽屉与传方详情共用,避免 pid / rule_id 分派逻辑复制两份
|
||||
*/
|
||||
import { unwrapXkApi } from '@/utils/api-response.js';
|
||||
import { getProcessRuleList } from '@/api/reception.js';
|
||||
|
||||
/**
|
||||
* 解包 process-rule-list 响应为数组
|
||||
* 医生/诊所接口都走 jok,统一 unwrapXkApi;兼容偶发裸数组
|
||||
*/
|
||||
function unwrapRuleList(res) {
|
||||
const { ok, payload } = unwrapXkApi(res);
|
||||
if (ok && Array.isArray(payload)) return payload;
|
||||
if (Array.isArray(payload)) return payload;
|
||||
if (res && Array.isArray(res.result)) return res.result;
|
||||
if (Array.isArray(res)) return res;
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 pid / rule_id 拉一级列表并写回 target
|
||||
* @param {Object} opts
|
||||
* @param {Function} [opts.api] process-rule-list,默认医生 getProcessRuleList
|
||||
* @param {number} [opts.pid=0] 父级 id;0 表示顶级制剂
|
||||
* @param {number} [opts.ruleId=0] 有值时按 rule_id 拉备注列表
|
||||
* @param {{processRuleList:Array,childProcessRuleList:Array,processRuleNoteList:Array}} opts.target 写回容器(通常是组件 this)
|
||||
*/
|
||||
export async function loadProcessRuleListInto(opts) {
|
||||
const api = (opts && opts.api) || getProcessRuleList;
|
||||
const pid = Number((opts && opts.pid) || 0);
|
||||
const ruleId = Number((opts && opts.ruleId) || 0);
|
||||
const target = (opts && opts.target) || {};
|
||||
const data = ruleId === 0 ? { pid } : { rule_id: ruleId };
|
||||
let list = [];
|
||||
try {
|
||||
const res = await api(data);
|
||||
list = unwrapRuleList(res);
|
||||
} catch (e) {
|
||||
list = [];
|
||||
throw e;
|
||||
} finally {
|
||||
if (ruleId !== 0) {
|
||||
target.processRuleNoteList = list;
|
||||
} else if (pid === 0) {
|
||||
target.processRuleList = list;
|
||||
} else {
|
||||
target.childProcessRuleList = list;
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 chineseConfig 中的 ID 依次拉齐三级列表(OCR / 详情回填后用)
|
||||
* 为什么先拉顶级:子级接口依赖已选制剂 id,列表空时无法校验回填是否仍有效
|
||||
* @param {Object} opts
|
||||
* @param {Function} [opts.api]
|
||||
* @param {{ruleType:number,processRuleId:*,childProcessRuleId:*}} opts.config
|
||||
* @param {Object} opts.target
|
||||
*/
|
||||
export async function hydrateProcessRuleListsFromConfig(opts) {
|
||||
const config = (opts && opts.config) || {};
|
||||
const target = (opts && opts.target) || {};
|
||||
const api = opts && opts.api;
|
||||
if (Number(config.ruleType) !== 2) return;
|
||||
if (!target.processRuleList || !target.processRuleList.length) {
|
||||
await loadProcessRuleListInto({ api, pid: 0, ruleId: 0, target });
|
||||
}
|
||||
const prId = Number(config.processRuleId || 0);
|
||||
if (prId > 0) {
|
||||
await loadProcessRuleListInto({ api, pid: prId, ruleId: 0, target });
|
||||
}
|
||||
const childId = Number(config.childProcessRuleId || 0);
|
||||
if (childId > 0) {
|
||||
await loadProcessRuleListInto({ api, pid: 0, ruleId: childId, target });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认中药代煎配置:传方默认委托调剂(ruleType=2)
|
||||
*/
|
||||
export function defaultChineseConfig(overrides) {
|
||||
return Object.assign(
|
||||
{
|
||||
ruleType: 2,
|
||||
packageMethodId: null,
|
||||
processRuleId: null,
|
||||
childProcessRuleId: null,
|
||||
processRuleNoteId: null,
|
||||
dosage: 7,
|
||||
dayDosage: 2,
|
||||
},
|
||||
overrides || {},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 传方代煎+用量必填校验(对齐开方 PrescriptionValidator)
|
||||
* @param {Object} config chineseConfig
|
||||
* @returns {{valid:boolean,keys:string[],message:string}}
|
||||
*/
|
||||
export function validateChineseConfigRequired(config) {
|
||||
const cfg = config || {};
|
||||
const keys = [];
|
||||
const messages = [];
|
||||
const ruleType = Number(cfg.ruleType || 0);
|
||||
if (ruleType !== 1 && ruleType !== 2) {
|
||||
keys.push('ruleType');
|
||||
messages.push('请选择制剂方式');
|
||||
}
|
||||
if (ruleType === 1) {
|
||||
if (!Number(cfg.packageMethodId)) {
|
||||
keys.push('packageMethodId');
|
||||
messages.push('请选择包法');
|
||||
}
|
||||
} else if (ruleType === 2) {
|
||||
if (!Number(cfg.processRuleId)) {
|
||||
keys.push('processRuleId');
|
||||
messages.push('请选择制剂要求');
|
||||
}
|
||||
if (!Number(cfg.childProcessRuleId)) {
|
||||
keys.push('childProcessRuleId');
|
||||
messages.push('请选择煎法说明');
|
||||
}
|
||||
if (!Number(cfg.processRuleNoteId)) {
|
||||
keys.push('processRuleNoteId');
|
||||
messages.push('请选择附加备注');
|
||||
}
|
||||
}
|
||||
if (!(Number(cfg.dosage) >= 1)) {
|
||||
keys.push('dosage');
|
||||
messages.push('请填写剂数');
|
||||
}
|
||||
if (!(Number(cfg.dayDosage) >= 1)) {
|
||||
keys.push('dayDosage');
|
||||
messages.push('请填写用药频次');
|
||||
}
|
||||
return {
|
||||
valid: keys.length === 0,
|
||||
keys,
|
||||
message: messages[0] || '',
|
||||
};
|
||||
}
|
||||
258
subPackages/sub_image_transfer/components/RejectReasonDrawer.vue
Normal file
258
subPackages/sub_image_transfer/components/RejectReasonDrawer.vue
Normal file
@@ -0,0 +1,258 @@
|
||||
<template>
|
||||
<!-- 传方审方驳回:文本域录入原因;pageGuard / show 拆分防意外退出 -->
|
||||
<view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container
|
||||
v-if="pageGuard"
|
||||
:show="visible"
|
||||
:overlay="true"
|
||||
position="bottom"
|
||||
:round="true"
|
||||
@leave="onClose"
|
||||
@afterleave="releasePageGuard"
|
||||
@clickoverlay="onClose"
|
||||
>
|
||||
<view class="reject-drawer">
|
||||
<view class="reject-head">
|
||||
<text class="reject-title">驳回原因</text>
|
||||
<text class="reject-close" @click="onClose">关闭</text>
|
||||
</view>
|
||||
<view class="reject-body">
|
||||
<text class="reject-hint">请说明驳回原因,便于诊所修改后再交</text>
|
||||
<textarea
|
||||
class="reject-textarea"
|
||||
:value="reason"
|
||||
:maxlength="200"
|
||||
:disabled="submitting"
|
||||
:auto-height="false"
|
||||
:show-confirm-bar="false"
|
||||
placeholder="例如:药品克数有误、地址不完整、代煎未选…"
|
||||
placeholder-class="reject-ph"
|
||||
@input="onInput"
|
||||
/>
|
||||
<view class="reject-count-row">
|
||||
<text class="reject-count">{{ reasonLen }}/200</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="reject-foot">
|
||||
<view
|
||||
class="reject-btn reject-btn-ghost"
|
||||
:class="{ 'is-disabled': submitting }"
|
||||
hover-class="hover-dim"
|
||||
@click="onClose"
|
||||
>取消</view>
|
||||
<view
|
||||
class="reject-btn reject-btn-primary"
|
||||
:class="{ 'is-disabled': submitting || !canSubmit }"
|
||||
hover-class="hover-dim"
|
||||
@click="onConfirm"
|
||||
>
|
||||
<u-loading v-if="submitting" mode="circle" color="#FFFFFF" size="28"></u-loading>
|
||||
<text class="reject-btn-text">{{ submitting ? '提交中…' : '确认驳回' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</page-container>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 传方订单驳回原因抽屉
|
||||
* 替代 uni.showModal editable(仅单行且样式差),用文本域 + 品牌青绿按钮
|
||||
*/
|
||||
export default {
|
||||
name: 'RejectReasonDrawer',
|
||||
props: {
|
||||
/** v-model 显隐 */
|
||||
value: { type: Boolean, default: false },
|
||||
/** 父页提交中:锁按钮防重复 */
|
||||
submitting: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageGuard: false,
|
||||
visible: false,
|
||||
releaseTimer: null,
|
||||
reason: '',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
reasonLen() {
|
||||
return String(this.reason || '').length;
|
||||
},
|
||||
canSubmit() {
|
||||
return String(this.reason || '').trim().length > 0;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(v) {
|
||||
if (v) {
|
||||
this.open();
|
||||
} else {
|
||||
// 父级强制关(驳回成功)即使 submitting 也要关,否则会卡在遮罩
|
||||
this.forceClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearReleaseTimer();
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
const v = (e && e.detail && e.detail.value != null) ? e.detail.value : '';
|
||||
this.reason = String(v);
|
||||
},
|
||||
open() {
|
||||
this.clearReleaseTimer();
|
||||
this.reason = '';
|
||||
this.pageGuard = true;
|
||||
this.$nextTick(() => {
|
||||
this.visible = true;
|
||||
});
|
||||
},
|
||||
/** 用户点取消/遮罩:提交中禁止关掉,避免半截请求 */
|
||||
onClose() {
|
||||
if (this.submitting) return;
|
||||
this.forceClose();
|
||||
},
|
||||
/** 真正关抽屉(父级 v-model=false 或用户取消) */
|
||||
forceClose() {
|
||||
this.visible = false;
|
||||
if (this.value) {
|
||||
this.$emit('input', false);
|
||||
}
|
||||
this.scheduleReleaseGuard();
|
||||
},
|
||||
onConfirm() {
|
||||
if (this.submitting) return;
|
||||
const text = String(this.reason || '').trim();
|
||||
if (!text) {
|
||||
uni.showToast({ title: '请填写驳回原因', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.$emit('confirm', text);
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = false;
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false;
|
||||
this.releaseTimer = null;
|
||||
}, 350);
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer);
|
||||
this.releaseTimer = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hover-dim {
|
||||
opacity: 0.86;
|
||||
}
|
||||
.reject-drawer {
|
||||
background: #fff;
|
||||
border-radius: 28rpx 28rpx 0 0;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
overflow: hidden;
|
||||
}
|
||||
.reject-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 32rpx 16rpx;
|
||||
}
|
||||
.reject-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2a33;
|
||||
}
|
||||
.reject-close {
|
||||
font-size: 28rpx;
|
||||
color: #00a88a;
|
||||
padding: 8rpx 4rpx;
|
||||
}
|
||||
.reject-body {
|
||||
padding: 8rpx 32rpx 12rpx;
|
||||
}
|
||||
.reject-hint {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #8a92a3;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.reject-textarea {
|
||||
width: 100%;
|
||||
min-height: 220rpx;
|
||||
max-height: 360rpx;
|
||||
box-sizing: border-box;
|
||||
padding: 24rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 1.55;
|
||||
color: #2a2e35;
|
||||
background: #f7f8fa;
|
||||
border-radius: 16rpx;
|
||||
border: 1px solid rgba(106, 205, 187, 0.35);
|
||||
box-shadow: 0 0 8rpx rgba(106, 205, 187, 0.12);
|
||||
}
|
||||
.reject-ph {
|
||||
color: #b0b7c3;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
.reject-count-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.reject-count {
|
||||
font-size: 22rpx;
|
||||
color: #9aa4ae;
|
||||
}
|
||||
.reject-foot {
|
||||
display: flex;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx 32rpx 8rpx;
|
||||
}
|
||||
.reject-btn {
|
||||
flex: 1;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.reject-btn-ghost {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
.reject-btn-ghost .reject-btn-text,
|
||||
.reject-btn-ghost {
|
||||
color: #5a6472;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.reject-btn-primary {
|
||||
background: linear-gradient(135deg, #00bfa6 0%, #6acdbb 100%);
|
||||
box-shadow: 0 10rpx 24rpx rgba(106, 205, 187, 0.32);
|
||||
}
|
||||
.reject-btn-text {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
.is-disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,211 @@
|
||||
<template>
|
||||
<!--
|
||||
开方医生选择:卡片(头像+姓名+科室/职称),盖在当前页上
|
||||
不用 page-container:同页只能有一个,识图抽屉/详情驳回抽屉已占用
|
||||
-->
|
||||
<view v-if="value" class="tdp-mask" @click="onClose">
|
||||
<view class="tdp-sheet" @click.stop>
|
||||
<view class="tdp-head">
|
||||
<text class="tdp-title">选择开方医生</text>
|
||||
<text class="tdp-close" @click="onClose">关闭</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="tdp-scroll">
|
||||
<view v-if="!list.length" class="tdp-empty">暂无本店医生</view>
|
||||
<view
|
||||
v-for="doc in list"
|
||||
:key="doc.id"
|
||||
class="tdp-card"
|
||||
:class="{ on: selectedId == doc.id }"
|
||||
@click="onPick(doc)"
|
||||
>
|
||||
<view
|
||||
v-if="!doc.avatar"
|
||||
class="tdp-avatar tdp-avatar-fallback"
|
||||
>
|
||||
<text class="tdp-avatar-char">{{ (doc.name || '医').slice(0, 1) }}</text>
|
||||
</view>
|
||||
<image
|
||||
v-else
|
||||
class="tdp-avatar"
|
||||
:src="doc.avatar"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="tdp-main">
|
||||
<view class="tdp-name-row">
|
||||
<text class="tdp-name">{{ doc.name || doc.real_name || ('医生#' + doc.id) }}</text>
|
||||
<text v-if="doc.title" class="tdp-title-tag">{{ doc.title }}</text>
|
||||
</view>
|
||||
<text class="tdp-dept">{{ doc.department || doc.depart || '未设置科室' }}</text>
|
||||
</view>
|
||||
<view class="tdp-check" :class="{ on: selectedId == doc.id }">
|
||||
<u-icon
|
||||
v-if="selectedId == doc.id"
|
||||
name="checkmark"
|
||||
size="26"
|
||||
color="#FFFFFF"
|
||||
></u-icon>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 传方开方医生选择器(卡片)
|
||||
* 列表项约定:id / name / avatar / department / title
|
||||
*/
|
||||
export default {
|
||||
name: 'TransferDoctorPicker',
|
||||
props: {
|
||||
value: { type: Boolean, default: false },
|
||||
list: { type: Array, default: () => [] },
|
||||
selectedId: { type: [Number, String], default: 0 },
|
||||
},
|
||||
methods: {
|
||||
onClose() {
|
||||
this.$emit('input', false);
|
||||
},
|
||||
onPick(doc) {
|
||||
if (!doc) return;
|
||||
this.$emit('select', doc);
|
||||
this.$emit('input', false);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.tdp-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 10100;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.tdp-sheet {
|
||||
max-height: 72vh;
|
||||
background: #f5f7f8;
|
||||
border-radius: 28rpx 28rpx 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
overflow: hidden;
|
||||
}
|
||||
.tdp-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 32rpx 16rpx;
|
||||
background: #fff;
|
||||
border-bottom: 1rpx solid #eef0f3;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tdp-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2a33;
|
||||
}
|
||||
.tdp-close {
|
||||
font-size: 28rpx;
|
||||
color: #00a88a;
|
||||
padding: 8rpx 4rpx;
|
||||
}
|
||||
.tdp-scroll {
|
||||
flex: 1;
|
||||
height: 56vh;
|
||||
padding: 20rpx 24rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.tdp-empty {
|
||||
padding: 100rpx 0;
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #98a2b3;
|
||||
}
|
||||
.tdp-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
border: 1px solid rgba(106, 205, 187, 0.22);
|
||||
box-shadow: 0 0 8rpx rgba(106, 205, 187, 0.1);
|
||||
}
|
||||
.tdp-card.on {
|
||||
border-color: rgba(0, 168, 138, 0.55);
|
||||
box-shadow: 0 0 12rpx rgba(106, 205, 187, 0.28);
|
||||
background: rgba(106, 205, 187, 0.08);
|
||||
}
|
||||
.tdp-avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: #e8f6f4;
|
||||
}
|
||||
.tdp-avatar-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #00bfa6 0%, #6acdbb 100%);
|
||||
}
|
||||
.tdp-avatar-char {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
}
|
||||
.tdp-main {
|
||||
flex: 1;
|
||||
margin-left: 20rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
.tdp-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.tdp-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #16302c;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
.tdp-title-tag {
|
||||
font-size: 22rpx;
|
||||
color: #008771;
|
||||
background: rgba(106, 205, 187, 0.16);
|
||||
border: 1px solid rgba(106, 205, 187, 0.35);
|
||||
border-radius: 8rpx;
|
||||
padding: 2rpx 10rpx;
|
||||
}
|
||||
.tdp-dept {
|
||||
display: block;
|
||||
margin-top: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #6b7785;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.tdp-check {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #d0d5dd;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
.tdp-check.on {
|
||||
border-color: #00a88a;
|
||||
background: #00a88a;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,481 @@
|
||||
<template>
|
||||
<!-- 传方订单只读处方笺:pageGuard / show 拆分,对齐切换账号 -->
|
||||
<view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container
|
||||
v-if="pageGuard"
|
||||
:show="visible"
|
||||
:overlay="true"
|
||||
position="bottom"
|
||||
:round="true"
|
||||
@leave="onClose"
|
||||
@afterleave="releasePageGuard"
|
||||
@clickoverlay="onClose"
|
||||
>
|
||||
<view class="rx-drawer">
|
||||
<view class="rx-drawer-head">
|
||||
<text class="rx-drawer-title">处方笺</text>
|
||||
<text class="rx-drawer-close" @click="onClose">关闭</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="popup-scroll">
|
||||
<view v-if="view" class="container safe-area-inset-bottom">
|
||||
<view class="head">
|
||||
<view class="head_record">
|
||||
<image
|
||||
:src="view.sealImage || defaultSeal"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<view class="record_yard">
|
||||
<view class="yard">{{ view.storeName }}</view>
|
||||
<view class="state">处方笺</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="view.createdAt" class="head_record_time">
|
||||
<text>开具日期:{{ view.createdAt }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="my_info">
|
||||
<view class="info">
|
||||
<view class="info_item">
|
||||
<view class="name">姓名:<text>{{ view.patientName }}</text></view>
|
||||
</view>
|
||||
<view class="info_item">
|
||||
<view class="name">性别:<text>{{ view.patientSex }}</text></view>
|
||||
</view>
|
||||
<view class="info_item">
|
||||
<view class="name">年龄:<text>{{ view.patientAge }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="view.diagnose" class="info">
|
||||
<view class="info_item">
|
||||
<view class="names">诊断:<text>{{ view.diagnose }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bg">
|
||||
<view class="my_medical">
|
||||
<view class="title">Rp</view>
|
||||
<view class="item">
|
||||
<view class="name">
|
||||
<view v-for="(drug, dIdx) in view.drugs" :key="dIdx" class="_name">
|
||||
<view class="text">
|
||||
<text>{{ drug.name }}</text>
|
||||
<text v-if="drug.usage" class="_abbr">[{{ drug.usage }}]</text>
|
||||
</view>
|
||||
<text class="name_num">{{ drug.qty }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="view.process || view.dosageText" class="details">
|
||||
<view class="text">
|
||||
<text v-if="view.dosageText">用法:{{ view.dosageText }}</text>
|
||||
<text v-if="view.process" style="margin-left: 10rpx;">代煎:{{ view.process }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="doctor_order">
|
||||
<view class="yz">
|
||||
<text class="order_info_left">医嘱:</text>
|
||||
<view class="order_info">
|
||||
<view class="info">{{ view.doctorOrder || '无' }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="titles">处方开具已完毕</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="my_doctor">
|
||||
<view class="doctor_info">
|
||||
<view class="info">
|
||||
<text>医师</text>
|
||||
<!-- 有签名图只显示图;无图才回落姓名(对齐处方详情) -->
|
||||
<image
|
||||
v-if="view.doctorSignSrc"
|
||||
class="sign-img"
|
||||
:src="view.doctorSignSrc"
|
||||
mode="aspectFit"
|
||||
></image>
|
||||
<text v-else class="name">{{ view.doctorName || '' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="popup-loading">暂无数据</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</page-container>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 传方订单处方笺抽屉(只读)
|
||||
* 从 order 对象拼装视图,样式对齐诊所端 PrescriptionDetailPopup 处方笺核心区
|
||||
*/
|
||||
export default {
|
||||
name: 'TransferPrescriptionPad',
|
||||
props: {
|
||||
/** v-model 显隐 */
|
||||
value: { type: Boolean, default: false },
|
||||
/** 传方订单详情对象 */
|
||||
order: { type: Object, default: null },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageGuard: false,
|
||||
visible: false,
|
||||
releaseTimer: null,
|
||||
/** 无公章时的默认水印图 */
|
||||
defaultSeal: 'https://xiaokang88.oss-cn-hangzhou.aliyuncs.com/xk-doctor-wx/static/group/xkyard.png',
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 把传方订单字段映射成处方笺展示结构 */
|
||||
view() {
|
||||
const o = this.order || {};
|
||||
if (!o || !o.id) return null;
|
||||
const drugsRaw = Array.isArray(o.drugs)
|
||||
? o.drugs
|
||||
: (typeof o.drugs_json === 'string'
|
||||
? this.parseDrugsJson(o.drugs_json)
|
||||
: (Array.isArray(o.drugs_json) ? o.drugs_json : []));
|
||||
const drugs = [];
|
||||
for (let i = 0; i < drugsRaw.length; i++) {
|
||||
const d = drugsRaw[i] || {};
|
||||
const name = String(d.name || d.drug_name || '').trim();
|
||||
const num = d.number != null ? d.number : (d.quantity != null ? d.quantity : '');
|
||||
const unit = d.unit || 'g';
|
||||
drugs.push({
|
||||
name: name || '未命名',
|
||||
qty: num !== '' ? (String(num) + unit) : '',
|
||||
usage: d.usage || '',
|
||||
});
|
||||
}
|
||||
const sexMap = { 1: '男', 2: '女' };
|
||||
const sexNum = Number(o.sex || o.patient_sex || 0);
|
||||
const dosage = Number(o.dosage || 0);
|
||||
const dayDosage = Number(o.day_dosage || 0);
|
||||
let dosageText = '';
|
||||
if (dosage > 0 || dayDosage > 0) {
|
||||
dosageText = (dosage > 0 ? dosage + '剂' : '') + (dayDosage > 0 ? ' 每日' + dayDosage + '次' : '');
|
||||
}
|
||||
// 抬头必须是上传诊所名(后端 store_name),禁止「传方处方」占位
|
||||
const storeName = String(o.store_name || o.storeName || '').trim();
|
||||
return {
|
||||
storeName: storeName || '未知诊所',
|
||||
sealImage: this.normalizeSignUrl(o.store_seal || o.storeSeal || o.offical_seal || ''),
|
||||
createdAt: o.created_at || '',
|
||||
patientName: o.patient_name || '—',
|
||||
patientSex: sexMap[sexNum] || '未知',
|
||||
patientAge: o.age != null && o.age !== '' ? (o.age + '岁') : '—',
|
||||
diagnose: o.clinical_diagnose || '',
|
||||
doctorOrder: o.doctor_order || o.medical_advice || '',
|
||||
process: o.process_rule_label || '',
|
||||
doctorName: o.doctor_name || '',
|
||||
// 优先电子签名图;无图才回落文字姓名
|
||||
doctorSignSrc: this.normalizeSignUrl(
|
||||
o.doctor_sign_image
|
||||
|| (o.doctor_info && o.doctor_info.identity_info && o.doctor_info.identity_info.sign_image)
|
||||
|| ''
|
||||
),
|
||||
dosageText,
|
||||
drugs,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(v) {
|
||||
if (v) {
|
||||
this.open();
|
||||
} else {
|
||||
this.onClose();
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.clearReleaseTimer();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 签名/公章图:兼容 http(s) 与纯 base64(对齐处方详情 checkImageUrl)
|
||||
*/
|
||||
normalizeSignUrl(url) {
|
||||
const raw = String(url || '').trim();
|
||||
if (!raw) return '';
|
||||
// 含 http 视为可直链(含 https、协议相对 //cdn)
|
||||
if (raw.indexOf('http') !== -1 || raw.indexOf('data:') === 0) {
|
||||
if (raw.indexOf('//') === 0) return 'https:' + raw;
|
||||
return raw;
|
||||
}
|
||||
return 'data:image/jpeg;base64,' + raw;
|
||||
},
|
||||
/** 解析 drugs_json 字符串,失败返回空数组 */
|
||||
parseDrugsJson(raw) {
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
},
|
||||
/** 打开处方笺:先挂 pageGuard 再下一帧 show */
|
||||
open() {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = true;
|
||||
this.$nextTick(() => {
|
||||
this.visible = true;
|
||||
});
|
||||
},
|
||||
/** 关闭:先离场再卸挂载 */
|
||||
onClose() {
|
||||
this.visible = false;
|
||||
this.$emit('input', false);
|
||||
this.scheduleReleaseGuard();
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.pageGuard = false;
|
||||
},
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer();
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false;
|
||||
this.releaseTimer = null;
|
||||
}, 350);
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer);
|
||||
this.releaseTimer = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rx-drawer {
|
||||
height: 85vh;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
border-radius: 24rpx 24rpx 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.rx-drawer-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 28rpx 32rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.rx-drawer-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1d2129;
|
||||
}
|
||||
.rx-drawer-close {
|
||||
font-size: 28rpx;
|
||||
color: #6acdbb;
|
||||
}
|
||||
.popup-scroll {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
.popup-loading {
|
||||
text-align: center;
|
||||
color: #999;
|
||||
padding: 80rpx;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
padding: 0 20rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
.bg {
|
||||
min-height: 400rpx;
|
||||
}
|
||||
.head {
|
||||
.head_record {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
image {
|
||||
width: 160rpx;
|
||||
height: 150rpx;
|
||||
position: absolute;
|
||||
left: 40%;
|
||||
top: -8rpx;
|
||||
opacity: 0.2;
|
||||
z-index: 0;
|
||||
}
|
||||
.record_yard {
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
.yard {
|
||||
font-size: 46rpx;
|
||||
font-weight: 400;
|
||||
color: #31353d;
|
||||
}
|
||||
.state {
|
||||
height: 44rpx;
|
||||
font-size: 32rpx;
|
||||
color: #31353d;
|
||||
line-height: 38rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
.head_record_time {
|
||||
text-align: right;
|
||||
margin-top: 20rpx;
|
||||
width: 100%;
|
||||
font-size: 24rpx;
|
||||
color: #6c7380;
|
||||
line-height: 28rpx;
|
||||
}
|
||||
}
|
||||
.my_info {
|
||||
width: 100%;
|
||||
margin: 4rpx auto;
|
||||
border-top: 1rpx solid #31353d;
|
||||
border-bottom: 1rpx solid #31353d;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 4rpx;
|
||||
.info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
.info_item {
|
||||
.names,
|
||||
.name {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 28rpx;
|
||||
color: #6c7380;
|
||||
text {
|
||||
flex: 1;
|
||||
display: block;
|
||||
height: 40rpx;
|
||||
font-size: 28rpx;
|
||||
color: #31353d;
|
||||
line-height: 40rpx;
|
||||
margin: 0 26rpx 0 6rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.my_medical {
|
||||
width: 100%;
|
||||
margin: 4rpx auto;
|
||||
padding: 1rpx 10rpx;
|
||||
box-sizing: border-box;
|
||||
.title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #31353d;
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
.item {
|
||||
width: 100%;
|
||||
padding: 1rpx 0;
|
||||
.name {
|
||||
font-size: 28rpx;
|
||||
color: #31353d;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
._name {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 2rpx 80rpx 2rpx 0;
|
||||
.text {
|
||||
margin-right: 16rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
._abbr {
|
||||
color: #a7abb0;
|
||||
font-size: 20rpx;
|
||||
margin-top: 2rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.details {
|
||||
font-size: 28rpx;
|
||||
color: #6c7380;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
.doctor_order {
|
||||
width: 100%;
|
||||
margin: 2rpx auto;
|
||||
padding: 0 10rpx;
|
||||
box-sizing: border-box;
|
||||
.yz {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
color: #6c7380;
|
||||
.order_info_left {
|
||||
margin-right: 3rpx;
|
||||
}
|
||||
.order_info {
|
||||
font-size: 28rpx;
|
||||
line-height: 40rpx;
|
||||
margin-bottom: 2rpx;
|
||||
}
|
||||
}
|
||||
.titles {
|
||||
width: 100%;
|
||||
font-size: 24rpx;
|
||||
color: #a7abb0;
|
||||
margin: 8rpx 0 20rpx;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
.my_doctor {
|
||||
width: 100%;
|
||||
margin: 2rpx auto;
|
||||
border-top: 1rpx solid #31353d;
|
||||
border-bottom: 1rpx solid #31353d;
|
||||
padding: 2rpx 4rpx;
|
||||
box-sizing: border-box;
|
||||
.doctor_info {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
.info {
|
||||
min-width: 222rpx;
|
||||
margin: 5rpx 0 8rpx;
|
||||
text {
|
||||
font-size: 24rpx;
|
||||
color: #31353d;
|
||||
}
|
||||
.name {
|
||||
display: inline-block;
|
||||
width: 106rpx;
|
||||
font-size: 32rpx;
|
||||
color: #31353d;
|
||||
line-height: 38rpx;
|
||||
border-bottom: 2rpx solid #000000;
|
||||
margin-left: 16rpx;
|
||||
text-align: center;
|
||||
min-height: 38rpx;
|
||||
}
|
||||
.sign-img {
|
||||
width: 82rpx;
|
||||
height: 62rpx;
|
||||
vertical-align: middle;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1808
subPackages/sub_image_transfer/order-detail.vue
Normal file
1808
subPackages/sub_image_transfer/order-detail.vue
Normal file
File diff suppressed because it is too large
Load Diff
847
subPackages/sub_image_transfer/order-list.vue
Normal file
847
subPackages/sub_image_transfer/order-list.vue
Normal file
@@ -0,0 +1,847 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部:数量 + 识图入口(液态玻璃条) -->
|
||||
<view class="topbar glass">
|
||||
<view class="topbar-info">
|
||||
<text class="topbar-num">{{ items.length }}</text>
|
||||
<text class="topbar-desc">{{ topbarDesc }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-if="scope !== 'all'"
|
||||
class="upload-btn"
|
||||
hover-class="hover-dim"
|
||||
@click="goUpload"
|
||||
>
|
||||
<u-icon name="camera-fill" size="28" color="#FFFFFF"></u-icon>
|
||||
<text class="upload-text">识图传方</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 状态 Tab:点击 + 下方 swiper 左右滑双向同步,本地记忆上次选中 -->
|
||||
<view class="tabs-wrap glass">
|
||||
<scroll-view
|
||||
class="tabs-scroll"
|
||||
scroll-x
|
||||
:scroll-into-view="activeTabSid"
|
||||
:show-scrollbar="false"
|
||||
scroll-with-animation
|
||||
>
|
||||
<view class="tabs-row">
|
||||
<view
|
||||
v-for="(tab, idx) in tabs"
|
||||
:id="tab.sid"
|
||||
:key="tab.value"
|
||||
class="tab"
|
||||
:class="{ on: statusIndex === idx }"
|
||||
@click="onTabClick(idx)"
|
||||
>{{ tab.label }}</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 左右滑切换状态;内层 scroll-view 负责下拉刷新 -->
|
||||
<swiper
|
||||
class="list-swiper"
|
||||
:current="statusIndex"
|
||||
:disable-touch="tabs.length < 2"
|
||||
@change="onSwiperChange"
|
||||
>
|
||||
<swiper-item v-for="(tab, idx) in tabs" :key="tab.value">
|
||||
<scroll-view
|
||||
scroll-y
|
||||
class="list-scroll"
|
||||
refresher-enabled
|
||||
:refresher-triggered="isRefreshing"
|
||||
@refresherrefresh="onRefresherRefresh"
|
||||
>
|
||||
<block v-if="statusIndex === idx">
|
||||
<!-- 骨架:首屏加载且无数据时避免空态闪一下 -->
|
||||
<view v-if="loading && !items.length" class="list-pad">
|
||||
<view v-for="n in 3" :key="n" class="sk-card glass">
|
||||
<view class="sk-avatar"></view>
|
||||
<view class="sk-body">
|
||||
<view class="sk-line sk-60"></view>
|
||||
<view class="sk-line sk-90"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-else-if="!items.length" class="empty">
|
||||
<d-empty text="暂无传方订单" />
|
||||
<view
|
||||
v-if="scope !== 'all'"
|
||||
class="empty-btn"
|
||||
hover-class="hover-dim"
|
||||
@click="goUpload"
|
||||
>去识图传方</view>
|
||||
</view>
|
||||
|
||||
<view v-else class="list-pad">
|
||||
<view
|
||||
v-for="row in items"
|
||||
:key="row.id"
|
||||
class="order-card glass"
|
||||
hover-class="card-press"
|
||||
@click="openDetail(row)"
|
||||
>
|
||||
<view class="card-head">
|
||||
<view class="avatar">
|
||||
<text class="avatar-text">{{ avatarChar(row) }}</text>
|
||||
</view>
|
||||
<view class="head-main">
|
||||
<view class="title-row">
|
||||
<text class="title">{{ row.patient_name || '未填姓名' }}</text>
|
||||
<view class="st-pill" :style="pillStyle(row.status)">
|
||||
<view class="st-dot" :style="{ backgroundColor: statusMeta(row.status).color }"></view>
|
||||
<text class="st-text" :style="{ color: statusMeta(row.status).color }">{{ statusText(row) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="meta">{{ metaText(row) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="row.reject_reason" class="alert">
|
||||
<u-icon name="close-circle-fill" size="28" color="#FA3534"></u-icon>
|
||||
<text class="alert-text">{{ row.reject_reason }}</text>
|
||||
</view>
|
||||
|
||||
<view v-if="drugPreview(row).length" class="chips">
|
||||
<view v-for="(name, di) in drugPreview(row)" :key="di" class="chip">{{ name }}</view>
|
||||
<text v-if="drugRest(row)" class="chip-more">共 {{ row.drug_count }} 味</text>
|
||||
</view>
|
||||
<text v-else-if="row.drug_count" class="drug-count">共 {{ row.drug_count }} 味药</text>
|
||||
|
||||
<view v-if="row.mes_message" class="mes">
|
||||
<u-icon name="info-circle" size="26" color="#9AA4AE"></u-icon>
|
||||
<text class="mes-text">{{ row.mes_message }}</text>
|
||||
</view>
|
||||
|
||||
<view class="card-foot">
|
||||
<view class="foot-left">
|
||||
<text class="time">{{ row.created_at }}</text>
|
||||
<text v-if="showRowTotal(row)" class="foot-price">合计 ¥{{ row.total_price }}</text>
|
||||
</view>
|
||||
<view
|
||||
v-for="(act, ai) in actionsOf(row)"
|
||||
:key="ai"
|
||||
class="act-btn"
|
||||
:class="act.type === 'primary' ? 'act-primary' : 'act-ghost'"
|
||||
hover-class="hover-dim"
|
||||
@click.stop="onAction(act, row)"
|
||||
>{{ act.text }}</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view v-else class="swiper-placeholder"></view>
|
||||
</scroll-view>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<!-- 平台审方驳回:文本域抽屉 -->
|
||||
<RejectReasonDrawer
|
||||
v-model="showRejectDrawer"
|
||||
:submitting="actionLock"
|
||||
@confirm="submitReject"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 传方订单列表
|
||||
* scope=mine 医生只看自己;scope=store 诊所管理员看本店;scope=all 平台审方全量
|
||||
* Tab:点击 / 左右滑双向同步,并按 scope 本地记忆上次选中状态
|
||||
* 卡片:苹果液态玻璃风格,对齐识图抽屉 ocr-glass 视觉语言
|
||||
*/
|
||||
import { unwrapXkApi } from '@/utils/api-response.js';
|
||||
import {
|
||||
listMyImageTransferOrders,
|
||||
listStoreImageTransferOrders,
|
||||
listPlatformImageTransferOrders,
|
||||
approvePlatformImageTransferOrder,
|
||||
rejectPlatformImageTransferOrder,
|
||||
retryPlatformImageTransferOrder,
|
||||
} from '@/api/imageTransferOrder.js';
|
||||
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js';
|
||||
import { transferStatusMeta, transferPillStyle, transferStatusText } from './common/display.js';
|
||||
import RejectReasonDrawer from './components/RejectReasonDrawer.vue';
|
||||
|
||||
/** 本地 Tab 记忆前缀:mine / store / all 分开记,避免各端互相覆盖 */
|
||||
const TAB_STORAGE_PREFIX = 'image_transfer_order_status_tab_';
|
||||
|
||||
export default {
|
||||
mixins: [pullRefreshMixin],
|
||||
components: { RejectReasonDrawer },
|
||||
data() {
|
||||
return {
|
||||
scope: 'mine',
|
||||
statusIndex: 0,
|
||||
items: [],
|
||||
loading: true,
|
||||
/** 防止 tabs 与 swiper 互相触发形成回路(对齐商品订单列表) */
|
||||
tabChanging: false,
|
||||
/** 平台审方操作防重复 */
|
||||
actionLock: false,
|
||||
showRejectDrawer: false,
|
||||
/** 当前待驳回的订单 id */
|
||||
rejectTargetId: 0,
|
||||
tabs: [
|
||||
{ label: '全部', value: 0, sid: 'tab0' },
|
||||
{ label: '待审', value: 1, sid: 'tab1' },
|
||||
{ label: '驳回', value: 2, sid: 'tab2' },
|
||||
{ label: '已传', value: 3, sid: 'tab3' },
|
||||
{ label: '失败', value: 4, sid: 'tab4' },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
/** 当前选中状态值:接口 status 筛选用 */
|
||||
status() {
|
||||
const hit = this.tabs[this.statusIndex];
|
||||
return hit ? hit.value : 0;
|
||||
},
|
||||
/** 当前 Tab 的 scroll-into-view 锚点(微信模板禁止 'tab-'+idx 拼接) */
|
||||
activeTabSid() {
|
||||
const hit = this.tabs[this.statusIndex];
|
||||
return hit ? hit.sid : 'tab0';
|
||||
},
|
||||
topbarDesc() {
|
||||
const hit = this.tabs[this.statusIndex];
|
||||
return `条${hit && hit.value !== 0 ? hit.label : '记录'}`;
|
||||
},
|
||||
tabStorageKey() {
|
||||
return TAB_STORAGE_PREFIX + (this.scope || 'mine');
|
||||
},
|
||||
},
|
||||
onLoad(query) {
|
||||
// store=本店,all=平台全量,其余默认医生本人
|
||||
if (query && query.scope === 'store') {
|
||||
this.scope = 'store';
|
||||
} else if (query && query.scope === 'all') {
|
||||
this.scope = 'all';
|
||||
} else {
|
||||
this.scope = 'mine';
|
||||
}
|
||||
this.statusIndex = this.readStoredIndex();
|
||||
},
|
||||
onShow() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 同步读本地 Tab 下标:避免先渲染「全部」再跳回,把下拉刷新冲掉
|
||||
* 存的是 status value,再反查下标,避免 tabs 顺序调整后错位
|
||||
*/
|
||||
readStoredIndex() {
|
||||
const stored = Number(uni.getStorageSync(this.tabStorageKey));
|
||||
const idx = this.tabs.findIndex((tab) => tab.value === stored);
|
||||
return idx >= 0 ? idx : 0;
|
||||
},
|
||||
/** 记住当前 Tab,下次进页停在同一状态 */
|
||||
persistTab(idx) {
|
||||
const hit = this.tabs[idx];
|
||||
if (!hit) return;
|
||||
uni.setStorageSync(this.tabStorageKey, hit.value);
|
||||
},
|
||||
statusMeta(status) {
|
||||
return transferStatusMeta(status);
|
||||
},
|
||||
pillStyle(status) {
|
||||
return transferPillStyle(status);
|
||||
},
|
||||
statusText(row) {
|
||||
return transferStatusText(row);
|
||||
},
|
||||
avatarChar(row) {
|
||||
const name = (row && row.patient_name) || '';
|
||||
return name ? name.slice(0, 1) : '患';
|
||||
},
|
||||
metaText(row) {
|
||||
const doctor = row.doctor_name ? ('医生·' + row.doctor_name) : '';
|
||||
return [row.patient_mobile, doctor, row.uploader_role_text].filter(Boolean).join(' · ');
|
||||
},
|
||||
drugPreview(row) {
|
||||
return Array.isArray(row.drug_preview) ? row.drug_preview.slice(0, 4) : [];
|
||||
},
|
||||
drugRest(row) {
|
||||
const shown = this.drugPreview(row).length;
|
||||
return Number(row.drug_count || 0) > shown ? Number(row.drug_count) : 0;
|
||||
},
|
||||
/**
|
||||
* 列表合计:白名单门店 can_view_price=1 且有 total_price 才展示,缺字段不崩
|
||||
*/
|
||||
showRowTotal(row) {
|
||||
if (!row) return false;
|
||||
if (Number(row.can_view_price) !== 1) return false;
|
||||
return row.total_price != null && row.total_price !== '';
|
||||
},
|
||||
/**
|
||||
* 卡片主操作:
|
||||
* - 平台 scope=all:待审通过/驳回,失败可重试,其余看详情
|
||||
* - 医生/诊所:待审或驳回可改(直达详情编辑态),其余只看详情
|
||||
*/
|
||||
actionsOf(row) {
|
||||
const s = Number(row.status);
|
||||
if (this.scope === 'all') {
|
||||
if (s === 1) {
|
||||
return [
|
||||
{ key: 'approve', text: '通过', type: 'primary' },
|
||||
{ key: 'reject', text: '驳回', type: 'ghost' },
|
||||
];
|
||||
}
|
||||
if (s === 4) {
|
||||
return [
|
||||
{ key: 'retry', text: '重试', type: 'primary' },
|
||||
{ key: 'detail', text: '查看详情', type: 'ghost' },
|
||||
];
|
||||
}
|
||||
return [{ key: 'detail', text: '查看详情', type: 'ghost' }];
|
||||
}
|
||||
if (s === 1 || s === 2) {
|
||||
return [{ key: 'resubmit', text: s === 2 ? '修改后再交' : '修改', type: 'primary' }];
|
||||
}
|
||||
return [{ key: 'detail', text: '查看详情', type: 'ghost' }];
|
||||
},
|
||||
onAction(act, row) {
|
||||
if (!act || !row) return;
|
||||
if (act.key === 'approve') {
|
||||
this.confirmApprove(row);
|
||||
return;
|
||||
}
|
||||
if (act.key === 'reject') {
|
||||
this.confirmReject(row);
|
||||
return;
|
||||
}
|
||||
if (act.key === 'retry') {
|
||||
this.confirmRetry(row);
|
||||
return;
|
||||
}
|
||||
this.openDetail(row, act.key === 'resubmit');
|
||||
},
|
||||
/** 平台审方通过:二次确认后调 approve */
|
||||
confirmApprove(row) {
|
||||
if (this.actionLock) return;
|
||||
uni.showModal({
|
||||
title: '确认通过',
|
||||
content: '通过后将推送传方,是否继续?',
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return;
|
||||
this.actionLock = true;
|
||||
uni.showLoading({ title: '处理中' });
|
||||
try {
|
||||
const res = await approvePlatformImageTransferOrder({ id: row.id });
|
||||
const parsed = unwrapXkApi(res);
|
||||
if (!parsed.ok) {
|
||||
uni.showToast({ title: parsed.message || '操作失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showToast({ title: '已通过', icon: 'none' });
|
||||
this.load();
|
||||
} catch (e) {
|
||||
uni.showToast({ title: (e && e.message) || '操作失败', icon: 'none' });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
this.actionLock = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
/** 平台审方驳回:打开文本域抽屉 */
|
||||
confirmReject(row) {
|
||||
if (this.actionLock || !row || !row.id) return;
|
||||
this.rejectTargetId = Number(row.id);
|
||||
this.showRejectDrawer = true;
|
||||
},
|
||||
/**
|
||||
* 抽屉确认驳回
|
||||
*/
|
||||
async submitReject(reason) {
|
||||
const text = String(reason || '').trim();
|
||||
const id = Number(this.rejectTargetId || 0);
|
||||
if (!text || !id || this.actionLock) return;
|
||||
this.actionLock = true;
|
||||
try {
|
||||
const res = await rejectPlatformImageTransferOrder({ id, reason: text });
|
||||
const parsed = unwrapXkApi(res);
|
||||
if (!parsed.ok) {
|
||||
uni.showToast({ title: parsed.message || '操作失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
this.showRejectDrawer = false;
|
||||
this.rejectTargetId = 0;
|
||||
uni.showToast({ title: '已驳回', icon: 'none' });
|
||||
this.load();
|
||||
} catch (e) {
|
||||
uni.showToast({ title: (e && e.message) || '操作失败', icon: 'none' });
|
||||
} finally {
|
||||
this.actionLock = false;
|
||||
}
|
||||
},
|
||||
/** 平台推送失败重试 */
|
||||
confirmRetry(row) {
|
||||
if (this.actionLock) return;
|
||||
uni.showModal({
|
||||
title: '确认重试',
|
||||
content: '将重新推送该传方,是否继续?',
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return;
|
||||
this.actionLock = true;
|
||||
uni.showLoading({ title: '处理中' });
|
||||
try {
|
||||
const res = await retryPlatformImageTransferOrder({ id: row.id });
|
||||
const parsed = unwrapXkApi(res);
|
||||
if (!parsed.ok) {
|
||||
uni.showToast({ title: parsed.message || '操作失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showToast({ title: '已重试', icon: 'none' });
|
||||
this.load();
|
||||
} catch (e) {
|
||||
uni.showToast({ title: (e && e.message) || '操作失败', icon: 'none' });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
this.actionLock = false;
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
/** Tab 点击:同步 swiper + 记本地 + 重拉 */
|
||||
onTabClick(idx) {
|
||||
if (this.tabChanging) return;
|
||||
this.tabChanging = true;
|
||||
const next = Number(idx);
|
||||
if (!Number.isNaN(next) && next !== this.statusIndex) {
|
||||
this.statusIndex = next;
|
||||
this.persistTab(next);
|
||||
this.load();
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.tabChanging = false;
|
||||
});
|
||||
},
|
||||
/** 左右滑切换状态:与 Tab 双向同步 */
|
||||
onSwiperChange(e) {
|
||||
if (this.tabChanging) return;
|
||||
this.tabChanging = true;
|
||||
const idx = Number((e.detail && e.detail.current) || 0);
|
||||
if (idx !== this.statusIndex) {
|
||||
this.statusIndex = idx;
|
||||
this.persistTab(idx);
|
||||
this.load();
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.tabChanging = false;
|
||||
});
|
||||
},
|
||||
reload() {
|
||||
return this.load();
|
||||
},
|
||||
async load() {
|
||||
let api = listMyImageTransferOrders;
|
||||
if (this.scope === 'all') {
|
||||
api = listPlatformImageTransferOrders;
|
||||
} else if (this.scope === 'store') {
|
||||
api = listStoreImageTransferOrders;
|
||||
}
|
||||
this.loading = true;
|
||||
try {
|
||||
const res = await api({ page: 1, page_size: 30, status: this.status });
|
||||
const parsed = unwrapXkApi(res);
|
||||
const data = parsed.payload || {};
|
||||
this.items = parsed.ok && data.items ? data.items : [];
|
||||
} catch (e) {
|
||||
this.items = [];
|
||||
} finally {
|
||||
this.loading = false;
|
||||
this.finishRefresh();
|
||||
}
|
||||
},
|
||||
openDetail(row, edit) {
|
||||
uni.navigateTo({
|
||||
url:
|
||||
'/subPackages/sub_image_transfer/order-detail?id=' +
|
||||
row.id +
|
||||
'&scope=' +
|
||||
this.scope +
|
||||
(edit ? '&edit=1' : ''),
|
||||
});
|
||||
},
|
||||
/** 识图入口:平台审方列表不提供上传 */
|
||||
goUpload() {
|
||||
if (this.scope === 'all') {
|
||||
uni.showToast({ title: '平台审方请在列表操作', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const url = this.scope === 'store'
|
||||
? '/subPackages/sub_image_transfer/upload'
|
||||
: '/subPackages/sub_workbench/image-transfer-prescription/index';
|
||||
uni.navigateTo({ url });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 液态玻璃:半透明白 + 青绿描边微光 + blur,对齐识图抽屉 ocr-glass */
|
||||
$glass-bg: rgba(255, 255, 255, 0.55);
|
||||
$glass-border: rgba(106, 205, 187, 0.28);
|
||||
$glass-glow: 0 0 16rpx rgba(106, 205, 187, 0.14), inset 0 1px 0 rgba(255, 255, 255, 0.7);
|
||||
$primary: #6acdbb;
|
||||
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
/* 青绿到浅灰渐变垫底,玻璃层才看得出通透感 */
|
||||
background: linear-gradient(180deg, #e8f6f4 0%, #f4f6f8 42%, #eef2f5 100%);
|
||||
}
|
||||
|
||||
.hover-dim {
|
||||
opacity: 0.84;
|
||||
}
|
||||
|
||||
.glass {
|
||||
background: $glass-bg;
|
||||
border: 1px solid $glass-border;
|
||||
box-shadow: $glass-glow;
|
||||
backdrop-filter: blur(22px);
|
||||
}
|
||||
|
||||
/* ===== 概览条 ===== */
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 16rpx 24rpx 0;
|
||||
padding: 20rpx 28rpx;
|
||||
border-radius: 28rpx;
|
||||
}
|
||||
.topbar-info {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
}
|
||||
.topbar-num {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #16302c;
|
||||
line-height: 1;
|
||||
}
|
||||
.topbar-desc {
|
||||
margin-left: 10rpx;
|
||||
font-size: 24rpx;
|
||||
color: #7a8794;
|
||||
}
|
||||
.upload-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #5ec8b4 0%, #6acdbb 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(106, 205, 187, 0.32);
|
||||
}
|
||||
.upload-text {
|
||||
margin-left: 8rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* ===== Tab ===== */
|
||||
.tabs-wrap {
|
||||
margin: 16rpx 24rpx 0;
|
||||
padding: 12rpx 8rpx;
|
||||
border-radius: 999rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.tabs-scroll {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.tabs-row {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
.tab {
|
||||
display: inline-block;
|
||||
padding: 12rpx 28rpx;
|
||||
margin: 0 4rpx;
|
||||
font-size: 26rpx;
|
||||
color: #667085;
|
||||
border-radius: 999rpx;
|
||||
transition: all 0.18s;
|
||||
}
|
||||
.tab.on {
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
background: linear-gradient(135deg, #5ec8b4 0%, #6acdbb 100%);
|
||||
box-shadow: 0 6rpx 14rpx rgba(106, 205, 187, 0.32);
|
||||
}
|
||||
|
||||
/* ===== Swiper 列表区 ===== */
|
||||
.list-swiper {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.list-scroll {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.list-pad {
|
||||
padding: 12rpx 24rpx calc(40rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
.swiper-placeholder {
|
||||
min-height: 200rpx;
|
||||
}
|
||||
|
||||
/* ===== 订单玻璃卡 ===== */
|
||||
.order-card {
|
||||
padding: 28rpx 24rpx 22rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 28rpx;
|
||||
}
|
||||
.card-press {
|
||||
opacity: 0.92;
|
||||
transform: translateY(2rpx);
|
||||
}
|
||||
.card-head {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
background: rgba(106, 205, 187, 0.18);
|
||||
border: 1px solid rgba(106, 205, 187, 0.35);
|
||||
}
|
||||
.avatar-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #008771;
|
||||
}
|
||||
.head-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #16302c;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.meta {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: #7a8794;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.st-pill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
border: 1rpx solid transparent;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.st-dot {
|
||||
width: 10rpx;
|
||||
height: 10rpx;
|
||||
margin-right: 8rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.st-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.alert {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 16rpx;
|
||||
padding: 14rpx 18rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(254, 240, 240, 0.85);
|
||||
border: 1px solid rgba(250, 53, 52, 0.18);
|
||||
}
|
||||
.alert-text {
|
||||
flex: 1;
|
||||
margin-left: 8rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
color: #d92d20;
|
||||
}
|
||||
|
||||
.chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 14rpx;
|
||||
}
|
||||
.chip {
|
||||
padding: 4rpx 16rpx;
|
||||
margin: 0 10rpx 10rpx 0;
|
||||
font-size: 22rpx;
|
||||
color: #3faf9c;
|
||||
background: rgba(240, 249, 247, 0.9);
|
||||
border: 1px solid rgba(106, 205, 187, 0.28);
|
||||
border-radius: 999rpx;
|
||||
box-shadow: 0 0 6rpx rgba(106, 205, 187, 0.12);
|
||||
}
|
||||
.chip-more {
|
||||
font-size: 22rpx;
|
||||
line-height: 1.8;
|
||||
color: #98a2b3;
|
||||
}
|
||||
.drug-count {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.mes {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
.mes-text {
|
||||
flex: 1;
|
||||
margin-left: 6rpx;
|
||||
font-size: 24rpx;
|
||||
line-height: 1.5;
|
||||
color: #98a2b3;
|
||||
}
|
||||
|
||||
.card-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16rpx;
|
||||
padding-top: 16rpx;
|
||||
border-top: 1px solid rgba(106, 205, 187, 0.16);
|
||||
}
|
||||
.foot-left {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.time {
|
||||
font-size: 22rpx;
|
||||
color: #98a2b3;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.foot-price {
|
||||
margin-top: 4rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: #008771;
|
||||
}
|
||||
.act-btn {
|
||||
flex-shrink: 0;
|
||||
margin-left: 12rpx;
|
||||
padding: 10rpx 28rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.act-primary {
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #5ec8b4 0%, #6acdbb 100%);
|
||||
box-shadow: 0 6rpx 14rpx rgba(106, 205, 187, 0.28);
|
||||
}
|
||||
.act-ghost {
|
||||
color: #008771;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px solid rgba(106, 205, 187, 0.35);
|
||||
}
|
||||
|
||||
/* ===== 骨架 ===== */
|
||||
.sk-card {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 28rpx 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 28rpx;
|
||||
}
|
||||
.sk-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sk-body {
|
||||
flex: 1;
|
||||
padding-top: 6rpx;
|
||||
}
|
||||
.sk-line {
|
||||
height: 24rpx;
|
||||
margin-bottom: 18rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
.sk-60 {
|
||||
width: 55%;
|
||||
}
|
||||
.sk-90 {
|
||||
width: 85%;
|
||||
}
|
||||
.sk-avatar,
|
||||
.sk-line {
|
||||
background: linear-gradient(90deg, rgba(238, 241, 243, 0.8) 25%, rgba(247, 249, 250, 0.95) 37%, rgba(238, 241, 243, 0.8) 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: sk-shine 1.4s ease infinite;
|
||||
}
|
||||
@keyframes sk-shine {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 空态 ===== */
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0 0;
|
||||
}
|
||||
.empty-btn {
|
||||
margin-top: 8rpx;
|
||||
padding: 20rpx 56rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #5ec8b4 0%, #6acdbb 100%);
|
||||
box-shadow: 0 8rpx 18rpx rgba(106, 205, 187, 0.32);
|
||||
}
|
||||
</style>
|
||||
271
subPackages/sub_image_transfer/upload.vue
Normal file
271
subPackages/sub_image_transfer/upload.vue
Normal file
@@ -0,0 +1,271 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 入口态:onReady 会立刻拉起识图抽屉,这一屏只在唤起间隙与抽屉开启失败时可见,
|
||||
所以做成品牌化的落地页而不是一行「正在打开…」 -->
|
||||
<view v-if="!showOcr" class="gate">
|
||||
<view class="gate-badge">
|
||||
<u-icon name="camera-fill" size="76" color="#FFFFFF"></u-icon>
|
||||
</view>
|
||||
<text class="gate-title">识图传方</text>
|
||||
<text class="gate-desc">拍摄或上传处方图片,自动识别患者与药味,核对无误后即可提交传方</text>
|
||||
|
||||
<view class="gate-btn" hover-class="hover-dim" @click="openOcr">
|
||||
<text class="gate-btn-text">上传处方图识别</text>
|
||||
<u-icon name="arrow-right" size="28" color="#FFFFFF"></u-icon>
|
||||
</view>
|
||||
|
||||
<view class="gate-steps">
|
||||
<view v-for="(step, idx) in steps" :key="idx" class="step">
|
||||
<view class="step-idx">
|
||||
<text class="step-idx-text">{{ idx + 1 }}</text>
|
||||
</view>
|
||||
<text class="step-text">{{ step }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<ImageOcrDrawer
|
||||
v-model="showOcr"
|
||||
mode="transfer"
|
||||
chrome-mode="embedded"
|
||||
:store-id="storeId"
|
||||
:ocr-api="ocrApi"
|
||||
:search-api="searchApi"
|
||||
:history-list-api="historyListApi"
|
||||
:history-detail-api="historyDetailApi"
|
||||
:match-patient-api="matchPatientApi"
|
||||
:quote-api="quoteApi"
|
||||
:process-rule-list-api="processRuleListApi"
|
||||
:require-doctor-select="true"
|
||||
:doctor-list-api="doctorListApi"
|
||||
@confirm-transfer="onConfirmTransfer"
|
||||
@closed="flushPendingNav"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 诊所管理员识图传方上传
|
||||
* 和医生共用识别抽屉,接口按诊所管理员身份
|
||||
*/
|
||||
import ImageOcrDrawer from '@/subPackages/sub_workbench/prescription_v2/components/ImageOcrDrawer.vue';
|
||||
import {
|
||||
clinicConfirmImageTransfer,
|
||||
clinicOcrPrescriptionImage,
|
||||
clinicSearchTransferDrugs,
|
||||
clinicAiListGenerations,
|
||||
clinicAiGenerationDetail,
|
||||
clinicMatchTransferPatient,
|
||||
clinicProcessRuleList,
|
||||
clinicTransferDoctorOption,
|
||||
quoteStoreImageTransferOrder,
|
||||
} from '@/api/imageTransferOrder.js';
|
||||
import { unwrapXkApi } from '@/utils/api-response.js';
|
||||
|
||||
export default {
|
||||
name: 'ImageTransferUpload',
|
||||
components: { ImageOcrDrawer },
|
||||
data() {
|
||||
return {
|
||||
showOcr: false,
|
||||
storeId: 0,
|
||||
ocrApi: clinicOcrPrescriptionImage,
|
||||
searchApi: clinicSearchTransferDrugs,
|
||||
historyListApi: clinicAiListGenerations,
|
||||
historyDetailApi: clinicAiGenerationDetail,
|
||||
/** 诊所端就诊人匹配,注入抽屉后走 clinic 接口而非医生默认 */
|
||||
matchPatientApi: clinicMatchTransferPatient,
|
||||
/** 诊所报价 + 代煎级联 */
|
||||
quoteApi: quoteStoreImageTransferOrder,
|
||||
processRuleListApi: clinicProcessRuleList,
|
||||
/** 本店医生选项:诊所识图传方必须指定开方医生 */
|
||||
doctorListApi: clinicTransferDoctorOption,
|
||||
pendingNav: null,
|
||||
/** 入口页三步提示:让用户在抽屉出来前就知道流程,减少中途放弃 */
|
||||
steps: ['上传或拍摄处方原图', '核对识别出的患者与药味', '提交后进入审方传方'],
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
const user = uni.getStorageSync('clinic_admin_user') || {};
|
||||
this.storeId = Number(user.store_id || uni.getStorageSync('store_id') || 0);
|
||||
},
|
||||
onReady() {
|
||||
// 进页直接开抽屉,避免中间页卡片主题与识图层不一致
|
||||
this.openOcr();
|
||||
},
|
||||
methods: {
|
||||
openOcr() {
|
||||
this.showOcr = true;
|
||||
},
|
||||
flushPendingNav() {
|
||||
const fn = this.pendingNav;
|
||||
this.pendingNav = null;
|
||||
if (typeof fn === 'function') {
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
// 用户关抽屉且无跳转:退回上一页,避免留空白入口页
|
||||
uni.navigateBack({ fail: () => {} });
|
||||
},
|
||||
/**
|
||||
* 解包跨分包原生组件 emit 的参数。
|
||||
* ImageOcrDrawer 在 sub_workbench,本页跨分包引用时微信会把 $emit 包成原生事件,
|
||||
* 业务数据落在 detail.__args__[0];不解包会导致 ...payload 把事件对象当业务字段提交,
|
||||
* 顶层无 patient_name,后端报「请确认患者姓名和手机号」。
|
||||
*/
|
||||
unwrapComponentEventPayload(payload) {
|
||||
if (payload && payload.detail && Array.isArray(payload.detail.__args__)) {
|
||||
return payload.detail.__args__[0];
|
||||
}
|
||||
if (payload && Array.isArray(payload.__args__)) {
|
||||
return payload.__args__[0];
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
/** 确认识图传方:先解包再显式组字段,禁止 spread 整包事件 */
|
||||
async onConfirmTransfer(e) {
|
||||
const data = this.unwrapComponentEventPayload(e) || {};
|
||||
uni.showLoading({ title: '提交中' });
|
||||
try {
|
||||
// patient_mobile 优先 _txt:拦截器解密后可能写出明文,避免密文/脱敏串被再次加密
|
||||
const res = await clinicConfirmImageTransfer({
|
||||
patient_name: data.patient_name,
|
||||
patient_mobile: data.patient_mobile_txt || data.patient_mobile,
|
||||
patient_address: data.patient_address,
|
||||
sex: data.sex,
|
||||
age: data.age,
|
||||
user_patient_id: data.user_patient_id,
|
||||
doctor_id: data.doctor_id,
|
||||
clinical_diagnose: data.clinical_diagnose,
|
||||
medical_advice: data.medical_advice,
|
||||
doctor_order: data.doctor_order || data.medical_advice,
|
||||
dosage: data.dosage,
|
||||
day_dosage: data.day_dosage,
|
||||
image_urls: data.image_urls,
|
||||
drugs: data.drugs,
|
||||
rule_type: data.rule_type,
|
||||
process_rule_id: data.process_rule_id,
|
||||
child_process_rule_id: data.child_process_rule_id,
|
||||
process_rule_note_id: data.process_rule_note_id,
|
||||
package_method_id: data.package_method_id,
|
||||
store_id: this.storeId || undefined,
|
||||
});
|
||||
const parsed = unwrapXkApi(res);
|
||||
if (!parsed.ok || !parsed.payload) {
|
||||
uni.showToast({ title: parsed.message || '失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const result = parsed.payload;
|
||||
if (result.next === 'reception') {
|
||||
uni.showToast({ title: '当前配置需要医生进接诊', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.showToast({ title: result.status_text || '已提交', icon: 'none' });
|
||||
this.pendingNav = () => {
|
||||
uni.redirectTo({ url: '/subPackages/sub_image_transfer/order-list?scope=store' });
|
||||
};
|
||||
this.showOcr = false;
|
||||
} catch (err) {
|
||||
uni.showToast({ title: (err && err.message) || '失败', icon: 'none' });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
/* 顶部浅青绿渐变落到灰底:与识图抽屉的青绿主题衔接,避免切层闪白 */
|
||||
background: linear-gradient(180deg, #eaf8f4 0%, #f6f8f9 46%);
|
||||
}
|
||||
|
||||
.hover-dim {
|
||||
opacity: 0.84;
|
||||
}
|
||||
|
||||
.gate {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 140rpx 56rpx 0;
|
||||
}
|
||||
.gate-badge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 168rpx;
|
||||
height: 168rpx;
|
||||
border-radius: 48rpx;
|
||||
background: linear-gradient(135deg, #00bfa6 0%, #6acdbb 100%);
|
||||
box-shadow: 0 20rpx 44rpx rgba(106, 205, 187, 0.36);
|
||||
}
|
||||
.gate-title {
|
||||
margin-top: 40rpx;
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #1f2a33;
|
||||
}
|
||||
.gate-desc {
|
||||
margin-top: 20rpx;
|
||||
font-size: 27rpx;
|
||||
line-height: 1.7;
|
||||
color: #7a8794;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.gate-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 92rpx;
|
||||
margin-top: 64rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #00bfa6 0%, #6acdbb 100%);
|
||||
box-shadow: 0 14rpx 30rpx rgba(106, 205, 187, 0.34);
|
||||
}
|
||||
.gate-btn-text {
|
||||
margin-right: 6rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.gate-steps {
|
||||
width: 100%;
|
||||
margin-top: 72rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
background: #ffffff;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(17, 24, 28, 0.04);
|
||||
}
|
||||
.step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 14rpx 0;
|
||||
}
|
||||
.step-idx {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
margin-right: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: #f0f9f7;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.step-idx-text {
|
||||
font-size: 21rpx;
|
||||
font-weight: 600;
|
||||
color: #3faf9c;
|
||||
}
|
||||
.step-text {
|
||||
font-size: 25rpx;
|
||||
color: #667085;
|
||||
}
|
||||
</style>
|
||||
@@ -55,7 +55,12 @@ export function parseTransferPrescriptionContent(transferData) {
|
||||
clinical_diagnose: content.clinical_diagnose || orig.clinical_diagnose || '',
|
||||
doctor_order: content.doctor_order || orig.doctor_order || '',
|
||||
drugList,
|
||||
prescription_type: prescriptionType
|
||||
prescription_type: prescriptionType,
|
||||
dosage: Number(content.dosage || 0),
|
||||
day_dosage: Number(content.day_dosage || 0),
|
||||
process_rule_id: Number(content.process_rule_id || 0),
|
||||
child_process_rule_id: Number(content.child_process_rule_id || 0),
|
||||
process_rule_note_id: Number(content.process_rule_note_id || 0),
|
||||
};
|
||||
}
|
||||
if (Array.isArray(content)) {
|
||||
|
||||
@@ -102,7 +102,14 @@
|
||||
</view>
|
||||
<view class="m-t-16">
|
||||
<d-text text="药品:"></d-text>
|
||||
<u-tag v-for="(value,key) in item.p_drugs" v-if="key < 5" :text="value.toString()" style="margin: 10rpx; display: inline-block" type="info" />
|
||||
<u-tag
|
||||
v-for="(value, key) in item.p_drugs"
|
||||
v-if="key < 5"
|
||||
:key="key"
|
||||
:text="value.toString()"
|
||||
style="margin: 10rpx; display: inline-block"
|
||||
type="info"
|
||||
/>
|
||||
<d-text v-if="item.drugs_count >= 5" style="margin: 10rpx; display: inline-block" :text="'等,共:' + item.drugs_count+' 个'"></d-text>
|
||||
</view>
|
||||
<view class="flex-row flex-jus-sp flex-ali-centerc m-t-16">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<business-page-layout title="我的" :show-tabbar="true" :tab-index="1">
|
||||
<!-- tab 顺序是工作台/功能/我的,我的必须是 2;写成 1 时点「功能」会被当成当前页直接 return -->
|
||||
<business-page-layout title="我的" :show-tabbar="true" :tab-index="2">
|
||||
<!-- 页面主体统一收进 admin-my-panel(资料区 + 个人中心入口 + 菜单卡 + 退出) -->
|
||||
<admin-my-panel
|
||||
ref="myPanel"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<business-page-layout title="我的" :show-tabbar="true" :tab-index="1">
|
||||
<!-- tab 顺序是工作台/功能/我的,我的必须是 2;写成 1 时点「功能」会被当成当前页直接 return -->
|
||||
<business-page-layout title="我的" :show-tabbar="true" :tab-index="2">
|
||||
<!-- 页面主体统一收进 admin-my-panel(资料区 + 个人中心入口 + 菜单卡 + 退出) -->
|
||||
<admin-my-panel
|
||||
ref="myPanel"
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<template>
|
||||
<view>
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<page-container v-if="pageGuard" :show="show" :overlay="false" @leave="close" @afterleave="releasePageGuard" />
|
||||
<!-- pageGuard(v-if) 与 panelShow(:show) 拆开:关弹窗先离场再卸,二次打开才能再拦返回 -->
|
||||
<page-container
|
||||
v-if="pageGuard"
|
||||
:show="panelShow"
|
||||
:overlay="false"
|
||||
@leave="close"
|
||||
@afterleave="releasePageGuard"
|
||||
/>
|
||||
<!-- #endif -->
|
||||
<u-popup :value="show" mode="center" border-radius="24" :mask-close-able="true" @input="onPopupInput" @close="close">
|
||||
<u-popup :value="panelShow" mode="center" border-radius="24" :mask-close-able="true" @input="onPopupInput" @close="close">
|
||||
<view class="claim">
|
||||
<view class="claim__header">
|
||||
<text class="claim__title">就诊人认领码</text>
|
||||
@@ -32,6 +39,7 @@
|
||||
/**
|
||||
* 就诊人认领码弹窗(医生创建未认领就诊人)
|
||||
* 取值:xk-api unwrapXkApi
|
||||
* 返回拦截对齐切换账号:pageGuard / panelShow 拆分 + 关后 350ms 再卸挂载
|
||||
*/
|
||||
import { getUserPatientClaimQrcodeApi } from '@/api/reception.js'
|
||||
import { unwrapXkApi } from '@/utils/api-response.js'
|
||||
@@ -50,43 +58,67 @@ export default {
|
||||
patientName: '',
|
||||
errorText: '',
|
||||
pageGuard: false,
|
||||
panelShow: false,
|
||||
releaseTimer: null,
|
||||
localPath: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
show: {
|
||||
get() {
|
||||
return this.value
|
||||
},
|
||||
set(v) {
|
||||
this.$emit('input', v)
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
if (val) {
|
||||
this.pageGuard = true
|
||||
this.load()
|
||||
}
|
||||
/**
|
||||
* 打开:先挂 pageGuard,nextTick 再 panelShow;关闭:先卸 show,350ms 后再卸挂载
|
||||
*/
|
||||
value: {
|
||||
immediate: true,
|
||||
handler(val) {
|
||||
if (val) {
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = true
|
||||
this.$nextTick(() => {
|
||||
this.panelShow = true
|
||||
})
|
||||
this.load()
|
||||
} else {
|
||||
this.panelShow = false
|
||||
this.scheduleReleaseGuard()
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.cleanupLocal()
|
||||
if (this.releaseTimer) clearTimeout(this.releaseTimer)
|
||||
this.clearReleaseTimer()
|
||||
},
|
||||
methods: {
|
||||
onPopupInput(v) {
|
||||
this.show = v
|
||||
if (!v) this.close()
|
||||
},
|
||||
/** 关闭弹窗:先关 show,再延迟卸 pageGuard,并同步 v-model */
|
||||
close() {
|
||||
this.show = false
|
||||
this.panelShow = false
|
||||
this.scheduleReleaseGuard()
|
||||
if (this.value) {
|
||||
this.$emit('input', false)
|
||||
}
|
||||
this.cleanupLocal()
|
||||
},
|
||||
releasePageGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.pageGuard = false
|
||||
},
|
||||
/** 离场动画约 300ms,延迟卸挂载避免二次侧滑失效 */
|
||||
scheduleReleaseGuard() {
|
||||
this.clearReleaseTimer()
|
||||
this.releaseTimer = setTimeout(() => {
|
||||
this.pageGuard = false
|
||||
this.releaseTimer = null
|
||||
}, 350)
|
||||
},
|
||||
clearReleaseTimer() {
|
||||
if (this.releaseTimer) {
|
||||
clearTimeout(this.releaseTimer)
|
||||
this.releaseTimer = null
|
||||
}
|
||||
},
|
||||
cleanupLocal() {
|
||||
if (this.localPath) {
|
||||
unlinkLocalFile(this.localPath)
|
||||
@@ -53,8 +53,9 @@
|
||||
* 用途:本次挂号处方存在待支付订单时,医生出示小程序码给患者扫码代付
|
||||
* (复用 PC 接诊台同款后端 proxy-pay-qrcode,scene=ppay={orderId},患者端解析链路零改动)。
|
||||
* 展示:接口返回 data URI,写入 USER_DATA_PATH 再绑 <image>,不上传 OSS。
|
||||
* 必须放主包 components/:easycom 会把 <proxy-pay-qrcode-modal> 解析成 /components/proxy-pay-qrcode-modal,
|
||||
* 源码若只在分包,发行产物对不上会报 ENOENT(找不到 wxml/wxss)。lazyCodeLoading 按页注入,不常驻主包。
|
||||
* 现放在 sub_workbench 分包 components/,pages.json easycom 指向
|
||||
* @/subPackages/sub_workbench/components/proxy-pay-qrcode-modal/...
|
||||
* lazyCodeLoading 按页注入,不常驻主包。
|
||||
* 读值:xk-api 形态,拦截器已返回 body → code / result / message(unwrapXkApi)
|
||||
*/
|
||||
import { getProxyPayQrcodeApi } from '@/api/reception.js'
|
||||
@@ -71,7 +71,7 @@
|
||||
*/
|
||||
import { createUserPatientApi } from '@/api/reception.js'
|
||||
import { unwrapXkApi } from '@/utils/api-response.js'
|
||||
import ClaimQrcodeModal from '@/components/claim-qrcode-modal/claim-qrcode-modal.vue'
|
||||
import ClaimQrcodeModal from '@/subPackages/sub_workbench/components/claim-qrcode-modal/claim-qrcode-modal.vue'
|
||||
|
||||
function buildEmptyForm() {
|
||||
return {
|
||||
|
||||
@@ -41,7 +41,7 @@ import {
|
||||
startOfflineReceptionByPatientApi,
|
||||
} from '@/api/reception.js'
|
||||
import { unwrapXkApi } from '@/utils/api-response.js'
|
||||
import ClaimQrcodeModal from '@/components/claim-qrcode-modal/claim-qrcode-modal.vue'
|
||||
import ClaimQrcodeModal from '@/subPackages/sub_workbench/components/claim-qrcode-modal/claim-qrcode-modal.vue'
|
||||
|
||||
const STATUS_MAP = {
|
||||
0: '待支付',
|
||||
|
||||
170
subPackages/sub_workbench/image-transfer-prescription/index.vue
Normal file
170
subPackages/sub_workbench/image-transfer-prescription/index.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 医生识图传方:进页直接开抽屉,主题与开方页统一 -->
|
||||
<view v-if="!showOcr" class="gate">
|
||||
<text class="gate-tip">正在打开识图传方…</text>
|
||||
<u-button type="primary" :custom-style="btnStyle" @click="openOcr">上传处方图识别</u-button>
|
||||
</view>
|
||||
<ImageOcrDrawer
|
||||
v-model="showOcr"
|
||||
mode="transfer"
|
||||
chrome-mode="embedded"
|
||||
:store-id="storeId"
|
||||
:quote-api="quoteApi"
|
||||
:process-rule-list-api="processRuleListApi"
|
||||
@confirm-transfer="onConfirmTransfer"
|
||||
@closed="flushPendingNav"
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 医生识图传方页:全量 OCR → 确认 → 待导入传方单 → 跳接诊
|
||||
* 返回拦截由 ImageOcrDrawer 内置 page-container 处理
|
||||
*/
|
||||
import ImageOcrDrawer from '../prescription_v2/components/ImageOcrDrawer.vue';
|
||||
import { confirmTransferImageOcrApi } from '@/api/prescriptionImageOcr.js';
|
||||
import { quoteMyImageTransferOrder } from '@/api/imageTransferOrder.js';
|
||||
import { getProcessRuleList } from '@/api/reception.js';
|
||||
import { unwrapXkApi } from '@/utils/api-response.js';
|
||||
|
||||
export default {
|
||||
name: 'ImageTransferPrescription',
|
||||
components: { ImageOcrDrawer },
|
||||
data() {
|
||||
return {
|
||||
showOcr: false,
|
||||
storeId: 0,
|
||||
pendingNav: null,
|
||||
/** 注入抽屉:医生报价 + 委托调剂级联 */
|
||||
quoteApi: quoteMyImageTransferOrder,
|
||||
processRuleListApi: getProcessRuleList,
|
||||
btnStyle: {
|
||||
height: '88rpx',
|
||||
fontSize: '30rpx',
|
||||
fontWeight: '600',
|
||||
backgroundColor: '#6ACDBB',
|
||||
borderColor: '#6ACDBB',
|
||||
color: '#fff',
|
||||
borderRadius: '16rpx',
|
||||
},
|
||||
};
|
||||
},
|
||||
onLoad() {
|
||||
this.storeId = Number(uni.getStorageSync('store_id') || 0);
|
||||
},
|
||||
onReady() {
|
||||
this.openOcr();
|
||||
},
|
||||
methods: {
|
||||
openOcr() {
|
||||
this.showOcr = true;
|
||||
},
|
||||
flushPendingNav() {
|
||||
const fn = this.pendingNav;
|
||||
this.pendingNav = null;
|
||||
if (typeof fn === 'function') {
|
||||
fn();
|
||||
return;
|
||||
}
|
||||
uni.navigateBack({ fail: () => {} });
|
||||
},
|
||||
/**
|
||||
* 解包跨组件原生 emit 参数。
|
||||
* 微信小程序可能把 $emit('confirm-transfer', payload) 包成原生事件,
|
||||
* 业务数据在 detail.__args__[0];不解包直接 ...payload 会丢 patient_name 等字段。
|
||||
*/
|
||||
unwrapComponentEventPayload(payload) {
|
||||
if (payload && payload.detail && Array.isArray(payload.detail.__args__)) {
|
||||
return payload.detail.__args__[0];
|
||||
}
|
||||
if (payload && Array.isArray(payload.__args__)) {
|
||||
return payload.__args__[0];
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
/** 确认识图传方:解包后显式组字段,禁止 spread 事件对象 */
|
||||
async onConfirmTransfer(e) {
|
||||
const data = this.unwrapComponentEventPayload(e) || {};
|
||||
uni.showLoading({ title: '提交中' });
|
||||
try {
|
||||
// patient_mobile 优先 _txt:拦截器解密后可能写出明文,避免密文/脱敏串被再次加密
|
||||
const res = await confirmTransferImageOcrApi({
|
||||
patient_name: data.patient_name,
|
||||
patient_mobile: data.patient_mobile_txt || data.patient_mobile,
|
||||
patient_address: data.patient_address,
|
||||
sex: data.sex,
|
||||
age: data.age,
|
||||
user_patient_id: data.user_patient_id,
|
||||
clinical_diagnose: data.clinical_diagnose,
|
||||
medical_advice: data.medical_advice,
|
||||
doctor_order: data.doctor_order || data.medical_advice,
|
||||
dosage: data.dosage,
|
||||
day_dosage: data.day_dosage,
|
||||
image_urls: data.image_urls,
|
||||
drugs: data.drugs,
|
||||
rule_type: data.rule_type,
|
||||
process_rule_id: data.process_rule_id,
|
||||
child_process_rule_id: data.child_process_rule_id,
|
||||
process_rule_note_id: data.process_rule_note_id,
|
||||
package_method_id: data.package_method_id,
|
||||
store_id: this.storeId || undefined,
|
||||
});
|
||||
const { ok, payload: result, message } = unwrapXkApi(res);
|
||||
if (!ok || !result) {
|
||||
uni.showToast({ title: message || '失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
const transferId = Number(result.transfer_id || 0);
|
||||
const registerId = Number(result.register_id || 0);
|
||||
if (result.next === 'order' || Number(result.order_id || 0) > 0) {
|
||||
uni.showToast({ title: result.status_text || '已提交传方订单', icon: 'none' });
|
||||
this.pendingNav = () => {
|
||||
uni.redirectTo({
|
||||
url: '/subPackages/sub_image_transfer/order-list?scope=mine',
|
||||
});
|
||||
};
|
||||
this.showOcr = false;
|
||||
return;
|
||||
}
|
||||
if (!transferId || !registerId) {
|
||||
uni.showToast({ title: '传方生成失败', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
uni.setStorageSync('doctor_image_transfer_import', JSON.stringify({
|
||||
transfer_id: transferId,
|
||||
register_id: registerId,
|
||||
}));
|
||||
uni.showToast({ title: '已生成,进入接诊', icon: 'none' });
|
||||
this.pendingNav = () => {
|
||||
uni.navigateTo({
|
||||
url: `/subPackages/sub_workbench/prescription_v2/index?register_id=${registerId}&transfer_id=${transferId}&initial_category=1`,
|
||||
});
|
||||
};
|
||||
this.showOcr = false;
|
||||
} catch (err) {
|
||||
uni.showToast({ title: (err && err.message) || '失败', icon: 'none' });
|
||||
} finally {
|
||||
uni.hideLoading();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f4f6f8;
|
||||
}
|
||||
.gate {
|
||||
padding: 48rpx 32rpx;
|
||||
}
|
||||
.gate-tip {
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
</style>
|
||||
@@ -118,13 +118,14 @@
|
||||
<text class="fs-20 color-sub">{{ m.doseUnit }}{{ m.usageText }}</text>
|
||||
<view class="flex-row flex-wrap m-t-8">
|
||||
<view
|
||||
v-for="c in m.candidates"
|
||||
v-for="(c, ci) in m.candidates"
|
||||
:key="c.drug_id"
|
||||
class="ai-cand ai-cand--sm"
|
||||
:class="{ 'ai-cand--on': m.selected_drug_id == c.drug_id }"
|
||||
:data-mi="m.mi"
|
||||
:data-drug-id="c.drug_id"
|
||||
@click="onPickCandidateTap"
|
||||
hover-class="ai-cand--hover"
|
||||
@tap.stop="onPickCandidateTap"
|
||||
>
|
||||
<text class="fs-18">{{ c.drug_name }}</text>
|
||||
</view>
|
||||
@@ -149,13 +150,14 @@
|
||||
</view>
|
||||
<view class="flex-row flex-wrap m-t-8">
|
||||
<view
|
||||
v-for="c in m.candidates"
|
||||
v-for="(c, ci) in m.candidates"
|
||||
:key="c.drug_id"
|
||||
class="ai-cand ai-cand--sm"
|
||||
:class="{ 'ai-cand--on': m.selected_drug_id == c.drug_id }"
|
||||
:data-mi="m.mi"
|
||||
:data-drug-id="c.drug_id"
|
||||
@click="onPickCandidateTap"
|
||||
hover-class="ai-cand--hover"
|
||||
@tap.stop="onPickCandidateTap"
|
||||
>
|
||||
<text class="fs-20">{{ c.drug_name }}</text>
|
||||
</view>
|
||||
@@ -315,22 +317,16 @@
|
||||
<view class="ai-gen-scroll-pad" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
<!-- 二级:语音开方面板(录音转写 + A/B 卡片,不用第二个 page-container) -->
|
||||
<!-- 二级:语音开方面板(内容可滚;按住说话固定底栏) -->
|
||||
<view v-else-if="!consentVisible && voiceModalVisible" class="ai-gen-panel">
|
||||
<view class="ai-gen-hd flex-row flex-jus-sp flex-ali-center">
|
||||
<text class="fs-30 font-bold">语音开方</text>
|
||||
<text class="ai-rx-sub" @click="closeVoiceModal">返回</text>
|
||||
</view>
|
||||
<scroll-view scroll-y class="ai-gen-scroll">
|
||||
<view class="ai-voice-rec flex-row flex-ali-center">
|
||||
<view
|
||||
class="ai-btn ai-btn--ghost"
|
||||
:class="{ 'ai-btn--recording': voiceRecording }"
|
||||
@click="onToggleVoiceRecord"
|
||||
>
|
||||
{{ voiceAsrLoading ? '转写中…' : (voiceRecording ? '点击结束录音' : '点击开始录音') }}
|
||||
</view>
|
||||
<text class="fs-22 color-sub m-l-16">说完后点结束,可改字</text>
|
||||
<view class="ai-voice-tip">
|
||||
<u-icon name="mic" color="#6ACDBB" size="28"></u-icon>
|
||||
<text class="ai-voice-tip__text">底部按住口述药方,松手后自动转写,可改字</text>
|
||||
</view>
|
||||
<view class="ai-gen-row ai-gen-row--full m-t-16">
|
||||
<text class="ai-gen-label">转写文本(可改)</text>
|
||||
@@ -389,7 +385,7 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ai-gen-scroll-pad" />
|
||||
<view class="ai-gen-scroll-pad ai-gen-scroll-pad--voice" />
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="ai-rx-footer safe-area-inset-bottom">
|
||||
@@ -399,10 +395,49 @@
|
||||
{{ consentSubmitting ? '提交中…' : '我已知悉并同意' }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="voiceModalVisible" class="ai-actions">
|
||||
<view class="ai-btn ai-btn--ghost" @click="closeVoiceModal">返回</view>
|
||||
<view class="ai-btn ai-btn--primary" @click="onConfirmVoiceGenerate">
|
||||
{{ generating ? '生成中…' : '确认生成' }}
|
||||
<view v-else-if="voiceModalVisible" class="ai-voice-dock">
|
||||
<view class="ai-actions">
|
||||
<view class="ai-btn ai-btn--ghost" @click="closeVoiceModal">返回</view>
|
||||
<view class="ai-btn ai-btn--primary" @click="onConfirmVoiceGenerate">
|
||||
{{ generating ? '生成中…' : '确认生成' }}
|
||||
</view>
|
||||
</view>
|
||||
<!-- 录音/转写时:波形动画贴在按住按钮上方,不挡中间内容 -->
|
||||
<view v-if="voiceRecording || voiceAsrLoading" class="ai-voice-wave">
|
||||
<view class="ai-voice-wave__bars">
|
||||
<view
|
||||
v-for="n in voiceWaveBars"
|
||||
:key="n"
|
||||
class="ai-voice-wave__bar"
|
||||
:class="['ai-voice-wave__bar--' + n, voiceAsrLoading ? 'is-paused' : '']"
|
||||
></view>
|
||||
</view>
|
||||
<text class="ai-voice-wave__hint">
|
||||
{{ voiceAsrLoading ? '正在转写…' : '正在聆听,松开发送' }}
|
||||
</text>
|
||||
</view>
|
||||
<view
|
||||
class="ai-voice-hold"
|
||||
:class="{
|
||||
'ai-voice-hold--on': voiceRecording,
|
||||
'ai-voice-hold--busy': voiceAsrLoading
|
||||
}"
|
||||
@touchstart.prevent="onVoiceTouchStart"
|
||||
@touchend.prevent="onVoiceTouchEnd"
|
||||
@touchcancel.prevent="onVoiceTouchCancel"
|
||||
@touchmove.stop.prevent="onVoiceTouchMove"
|
||||
>
|
||||
<view v-if="voiceRecording" class="ai-voice-hold__pulse ai-voice-hold__pulse--1"></view>
|
||||
<view v-if="voiceRecording" class="ai-voice-hold__pulse ai-voice-hold__pulse--2"></view>
|
||||
<u-icon
|
||||
:name="voiceAsrLoading ? 'reload' : 'mic'"
|
||||
:color="voiceRecording ? '#EF4444' : '#6ACDBB'"
|
||||
size="28"
|
||||
class="ai-voice-hold__mic"
|
||||
></u-icon>
|
||||
<text class="ai-voice-hold__text">
|
||||
{{ voiceAsrLoading ? '转写中…' : (voiceRecording ? '松开发送' : '按住说话') }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else-if="!genModalVisible" class="ai-actions">
|
||||
@@ -489,6 +524,8 @@ export default {
|
||||
voiceAsrLoading: false,
|
||||
voiceTranscript: '',
|
||||
voiceTranscriptTrimmed: '',
|
||||
/** 录音波形柱(固定 7 根,CSS 错峰动画) */
|
||||
voiceWaveBars: [1, 2, 3, 4, 5, 6, 7],
|
||||
/** dialectic | dictation;默认口述落方 */
|
||||
voiceMode: 'dictation',
|
||||
voiceAsrProvider: '',
|
||||
@@ -609,6 +646,7 @@ export default {
|
||||
* 重置语音开方状态并停止录音
|
||||
*/
|
||||
resetVoiceState() {
|
||||
this._voiceFingerDown = false
|
||||
this.stopVoiceRecording(true)
|
||||
this.voiceAsrLoading = false
|
||||
this.voiceTranscript = ''
|
||||
@@ -1128,15 +1166,25 @@ export default {
|
||||
}
|
||||
},
|
||||
/**
|
||||
* 确保 RecorderManager 已初始化(mp3 16k,适配讯飞 lame)
|
||||
* 确保 RecorderManager 已初始化(wav 16k,适配讯飞 encoding=raw)
|
||||
* 为何不用 mp3:服务端按 1280B 切帧会破坏 MP3 帧 → 讯飞 10043
|
||||
* 为何改用 wav 而非 PCM:开发者工具/部分机型对 PCM 不稳定,wav 有明确 RIFF 头,服务端会剥头
|
||||
*/
|
||||
ensureRecorder() {
|
||||
if (this._voiceRecorder) return this._voiceRecorder
|
||||
const rm = uni.getRecorderManager()
|
||||
rm.onStop((res) => {
|
||||
const discard = !!this._voiceDiscard
|
||||
this._voiceDiscard = false
|
||||
this.voiceRecording = false
|
||||
if (discard) return
|
||||
const path = (res && res.tempFilePath) || ''
|
||||
const duration = Number((res && res.duration) || 0)
|
||||
// 与微信/聊天页一致:不足 1 秒不调 ASR
|
||||
if (duration > 0 && duration < 1000) {
|
||||
uni.showToast({ title: '说话时间太短', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!path) {
|
||||
uni.showToast({ title: '未录到有效音频', icon: 'none' })
|
||||
return
|
||||
@@ -1144,65 +1192,118 @@ export default {
|
||||
this.handleVoiceAsr(path, duration)
|
||||
})
|
||||
rm.onError(() => {
|
||||
this._voiceDiscard = false
|
||||
this.voiceRecording = false
|
||||
uni.showToast({ title: '录音失败', icon: 'none' })
|
||||
})
|
||||
this._voiceRecorder = rm
|
||||
return rm
|
||||
},
|
||||
onToggleVoiceRecord() {
|
||||
if (this.voiceAsrLoading) return
|
||||
if (this.voiceRecording) {
|
||||
this.stopVoiceRecording(false)
|
||||
return
|
||||
}
|
||||
uni.authorize({
|
||||
scope: 'scope.record',
|
||||
success: () => this.startVoiceRecording(),
|
||||
/**
|
||||
* 按住说话:touchstart 开录(对齐 chat.vue)
|
||||
* 用 _voiceFingerDown 避免授权弹窗回来时手指已松开仍开录
|
||||
*/
|
||||
onVoiceTouchStart() {
|
||||
if (this.voiceAsrLoading || this.voiceRecording) return
|
||||
this._voiceDiscard = false
|
||||
this._voiceFingerDown = true
|
||||
uni.getSetting({
|
||||
success: (res) => {
|
||||
if (!this._voiceFingerDown) return
|
||||
const auth = res && res.authSetting ? res.authSetting['scope.record'] : false
|
||||
if (auth) {
|
||||
this.startVoiceRecording()
|
||||
return
|
||||
}
|
||||
uni.authorize({
|
||||
scope: 'scope.record',
|
||||
success: () => {
|
||||
if (!this._voiceFingerDown) return
|
||||
this.startVoiceRecording()
|
||||
},
|
||||
fail: () => {
|
||||
this._voiceFingerDown = false
|
||||
uni.showModal({
|
||||
title: '需要录音权限',
|
||||
content: '请在设置中开启麦克风权限后重试',
|
||||
success: (r) => {
|
||||
if (r.confirm) uni.openSetting({})
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.showModal({
|
||||
title: '需要录音权限',
|
||||
content: '请在设置中开启麦克风权限后重试',
|
||||
success: (r) => {
|
||||
if (r.confirm) uni.openSetting({})
|
||||
if (!this._voiceFingerDown) return
|
||||
uni.authorize({
|
||||
scope: 'scope.record',
|
||||
success: () => {
|
||||
if (!this._voiceFingerDown) return
|
||||
this.startVoiceRecording()
|
||||
},
|
||||
fail: () => {
|
||||
this._voiceFingerDown = false
|
||||
uni.showToast({ title: '需要录音权限', icon: 'none' })
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
},
|
||||
/**
|
||||
* 松手结束:触发 onStop → ASR
|
||||
*/
|
||||
onVoiceTouchEnd() {
|
||||
this._voiceFingerDown = false
|
||||
if (!this.voiceRecording) return
|
||||
this.stopVoiceRecording(false)
|
||||
},
|
||||
/**
|
||||
* 触摸取消:丢弃本次录音
|
||||
*/
|
||||
onVoiceTouchCancel() {
|
||||
this._voiceFingerDown = false
|
||||
if (!this.voiceRecording) return
|
||||
this.stopVoiceRecording(true)
|
||||
},
|
||||
/**
|
||||
* 按住时吞掉 move,避免 scroll-view 抢手势导致误停
|
||||
*/
|
||||
onVoiceTouchMove() {
|
||||
/* prevent scroll steal */
|
||||
},
|
||||
startVoiceRecording() {
|
||||
if (this.voiceAsrLoading || this.voiceRecording) return
|
||||
const rm = this.ensureRecorder()
|
||||
this._voiceDiscard = false
|
||||
this.voiceRecording = true
|
||||
// wav:16k 单声道;服务端剥 RIFF 后按 raw PCM 推讯飞(勿用 mp3)
|
||||
rm.start({
|
||||
duration: 60000,
|
||||
sampleRate: 16000,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 96000,
|
||||
format: 'mp3',
|
||||
encodeBitRate: 48000,
|
||||
format: 'wav',
|
||||
})
|
||||
},
|
||||
/**
|
||||
* @param {boolean} discard 丢弃本次录音(关闭面板时)
|
||||
* @param {boolean} discard 丢弃本次录音(关闭面板 / touchcancel)
|
||||
*/
|
||||
stopVoiceRecording(discard) {
|
||||
if (!this.voiceRecording && !this._voiceRecorder) return
|
||||
if (discard && this._voiceRecorder) {
|
||||
// 丢弃:先卸 onStop 再 stop,避免触发转写
|
||||
try {
|
||||
this._voiceRecorder.onStop(() => {})
|
||||
} catch (e) {
|
||||
/* ignore */
|
||||
}
|
||||
try {
|
||||
this._voiceRecorder.stop()
|
||||
} catch (e2) {
|
||||
/* ignore */
|
||||
if (discard) {
|
||||
this._voiceDiscard = true
|
||||
if (this._voiceRecorder) {
|
||||
try {
|
||||
this._voiceRecorder.stop()
|
||||
} catch (e2) {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
this.voiceRecording = false
|
||||
this._voiceRecorder = null
|
||||
return
|
||||
}
|
||||
if (this.voiceRecording && this._voiceRecorder) {
|
||||
this._voiceDiscard = false
|
||||
this._voiceRecorder.stop()
|
||||
}
|
||||
},
|
||||
@@ -1212,6 +1313,7 @@ export default {
|
||||
const res = await aiVoiceAsrApi({
|
||||
filePath,
|
||||
duration_ms: durationMs || 0,
|
||||
audio_format: 'wav',
|
||||
})
|
||||
// upload 拦截器可能直接返回字符串 JSON
|
||||
let body = res
|
||||
@@ -2004,8 +2106,133 @@ export default {
|
||||
border-color: #ef4444 !important;
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
.ai-voice-rec {
|
||||
padding: 8rpx 0;
|
||||
.ai-voice-tip {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 16rpx 20rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(106, 205, 187, 0.08);
|
||||
border: 1rpx solid rgba(106, 205, 187, 0.28);
|
||||
}
|
||||
.ai-voice-tip__text {
|
||||
margin-left: 12rpx;
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
line-height: 1.4;
|
||||
flex: 1;
|
||||
}
|
||||
/* 语音底栏:确认行 + 波形 + 按住说话(贴底,不占中间) */
|
||||
.ai-voice-dock {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
.ai-voice-dock .ai-actions {
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.ai-voice-wave {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
min-height: 72rpx;
|
||||
}
|
||||
.ai-voice-wave__bars {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
height: 48rpx;
|
||||
}
|
||||
.ai-voice-wave__bar {
|
||||
width: 8rpx;
|
||||
margin: 0 5rpx;
|
||||
border-radius: 8rpx;
|
||||
background: #6acdbb;
|
||||
height: 16rpx;
|
||||
animation: ai-voice-bar 0.9s ease-in-out infinite;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
.ai-voice-wave__bar.is-paused {
|
||||
animation: none;
|
||||
height: 14rpx !important;
|
||||
opacity: 0.55;
|
||||
background: #94a3b8;
|
||||
}
|
||||
.ai-voice-wave__bar--1 { animation-delay: 0s; }
|
||||
.ai-voice-wave__bar--2 { animation-delay: 0.1s; }
|
||||
.ai-voice-wave__bar--3 { animation-delay: 0.2s; }
|
||||
.ai-voice-wave__bar--4 { animation-delay: 0.05s; }
|
||||
.ai-voice-wave__bar--5 { animation-delay: 0.25s; }
|
||||
.ai-voice-wave__bar--6 { animation-delay: 0.15s; }
|
||||
.ai-voice-wave__bar--7 { animation-delay: 0.3s; }
|
||||
@keyframes ai-voice-bar {
|
||||
0%, 100% { height: 12rpx; opacity: 0.55; }
|
||||
50% { height: 44rpx; opacity: 1; }
|
||||
}
|
||||
.ai-voice-wave__hint {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #ef4444;
|
||||
}
|
||||
.ai-voice-hold {
|
||||
position: relative;
|
||||
height: 92rpx;
|
||||
border-radius: 46rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(106, 205, 187, 0.12);
|
||||
border: 1rpx solid rgba(106, 205, 187, 0.45);
|
||||
}
|
||||
.ai-voice-hold--on {
|
||||
background: rgba(239, 68, 68, 0.1);
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
}
|
||||
.ai-voice-hold--busy {
|
||||
opacity: 0.65;
|
||||
}
|
||||
.ai-voice-hold__mic {
|
||||
margin-right: 10rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
.ai-voice-hold__text {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #6acdbb;
|
||||
}
|
||||
.ai-voice-hold--on .ai-voice-hold__text {
|
||||
color: #ef4444;
|
||||
}
|
||||
/* 录音时按钮内双圈脉冲 */
|
||||
.ai-voice-hold__pulse {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
margin-left: -60rpx;
|
||||
margin-top: -60rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(239, 68, 68, 0.35);
|
||||
pointer-events: none;
|
||||
animation: ai-voice-pulse 1.4s ease-out infinite;
|
||||
}
|
||||
.ai-voice-hold__pulse--2 {
|
||||
animation-delay: 0.7s;
|
||||
}
|
||||
@keyframes ai-voice-pulse {
|
||||
0% { transform: scale(0.55); opacity: 0.7; }
|
||||
100% { transform: scale(2.1); opacity: 0; }
|
||||
}
|
||||
.ai-gen-scroll-pad--voice {
|
||||
height: 48rpx;
|
||||
}
|
||||
.ai-match-name {
|
||||
flex: 1;
|
||||
@@ -2018,15 +2245,23 @@ export default {
|
||||
.ai-cand {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8rpx;
|
||||
padding: 8rpx 12rpx;
|
||||
padding: 12rpx 18rpx;
|
||||
min-height: 56rpx;
|
||||
box-sizing: border-box;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 12rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.ai-cand--sm {
|
||||
padding: 4rpx 10rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
min-height: 52rpx;
|
||||
margin-right: 8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.ai-cand--hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.ai-cand--on {
|
||||
border-color: #6acdbb;
|
||||
background: #e8f8f4;
|
||||
|
||||
@@ -1,151 +1,171 @@
|
||||
<template>
|
||||
<view class="chinese-medicine-config">
|
||||
<!-- 制剂方式 -->
|
||||
<view class="config-section m-b-32">
|
||||
<view class="config-section m-b-32" :class="{ 'sec-error': errorMap.ruleType }">
|
||||
<view class="section-title m-b-16">
|
||||
<d-text text="制剂方式" className="fs-30 font-bold color-title"></d-text>
|
||||
<text class="req-star">*</text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<view class="modern-pill-group">
|
||||
<view
|
||||
class="modern-pill"
|
||||
:class="{ active: localConfig.ruleType === 1, disabled: disabled }"
|
||||
@click="!disabled && handleRuleTypeChange(1)"
|
||||
>
|
||||
自制剂
|
||||
</view>
|
||||
class="modern-pill"
|
||||
:class="{ active: Number(localConfig.ruleType) === 1, disabled: disabled }"
|
||||
data-rule="1"
|
||||
@click="onRuleTypeClick"
|
||||
>自制剂</view>
|
||||
<view
|
||||
class="modern-pill"
|
||||
:class="{ active: localConfig.ruleType === 2, disabled: disabled }"
|
||||
@click="!disabled && handleRuleTypeChange(2)"
|
||||
>
|
||||
委托调剂
|
||||
</view>
|
||||
class="modern-pill"
|
||||
:class="{ active: Number(localConfig.ruleType) === 2, disabled: disabled }"
|
||||
data-rule="2"
|
||||
@click="onRuleTypeClick"
|
||||
>委托调剂</view>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="errorMap.ruleType" class="err-tip">请选择制剂方式</text>
|
||||
</view>
|
||||
|
||||
<!-- 自制剂配置 -->
|
||||
<view class="config-section border-t-dashed pt-32 m-b-32" v-if="localConfig.ruleType === 1">
|
||||
<!-- 自制剂:包法 -->
|
||||
<view
|
||||
v-if="Number(localConfig.ruleType) === 1"
|
||||
class="config-section border-t-dashed pt-32 m-b-32"
|
||||
:class="{ 'sec-error': errorMap.packageMethodId }"
|
||||
>
|
||||
<view class="section-title m-b-16">
|
||||
<d-text text="包法选择" className="fs-30 font-bold color-title"></d-text>
|
||||
<text class="req-star">*</text>
|
||||
</view>
|
||||
<view class="section-content">
|
||||
<view class="modern-pill-group">
|
||||
<view
|
||||
v-for="item in packageMethodList"
|
||||
:key="item.id"
|
||||
class="modern-pill outline"
|
||||
:class="{ active: localConfig.packageMethodId === item.id, disabled: disabled }"
|
||||
@click="!disabled && handlePackageMethodTagClick(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
</view>
|
||||
v-for="item in packageMethodList"
|
||||
:key="item.id"
|
||||
class="modern-pill outline"
|
||||
:class="{ active: Number(localConfig.packageMethodId) === Number(item.id), disabled: disabled }"
|
||||
:data-id="item.id"
|
||||
@click="onPackageMethodClick"
|
||||
>{{ item.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="errorMap.packageMethodId" class="err-tip">请选择包法</text>
|
||||
</view>
|
||||
|
||||
<!-- 委托调剂配置 -->
|
||||
<view class="config-section border-t-dashed pt-32" v-if="localConfig.ruleType === 2">
|
||||
<view class="section-title m-b-16">
|
||||
<d-text text="制剂要求" className="fs-30 font-bold color-title"></d-text>
|
||||
</view>
|
||||
<view class="section-content m-b-24">
|
||||
<view class="modern-pill-group">
|
||||
<view
|
||||
<!-- 委托调剂 -->
|
||||
<view v-if="Number(localConfig.ruleType) === 2" class="config-section border-t-dashed pt-32">
|
||||
<view class="field-block" :class="{ 'sec-error': errorMap.processRuleId }">
|
||||
<view class="section-title m-b-16">
|
||||
<d-text text="制剂要求" className="fs-30 font-bold color-title"></d-text>
|
||||
<text class="req-star">*</text>
|
||||
</view>
|
||||
<view class="section-content m-b-8">
|
||||
<view class="modern-pill-group">
|
||||
<view
|
||||
v-for="item in processRuleList"
|
||||
:key="item.id"
|
||||
class="modern-pill outline"
|
||||
:class="{ active: localConfig.processRuleId === item.id, disabled: disabled }"
|
||||
@click="!disabled && handleProcessRuleTagClick(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
:class="{ active: Number(localConfig.processRuleId) === Number(item.id), disabled: disabled }"
|
||||
:data-id="item.id"
|
||||
@click="onProcessRuleClick"
|
||||
>{{ item.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="errorMap.processRuleId" class="err-tip">请选择制剂要求</text>
|
||||
</view>
|
||||
|
||||
<view class="section-title m-b-16 m-t-24">
|
||||
<d-text text="煎法说明" className="fs-30 font-bold color-title"></d-text>
|
||||
</view>
|
||||
<view class="section-content m-b-24">
|
||||
<view class="modern-pill-group">
|
||||
<view
|
||||
<view class="field-block m-t-24" :class="{ 'sec-error': errorMap.childProcessRuleId }">
|
||||
<view class="section-title m-b-16">
|
||||
<d-text text="煎法说明" className="fs-30 font-bold color-title"></d-text>
|
||||
<text class="req-star">*</text>
|
||||
</view>
|
||||
<view class="section-content m-b-8">
|
||||
<view class="modern-pill-group">
|
||||
<view
|
||||
v-for="item in childProcessRuleList"
|
||||
:key="item.id"
|
||||
class="modern-pill outline"
|
||||
:class="{ active: localConfig.childProcessRuleId === item.id, disabled: disabled }"
|
||||
@click="!disabled && handleChildProcessRuleTagClick(item)"
|
||||
>
|
||||
{{ item.name }}
|
||||
:class="{ active: Number(localConfig.childProcessRuleId) === Number(item.id), disabled: disabled }"
|
||||
:data-id="item.id"
|
||||
@click="onChildProcessRuleClick"
|
||||
>{{ item.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="errorMap.childProcessRuleId" class="err-tip">请选择煎法说明</text>
|
||||
</view>
|
||||
|
||||
<view class="section-title m-b-16 m-t-24">
|
||||
<d-text text="附加备注" className="fs-30 font-bold color-title"></d-text>
|
||||
</view>
|
||||
<view class="section-content m-b-32">
|
||||
<view class="modern-pill-group">
|
||||
<view
|
||||
<view class="field-block m-t-24 m-b-32" :class="{ 'sec-error': errorMap.processRuleNoteId }">
|
||||
<view class="section-title m-b-16">
|
||||
<d-text text="附加备注" className="fs-30 font-bold color-title"></d-text>
|
||||
<text class="req-star">*</text>
|
||||
</view>
|
||||
<view class="section-content m-b-8">
|
||||
<view class="modern-pill-group">
|
||||
<view
|
||||
v-for="item in processRuleNoteList"
|
||||
:key="item.id"
|
||||
class="modern-pill outline"
|
||||
:class="{ active: localConfig.processRuleNoteId === item.id, disabled: disabled }"
|
||||
@click="!disabled && handleProcessRuleNoteTagClick(item)"
|
||||
>
|
||||
{{ item.note || item.name }}
|
||||
:class="{ active: Number(localConfig.processRuleNoteId) === Number(item.id), disabled: disabled }"
|
||||
:data-id="item.id"
|
||||
@click="onProcessRuleNoteClick"
|
||||
>{{ item.note || item.name }}</view>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="errorMap.processRuleNoteId" class="err-tip">请选择附加备注</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 用量和频次:与 WesternUsageModal 保持一致的表单布局 -->
|
||||
<!-- 用量 -->
|
||||
<view class="config-form border-t-dashed pt-20">
|
||||
<view class="form-item flex-row flex-jus-sp flex-ali-center p-y-24">
|
||||
<view class="item-label">
|
||||
<view class="form-item flex-row flex-jus-sp flex-ali-center p-y-24" :class="{ 'sec-error': errorMap.dosage }">
|
||||
<view class="item-label flex-row flex-ali-center">
|
||||
<d-text text="剂数" className="fs-30 font-bold color-title"></d-text>
|
||||
<text class="req-star">*</text>
|
||||
</view>
|
||||
<view class="item-content flex-row flex-ali-center">
|
||||
<u-number-box
|
||||
v-model="localConfig.dosage"
|
||||
:disabled="disabled"
|
||||
:min="1"
|
||||
:input-width="80"
|
||||
:input-height="60"
|
||||
bg-color="#F7F8FA"
|
||||
v-model="localConfig.dosage"
|
||||
:disabled="disabled"
|
||||
:min="1"
|
||||
:input-width="80"
|
||||
:input-height="60"
|
||||
bg-color="#F7F8FA"
|
||||
/>
|
||||
<d-text text="剂(天)" className="fs-28 color-sub m-l-16"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="errorMap.dosage" class="err-tip err-tip-form">请填写剂数</text>
|
||||
|
||||
<view class="form-item flex-row flex-jus-sp flex-ali-center p-y-24">
|
||||
<view class="item-label">
|
||||
<view class="form-item flex-row flex-jus-sp flex-ali-center p-y-24" :class="{ 'sec-error': errorMap.dayDosage }">
|
||||
<view class="item-label flex-row flex-ali-center">
|
||||
<d-text text="用药频次" className="fs-30 font-bold color-title"></d-text>
|
||||
<text class="req-star">*</text>
|
||||
</view>
|
||||
<view class="item-content flex-row flex-ali-center">
|
||||
<u-number-box
|
||||
v-model="localConfig.dayDosage"
|
||||
:disabled="disabled"
|
||||
:min="1"
|
||||
:input-width="80"
|
||||
:input-height="60"
|
||||
bg-color="#F7F8FA"
|
||||
v-model="localConfig.dayDosage"
|
||||
:disabled="disabled"
|
||||
:min="1"
|
||||
:input-width="80"
|
||||
:input-height="60"
|
||||
bg-color="#F7F8FA"
|
||||
/>
|
||||
<d-text text="次/天" className="fs-28 color-sub m-l-16"></d-text>
|
||||
</view>
|
||||
</view>
|
||||
<text v-if="errorMap.dayDosage" class="err-tip err-tip-form">请填写用药频次</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// 保留原有逻辑
|
||||
/**
|
||||
* 中药代煎/用量配置
|
||||
* - 点击一律走 methods + dataset-id(微信模板勿写 !disabled && fn(item))
|
||||
* - 跨分包时父页需解包 update:config(detail.__args__[0])
|
||||
* - syncingFromParent:父 props 回写时不二次 emit,避免点选被冲掉
|
||||
*/
|
||||
export default {
|
||||
name: 'ChineseMedicineConfig',
|
||||
props: {
|
||||
/**
|
||||
* 中药配置
|
||||
*/
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
@@ -155,175 +175,134 @@ export default {
|
||||
childProcessRuleId: null,
|
||||
processRuleNoteId: null,
|
||||
dosage: 7,
|
||||
dayDosage: 2
|
||||
})
|
||||
dayDosage: 2,
|
||||
}),
|
||||
},
|
||||
/**
|
||||
* 制剂列表
|
||||
*/
|
||||
processRuleList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 煎法列表
|
||||
*/
|
||||
childProcessRuleList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 备注列表
|
||||
*/
|
||||
processRuleNoteList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 特色方锁定后禁止修改配置
|
||||
*/
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
processRuleList: { type: Array, default: () => [] },
|
||||
childProcessRuleList: { type: Array, default: () => [] },
|
||||
processRuleNoteList: { type: Array, default: () => [] },
|
||||
disabled: { type: Boolean, default: false },
|
||||
/** 未填字段名;父页提交失败传入,改选后由父清空 */
|
||||
errorKeys: { type: Array, default: () => [] },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localConfig: {},
|
||||
localConfig: {
|
||||
ruleType: 2,
|
||||
packageMethodId: null,
|
||||
processRuleId: null,
|
||||
childProcessRuleId: null,
|
||||
processRuleNoteId: null,
|
||||
dosage: 7,
|
||||
dayDosage: 2,
|
||||
},
|
||||
/** 正在被父 props 覆盖,跳过 emit,打断回写回路 */
|
||||
syncingFromParent: false,
|
||||
packageMethodList: [
|
||||
{ id: 1, name: '味包' },
|
||||
{ id: 2, name: '剂包' }
|
||||
]
|
||||
{ id: 2, name: '剂包' },
|
||||
],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
errorMap() {
|
||||
const map = {
|
||||
ruleType: false,
|
||||
packageMethodId: false,
|
||||
processRuleId: false,
|
||||
childProcessRuleId: false,
|
||||
processRuleNoteId: false,
|
||||
dosage: false,
|
||||
dayDosage: false,
|
||||
};
|
||||
const keys = this.errorKeys || [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const k = keys[i];
|
||||
if (Object.prototype.hasOwnProperty.call(map, k)) map[k] = true;
|
||||
}
|
||||
return map;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
config: {
|
||||
handler(newVal) {
|
||||
this.localConfig = { ...newVal };
|
||||
if (!newVal || typeof newVal !== 'object') return;
|
||||
this.syncingFromParent = true;
|
||||
this.localConfig = Object.assign({}, this.localConfig, newVal);
|
||||
this.$nextTick(() => {
|
||||
this.syncingFromParent = false;
|
||||
});
|
||||
},
|
||||
immediate: true,
|
||||
deep: true
|
||||
deep: true,
|
||||
},
|
||||
localConfig: {
|
||||
handler(newVal) {
|
||||
this.$emit('update:config', { ...newVal });
|
||||
if (this.syncingFromParent) return;
|
||||
this.$emit('update:config', Object.assign({}, newVal));
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* 处理制剂方式改变
|
||||
* @param {number} value - 制剂方式:1=自制剂,2=委托调剂
|
||||
* 职责:更新制剂方式,触发配置更新
|
||||
*/
|
||||
handleRuleTypeChange(value) {
|
||||
datasetNum(e, key) {
|
||||
const ds = (e && e.currentTarget && e.currentTarget.dataset) || {};
|
||||
return Number(ds[key] || 0);
|
||||
},
|
||||
onRuleTypeClick(e) {
|
||||
if (this.disabled) return;
|
||||
const value = this.datasetNum(e, 'rule');
|
||||
if (value !== 1 && value !== 2) return;
|
||||
this.localConfig.ruleType = value;
|
||||
// 切换时清空相关配置
|
||||
if (value === 1) {
|
||||
// 自制剂:清空委托调剂相关配置,默认选择第一个包法
|
||||
this.localConfig.processRuleId = null;
|
||||
this.localConfig.childProcessRuleId = null;
|
||||
this.localConfig.processRuleNoteId = null;
|
||||
// 如果包法未选择,默认选择第一个
|
||||
if (!this.localConfig.packageMethodId && this.packageMethodList.length > 0) {
|
||||
if (!this.localConfig.packageMethodId && this.packageMethodList.length) {
|
||||
this.localConfig.packageMethodId = this.packageMethodList[0].id;
|
||||
}
|
||||
} else {
|
||||
// 委托调剂:清空自制剂相关配置
|
||||
this.localConfig.packageMethodId = null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理包法改变(标签点击)
|
||||
* @param {Object} item - 包法项
|
||||
* 职责:更新包法ID
|
||||
*/
|
||||
handlePackageMethodTagClick(item) {
|
||||
this.localConfig.packageMethodId = item.id;
|
||||
onPackageMethodClick(e) {
|
||||
if (this.disabled) return;
|
||||
const id = this.datasetNum(e, 'id');
|
||||
if (!id) return;
|
||||
this.localConfig.packageMethodId = id;
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理制剂改变(标签点击)
|
||||
* @param {Object} item - 制剂项
|
||||
* 职责:更新制剂ID,触发加载煎法列表
|
||||
*/
|
||||
handleProcessRuleTagClick(item) {
|
||||
// 清空煎法和备注ID(级联清空)
|
||||
this.localConfig.processRuleId = item.id;
|
||||
onProcessRuleClick(e) {
|
||||
if (this.disabled) return;
|
||||
const id = this.datasetNum(e, 'id');
|
||||
if (!id) return;
|
||||
this.localConfig.processRuleId = id;
|
||||
this.localConfig.childProcessRuleId = null;
|
||||
this.localConfig.processRuleNoteId = null;
|
||||
// localConfig 的变化会通过 watch 自动触发 @update:config
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理煎法改变(标签点击)
|
||||
* @param {Object} item - 煎法项
|
||||
* 职责:更新煎法ID,触发加载备注列表
|
||||
*/
|
||||
handleChildProcessRuleTagClick(item) {
|
||||
// 清空备注ID(级联清空)
|
||||
this.localConfig.childProcessRuleId = item.id;
|
||||
onChildProcessRuleClick(e) {
|
||||
if (this.disabled) return;
|
||||
const id = this.datasetNum(e, 'id');
|
||||
if (!id) return;
|
||||
this.localConfig.childProcessRuleId = id;
|
||||
this.localConfig.processRuleNoteId = null;
|
||||
// localConfig 的变化会通过 watch 自动触发 @update:config
|
||||
},
|
||||
|
||||
/**
|
||||
* 处理备注改变(标签点击)
|
||||
* @param {Object} item - 备注项
|
||||
* 职责:更新备注ID
|
||||
*/
|
||||
handleProcessRuleNoteTagClick(item) {
|
||||
this.localConfig.processRuleNoteId = item.id;
|
||||
onProcessRuleNoteClick(e) {
|
||||
if (this.disabled) return;
|
||||
const id = this.datasetNum(e, 'id');
|
||||
if (!id) return;
|
||||
this.localConfig.processRuleNoteId = id;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取包法名称
|
||||
* @returns {string} 包法名称
|
||||
*/
|
||||
getPackageMethodName() {
|
||||
const item = this.packageMethodList.find(item => item.id === this.localConfig.packageMethodId);
|
||||
return item?.name || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取制剂名称
|
||||
* @returns {string} 制剂名称
|
||||
*/
|
||||
getProcessRuleName() {
|
||||
const item = this.processRuleList.find(item => item.id === this.localConfig.processRuleId);
|
||||
return item?.name || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取煎法名称
|
||||
* @returns {string} 煎法名称
|
||||
*/
|
||||
getChildProcessRuleName() {
|
||||
const item = this.childProcessRuleList.find(item => item.id === this.localConfig.childProcessRuleId);
|
||||
return item?.name || '';
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取备注名称
|
||||
* @returns {string} 备注名称
|
||||
*/
|
||||
getProcessRuleNoteName() {
|
||||
const item = this.processRuleNoteList.find(item => item.id === this.localConfig.processRuleNoteId);
|
||||
return item?.name || '';
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 统一样式 */
|
||||
.color-title { color: #2A2E35; }
|
||||
.color-sub { color: #8A92A3; }
|
||||
.fs-28 { font-size: 28rpx; }
|
||||
.fs-30 { font-size: 30rpx; }
|
||||
.font-bold { font-weight: bold; }
|
||||
.m-b-8 { margin-bottom: 8rpx; }
|
||||
.m-b-16 { margin-bottom: 16rpx; }
|
||||
.m-b-24 { margin-bottom: 24rpx; }
|
||||
.m-b-32 { margin-bottom: 32rpx; }
|
||||
@@ -333,7 +312,6 @@ export default {
|
||||
.pt-32 { padding-top: 32rpx; }
|
||||
.p-y-24 { padding: 24rpx 0; }
|
||||
.border-t-dashed { border-top: 1px dashed #E2E8F0; }
|
||||
|
||||
.flex-row { display: flex; flex-direction: row; }
|
||||
.flex-jus-sp { justify-content: space-between; }
|
||||
.flex-ali-center { align-items: center; }
|
||||
@@ -342,13 +320,40 @@ export default {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.req-star {
|
||||
margin-left: 6rpx;
|
||||
font-size: 28rpx;
|
||||
color: #fa3534;
|
||||
line-height: 1;
|
||||
}
|
||||
.sec-error {
|
||||
padding: 16rpx;
|
||||
margin-left: -16rpx;
|
||||
margin-right: -16rpx;
|
||||
border-radius: 16rpx;
|
||||
border: 1px solid rgba(250, 53, 52, 0.45);
|
||||
box-shadow: 0 0 8rpx rgba(250, 53, 52, 0.18);
|
||||
background: rgba(254, 240, 240, 0.55);
|
||||
}
|
||||
.err-tip {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #fa3534;
|
||||
}
|
||||
.err-tip-form {
|
||||
margin-top: -8rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.modern-pill-group {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.modern-pill {
|
||||
padding: 12rpx 36rpx;
|
||||
background-color: #F4F6F8;
|
||||
@@ -356,31 +361,25 @@ export default {
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition: all 0.2s;
|
||||
|
||||
&.active {
|
||||
background-color: #00A88A;
|
||||
color: #fff;
|
||||
border-color: #00A88A;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.outline {
|
||||
background-color: #fff;
|
||||
border: 1px solid #E2E8F0;
|
||||
|
||||
&.active {
|
||||
background-color: #E8F6F4;
|
||||
color: #00A88A;
|
||||
border-color: #00A88A;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,309 @@
|
||||
<template>
|
||||
<!--
|
||||
识图传方:就诊人信息独立模块
|
||||
本地 form + v-model,避免小程序 u-input :value/@input 回写失败;
|
||||
父组件确认时通过 getValues() 强制取最新值
|
||||
模板禁止 ?? ?. 与 :class 方法调用
|
||||
-->
|
||||
<view class="patient-panel">
|
||||
<view class="panel-head">
|
||||
<text class="sec-title">就诊人信息</text>
|
||||
<view class="search-btn" :class="{ disabled: searching }" @click="onSearchClick">
|
||||
{{ searching ? '搜索中…' : '搜索匹配' }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">姓名</text>
|
||||
<u-input v-model="formName" placeholder="患者姓名" @blur="onBlurSearch" />
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">手机号</text>
|
||||
<u-input v-model="formMobile" type="number" maxlength="11" placeholder="11位手机号" @blur="onBlurSearch" />
|
||||
</view>
|
||||
<view class="field-row">
|
||||
<view class="field-col">
|
||||
<text class="label">性别</text>
|
||||
<u-radio-group v-model="formSex">
|
||||
<u-radio :name="1">男</u-radio>
|
||||
<u-radio :name="2">女</u-radio>
|
||||
</u-radio-group>
|
||||
</view>
|
||||
<view class="field-col">
|
||||
<text class="label">年龄</text>
|
||||
<u-input v-model="formAge" type="number" placeholder="年龄" />
|
||||
</view>
|
||||
</view>
|
||||
<view class="field">
|
||||
<text class="label">地址</text>
|
||||
<u-input v-model="formAddress" placeholder="可后补" />
|
||||
</view>
|
||||
<view class="match-flag" :class="matchFlagClass">
|
||||
{{ matchFlagText }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
/**
|
||||
* 识图传方就诊人面板:从 ImageOcrDrawer 拆出,负责编辑 + 主动匹配
|
||||
* 用本地 form 承接输入(微信小程序 v-model 更稳),再 change 回写父组件;
|
||||
* 确认提交时父组件调 getValues() 取最新值,不依赖每次 input 是否同步成功
|
||||
*/
|
||||
import { unwrapXkApi } from '@/utils/api-response.js';
|
||||
import { matchTransferPatientApi } from '@/api/prescriptionImageOcr.js';
|
||||
|
||||
export default {
|
||||
name: 'TransferPatientPanel',
|
||||
props: {
|
||||
patientName: { type: String, default: '' },
|
||||
patientMobile: { type: String, default: '' },
|
||||
patientAddress: { type: String, default: '' },
|
||||
patientSex: { type: [Number, String], default: 0 },
|
||||
patientAge: { type: [Number, String], default: '' },
|
||||
userPatientId: { type: [Number, String], default: 0 },
|
||||
patientMatched: { type: Boolean, default: false },
|
||||
/** 诊所管理员等传入专用匹配接口;默认医生 match-transfer-patient */
|
||||
matchApi: { type: Function, default: null },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
/** 本地表单:真正绑定输入,避免 :value 单向绑定回写失败 */
|
||||
formName: '',
|
||||
formMobile: '',
|
||||
formAddress: '',
|
||||
formSex: 0,
|
||||
formAge: '',
|
||||
/** 匹配态也落本地,getValues 时与表单一并返回 */
|
||||
formUserPatientId: 0,
|
||||
formMatched: false,
|
||||
searching: false,
|
||||
/** 防抖:失焦后短延迟再搜,避免连点 */
|
||||
blurTimer: null,
|
||||
/** 上次已搜过的指纹,避免重复请求 */
|
||||
lastSearchKey: '',
|
||||
/** props→form 同步中,跳过 form watch 的 emit,避免回环 */
|
||||
syncingFromProps: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
matchFlagClass() {
|
||||
if (this.formMatched) return 'text-primary';
|
||||
return 'text-warn';
|
||||
},
|
||||
matchFlagText() {
|
||||
if (this.formMatched) {
|
||||
return '已匹配到已有就诊人,提交将复用';
|
||||
}
|
||||
// 进接诊配置下确认时会自动建档;落传方订单时按填写信息提交
|
||||
return '未匹配到已有就诊人,提交时将自动创建或按填写信息落单';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
/** OCR/父组件回填时同步进本地 form */
|
||||
patientName: { immediate: true, handler(v) { this.applyPropToForm('formName', String(v == null ? '' : v)); } },
|
||||
patientMobile: { immediate: true, handler(v) { this.applyPropToForm('formMobile', String(v == null ? '' : v)); } },
|
||||
patientAddress: { immediate: true, handler(v) { this.applyPropToForm('formAddress', String(v == null ? '' : v)); } },
|
||||
patientSex: { immediate: true, handler(v) { this.applyPropToForm('formSex', Number(v || 0)); } },
|
||||
patientAge: { immediate: true, handler(v) { this.applyPropToForm('formAge', v == null ? '' : String(v)); } },
|
||||
userPatientId: { immediate: true, handler(v) { this.applyPropToForm('formUserPatientId', Number(v || 0)); } },
|
||||
patientMatched: { immediate: true, handler(v) { this.applyPropToForm('formMatched', !!v); } },
|
||||
/** 本地表单变更回写父组件;改姓名/手机时清匹配态 */
|
||||
formName() { this.onLocalIdentityChange(); },
|
||||
formMobile() { this.onLocalIdentityChange(); },
|
||||
formAddress() { this.emitFormChange(); },
|
||||
formSex() { this.emitFormChange(); },
|
||||
formAge() { this.emitFormChange(); },
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.blurTimer) {
|
||||
clearTimeout(this.blurTimer);
|
||||
this.blurTimer = null;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* props 同步进本地字段;标记 syncingFromProps 避免立刻再 emit 回父组件
|
||||
*/
|
||||
applyPropToForm(key, value) {
|
||||
if (this[key] === value) return;
|
||||
this.syncingFromProps = true;
|
||||
this[key] = value;
|
||||
this.$nextTick(() => {
|
||||
this.syncingFromProps = false;
|
||||
});
|
||||
},
|
||||
/**
|
||||
* 姓名/手机变更:清匹配并回写(换人后旧 user_patient_id 不能沿用)
|
||||
*/
|
||||
onLocalIdentityChange() {
|
||||
if (this.syncingFromProps) return;
|
||||
this.formMatched = false;
|
||||
this.formUserPatientId = 0;
|
||||
this.emitFormChange();
|
||||
},
|
||||
/** 把本地 form 整包 emit 给父组件 */
|
||||
emitFormChange() {
|
||||
if (this.syncingFromProps) return;
|
||||
this.$emit('change', this.getValues());
|
||||
},
|
||||
/**
|
||||
* 供父组件确认提交时强制取最新值
|
||||
* 不依赖每次 input 是否成功同步到父组件 data
|
||||
*/
|
||||
getValues() {
|
||||
return {
|
||||
patientName: String(this.formName || ''),
|
||||
patientMobile: String(this.formMobile || ''),
|
||||
patientAddress: String(this.formAddress || ''),
|
||||
patientSex: Number(this.formSex || 0),
|
||||
patientAge: this.formAge == null ? '' : String(this.formAge),
|
||||
userPatientId: Number(this.formUserPatientId || 0),
|
||||
patientMatched: !!this.formMatched,
|
||||
};
|
||||
},
|
||||
/** 失焦后若姓名或手机有变化再搜一次 */
|
||||
onBlurSearch() {
|
||||
if (this.blurTimer) clearTimeout(this.blurTimer);
|
||||
this.blurTimer = setTimeout(() => {
|
||||
this.blurTimer = null;
|
||||
this.doSearch({ silent: true });
|
||||
}, 280);
|
||||
},
|
||||
/** 点击「搜索匹配」:显式非静默,缺字段会 toast */
|
||||
onSearchClick() {
|
||||
this.doSearch({ silent: false });
|
||||
},
|
||||
/**
|
||||
* 按本地姓名/手机查是否已有就诊人(不用 props,避免回写失败导致搜空)
|
||||
* @param {{ silent?: boolean }} opts silent=true 时缺字段不 toast
|
||||
*/
|
||||
async doSearch(opts) {
|
||||
const silent = !!(opts && opts.silent);
|
||||
const name = String(this.formName || '').trim();
|
||||
const mobile = String(this.formMobile || '').trim();
|
||||
if (!name && !mobile) {
|
||||
if (!silent) {
|
||||
uni.showToast({ title: '请先填写姓名或手机号', icon: 'none' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
const key = name + '|' + mobile;
|
||||
if (key === this.lastSearchKey && this.formMatched) {
|
||||
return;
|
||||
}
|
||||
if (this.searching) return;
|
||||
this.searching = true;
|
||||
try {
|
||||
const requestMatch = this.matchApi || matchTransferPatientApi;
|
||||
const res = await requestMatch({
|
||||
patient_name: name,
|
||||
patient_mobile: mobile,
|
||||
});
|
||||
const { ok, payload, message } = unwrapXkApi(res);
|
||||
if (!ok || !payload) {
|
||||
if (!silent) {
|
||||
uni.showToast({ title: message || '搜索失败', icon: 'none' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.lastSearchKey = key;
|
||||
const matched = !!payload.matched;
|
||||
// 先写本地 form,再 emit,保证 getValues 立刻可读到匹配结果
|
||||
this.syncingFromProps = true;
|
||||
this.formMatched = matched;
|
||||
this.formUserPatientId = Number(payload.user_patient_id || 0);
|
||||
if (matched) {
|
||||
if (payload.name) this.formName = String(payload.name);
|
||||
if (payload.mobile) this.formMobile = String(payload.mobile);
|
||||
if (payload.sex) this.formSex = Number(payload.sex);
|
||||
if (payload.age) this.formAge = String(payload.age);
|
||||
if (payload.address) this.formAddress = String(payload.address);
|
||||
}
|
||||
this.$nextTick(() => {
|
||||
this.syncingFromProps = false;
|
||||
this.emitFormChange();
|
||||
});
|
||||
if (!silent) {
|
||||
uni.showToast({
|
||||
title: matched ? '已匹配到就诊人' : '未匹配,提交时将自动创建或按填写信息落单',
|
||||
icon: 'none',
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (!silent) {
|
||||
uni.showToast({ title: (e && e.message) || '搜索失败', icon: 'none' });
|
||||
}
|
||||
} finally {
|
||||
this.searching = false;
|
||||
}
|
||||
},
|
||||
/** 供父组件 OCR 回填后立刻触发一次匹配 */
|
||||
searchAfterOcr() {
|
||||
this.lastSearchKey = '';
|
||||
return this.doSearch({ silent: true });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.patient-panel {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.panel-head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.sec-title {
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #16302c;
|
||||
}
|
||||
.search-btn {
|
||||
padding: 8rpx 20rpx;
|
||||
font-size: 24rpx;
|
||||
color: #008771;
|
||||
border-radius: 12rpx;
|
||||
border: 1px solid rgba(106, 205, 187, 0.45);
|
||||
box-shadow: 0 0 6px rgba(106, 205, 187, 0.16);
|
||||
background: rgba(106, 205, 187, 0.1);
|
||||
}
|
||||
.search-btn.disabled {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.field {
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.field-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
.field-col {
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
.field-col:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.match-flag {
|
||||
margin-bottom: 12rpx;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.text-primary {
|
||||
color: #008771;
|
||||
}
|
||||
.text-warn {
|
||||
color: #c47a12;
|
||||
}
|
||||
</style>
|
||||
@@ -321,6 +321,19 @@ export default {
|
||||
drugUseWayList: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
/**
|
||||
* 可注入药库搜索(诊所传方传 clinicSearchTransferDrugs)
|
||||
* 不传则走医生 getProductListDoctorReception / 推广员列表
|
||||
*/
|
||||
drugListApi: {
|
||||
type: Function,
|
||||
default: null
|
||||
},
|
||||
/** 取价门店;传方编辑页用订单 store_id,避免只靠本地缓存 */
|
||||
storeId: {
|
||||
type: [String, Number],
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -529,6 +542,20 @@ export default {
|
||||
* @param {string} keyword - 搜索关键词
|
||||
* 职责:调用接口获取中药列表
|
||||
*/
|
||||
/**
|
||||
* 从各端药库响应里抽出列表(xk-api result / Yii data / 直数组)
|
||||
* 为什么单独抽:诊所注入 API 与医生默认接口返回壳不一致,不能写死一种
|
||||
*/
|
||||
unwrapDrugListPayload(res) {
|
||||
if (!res) return [];
|
||||
if (Array.isArray(res)) return res;
|
||||
if (Array.isArray(res.result)) return res.result;
|
||||
if (Array.isArray(res.data)) return res.data;
|
||||
if (res.result && Array.isArray(res.result.items)) return res.result.items;
|
||||
if (res.data && Array.isArray(res.data.items)) return res.data.items;
|
||||
if (res.payload && Array.isArray(res.payload)) return res.payload;
|
||||
return [];
|
||||
},
|
||||
async loadDrugList(keyword = '') {
|
||||
const name = (keyword || '').trim();
|
||||
if (!name) {
|
||||
@@ -543,24 +570,29 @@ export default {
|
||||
this.loading = true;
|
||||
try {
|
||||
let list = [];
|
||||
if (this.salespersonTransferMode) {
|
||||
const storeId = Number(this.storeId || 0) || uni.getStorageSync('store_id') || 0;
|
||||
const params = {
|
||||
store_id: storeId || undefined,
|
||||
type: 1,
|
||||
name: name,
|
||||
page: this.page,
|
||||
page_size: this.pageSize
|
||||
};
|
||||
if (this.registerId) {
|
||||
params.register_id = this.registerId;
|
||||
}
|
||||
// 传方编辑等场景注入诊所/自定义搜药,优先走注入接口
|
||||
if (typeof this.drugListApi === 'function') {
|
||||
const res = await this.drugListApi(params);
|
||||
list = this.unwrapDrugListPayload(res);
|
||||
this.hasMore = list.length >= this.pageSize;
|
||||
} else if (this.salespersonTransferMode) {
|
||||
const wrap = await getClinicSalespersonChineseDrugList({ name });
|
||||
if (wrap && wrap.ok) {
|
||||
list = Array.isArray(wrap.data) ? wrap.data : [];
|
||||
}
|
||||
this.hasMore = false;
|
||||
} else {
|
||||
const storeId = uni.getStorageSync('store_id') || 11001;
|
||||
const params = {
|
||||
store_id: storeId,
|
||||
type: 1,
|
||||
name: name,
|
||||
page: this.page,
|
||||
page_size: this.pageSize
|
||||
};
|
||||
if (this.registerId) {
|
||||
params.register_id = this.registerId;
|
||||
}
|
||||
const res = await getProductListDoctorReception(params);
|
||||
if (res && (res.code === 0 || res.errcode === 0)) {
|
||||
list = res.data || res.result || [];
|
||||
|
||||
@@ -37,16 +37,16 @@
|
||||
:lower-threshold="100"
|
||||
>
|
||||
<view class="scroll-body-pad">
|
||||
<!-- 常用诊断:有搜索结果时隐藏,避免与搜索列表抢视线 -->
|
||||
<!-- 常用诊断:限制高度可内滚,避免占满弹层首屏 -->
|
||||
<view
|
||||
class="section white radius-16 p-32 m-b-24 shadow-sm"
|
||||
class="section white radius-16 p-24 m-b-24 shadow-sm common-dx-section"
|
||||
v-if="doctorMyDiseaseList.length > 0 && allDiagnosisList.length === 0"
|
||||
>
|
||||
<view class="section-header flex-row flex-jus-sp flex-ali-center m-b-24">
|
||||
<d-text text="常用诊断" className="fs-30 font-bold color-title"></d-text>
|
||||
<d-text :text="`${doctorMyDiseaseList.length}项`" className="fs-26 color-sub"></d-text>
|
||||
<view class="section-header flex-row flex-jus-sp flex-ali-center m-b-16">
|
||||
<d-text text="常用诊断" className="fs-28 font-bold color-title"></d-text>
|
||||
<d-text :text="`${doctorMyDiseaseList.length}项`" className="fs-24 color-sub"></d-text>
|
||||
</view>
|
||||
<view class="modern-tag-container">
|
||||
<view class="modern-tag-container common-dx-tags">
|
||||
<view
|
||||
v-for="item in doctorMyDiseaseList"
|
||||
:key="item.id"
|
||||
@@ -539,6 +539,20 @@ export default {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
/* 常用诊断:高度封顶,多标签时区内滚动,不把搜索区顶没 */
|
||||
.common-dx-section {
|
||||
max-height: 240rpx;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.common-dx-tags {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
max-height: 160rpx;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modern-tag-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,143 @@
|
||||
/**
|
||||
* 中药复用 / 常用方填方前,与当前门店价对齐
|
||||
* 为什么:保存快照不会跟着供货比例变,价格不一致时必须医生确认后再决定用最新价还是原价
|
||||
*/
|
||||
import { compareChineseDrugPricesApi } from '@/api/reception.js';
|
||||
import { unwrapXkApi } from '@/utils/api-response.js';
|
||||
|
||||
/** 弹窗里展示的金额;空快照显示「无」 */
|
||||
function formatMoney(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return '无';
|
||||
}
|
||||
const num = Number(value);
|
||||
if (Number.isNaN(num)) {
|
||||
return '无';
|
||||
}
|
||||
return num.toFixed(2);
|
||||
}
|
||||
|
||||
/** 拼确认文案,控制长度避免小程序弹窗被截断 */
|
||||
function buildPriceChangeText(items, missing) {
|
||||
const lines = ['以下药品与当前门店价不一致,是否按最新供货价和零售价计算?'];
|
||||
const shown = items.slice(0, 5);
|
||||
shown.forEach((item) => {
|
||||
const parts = [];
|
||||
if (Number(item.buy_price_changed) === 1) {
|
||||
parts.push('供货价 ' + formatMoney(item.old_buy_price) + '→' + formatMoney(item.latest_buy_price));
|
||||
}
|
||||
if (Number(item.price_changed) === 1) {
|
||||
parts.push('零售价 ' + formatMoney(item.old_price) + '→' + formatMoney(item.latest_price));
|
||||
}
|
||||
lines.push((item.drug_name || '药品') + ':' + parts.join(','));
|
||||
});
|
||||
if (items.length > shown.length) {
|
||||
lines.push('等共 ' + items.length + ' 味');
|
||||
}
|
||||
if (missing && missing.length) {
|
||||
const names = missing.slice(0, 3).map((item) => item.drug_name || '药品').join('、');
|
||||
lines.push('未上架将保留原价:' + names + (missing.length > 3 ? '等' : ''));
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/** 确认是否改用最新价;取消或关闭都保留原价 */
|
||||
function confirmUseLatest(items, missing) {
|
||||
return new Promise((resolve) => {
|
||||
uni.showModal({
|
||||
title: '药品价格已变化',
|
||||
content: buildPriceChangeText(items, missing),
|
||||
confirmText: '用最新价',
|
||||
cancelText: '保留原价',
|
||||
success: (res) => resolve(!!(res && res.confirm)),
|
||||
fail: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** 把选中的最新价写回药品行,并锁定为后续调价的基准 */
|
||||
export function applyLatestPrices(drugs, items) {
|
||||
const latestById = {};
|
||||
items.forEach((item) => {
|
||||
latestById[Number(item.drug_id)] = item;
|
||||
});
|
||||
drugs.forEach((drug) => {
|
||||
const hit = latestById[Number(drug.id || drug.drug_id || 0)];
|
||||
if (!hit) {
|
||||
return;
|
||||
}
|
||||
drug.price = hit.latest_price;
|
||||
drug.origin_price = hit.latest_price;
|
||||
// 诊所不可查看毛利率时接口不回供货价,不能写成空,结算仍用原来的供货价
|
||||
if (hit.latest_buy_price !== null && hit.latest_buy_price !== undefined && hit.latest_buy_price !== '') {
|
||||
drug.buy_price = hit.latest_buy_price;
|
||||
drug.origin_buy_price = hit.latest_buy_price;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 拉取与当前门店价不一致的中药行
|
||||
* 为什么:接诊页要标出可更新的药,导入确认和批量按钮共用这一次对比
|
||||
* @param {{registerId?: number, storeId?: number, drugs: Array, compareOrigin?: boolean, silent?: boolean}} options
|
||||
*/
|
||||
export async function fetchChinesePriceDiff(options) {
|
||||
const drugs = (options && options.drugs) || [];
|
||||
if (!drugs.length) {
|
||||
return { items: [], missing: [] };
|
||||
}
|
||||
const compareOrigin = !!(options && options.compareOrigin);
|
||||
let res;
|
||||
try {
|
||||
res = await compareChineseDrugPricesApi({
|
||||
register_id: Number(options.registerId || 0),
|
||||
store_id: Number(options.storeId || 0),
|
||||
drugs: drugs.map((drug) => ({
|
||||
id: Number(drug.id || drug.drug_id || 0),
|
||||
drug_name: drug.drug_name || drug.name || '',
|
||||
price: compareOrigin ? (drug.origin_price != null ? drug.origin_price : drug.price) : (drug.price != null ? drug.price : null),
|
||||
buy_price: compareOrigin
|
||||
? (drug.origin_buy_price != null ? drug.origin_buy_price : (drug.buy_price != null ? drug.buy_price : null))
|
||||
: (drug.buy_price != null ? drug.buy_price : null),
|
||||
})),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('核对中药最新价格失败', error);
|
||||
if (!options.silent) {
|
||||
uni.showToast({ title: '价格核对失败,已按原价填入', icon: 'none' });
|
||||
}
|
||||
return { items: [], missing: [] };
|
||||
}
|
||||
const wrap = unwrapXkApi(res);
|
||||
if (!wrap.ok) {
|
||||
if (!options.silent) {
|
||||
uni.showToast({ title: wrap.message || '价格核对失败,已按原价填入', icon: 'none' });
|
||||
}
|
||||
return { items: [], missing: [] };
|
||||
}
|
||||
const data = wrap.payload || {};
|
||||
return {
|
||||
items: data.changed && Array.isArray(data.items) ? data.items : [],
|
||||
missing: Array.isArray(data.missing) ? data.missing : [],
|
||||
// null 表示旧接口没带开关,调用方不要覆盖页面上已有的 seeRate
|
||||
see_rate: data.see_rate == null ? null : (Number(data.see_rate) === 1 ? 1 : 0),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 就地决定药品行用快照价还是最新门店价
|
||||
* 核对失败不阻断填方,按原价继续
|
||||
*/
|
||||
export async function alignChineseDrugsToLatestPrice(options) {
|
||||
const drugs = (options && options.drugs) || [];
|
||||
const diff = await fetchChinesePriceDiff(options);
|
||||
const items = diff.items || [];
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
const useLatest = await confirmUseLatest(items, diff.missing || []);
|
||||
if (!useLatest) {
|
||||
return;
|
||||
}
|
||||
applyLatestPrices(drugs, items);
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
<template>
|
||||
<view class="lock-page" :class="cardToneClass">
|
||||
<!-- #ifdef MP-WEIXIN -->
|
||||
<!-- pageGuard(v-if) 与 panelShow(:show) 拆开:拦截返回时先关 show 再 nextTick 挂回,二次侧滑才继续生效 -->
|
||||
<page-container
|
||||
v-if="guardShow && !softMode"
|
||||
:show="guardShow"
|
||||
v-if="pageGuard && !softMode"
|
||||
:show="panelShow"
|
||||
:overlay="false"
|
||||
@beforeleave="onLeave"
|
||||
@leave="onLeave"
|
||||
@@ -234,7 +235,10 @@ export default {
|
||||
components: { LockWarningDecor },
|
||||
data() {
|
||||
return {
|
||||
guardShow: true,
|
||||
/** page-container 挂载态(v-if) */
|
||||
pageGuard: true,
|
||||
/** page-container 可见态(:show),与 pageGuard 拆开避免二次侧滑失效 */
|
||||
panelShow: true,
|
||||
step: 'hint',
|
||||
payload: {},
|
||||
dailyId: 0,
|
||||
@@ -377,7 +381,8 @@ export default {
|
||||
if (soft) {
|
||||
this.softMode = true
|
||||
this.allowLeave = true
|
||||
this.guardShow = false
|
||||
this.panelShow = false
|
||||
this.pageGuard = false
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -439,7 +444,8 @@ export default {
|
||||
if (live.locked === false || live.locked === 0) {
|
||||
this.allowLeave = true
|
||||
clearPublicAccountLock()
|
||||
this.guardShow = false
|
||||
this.panelShow = false
|
||||
this.pageGuard = false
|
||||
goHomeAfterPublicAccountUnlock()
|
||||
return
|
||||
}
|
||||
@@ -551,10 +557,19 @@ export default {
|
||||
onRemarkInput(e) {
|
||||
this.payRemark = (e && e.detail && e.detail.value) || ''
|
||||
},
|
||||
/**
|
||||
* 硬锁页拦截返回:先关 panelShow,再 nextTick 挂回,否则二次侧滑失效
|
||||
* allowLeave 时真正放行(解锁/登出)
|
||||
*/
|
||||
onLeave() {
|
||||
this.guardShow = false
|
||||
if (this.allowLeave || this.softMode) {
|
||||
this.panelShow = false
|
||||
this.pageGuard = false
|
||||
return
|
||||
}
|
||||
this.panelShow = false
|
||||
this.$nextTick(() => {
|
||||
this.guardShow = true
|
||||
this.panelShow = true
|
||||
})
|
||||
},
|
||||
/**
|
||||
@@ -571,7 +586,8 @@ export default {
|
||||
return
|
||||
}
|
||||
this.allowLeave = true
|
||||
this.guardShow = false
|
||||
this.panelShow = false
|
||||
this.pageGuard = false
|
||||
clearPublicAccountLock()
|
||||
clearLoginStorage()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
@@ -809,7 +825,8 @@ export default {
|
||||
uni.showToast({ title: '已解锁', icon: 'none' })
|
||||
this.allowLeave = true
|
||||
clearPublicAccountLock()
|
||||
this.guardShow = false
|
||||
this.panelShow = false
|
||||
this.pageGuard = false
|
||||
setTimeout(() => {
|
||||
goHomeAfterPublicAccountUnlock()
|
||||
}, 300)
|
||||
|
||||
@@ -324,8 +324,8 @@ import {
|
||||
hasVipPermission,
|
||||
VIP_FEATURE,
|
||||
} from '@/utils/vip.js'
|
||||
import VipGate from '@/components/vip-gate/vip-gate.vue'
|
||||
import ProxyPayQrcodeModal from '@/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue'
|
||||
import VipGate from '@/subPackages/sub_workbench/components/vip-gate/vip-gate.vue'
|
||||
import ProxyPayQrcodeModal from '@/subPackages/sub_workbench/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue'
|
||||
|
||||
function emptyHealthInquiry() {
|
||||
return {
|
||||
|
||||
62
utils/imageTransferQuote.js
Normal file
62
utils/imageTransferQuote.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 识图传方报价:请求侧增量组装
|
||||
* 首次全量 drugs;同味数下只传 quote_token + drugs_patch(改了的下标),省流量
|
||||
*/
|
||||
|
||||
/**
|
||||
* 比较两味报价药品是否等价(用于算 patch)
|
||||
* @param {Record<string, any>|null|undefined} a
|
||||
* @param {Record<string, any>|null|undefined} b
|
||||
*/
|
||||
export function isSameQuoteDrug(a, b) {
|
||||
if (!a || !b) return false
|
||||
return (
|
||||
Number(a.drug_id || 0) === Number(b.drug_id || 0) &&
|
||||
Number(a.number || 0) === Number(b.number || 0) &&
|
||||
Number(a.price || 0) === Number(b.price || 0) &&
|
||||
String(a.name || '') === String(b.name || '') &&
|
||||
Number(a.way_id || 0) === Number(b.way_id || 0)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装增量报价入参
|
||||
* @param {{
|
||||
* scalars: Record<string, any>,
|
||||
* drugs: Array<Record<string, any>>,
|
||||
* quoteToken?: string,
|
||||
* lastQuoteDrugs?: Array<Record<string, any>>|null
|
||||
* }} opts
|
||||
* @returns {{ payload: Record<string, any>, nextLastDrugs: Array<Record<string, any>> }}
|
||||
*/
|
||||
export function buildIncrementalQuotePayload(opts) {
|
||||
const scalars = opts.scalars || {}
|
||||
const drugs = Array.isArray(opts.drugs) ? opts.drugs : []
|
||||
const quoteToken = String(opts.quoteToken || '').trim()
|
||||
const prev = Array.isArray(opts.lastQuoteDrugs) ? opts.lastQuoteDrugs : null
|
||||
const payload = Object.assign({}, scalars)
|
||||
if (quoteToken) {
|
||||
payload.quote_token = quoteToken
|
||||
}
|
||||
// 无 token、味数变化、或上次快照缺失 → 全量
|
||||
if (!quoteToken || !prev || prev.length !== drugs.length) {
|
||||
payload.drugs = drugs
|
||||
return { payload, nextLastDrugs: drugs }
|
||||
}
|
||||
const patches = []
|
||||
for (let i = 0; i < drugs.length; i++) {
|
||||
if (!isSameQuoteDrug(prev[i], drugs[i])) {
|
||||
patches.push({ index: i, drug: drugs[i] })
|
||||
}
|
||||
}
|
||||
if (patches.length === 0) {
|
||||
// 只改了地址/剂数/煎法,不带 drugs
|
||||
return { payload, nextLastDrugs: prev }
|
||||
}
|
||||
if (patches.length >= drugs.length) {
|
||||
payload.drugs = drugs
|
||||
} else {
|
||||
payload.drugs_patch = patches
|
||||
}
|
||||
return { payload, nextLastDrugs: drugs }
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
* VIP 权限判断(医生小程序全局方法)
|
||||
* - 履约诊所 permissions 来自 get-current-store-type 的 vip.permissions
|
||||
* - 业务里用 hasVipPermission(code, permissions) / fetchStoreVipPermissions
|
||||
* - 模板显隐可用 components/vip-gate/vip-gate.vue
|
||||
* - 模板显隐可用 subPackages/sub_workbench/components/vip-gate/vip-gate.vue
|
||||
*/
|
||||
import { getCurrentStoreTypeApi } from '@/api/reception.js'
|
||||
|
||||
|
||||
62
xaa
Normal file
62
xaa
Normal file
@@ -0,0 +1,62 @@
|
||||
CURRENT:
|
||||
feature/invoice
|
||||
---STATUS---
|
||||
## feature/invoice...origin/dev
|
||||
M app/Enum/ai/AiGenerationSceneEnum.php
|
||||
M app/Http/Controllers/DoctorWx/DoctorReceptionWxController.php
|
||||
M app/Http/Controllers/admin/ai/AiGenerationController.php
|
||||
M app/Http/Controllers/admin/business/doctor/DoctorReceptionController.php
|
||||
M app/Http/Controllers/admin/finance/TreatmentFeeCashBackfillController.php
|
||||
M app/Http/Controllers/admin/log/OldController.php
|
||||
M app/Http/Controllers/admin/system/StoreController.php
|
||||
M app/Jobs/ProductOrderPaidJob.php
|
||||
M app/Service/DoctorWx/DoctorReceptionWxService.php
|
||||
M app/Service/admin/ai/AiGenerationService.php
|
||||
M app/Service/admin/business/doctor/DoctorReceptionService.php
|
||||
M app/Service/admin/business/order/OrderTraceService.php
|
||||
M app/Service/admin/finance/FundWaterService.php
|
||||
M app/Service/admin/finance/PublicAccountDailyBillService.php
|
||||
M app/Service/admin/finance/ReconciliationService.php
|
||||
M app/Service/admin/finance/TreatmentFeeCashBackfillService.php
|
||||
M app/Service/admin/log/OldLogService.php
|
||||
M app/Service/admin/salesperson/SalespersonTransferPrescriptionService.php
|
||||
M app/Service/admin/system/StoreService.php
|
||||
M app/Service/callback/EplpayCallbackService.php
|
||||
M app/Service/common/ai/AiAgentFactory.php
|
||||
M app/Service/common/ai/AiMedicalAssistService.php
|
||||
M app/Service/common/ai/DeepSeekAiAgent.php
|
||||
M app/Service/common/http/WeChatApiClient.php
|
||||
M app/Service/common/http/mes/MesFailoverClient.php
|
||||
M app/Service/common/http/mes/MesHttpH001Client.php
|
||||
M app/Service/common/http/mes/MesPrescriptionPayloadMapper.php
|
||||
M app/Service/common/patient/UnclaimedPatientService.php
|
||||
M app/Service/finance/AccountAbleChangeLogService.php
|
||||
M app/Service/finance/LedgerService.php
|
||||
M app/Support/ChineseDrugBuyPriceResolver.php
|
||||
M config/ase.php
|
||||
M config/xk.php
|
||||
M routes/admin.php
|
||||
M routes/doctor.php
|
||||
M routes/open-api.php
|
||||
?? app/Enum/imageTransfer/
|
||||
?? app/Enum/store/
|
||||
?? app/Http/Controllers/ClinicAdminWx/ImageTransferOrderWxController.php
|
||||
?? app/Http/Controllers/DoctorWx/ImageTransferOrderWxController.php
|
||||
?? app/Http/Controllers/admin/business/prescription/ImageTransferOrderController.php
|
||||
?? app/Http/Controllers/admin/system/MesOpenAppController.php
|
||||
?? app/Http/Controllers/common/MesOpenApiController.php
|
||||
?? app/Http/Middleware/openApi/
|
||||
?? app/Models/new/finance/OrderLedgerSplitModel.php
|
||||
?? app/Models/new/finance/TreatmentFeeCashBackfillIgnoreModel.php
|
||||
?? app/Models/new/imageTransfer/
|
||||
?? app/Models/new/mes/
|
||||
?? app/Service/DoctorWx/ImageTransferOrderWxService.php
|
||||
?? app/Service/admin/system/MesOpenAppService.php
|
||||
?? app/Service/common/ai/PrescriptionImageOcrService.php
|
||||
?? app/Service/common/http/mes/MesPrescriptionPayloadValidator.php
|
||||
?? app/Service/common/imageTransfer/
|
||||
?? app/Service/common/mes/
|
||||
?? "docs/MES\345\244\204\346\226\271\344\270\255\350\275\254-\347\254\254\344\270\211\346\226\271\345\257\271\346\216\245\350\257\264\346\230\216.md"
|
||||
?? docs/mes-open-api.md
|
||||
?? scripts/mes_open_api_sign_vector.php
|
||||
---BRANCHES---
|
||||
Reference in New Issue
Block a user