Files
xk-doctor-wx/subPackages/sub_business_shared/order/trace/index.vue
2026-06-18 10:20:24 +08:00

567 lines
20 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 class="row" v-if="prescriptionStatusLabel"><text class="label">审方状态</text><text class="value status-audit">{{ prescriptionStatusLabel }}</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 in timeline" :key="node._wxKey" 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="isPlatformAdmin" class="scope-tabs">
<view
v-for="tab in ledgerScopeTabs"
:key="tab.key"
class="scope-tab"
:class="{ active: ledgerScope === tab.key }"
@click="switchLedgerScope(tab.key)"
>{{ tab.label }}</view>
</view>
<view v-if="hasRegisterLedger" class="sub-tabs">
<view
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="currentLedgerSumMoney != null" class="ledger-sum">
合计分账{{ formatMoney(currentLedgerSumMoney) }}
</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',
ledgerScope: 'store',
ledgerProduct: null,
ledgerRegister: null,
ledgerLoading: false,
reconciliationData: null,
reconciliationLoading: false,
loadedTabs: {},
mainTabs: [
{ key: 'timeline', label: '时间线' },
{ key: 'ledger', label: '分账' },
{ key: 'reconciliation', label: '对账' },
],
ledgerScopeTabs: [
{ key: 'all', label: '全部' },
{ key: 'store', label: '门店' },
{ key: 'platform', label: '平台' },
],
}
},
computed: {
isPlatformAdmin() {
if (this.bizCtx && this.bizCtx.mode === 'platform_admin') return true
return !!(this.traceData && this.traceData.meta && this.traceData.meta.is_platform_admin)
},
summary() {
return this.traceData?.summary || {}
},
timeline() {
const items = this.traceData?.timeline || []
return withWxKey(items, 'key', 'tl')
},
links() {
return this.traceData?.links || {}
},
hasRegisterLedger() {
return !!(this.links.register_id && this.ledgerRegister !== null)
},
currentLedgerData() {
return this.ledgerSubTab === 'register' ? this.ledgerRegister : this.ledgerProduct
},
currentLedgerSumMoney() {
const data = this.currentLedgerData
if (!data || data.sum_money == null || data.sum_money === '') return null
return data.sum_money
},
currentLedgerItems() {
const items = this.currentLedgerData?.items || []
return withWxKey(items, 'id', 'traceLed')
},
prescriptionStatusLabel() {
if (!this.summary.prescription_id) return ''
const auditNode = (this.timeline || []).find(
n => n.key === 'prescription_audited' || n.key === 'prescription_rejected'
)
if (auditNode && auditNode.status) return auditNode.status
return '待审核'
},
reconciliationItems() {
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
this.loadedTabs = {}
const meta = w.data && w.data.meta
let scope = 'store'
if (this.bizCtx && this.bizCtx.mode === 'platform_admin') {
scope = 'all'
} else if (meta && meta.is_platform_admin) {
scope = 'all'
}
if (meta && meta.is_platform_admin === false) {
scope = 'store'
}
this.ledgerScope = scope
} else {
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.ledgerScope}`]) {
this.loadLedger()
} else if (key === 'reconciliation' && !this.loadedTabs.reconciliation) {
this.loadReconciliation()
}
},
switchLedgerSub(key) {
this.ledgerSubTab = key
},
switchLedgerScope(scope) {
if (this.ledgerScope === scope) return
this.ledgerScope = scope
this.loadedTabs[`ledger:${scope}`] = false
this.loadLedger(true)
},
loadLedger(force = false) {
const cacheKey = `ledger:${this.ledgerScope}`
if (!force && this.loadedTabs[cacheKey]) return
const productId = this.links.product_order_id
const registerId = this.links.register_id
if (!productId && !registerId) {
this.loadedTabs[cacheKey] = true
return
}
this.ledgerLoading = true
const scope = this.ledgerScope
const reqs = []
if (productId) {
reqs.push(
this.bizApi.getOrderLedgerDetail({ order_id: productId, order_type: 1, scope }).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 }).then(w => {
this.ledgerRegister = w.ok ? w.data : null
})
)
} else {
this.ledgerRegister = null
}
Promise.all(reqs).finally(() => {
this.ledgerLoading = false
this.loadedTabs[cacheKey] = 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;
}
.scope-tabs {
display: flex;
background: #fff;
border-radius: 12rpx;
margin-bottom: 16rpx;
overflow: hidden;
}
.scope-tab {
flex: 1;
text-align: center;
padding: 16rpx 0;
font-size: 26rpx;
color: #666;
}
.scope-tab.active {
color: #1677ff;
font-weight: 600;
background: #e6f4ff;
}
.ledger-sum {
font-size: 28rpx;
color: #333;
font-weight: 600;
padding: 8rpx 8rpx 16rpx;
}
.status-audit { color: #6acdbb; }
.tab-panel { min-height: 200rpx; }
.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>