feat(statistics): 添加店铺业绩、商品同比环比、销售订单明细及商品销售汇总报表视图和相关 API 接口

This commit is contained in:
pikachu1995@126.com
2026-07-20 15:31:41 +08:00
parent 6c9a3820e8
commit 3be9aa5c14
24 changed files with 2010 additions and 8 deletions

View File

@@ -71,8 +71,8 @@
<% for(var js of htmlWebpackPlugin.options.cdn.js) { %>
<script src="<%=js%>"></script>
<% } %>
<script src="/config.js"></script>
<script src="/tinymce/tinymce.min.js"></script>
<script src="<%= BASE_URL %>config.js"></script>
<script src="<%= BASE_URL %>tinymce/tinymce.min.js"></script>
<noscript>
<strong
>We're sorry doesn't work properly without JavaScript

View File

@@ -16,5 +16,38 @@ export const getOrderStatistics = (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');
};

View File

@@ -368,6 +368,15 @@ export const result = [{
component: "statistics/traffic",
children: null
},
{
name: "dataReport",
level: 2,
type: 0,
title: "数据报表",
path: "dataReport",
component: "statistics/data-report",
children: null
},
]
}]
},

View File

@@ -169,6 +169,34 @@ export const otherRouter = {
title: "财务汇总",
name: "shop-finance-summary",
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")
}
]
};

View File

@@ -1,6 +1,6 @@
import plugins from "./plugins";
import toobar from "./toolbar";
const localCDN = window.location.origin + "/tinymce"; //本地引入
const localCDN = window.location.origin + process.env.BASE_URL + "tinymce"; //本地引入兼容子路径部署publicPath
export const initEditor = {
base_url: localCDN,
height: "400px",

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -7,6 +7,7 @@ const resolve = (dir) => path.join(__dirname, dir);
const enableProduction = process.env.NODE_ENV === "production";
module.exports = {
publicPath: process.env.VUE_APP_PUBLIC_PATH || "/",
outputDir: "dist",
assetsDir: "static",
css: {