diff --git a/api/businessApi.js b/api/businessApi.js index 29de5d6..a371721 100644 --- a/api/businessApi.js +++ b/api/businessApi.js @@ -113,6 +113,21 @@ export function createBusinessApi(prefix, headerFlag) { 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) { return bizRequest({ url: `${prefix}-warehouse/list`, method: 'GET', data: params }); }, diff --git a/api/clinicAdmin.js b/api/clinicAdmin.js index d117dd4..e114b48 100644 --- a/api/clinicAdmin.js +++ b/api/clinicAdmin.js @@ -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) { return clinicRequest({ url: `${PREFIX}-warehouse/list`, method: 'GET', data: params }); } diff --git a/api/reception.js b/api/reception.js index cf8704b..c12b8c0 100644 --- a/api/reception.js +++ b/api/reception.js @@ -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) { return req.request({ diff --git a/api/upload.js b/api/upload.js index ab122df..53af33e 100644 --- a/api/upload.js +++ b/api/upload.js @@ -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 */ @@ -77,6 +87,44 @@ export function extractUploadUrl(res) { 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'; /** diff --git a/common/js/oss-upload.js b/common/js/oss-upload.js new file mode 100644 index 0000000..71d9a6e --- /dev/null +++ b/common/js/oss-upload.js @@ -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); + } + }); + } + }); +} diff --git a/pages/workbench/index.vue b/pages/workbench/index.vue index e5dbc1c..52788ec 100644 --- a/pages/workbench/index.vue +++ b/pages/workbench/index.vue @@ -86,7 +86,7 @@ {{ acceptingOfflineOrderText(item) }} + 推广员:{{ (item.salesperson && item.salesperson.nick_name) || '无' }} @@ -629,6 +633,10 @@ export default { const room = item.room_id != null && item.room_id !== '' ? String(item.room_id) : 'noroom'; 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) { const p = item.last_message_preview; if (p != null && String(p).trim() !== '') return String(p); diff --git a/subPackages/sub_business_shared/common/display.js b/subPackages/sub_business_shared/common/display.js index 9231f04..2f166b0 100644 --- a/subPackages/sub_business_shared/common/display.js +++ b/subPackages/sub_business_shared/common/display.js @@ -135,6 +135,11 @@ export function erpSyncText(v) { return '未同步' } +export function prescriptionStatusText(status) { + const m = { 0: '待审核', 1: '已审核', 2: '已驳回', 3: '已过期' } + return m[status] != null ? m[status] : '-' +} + export function freeShippingText(v) { if (v === 1) return '包邮' return '不包邮' @@ -238,6 +243,7 @@ export function mapOrderProductItem(p, prescriptionType) { : '×' + (p.number != null ? p.number : 0) const isTcm = prescriptionType === 1 return { + id: p.id, drugName: p.drug_name || drug.drug_name || '药品', drugNumber: drug.drug_number || '', imageUrl: resolveDrugImage(p.drug_image || drug.image), @@ -276,6 +282,9 @@ export function mapOrderDetailDisplay(info) { if (!info || !info.id) return {} 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 presStatus = info.prescription && info.prescription.status != null + ? info.prescription.status + : null return { orderTypeText: orderTypeText(info.order_type), marketPrice: formatMoney(info.market_price), @@ -284,6 +293,7 @@ export function mapOrderDetailDisplay(info) { showPrescription: mapOrderPrescription(info), prescriptionId: info.p_id, canViewPrescription: canView, + prescriptionStatusText: canView && presStatus != null ? prescriptionStatusText(presStatus) : '', statusText: orderStatusText(info.status), prescriptionTypeText: prescriptionTypeText(info.prescription_type), deliveryText: deliveryMethodText(info.delivery_method), @@ -398,16 +408,20 @@ export function warehouseStatusText(status) { return '-' } -export function mapWarehouseRow(item) { +export function mapWarehouseRow(item, warehouseType = 'store') { if (!item) return {} const drug = item.drug || {} const storeDrug = drug.drug_store_drug || {} 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 { id: item.id, drugName: drug.drug_name || '-', imageUrl: resolveDrugImage(drug.image), - buyPrice: formatMoney(item.buy_price), + buyPrice: formatMoney(supplyRaw), price: formatMoney(item.price), suggestPrice: formatMoney(suggestPrice), stock: item.stock != null ? item.stock : '-', diff --git a/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js b/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js new file mode 100644 index 0000000..8eef72a --- /dev/null +++ b/subPackages/sub_business_shared/common/orderPriceAdjustMixin.js @@ -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' + diff --git a/subPackages/sub_business_shared/common/pricePercentAdjust.js b/subPackages/sub_business_shared/common/pricePercentAdjust.js new file mode 100644 index 0000000..e014bf9 --- /dev/null +++ b/subPackages/sub_business_shared/common/pricePercentAdjust.js @@ -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) +} diff --git a/subPackages/sub_business_shared/components/DrawerPageContainer.vue b/subPackages/sub_business_shared/components/DrawerPageContainer.vue new file mode 100644 index 0000000..0f23d68 --- /dev/null +++ b/subPackages/sub_business_shared/components/DrawerPageContainer.vue @@ -0,0 +1,94 @@ + + + + + diff --git a/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue b/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue new file mode 100644 index 0000000..97150da --- /dev/null +++ b/subPackages/sub_business_shared/components/OrderPricePercentAdjustPopup.vue @@ -0,0 +1,128 @@ + + + + + diff --git a/subPackages/sub_business_shared/order/components/ErpSyncPopup.vue b/subPackages/sub_business_shared/order/components/ErpSyncPopup.vue new file mode 100644 index 0000000..2f7d7e0 --- /dev/null +++ b/subPackages/sub_business_shared/order/components/ErpSyncPopup.vue @@ -0,0 +1,175 @@ + + + + + diff --git a/subPackages/sub_business_shared/order/components/ExpressCompanyPicker.vue b/subPackages/sub_business_shared/order/components/ExpressCompanyPicker.vue new file mode 100644 index 0000000..7497c08 --- /dev/null +++ b/subPackages/sub_business_shared/order/components/ExpressCompanyPicker.vue @@ -0,0 +1,186 @@ + + + + + diff --git a/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue b/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue index 69cb2c5..37aa029 100644 --- a/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue +++ b/subPackages/sub_business_shared/order/components/PrescriptionDetailPopup.vue @@ -1,5 +1,13 @@ + + diff --git a/subPackages/sub_business_shared/order/components/ShipOrderPopup.vue b/subPackages/sub_business_shared/order/components/ShipOrderPopup.vue new file mode 100644 index 0000000..4b146f4 --- /dev/null +++ b/subPackages/sub_business_shared/order/components/ShipOrderPopup.vue @@ -0,0 +1,166 @@ +