diff --git a/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue b/subPackages/sub_workbench/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue similarity index 84% rename from components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue rename to subPackages/sub_workbench/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue index 991ae54..c0b27cc 100644 --- a/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue +++ b/subPackages/sub_workbench/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue @@ -13,7 +13,7 @@ - 患者代付码 + 患者订单代付码 关闭 @@ -22,7 +22,7 @@ 代付码生成中... - + @@ -34,7 +34,7 @@ ¥{{ totalPayPrice }} - 请让患者用微信「扫一扫」,进入小程序完成支付 + 请让患者用微信「扫一扫」或者长按图片识别二维码打开「惠佑康」小程序,进入小程序完成支付 {{ errorText || '代付码生成失败' }} @@ -52,10 +52,13 @@ * * 用途:本次挂号处方存在待支付订单时,医生出示小程序码给患者扫码代付 * (复用 PC 接诊台同款后端 proxy-pay-qrcode,scene=ppay={orderId},患者端解析链路零改动)。 + * 展示:接口返回 data URI,写入 USER_DATA_PATH 再绑 ,不上传 OSS。 + * 本组件与 base64ToLocalPath 都在 sub_workbench 分包,避免打进主包。 * 读值:xk-api 形态,拦截器已返回 body → code / result / message(unwrapXkApi) */ import { getProxyPayQrcodeApi } from '@/api/reception.js' import { unwrapXkApi } from '@/utils/api-response.js' +import { base64ToLocalPath, unlinkLocalFile } from '../../utils/base64ToLocalPath.js' export default { name: 'ProxyPayQrcodeModal', @@ -89,12 +92,14 @@ export default { this.pageGuard = true this.load() } else { + this.clearLocalQrcode() this.scheduleReleaseGuard() } }, }, beforeDestroy() { this.clearReleaseTimer() + this.clearLocalQrcode() }, methods: { releasePageGuard() { @@ -120,21 +125,30 @@ export default { onPopupInput(v) { this.show = v }, - /** 请求代付码(xk-api,取值 code/result/message) */ + /** + * 清掉上一张本地码,避免 USER_DATA_PATH 堆积 + * 为什么先置空再 unlink:避免删除过程中 仍指向已删文件 + */ + async clearLocalQrcode() { + const path = this.qrcode + this.qrcode = '' + await unlinkLocalFile(path) + }, + /** 请求代付码:接口仍返回 base64,写入本地后再绑 (不上传 OSS) */ async load() { if (!this.orderId) { - this.qrcode = '' + await this.clearLocalQrcode() this.errorText = '缺少订单信息' return } this.loading = true - this.qrcode = '' this.errorText = '' + await this.clearLocalQrcode() try { const res = await getProxyPayQrcodeApi({ order_id: this.orderId }) const { ok, payload, message } = unwrapXkApi(res) if (ok && payload && payload.qrcode) { - this.qrcode = payload.qrcode + this.qrcode = await base64ToLocalPath(payload.qrcode, 'ppay') this.orderNo = payload.order_no || '' this.totalPayPrice = payload.total_pay_price || '' } else { diff --git a/subPackages/sub_workbench/prescription_v2/index.vue b/subPackages/sub_workbench/prescription_v2/index.vue index 6013f09..39b94da 100644 --- a/subPackages/sub_workbench/prescription_v2/index.vue +++ b/subPackages/sub_workbench/prescription_v2/index.vue @@ -58,9 +58,9 @@ - - - + + 代付码 + 申请公账支付 @@ -863,7 +863,7 @@ import TraditionalTermPickerModal from './components/modals/TraditionalTermPicke import TransferPatientDrawer from './components/modals/TransferPatientDrawer.vue'; import ApplyPublicAccountPayDrawer from './components/modals/ApplyPublicAccountPayDrawer.vue'; import { unwrapXkApi } from '@/utils/api-response.js'; -import ProxyPayQrcodeModal from '@/components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue'; +import ProxyPayQrcodeModal from '../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'; @@ -1408,6 +1408,10 @@ export default { } }, onUnload() { + if (this._tabChangingTimer) { + clearTimeout(this._tabChangingTimer); + this._tabChangingTimer = null; + } if (this.isCommonPrescription) { uni.removeStorageSync('add'); } @@ -1499,6 +1503,11 @@ export default { // 延后关掉 loading,避免小程序在同帧大块挂载时触发 clickCheckTask / not iterable this.$nextTick(() => { this.pageLoading = false; + this.$nextTick(() => { + const holdMs = this._needHoldRxTabAfterReuse ? 320 : 50; + this._needHoldRxTabAfterReuse = false; + this.syncSwiperCurrentFromCategory(holdMs); + }); }); } }, @@ -1601,18 +1610,45 @@ export default { focusActiveTab() { this.syncSwiperCurrentFromCategory(); }, - /** 根据 activeCategory 同步 swiperCurrent */ - syncSwiperCurrentFromCategory() { + /** + * 复用/导入后切到处方主 Tab,并按种类对齐 u-tabs + swiper + * 为什么单独抽:只改 activeCategory 不会动 swiperCurrent,微信里 Tab 高亮和内容会错位 + * @param {number} category 处方类型 1中药 2西药 3/5/6/7 简单商品 + */ + activateRxCategoryTab(category) { + const pt = Number(category); + if (pt) { + this.activeCategory = pt; + } + if (this.canUseMedicalRecord && this.mainTab !== 'rx') { + this.mainTab = 'rx'; + if (this.registerId) { + PrescriptionStorage.saveRxMrTab('rx', this.registerId); + } + } + this.syncSwiperCurrentFromCategory(320); + this._needHoldRxTabAfterReuse = true; + }, + /** + * 根据 activeCategory 同步 swiper / u-tabs 下标 + * 微信 swiper 挂载或改 current 会异步抛 @change,nextTick 太短挡不住,用短延时锁回路 + * @param {number} holdMs 锁住 tabs↔swiper 回路的毫秒数 + */ + syncSwiperCurrentFromCategory(holdMs = 50) { const list = this.swiperCategories || []; const idx = list.findIndex((c) => Number(c.value) === Number(this.activeCategory)); const next = idx >= 0 ? idx : 0; - if (next === this.swiperCurrent) return; - // 程序化改 current 会触发 swiper @change,用 tabChanging 挡住回路 this.tabChanging = true; - this.swiperCurrent = next; - this.$nextTick(() => { + if (next !== this.swiperCurrent) { + this.swiperCurrent = next; + } + if (this._tabChangingTimer) { + clearTimeout(this._tabChangingTimer); + } + this._tabChangingTimer = setTimeout(() => { this.tabChanging = false; - }); + this._tabChangingTimer = null; + }, holdMs); }, /** Tab 点击切换(与订单列表 onTabChange 一致) */ onRxTabChange(index) { @@ -3013,10 +3049,17 @@ export default { this.$toast('患者信息异常'); return false; } + const typeTabs = this.swiperCategories || []; + if (typeTabs.length && !typeTabs.some((c) => Number(c.value) === pt)) { + this.$toast('当前门店未开通该处方类型,无法复用'); + return false; + } this.saveToLocalStorage(); this.activeCategory = pt; PrescriptionStorage.saveActiveCategory(pt, this.registerId); + // 复用必须切到「处方」主 Tab + 对应种类;只改 activeCategory 不会动 u-tabs/swiper + this.activateRxCategoryTab(pt); const repiceList = Array.isArray(content.repice) ? content.repice : []; if (pt === 1) { @@ -3100,7 +3143,12 @@ export default { } const detail = res.result || res.data || res; const ok = await this.applyHistoricalPrescriptionDetail(detail); - if (ok) this.$toast('已复用历史处方,请核对后发送'); + if (ok) { + this.$toast('已复用历史处方,请核对后发送'); + this.$nextTick(() => { + this.activateRxCategoryTab(this.activeCategory); + }); + } } catch (e) { console.error('复用处方失败:', e); this.$toast('加载处方详情失败'); diff --git a/subPackages/sub_workbench/utils/base64ToLocalPath.js b/subPackages/sub_workbench/utils/base64ToLocalPath.js new file mode 100644 index 0000000..160f824 --- /dev/null +++ b/subPackages/sub_workbench/utils/base64ToLocalPath.js @@ -0,0 +1,83 @@ +/** + * 把后端返回的 data URI / 纯 base64 写成小程序本地文件 + * 为什么:微信 不能稳定渲染 data:image/...;base64(体积大或带 chunk_split 换行时真机空白), + * 写成 USER_DATA_PATH 后用本地路径展示,无需上传 OSS + * 放在 sub_workbench 分包:主包不能引用本文件,避免把工具打进主包 + */ + +/** + * 取可写的用户目录(真机优先 wx.env,H5/开发工具兜底 uni.env) + * @returns {string} + */ +function getUserDataPath() { + if (typeof wx !== 'undefined' && wx.env && wx.env.USER_DATA_PATH) { + return wx.env.USER_DATA_PATH + } + if (uni.env && uni.env.USER_DATA_PATH) { + return uni.env.USER_DATA_PATH + } + return '' +} + +/** + * data URI / 纯 base64 写入本地临时文件,返回可供 绑定的路径 + * @param {string} base64 完整 data:image/xxx;base64,xxx 或纯 base64(可含 chunk_split 空白) + * @param {string} prefix 文件名前缀,默认 img + * @returns {Promise} 本地文件路径 + */ +export function base64ToLocalPath(base64, prefix = 'img') { + return new Promise((resolve, reject) => { + const raw = String(base64 || '').trim() + if (!raw) { + reject(new Error('图片数据为空')) + return + } + const root = getUserDataPath() + if (!root) { + reject(new Error('无法获取临时目录')) + return + } + const match = /^data:image\/(\w+);base64,/i.exec(raw) + const format = (match && match[1] ? match[1] : 'png').toLowerCase() + const body = (match ? raw.slice(match[0].length) : raw).replace(/\s/g, '') + if (!body) { + reject(new Error('图片数据无效')) + return + } + const filePath = `${root}/${prefix}_${Date.now()}.${format}` + uni.getFileSystemManager().writeFile({ + filePath, + data: body, + encoding: 'base64', + success() { + resolve(filePath) + }, + fail() { + reject(new Error('本地图片写入失败')) + }, + }) + }) +} + +/** + * 删除本地临时文件;已删或不存在不抛错,避免阻断关弹窗 + * @param {string} filePath writeFile 得到的路径 + * @returns {Promise} + */ +export function unlinkLocalFile(filePath) { + return new Promise((resolve) => { + if (!filePath) { + resolve() + return + } + uni.getFileSystemManager().unlink({ + filePath, + success() { + resolve() + }, + fail() { + resolve() + }, + }) + }) +} diff --git a/subPackages/sub_workbench/workbench_infoPatient.vue b/subPackages/sub_workbench/workbench_infoPatient.vue index 763a4db..273d0c5 100644 --- a/subPackages/sub_workbench/workbench_infoPatient.vue +++ b/subPackages/sub_workbench/workbench_infoPatient.vue @@ -325,7 +325,7 @@ import { 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 ProxyPayQrcodeModal from './components/proxy-pay-qrcode-modal/proxy-pay-qrcode-modal.vue' function emptyHealthInquiry() { return {