diff --git a/src/views/system/orderrows/index.vue b/src/views/system/orderrows/index.vue index eeb321d..30221ce 100644 --- a/src/views/system/orderrows/index.vue +++ b/src/views/system/orderrows/index.vue @@ -87,27 +87,30 @@ - - - - 按状态统计 - - - {{ stat.label }} - {{ stat.count }}单 - ¥{{ stat.amount.toFixed(2) }} - - + + + + 订单状态分布 + - - - 按账号统计 - - - {{ getAdminName(unionId) }} - {{ stat.count }}单 - ¥{{ stat.amount.toFixed(2) }} + + + 按状态统计 + + + + {{ stat.label }} + {{ stat.count }}单 + + + 预估 ¥{{ stat.estimateAmount.toFixed(2) }} + 实际 ¥{{ stat.actualAmount.toFixed(2) }} + @@ -321,9 +324,14 @@ import { listOrderrows, getOrderrows, delOrderrows, addOrderrows, updateOrderrows, getValidCodeSelectData } from "@/api/system/orderrows"; import { getAdminSelectData } from "@/api/system/superadmin"; import { mapGetters } from 'vuex' +import * as echarts from 'echarts' +import { debounce } from '@/utils' import MobileSearchForm from '@/components/MobileSearchForm' import MobileButtonGroup from '@/components/MobileButtonGroup' +/** 与后端分组顺序一致,便于阅读 */ +const STATUS_STAT_ORDER = ['pending', 'paid', 'deposit', 'finished', 'cancel', 'invalid', 'illegal'] + export default { name: "Orderrows", components: { @@ -381,13 +389,20 @@ export default { totalCosPrice: 0, totalEstimateFee: 0, totalActualFee: 0, - statusStats: {}, - accountStats: {} - } + statusStats: {} + }, + statusPieInstance: null, + _statusPieResizeHandler: null }; }, computed: { ...mapGetters(['device']), + orderedStatusStatRows() { + return STATUS_STAT_ORDER.filter(key => this.statistics.statusStats[key]).map(key => ({ + key, + stat: this.statistics.statusStats[key] + })) + }, isMobile() { if (this.device === 'mobile') { return true @@ -411,6 +426,9 @@ export default { this.getAdminList(); this.getStatusList(); }, + beforeDestroy() { + this.disposeStatusPieChart(); + }, watch: { // 监听日期范围变化,自动调整分页条数 dateRange: { @@ -426,6 +444,15 @@ export default { } }, deep: true + }, + statistics: { + deep: true, + handler() { + this.$nextTick(() => this.renderStatusPieChart()); + } + }, + showStatistics(val) { + if (val) this.$nextTick(() => this.renderStatusPieChart()); } }, methods: { @@ -448,7 +475,7 @@ export default { applyStatistics(data) { if (!data) { if (this.orderrowsList.length > 0) this.calculateStatistics(); - else this.statistics = { totalOrders: 0, totalCosPrice: 0, totalEstimateFee: 0, totalActualFee: 0, statusStats: {}, accountStats: {} }; + else this.statistics = { totalOrders: 0, totalCosPrice: 0, totalEstimateFee: 0, totalActualFee: 0, statusStats: {} }; return; } const groupStats = data.groupStats || {}; @@ -465,38 +492,79 @@ export default { finished: this.convertGroupStat(groupStats.finished), deposit: this.convertGroupStat(groupStats.deposit), illegal: this.convertGroupStat(groupStats.illegal) - }, - accountStats: {} + } }; - // 按账号统计仅用当前页列表轻量汇总 - if (this.orderrowsList.length > 0) { - const accountStats = {}; - this.orderrowsList.forEach(order => { - const unionId = order.unionId; - if (!accountStats[unionId]) accountStats[unionId] = { count: 0, amount: 0 }; - accountStats[unionId].count++; - const validCode = String(order.validCode); - const isCancel = validCode === '3'; - const isIllegal = ['25', '26', '27', '28'].includes(validCode); - let commissionAmount = parseFloat(order.actualFee) || 0; - if (isIllegal && order.estimateCosPrice && order.commissionRate) { - commissionAmount = parseFloat(order.estimateCosPrice) * parseFloat(order.commissionRate) / 100; - } else if (isCancel && (!order.actualFee || parseFloat(order.actualFee) === 0) && order.estimateCosPrice && order.commissionRate) { - commissionAmount = parseFloat(order.estimateCosPrice) * parseFloat(order.commissionRate) / 100; - } - accountStats[unionId].amount += commissionAmount; - }); - this.statistics.accountStats = accountStats; - } }, + /** 与 OrderRowsController.buildStatistics 一致:commission→预估口径,actualFee→实际口径 */ convertGroupStat(groupStat) { - if (!groupStat) return { label: '', count: 0, amount: 0 }; + if (!groupStat) return { label: '', count: 0, estimateAmount: 0, actualAmount: 0 }; + const est = groupStat.commission != null ? Number(groupStat.commission) : 0; + const act = groupStat.actualFee != null ? Number(groupStat.actualFee) : 0; return { label: groupStat.label || '', count: groupStat.count || 0, - amount: groupStat.actualFee ?? 0 + estimateAmount: est, + actualAmount: act }; }, + disposeStatusPieChart() { + if (this._statusPieResizeHandler) { + window.removeEventListener('resize', this._statusPieResizeHandler); + this._statusPieResizeHandler = null; + } + if (this.statusPieInstance) { + this.statusPieInstance.dispose(); + this.statusPieInstance = null; + } + }, + renderStatusPieChart() { + if (!this.showStatistics || !this.$refs.statusPieRef) return; + const pieData = []; + STATUS_STAT_ORDER.forEach(key => { + const s = this.statistics.statusStats[key]; + if (s && s.count > 0) pieData.push({ name: s.label, value: s.count }); + }); + const el = this.$refs.statusPieRef; + if (!this.statusPieInstance) { + this.statusPieInstance = echarts.init(el); + this._statusPieResizeHandler = debounce(() => { + this.statusPieInstance && this.statusPieInstance.resize(); + }, 100); + window.addEventListener('resize', this._statusPieResizeHandler); + } + if (!pieData.length) { + this.statusPieInstance.setOption({ + tooltip: { show: false }, + graphic: [{ + type: 'text', + left: 'center', + top: 'middle', + style: { text: '暂无分布数据', fill: '#909399', fontSize: 14 } + }], + series: [] + }, true); + return; + } + this.statusPieInstance.setOption({ + tooltip: { + trigger: 'item', + formatter: '{b}{c} 单 ({d}%)' + }, + graphic: [], + series: [ + { + name: '订单数', + type: 'pie', + radius: ['40%', '68%'], + center: ['50%', '52%'], + data: pieData, + label: { formatter: '{b}\n{d}%' }, + emphasis: { itemStyle: { shadowBlur: 8, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.15)' } } + } + ], + color: ['#e6a23c', '#409eff', '#909399', '#67c23a', '#f56c6c', '#c0c4cc', '#f78989'] + }, true); + }, /** 获取管理员列表 */ getAdminList() { getAdminSelectData().then(response => { @@ -773,73 +841,79 @@ export default { toggleStatistics() { this.showStatistics = !this.showStatistics; }, - /** 计算统计数据 */ + /** 无后端 statistics 时按当前页推算(与 OrderRowsController.buildStatistics 口径一致) */ calculateStatistics() { const stats = { totalOrders: this.orderrowsList.length, totalCosPrice: 0, totalEstimateFee: 0, totalActualFee: 0, - statusStats: {}, - accountStats: {} + statusStats: {} }; - // 状态分组映射 const statusGroups = { - 'cancel': { label: '取消', codes: ['3'] }, - 'invalid': { label: '无效', codes: ['2','4','5','6','7','8','9','10','11','14','19','20','21','22','23','29','30','31','32','33','34'] }, - 'pending': { label: '待付款', codes: ['15'] }, - 'paid': { label: '已付款', codes: ['16'] }, - 'finished': { label: '已完成', codes: ['17'] }, - 'deposit': { label: '已付定金', codes: ['24'] }, - 'illegal': { label: '违规', codes: ['25','26','27','28'] } + cancel: { label: '取消', codes: ['3'] }, + invalid: { label: '无效', codes: ['2', '4', '5', '6', '7', '8', '9', '10', '11', '14', '19', '20', '21', '22', '23', '29', '30', '31', '32', '33', '34'] }, + pending: { label: '待付款', codes: ['15'] }, + paid: { label: '已付款', codes: ['16'] }, + finished: { label: '已完成', codes: ['17'] }, + deposit: { label: '已付定金', codes: ['24'] }, + illegal: { label: '违规', codes: ['25', '26', '27', '28'] } }; - // 初始化状态统计 Object.keys(statusGroups).forEach(key => { stats.statusStats[key] = { label: statusGroups[key].label, count: 0, - amount: 0 + estimateAmount: 0, + actualAmount: 0 }; }); - // 遍历订单数据计算统计 this.orderrowsList.forEach(order => { - // 总计佣金额 - if (order.estimateCosPrice) { + if (order.estimateCosPrice != null && order.estimateCosPrice !== '') { stats.totalCosPrice += parseFloat(order.estimateCosPrice) || 0; } - // 预估佣金 - if (order.estimateFee) { - stats.totalEstimateFee += parseFloat(order.estimateFee) || 0; - } - - // 计算实际佣金或预估佣金 - // 对于违规订单(25,26,27,28),始终使用 estimateCosPrice * commissionRate / 100 计算 - // 对于取消订单(3),如果actualFee为空或0,则通过公式计算 - const validCode = String(order.validCode); - const isCancel = validCode === '3'; // 取消订单 - const isIllegal = ['25', '26', '27', '28'].includes(validCode); // 违规订单 - - let commissionAmount = parseFloat(order.actualFee) || 0; - const estimateCosPrice = parseFloat(order.estimateCosPrice) || 0; - const commissionRate = parseFloat(order.commissionRate) || 0; - - // 违规订单始终使用公式计算佣金 - if (isIllegal && estimateCosPrice > 0 && commissionRate > 0) { - commissionAmount = estimateCosPrice * commissionRate / 100; - } - // 取消订单:如果actualFee为空或0,则通过公式计算 - else if (isCancel && (!order.actualFee || parseFloat(order.actualFee) === 0) && estimateCosPrice > 0 && commissionRate > 0) { - commissionAmount = estimateCosPrice * commissionRate / 100; - } - - // 实际佣金累计(包含计算出的违规和取消订单佣金) - stats.totalActualFee += commissionAmount; - // 按状态统计 - let statusKey = 'invalid'; // 默认无效 + const validCode = String(order.validCode); + const isCancel = validCode === '3'; + const isIllegal = ['25', '26', '27', '28'].includes(validCode); + const estCos = parseFloat(order.estimateCosPrice) || 0; + const rate = parseFloat(order.commissionRate) || 0; + const estFee = parseFloat(order.estimateFee) || 0; + const actFee = parseFloat(order.actualFee) || 0; + + let commissionAmount = 0; + let actualFeeAmount = 0; + + if (isIllegal) { + if (estCos > 0 && rate > 0) { + commissionAmount = estCos * rate / 100; + actualFeeAmount = commissionAmount; + } else if (estFee) { + commissionAmount = estFee; + actualFeeAmount = estFee; + } + } else if (isCancel) { + if (actFee > 0) { + actualFeeAmount = actFee; + commissionAmount = estFee; + } else if (estCos > 0 && rate > 0) { + commissionAmount = estCos * rate / 100; + actualFeeAmount = commissionAmount; + } else { + commissionAmount = estFee; + actualFeeAmount = actFee; + } + } else { + commissionAmount = estFee; + actualFeeAmount = actFee; + } + + stats.totalEstimateFee += commissionAmount; + stats.totalActualFee += actualFeeAmount; + + let statusKey = 'invalid'; for (const [key, group] of Object.entries(statusGroups)) { if (group.codes.includes(validCode)) { statusKey = key; @@ -847,18 +921,8 @@ export default { } } stats.statusStats[statusKey].count++; - stats.statusStats[statusKey].amount += commissionAmount; - - // 按账号统计 - const unionId = order.unionId; - if (!stats.accountStats[unionId]) { - stats.accountStats[unionId] = { - count: 0, - amount: 0 - }; - } - stats.accountStats[unionId].count++; - stats.accountStats[unionId].amount += commissionAmount; + stats.statusStats[statusKey].estimateAmount += commissionAmount; + stats.statusStats[statusKey].actualAmount += actualFeeAmount; }); this.statistics = stats; @@ -978,15 +1042,22 @@ export default { color: #2c3e50; } -.status-stats, .account-stats { +.status-chart-row { + align-items: stretch; +} + +.status-pie-wrap, +.status-stats-expanded { background: #f8f9fa; padding: 15px; border-radius: 6px; height: 100%; + min-height: 280px; } -.status-stats h4, .account-stats h4 { - margin: 0 0 15px 0; +.status-pie-wrap h4, +.status-stats-expanded h4 { + margin: 0 0 12px 0; color: #2c3e50; font-size: 16px; font-weight: 600; @@ -994,24 +1065,61 @@ export default { padding-bottom: 8px; } -.status-list, .account-list { - max-height: 200px; +.status-pie-chart { + width: 100%; + height: 260px; +} + +.status-list-expanded { + max-height: 320px; overflow-y: auto; } -.status-item, .account-item { +.status-item-expanded { + flex-wrap: wrap; + gap: 8px 0; +} + +.status-item-left { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; +} + +.status-item-amounts { + display: flex; + flex-wrap: wrap; + gap: 8px 16px; + justify-content: flex-end; + width: 100%; +} + +@media (min-width: 992px) { + .status-item-expanded { + flex-wrap: nowrap; + align-items: center; + } + .status-item-amounts { + width: auto; + flex: 0 0 auto; + } +} + +.status-item { display: flex; justify-content: space-between; align-items: center; - padding: 8px 0; + padding: 10px 0; border-bottom: 1px solid #e9ecef; } -.status-item:last-child, .account-item:last-child { +.status-item:last-child { border-bottom: none; } -.status-count, .account-count { +.status-count { font-size: 12px; color: #666; background: #e9ecef; @@ -1021,35 +1129,36 @@ export default { text-align: center; } -.status-amount, .account-amount { +.status-amt-est { font-weight: 600; - color: #27ae60; - font-size: 14px; + color: #409eff; + font-size: 13px; + white-space: nowrap; } -.account-name { - font-weight: 500; - color: #2c3e50; - flex: 1; - margin-right: 10px; +.status-amt-act { + font-weight: 600; + color: #67c23a; + font-size: 13px; + white-space: nowrap; } /* 滚动条样式 */ -.status-list::-webkit-scrollbar, .account-list::-webkit-scrollbar { +.status-list-expanded::-webkit-scrollbar { width: 4px; } -.status-list::-webkit-scrollbar-track, .account-list::-webkit-scrollbar-track { +.status-list-expanded::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 2px; } -.status-list::-webkit-scrollbar-thumb, .account-list::-webkit-scrollbar-thumb { +.status-list-expanded::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 2px; } -.status-list::-webkit-scrollbar-thumb:hover, .account-list::-webkit-scrollbar-thumb:hover { +.status-list-expanded::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }