mirror of
https://gitee.com/beijing_hongye_huicheng/lilishop-ui.git
synced 2026-08-06 10:57:26 +08:00
Merge branch 'master' of https://gitee.com/beijing_hongye_huicheng/lilishop-ui
This commit is contained in:
@@ -9,7 +9,7 @@
|
|||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script src="/config.js"></script>
|
<script src="<%= BASE_URL %>config.js"></script>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
||||||
</noscript>
|
</noscript>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const resolve = (dir) => path.join(__dirname, dir);
|
|||||||
const enableProduction = process.env.NODE_ENV === "production";
|
const enableProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
publicPath: process.env.VUE_APP_PUBLIC_PATH || "/",
|
||||||
outputDir: "dist",
|
outputDir: "dist",
|
||||||
assetsDir: "static",
|
assetsDir: "static",
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ function resolve(dir) {
|
|||||||
// vue.config.js
|
// vue.config.js
|
||||||
const vueConfig = {
|
const vueConfig = {
|
||||||
// 公共路径(必须有的)
|
// 公共路径(必须有的)
|
||||||
publicPath: '/',
|
publicPath: process.env.VUE_APP_PUBLIC_PATH || '/',
|
||||||
// 输出文件目录
|
// 输出文件目录
|
||||||
outputDir: 'dist',
|
outputDir: 'dist',
|
||||||
// 静态资源存放的文件夹(相对于ouputDir)
|
// 静态资源存放的文件夹(相对于ouputDir)
|
||||||
|
|||||||
@@ -72,8 +72,8 @@
|
|||||||
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
|
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
|
||||||
<script src="<%=js%>"></script>
|
<script src="<%=js%>"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
<script src="/config.js"></script>
|
<script src="<%= BASE_URL %>config.js"></script>
|
||||||
<script src="/tinymce/tinymce.min.js"></script>
|
<script src="<%= BASE_URL %>tinymce/tinymce.min.js"></script>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong
|
<strong
|
||||||
>We're sorry but admin doesn't work properly without JavaScript
|
>We're sorry but admin doesn't work properly without JavaScript
|
||||||
|
|||||||
152
manager/src/api/statistics.js
Normal file
152
manager/src/api/statistics.js
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { getRequest } from "@/libs/axios";
|
||||||
|
|
||||||
|
/** 营业概况 */
|
||||||
|
export const getBusinessOverview = (params) => {
|
||||||
|
return getRequest("/statistics/overview", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 收款构成 */
|
||||||
|
export const getBusinessSource = (params) => {
|
||||||
|
return getRequest("/statistics/overview/source", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 营业构成 */
|
||||||
|
export const getBusinessComposition = (params) => {
|
||||||
|
return getRequest("/statistics/overview/businessComposition", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 商品概况 */
|
||||||
|
export const getGoodsOverview = (params) => {
|
||||||
|
return getRequest("/statistics/goods/overview", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 退货排行榜 TOP10 */
|
||||||
|
export const getGoodsRefundRank = (params) => {
|
||||||
|
return getRequest("/statistics/goods/rank/refund", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 畅销排行榜 TOP10 */
|
||||||
|
export const getGoodsSalesRank = (params) => {
|
||||||
|
return getRequest("/statistics/goods/rank/sales", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 分类排行 */
|
||||||
|
export const getGoodsCategoryRank = (params) => {
|
||||||
|
return getRequest("/statistics/goods/getCategoryByPage", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 会员概况 */
|
||||||
|
export const getMemberOverview = (params) => {
|
||||||
|
return getRequest("/statistics/member/overview", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 会员新增人数趋势 */
|
||||||
|
export const getMemberNewTrend = (params) => {
|
||||||
|
return getRequest("/statistics/member/trend", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 客户分析 */
|
||||||
|
export const getMemberAnalysis = (params) => {
|
||||||
|
return getRequest("/statistics/member/analysis", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 性别分布 */
|
||||||
|
export const getMemberGenderDistribution = (params) => {
|
||||||
|
return getRequest("/statistics/member/distribution/gender", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 地域分布 */
|
||||||
|
export const getMemberRegionDistribution = (params) => {
|
||||||
|
return getRequest("/statistics/member/distribution/region", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 积分分析概览 */
|
||||||
|
export const getPointsOverview = () => {
|
||||||
|
return getRequest("/statistics/points");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 客户可用积分分布 */
|
||||||
|
export const getPointsDistribution = () => {
|
||||||
|
return getRequest("/statistics/points/distribution");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 积分累计分发分布 */
|
||||||
|
export const getPointsSourceDistribution = () => {
|
||||||
|
return getRequest("/statistics/points/sourceDistribution");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 客户身份积分累计统计 */
|
||||||
|
export const getPointsIdentityStat = () => {
|
||||||
|
return getRequest("/statistics/points/identityStat");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 储值余额分布 */
|
||||||
|
export const getDepositBalanceDistribution = () => {
|
||||||
|
return getRequest("/statistics/deposit/balanceDistribution");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 储值充值次数分布 */
|
||||||
|
export const getDepositRechargeTimesDistribution = () => {
|
||||||
|
return getRequest("/statistics/deposit/rechargeTimesDistribution");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 储值充值金额分布 */
|
||||||
|
export const getDepositRechargeAmountDistribution = () => {
|
||||||
|
return getRequest("/statistics/deposit/rechargeAmountDistribution");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 营销概况 */
|
||||||
|
export const getMarketingOverview = (params) => {
|
||||||
|
return getRequest("/statistics/marketing", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 分销概况 */
|
||||||
|
export const getDistributionOverview = (params) => {
|
||||||
|
return getRequest("/statistics/distribution", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** TOP分销员 */
|
||||||
|
export const getDistributionRankDistributor = (params) => {
|
||||||
|
return getRequest("/statistics/distribution/rank/distributor", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** TOP分销商品 */
|
||||||
|
export const getDistributionRankGoods = (params) => {
|
||||||
|
return getRequest("/statistics/distribution/rank/goods", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 店铺业绩报表 */
|
||||||
|
export const getStorePerformanceReport = (params) => {
|
||||||
|
return getRequest("/statistics/report/store-performance", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportStorePerformanceReport = (params) => {
|
||||||
|
return getRequest("/statistics/report/store-performance/export", params, "blob");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 商品同比环比报表 */
|
||||||
|
export const getGoodsComparisonReport = (params) => {
|
||||||
|
return getRequest("/statistics/report/goods-comparison", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportGoodsComparisonReport = (params) => {
|
||||||
|
return getRequest("/statistics/report/goods-comparison/export", params, "blob");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 销售订单明细报表 */
|
||||||
|
export const getSalesOrderDetailReport = (params) => {
|
||||||
|
return getRequest("/statistics/report/sales-order-detail", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportSalesOrderDetailReport = (params) => {
|
||||||
|
return getRequest("/statistics/report/sales-order-detail/export", params, "blob");
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 商品销售汇总报表 */
|
||||||
|
export const getGoodsSalesSummaryReport = (params) => {
|
||||||
|
return getRequest("/statistics/report/goods-sales-summary", params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportGoodsSalesSummaryReport = (params) => {
|
||||||
|
return getRequest("/statistics/report/goods-sales-summary/export", params, "blob");
|
||||||
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import plugins from "./plugins";
|
import plugins from "./plugins";
|
||||||
import toobar from "./toolbar";
|
import toobar from "./toolbar";
|
||||||
|
|
||||||
const localCDN = window.location.origin + "/tinymce"; //本地引入
|
const localCDN = window.location.origin + process.env.BASE_URL + "tinymce"; //本地引入,兼容子路径部署(publicPath)
|
||||||
export const initEditor = {
|
export const initEditor = {
|
||||||
base_url: localCDN,
|
base_url: localCDN,
|
||||||
height: "400px",
|
height: "400px",
|
||||||
|
|||||||
@@ -426,6 +426,34 @@ export const otherRouter = {
|
|||||||
title: "钱包流水",
|
title: "钱包流水",
|
||||||
name: "finance-wallet-log",
|
name: "finance-wallet-log",
|
||||||
component: () => import("@/views/finance/walletLog.vue")
|
component: () => import("@/views/finance/walletLog.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "statistics/store-performance",
|
||||||
|
title: "店铺业绩报表",
|
||||||
|
name: "storePerformanceReport",
|
||||||
|
meta: { title: "店铺业绩报表" },
|
||||||
|
component: () => import("@/views/statistics/store-performance.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "statistics/goods-comparison",
|
||||||
|
title: "商品同比环比报表",
|
||||||
|
name: "goodsComparisonReport",
|
||||||
|
meta: { title: "商品同比环比报表" },
|
||||||
|
component: () => import("@/views/statistics/goods-comparison.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "statistics/sales-order-detail",
|
||||||
|
title: "销售订单明细报表",
|
||||||
|
name: "salesOrderDetailReport",
|
||||||
|
meta: { title: "销售订单明细报表" },
|
||||||
|
component: () => import("@/views/statistics/sales-order-detail.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "statistics/goods-sales-summary",
|
||||||
|
title: "商品销售汇总报表",
|
||||||
|
name: "goodsSalesSummaryReport",
|
||||||
|
meta: { title: "商品销售汇总报表" },
|
||||||
|
component: () => import("@/views/statistics/goods-sales-summary.vue")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
161
manager/src/views/statistics/data-report.vue
Normal file
161
manager/src/views/statistics/data-report.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="data-report">
|
||||||
|
<el-card>
|
||||||
|
<h4 class="page-title">数据报表</h4>
|
||||||
|
<p class="page-desc">选择报表模块查看销售数据统计与分析</p>
|
||||||
|
<div class="module-grid">
|
||||||
|
<div
|
||||||
|
v-for="item in modules"
|
||||||
|
:key="item.name"
|
||||||
|
class="module-card"
|
||||||
|
@click="goReport(item.name)"
|
||||||
|
>
|
||||||
|
<div class="module-icon">
|
||||||
|
<el-icon :size="32">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="module-title">{{ item.title }}</div>
|
||||||
|
<div class="module-desc">{{ item.desc }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="module-arrow"><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
DataAnalysis,
|
||||||
|
Document,
|
||||||
|
Goods,
|
||||||
|
TrendCharts,
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
const REPORT_MODULES = [
|
||||||
|
{
|
||||||
|
name: "storePerformanceReport",
|
||||||
|
title: "店铺业绩报表",
|
||||||
|
desc: "支付金额、营业额、转化率及同环比分析",
|
||||||
|
icon: TrendCharts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "goodsComparisonReport",
|
||||||
|
title: "商品同比环比报表",
|
||||||
|
desc: "商品销售金额、销量占比及同环比对比",
|
||||||
|
icon: DataAnalysis,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "salesOrderDetailReport",
|
||||||
|
title: "销售订单明细报表",
|
||||||
|
desc: "订单流水、收款明细与优惠明细",
|
||||||
|
icon: Document,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "goodsSalesSummaryReport",
|
||||||
|
title: "商品销售汇总报表",
|
||||||
|
desc: "商品销量、净销售额及优惠汇总",
|
||||||
|
icon: Goods,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "data-report",
|
||||||
|
components: {
|
||||||
|
ArrowRight,
|
||||||
|
TrendCharts,
|
||||||
|
DataAnalysis,
|
||||||
|
Document,
|
||||||
|
Goods,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modules: REPORT_MODULES,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goReport(name) {
|
||||||
|
this.$router.push({ name });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.data-report {
|
||||||
|
.page-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-desc {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--el-color-primary-light-5);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-arrow {
|
||||||
|
color: #c0c4cc;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
148
manager/src/views/statistics/deposit.vue
Normal file
148
manager/src/views/statistics/deposit.vue
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>储值余额分布</h4>
|
||||||
|
<div id="depositBalanceChart"></div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>充值次数分布</h4>
|
||||||
|
<div id="depositTimesChart"></div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>充值金额分布</h4>
|
||||||
|
<div id="depositAmountChart"></div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { Chart } from "@antv/g2";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
balanceChart: null,
|
||||||
|
timesChart: null,
|
||||||
|
amountChart: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
beforeUnmount() {
|
||||||
|
this.destroyCharts();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
destroyCharts() {
|
||||||
|
["balanceChart", "timesChart", "amountChart"].forEach((key) => {
|
||||||
|
if (this[key]) {
|
||||||
|
this[key].destroy();
|
||||||
|
this[key] = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
withProportion(list) {
|
||||||
|
const rows = list || [];
|
||||||
|
const total = rows.reduce((sum, item) => sum + (Number(item.memberNum) || 0), 0);
|
||||||
|
return rows.map((item) => {
|
||||||
|
const num = Number(item.memberNum) || 0;
|
||||||
|
const proportion = total > 0 ? Math.round((num / total) * 10000) / 100 : 0;
|
||||||
|
return {
|
||||||
|
label: item.label,
|
||||||
|
memberNum: num,
|
||||||
|
proportion,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
renderBarChart(chartKey, containerId, data, xTitle) {
|
||||||
|
if (this[chartKey]) {
|
||||||
|
this[chartKey].destroy();
|
||||||
|
this[chartKey] = null;
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
const chart = new Chart({
|
||||||
|
container: containerId,
|
||||||
|
autoFit: true,
|
||||||
|
height: 320,
|
||||||
|
padding: [40, 40, 50, 60],
|
||||||
|
});
|
||||||
|
chart.data(data || []);
|
||||||
|
chart.scale("memberNum", {
|
||||||
|
nice: true,
|
||||||
|
min: 0,
|
||||||
|
alias: "客户数",
|
||||||
|
});
|
||||||
|
chart.axis("label", {
|
||||||
|
title: {
|
||||||
|
text: xTitle,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
chart.tooltip({
|
||||||
|
showMarkers: false,
|
||||||
|
shared: true,
|
||||||
|
});
|
||||||
|
chart
|
||||||
|
.interval()
|
||||||
|
.position("label*memberNum")
|
||||||
|
.color("#409EFF")
|
||||||
|
.label("memberNum", {
|
||||||
|
offset: 8,
|
||||||
|
})
|
||||||
|
.tooltip("label*memberNum*proportion", (label, memberNum, proportion) => ({
|
||||||
|
name: label,
|
||||||
|
value: `${memberNum}人(${proportion}%)`,
|
||||||
|
}));
|
||||||
|
chart.interaction("active-region");
|
||||||
|
chart.render();
|
||||||
|
this[chartKey] = chart;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
const [balanceRes, timesRes, amountRes] = await Promise.all([
|
||||||
|
API_Statistics.getDepositBalanceDistribution(),
|
||||||
|
API_Statistics.getDepositRechargeTimesDistribution(),
|
||||||
|
API_Statistics.getDepositRechargeAmountDistribution(),
|
||||||
|
]);
|
||||||
|
if (balanceRes && balanceRes.success) {
|
||||||
|
this.renderBarChart(
|
||||||
|
"balanceChart",
|
||||||
|
"depositBalanceChart",
|
||||||
|
this.withProportion(balanceRes.result || []),
|
||||||
|
"余额区间(元)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (timesRes && timesRes.success) {
|
||||||
|
this.renderBarChart(
|
||||||
|
"timesChart",
|
||||||
|
"depositTimesChart",
|
||||||
|
this.withProportion(timesRes.result || []),
|
||||||
|
"充值次数区间"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (amountRes && amountRes.success) {
|
||||||
|
this.renderBarChart(
|
||||||
|
"amountChart",
|
||||||
|
"depositAmountChart",
|
||||||
|
this.withProportion(amountRes.result || []),
|
||||||
|
"累计充值金额区间(元)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.wrapper {
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
213
manager/src/views/statistics/distribution.vue
Normal file
213
manager/src/views/statistics/distribution.vue
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<el-affix :offset="100">
|
||||||
|
<el-card class="card fixed-bottom">
|
||||||
|
<affixTime @selected="clickBreadcrumb" />
|
||||||
|
</el-card>
|
||||||
|
</el-affix>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>分销概况</h4>
|
||||||
|
<div class="metric-grid">
|
||||||
|
<div class="metric-item" v-for="item in metricDefs" :key="item.key">
|
||||||
|
<div class="metric-label">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<el-tooltip v-if="item.tip" :content="item.tip" placement="top">
|
||||||
|
<el-icon class="metric-tip"><QuestionFilled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
<template v-if="item.isMoney">
|
||||||
|
{{ $filters.unitPrice(overview[item.key] || 0, "¥") }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ formatNumber(overview[item.key]) }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>分销排行</h4>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :xs="24" :md="12">
|
||||||
|
<div class="rank-title">TOP分销员</div>
|
||||||
|
<el-table :data="distributorRank" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="rank" label="排名" width="70" />
|
||||||
|
<el-table-column prop="name" label="分销员" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column label="佣金" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ $filters.unitPrice(row.amount || 0, "¥") }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="num" label="订单数" min-width="100" />
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :md="12">
|
||||||
|
<div class="rank-title">TOP分销商品</div>
|
||||||
|
<el-table :data="goodsRank" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="rank" label="排名" width="70" />
|
||||||
|
<el-table-column prop="name" label="商品" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="佣金" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ $filters.unitPrice(row.amount || 0, "¥") }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="num" label="销量" min-width="100" />
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import affixTime from "@/components/affix-time";
|
||||||
|
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { affixTime, QuestionFilled },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
searchType: "LAST_SEVEN",
|
||||||
|
year: "",
|
||||||
|
month: "",
|
||||||
|
storeId: "",
|
||||||
|
},
|
||||||
|
overview: {},
|
||||||
|
distributorRank: [],
|
||||||
|
goodsRank: [],
|
||||||
|
metricDefs: [
|
||||||
|
{ key: "distributorNum", label: "分销员数", tip: "已通过审核的分销员数量" },
|
||||||
|
{ key: "applyNum", label: "待审核分销员", tip: "申请中待审核的分销员数量" },
|
||||||
|
{ key: "distributionOrderNum", label: "分销订单数", tip: "周期内分销订单笔数" },
|
||||||
|
{
|
||||||
|
key: "distributionOrderAmount",
|
||||||
|
label: "分销订单金额",
|
||||||
|
isMoney: true,
|
||||||
|
tip: "周期内分销订单佣金合计",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "settledCommission",
|
||||||
|
label: "已结算佣金",
|
||||||
|
isMoney: true,
|
||||||
|
tip: "周期内已完成结算的佣金",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "pendingCommission",
|
||||||
|
label: "待结算佣金",
|
||||||
|
isMoney: true,
|
||||||
|
tip: "周期内待结算的佣金",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "cashAmount",
|
||||||
|
label: "提现金额",
|
||||||
|
isMoney: true,
|
||||||
|
tip: "周期内分销员提现金额",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
params: {
|
||||||
|
handler() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickBreadcrumb(item) {
|
||||||
|
this.params = {
|
||||||
|
searchType: item.searchType || "",
|
||||||
|
year: item.year || "",
|
||||||
|
month: item.month || "",
|
||||||
|
storeId: item.storeId || "",
|
||||||
|
startTime: item.startTime || "",
|
||||||
|
endTime: item.endTime || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
formatNumber(val) {
|
||||||
|
if (val == null || val === "") return 0;
|
||||||
|
const num = Number(val);
|
||||||
|
return Number.isNaN(num) ? 0 : Math.round(num);
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
const query = { ...this.params };
|
||||||
|
const [overviewRes, distributorRes, goodsRes] = await Promise.all([
|
||||||
|
API_Statistics.getDistributionOverview(query),
|
||||||
|
API_Statistics.getDistributionRankDistributor(query),
|
||||||
|
API_Statistics.getDistributionRankGoods(query),
|
||||||
|
]);
|
||||||
|
if (overviewRes && overviewRes.success) {
|
||||||
|
this.overview = overviewRes.result || {};
|
||||||
|
}
|
||||||
|
if (distributorRes && distributorRes.success) {
|
||||||
|
this.distributorRank = distributorRes.result || [];
|
||||||
|
}
|
||||||
|
if (goodsRes && goodsRes.success) {
|
||||||
|
this.goodsRank = goodsRes.result || [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.wrapper {
|
||||||
|
padding-bottom: 200px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.metric-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.metric-item {
|
||||||
|
background: #f3f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
min-height: 90px;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.metric-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
.rank-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.metric-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.rank-title {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
287
manager/src/views/statistics/goods-comparison.vue
Normal file
287
manager/src/views/statistics/goods-comparison.vue
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card>
|
||||||
|
<el-form inline class="goods-comparison-form">
|
||||||
|
<div class="filter-row">
|
||||||
|
<el-form-item label="时间类型">
|
||||||
|
<el-radio-group v-model="dateType" @change="onDateTypeChange">
|
||||||
|
<el-radio label="DAY">日</el-radio>
|
||||||
|
<el-radio label="PERIOD">指定周期</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="dateType === 'DAY'" label="日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryDate"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else label="日期范围">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<el-form-item label="商品分类">
|
||||||
|
<el-cascader
|
||||||
|
v-model="params.categoryId"
|
||||||
|
:options="categoryList"
|
||||||
|
:props="categoryProps"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="全部分类"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="品牌">
|
||||||
|
<el-select v-model="params.brandId" clearable filterable placeholder="全部品牌" style="width: 160px">
|
||||||
|
<el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品名称">
|
||||||
|
<el-input v-model="params.keyword" clearable placeholder="商品名称" style="width: 160px" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<el-form-item label="订单来源">
|
||||||
|
<el-select v-model="params.clientType" clearable placeholder="全部来源" style="width: 140px">
|
||||||
|
<el-option v-for="item in clientTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="店铺">
|
||||||
|
<el-select v-model="params.storeId" clearable filterable placeholder="全部店铺" style="width: 200px">
|
||||||
|
<el-option v-for="item in shops" :key="item.id" :label="item.storeName" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-select v-model="params.sortType" style="width: 120px">
|
||||||
|
<el-option label="按销售额" value="PRICE" />
|
||||||
|
<el-option label="按销量" value="NUM" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="mt_10">
|
||||||
|
<el-table v-loading="loading" border :data="data" style="width: 100%">
|
||||||
|
<el-table-column label="序号" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ (params.pageNumber - 1) * params.pageSize + $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="goodsName" label="商品名称" min-width="160" />
|
||||||
|
<el-table-column prop="reportTime" label="时间" width="200" />
|
||||||
|
<el-table-column label="商品销售金额" width="120" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.salesAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品销售总金额" width="130" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.totalSalesAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="占比" width="90" align="right">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.amountPercent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="环比" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.momAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="环比率" width="90" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="rateClass(row.momRate)">{{ row.momRate || "0%" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="环比差额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.momDiff) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.yoyAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比率" width="90" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="rateClass(row.yoyRate)">{{ row.yoyRate || "0%" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比差额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.yoyDiff) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="salesNum" label="商品销售数" width="110" align="right" />
|
||||||
|
<el-table-column label="销售占比" width="90" align="right">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.salesNumPercent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="storeName" label="店铺名称" width="140" />
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
class="mt_10"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="loadData"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { getShopListData } from "@/api/shops";
|
||||||
|
import { getBrandListData, getCategoryTree } from "@/api/goods";
|
||||||
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
|
||||||
|
const CLIENT_TYPE_OPTIONS = [
|
||||||
|
{ label: "移动端", value: "H5" },
|
||||||
|
{ label: "PC端", value: "PC" },
|
||||||
|
{ label: "小程序", value: "WECHAT_MP" },
|
||||||
|
{ label: "移动应用端", value: "APP" },
|
||||||
|
{ label: "未知", value: "UNKNOWN" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "goods-comparison-report",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dateType: "DAY",
|
||||||
|
queryDate: "",
|
||||||
|
dateRange: [],
|
||||||
|
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)],
|
||||||
|
params: {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
storeId: "",
|
||||||
|
categoryId: "",
|
||||||
|
brandId: "",
|
||||||
|
clientType: "",
|
||||||
|
keyword: "",
|
||||||
|
sortType: "PRICE",
|
||||||
|
},
|
||||||
|
categoryList: [],
|
||||||
|
categoryProps: { value: "id", label: "name", children: "children", checkStrictly: true, emitPath: false },
|
||||||
|
brandList: [],
|
||||||
|
clientTypeOptions: CLIENT_TYPE_OPTIONS,
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
shops: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDefaultRange();
|
||||||
|
this.loadOptions();
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initDefaultRange() {
|
||||||
|
const today = new Date();
|
||||||
|
this.queryDate = this.formatDay(today);
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
this.dateRange = [this.formatDate(start, true), this.formatDate(end, false)];
|
||||||
|
},
|
||||||
|
formatDay(date) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
},
|
||||||
|
onDateTypeChange() {
|
||||||
|
if (this.dateType === "DAY" && !this.queryDate) {
|
||||||
|
this.queryDate = this.formatDay(new Date());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadOptions() {
|
||||||
|
getCategoryTree().then((res) => {
|
||||||
|
if (res.success) this.categoryList = res.result || [];
|
||||||
|
});
|
||||||
|
getBrandListData().then((res) => {
|
||||||
|
if (res.success) this.brandList = res.result || [];
|
||||||
|
});
|
||||||
|
getShopListData({ pageNumber: 1, pageSize: 200 }).then((res) => {
|
||||||
|
if (res.success) this.shops = res.result.records || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatDate(date, startOfDay) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
|
||||||
|
},
|
||||||
|
buildParams() {
|
||||||
|
const p = { ...this.params, reportDateType: this.dateType };
|
||||||
|
if (this.dateType === "DAY") {
|
||||||
|
if (this.queryDate) {
|
||||||
|
p.queryDate = `${this.queryDate} 00:00:00`;
|
||||||
|
}
|
||||||
|
} else if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
p.startTime = this.dateRange[0];
|
||||||
|
p.endTime = this.dateRange[1];
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.params.pageNumber = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
API_Statistics.getGoodsComparisonReport(this.buildParams()).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.success) {
|
||||||
|
this.data = res.result.records || [];
|
||||||
|
this.total = res.result.total || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
API_Statistics.exportGoodsComparisonReport(this.buildParams()).then((blob) => {
|
||||||
|
downloadBlob(blob, "商品同比环比报表.xlsx");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
return this.$filters.unitPrice(val, "¥");
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
return `${Number(val).toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
rateClass(rate) {
|
||||||
|
if (!rate) return "";
|
||||||
|
if (rate.startsWith("+")) return "trend-up";
|
||||||
|
if (rate.startsWith("-") && rate !== "0%") return "trend-down";
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.goods-comparison-form .filter-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.goods-comparison-form .filter-row + .filter-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.trend-up {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.trend-down {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
160
manager/src/views/statistics/goods-sales-summary.vue
Normal file
160
manager/src/views/statistics/goods-sales-summary.vue
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card>
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="日期范围">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品关键词">
|
||||||
|
<el-input v-model="params.keyword" clearable placeholder="商品名称" style="width: 180px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-select v-model="params.sortType" style="width: 120px">
|
||||||
|
<el-option label="按销售额" value="PRICE" />
|
||||||
|
<el-option label="按销量" value="NUM" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="店铺">
|
||||||
|
<el-select v-model="params.storeId" clearable filterable placeholder="全部店铺" style="width: 200px">
|
||||||
|
<el-option v-for="item in shops" :key="item.id" :label="item.storeName" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="mt_10">
|
||||||
|
<el-table v-loading="loading" border :data="data" style="width: 100%">
|
||||||
|
<el-table-column label="序号" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ (params.pageNumber - 1) * params.pageSize + $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="goodsName" label="销售商品" min-width="160" />
|
||||||
|
<el-table-column prop="goodsId" label="商品ID" width="140" />
|
||||||
|
<el-table-column prop="salesNum" label="商品销售数量" width="110" />
|
||||||
|
<el-table-column prop="salesAmount" label="商品销售金额" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.salesAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="refundNum" label="商品退货数量" width="110" />
|
||||||
|
<el-table-column prop="refundAmount" label="商品退款金额" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.refundAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="netNum" label="商品净销售数量" width="140" />
|
||||||
|
<el-table-column prop="netAmount" label="商品净销售金额" width="145">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.netAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="netAmountPercent" label="占净销售金额百分比" width="150">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.netAmountPercent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="avgPrice" label="商品平均单价" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.avgPrice) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="salePriceAmount" label="商品售价金额" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.salePriceAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="discountAmount" label="商品优惠金额" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.discountAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
class="mt_10"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="loadData"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { getShopListData } from "@/api/shops";
|
||||||
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "goods-sales-summary-report",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dateRange: [],
|
||||||
|
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)],
|
||||||
|
params: { pageNumber: 1, pageSize: 20, storeId: "", keyword: "", sortType: "PRICE" },
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
shops: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDefaultRange();
|
||||||
|
this.loadShops();
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initDefaultRange() {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
this.dateRange = [this.formatDate(start, true), this.formatDate(end, false)];
|
||||||
|
},
|
||||||
|
formatDate(date, startOfDay) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
|
||||||
|
},
|
||||||
|
loadShops() {
|
||||||
|
getShopListData({ pageNumber: 1, pageSize: 200 }).then((res) => {
|
||||||
|
if (res.success) this.shops = res.result.records || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildParams() {
|
||||||
|
const p = { ...this.params };
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
p.startTime = this.dateRange[0];
|
||||||
|
p.endTime = this.dateRange[1];
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.params.pageNumber = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
API_Statistics.getGoodsSalesSummaryReport(this.buildParams()).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.success) {
|
||||||
|
this.data = res.result.records || [];
|
||||||
|
this.total = res.result.total || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
API_Statistics.exportGoodsSalesSummaryReport(this.buildParams()).then((blob) => {
|
||||||
|
downloadBlob(blob, "商品销售汇总报表.xlsx");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
return this.$filters.unitPrice(val, "¥");
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
return `${Number(val).toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,107 +1,293 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="wrapper">
|
||||||
<el-affix :offset="100">
|
<el-affix :offset="100">
|
||||||
<el-card class="card fixed-bottom">
|
<el-card class="card fixed-bottom">
|
||||||
<affixTime :closeShop="true" @selected="clickBreadcrumb" />
|
<affixTime @selected="clickBreadcrumb" />
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-affix>
|
</el-affix>
|
||||||
|
|
||||||
<el-card class="card">
|
<el-card class="card">
|
||||||
<el-tabs v-model="params.type" @tab-click="handleClickType">
|
<h4>商品概况</h4>
|
||||||
<el-tab-pane label="热门商品订单数量" name="NUM">
|
<div class="metric-grid">
|
||||||
<el-table :data="data" stripe style="width: 100%">
|
<div class="metric-item" v-for="item in metricList" :key="item.key">
|
||||||
<el-table-column prop="goodsName" label="商品名称" min-width="160" />
|
<div class="metric-label">
|
||||||
<el-table-column prop="num" label="销售数量" min-width="120" />
|
<span>{{ item.label }}</span>
|
||||||
<el-table-column label="销售金额" min-width="120">
|
<el-tooltip v-if="item.tip" :content="item.tip" placement="top">
|
||||||
|
<el-icon class="metric-tip"><QuestionFilled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
<template v-if="item.isCount">
|
||||||
|
{{ formatCount(item.metric) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $filters.unitPrice(metricCurrent(item.metric), "¥") }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="metric-rate" :class="rateClass(item.metric)">
|
||||||
|
环比 {{ rateText(item.metric) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>商品排行</h4>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :xs="24" :md="12">
|
||||||
|
<div class="rank-title">退货排行榜 TOP10</div>
|
||||||
|
<el-table :data="refundRank" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="rank" label="排名" width="70" />
|
||||||
|
<el-table-column prop="skuName" label="商品名称" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="退款金额" min-width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<priceColorScheme v-if="row" :value="row.price" :color="$mainColor" />
|
{{ $filters.unitPrice(row.amount || 0, "¥") }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-col>
|
||||||
<el-tab-pane label="热门商品订单金额" name="PRICE">
|
<el-col :xs="24" :md="12">
|
||||||
<el-table :data="data" stripe style="width: 100%">
|
<div class="rank-title">畅销排行榜 TOP10</div>
|
||||||
<el-table-column prop="goodsName" label="商品名称" min-width="160" />
|
<el-table :data="salesRank" stripe style="width: 100%">
|
||||||
<el-table-column prop="num" label="销售数量" min-width="120" />
|
<el-table-column prop="rank" label="排名" width="70" />
|
||||||
<el-table-column label="销售金额" min-width="120">
|
<el-table-column prop="skuName" label="商品" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column label="净销售额" min-width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<priceColorScheme v-if="row" :value="row.price" :color="$mainColor" />
|
{{ $filters.unitPrice(row.amount || 0, "¥") }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
</el-tab-pane>
|
</el-col>
|
||||||
</el-tabs>
|
</el-row>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>分类排行</h4>
|
||||||
|
<el-table :data="categoryRank" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="rank" label="排名" width="70" />
|
||||||
|
<el-table-column prop="categoryName" label="分类名称" min-width="180" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="num" label="销售数量" min-width="120" />
|
||||||
|
<el-table-column label="销售金额" min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ $filters.unitPrice(row.price || 0, "¥") }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as API_Goods from "@/api/goods";
|
import * as API_Statistics from "@/api/statistics";
|
||||||
import affixTime from "@/components/affix-time";
|
import affixTime from "@/components/affix-time";
|
||||||
|
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: { affixTime, QuestionFilled },
|
||||||
affixTime,
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
params: {
|
params: {
|
||||||
searchType: "LAST_SEVEN",
|
searchType: "LAST_SEVEN",
|
||||||
year: "",
|
year: "",
|
||||||
month: "",
|
month: "",
|
||||||
shopId: "",
|
storeId: "",
|
||||||
type: "NUM",
|
|
||||||
},
|
},
|
||||||
data: [],
|
overview: {},
|
||||||
|
refundRank: [],
|
||||||
|
salesRank: [],
|
||||||
|
categoryRank: [],
|
||||||
|
metricDefs: [
|
||||||
|
{
|
||||||
|
key: "netSalesAmount",
|
||||||
|
label: "商品净销售金额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "商品净销售金额=商品销售金额-商品退款金额(扣除退款,不包含储值充值、商品返现,运费)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "salePriceAmount",
|
||||||
|
label: "商品售价金额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "商品售价金额=商品净销售金额+商品优惠金额,统计时间范围内销售商品折前金额(扣除退款,不包含储值充值、商品返现,运费)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "discountAmount",
|
||||||
|
label: "商品优惠金额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "统计时间范围内,商品销售时的优惠金额(扣除退款,不包含储值充值、商品返现,运费)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "salesAmount",
|
||||||
|
label: "商品销售金额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "统计时间范围内,商品折扣后支付金额(不扣除退款,不包含储值充值、商品返现,运费)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "refundAmount",
|
||||||
|
label: "商品退款金额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "统计时间范围内,商品成功退款金额(不包含储值充值、商品返现,运费)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "netSalesNum",
|
||||||
|
label: "商品净销售数量",
|
||||||
|
isCount: true,
|
||||||
|
tip: "商品净销售数量=商品销售数量-商品退货数量(含退款,不包含储值充值、商品返现,运费)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "salesNum",
|
||||||
|
label: "商品销售数量",
|
||||||
|
isCount: true,
|
||||||
|
tip: "按客户支付完成时间统计的订单商品销售数量(包含商品下单,不包含储值充值)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "refundNum",
|
||||||
|
label: "商品退货数量",
|
||||||
|
isCount: true,
|
||||||
|
tip: "按商家同意退款时间统计的退款单商品退货数量(不包含储值退款、商品返现,运费)",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
computed: {
|
||||||
handleClickType(tab) {
|
metricList() {
|
||||||
this.params.type = tab.paneName;
|
return this.metricDefs.map((def) => ({
|
||||||
this.getData();
|
...def,
|
||||||
},
|
metric: this.overview[def.key] || {},
|
||||||
clickBreadcrumb(item) {
|
}));
|
||||||
const type = this.params.type;
|
|
||||||
this.params = { ...item };
|
|
||||||
this.params.type = type;
|
|
||||||
this.getData();
|
|
||||||
},
|
|
||||||
getData() {
|
|
||||||
Promise.all([API_Goods.goodsStatistics(this.params)]).then((res) => {
|
|
||||||
if (res[0].result) {
|
|
||||||
this.data = res[0].result;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
watch: {
|
||||||
this.getData();
|
params: {
|
||||||
|
handler() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickBreadcrumb(item) {
|
||||||
|
this.params = {
|
||||||
|
searchType: item.searchType || "",
|
||||||
|
year: item.year || "",
|
||||||
|
month: item.month || "",
|
||||||
|
storeId: item.storeId || "",
|
||||||
|
startTime: item.startTime || "",
|
||||||
|
endTime: item.endTime || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
metricCurrent(metric) {
|
||||||
|
if (!metric || metric.current == null) return 0;
|
||||||
|
return Number(metric.current) || 0;
|
||||||
|
},
|
||||||
|
formatCount(metric) {
|
||||||
|
return Math.round(this.metricCurrent(metric));
|
||||||
|
},
|
||||||
|
rateText(metric) {
|
||||||
|
if (!metric || !metric.rate) return "0%";
|
||||||
|
return metric.rate;
|
||||||
|
},
|
||||||
|
rateClass(metric) {
|
||||||
|
const trend = metric && metric.trend;
|
||||||
|
if (trend === "UP") return "up";
|
||||||
|
if (trend === "DOWN") return "down";
|
||||||
|
return "flat";
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
const query = { ...this.params };
|
||||||
|
const categoryQuery = { ...query, type: "PRICE" };
|
||||||
|
const [overviewRes, refundRes, salesRes, categoryRes] = await Promise.all([
|
||||||
|
API_Statistics.getGoodsOverview(query),
|
||||||
|
API_Statistics.getGoodsRefundRank(query),
|
||||||
|
API_Statistics.getGoodsSalesRank(query),
|
||||||
|
API_Statistics.getGoodsCategoryRank(categoryQuery),
|
||||||
|
]);
|
||||||
|
if (overviewRes && overviewRes.success) {
|
||||||
|
this.overview = overviewRes.result || {};
|
||||||
|
}
|
||||||
|
if (refundRes && refundRes.success) {
|
||||||
|
this.refundRank = refundRes.result || [];
|
||||||
|
}
|
||||||
|
if (salesRes && salesRes.success) {
|
||||||
|
this.salesRank = salesRes.result || [];
|
||||||
|
}
|
||||||
|
if (categoryRes && categoryRes.success) {
|
||||||
|
const list = categoryRes.result || [];
|
||||||
|
this.categoryRank = list.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
rank: index + 1,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.page-col {
|
.wrapper {
|
||||||
text-align: right;
|
padding-bottom: 200px;
|
||||||
margin: 10px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.order-col {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
margin-right: 8px;
|
|
||||||
padding: 16px;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.order-list {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tips {
|
|
||||||
margin: 0 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.metric-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.metric-item {
|
||||||
|
background: #f3f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
min-height: 110px;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.metric-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $theme_color;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.metric-rate {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.metric-rate.up {
|
||||||
|
color: #f5222d;
|
||||||
|
}
|
||||||
|
.metric-rate.down {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
.metric-rate.flat {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.rank-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.metric-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.rank-title {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
171
manager/src/views/statistics/marketing.vue
Normal file
171
manager/src/views/statistics/marketing.vue
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<el-affix :offset="100">
|
||||||
|
<el-card class="card fixed-bottom">
|
||||||
|
<affixTime @selected="clickBreadcrumb" />
|
||||||
|
</el-card>
|
||||||
|
</el-affix>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>营销活动</h4>
|
||||||
|
<div class="metric-grid">
|
||||||
|
<div class="metric-item" v-for="item in activityMetrics" :key="item.key">
|
||||||
|
<div class="metric-label">{{ item.label }}</div>
|
||||||
|
<div class="metric-value">{{ formatNumber(overview[item.key]) }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>优惠券与优惠</h4>
|
||||||
|
<div class="metric-grid">
|
||||||
|
<div class="metric-item" v-for="item in couponMetrics" :key="item.key">
|
||||||
|
<div class="metric-label">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<el-tooltip v-if="item.tip" :content="item.tip" placement="top">
|
||||||
|
<el-icon class="metric-tip"><QuestionFilled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
<template v-if="item.isMoney">
|
||||||
|
{{ $filters.unitPrice(overview[item.key] || 0, "¥") }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ formatNumber(overview[item.key]) }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import affixTime from "@/components/affix-time";
|
||||||
|
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { affixTime, QuestionFilled },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
searchType: "LAST_SEVEN",
|
||||||
|
year: "",
|
||||||
|
month: "",
|
||||||
|
storeId: "",
|
||||||
|
},
|
||||||
|
overview: {},
|
||||||
|
activityMetrics: [
|
||||||
|
{ key: "couponActiveNum", label: "进行中优惠券" },
|
||||||
|
{ key: "seckillActiveNum", label: "进行中秒杀" },
|
||||||
|
{ key: "pintuanActiveNum", label: "进行中拼团" },
|
||||||
|
{ key: "fullDiscountActiveNum", label: "进行中满减" },
|
||||||
|
{ key: "kanjiaActiveNum", label: "进行中砍价商品" },
|
||||||
|
{ key: "pointsGoodsActiveNum", label: "进行中积分商品" },
|
||||||
|
],
|
||||||
|
couponMetrics: [
|
||||||
|
{
|
||||||
|
key: "couponPublishNum",
|
||||||
|
label: "优惠券发放量",
|
||||||
|
tip: "优惠券配置的累计发放数量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "couponReceivedNum",
|
||||||
|
label: "优惠券领取量",
|
||||||
|
tip: "客户已领取的优惠券数量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "couponUsedNum",
|
||||||
|
label: "优惠券核销量",
|
||||||
|
tip: "客户已使用的优惠券数量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "discountAmount",
|
||||||
|
label: "活动优惠总额",
|
||||||
|
isMoney: true,
|
||||||
|
tip: "统计周期内订单产生的优惠金额合计",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
params: {
|
||||||
|
handler() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickBreadcrumb(item) {
|
||||||
|
this.params = {
|
||||||
|
searchType: item.searchType || "",
|
||||||
|
year: item.year || "",
|
||||||
|
month: item.month || "",
|
||||||
|
storeId: item.storeId || "",
|
||||||
|
startTime: item.startTime || "",
|
||||||
|
endTime: item.endTime || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
formatNumber(val) {
|
||||||
|
if (val == null || val === "") return 0;
|
||||||
|
const num = Number(val);
|
||||||
|
return Number.isNaN(num) ? 0 : Math.round(num);
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
const res = await API_Statistics.getMarketingOverview({ ...this.params });
|
||||||
|
if (res && res.success) {
|
||||||
|
this.overview = res.result || {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.wrapper {
|
||||||
|
padding-bottom: 200px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.metric-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.metric-item {
|
||||||
|
background: #f3f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
min-height: 90px;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.metric-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.metric-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -2,167 +2,311 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<el-affix :offset="100">
|
<el-affix :offset="100">
|
||||||
<el-card class="card fixed-bottom">
|
<el-card class="card fixed-bottom">
|
||||||
<affixTime :closeShop="true" @selected="clickBreadcrumb" />
|
<affixTime @selected="clickBreadcrumb" />
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-affix>
|
</el-affix>
|
||||||
|
|
||||||
<el-card class="card">
|
<el-card class="card">
|
||||||
<div>
|
<h4>会员概况</h4>
|
||||||
<h4>
|
<div class="metric-grid">
|
||||||
客户增长趋势
|
<div class="metric-item" v-for="item in metricList" :key="item.key">
|
||||||
<el-button style="margin-left: 10px" size="small" @click="toggleMemberCount">
|
<div class="metric-label">
|
||||||
{{ enableShowMemberCount ? "关闭" : "显示" }}用户总人数
|
<span>{{ item.label }}</span>
|
||||||
</el-button>
|
<el-tooltip v-if="item.tip" :content="item.tip" placement="top">
|
||||||
</h4>
|
<el-icon class="metric-tip"><QuestionFilled /></el-icon>
|
||||||
<div id="orderChart"></div>
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
<template v-if="item.isMoney">
|
||||||
|
{{ $filters.unitPrice(metricCurrent(item.metric), "¥") }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ formatCount(item.metric) }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="metric-rate" :class="rateClass(item.metric)">
|
||||||
|
环比 {{ rateText(item.metric) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="card">
|
<el-card class="card">
|
||||||
<div>
|
<h4>会员人数趋势图</h4>
|
||||||
<h4>客户增长报表</h4>
|
<div id="memberTrendChart"></div>
|
||||||
<el-table class="mt_10" stripe :data="data" style="width: 100%">
|
</el-card>
|
||||||
<el-table-column prop="createDate" label="日期" min-width="120" sortable />
|
|
||||||
<el-table-column prop="memberCount" label="当前会员" min-width="120" />
|
<el-card class="card">
|
||||||
<el-table-column prop="newlyAdded" label="新增会员" min-width="120" />
|
<h4>客户分析</h4>
|
||||||
<el-table-column prop="activeQuantity" label="活跃会员" min-width="120" />
|
<div class="analysis-grid">
|
||||||
</el-table>
|
<div class="analysis-item">
|
||||||
|
<div class="metric-label">活跃客户数</div>
|
||||||
|
<div class="metric-value">{{ analysis.activeMemberNum || 0 }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="analysis-item">
|
||||||
|
<div class="metric-label">复购率</div>
|
||||||
|
<div class="metric-value">{{ formatPercent(analysis.repurchaseRate) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="analysis-item">
|
||||||
|
<div class="metric-label">新客占比</div>
|
||||||
|
<div class="metric-value">{{ formatPercent(analysis.newCustomerRatio) }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="analysis-item">
|
||||||
|
<div class="metric-label">老客占比</div>
|
||||||
|
<div class="metric-value">{{ formatPercent(analysis.oldCustomerRatio) }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>客户分布</h4>
|
||||||
|
<el-row :gutter="16">
|
||||||
|
<el-col :xs="24" :md="12">
|
||||||
|
<div class="rank-title">性别分布</div>
|
||||||
|
<el-table :data="genderList" stripe style="width: 100%">
|
||||||
|
<el-table-column label="性别" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ sexText(row.sex) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="num" label="客户数" min-width="100" />
|
||||||
|
<el-table-column label="占比" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPercent(row.proportion) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
<el-col :xs="24" :md="12">
|
||||||
|
<div class="rank-title">地域分布</div>
|
||||||
|
<el-table :data="regionList" stripe style="width: 100%" max-height="360">
|
||||||
|
<el-table-column prop="region" label="地域" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="num" label="客户数" min-width="100" />
|
||||||
|
<el-table-column label="占比" min-width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPercent(row.proportion) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import * as API_Member from "@/api/member";
|
import * as API_Statistics from "@/api/statistics";
|
||||||
import { Chart } from "@antv/g2";
|
|
||||||
import affixTime from "@/components/affix-time";
|
import affixTime from "@/components/affix-time";
|
||||||
|
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||||
|
import { Chart } from "@antv/g2";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { affixTime },
|
components: { affixTime, QuestionFilled },
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
orderChart: "",
|
|
||||||
params: {
|
params: {
|
||||||
searchType: "LAST_SEVEN",
|
searchType: "LAST_SEVEN",
|
||||||
year: "",
|
year: "",
|
||||||
month: "",
|
month: "",
|
||||||
shopId: "",
|
storeId: "",
|
||||||
},
|
},
|
||||||
data: [],
|
overview: {},
|
||||||
enableShowMemberCount: false,
|
analysis: {},
|
||||||
year: "",
|
genderList: [],
|
||||||
|
regionList: [],
|
||||||
|
trendChart: null,
|
||||||
|
metricDefs: [
|
||||||
|
{
|
||||||
|
key: "totalMemberNum",
|
||||||
|
label: "累积会员数",
|
||||||
|
isMoney: false,
|
||||||
|
tip: "累积会员数:合计会员数量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "newMemberNum",
|
||||||
|
label: "新增会员数",
|
||||||
|
isMoney: false,
|
||||||
|
tip: "新增会员数:周期内新增的会员数量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "payMemberNum",
|
||||||
|
label: "支付会员数",
|
||||||
|
isMoney: false,
|
||||||
|
tip: "支付会员数:周期内支付的会员数量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "rechargeMemberNum",
|
||||||
|
label: "储值会员数",
|
||||||
|
isMoney: false,
|
||||||
|
tip: "储值会员数:周期内充值的会员数量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "payAmount",
|
||||||
|
label: "会员支付金额",
|
||||||
|
isMoney: true,
|
||||||
|
tip: "会员支付金额:周期内会员支付的金额,四舍五入保留2位小数",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "payOrderNum",
|
||||||
|
label: "会员支付订单数",
|
||||||
|
isMoney: false,
|
||||||
|
tip: "会员支付订单数:周期内会员支付的订单数量",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "customerPrice",
|
||||||
|
label: "会员客单价",
|
||||||
|
isMoney: true,
|
||||||
|
tip: "会员客单价:周期内支付金额/支付人数,四舍五入保留2位小数",
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
metricList() {
|
||||||
|
return this.metricDefs.map((def) => ({
|
||||||
|
...def,
|
||||||
|
metric: this.overview[def.key] || {},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
params: {
|
params: {
|
||||||
handler() {
|
handler() {
|
||||||
this.init();
|
this.loadData();
|
||||||
},
|
},
|
||||||
deep: true,
|
deep: true,
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
year(val) {
|
},
|
||||||
this.params.year = new Date(val).getFullYear();
|
beforeUnmount() {
|
||||||
},
|
if (this.trendChart) {
|
||||||
|
this.trendChart.destroy();
|
||||||
|
this.trendChart = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleMemberCount() {
|
|
||||||
this.enableShowMemberCount = !this.enableShowMemberCount;
|
|
||||||
this.initMemberChart();
|
|
||||||
},
|
|
||||||
initMemberChart() {
|
|
||||||
const count = [];
|
|
||||||
const newly = [];
|
|
||||||
const actives = [];
|
|
||||||
|
|
||||||
this.data.forEach((item) => {
|
|
||||||
if (this.enableShowMemberCount && (item.memberCount != "" || item.memberCount != null)) {
|
|
||||||
count.push({
|
|
||||||
createDate: item.createDate,
|
|
||||||
memberCount: item.memberCount,
|
|
||||||
title: "当前会员数量",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!this.enableShowMemberCount && (item.newlyAdded != "" || item.newlyAdded != null)) {
|
|
||||||
newly.push({
|
|
||||||
createDate: item.createDate,
|
|
||||||
memberCount: item.newlyAdded,
|
|
||||||
title: "新增会员数量",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (!this.enableShowMemberCount && (item.activeQuantity != "" || item.activeQuantity != null)) {
|
|
||||||
actives.push({
|
|
||||||
createDate: item.createDate,
|
|
||||||
memberCount: item.activeQuantity,
|
|
||||||
title: "当日活跃数量",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const chartData = [...count, ...newly, ...actives];
|
|
||||||
|
|
||||||
this.orderChart.data(chartData);
|
|
||||||
this.orderChart.scale({
|
|
||||||
activeQuantity: {
|
|
||||||
range: [0, 1],
|
|
||||||
nice: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.orderChart.tooltip({
|
|
||||||
showCrosshairs: true,
|
|
||||||
shared: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.orderChart
|
|
||||||
.line()
|
|
||||||
.position("createDate*memberCount")
|
|
||||||
.color("title")
|
|
||||||
.label("memberCount")
|
|
||||||
.shape("smooth");
|
|
||||||
|
|
||||||
this.orderChart
|
|
||||||
.point()
|
|
||||||
.position("createDate*memberCount")
|
|
||||||
.color("title")
|
|
||||||
.label("memberCount")
|
|
||||||
.shape("circle")
|
|
||||||
.style({
|
|
||||||
stroke: "#fff",
|
|
||||||
lineWidth: 1,
|
|
||||||
});
|
|
||||||
this.orderChart.area().position("createDate*memberCount").color("title").shape("smooth");
|
|
||||||
|
|
||||||
this.orderChart.render();
|
|
||||||
},
|
|
||||||
clickBreadcrumb(item) {
|
clickBreadcrumb(item) {
|
||||||
this.params = { ...item };
|
this.params = {
|
||||||
|
...this.params,
|
||||||
|
searchType: item.searchType || "",
|
||||||
|
year: item.year || "",
|
||||||
|
month: item.month || "",
|
||||||
|
storeId: item.storeId || "",
|
||||||
|
startTime: item.startTime || "",
|
||||||
|
endTime: item.endTime || "",
|
||||||
|
};
|
||||||
},
|
},
|
||||||
init() {
|
metricCurrent(metric) {
|
||||||
this.orderChart ? this.orderChart.clear() : "";
|
if (!metric || metric.current == null) return 0;
|
||||||
API_Member.getMemberStatistics(this.params).then((res) => {
|
return Number(metric.current) || 0;
|
||||||
if (res.result) {
|
},
|
||||||
res.result.forEach((item) => {
|
formatCount(metric) {
|
||||||
item.activeQuantity += "";
|
return Math.round(this.metricCurrent(metric));
|
||||||
|
},
|
||||||
|
rateText(metric) {
|
||||||
|
if (!metric || !metric.rate) return "0%";
|
||||||
|
return metric.rate;
|
||||||
|
},
|
||||||
|
rateClass(metric) {
|
||||||
|
const trend = metric && metric.trend;
|
||||||
|
if (trend === "UP") return "up";
|
||||||
|
if (trend === "DOWN") return "down";
|
||||||
|
return "flat";
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
const num = Number(val);
|
||||||
|
if (Number.isNaN(num)) return val;
|
||||||
|
return `${num}%`;
|
||||||
|
},
|
||||||
|
sexText(sex) {
|
||||||
|
if (sex === 1 || sex === "1") return "男";
|
||||||
|
if (sex === 0 || sex === "0") return "女";
|
||||||
|
return "未知";
|
||||||
|
},
|
||||||
|
formatTrendDate(val) {
|
||||||
|
if (!val) return "";
|
||||||
|
if (typeof val === "string") return val.slice(0, 10);
|
||||||
|
const d = new Date(val);
|
||||||
|
if (Number.isNaN(d.getTime())) return String(val);
|
||||||
|
const m = `${d.getMonth() + 1}`.padStart(2, "0");
|
||||||
|
const day = `${d.getDate()}`.padStart(2, "0");
|
||||||
|
return `${d.getFullYear()}-${m}-${day}`;
|
||||||
|
},
|
||||||
|
renderTrendChart(list) {
|
||||||
|
const chartData = (list || []).map((item) => ({
|
||||||
|
date: this.formatTrendDate(item.date),
|
||||||
|
newlyAdded: Number(item.newlyAdded) || 0,
|
||||||
|
title: "新增会员数",
|
||||||
|
}));
|
||||||
|
if (this.trendChart) {
|
||||||
|
this.trendChart.destroy();
|
||||||
|
this.trendChart = null;
|
||||||
|
}
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.trendChart = new Chart({
|
||||||
|
container: "memberTrendChart",
|
||||||
|
autoFit: true,
|
||||||
|
height: 360,
|
||||||
|
padding: [40, 40, 50, 50],
|
||||||
|
});
|
||||||
|
this.trendChart.data(chartData);
|
||||||
|
this.trendChart.scale({
|
||||||
|
newlyAdded: {
|
||||||
|
nice: true,
|
||||||
|
min: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.trendChart.tooltip({
|
||||||
|
showCrosshairs: true,
|
||||||
|
shared: true,
|
||||||
|
});
|
||||||
|
this.trendChart
|
||||||
|
.line()
|
||||||
|
.position("date*newlyAdded")
|
||||||
|
.color("title")
|
||||||
|
.shape("smooth");
|
||||||
|
this.trendChart
|
||||||
|
.point()
|
||||||
|
.position("date*newlyAdded")
|
||||||
|
.color("title")
|
||||||
|
.shape("circle")
|
||||||
|
.style({
|
||||||
|
stroke: "#fff",
|
||||||
|
lineWidth: 1,
|
||||||
});
|
});
|
||||||
this.data = res.result;
|
this.trendChart.render();
|
||||||
|
|
||||||
if (!this.orderChart) {
|
|
||||||
this.orderChart = new Chart({
|
|
||||||
container: "orderChart",
|
|
||||||
autoFit: true,
|
|
||||||
height: 500,
|
|
||||||
padding: [70, 70, 70, 70],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.initMemberChart();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
async loadData() {
|
||||||
mounted() {
|
const query = { ...this.params };
|
||||||
this.year = new Date();
|
const [overviewRes, analysisRes, genderRes, regionRes, trendRes] = await Promise.all([
|
||||||
|
API_Statistics.getMemberOverview(query),
|
||||||
|
API_Statistics.getMemberAnalysis(query),
|
||||||
|
API_Statistics.getMemberGenderDistribution(),
|
||||||
|
API_Statistics.getMemberRegionDistribution(),
|
||||||
|
API_Statistics.getMemberNewTrend(query),
|
||||||
|
]);
|
||||||
|
if (overviewRes && overviewRes.success) {
|
||||||
|
this.overview = overviewRes.result || {};
|
||||||
|
}
|
||||||
|
if (analysisRes && analysisRes.success) {
|
||||||
|
this.analysis = analysisRes.result || {};
|
||||||
|
}
|
||||||
|
if (genderRes && genderRes.success) {
|
||||||
|
this.genderList = genderRes.result || [];
|
||||||
|
}
|
||||||
|
if (regionRes && regionRes.success) {
|
||||||
|
this.regionList = regionRes.result || [];
|
||||||
|
}
|
||||||
|
if (trendRes && trendRes.success) {
|
||||||
|
this.renderTrendChart(trendRes.result || []);
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
.wrapper {
|
.wrapper {
|
||||||
padding-bottom: 200px;
|
padding-bottom: 200px;
|
||||||
@@ -170,7 +314,67 @@ export default {
|
|||||||
.card {
|
.card {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.mt_10 {
|
h4 {
|
||||||
margin-top: 10px;
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.metric-grid,
|
||||||
|
.analysis-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.metric-item,
|
||||||
|
.analysis-item {
|
||||||
|
background: #f3f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
min-height: 110px;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.metric-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $theme_color;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.metric-rate {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.metric-rate.up {
|
||||||
|
color: #f5222d;
|
||||||
|
}
|
||||||
|
.metric-rate.down {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
.metric-rate.flat {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.rank-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.metric-grid,
|
||||||
|
.analysis-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.rank-title {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
351
manager/src/views/statistics/overview.vue
Normal file
351
manager/src/views/statistics/overview.vue
Normal file
@@ -0,0 +1,351 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<el-affix :offset="100">
|
||||||
|
<el-card class="card fixed-bottom">
|
||||||
|
<affixTime @selected="clickBreadcrumb" />
|
||||||
|
</el-card>
|
||||||
|
</el-affix>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>营业概况</h4>
|
||||||
|
<div class="metric-grid">
|
||||||
|
<div class="metric-item" v-for="item in metricList" :key="item.key">
|
||||||
|
<div class="metric-label">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<el-tooltip v-if="item.tip" :content="item.tip" placement="top">
|
||||||
|
<el-icon class="metric-tip"><QuestionFilled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
<template v-if="item.isCount">
|
||||||
|
{{ formatCount(item.metric) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $filters.unitPrice(metricCurrent(item.metric), "¥") }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="metric-rate" :class="rateClass(item.metric)">
|
||||||
|
环比 {{ rateText(item.metric) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>收款构成</h4>
|
||||||
|
<el-table :data="sourceList" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="payType" label="支付方式" min-width="120" />
|
||||||
|
<el-table-column label="收款合计" min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ $filters.unitPrice(row.total || 0, "¥") }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="营业收入" min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ $filters.unitPrice(row.income || 0, "¥") }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="新增储值金额" min-width="140">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ $filters.unitPrice(row.recharge || 0, "¥") }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>营业构成</h4>
|
||||||
|
<div class="composition-sections">
|
||||||
|
<div class="composition-block">
|
||||||
|
<div class="block-title">订单分类构成</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>到店自提</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.storeSelf || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>快递发货</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.express || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>线上无需配送</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.online || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="composition-block">
|
||||||
|
<div class="block-title">营业收入</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>商品销售</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.income || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>运费</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.freight || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>商品返现(分销返佣)</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.incomeBack || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>商品销售+费用构成</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.incomeComposition || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="composition-block">
|
||||||
|
<div class="block-title">退款统计</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>退款订单笔数</span>
|
||||||
|
<span>{{ composition.refundOrderNum || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>退款金额</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.refund || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>退款率</span>
|
||||||
|
<span>{{ formatPercent(composition.refundRate) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="composition-block">
|
||||||
|
<div class="block-title">消费指标</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>支付金额</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.pay || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>折后笔单价</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.price || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>支付人数</span>
|
||||||
|
<span>{{ composition.payNum || 0 }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="block-row">
|
||||||
|
<span>折后客单价</span>
|
||||||
|
<span>{{ $filters.unitPrice(composition.priceNum || 0, "¥") }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import affixTime from "@/components/affix-time";
|
||||||
|
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { affixTime, QuestionFilled },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
params: {
|
||||||
|
searchType: "LAST_SEVEN",
|
||||||
|
year: "",
|
||||||
|
month: "",
|
||||||
|
storeId: "",
|
||||||
|
},
|
||||||
|
overview: {},
|
||||||
|
sourceList: [],
|
||||||
|
composition: {},
|
||||||
|
metricDefs: [
|
||||||
|
{
|
||||||
|
key: "income",
|
||||||
|
label: "营业收入",
|
||||||
|
isCount: false,
|
||||||
|
tip: "营业收入:订单扣除退款折扣后金额(不含储值充值)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "turnover",
|
||||||
|
label: "营业额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "营业额:营业额=营业收入+优惠金额,订单扣除退款折扣前金额(不含储值充值)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "discount",
|
||||||
|
label: "优惠金额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "优惠金额:订单的优惠金额(扣除退款,不含储值充值)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "incomeNoStoreValue",
|
||||||
|
label: "营业收入不含充值",
|
||||||
|
isCount: false,
|
||||||
|
tip: "营业收入不含储值金额:订单扣除退款折扣后金额(不含储值充值)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "payOrderNum",
|
||||||
|
label: "支付订单数",
|
||||||
|
isCount: true,
|
||||||
|
tip: "支付订单数:按客户支付完成时间统计的成功付款的订单数(不含退款、储值充值)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "recharge",
|
||||||
|
label: "新增充值金额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "新增充值金额:按客户支付完成时间统计的客户储值充值本金金额",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "rechargeUse",
|
||||||
|
label: "使用充值金额",
|
||||||
|
isCount: false,
|
||||||
|
tip: "使用储值支付本金金额:统计时间范围内,支付和退款成功的订单中,使用储值支付且扣除退款的金额",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
metricList() {
|
||||||
|
return this.metricDefs.map((def) => ({
|
||||||
|
...def,
|
||||||
|
metric: this.overview[def.key] || {},
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
params: {
|
||||||
|
handler() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickBreadcrumb(item) {
|
||||||
|
this.params = {
|
||||||
|
searchType: item.searchType || "",
|
||||||
|
year: item.year || "",
|
||||||
|
month: item.month || "",
|
||||||
|
storeId: item.storeId || "",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
metricCurrent(metric) {
|
||||||
|
if (!metric || metric.current == null) return 0;
|
||||||
|
return Number(metric.current) || 0;
|
||||||
|
},
|
||||||
|
formatCount(metric) {
|
||||||
|
return Math.round(this.metricCurrent(metric));
|
||||||
|
},
|
||||||
|
rateText(metric) {
|
||||||
|
if (!metric || !metric.rate) return "0%";
|
||||||
|
return metric.rate;
|
||||||
|
},
|
||||||
|
rateClass(metric) {
|
||||||
|
const trend = metric && metric.trend;
|
||||||
|
if (trend === "UP") return "up";
|
||||||
|
if (trend === "DOWN") return "down";
|
||||||
|
return "flat";
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
const num = Number(val);
|
||||||
|
if (Number.isNaN(num)) return val;
|
||||||
|
return `${num}%`;
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
const query = { ...this.params };
|
||||||
|
const [overviewRes, sourceRes, compositionRes] = await Promise.all([
|
||||||
|
API_Statistics.getBusinessOverview(query),
|
||||||
|
API_Statistics.getBusinessSource(query),
|
||||||
|
API_Statistics.getBusinessComposition(query),
|
||||||
|
]);
|
||||||
|
if (overviewRes && overviewRes.success) {
|
||||||
|
this.overview = overviewRes.result || {};
|
||||||
|
}
|
||||||
|
if (sourceRes && sourceRes.success) {
|
||||||
|
this.sourceList = sourceRes.result || [];
|
||||||
|
}
|
||||||
|
if (compositionRes && compositionRes.success) {
|
||||||
|
this.composition = compositionRes.result || {};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.wrapper {
|
||||||
|
padding-bottom: 200px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.metric-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.metric-item {
|
||||||
|
background: #f3f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
min-height: 110px;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.metric-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $theme_color;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.metric-rate {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.metric-rate.up {
|
||||||
|
color: #f5222d;
|
||||||
|
}
|
||||||
|
.metric-rate.down {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
.metric-rate.flat {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
.composition-sections {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.composition-block {
|
||||||
|
background: #f3f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
.block-title {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.block-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px dashed #e5e5e5;
|
||||||
|
}
|
||||||
|
.block-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.metric-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
.composition-sections {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
202
manager/src/views/statistics/points.vue
Normal file
202
manager/src/views/statistics/points.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<div class="wrapper">
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>积分分析</h4>
|
||||||
|
<div class="metric-grid">
|
||||||
|
<div class="metric-item" v-for="item in metricList" :key="item.key">
|
||||||
|
<div class="metric-label">
|
||||||
|
<span>{{ item.label }}</span>
|
||||||
|
<el-tooltip v-if="item.tip" :content="item.tip" placement="top">
|
||||||
|
<el-icon class="metric-tip"><QuestionFilled /></el-icon>
|
||||||
|
</el-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="metric-value">
|
||||||
|
<template v-if="item.isRate">
|
||||||
|
{{ formatPercent(overview[item.key]) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ formatNumber(overview[item.key]) }}
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>客户可用积分分布</h4>
|
||||||
|
<el-table :data="distributionList" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="pointRange" label="积分值区间" min-width="140" />
|
||||||
|
<el-table-column prop="memberNum" label="客户数" min-width="120" />
|
||||||
|
<el-table-column label="占比" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPercent(row.proportion) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>积分累计分发分布</h4>
|
||||||
|
<el-table :data="sourceList" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="sourceName" label="发放途径" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="point" label="发送积分值" min-width="140" />
|
||||||
|
<el-table-column label="发送占比" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPercent(row.proportion) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="card">
|
||||||
|
<h4>客户身份积分累计统计</h4>
|
||||||
|
<el-table :data="identityList" stripe style="width: 100%">
|
||||||
|
<el-table-column prop="identity" label="客户身份" min-width="140" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="totalIssued" label="累计发送积分" min-width="130" />
|
||||||
|
<el-table-column prop="usedPoint" label="累计消耗积分" min-width="130" />
|
||||||
|
<el-table-column prop="available" label="可用积分" min-width="120" />
|
||||||
|
<el-table-column label="可用积分占比" min-width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatPercent(row.proportion) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="人均可用积分" min-width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
{{ formatNumber(row.avgAvailable) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { QuestionFilled } from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: { QuestionFilled },
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
overview: {},
|
||||||
|
distributionList: [],
|
||||||
|
sourceList: [],
|
||||||
|
identityList: [],
|
||||||
|
metricDefs: [
|
||||||
|
{
|
||||||
|
key: "totalIssued",
|
||||||
|
label: "累积发放积分",
|
||||||
|
isRate: false,
|
||||||
|
tip: "累积发放积分:历史累计发放给客户的积分总数",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "availablePoint",
|
||||||
|
label: "可用积分",
|
||||||
|
isRate: false,
|
||||||
|
tip: "可用积分:当前客户账户中尚未消耗的积分总和",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "usedPoint",
|
||||||
|
label: "累积消耗积分",
|
||||||
|
isRate: false,
|
||||||
|
tip: "累积消耗积分:历史累计已消耗的积分",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "usedRate",
|
||||||
|
label: "积分消耗率",
|
||||||
|
isRate: true,
|
||||||
|
tip: "积分消耗率:累积消耗积分 / 累积发放积分",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
metricList() {
|
||||||
|
return this.metricDefs;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatNumber(val) {
|
||||||
|
if (val == null || val === "") return 0;
|
||||||
|
const num = Number(val);
|
||||||
|
if (Number.isNaN(num)) return val;
|
||||||
|
return Math.round(num * 100) / 100;
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
const num = Number(val);
|
||||||
|
if (Number.isNaN(num)) return val;
|
||||||
|
return `${num}%`;
|
||||||
|
},
|
||||||
|
async loadData() {
|
||||||
|
const [overviewRes, distributionRes, sourceRes, identityRes] = await Promise.all([
|
||||||
|
API_Statistics.getPointsOverview(),
|
||||||
|
API_Statistics.getPointsDistribution(),
|
||||||
|
API_Statistics.getPointsSourceDistribution(),
|
||||||
|
API_Statistics.getPointsIdentityStat(),
|
||||||
|
]);
|
||||||
|
if (overviewRes && overviewRes.success) {
|
||||||
|
this.overview = overviewRes.result || {};
|
||||||
|
}
|
||||||
|
if (distributionRes && distributionRes.success) {
|
||||||
|
this.distributionList = distributionRes.result || [];
|
||||||
|
}
|
||||||
|
if (sourceRes && sourceRes.success) {
|
||||||
|
this.sourceList = sourceRes.result || [];
|
||||||
|
}
|
||||||
|
if (identityRes && identityRes.success) {
|
||||||
|
this.identityList = identityRes.result || [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.wrapper {
|
||||||
|
padding-bottom: 40px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 20px 0;
|
||||||
|
}
|
||||||
|
.metric-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.metric-item {
|
||||||
|
background: #f3f5f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px 16px;
|
||||||
|
min-height: 90px;
|
||||||
|
}
|
||||||
|
.metric-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.metric-tip {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #999;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.metric-value {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $theme_color;
|
||||||
|
}
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.metric-grid {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
165
manager/src/views/statistics/sales-order-detail.vue
Normal file
165
manager/src/views/statistics/sales-order-detail.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card>
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="发生日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单号">
|
||||||
|
<el-input v-model="params.orderSn" clearable placeholder="订单编号" style="width: 160px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="买家">
|
||||||
|
<el-input v-model="params.memberName" clearable placeholder="买家名称" style="width: 140px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品关键词">
|
||||||
|
<el-input v-model="params.keyword" clearable placeholder="商品名称" style="width: 160px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="店铺">
|
||||||
|
<el-select v-model="params.storeId" clearable filterable placeholder="全部店铺" style="width: 180px">
|
||||||
|
<el-option v-for="item in shops" :key="item.id" :label="item.storeName" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="mt_10">
|
||||||
|
<el-table v-loading="loading" border :data="data" style="width: 100%">
|
||||||
|
<el-table-column label="序号" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ (params.pageNumber - 1) * params.pageSize + $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="orderRefundSn" label="订单编号|售后单号" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="occurTime" label="发生时间" width="160" />
|
||||||
|
<el-table-column prop="flowTypeName" label="交易类型" width="90" />
|
||||||
|
<el-table-column prop="num" label="成交数量" width="90" align="right" />
|
||||||
|
<el-table-column label="成交金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.transactionAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="售价金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.salePriceAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="优惠金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.discountAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="paymentDetail" label="收款明细" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="discountDetail" label="优惠明细" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="订单来源" width="100">
|
||||||
|
<template #default="{ row }">{{ clientTypeText(row.clientType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
class="mt_10"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="loadData"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { getShopListData } from "@/api/shops";
|
||||||
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "sales-order-detail-report",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dateRange: [],
|
||||||
|
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)],
|
||||||
|
params: {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
storeId: "",
|
||||||
|
orderSn: "",
|
||||||
|
memberName: "",
|
||||||
|
keyword: "",
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
shops: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDefaultRange();
|
||||||
|
this.loadShops();
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initDefaultRange() {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
this.dateRange = [this.formatDate(start, true), this.formatDate(end, false)];
|
||||||
|
},
|
||||||
|
formatDate(date, startOfDay) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
|
||||||
|
},
|
||||||
|
loadShops() {
|
||||||
|
getShopListData({ pageNumber: 1, pageSize: 200 }).then((res) => {
|
||||||
|
if (res.success) this.shops = res.result.records || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildParams() {
|
||||||
|
const p = { ...this.params };
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
p.startTime = this.dateRange[0];
|
||||||
|
p.endTime = this.dateRange[1];
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.params.pageNumber = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
API_Statistics.getSalesOrderDetailReport(this.buildParams()).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.success) {
|
||||||
|
this.data = res.result.records || [];
|
||||||
|
this.total = res.result.total || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
API_Statistics.exportSalesOrderDetailReport(this.buildParams()).then((blob) => {
|
||||||
|
downloadBlob(blob, "销售订单明细报表.xlsx");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
return this.$filters.unitPrice(val, "¥");
|
||||||
|
},
|
||||||
|
clientTypeText(val) {
|
||||||
|
const map = {
|
||||||
|
H5: "移动端",
|
||||||
|
PC: "PC端",
|
||||||
|
WECHAT_MP: "小程序",
|
||||||
|
APP: "移动应用端",
|
||||||
|
UNKNOWN: "未知",
|
||||||
|
};
|
||||||
|
return map[val] || val || "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
185
manager/src/views/statistics/store-performance.vue
Normal file
185
manager/src/views/statistics/store-performance.vue
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card>
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="日期范围">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="店铺">
|
||||||
|
<el-select v-model="params.storeId" clearable filterable placeholder="全部店铺" style="width: 200px">
|
||||||
|
<el-option v-for="item in shops" :key="item.id" :label="item.storeName" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="mt_10">
|
||||||
|
<el-table v-loading="loading" border :data="data" style="width: 100%">
|
||||||
|
<el-table-column label="序号" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ (params.pageNumber - 1) * params.pageSize + $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="storeName" label="店铺名称" min-width="140" />
|
||||||
|
<el-table-column prop="payOrderCount" label="订单支付笔数" width="120" align="right" />
|
||||||
|
<el-table-column label="支付金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.payAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="营业额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.turnover) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="优惠金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.discountAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="折扣率" width="90" align="right">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.discountRate) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="营业收入" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.operatingIncome) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="营业收入占比" width="120" align="right">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.operatingIncomePercent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="笔单价" width="100" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.avgOrderPrice) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="refundCount" label="退款笔数" width="100" align="right" />
|
||||||
|
<el-table-column label="订单退款金额" width="120" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.refundAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="orderConversionRate" label="下单转化率" width="110" align="right" />
|
||||||
|
<el-table-column prop="payConversionRate" label="支付转化率" width="110" align="right" />
|
||||||
|
<el-table-column label="环比差额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.momDiff) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="环比增长率" width="110" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="rateClass(row.momRate)">{{ row.momRate || "0%" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比差额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.yoyDiff) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比增长率" width="110" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="rateClass(row.yoyRate)">{{ row.yoyRate || "0%" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
class="mt_10"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="loadData"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { getShopListData } from "@/api/shops";
|
||||||
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "store-performance-report",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dateRange: [],
|
||||||
|
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)],
|
||||||
|
params: { pageNumber: 1, pageSize: 20, storeId: "" },
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
shops: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDefaultRange();
|
||||||
|
this.loadShops();
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initDefaultRange() {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
this.dateRange = [this.formatDate(start, true), this.formatDate(end, false)];
|
||||||
|
},
|
||||||
|
formatDate(date, startOfDay) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
|
||||||
|
},
|
||||||
|
loadShops() {
|
||||||
|
getShopListData({ pageNumber: 1, pageSize: 200 }).then((res) => {
|
||||||
|
if (res.success) this.shops = res.result.records || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
buildParams() {
|
||||||
|
const p = { ...this.params };
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
p.startTime = this.dateRange[0];
|
||||||
|
p.endTime = this.dateRange[1];
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.params.pageNumber = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
API_Statistics.getStorePerformanceReport(this.buildParams()).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.success) {
|
||||||
|
this.data = res.result.records || [];
|
||||||
|
this.total = res.result.total || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
API_Statistics.exportStorePerformanceReport(this.buildParams()).then((blob) => {
|
||||||
|
downloadBlob(blob, "店铺业绩报表.xlsx");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
return this.$filters.unitPrice(val, "¥");
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
return `${Number(val).toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
rateClass(rate) {
|
||||||
|
if (!rate) return "";
|
||||||
|
if (rate.startsWith("+")) return "trend-up";
|
||||||
|
if (rate.startsWith("-") && rate !== "0%") return "trend-down";
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trend-up {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.trend-down {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,6 +7,7 @@ const resolve = (dir) => path.join(__dirname, dir);
|
|||||||
const enableProduction = process.env.NODE_ENV === "production";
|
const enableProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
publicPath: process.env.VUE_APP_PUBLIC_PATH || "/",
|
||||||
css: {
|
css: {
|
||||||
loaderOptions: {
|
loaderOptions: {
|
||||||
less: {
|
less: {
|
||||||
|
|||||||
@@ -71,8 +71,8 @@
|
|||||||
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
|
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
|
||||||
<script src="<%=js%>"></script>
|
<script src="<%=js%>"></script>
|
||||||
<% } %>
|
<% } %>
|
||||||
<script src="/config.js"></script>
|
<script src="<%= BASE_URL %>config.js"></script>
|
||||||
<script src="/tinymce/tinymce.min.js"></script>
|
<script src="<%= BASE_URL %>tinymce/tinymce.min.js"></script>
|
||||||
<noscript>
|
<noscript>
|
||||||
<strong
|
<strong
|
||||||
>We're sorry doesn't work properly without JavaScript
|
>We're sorry doesn't work properly without JavaScript
|
||||||
|
|||||||
@@ -16,5 +16,38 @@ export const getOrderStatistics = (params) => {
|
|||||||
return getRequest(`/statistics/order/getByPage`, params)
|
return getRequest(`/statistics/order/getByPage`, params)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 店铺业绩报表 */
|
||||||
|
export const getStorePerformanceReport = (params) => {
|
||||||
|
return getRequest('/statistics/report/store-performance', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportStorePerformanceReport = (params) => {
|
||||||
|
return getRequest('/statistics/report/store-performance/export', params, 'blob');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 商品同比环比报表 */
|
||||||
|
export const getGoodsComparisonReport = (params) => {
|
||||||
|
return getRequest('/statistics/report/goods-comparison', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportGoodsComparisonReport = (params) => {
|
||||||
|
return getRequest('/statistics/report/goods-comparison/export', params, 'blob');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 销售订单明细报表 */
|
||||||
|
export const getSalesOrderDetailReport = (params) => {
|
||||||
|
return getRequest('/statistics/report/sales-order-detail', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportSalesOrderDetailReport = (params) => {
|
||||||
|
return getRequest('/statistics/report/sales-order-detail/export', params, 'blob');
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 商品销售汇总报表 */
|
||||||
|
export const getGoodsSalesSummaryReport = (params) => {
|
||||||
|
return getRequest('/statistics/report/goods-sales-summary', params);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const exportGoodsSalesSummaryReport = (params) => {
|
||||||
|
return getRequest('/statistics/report/goods-sales-summary/export', params, 'blob');
|
||||||
|
};
|
||||||
|
|||||||
@@ -368,6 +368,15 @@ export const result = [{
|
|||||||
component: "statistics/traffic",
|
component: "statistics/traffic",
|
||||||
children: null
|
children: null
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "dataReport",
|
||||||
|
level: 2,
|
||||||
|
type: 0,
|
||||||
|
title: "数据报表",
|
||||||
|
path: "dataReport",
|
||||||
|
component: "statistics/data-report",
|
||||||
|
children: null
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -169,6 +169,34 @@ export const otherRouter = {
|
|||||||
title: "财务汇总",
|
title: "财务汇总",
|
||||||
name: "shop-finance-summary",
|
name: "shop-finance-summary",
|
||||||
component: () => import("@/views/shop/finance/summary.vue")
|
component: () => import("@/views/shop/finance/summary.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "statistics/store-performance",
|
||||||
|
title: "店铺业绩报表",
|
||||||
|
name: "storePerformanceReport",
|
||||||
|
meta: { title: "店铺业绩报表" },
|
||||||
|
component: () => import("@/views/statistics/store-performance.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "statistics/goods-comparison",
|
||||||
|
title: "商品同比环比报表",
|
||||||
|
name: "goodsComparisonReport",
|
||||||
|
meta: { title: "商品同比环比报表" },
|
||||||
|
component: () => import("@/views/statistics/goods-comparison.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "statistics/sales-order-detail",
|
||||||
|
title: "销售订单明细报表",
|
||||||
|
name: "salesOrderDetailReport",
|
||||||
|
meta: { title: "销售订单明细报表" },
|
||||||
|
component: () => import("@/views/statistics/sales-order-detail.vue")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "statistics/goods-sales-summary",
|
||||||
|
title: "商品销售汇总报表",
|
||||||
|
name: "goodsSalesSummaryReport",
|
||||||
|
meta: { title: "商品销售汇总报表" },
|
||||||
|
component: () => import("@/views/statistics/goods-sales-summary.vue")
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import plugins from "./plugins";
|
import plugins from "./plugins";
|
||||||
import toobar from "./toolbar";
|
import toobar from "./toolbar";
|
||||||
const localCDN = window.location.origin + "/tinymce"; //本地引入
|
const localCDN = window.location.origin + process.env.BASE_URL + "tinymce"; //本地引入,兼容子路径部署(publicPath)
|
||||||
export const initEditor = {
|
export const initEditor = {
|
||||||
base_url: localCDN,
|
base_url: localCDN,
|
||||||
height: "400px",
|
height: "400px",
|
||||||
|
|||||||
161
seller/src/views/statistics/data-report.vue
Normal file
161
seller/src/views/statistics/data-report.vue
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
<template>
|
||||||
|
<div class="data-report">
|
||||||
|
<el-card>
|
||||||
|
<h4 class="page-title">数据报表</h4>
|
||||||
|
<p class="page-desc">选择报表模块查看本店销售数据统计与分析</p>
|
||||||
|
<div class="module-grid">
|
||||||
|
<div
|
||||||
|
v-for="item in modules"
|
||||||
|
:key="item.name"
|
||||||
|
class="module-card"
|
||||||
|
@click="goReport(item.name)"
|
||||||
|
>
|
||||||
|
<div class="module-icon">
|
||||||
|
<el-icon :size="32">
|
||||||
|
<component :is="item.icon" />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="module-body">
|
||||||
|
<div class="module-title">{{ item.title }}</div>
|
||||||
|
<div class="module-desc">{{ item.desc }}</div>
|
||||||
|
</div>
|
||||||
|
<el-icon class="module-arrow"><ArrowRight /></el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
DataAnalysis,
|
||||||
|
Document,
|
||||||
|
Goods,
|
||||||
|
TrendCharts,
|
||||||
|
} from "@element-plus/icons-vue";
|
||||||
|
|
||||||
|
const REPORT_MODULES = [
|
||||||
|
{
|
||||||
|
name: "storePerformanceReport",
|
||||||
|
title: "店铺业绩报表",
|
||||||
|
desc: "支付金额、营业额、转化率及同环比分析",
|
||||||
|
icon: TrendCharts,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "goodsComparisonReport",
|
||||||
|
title: "商品同比环比报表",
|
||||||
|
desc: "商品销售金额、销量占比及同环比对比",
|
||||||
|
icon: DataAnalysis,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "salesOrderDetailReport",
|
||||||
|
title: "销售订单明细报表",
|
||||||
|
desc: "订单流水、收款明细与优惠明细",
|
||||||
|
icon: Document,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "goodsSalesSummaryReport",
|
||||||
|
title: "商品销售汇总报表",
|
||||||
|
desc: "商品销量、净销售额及优惠汇总",
|
||||||
|
icon: Goods,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "seller-data-report",
|
||||||
|
components: {
|
||||||
|
ArrowRight,
|
||||||
|
TrendCharts,
|
||||||
|
DataAnalysis,
|
||||||
|
Document,
|
||||||
|
Goods,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
modules: REPORT_MODULES,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
goReport(name) {
|
||||||
|
this.$router.push({ name });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.data-report {
|
||||||
|
.page-title {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-desc {
|
||||||
|
margin: 0 0 20px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fafafa;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--el-color-primary-light-5);
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--el-color-primary);
|
||||||
|
background: var(--el-color-primary-light-9);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-title {
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-desc {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-arrow {
|
||||||
|
color: #c0c4cc;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
276
seller/src/views/statistics/goods-comparison.vue
Normal file
276
seller/src/views/statistics/goods-comparison.vue
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card>
|
||||||
|
<el-form inline class="goods-comparison-form">
|
||||||
|
<div class="filter-row">
|
||||||
|
<el-form-item label="时间类型">
|
||||||
|
<el-radio-group v-model="dateType" @change="onDateTypeChange">
|
||||||
|
<el-radio label="DAY">日</el-radio>
|
||||||
|
<el-radio label="PERIOD">指定周期</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="dateType === 'DAY'" label="日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryDate"
|
||||||
|
type="date"
|
||||||
|
value-format="YYYY-MM-DD"
|
||||||
|
placeholder="选择日期"
|
||||||
|
style="width: 160px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-else label="日期范围">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<el-form-item label="商品分类">
|
||||||
|
<el-cascader
|
||||||
|
v-model="params.categoryId"
|
||||||
|
:options="categoryList"
|
||||||
|
:props="categoryProps"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
placeholder="全部分类"
|
||||||
|
style="width: 200px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="品牌">
|
||||||
|
<el-select v-model="params.brandId" clearable filterable placeholder="全部品牌" style="width: 160px">
|
||||||
|
<el-option v-for="item in brandList" :key="item.id" :label="item.name" :value="item.id" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品名称">
|
||||||
|
<el-input v-model="params.keyword" clearable placeholder="商品名称" style="width: 160px" />
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<el-form-item label="订单来源">
|
||||||
|
<el-select v-model="params.clientType" clearable placeholder="全部来源" style="width: 140px">
|
||||||
|
<el-option v-for="item in clientTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
<div class="filter-row">
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-select v-model="params.sortType" style="width: 120px">
|
||||||
|
<el-option label="按销售额" value="PRICE" />
|
||||||
|
<el-option label="按销量" value="NUM" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="mt_10">
|
||||||
|
<el-table v-loading="loading" border :data="data" style="width: 100%">
|
||||||
|
<el-table-column label="序号" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ (params.pageNumber - 1) * params.pageSize + $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="goodsName" label="商品名称" min-width="160" />
|
||||||
|
<el-table-column prop="reportTime" label="时间" width="200" />
|
||||||
|
<el-table-column label="商品销售金额" width="120" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.salesAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="商品销售总金额" width="130" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.totalSalesAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="占比" width="90" align="right">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.amountPercent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="环比" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.momAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="环比率" width="90" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="rateClass(row.momRate)">{{ row.momRate || "0%" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="环比差额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.momDiff) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.yoyAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比率" width="90" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="rateClass(row.yoyRate)">{{ row.yoyRate || "0%" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比差额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.yoyDiff) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="salesNum" label="商品销售数" width="110" align="right" />
|
||||||
|
<el-table-column label="销售占比" width="90" align="right">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.salesNumPercent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="storeName" label="店铺名称" width="140" />
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
class="mt_10"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="loadData"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { getBrandListData, getCategoryTree } from "@/api/goods";
|
||||||
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
|
||||||
|
const CLIENT_TYPE_OPTIONS = [
|
||||||
|
{ label: "移动端", value: "H5" },
|
||||||
|
{ label: "PC端", value: "PC" },
|
||||||
|
{ label: "小程序", value: "WECHAT_MP" },
|
||||||
|
{ label: "移动应用端", value: "APP" },
|
||||||
|
{ label: "未知", value: "UNKNOWN" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "seller-goods-comparison-report",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dateType: "DAY",
|
||||||
|
queryDate: "",
|
||||||
|
dateRange: [],
|
||||||
|
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)],
|
||||||
|
params: {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
categoryId: "",
|
||||||
|
brandId: "",
|
||||||
|
clientType: "",
|
||||||
|
keyword: "",
|
||||||
|
sortType: "PRICE",
|
||||||
|
},
|
||||||
|
categoryList: [],
|
||||||
|
categoryProps: { value: "id", label: "name", children: "children", checkStrictly: true, emitPath: false },
|
||||||
|
brandList: [],
|
||||||
|
clientTypeOptions: CLIENT_TYPE_OPTIONS,
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDefaultRange();
|
||||||
|
this.loadOptions();
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initDefaultRange() {
|
||||||
|
const today = new Date();
|
||||||
|
this.queryDate = this.formatDay(today);
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
this.dateRange = [this.formatDate(start, true), this.formatDate(end, false)];
|
||||||
|
},
|
||||||
|
formatDay(date) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return `${y}-${m}-${d}`;
|
||||||
|
},
|
||||||
|
onDateTypeChange() {
|
||||||
|
if (this.dateType === "DAY" && !this.queryDate) {
|
||||||
|
this.queryDate = this.formatDay(new Date());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadOptions() {
|
||||||
|
getCategoryTree().then((res) => {
|
||||||
|
if (res.success) this.categoryList = res.result || [];
|
||||||
|
});
|
||||||
|
getBrandListData().then((res) => {
|
||||||
|
if (res.success) this.brandList = res.result || [];
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatDate(date, startOfDay) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
|
||||||
|
},
|
||||||
|
buildParams() {
|
||||||
|
const p = { ...this.params, reportDateType: this.dateType };
|
||||||
|
if (this.dateType === "DAY") {
|
||||||
|
if (this.queryDate) {
|
||||||
|
p.queryDate = `${this.queryDate} 00:00:00`;
|
||||||
|
}
|
||||||
|
} else if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
p.startTime = this.dateRange[0];
|
||||||
|
p.endTime = this.dateRange[1];
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.params.pageNumber = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
API_Statistics.getGoodsComparisonReport(this.buildParams()).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.success) {
|
||||||
|
this.data = res.result.records || [];
|
||||||
|
this.total = res.result.total || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
API_Statistics.exportGoodsComparisonReport(this.buildParams()).then((blob) => {
|
||||||
|
downloadBlob(blob, "商品同比环比报表.xlsx");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
return this.$filters.unitPrice(val, "¥");
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
return `${Number(val).toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
rateClass(rate) {
|
||||||
|
if (!rate) return "";
|
||||||
|
if (rate.startsWith("+")) return "trend-up";
|
||||||
|
if (rate.startsWith("-") && rate !== "0%") return "trend-down";
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.goods-comparison-form .filter-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.goods-comparison-form .filter-row + .filter-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.trend-up {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.trend-down {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
147
seller/src/views/statistics/goods-sales-summary.vue
Normal file
147
seller/src/views/statistics/goods-sales-summary.vue
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card>
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="日期范围">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品关键词">
|
||||||
|
<el-input v-model="params.keyword" clearable placeholder="商品名称" style="width: 180px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="排序">
|
||||||
|
<el-select v-model="params.sortType" style="width: 120px">
|
||||||
|
<el-option label="按销售额" value="PRICE" />
|
||||||
|
<el-option label="按销量" value="NUM" />
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="mt_10">
|
||||||
|
<el-table v-loading="loading" border :data="data" style="width: 100%">
|
||||||
|
<el-table-column label="序号" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ (params.pageNumber - 1) * params.pageSize + $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="goodsName" label="销售商品" min-width="160" />
|
||||||
|
<el-table-column prop="goodsId" label="商品ID" width="140" />
|
||||||
|
<el-table-column prop="salesNum" label="商品销售数量" width="110" />
|
||||||
|
<el-table-column prop="salesAmount" label="商品销售金额" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.salesAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="refundNum" label="商品退货数量" width="110" />
|
||||||
|
<el-table-column prop="refundAmount" label="商品退款金额" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.refundAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="netNum" label="商品净销售数量" width="140" />
|
||||||
|
<el-table-column prop="netAmount" label="商品净销售金额" width="145">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.netAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="netAmountPercent" label="占净销售金额百分比" width="150">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.netAmountPercent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="avgPrice" label="商品平均单价" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.avgPrice) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="salePriceAmount" label="商品售价金额" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.salePriceAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="discountAmount" label="商品优惠金额" width="120">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.discountAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
class="mt_10"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="loadData"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "seller-goods-sales-summary-report",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dateRange: [],
|
||||||
|
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)],
|
||||||
|
params: { pageNumber: 1, pageSize: 20, keyword: "", sortType: "PRICE" },
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDefaultRange();
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initDefaultRange() {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
this.dateRange = [this.formatDate(start, true), this.formatDate(end, false)];
|
||||||
|
},
|
||||||
|
formatDate(date, startOfDay) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
|
||||||
|
},
|
||||||
|
buildParams() {
|
||||||
|
const p = { ...this.params };
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
p.startTime = this.dateRange[0];
|
||||||
|
p.endTime = this.dateRange[1];
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.params.pageNumber = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
API_Statistics.getGoodsSalesSummaryReport(this.buildParams()).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.success) {
|
||||||
|
this.data = res.result.records || [];
|
||||||
|
this.total = res.result.total || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
API_Statistics.exportGoodsSalesSummaryReport(this.buildParams()).then((blob) => {
|
||||||
|
downloadBlob(blob, "商品销售汇总报表.xlsx");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
return this.$filters.unitPrice(val, "¥");
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
return `${Number(val).toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
151
seller/src/views/statistics/sales-order-detail.vue
Normal file
151
seller/src/views/statistics/sales-order-detail.vue
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card>
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="发生日期">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="订单号">
|
||||||
|
<el-input v-model="params.orderSn" clearable placeholder="订单编号" style="width: 160px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="买家">
|
||||||
|
<el-input v-model="params.memberName" clearable placeholder="买家名称" style="width: 140px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="商品关键词">
|
||||||
|
<el-input v-model="params.keyword" clearable placeholder="商品名称" style="width: 160px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="mt_10">
|
||||||
|
<el-table v-loading="loading" border :data="data" style="width: 100%">
|
||||||
|
<el-table-column label="序号" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ (params.pageNumber - 1) * params.pageSize + $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="orderRefundSn" label="订单编号|售后单号" min-width="200" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="occurTime" label="发生时间" width="160" />
|
||||||
|
<el-table-column prop="flowTypeName" label="交易类型" width="90" />
|
||||||
|
<el-table-column prop="num" label="成交数量" width="90" align="right" />
|
||||||
|
<el-table-column label="成交金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.transactionAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="售价金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.salePriceAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="优惠金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.discountAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="paymentDetail" label="收款明细" width="120" show-overflow-tooltip />
|
||||||
|
<el-table-column prop="discountDetail" label="优惠明细" min-width="160" show-overflow-tooltip />
|
||||||
|
<el-table-column label="订单来源" width="100">
|
||||||
|
<template #default="{ row }">{{ clientTypeText(row.clientType) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
class="mt_10"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="loadData"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "seller-sales-order-detail-report",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dateRange: [],
|
||||||
|
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)],
|
||||||
|
params: {
|
||||||
|
pageNumber: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
orderSn: "",
|
||||||
|
memberName: "",
|
||||||
|
keyword: "",
|
||||||
|
},
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDefaultRange();
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initDefaultRange() {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
this.dateRange = [this.formatDate(start, true), this.formatDate(end, false)];
|
||||||
|
},
|
||||||
|
formatDate(date, startOfDay) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
|
||||||
|
},
|
||||||
|
buildParams() {
|
||||||
|
const p = { ...this.params };
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
p.startTime = this.dateRange[0];
|
||||||
|
p.endTime = this.dateRange[1];
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.params.pageNumber = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
API_Statistics.getSalesOrderDetailReport(this.buildParams()).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.success) {
|
||||||
|
this.data = res.result.records || [];
|
||||||
|
this.total = res.result.total || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
API_Statistics.exportSalesOrderDetailReport(this.buildParams()).then((blob) => {
|
||||||
|
downloadBlob(blob, "销售订单明细报表.xlsx");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
return this.$filters.unitPrice(val, "¥");
|
||||||
|
},
|
||||||
|
clientTypeText(val) {
|
||||||
|
const map = {
|
||||||
|
H5: "移动端",
|
||||||
|
PC: "PC端",
|
||||||
|
WECHAT_MP: "小程序",
|
||||||
|
APP: "移动应用端",
|
||||||
|
UNKNOWN: "未知",
|
||||||
|
};
|
||||||
|
return map[val] || val || "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
172
seller/src/views/statistics/store-performance.vue
Normal file
172
seller/src/views/statistics/store-performance.vue
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<div class="search">
|
||||||
|
<el-card>
|
||||||
|
<el-form inline>
|
||||||
|
<el-form-item label="日期范围">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="dateRange"
|
||||||
|
type="daterange"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
:default-time="defaultTime"
|
||||||
|
start-placeholder="开始"
|
||||||
|
end-placeholder="结束"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleSearch">查询</el-button>
|
||||||
|
<el-button @click="handleExport">导出</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
<el-card class="mt_10">
|
||||||
|
<el-table v-loading="loading" border :data="data" style="width: 100%">
|
||||||
|
<el-table-column label="序号" width="60" align="center">
|
||||||
|
<template #default="{ $index }">
|
||||||
|
{{ (params.pageNumber - 1) * params.pageSize + $index + 1 }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="storeName" label="店铺名称" min-width="140" />
|
||||||
|
<el-table-column prop="payOrderCount" label="订单支付笔数" width="120" align="right" />
|
||||||
|
<el-table-column label="支付金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.payAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="营业额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.turnover) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="优惠金额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.discountAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="折扣率" width="90" align="right">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.discountRate) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="营业收入" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.operatingIncome) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="营业收入占比" width="120" align="right">
|
||||||
|
<template #default="{ row }">{{ formatPercent(row.operatingIncomePercent) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="笔单价" width="100" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.avgOrderPrice) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="refundCount" label="退款笔数" width="100" align="right" />
|
||||||
|
<el-table-column label="订单退款金额" width="120" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.refundAmount) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="orderConversionRate" label="下单转化率" width="110" align="right" />
|
||||||
|
<el-table-column prop="payConversionRate" label="支付转化率" width="110" align="right" />
|
||||||
|
<el-table-column label="环比差额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.momDiff) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="环比增长率" width="110" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="rateClass(row.momRate)">{{ row.momRate || "0%" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比差额" width="110" align="right">
|
||||||
|
<template #default="{ row }">{{ formatMoney(row.yoyDiff) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="同比增长率" width="110" align="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span :class="rateClass(row.yoyRate)">{{ row.yoyRate || "0%" }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="params.pageNumber"
|
||||||
|
v-model:page-size="params.pageSize"
|
||||||
|
class="mt_10"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@current-change="loadData"
|
||||||
|
@size-change="loadData"
|
||||||
|
/>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as API_Statistics from "@/api/statistics";
|
||||||
|
import { downloadBlob } from "@/utils/downloadBlob";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "seller-store-performance-report",
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: false,
|
||||||
|
dateRange: [],
|
||||||
|
defaultTime: [new Date(2000, 0, 1, 0, 0, 0), new Date(2000, 0, 1, 23, 59, 59)],
|
||||||
|
params: { pageNumber: 1, pageSize: 20 },
|
||||||
|
data: [],
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initDefaultRange();
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
initDefaultRange() {
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date();
|
||||||
|
start.setDate(start.getDate() - 6);
|
||||||
|
this.dateRange = [this.formatDate(start, true), this.formatDate(end, false)];
|
||||||
|
},
|
||||||
|
formatDate(date, startOfDay) {
|
||||||
|
const y = date.getFullYear();
|
||||||
|
const m = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const d = String(date.getDate()).padStart(2, "0");
|
||||||
|
return startOfDay ? `${y}-${m}-${d} 00:00:00` : `${y}-${m}-${d} 23:59:59`;
|
||||||
|
},
|
||||||
|
buildParams() {
|
||||||
|
const p = { ...this.params };
|
||||||
|
if (this.dateRange && this.dateRange.length === 2) {
|
||||||
|
p.startTime = this.dateRange[0];
|
||||||
|
p.endTime = this.dateRange[1];
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
this.params.pageNumber = 1;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
this.loading = true;
|
||||||
|
API_Statistics.getStorePerformanceReport(this.buildParams()).then((res) => {
|
||||||
|
this.loading = false;
|
||||||
|
if (res.success) {
|
||||||
|
this.data = res.result.records || [];
|
||||||
|
this.total = res.result.total || 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handleExport() {
|
||||||
|
API_Statistics.exportStorePerformanceReport(this.buildParams()).then((blob) => {
|
||||||
|
downloadBlob(blob, "店铺业绩报表.xlsx");
|
||||||
|
});
|
||||||
|
},
|
||||||
|
formatMoney(val) {
|
||||||
|
return this.$filters.unitPrice(val, "¥");
|
||||||
|
},
|
||||||
|
formatPercent(val) {
|
||||||
|
if (val == null || val === "") return "0%";
|
||||||
|
return `${Number(val).toFixed(2)}%`;
|
||||||
|
},
|
||||||
|
rateClass(rate) {
|
||||||
|
if (!rate) return "";
|
||||||
|
if (rate.startsWith("+")) return "trend-up";
|
||||||
|
if (rate.startsWith("-") && rate !== "0%") return "trend-down";
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.trend-up {
|
||||||
|
color: #f56c6c;
|
||||||
|
}
|
||||||
|
.trend-down {
|
||||||
|
color: #67c23a;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,6 +7,7 @@ const resolve = (dir) => path.join(__dirname, dir);
|
|||||||
const enableProduction = process.env.NODE_ENV === "production";
|
const enableProduction = process.env.NODE_ENV === "production";
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
publicPath: process.env.VUE_APP_PUBLIC_PATH || "/",
|
||||||
outputDir: "dist",
|
outputDir: "dist",
|
||||||
assetsDir: "static",
|
assetsDir: "static",
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
Reference in New Issue
Block a user