Files
xk-doctor-wx/subPackages/sub_business_shared/order/trace/index.vue
2026-06-05 12:28:36 +08:00

481 lines
17 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>
</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>
<view v-for="(node, idx) in timeline" :key="node.key || idx" class="timeline-item">
<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>
<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>
<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',
ledgerProduct: null,
ledgerRegister: null,
ledgerLoading: false,
reconciliationData: null,
reconciliationLoading: false,
loadedTabs: {},
mainTabs: [
{ key: 'timeline', label: '时间线' },
{ key: 'ledger', label: '分账' },
{ key: 'reconciliation', label: '对账' },
],
}
},
computed: {
summary() {
return this.traceData?.summary || {}
},
timeline() {
return this.traceData?.timeline || []
},
links() {
return this.traceData?.links || {}
},
hasRegisterLedger() {
return !!(this.links.register_id && this.ledgerRegister !== null)
},
currentLedgerItems() {
const data = this.ledgerSubTab === 'register' ? this.ledgerRegister : this.ledgerProduct
const items = data?.items || []
return withWxKey(items, 'id', 'traceLed')
},
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
} else {
uni.showToast({ title: (w.res && w.res.message) || '加载失败', icon: 'none' })
}
}).finally(() => { this.loading = false })
},
switchMainTab(key) {
this.activeTab = key
if (key === 'ledger' && !this.loadedTabs.ledger) {
this.loadLedger()
} else if (key === 'reconciliation' && !this.loadedTabs.reconciliation) {
this.loadReconciliation()
}
},
switchLedgerSub(key) {
this.ledgerSubTab = key
},
loadLedger() {
const productId = this.links.product_order_id
const registerId = this.links.register_id
if (!productId && !registerId) {
this.loadedTabs.ledger = true
return
}
this.ledgerLoading = true
const reqs = []
if (productId) {
reqs.push(
this.bizApi.getOrderLedgerDetail({ order_id: productId, order_type: 1, scope: 'store' }).then(w => {
this.ledgerProduct = w.ok ? w.data : null
})
)
} else {
this.ledgerProduct = null
}
if (registerId) {
reqs.push(
this.bizApi.getOrderLedgerDetail({ order_id: registerId, order_type: 2, scope: 'store' }).then(w => {
this.ledgerRegister = w.ok ? w.data : null
})
)
} else {
this.ledgerRegister = null
}
Promise.all(reqs).finally(() => {
this.ledgerLoading = false
this.loadedTabs.ledger = true
})
},
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;
}
.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>