feat: 锁定诊所、公账支付

This commit is contained in:
李琦
2026-08-19 17:33:05 +08:00
parent 2a1fe884d7
commit 22e93c5dba
4 changed files with 165 additions and 20 deletions

View File

@@ -13,7 +13,7 @@
<u-popup :value="show" mode="center" border-radius="24" :mask-close-able="true" @input="onPopupInput" @close="close">
<view class="ppay">
<view class="ppay__header">
<text class="ppay__title">患者代付码</text>
<text class="ppay__title">患者订单代付码</text>
<text class="ppay__close" @click="close">关闭</text>
</view>
<view class="ppay__body">
@@ -22,7 +22,7 @@
<text class="ppay__loading-text">代付码生成中...</text>
</view>
<block v-else-if="qrcode">
<!-- 后端返回完整 data:image/png;base64 数据,长按可转发给患者 -->
<!-- src 是本地临时文件:小程序 <image> 不能稳渲染 data URI,长按仍可转发给患者 -->
<image class="ppay__qrcode" :src="qrcode" mode="aspectFit" show-menu-by-longpress></image>
<view class="ppay__meta">
<view class="ppay__meta-row">
@@ -34,7 +34,7 @@
<text class="ppay__meta-value ppay__meta-value--price">¥{{ totalPayPrice }}</text>
</view>
</view>
<view class="ppay__tip">请让患者用微信「扫一扫」,进入小程序完成支付</view>
<view class="ppay__tip">请让患者用微信「扫一扫」或者长按图片识别二维码打开「惠佑康」小程序,进入小程序完成支付</view>
</block>
<view v-else class="ppay__error">
<text class="ppay__error-text">{{ errorText || '代付码生成失败' }}</text>
@@ -52,10 +52,13 @@
*
* 用途:本次挂号处方存在待支付订单时,医生出示小程序码给患者扫码代付
* (复用 PC 接诊台同款后端 proxy-pay-qrcode,scene=ppay={orderId},患者端解析链路零改动)。
* 展示:接口返回 data URI,写入 USER_DATA_PATH 再绑 <image>,不上传 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:避免删除过程中 <image> 仍指向已删文件
*/
async clearLocalQrcode() {
const path = this.qrcode
this.qrcode = ''
await unlinkLocalFile(path)
},
/** 请求代付码:接口仍返回 base64,写入本地后再绑 <image>(不上传 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 {

View File

@@ -58,9 +58,9 @@
</view>
</view>
<view class="rx-sent-ops">
<!-- <view v-if="sentOrderId && sentOrderPaid != 1" class="rx-sent-btn rx-sent-btn&#45;&#45;main" @click="openProxyPayFromSuccess">-->
<!-- <text class="rx-sent-btn-txt">代付码</text>-->
<!-- </view>-->
<view v-if="sentOrderId && sentOrderPaid != 1" class="rx-sent-btn rx-sent-btn--main" @click="openProxyPayFromSuccess">
<text class="rx-sent-btn-txt">代付码</text>
</view>
<view v-if="sentOrderId && publicAccountPayEnabled && !publicAccountDailyMode && sentOrderPaid != 1" class="rx-sent-btn" @click="openPublicAccountApply">
<text class="rx-sent-btn-txt rx-sent-btn-txt--dark">申请公账支付</text>
</view>
@@ -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('加载处方详情失败');

View File

@@ -0,0 +1,83 @@
/**
* 把后端返回的 data URI / 纯 base64 写成小程序本地文件
* 为什么:微信 <image> 不能稳定渲染 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 写入本地临时文件,返回可供 <image :src> 绑定的路径
* @param {string} base64 完整 data:image/xxx;base64,xxx 或纯 base64(可含 chunk_split 空白)
* @param {string} prefix 文件名前缀,默认 img
* @returns {Promise<string>} 本地文件路径
*/
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<void>}
*/
export function unlinkLocalFile(filePath) {
return new Promise((resolve) => {
if (!filePath) {
resolve()
return
}
uni.getFileSystemManager().unlink({
filePath,
success() {
resolve()
},
fail() {
resolve()
},
})
})
}

View File

@@ -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 {