feat: 锁定诊所、公账支付

This commit is contained in:
李琦
2026-08-19 16:48:43 +08:00
parent 62ffb6fd31
commit 2a1fe884d7
28 changed files with 2859 additions and 181 deletions

View File

@@ -70,6 +70,9 @@ export function createBusinessApi(prefix, headerFlag) {
submitWithdrawal(data) {
return bizRequest({ url: `${prefix}-withdrawal/withdrawal`, method: 'POST', data });
},
getWithdrawableOrders(params) {
return bizRequest({ url: `${prefix}-withdrawal/withdrawable-orders`, method: 'GET', data: params || {} });
},
passWithdrawal(data) {
return bizRequest({ url: `${prefix}-withdrawal/pass-application`, method: 'POST', data });
},

View File

@@ -178,6 +178,11 @@ export function submitWithdrawal(data) {
return clinicRequest({ url: `${PREFIX}-withdrawal/withdrawal`, method: 'POST', data });
}
/** 可提订单明细 xk-api GET /clinic-admin-withdrawal/withdrawable-orders */
export function getWithdrawableOrders(params) {
return clinicRequest({ url: `${PREFIX}-withdrawal/withdrawable-orders`, method: 'GET', data: params || {} });
}
export function getReconciliationList(params) {
return clinicRequest({ url: `${PREFIX}-reconciliation/list`, method: 'GET', data: params });
}

78
api/publicAccountPay.js Normal file
View File

@@ -0,0 +1,78 @@
/**
* 公账支付(xk-api,取值 unwrapXkApi → code / result / message)
* 医生走 /public-account-pay;诊所管理员走 /clinic-admin-public-account-pay
*/
import { req } from '@/common/js/index.js'
function isClinicAdmin() {
return uni.getStorageSync('loginMode') === 'clinic_admin'
}
function dailyPrefix() {
return isClinicAdmin()
? '/newApi/clinic-admin-public-account-pay/'
: '/newApi/public-account-pay/'
}
function dailyHeader() {
return isClinicAdmin() ? { _clinicAdmin: '1' } : {}
}
/** 申请公账支付 xk-api POST /public-account-pay/apply */
export function applyPublicAccountPayApi(data) {
return req.request({
url: '/newApi/public-account-pay/apply',
method: 'POST',
data,
})
}
/** 日汇总详情 xk-api GET .../daily-detail */
export function getPublicAccountDailyBillInfoApi(id) {
return req.request({
url: dailyPrefix() + 'daily-detail',
method: 'GET',
data: { id },
header: dailyHeader(),
})
}
/** 补出日汇总 xk-api POST .../daily-generate */
export function generatePublicAccountDailyBillApi(data) {
return req.request({
url: dailyPrefix() + 'daily-generate',
method: 'POST',
data,
header: dailyHeader(),
})
}
/** 上传日汇总凭证 xk-api POST .../daily-upload */
export function uploadPublicAccountDailyBillApi(data) {
return req.request({
url: dailyPrefix() + 'daily-upload',
method: 'POST',
data,
header: dailyHeader(),
})
}
/** 催办确认到账 xk-api POST .../daily-urge */
export function urgePublicAccountDailyBillApi(data) {
return req.request({
url: dailyPrefix() + 'daily-urge',
method: 'POST',
data,
header: dailyHeader(),
})
}
/** 锁店现算状态(配色/日汇总状态/联系人)xk-api GET .../lock-status */
export function getPublicAccountLockStatusApi() {
return req.request({
url: dailyPrefix() + 'lock-status',
method: 'GET',
data: {},
header: dailyHeader(),
})
}

View File

@@ -3,6 +3,7 @@ import errorCode from './errorCode.js';
import {Base64} from "js-base64";
import { getDoctorWxEnv } from '@/config/app-env.js';
import { resolveDoctorWxAuthToken } from '@/utils/doctorWxAuthToken.js';
import { setPublicAccountLock } from '@/utils/publicAccountLock.js'
const { legacyApiBase } = getDoctorWxEnv();
@@ -156,6 +157,20 @@ const resInterceptor = (response, conf = {}) => {
if (res == null || typeof res !== 'object') {
return res
}
// 4101 锁店:先解密联系人(会写出 phone_txt 再脱敏 phone),再落库给锁店页拨号
const bizCode = Number(res.code != null ? res.code : res.errcode)
if (bizCode === 4101) {
const payload = res.result || {}
if (payload.contact_phones) {
payload.contact_phones = getRes(payload.contact_phones, true)
}
setPublicAccountLock(payload)
return {
wakaryReqToReject: true,
msg: res.message || '公账日账单已逾期',
res: response
}
}
if (res.data != null) {
res.data = getRes(res.data)
}
@@ -163,7 +178,6 @@ const resInterceptor = (response, conf = {}) => {
res.result = getRes(res.result)
}
// Laravel notAuth 返回 HTTP 200 + 业务码 401,需与 HTTP 401 走同一套登录失效处理
const bizCode = Number(res.code != null ? res.code : res.errcode)
if (bizCode === 401) {
return handleAuthExpired(res.msg || res.message || '账号需重新登录', response, conf)
}
@@ -222,62 +236,68 @@ function _responseLog(res, conf = {}, describe = null) {
}
}
/** 解密后写出 {字段}_txt 明文,给锁店拨号等场景用;原字段仍脱敏 */
const PHONE_TXT_KEYS = ['phone', 'mobile', 'call_mobile', 'express_mobile', 'patient_mobile', 'accept_tel']
function getRes(obj, isDecode = true) {
if (obj == null || typeof obj !== 'object') {
return obj
}
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
if (obj[i] != null && typeof obj[i] === 'object') {
obj[i] = getRes(obj[i], isDecode)
}
}
return obj
}
try {
for (const key in obj) {
if (Array.isArray(obj[key])) {
obj[key] = getRes(obj[key])
} else if (typeof obj[key] === 'object' && obj[key] !== null) {
obj[key] = getRes(obj[key])
} else {
// 判断obj[key]是否在arr中
if (arr.indexOf(key) !== -1) {
if (obj[key] == null || obj[key] === '') continue
let aseFile = ''
if (isDecode === true) {
aseFile = customBase64Decode(obj[key])
} else {
aseFile = customBase64Encode(obj[key])
}
const isGarbled = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]|[\u{E000}-\u{F8FF}]/u.test(
typeof aseFile === 'string' ? aseFile : ''
)
if (isGarbled) {
aseFile = obj[key]
}
if (isDecode === true) {
if (sensitiveData.indexOf(key) !== -1) {
// 数据脱敏,自动匹配姓名、手机号、身份证
switch (key) {
case 'express_name':
case 'express_region':
case 'accept_name':
case 'patient':
// 保留前两个字符,其余用星号代替
aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
break;
case 'mobile':
case 'express_mobile':
case 'phone':
// 保留前三位和后四位,中间用星号代替
aseFile = aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
break;
case 'id_card':
case 'idcard':
// 保留前六位和后四位,中间用星号代替
aseFile = aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
break;
default:
// 默认情况下,全部用星号代替
break;
}
}
}
obj[key] = aseFile
if (obj[key] != null && typeof obj[key] === 'object') {
obj[key] = getRes(obj[key], isDecode)
} else if (arr.indexOf(key) !== -1) {
if (obj[key] == null || obj[key] === '') continue
let aseFile = ''
if (isDecode === true) {
aseFile = customBase64Decode(obj[key])
} else {
aseFile = customBase64Encode(obj[key])
}
const isGarbled = /[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\xFF]|[\u{E000}-\u{F8FF}]/u.test(
typeof aseFile === 'string' ? aseFile : ''
)
if (isGarbled) {
aseFile = obj[key]
}
// 请求必须加密,不能跳过;解密后先留明文 _txt 再脱敏
if (isDecode === true && PHONE_TXT_KEYS.indexOf(key) !== -1) {
const plain = String(aseFile || '')
if (/^1\d{10}$/.test(plain)) {
obj[key + '_txt'] = plain
}
}
if (isDecode === true && sensitiveData.indexOf(key) !== -1) {
switch (key) {
case 'express_name':
case 'express_region':
case 'accept_name':
case 'patient':
aseFile = aseFile.slice(0, 1).padEnd(aseFile.length, '*');
break;
case 'mobile':
case 'express_mobile':
case 'phone':
aseFile = aseFile.slice(0, 3).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
break;
case 'id_card':
case 'idcard':
aseFile = aseFile.slice(0, 6).padEnd(aseFile.length - 4, '*') + aseFile.slice(-4);
break;
default:
break;
}
}
obj[key] = aseFile
}
}
return obj

View File

@@ -10,11 +10,13 @@
@afterleave="releasePageGuard"
/>
<!-- #endif -->
<!-- z-index 高于自定义 tabbar(998) / 工作台看板浮卡,避免编辑抽屉被统计卡盖住 -->
<u-popup
:value="popupShow"
mode="bottom"
border-radius="24"
height="80%"
:z-index="10080"
:safe-area-inset-bottom="true"
:mask-close-able="true"
@input="onPopupInput"

View File

@@ -150,6 +150,14 @@
"style": {
"navigationBarTitleText": "处方详情"
}
},
{
"path": "public_account_lock/index",
"style": {
"navigationBarTitleText": "公账已锁定",
"navigationStyle": "custom",
"disableSwipeBack": true
}
}
]
},
@@ -324,6 +332,10 @@
"path": "withdrawal/record-detail",
"style": { "navigationBarTitleText": "提现详情", "navigationStyle": "custom" }
},
{
"path": "withdrawal/withdrawable-orders",
"style": { "navigationBarTitleText": "可提订单", "navigationStyle": "custom", "enablePullDownRefresh": true }
},
{
"path": "reconciliation/index",
"style": { "navigationBarTitleText": "对账单", "navigationStyle": "custom", "enablePullDownRefresh": true }
@@ -375,6 +387,7 @@
{ "path": "withdrawal/apply", "style": { "navigationBarTitleText": "申请提现", "navigationStyle": "custom" } },
{ "path": "withdrawal/card-edit", "style": { "navigationBarTitleText": "编辑账户", "navigationStyle": "custom" } },
{ "path": "withdrawal/record-detail", "style": { "navigationBarTitleText": "提现详情", "navigationStyle": "custom" } },
{ "path": "withdrawal/withdrawable-orders", "style": { "navigationBarTitleText": "可提订单", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/settlement/index", "style": { "navigationBarTitleText": "结算记录", "navigationStyle": "custom", "enablePullDownRefresh": true } },
{ "path": "withdrawal/settlement/detail", "style": { "navigationBarTitleText": "结算明细", "navigationStyle": "custom" } },
{ "path": "withdrawal/account-change/index", "style": { "navigationBarTitleText": "资金变动", "navigationStyle": "custom", "enablePullDownRefresh": true } },

View File

@@ -30,7 +30,7 @@
<view class="row"><text class="label">时间</text><text class="value">{{ item.created_at }}</text></view>
<view class="row" v-if="item.remark"><text class="label">备注</text><text class="value">{{ item.remark }}</text></view>
<view
v-if="Number(item.order_type) === 1 && item.order_id"
v-if="item.order_id || (item.orders && item.orders.length)"
class="order-link"
@click="goOrderDetail(item)"
>查看订单详情</view>
@@ -87,23 +87,39 @@ export default {
this.reload()
},
reload() {
this.load(1, false)
return this.load(1, false)
},
load(page, append) {
this.loading = true
const params = { page, pageSize: 15 }
if (this.sourceType) params.source_type = this.sourceType
if (this.orderNo.trim()) params.order_no = this.orderNo.trim()
this.bizApi.getAccountChangeLogList(params).then(w => {
return this.bizApi.getAccountChangeLogList(params).then(w => {
const items = (w.data && w.data.items) || []
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
},
canViewChangeOrder(item) {
const orders = item.orders || []
return (Number(item.order_id) > 0) || orders.length > 0
},
goOrderDetail(item) {
uni.navigateTo({
url: `${this.bizRoot}/order/detail?id=${item.order_id}`,
})
const orders = item.orders || []
const target = orders.length > 0 ? orders[0] : item
const orderType = Number(target.order_type || item.order_type || 0)
const orderId = target.order_id || item.order_id
if (orderType === 1 && orderId) {
uni.navigateTo({ url: `${this.bizRoot}/order/detail?id=${orderId}` })
return
}
if (orderType === 2 && orderId) {
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${orderId}`,
})
return
}
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
},
},
}

View File

@@ -1,12 +1,61 @@
<template>
<business-page-layout title="申请提现" :show-back="true">
<view class="tip">可提现余额:{{ balanceText }}</view>
<view v-if="gapText" class="gap">
<text>{{ gapText }}</text>
<view
v-for="row in gapApplies"
:key="row.id"
class="gap-order"
@click.stop="goApplyDetail(row.id)"
>{{ row.order_no }} · {{ row.check_status_txt || '' }} · {{ row.dakuan_status_txt || '' }}</view>
</view>
<view class="field">
<text class="label">提现方式</text>
<view class="mode-row">
<text
v-for="m in modes"
:key="m.value"
class="mode-btn"
:class="{ on: mode === m.value }"
@click="switchMode(m.value)"
>{{ m.label }}</text>
</view>
</view>
<view v-if="mode === 1" class="field">
<text class="label">提现金额</text>
<input v-model="amount" type="digit" placeholder="请输入金额" />
<view class="quick">
<text v-for="q in quickAmounts" :key="q.key" class="q-btn" @click="amount = String(q.value)">{{ q.value }}</text>
</view>
</view>
<view class="quick">
<text v-for="q in quickAmounts" :key="q.key" class="q-btn" @click="amount = String(q.value)">{{ q.value }}</text>
<view v-if="mode === 3" class="field">
<text class="label">时间范围</text>
<picker mode="date" :value="startDate" @change="onStartDate">
<view class="picker">开始:{{ startDate || '请选择' }}</view>
</picker>
<picker mode="date" :value="endDate" @change="onEndDate">
<view class="picker">结束:{{ endDate || '请选择' }}</view>
</picker>
<picker :range="categoryLabels" @change="onCategory">
<view class="picker">类型:{{ currentCategoryLabel }}</view>
</picker>
<view class="preview" @click="loadOrders">预览订单({{ orderList.length }} 笔)</view>
</view>
<view v-if="mode === 2 || mode === 3" class="order-list">
<view
v-for="item in orderList"
:key="item.uk"
class="order-item"
@click="toggleOrder(item)"
>
<text class="chk">{{ isSelected(item) ? '☑' : '☐' }}</text>
<view class="order-main">
<text class="no">{{ item.order_no || '-' }}</text>
<text class="sub">{{ item.category_txt }} · {{ item.remain }}</text>
</view>
</view>
<view v-if="orderList.length === 0" class="empty">暂无可提订单</view>
</view>
<button class="submit" @click="submit" :loading="loading">提交申请</button>
</business-page-layout>
@@ -15,7 +64,7 @@
<script>
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
export default {
mixins: [businessMixin],
@@ -24,7 +73,27 @@ export default {
return {
amount: '',
balanceText: '-',
balance: 0,
gapText: '',
gapApplies: [],
loading: false,
mode: 2,
category: 0,
startDate: '',
endDate: '',
orderList: [],
selectedUks: [],
modes: [
{ label: '按订单', value: 2 },
{ label: '按时间', value: 3 },
{ label: '自定义', value: 1 },
],
categoryOptions: [
{ label: '全部类型', value: 0 },
{ label: '中药', value: 1 },
{ label: '西药', value: 2 },
{ label: '其他', value: 3 },
],
quickAmounts: [
{ key: '1000', value: 1000 },
{ key: '3000', value: 3000 },
@@ -33,24 +102,151 @@ export default {
],
}
},
computed: {
categoryLabels() {
return this.categoryOptions.map(o => o.label)
},
currentCategoryLabel() {
const hit = this.categoryOptions.find(o => o.value === this.category)
return hit ? hit.label : '全部类型'
},
selectedSum() {
const picked = this.orderList.filter(item => this.selectedUks.indexOf(item.uk) >= 0)
return picked.reduce((sum, item) => sum + Number(item.remain || 0), 0)
},
rangeSum() {
return this.orderList.reduce((sum, item) => sum + Number(item.remain || 0), 0)
},
requestedSum() {
if (this.mode === 2) return this.selectedSum
if (this.mode === 3) return this.rangeSum
return 0
},
},
onLoad() {
if (!this.bizApi.getMyBalance) return
this.bizApi.getMyBalance().then(w => {
if (w.ok && w.data) {
const b = w.data.balance
this.balance = Number(b || 0)
this.balanceText = b != null ? formatMoney(b) : '-'
const gap = Number(w.data.custom_gap || 0)
this.gapApplies = w.data.custom_gap_applies || []
if (gap > 0) {
this.gapText = `按单合计比余额多 ${gap.toFixed(2)} 元,点打款单号可看申请→审核→打款进度`
}
}
})
this.loadOrders()
},
methods: {
/**
* 切换提现方式:清已选和旧列表,避免按时间窄集残留到按订单导致漏提
* 按订单立刻重拉全量;按时间等用户点预览;自定义不拉单
*/
switchMode(next) {
if (this.mode === next) return
this.mode = next
this.selectedUks = []
this.orderList = []
if (next === 2) {
this.loadOrders()
}
},
onStartDate(e) { this.startDate = e.detail.value },
onEndDate(e) { this.endDate = e.detail.value },
onCategory(e) {
this.category = this.categoryOptions[Number(e.detail.value || 0)].value
},
toTs(dateStr, end) {
if (!dateStr) return 0
const t = new Date(dateStr.replace(/-/g, '/') + (end ? ' 23:59:59' : ' 00:00:00')).getTime()
return t ? Math.floor(t / 1000) : 0
},
loadOrders() {
if (!this.bizApi.getWithdrawableOrders) return
const params = { page: 1, pageSize: 50, category: this.category }
if (this.mode === 3) {
params.start_at = this.toTs(this.startDate, false)
params.end_at = this.toTs(this.endDate, true)
}
this.bizApi.getWithdrawableOrders(params).then(w => {
const items = ((w.data && w.data.items) || []).map(item => ({
...item,
uk: item.order_id + '_' + item.order_type,
}))
this.orderList = items
})
},
isSelected(item) {
return this.selectedUks.indexOf(item.uk) >= 0
},
toggleOrder(item) {
if (this.mode !== 2) return
const idx = this.selectedUks.indexOf(item.uk)
if (idx >= 0) {
this.selectedUks.splice(idx, 1)
} else {
this.selectedUks.push(item.uk)
}
},
goApplyDetail(id) {
if (!id) return
uni.navigateTo({ url: `${this.bizRoot}/withdrawal/record-detail?id=${id}` })
},
submit() {
const amt = Number(this.amount)
if (!amt || amt <= 0) {
uni.showToast({ title: '请输入有效金额', icon: 'none' })
let payload = { mode: this.mode, category: this.category }
if (this.mode === 1) {
const amt = Number(this.amount)
if (!amt || amt <= 0) {
uni.showToast({ title: '请输入有效金额', icon: 'none' })
return
}
payload.amount = amt
} else if (this.mode === 2) {
const picked = this.orderList.filter(item => this.selectedUks.indexOf(item.uk) >= 0)
if (picked.length === 0) {
uni.showToast({ title: '请选择订单', icon: 'none' })
return
}
payload.order_ids = picked.map(item => ({
order_id: item.order_id,
order_type: item.order_type,
}))
} else {
if (!this.startDate || !this.endDate) {
uni.showToast({ title: '请选择时间范围', icon: 'none' })
return
}
payload.start_at = this.toTs(this.startDate, false)
payload.end_at = this.toTs(this.endDate, true)
}
if (this.mode !== 1 && this.requestedSum > Number(this.balance || 0) + 0.0001) {
this.confirmGapThenSubmit(payload)
return
}
this.doSubmit(payload)
},
confirmGapThenSubmit(payload) {
const requested = this.requestedSum
const balance = Number(this.balance || 0)
const gap = (requested - balance).toFixed(2)
uni.showModal({
title: '超过可提余额',
content: `按单合计 ${requested.toFixed(2)} 元超过余额 ${balance.toFixed(2)} 元,差额 ${gap} 元。确认后按余额申请。`,
confirmText: '按余额提',
cancelText: '暂不提现',
success: (res) => {
if (res.confirm) {
payload.accept_gap = 1
this.doSubmit(payload)
}
},
})
},
doSubmit(payload) {
this.loading = true
this.bizApi.submitWithdrawal({ amount: amt }).then(w => {
this.bizApi.submitWithdrawal(payload).then(w => {
if (w.ok) {
uni.showToast({ title: '提交成功' })
setTimeout(() => uni.navigateBack(), 800)
@@ -64,42 +260,35 @@ export default {
</script>
<style lang="scss" scoped>
.tip {
.tip, .field, .gap {
font-size: 28rpx;
color: #666;
margin-bottom: 32rpx;
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
}
.field {
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
margin-bottom: 24rpx;
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
}
.label {
display: block;
font-size: 26rpx;
color: #999;
margin-bottom: 12rpx;
}
.quick {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 48rpx;
}
.q-btn {
padding: 12rpx 24rpx;
background: #eef8f6;
color: #6acdbb;
.gap { color: #B45309; background: #FFF7ED; }
.gap-order { display: block; margin-top: 12rpx; color: #0F766E; }
.label { display: block; font-size: 26rpx; color: #999; margin-bottom: 12rpx; }
.mode-row { display: flex; gap: 16rpx; }
.mode-btn {
padding: 10rpx 20rpx;
border-radius: 8rpx;
background: #F2F3F5;
color: #4E5969;
font-size: 26rpx;
&.on { background: #eef8f6; color: #6acdbb; }
}
.submit {
background: #6acdbb;
color: #fff;
border-radius: 44rpx;
}
.quick { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 16rpx; }
.q-btn { padding: 12rpx 24rpx; background: #eef8f6; color: #6acdbb; border-radius: 8rpx; font-size: 26rpx; }
.picker { padding: 16rpx 0; color: #1D2129; }
.preview { margin-top: 12rpx; color: #6acdbb; }
.order-list { background: #fff; border-radius: 16rpx; padding: 8rpx 24rpx; margin-bottom: 32rpx; }
.order-item { display: flex; gap: 16rpx; padding: 20rpx 0; border-bottom: 1rpx solid #F2F3F5; }
.chk { color: #6acdbb; font-size: 32rpx; }
.order-main { flex: 1; }
.no { display: block; color: #1D2129; font-size: 26rpx; }
.sub { color: #86909C; font-size: 24rpx; }
.empty { text-align: center; color: #86909C; padding: 32rpx 0; }
.submit { background: #6acdbb; color: #fff; border-radius: 44rpx; }
</style>

View File

@@ -21,9 +21,30 @@
<!-- 核心统计网格 -->
<view class="stats-grid">
<view v-for="b in balanceItems" :key="b.key" class="stat-island">
<view
v-for="b in balanceItems"
:key="b.key"
class="stat-island"
:class="{ 'stat-island-balance': b.key == 'balance' }"
hover-class="stat-hover"
@click="onStatClick(b.key)"
>
<text class="stat-label">{{ b.label }}</text>
<text class="stat-val">{{ b.display }}</text>
<view v-if="b.key == 'balance'" class="stat-break">
<view class="stat-chip" @click.stop="onStatClick('tcm')">
<text class="stat-chip-label">中药</text>
<text class="stat-chip-val">{{ tcmDisplay }}</text>
</view>
<view class="stat-chip" @click.stop="onStatClick('western')">
<text class="stat-chip-label">西药</text>
<text class="stat-chip-val">{{ westernDisplay }}</text>
</view>
<view class="stat-chip" @click.stop="onStatClick('other')">
<text class="stat-chip-label">其他</text>
<text class="stat-chip-val">{{ otherDisplay }}</text>
</view>
</view>
</view>
</view>
@@ -97,6 +118,9 @@ export default {
{ key: 'reviewing', label: '审核中金额 (元)', display: '0.00' },
{ key: 'pending', label: '待结算收益 (元)', display: '0.00' },
],
tcmDisplay: '0.00',
westernDisplay: '0.00',
otherDisplay: '0.00',
actions: [
{ key: 'apply', label: '申请提现' },
{ key: 'settlement', label: '结算记录' },
@@ -125,6 +149,16 @@ export default {
return `**** **** **** ${last4}`;
},
onStatClick(key) {
const map = { balance: 0, tcm: 1, western: 2, other: 3 }
if (map[key] !== undefined) {
uni.navigateTo({ url: `${this.bizRoot}/withdrawal/withdrawable-orders?category=${map[key]}` })
return
}
if (key === 'pending') {
uni.navigateTo({ url: `${this.bizRoot}/withdrawal/settlement/index` })
}
},
// 快捷操作跳转
onAction(key) {
if (key === 'apply') {
@@ -145,8 +179,8 @@ export default {
// 获取绑定的银行卡信息
loadCard() {
if (!this.bizApi.getMyCard) return
this.bizApi.getMyCard().then(w => {
if (!this.bizApi.getMyCard) return Promise.resolve()
return this.bizApi.getMyCard().then(w => {
if (w.ok && w.data) {
this.card = w.data;
this.cardTitle = w.data.bank_user_name || '我的账户';
@@ -156,8 +190,8 @@ export default {
// 获取余额数据
loadBalance() {
if (!this.bizApi.getMyBalance) return
this.bizApi.getMyBalance().then(w => {
if (!this.bizApi.getMyBalance) return Promise.resolve()
return this.bizApi.getMyBalance().then(w => {
if (w.ok && w.data) {
const fmt = (v) => formatMoney(v || 0).replace('¥', '')
const map = {
@@ -165,6 +199,9 @@ export default {
reviewing: fmt(w.data.withdrawn_frozen),
pending: fmt(w.data.pending_earnings),
}
this.tcmDisplay = fmt(w.data.tcm_balance)
this.westernDisplay = fmt(w.data.western_balance)
this.otherDisplay = fmt(w.data.other_balance)
this.balanceItems = this.balanceItems.map(item => ({
...item,
display: map[item.key] ?? '0.00',
@@ -173,24 +210,25 @@ export default {
});
},
// 刷新提现列表
/**
* 下拉刷新:卡、余额、列表一起重拉
* 下拉刷新:卡、余额、列表一起重拉,返回 Promise 给 mixin 等请求结束再收起转圈
*/
reload() {
this.loadCard()
this.loadBalance()
this.reloadList()
return Promise.all([
this.loadCard(),
this.loadBalance(),
this.reloadList(),
])
},
reloadList() {
this.page = 1;
this.loadList(1, false);
return this.loadList(1, false);
},
// 加载提现列表数据
loadList(page, append) {
this.loading = true;
this.bizApi.getWithdrawalList({ page, pageSize: 15 }).then(w => {
return this.bizApi.getWithdrawalList({ page, pageSize: 15 }).then(w => {
if (w.ok && w.data) {
const items = (w.data.items || []).map(item => ({
...item,
@@ -288,11 +326,15 @@ $color-text-muted: #86909C;
/* 统计网格 */
.stats-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
margin-bottom: 24rpx;
}
.stat-island-balance {
grid-column: 1 / -1;
}
.stat-island {
background: #fff;
border-radius: 24rpx;
@@ -301,6 +343,7 @@ $color-text-muted: #86909C;
flex-direction: column;
justify-content: center;
}
.stat-hover { background: #F7F8FA; }
.stat-label {
font-size: 24rpx;
@@ -315,6 +358,35 @@ $color-text-muted: #86909C;
font-family: DIN Alternate, monospace, sans-serif;
}
.stat-break {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
margin-top: 16rpx;
}
.stat-chip {
display: flex;
align-items: center;
gap: 8rpx;
padding: 6rpx 16rpx;
background: #F5F7F8;
border: 1px solid rgba(106, 205, 187, 0.3);
border-radius: 999rpx;
box-shadow: 0 0 8rpx rgba(106, 205, 187, 0.12);
}
.stat-chip-label {
font-size: 20rpx;
color: $color-text-muted;
}
.stat-chip-val {
font-size: 22rpx;
font-weight: 600;
color: $color-text-main;
}
/* 金刚区操作栏 */
.action-bar {
display: flex;

View File

@@ -17,10 +17,26 @@
</view>
</view>
<view class="progress">
<view
v-for="n in progressNodes"
:key="n.title"
class="progress-item"
:class="{ 'is-done': n.state === 'done', 'is-on': n.state === 'on', 'is-fail': n.state === 'fail' }"
>
<text class="progress-title">{{ n.title }}</text>
<text class="progress-desc">{{ n.desc }}</text>
</view>
</view>
<!-- 明细信息 -->
<view class="info-list">
<view class="row"><text class="label">订单号</text><text class="value font-mono">{{ info.order_no || '-' }}</text></view>
<view class="row"><text class="label">申请金额</text><text class="value">{{ formatMoney(info.apply_cash) }}</text></view>
<view class="row" v-if="info.mode_txt"><text class="label">提现方式</text><text class="value">{{ info.mode_txt }}</text></view>
<view class="row"><text class="label">中药</text><text class="value">{{ formatMoney(info.tcm_amount) }}</text></view>
<view class="row"><text class="label">西药</text><text class="value">{{ formatMoney(info.western_amount) }}</text></view>
<view class="row"><text class="label">其他</text><text class="value">{{ formatMoney(info.other_amount) }}</text></view>
<view class="row"><text class="label">手续费</text><text class="value">{{ formatMoney(info.charge_cash) }}</text></view>
<view class="row"><text class="label">申请时间</text><text class="value">{{ formatDateTime(info.apply_time) }}</text></view>
@@ -35,6 +51,18 @@
<view class="row"><text class="label">打款状态</text><text class="value">{{ dakuanStatusText(info.dakuan_status) }}</text></view>
<view class="row" v-if="info.dakuan_time"><text class="label">打款时间</text><text class="value">{{ formatDateTime(info.dakuan_time) }}</text></view>
</view>
<view v-if="info.orders && info.orders.length" class="order-box">
<text class="order-hd">关联订单</text>
<view
v-for="od in orderRows"
:key="od.row_key"
class="order-row"
@click="goOrder(od)"
>
<text>{{ od.order_no || '-' }} · {{ od.category_txt || '' }}</text>
<text class="order-amt">{{ od.amount }}</text>
</view>
</view>
</view>
</view>
@@ -51,12 +79,59 @@ import {
} from '@/subPackages/sub_business_shared/common/display.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
function buildProgressNodes(info) {
const check = Number((info && info.check_status) || 0)
const dakuan = Number((info && info.dakuan_status) || 0)
const applyTime = (info && info.apply_time) || '已提交'
const checkTime = (info && info.check_time) || ''
const dakuanTime = (info && info.dakuan_time) || ''
const checkResult = (info && info.check_result) || ''
const apply = { title: '申请', desc: applyTime, state: 'done' }
let audit = { title: '审核', desc: '待审核', state: 'wait' }
if (check === 1) audit = { title: '审核', desc: '待审核', state: 'on' }
else if (check === 2) audit = { title: '审核', desc: checkTime || '审核通过', state: 'done' }
else if (check === 3) audit = { title: '审核', desc: checkResult || '已拒绝', state: 'fail' }
else if (check === 4) audit = { title: '审核', desc: checkResult || '提现失败', state: 'fail' }
let pay = { title: '打款', desc: '待打款', state: 'wait' }
let arrived = { title: '到账', desc: '尚未打款', state: 'wait' }
if (check === 1) {
pay = { title: '打款', desc: '待审核通过后打款', state: 'wait' }
} else if (check === 3) {
pay = { title: '打款', desc: '已拒绝,未打款', state: 'wait' }
arrived = { title: '到账', desc: '未到账', state: 'wait' }
} else if (check === 4 || dakuan === -1) {
pay = { title: '打款', desc: dakuanTime || '打款失败', state: 'fail' }
arrived = { title: '到账', desc: '未到账', state: 'fail' }
} else if (dakuan === 1) {
pay = { title: '打款', desc: dakuanTime || '打款成功', state: 'done' }
arrived = { title: '到账', desc: dakuanTime || '已到账', state: 'done' }
} else if (check === 2) {
pay = { title: '打款', desc: '打款处理中', state: 'on' }
arrived = { title: '到账', desc: '等待到账', state: 'wait' }
}
return [apply, audit, pay, arrived]
}
export default {
mixins: [businessMixin],
components: { BusinessPageLayout },
data() {
return { id: 0, info: {} }
},
computed: {
progressNodes() {
return buildProgressNodes(this.info)
},
// 非 H5 的 :key 不能写表达式,预先落到单一字段
orderRows() {
const list = (this.info && this.info.orders) || []
return list.map((od, i) => {
const row = Object.assign({}, od)
row.row_key = String(od.id || od.order_id || i)
return row
})
},
},
onLoad(q) {
this.id = Number(q.id || 0)
this.bizApi.getWithdrawalDetail(this.id).then(w => {
@@ -71,6 +146,20 @@ export default {
},
dakuanStatusText(s) {
return dakuanStatusLabel(s)
},
goOrder(item) {
const orderType = Number(item.order_type || 0)
if (orderType === 1 && item.order_id) {
uni.navigateTo({ url: `${this.bizRoot}/order/detail?id=${item.order_id}` })
return
}
if (orderType === 2 && item.order_id) {
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${item.order_id}`,
})
return
}
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
}
},
}
@@ -168,4 +257,42 @@ $color-text-muted: #86909C;
background-color: #EAEFEF;
margin: 16rpx 0;
}
.order-box { margin-top: 24rpx; }
.order-hd { display: block; font-size: 26rpx; color: #86909C; margin-bottom: 12rpx; }
.order-row {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
font-size: 26rpx;
color: #6acdbb;
}
.order-amt { color: #1D2129; }
.progress {
display: flex;
justify-content: space-between;
gap: 8rpx;
margin-bottom: 32rpx;
}
.progress-item {
flex: 1;
padding: 16rpx 8rpx;
border-radius: 12rpx;
text-align: center;
background: #F5F7F8;
}
.progress-item.is-done { background: rgba(106, 205, 187, 0.12); }
.progress-item.is-on { background: #FFF3E8; }
.progress-item.is-fail { background: #FFECE8; }
.progress-title {
display: block;
font-size: 24rpx;
font-weight: 600;
color: #1D2129;
}
.progress-desc {
display: block;
margin-top: 6rpx;
font-size: 20rpx;
color: #86909C;
}
</style>

View File

@@ -55,7 +55,7 @@
<view class="card-actions">
<text class="action-link" @click.stop="goSettlementDetail(item)">结算明细</text>
<text
v-if="Number(item.order_type) === 1"
v-if="Number(item.order_type) === 1 || Number(item.order_type) === 2"
class="action-link primary"
@click.stop="goOrderDetail(item)"
>订单详情</text>
@@ -103,7 +103,7 @@ export default {
this.reload()
},
reload() {
this.load(1, false)
return this.load(1, false)
},
load(page, append) {
this.loading = true
@@ -116,7 +116,7 @@ export default {
if (this.orderNo.trim()) {
params.order_no = this.orderNo.trim()
}
this.bizApi.getLedgerLogList(params).then(w => {
return this.bizApi.getLedgerLogList(params).then(w => {
const items = (w.data && w.data.items) || []
this.list = append ? this.list.concat(items) : items
this.page = page
@@ -134,13 +134,18 @@ export default {
})
},
goOrderDetail(item) {
if (Number(item.order_type) !== 1 || !item.order_id) {
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
const orderType = Number(item.order_type || 0)
if (orderType === 1 && item.order_id) {
uni.navigateTo({ url: `${this.bizRoot}/order/detail?id=${item.order_id}` })
return
}
uni.navigateTo({
url: `${this.bizRoot}/order/detail?id=${item.order_id}`,
})
if (orderType === 2 && item.order_id) {
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${item.order_id}`,
})
return
}
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
},
},
}

View File

@@ -0,0 +1,107 @@
<template>
<business-page-layout :title="pageTitle" :show-back="true">
<view class="page">
<view
v-for="item in list"
:key="item._wxKey"
class="card"
hover-class="card-hover"
>
<view class="row head">
<text class="no">{{ item.order_no || '-' }}</text>
<text class="amt">{{ item.remain }}</text>
</view>
<view class="row"><text class="label">分类</text><text class="value">{{ item.category_txt || '-' }}</text></view>
<view class="row"><text class="label">订单类型</text><text class="value">{{ item.order_type_txt || '-' }}</text></view>
<view class="row"><text class="label">结算时间</text><text class="value">{{ item.settled_at_txt || '-' }}</text></view>
<text class="link" @click="goOrder(item)">查看订单</text>
</view>
<view v-if="!loading && list.length === 0" class="empty">暂无可提订单</view>
</view>
</business-page-layout>
</template>
<script>
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
import { withWxKey } from '@/utils/wxListKey.js'
export default {
mixins: [businessMixin, pullRefreshMixin],
components: { BusinessPageLayout },
data() {
return {
category: 0,
pageTitle: '可提订单',
list: [],
page: 1,
loading: false,
}
},
onLoad(q) {
this.category = Number(q.category || 0)
const titles = { 0: '全部可提订单', 1: '中药可提订单', 2: '西药可提订单', 3: '其他可提订单' }
this.pageTitle = titles[this.category] || '可提订单'
},
onShow() {
this.reload()
},
onReachBottom() {
this.load(this.page + 1, true)
},
methods: {
reload() {
return this.load(1, false)
},
load(page, append) {
if (!this.bizApi.getWithdrawableOrders) return Promise.resolve()
this.loading = true
return this.bizApi.getWithdrawableOrders({
page,
pageSize: 15,
category: this.category,
}).then(w => {
const items = withWxKey((w.data && w.data.items) || [], 'order_id', 'wo')
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
},
goOrder(item) {
const orderType = Number(item.order_type || 0)
if (orderType === 1 && item.order_id) {
uni.navigateTo({ url: `${this.bizRoot}/order/detail?id=${item.order_id}` })
return
}
if (orderType === 2 && item.order_id) {
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${item.order_id}`,
})
return
}
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
},
},
}
</script>
<style lang="scss" scoped>
.page { min-height: 100vh; padding: 24rpx; background: #F5F7F8; }
.card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 1px solid rgba(106, 205, 187, 0.3);
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
}
.card-hover { background: #FAFAFA; }
.row { display: flex; justify-content: space-between; padding: 8rpx 0; font-size: 26rpx; }
.head { margin-bottom: 8rpx; }
.no { font-weight: 600; color: #1D2129; }
.amt { font-weight: 700; color: #1D2129; font-family: DIN Alternate, monospace; }
.label { color: #86909C; }
.value { color: #1D2129; }
.link { display: block; margin-top: 12rpx; color: #6acdbb; font-size: 26rpx; }
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
</style>

View File

@@ -356,10 +356,11 @@ export default {
}
/* ================= 经营看板:hero 下沉浮白卡(统一工作台看板规格) ================= */
/* z-index 只为压住 hero 圆角,必须远低于弹层;禁止再给下方 content 加 z-index,否则常用编辑弹层会被看板盖住 */
.dashboard-wrapper {
margin: -30rpx 30rpx 0;
position: relative;
z-index: 11;
z-index: 2;
}
.dashboard-card {
@@ -414,7 +415,5 @@ export default {
/* ================= 主体内容区:个人功能区(分区/宫格样式在 workbench-user-entries 组件内) ================= */
.content-wrapper {
margin-top: 30rpx; /* 轻微向上偏移,与顶部区域产生连接感 */
position: relative;
z-index: 10;
}
</style>

View File

@@ -30,7 +30,7 @@
<view class="row"><text class="label">时间</text><text class="value">{{ item.created_at }}</text></view>
<view class="row" v-if="item.remark"><text class="label">备注</text><text class="value">{{ item.remark }}</text></view>
<view
v-if="Number(item.order_type) === 1 && item.order_id"
v-if="item.order_id || (item.orders && item.orders.length)"
class="order-link"
@click="goOrderDetail(item)"
>查看订单详情</view>
@@ -100,10 +100,26 @@ export default {
this.page = page
}).finally(() => { this.loading = false })
},
canViewChangeOrder(item) {
const orders = item.orders || []
return (Number(item.order_id) > 0) || orders.length > 0
},
goOrderDetail(item) {
uni.navigateTo({
url: '/subPackages/sub_clinic_admin/order/detail?id=' + item.order_id,
})
const orders = item.orders || []
const target = orders.length > 0 ? orders[0] : item
const orderType = Number(target.order_type || item.order_type || 0)
const orderId = target.order_id || item.order_id
if (orderType === 1 && orderId) {
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/order/detail?id=' + orderId })
return
}
if (orderType === 2 && orderId) {
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${orderId}`,
})
return
}
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
},
},
}

View File

@@ -1,12 +1,61 @@
<template>
<clinic-page-layout title="申请提现" :show-back="true">
<view class="tip">可提现余额:{{ balanceText }}</view>
<view v-if="gapText" class="gap">
<text>{{ gapText }}</text>
<view
v-for="row in gapApplies"
:key="row.id"
class="gap-order"
@click.stop="goApplyDetail(row.id)"
>{{ row.order_no }} · {{ row.check_status_txt || '' }} · {{ row.dakuan_status_txt || '' }}</view>
</view>
<view class="field">
<text class="label">提现方式</text>
<view class="mode-row">
<text
v-for="m in modes"
:key="m.value"
class="mode-btn"
:class="{ on: mode === m.value }"
@click="mode = m.value"
>{{ m.label }}</text>
</view>
</view>
<view v-if="mode === 1" class="field">
<text class="label">提现金额</text>
<input v-model="amount" type="digit" placeholder="请输入金额" />
<view class="quick">
<text v-for="q in quickAmounts" :key="q.key" class="q-btn" @click="amount = String(q.value)">{{ q.value }}</text>
</view>
</view>
<view class="quick">
<text v-for="q in quickAmounts" :key="q.key" class="q-btn" @click="amount = String(q.value)">{{ q.value }}</text>
<view v-if="mode === 3" class="field">
<text class="label">时间范围</text>
<picker mode="date" :value="startDate" @change="onStartDate">
<view class="picker">开始:{{ startDate || '请选择' }}</view>
</picker>
<picker mode="date" :value="endDate" @change="onEndDate">
<view class="picker">结束:{{ endDate || '请选择' }}</view>
</picker>
<picker :range="categoryLabels" @change="onCategory">
<view class="picker">类型:{{ currentCategoryLabel }}</view>
</picker>
<view class="preview" @click="loadOrders">预览订单({{ orderList.length }} 笔)</view>
</view>
<view v-if="mode === 2 || mode === 3" class="order-list">
<view
v-for="item in orderList"
:key="item.uk"
class="order-item"
@click="toggleOrder(item)"
>
<text class="chk">{{ isSelected(item) ? '☑' : '☐' }}</text>
<view class="order-main">
<text class="no">{{ item.order_no || '-' }}</text>
<text class="sub">{{ item.category_txt }} · {{ item.remain }}</text>
</view>
</view>
<view v-if="orderList.length === 0" class="empty">暂无可提订单</view>
</view>
<button class="submit" @click="submit" :loading="loading">提交申请</button>
</clinic-page-layout>
@@ -15,7 +64,7 @@
<script>
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
import { getClinicAdminBalance, submitWithdrawal } from '@/api/clinicAdmin.js'
import { getClinicAdminBalance, getWithdrawableOrders, submitWithdrawal } from '@/api/clinicAdmin.js'
export default {
components: { ClinicPageLayout },
@@ -23,7 +72,27 @@
return {
amount: '',
balanceText: '-',
balance: 0,
gapText: '',
gapApplies: [],
loading: false,
mode: 2,
category: 0,
startDate: '',
endDate: '',
orderList: [],
selectedUks: [],
modes: [
{ label: '按订单', value: 2 },
{ label: '按时间', value: 3 },
{ label: '自定义', value: 1 },
],
categoryOptions: [
{ label: '全部类型', value: 0 },
{ label: '中药', value: 1 },
{ label: '西药', value: 2 },
{ label: '其他', value: 3 },
],
quickAmounts: [
{ key: '1000', value: 1000 },
{ key: '3000', value: 3000 },
@@ -32,23 +101,136 @@
],
}
},
computed: {
categoryLabels() {
return this.categoryOptions.map(o => o.label)
},
currentCategoryLabel() {
const hit = this.categoryOptions.find(o => o.value === this.category)
return hit ? hit.label : '全部类型'
},
selectedSum() {
const picked = this.orderList.filter(item => this.selectedUks.indexOf(item.uk) >= 0)
return picked.reduce((sum, item) => sum + Number(item.remain || 0), 0)
},
rangeSum() {
return this.orderList.reduce((sum, item) => sum + Number(item.remain || 0), 0)
},
requestedSum() {
if (this.mode === 2) return this.selectedSum
if (this.mode === 3) return this.rangeSum
return 0
},
},
onLoad() {
getClinicAdminBalance().then(w => {
if (w.ok && w.data) {
const b = w.data.balance
this.balance = Number(b || 0)
this.balanceText = b != null ? formatMoney(b) : '-'
const gap = Number(w.data.custom_gap || 0)
this.gapApplies = w.data.custom_gap_applies || []
if (gap > 0) {
this.gapText = `按单合计比余额多 ${gap.toFixed(2)} 元,点打款单号可看申请→审核→打款进度`
}
}
})
this.loadOrders()
},
methods: {
onStartDate(e) { this.startDate = e.detail.value },
onEndDate(e) { this.endDate = e.detail.value },
onCategory(e) {
this.category = this.categoryOptions[Number(e.detail.value || 0)].value
},
toTs(dateStr, end) {
if (!dateStr) return 0
const t = new Date(dateStr.replace(/-/g, '/') + (end ? ' 23:59:59' : ' 00:00:00')).getTime()
return t ? Math.floor(t / 1000) : 0
},
loadOrders() {
const params = { page: 1, pageSize: 50, category: this.category }
if (this.mode === 3) {
params.start_at = this.toTs(this.startDate, false)
params.end_at = this.toTs(this.endDate, true)
}
getWithdrawableOrders(params).then(w => {
const items = ((w.data && w.data.items) || []).map(item => ({
...item,
uk: item.order_id + '_' + item.order_type,
}))
this.orderList = items
})
},
isSelected(item) {
return this.selectedUks.indexOf(item.uk) >= 0
},
toggleOrder(item) {
if (this.mode !== 2) return
const idx = this.selectedUks.indexOf(item.uk)
if (idx >= 0) {
this.selectedUks.splice(idx, 1)
} else {
this.selectedUks.push(item.uk)
}
},
goApplyDetail(id) {
if (!id) return
uni.navigateTo({ url: `/subPackages/sub_clinic_admin/withdrawal/record-detail?id=${id}` })
},
submit() {
const amt = Number(this.amount)
if (!amt || amt <= 0) {
uni.showToast({ title: '请输入有效金额', icon: 'none' })
let payload = { mode: this.mode, category: this.category }
if (this.mode === 1) {
const amt = Number(this.amount)
if (!amt || amt <= 0) {
uni.showToast({ title: '请输入有效金额', icon: 'none' })
return
}
payload.amount = amt
} else if (this.mode === 2) {
const picked = this.orderList.filter(item => this.selectedUks.indexOf(item.uk) >= 0)
if (picked.length === 0) {
uni.showToast({ title: '请选择订单', icon: 'none' })
return
}
payload.order_ids = picked.map(item => ({
order_id: item.order_id,
order_type: item.order_type,
}))
} else {
if (!this.startDate || !this.endDate) {
uni.showToast({ title: '请选择时间范围', icon: 'none' })
return
}
payload.start_at = this.toTs(this.startDate, false)
payload.end_at = this.toTs(this.endDate, true)
}
if (this.mode !== 1 && this.requestedSum > Number(this.balance || 0) + 0.0001) {
this.confirmGapThenSubmit(payload)
return
}
this.doSubmit(payload)
},
confirmGapThenSubmit(payload) {
const requested = this.requestedSum
const balance = Number(this.balance || 0)
const gap = (requested - balance).toFixed(2)
uni.showModal({
title: '超过可提余额',
content: `按单合计 ${requested.toFixed(2)} 元超过余额 ${balance.toFixed(2)} 元,差额 ${gap} 元。确认后按余额申请。`,
confirmText: '按余额提',
cancelText: '暂不提现',
success: (res) => {
if (res.confirm) {
payload.accept_gap = 1
this.doSubmit(payload)
}
},
})
},
doSubmit(payload) {
this.loading = true
submitWithdrawal({ amount: amt }).then(w => {
submitWithdrawal(payload).then(w => {
if (w.ok) {
uni.showToast({ title: '提交成功' })
setTimeout(() => uni.navigateBack(), 800)
@@ -62,42 +244,35 @@
</script>
<style lang="scss" scoped>
.tip {
.tip, .field, .gap {
font-size: 28rpx;
color: #666;
margin-bottom: 32rpx;
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
}
.field {
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
margin-bottom: 24rpx;
background: #fff;
padding: 24rpx;
border-radius: 16rpx;
}
.label {
display: block;
font-size: 26rpx;
color: #999;
margin-bottom: 12rpx;
}
.quick {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 48rpx;
}
.q-btn {
padding: 12rpx 24rpx;
background: #eef8f6;
color: #6acdbb;
.gap { color: #B45309; background: #FFF7ED; }
.gap-order { display: block; margin-top: 12rpx; color: #0F766E; }
.label { display: block; font-size: 26rpx; color: #999; margin-bottom: 12rpx; }
.mode-row { display: flex; gap: 16rpx; }
.mode-btn {
padding: 10rpx 20rpx;
border-radius: 8rpx;
background: #F2F3F5;
color: #4E5969;
font-size: 26rpx;
&.on { background: #eef8f6; color: #6acdbb; }
}
.submit {
background: #6acdbb;
color: #fff;
border-radius: 44rpx;
}
.quick { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 16rpx; }
.q-btn { padding: 12rpx 24rpx; background: #eef8f6; color: #6acdbb; border-radius: 8rpx; font-size: 26rpx; }
.picker { padding: 16rpx 0; color: #1D2129; }
.preview { margin-top: 12rpx; color: #6acdbb; }
.order-list { background: #fff; border-radius: 16rpx; padding: 8rpx 24rpx; margin-bottom: 32rpx; }
.order-item { display: flex; gap: 16rpx; padding: 20rpx 0; border-bottom: 1rpx solid #F2F3F5; }
.chk { color: #6acdbb; font-size: 32rpx; }
.order-main { flex: 1; }
.no { display: block; color: #1D2129; font-size: 26rpx; }
.sub { color: #86909C; font-size: 24rpx; }
.empty { text-align: center; color: #86909C; padding: 32rpx 0; }
.submit { background: #6acdbb; color: #fff; border-radius: 44rpx; }
</style>

View File

@@ -21,7 +21,13 @@
<!-- 核心统计网格 -->
<view class="stats-grid">
<view v-for="b in balanceItems" :key="b.key" class="stat-island">
<view
v-for="b in balanceItems"
:key="b.key"
class="stat-island"
hover-class="stat-hover"
@click="onStatClick(b.key)"
>
<text class="stat-label">{{ b.label }}</text>
<text class="stat-val">{{ b.display }}</text>
</view>
@@ -93,6 +99,9 @@ export default {
},
balanceItems: [
{ key: 'balance', label: '可提现余额 (元)', display: '0.00' },
{ key: 'tcm', label: '中药可提 (元)', display: '0.00' },
{ key: 'western', label: '西药可提 (元)', display: '0.00' },
{ key: 'other', label: '其他可提 (元)', display: '0.00' },
{ key: 'reviewing', label: '审核中金额 (元)', display: '0.00' },
{ key: 'pending', label: '待结算收益 (元)', display: '0.00' },
],
@@ -124,6 +133,16 @@ export default {
return `**** **** **** ${last4}`;
},
onStatClick(key) {
const map = { balance: 0, tcm: 1, western: 2, other: 3 }
if (map[key] !== undefined) {
uni.navigateTo({ url: `/subPackages/sub_clinic_admin/withdrawal/withdrawable-orders?category=${map[key]}` })
return
}
if (key === 'pending') {
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/withdrawal/settlement/index' })
}
},
// 快捷操作跳转
onAction(key) {
if (key === 'apply') {
@@ -159,6 +178,9 @@ export default {
const fmt = (v) => formatMoney(v || 0).replace('¥', '')
const map = {
balance: fmt(w.data.balance),
tcm: fmt(w.data.tcm_balance),
western: fmt(w.data.western_balance),
other: fmt(w.data.other_balance),
reviewing: fmt(w.data.withdrawn_frozen),
pending: fmt(w.data.pending_earnings),
}
@@ -298,6 +320,7 @@ $color-text-muted: #86909C;
flex-direction: column;
justify-content: center;
}
.stat-hover { background: #F7F8FA; }
.stat-label {
font-size: 24rpx;

View File

@@ -17,10 +17,26 @@
</view>
</view>
<view class="progress">
<view
v-for="n in progressNodes"
:key="n.title"
class="progress-item"
:class="{ 'is-done': n.state === 'done', 'is-on': n.state === 'on', 'is-fail': n.state === 'fail' }"
>
<text class="progress-title">{{ n.title }}</text>
<text class="progress-desc">{{ n.desc }}</text>
</view>
</view>
<!-- 明细信息 -->
<view class="info-list">
<view class="row"><text class="label">订单号</text><text class="value font-mono">{{ info.order_no || '-' }}</text></view>
<view class="row"><text class="label">申请金额</text><text class="value">{{ formatMoney(info.apply_cash) }}</text></view>
<view class="row" v-if="info.mode_txt"><text class="label">提现方式</text><text class="value">{{ info.mode_txt }}</text></view>
<view class="row"><text class="label">中药</text><text class="value">{{ formatMoney(info.tcm_amount) }}</text></view>
<view class="row"><text class="label">西药</text><text class="value">{{ formatMoney(info.western_amount) }}</text></view>
<view class="row"><text class="label">其他</text><text class="value">{{ formatMoney(info.other_amount) }}</text></view>
<view class="row"><text class="label">手续费</text><text class="value">{{ formatMoney(info.charge_cash) }}</text></view>
<view class="row"><text class="label">申请时间</text><text class="value">{{ formatDateTime(info.apply_time) }}</text></view>
@@ -35,6 +51,18 @@
<view class="row"><text class="label">打款状态</text><text class="value">{{ dakuanStatusText(info.dakuan_status) }}</text></view>
<view class="row" v-if="info.dakuan_time"><text class="label">打款时间</text><text class="value">{{ formatDateTime(info.dakuan_time) }}</text></view>
</view>
<view v-if="info.orders && info.orders.length" class="order-box">
<text class="order-hd">关联订单</text>
<view
v-for="od in orderRows"
:key="od.row_key"
class="order-row"
@click="goOrder(od)"
>
<text>{{ od.order_no || '-' }} · {{ od.category_txt || '' }}</text>
<text class="order-amt">{{ od.amount }}</text>
</view>
</view>
</view>
</view>
@@ -51,11 +79,58 @@ import {
} from '@/subPackages/sub_clinic_admin/common/display.js'
import { getWithdrawalDetail } from '@/api/clinicAdmin.js'
function buildProgressNodes(info) {
const check = Number((info && info.check_status) || 0)
const dakuan = Number((info && info.dakuan_status) || 0)
const applyTime = (info && info.apply_time) || '已提交'
const checkTime = (info && info.check_time) || ''
const dakuanTime = (info && info.dakuan_time) || ''
const checkResult = (info && info.check_result) || ''
const apply = { title: '申请', desc: applyTime, state: 'done' }
let audit = { title: '审核', desc: '待审核', state: 'wait' }
if (check === 1) audit = { title: '审核', desc: '待审核', state: 'on' }
else if (check === 2) audit = { title: '审核', desc: checkTime || '审核通过', state: 'done' }
else if (check === 3) audit = { title: '审核', desc: checkResult || '已拒绝', state: 'fail' }
else if (check === 4) audit = { title: '审核', desc: checkResult || '提现失败', state: 'fail' }
let pay = { title: '打款', desc: '待打款', state: 'wait' }
let arrived = { title: '到账', desc: '尚未打款', state: 'wait' }
if (check === 1) {
pay = { title: '打款', desc: '待审核通过后打款', state: 'wait' }
} else if (check === 3) {
pay = { title: '打款', desc: '已拒绝,未打款', state: 'wait' }
arrived = { title: '到账', desc: '未到账', state: 'wait' }
} else if (check === 4 || dakuan === -1) {
pay = { title: '打款', desc: dakuanTime || '打款失败', state: 'fail' }
arrived = { title: '到账', desc: '未到账', state: 'fail' }
} else if (dakuan === 1) {
pay = { title: '打款', desc: dakuanTime || '打款成功', state: 'done' }
arrived = { title: '到账', desc: dakuanTime || '已到账', state: 'done' }
} else if (check === 2) {
pay = { title: '打款', desc: '打款处理中', state: 'on' }
arrived = { title: '到账', desc: '等待到账', state: 'wait' }
}
return [apply, audit, pay, arrived]
}
export default {
components: { ClinicPageLayout },
data() {
return { id: 0, info: {} }
},
computed: {
progressNodes() {
return buildProgressNodes(this.info)
},
// 非 H5 的 :key 不能写表达式,预先落到单一字段
orderRows() {
const list = (this.info && this.info.orders) || []
return list.map((od, i) => {
const row = Object.assign({}, od)
row.row_key = String(od.id || od.order_id || i)
return row
})
},
},
onLoad(q) {
this.id = Number(q.id || 0)
getWithdrawalDetail(this.id).then(w => {
@@ -70,6 +145,20 @@ export default {
},
dakuanStatusText(s) {
return dakuanStatusLabel(s)
},
goOrder(item) {
const orderType = Number(item.order_type || 0)
if (orderType === 1 && item.order_id) {
uni.navigateTo({ url: `/subPackages/sub_clinic_admin/order/detail?id=${item.order_id}` })
return
}
if (orderType === 2 && item.order_id) {
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${item.order_id}`,
})
return
}
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
}
},
}
@@ -167,4 +256,42 @@ $color-text-muted: #86909C;
background-color: #EAEFEF;
margin: 16rpx 0;
}
.order-box { margin-top: 24rpx; }
.order-hd { display: block; font-size: 26rpx; color: #86909C; margin-bottom: 12rpx; }
.order-row {
display: flex;
justify-content: space-between;
padding: 16rpx 0;
font-size: 26rpx;
color: #6acdbb;
}
.order-amt { color: #1D2129; }
.progress {
display: flex;
justify-content: space-between;
gap: 8rpx;
margin-bottom: 32rpx;
}
.progress-item {
flex: 1;
padding: 16rpx 8rpx;
border-radius: 12rpx;
text-align: center;
background: #F5F7F8;
}
.progress-item.is-done { background: rgba(106, 205, 187, 0.12); }
.progress-item.is-on { background: #FFF3E8; }
.progress-item.is-fail { background: #FFECE8; }
.progress-title {
display: block;
font-size: 24rpx;
font-weight: 600;
color: #1D2129;
}
.progress-desc {
display: block;
margin-top: 6rpx;
font-size: 20rpx;
color: #86909C;
}
</style>

View File

@@ -55,7 +55,7 @@
<view class="card-actions">
<text class="action-link" @click.stop="goSettlementDetail(item)">结算明细</text>
<text
v-if="Number(item.order_type) === 1"
v-if="Number(item.order_type) === 1 || Number(item.order_type) === 2"
class="action-link primary"
@click.stop="goOrderDetail(item)"
>订单详情</text>
@@ -135,13 +135,18 @@ export default {
})
},
goOrderDetail(item) {
if (Number(item.order_type) !== 1 || !item.order_id) {
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
const orderType = Number(item.order_type || 0)
if (orderType === 1 && item.order_id) {
uni.navigateTo({ url: '/subPackages/sub_clinic_admin/order/detail?id=' + item.order_id })
return
}
uni.navigateTo({
url: '/subPackages/sub_clinic_admin/order/detail?id=' + item.order_id,
})
if (orderType === 2 && item.order_id) {
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${item.order_id}`,
})
return
}
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
},
},
}

View File

@@ -0,0 +1,106 @@
<template>
<clinic-page-layout :title="pageTitle" :show-back="true">
<view class="page">
<view
v-for="item in list"
:key="item._wxKey"
class="card"
hover-class="card-hover"
>
<view class="row head">
<text class="no">{{ item.order_no || '-' }}</text>
<text class="amt">{{ item.remain }}</text>
</view>
<view class="row"><text class="label">分类</text><text class="value">{{ item.category_txt || '-' }}</text></view>
<view class="row"><text class="label">订单类型</text><text class="value">{{ item.order_type_txt || '-' }}</text></view>
<view class="row"><text class="label">结算时间</text><text class="value">{{ item.settled_at_txt || '-' }}</text></view>
<text class="link" @click="goOrder(item)">查看订单</text>
</view>
<view v-if="!loading && list.length === 0" class="empty">暂无可提订单</view>
</view>
</clinic-page-layout>
</template>
<script>
import ClinicPageLayout from '@/subPackages/sub_clinic_admin/components/clinic-page-layout.vue'
import { getWithdrawableOrders } from '@/api/clinicAdmin.js'
import pullRefreshMixin from '@/subPackages/sub_business_shared/common/pullRefreshMixin.js'
import { withWxKey } from '@/utils/wxListKey.js'
export default {
mixins: [pullRefreshMixin],
components: { ClinicPageLayout },
data() {
return {
category: 0,
pageTitle: '可提订单',
list: [],
page: 1,
loading: false,
}
},
onLoad(q) {
this.category = Number(q.category || 0)
const titles = { 0: '全部可提订单', 1: '中药可提订单', 2: '西药可提订单', 3: '其他可提订单' }
this.pageTitle = titles[this.category] || '可提订单'
},
onShow() {
this.reload()
},
onReachBottom() {
this.load(this.page + 1, true)
},
methods: {
reload() {
this.load(1, false)
},
load(page, append) {
this.loading = true
getWithdrawableOrders({
page,
pageSize: 15,
category: this.category,
}).then(w => {
const items = withWxKey((w.data && w.data.items) || [], 'order_id', 'wo')
this.list = append ? this.list.concat(items) : items
this.page = page
}).finally(() => { this.loading = false })
},
goOrder(item) {
const orderType = Number(item.order_type || 0)
if (orderType === 1 && item.order_id) {
uni.navigateTo({ url: `/subPackages/sub_clinic_admin/order/detail?id=${item.order_id}` })
return
}
if (orderType === 2 && item.order_id) {
uni.navigateTo({
url: `/subPackages/sub_business_shared/user-patient/register-detail?id=${item.order_id}`,
})
return
}
uni.showToast({ title: '暂不支持查看该订单详情', icon: 'none' })
},
},
}
</script>
<style lang="scss" scoped>
.page { min-height: 100vh; padding: 24rpx; background: #F5F7F8; }
.card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 16rpx;
border: 1px solid rgba(106, 205, 187, 0.3);
box-shadow: 0 0 6px rgba(106, 205, 187, 0.18);
}
.card-hover { background: #FAFAFA; }
.row { display: flex; justify-content: space-between; padding: 8rpx 0; font-size: 26rpx; }
.head { margin-bottom: 8rpx; }
.no { font-weight: 600; color: #1D2129; }
.amt { font-weight: 700; color: #1D2129; font-family: DIN Alternate, monospace; }
.label { color: #86909C; }
.value { color: #1D2129; }
.link { display: block; margin-top: 12rpx; color: #6acdbb; font-size: 26rpx; }
.empty { text-align: center; color: #86909C; padding: 80rpx 0; }
</style>

View File

@@ -395,10 +395,11 @@ export default {
}
/* ================= 核心资产看板区 ================= */
/* z-index 只为压住 hero,必须低于常用编辑弹层(约 10080),避免浮卡盖住抽屉 */
.asset-dashboard {
margin: -80rpx 30rpx 40rpx;
position: relative;
z-index: 10;
z-index: 2;
}
/* 主卡片:黑金/深色沉浸质感 */

View File

@@ -321,10 +321,11 @@ export default {
}
/* ================= 平台看板:hero 下沉浮白卡(统一工作台看板规格) ================= */
/* z-index 只为压住 hero 圆角,必须远低于弹层(常用编辑 / 预约抽屉约 10080);禁止再给下方 content 加 z-index,否则会形成层叠上下文把 u-popup 困在看板下面 */
.dashboard-wrapper {
margin: -30rpx 30rpx 0;
position: relative;
z-index: 11;
z-index: 2;
}
.dashboard-card {
@@ -379,7 +380,5 @@ export default {
/* ================= 主体内容区:个人功能区(分区/宫格样式在 workbench-user-entries 组件内) ================= */
.content-wrapper {
margin-top: 30rpx; /* 轻微向上偏移,与顶部区域产生连接感 */
position: relative;
z-index: 10;
}
</style>

View File

@@ -233,7 +233,6 @@ export default {
],
form: createDefaultDoctorInputForm(),
}
}
},
computed: {
pageTitle() { return this.id ? '编辑医生预填' : '新建医生预填' },

View File

@@ -383,6 +383,5 @@ export default {
.stat-value-highlight { font-size: 36rpx; font-weight: 700; color: #059669; font-family: "DIN Alternate", sans-serif; }
.stat-unit { font-size: 20rpx; color: #94A3B8; margin-left: 4rpx; }
/* 个人功能区(分区/宫格样式在 workbench-user-entries 组件内) */
.content-wrapper { position: relative; z-index: 10; }
/* 个人功能区不要加 z-index:否则会形成层叠上下文,把「编辑常用」弹层困在看板/tabbar 下面 */
</style>

View File

@@ -0,0 +1,311 @@
<template>
<view v-if="show" class="pap-mask">
<view class="pap-sheet">
<view class="pap-head">
<text class="pap-title">申请公账支付</text>
<text class="pap-close" @click="close">关闭</text>
</view>
<view class="pap-body">
<view class="pap-card">
<view class="pap-row">
<text class="pap-label">订单号</text>
<text class="pap-value">{{ orderNo || '—' }}</text>
</view>
<view class="pap-row">
<text class="pap-label">待支付</text>
<text class="pap-value pap-price">¥{{ amount || '0.00' }}</text>
</view>
</view>
<text class="pap-hint">付款凭证选填。按天模式下订单会立即标为已付并计入日汇总;按订单模式需管理员确认到账。</text>
<view class="pap-upload">
<text class="pap-label">付款凭证(选填)</text>
<view class="pap-imgs">
<view
v-for="(url, idx) in voucherUrls"
:key="idx"
class="pap-img-wrap"
>
<image class="pap-img" :src="url" mode="aspectFill"></image>
<text class="pap-img-del" @click="removeImage(idx)">×</text>
</view>
<view v-if="voucherUrls.length < 9" class="pap-img-add" @click="chooseImage">
<text class="pap-img-add-txt">+</text>
</view>
</view>
</view>
<view class="pap-remark">
<text class="pap-label">备注</text>
<textarea
class="pap-textarea"
:value="applyRemark"
placeholder="可选"
maxlength="200"
@input="onRemarkInput"
/>
</view>
</view>
<view class="pap-foot">
<view class="pap-btn" :class="{ 'pap-btn--off': submitting }" @click="submit">
<text class="pap-btn-txt">{{ submitting ? '提交中...' : '提交申请' }}</text>
</view>
</view>
</view>
</view>
</template>
<script>
/**
* 医生小程序申请公账支付抽屉(父级用 page-container 拦截返回)
*/
import { applyPublicAccountPayApi } from '@/api/publicAccountPay.js';
import { unwrapXkApi } from '@/utils/api-response.js';
import { uploadLegacyAdminImage } from '@/api/upload.js';
export default {
name: 'ApplyPublicAccountPayDrawer',
props: {
value: { type: Boolean, default: false },
orderId: { type: [Number, String], default: 0 },
orderNo: { type: String, default: '' },
amount: { type: [Number, String], default: '' },
storeId: { type: [Number, String], default: 0 },
},
data() {
return {
voucherUrls: [],
applyRemark: '',
submitting: false,
};
},
computed: {
show() {
return this.value;
},
},
watch: {
value(val) {
if (val) {
this.voucherUrls = [];
this.applyRemark = '';
this.submitting = false;
}
},
},
methods: {
close() {
this.$emit('input', false);
},
onRemarkInput(e) {
this.applyRemark = (e && e.detail && e.detail.value) || '';
},
chooseImage() {
const remain = 9 - this.voucherUrls.length;
if (remain <= 0) return;
uni.chooseImage({
count: remain,
success: async (res) => {
const paths = res.tempFilePaths || [];
try {
uni.showLoading({ title: '上传中' });
for (let i = 0; i < paths.length; i++) {
const url = await uploadLegacyAdminImage({
filePath: paths[i],
formData: { store_id: String(this.storeId || '11001') },
});
if (url) this.voucherUrls.push(url);
}
} catch (e) {
uni.showToast({ title: '上传失败', icon: 'none' });
} finally {
uni.hideLoading();
}
},
});
},
removeImage(idx) {
this.voucherUrls.splice(idx, 1);
},
async submit() {
const oid = Number(this.orderId || 0);
if (!oid) {
uni.showToast({ title: '订单无效', icon: 'none' });
return;
}
if (this.submitting) return;
this.submitting = true;
try {
const res = await applyPublicAccountPayApi({
order_id: oid,
voucher_urls: this.voucherUrls,
apply_remark: this.applyRemark,
});
const wrap = unwrapXkApi(res);
if (!wrap.ok) {
uni.showToast({ title: wrap.message || '申请失败', icon: 'none' });
return;
}
const payload = wrap.payload || {}
const daily = payload.bill_mode === 'daily'
uni.showToast({ title: daily ? '已申请,订单已记为已付' : '已提交,等待确认到账', icon: 'none' });
this.$emit('success');
this.close();
} catch (e) {
uni.showToast({ title: (e && e.message) || '申请失败', icon: 'none' });
} finally {
this.submitting = false;
}
},
},
};
</script>
<style lang="scss" scoped>
.pap-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
z-index: 200;
background: rgba(15, 23, 42, 0.45);
display: flex;
align-items: flex-end;
}
.pap-sheet {
width: 100%;
max-height: 86vh;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
display: flex;
flex-direction: column;
}
.pap-head {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1px solid #f0f0f0;
}
.pap-title {
font-size: 32rpx;
font-weight: 600;
color: #1f2937;
}
.pap-close {
font-size: 26rpx;
color: #6b7280;
}
.pap-body {
flex: 1;
overflow-y: auto;
padding: 24rpx 32rpx 16rpx;
}
.pap-card {
padding: 24rpx;
border-radius: 16rpx;
border: 1px solid rgba(106, 205, 187, 0.35);
box-shadow: 0 0 12rpx rgba(106, 205, 187, 0.18);
background: #fff;
}
.pap-row {
display: flex;
justify-content: space-between;
margin-bottom: 12rpx;
}
.pap-row:last-child {
margin-bottom: 0;
}
.pap-label {
font-size: 26rpx;
color: #6b7280;
}
.pap-value {
font-size: 26rpx;
color: #111827;
font-weight: 600;
}
.pap-price {
color: #0f766e;
}
.pap-hint {
display: block;
margin: 20rpx 0;
font-size: 24rpx;
color: #9ca3af;
line-height: 1.5;
}
.pap-upload,
.pap-remark {
margin-bottom: 20rpx;
}
.pap-imgs {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-top: 12rpx;
}
.pap-img-wrap,
.pap-img-add {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
position: relative;
}
.pap-img {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
}
.pap-img-del {
position: absolute;
top: -8rpx;
right: -8rpx;
width: 36rpx;
height: 36rpx;
line-height: 32rpx;
text-align: center;
background: #ef4444;
color: #fff;
border-radius: 50%;
font-size: 28rpx;
}
.pap-img-add {
border: 1px dashed #d1d5db;
display: flex;
align-items: center;
justify-content: center;
}
.pap-img-add-txt {
font-size: 48rpx;
color: #9ca3af;
}
.pap-textarea {
margin-top: 12rpx;
width: 100%;
min-height: 140rpx;
padding: 16rpx;
box-sizing: border-box;
background: #f8fafc;
border-radius: 12rpx;
font-size: 26rpx;
}
.pap-foot {
padding: 16rpx 32rpx 40rpx;
}
.pap-btn {
height: 88rpx;
border-radius: 44rpx;
background: #6acdbb;
display: flex;
align-items: center;
justify-content: center;
}
.pap-btn--off {
opacity: 0.6;
}
.pap-btn-txt {
color: #fff;
font-size: 30rpx;
font-weight: 600;
}
</style>

View File

@@ -33,6 +33,51 @@
<text class="rx-permission-loading__text">{{ prescriptionTypeDenyMessage || '当前门店未开通开方权限' }}</text>
</view>
<!-- 开方成功页:代付码 / 申请公账 / 继续开方 -->
<view v-else-if="sentSuccess" class="rx-sent-page">
<view class="rx-sent-card">
<text class="rx-sent-ok">处方已发送</text>
<text class="rx-sent-sub">已推送到患者微信 · 支付完成后门店按方发药</text>
</view>
<view class="rx-sent-card">
<view class="rx-sent-row">
<text class="rx-sent-label">订单号</text>
<text class="rx-sent-value">{{ sentOrderNo || '—' }}</text>
</view>
<view class="rx-sent-row">
<text class="rx-sent-label">患者</text>
<text class="rx-sent-value">{{ sentPatientName || '—' }}</text>
</view>
<view class="rx-sent-row">
<text class="rx-sent-label">订单状态</text>
<text class="rx-sent-tag">{{ sentOrderPaid == 1 ? '已支付(公账先用后付)' : '待患者支付' }}</text>
</view>
<view class="rx-sent-row">
<text class="rx-sent-label">总金额</text>
<text class="rx-sent-price">¥{{ sentTotalPayPrice || '0.00' }}</text>
</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 && publicAccountPayEnabled && !publicAccountDailyMode && sentOrderPaid != 1" class="rx-sent-btn" @click="openPublicAccountApply">
<text class="rx-sent-btn-txt rx-sent-btn-txt--dark">申请公账支付</text>
</view>
<view class="rx-sent-btn" @click="continuePrescribe">
<text class="rx-sent-btn-txt rx-sent-btn-txt--dark">继续开方</text>
</view>
</view>
<proxy-pay-qrcode-modal v-model="proxyPayShow" :order-id="sentOrderId"></proxy-pay-qrcode-modal>
<apply-public-account-pay-drawer
v-model="showPublicAccountApply"
:order-id="sentOrderId"
:order-no="sentOrderNo"
:amount="sentTotalPayPrice"
:store-id="currentStoreId"
/>
</view>
<!-- 用 block 替代 template:微信小程序对 template+v-else 大块切换易触发渲染层 not iterable -->
<block v-else>
<!-- 外层:处方 | 病历(VIP) -->
@@ -816,6 +861,9 @@ import WesternMedicineUsageModal from './components/modals/WesternMedicineUsageM
import ChineseMedicineConfig from './components/ChineseMedicineConfig.vue';
import TraditionalTermPickerModal from './components/modals/TraditionalTermPickerModal.vue';
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 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';
@@ -853,6 +901,7 @@ const PRESCRIPTION_OVERLAY_KEYS = [
'showMedicalRecordOverlay',
'showAiPrescriptionDrawer',
'showGoldenFormulaModal',
'showPublicAccountApply',
];
export default {
@@ -871,6 +920,8 @@ export default {
ChineseMedicineConfig,
TraditionalTermPickerModal,
TransferPatientDrawer,
ApplyPublicAccountPayDrawer,
ProxyPayQrcodeModal,
WarehouseSelectDrawer,
OrderPricePercentAdjustPopup,
},
@@ -938,6 +989,15 @@ export default {
showCommonPrescriptionModal: false,
showAiPrescriptionDrawer: false,
showGoldenFormulaModal: false,
/** 开方成功页 + 申请公账抽屉 */
sentSuccess: false,
sentOrderId: 0,
sentOrderNo: '',
sentTotalPayPrice: '',
sentOrderPaid: 0,
sentPatientName: '',
showPublicAccountApply: false,
proxyPayShow: false,
/** 金方抽屉层级:0关闭 / 1列表 / 2对照(配合 page-container 返回栈) */
goldenFormulaLayer: 0,
/** 打开 AI 出方时带入的主诉(可在抽屉内再编辑) */
@@ -967,6 +1027,8 @@ export default {
/** 是否可查看毛利率 0/1(xk-api see_rate) */
seeRate: 0,
enableOrderPricePercentAdjust: 0,
publicAccountPayEnabled: false,
publicAccountDailyMode: false,
priceAdjustVisible: false,
priceDiscount: 100,
priceAdjustQuickOptions: [],
@@ -1226,6 +1288,9 @@ export default {
return this.hasStoreVipPermission(VIP_FEATURE.GOLDEN_FORMULA);
},
navbarTitle() {
if (this.sentSuccess) {
return '处方已发送';
}
if (this.isSalespersonTransferMode) {
return '传方';
}
@@ -1811,6 +1876,8 @@ export default {
const data = (res.result != null ? res.result : res.data != null ? res.data : res) || res;
this.seeRate = Number(data.see_rate ?? 0);
this.enableOrderPricePercentAdjust = Number(data.enable_order_price_percent_adjust ?? 0);
this.publicAccountPayEnabled = Number(data.public_account_pay_enabled ?? 0) === 1;
this.publicAccountDailyMode = String(data.public_account_bill_mode || '') === 'daily';
await this.loadPriceAdjustConfigForStore(params.store_id || storeId);
}
} catch (error) {
@@ -1847,6 +1914,8 @@ export default {
}
const priceStoreId = this.selectedStoreId || data.store_id || storeId;
this.enableOrderPricePercentAdjust = Number(data.enable_order_price_percent_adjust ?? 0);
this.publicAccountPayEnabled = Number(data.public_account_pay_enabled ?? 0) === 1;
this.publicAccountDailyMode = String(data.public_account_bill_mode || '') === 'daily';
await this.loadPriceAdjustConfigForStore(priceStoreId);
}
} catch (error) {
@@ -3350,12 +3419,14 @@ export default {
}
const res = await addWestPrescription(params);
if (res && (res.code === 0 || res.errcode === 0)) {
const wrap = unwrapXkApi(res);
if (wrap.ok) {
this.$toast('发送成功');
await this.syncMedicalRecordAfterSend();
const needTransfer = res?.result?.need_transfer || res?.data?.need_transfer || res?.need_transfer;
const payload = wrap.payload || {};
const needTransfer = payload.need_transfer || res.need_transfer;
if (needTransfer) {
const transferId = res?.result?.transfer_prescription_id || res?.data?.transfer_prescription_id || res?.transfer_prescription_id;
const transferId = payload.transfer_prescription_id || res.transfer_prescription_id;
if (transferId) {
uni.showModal({
title: '转诊提示', content: '该处方已触发转诊流程,转诊消息已发送给委托诊所的在线问诊医生。', showCancel: false, confirmText: '确定',
@@ -3372,11 +3443,16 @@ export default {
uni.showToast({ title: '处方消息同步失败', icon: 'none' });
}
}
this.sentOrderId = Number(payload.order_id || 0);
this.sentOrderNo = payload.order_no || '';
this.sentTotalPayPrice = payload.total_pay_price || this.totalPrice || '0.00';
this.sentOrderPaid = Number(payload.is_pay || 0);
this.sentPatientName = (this.patientInfo && this.patientInfo.name) || '';
this.clearPrescriptionData();
PrescriptionStorage.clearAllPrescriptionData(this.registerId);
this.safeNavigateBack(1500);
this.sentSuccess = true;
} else {
this.$toast(res.msg || res.message || '发送失败');
this.$toast(wrap.message || '发送失败');
}
} catch (error) {
console.error('发送处方失败:', error);
@@ -3664,6 +3740,26 @@ export default {
return;
}
this.safeNavigateBack(0);
},
/** 成功页继续开方:回到本页编辑态 */
continuePrescribe() {
this.sentSuccess = false;
this.showPublicAccountApply = false;
this.proxyPayShow = false;
},
openProxyPayFromSuccess() {
if (!this.sentOrderId) {
this.$toast('订单尚未生成');
return;
}
this.proxyPayShow = true;
},
openPublicAccountApply() {
if (!this.sentOrderId) {
this.$toast('订单尚未生成');
return;
}
this.showPublicAccountApply = true;
}
}
};
@@ -3681,6 +3777,81 @@ export default {
color: #909399;
text-align: center;
}
.rx-sent-page {
flex: 1;
padding: 32rpx;
}
.rx-sent-card {
margin-bottom: 24rpx;
padding: 32rpx;
background: #fff;
border-radius: 16rpx;
border: 1px solid rgba(106, 205, 187, 0.35);
box-shadow: 0 0 12rpx rgba(106, 205, 187, 0.18);
}
.rx-sent-ok {
display: block;
font-size: 36rpx;
font-weight: 700;
color: #111827;
text-align: center;
}
.rx-sent-sub {
display: block;
margin-top: 12rpx;
font-size: 24rpx;
color: #6b7280;
text-align: center;
}
.rx-sent-row {
display: flex;
justify-content: space-between;
margin-bottom: 16rpx;
}
.rx-sent-label {
font-size: 26rpx;
color: #6b7280;
}
.rx-sent-value {
font-size: 26rpx;
color: #111827;
font-weight: 600;
}
.rx-sent-tag {
font-size: 24rpx;
color: #c2410c;
}
.rx-sent-price {
font-size: 32rpx;
font-weight: 700;
color: #0f766e;
}
.rx-sent-ops {
display: flex;
flex-direction: column;
gap: 16rpx;
}
.rx-sent-btn {
height: 88rpx;
border-radius: 44rpx;
border: 1px solid rgba(106, 205, 187, 0.45);
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
.rx-sent-btn--main {
background: #6acdbb;
border-color: #6acdbb;
}
.rx-sent-btn-txt {
font-size: 30rpx;
font-weight: 600;
color: #fff;
}
.rx-sent-btn-txt--dark {
color: #0f766e;
}
/* 基础与通用样式 */
.page-bg {
background-color: #F4F6F8;

View File

@@ -0,0 +1,821 @@
<template>
<view class="lock-page" :class="cardToneClass">
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="guardShow"
:show="guardShow"
:overlay="false"
@beforeleave="onLeave"
@leave="onLeave"
/>
<!-- #endif -->
<u-navbar title="公账已锁定" :is-back="false" title-size="34" title-color="#fff" :background="navbarBg"></u-navbar>
<lock-warning-decor :color="alertColor" :tapes-only="step != 'hint'">
<view class="lock-body" :class="{ 'is-form': step != 'hint' }">
<view class="lock-card" :class="cardToneClass">
<block v-if="step == 'upload'">
<text class="lock-badge">上传付款凭证</text>
<text class="lock-title">处理逾期日账单</text>
<text class="lock-desc">{{ unlockHint }}</text>
<text class="lock-label">付款凭证(必传)</text>
<view class="lock-imgs">
<view v-for="(url, idx) in voucherUrls" :key="idx" class="lock-img-wrap">
<image class="lock-img" :src="url" mode="aspectFill"></image>
<text class="lock-img-del" @click="removeImage(idx)">×</text>
</view>
<view v-if="voucherUrls.length < 9" class="lock-img-add" @click="chooseImage">
<text class="lock-img-add-txt">+</text>
</view>
</view>
<textarea
class="lock-remark"
:value="payRemark"
placeholder="备注(选填)"
maxlength="200"
@input="onRemarkInput"
/>
<view class="lock-btn-row">
<view class="lock-btn lock-btn--ghost" @click="backToHint">
<text class="lock-btn-txt lock-btn-txt--dark">返回说明</text>
</view>
<view class="lock-btn" :class="{ 'is-off': submitting }" @click="submit">
<text class="lock-btn-txt">{{ submitting ? '提交中...' : '提交凭证' }}</text>
</view>
</view>
</block>
<block v-else-if="step == 'wait'">
<text class="lock-badge">等待管理员审核</text>
<text class="lock-title">凭证已提交</text>
<text class="lock-desc">平台管理员确认到账后自动解锁。可催办,或拨打下方号码联系管理员。</text>
<view class="lock-stat">
<view class="lock-stat-item">
<text class="lock-stat-label">账单日</text>
<text class="lock-stat-val">{{ billDateTxt || '—' }}</text>
</view>
<view class="lock-stat-item">
<text class="lock-stat-label">应付款</text>
<text class="lock-stat-val lock-stat-val--amt">¥{{ amountTxt }}</text>
</view>
</view>
<view v-if="contactPhones.length" class="lock-flow">
<text class="lock-flow-title">联系管理员</text>
<text class="lock-phone-hint">点击下方手机号即可拨打电话</text>
<view
v-for="item in contactPhones"
:key="item.row_key"
class="lock-phone"
@click="callPhone(item.tel)"
>
<text class="lock-phone-name">{{ item.name || '管理员' }}</text>
<view class="lock-phone-right">
<text class="lock-phone-num">{{ item.tel }}</text>
<text class="lock-phone-call">拨打</text>
</view>
</view>
</view>
<text v-else class="lock-desc">暂未配置可拨打的管理员号码,请使用催办。</text>
<view class="lock-btn-row">
<view class="lock-btn lock-btn--ghost" @click="goPay">
<text class="lock-btn-txt lock-btn-txt--dark">重新上传</text>
</view>
<view class="lock-btn" :class="{ 'is-off': urging }" @click="urge">
<text class="lock-btn-txt">{{ urging ? '催办中...' : '催办' }}</text>
</view>
</view>
</block>
<block v-else>
<text class="lock-badge">功能已锁定</text>
<text class="lock-title">公账日账单已逾期</text>
<text class="lock-desc">历史日账单未结清,接诊、开方已暂停。本提示不可关闭。</text>
<view class="lock-stat">
<view class="lock-stat-item">
<text class="lock-stat-label">账单日</text>
<text class="lock-stat-val">{{ billDateTxt || '—' }}</text>
</view>
<view class="lock-stat-item">
<text class="lock-stat-label">应付款</text>
<text class="lock-stat-val lock-stat-val--amt">¥{{ amountTxt }}</text>
</view>
</view>
<view class="lock-flow">
<text class="lock-flow-title">解锁流程</text>
<text class="lock-flow-item">1. 账单日次日 {{ lockTimeTxt }} 后未处理即锁定;更早的账单全天锁定。</text>
<text class="lock-flow-item">2. 在本提示中上传付款凭证(转账截图)。</text>
<text v-if="unlockByVoucher" class="lock-flow-item">3. 提交成功后立刻解锁,接诊和开方可继续。</text>
<text v-else class="lock-flow-item">3. 提交后仍锁定,等平台管理员确认到账后自动解锁。</text>
<text class="lock-flow-item">4. 未达解锁条件前,无法返回,也无法继续业务。</text>
</view>
<view class="lock-btn-row">
<view class="lock-btn" :class="{ 'is-off': openingPay }" @click="goPay">
<text class="lock-btn-txt">{{ openingPay ? '准备中...' : '去上传凭证' }}</text>
</view>
<view v-if="canViewProgress" class="lock-btn lock-btn--ghost" @click="goWait">
<text class="lock-btn-txt lock-btn-txt--dark">查看审核进度</text>
</view>
</view>
</block>
</view>
</view>
</lock-warning-decor>
</view>
</template>
<script>
/**
* 分包锁店页:锁定说明与上传在同一卡片内切换,警示 UI 对齐 PC 锁框
*/
import LockWarningDecor from './lock-warning-decor.vue'
import {
getPublicAccountDailyBillInfoApi,
generatePublicAccountDailyBillApi,
uploadPublicAccountDailyBillApi,
urgePublicAccountDailyBillApi,
getPublicAccountLockStatusApi,
} from '@/api/publicAccountPay.js'
import {
uploadLegacyAdminImage,
uploadClinicAdminChatFileApi,
extractUploadUrl,
} from '@/api/upload.js'
import { unwrapXkApi } from '@/utils/api-response.js'
import {
getPublicAccountLock,
isPublicAccountLocked,
clearPublicAccountLock,
goHomeAfterPublicAccountUnlock,
} from '@/utils/publicAccountLock.js'
export default {
components: { LockWarningDecor },
data() {
return {
guardShow: true,
step: 'hint',
payload: {},
dailyId: 0,
voucherUrls: [],
payRemark: '',
submitting: false,
openingPay: false,
urging: false,
allowLeave: false,
}
},
computed: {
navbarBg() {
if (this.alertColor === 'blue') {
return { background: 'linear-gradient(90deg, #1d4ed8 0%, #2563eb 100%)' }
}
if (this.alertColor === 'red') {
return { background: 'linear-gradient(90deg, #b91c1c 0%, #dc2626 100%)' }
}
return { background: 'linear-gradient(90deg, #b45309 0%, #d97706 100%)' }
},
billDateTxt() {
const date = String((this.payload && this.payload.bill_date) || '')
if (date.length === 8) {
return date.slice(0, 4) + '-' + date.slice(4, 6) + '-' + date.slice(6, 8)
}
return date
},
amountTxt() {
return (this.payload && this.payload.platform_amount) || '0.00'
},
alertColor() {
const color = (this.payload && this.payload.alert_color) || 'yellow'
if (color === 'blue' || color === 'red') {
return color
}
return 'yellow'
},
cardToneClass() {
return {
'is-blue': this.alertColor === 'blue',
'is-yellow': this.alertColor === 'yellow',
'is-red': this.alertColor === 'red',
}
},
unlockByVoucher() {
return String((this.payload && this.payload.unlock_mode) || '') === 'voucher'
},
lockTimeTxt() {
return (this.payload && this.payload.lock_time) || '09:00'
},
unlockHint() {
if (this.unlockByVoucher) {
return '提交成功后立刻解锁,接诊和开方可继续。'
}
return '提交后仍锁定,需平台管理员确认到账后才会解锁。'
},
// 只展示配置里允许拨打的号码
contactPhones() {
const list = (this.payload && this.payload.contact_phones) || []
const rows = []
for (let i = 0; i < list.length; i++) {
const item = list[i]
const tel = this.resolveDialPhone(item)
if (!tel) {
continue
}
rows.push({
name: (item && item.name) || '管理员',
tel: tel,
row_key: tel + '-' + i,
})
}
return rows
},
// 任一字段是 3 就给看进度;不能 status=0 优先盖掉 bill_status=3
canViewProgress() {
return this.resolveBillStatus(
this.payload && this.payload.status,
this.payload && this.payload.bill_status
) === 3
},
},
onShow() {
this.syncPayload()
this.refreshLockStatus()
},
onBackPress() {
return !this.allowLeave
},
methods: {
syncPayload() {
const cached = getPublicAccountLock()
if (isPublicAccountLocked(cached)) {
this.payload = cached
this.dailyId = Number((cached && cached.daily_id) || 0)
}
},
/**
* 已传凭证优先认 3,避免锁缓存里的 0 盖掉进度入口
*/
resolveBillStatus() {
let found = 0
let has3 = false
for (let i = 0; i < arguments.length; i++) {
if (arguments[i] == null || arguments[i] === '') {
continue
}
const num = Number(arguments[i])
if (num === 3) {
has3 = true
}
if (found === 0) {
found = num
}
}
return has3 ? 3 : found
},
applyBillStatus(status) {
const num = Number(status)
if (num !== 0 && num !== 1 && num !== 2 && num !== 3) {
return
}
this.payload.status = num
this.payload.bill_status = num
},
/**
* 进页拉系统配置配色、日汇总状态、可拨打号码
* 为什么:4101 本地缓存可能缺 alert_color,改后台配置后也不会自动刷新
*/
async refreshLockStatus() {
try {
const res = await getPublicAccountLockStatusApi()
const wrap = unwrapXkApi(res)
if (!wrap.ok) {
await this.syncBillStatusFromDetail()
return
}
const live = wrap.payload || {}
if (live.locked === false || live.locked === 0) {
this.allowLeave = true
clearPublicAccountLock()
this.guardShow = false
goHomeAfterPublicAccountUnlock()
return
}
const nextStatus = this.resolveBillStatus(
live.status,
live.bill_status,
this.payload && this.payload.status,
this.payload && this.payload.bill_status
)
this.payload = Object.assign({}, this.payload || {}, live)
this.applyBillStatus(nextStatus)
this.dailyId = Number((this.payload && this.payload.daily_id) || 0)
this.persistPayload()
await this.syncBillStatusFromDetail()
} catch (e) {
await this.syncBillStatusFromDetail()
}
},
/**
* 日汇总详情才是 status 真源,锁缓存停在 0 时这里能把进度入口拉出来
*/
async syncBillStatusFromDetail() {
if (this.dailyId < 1) {
this.dailyId = Number((this.payload && this.payload.daily_id) || 0)
}
if (this.dailyId < 1) {
return
}
const res = await getPublicAccountDailyBillInfoApi(this.dailyId)
const wrap = unwrapXkApi(res)
if (!wrap.ok) {
return
}
const detail = wrap.payload || {}
if (detail.platform_amount) {
this.payload.platform_amount = detail.platform_amount
}
if (detail.status != null) {
this.applyBillStatus(detail.status)
}
this.persistPayload()
},
goWait() {
if (!this.canViewProgress) {
uni.showToast({ title: '暂无待审核进度', icon: 'none' })
return
}
this.step = 'wait'
},
persistPayload() {
if (this.payload) {
uni.setStorageSync('xk_public_account_lock', this.payload)
}
},
/**
* 拨号用明文。phone 会被拦截器脱敏,优先 phone_txt(拦截器解密后写出)
*/
resolveDialPhone(item) {
if (!item) {
return ''
}
const raw = String(item.phone_txt || item.tel || item.phone || '')
const digits = raw.replace(/[^\d]/g, '')
if (/^1\d{10}$/.test(digits)) {
return digits
}
if (/^1\d{10}$/.test(raw)) {
return raw
}
return ''
},
callPhone(phone) {
const num = String(phone || '').replace(/[^\d]/g, '')
if (!/^1\d{10}$/.test(num)) {
uni.showToast({ title: '号码无效,请催办联系管理员', icon: 'none' })
return
}
uni.makePhoneCall({ phoneNumber: num })
},
async urge() {
if (this.urging) {
return
}
if (this.dailyId < 1) {
uni.showToast({ title: '暂无待审核日账单', icon: 'none' })
return
}
this.urging = true
try {
const res = await urgePublicAccountDailyBillApi({ id: this.dailyId })
const wrap = unwrapXkApi(res)
if (!wrap.ok) {
uni.showToast({ title: wrap.message || '催办失败', icon: 'none' })
return
}
uni.showToast({ title: '已向管理员发送催办', icon: 'none' })
} catch (e) {
uni.showToast({ title: (e && e.message) || '催办失败', icon: 'none' })
} finally {
this.urging = false
}
},
onRemarkInput(e) {
this.payRemark = (e && e.detail && e.detail.value) || ''
},
onLeave() {
this.guardShow = false
this.$nextTick(() => {
this.guardShow = true
})
},
backToHint() {
this.step = 'hint'
},
async goPay() {
if (this.openingPay) return
this.openingPay = true
try {
await this.ensureDailyId()
if (this.dailyId < 1) {
uni.showToast({ title: '暂无待支付日账单', icon: 'none' })
return
}
this.step = 'upload'
} finally {
this.openingPay = false
}
},
async ensureDailyId() {
if (this.dailyId > 0) {
await this.loadDetail()
return
}
const storeId = Number((this.payload && this.payload.store_id) || 0)
if (!this.billDateTxt) {
return
}
const res = await generatePublicAccountDailyBillApi({
store_id: storeId,
bill_date: this.billDateTxt,
})
const wrap = unwrapXkApi(res)
if (!wrap.ok) {
uni.showToast({ title: wrap.message || '暂无待支付日账单', icon: 'none' })
return
}
this.dailyId = Number((wrap.payload && wrap.payload.id) || 0)
if (this.payload) {
this.payload.daily_id = this.dailyId
this.persistPayload()
}
await this.loadDetail()
},
async loadDetail() {
await this.syncBillStatusFromDetail()
},
chooseImage() {
const remain = 9 - this.voucherUrls.length
if (remain <= 0) return
uni.chooseImage({
count: remain,
success: async (res) => {
const paths = res.tempFilePaths || []
try {
uni.showLoading({ title: '上传中' })
for (let i = 0; i < paths.length; i++) {
const url = await this.uploadOne(paths[i])
if (url) this.voucherUrls.push(url)
}
} catch (e) {
uni.showToast({ title: '上传失败', icon: 'none' })
} finally {
uni.hideLoading()
}
},
})
},
async uploadOne(filePath) {
if (uni.getStorageSync('loginMode') === 'clinic_admin') {
const res = await uploadClinicAdminChatFileApi({ filePath, formData: {} })
return extractUploadUrl(res)
}
const storeId = String((this.payload && this.payload.store_id) || '11001')
return uploadLegacyAdminImage({
filePath,
formData: { store_id: storeId },
})
},
removeImage(idx) {
this.voucherUrls.splice(idx, 1)
},
async submit() {
if (!this.voucherUrls.length) {
uni.showToast({ title: '请至少上传 1 张付款凭证', icon: 'none' })
return
}
if (this.dailyId < 1) {
uni.showToast({ title: '暂无待支付日账单', icon: 'none' })
return
}
if (this.submitting) return
this.submitting = true
try {
const res = await uploadPublicAccountDailyBillApi({
id: this.dailyId,
voucher_urls: this.voucherUrls,
pay_remark: this.payRemark,
})
const wrap = unwrapXkApi(res)
if (!wrap.ok) {
uni.showToast({ title: wrap.message || '上传失败', icon: 'none' })
return
}
if (wrap.payload && wrap.payload.unlocked) {
uni.showToast({ title: '已解锁', icon: 'none' })
this.allowLeave = true
clearPublicAccountLock()
this.guardShow = false
setTimeout(() => {
goHomeAfterPublicAccountUnlock()
}, 300)
return
}
const result = wrap.payload || {}
this.applyBillStatus(this.resolveBillStatus(result.status, result.bill_status, 3))
if (result.contact_phones && result.contact_phones.length) {
this.payload.contact_phones = result.contact_phones
}
this.persistPayload()
uni.showToast({ title: '已提交,等待管理员审核', icon: 'none' })
this.step = 'wait'
} catch (e) {
uni.showToast({ title: (e && e.message) || '上传失败', icon: 'none' })
} finally {
this.submitting = false
}
},
},
}
</script>
<style lang="scss" scoped>
.lock-page {
min-height: 100vh;
background: #17120a;
}
.lock-page.is-blue {
background: #0a1220;
}
.lock-page.is-red {
background: #200a0a;
}
.lock-body {
padding: 8rpx 32rpx 80rpx;
text-align: center;
}
.lock-body.is-form {
text-align: left;
}
.lock-card {
padding: 24rpx 8rpx 8rpx;
background: transparent;
}
.lock-badge {
display: inline-block;
padding: 4rpx 16rpx;
margin-bottom: 16rpx;
font-size: 22rpx;
color: #d97706;
border: 1px solid rgba(217, 119, 6, 0.4);
border-radius: 999rpx;
background: rgba(217, 119, 6, 0.12);
}
.lock-card.is-blue .lock-badge {
color: #2563eb;
border-color: rgba(37, 99, 235, 0.4);
background: rgba(37, 99, 235, 0.12);
}
.lock-card.is-red .lock-badge {
color: #dc2626;
border-color: rgba(220, 38, 38, 0.4);
background: rgba(220, 38, 38, 0.12);
}
.lock-title {
display: block;
margin-bottom: 12rpx;
font-size: 40rpx;
font-weight: 700;
color: #f8fafc;
}
.lock-desc {
display: block;
margin-bottom: 24rpx;
font-size: 26rpx;
line-height: 1.6;
color: #94a3b8;
}
.lock-stat {
display: flex;
gap: 16rpx;
margin-bottom: 20rpx;
}
.lock-stat-item,
.lock-flow {
padding: 20rpx;
border-radius: 12rpx;
border: 1px solid rgba(217, 119, 6, 0.3);
box-shadow: 0 0 12rpx rgba(217, 119, 6, 0.16);
background: rgba(15, 23, 42, 0.55);
}
.lock-card.is-blue .lock-stat-item,
.lock-card.is-blue .lock-flow {
border-color: rgba(37, 99, 235, 0.3);
box-shadow: 0 0 12rpx rgba(37, 99, 235, 0.16);
}
.lock-card.is-red .lock-stat-item,
.lock-card.is-red .lock-flow {
border-color: rgba(220, 38, 38, 0.3);
box-shadow: 0 0 12rpx rgba(220, 38, 38, 0.16);
}
.lock-stat-item {
flex: 1;
}
.lock-stat-label {
display: block;
margin-bottom: 8rpx;
font-size: 22rpx;
color: #94a3b8;
}
.lock-stat-val {
font-size: 30rpx;
font-weight: 600;
color: #f8fafc;
}
.lock-stat-val--amt {
color: #d97706;
}
.lock-card.is-blue .lock-stat-val--amt {
color: #2563eb;
}
.lock-card.is-red .lock-stat-val--amt {
color: #dc2626;
}
.lock-flow {
margin-bottom: 28rpx;
}
.lock-flow-title {
display: block;
margin-bottom: 12rpx;
font-size: 26rpx;
font-weight: 600;
color: #d97706;
}
.lock-card.is-blue .lock-flow-title {
color: #2563eb;
}
.lock-card.is-red .lock-flow-title {
color: #dc2626;
}
.lock-flow-item {
display: block;
margin-bottom: 8rpx;
font-size: 24rpx;
line-height: 1.6;
color: #64748b;
}
.lock-flow-item:last-child {
margin-bottom: 0;
}
.lock-label {
display: block;
margin-bottom: 16rpx;
font-size: 28rpx;
color: #e2e8f0;
}
.lock-imgs {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
margin-bottom: 20rpx;
}
.lock-img-wrap,
.lock-img-add {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
position: relative;
}
.lock-img {
width: 140rpx;
height: 140rpx;
border-radius: 12rpx;
}
.lock-img-del {
position: absolute;
top: -8rpx;
right: -8rpx;
width: 36rpx;
height: 36rpx;
line-height: 32rpx;
text-align: center;
background: #ef4444;
color: #fff;
border-radius: 50%;
font-size: 28rpx;
}
.lock-img-add {
border: 1px dashed rgba(148, 163, 184, 0.45);
display: flex;
align-items: center;
justify-content: center;
background: rgba(15, 23, 42, 0.45);
}
.lock-img-add-txt {
font-size: 48rpx;
color: #94a3b8;
}
.lock-remark {
width: 100%;
min-height: 140rpx;
padding: 16rpx;
box-sizing: border-box;
margin-bottom: 24rpx;
background: rgba(15, 23, 42, 0.55);
color: #e2e8f0;
border-radius: 12rpx;
font-size: 26rpx;
}
.lock-btn-row {
display: flex;
gap: 16rpx;
}
.lock-btn-row .lock-btn {
flex: 1;
}
.lock-btn {
height: 88rpx;
border-radius: 16rpx;
background: #d97706;
display: flex;
align-items: center;
justify-content: center;
}
.lock-page.is-blue .lock-btn {
background: #2563eb;
}
.lock-page.is-red .lock-btn {
background: #dc2626;
}
.lock-btn--ghost {
background: transparent;
border: 1px solid rgba(217, 119, 6, 0.45);
}
.lock-page.is-blue .lock-btn--ghost {
border-color: rgba(37, 99, 235, 0.45);
}
.lock-page.is-red .lock-btn--ghost {
border-color: rgba(220, 38, 38, 0.45);
}
.lock-btn.is-off {
opacity: 0.6;
}
.lock-btn-txt {
color: #fff;
font-size: 30rpx;
font-weight: 600;
}
.lock-btn-txt--dark {
color: #e2e8f0;
}
.lock-btn--mb {
margin-bottom: 16rpx;
}
.lock-phone-hint {
display: block;
margin-bottom: 12rpx;
font-size: 22rpx;
color: #94a3b8;
}
.lock-phone {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12rpx 0;
border-bottom: 1px solid #eef2f6;
}
.lock-phone:last-child {
border-bottom: 0;
padding-bottom: 0;
}
.lock-phone-name {
font-size: 26rpx;
color: #e2e8f0;
}
.lock-phone-right {
display: flex;
align-items: center;
gap: 12rpx;
}
.lock-phone-num {
font-size: 28rpx;
font-weight: 600;
color: #d97706;
}
.lock-phone-call {
font-size: 22rpx;
color: #d97706;
padding: 4rpx 12rpx;
border: 1px solid rgba(217, 119, 6, 0.4);
border-radius: 999rpx;
}
.lock-page.is-blue .lock-phone-num,
.lock-page.is-blue .lock-phone-call {
color: #2563eb;
}
.lock-page.is-blue .lock-phone-call {
border-color: rgba(37, 99, 235, 0.4);
}
.lock-page.is-red .lock-phone-num,
.lock-page.is-red .lock-phone-call {
color: #dc2626;
}
.lock-page.is-red .lock-phone-call {
border-color: rgba(220, 38, 38, 0.4);
}
</style>

View File

@@ -0,0 +1,212 @@
<template>
<!-- 对齐 PC LockWarningCanvas:说明页三角+脉冲环,各页都有上下警戒条 -->
<view class="decor" :class="toneClass">
<view class="tape tape--top">
<view class="tape-bar tape-bar--fwd"></view>
</view>
<view v-if="!tapesOnly" class="hero">
<view class="ring ring--1"></view>
<view class="ring ring--2"></view>
<view class="ring ring--3"></view>
<view class="glow"></view>
<view class="tri-wrap">
<view class="tri"></view>
<text class="bang">!</text>
</view>
</view>
<view class="slot-wrap">
<slot></slot>
</view>
<view class="tape tape--bottom">
<view class="tape-bar tape-bar--rev"></view>
</view>
</view>
</template>
<script>
/**
* 锁店警示装饰,视觉对齐 PC 锁框(警戒条 / 三角 / 脉冲环)
* 小程序不用 canvas,用 CSS 动画避免真机画布兼容问题
*/
export default {
props: {
color: {
type: String,
default: 'yellow',
},
tapesOnly: {
type: Boolean,
default: false,
},
},
computed: {
toneClass() {
return {
'is-blue': this.color === 'blue',
'is-yellow': this.color === 'yellow',
'is-red': this.color === 'red',
}
},
},
}
</script>
<style lang="scss" scoped>
.decor {
--tape: #d97706;
--ink: #1c1917;
--ring: rgba(217, 119, 6, 0.35);
--tri: #d97706;
--glow: rgba(217, 119, 6, 0.22);
pointer-events: none;
}
.slot-wrap {
pointer-events: auto;
}
.decor.is-blue {
--tape: #2563eb;
--ink: #0f172a;
--ring: rgba(37, 99, 235, 0.35);
--tri: #2563eb;
--glow: rgba(37, 99, 235, 0.22);
}
.decor.is-red {
--tape: #dc2626;
--ink: #1c1917;
--ring: rgba(220, 38, 38, 0.35);
--tri: #dc2626;
--glow: rgba(220, 38, 38, 0.22);
}
.tape {
left: 0;
right: 0;
height: 36rpx;
overflow: hidden;
background: var(--tape);
}
.tape--top {
position: relative;
z-index: 20;
}
.tape--bottom {
position: fixed;
bottom: 0;
z-index: 20;
}
.tape-bar {
width: 240%;
height: 100%;
background-image: repeating-linear-gradient(
-45deg,
var(--tape) 0,
var(--tape) 16rpx,
var(--ink) 16rpx,
var(--ink) 32rpx
);
}
.tape-bar--fwd {
animation: tape-fwd 0.9s linear infinite;
}
.tape-bar--rev {
animation: tape-rev 0.9s linear infinite;
}
@keyframes tape-fwd {
from {
transform: translateX(0);
}
to {
transform: translateX(-32rpx);
}
}
@keyframes tape-rev {
from {
transform: translateX(-32rpx);
}
to {
transform: translateX(0);
}
}
.hero {
position: relative;
height: 280rpx;
margin-bottom: 12rpx;
}
.glow {
position: absolute;
left: 50%;
top: 50%;
width: 280rpx;
height: 280rpx;
margin-left: -140rpx;
margin-top: -140rpx;
border-radius: 50%;
background: var(--glow);
}
.ring {
position: absolute;
left: 50%;
top: 50%;
border: 2rpx solid var(--ring);
border-radius: 50%;
animation: ring-pulse 2.4s ease-out infinite;
}
.ring--1 {
width: 80rpx;
height: 80rpx;
margin-left: -40rpx;
margin-top: -40rpx;
}
.ring--2 {
width: 80rpx;
height: 80rpx;
margin-left: -40rpx;
margin-top: -40rpx;
animation-delay: 0.8s;
}
.ring--3 {
width: 80rpx;
height: 80rpx;
margin-left: -40rpx;
margin-top: -40rpx;
animation-delay: 1.6s;
}
@keyframes ring-pulse {
0% {
transform: scale(1);
opacity: 0.45;
}
100% {
transform: scale(3.4);
opacity: 0;
}
}
.tri-wrap {
position: absolute;
left: 50%;
top: 50%;
width: 120rpx;
height: 110rpx;
margin-left: -60rpx;
margin-top: -58rpx;
}
.tri {
width: 0;
height: 0;
margin: 0 auto;
border-left: 60rpx solid transparent;
border-right: 60rpx solid transparent;
border-bottom: 104rpx solid var(--tri);
filter: drop-shadow(0 0 12rpx var(--glow));
}
.bang {
position: absolute;
left: 0;
right: 0;
top: 36rpx;
text-align: center;
font-size: 48rpx;
font-weight: 800;
line-height: 1;
color: #fff;
}
</style>

View File

@@ -0,0 +1,77 @@
/**
* 公账逾期锁店:4101 写入本地并强制跳到分包锁店页
*/
const STORAGE_KEY = 'xk_public_account_lock'
export const PUBLIC_ACCOUNT_LOCK_PAGE = '/subPackages/sub_workbench/public_account_lock/index'
let jumping = false
export function setPublicAccountLock(payload) {
const data = payload && typeof payload === 'object' ? payload : {}
uni.setStorageSync(STORAGE_KEY, data)
uni.$emit('public-account-lock', data)
redirectToPublicAccountLockPage()
}
export function getPublicAccountLock() {
return uni.getStorageSync(STORAGE_KEY) || null
}
/**
* 是否已锁店:漏单未出日汇总时 daily_id=0,要用 bill_date / locked
*/
export function isPublicAccountLocked(payload) {
if (!payload || typeof payload !== 'object') {
return false
}
if (payload.locked === true || payload.locked === 1) {
return true
}
return !!(payload.daily_id || payload.bill_date)
}
export function clearPublicAccountLock() {
uni.removeStorageSync(STORAGE_KEY)
uni.$emit('public-account-lock-clear')
}
export function isOnPublicAccountLockPage() {
const pages = getCurrentPages()
const cur = pages.length ? pages[pages.length - 1] : null
const route = cur && cur.route ? String(cur.route) : ''
return route.indexOf('public_account_lock') !== -1
}
/**
* 拦截器命中 4101 后跳分包锁店页;已在该页则不再跳,避免死循环
*/
export function redirectToPublicAccountLockPage() {
if (isOnPublicAccountLockPage() || jumping) {
return
}
jumping = true
// reLaunch 清掉上一页,模拟器返回不能退回业务页;tabBar 页也不能用 redirectTo
uni.reLaunch({
url: PUBLIC_ACCOUNT_LOCK_PAGE,
complete() {
jumping = false
},
})
}
/**
* 解锁后回各自工作台
*/
export function goHomeAfterPublicAccountUnlock() {
const mode = uni.getStorageSync('loginMode')
const identity = Number(uni.getStorageSync('identity') || 1)
if (mode === 'clinic_admin') {
uni.reLaunch({ url: '/subPackages/sub_clinic_admin/home/index' })
return
}
if (identity === 2) {
uni.switchTab({ url: '/pages/pharmacist/index' })
return
}
uni.switchTab({ url: '/pages/workbench/index' })
}