diff --git a/.cursor/rules/Code-Standards.md b/.cursor/rules/Code-Standards.md
index 600712f..bae60c7 100644
--- a/.cursor/rules/Code-Standards.md
+++ b/.cursor/rules/Code-Standards.md
@@ -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 再绑定
diff --git a/.cursor/rules/Code-Standards.mdc b/.cursor/rules/Code-Standards.mdc
index 49395ef..5597283 100644
--- a/.cursor/rules/Code-Standards.mdc
+++ b/.cursor/rules/Code-Standards.mdc
@@ -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 再绑定
diff --git a/.cursor/rules/Page-Container-Drawer.mdc b/.cursor/rules/Page-Container-Drawer.mdc
new file mode 100644
index 0000000..7128f12
--- /dev/null
+++ b/.cursor/rules/Page-Container-Drawer.mdc
@@ -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
+
+
+
+```
+
+```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
+
+
+
+
+
+```
+
+## 同页多抽屉互斥(工作台切换诊所已落地)
+
+打开 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 卸挂载以本规则 + 切换账号组件为准**。
diff --git a/api/businessApi.js b/api/businessApi.js
index 6801ad5..3af3ce6 100644
--- a/api/businessApi.js
+++ b/api/businessApi.js
@@ -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 });
},
diff --git a/api/clinicAdmin.js b/api/clinicAdmin.js
index ec160ac..db58dc4 100644
--- a/api/clinicAdmin.js
+++ b/api/clinicAdmin.js
@@ -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 });
}
diff --git a/api/imageTransferOrder.js b/api/imageTransferOrder.js
new file mode 100644
index 0000000..e8830bc
--- /dev/null
+++ b/api/imageTransferOrder.js
@@ -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,
+ });
+}
diff --git a/api/prescriptionImageOcr.js b/api/prescriptionImageOcr.js
new file mode 100644
index 0000000..e4b6fde
--- /dev/null
+++ b/api/prescriptionImageOcr.js
@@ -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 },
+ });
+}
diff --git a/api/reception.js b/api/reception.js
index c2fbbbe..8211875 100644
--- a/api/reception.js
+++ b/api/reception.js
@@ -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')
}
})
}
diff --git a/api/upload.js b/api/upload.js
index 4a58e83..340f2fa 100644
--- a/api/upload.js
+++ b/api/upload.js
@@ -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} 可访问的图片 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 });
+}
diff --git a/pages.json b/pages.json
index b663c68..bc816e9 100644
--- a/pages.json
+++ b/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": {
diff --git a/pages/workbench/index.vue b/pages/workbench/index.vue
index c2d2b23..aa042fc 100644
--- a/pages/workbench/index.vue
+++ b/pages/workbench/index.vue
@@ -8,7 +8,7 @@
-
-
+
+
+
+
+
@@ -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;
},
/** 抽屉内改期成功:预约分布/待接诊数已变化,静默刷新看板(不打断用户继续操作抽屉) */
diff --git a/subPackages/sub_business_shared/components/CallLogDrawer.vue b/subPackages/sub_business_shared/components/CallLogDrawer.vue
index fa0f0c1..05e49f0 100644
--- a/subPackages/sub_business_shared/components/CallLogDrawer.vue
+++ b/subPackages/sub_business_shared/components/CallLogDrawer.vue
@@ -1,13 +1,14 @@
-
+
@@ -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) {
diff --git a/subPackages/sub_business_shared/components/UserPatientDetail.vue b/subPackages/sub_business_shared/components/UserPatientDetail.vue
index 0bde474..90928dc 100644
--- a/subPackages/sub_business_shared/components/UserPatientDetail.vue
+++ b/subPackages/sub_business_shared/components/UserPatientDetail.vue
@@ -1,10 +1,10 @@
-
+
{
+ 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();
},
diff --git a/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue b/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue
index 653431b..c2ccdf3 100644
--- a/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue
+++ b/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue
@@ -1,12 +1,14 @@
-
+
+
@@ -170,11 +172,13 @@
+
+
+
diff --git a/subPackages/sub_business_shared/withdrawal/index.vue b/subPackages/sub_business_shared/withdrawal/index.vue
index b7e26ab..1066570 100644
--- a/subPackages/sub_business_shared/withdrawal/index.vue
+++ b/subPackages/sub_business_shared/withdrawal/index.vue
@@ -59,6 +59,9 @@
提现记录
+
+ 提现规则
+
@@ -91,11 +94,13 @@
+
+
+
diff --git a/subPackages/sub_image_transfer/components/TransferDoctorPicker.vue b/subPackages/sub_image_transfer/components/TransferDoctorPicker.vue
new file mode 100644
index 0000000..4a5cc11
--- /dev/null
+++ b/subPackages/sub_image_transfer/components/TransferDoctorPicker.vue
@@ -0,0 +1,211 @@
+
+
+
+
+
+ 选择开方医生
+ 关闭
+
+
+ 暂无本店医生
+
+
+ {{ (doc.name || '医').slice(0, 1) }}
+
+
+
+
+ {{ doc.name || doc.real_name || ('医生#' + doc.id) }}
+ {{ doc.title }}
+
+ {{ doc.department || doc.depart || '未设置科室' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_image_transfer/components/TransferPrescriptionPad.vue b/subPackages/sub_image_transfer/components/TransferPrescriptionPad.vue
new file mode 100644
index 0000000..63ae6b8
--- /dev/null
+++ b/subPackages/sub_image_transfer/components/TransferPrescriptionPad.vue
@@ -0,0 +1,481 @@
+
+
+
+
+
+
+
+ 处方笺
+ 关闭
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_image_transfer/order-detail.vue b/subPackages/sub_image_transfer/order-detail.vue
new file mode 100644
index 0000000..e3d81d1
--- /dev/null
+++ b/subPackages/sub_image_transfer/order-detail.vue
@@ -0,0 +1,1808 @@
+
+
+
+
+
+ 加载中…
+
+
+
+
+
+
+
+ {{ avatarChar }}
+
+
+ {{ order.patient_name || '未填姓名' }}
+ {{ heroSub }}
+
+
+
+ {{ statusText }}
+
+
+
+
+
+
+ {{ drugs.length }}
+ 药味
+
+
+
+ {{ order.dosage || '-' }}
+ 剂数
+
+
+
+ {{ order.day_dosage || '-' }}
+ 每日次数
+
+
+
+
+
+ 查看处方笺
+
+
+
+
+
+
+
+ {{ Number(order.status) === 2 ? '已驳回,可修改后重新提交' : '驳回原因' }}
+ {{ order.reject_reason }}
+
+
+
+
+
+
+
+ {{ Number(order.status) === 4 ? '推送失败原因' : '传方回执' }}
+ {{ order.mes_message }}
+
+
+
+
+
+
+
+
+ 处方原图
+
+ 点击放大
+
+
+
+
+
+
+
+
+
+
+
+
+ 用药明细
+
+ 共 {{ drugs.length }} 味
+
+ 暂无药品
+
+
+ {{ idx + 1 }}
+
+ {{ drug.name }}
+ {{ drug.number }}g
+
+
+
+
+
+
+
+ 患者信息
+
+
+
+
+ 手机
+ {{ order.patient_mobile || '—' }}
+
+
+
+ 地址
+ {{ order.patient_address || '—' }}
+
+
+
+ 性别年龄
+ {{ patientExtra }}
+
+
+
+
+
+
+
+ 处方信息
+
+
+
+
+ 开方医生
+ {{ order.doctor_name }}
+
+
+
+ 上传诊所
+ {{ order.store_name }}
+
+
+
+ 诊断
+ {{ order.clinical_diagnose }}
+
+
+
+ 医嘱
+ {{ order.doctor_order }}
+
+
+
+ 代煎
+ {{ order.process_rule_label }}
+
+
+
+
+
+
+
+ 费用信息
+
+ {{ orderRegionText }}
+
+
+ 药价
+ ¥{{ order.items_price }}
+
+
+ 代煎费
+ ¥{{ order.process_price }}
+
+
+ 运费
+ ¥{{ order.trans_expenses }}
+
+
+ 合计
+ ¥{{ order.total_price }}
+
+
+
+
+
+
+
+
+
+
+ 患者信息
+
+
+
+ 姓名
+
+
+
+ 手机号
+
+
+
+ 收货地址
+
+
+
+
+
+
+
+
+ 处方信息
+
+
+
+
+ 开方医生*
+
+
+ {{ form.doctor_name || '请选择开方医生' }}
+
+
+
+
+
+ 诊断
+
+
+
+ 医嘱
+
+
+
+
+
+
+
+ 代煎与用量
+ *
+
+ 有未填项
+
+
+
+
+
+
+
+ 费用预览
+
+ {{ quoteRegionText }}
+
+
+ 药价
+ ¥{{ quotePreview.items_price }}
+
+
+ 代煎费
+ ¥{{ quotePreview.process_price }}
+
+
+ 运费
+ ¥{{ quotePreview.trans_expenses }}
+
+
+ 合计
+ ¥{{ quotePreview.total_price }}
+
+ 报价计算中…
+
+
+
+
+
+
+ 用药明细
+
+
+ 共 {{ form.drugs.length }} 味 · 点药换药
+
+
+ 加药
+
+
+
+ 暂无药品,点右上角加药
+
+
+
+ {{ idx + 1 }}
+
+
+ {{ drug.name }}
+ 点击编辑
+
+ {{ drug.number }}g
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_image_transfer/order-list.vue b/subPackages/sub_image_transfer/order-list.vue
new file mode 100644
index 0000000..a2c80fc
--- /dev/null
+++ b/subPackages/sub_image_transfer/order-list.vue
@@ -0,0 +1,847 @@
+
+
+
+
+
+ {{ items.length }}
+ {{ topbarDesc }}
+
+
+
+ 识图传方
+
+
+
+
+
+
+
+ {{ tab.label }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 去识图传方
+
+
+
+
+
+
+ {{ avatarChar(row) }}
+
+
+
+ {{ row.patient_name || '未填姓名' }}
+
+
+ {{ statusText(row) }}
+
+
+ {{ metaText(row) }}
+
+
+
+
+
+ {{ row.reject_reason }}
+
+
+
+ {{ name }}
+ 共 {{ row.drug_count }} 味
+
+ 共 {{ row.drug_count }} 味药
+
+
+
+ {{ row.mes_message }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_image_transfer/upload.vue b/subPackages/sub_image_transfer/upload.vue
new file mode 100644
index 0000000..5081cfc
--- /dev/null
+++ b/subPackages/sub_image_transfer/upload.vue
@@ -0,0 +1,271 @@
+
+
+
+
+
+
+
+ 识图传方
+ 拍摄或上传处方图片,自动识别患者与药味,核对无误后即可提交传方
+
+
+ 上传处方图识别
+
+
+
+
+
+
+ {{ idx + 1 }}
+
+ {{ step }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_online_reception/utils/transferPrescriptionImport.js b/subPackages/sub_online_reception/utils/transferPrescriptionImport.js
index e6d90b4..2e12197 100644
--- a/subPackages/sub_online_reception/utils/transferPrescriptionImport.js
+++ b/subPackages/sub_online_reception/utils/transferPrescriptionImport.js
@@ -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)) {
diff --git a/subPackages/sub_patient/patient_detail.vue b/subPackages/sub_patient/patient_detail.vue
index 9ebcd88..7fad716 100644
--- a/subPackages/sub_patient/patient_detail.vue
+++ b/subPackages/sub_patient/patient_detail.vue
@@ -102,7 +102,14 @@
-
+
diff --git a/subPackages/sub_platform_admin/my/index.vue b/subPackages/sub_platform_admin/my/index.vue
index c3169ba..7c4c417 100644
--- a/subPackages/sub_platform_admin/my/index.vue
+++ b/subPackages/sub_platform_admin/my/index.vue
@@ -1,5 +1,6 @@
-
+
+
-
+
+
-
+
+
-
+
-
- 返回
-
- {{ generating ? '生成中…' : '确认生成' }}
+
+
+ 返回
+
+ {{ generating ? '生成中…' : '确认生成' }}
+
+
+
+
+
+
+
+
+ {{ voiceAsrLoading ? '正在转写…' : '正在聆听,松开发送' }}
+
+
+
+
+
+
+
+ {{ voiceAsrLoading ? '转写中…' : (voiceRecording ? '松开发送' : '按住说话') }}
+
@@ -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;
diff --git a/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig.vue b/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig.vue
index 3c13a6f..0e33d74 100644
--- a/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig.vue
+++ b/subPackages/sub_workbench/prescription_v2/components/ChineseMedicineConfig.vue
@@ -1,151 +1,171 @@
-
+
+ *
- 自制剂
-
+ class="modern-pill"
+ :class="{ active: Number(localConfig.ruleType) === 1, disabled: disabled }"
+ data-rule="1"
+ @click="onRuleTypeClick"
+ >自制剂
- 委托调剂
-
+ class="modern-pill"
+ :class="{ active: Number(localConfig.ruleType) === 2, disabled: disabled }"
+ data-rule="2"
+ @click="onRuleTypeClick"
+ >委托调剂
+ 请选择制剂方式
-
-
+
+
+ *
- {{ item.name }}
-
+ 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 }}
+ 请选择包法
-
-
-
-
-
-
-
-
+
+
+
+
+ *
+
+
+
+
- {{ item.name }}
+ :class="{ active: Number(localConfig.processRuleId) === Number(item.id), disabled: disabled }"
+ :data-id="item.id"
+ @click="onProcessRuleClick"
+ >{{ item.name }}
+ 请选择制剂要求
-
-
-
-
-
-
+
+
+ *
+
+
+
+
- {{ item.name }}
+ :class="{ active: Number(localConfig.childProcessRuleId) === Number(item.id), disabled: disabled }"
+ :data-id="item.id"
+ @click="onChildProcessRuleClick"
+ >{{ item.name }}
+ 请选择煎法说明
-
-
-
-
-
-
+
+
+ *
+
+
+
+
- {{ item.note || item.name }}
+ :class="{ active: Number(localConfig.processRuleNoteId) === Number(item.id), disabled: disabled }"
+ :data-id="item.id"
+ @click="onProcessRuleNoteClick"
+ >{{ item.note || item.name }}
+ 请选择附加备注
-
+
-
-
+
+
+ *
+ 请填写剂数
-
-
+
+
+ *
+ 请填写用药频次
\ No newline at end of file
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/ImageOcrDrawer.vue b/subPackages/sub_workbench/prescription_v2/components/ImageOcrDrawer.vue
new file mode 100644
index 0000000..3e54fa6
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/ImageOcrDrawer.vue
@@ -0,0 +1,1969 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ 仅支持中药处方图。未匹配药可按药名、首拼或全拼搜库。
+
+ 我的生成记录
+ 加载中…
+ 暂无记录,识别后会出现在这里
+
+
+
+ {{ row.name || ('识图 #' + row.id) }}
+ {{ row.item_count || 0 }}味 · {{ row.time_text }}
+ 重新识别
+
+
+
+
+
+
+
+ 处方图片
+
+
+
+ ×
+
+ +
+
+
+
+
+ AI 处方识别
+ 正在解析处方图像…
+
+
+
+
+ +
+
+
+ 开始识别
+
+
+
+
+ 核对识别结果
+ 重新上传识别
+
+
+ 处方原图(点图放大)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 开方医生*
+
+
+ {{ selectedDoctorName || '请选择开方医生' }}
+
+
+
+
+
+
+ 处方信息
+
+ 诊断
+
+
+
+ 医嘱
+
+
+ {{ herbStatText }}
+
+
+ 代煎方式:{{ processRuleLabel }}
+ 未匹配到已配置的代煎方式
+
+
+ 剂数
+ 已按天数生成
+
+
+
+ 每日次数
+
+
+
+
+
+
+ 剂数已按处方天数生成,可再改
+
+
+
+
+
+
+ 费用预览
+ 解析地区:{{ quoteRegionText }}
+
+ 药价
+ ¥{{ quotePreview.items_price }}
+
+
+ 代煎费
+ ¥{{ quotePreview.process_price }}
+
+
+ 运费
+ ¥{{ quotePreview.trans_expenses }}
+
+
+ 合计
+ ¥{{ quotePreview.total_price }}
+
+ 报价计算中…
+
+
+
+ 已对照({{ matchedList.length }})
+
+
+ {{ item._import ? '✓' : '' }}
+ {{ item.ai_name }}
+ {{ item.dose }}{{ item.unit }}
+
+
+
+ {{ c.drug_name }}
+
+
+
+
+
+ 未对照({{ unmatchedList.length }})
+
+ {{ u.ai_name }} {{ u.dose }}{{ u.unit }}
+ 已选:{{ unmatchedPicked[ui].drug_name }}
+
+
+ 搜
+
+
+ {{ c.drug_name || c.name }}
+
+ 去开方搜
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/TransferPatientPanel.vue b/subPackages/sub_workbench/prescription_v2/components/TransferPatientPanel.vue
new file mode 100644
index 0000000..2bc86ee
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/components/TransferPatientPanel.vue
@@ -0,0 +1,309 @@
+
+
+
+
+ 就诊人信息
+
+ {{ searching ? '搜索中…' : '搜索匹配' }}
+
+
+
+ 姓名
+
+
+
+ 手机号
+
+
+
+
+ 性别
+
+ 男
+ 女
+
+
+
+ 年龄
+
+
+
+
+ 地址
+
+
+
+ {{ matchFlagText }}
+
+
+
+
+
+
+
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue
index cd763f8..8f90a08 100644
--- a/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/ChineseMedicineModal.vue
@@ -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 || [];
diff --git a/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue b/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue
index df27a4b..32d301d 100644
--- a/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue
+++ b/subPackages/sub_workbench/prescription_v2/components/modals/DiagnosisModal.vue
@@ -37,16 +37,16 @@
:lower-threshold="100"
>
-
+
-
@@ -853,6 +863,7 @@ import {
import { parseTransferPrescriptionContent } from '@/subPackages/sub_online_reception/utils/transferPrescriptionImport.js';
import { getChatRoomByRegisterApi, sendToUserHttpApi } from '@/api/chat.js';
import { PrescriptionStorage } from './utils/prescriptionStorage.js';
+import { alignChineseDrugsToLatestPrice, applyLatestPrices, fetchChinesePriceDiff } from './utils/alignChineseDrugPrice.js';
import { PrescriptionCalculator } from './utils/prescriptionCalculator.js';
import { PrescriptionValidator } from './utils/prescriptionValidator.js';
import {
@@ -864,6 +875,7 @@ import DiagnosisModal from './components/modals/DiagnosisModal.vue';
import DoctorOrderModal from './components/modals/DoctorOrderModal.vue';
import MedicalRecordPanel from './components/MedicalRecordPanel.vue';
import AiPrescriptionDrawer from './components/AiPrescriptionDrawer.vue';
+import ImageOcrDrawer from './components/ImageOcrDrawer.vue';
import { saveMedicalRecordApi } from '@/api/reception.js';
import CommonPrescriptionModal from './components/modals/CommonPrescriptionModal.vue';
import GoldenFormulaModal from './components/modals/GoldenFormulaModal.vue';
@@ -877,7 +889,8 @@ import TransferPatientDrawer from './components/modals/TransferPatientDrawer.vue
import ApplyPublicAccountPayDrawer from './components/modals/ApplyPublicAccountPayDrawer.vue';
import { applyPublicAccountPayApi } from '@/api/publicAccountPay.js';
import { unwrapXkApi } from '@/utils/api-response.js';
-import ProxyPayQrcodeModal from '@/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue';
+import { prepareDoctorImportApi } from '@/api/prescriptionImageOcr.js';
+import ProxyPayQrcodeModal from '@/subPackages/sub_workbench/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue';
import WarehouseSelectDrawer from './components/modals/WarehouseSelectDrawer.vue';
import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue';
import { applyRatioToDrugs, formatPriceDiscountLabel, normalizeQuickOptions } from '@/subPackages/sub_business_shared/common/pricePercentAdjust.js';
@@ -914,6 +927,7 @@ const PRESCRIPTION_OVERLAY_KEYS = [
'showWarehouseSelectDrawer',
'showMedicalRecordOverlay',
'showAiPrescriptionDrawer',
+ 'showImageOcrDrawer',
'showGoldenFormulaModal',
'showPublicAccountApply',
];
@@ -925,6 +939,7 @@ export default {
DoctorOrderModal,
MedicalRecordPanel,
AiPrescriptionDrawer,
+ ImageOcrDrawer,
CommonPrescriptionModal,
GoldenFormulaModal,
WesternMedicineModal,
@@ -974,6 +989,9 @@ export default {
tabChanging: false,
activeCategory: 2,
currentDrugs: [],
+ /** 中药与当前门店价不一致的行,key 为药品 id */
+ staleChinesePriceMap: {},
+ staleChinesePriceCount: 0,
diagnoses: [],
diagnosisText: '',
medicalAdvice: '',
@@ -1002,6 +1020,7 @@ export default {
showDoctorOrderModal: false,
showCommonPrescriptionModal: false,
showAiPrescriptionDrawer: false,
+ showImageOcrDrawer: false,
showGoldenFormulaModal: false,
/** 开方成功页 + 申请公账抽屉 */
sentSuccess: false,
@@ -1091,6 +1110,14 @@ export default {
height: '76rpx',
padding: '0 32rpx'
},
+ /** 临床诊断紧凑输入:比 illnessStyle 更矮,少占滚动首屏 */
+ dxCompactInputStyle: {
+ fontSize: '26rpx',
+ backgroundColor: '#F7F8FA',
+ borderRadius: '12rpx',
+ height: '64rpx',
+ padding: '0 20rpx'
+ },
entrustStyle: {
fontSize: '28rpx',
backgroundColor: '#F7F8FA',
@@ -1109,9 +1136,13 @@ export default {
borderRadius: '8rpx',
padding: '0 16rpx'
},
- /** 微信小程序 page-container:有抽屉/弹层时为 true,拦截侧滑等返回 */
+ /** 对齐切换账号 pageGuard:原生 page-container 挂载开关(v-if) */
+ pageBackGuardMounted: false,
+ /** 对齐切换账号 visible:page-container :show */
pageBackGuardShow: false,
- /** 程序化隐藏 page-container 时抑制 @leave,避免误触发页面退出确认 */
+ /** 对齐切换账号 releaseTimer:关 show 后延迟卸挂载 */
+ _pageBackGuardReleaseTimer: null,
+ /** 程序化关拦截时抑制 @leave(退栈/跳转用,切换账号无此场景) */
_suppressPageBackGuardLeave: false,
/** 推广员传方模式 */
isSalespersonTransferMode: false,
@@ -1246,10 +1277,15 @@ export default {
if (this.activeCategory !== 1 || Number(this.seeRate) !== 1) {
return '';
}
- return PrescriptionCalculator.calculateChineseTotalGrossMarginPercent(
+ const text = PrescriptionCalculator.calculateChineseTotalGrossMarginPercent(
this.currentDrugs,
this.chineseConfig.dosage
);
+ // 供货价不齐时计算器返回 --,不能当成可展示的毛利率
+ if (!text || text === '--') {
+ return '';
+ }
+ return text;
},
showSalespersonTransferPrice() {
return !this.isSalespersonTransferMode || Number(this.salespersonSeePrice) === 1;
@@ -1280,18 +1316,90 @@ export default {
return isSpecialPrescriptionCartLocked(this);
},
/**
- * 底部发送按钮宽度
- * 与病历一致:右侧有 AI/语音胶囊时主按钮收窄;无则拉宽
+ * 是否展示「另存常用方」(上行次要)
*/
- bottomSendBtnWidth() {
- if (this.isSalespersonTransferMode || this.isCommonPrescription) {
- return '686rpx';
+ showRxSaveAsCommon() {
+ return !this.isSalespersonTransferMode
+ && !this.isCommonPrescription
+ && this.supportsCommonPrescriptionTemplate;
+ },
+ /**
+ * 是否展示 AI辅助 / 语音开方(上行次要)
+ */
+ showRxAiActions() {
+ return !this.isSpecialPrescriptionCartLocked
+ && !this.isSalespersonTransferMode
+ && !this.isCommonPrescription
+ && this.canUseAiPrescription;
+ },
+ /**
+ * 是否展示金方(上行 VIP;仅中药 Tab,与原中药区入口一致)
+ */
+ showRxGoldenFormula() {
+ return !this.isSpecialPrescriptionCartLocked
+ && !this.isSalespersonTransferMode
+ && !this.isCommonPrescription
+ && Number(this.activeCategory) === 1
+ && this.canUseGoldenFormula;
+ },
+ /**
+ * 上行次要/VIP 操作列表(后续新 VIP 入口往这里加即可)
+ * 短文案 + icon,配合横滑,避免底栏挤爆
+ */
+ rxBottomSecondaryActions() {
+ const list = [];
+ if (this.showRxSaveAsCommon) {
+ list.push({
+ key: 'save_common',
+ label: '另存',
+ icon: 'star',
+ color: '#6ACDBB',
+ tone: 'teal',
+ });
}
- // 右侧「AI辅助」+「语音开方」两枚胶囊 + 发送
- if (!this.isSpecialPrescriptionCartLocked && this.canUseAiPrescription) {
- return '180rpx';
+ if (this.showRxAiActions) {
+ list.push({
+ key: 'ai_assist',
+ label: 'AI出方',
+ icon: 'edit-pen',
+ color: '#6ACDBB',
+ tone: 'teal',
+ });
+ list.push({
+ key: 'ai_voice',
+ label: '语音',
+ icon: 'mic',
+ color: '#6ACDBB',
+ tone: 'teal',
+ });
}
- return '480rpx';
+ if (!this.isSpecialPrescriptionCartLocked
+ && !this.isSalespersonTransferMode
+ && !this.isCommonPrescription) {
+ list.push({
+ key: 'image_ocr',
+ label: '识图',
+ icon: 'camera',
+ color: '#6ACDBB',
+ tone: 'teal',
+ });
+ }
+ if (this.showRxGoldenFormula) {
+ list.push({
+ key: 'golden',
+ label: '金方',
+ icon: 'level',
+ color: '#D4A017',
+ tone: 'gold',
+ });
+ }
+ return list;
+ },
+ /**
+ * 上行是否有任一操作;全无则整行不渲染
+ */
+ showRxBottomSecondary() {
+ return this.rxBottomSecondaryActions.length > 0;
},
/** 门店 VIP:AI辅助出方 */
canUseAiPrescription() {
@@ -1358,6 +1466,18 @@ export default {
if (this[PRESCRIPTION_OVERLAY_KEYS[i]]) return true;
}
return false;
+ },
+ /** 中药基准价签名,变化后才去核对门店最新价 */
+ chinesePriceSignature() {
+ if (this.activeCategory !== 1) {
+ return '';
+ }
+ const drugs = this.currentDrugs || [];
+ return drugs.map((drug) => {
+ const price = drug.origin_price != null ? drug.origin_price : drug.price;
+ const buy = drug.origin_buy_price != null ? drug.origin_buy_price : drug.buy_price;
+ return String(drug.id || drug.drug_id || '') + ':' + price + ':' + buy;
+ }).join('|');
}
},
watch: {
@@ -1369,13 +1489,21 @@ export default {
},
// #ifdef MP-WEIXIN
shouldEnablePageBackGuard(val) {
- this.setPageBackGuardShow(!!val, { suppressLeave: !val });
+ if (val) {
+ this.openPageBackGuard();
+ } else {
+ this.closePageBackGuard({ suppressLeave: true });
+ }
this.syncSalespersonTransferUnloadAlert();
},
hasTransferDraft() {
this.syncSalespersonTransferUnloadAlert();
},
// #endif
+ /** 中药基准价变化后核对是否落后于当前门店价 */
+ chinesePriceSignature() {
+ this.scheduleChinesePriceDiff();
+ },
},
created() {
// u-navbar 在小程序内 bind($parent) 可能拿不到页面实例,用箭头函数固定 this
@@ -1414,6 +1542,20 @@ export default {
this._categoryLockedByQuery = true;
}
}
+ this._pendingTransferId = options.transfer_id ? String(options.transfer_id) : '';
+ try {
+ const raw = uni.getStorageSync('doctor_image_transfer_import');
+ if (raw && !this._pendingTransferId) {
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
+ if (parsed && parsed.transfer_id) {
+ this._pendingTransferId = String(parsed.transfer_id);
+ if (!this.registerId && parsed.register_id) {
+ this.registerId = String(parsed.register_id);
+ }
+ }
+ uni.removeStorageSync('doctor_image_transfer_import');
+ }
+ } catch (e) { /* ignore */ }
this.initializePageData();
},
async onShow() {
@@ -1424,6 +1566,11 @@ export default {
if (!this.isSalespersonTransferMode && this.activeCategory === 1) {
await this.checkAndShowTransferTip();
}
+ if (this._pendingTransferId && this.registerId) {
+ const tid = this._pendingTransferId;
+ this._pendingTransferId = '';
+ await this.autoImportImageTransfer(tid);
+ }
},
onUnload() {
if (this._tabChangingTimer) {
@@ -1434,16 +1581,168 @@ export default {
uni.removeStorageSync('add');
}
// #ifdef MP-WEIXIN
+ this.clearPageBackGuardReleaseTimer();
if (this.isSalespersonTransferMode) {
uni.disableAlertBeforeUnload();
}
// #endif
},
methods: {
+ /**
+ * 稍后再核对中药价,避免清单连续变动时重复请求
+ */
+ scheduleChinesePriceDiff() {
+ if (this._chinesePriceDiffTimer) {
+ clearTimeout(this._chinesePriceDiffTimer);
+ }
+ this._chinesePriceDiffTimer = setTimeout(() => {
+ this.refreshChinesePriceDiff();
+ }, 300);
+ },
+ /**
+ * 标记哪些中药还不是当前门店价
+ * 为什么:保留原价填入后,批量按钮和单味「更新价格」要知道点谁
+ */
+ async refreshChinesePriceDiff() {
+ if (this.activeCategory !== 1 || this.isSpecialPrescriptionCartLocked || !this.currentDrugs.length) {
+ this.staleChinesePriceMap = {};
+ this.staleChinesePriceCount = 0;
+ return;
+ }
+ const diff = await fetchChinesePriceDiff({
+ registerId: this.registerId,
+ storeId: this.currentStoreId,
+ drugs: this.currentDrugs,
+ compareOrigin: true,
+ silent: true,
+ });
+ // 以这次取价门店的开关为准,避免页面上还留着旧的 seeRate,更新零售价后把毛利率露出来
+ if (diff.see_rate === 0 || diff.see_rate === 1) {
+ this.seeRate = diff.see_rate;
+ }
+ const next = {};
+ const items = diff.items || [];
+ items.forEach((item) => {
+ next[String(item.drug_id)] = item;
+ });
+ this.staleChinesePriceMap = next;
+ this.staleChinesePriceCount = items.length;
+ },
+ /**
+ * 把指定差异行改成最新供货价和零售价
+ * 为什么:已有价格浮动时按新基准重算,避免更新后折扣还压在旧原价上
+ */
+ commitLatestChinesePrices(items) {
+ applyLatestPrices(this.currentDrugs, items || []);
+ if (Number(this.priceDiscount) !== 100) {
+ this.currentDrugs = applyRatioToDrugs(
+ this.snapshotDrugOrigins(this.currentDrugs),
+ this.priceDiscount,
+ this.priceAdjustScopeMode,
+ );
+ }
+ this.saveToLocalStorage();
+ this.scheduleChinesePriceDiff();
+ },
+ /** 批量把当前中药改为最新门店价 */
+ async onBatchUpdateChinesePrices() {
+ if (guardSpecialPrescriptionCartEdit(this)) return;
+ await this.refreshChinesePriceDiff();
+ const items = [];
+ const map = this.staleChinesePriceMap || {};
+ Object.keys(map).forEach((key) => {
+ items.push(map[key]);
+ });
+ if (!items.length) {
+ this.$toast('已是最新价格');
+ return;
+ }
+ uni.showModal({
+ title: '批量更新价格',
+ content: '有 ' + items.length + ' 味药与当前门店价不一致,是否全部改为最新供货价和零售价?',
+ confirmText: '更新',
+ cancelText: '取消',
+ success: (res) => {
+ if (!res.confirm) return;
+ this.commitLatestChinesePrices(items);
+ this.$toast('已更新 ' + items.length + ' 味药');
+ },
+ });
+ },
+ /**
+ * 只更新这一味中药的供货价和零售价
+ * 按钮常驻,点之前再对一次门店价:已经一致就提示,不再把按钮藏掉
+ */
+ async onUpdateOneChinesePrice(index) {
+ if (guardSpecialPrescriptionCartEdit(this)) return;
+ const drug = this.currentDrugs[index];
+ if (!drug) return;
+ await this.refreshChinesePriceDiff();
+ const item = (this.staleChinesePriceMap || {})[String(drug.id || drug.drug_id)];
+ if (!item) {
+ this.$toast('已是最新价格');
+ return;
+ }
+ this.commitLatestChinesePrices([item]);
+ this.$toast('已更新' + (drug.drug_name || drug.name || '') + '的价格');
+ },
// #ifdef MP-WEIXIN
- setPageBackGuardShow(show, { suppressLeave = false } = {}) {
+ /**
+ * 打开返回拦截 — 对齐 account-switch-panel.showPanel
+ * 先挂 pageGuard,下一帧再 show
+ */
+ openPageBackGuard() {
+ this.clearPageBackGuardReleaseTimer();
+ this.pageBackGuardMounted = true;
+ this.$nextTick(() => {
+ this.pageBackGuardShow = true;
+ });
+ },
+ /**
+ * 关闭返回拦截 — 对齐 account-switch-panel.close
+ * 先卸 show,350ms 兜底再卸 pageGuard(afterleave 会提前卸)
+ */
+ closePageBackGuard({ suppressLeave = false } = {}) {
if (suppressLeave) this._suppressPageBackGuardLeave = true;
- this.pageBackGuardShow = !!show;
+ this.pageBackGuardShow = false;
+ this.schedulePageBackGuardRelease();
+ },
+ /** 兼容旧调用名:true→open,false→close */
+ setPageBackGuardShow(show, { suppressLeave = false } = {}) {
+ if (show) {
+ this.openPageBackGuard();
+ return;
+ }
+ this.closePageBackGuard({ suppressLeave });
+ },
+ /** 对齐 account-switch-panel.releasePageGuard(afterleave) */
+ releasePageBackGuard() {
+ this.clearPageBackGuardReleaseTimer();
+ this.pageBackGuardMounted = false;
+ // 开方页一页多弹层:关完一层若还有下一层,等同切换账号再次 open
+ if (this.shouldEnablePageBackGuard) {
+ this.$nextTick(() => {
+ this.openPageBackGuard();
+ });
+ }
+ },
+ /** 对齐 account-switch-panel.scheduleReleaseGuard */
+ schedulePageBackGuardRelease() {
+ this.clearPageBackGuardReleaseTimer();
+ this._pageBackGuardReleaseTimer = setTimeout(() => {
+ this.pageBackGuardMounted = false;
+ this._pageBackGuardReleaseTimer = null;
+ // 与 afterleave 相同:若仍有弹层则再 open(按钮关抽屉、无 afterleave 的机型)
+ if (this.shouldEnablePageBackGuard) {
+ this.openPageBackGuard();
+ }
+ }, 350);
+ },
+ clearPageBackGuardReleaseTimer() {
+ if (this._pageBackGuardReleaseTimer) {
+ clearTimeout(this._pageBackGuardReleaseTimer);
+ this._pageBackGuardReleaseTimer = null;
+ }
},
syncSalespersonTransferUnloadAlert() {
if (!this.isSalespersonTransferMode) return;
@@ -1986,6 +2285,26 @@ export default {
console.error('获取挂号诊所信息失败:', error);
}
},
+ /**
+ * 传方导入后回填剂数和已匹配的委托调剂(煎服等),只使用内容里已有的规则 ID
+ */
+ async applyTransferChineseMeta(parsed) {
+ if (!parsed) return;
+ const next = Object.assign({}, this.chineseConfig);
+ if (Number(parsed.dosage) > 0) next.dosage = Number(parsed.dosage);
+ if (Number(parsed.day_dosage) > 0) next.dayDosage = Number(parsed.day_dosage);
+ const prId = Number(parsed.process_rule_id || 0);
+ if (prId > 0) {
+ next.ruleType = 2;
+ next.processRuleId = prId;
+ next.childProcessRuleId = Number(parsed.child_process_rule_id || 0) || null;
+ next.processRuleNoteId = Number(parsed.process_rule_note_id || 0) || null;
+ }
+ this.chineseConfig = next;
+ if (prId > 0) {
+ await this.hydrateChineseProcessRuleListsFromConfig();
+ }
+ },
async checkSalespersonTransfer() {
if (!this.registerId) {
this.hasSalespersonTransfer = false;
@@ -2032,6 +2351,7 @@ export default {
this.diagnoses = [{ id: Date.now(), name: parsed.clinical_diagnose }];
}
if (parsed.doctor_order) this.medicalAdvice = parsed.doctor_order;
+ await this.applyTransferChineseMeta(parsed);
this.saveToLocalStorage();
this.$toast('传方已导入');
} catch (e) {
@@ -2039,6 +2359,50 @@ export default {
this.$toast('加载传方失败');
}
},
+ /**
+ * 识图传方自动导入:按 transfer_id + register_id 拉取并写入中药区
+ */
+ async autoImportImageTransfer(transferId) {
+ try {
+ const res = await prepareDoctorImportApi(Number(transferId), Number(this.registerId));
+ const { ok, payload, message } = unwrapXkApi(res);
+ if (!ok || !payload) {
+ this.$toast(message || '导入失败');
+ return;
+ }
+ const parsed = parseTransferPrescriptionContent(payload);
+ if (!parsed || !parsed.drugList || !parsed.drugList.length) {
+ this.$toast('传方药品为空');
+ return;
+ }
+ this.salespersonTransferPrescriptionId = Number(transferId);
+ await this.handleSwitchCategory(1);
+ this.currentDrugs = [];
+ parsed.drugList.forEach((drug) => {
+ this.currentDrugs.push(this.prepareDrugForCart({
+ index_id: drug.index_id || drug.drug_id || drug.id,
+ id: drug.drug_id || drug.id,
+ drug_id: drug.drug_id || drug.id,
+ drug_name: drug.drug_name || drug.name,
+ name: drug.drug_name || drug.name,
+ number: drug.number || 1,
+ way_id: drug.way_id || 0,
+ price: drug.price != null ? drug.price : 0,
+ buy_price: drug.buy_price,
+ }));
+ });
+ if (parsed.clinical_diagnose) {
+ this.diagnoses = [{ id: Date.now(), name: parsed.clinical_diagnose }];
+ }
+ if (parsed.doctor_order) this.medicalAdvice = parsed.doctor_order;
+ await this.applyTransferChineseMeta(parsed);
+ this.saveToLocalStorage();
+ this.$toast('识图传方已导入');
+ } catch (e) {
+ console.error(e);
+ this.$toast('自动导入失败');
+ }
+ },
async handleSwitchCategory(category, options = {}) {
this.saveToLocalStorage();
this.activeCategory = category;
@@ -2343,6 +2707,88 @@ export default {
this.saveToLocalStorage();
},
handleOpenCommonPrescriptionModal() { this.showCommonPrescriptionModal = true; },
+ /**
+ * 底栏上行 VIP/次要操作点击分发(用 data-key,避免模板里写多个方法调用)
+ */
+ onRxSecondaryActionTap(e) {
+ const key = e && e.currentTarget && e.currentTarget.dataset
+ ? e.currentTarget.dataset.key
+ : '';
+ if (key === 'save_common') {
+ this.handleSaveAsCommonPrescription();
+ return;
+ }
+ if (key === 'ai_assist') {
+ this.handleOpenAiPrescriptionDrawer();
+ return;
+ }
+ if (key === 'ai_voice') {
+ this.handleOpenVoicePrescription();
+ return;
+ }
+ if (key === 'image_ocr') {
+ this.handleOpenImageOcrDrawer();
+ return;
+ }
+ if (key === 'golden') {
+ this.handleOpenGoldenFormulaModal();
+ }
+ },
+ /**
+ * 打开识图开方:仅中药;其它 Tab 先提示并切换
+ */
+ handleOpenImageOcrDrawer() {
+ if (guardSpecialPrescriptionCartEdit(this)) return;
+ if (!this.registerId) {
+ this.$toast('挂号无效');
+ return;
+ }
+ const open = () => {
+ this.showAiPrescriptionDrawer = false;
+ this.showGoldenFormulaModal = false;
+ this.showImageOcrDrawer = true;
+ };
+ if (Number(this.activeCategory) !== 1) {
+ uni.showModal({
+ title: '仅支持中药处方',
+ content: '识图开方目前只支持中药,是否切换到中药后继续?',
+ success: (res) => {
+ if (res.confirm) {
+ this.handleSwitchCategory(1).then(open);
+ }
+ },
+ });
+ return;
+ }
+ open();
+ },
+ /**
+ * 识图确认导入:写诊断 + 医嘱 + 药品
+ */
+ async handleImportImageOcr(payload) {
+ if (Number(this.activeCategory) !== 1) {
+ await this.handleSwitchCategory(1);
+ }
+ const dx = payload && payload.clinical_diagnose
+ ? String(payload.clinical_diagnose).trim()
+ : '';
+ if (dx) {
+ this.diagnosisText = dx;
+ this.diagnoses = [{ id: Date.now(), name: dx }];
+ }
+ if (payload && payload.medical_advice) {
+ this.medicalAdvice = String(payload.medical_advice);
+ }
+ await this.handleImportAiPrescription({
+ rows: (payload && payload.rows) || [],
+ dosage: payload && payload.dosage,
+ day_dosage: payload && payload.day_dosage,
+ rule_type: payload && payload.rule_type,
+ process_rule_id: payload && payload.process_rule_id,
+ child_process_rule_id: payload && payload.child_process_rule_id,
+ process_rule_note_id: payload && payload.process_rule_note_id,
+ });
+ },
/**
* 打开金方导入(VIP:golden_formula);仅中药处方可用
*/
@@ -2625,7 +3071,15 @@ export default {
this.$toast('常用方数据异常');
return;
}
- this.currentDrugs.splice(0, this.currentDrugs.length, ...payload.drugs);
+ const drugs = payload.drugs.map((drug) => ({ ...drug }));
+ if (this.activeCategory === 1) {
+ await alignChineseDrugsToLatestPrice({
+ registerId: this.registerId,
+ storeId: this.currentStoreId,
+ drugs,
+ });
+ }
+ this.currentDrugs.splice(0, this.currentDrugs.length, ...drugs);
if (payload.diagnosisText) {
this.diagnosisText = payload.diagnosisText;
const names = payload.diagnosisText.split(',').filter(Boolean);
@@ -3125,7 +3579,7 @@ export default {
if (!Array.isArray(rows) || rows.length === 0) return false;
const list = this.drugUseList || {};
- this.currentDrugs = rows.map((d) => {
+ const mappedRows = rows.map((d) => {
const drugId = Number(d.drug_id ?? d.id);
const wayId = d.way_id ?? d.use_way?.id ?? d.useWay?.id ?? d.order;
const unitId = d.unit_id;
@@ -3133,6 +3587,7 @@ export default {
index_id: d.id ?? drugId, id: drugId,
drug_name: d.name || d.drug_name || '',
number: d.number ?? 1, price: Number(d.price ?? 0),
+ buy_price: d.buy_price != null ? d.buy_price : null,
way_id: wayId,
use_ways: (list.drug_use_way || []).find((item) => item.id === wayId) ?? d.use_way ?? d.useWay,
use_num: (list.drug_time || []).find((item) => item.id === d.time_id),
@@ -3143,6 +3598,12 @@ export default {
image: d.image || '', instruction: d.instruction || '', type: d.type
};
});
+ await alignChineseDrugsToLatestPrice({
+ registerId: this.registerId,
+ storeId: this.currentStoreId,
+ drugs: mappedRows,
+ });
+ this.currentDrugs = mappedRows;
const ruleType = rep0.deployment === 2 ? 1 : 2;
this.chineseConfig = {
@@ -3477,7 +3938,14 @@ export default {
let drugsData;
if (this.activeCategory === 1) {
- drugsData = this.currentDrugs.map((drug) => ({ index_id: drug.index_id, id: drug.id, drug_name: drug.drug_name || drug.name, number: drug.number || drug._quantity || 0, price: drug.price || '0.0000', way_id: drug.way_id || 0, select_number: drug.select_number || 1 }));
+ drugsData = this.currentDrugs.map((drug) => {
+ const row = { index_id: drug.index_id, id: drug.id, drug_name: drug.drug_name || drug.name, number: drug.number || drug._quantity || 0, price: drug.price || '0.0000', way_id: drug.way_id || 0, select_number: drug.select_number || 1 };
+ // 带上供货价:保留原价时后端不能再拿关系表最新供货价覆盖
+ if (drug.buy_price !== undefined && drug.buy_price !== null && drug.buy_price !== '') {
+ row.buy_price = drug.buy_price;
+ }
+ return row;
+ });
} else if ([3, 5, 6, 7].includes(this.activeCategory)) {
drugsData = this.currentDrugs.map((drug) => {
const cleanDrug = { index_id: drug.index_id, id: drug.id, drug_name: drug.drug_name || drug.name, number: drug.number || 1, price: drug.price || '0.0000', select_number: drug.select_number || 1 };
@@ -3813,19 +4281,29 @@ export default {
}
},
// #ifdef MP-WEIXIN
+ /**
+ * 对齐 account-switch-panel.handlePageLeave → close
+ * 有弹层:先关最上层;无弹层:再退页
+ */
onPageBackGuardLeave() {
if (this._suppressPageBackGuardLeave) {
this._suppressPageBackGuardLeave = false;
return;
}
if (this.closeTopOverlayIfAny()) {
+ // 等同 close():只关 show,卸挂载交给 afterleave / scheduleReleaseGuard
+ this.pageBackGuardShow = false;
+ this.schedulePageBackGuardRelease();
+ // 关弹层时 watch 也可能 suppress;吞掉同一次 leave 的重入,下一帧恢复可拦截
+ this._suppressPageBackGuardLeave = true;
this.$nextTick(() => {
- this.setPageBackGuardShow(this.shouldEnablePageBackGuard, { suppressLeave: true });
- this.syncSalespersonTransferUnloadAlert();
+ this._suppressPageBackGuardLeave = false;
});
+ this.syncSalespersonTransferUnloadAlert();
return;
}
- this.setPageBackGuardShow(false);
+ this.pageBackGuardShow = false;
+ this.schedulePageBackGuardRelease();
if (this.isSalespersonTransferMode) {
this.tryLeaveSalespersonTransferPage(() => this.navigateToSalespersonWorkbench());
return;
@@ -4144,14 +4622,17 @@ export default {
min-height: 0;
width: 100%;
}
-/* 内部滚动区铺满 swiper;底部留白避开 fixed 操作栏 */
+/**
+ * 滚动区铺满 swiper 剩余高度(勿再写死 66vh:诊断改为随滚后顶栏变矮,66vh 会在底栏上方留大块空白)
+ * padding-bottom 对齐两行底栏约高:次要 56+12 + 发送 86 + 内边距 ≈ 200rpx
+ */
.rx-order-scroll {
- height: 66vh;
+ height: 100%;
box-sizing: border-box;
padding-bottom: calc(200rpx + env(safe-area-inset-bottom));
}
.rx-swiper-placeholder {
- min-height: 400rpx;
+ min-height: 200rpx;
}
/* 现代卡片设计 (西药 / 产品) */
@@ -4206,30 +4687,65 @@ export default {
}
}
-/* 中药极简高密度标签云 */
+/* 中药清单:标题和操作分行,标签固定两列,避免味数多了以后挤乱 */
+.tcm-list-head {
+ margin-bottom: 24rpx;
+}
+.tcm-list-hint {
+ display: block;
+ margin-top: 8rpx;
+ font-size: 22rpx;
+ color: #8b93a1;
+}
+.tcm-list-ops {
+ display: flex;
+ flex-wrap: wrap;
+ margin-top: 16rpx;
+}
+.tcm-op {
+ margin-right: 28rpx;
+ margin-bottom: 8rpx;
+ font-size: 26rpx;
+ color: #00A88A;
+}
+.tcm-op--danger {
+ color: #e85d5d;
+}
.tcm-tag-cloud {
display: flex;
flex-wrap: wrap;
- gap: 16rpx;
+ justify-content: space-between;
}
.tcm-tag {
- display: inline-flex;
- align-items: center;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ width: 48%;
+ box-sizing: border-box;
+ margin-bottom: 16rpx;
background: #F7F8FA;
- padding: 12rpx 20rpx;
- border-radius: 8rpx;
+ padding: 16rpx 18rpx;
+ border-radius: 12rpx;
border: 1px solid #EBEFF5;
- transition: all 0.2s;
.tcm-name {
+ width: 100%;
font-size: 28rpx;
color: #2A2E35;
font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ }
+ .tcm-meta {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ margin-top: 8rpx;
}
.tcm-weight {
font-size: 26rpx;
color: #00A88A;
- margin-left: 12rpx;
}
.tcm-way {
font-size: 22rpx;
@@ -4243,6 +4759,11 @@ export default {
margin-left: 8rpx;
font-weight: bold;
}
+ .tcm-update-price {
+ font-size: 22rpx;
+ color: #00A88A;
+ margin-left: 8rpx;
+ }
}
/* 简单产品精美步进器 */
@@ -4283,6 +4804,42 @@ export default {
background: #e8f6f4;
border-radius: 12rpx;
}
+/* 临床诊断紧凑块:标题+选择同一行,标签紧凑,随药品区滚动 */
+.dx-compact {
+ padding: 16rpx 20rpx 18rpx;
+}
+.dx-compact__head {
+ justify-content: space-between;
+ margin-bottom: 4rpx;
+}
+.dx-compact__title {
+ font-size: 26rpx;
+ font-weight: 600;
+ color: #333;
+}
+.dx-compact__pick {
+ padding: 4rpx 16rpx;
+ background: #e8f6f4;
+ border-radius: 999rpx;
+}
+.dx-compact__pick-txt {
+ font-size: 22rpx;
+ color: #6acdbb;
+}
+.dx-compact__tags {
+ margin-top: 8rpx;
+ gap: 8rpx;
+}
+.dx-compact__tag {
+ margin: 0;
+}
+.dx-compact__cc {
+ margin-top: 8rpx;
+}
+.dx-compact__cc-txt {
+ font-size: 22rpx;
+ color: #8a92a3;
+}
.register-mode-hint__text {
font-size: 24rpx;
color: #008771;
@@ -4300,21 +4857,74 @@ export default {
.special-rx-card__action {
width: 100%;
}
-/* 与病历 mr-bottom 对齐:左文案 | 右 AI 胶囊 + 主按钮 */
+/* 两行底栏:上行次要(小)+ 下行全宽发送;安全区只在本层加一次 */
.bottom {
position: fixed;
bottom: 0;
left: 0;
right: 0;
- padding: 20rpx 28rpx;
+ padding: 12rpx 28rpx calc(16rpx + env(safe-area-inset-bottom));
background: rgba(255, 255, 255, 0.96);
border-top: 1rpx solid #edf0f2;
z-index: 99;
display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.06);
+ box-sizing: border-box;
+}
+.bottom-secondary {
+ width: 100%;
+ margin-bottom: 12rpx;
+}
+.bottom-primary {
+ width: 100%;
+}
+/* 横滑容器:内容超出时可左右拖拽 */
+.bottom-secondary-scroll {
+ width: 100%;
+ height: 56rpx;
+ white-space: nowrap;
+}
+.bottom-secondary-inner {
+ display: inline-flex;
flex-direction: row;
align-items: center;
- justify-content: space-between;
- box-shadow: 0 -4rpx 24rpx rgba(0, 0, 0, 0.06);
+ height: 56rpx;
+ padding-right: 4rpx;
+}
+/* 上行紧凑胶囊:不定宽均分,按内容收缩,方便横滑塞更多 VIP 入口 */
+.rx-sec-btn {
+ display: inline-flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+ height: 56rpx;
+ padding: 0 18rpx;
+ margin-right: 12rpx;
+ border-radius: 28rpx;
+ background: rgba(106, 205, 187, 0.08);
+ border: 1rpx solid rgba(106, 205, 187, 0.35);
+ box-sizing: border-box;
+}
+.rx-sec-btn:last-child {
+ margin-right: 0;
+}
+.rx-sec-btn--gold {
+ background: rgba(212, 160, 23, 0.1);
+ border-color: rgba(212, 160, 23, 0.45);
+}
+.rx-sec-btn__icon {
+ margin-right: 6rpx;
+ flex-shrink: 0;
+ line-height: 1;
+}
+.rx-sec-btn__text {
+ font-size: 22rpx;
+ color: #6ACDBB;
+ font-weight: 500;
+ line-height: 1.2;
}
.bottom-left {
flex: 1;
@@ -4333,7 +4943,7 @@ export default {
.btnText--danger {
color: #f55353;
}
-/* 与病历 .mr-ai-btn 同款胶囊 */
+/* 历史单行胶囊样式保留,避免其它引用丢失;处方底栏已改用 .rx-sec-btn */
.rx-ai-btn {
display: flex;
flex-direction: row;
diff --git a/subPackages/sub_workbench/prescription_v2/utils/alignChineseDrugPrice.js b/subPackages/sub_workbench/prescription_v2/utils/alignChineseDrugPrice.js
new file mode 100644
index 0000000..d63cf0f
--- /dev/null
+++ b/subPackages/sub_workbench/prescription_v2/utils/alignChineseDrugPrice.js
@@ -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);
+}
diff --git a/subPackages/sub_workbench/public_account_lock/index.vue b/subPackages/sub_workbench/public_account_lock/index.vue
index bb22d9c..5e0209d 100644
--- a/subPackages/sub_workbench/public_account_lock/index.vue
+++ b/subPackages/sub_workbench/public_account_lock/index.vue
@@ -1,9 +1,10 @@
+
{
- 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)
diff --git a/subPackages/sub_workbench/workbench_infoPatient.vue b/subPackages/sub_workbench/workbench_infoPatient.vue
index 5fe8b42..37de97c 100644
--- a/subPackages/sub_workbench/workbench_infoPatient.vue
+++ b/subPackages/sub_workbench/workbench_infoPatient.vue
@@ -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 {
diff --git a/utils/imageTransferQuote.js b/utils/imageTransferQuote.js
new file mode 100644
index 0000000..e386513
--- /dev/null
+++ b/utils/imageTransferQuote.js
@@ -0,0 +1,62 @@
+/**
+ * 识图传方报价:请求侧增量组装
+ * 首次全量 drugs;同味数下只传 quote_token + drugs_patch(改了的下标),省流量
+ */
+
+/**
+ * 比较两味报价药品是否等价(用于算 patch)
+ * @param {Record|null|undefined} a
+ * @param {Record|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,
+ * drugs: Array>,
+ * quoteToken?: string,
+ * lastQuoteDrugs?: Array>|null
+ * }} opts
+ * @returns {{ payload: Record, nextLastDrugs: Array> }}
+ */
+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 }
+}
diff --git a/utils/vip.js b/utils/vip.js
index e3e6ea3..32c90de 100644
--- a/utils/vip.js
+++ b/utils/vip.js
@@ -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'
diff --git a/xaa b/xaa
new file mode 100644
index 0000000..905eb1c
--- /dev/null
+++ b/xaa
@@ -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---