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-input v-model="queryParams.skuName" placeholder="请输入商品名称" clearable style="width: 240px" @keyup.enter.native="handleQuery" />
</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-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" />
@@ -50,7 +53,7 @@
</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">
<span><i class="el-icon-data-analysis"></i> 佣金统计</span>
<el-button style="float: right; padding: 3px 0" type="text" @click="toggleStatistics">
@@ -367,6 +370,7 @@ export default {
unionId: null,
orderId: null,
skuName: null,
skuId: null,
validCode: null,
statusGroup: null, // 新增
orderBy: null, // 排序字段
@@ -725,21 +729,11 @@ export default {
/** 搜索按钮操作 */
handleQuery() {
this.queryParams.pageNum = 1;
console.log(this.queryParams.validCode);
// 合并项转为原始code数组
if (this.queryParams.statusGroup) {
this.queryParams.validCodes = this.statusValueMap[this.queryParams.statusGroup].map(code => Number(code));
} else if (this.queryParams.validCodes && Array.isArray(this.queryParams.validCodes)) {
this.queryParams.validCodes = this.queryParams.validCodes.map(code => Number(code));
const codes = this.statusValueMap[this.queryParams.statusGroup]
this.queryParams.validCodes = codes && codes.length ? codes.map(code => Number(code)) : null
} else {
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.queryParams.validCodes = null
}
this.getList();
},
@@ -747,7 +741,7 @@ export default {
resetQuery() {
this.dateRange = [];
this.resetForm("queryForm");
this.queryParams.validCodes = [];
this.queryParams.validCodes = null;
// 重置时恢复默认分页条数
this.queryParams.pageSize = 10;
this.queryParams.pageNum = 1;

View File

@@ -1,9 +1,19 @@
<template>
<div class="app-container">
<div class="app-container" v-loading="loading">
<!-- 查询条件 -->
<el-card style="margin-bottom: 20px;">
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item label="时间范围" prop="dateRange">
<el-card class="filter-card">
<el-form :model="queryParams" ref="queryForm" :inline="true" size="small" v-show="showSearch" label-width="88px">
<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
v-model="dateRange"
type="daterange"
@@ -11,131 +21,95 @@
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="yyyy-MM-dd"
@change="handleDateRangeChange">
</el-date-picker>
style="width: 260px"
@change="handleDateRangeChange"
/>
</el-form-item>
<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
v-for="dict in validCodeOptions"
:key="dict.value"
:label="dict.label"
:value="dict.value">
</el-option>
:value="dict.value"
/>
</el-select>
</el-form-item>
<el-form-item label="商品ID" prop="skuId">
<el-form-item label="商品SKU" prop="skuId">
<el-input
v-model="queryParams.skuId"
placeholder="请输入商品ID"
placeholder="SKU ID"
clearable
style="width: 200px">
</el-input>
style="width: 160px"
@keyup.enter.native="handleQuery"
/>
</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-form-item>
</el-form>
</el-card>
<!-- 统计卡片 -->
<el-row :gutter="20">
<el-col :span="6">
<el-card>
<div slot="header">
<span>总订单数</span>
</div>
<div class="card-body">
<h2>{{ statistics.totalOrders || 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.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>
<!-- 统计卡片与列表页口径对齐总佣金=后端 totalCommission预估佣金口径 -->
<el-row :gutter="16" class="summary-row">
<el-col v-for="item in summaryItems" :key="item.key" :xs="24" :sm="12" :md="8" :lg="4">
<el-card class="summary-card" shadow="hover">
<div class="summary-card-inner">
<div class="summary-title">{{ item.title }}</div>
<div class="summary-value">
<template v-if="item.money">¥{{ item.display }}</template>
<template v-else>{{ item.display }}</template>
</div>
<div class="summary-hint">{{ item.hint }}</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表区域 -->
<el-row :gutter="20" style="margin-top: 20px;">
<el-col :span="12">
<el-card>
<div slot="header">
<el-row :gutter="16" class="charts-row">
<el-col :xs="24" :lg="12">
<el-card shadow="hover">
<div slot="header" class="card-header-plain">
<span>订单状态分布</span>
</div>
<div class="chart-container">
<div ref="statusChart" style="height: 300px;"></div>
<div ref="statusChart" class="chart-el" />
</div>
</el-card>
</el-col>
<el-col :span="12">
<el-card>
<div slot="header">
<span>佣金分布</span>
<el-col :xs="24" :lg="12">
<el-card shadow="hover">
<div slot="header" class="card-header-plain">
<span>佣金分布预估口径</span>
</div>
<div class="chart-container">
<div ref="commissionChart" style="height: 300px;"></div>
<div ref="commissionChart" class="chart-el" />
</div>
</el-card>
</el-col>
</el-row>
<!-- 状态详情表格 -->
<el-row :gutter="20" style="margin-top: 20px;">
<!-- 状态详情表格分组顺序与列表页一致 -->
<el-row :gutter="16" class="table-row">
<el-col :span="24">
<el-card>
<div slot="header">
<el-card shadow="hover">
<div slot="header" class="card-header-plain">
<span>订单状态详情</span>
</div>
<el-table :data="statusDetails" style="width: 100%">
<el-table-column prop="label" label="状态" width="120" />
<el-table-column prop="count" label="订单数" width="120" align="center" />
<el-table-column prop="skuNum" label="商品数" width="120" align="center" />
<el-table-column prop="commission" label="佣金" width="120" align="center">
<template slot-scope="scope">
¥{{ formatMoney(scope.row.commission) }}
</template>
<el-table :data="statusDetails" stripe style="width: 100%">
<el-table-column prop="label" label="状态" min-width="100" />
<el-table-column prop="count" label="订单数" width="100" align="center" />
<el-table-column prop="skuNum" label="商品数" width="100" align="center" />
<el-table-column prop="commission" label="预估佣金" min-width="110" align="right">
<template slot-scope="scope">¥{{ formatMoney(scope.row.commission) }}</template>
</el-table-column>
<el-table-column prop="actualFee" label="实际费用" width="120" align="center">
<template slot-scope="scope">
¥{{ formatMoney(scope.row.actualFee) }}
</template>
<el-table-column prop="actualFee" label="实际费用" min-width="110" align="right">
<template slot-scope="scope">¥{{ formatMoney(scope.row.actualFee) }}</template>
</el-table-column>
<el-table-column prop="percentage" label="占比" width="100" align="center">
<template slot-scope="scope">
{{ scope.row.percentage }}%
</template>
<el-table-column prop="percentage" label="占比" width="88" align="center">
<template slot-scope="scope">{{ scope.row.percentage }}%</template>
</el-table-column>
</el-table>
</el-card>
@@ -146,86 +120,173 @@
<script>
import * as echarts from 'echarts'
import { debounce } from '@/utils'
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 {
name: "OrderStatistics",
name: 'OrderStatistics',
data() {
return {
// 遮罩层
loading: true,
// 显示搜索条件
loading: false,
showSearch: true,
// 查询参数
queryParams: {
beginTime: null,
endTime: null,
validCode: null,
skuId: null
skuId: null,
unionId: null
},
// 日期范围
dateRange: [],
// 订单状态选项
validCodeOptions: [],
// 统计数据
adminList: [],
statistics: {
totalOrders: 0,
totalCosPrice: 0,
totalCommission: 0,
totalSkuNum: 0,
totalActualFee: 0,
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() {
this.getValidCodeOptions()
this.getStatistics()
this.bootstrap()
},
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: {
/** 获取订单状态选项 */
getValidCodeOptions() {
getValidCodeSelectData().then(response => {
this.validCodeOptions = response.data
})
bootstrap() {
Promise.all([
getValidCodeSelectData().then(res => {
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() {
this.loading = true
getOrderStatistics(this.queryParams).then(response => {
this.statistics = response.data
this.processStatusDetails()
this.$nextTick(() => {
this.initCharts()
const params = { ...this.queryParams }
if (params.skuId === '' || params.skuId === undefined) {
params.skuId = null
}
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() {
const groupStats = this.statistics.groupStats || {}
this.statusDetails = Object.values(groupStats).map(item => {
const percentage = this.statistics.totalOrders > 0
? ((item.count / this.statistics.totalOrders) * 100).toFixed(2)
: '0.00'
return {
const total = this.statistics.totalOrders || 0
this.statusDetails = GROUP_STAT_ORDER.map(key => groupStats[key])
.filter(Boolean)
.map(item => ({
...item,
percentage
}
})
skuNum: item.skuNum != null ? item.skuNum : 0,
percentage: total > 0 ? ((item.count / total) * 100).toFixed(2) : '0.00'
}))
},
/** 格式化金额 */
formatMoney(amount) {
if (!amount && amount !== 0) return '0.00'
return parseFloat(amount).toFixed(2)
if (amount == null || amount === '') return '0.00'
const n = parseFloat(amount)
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
},
/** 日期范围变化处理 */
handleDateRangeChange(dates) {
if (dates && dates.length === 2) {
this.queryParams.beginTime = dates[0]
@@ -235,150 +296,175 @@ export default {
this.queryParams.endTime = null
}
},
/** 搜索按钮操作 */
handleQuery() {
this.getStatistics()
},
/** 重置按钮操作 */
resetQuery() {
this.dateRange = []
this.resetForm("queryForm")
this.resetForm('queryForm')
this.queryParams = {
beginTime: null,
endTime: null,
validCode: null,
skuId: null
skuId: null,
unionId: null
}
this.getStatistics()
},
/** 初始化图表 */
initCharts() {
this.initStatusChart()
this.initCommissionChart()
},
/** 初始化状态分布图表 */
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,
updateCharts() {
this._renderPie(this.$refs.statusChart, '_statusChart', (groupStats) => {
const data = GROUP_STAT_ORDER.map(k => groupStats[k]).filter(Boolean).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
}
]
return {
name: '订单状态',
data,
isAmount: false
}
})
this._renderPie(this.$refs.commissionChart, '_commissionChart', (groupStats) => {
const data = GROUP_STAT_ORDER.map(k => groupStats[k])
.filter(Boolean)
.filter(item => Number(item.commission) > 0)
.map(item => ({
value: item.commission,
name: item.label
}))
return {
name: '佣金分布',
data,
isAmount: true
}
})
this._chartResize && this._chartResize()
},
_renderPie(domRef, instKey, build) {
if (!domRef) return
const groupStats = this.statistics.groupStats || {}
const built = build(groupStats)
let chart = this[instKey]
if (!chart) {
chart = echarts.init(domRef)
this[instKey] = chart
}
myChart.setOption(option)
if (!built.data.length) {
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>
<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;
padding: 6px 4px 4px;
}
.card-body h2 {
color: #409EFF;
margin: 10px 0;
font-size: 28px;
.summary-title {
font-size: 13px;
color: #606266;
margin-bottom: 6px;
}
.card-body p {
color: #666;
margin: 5px 0;
.summary-value {
font-size: 22px;
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 {
padding: 10px;
padding: 8px 8px 4px;
}
</style>
.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>