1. 诊所管理

This commit is contained in:
李琦
2026-06-18 10:20:24 +08:00
parent 3f6da79ece
commit 73d535378e
32 changed files with 1859 additions and 136 deletions

View File

@@ -113,6 +113,21 @@ export function createBusinessApi(prefix, headerFlag) {
data: { order_id: orderId }, data: { order_id: orderId },
}); });
}, },
getExpressCompaniesOption() {
return bizRequest({ url: `${prefix}-express-companies/option`, method: 'GET' });
},
getChinaErpSyncLogs(params) {
return bizRequest({ url: `${prefix}-order/china-erp-sync-logs`, method: 'GET', data: params });
},
syncChinaOrderToErp(data) {
return bizRequest({ url: `${prefix}-order/sync-china-erp`, method: 'POST', data });
},
getPriceAdjustConfig(storeId) {
return bizRequest({ url: `${prefix}-order/price-adjust-config`, method: 'GET', data: storeId ? { store_id: storeId } : {} });
},
adjustOrderPercent(data) {
return bizRequest({ url: `${prefix}-order/adjust-order-percent`, method: 'POST', data });
},
getWarehouseList(params) { getWarehouseList(params) {
return bizRequest({ url: `${prefix}-warehouse/list`, method: 'GET', data: params }); return bizRequest({ url: `${prefix}-warehouse/list`, method: 'GET', data: params });
}, },

View File

@@ -155,6 +155,19 @@ export function getExpressDetailByOrderId(orderId) {
}); });
} }
export function getPriceAdjustConfig(storeId) {
return clinicRequest({
url: `${PREFIX}-order/price-adjust-config`,
method: 'GET',
data: storeId ? { store_id: storeId } : {},
});
}
export function adjustOrderPercent(data) {
return clinicRequest({ url: `${PREFIX}-order/adjust-order-percent`, method: 'POST', data });
}
export function getWarehouseList(params) { export function getWarehouseList(params) {
return clinicRequest({ url: `${PREFIX}-warehouse/list`, method: 'GET', data: params }); return clinicRequest({ url: `${PREFIX}-warehouse/list`, method: 'GET', data: params });
} }

View File

@@ -314,6 +314,15 @@ export function getCurrentStoreTypeApi(data) {
}) })
} }
// 订单百分比调价配置(开方前)
export function getPriceAdjustConfigApi(storeId) {
return req.request({
url: '/newApi/doctor-reception-wx/price-adjust-config',
method: 'GET',
data: storeId ? { store_id: storeId } : {}
})
}
// 接诊页匹配推广员传方 // 接诊页匹配推广员传方
export function getSalespersonTransferByRegisterApi(registerId) { export function getSalespersonTransferByRegisterApi(registerId) {
return req.request({ return req.request({

View File

@@ -18,6 +18,16 @@ function uploadChatFileByPrefix(prefix, headerKey, params) {
}); });
} }
function getOssSignatureByPrefix(prefix, headerKey) {
return req.request({
url: `${prefix}oss-signature`,
method: 'GET',
header: {
[headerKey]: '1',
},
});
}
/** /**
* 业务员录入/聊天同款:服务端转存 OSS返回 url * 业务员录入/聊天同款:服务端转存 OSS返回 url
*/ */
@@ -77,6 +87,44 @@ export function extractUploadUrl(res) {
return inner.url || inner.path || null; return inner.url || inner.path || null;
} }
/**
* 从 Laravel jok 响应解包 OSS 签名
*/
export function unwrapOssSignature(res) {
const inner = unwrapUploadResult(res);
if (!inner) return null;
if (!inner.accessKeyId || !inner.policy || !inner.signature || !inner.host || !inner.bucket) {
return null;
}
return inner;
}
export function getSalespersonOssSignatureApi() {
return getOssSignatureByPrefix(SALESPERSON_PREFIX, '_salesperson');
}
export function getPlatformAdminOssSignatureApi() {
return getOssSignatureByPrefix(PLATFORM_PREFIX, '_platformAdmin');
}
export function getClinicAdminOssSignatureApi() {
return getOssSignatureByPrefix(CLINIC_ADMIN_PREFIX, '_clinicAdmin');
}
/**
* 按当前 loginMode 获取 OSS 直传签名
*/
export function getBusinessOssSignatureApi() {
const mode = uni.getStorageSync('loginMode');
if (mode === 'platform_admin') {
return getPlatformAdminOssSignatureApi();
}
if (mode === 'clinic_admin') {
return getClinicAdminOssSignatureApi();
}
return getSalespersonOssSignatureApi();
}
const LEGACY_ADMIN_UPLOAD_URL = 'https://api.xiaokang88.com/open-api/upload/old-admin-img'; const LEGACY_ADMIN_UPLOAD_URL = 'https://api.xiaokang88.com/open-api/upload/old-admin-img';
/** /**

100
common/js/oss-upload.js Normal file
View File

@@ -0,0 +1,100 @@
/**
* 阿里云 OSS PostObject 直传(对齐 xk-admin oss-upload.ts
*
* 运维:微信小程序 uploadFile 合法域名需包含 OSS host
* 例如 https://xiaokang88.oss-cn-hangzhou.aliyuncs.com
*/
import { getBusinessOssSignatureApi, unwrapOssSignature } from '@/api/upload.js';
/**
* @param {string} filePath 本地临时文件路径
* @returns {string} xk_upload_YYYYMMDD/uuid_timestamp.ext
*/
export function generateObjectName(filePath) {
const today = new Date();
const year = today.getFullYear();
const month = String(today.getMonth() + 1).padStart(2, '0');
const day = String(today.getDate()).padStart(2, '0');
const dateStr = `${year}${month}${day}`;
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
const timestamp = Math.floor(Date.now() / 1000);
const pathStr = String(filePath || '');
const lastDotIndex = pathStr.lastIndexOf('.');
const extension = lastDotIndex > 0 ? pathStr.substring(lastDotIndex + 1).toLowerCase() : 'jpg';
return `xk_upload_${dateStr}/${uuid}_${timestamp}.${extension}`;
}
/**
* @returns {Promise<{ accessKeyId, policy, signature, host, bucket, key, expire }>}
*/
export async function getOssSignatureFromBackend() {
const res = await getBusinessOssSignatureApi();
const signature = unwrapOssSignature(res);
if (!signature) {
throw new Error('获取 OSS 签名失败');
}
return signature;
}
/**
* @param {{ filePath: string, onProgress?: (percent: number) => void }} options
* @returns {Promise<{ url: string, objectName: string }>}
*/
export async function uploadToOss(options) {
const { filePath, onProgress } = options || {};
if (!filePath) {
throw new Error('文件路径为空');
}
const signature = await getOssSignatureFromBackend();
const objectName = generateObjectName(filePath);
const formData = {
key: objectName,
policy: signature.policy,
OSSAccessKeyId: signature.accessKeyId,
signature: signature.signature,
success_action_status: '200',
'x-oss-object-acl': 'public-read',
};
return new Promise((resolve, reject) => {
const uploadTask = uni.uploadFile({
url: signature.host,
filePath,
name: 'file',
formData,
success: (uploadRes) => {
const code = uploadRes.statusCode;
if (code >= 200 && code < 300 || code === 204) {
const host = String(signature.host).replace(/\/$/, '');
resolve({
url: `${host}/${objectName}`,
objectName,
});
return;
}
reject(new Error(`OSS 上传失败HTTP ${code}`));
},
fail: (err) => {
reject(err instanceof Error ? err : new Error((err && err.errMsg) || 'OSS 上传失败'));
},
});
if (onProgress && uploadTask && typeof uploadTask.onProgressUpdate === 'function') {
uploadTask.onProgressUpdate((res) => {
if (res.totalBytesExpectedToSend > 0) {
const percent = Math.round((res.totalBytesSent / res.totalBytesExpectedToSend) * 100);
onProgress(percent);
}
});
}
});
}

View File

