This commit is contained in:
van
2026-05-06 01:38:18 +08:00
parent 29e404149f
commit 312ff3efc6
2 changed files with 341 additions and 261 deletions

View File

@@ -32,6 +32,9 @@
<el-form-item label="商品名称" prop="skuName"> <el-form-item label="商品名称" prop="skuName">
<el-input v-model="queryParams.skuName" placeholder="请输入商品名称" clearable style="width: 240px" @keyup.enter.native="handleQuery" /> <el-input v-model="queryParams.skuName" placeholder="请输入商品名称" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
</el-form-item> </el-form-item>
<el-form-item label="商品SKU" prop="skuId">
<el-input v-model="queryParams.skuId" placeholder="SKU ID" clearable style="width: 200px" @keyup.enter.native="handleQuery" />
</el-form-item>
<el-form-item label="订单状态" prop="statusGroup"> <el-form-item label="订单状态" prop="statusGroup">
<el-select v-model="queryParams.statusGroup" placeholder="订单状态" clearable style="width: 240px"> <el-select v-model="queryParams.statusGroup" placeholder="订单状态" clearable style="width: 240px">
<el-option v-for="status in mergedStatusList" :key="status.value" :label="status.label" :value="status.value" /> <el-option v-for="status in mergedStatusList" :key="status.value" :label="status.label" :value="status.value" />
@@ -50,7 +53,7 @@
</mobile-search-form> </mobile-search-form>
<!-- 统计悬浮模块 --> <!-- 统计悬浮模块 -->
<el-card class="statistics-card" shadow="hover" v-if="orderrowsList.length > 0"> <el-card class="statistics-card" shadow="hover">
<div slot="header" class="clearfix"> <div slot="header" class="clearfix">
<span><i class="el-icon-data-analysis"></i> 佣金统计</span> <span><i class="el-icon-data-analysis"></i> 佣金统计</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="toggleStatistics"> <el-button style="float: right; padding: 3px 0" type="text" @click="toggleStatistics">
@@ -367,6 +370,7 @@ export default {
unionId: null, unionId: null,
orderId: null, orderId: null,
skuName: null, skuName: null,
skuId: null,
validCode: null, validCode: null,
statusGroup: null, // 新增 statusGroup: null, // 新增
orderBy: null, // 排序字段 orderBy: null, // 排序字段
@@ -725,21 +729,11 @@ export default {
/** 搜索按钮操作 */ /** 搜索按钮操作 */
handleQuery() { handleQuery() {
this.queryParams.pageNum = 1; this.queryParams.pageNum = 1;
console.log(this.queryParams.validCode);
// 合并项转为原始code数组
if (this.queryParams.statusGroup) { if (this.queryParams.statusGroup) {
this.queryParams.validCodes = this.statusValueMap[this.queryParams.statusGroup].map(code => Number(code)); const codes = this.statusValueMap[this.queryParams.statusGroup]
} else if (this.queryParams.validCodes && Array.isArray(this.queryParams.validCodes)) { this.queryParams.validCodes = codes && codes.length ? codes.map(code => Number(code)) : null
this.queryParams.validCodes = this.queryParams.validCodes.map(code => Number(code));
} else { } else {
this.queryParams.validCodes = null; this.queryParams.validCodes = null
}
// 打印类型检查
if (this.queryParams.validCode) {
this.queryParams.validCode = this.queryParams.validCode.map(item => Number(item));
console.log('validCode值:', this.queryParams.validCodes);
console.log('validCode类型:', this.queryParams.validCodes.map(item => typeof item));
} }
this.getList(); this.getList();
}, },
@@ -747,7 +741,7 @@ export default {
resetQuery() { resetQuery() {
this.dateRange = []; this.dateRange = [];
this.resetForm("queryForm"); this.resetForm("queryForm");
this.queryParams.validCodes = []; this.queryParams.validCodes = null;
// 重置时恢复默认分页条数 // 重置时恢复默认分页条数
this.queryParams.pageSize = 10; this.queryParams.pageSize = 10;
this.queryParams.pageNum = 1; this.queryParams.pageNum = 1;

View File

@@ -1,9 +1,19 @@
<template> <template>
<div class="app-container"> <div class="app-container" v-loading="loading">
<!-- 查询条件 --> <!-- 查询条件 -->
<el-card style="margin-bottom: 20px;"> <el-card class="filter-card">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch"> <el-form :model="queryParams" ref="queryForm" :inline="true" size="small" v-show="showSearch" label-width="88px">
<el-form-item label="时间范围" prop="dateRange"> <el-form-item label="京粉账号" prop="unionId">
<el-select v-model="queryParams.unionId" placeholder="全部京东联盟账号" clearable filterable style="width: 220px">
<el-option
v-for="admin in adminList"
:key="admin.value || admin.id"
:label="admin.label || (admin.name + ' (' + admin.wxid + ')')"
:value="admin.value || admin.id"
/>
</el-select>
</el-form-item>
<el-form-item label="时间范围">
<el-date-picker <el-date-picker
v-model="dateRange" v-model="dateRange"
type="daterange" type="daterange"
@@ -11,131 +21,95 @@
start-placeholder="开始日期" start-placeholder="开始日期"
end-placeholder="结束日期" end-placeholder="结束日期"
value-format="yyyy-MM-dd" value-format="yyyy-MM-dd"
@change="handleDateRangeChange"> style="width: 260px"
</el-date-picker> @change="handleDateRangeChange"
/>
</el-form-item> </el-form-item>
<el-form-item label="订单状态" prop="validCode"> <el-form-item label="订单状态" prop="validCode">
<el-select v-model="queryParams.validCode" placeholder="请选择订单状态" clearable> <el-select v-model="queryParams.validCode" placeholder="全部状态" clearable style="width: 160px">
<el-option <el-option
v-for="dict in validCodeOptions" v-for="dict in validCodeOptions"
:key="dict.value" :key="dict.value"
:label="dict.label" :label="dict.label"
:value="dict.value"> :value="dict.value"
</el-option> />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="商品ID" prop="skuId"> <el-form-item label="商品SKU" prop="skuId">
<el-input <el-input
v-model="queryParams.skuId" v-model="queryParams.skuId"
placeholder="请输入商品ID" placeholder="SKU ID"
clearable clearable
style="width: 200px"> style="width: 160px"
</el-input> @keyup.enter.native="handleQuery"
/>
</el-form-item> </el-form-item>
<el-form-item> <el-form-item>
<el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button> <el-button type="primary" icon="el-icon-search" @click="handleQuery">查询</el-button>
<el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button> <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
</el-card> </el-card>
<!-- 统计卡片 --> <!-- 统计卡片与列表页口径对齐总佣金=后端 totalCommission预估佣金口径 -->
<el-row :gutter="20"> <el-row :gutter="16" class="summary-row">
<el-col :span="6"> <el-col v-for="item in summaryItems" :key="item.key" :xs="24" :sm="12" :md="8" :lg="4">
<el-card> <el-card class="summary-card" shadow="hover">
<div slot="header"> <div class="summary-card-inner">
<span>总订单数</span> <div class="summary-title">{{ item.title }}</div>
</div> <div class="summary-value">
<div class="card-body"> <template v-if="item.money">¥{{ item.display }}</template>
<h2>{{ statistics.totalOrders || 0 }}</h2> <template v-else>{{ item.display }}</template>
<p>累计订单数量</p> </div>
</div> <div class="summary-hint">{{ item.hint }}</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div slot="header">
<span>总佣金</span>
</div>
<div class="card-body">
<h2>¥{{ formatMoney(statistics.totalCommission) }}</h2>
<p>累计佣金收入</p>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div slot="header">
<span>总商品数</span>
</div>
<div class="card-body">
<h2>{{ statistics.totalSkuNum || 0 }}</h2>
<p>累计商品数量</p>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<div slot="header">
<span>实际费用</span>
</div>
<div class="card-body">
<h2>¥{{ formatMoney(statistics.totalActualFee) }}</h2>
<p>累计实际费用</p>
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<!-- 图表区域 --> <!-- 图表区域 -->
<el-row :gutter="20" style="margin-top: 20px;"> <el-row :gutter="16" class="charts-row">
<el-col :span="12"> <el-col :xs="24" :lg="12">
<el-card> <el-card shadow="hover">
<div slot="header"> <div slot="header" class="card-header-plain">
<span>订单状态分布</span> <span>订单状态分布</span>
</div> </div>
<div class="chart-container"> <div class="chart-container">
<div ref="statusChart" style="height: 300px;"></div> <div ref="statusChart" class="chart-el" />
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
<el-col :span="12"> <el-col :xs="24" :lg="12">
<el-card> <el-card shadow="hover">
<div slot="header"> <div slot="header" class="card-header-plain">
<span>佣金分布</span> <span>佣金分布预估口径</span>
</div> </div>
<div class="chart-container"> <div class="chart-container">
<div ref="commissionChart" style="height: 300px;"></div> <div ref="commissionChart" class="chart-el" />
</div> </div>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<!-- 状态详情表格 --> <!-- 状态详情表格分组顺序与列表页一致 -->
<el-row :gutter="20" style="margin-top: 20px;"> <el-row :gutter="16" class="table-row">
<el-col :span="24"> <el-col :span="24">
<el-card> <el-card shadow="hover">
<div slot="header"> <div slot="header" class="card-header-plain">
<span>订单状态详情</span> <span>订单状态详情</span>
</div> </div>
<el-table :data="statusDetails" style="width: 100%"> <el-table :data="statusDetails" stripe style="width: 100%">
<el-table-column prop="label" label="状态" width="120" /> <el-table-column prop="label" label="状态" min-width="100" />
<el-table-column prop="count" label="订单数" width="120" align="center" /> <el-table-column prop="count" label="订单数" width="100" align="center" />
<el-table-column prop="skuNum" label="商品数" width="120" align="center" /> <el-table-column prop="skuNum" label="商品数" width="100" align="center" />
<el-table-column prop="commission" label="佣金" width="120" align="center"> <el-table-column prop="commission" label="预估佣金" min-width="110" align="right">
<template slot-scope="scope"> <template slot-scope="scope">¥{{ formatMoney(scope.row.commission) }}</template>
¥{{ formatMoney(scope.row.commission) }}
</template>
</el-table-column> </el-table-column>
<el-table-column prop="actualFee" label="实际费用" width="120" align="center"> <el-table-column prop="actualFee" label="实际费用" min-width="110" align="right">
<template slot-scope="scope"> <template slot-scope="scope">¥{{ formatMoney(scope.row.actualFee) }}</template>
¥{{ formatMoney(scope.row.actualFee) }}
</template>
</el-table-column> </el-table-column>
<el-table-column prop="percentage" label="占比" width="100" align="center"> <el-table-column prop="percentage" label="占比" width="88" align="center">
<template slot-scope="scope"> <template slot-scope="scope">{{ scope.row.percentage }}%</template>
{{ scope.row.percentage }}%
</template>
</el-table-column> </el-table-column>
</el-table> </el-table>
</el-card> </el-card>
@@ -146,86 +120,173 @@
<script> <script>
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { debounce } from '@/utils'
import { getOrderStatistics, getValidCodeSelectData } from '@/api/system/orderrows' import { getOrderStatistics, getValidCodeSelectData } from '@/api/system/orderrows'
import { getAdminSelectData } from '@/api/system/superadmin'
/** 与列表页、后端 groupStats 逻辑顺序一致 */
const GROUP_STAT_ORDER = ['pending', 'paid', 'deposit', 'finished', 'cancel', 'invalid', 'illegal']
const PIE_COLORS = ['#e6a23c', '#409eff', '#909399', '#67c23a', '#f56c6c', '#c0c4cc', '#f78989']
export default { export default {
name: "OrderStatistics", name: 'OrderStatistics',
data() { data() {
return { return {
// 遮罩层 loading: false,
loading: true,
// 显示搜索条件
showSearch: true, showSearch: true,
// 查询参数
queryParams: { queryParams: {
beginTime: null, beginTime: null,
endTime: null, endTime: null,
validCode: null, validCode: null,
skuId: null skuId: null,
unionId: null
}, },
// 日期范围
dateRange: [], dateRange: [],
// 订单状态选项
validCodeOptions: [], validCodeOptions: [],
// 统计数据 adminList: [],
statistics: { statistics: {
totalOrders: 0, totalOrders: 0,
totalCosPrice: 0,
totalCommission: 0, totalCommission: 0,
totalSkuNum: 0, totalSkuNum: 0,
totalActualFee: 0, totalActualFee: 0,
groupStats: {} groupStats: {}
}, },
// 状态详情数据 statusDetails: [],
statusDetails: [] _statusChart: null,
_commissionChart: null,
_chartResize: null
}
},
computed: {
summaryItems() {
const s = this.statistics
return [
{
key: 'orders',
title: '总订单数',
money: false,
display: s.totalOrders || 0,
hint: '符合当前筛选'
},
{
key: 'cos',
title: '总计佣金额',
money: true,
display: this.formatMoney(s.totalCosPrice),
hint: 'estimateCosPrice 汇总'
},
{
key: 'commission',
title: '预估佣金',
money: true,
display: this.formatMoney(s.totalCommission),
hint: '与列表页「预估佣金」同口径'
},
{
key: 'sku',
title: '总商品件数',
money: false,
display: s.totalSkuNum || 0,
hint: 'skuNum 汇总'
},
{
key: 'actual',
title: '实际费用',
money: true,
display: this.formatMoney(s.totalActualFee),
hint: 'actualFee 汇总'
}
]
} }
}, },
created() { created() {
this.getValidCodeOptions() this.bootstrap()
this.getStatistics()
}, },
mounted() { mounted() {
this.initCharts() this._chartResize = debounce(() => {
this._statusChart && this._statusChart.resize()
this._commissionChart && this._commissionChart.resize()
}, 120)
window.addEventListener('resize', this._chartResize)
},
beforeDestroy() {
window.removeEventListener('resize', this._chartResize)
this._disposeCharts()
}, },
methods: { methods: {
/** 获取订单状态选项 */ bootstrap() {
getValidCodeOptions() { Promise.all([
getValidCodeSelectData().then(response => { getValidCodeSelectData().then(res => {
this.validCodeOptions = response.data this.validCodeOptions = res.data || []
}) }),
getAdminSelectData().then(res => {
this.adminList = res.data || res || []
})
])
.catch(() => {
this.$message.error('加载筛选项失败')
})
.finally(() => {
this.getStatistics()
})
},
_disposeCharts() {
if (this._statusChart) {
this._statusChart.dispose()
this._statusChart = null
}
if (this._commissionChart) {
this._commissionChart.dispose()
this._commissionChart = null
}
}, },
/** 获取统计数据 */
getStatistics() { getStatistics() {
this.loading = true this.loading = true
getOrderStatistics(this.queryParams).then(response => { const params = { ...this.queryParams }
this.statistics = response.data if (params.skuId === '' || params.skuId === undefined) {
this.processStatusDetails() params.skuId = null
this.$nextTick(() => { }
this.initCharts() getOrderStatistics(params)
.then(response => {
this.statistics = Object.assign(
{
totalOrders: 0,
totalCosPrice: 0,
totalCommission: 0,
totalSkuNum: 0,
totalActualFee: 0,
groupStats: {}
},
response.data || {}
)
this.processStatusDetails()
this.$nextTick(() => this.updateCharts())
})
.catch(() => {
this.$message.error('加载统计数据失败')
})
.finally(() => {
this.loading = false
}) })
this.loading = false
}).catch(() => {
this.loading = false
})
}, },
/** 处理状态详情数据 */
processStatusDetails() { processStatusDetails() {
const groupStats = this.statistics.groupStats || {} const groupStats = this.statistics.groupStats || {}
this.statusDetails = Object.values(groupStats).map(item => { const total = this.statistics.totalOrders || 0
const percentage = this.statistics.totalOrders > 0 this.statusDetails = GROUP_STAT_ORDER.map(key => groupStats[key])
? ((item.count / this.statistics.totalOrders) * 100).toFixed(2) .filter(Boolean)
: '0.00' .map(item => ({
return {
...item, ...item,
percentage skuNum: item.skuNum != null ? item.skuNum : 0,
} percentage: total > 0 ? ((item.count / total) * 100).toFixed(2) : '0.00'
}) }))
}, },
/** 格式化金额 */
formatMoney(amount) { formatMoney(amount) {
if (!amount && amount !== 0) return '0.00' if (amount == null || amount === '') return '0.00'
return parseFloat(amount).toFixed(2) const n = parseFloat(amount)
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
}, },
/** 日期范围变化处理 */
handleDateRangeChange(dates) { handleDateRangeChange(dates) {
if (dates && dates.length === 2) { if (dates && dates.length === 2) {
this.queryParams.beginTime = dates[0] this.queryParams.beginTime = dates[0]
@@ -235,150 +296,175 @@ export default {
this.queryParams.endTime = null this.queryParams.endTime = null
} }
}, },
/** 搜索按钮操作 */
handleQuery() { handleQuery() {
this.getStatistics() this.getStatistics()
}, },
/** 重置按钮操作 */
resetQuery() { resetQuery() {
this.dateRange = [] this.dateRange = []
this.resetForm("queryForm") this.resetForm('queryForm')
this.queryParams = { this.queryParams = {
beginTime: null, beginTime: null,
endTime: null, endTime: null,
validCode: null, validCode: null,
skuId: null skuId: null,
unionId: null
} }
this.getStatistics() this.getStatistics()
}, },
/** 初始化图表 */ updateCharts() {
initCharts() { this._renderPie(this.$refs.statusChart, '_statusChart', (groupStats) => {
this.initStatusChart() const data = GROUP_STAT_ORDER.map(k => groupStats[k]).filter(Boolean).map(item => ({
this.initCommissionChart() value: item.count,
},
/** 初始化状态分布图表 */
initStatusChart() {
const chartDom = this.$refs.statusChart
if (!chartDom) return
const myChart = echarts.init(chartDom)
const groupStats = this.statistics.groupStats || {}
const data = Object.values(groupStats).map(item => ({
value: item.count,
name: item.label
}))
const option = {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'vertical',
left: 'left',
type: 'scroll'
},
series: [
{
name: '订单状态',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data
}
]
}
myChart.setOption(option)
},
/** 初始化佣金分布图表 */
initCommissionChart() {
const chartDom = this.$refs.commissionChart
if (!chartDom) return
const myChart = echarts.init(chartDom)
const groupStats = this.statistics.groupStats || {}
const data = Object.values(groupStats)
.filter(item => item.commission > 0)
.map(item => ({
value: item.commission,
name: item.label name: item.label
})) }))
return {
const option = { name: '订单状态',
tooltip: { data,
trigger: 'item', isAmount: false
formatter: '{a} <br/>{b}: ¥{c} ({d}%)' }
}, })
legend: { this._renderPie(this.$refs.commissionChart, '_commissionChart', (groupStats) => {
orient: 'vertical', const data = GROUP_STAT_ORDER.map(k => groupStats[k])
left: 'left', .filter(Boolean)
type: 'scroll' .filter(item => Number(item.commission) > 0)
}, .map(item => ({
series: [ value: item.commission,
{ name: item.label
name: '佣金分布', }))
type: 'pie', return {
radius: ['40%', '70%'], name: '佣金分布',
avoidLabelOverlap: false, data,
label: { isAmount: true
show: false, }
position: 'center' })
}, this._chartResize && this._chartResize()
emphasis: { },
label: { _renderPie(domRef, instKey, build) {
show: true, if (!domRef) return
fontSize: '18', const groupStats = this.statistics.groupStats || {}
fontWeight: 'bold' const built = build(groupStats)
} let chart = this[instKey]
}, if (!chart) {
labelLine: { chart = echarts.init(domRef)
show: false this[instKey] = chart
},
data: data
}
]
} }
if (!built.data.length) {
myChart.setOption(option) chart.setOption(
{
tooltip: { show: false },
color: PIE_COLORS,
graphic: [
{
type: 'text',
left: 'center',
top: 'middle',
style: { text: '暂无数据', fill: '#909399', fontSize: 14 }
}
],
series: []
},
true
)
return
}
chart.setOption(
{
tooltip: {
trigger: 'item',
formatter: built.isAmount
? (p) => `${p.seriesName}<br/>${p.name}: ¥${this.formatMoney(p.value)} (${p.percent}%)`
: '{a} <br/>{b}: {c} ({d}%)'
},
color: PIE_COLORS,
graphic: [],
legend: { orient: 'vertical', left: 'left', type: 'scroll' },
series: [
{
name: built.name,
type: 'pie',
radius: ['42%', '72%'],
center: ['56%', '52%'],
avoidLabelOverlap: true,
itemStyle: { borderRadius: 4, borderColor: '#fff', borderWidth: 1 },
label: { show: true, formatter: '{b}\n{d}%' },
emphasis: {
label: { show: true, fontSize: 16, fontWeight: 'bold' }
},
data: built.data
}
]
},
true
)
} }
} }
} }
</script> </script>
<style scoped> <style scoped>
.card-body { .filter-card {
margin-bottom: 16px;
}
.summary-row {
margin-bottom: 8px;
}
.summary-card {
margin-bottom: 16px;
border-radius: 8px;
}
.summary-card-inner {
text-align: center; text-align: center;
padding: 6px 4px 4px;
} }
.card-body h2 { .summary-title {
color: #409EFF; font-size: 13px;
margin: 10px 0; color: #606266;
font-size: 28px; margin-bottom: 6px;
} }
.card-body p { .summary-value {
color: #666; font-size: 22px;
margin: 5px 0; font-weight: 600;
color: #409eff;
line-height: 1.3;
}
.summary-hint {
font-size: 12px;
color: #909399;
margin-top: 6px;
}
.charts-row {
margin-bottom: 8px;
}
.table-row {
margin-bottom: 24px;
}
.card-header-plain {
font-weight: 600;
color: #303133;
} }
.chart-container { .chart-container {
padding: 10px; padding: 8px 8px 4px;
}
.chart-el {
width: 100%;
height: 300px;
}
@media (max-width: 768px) {
.filter-card ::v-deep .el-form--inline .el-form-item {
display: block;
margin-right: 0;
}
} }
</style> </style>