From 27202c07403221f1d0f06832718b22a288f89a16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E7=90=A6?= Date: Sat, 30 May 2026 13:53:14 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E4=BC=98=E5=8C=96=E4=BA=86=E4=B8=80?= =?UTF-8?q?=E4=BA=9B=E4=BA=A4=E4=BA=92=EF=BC=8C=E4=B8=AD=E8=8D=AF=E8=8D=AF?= =?UTF-8?q?=E5=93=81=E7=BC=96=E8=BE=91=E4=BA=A4=E4=BA=92=202.=20dev?= =?UTF-8?q?=E5=92=8C=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83=E7=BB=9F=E4=B8=80?= =?UTF-8?q?=E9=89=B4=E5=88=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/clinicAdmin.js | 152 ++++ common/js/image-compress-modal.js | 23 + common/js/image-compress.js | 95 +++ common/js/select-avatar-image.js | 60 ++ .../image-compress-popup.vue | 676 +++++++++++++++ .../sub_clinic_admin/common/display.js | 513 +++++++++++ .../sub_clinic_admin/common/filterCache.js | 121 +++ subPackages/sub_clinic_admin/common/format.js | 7 + .../sub_clinic_admin/common/orderParams.js | 32 + .../common/reconciliationParams.js | 70 ++ subPackages/sub_clinic_admin/common/tabbar.js | 28 + .../components/CollapsibleFilterPanel.vue | 80 ++ .../components/PageStickyHeader.vue | 21 + .../components/clinic-page-layout.vue | 81 ++ subPackages/sub_clinic_admin/home/index.vue | 1 + subPackages/sub_clinic_admin/my/index.vue | 212 +++++ .../order/components/ExpressTimeline.vue | 168 ++++ .../components/PrescriptionDetailPopup.vue | 591 +++++++++++++ subPackages/sub_clinic_admin/order/detail.vue | 558 ++++++++++++ subPackages/sub_clinic_admin/order/index.vue | 582 +++++++++++++ .../sub_clinic_admin/reconciliation/index.vue | 799 ++++++++++++++++++ .../warehouse/components/PriceEditModal.vue | 130 +++ .../sub_clinic_admin/warehouse/detail.vue | 135 +++ .../sub_clinic_admin/warehouse/index.vue | 231 +++++ .../sub_clinic_admin/withdrawal/apply.vue | 103 +++ .../sub_clinic_admin/withdrawal/card-edit.vue | 143 ++++ .../sub_clinic_admin/withdrawal/index.vue | 415 +++++++++ .../withdrawal/record-detail.vue | 170 ++++ .../withdrawal/settlement/detail.vue | 196 +++++ .../withdrawal/settlement/index.vue | 194 +++++ utils/wxListKey.js | 23 + 31 files changed, 6610 insertions(+) create mode 100644 api/clinicAdmin.js create mode 100644 common/js/image-compress-modal.js create mode 100644 common/js/image-compress.js create mode 100644 common/js/select-avatar-image.js create mode 100644 components/image-compress-popup/image-compress-popup.vue create mode 100644 subPackages/sub_clinic_admin/common/display.js create mode 100644 subPackages/sub_clinic_admin/common/filterCache.js create mode 100644 subPackages/sub_clinic_admin/common/format.js create mode 100644 subPackages/sub_clinic_admin/common/orderParams.js create mode 100644 subPackages/sub_clinic_admin/common/reconciliationParams.js create mode 100644 subPackages/sub_clinic_admin/common/tabbar.js create mode 100644 subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue create mode 100644 subPackages/sub_clinic_admin/components/PageStickyHeader.vue create mode 100644 subPackages/sub_clinic_admin/components/clinic-page-layout.vue create mode 100644 subPackages/sub_clinic_admin/home/index.vue create mode 100644 subPackages/sub_clinic_admin/my/index.vue create mode 100644 subPackages/sub_clinic_admin/order/components/ExpressTimeline.vue create mode 100644 subPackages/sub_clinic_admin/order/components/PrescriptionDetailPopup.vue create mode 100644 subPackages/sub_clinic_admin/order/detail.vue create mode 100644 subPackages/sub_clinic_admin/order/index.vue create mode 100644 subPackages/sub_clinic_admin/reconciliation/index.vue create mode 100644 subPackages/sub_clinic_admin/warehouse/components/PriceEditModal.vue create mode 100644 subPackages/sub_clinic_admin/warehouse/detail.vue create mode 100644 subPackages/sub_clinic_admin/warehouse/index.vue create mode 100644 subPackages/sub_clinic_admin/withdrawal/apply.vue create mode 100644 subPackages/sub_clinic_admin/withdrawal/card-edit.vue create mode 100644 subPackages/sub_clinic_admin/withdrawal/index.vue create mode 100644 subPackages/sub_clinic_admin/withdrawal/record-detail.vue create mode 100644 subPackages/sub_clinic_admin/withdrawal/settlement/detail.vue create mode 100644 subPackages/sub_clinic_admin/withdrawal/settlement/index.vue create mode 100644 utils/wxListKey.js diff --git a/api/clinicAdmin.js b/api/clinicAdmin.js new file mode 100644 index 0000000..fffc254 --- /dev/null +++ b/api/clinicAdmin.js @@ -0,0 +1,152 @@ +import { req } from '@/common/js/index.js'; + +const PREFIX = '/newApi/clinic-admin'; + +/** 解析 Laravel jok 或 Yii 响应 */ +export function unwrapClinicRes(res) { + if (!res) return null; + if (res.code === 0 && res.result !== undefined) return res.result; + if (res.errcode != null && res.errcode !== -1) return res.data; + if (res.result !== undefined) return res.result; + return res.data != null ? res.data : res; +} + +function clinicRequest(options) { + return req.request({ + ...options, + header: { + ...(options.header || {}), + _clinicAdmin: '1', + }, + }).then((res) => { + const data = unwrapClinicRes(res); + const ok = res && (res.code === 0 || (res.errcode != null && Number(res.errcode) !== -1)); + return { res, data, ok }; + }); +} + +export function sendClinicAdminCode(phone) { + return req.request({ + url: '/newApi/clinic-admin-auth/send-verification-code', + method: 'POST', + data: { account: phone }, + header: { isToken: false, _clinicAdmin: '1' }, + }); +} + +export function clinicAdminLogin(phone, password, code) { + return req.request({ + url: '/newApi/clinic-admin-auth/login', + method: 'POST', + data: { account: phone, password, code }, + header: { isToken: false, _clinicAdmin: '1' }, + }); +} + +export function getClinicAdminMyInfo() { + return clinicRequest({ url: `${PREFIX}-auth/my-info`, method: 'GET' }); +} + +export function getClinicAdminBalance() { + return clinicRequest({ url: `${PREFIX}-account/my-balance`, method: 'GET' }); +} + +export function getClinicAdminCard() { + return clinicRequest({ url: `${PREFIX}-account/my-card`, method: 'GET' }); +} + +export function saveClinicAdminCard(data) { + return clinicRequest({ url: `${PREFIX}-account/save-card`, method: 'POST', data }); +} + +export function getLedgerLogList(params) { + return clinicRequest({ url: `${PREFIX}-settlement/ledger-log-list`, method: 'GET', data: params }); +} + +export function getLedgerDetail(params) { + return clinicRequest({ url: `${PREFIX}-settlement/ledger-detail`, method: 'GET', data: params }); +} + +export function getWithdrawalList(params) { + return clinicRequest({ url: `${PREFIX}-withdrawal/list`, method: 'GET', data: params }); +} + +export function getWithdrawalDetail(id) { + return clinicRequest({ url: `${PREFIX}-withdrawal/detail`, method: 'GET', data: { id } }); +} + +export function submitWithdrawal(data) { + return clinicRequest({ url: `${PREFIX}-withdrawal/withdrawal`, method: 'POST', data }); +} + +export function getReconciliationList(params) { + return clinicRequest({ url: `${PREFIX}-reconciliation/list`, method: 'GET', data: params }); +} + +export function getReconciliationDrugList(params) { + return clinicRequest({ url: `${PREFIX}-reconciliation/drug-list`, method: 'GET', data: params }); +} + +export function getReconciliationDrugOptions(params) { + return clinicRequest({ url: `${PREFIX}-reconciliation/drug-options`, method: 'GET', data: params }); +} + +export function getReconciliationOrderOptions(params) { + return clinicRequest({ url: `${PREFIX}-reconciliation/order-options`, method: 'GET', data: params }); +} + +export function getOrderSaleAmount(params) { + return clinicRequest({ url: `${PREFIX}-order/sale-amount`, method: 'GET', data: params }); +} + +export function getPrescriptionDetail(id) { + return clinicRequest({ url: `${PREFIX}-prescription/detail`, method: 'GET', data: { id } }); +} + +export function getOrderList(params) { + return clinicRequest({ url: `${PREFIX}-order/list`, method: 'GET', data: params }); +} + +export function getOrderStatusOption() { + return clinicRequest({ url: `${PREFIX}-order/status-option`, method: 'GET' }); +} + +export function getOrderDetail(id) { + return clinicRequest({ url: `${PREFIX}-order/detail`, method: 'GET', data: { id } }); +} + +export function refundOrder(data) { + return clinicRequest({ url: `${PREFIX}-order/refund`, method: 'POST', data }); +} + +export function wareSendOrder(data) { + return clinicRequest({ url: `${PREFIX}-order/ware-send`, method: 'POST', data }); +} + +export function getExpressDetailByOrderId(orderId) { + return clinicRequest({ + url: `${PREFIX}-express-detail/detail-by-order`, + method: 'POST', + data: { order_id: orderId }, + }); +} + +export function getWarehouseList(params) { + return clinicRequest({ url: `${PREFIX}-warehouse/list`, method: 'GET', data: params }); +} + +export function getWarehouseDetail(id) { + return clinicRequest({ url: `${PREFIX}-warehouse/detail`, method: 'GET', data: { id } }); +} + +export function updateWarehouseStatus(id) { + return clinicRequest({ url: `${PREFIX}-warehouse/update-status`, method: 'GET', data: { id } }); +} + +export function updateWarehousePrice(data) { + return clinicRequest({ url: `${PREFIX}-warehouse/update`, method: 'POST', data }); +} + +export function getWarehouseProductTypeOptions() { + return clinicRequest({ url: `${PREFIX}-warehouse/product-type-options`, method: 'GET' }); +} diff --git a/common/js/image-compress-modal.js b/common/js/image-compress-modal.js new file mode 100644 index 0000000..2652de6 --- /dev/null +++ b/common/js/image-compress-modal.js @@ -0,0 +1,23 @@ +let popupInstance = null; + +export function registerImageCompressPopup(instance) { + popupInstance = instance; +} + +export function openImageCompressModal({ path, size }) { + if (!popupInstance || typeof popupInstance.open !== 'function') { + console.warn('image-compress-popup 未注册,将直接使用原图'); + return Promise.resolve({ + path, + size, + usedCompress: false, + }); + } + + return popupInstance.open({ path, size }).then((result) => { + if (!result) { + return null; + } + return result; + }); +} diff --git a/common/js/image-compress.js b/common/js/image-compress.js new file mode 100644 index 0000000..4891080 --- /dev/null +++ b/common/js/image-compress.js @@ -0,0 +1,95 @@ +import { openImageCompressModal } from '@/common/js/image-compress-modal.js'; + +export const IMAGE_COMPRESS_THRESHOLD = 1 * 1024 * 1024; + +export function formatFileSize(bytes) { + if (bytes === 0) return '0 B'; + const k = 1024; + const sizes = ['B', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + const value = bytes / k ** i; + return `${Number.parseFloat(value.toFixed(2))} ${sizes[i]}`; +} + +/** 压缩节省比例文案,如「(约节省 42%)」 */ +export function formatSavingsPercent(originalBytes, compressedBytes) { + if (originalBytes <= 0 || compressedBytes >= originalBytes) { + return ''; + } + const percent = Math.round( + ((originalBytes - compressedBytes) / originalBytes) * 100, + ); + return percent > 0 ? `(约节省 ${percent}%)` : ''; +} + +function getFileInfo(filePath) { + return new Promise((resolve, reject) => { + uni.getFileInfo({ + filePath, + success: resolve, + fail: reject, + }); + }); +} + +export function compressImagePath(src, quality) { + return new Promise((resolve, reject) => { + uni.compressImage({ + src, + quality, + success: async (res) => { + try { + const info = await getFileInfo(res.tempFilePath); + resolve({ + path: res.tempFilePath, + size: info.size, + }); + } catch (error) { + reject(error); + } + }, + fail: reject, + }); + }); +} + +export async function tryCompressImage( + src, + qualities = [80, 60, 40], + onProgress, +) { + let result = null; + for (const quality of qualities) { + onProgress && onProgress(quality); + result = await compressImagePath(src, quality); + if (result.size <= IMAGE_COMPRESS_THRESHOLD) { + break; + } + } + return result; +} + +export function isGifPath(path) { + return /\.gif$/i.test(path || ''); +} + +export async function prepareImagePath({ path, size }) { + if (size <= IMAGE_COMPRESS_THRESHOLD) { + return { + path, + size, + usedCompress: false, + }; + } + + const result = await openImageCompressModal({ path, size }); + if (!result) { + return null; + } + + return { + path: result.path, + size: result.size, + usedCompress: !!result.usedCompress, + }; +} diff --git a/common/js/select-avatar-image.js b/common/js/select-avatar-image.js new file mode 100644 index 0000000..ad53602 --- /dev/null +++ b/common/js/select-avatar-image.js @@ -0,0 +1,60 @@ +import { prepareImagePath } from '@/common/js/image-compress.js'; + +export function chooseAvatarImage(options = {}) { + const { + uploadUrl = 'https://api.xiaokang88.com/open-api/upload/old-admin-img', + formData = { store_id: '11001' }, + onSuccess, + onFail, + onCancel, + } = options; + + uni.chooseImage({ + count: 1, + sizeType: ['original', 'compressed'], + sourceType: ['album', 'camera'], + success: async (res) => { + uni.showLoading({ title: '请稍后...' }); + + try { + const tempFile = res.tempFiles[0]; + const prepared = await prepareImagePath({ + path: tempFile.path, + size: tempFile.size, + }); + + if (!prepared) { + uni.hideLoading(); + onCancel && onCancel(); + return; + } + + uni.uploadFile({ + url: uploadUrl, + filePath: prepared.path, + name: 'file', + header: { + authorization: `Bearer ${uni.getStorageSync('token')}`, + }, + formData, + success: (uploadRes) => { + uni.hideLoading(); + onSuccess && onSuccess(uploadRes); + }, + fail: (error) => { + uni.hideLoading(); + onFail && onFail(error); + }, + }); + } catch (error) { + uni.hideLoading(); + onFail && onFail(error); + } + }, + complete: (res) => { + if (res.errMsg === 'chooseImage:fail cancel') { + onCancel && onCancel(); + } + }, + }); +} diff --git a/components/image-compress-popup/image-compress-popup.vue b/components/image-compress-popup/image-compress-popup.vue new file mode 100644 index 0000000..a41b13e --- /dev/null +++ b/components/image-compress-popup/image-compress-popup.vue @@ -0,0 +1,676 @@ + + + + + + + + + + diff --git a/subPackages/sub_clinic_admin/common/display.js b/subPackages/sub_clinic_admin/common/display.js new file mode 100644 index 0000000..cc05166 --- /dev/null +++ b/subPackages/sub_clinic_admin/common/display.js @@ -0,0 +1,513 @@ +import { formatMoney } from './format.js' + +const DEFAULT_DRUG_IMAGE = + 'https://xkyp-web2.obs.cn-south-1.myhuaweicloud.com/buy/public/drug_no_mage.png' + +/** 兼容 int 时间戳 / datetime 字符串 */ +export function formatDateTime(v) { + if (v === null || v === undefined || v === '') return '-' + if (typeof v === 'number') { + const ms = v < 1e12 ? v * 1000 : v + const d = new Date(ms) + if (Number.isNaN(d.getTime())) return '-' + return d.toLocaleString() + } + const s = String(v) + if (s === '2021-01-01 00:00:00') return '-' + return s +} + +export function withdrawalCheckStatusText(s) { + const m = { 1: '待审核', 2: '审核成功', 3: '拒绝', 4: '提现失败' } + return m[s] || '-' +} + +export function dakuanStatusText(s) { + if (s === -1) return '打款失败' + if (s === 0) return '处理中' + if (s === 1) return '打款成功' + return '-' +} + +export function isWithdrawalRejected(item) { + return item && Number(item.check_status) === 3 +} + +/** 提现列表行展示(yii_cash_apply) */ +export function mapWithdrawalRow(item) { + if (!item) return {} + const checkStatus = item.check_status + return { + id: item.id, + orderNo: item.order_no || '-', + applyCash: formatMoney(item.apply_cash), + trueCash: formatMoney(item.true_cash), + chargeCash: formatMoney(item.charge_cash), + applyTime: formatDateTime(item.apply_time), + checkStatusText: item.check_status_txt || withdrawalCheckStatusText(checkStatus), + checkResult: item.check_result || '', + isRejected: isWithdrawalRejected(item), + dakuanStatusText: item.dakuan_status_txt || dakuanStatusText(item.dakuan_status), + dakuanTime: formatDateTime(item.dakuan_time), + _raw: item, + } +} + +/** 结算记录列表行(yii_ledger_log) */ +export function mapSettlementRow(item) { + if (!item) return {} + return { + id: item.id, + orderNo: item.order_no || (item.order_id ? '订单#' + item.order_id : '-'), + feeTypeText: item.fee_type_txt || '-', + amountText: formatMoney(item.amount), + amountClass: ledgerTypeClass(item), + typeText: item.type_txt || '-', + content: item.content || '', + createdAt: formatDateTime(item.created_at), + _raw: item, + } +} + +export function ledgerTypeClass(item) { + return item && item.type === 1 ? 'inc' : 'dec' +} + +/** 商品订单状态(对齐 PC product-order) */ +export function orderStatusText(status) { + const m = { + 0: '待支付', + 1: '待发货', + 2: '待收货', + 3: '待评价', + 4: '已退款', + 5: '退款中', + 6: '已收货', + 7: '确认收货', + 8: '拒绝退款', + 9: '已取消', + } + return m[status] != null ? m[status] : '-' +} + +export function deliveryMethodText(dm) { + if (dm === 0) return '快递到家' + if (dm === 1) return '药店自提' + return '-' +} + +export function orderOnlineText(isOnline) { + if (isOnline === 1) return '在线问诊' + if (isOnline === 2) return '在线复诊' + return '线下就诊' +} + +export function prescriptionTypeText(pt) { + const m = { + 1: '中药', + 2: '西药', + 3: '保健食品', + 5: '产品服务包', + 6: '非药品', + 7: '医疗器械', + } + return m[pt] != null ? m[pt] : '其他' +} + +export function orderTypeText(ot) { + const m = { + 1: '处方订单', + 2: '预约购药订单', + 3: '商城处方订单', + } + return m[ot] != null ? m[ot] : '其他' +} + +export function erpSyncText(v) { + if (v === 1) return '已同步' + return '未同步' +} + +export function freeShippingText(v) { + if (v === 1) return '包邮' + return '不包邮' +} + +function pickReceiverMobile(item) { + const addr = item.address + if (addr && typeof addr === 'object' && addr.mobile) return addr.mobile + return item.patient_mobile || '' +} + +function buildProductSummary(items, prescriptionType, max = 2) { + if (!Array.isArray(items) || !items.length) return '' + return items.slice(0, max).map(p => formatOrderProductLine(p, prescriptionType)).join(';') +} + +function pickAddrName(item) { + const addr = item.address + if (addr && typeof addr === 'object' && addr.name) return addr.name + if (typeof addr === 'string') { + try { + const o = JSON.parse(addr) + return o.name || '' + } catch (e) { + return '' + } + } + return item.patient || item.express_name || '-' +} + +/** 订单列表卡片 */ +export function mapOrderListRow(item) { + if (!item) return {} + const store = item.store || {} + const user = item.user || {} + const pt = item.prescription_type + return { + id: item.id, + orderNo: item.order_no || '-', + totalPayPrice: formatMoney(item.total_pay_price), + itemsPrice: formatMoney(item.items_price), + transExpenses: formatMoney(item.trans_expenses), + registerPrice: item.register_id != null && item.register_price != null + ? formatMoney(item.register_price) + : '', + registerOrderNo: item.register_order_no || '', + statusText: orderStatusText(item.status), + statusClass: 'tag-status', + prescriptionTypeText: prescriptionTypeText(pt), + prescriptionTypeClass: 'tag-type', + orderTypeText: orderTypeText(item.order_type), + onlineText: orderOnlineText(item.is_online), + erpStatusText: erpSyncText(item.is_sync_erp), + freeShippingText: freeShippingText(item.is_free_shipping), + receiverName: pickAddrName(item), + receiverMobile: pickReceiverMobile(item), + deliveryText: deliveryMethodText(item.delivery_method), + deliveryClass: 'tag-delivery', + userNickname: user.nickname || '-', + payTime: formatDateTime(item.pay_time), + createdAt: formatDateTime(item.created_at), + storeName: store.name || '-', + productSummary: buildProductSummary(item.product_order_items, pt), + pId: item.p_id, + canViewPrescription: !!(item.p_id && item.order_type !== 2 && item.order_type !== 3), + _raw: item, + } +} + +/** 物流详情(对齐 PC express-detail/detail-by-order) */ +export function mapExpressDetail(data) { + if (!data || typeof data !== 'object') { + return { hasData: false, companyName: '', expressNo: '', stateText: '', tracks: [] } + } + const rawDetail = data.detail + const tracks = Array.isArray(rawDetail) + ? rawDetail.map(d => ({ + status: d.status || '-', + time: d.detail_at || '', + detail: d.detail || '', + })) + : [] + return { + hasData: !!(data.express_no || data.express_company_name || tracks.length), + companyName: data.express_company_name || '-', + expressNo: data.express_no || '-', + stateText: data.state_txt || '-', + tracks, + } +} + +/** 订单详情商品行 */ +export function mapOrderProductItem(p, prescriptionType) { + if (!p) return {} + const drug = p.drug || {} + const sourceInfo = drug.source_info || {} + const manufacturer = sourceInfo.name || drug.source || '暂无' + const dosage = p.dosage > 0 ? p.dosage : 1 + const qty = prescriptionType === 1 + ? '*' + ((p.number != null ? p.number : 0) * dosage) + : '×' + (p.number != null ? p.number : 0) + const isTcm = prescriptionType === 1 + return { + drugName: p.drug_name || drug.drug_name || '药品', + drugNumber: drug.drug_number || '', + imageUrl: resolveDrugImage(p.drug_image || drug.image), + specification: drug.specification || (isTcm ? 'g' : '暂无'), + manufacturer, + qty, + price: isTcm + ? (p.price != null && p.price !== '' ? p.price + ' 元/g' : '—') + : formatMoney(p.price), + buyPrice: p.buy_price != null && p.buy_price !== '' + ? p.buy_price + ' 元/g' + : '—', + isTcm, + } +} + +/** 订单详情处方块 */ +export function mapOrderPrescription(info) { + const pres = info && info.prescription + const content = pres && pres.content + if (!content || info.order_type === 2 || info.order_type === 3) return null + const doctor = content.doctor || {} + const depart = doctor.depart || {} + const title = doctor.title || {} + return { + prescriptionNo: content.prescription_no || '-', + diagnose: content.clinical_diagnose || '-', + doctorOrder: content.doctor_order || '-', + doctorName: doctor.name || '-', + departName: depart.name || '-', + titleName: title.name || '-', + } +} + +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) + return { + orderTypeText: orderTypeText(info.order_type), + marketPrice: formatMoney(info.market_price), + freeShippingText: freeShip ? '是' : '否', + cancelRemark: info.cancel_remark || '无', + showPrescription: mapOrderPrescription(info), + prescriptionId: info.p_id, + canViewPrescription: canView, + statusText: orderStatusText(info.status), + prescriptionTypeText: prescriptionTypeText(info.prescription_type), + deliveryText: deliveryMethodText(info.delivery_method), + } +} + +function parseRecipeContent(raw) { + if (!raw) return null + if (typeof raw === 'object') return raw + try { + return JSON.parse(raw) + } catch (e) { + return null + } +} + +/** 完整处方详情(对齐 PC PrescriptionDetail.vue) */ +export function mapPrescriptionDetailView(item) { + if (!item) return null + const content = item.content || {} + const patient = content.patient || {} + const doctor = content.doctor || {} + const depart = doctor.depart || {} + const title = doctor.title || {} + const pt = item.prescription_type + const recipes = [] + const repice = content.repice || content.recipe || [] + const list = Array.isArray(repice) ? repice : [] + + if (pt === 1) { + list.forEach((recipe) => { + const drugs = parseRecipeContent(recipe.content) + const drugLines = Array.isArray(drugs) + ? drugs.map(d => { + const name = d.name || d.drug_name || '—' + const num = d.number != null ? d.number : '' + const way = (d.use_way && d.use_way.name) || d.useWay || '煎服' + return { name, qty: num ? num + ' /g' : '', usage: way } + }) + : [] + recipes.push({ + type: 'tcm', + drugs: drugLines, + usage: recipe.consumption != null && recipe.dosage != null + ? `每日${recipe.consumption}次,共${recipe.dosage}剂` + : '', + process: [recipe.process_rule_note, recipe.process_rule].filter(Boolean).join(','), + }) + }) + } else { + list.forEach((recipe) => { + const d = parseRecipeContent(recipe.content) + if (!d) return + const unitName = (d.unit && d.unit.name) || d.unit || '' + recipes.push({ + type: 'west', + drugs: [{ + name: d.name || d.drug_name || '—', + qty: (d.number != null ? d.number : '') + unitName, + usage: d.useWay || (d.use_way && d.use_way.name) || '', + }], + usage: '', + process: '', + }) + }) + } + + const tcm = item.online_tcm_print || {} + return { + prescriptionNo: item.prescription_no || content.prescription_no || '-', + createdAt: item.created_at || '-', + patientName: patient.name || '-', + patientSex: patient.sex === 1 ? '男' : patient.sex === 2 ? '女' : '-', + patientAge: patient.age != null ? String(patient.age) : '-', + category: content.category || '-', + diagnose: content.clinical_diagnose || '-', + doctorOrder: content.doctor_order || '-', + doctorName: doctor.name || '-', + departName: depart.name || '-', + titleName: title.name || '-', + tcmSyndrome: tcm.show && tcm.tcm_syndrome ? tcm.tcm_syndrome : '', + tcmMethod: tcm.show && tcm.tcm_method ? tcm.tcm_method : '', + tcmDisease: tcm.show && tcm.tcm_disease ? tcm.tcm_disease : '', + recipes, + prescriptionType: pt, + } +} + +/** 订单商品行展示 */ +export function formatOrderProductLine(p, prescriptionType) { + if (!p) return '-' + const name = p.drug_name || (p.drug && p.drug.drug_name) || '药品' + const num = p.number != null ? p.number : '' + if (prescriptionType === 1) { + const dosage = p.dosage > 0 ? p.dosage : 1 + return name + ' ×' + (num * dosage) + } + return name + ' ×' + num +} + +export function resolveDrugImage(url) { + if (!url) return DEFAULT_DRUG_IMAGE + const s = String(url) + if (s.indexOf('http://') === 0 || s.indexOf('https://') === 0) return s + return DEFAULT_DRUG_IMAGE +} + +/** 仓库 1=下架 2=上架 */ +export function warehouseStatusText(status) { + if (status === 2) return '上架' + if (status === 1) return '下架' + return '-' +} + +export function mapWarehouseRow(item) { + if (!item) return {} + const drug = item.drug || {} + const storeDrug = drug.drug_store_drug || {} + const suggestPrice = storeDrug.price != null ? storeDrug.price : drug.price + return { + id: item.id, + drugName: drug.drug_name || '-', + imageUrl: resolveDrugImage(drug.image), + buyPrice: formatMoney(item.buy_price), + price: formatMoney(item.price), + suggestPrice: formatMoney(suggestPrice), + stock: item.stock != null ? item.stock : '-', + statusText: warehouseStatusText(item.status), + isOnShelf: item.status === 2, + pinyin: drug.pinyin || '-', + _raw: item, + } +} + +function mapReconciliationCategory(rec, prefix) { + const sales = rec[prefix + '_sales'] + const salesPrice = rec[prefix + '_sales_price'] + const supplyPrice = rec[prefix + '_supply_price'] + return { + sales: sales != null ? String(sales) : '-', + salesPrice: formatMoney(salesPrice), + supplyPrice: formatMoney(supplyPrice), + } +} + +export function mapReconciliationStoreRow(item) { + if (!item) return {} + const rec = item.reconciliation || {} + return { + id: item.id, + name: item.name || '-', + totalSales: formatMoney(rec.total_sales_price), + totalSupply: formatMoney(rec.total_supply_price), + herbalSalesPrice: formatMoney(rec.herbal_sales_price), + medicineSalesPrice: formatMoney(rec.medicine_sales_price), + registrationPrice: formatMoney(rec.registration_price), + herbal: mapReconciliationCategory(rec, 'herbal'), + medicine: mapReconciliationCategory(rec, 'medicine'), + servicePackage: mapReconciliationCategory(rec, 'service_package'), + otherFees: { + express: formatMoney(rec.express_price), + process: formatMoney(rec.process_price), + treatment: formatMoney(rec.treatment_price), + }, + _raw: item, + } +} + +export function mapReconciliationDrugRow(item) { + if (!item) return {} + return { + drugId: item.drug_id, + drugName: item.drug_name || '-', + drugNumber: item.drug_number || '', + typeText: item.type_txt || '-', + number: item.number != null ? item.number : '-', + marketPrice: formatMoney(item.market_price), + totalSales: formatMoney(item.total_sales_price), + totalSupply: formatMoney(item.total_supply_price), + orderNo: item.order_no || '', + _raw: item, + } +} + +export function formatPricePlain(v) { + if (v === null || v === undefined || v === '') return '-' + return '¥' + Number(v).toFixed(2) +} + +/** 汇总项:隐藏全零噪音 */ +export function filterReconciliationStatItems(items) { + return items.filter(s => { + if (s.key === 'total_sales_price' || s.key === 'total_supply_price') return true + if (s.isCount) { + const n = Number(s.raw) + return !Number.isNaN(n) && n !== 0 + } + const v = s.raw + if (v === null || v === undefined || v === '') return false + const n = Number(v) + return !Number.isNaN(n) && n !== 0 + }) +} + +/** 诊所对账汇总(对齐 PC setStoreStatistics) */ +export function mapReconciliationStoreCount(count) { + if (!count) return [] + const items = [ + { key: 'total_sales_price', label: '总销售金额', value: formatPricePlain(count.total_sales_price), raw: count.total_sales_price }, + { key: 'total_supply_price', label: '总供货金额', value: formatPricePlain(count.total_supply_price), raw: count.total_supply_price }, + { key: 'herbal_sales', label: '草药销量', value: count.herbal_sales != null ? String(count.herbal_sales) : '-', raw: count.herbal_sales, isCount: true }, + { key: 'herbal_sales_price', label: '草药销售金额', value: formatPricePlain(count.herbal_sales_price), raw: count.herbal_sales_price }, + { key: 'herbal_supply_price', label: '草药供货金额', value: formatPricePlain(count.herbal_supply_price), raw: count.herbal_supply_price }, + { key: 'medicine_sales', label: '成药销量', value: count.medicine_sales != null ? String(count.medicine_sales) : '-', raw: count.medicine_sales, isCount: true }, + { key: 'medicine_sales_price', label: '成药销售金额', value: formatPricePlain(count.medicine_sales_price), raw: count.medicine_sales_price }, + { key: 'medicine_supply_price', label: '成药供货金额', value: formatPricePlain(count.medicine_supply_price), raw: count.medicine_supply_price }, + { key: 'express_price', label: '快递费', value: formatPricePlain(count.express_price), raw: count.express_price }, + { key: 'process_price', label: '加工费', value: formatPricePlain(count.process_price), raw: count.process_price }, + { key: 'treatment_price', label: '诊疗费', value: formatPricePlain(count.treatment_price), raw: count.treatment_price }, + { key: 'outpatient_price', label: '门诊收入', value: formatPricePlain(count.outpatient_price), raw: count.outpatient_price }, + { key: 'registration_price', label: '挂号费', value: formatPricePlain(count.registration_price), raw: count.registration_price }, + ] + return filterReconciliationStatItems(items) +} + +/** 药品对账汇总(对齐 PC setDrugStatistics) */ +export function mapReconciliationDrugCount(count) { + if (!count) return [] + return [ + { key: 'total_sales_price', label: '总销售金额', value: formatPricePlain(count.total_sales_price) }, + { key: 'total_supply_price', label: '总供货金额', value: formatPricePlain(count.total_supply_price) }, + { key: 'number', label: '销量', value: count.number != null ? String(count.number) : '-' }, + ] +} diff --git a/subPackages/sub_clinic_admin/common/filterCache.js b/subPackages/sub_clinic_admin/common/filterCache.js new file mode 100644 index 0000000..328336b --- /dev/null +++ b/subPackages/sub_clinic_admin/common/filterCache.js @@ -0,0 +1,121 @@ +/** + * 诊所端列表筛选条件本地缓存(对齐 PC reconciliation formCache) + */ + +import { + defaultMonthStartDate, + defaultTodayDate, +} from './reconciliationParams.js' + +const KEY_ORDER = 'clinic_admin.order.filter' +const KEY_RECONCILIATION = 'clinic_admin.reconciliation.filter' + +function readJson(key) { + try { + const raw = uni.getStorageSync(key) + if (!raw) return null + return typeof raw === 'string' ? JSON.parse(raw) : raw + } catch (e) { + return null + } +} + +function writeJson(key, data) { + uni.setStorageSync(key, JSON.stringify(data)) +} + +export function defaultOrderFilter() { + return { + orderNo: '', + statusIndex: 0, + deliveryIndex: 0, + prescriptionTypeIndex: 0, + dateStart: defaultMonthStartDate(), + dateEnd: defaultTodayDate(), + } +} + +export function loadOrderFilter() { + const cached = readJson(KEY_ORDER) + if (!cached || typeof cached !== 'object') return defaultOrderFilter() + const d = defaultOrderFilter() + return { + orderNo: cached.orderNo != null ? String(cached.orderNo) : d.orderNo, + statusIndex: Number(cached.statusIndex) || 0, + deliveryIndex: Number(cached.deliveryIndex) || 0, + prescriptionTypeIndex: Number(cached.prescriptionTypeIndex) || 0, + dateStart: cached.dateStart || d.dateStart, + dateEnd: cached.dateEnd || d.dateEnd, + } +} + +export function saveOrderFilter(data) { + writeJson(KEY_ORDER, { + orderNo: data.orderNo || '', + statusIndex: data.statusIndex, + deliveryIndex: data.deliveryIndex, + prescriptionTypeIndex: data.prescriptionTypeIndex, + dateStart: data.dateStart, + dateEnd: data.dateEnd, + }) +} + +export function clearOrderFilter() { + try { + uni.removeStorageSync(KEY_ORDER) + } catch (e) { /* ignore */ } +} + +export function defaultReconciliationFilter() { + return { + tabIndex: 0, + dateStart: defaultMonthStartDate(), + dateEnd: defaultTodayDate(), + byOrder: false, + drugSearchQ: '', + orderSearchQ: '', + drugId: null, + orderId: null, + selectedDrugLabel: '', + selectedOrderLabel: '', + } +} + +export function loadReconciliationFilter() { + const cached = readJson(KEY_RECONCILIATION) + if (!cached || typeof cached !== 'object') return defaultReconciliationFilter() + const d = defaultReconciliationFilter() + return { + tabIndex: cached.tabIndex === 1 ? 1 : 0, + dateStart: cached.dateStart || d.dateStart, + dateEnd: cached.dateEnd || d.dateEnd, + byOrder: !!cached.byOrder, + drugSearchQ: cached.drugSearchQ != null ? String(cached.drugSearchQ) : '', + orderSearchQ: cached.orderSearchQ != null ? String(cached.orderSearchQ) : '', + drugId: cached.drugId != null && cached.drugId !== '' ? Number(cached.drugId) : null, + orderId: cached.orderId != null && cached.orderId !== '' ? Number(cached.orderId) : null, + selectedDrugLabel: cached.selectedDrugLabel || '', + selectedOrderLabel: cached.selectedOrderLabel || '', + } +} + +export function saveReconciliationFilter(data) { + writeJson(KEY_RECONCILIATION, { + tabIndex: data.tabIndex, + dateStart: data.dateStart, + dateEnd: data.dateEnd, + byOrder: data.byOrder, + drugSearchQ: data.drugSearchQ || '', + orderSearchQ: data.orderSearchQ || '', + drugId: data.drugId, + orderId: data.orderId, + selectedDrugLabel: data.selectedDrugLabel || '', + selectedOrderLabel: data.selectedOrderLabel || '', + }) +} + +export function clearReconciliationFilter() { + try { + uni.removeStorageSync(KEY_RECONCILIATION) + } catch (e) { /* ignore */ } +} diff --git a/subPackages/sub_clinic_admin/common/format.js b/subPackages/sub_clinic_admin/common/format.js new file mode 100644 index 0000000..52b3bc3 --- /dev/null +++ b/subPackages/sub_clinic_admin/common/format.js @@ -0,0 +1,7 @@ +/** 金额展示(WXML 模板用 methods 调用) */ +export function formatMoney(v) { + if (v === null || v === undefined || v === '' || v === '-') return '-' + const n = Number(v) + if (Number.isNaN(n)) return String(v) + return '¥' + n.toFixed(2) +} diff --git a/subPackages/sub_clinic_admin/common/orderParams.js b/subPackages/sub_clinic_admin/common/orderParams.js new file mode 100644 index 0000000..04daa6c --- /dev/null +++ b/subPackages/sub_clinic_admin/common/orderParams.js @@ -0,0 +1,32 @@ +/** + * 商品订单请求参数(对齐 PC product-order/config/search.ts) + */ + +import { buildSearchTimeQueryParams } from './reconciliationParams.js' + +export function buildOrderListParams({ + page = 1, + pageSize = 15, + orderNo = '', + status, + deliveryMethod, + prescriptionType, + dateStart, + dateEnd, +}) { + const params = { + page, + pageSize, + ...buildSearchTimeQueryParams(dateStart, dateEnd), + } + if (orderNo) params.order_no = orderNo + if (status !== '' && status != null) params.status = status + if (deliveryMethod !== '' && deliveryMethod != null) params.delivery_method = deliveryMethod + if (prescriptionType !== '' && prescriptionType != null) params.prescription_type = prescriptionType + return params +} + +export function buildOrderSaleAmountParams(opts) { + const { page, pageSize, ...rest } = opts || {} + return buildOrderListParams(rest) +} diff --git a/subPackages/sub_clinic_admin/common/reconciliationParams.js b/subPackages/sub_clinic_admin/common/reconciliationParams.js new file mode 100644 index 0000000..1ba5c6b --- /dev/null +++ b/subPackages/sub_clinic_admin/common/reconciliationParams.js @@ -0,0 +1,70 @@ +/** + * 对账请求参数(对齐 PC finance/reconciliation/index.vue) + */ + +export const STORE_LIST_PAGE_SIZE = 9999 + +/** Laravel GET 数组:search_time[0] / search_time[1](uni.request 勿传 JS 数组,否则会变成逗号字符串) */ +export function buildSearchTimeQueryParams(dateStart, dateEnd) { + return { + 'search_time[0]': dateStart, + 'search_time[1]': dateEnd, + } +} + +export function buildStoreListParams({ + page = 1, + pageSize = STORE_LIST_PAGE_SIZE, + dateStart, + dateEnd, + storeName = '', + excludeZeroSales = 0, +}) { + const params = { + page, + pageSize, + exclude_zero_sales: excludeZeroSales, + ...buildSearchTimeQueryParams(dateStart, dateEnd), + } + if (storeName) params.store_name = storeName + return params +} + +export function buildDrugListParams({ + dateStart, + dateEnd, + drugId, + orderId, + byOrder = false, + storeId, +}) { + const params = { + ...buildSearchTimeQueryParams(dateStart, dateEnd), + } + if (drugId) params.drug_id = drugId + if (orderId) params.order_id = orderId + if (byOrder) params.by_order = 1 + if (storeId != null) params.store_id = storeId + return params +} + +export function normalizeReconciliationResponse(data) { + const d = data || {} + const listWrap = d.list || {} + const raw = listWrap.items + return { + count: d.count || {}, + items: Array.isArray(raw) ? raw : [], + pageMeta: listWrap, + } +} + +/** 本月 1 日 YYYY-MM-DD */ +export function defaultMonthStartDate() { + const d = new Date() + return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-01` +} + +export function defaultTodayDate() { + return new Date().toISOString().slice(0, 10) +} diff --git a/subPackages/sub_clinic_admin/common/tabbar.js b/subPackages/sub_clinic_admin/common/tabbar.js new file mode 100644 index 0000000..389eb90 --- /dev/null +++ b/subPackages/sub_clinic_admin/common/tabbar.js @@ -0,0 +1,28 @@ +export const TAB_WORKBENCH = 0 +export const TAB_MY = 1 + +export const CLINIC_TAB_ROUTES = [ + '/subPackages/sub_clinic_admin/home/index', + '/subPackages/sub_clinic_admin/my/index', +] + +export function getClinicTabList() { + return [ + { + iconPath: require('@/static/image/w-sf.png'), + selectedIconPath: require('@/static/image/sf.png'), + text: '工作台', + }, + { + iconPath: require('@/static/image/w-wd.png'), + selectedIconPath: require('@/static/image/wd.png'), + text: '我的', + }, + ] +} + +export function switchClinicTab(index) { + const url = CLINIC_TAB_ROUTES[index] + if (!url) return + uni.reLaunch({ url }) +} diff --git a/subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue b/subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue new file mode 100644 index 0000000..7639983 --- /dev/null +++ b/subPackages/sub_clinic_admin/components/CollapsibleFilterPanel.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/subPackages/sub_clinic_admin/components/PageStickyHeader.vue b/subPackages/sub_clinic_admin/components/PageStickyHeader.vue new file mode 100644 index 0000000..bd38d94 --- /dev/null +++ b/subPackages/sub_clinic_admin/components/PageStickyHeader.vue @@ -0,0 +1,21 @@ + + + + + diff --git a/subPackages/sub_clinic_admin/components/clinic-page-layout.vue b/subPackages/sub_clinic_admin/components/clinic-page-layout.vue new file mode 100644 index 0000000..9066bde --- /dev/null +++ b/subPackages/sub_clinic_admin/components/clinic-page-layout.vue @@ -0,0 +1,81 @@ + + + + + diff --git a/subPackages/sub_clinic_admin/home/index.vue b/subPackages/sub_clinic_admin/home/index.vue new file mode 100644 index 0000000..c1f2a37 --- /dev/null +++ b/subPackages/sub_clinic_admin/home/index.vue @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/my/index.vue b/subPackages/sub_clinic_admin/my/index.vue new file mode 100644 index 0000000..de6c3e6 --- /dev/null +++ b/subPackages/sub_clinic_admin/my/index.vue @@ -0,0 +1,212 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/order/components/ExpressTimeline.vue b/subPackages/sub_clinic_admin/order/components/ExpressTimeline.vue new file mode 100644 index 0000000..3a8a65b --- /dev/null +++ b/subPackages/sub_clinic_admin/order/components/ExpressTimeline.vue @@ -0,0 +1,168 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/order/components/PrescriptionDetailPopup.vue b/subPackages/sub_clinic_admin/order/components/PrescriptionDetailPopup.vue new file mode 100644 index 0000000..3e5ce62 --- /dev/null +++ b/subPackages/sub_clinic_admin/order/components/PrescriptionDetailPopup.vue @@ -0,0 +1,591 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/order/detail.vue b/subPackages/sub_clinic_admin/order/detail.vue new file mode 100644 index 0000000..00a5037 --- /dev/null +++ b/subPackages/sub_clinic_admin/order/detail.vue @@ -0,0 +1,558 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/order/index.vue b/subPackages/sub_clinic_admin/order/index.vue new file mode 100644 index 0000000..3a22199 --- /dev/null +++ b/subPackages/sub_clinic_admin/order/index.vue @@ -0,0 +1,582 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/reconciliation/index.vue b/subPackages/sub_clinic_admin/reconciliation/index.vue new file mode 100644 index 0000000..b4bfcb9 --- /dev/null +++ b/subPackages/sub_clinic_admin/reconciliation/index.vue @@ -0,0 +1,799 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/warehouse/components/PriceEditModal.vue b/subPackages/sub_clinic_admin/warehouse/components/PriceEditModal.vue new file mode 100644 index 0000000..aa0215a --- /dev/null +++ b/subPackages/sub_clinic_admin/warehouse/components/PriceEditModal.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/subPackages/sub_clinic_admin/warehouse/detail.vue b/subPackages/sub_clinic_admin/warehouse/detail.vue new file mode 100644 index 0000000..74804da --- /dev/null +++ b/subPackages/sub_clinic_admin/warehouse/detail.vue @@ -0,0 +1,135 @@ + + + + + diff --git a/subPackages/sub_clinic_admin/warehouse/index.vue b/subPackages/sub_clinic_admin/warehouse/index.vue new file mode 100644 index 0000000..1ac25f4 --- /dev/null +++ b/subPackages/sub_clinic_admin/warehouse/index.vue @@ -0,0 +1,231 @@ + + + + + diff --git a/subPackages/sub_clinic_admin/withdrawal/apply.vue b/subPackages/sub_clinic_admin/withdrawal/apply.vue new file mode 100644 index 0000000..b1d7eb3 --- /dev/null +++ b/subPackages/sub_clinic_admin/withdrawal/apply.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/subPackages/sub_clinic_admin/withdrawal/card-edit.vue b/subPackages/sub_clinic_admin/withdrawal/card-edit.vue new file mode 100644 index 0000000..96f76e4 --- /dev/null +++ b/subPackages/sub_clinic_admin/withdrawal/card-edit.vue @@ -0,0 +1,143 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/withdrawal/index.vue b/subPackages/sub_clinic_admin/withdrawal/index.vue new file mode 100644 index 0000000..b863cb3 --- /dev/null +++ b/subPackages/sub_clinic_admin/withdrawal/index.vue @@ -0,0 +1,415 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/withdrawal/record-detail.vue b/subPackages/sub_clinic_admin/withdrawal/record-detail.vue new file mode 100644 index 0000000..1deb127 --- /dev/null +++ b/subPackages/sub_clinic_admin/withdrawal/record-detail.vue @@ -0,0 +1,170 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/withdrawal/settlement/detail.vue b/subPackages/sub_clinic_admin/withdrawal/settlement/detail.vue new file mode 100644 index 0000000..85069de --- /dev/null +++ b/subPackages/sub_clinic_admin/withdrawal/settlement/detail.vue @@ -0,0 +1,196 @@ + + + + + \ No newline at end of file diff --git a/subPackages/sub_clinic_admin/withdrawal/settlement/index.vue b/subPackages/sub_clinic_admin/withdrawal/settlement/index.vue new file mode 100644 index 0000000..782941d --- /dev/null +++ b/subPackages/sub_clinic_admin/withdrawal/settlement/index.vue @@ -0,0 +1,194 @@ + + + + + \ No newline at end of file diff --git a/utils/wxListKey.js b/utils/wxListKey.js new file mode 100644 index 0000000..f031523 --- /dev/null +++ b/utils/wxListKey.js @@ -0,0 +1,23 @@ +/** + * 微信小程序 v-for :key 仅支持简单属性访问,不支持模板表达式。 + * 在 JS 中为列表项预生成 _wxKey 后,模板使用 :key="item._wxKey" + */ +export function withWxKey(list, idField = 'id', fallbackPrefix = 'i') { + return (list || []).map((item, index) => ({ + ...item, + _wxKey: item[idField] != null && item[idField] !== '' + ? String(item[idField]) + : `${fallbackPrefix}${index}`, + })) +} + +/** 单条消息/列表项补全 _wxKey(用于 push、$set 等增量更新) */ +export function ensureWxKey(item, idField = 'id', fallback) { + if (!item || typeof item !== 'object') return item + if (item._wxKey != null && item._wxKey !== '') return item + const id = item[idField] + return { + ...item, + _wxKey: id != null && id !== '' ? String(id) : String(fallback), + } +}