@@ -86,7 +86,7 @@
<u-loading slot="loading"></u-loading> <u-loading slot="loading"></u-loading>
</u-image> </u-image>
<u-badge <u-badge
:key="'wb-ub-' + acceptingRowKey(item, aix) + '-' + Number(item.unread_count || 0)" :key="acceptingBadgeKey(item, aix)"
v-if="item.consultation_channel === 'online' && Number(item.unread_count) > 0" v-if="item.consultation_channel === 'online' && Number(item.unread_count) > 0"
:count="Number(item.unread_count)" :count="Number(item.unread_count)"
:offset="[-4, -4]" :offset="[-4, -4]"
@@ -121,6 +121,10 @@
v-if="item.consultation_channel !== 'online' && acceptingOfflineOrderText(item)" v-if="item.consultation_channel !== 'online' && acceptingOfflineOrderText(item)"
class="wb-sub text-hide m-t-8" class="wb-sub text-hide m-t-8"
>{{ acceptingOfflineOrderText(item) }}</text> >{{ acceptingOfflineOrderText(item) }}</text>
<text
v-if="item.consultation_channel !== 'online'"
class="wb-sub text-hide m-t-8"
>推广员{{ (item.salesperson && item.salesperson.nick_name) || '无' }}</text>
<!-- 第三行状态与操作按钮 --> <!-- 第三行状态与操作按钮 -->
<view class="flex-row flex-jus-sp flex-ali-center m-t-12"> <view class="flex-row flex-jus-sp flex-ali-center m-t-12">
@@ -629,6 +633,10 @@ export default {
const room = item.room_id != null && item.room_id !== '' ? String(item.room_id) : 'noroom'; const room = item.room_id != null && item.room_id !== '' ? String(item.room_id) : 'noroom';
return `${item.consultation_channel || 'off'}-${room}-${rid}-${index}`; return `${item.consultation_channel || 'off'}-${room}-${rid}-${index}`;
}, },
acceptingBadgeKey(item, index) {
const unread = item.unread_count != null ? Number(item.unread_count) : 0;
return `wb-ub-${this.acceptingRowKey(item, index)}-${unread}`;
},
acceptingPreview(item) { acceptingPreview(item) {
const p = item.last_message_preview; const p = item.last_message_preview;
if (p != null && String(p).trim() !== '') return String(p); if (p != null && String(p).trim() !== '') return String(p);

View File

@@ -135,6 +135,11 @@ export function erpSyncText(v) {
return '未同步' return '未同步'
} }
export function prescriptionStatusText(status) {
const m = { 0: '待审核', 1: '已审核', 2: '已驳回', 3: '已过期' }
return m[status] != null ? m[status] : '-'
}
export function freeShippingText(v) { export function freeShippingText(v) {
if (v === 1) return '包邮' if (v === 1) return '包邮'
return '不包邮' return '不包邮'
@@ -238,6 +243,7 @@ export function mapOrderProductItem(p, prescriptionType) {
: '×' + (p.number != null ? p.number : 0) : '×' + (p.number != null ? p.number : 0)
const isTcm = prescriptionType === 1 const isTcm = prescriptionType === 1
return { return {
id: p.id,
drugName: p.drug_name || drug.drug_name || '药品', drugName: p.drug_name || drug.drug_name || '药品',
drugNumber: drug.drug_number || '', drugNumber: drug.drug_number || '',
imageUrl: resolveDrugImage(p.drug_image || drug.image), imageUrl: resolveDrugImage(p.drug_image || drug.image),
@@ -276,6 +282,9 @@ export function mapOrderDetailDisplay(info) {
if (!info || !info.id) return {} if (!info || !info.id) return {}
const freeShip = info.is_free_shipping === 1 || info.free_ship === 1 const freeShip = info.is_free_shipping === 1 || info.free_ship === 1
const canView = !!(info.p_id && info.order_type !== 2 && info.order_type !== 3) const canView = !!(info.p_id && info.order_type !== 2 && info.order_type !== 3)
const presStatus = info.prescription && info.prescription.status != null
? info.prescription.status
: null
return { return {
orderTypeText: orderTypeText(info.order_type), orderTypeText: orderTypeText(info.order_type),
marketPrice: formatMoney(info.market_price), marketPrice: formatMoney(info.market_price),
@@ -284,6 +293,7 @@ export function mapOrderDetailDisplay(info) {
showPrescription: mapOrderPrescription(info), showPrescription: mapOrderPrescription(info),
prescriptionId: info.p_id, prescriptionId: info.p_id,
canViewPrescription: canView, canViewPrescription: canView,
prescriptionStatusText: canView && presStatus != null ? prescriptionStatusText(presStatus) : '',
statusText: orderStatusText(info.status), statusText: orderStatusText(info.status),
prescriptionTypeText: prescriptionTypeText(info.prescription_type), prescriptionTypeText: prescriptionTypeText(info.prescription_type),
deliveryText: deliveryMethodText(info.delivery_method), deliveryText: deliveryMethodText(info.delivery_method),
@@ -398,16 +408,20 @@ export function warehouseStatusText(status) {
return '-' return '-'
} }
export function mapWarehouseRow(item) { export function mapWarehouseRow(item, warehouseType = 'store') {
if (!item) return {} if (!item) return {}
const drug = item.drug || {} const drug = item.drug || {}
const storeDrug = drug.drug_store_drug || {} const storeDrug = drug.drug_store_drug || {}
const suggestPrice = storeDrug.price != null ? storeDrug.price : drug.price const suggestPrice = storeDrug.price != null ? storeDrug.price : drug.price
const isPlatform = warehouseType === 'platform'
const supplyRaw = isPlatform
? (item.market_price ?? item.buy_price)
: (item.buy_price ?? item.market_price)
return { return {
id: item.id, id: item.id,
drugName: drug.drug_name || '-', drugName: drug.drug_name || '-',
imageUrl: resolveDrugImage(drug.image), imageUrl: resolveDrugImage(drug.image),
buyPrice: formatMoney(item.buy_price), buyPrice: formatMoney(supplyRaw),
price: formatMoney(item.price), price: formatMoney(item.price),
suggestPrice: formatMoney(suggestPrice), suggestPrice: formatMoney(suggestPrice),
stock: item.stock != null ? item.stock : '-', stock: item.stock != null ? item.stock : '-',

View File

@@ -0,0 +1,144 @@
import { normalizeQuickOptions as normalizeQuickOptionsFromUtil } from '@/subPackages/sub_business_shared/common/pricePercentAdjust.js'
export const orderPriceAdjustMixin = {
data() {
return {
priceAdjustVisible: false,
priceAdjustQuickOptions: [],
priceAdjustScopeMode: 'sale_only',
priceAdjustSubmitting: false,
}
},
computed: {
canOrderPercentAdjust() {
return Number(this.info?.is_pay) !== 1
&& Number(this.info?.store?.enable_order_price_percent_adjust) === 1
},
orderPriceDiscountLabel() {
const d = Number(this.info?.price_discount ?? 100)
if (d === 100) return ''
return this.info?.price_discount_label || this.formatDiscountLabel(d)
},
},
methods: {
formatDiscountLabel(discount) {
const d = Number(discount) || 100
if (d <= 0 || d === 100) return ''
if (d < 100) {
if (d % 10 === 0 && d >= 10) return `${d / 10}`
return `折扣 ${d}%`
}
return `${d - 100}%`
},
normalizeQuickOptions(raw) {
return normalizeQuickOptionsFromUtil(raw)
},
async loadPriceAdjustMeta(apiFn) {
try {
const w = await apiFn()
if (w?.ok && w.data) {
this.priceAdjustScopeMode = w.data.order_price_adjust_scope === 'both' ? 'both' : 'sale_only'
this.priceAdjustQuickOptions = this.normalizeQuickOptions(w.data.order_discount_quick_options)
}
} catch (e) { /* ignore */ }
},
openOrderPriceAdjust() {
if (!this.canOrderPercentAdjust) return
this.priceAdjustVisible = true
},
closePriceAdjust() {
this.priceAdjustVisible = false
},
async onPriceAdjustConfirm(payload, api) {
if (this.priceAdjustSubmitting) return
this.priceAdjustSubmitting = true
try {
const w = await api.adjustOrderPercent({
product_order_id: this.info.id,
price_discount: payload.priceDiscount,
})
if (!w.ok) return
uni.showToast({ title: payload.priceDiscount === 100 ? '已清除浮动' : '调价成功' })
this.closePriceAdjust()
await this.load()
} finally {
this.priceAdjustSubmitting = false
}
},
async clearOrderPriceDiscount(api) {
return this.onPriceAdjustConfirm({ priceDiscount: 100 }, api)
},
},
}
export { applyRatioToDrug, applyRatioToDrugs } from '@/subPackages/sub_business_shared/common/pricePercentAdjust.js'

View File

@@ -0,0 +1,77 @@
export const DEFAULT_QUICK_OPTIONS = [
{ name: '九五折', value: 95 },
{ name: '九折', value: 90 },
{ name: '八五折', value: 85 },
{ name: '八折', value: 80 },
{ name: '涨10%', value: 110 },
{ name: '涨20%', value: 120 },
]
const DEFAULT_MARKUP_OPTIONS = [
{ name: '涨10%', value: 110 },
{ name: '涨20%', value: 120 },
]
export function normalizeQuickOptions(raw) {
if (!Array.isArray(raw)) return [...DEFAULT_QUICK_OPTIONS]
const list = []
for (const item of raw) {
if (item && typeof item === 'object' && item.value != null) {
const val = Number(item.value)
if (val > 0) {
list.push({ name: String(item.name || `${val}%`), value: val })
}
} else if (typeof item === 'number' && item > 0) {
list.push({ name: `${item}%`, value: item })
}
}
if (!list.length) return [...DEFAULT_QUICK_OPTIONS]
if (!list.some((o) => o.value > 100)) {
return [...list, ...DEFAULT_MARKUP_OPTIONS]
}
return list
}
export function applyRatioPrice(originPrice, discount) {
const origin = Number(originPrice || 0)
const ratio = Number(discount) > 0 ? Number(discount) : 100
if (!origin || origin <= 0) return 0
return Math.round(origin * (ratio / 100) * 10000) / 10000
}
export function formatPriceDiscountLabel(discount) {
const d = Number(discount) || 100
if (d <= 0 || d === 100) return null
if (d < 100) {
if (d % 10 === 0 && d >= 10) return `${d / 10}`
return `折扣 ${d}%`
}
return `${d - 100}%`
}
export function applyRatioToDrug(drug, discount, scope) {
if (!drug) return drug
const originPrice = Number(drug.origin_price ?? drug.price ?? 0)
const originBuy = Number(drug.origin_buy_price ?? drug.buy_price ?? 0)
const next = {
...drug,
origin_price: drug.origin_price ?? originPrice,
origin_buy_price: drug.origin_buy_price ?? originBuy,
price: applyRatioPrice(originPrice, discount),
}
if (scope === 'both' && originBuy > 0) {
next.buy_price = applyRatioPrice(originBuy, discount)
}
return next
}
export function applyRatioToDrugs(drugs, discount, scope) {
return (drugs || []).map((d) => applyRatioToDrug(d, discount, scope))
}
/** @deprecated */
export function applyPercentToDrug(drug, adjustType, percent, scope) {
let discount = Number(percent)
if (adjustType === 'markup') discount = Math.round(100 + Number(percent))
return applyRatioToDrug(drug, discount, scope)
}

View File

@@ -0,0 +1,94 @@
<template>
<view>
<!-- #ifdef MP-WEIXIN -->
<page-container
v-if="usePageGuard && show"
:show="show"
:overlay="false"
@leave="onPageLeave"
/>
<!-- #endif -->
<u-popup
:value="show"
mode="bottom"
border-radius="24"
:height="height"
:safe-area-inset-bottom="true"
:mask-close-able="maskCloseable"
:closeable="closeable"
@input="onPopupInput"
@close="close"
>
<view class="drawer-inner" :class="{ 'drawer-inner-tall': tall }">
<view v-if="title && !closeable" class="drawer-header">
<text class="drawer-title">{{ title }}</text>
<text v-if="showClose" class="drawer-close" @click="close">关闭</text>
</view>
<view v-else-if="title && closeable" class="drawer-header drawer-header-compact">
<text class="drawer-title">{{ title }}</text>
</view>
<slot />
</view>
</u-popup>
</view>
</template>
<script>
export default {
name: 'DrawerPageContainer',
props: {
value: { type: Boolean, default: false },
title: { type: String, default: '' },
showClose: { type: Boolean, default: true },
maskCloseable: { type: Boolean, default: true },
closeable: { type: Boolean, default: false },
usePageGuard: { type: Boolean, default: true },
height: { type: String, default: '' },
tall: { type: Boolean, default: false },
},
computed: {
show: {
get() { return this.value },
set(v) { this.$emit('input', v) },
},
},
methods: {
close() {
this.show = false
this.$emit('close')
},
onPopupInput(v) {
this.show = v
},
onPageLeave() {
this.close()
},
},
}
</script>
<style scoped>
.drawer-inner {
background: #fff;
max-height: 80vh;
display: flex;
flex-direction: column;
}
.drawer-inner-tall {
max-height: 85vh;
min-height: 55vh;
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
flex-shrink: 0;
}
.drawer-header-compact {
padding-top: 48rpx;
}
.drawer-title { font-size: 32rpx; font-weight: 600; color: #1d2129; }
.drawer-close { font-size: 28rpx; color: #6acdbb; }
</style>

View File

@@ -0,0 +1,128 @@
<template>
<u-popup v-model="show" mode="bottom" border-radius="16" height="auto" :closeable="true" @close="onClose">
<view class="popup">
<view class="title">整单调价</view>
<view v-if="currentLabel" class="hint">当前浮动{{ currentLabel }}</view>
<view class="row">
<text class="label">价格比例100=原价</text>
<input class="input" type="digit" v-model="discountInput" placeholder="如 90=九折120=涨20%" />
</view>
<view v-if="discountQuickOptions.length" class="quick-row">
<text class="quick-label">快捷打折</text>
<view class="quick-wrap">
<text
v-for="opt in discountQuickOptions"
:key="'d-' + opt.value"
class="quick"
@click="discountInput = String(opt.value)"
>{{ opt.name }}</text>
</view>
</view>
<view v-if="markupQuickOptions.length" class="quick-row">
<text class="quick-label">快捷涨价</text>
<view class="quick-wrap">
<text
v-for="opt in markupQuickOptions"
:key="'m-' + opt.value"
class="quick markup"
@click="discountInput = String(opt.value)"
>{{ opt.name }}</text>
</view>
</view>
<view class="actions">
<button class="btn cancel" @click="onClose">取消</button>
<button class="btn ghost" @click="discountInput = '100'">清除浮动</button>
<button class="btn confirm" :loading="submitting" @click="onConfirm">确定</button>
</view>
</view>
</u-popup>
</template>
<script>
import { formatPriceDiscountLabel } from '@/subPackages/sub_business_shared/common/pricePercentAdjust.js'
export default {
name: 'OrderPricePercentAdjustPopup',
props: {
visible: { type: Boolean, default: false },
currentDiscount: { type: [Number, String], default: 100 },
quickOptions: { type: Array, default: () => [] },
submitting: { type: Boolean, default: false },
},
data() {
return {
show: false,
discountInput: '100',
}
},
computed: {
currentLabel() {
return formatPriceDiscountLabel(Number(this.currentDiscount) || 100)
},
discountQuickOptions() {
return (this.quickOptions || []).filter((o) => Number(o.value) < 100)
},
markupQuickOptions() {
return (this.quickOptions || []).filter((o) => Number(o.value) > 100)
},
},
watch: {
visible: {
immediate: true,
handler(v) {
this.show = v
if (v) {
this.syncDiscountInput()
}
},
},
currentDiscount(v) {
if (this.show) {
this.syncDiscountInput(v)
}
},
show(v) {
if (!v) this.$emit('close')
},
},
methods: {
syncDiscountInput(discount) {
const d = Number(discount ?? this.currentDiscount) || 100
this.discountInput = String(d)
},
onClose() {
this.show = false
},
onConfirm() {
const priceDiscount = parseInt(String(this.discountInput).trim(), 10)
if (!priceDiscount || priceDiscount <= 0) {
uni.showToast({ title: '请输入有效比例', icon: 'none' })
return
}
this.$emit('confirm', { priceDiscount })
},
},
}
</script>
<style lang="scss" scoped>
.popup { padding: 32rpx 32rpx 48rpx; }
.title { font-size: 32rpx; font-weight: 600; text-align: center; }
.hint { font-size: 26rpx; color: #666; text-align: center; margin-top: 12rpx; }
.row { margin-top: 28rpx; }
.label, .quick-label { font-size: 26rpx; color: #666; display: block; margin-bottom: 12rpx; }
.input { background: #f5f5f5; border-radius: 12rpx; padding: 20rpx; font-size: 28rpx; }
.quick-row { margin-top: 20rpx; }
.quick-wrap { display: flex; flex-wrap: wrap; gap: 16rpx; }
.quick { padding: 10rpx 24rpx; background: #eef9f6; color: #6acdbb; border-radius: 20rpx; font-size: 24rpx; }
.quick.markup { background: #fff7e6; color: #fa8c16; }
.actions { display: flex; gap: 16rpx; margin-top: 36rpx; }
.btn { flex: 1; font-size: 28rpx; border-radius: 12rpx; }
.cancel { background: #f5f5f5; color: #666; }
.ghost { background: #fff; color: #6acdbb; border: 1rpx solid #6acdbb; }
.confirm { background: #6acdbb; color: #fff; }
</style>

View File

@@ -0,0 +1,175 @@
<template>
<drawer-page-container
v-model="show"
title="ERP 同步记录"
height="80%"
tall
:show-close="true"
:closeable="false"
@close="onClose"
>
<view class="popup-wrap">
<view class="summary">
<text>订单号{{ orderNo || '—' }}</text>
<text class="status-tag" :class="{ synced: isSyncErp === 1 }">{{ isSyncErp === 1 ? '已同步' : '未同步' }}</text>
</view>
<view v-if="showSyncBtn" class="sync-bar">
<button class="btn-sync" :loading="syncing" @click="handleSync">同步到 ERP</button>
</view>
<scroll-view scroll-y class="log-scroll" @scrolltolower="loadMore">
<view v-if="loading && !logs.length" class="hint">加载中...</view>
<view v-else-if="!logs.length" class="hint">暂无同步记录</view>
<view v-for="row in logs" :key="row.id" class="log-item">
<view class="log-head">
<text class="log-time">{{ row.created_at }}</text>
<text class="log-result" :class="{ ok: row.is_success === 1 }">
{{ row.is_success === 1 ? '成功' : '失败' }}
</text>
</view>
<view class="log-preview">{{ row.content_preview || row.content || '—' }}</view>
</view>
<view v-if="loadingMore" class="hint">加载更多...</view>
</scroll-view>
</view>
</drawer-page-container>
</template>
<script>
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue'
export default {
name: 'ErpSyncPopup',
components: { DrawerPageContainer },
props: {
bizApi: { type: Object, required: true },
},
data() {
return {
show: false,
orderId: 0,
orderNo: '',
isSyncErp: 0,
logs: [],
page: 1,
pageSize: 10,
total: 0,
loading: false,
loadingMore: false,
syncing: false,
}
},
computed: {
showSyncBtn() {
return this.isSyncErp !== 1
},
},
methods: {
open(order) {
this.orderId = order.id
this.orderNo = order.order_no || ''
this.isSyncErp = Number(order.is_sync_erp)
this.logs = []
this.page = 1
this.total = 0
this.show = true
this.loadLogs(false)
},
onClose() {
this.show = false
},
loadLogs(append) {
if (!this.orderId) return
if (append) {
if (this.logs.length >= this.total) return
this.loadingMore = true
} else {
this.loading = true
}
const page = append ? this.page + 1 : 1
this.bizApi.getChinaErpSyncLogs({
order_id: this.orderId,
page,
pageSize: this.pageSize,
}).then(w => {
if (!w.ok) return
const d = w.data || {}
const list = d.list || []
this.total = d.total != null ? d.total : list.length
this.page = page
this.logs = append ? this.logs.concat(list) : list
}).finally(() => {
this.loading = false
this.loadingMore = false
})
},
loadMore() {
if (!this.loading && !this.loadingMore) {
this.loadLogs(true)
}
},
handleSync() {
uni.showModal({
title: '同步到 ERP',
content: `确认将订单 ${this.orderNo || this.orderId} 同步到 MES`,
success: (r) => {
if (!r.confirm) return
this.syncing = true
this.bizApi.syncChinaOrderToErp({ order_id: this.orderId }).then(w => {
if (w.ok) {
uni.showToast({ title: '同步成功' })
this.isSyncErp = 1
this.page = 1
this.loadLogs(false)
this.$emit('success')
}
}).finally(() => { this.syncing = false })
},
})
},
},
}
</script>
<style lang="scss" scoped>
.popup-wrap {
padding: 0 32rpx 32rpx;
height: 100%;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.summary {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
color: #4E5969;
margin-bottom: 16rpx;
flex-shrink: 0;
}
.status-tag {
padding: 4rpx 16rpx;
border-radius: 8rpx;
background: #FFF3E8;
color: #FF7D00;
font-size: 22rpx;
}
.status-tag.synced { background: rgba(106,205,187,.1); color: #6acdbb; }
.sync-bar { margin-bottom: 16rpx; flex-shrink: 0; }
.btn-sync {
margin: 0;
background: #6acdbb;
color: #fff;
border-radius: 12rpx;
font-size: 28rpx;
}
.btn-sync::after { display: none; }
.log-scroll { flex: 1; min-height: 0; }
.hint { text-align: center; color: #86909C; font-size: 26rpx; padding: 32rpx 0; }
.log-item { background: #F7F8FA; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; }
.log-head { display: flex; justify-content: space-between; margin-bottom: 8rpx; }
.log-time { font-size: 24rpx; color: #86909C; }
.log-result { font-size: 24rpx; color: #F53F3F; }
.log-result.ok { color: #6acdbb; }
.log-preview { font-size: 26rpx; color: #4E5969; word-break: break-all; }
</style>

View File

@@ -0,0 +1,186 @@
<template>
<view class="picker-wrap">
<view class="picker-trigger" @click="openDrawer">
<text :class="selected ? 'summary-text' : 'placeholder-text'">{{ summaryText }}</text>
<text class="arrow"></text>
</view>
<drawer-page-container
v-model="showPopup"
title="选择快递公司"
:use-page-guard="false"
:show-close="true"
>
<view class="drawer-body">
<view class="search-bar">
<input
class="search-input"
v-model="keyword"
placeholder="搜索公司名称或编码"
placeholder-style="color:#BDBDBD"
confirm-type="search"
/>
</view>
<scroll-view scroll-y class="list-scroll">
<view v-if="!filteredOptions.length" class="empty">无匹配快递公司</view>
<view
v-for="item in filteredOptions"
:key="item._wxKey"
class="option-item"
@click="onPick(item)"
>
<view class="option-main">
<text class="option-name">{{ item.name || '-' }}</text>
<text class="option-code">{{ item.code || '' }}</text>
</view>
<text v-if="value === item.code" class="check"></text>
</view>
</scroll-view>
</view>
</drawer-page-container>
</view>
</template>
<script>
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue'
export default {
name: 'ExpressCompanyPicker',
components: { DrawerPageContainer },
props: {
value: { type: String, default: '' },
options: { type: Array, default: () => [] },
disabled: { type: Boolean, default: false },
placeholder: { type: String, default: '请选择快递公司' },
},
data() {
return {
showPopup: false,
keyword: '',
}
},
computed: {
selected() {
const code = this.value
if (!code) return null
return this.options.find(o => o.code === code) || null
},
summaryText() {
if (!this.selected) return this.placeholder
const name = this.selected.name || '-'
const code = this.selected.code || ''
return code ? `${name}${code}` : name
},
filteredOptions() {
const kw = (this.keyword || '').trim().toLowerCase()
if (!kw) return this.options
return this.options.filter(o => {
const name = String(o.name || '').toLowerCase()
const code = String(o.code || '').toLowerCase()
return name.includes(kw) || code.includes(kw)
})
},
},
methods: {
openDrawer() {
if (this.disabled) return
this.keyword = ''
this.showPopup = true
},
onPick(item) {
this.$emit('input', item.code || '')
this.$emit('change', item)
this.showPopup = false

View File

@@ -1,5 +1,13 @@
<template> <template>
<u-popup v-model="show" mode="bottom" border-radius="20" height="85%" :closeable="true" @close="onClose"> <drawer-page-container
v-model="show"
title="处方详情"
height="85%"
tall
:show-close="true"
:closeable="true"
@close="onClose"
>
<view class="popup-wrap"> <view class="popup-wrap">
<scroll-view v-if="view" scroll-y class="popup-scroll"> <scroll-view v-if="view" scroll-y class="popup-scroll">
<view class="container safe-area-inset-bottom"> <view class="container safe-area-inset-bottom">
@@ -164,16 +172,18 @@
<view v-else-if="loading" class="popup-loading">加载中...</view> <view v-else-if="loading" class="popup-loading">加载中...</view>
<view v-else class="popup-loading">暂无数据</view> <view v-else class="popup-loading">暂无数据</view>
</view> </view>
</u-popup> </drawer-page-container>
</template> </template>
<script> <script>
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue'
import { mapPrescriptionDetailView } from '@/subPackages/sub_business_shared/common/display.js' import { mapPrescriptionDetailView } from '@/subPackages/sub_business_shared/common/display.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js' import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
export default { export default {
mixins: [businessMixin], mixins: [businessMixin],
components: { DrawerPageContainer },
name: 'PrescriptionDetailPopup', name: 'PrescriptionDetailPopup',
data() { data() {
return { return {
@@ -217,6 +227,7 @@ export default {
}, },
onClose() { onClose() {
this.view = null this.view = null
this.show = false
}, },
}, },
} }

View File

@@ -0,0 +1,108 @@
<template>
<drawer-page-container
v-model="show"
title="订单退款"
height="55%"
:show-close="true"
:closeable="false"
@close="onClose"
>
<view class="popup-wrap">
<view class="order-no" v-if="orderNo">订单号{{ orderNo }}</view>
<view class="field">
<text class="label">退款原因</text>
<textarea
class="textarea"
v-model="refundReason"
placeholder="请输入退款原因"
placeholder-style="color:#BDBDBD"
/>
</view>
<view class="footer">
<button class="btn-cancel" @click="onClose">取消</button>
<button class="btn-submit" :loading="submitting" @click="submit">确认退款</button>
</view>
</view>
</drawer-page-container>
</template>
<script>
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue'
export default {
name: 'RefundOrderPopup',
components: { DrawerPageContainer },
props: {
bizApi: { type: Object, required: true },
},
data() {
return {
show: false,
orderId: 0,
orderNo: '',
refundReason: '',
submitting: false,
}
},
methods: {
open(order) {
this.orderId = order.id
this.orderNo = order.order_no || ''
this.refundReason = ''
this.show = true
},
onClose() {
this.show = false
},
submit() {
if (!this.refundReason.trim()) {
uni.showToast({ title: '请输入退款原因', icon: 'none' })
return
}
this.submitting = true
this.bizApi.refundOrder({
order_id: this.orderId,
refund_reason: this.refundReason.trim(),
}).then(w => {
if (w.ok) {
uni.showToast({ title: '已提交' })
this.show = false
this.$emit('success')
}
}).finally(() => { this.submitting = false })
},
},
}
</script>
<style lang="scss" scoped>
.popup-wrap {
padding: 0 32rpx 32rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
}
.order-no { font-size: 26rpx; color: #86909C; margin-bottom: 24rpx; }
.field { margin-bottom: 24rpx; }
.label { display: block; font-size: 28rpx; color: #4E5969; margin-bottom: 12rpx; }
.textarea {
width: 100%;
box-sizing: border-box;
background: #F7F8FA;
border-radius: 12rpx;
min-height: 160rpx;
padding: 24rpx;
line-height: 1.5;
font-size: 28rpx;
}
.footer { display: flex; gap: 24rpx; margin-top: 32rpx; }
.footer button {
flex: 1;
margin: 0;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 30rpx;
}
.footer button::after { display: none; }
.btn-cancel { background: #fff; color: #4E5969; border: 1rpx solid #E5E6EB; }
.btn-submit { background: #F53F3F; color: #fff; }
</style>

View File

@@ -0,0 +1,166 @@
<template>
<drawer-page-container
v-model="show"
title="订单发货"
height="70%"
tall
:show-close="true"
:closeable="false"
@close="onClose"
>
<view class="popup-wrap">
<view class="order-no" v-if="orderNo">订单号{{ orderNo }}</view>
<view class="form">
<view v-if="deliveryMethod === 0" class="field">
<text class="label">快递单号</text>
<input
class="input"
v-model="expressNo"
placeholder="请输入快递单号"
placeholder-style="color:#BDBDBD"
/>
</view>
<view v-if="deliveryMethod === 0" class="field">
<text class="label">快递公司</text>
<express-company-picker
v-model="expressCompanyCode"
:options="companyOptions"
/>
</view>
<view class="field">
<text class="label">发货备注</text>
<textarea
class="textarea"
v-model="introduce"
placeholder="选填"
placeholder-style="color:#BDBDBD"
/>
</view>
</view>
<view class="footer">
<button class="btn-cancel" @click="onClose">取消</button>
<button class="btn-submit" :loading="submitting" @click="submit">确认发货</button>
</view>
</view>
</drawer-page-container>
</template>
<script>
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue'
import ExpressCompanyPicker from './ExpressCompanyPicker.vue'
import { withWxKey } from '@/utils/wxListKey.js'
export default {
name: 'ShipOrderPopup',
components: { DrawerPageContainer, ExpressCompanyPicker },
props: {
bizApi: { type: Object, required: true },
},
data() {
return {
show: false,
orderId: 0,
orderNo: '',
deliveryMethod: 0,
expressNo: '',
expressCompanyCode: '',
introduce: '',
companyOptions: [],
submitting: false,
}
},
methods: {
open(order) {
this.orderId = order.id
this.orderNo = order.order_no || ''
this.deliveryMethod = Number(order.delivery_method)
this.expressNo = ''
this.expressCompanyCode = ''
this.introduce = ''
this.companyOptions = []
this.show = true
if (this.deliveryMethod === 0) {
this.loadCompanies()
}
},
loadCompanies() {
this.bizApi.getExpressCompaniesOption().then(w => {
const items = (w.data && (w.data.items || w.data)) || []
this.companyOptions = withWxKey(Array.isArray(items) ? items : [], 'id', 'ec')
})
},
onClose() {
this.show = false
},
submit() {
if (this.deliveryMethod === 0) {
if (!this.expressNo.trim()) {
uni.showToast({ title: '请输入快递单号', icon: 'none' })
return
}
if (!this.expressCompanyCode) {
uni.showToast({ title: '请选择快递公司', icon: 'none' })
return
}
}
this.submitting = true
const payload = {
order_id: this.orderId,
express_no: this.expressNo.trim(),
express_company_code: this.expressCompanyCode,
introduce: this.introduce.trim(),
}
this.bizApi.wareSendOrder(payload).then(w => {
if (w.ok) {
uni.showToast({ title: '发货成功' })
this.show = false
this.$emit('success')
}
}).finally(() => { this.submitting = false })
},
},
}
</script>
<style lang="scss" scoped>
.popup-wrap {
padding: 0 32rpx 32rpx;
padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
}
.order-no { font-size: 26rpx; color: #86909C; margin-bottom: 24rpx; }
.field { margin-bottom: 24rpx; }
.label { display: block; font-size: 28rpx; color: #4E5969; margin-bottom: 12rpx; }
.input {
width: 100%;
box-sizing: border-box;
background: #F7F8FA;
border-radius: 12rpx;
min-height: 88rpx;
height: 88rpx;
line-height: 88rpx;
padding: 0 24rpx;
font-size: 28rpx;
}
.textarea {
width: 100%;
box-sizing: border-box;
background: #F7F8FA;
border-radius: 12rpx;
min-height: 160rpx;
padding: 24rpx;
line-height: 1.5;
font-size: 28rpx;
}
.footer { display: flex; gap: 24rpx; margin-top: 32rpx; }
.footer button {
flex: 1;
margin: 0;
height: 88rpx;
line-height: 88rpx;
border-radius: 44rpx;
font-size: 30rpx;
}
.footer button::after { display: none; }
.btn-cancel { background: #fff; color: #4E5969; border: 1rpx solid #E5E6EB; }
.btn-submit { background: #6acdbb; color: #fff; }
</style>

View File

@@ -21,7 +21,8 @@
<view class="row"><text>医生</text><text>{{ doctorName }}</text></view> <view class="row"><text>医生</text><text>{{ doctorName }}</text></view>
<view class="row"><text>处方来源</text><text>{{ storeName }}</text></view> <view class="row"><text>处方来源</text><text>{{ storeName }}</text></view>
<view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view> <view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view>
<view class="row"><text>ERP状态</text><text>{{ erpSyncText(info.is_sync_erp) }}</text></view> <view class="row" v-if="canViewErp"><text>ERP状态</text><text>{{ erpSyncText(info.is_sync_erp) }}</text></view>
<view class="row" v-if="detailExtra.prescriptionStatusText"><text>处方状态</text><text>{{ detailExtra.prescriptionStatusText }}</text></view>
<view class="row"><text>支付时间</text><text>{{ info.pay_time || '-' }}</text></view> <view class="row"><text>支付时间</text><text>{{ info.pay_time || '-' }}</text></view>
<view class="row"><text>下单时间</text><text>{{ info.created_at || '-' }}</text></view> <view class="row"><text>下单时间</text><text>{{ info.created_at || '-' }}</text></view>
<view class="row" v-if="detailExtra.cancelRemark"><text>发货备注</text><text>{{ detailExtra.cancelRemark }}</text></view> <view class="row" v-if="detailExtra.cancelRemark"><text>发货备注</text><text>{{ detailExtra.cancelRemark }}</text></view>
@@ -29,11 +30,20 @@
<view class="sub-block"> <view class="sub-block">
<view class="sub-block-title">费用明细</view> <view class="sub-block-title">费用明细</view>
<view class="row"><text>药品总价</text><text>{{ money(info.items_price) }}</text></view> <view class="row">
<text>药品总价</text>
<text
v-if="canOrderPercentAdjust"
class="price-link"
@click="openOrderPriceAdjust"
>{{ money(info.items_price) }}</text>
<text v-else>{{ money(info.items_price) }}</text>
</view>
<view class="row"><text>供货价</text><text>{{ detailExtra.marketPrice }}</text></view> <view class="row"><text>供货价</text><text>{{ detailExtra.marketPrice }}</text></view>
<view class="row"><text>运费</text><text>{{ money(info.trans_expenses) }}</text></view> <view class="row"><text>运费</text><text>{{ money(info.trans_expenses) }}</text></view>
<view class="row"><text>挂号费</text><text>{{ money(info.register_price) }}</text></view> <view class="row"><text>挂号费</text><text>{{ money(info.register_price) }}</text></view>
<view class="row"><text>是否包邮</text><text>{{ detailExtra.freeShippingText }}</text></view> <view class="row"><text>是否包邮</text><text>{{ detailExtra.freeShippingText }}</text></view>
<view class="row" v-if="orderPriceDiscountLabel"><text>价格浮动</text><text>{{ orderPriceDiscountLabel }}</text></view>
</view> </view>
</view> </view>
@@ -124,16 +134,33 @@
<!-- 底部悬浮操作栏 --> <!-- 底部悬浮操作栏 -->
<view class="actions-bar" v-if="info.id"> <view class="actions-bar" v-if="info.id">
<button class="btn-outline" @click="goTrace">订单追溯</button> <button
<button v-if="canShip" class="btn-primary" @click="ship">发货</button> v-if="detailExtra.canViewPrescription"
<button v-if="canRefund" class="btn-warn" @click="refund">退款</button> class="btn-outline btn-left"
<button v-if="detailExtra.canViewPrescription" class="btn-primary btn-full" @click="openPrescription">查看处方</button> @click="openPrescription"
>查看处方</button>
<button
class="btn-primary btn-more"
:class="{ 'btn-full': !detailExtra.canViewPrescription }"
@click="openMoreMenu"
>更多</button>
</view> </view>
</view> </view>
<!-- 处方弹窗 --> <!-- 处方弹窗 -->
<prescription-detail-popup ref="prescriptionPopup" /> <prescription-detail-popup ref="prescriptionPopup" />
<ship-order-popup ref="shipPopup" :biz-api="bizApi" @success="load" />
<refund-order-popup ref="refundPopup" :biz-api="bizApi" @success="load" />
<erp-sync-popup ref="erpPopup" :biz-api="bizApi" @success="load" />
<order-price-percent-adjust-popup
:visible="priceAdjustVisible"
:current-discount="info.price_discount || 100"
:quick-options="priceAdjustQuickOptions"
:submitting="priceAdjustSubmitting"
@close="closePriceAdjust"
@confirm="onSharedPriceAdjustConfirm"
/>
</business-page-layout> </business-page-layout>
</template> </template>
@@ -141,6 +168,9 @@
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue' import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import ExpressTimeline from './components/ExpressTimeline.vue' import ExpressTimeline from './components/ExpressTimeline.vue'
import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue' import PrescriptionDetailPopup from './components/PrescriptionDetailPopup.vue'
import ShipOrderPopup from './components/ShipOrderPopup.vue'
import RefundOrderPopup from './components/RefundOrderPopup.vue'
import ErpSyncPopup from './components/ErpSyncPopup.vue'
import { import {
orderStatusText, orderStatusText,
deliveryMethodText, deliveryMethodText,
@@ -153,10 +183,20 @@ import {
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js' 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'
import { orderPriceAdjustMixin } from '@/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js'
import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue'
export default { export default {
mixins: [businessMixin], mixins: [businessMixin, orderPriceAdjustMixin],
components: { BusinessPageLayout, ExpressTimeline, PrescriptionDetailPopup }, components: {
BusinessPageLayout,
ExpressTimeline,
PrescriptionDetailPopup,
ShipOrderPopup,
RefundOrderPopup,
ErpSyncPopup,
OrderPricePercentAdjustPopup,
},
data() { data() {
return { return {
id: 0, id: 0,
@@ -213,13 +253,19 @@ export default {
return (s && s.position) ? s.position : '-' return (s && s.position) ? s.position : '-'
}, },
canRefund() { canRefund() {
return false if (!this.bizCap.canRefund) return false
// if (!this.bizCap.canRefund) return false return Number(this.info.is_pay) === 1
// return Number(this.info.is_pay) === 1
}, },
canShip() { canShip() {
return false return this.bizCap.canShip
// return this.bizCap.canShip && Number(this.info.status) === 1 && Number(this.info.is_pay) === 1 && Number(this.info.status) === 1
&& Number(this.info.is_pay) === 1
},
canViewErp() {
return !!this.bizCap.canShip
},
canViewErpMenu() {
return this.canViewErp && Number(this.info.prescription_type) === 1
}, },
}, },
onLoad(q) { onLoad(q) {
@@ -232,6 +278,7 @@ export default {
if (!w.ok) return if (!w.ok) return
this.info = w.data || {} this.info = w.data || {}
this.detailExtra = mapOrderDetailDisplay(this.info) this.detailExtra = mapOrderDetailDisplay(this.info)
this.loadPriceAdjustMeta(() => this.bizApi.getPriceAdjustConfig(this.info.store_id))
if (this.info.delivery_method === 0) { if (this.info.delivery_method === 0) {
this.loadExpress() this.loadExpress()
} }
@@ -242,6 +289,11 @@ export default {
if (w.ok) this.expressInfo = w.data if (w.ok) this.expressInfo = w.data
}) })
}, },
onSharedPriceAdjustConfirm(payload) {
return this.onPriceAdjustConfirm(payload, {
adjustOrderPercent: (data) => this.bizApi.adjustOrderPercent(data),
})
},
money(v) { money(v) {
return formatMoney(v) return formatMoney(v)
}, },
@@ -260,33 +312,30 @@ export default {
erpSyncText(v) { erpSyncText(v) {
return erpSyncText(v) return erpSyncText(v)
}, },
refund() { openMoreMenu() {
uni.showModal({ const items = ['订单溯源']
title: '确认退款', const handlers = [() => this.goTrace()]
confirmColor: '#F53F3F', if (this.canOrderPercentAdjust && Number(this.info.price_discount) !== 100) {
success: (r) => { items.push('清除浮动')
if (!r.confirm) return handlers.push(() => this.clearOrderPriceDiscount({ adjustOrderPercent: (data) => this.bizApi.adjustOrderPercent(data) }))
this.bizApi.refundOrder({ id: this.id, refund_reason: '管理员退款' }).then(w => { }
if (w.ok) { if (this.canShip) {
uni.showToast({ title: '已提交' }) items.push('发货')
this.load() handlers.push(() => this.$refs.shipPopup.open(this.info))
} }
}) if (this.canRefund) {
}, items.push('退款')
}) handlers.push(() => this.$refs.refundPopup.open(this.info))
}, }
ship() { if (this.canViewErpMenu) {
uni.showModal({ items.push('ERP同步记录')
title: '确认发货', handlers.push(() => this.$refs.erpPopup.open(this.info))
content: '确定将该订单标记为已发货?', }
success: (r) => { uni.showActionSheet({
if (!r.confirm) return itemList: items,
this.bizApi.wareSendOrder({ id: this.id, cancel_remark: '' }).then(w => { success: (res) => {
if (w.ok) { const fn = handlers[res.tapIndex]
uni.showToast({ title: '发货成功' }) if (fn) fn()
this.load()
}
})
}, },
}) })
}, },
@@ -534,6 +583,7 @@ $color-danger: #F53F3F;
} }
.price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; } .price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; }
.price-link { color: $theme-primary; text-decoration: underline; }
/* ================= 底部动作栏 ================= */ /* ================= 底部动作栏 ================= */
.actions-bar { .actions-bar {
@@ -562,6 +612,15 @@ $color-danger: #F53F3F;
.actions-bar button::after { display: none; } .actions-bar button::after { display: none; }
.btn-left {
flex: 1;
}
.btn-more {
width: 220rpx;
flex-shrink: 0;
}
/* 主按钮使用 flex: 1 铺满 */ /* 主按钮使用 flex: 1 铺满 */
.btn-full { .btn-full {
flex: 1; flex: 1;

View File

@@ -3,6 +3,18 @@
<view class="order-list-container"> <view class="order-list-container">
<!-- 顶部筛选 --> <!-- 顶部筛选 -->
<page-sticky-header> <page-sticky-header>
<view class="quick-filter">
<view class="filter-row date-row">
<picker mode="date" :value="dateStart" @change="onStart">
<view class="filter-picker date-picker">{{ dateStart || '开始日期' }}</view>
</picker>
<text class="sep"></text>
<picker mode="date" :value="dateEnd" @change="onEnd">
<view class="filter-picker date-picker">{{ dateEnd || '结束日期' }}</view>
</picker>
</view>
<view class="filter-query-btn" @click="reload">查询</view>
</view>
<collapsible-filter-panel title="订单筛选" @clear="resetFilter"> <collapsible-filter-panel title="订单筛选" @clear="resetFilter">
<view class="filter-row"> <view class="filter-row">
<text class="filter-label">订单号</text> <text class="filter-label">订单号</text>
@@ -26,16 +38,6 @@
<view class="filter-picker">{{ prescriptionTypeLabels[prescriptionTypeIndex] }}</view> <view class="filter-picker">{{ prescriptionTypeLabels[prescriptionTypeIndex] }}</view>
</picker> </picker>
</view> </view>
<view class="filter-row date-row">
<picker mode="date" :value="dateStart" @change="onStart">
<view class="filter-picker date-picker">{{ dateStart || '开始日期' }}</view>
</picker>
<text class="sep"></text>
<picker mode="date" :value="dateEnd" @change="onEnd">
<view class="filter-picker date-picker">{{ dateEnd || '结束日期' }}</view>
</picker>
</view>
<view class="filter-query-btn" @click="reload">查询</view>
</collapsible-filter-panel> </collapsible-filter-panel>
</page-sticky-header> </page-sticky-header>
@@ -152,6 +154,7 @@ import {
clearOrderFilter, clearOrderFilter,
defaultOrderFilter, defaultOrderFilter,
} from '@/subPackages/sub_business_shared/common/filterCache.js' } from '@/subPackages/sub_business_shared/common/filterCache.js'
import { defaultTodayDate } from '@/subPackages/sub_business_shared/common/reconciliationParams.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js' import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
const DELIVERY_OPTIONS = [ const DELIVERY_OPTIONS = [
@@ -207,7 +210,7 @@ export default {
}))) })))
} }
}) })
this.reload() this.checkDateEndAndReload()
}, },
onReachBottom() { onReachBottom() {
this.load(this.page + 1, true) this.load(this.page + 1, true)
@@ -236,6 +239,24 @@ export default {
clearOrderFilter() clearOrderFilter()
this.reload() this.reload()
}, },
checkDateEndAndReload() {
const today = defaultTodayDate()
if (this.dateEnd && this.dateEnd !== today) {
uni.showModal({
title: '提示',
content: '筛选结束日期不是今天,是否更新到今天?',
success: (r) => {
if (r.confirm) {
this.dateEnd = today
this.persistOrderFilter()
}
this.reload()
},
})
return
}
this.reload()
},
filterValues() { filterValues() {
const st = this.statusOptions[this.statusIndex] const st = this.statusOptions[this.statusIndex]
const del = DELIVERY_OPTIONS[this.deliveryIndex] const del = DELIVERY_OPTIONS[this.deliveryIndex]
@@ -321,6 +342,13 @@ $color-text-muted: #86909C;
} }
/* 筛选区 */ /* 筛选区 */
.quick-filter {
background: #fff;
padding: 24rpx;
margin-bottom: 16rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.filter-row { .filter-row {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -10,6 +10,7 @@
<view class="row" v-if="summary.register_order_no"><text class="label">挂号订单</text><text class="value">{{ summary.register_order_no }}</text></view> <view class="row" v-if="summary.register_order_no"><text class="label">挂号订单</text><text class="value">{{ summary.register_order_no }}</text></view>
<view class="row" v-if="summary.prescription_no"><text class="label">处方号</text><text class="value">{{ summary.prescription_no }}</text></view> <view class="row" v-if="summary.prescription_no"><text class="label">处方号</text><text class="value">{{ summary.prescription_no }}</text></view>
<view class="row"><text class="label">结算状态</text><text class="value">{{ summary.is_settled === 1 ? '已结算' : '未结算' }}</text></view> <view class="row"><text class="label">结算状态</text><text class="value">{{ summary.is_settled === 1 ? '已结算' : '未结算' }}</text></view>
<view class="row" v-if="prescriptionStatusLabel"><text class="label">审方状态</text><text class="value status-audit">{{ prescriptionStatusLabel }}</text></view>
</view> </view>
<!-- Tab --> <!-- Tab -->
@@ -26,7 +27,7 @@
<!-- 时间线 --> <!-- 时间线 -->
<view v-show="activeTab === 'timeline'" class="tab-panel"> <view v-show="activeTab === 'timeline'" class="tab-panel">
<view v-if="!timeline.length" class="empty">暂无时间线数据</view> <view v-if="!timeline.length" class="empty">暂无时间线数据</view>
<view v-for="(node, idx) in timeline" :key="node.key || idx" class="timeline-item"> <view v-for="node in timeline" :key="node._wxKey" class="timeline-item">
<view class="tl-dot" /> <view class="tl-dot" />
<view class="tl-body"> <view class="tl-body">
<view class="tl-title">{{ node.title }}</view> <view class="tl-title">{{ node.title }}</view>
@@ -43,6 +44,15 @@
<view v-show="activeTab === 'ledger'" class="tab-panel"> <view v-show="activeTab === 'ledger'" class="tab-panel">
<view v-if="ledgerLoading" class="loading-tip">加载分账...</view> <view v-if="ledgerLoading" class="loading-tip">加载分账...</view>
<template v-else> <template v-else>
<view v-if="isPlatformAdmin" class="scope-tabs">
<view
v-for="tab in ledgerScopeTabs"
:key="tab.key"
class="scope-tab"
:class="{ active: ledgerScope === tab.key }"
@click="switchLedgerScope(tab.key)"
>{{ tab.label }}</view>
</view>
<view v-if="hasRegisterLedger" class="sub-tabs"> <view v-if="hasRegisterLedger" class="sub-tabs">
<view <view
class="sub-tab" class="sub-tab"
@@ -55,6 +65,9 @@
@click="switchLedgerSub('register')" @click="switchLedgerSub('register')"
>挂号订单</view> >挂号订单</view>
</view> </view>
<view v-if="currentLedgerSumMoney != null" class="ledger-sum">
合计分账{{ formatMoney(currentLedgerSumMoney) }}
</view>
<view v-if="currentLedgerItems.length"> <view v-if="currentLedgerItems.length">
<view v-for="row in currentLedgerItems" :key="row._wxKey" class="detail-card"> <view v-for="row in currentLedgerItems" :key="row._wxKey" class="detail-card">
<view class="row"><text class="label">费用类型</text><text class="value">{{ row.fee_type_txt || '-' }}</text></view> <view class="row"><text class="label">费用类型</text><text class="value">{{ row.fee_type_txt || '-' }}</text></view>
@@ -170,6 +183,7 @@ export default {
traceData: null, traceData: null,
activeTab: 'timeline', activeTab: 'timeline',
ledgerSubTab: 'product', ledgerSubTab: 'product',
ledgerScope: 'store',
ledgerProduct: null, ledgerProduct: null,
ledgerRegister: null, ledgerRegister: null,
ledgerLoading: false, ledgerLoading: false,
@@ -181,14 +195,24 @@ export default {
{ key: 'ledger', label: '分账' }, { key: 'ledger', label: '分账' },
{ key: 'reconciliation', label: '对账' }, { key: 'reconciliation', label: '对账' },
], ],
ledgerScopeTabs: [
{ key: 'all', label: '全部' },
{ key: 'store', label: '门店' },
{ key: 'platform', label: '平台' },
],
} }
}, },
computed: { computed: {
isPlatformAdmin() {
if (this.bizCtx && this.bizCtx.mode === 'platform_admin') return true
return !!(this.traceData && this.traceData.meta && this.traceData.meta.is_platform_admin)
},
summary() { summary() {
return this.traceData?.summary || {} return this.traceData?.summary || {}
}, },
timeline() { timeline() {
return this.traceData?.timeline || [] const items = this.traceData?.timeline || []
return withWxKey(items, 'key', 'tl')
}, },
links() { links() {
return this.traceData?.links || {} return this.traceData?.links || {}
@@ -196,11 +220,26 @@ export default {
hasRegisterLedger() { hasRegisterLedger() {
return !!(this.links.register_id && this.ledgerRegister !== null) return !!(this.links.register_id && this.ledgerRegister !== null)
}, },
currentLedgerData() {
return this.ledgerSubTab === 'register' ? this.ledgerRegister : this.ledgerProduct
},
currentLedgerSumMoney() {
const data = this.currentLedgerData
if (!data || data.sum_money == null || data.sum_money === '') return null
return data.sum_money
},
currentLedgerItems() { currentLedgerItems() {
const data = this.ledgerSubTab === 'register' ? this.ledgerRegister : this.ledgerProduct const items = this.currentLedgerData?.items || []
const items = data?.items || []
return withWxKey(items, 'id', 'traceLed') return withWxKey(items, 'id', 'traceLed')
}, },
prescriptionStatusLabel() {
if (!this.summary.prescription_id) return ''
const auditNode = (this.timeline || []).find(
n => n.key === 'prescription_audited' || n.key === 'prescription_rejected'
)
if (auditNode && auditNode.status) return auditNode.status
return '待审核'
},
reconciliationItems() { reconciliationItems() {
const items = this.reconciliationData?.items || [] const items = this.reconciliationData?.items || []
return withWxKey(items, 'id', 'traceRec') return withWxKey(items, 'id', 'traceRec')
@@ -228,6 +267,18 @@ export default {
this.bizApi.getOrderTrace({ scene: this.scene, order_id: this.orderId }).then(w => { this.bizApi.getOrderTrace({ scene: this.scene, order_id: this.orderId }).then(w => {
if (w.ok) { if (w.ok) {
this.traceData = w.data this.traceData = w.data
this.loadedTabs = {}
const meta = w.data && w.data.meta
let scope = 'store'
if (this.bizCtx && this.bizCtx.mode === 'platform_admin') {
scope = 'all'
} else if (meta && meta.is_platform_admin) {
scope = 'all'
}
if (meta && meta.is_platform_admin === false) {
scope = 'store'
}
this.ledgerScope = scope
} else { } else {
uni.showToast({ title: (w.res && w.res.message) || '加载失败', icon: 'none' }) uni.showToast({ title: (w.res && w.res.message) || '加载失败', icon: 'none' })
} }
@@ -235,7 +286,7 @@ export default {
}, },
switchMainTab(key) { switchMainTab(key) {
this.activeTab = key this.activeTab = key
if (key === 'ledger' && !this.loadedTabs.ledger) { if (key === 'ledger' && !this.loadedTabs[`ledger:${this.ledgerScope}`]) {
this.loadLedger() this.loadLedger()
} else if (key === 'reconciliation' && !this.loadedTabs.reconciliation) { } else if (key === 'reconciliation' && !this.loadedTabs.reconciliation) {
this.loadReconciliation() this.loadReconciliation()
@@ -244,18 +295,27 @@ export default {
switchLedgerSub(key) { switchLedgerSub(key) {
this.ledgerSubTab = key this.ledgerSubTab = key
}, },
loadLedger() { switchLedgerScope(scope) {
if (this.ledgerScope === scope) return
this.ledgerScope = scope
this.loadedTabs[`ledger:${scope}`] = false
this.loadLedger(true)
},
loadLedger(force = false) {
const cacheKey = `ledger:${this.ledgerScope}`
if (!force && this.loadedTabs[cacheKey]) return
const productId = this.links.product_order_id const productId = this.links.product_order_id
const registerId = this.links.register_id const registerId = this.links.register_id
if (!productId && !registerId) { if (!productId && !registerId) {
this.loadedTabs.ledger = true this.loadedTabs[cacheKey] = true
return return
} }
this.ledgerLoading = true this.ledgerLoading = true
const scope = this.ledgerScope
const reqs = [] const reqs = []
if (productId) { if (productId) {
reqs.push( reqs.push(
this.bizApi.getOrderLedgerDetail({ order_id: productId, order_type: 1, scope: 'store' }).then(w => { this.bizApi.getOrderLedgerDetail({ order_id: productId, order_type: 1, scope }).then(w => {
this.ledgerProduct = w.ok ? w.data : null this.ledgerProduct = w.ok ? w.data : null
}) })
) )
@@ -264,7 +324,7 @@ export default {
} }
if (registerId) { if (registerId) {
reqs.push( reqs.push(
this.bizApi.getOrderLedgerDetail({ order_id: registerId, order_type: 2, scope: 'store' }).then(w => { this.bizApi.getOrderLedgerDetail({ order_id: registerId, order_type: 2, scope }).then(w => {
this.ledgerRegister = w.ok ? w.data : null this.ledgerRegister = w.ok ? w.data : null
}) })
) )
@@ -273,7 +333,7 @@ export default {
} }
Promise.all(reqs).finally(() => { Promise.all(reqs).finally(() => {
this.ledgerLoading = false this.ledgerLoading = false
this.loadedTabs.ledger = true this.loadedTabs[cacheKey] = true
}) })
}, },
loadReconciliation() { loadReconciliation() {
@@ -343,6 +403,32 @@ export default {
font-weight: 600; font-weight: 600;
background: #e6f4ff; background: #e6f4ff;
} }
.scope-tabs {
display: flex;
background: #fff;
border-radius: 12rpx;
margin-bottom: 16rpx;
overflow: hidden;
}
.scope-tab {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 26rpx;
color: #666;
}
.scope-tab.active {
color: #1677ff;
font-weight: 600;
background: #e6f4ff;
}
.ledger-sum {
font-size: 28rpx;
color: #333;
font-weight: 600;
padding: 8rpx 8rpx 16rpx;
}
.status-audit { color: #6acdbb; }
.tab-panel { min-height: 200rpx; } .tab-panel { min-height: 200rpx; }
.timeline-item { .timeline-item {
display: flex; display: flex;

View File

@@ -1,15 +1,27 @@
<template> <template>
<u-popup v-model="show" mode="center" border-radius="16" width="85%" :closeable="true" @close="onClose"> <u-popup v-model="show" mode="center" border-radius="16" width="85%" :closeable="true" @close="onClose">
<view class="modal"> <view class="modal">
<view class="modal-title">修改售价</view> <view class="modal-title">{{ isPlatform ? '修改价格' : '修改售价' }}</view>
<view v-if="drugName" class="modal-drug">{{ drugName }}</view> <view v-if="drugName" class="modal-drug">{{ drugName }}</view>
<view v-if="suggestPrice" class="modal-hint">建议售价 {{ suggestPrice }}</view> <view v-if="suggestPrice && !isPlatform" class="modal-hint">建议售价 {{ suggestPrice }}</view>
<input <view v-if="isPlatform" class="field-block">
class="modal-input" <text class="field-label">供货价</text>
type="digit" <input
v-model="priceInput" class="modal-input"
placeholder="请输入新售价" type="digit"
/> v-model="marketPriceInput"
placeholder="请输入供货价"
/>
</view>
<view class="field-block">
<text class="field-label">{{ isPlatform ? '建议售价' : '售价' }}</text>
<input
class="modal-input"
type="digit"
v-model="priceInput"
placeholder="请输入新售价"
/>
</view>
<view class="modal-actions"> <view class="modal-actions">
<button class="btn cancel" @click="onClose">取消</button> <button class="btn cancel" @click="onClose">取消</button>
<button class="btn confirm" :loading="submitting" @click="onConfirm">确定</button> <button class="btn confirm" :loading="submitting" @click="onConfirm">确定</button>
@@ -32,10 +44,14 @@ export default {
return { return {
show: false, show: false,
priceInput: '', priceInput: '',
marketPriceInput: '',
submitting: false, submitting: false,
} }
}, },
computed: { computed: {
isPlatform() {
return this.bizCap.warehouseType === 'platform'
},
drugName() { drugName() {
const drug = (this.item && this.item.drug) || {} const drug = (this.item && this.item.drug) || {}
return drug.drug_name || '' return drug.drug_name || ''
@@ -57,6 +73,8 @@ export default {
if (v && this.item) { if (v && this.item) {
const raw = this.item.price const raw = this.item.price
this.priceInput = raw != null && raw !== '' ? String(raw) : '' this.priceInput = raw != null && raw !== '' ? String(raw) : ''
const supply = this.item.market_price ?? this.item.buy_price
this.marketPriceInput = supply != null && supply !== '' ? String(supply) : ''
} }
}, },
}, },
@@ -76,6 +94,18 @@ export default {
return return
} }
const rounded = Math.round(price * 100) / 100 const rounded = Math.round(price * 100) / 100
if (this.isPlatform) {
const marketPrice = parseFloat(String(this.marketPriceInput).trim())
if (!marketPrice || marketPrice <= 0 || Number.isNaN(marketPrice)) {
uni.showToast({ title: '请输入有效供货价', icon: 'none' })
return
}
this.$emit('confirm', {
price: rounded,
market_price: Math.round(marketPrice * 100) / 100,
})
return
}
this.$emit('confirm', rounded) this.$emit('confirm', rounded)
}, },
}, },
@@ -104,8 +134,16 @@ export default {
margin-top: 8rpx; margin-top: 8rpx;
text-align: center; text-align: center;
} }
.field-block {
margin-top: 24rpx;
}
.field-label {
display: block;
font-size: 24rpx;
color: #666;
margin-bottom: 8rpx;
}
.modal-input { .modal-input {
margin-top: 32rpx;
padding: 20rpx 24rpx; padding: 20rpx 24rpx;
background: #f5f5f5; background: #f5f5f5;
border-radius: 12rpx; border-radius: 12rpx;

View File

@@ -44,7 +44,7 @@
<!-- 底部操作按钮组 --> <!-- 底部操作按钮组 -->
<view v-if="info.id" class="action-group"> <view v-if="info.id" class="action-group">
<view class="btn-large btn-outline" hover-class="btn-hover-opacity" @click="openPriceEdit"> <view class="btn-large btn-outline" hover-class="btn-hover-opacity" @click="openPriceEdit">
修改售价 {{ isPlatformWarehouse ? '修改价格' : '修改售价' }}
</view> </view>
<view <view
class="btn-large" class="btn-large"
@@ -86,7 +86,10 @@ export default {
}, },
computed: { computed: {
display() { display() {
return mapWarehouseRow(this.info) return mapWarehouseRow(this.info, this.bizCap.warehouseType || 'store')
},
isPlatformWarehouse() {
return this.bizCap.warehouseType === 'platform'
}, },
}, },
onLoad(q) { onLoad(q) {
@@ -105,10 +108,13 @@ export default {
closePriceEdit() { closePriceEdit() {
this.priceModalVisible = false this.priceModalVisible = false
}, },
onPriceConfirm(price) { onPriceConfirm(payload) {
if (this.priceSubmitting) return if (this.priceSubmitting) return
this.priceSubmitting = true this.priceSubmitting = true
this.bizApi.updateWarehousePrice({ id: this.id, price }).then(w => { const data = typeof payload === 'object' && payload !== null
? { id: this.id, ...payload }
: { id: this.id, price: payload }
this.bizApi.updateWarehousePrice(data).then(w => {
if (w.ok) { if (w.ok) {
uni.showToast({ title: '修改成功' }) uni.showToast({ title: '修改成功' })
this.closePriceEdit() this.closePriceEdit()

View File

@@ -101,7 +101,7 @@ export default {
}, },
methods: { methods: {
rowDisplay(item) { rowDisplay(item) {
return mapWarehouseRow(item) return mapWarehouseRow(item, this.bizCap.warehouseType || 'store')
}, },
reload() { reload() {
this.page = 1 this.page = 1
@@ -131,10 +131,13 @@ export default {
this.priceModalVisible = false this.priceModalVisible = false
this.priceEditItem = null this.priceEditItem = null
}, },
onPriceConfirm(price) { onPriceConfirm(payload) {
if (!this.priceEditItem || this.priceSubmitting) return if (!this.priceEditItem || this.priceSubmitting) return
this.priceSubmitting = true this.priceSubmitting = true
this.bizApi.updateWarehousePrice({ id: this.priceEditItem.id, price }).then(w => { const data = typeof payload === 'object' && payload !== null
? { id: this.priceEditItem.id, ...payload }
: { id: this.priceEditItem.id, price: payload }
this.bizApi.updateWarehousePrice(data).then(w => {
if (w.ok) { if (w.ok) {
uni.showToast({ title: '修改成功' }) uni.showToast({ title: '修改成功' })
this.closePriceEdit() this.closePriceEdit()

View File

@@ -135,6 +135,11 @@ export function erpSyncText(v) {
return '未同步' return '未同步'
} }
export function prescriptionStatusText(status) {
const m = { 0: '待审核', 1: '已审核', 2: '已驳回', 3: '已过期' }
return m[status] != null ? m[status] : '-'
}
export function freeShippingText(v) { export function freeShippingText(v) {
if (v === 1) return '包邮' if (v === 1) return '包邮'
return '不包邮' return '不包邮'
@@ -238,6 +243,7 @@ export function mapOrderProductItem(p, prescriptionType) {
: '×' + (p.number != null ? p.number : 0) : '×' + (p.number != null ? p.number : 0)
const isTcm = prescriptionType === 1 const isTcm = prescriptionType === 1
return { return {
id: p.id,
drugName: p.drug_name || drug.drug_name || '药品', drugName: p.drug_name || drug.drug_name || '药品',
drugNumber: drug.drug_number || '', drugNumber: drug.drug_number || '',
imageUrl: resolveDrugImage(p.drug_image || drug.image), imageUrl: resolveDrugImage(p.drug_image || drug.image),
@@ -276,6 +282,9 @@ export function mapOrderDetailDisplay(info) {
if (!info || !info.id) return {} if (!info || !info.id) return {}
const freeShip = info.is_free_shipping === 1 || info.free_ship === 1 const freeShip = info.is_free_shipping === 1 || info.free_ship === 1
const canView = !!(info.p_id && info.order_type !== 2 && info.order_type !== 3) const canView = !!(info.p_id && info.order_type !== 2 && info.order_type !== 3)
const presStatus = info.prescription && info.prescription.status != null
? info.prescription.status
: null
return { return {
orderTypeText: orderTypeText(info.order_type), orderTypeText: orderTypeText(info.order_type),
marketPrice: formatMoney(info.market_price), marketPrice: formatMoney(info.market_price),
@@ -284,6 +293,7 @@ export function mapOrderDetailDisplay(info) {
showPrescription: mapOrderPrescription(info), showPrescription: mapOrderPrescription(info),
prescriptionId: info.p_id, prescriptionId: info.p_id,
canViewPrescription: canView, canViewPrescription: canView,
prescriptionStatusText: canView && presStatus != null ? prescriptionStatusText(presStatus) : '',
statusText: orderStatusText(info.status), statusText: orderStatusText(info.status),
prescriptionTypeText: prescriptionTypeText(info.prescription_type), prescriptionTypeText: prescriptionTypeText(info.prescription_type),
deliveryText: deliveryMethodText(info.delivery_method), deliveryText: deliveryMethodText(info.delivery_method),
@@ -398,16 +408,20 @@ export function warehouseStatusText(status) {
return '-' return '-'
} }
export function mapWarehouseRow(item) { export function mapWarehouseRow(item, warehouseType = 'store') {
if (!item) return {} if (!item) return {}
const drug = item.drug || {} const drug = item.drug || {}
const storeDrug = drug.drug_store_drug || {} const storeDrug = drug.drug_store_drug || {}
const suggestPrice = storeDrug.price != null ? storeDrug.price : drug.price const suggestPrice = storeDrug.price != null ? storeDrug.price : drug.price
const isPlatform = warehouseType === 'platform'
const supplyRaw = isPlatform
? (item.market_price ?? item.buy_price)
: (item.buy_price ?? item.market_price)
return { return {
id: item.id, id: item.id,
drugName: drug.drug_name || '-', drugName: drug.drug_name || '-',
imageUrl: resolveDrugImage(drug.image), imageUrl: resolveDrugImage(drug.image),
buyPrice: formatMoney(item.buy_price), buyPrice: formatMoney(supplyRaw),
price: formatMoney(item.price), price: formatMoney(item.price),
suggestPrice: formatMoney(suggestPrice), suggestPrice: formatMoney(suggestPrice),
stock: item.stock != null ? item.stock : '-', stock: item.stock != null ? item.stock : '-',

View File

@@ -1,5 +1,13 @@
<template> <template>
<u-popup v-model="show" mode="bottom" border-radius="20" height="85%" :closeable="true" @close="onClose"> <drawer-page-container
v-model="show"
title="处方详情"
height="85%"
tall
:show-close="true"
:closeable="true"
@close="onClose"
>
<view class="popup-wrap"> <view class="popup-wrap">
<scroll-view v-if="view" scroll-y class="popup-scroll"> <scroll-view v-if="view" scroll-y class="popup-scroll">
<view class="container safe-area-inset-bottom"> <view class="container safe-area-inset-bottom">
@@ -164,14 +172,16 @@
<view v-else-if="loading" class="popup-loading">加载中...</view> <view v-else-if="loading" class="popup-loading">加载中...</view>
<view v-else class="popup-loading">暂无数据</view> <view v-else class="popup-loading">暂无数据</view>
</view> </view>
</u-popup> </drawer-page-container>
</template> </template>
<script> <script>
import DrawerPageContainer from '@/subPackages/sub_business_shared/components/DrawerPageContainer.vue'
import { getPrescriptionDetail } from '@/api/clinicAdmin.js' import { getPrescriptionDetail } from '@/api/clinicAdmin.js'
import { mapPrescriptionDetailView } from '@/subPackages/sub_clinic_admin/common/display.js' import { mapPrescriptionDetailView } from '@/subPackages/sub_clinic_admin/common/display.js'
export default { export default {
components: { DrawerPageContainer },
name: 'PrescriptionDetailPopup', name: 'PrescriptionDetailPopup',
data() { data() {
return { return {
@@ -215,6 +225,7 @@ export default {
}, },
onClose() { onClose() {
this.view = null this.view = null
this.show = false
}, },
}, },
} }

View File

@@ -21,7 +21,7 @@
<view class="row"><text>医生</text><text>{{ doctorName }}</text></view> <view class="row"><text>医生</text><text>{{ doctorName }}</text></view>
<view class="row"><text>处方来源</text><text>{{ storeName }}</text></view> <view class="row"><text>处方来源</text><text>{{ storeName }}</text></view>
<view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view> <view class="row"><text>订单来源</text><text>{{ onlineText(info.is_online) }}</text></view>
<view class="row"><text>ERP状态</text><text>{{ erpSyncText(info.is_sync_erp) }}</text></view> <view class="row" v-if="detailExtra.prescriptionStatusText"><text>处方状态</text><text>{{ detailExtra.prescriptionStatusText }}</text></view>
<view class="row"><text>支付时间</text><text>{{ info.pay_time || '-' }}</text></view> <view class="row"><text>支付时间</text><text>{{ info.pay_time || '-' }}</text></view>
<view class="row"><text>下单时间</text><text>{{ info.created_at || '-' }}</text></view> <view class="row"><text>下单时间</text><text>{{ info.created_at || '-' }}</text></view>
<view class="row" v-if="detailExtra.cancelRemark"><text>发货备注</text><text>{{ detailExtra.cancelRemark }}</text></view> <view class="row" v-if="detailExtra.cancelRemark"><text>发货备注</text><text>{{ detailExtra.cancelRemark }}</text></view>
@@ -29,11 +29,20 @@
<view class="sub-block"> <view class="sub-block">
<view class="sub-block-title">费用明细</view> <view class="sub-block-title">费用明细</view>
<view class="row"><text>药品总价</text><text>{{ money(info.items_price) }}</text></view> <view class="row">
<text>药品总价</text>
<text
v-if="canOrderPercentAdjust"
class="price-link"
@click="openOrderPriceAdjust"
>{{ money(info.items_price) }}</text>
<text v-else>{{ money(info.items_price) }}</text>
</view>
<view class="row"><text>供货价</text><text>{{ detailExtra.marketPrice }}</text></view> <view class="row"><text>供货价</text><text>{{ detailExtra.marketPrice }}</text></view>
<view class="row"><text>运费</text><text>{{ money(info.trans_expenses) }}</text></view> <view class="row"><text>运费</text><text>{{ money(info.trans_expenses) }}</text></view>
<view class="row"><text>挂号费</text><text>{{ money(info.register_price) }}</text></view> <view class="row"><text>挂号费</text><text>{{ money(info.register_price) }}</text></view>
<view class="row"><text>是否包邮</text><text>{{ detailExtra.freeShippingText }}</text></view> <view class="row"><text>是否包邮</text><text>{{ detailExtra.freeShippingText }}</text></view>
<view class="row" v-if="orderPriceDiscountLabel"><text>价格浮动</text><text>{{ orderPriceDiscountLabel }}</text></view>
</view> </view>
</view> </view>
@@ -124,6 +133,7 @@
<!-- 底部悬浮操作栏 --> <!-- 底部悬浮操作栏 -->
<view class="actions-bar" v-if="info.id"> <view class="actions-bar" v-if="info.id">
<button v-if="canOrderPercentAdjust && Number(info.price_discount) !== 100" class="btn-outline" @click="clearOrderPriceDiscount({ adjustOrderPercent })">清除浮动</button>
<button class="btn-outline" @click="goTrace">订单追溯</button> <button class="btn-outline" @click="goTrace">订单追溯</button>
<button v-if="canRefund" class="btn-warn" @click="refund">申请退款</button> <button v-if="canRefund" class="btn-warn" @click="refund">申请退款</button>
<button v-if="detailExtra.canViewPrescription" class="btn-primary btn-full" @click="openPrescription">查看处方</button> <button v-if="detailExtra.canViewPrescription" class="btn-primary btn-full" @click="openPrescription">查看处方</button>
@@ -133,6 +143,14 @@
<!-- 处方弹窗 --> <!-- 处方弹窗 -->
<prescription-detail-popup ref="prescriptionPopup" /> <prescription-detail-popup ref="prescriptionPopup" />
<order-price-percent-adjust-popup
:visible="priceAdjustVisible"
:current-discount="info.price_discount || 100"
:quick-options="priceAdjustQuickOptions"
:submitting="priceAdjustSubmitting"
@close="closePriceAdjust"
@confirm="(p) => onPriceAdjustConfirm(p, { adjustOrderPercent })"
/>
</clinic-page-layout> </clinic-page-layout>
</template> </template>
@@ -144,7 +162,11 @@ import {
getOrderDetail, getOrderDetail,
refundOrder, refundOrder,
getExpressDetailByOrderId, getExpressDetailByOrderId,
getPriceAdjustConfig,
adjustOrderPercent,
} from '@/api/clinicAdmin.js' } from '@/api/clinicAdmin.js'
import { orderPriceAdjustMixin } from '@/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js'
import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue'
import { import {
orderStatusText, orderStatusText,
deliveryMethodText, deliveryMethodText,
@@ -157,7 +179,8 @@ import {
import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js' import { formatMoney } from '@/subPackages/sub_clinic_admin/common/format.js'
export default { export default {
components: { ClinicPageLayout, ExpressTimeline, PrescriptionDetailPopup }, mixins: [orderPriceAdjustMixin],
components: { ClinicPageLayout, ExpressTimeline, PrescriptionDetailPopup, OrderPricePercentAdjustPopup },
data() { data() {
return { return {
id: 0, id: 0,
@@ -227,6 +250,7 @@ export default {
if (!w.ok) return if (!w.ok) return
this.info = w.data || {} this.info = w.data || {}
this.detailExtra = mapOrderDetailDisplay(this.info) this.detailExtra = mapOrderDetailDisplay(this.info)
this.loadPriceAdjustMeta(() => getPriceAdjustConfig(this.info.store_id))
if (this.info.delivery_method === 0) { if (this.info.delivery_method === 0) {
this.loadExpress() this.loadExpress()
} }
@@ -514,6 +538,7 @@ $color-danger: #F53F3F;
} }
.price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; } .price-main { font-size: 28rpx; color: $color-text-title; font-weight: 600; }
.price-link { color: $theme-primary; text-decoration: underline; }
/* ================= 底部动作栏 ================= */ /* ================= 底部动作栏 ================= */
.actions-bar { .actions-bar {

View File

@@ -3,6 +3,18 @@
<view class="order-list-container"> <view class="order-list-container">
<!-- 顶部筛选 --> <!-- 顶部筛选 -->
<page-sticky-header> <page-sticky-header>
<view class="quick-filter">
<view class="filter-row date-row">
<picker mode="date" :value="dateStart" @change="onStart">
<view class="filter-picker date-picker">{{ dateStart || '开始日期' }}</view>
</picker>
<text class="sep"></text>
<picker mode="date" :value="dateEnd" @change="onEnd">
<view class="filter-picker date-picker">{{ dateEnd || '结束日期' }}</view>
</picker>
</view>
<view class="filter-query-btn" @click="reload">查询</view>
</view>
<collapsible-filter-panel title="订单筛选" @clear="resetFilter"> <collapsible-filter-panel title="订单筛选" @clear="resetFilter">
<view class="filter-row"> <view class="filter-row">
<text class="filter-label">订单号</text> <text class="filter-label">订单号</text>
@@ -26,16 +38,6 @@
<view class="filter-picker">{{ prescriptionTypeLabels[prescriptionTypeIndex] }}</view> <view class="filter-picker">{{ prescriptionTypeLabels[prescriptionTypeIndex] }}</view>
</picker> </picker>
</view> </view>
<view class="filter-row date-row">
<picker mode="date" :value="dateStart" @change="onStart">
<view class="filter-picker date-picker">{{ dateStart || '开始日期' }}</view>
</picker>
<text class="sep"></text>
<picker mode="date" :value="dateEnd" @change="onEnd">
<view class="filter-picker date-picker">{{ dateEnd || '结束日期' }}</view>
</picker>
</view>
<view class="filter-query-btn" @click="reload">查询</view>
</collapsible-filter-panel> </collapsible-filter-panel>
</page-sticky-header> </page-sticky-header>
@@ -153,6 +155,7 @@ import {
clearOrderFilter, clearOrderFilter,
defaultOrderFilter, defaultOrderFilter,
} from '@/subPackages/sub_clinic_admin/common/filterCache.js' } from '@/subPackages/sub_clinic_admin/common/filterCache.js'
import { defaultTodayDate } from '@/subPackages/sub_clinic_admin/common/reconciliationParams.js'
const DELIVERY_OPTIONS = [ const DELIVERY_OPTIONS = [
{ label: '全部', value: '' }, { label: '全部', value: '' },
@@ -206,7 +209,7 @@ export default {
}))) })))
} }
}) })
this.reload() this.checkDateEndAndReload()
}, },
onReachBottom() { onReachBottom() {
this.load(this.page + 1, true) this.load(this.page + 1, true)
@@ -235,6 +238,24 @@ export default {
clearOrderFilter() clearOrderFilter()
this.reload() this.reload()
}, },
checkDateEndAndReload() {
const today = defaultTodayDate()
if (this.dateEnd && this.dateEnd !== today) {
uni.showModal({
title: '提示',
content: '筛选结束日期不是今天,是否更新到今天?',
success: (r) => {
if (r.confirm) {
this.dateEnd = today
this.persistOrderFilter()
}
this.reload()
},
})
return
}
this.reload()
},
filterValues() { filterValues() {
const st = this.statusOptions[this.statusIndex] const st = this.statusOptions[this.statusIndex]
const del = DELIVERY_OPTIONS[this.deliveryIndex] const del = DELIVERY_OPTIONS[this.deliveryIndex]
@@ -320,6 +341,13 @@ $color-text-muted: #86909C;
} }
/* 筛选区 */ /* 筛选区 */
.quick-filter {
background: #fff;
padding: 24rpx;
margin-bottom: 16rpx;
border-radius: 16rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.06);
}
.filter-row { .filter-row {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -10,6 +10,7 @@
<view class="row" v-if="summary.register_order_no"><text class="label">挂号订单</text><text class="value">{{ summary.register_order_no }}</text></view> <view class="row" v-if="summary.register_order_no"><text class="label">挂号订单</text><text class="value">{{ summary.register_order_no }}</text></view>
<view class="row" v-if="summary.prescription_no"><text class="label">处方号</text><text class="value">{{ summary.prescription_no }}</text></view> <view class="row" v-if="summary.prescription_no"><text class="label">处方号</text><text class="value">{{ summary.prescription_no }}</text></view>
<view class="row"><text class="label">结算状态</text><text class="value">{{ summary.is_settled === 1 ? '已结算' : '未结算' }}</text></view> <view class="row"><text class="label">结算状态</text><text class="value">{{ summary.is_settled === 1 ? '已结算' : '未结算' }}</text></view>
<view class="row" v-if="prescriptionStatusLabel"><text class="label">审方状态</text><text class="value status-audit">{{ prescriptionStatusLabel }}</text></view>
</view> </view>
<!-- Tab --> <!-- Tab -->
@@ -26,7 +27,7 @@
<!-- 时间线 --> <!-- 时间线 -->
<view v-show="activeTab === 'timeline'" class="tab-panel"> <view v-show="activeTab === 'timeline'" class="tab-panel">
<view v-if="!timeline.length" class="empty">暂无时间线数据</view> <view v-if="!timeline.length" class="empty">暂无时间线数据</view>
<view v-for="(node, idx) in timeline" :key="node.key || idx" class="timeline-item"> <view v-for="node in timeline" :key="node._wxKey" class="timeline-item">
<view class="tl-dot" /> <view class="tl-dot" />
<view class="tl-body"> <view class="tl-body">
<view class="tl-title">{{ node.title }}</view> <view class="tl-title">{{ node.title }}</view>
@@ -55,6 +56,9 @@
@click="switchLedgerSub('register')" @click="switchLedgerSub('register')"
>挂号订单</view> >挂号订单</view>
</view> </view>
<view v-if="currentLedgerSumMoney != null" class="ledger-sum">
合计分账{{ formatMoney(currentLedgerSumMoney) }}
</view>
<view v-if="currentLedgerItems.length"> <view v-if="currentLedgerItems.length">
<view v-for="row in currentLedgerItems" :key="row._wxKey" class="detail-card"> <view v-for="row in currentLedgerItems" :key="row._wxKey" class="detail-card">
<view class="row"><text class="label">费用类型</text><text class="value">{{ row.fee_type_txt || '-' }}</text></view> <view class="row"><text class="label">费用类型</text><text class="value">{{ row.fee_type_txt || '-' }}</text></view>
@@ -190,7 +194,8 @@ export default {
return this.traceData?.summary || {} return this.traceData?.summary || {}
}, },
timeline() { timeline() {
return this.traceData?.timeline || [] const items = this.traceData?.timeline || []
return withWxKey(items, 'key', 'tl')
}, },
links() { links() {
return this.traceData?.links || {} return this.traceData?.links || {}
@@ -198,11 +203,26 @@ export default {
hasRegisterLedger() { hasRegisterLedger() {
return !!(this.links.register_id && this.ledgerRegister !== null) return !!(this.links.register_id && this.ledgerRegister !== null)
}, },
currentLedgerData() {
return this.ledgerSubTab === 'register' ? this.ledgerRegister : this.ledgerProduct
},
currentLedgerSumMoney() {
const data = this.currentLedgerData
if (!data || data.sum_money == null || data.sum_money === '') return null
return data.sum_money
},
currentLedgerItems() { currentLedgerItems() {
const data = this.ledgerSubTab === 'register' ? this.ledgerRegister : this.ledgerProduct const items = this.currentLedgerData?.items || []
const items = data?.items || []
return withWxKey(items, 'id', 'traceLed') return withWxKey(items, 'id', 'traceLed')
}, },
prescriptionStatusLabel() {
if (!this.summary.prescription_id) return ''
const auditNode = (this.timeline || []).find(
n => n.key === 'prescription_audited' || n.key === 'prescription_rejected'
)
if (auditNode && auditNode.status) return auditNode.status
return '待审核'
},
reconciliationItems() { reconciliationItems() {
const items = this.reconciliationData?.items || [] const items = this.reconciliationData?.items || []
return withWxKey(items, 'id', 'traceRec') return withWxKey(items, 'id', 'traceRec')
@@ -345,6 +365,13 @@ export default {
font-weight: 600; font-weight: 600;
background: #e6f4ff; background: #e6f4ff;
} }
.ledger-sum {
font-size: 28rpx;
color: #333;
font-weight: 600;
padding: 8rpx 8rpx 16rpx;
}
.status-audit { color: #6acdbb; }
.tab-panel { min-height: 200rpx; } .tab-panel { min-height: 200rpx; }
.timeline-item { .timeline-item {
display: flex; display: flex;

View File

@@ -136,7 +136,7 @@
<block v-if="lists.prescription_type==2||lists.prescription_type==5||lists.prescription_type==7"> <block v-if="lists.prescription_type==2||lists.prescription_type==5||lists.prescription_type==7">
<view class="item" v-for="(it,index) in infoList.repice" :key="it.id || index"> <view class="item" v-for="it in infoList.repice" :key="it._wxKey">
<!-- v-for="(it,index) in item.content" :key="it.id" --> <!-- v-for="(it,index) in item.content" :key="it.id" -->
<view class="name" style="justify-content: space-between;"> <view class="name" style="justify-content: space-between;">
<view class="yp_name"> <view class="yp_name">
@@ -263,6 +263,7 @@
prescripRecordDetail, prescripRecordDetail,
getUseWay getUseWay
} from "@/api/all.js"; } from "@/api/all.js";
import { withWxKey } from '@/utils/wxListKey.js'
export default { export default {
data() { data() {
return { return {
@@ -350,7 +351,7 @@
return num return num
}, },
normalizeRepice(list) { normalizeRepice(list) {
return (list || []).map((it) => { const normalized = (list || []).map((it) => {
let content = it.content let content = it.content
if (typeof content === 'string') { if (typeof content === 'string') {
try { try {
@@ -367,6 +368,7 @@
content, content,
} }
}) })
return withWxKey(normalized, 'id', 'rp')
}, },
getInfo() { getInfo() {
prescripRecordDetail({ prescripRecordDetail({

View File

@@ -1,8 +1,8 @@
import { prepareImagePath } from '@/common/js/image-compress.js' import { prepareImagePath } from '@/common/js/image-compress.js'
import { uploadLegacyAdminImage, uploadBusinessChatFileApi, extractUploadUrl } from '@/api/upload.js' import { uploadToOss } from '@/common/js/oss-upload.js'
/** /**
* 选择并上传图片(压缩 + old-admin-img 接口 * 选择并上传图片(压缩 + OSS 直传
* @param {{ count?: number }} options count>1 时依次上传多张,返回 url 数组 * @param {{ count?: number }} options count>1 时依次上传多张,返回 url 数组
*/ */
export function pickAndUploadImage(options = {}) { export function pickAndUploadImage(options = {}) {
@@ -26,11 +26,8 @@ export function pickAndUploadImage(options = {}) {
size: files.find((f) => f.path === path)?.size, size: files.find((f) => f.path === path)?.size,
}) })
if (!prepared) continue if (!prepared) continue
const url = await uploadLegacyAdminImage({ const result = await uploadToOss({ filePath: prepared.path })
filePath: prepared.path, urls.push(result.url)
name: 'file',
})
urls.push(url)
} }
uni.hideLoading() uni.hideLoading()
if (!urls.length) { if (!urls.length) {
@@ -66,20 +63,11 @@ function getUserDataPath() {
} }
async function uploadSignFilePath(filePath) { async function uploadSignFilePath(filePath) {
let url = null const result = await uploadToOss({ filePath })
try { if (!result || !result.url) {
const res = await uploadBusinessChatFileApi({ filePath, name: 'file' })
url = extractUploadUrl(res)
} catch (e) {
// 回退与证照相同的上传接口
}
if (!url) {
url = await uploadLegacyAdminImage({ filePath, name: 'file' })
}
if (!url) {
throw new Error('上传成功但未返回地址') throw new Error('上传成功但未返回地址')
} }
return url return result.url
} }
/** /**

View File

@@ -133,7 +133,7 @@
<block v-if="lists.prescription_type==2||lists.prescription_type==5||lists.prescription_type==7"> <block v-if="lists.prescription_type==2||lists.prescription_type==5||lists.prescription_type==7">
<view class="item" v-for="(it,index) in infoList.repice" :key="it.id || index"> <view class="item" v-for="it in infoList.repice" :key="it._wxKey">
<!-- v-for="(it,index) in item.content" :key="it.id" --> <!-- v-for="(it,index) in item.content" :key="it.id" -->
<view class="name" style="justify-content: space-between;"> <view class="name" style="justify-content: space-between;">
<view class="yp_name"> <view class="yp_name">
@@ -207,6 +207,7 @@
prescripDetail, prescripDetail,
getUseWay getUseWay
} from "@/api/all.js"; } from "@/api/all.js";
import { withWxKey } from '@/utils/wxListKey.js'
export default { export default {
data() { data() {
return { return {
@@ -276,7 +277,7 @@
return num return num
}, },
normalizeRepice(list) { normalizeRepice(list) {
return (list || []).map((it) => { const normalized = (list || []).map((it) => {
let content = it.content let content = it.content
if (typeof content === 'string') { if (typeof content === 'string') {
try { try {
@@ -293,6 +294,7 @@
content, content,
} }
}) })
return withWxKey(normalized, 'id', 'rp')
}, },
getInfo() { getInfo() {
prescripDetail({ prescripDetail({

View File

@@ -376,7 +376,24 @@
class="fs-24 color-primary m-t-8" class="fs-24 color-primary m-t-8"
>毛利率 {{ grossMarginText }}%</text> >毛利率 {{ grossMarginText }}%</text>
</view> </view>
<text class="fs-30 color-title font-bold">{{totalProductCost.toFixed(2)}}</text> <text
v-if="canPreSendPriceAdjust"
class="fs-30 color-title font-bold price-link"
@click="openPreSendOrderPriceAdjust"
>{{totalProductCost.toFixed(2)}}</text>
<text v-else class="fs-30 color-title font-bold">{{totalProductCost.toFixed(2)}}</text>
</view>
<view
v-if="canPreSendPriceAdjust && currentDrugs.length > 0"
class="flex-row flex-jus-sp flex-ali-center m-b-24"
>
<text class="fs-26 color-sub">当前浮动{{ preSendDiscountLabel || '无' }}</text>
<text
v-if="priceDiscount !== 100"
class="fs-26 color-primary"
@click="clearPreSendPriceDiscount"
>清除浮动</text>
</view> </view>
<view <view
@@ -501,6 +518,14 @@
:border="true" /> :border="true" />
</view> </view>
</u-modal> </u-modal>
<order-price-percent-adjust-popup
:visible="priceAdjustVisible"
:current-discount="priceDiscount"
:quick-options="priceAdjustQuickOptions"
@close="closePreSendPriceAdjust"
@confirm="onPreSendPriceAdjustConfirm"
/>
</view> </view>
</template> </template>
@@ -519,6 +544,7 @@ import {
saveWestCommonPrescriptionApi, saveWestCommonPrescriptionApi,
saveChineseCommonPrescriptionApi, saveChineseCommonPrescriptionApi,
getCurrentStoreTypeApi, getCurrentStoreTypeApi,
getPriceAdjustConfigApi,
getSalespersonTransferByRegisterApi, getSalespersonTransferByRegisterApi,
getPrescriptionInfoApi, getPrescriptionInfoApi,
getTraditionalChineseMedicineJson getTraditionalChineseMedicineJson
@@ -538,6 +564,8 @@ import WesternMedicineUsageModal from './components/modals/WesternMedicineUsageM
import ChineseMedicineConfig from './components/ChineseMedicineConfig.vue'; import ChineseMedicineConfig from './components/ChineseMedicineConfig.vue';
import TraditionalTermPickerModal from './components/modals/TraditionalTermPickerModal.vue'; import TraditionalTermPickerModal from './components/modals/TraditionalTermPickerModal.vue';
import TransferPatientDrawer from './components/modals/TransferPatientDrawer.vue'; import TransferPatientDrawer from './components/modals/TransferPatientDrawer.vue';
import OrderPricePercentAdjustPopup from '@/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue';
import { applyRatioToDrugs, formatPriceDiscountLabel, normalizeQuickOptions } from '@/subPackages/sub_business_shared/common/pricePercentAdjust.js';
import { import {
createClinicSalespersonTransferPrescription, createClinicSalespersonTransferPrescription,
getClinicSalespersonSeePriceConfig, getClinicSalespersonSeePriceConfig,
@@ -574,7 +602,8 @@ export default {
WesternMedicineUsageModal, WesternMedicineUsageModal,
ChineseMedicineConfig, ChineseMedicineConfig,
TraditionalTermPickerModal, TraditionalTermPickerModal,
TransferPatientDrawer TransferPatientDrawer,
OrderPricePercentAdjustPopup,
}, },
data() { data() {
return { return {
@@ -639,6 +668,11 @@ export default {
hasSalespersonTransfer: false, hasSalespersonTransfer: false,
/** 是否可查看毛利率 0/1xk-api see_rate */ /** 是否可查看毛利率 0/1xk-api see_rate */
seeRate: 0, seeRate: 0,
enableOrderPricePercentAdjust: 0,
priceAdjustVisible: false,
priceDiscount: 100,
priceAdjustQuickOptions: [],
priceAdjustScopeMode: 'sale_only',
sendMode: 0, sendMode: 0,
selectedStoreId: null, selectedStoreId: null,
registerOrderType: null, registerOrderType: null,
@@ -703,6 +737,18 @@ export default {
allowInsuranceCategory() { allowInsuranceCategory() {
return Number(this.registerStoreInfo?.allow_insurance_category ?? 0) === 1; return Number(this.registerStoreInfo?.allow_insurance_category ?? 0) === 1;
}, },
canPreSendPriceAdjust() {
return Number(this.enableOrderPricePercentAdjust) === 1 && this.currentDrugs.length > 0;
},
preSendDiscountLabel() {
return formatPriceDiscountLabel(this.priceDiscount);
},
effectivePriceAdjustStoreId() {
return this.selectedStoreId
|| this.registerStoreInfo?.store_id
|| uni.getStorageSync('store_id')
|| null;
},
totalProductCost() { totalProductCost() {
if (this.currentDrugs.length === 0) return 0; if (this.currentDrugs.length === 0) return 0;
if (this.activeCategory === 1) { if (this.activeCategory === 1) {
@@ -1091,6 +1137,56 @@ export default {
drug.buy_price drug.buy_price
); );
}, },
async loadPriceAdjustConfigForStore(storeId) {
try {
const res = await getPriceAdjustConfigApi(storeId);
if (res) {
this.priceAdjustScopeMode = res.order_price_adjust_scope === 'both' ? 'both' : 'sale_only';
this.priceAdjustQuickOptions = normalizeQuickOptions(res.order_discount_quick_options);
if (res.enable_order_price_percent_adjust != null) {
this.enableOrderPricePercentAdjust = Number(res.enable_order_price_percent_adjust);
}
}
} catch (e) {
console.error('获取调价配置失败:', e);
}
},
snapshotDrugOrigins(drugs) {
return (drugs || []).map((drug) => ({
...drug,
origin_price: drug.origin_price ?? drug.price,
origin_buy_price: drug.origin_buy_price ?? drug.buy_price,
}));
},
prepareDrugForCart(drug) {
const [prepared] = applyRatioToDrugs(
this.snapshotDrugOrigins([drug]),
this.priceDiscount,
this.priceAdjustScopeMode,
);
return prepared;
},
applyPreSendPriceDiscount(discount) {
this.priceDiscount = discount;
const scope = this.priceAdjustScopeMode;
this.currentDrugs = applyRatioToDrugs(this.snapshotDrugOrigins(this.currentDrugs), discount, scope);
uni.showToast({ title: discount === 100 ? '已清除浮动' : '调价成功', icon: 'none' });
},
clearPreSendPriceDiscount() {
this.applyPreSendPriceDiscount(100);
},
openPreSendOrderPriceAdjust() {
if (!this.canPreSendPriceAdjust) return;
this.currentDrugs = this.snapshotDrugOrigins(this.currentDrugs);
this.priceAdjustVisible = true;
},
closePreSendPriceAdjust() {
this.priceAdjustVisible = false;
},
onPreSendPriceAdjustConfirm(payload) {
this.applyPreSendPriceDiscount(payload.priceDiscount);
this.closePreSendPriceAdjust();
},
async fetchStoreSeeRate() { async fetchStoreSeeRate() {
try { try {
const storeId = uni.getStorageSync('store_id'); const storeId = uni.getStorageSync('store_id');
@@ -1107,6 +1203,8 @@ export default {
const res = await getCurrentStoreTypeApi(params); const res = await getCurrentStoreTypeApi(params);
if (res) { if (res) {
this.seeRate = Number(res.see_rate ?? 0); this.seeRate = Number(res.see_rate ?? 0);
this.enableOrderPricePercentAdjust = Number(res.enable_order_price_percent_adjust ?? 0);
await this.loadPriceAdjustConfigForStore(params.store_id || storeId);
} }
} catch (error) { } catch (error) {
console.error('获取毛利率权限失败:', error); console.error('获取毛利率权限失败:', error);
@@ -1137,6 +1235,9 @@ export default {
this.selectedStoreId = res.store_id; this.selectedStoreId = res.store_id;
this.sendMode = res.can_change ? 1 : 0; this.sendMode = res.can_change ? 1 : 0;
} }
const priceStoreId = this.selectedStoreId || res.store_id || storeId;
this.enableOrderPricePercentAdjust = Number(res.enable_order_price_percent_adjust ?? 0);
await this.loadPriceAdjustConfigForStore(priceStoreId);
} }
} catch (error) { } catch (error) {
console.error('获取挂号诊所信息失败:', error); console.error('获取挂号诊所信息失败:', error);
@@ -1172,7 +1273,7 @@ export default {
this.salespersonTransferPrescriptionId = item.id; this.salespersonTransferPrescriptionId = item.id;
this.activeCategory = 1; this.activeCategory = 1;
parsed.drugList.forEach((drug) => { parsed.drugList.forEach((drug) => {
this.currentDrugs.push({ this.currentDrugs.push(this.prepareDrugForCart({
index_id: drug.index_id ?? drug.drug_id ?? drug.id, index_id: drug.index_id ?? drug.drug_id ?? drug.id,
id: drug.drug_id || drug.id, id: drug.drug_id || drug.id,
drug_id: drug.drug_id || drug.id, drug_id: drug.drug_id || drug.id,
@@ -1182,7 +1283,7 @@ export default {
way_id: drug.way_id || 0, way_id: drug.way_id || 0,
price: drug.price ?? 0, price: drug.price ?? 0,
buy_price: drug.buy_price, buy_price: drug.buy_price,
}); }));
}); });
if (parsed.clinical_diagnose) { if (parsed.clinical_diagnose) {
this.diagnoses = [{ id: Date.now(), name: parsed.clinical_diagnose }]; this.diagnoses = [{ id: Date.now(), name: parsed.clinical_diagnose }];
@@ -1424,7 +1525,7 @@ export default {
use_frequency: this.drugUseList.drug_use_frequency?.find((item) => item.id === (d.frequency_id || 0)), use_frequency: this.drugUseList.drug_use_frequency?.find((item) => item.id === (d.frequency_id || 0)),
unit: this.drugUseList.drug_unit?.find((item) => item.id === (d.unit_id || 0)) unit: this.drugUseList.drug_unit?.find((item) => item.id === (d.unit_id || 0))
}; };
this.currentDrugs.push(newDrug); this.currentDrugs.push(this.prepareDrugForCart(newDrug));
added++; added++;
} }
this.saveToLocalStorage(); this.saveToLocalStorage();
@@ -1440,7 +1541,7 @@ export default {
if (PrescriptionValidator.isDrugExists(this.currentDrugs, product)) { if (PrescriptionValidator.isDrugExists(this.currentDrugs, product)) {
continue; continue;
} }
this.currentDrugs.push({ ...product }); this.currentDrugs.push(this.prepareDrugForCart({ ...product }));
added++; added++;
} }
this.saveToLocalStorage(); this.saveToLocalStorage();
@@ -1479,7 +1580,7 @@ export default {
use_frequency: this.drugUseList.drug_use_frequency?.find(item => item.id === (drug.frequency_id || drug.drug?.frequency_id)), use_frequency: this.drugUseList.drug_use_frequency?.find(item => item.id === (drug.frequency_id || drug.drug?.frequency_id)),
unit: this.drugUseList.drug_unit?.find(item => item.id === (drug.unit_id || drug.drug?.unit_id)) unit: this.drugUseList.drug_unit?.find(item => item.id === (drug.unit_id || drug.drug?.unit_id))
}; };
this.currentDrugs.push(newDrug); this.currentDrugs.push(this.prepareDrugForCart(newDrug));
const index = this.currentDrugs.length - 1; const index = this.currentDrugs.length - 1;
this.handleEditWesternDrug(this.currentDrugs[index], index); this.handleEditWesternDrug(this.currentDrugs[index], index);
this.saveToLocalStorage(); this.saveToLocalStorage();
@@ -1517,7 +1618,11 @@ export default {
}); });
}, },
handleSelectChineseDrug(drugs) { handleSelectChineseDrug(drugs) {
this.currentDrugs = drugs || []; this.currentDrugs = applyRatioToDrugs(
this.snapshotDrugOrigins(drugs || []),
this.priceDiscount,
this.priceAdjustScopeMode,
);
this.saveToLocalStorage(); this.saveToLocalStorage();
}, },
handleOpenSimpleProductModal() { this.showSimpleProductModal = true; }, handleOpenSimpleProductModal() { this.showSimpleProductModal = true; },
@@ -1537,7 +1642,7 @@ export default {
type: product.drug?.type || product.type || this.activeCategory, type: product.drug?.type || product.type || this.activeCategory,
select_number: 1 select_number: 1
}; };
this.currentDrugs.push(newProduct); this.currentDrugs.push(this.prepareDrugForCart(newProduct));
this.saveToLocalStorage(); this.saveToLocalStorage();
this.$toast(`已添加 ${newProduct.drug_name}`); this.$toast(`已添加 ${newProduct.drug_name}`);
}, },
@@ -1968,6 +2073,7 @@ export default {
const params = { const params = {
patient: patientData, drugs: drugsData, diagnosis: clinical_diagnose, medicalAdvice: this.medicalAdvice, total: parseFloat(this.totalPrice), category: this.feeCategory, drug_type: 2, register_id: parseInt(this.registerId), treatment_price: parseFloat(this.treatmentPrice || 0), prescription_type: this.activeCategory, doctor_second_sign: doctorSecondSignValue, send_mode: finalSendMode, custom_store_id: finalSendMode === 1 ? finalStoreId : null, patient: patientData, drugs: drugsData, diagnosis: clinical_diagnose, medicalAdvice: this.medicalAdvice, total: parseFloat(this.totalPrice), category: this.feeCategory, drug_type: 2, register_id: parseInt(this.registerId), treatment_price: parseFloat(this.treatmentPrice || 0), prescription_type: this.activeCategory, doctor_second_sign: doctorSecondSignValue, send_mode: finalSendMode, custom_store_id: finalSendMode === 1 ? finalStoreId : null,
salesperson_transfer_prescription_id: this.salespersonTransferPrescriptionId || undefined, salesperson_transfer_prescription_id: this.salespersonTransferPrescriptionId || undefined,
price_discount: this.priceDiscount,
}; };
if (this.activeCategory === 1) { if (this.activeCategory === 1) {
params.package_method_id = this.chineseConfig.packageMethodId || null; params.process_rule_id = this.chineseConfig.processRuleId || null; params.process_rule_note_id = this.chineseConfig.processRuleNoteId || null; params.child_process_rule_id = this.chineseConfig.childProcessRuleId || null; params.process_rule_type = this.chineseConfig.ruleType || 1; params.processing_fee = this.processingFee; params.dosage = this.chineseConfig.dosage || 7; params.day_dosage = this.chineseConfig.dayDosage || 2; params.package_method_id = this.chineseConfig.packageMethodId || null; params.process_rule_id = this.chineseConfig.processRuleId || null; params.process_rule_note_id = this.chineseConfig.processRuleNoteId || null; params.child_process_rule_id = this.chineseConfig.childProcessRuleId || null; params.process_rule_type = this.chineseConfig.ruleType || 1; params.processing_fee = this.processingFee; params.dosage = this.chineseConfig.dosage || 7; params.day_dosage = this.chineseConfig.dayDosage || 2;
@@ -2271,6 +2377,7 @@ export default {
.color-primary { color: #00A88A; } .color-primary { color: #00A88A; }
.color-danger { color: #FF4D4F; } .color-danger { color: #FF4D4F; }
.color-price { color: #F53F3F; } .color-price { color: #F53F3F; }
.price-link { color: #6ACDBB; text-decoration: underline; }
.text-gray { color: #6C7380; } .text-gray { color: #6C7380; }
.white { background: #fff; } .white { background: #fff; }

View File

@@ -25,6 +25,9 @@
<text>{{item.sex %2 != 0 ? '男' : '女'}}</text> <text>{{item.sex %2 != 0 ? '男' : '女'}}</text>
<text>{{item.age + '岁'}}</text> <text>{{item.age + '岁'}}</text>
</view> </view>
<view class="flex-row m-t-1">
<text class="fs-24 color-gray">推广员{{ item.salesperson_name || '无' }}</text>
</view>
</view> </view>
</view> </view>
</view> </view>