Files
xk-doctor-wx/subPackages/sub_business_shared/order/trace/index.vue

567 lines
20 KiB
Vue
Raw Normal View History

2026-06-05 12:28:36 +08:00
<template>
<business-page-layout title="订单追溯" :show-back="true">
<view class="trace-container">
<view v-if="loading" class="loading-tip">加载中...</view>
<template v-else-if="traceData">
<!-- 摘要 -->
<view class="summary-card">
<view class="row"><text class="label">商品订单</text><text class="value">{{ summary.product_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"><text class="label">结算状态</text><text class="value">{{ summary.is_settled === 1 ? '已结算' : '未结算' }}</text></view>
2026-06-18 10:20:24 +08:00
<view class="row" v-if="prescriptionStatusLabel"><text class="label">审方状态</text><text class="value status-audit">{{ prescriptionStatusLabel }}</text></view>
2026-06-05 12:28:36 +08:00
</view>
<!-- Tab -->
<view class="main-tabs">
<view
v-for="tab in mainTabs"
:key="tab.key"
class="main-tab"
:class="{ active: activeTab === tab.key }"
@click="switchMainTab(tab.key)"
>{{ tab.label }}</view>
</view>
<!-- 时间线 -->
<view v-show="activeTab === 'timeline'" class="tab-panel">
<view v-if="!timeline.length" class="empty">暂无时间线数据</view>
2026-06-18 10:20:24 +08:00
<view v-for="node in timeline" :key="node._wxKey" class="timeline-item">
2026-06-05 12:28:36 +08:00
<view class="tl-dot" />
<view class="tl-body">
<view class="tl-title">{{ node.title }}</view>
<view class="tl-meta">
<text v-if="node.time">{{ node.time }}</text>
<text v-if="node.status" class="tl-status">{{ node.status }}</text>
</view>
<view v-if="node.extra" class="tl-extra">{{ node.extra }}</view>
</view>
</view>
</view>
<!-- 分账明细 -->
<view v-show="activeTab === 'ledger'" class="tab-panel">
<view v-if="ledgerLoading" class="loading-tip">加载分账...</view>
<template v-else>
2026-06-18 10:20:24 +08:00
<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>
2026-06-05 12:28:36 +08:00
<view v-if="hasRegisterLedger" class="sub-tabs">
<view
class="sub-tab"
:class="{ active: ledgerSubTab === 'product' }"
@click="switchLedgerSub('product')"
>商品订单</view>
<view
class="sub-tab"
:class="{ active: ledgerSubTab === 'register' }"
@click="switchLedgerSub('register')"
>挂号订单</view>
</view>
2026-06-18 10:20:24 +08:00
<view v-if="currentLedgerSumMoney != null" class="ledger-sum">
合计分账{{ formatMoney(currentLedgerSumMoney) }}
</view>
2026-06-05 12:28:36 +08:00
<view v-if="currentLedgerItems.length">
<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" v-if="row.drug_name"><text class="label">药品</text><text class="value">{{ row.drug_name }}</text></view>
<view class="row" v-if="row.specification"><text class="label">规格</text><text class="value">{{ row.specification }}</text></view>
<view class="row"><text class="label">分账金额</text><text class="value amount">{{ formatMoney(row.money) }}</text></view>
<view class="row"><text class="label">结算状态</text><text class="value">{{ row.status_txt || '-' }}</text></view>
<view class="row" v-if="row.created_at"><text class="label">时间</text><text class="value">{{ row.created_at }}</text></view>
</view>
</view>
<view v-else class="empty">暂无分账明细</view>
</template>
</view>
<!-- 对账明细 -->
<view v-show="activeTab === 'reconciliation'" class="tab-panel">
<view v-if="reconciliationLoading" class="loading-tip">加载对账...</view>
<template v-else>
<view v-if="reconciliationHint" class="hint-box">{{ reconciliationHint }}</view>
<template v-if="reconciliationSummary">
<view class="recon-summary-card">
<view class="recon-head">
<text class="recon-title">对账汇总</text>
<text v-if="reconciliationSummary.registrationPrice !== '¥0.00'" class="mini-tag">
挂号费 {{ reconciliationSummary.registrationPrice }}
</text>
</view>
<view v-if="reconciliationSummary.registerOrderNo" class="recon-sub">
挂号单号 {{ reconciliationSummary.registerOrderNo }}
</view>
<view class="hero-data">
<view class="data-box">
<text class="data-label">总销售额</text>
<text class="data-val text-primary">{{ reconciliationSummary.totalSales }}</text>
</view>
<view class="data-divider" />
<view class="data-box">
<text class="data-label">总供货额</text>
<text class="data-val">{{ reconciliationSummary.totalSupply }}</text>
</view>
</view>
<view class="dashboard-grid">
<view class="dashboard-island">
<view class="island-head">
<text class="island-title">草药</text>
<text class="island-sales">销量 {{ reconciliationSummary.herbal.sales }}</text>
</view>
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.herbal.salesPrice }}</text></view>
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.herbal.supplyPrice }}</text></view>
</view>
<view class="dashboard-island">
<view class="island-head">
<text class="island-title">西药</text>
<text class="island-sales">销量 {{ reconciliationSummary.medicine.sales }}</text>
</view>
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.medicine.salesPrice }}</text></view>
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.medicine.supplyPrice }}</text></view>
</view>
<view class="dashboard-island">
<view class="island-head">
<text class="island-title">服务包</text>
<text class="island-sales">销量 {{ reconciliationSummary.servicePackage.sales }}</text>
</view>
<view class="island-line"><text>销售</text><text>{{ reconciliationSummary.servicePackage.salesPrice }}</text></view>
<view class="island-line"><text>供货</text><text>{{ reconciliationSummary.servicePackage.supplyPrice }}</text></view>
</view>
<view class="dashboard-island">
<view class="island-head">
<text class="island-title">其他费用</text>
</view>
<view class="island-line"><text>诊疗费</text><text>{{ reconciliationSummary.otherFees.treatment }}</text></view>
<view class="island-line"><text>加工费</text><text>{{ reconciliationSummary.otherFees.process }}</text></view>
<view class="island-line"><text>快递费</text><text>{{ reconciliationSummary.otherFees.express }}</text></view>
</view>
</view>
</view>
</template>
<view v-if="reconciliationItems.length" class="recon-drug-section">
<view class="section-title">药品明细</view>
<view v-for="row in reconciliationItems" :key="row._wxKey" class="detail-card">
<view class="row"><text class="label">药品</text><text class="value">{{ row.drug_name || row.drug_id || '-' }}</text></view>
<view class="row"><text class="label">数量</text><text class="value">{{ row.number }}</text></view>
<view class="row"><text class="label">销售价</text><text class="value">{{ formatMoney(row.total_price) }}</text></view>
<view class="row"><text class="label">供货价</text><text class="value">{{ formatMoney(row.total_buy_price) }}</text></view>
</view>
</view>
<view v-else-if="!reconciliationHint && !reconciliationSummary" class="empty">暂无对账明细</view>
</template>
</view>
</template>
<view v-else-if="!loading" class="empty">加载失败请返回重试</view>
</view>
</business-page-layout>
</template>
<script>
import BusinessPageLayout from '@/subPackages/sub_business_shared/components/business-page-layout.vue'
import { formatMoney } from '@/subPackages/sub_business_shared/common/format.js'
import { mapReconciliationOrderSummary } from '@/subPackages/sub_business_shared/common/display.js'
import { withWxKey } from '@/utils/wxListKey.js'
import businessMixin from '@/subPackages/sub_business_shared/common/businessMixin.js'
export default {
mixins: [businessMixin],
components: { BusinessPageLayout },
data() {
return {
orderId: 0,
scene: 'product',
loading: true,
traceData: null,
activeTab: 'timeline',
ledgerSubTab: 'product',
2026-06-18 10:20:24 +08:00
ledgerScope: 'store',
2026-06-05 12:28:36 +08:00
ledgerProduct: null,
ledgerRegister: null,
ledgerLoading: false,
reconciliationData: null,
reconciliationLoading: false,
loadedTabs: {},
mainTabs: [
{ key: 'timeline', label: '时间线' },
{ key: 'ledger', label: '分账' },
{ key: 'reconciliation', label: '对账' },
],
2026-06-18 10:20:24 +08:00
ledgerScopeTabs: [
{ key: 'all', label: '全部' },
{ key: 'store', label: '门店' },
{ key: 'platform', label: '平台' },
],
2026-06-05 12:28:36 +08:00
}
},
computed: {
2026-06-18 10:20:24 +08:00
isPlatformAdmin() {
if (this.bizCtx && this.bizCtx.mode === 'platform_admin') return true
return !!(this.traceData && this.traceData.meta && this.traceData.meta.is_platform_admin)
},
2026-06-05 12:28:36 +08:00
summary() {
return this.traceData?.summary || {}
},
timeline() {
2026-06-18 10:20:24 +08:00
const items = this.traceData?.timeline || []
return withWxKey(items, 'key', 'tl')
2026-06-05 12:28:36 +08:00
},
links() {
return this.traceData?.links || {}
},
hasRegisterLedger() {
return !!(this.links.register_id && this.ledgerRegister !== null)
},
2026-06-18 10:20:24 +08:00
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
},
2026-06-05 12:28:36 +08:00
currentLedgerItems() {
2026-06-18 10:20:24 +08:00
const items = this.currentLedgerData?.items || []
2026-06-05 12:28:36 +08:00
return withWxKey(items, 'id', 'traceLed')
},
2026-06-18 10:20:24 +08:00
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 '待审核'
},
2026-06-05 12:28:36 +08:00
reconciliationItems() {
const items = this.reconciliationData?.items || []
return withWxKey(items, 'id', 'traceRec')
},
reconciliationSummary() {
return mapReconciliationOrderSummary(this.reconciliationData?.summary)
},
reconciliationHint() {
return this.reconciliationData?.reconciliation_hint || this.summary.reconciliation_hint || ''
},
},
onLoad(q) {
this.orderId = Number(q.order_id || 0)
this.scene = q.scene || 'product'
this.loadTrace()
},
methods: {
formatMoney,
loadTrace() {
if (this.orderId <= 0) {
this.loading = false
return
}
this.loading = true
this.bizApi.getOrderTrace({ scene: this.scene, order_id: this.orderId }).then(w => {
if (w.ok) {
this.traceData = w.data
2026-06-18 10:20:24 +08:00
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
2026-06-05 12:28:36 +08:00
} else {
uni.showToast({ title: (w.res && w.res.message) || '加载失败', icon: 'none' })
}
}).finally(() => { this.loading = false })
},
switchMainTab(key) {
this.activeTab = key
2026-06-18 10:20:24 +08:00
if (key === 'ledger' && !this.loadedTabs[`ledger:${this.ledgerScope}`]) {
2026-06-05 12:28:36 +08:00
this.loadLedger()
} else if (key === 'reconciliation' && !this.loadedTabs.reconciliation) {
this.loadReconciliation()
}
},
switchLedgerSub(key) {
this.ledgerSubTab = key
},
2026-06-18 10:20:24 +08:00
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
2026-06-05 12:28:36 +08:00
const productId = this.links.product_order_id
const registerId = this.links.register_id
if (!productId && !registerId) {
2026-06-18 10:20:24 +08:00
this.loadedTabs[cacheKey] = true
2026-06-05 12:28:36 +08:00
return
}
this.ledgerLoading = true
2026-06-18 10:20:24 +08:00
const scope = this.ledgerScope
2026-06-05 12:28:36 +08:00
const reqs = []
if (productId) {
reqs.push(
2026-06-18 10:20:24 +08:00
this.bizApi.getOrderLedgerDetail({ order_id: productId, order_type: 1, scope }).then(w => {
2026-06-05 12:28:36 +08:00
this.ledgerProduct = w.ok ? w.data : null
})
)
} else {
this.ledgerProduct = null
}
if (registerId) {
reqs.push(
2026-06-18 10:20:24 +08:00
this.bizApi.getOrderLedgerDetail({ order_id: registerId, order_type: 2, scope }).then(w => {
2026-06-05 12:28:36 +08:00
this.ledgerRegister = w.ok ? w.data : null
})
)
} else {
this.ledgerRegister = null
}
Promise.all(reqs).finally(() => {
this.ledgerLoading = false
2026-06-18 10:20:24 +08:00
this.loadedTabs[cacheKey] = true
2026-06-05 12:28:36 +08:00
})
},
loadReconciliation() {
const productId = this.links.product_order_id || (this.scene === 'product' ? this.orderId : 0)
if (!productId) {
this.reconciliationData = { items: [], reconciliation_hint: '暂无关联商品订单' }
this.loadedTabs.reconciliation = true
return
}
this.reconciliationLoading = true
this.bizApi.getOrderReconciliationDetail({ product_order_id: productId }).then(w => {
if (w.ok) {
this.reconciliationData = w.data
}
}).finally(() => {
this.reconciliationLoading = false
this.loadedTabs.reconciliation = true
})
},
},
}
</script>
<style lang="scss" scoped>
.trace-container {
padding: 24rpx;
padding-bottom: 48rpx;
}
.loading-tip, .empty {
text-align: center;
color: #999;
font-size: 28rpx;
padding: 48rpx 0;
}
.summary-card, .detail-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.summary-card .row, .detail-card .row {
display: flex;
justify-content: space-between;
font-size: 26rpx;
padding: 8rpx 0;
color: #333;
}
.label { color: #888; flex-shrink: 0; margin-right: 16rpx; }
.value { text-align: right; word-break: break-all; }
.amount { color: #1677ff; font-weight: 600; }
.main-tabs, .sub-tabs {
display: flex;
background: #fff;
border-radius: 12rpx;
margin-bottom: 24rpx;
overflow: hidden;
}
.main-tab, .sub-tab {
flex: 1;
text-align: center;
padding: 20rpx 0;
font-size: 28rpx;
color: #666;
}
.main-tab.active, .sub-tab.active {
color: #1677ff;
font-weight: 600;
background: #e6f4ff;
}
2026-06-18 10:20:24 +08:00
.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; }
2026-06-05 12:28:36 +08:00
.tab-panel { min-height: 200rpx; }
.timeline-item {
display: flex;
margin-bottom: 24rpx;
padding-left: 8rpx;
}
.tl-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
background: #1677ff;
margin-top: 10rpx;
margin-right: 20rpx;
flex-shrink: 0;
}
.tl-body { flex: 1; background: #fff; border-radius: 12rpx; padding: 20rpx; }
.tl-title { font-size: 28rpx; font-weight: 600; color: #333; }
.tl-meta { font-size: 24rpx; color: #999; margin-top: 8rpx; }
.tl-status { margin-left: 12rpx; color: #1677ff; }
.tl-extra { font-size: 24rpx; color: #666; margin-top: 8rpx; }
.hint-box {
background: #fff7e6;
color: #d48806;
font-size: 26rpx;
padding: 20rpx 24rpx;
border-radius: 12rpx;
margin-bottom: 24rpx;
}
.sum-row {
display: flex;
justify-content: space-between;
font-size: 26rpx;
color: #333;
margin-bottom: 16rpx;
padding: 0 8rpx;
}
.recon-summary-card {
background: #fff;
border-radius: 16rpx;
padding: 24rpx;
margin-bottom: 24rpx;
}
.recon-head {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12rpx;
}
.recon-title {
font-size: 30rpx;
font-weight: 600;
color: #333;
}
.recon-sub {
font-size: 24rpx;
color: #888;
margin-bottom: 16rpx;
}
.mini-tag {
font-size: 20rpx;
padding: 4rpx 12rpx;
background: rgba(106, 205, 187, 0.1);
color: #6ACDBB;
border-radius: 8rpx;
}
.hero-data {
display: flex;
align-items: center;
background: #F8FBFB;
border-radius: 12rpx;
padding: 24rpx 0;
margin-bottom: 20rpx;
}
.data-box {
flex: 1;
text-align: center;
}
.data-label {
display: block;
font-size: 24rpx;
color: #888;
margin-bottom: 8rpx;
}
.data-val {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.text-primary { color: #1677ff; }
.data-divider {
width: 1rpx;
height: 60rpx;
background: #eee;
}
.dashboard-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16rpx;
}
.dashboard-island {
background: #F8FBFB;
border-radius: 12rpx;
padding: 16rpx;
}
.island-head {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12rpx;
}
.island-title {
font-size: 26rpx;
font-weight: 600;
color: #333;
}
.island-sales {
font-size: 22rpx;
color: #888;
}
.island-line {
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #666;
padding: 4rpx 0;
}
.recon-drug-section { margin-top: 8rpx; }
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 16rpx;
padding: 0 8rpx;
}
</style>