超市会议

This commit is contained in:
zjf committed 2025-10-09 18:38:18 +08:00
1 parent 30e3bf6655
commit 0ebd6737fe
16 files changed
+1397 -162

No files matched your search

@@ -339,3 +339,6 @@ const handleCurrentChange = (page: number) => {
</style>
@@ -191,3 +191,6 @@ defineExpose({
</style>
@@ -203,3 +203,6 @@ const trendClass = computed(() => {
</style>
@@ -7,3 +7,6 @@ export { default as SummaryCard } from './SummaryCard.vue';
export { default as ProductRankingTable } from './ProductRankingTable.vue';
@@ -207,3 +207,6 @@ export const SUCCESS_MESSAGES = {
};
+328 -160
View File
@@ -254,7 +254,7 @@
<template #header>
<div class="card-header">
<span>畅销品TOP10</span>
<!-- <el-button size="small" @click="viewAllTopProducts">查看全部</el-button> -->
<el-button size="small" @click="viewAllTopProducts">查看全部</el-button>
</div>
</template>
<el-table :data="topSellingProducts" stripe v-loading="loading">
@@ -299,7 +299,7 @@
<template #header>
<div class="card-header">
<span>滞销品TOP10</span>
<!-- <el-button size="small" @click="viewAllSlowProducts">查看全部</el-button> -->
<el-button size="small" @click="viewAllSlowProducts">查看全部</el-button>
</div>
</template>
<el-table :data="slowSellingProducts" stripe v-loading="loading">
@@ -415,20 +415,11 @@
</el-table-column>
</el-table>
</el-card>
<!-- 商品排名详情弹窗 -->
<!-- <ProductRankingDialog
:visible="dialogVisible"
:type="dialogType"
:query-params="queryForm"
@close="handleDialogClose"
@view-detail="handleViewProductDetail"
/> -->
</div>
</template>
<script lang="ts" setup name="RealtimeSalesReport">
import { ref, reactive, onMounted, nextTick, watch } from 'vue';
import { ref, reactive, onMounted, nextTick } from 'vue';
import { ElMessage } from 'element-plus';
import {
Refresh,
@@ -451,21 +442,15 @@ import {
getCustomerAnalysis,
refreshRealtimeData,
} from '/@/api/supermarketerp/realtimeSalesReport';
// import ProductRankingDialog from './components/ProductRankingDialog.vue';
// 响应式数据
const refreshing = ref(false);
const loading = ref(false);
const isInitialized = ref(false);
const debounceTimer = ref<number | null>(null);
const debounceTimer = ref<NodeJS.Timeout | null>(null);
const salesTrendChart = ref<HTMLElement>();
const customerAnalysisChart = ref<HTMLElement>();
// 弹窗相关数据(已注释)
// const dialogVisible = ref(false);
// const dialogType = ref<'top' | 'slow'>('top');
// const selectedProduct = ref(null);
// 查询表单
const queryForm = reactive({
queryType: 'day',
@@ -479,16 +464,16 @@ const queryForm = reactive({
});
// 高级选项显示状态
const advancedOptionsVisible = ref<string[]>([]);
const advancedOptionsVisible = ref([]);
// 数据
const salesSummary = ref<any>();
const topSellingProducts = ref<any[]>([]);
const slowSellingProducts = ref<any[]>([]);
const timePeriodSales = ref<any[]>([]);
const customerAnalysis = ref<any>();
const salesTrend = ref<any[]>([]);
const expandedRows = ref<string[]>([]);
const salesSummary = ref();
const topSellingProducts = ref([]);
const slowSellingProducts = ref([]);
const timePeriodSales = ref([]);
const customerAnalysis = ref();
const salesTrend = ref([]);
const expandedRows = ref([]);
// 图表实例
let salesTrendChartInstance: echarts.ECharts | null = null;
@@ -507,11 +492,9 @@ const initQueryForm = () => {
queryForm.startDate = today.toISOString().slice(0, 10);
queryForm.endDate = now.toISOString().slice(0, 10);
} else if (queryForm.queryType === 'month') {
// 确保按月查询时设置正确的默认值
const thisMonth = now.toISOString().slice(0, 7);
queryForm.startDate = thisMonth;
queryForm.endDate = thisMonth;
console.log('初始化按月查询默认值:', { startDate: queryForm.startDate, endDate: queryForm.endDate });
}
};
@@ -613,99 +596,20 @@ watch(
{ deep: true }
);
// 处理查询参数,确保日期格式正确
const processQueryParams = (params: any) => {
const processedParams = { ...params };
// 如果是按月查询,需要将YYYY-MM格式转换为完整的日期范围
if (params.queryType === 'month') {
if (params.startDate) {
try {
// 确保日期格式正确,处理可能的格式问题
let dateStr = params.startDate;
if (typeof dateStr === 'string' && dateStr.length === 7 && dateStr.includes('-')) {
// 已经是 YYYY-MM 格式
const startDate = new Date(dateStr + '-01');
if (isNaN(startDate.getTime())) {
throw new Error('Invalid start date');
}
processedParams.startDate = startDate.toISOString().slice(0, 10);
console.log(`按月查询 - 开始日期转换: ${params.startDate} -> ${processedParams.startDate}`);
} else {
console.warn('按月查询 - 开始日期格式不正确:', params.startDate);
// 如果格式不正确,尝试使用当前月份
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7);
const fallbackDate = new Date(currentMonth + '-01');
processedParams.startDate = fallbackDate.toISOString().slice(0, 10);
console.log(`按月查询 - 使用默认开始日期: ${processedParams.startDate}`);
}
} catch (error) {
console.error('按月查询 - 开始日期转换失败:', error);
// 使用当前月份作为默认值
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7);
const fallbackDate = new Date(currentMonth + '-01');
processedParams.startDate = fallbackDate.toISOString().slice(0, 10);
}
}
if (params.endDate) {
try {
// 确保日期格式正确,处理可能的格式问题
let dateStr = params.endDate;
if (typeof dateStr === 'string' && dateStr.length === 7 && dateStr.includes('-')) {
// 已经是 YYYY-MM 格式
const endDate = new Date(dateStr + '-01');
if (isNaN(endDate.getTime())) {
throw new Error('Invalid end date');
}
endDate.setMonth(endDate.getMonth() + 1, 0); // 设置为下个月的第0天,即当前月的最后一天
processedParams.endDate = endDate.toISOString().slice(0, 10);
console.log(`按月查询 - 结束日期转换: ${params.endDate} -> ${processedParams.endDate}`);
} else {
console.warn('按月查询 - 结束日期格式不正确:', params.endDate);
// 如果格式不正确,尝试使用当前月份
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7);
const fallbackDate = new Date(currentMonth + '-01');
fallbackDate.setMonth(fallbackDate.getMonth() + 1, 0);
processedParams.endDate = fallbackDate.toISOString().slice(0, 10);
console.log(`按月查询 - 使用默认结束日期: ${processedParams.endDate}`);
}
} catch (error) {
console.error('按月查询 - 结束日期转换失败:', error);
// 使用当前月份作为默认值
const now = new Date();
const currentMonth = now.toISOString().slice(0, 7);
const fallbackDate = new Date(currentMonth + '-01');
fallbackDate.setMonth(fallbackDate.getMonth() + 1, 0);
processedParams.endDate = fallbackDate.toISOString().slice(0, 10);
}
}
}
console.log('处理后的查询参数:', processedParams);
return processedParams;
};
// 加载数据
const loadData = async () => {
loading.value = true;
try {
// 处理查询参数
const processedParams = processQueryParams(queryForm);
// 加载完整报表数据
const reportData = await getRealtimeSalesReport(processedParams);
const reportData = await getRealtimeSalesReport(queryForm);
if (reportData.code === 0 && reportData.data) {
// 使用后端返回的数据
salesSummary.value = reportData.data.salesSummary || {};
salesSummary.value = reportData.data.salesSummary || getMockSalesSummary();
topSellingProducts.value = reportData.data.topSellingProducts || [];
slowSellingProducts.value = reportData.data.slowSellingProducts || [];
timePeriodSales.value = reportData.data.timePeriodSales || [];
customerAnalysis.value = reportData.data.customerAnalysis || {};
customerAnalysis.value = reportData.data.customerAnalysis || getMockCustomerAnalysis();
salesTrend.value = reportData.data.salesTrend || [];
ElMessage.success('数据加载成功');
@@ -718,31 +622,245 @@ const loadData = async () => {
renderCharts();
} catch (error) {
console.error('加载数据失败:', error);
// 清空数据
salesSummary.value = {};
topSellingProducts.value = [];
slowSellingProducts.value = [];
timePeriodSales.value = [];
customerAnalysis.value = {};
salesTrend.value = [];
// 使用模拟数据
salesSummary.value = getMockSalesSummary();
topSellingProducts.value = getMockTopProducts();
slowSellingProducts.value = getMockSlowProducts();
timePeriodSales.value = getMockTimePeriodSales();
customerAnalysis.value = getMockCustomerAnalysis();
salesTrend.value = []; // 空数组,不显示模拟数据
await nextTick();
renderCharts();
// 根据错误类型显示不同的提示信息
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage.includes('权限')) {
if (error.message && error.message.includes('权限')) {
ElMessage.error('权限不足,无法访问即时销售报表');
} else if (errorMessage.includes('网络')) {
} else if (error.message && error.message.includes('网络')) {
ElMessage.error('网络连接异常,请检查网络设置');
} else {
ElMessage.error('数据加载失败,请稍后重试');
ElMessage.warning('数据加载失败,使用模拟数据展示');
}
} finally {
loading.value = false;
}
};
// 模拟数据生成函数 - 根据后端API文档调整
const getMockSalesSummary = () => ({
totalSalesAmount: 12500.50,
totalSalesCount: 45,
averageCustomerValue: 277.79,
productVarietyCount: 23,
totalSalesQuantity: 156.5,
salesGrowthRate: 15.5,
salesGrowthRateMonth: 8.2,
realtimeStatus: 'MEDIUM',
});
const getMockTopProducts = () => [
{
productId: 1001,
productName: '苹果',
productCode: 'P001',
salesAmount: 1250.00,
salesQuantity: 50.0,
salesCount: 12,
averagePrice: 25.00,
ranking: 1,
stockQuantity: 200.0,
stockStatus: 'SUFFICIENT',
growthRate: 12.5
},
{
productId: 1002,
productName: '牛奶',
productCode: 'M002',
salesAmount: 980.00,
salesQuantity: 49.0,
salesCount: 10,
averagePrice: 20.00,
ranking: 2,
stockQuantity: 150.0,
stockStatus: 'SUFFICIENT',
growthRate: 8.3
},
{
productId: 1003,
productName: '面包',
productCode: 'B003',
salesAmount: 850.00,
salesQuantity: 34.0,
salesCount: 8,
averagePrice: 25.00,
ranking: 3,
stockQuantity: 5.0,
stockStatus: 'LOW',
growthRate: 15.2
},
{
productId: 1004,
productName: '矿泉水',
productCode: 'W004',
salesAmount: 720.00,
salesQuantity: 36.0,
salesCount: 7,
averagePrice: 20.00,
ranking: 4,
stockQuantity: 0.0,
stockStatus: 'OUT',
growthRate: 6.7
},
{
productId: 1005,
productName: '鸡蛋',
productCode: 'E005',
salesAmount: 680.00,
salesQuantity: 68.0,
salesCount: 6,
averagePrice: 10.00,
ranking: 5,
stockQuantity: 100.0,
stockStatus: 'SUFFICIENT',
growthRate: 4.2
},
];
const getMockSlowProducts = () => [
{
productId: 2001,
productName: '高级红酒',
productCode: 'W001',
salesAmount: 120.00,
salesQuantity: 1.2,
salesCount: 1,
averagePrice: 100.00,
ranking: 1,
stockQuantity: 50.0,
stockStatus: 'SUFFICIENT',
growthRate: -15.3
},
{
productId: 2002,
productName: '进口巧克力',
productCode: 'C002',
salesAmount: 80.00,
salesQuantity: 2.0,
salesCount: 2,
averagePrice: 40.00,
ranking: 2,
stockQuantity: 8.0,
stockStatus: 'LOW',
growthRate: -8.7
},
{
productId: 2003,
productName: '有机蔬菜',
productCode: 'V003',
salesAmount: 60.00,
salesQuantity: 3.0,
salesCount: 1,
averagePrice: 20.00,
ranking: 3,
stockQuantity: 15.0,
stockStatus: 'SUFFICIENT',
growthRate: -12.1
},
{
productId: 2004,
productName: '高端茶叶',
productCode: 'T004',
salesAmount: 40.00,
salesQuantity: 0.8,
salesCount: 1,
averagePrice: 50.00,
ranking: 4,
stockQuantity: 0.0,
stockStatus: 'OUT',
growthRate: -20.5
},
{
productId: 2005,
productName: '进口咖啡',
productCode: 'C005',
salesAmount: 30.00,
salesQuantity: 0.6,
salesCount: 1,
averagePrice: 50.00,
ranking: 5,
stockQuantity: 2.0,
stockStatus: 'LOW',
growthRate: -25.8
},
];
const getMockTimePeriodSales = () => {
const data = [];
const products = [
{ name: '苹果', code: 'P001', category: '水果', unit: 'kg', unitName: 'kg' },
{ name: '牛奶', code: 'M002', category: '乳制品', unit: '瓶', unitName: '瓶' },
{ name: '面包', code: 'B003', category: '烘焙', unit: '个', unitName: '个' },
{ name: '矿泉水', code: 'W004', category: '饮料', unit: '瓶', unitName: '瓶' },
{ name: '鸡蛋', code: 'E005', category: '蛋类', unit: '个', unitName: '个' },
];
for (let i = 0; i < 24; i++) {
const productCount = Math.floor(Math.random() * 3) + 1;
const productDetails = [];
let totalAmount = 0;
let totalQuantity = 0;
for (let j = 0; j < productCount; j++) {
const product = products[Math.floor(Math.random() * products.length)];
const quantity = Math.floor(Math.random() * 5) + 1;
const unitPrice = Math.floor(Math.random() * 50) + 10;
const amount = quantity * unitPrice;
productDetails.push({
productId: `mock_${i}_${j}`,
productName: product.name,
productCode: product.code,
categoryId: `cat_${j}`,
categoryName: product.category,
unit: product.unit,
unitName: product.unitName,
quantity: quantity,
unitPrice: unitPrice,
totalAmount: amount,
salesCount: 1,
});
totalAmount += amount;
totalQuantity += quantity;
}
data.push({
timeLabel: `${i.toString().padStart(2, '0')}:00`,
salesAmount: totalAmount,
salesCount: productCount,
customerValue: totalAmount / productCount,
salesQuantity: totalQuantity,
growthRate: Math.floor(Math.random() * 20) - 10,
productDetails: productDetails,
});
}
return data;
};
const getMockCustomerAnalysis = () => ({
averageCustomerValue: 277.79,
maxCustomerValue: 850.00,
minCustomerValue: 15.50,
medianCustomerValue: 250.00,
highValueOrderRatio: 25.5,
valueRanges: [
{ rangeLabel: '0-50元', orderCount: 8, percentage: 17.78 },
{ rangeLabel: '50-100元', orderCount: 12, percentage: 26.67 },
{ rangeLabel: '100-200元', orderCount: 15, percentage: 33.33 },
{ rangeLabel: '200-500元', orderCount: 8, percentage: 17.78 },
{ rangeLabel: '500+元', orderCount: 2, percentage: 4.44 },
],
});
// 刷新数据
@@ -774,48 +892,30 @@ const refreshCharts = async () => {
// 刷新时段数据
const refreshTimePeriodData = async () => {
try {
const processedParams = processQueryParams(queryForm);
const timePeriodData = await getTimePeriodSales({
...processedParams,
...queryForm,
current: 1,
size: 50,
});
timePeriodSales.value = timePeriodData.data?.records || [];
timePeriodSales.value = timePeriodData.data?.records || getMockTimePeriodSales();
ElMessage.success('时段数据刷新成功');
} catch (error) {
console.error('刷新时段数据失败:', error);
timePeriodSales.value = [];
ElMessage.error('时段数据刷新失败');
timePeriodSales.value = getMockTimePeriodSales();
ElMessage.warning('使用模拟数据');
}
};
// 查看全部畅销品(已注释)
// const viewAllTopProducts = () => {
// dialogType.value = 'top';
// dialogVisible.value = true;
// };
// 查看全部滞销品(已注释)
// const viewAllSlowProducts = () => {
// dialogType.value = 'slow';
// dialogVisible.value = true;
// };
// 关闭弹窗(已注释)
// const handleDialogClose = () => {
// dialogVisible.value = false;
// selectedProduct.value = null;
// };
// 查看商品详情(已注释)
// const handleViewProductDetail = (product: any) => {
// selectedProduct.value = product;
// ElMessage.info(`查看商品详情: ${product.productName}`);
// // 这里可以打开商品详情弹窗或跳转到详情页面
// console.log('商品详情:', product);
// };
// 查看全部畅销品
const viewAllTopProducts = () => {
ElMessage.info('查看全部畅销品功能开发中...');
};
// 查看全部滞销品
const viewAllSlowProducts = () => {
ElMessage.info('查看全部滞销品功能开发中...');
};
// 渲染销售趋势图表
const renderSalesTrendChart = () => {
@@ -826,9 +926,9 @@ const renderSalesTrendChart = () => {
}
// 使用后端返回的销售趋势数据
let xAxisData: string[] = [];
let salesData: number[] = [];
let countData: number[] = [];
let xAxisData = [];
let salesData = [];
let countData = [];
if (salesTrend.value && salesTrend.value.length > 0) {
// 使用后端返回的真实数据
@@ -920,22 +1020,28 @@ const renderCustomerAnalysisChart = () => {
}
// 使用后端数据结构
let data: any[] = [];
let data = [];
if (customerAnalysis.value && customerAnalysis.value.valueRanges) {
data = customerAnalysis.value.valueRanges.map((range: any) => ({
data = customerAnalysis.value.valueRanges.map(range => ({
name: range.rangeLabel,
value: range.orderCount,
percentage: range.percentage
}));
} else {
// 没有数据时显示空数组
data = [];
// 模拟数据
data = [
{ name: '0-50元', value: 25, percentage: 15.5 },
{ name: '50-100元', value: 35, percentage: 21.7 },
{ name: '100-200元', value: 20, percentage: 12.4 },
{ name: '200-500元', value: 15, percentage: 9.3 },
{ name: '500+元', value: 5, percentage: 3.1 },
];
}
const option = {
tooltip: {
trigger: 'item',
formatter: function(params: any) {
formatter: function(params) {
return `${params.name}<br/>订单数: ${params.value}<br/>占比: ${params.data.percentage}%`;
},
},
@@ -1065,6 +1171,68 @@ const getNumericValue = (value: any) => {
};
// 根据类型生成模拟时段数据
const getMockTimePeriodSalesByType = (type: string) => {
const data = [];
if (type === 'hour') {
// 按小时生成24小时数据
for (let i = 0; i < 24; i++) {
data.push({
timePeriod: `${i}:00`,
timePeriodName: `${i}:00-${i + 1}:00`,
salesAmount: Math.floor(Math.random() * 5000) + 1000,
salesCount: Math.floor(Math.random() * 50) + 10,
salesQuantity: Math.floor(Math.random() * 100) + 20,
averageCustomerPrice: Math.floor(Math.random() * 50) + 50,
customerCount: Math.floor(Math.random() * 20) + 5,
productVarietyCount: Math.floor(Math.random() * 30) + 10,
yearOverYearSalesAmount: Math.floor(Math.random() * 1000) - 500,
monthOverMonthSalesAmount: Math.floor(Math.random() * 500) - 250,
});
}
} else if (type === 'day') {
// 按天生成7天数据
for (let i = 6; i >= 0; i--) {
const date = new Date();
date.setDate(date.getDate() - i);
const dateStr = date.toISOString().slice(0, 10);
data.push({
timePeriod: dateStr,
timePeriodName: dateStr,
salesAmount: Math.floor(Math.random() * 20000) + 5000,
salesCount: Math.floor(Math.random() * 200) + 50,
salesQuantity: Math.floor(Math.random() * 500) + 100,
averageCustomerPrice: Math.floor(Math.random() * 100) + 50,
customerCount: Math.floor(Math.random() * 50) + 20,
productVarietyCount: Math.floor(Math.random() * 50) + 30,
yearOverYearSalesAmount: Math.floor(Math.random() * 5000) - 2500,
monthOverMonthSalesAmount: Math.floor(Math.random() * 2000) - 1000,
});
}
} else if (type === 'month') {
// 按月生成12个月数据
for (let i = 11; i >= 0; i--) {
const date = new Date();
date.setMonth(date.getMonth() - i);
const monthStr = date.toISOString().slice(0, 7);
data.push({
timePeriod: monthStr,
timePeriodName: monthStr,
salesAmount: Math.floor(Math.random() * 100000) + 50000,
salesCount: Math.floor(Math.random() * 1000) + 500,
salesQuantity: Math.floor(Math.random() * 2000) + 1000,
averageCustomerPrice: Math.floor(Math.random() * 200) + 100,
customerCount: Math.floor(Math.random() * 200) + 100,
productVarietyCount: Math.floor(Math.random() * 100) + 50,
yearOverYearSalesAmount: Math.floor(Math.random() * 20000) - 10000,
monthOverMonthSalesAmount: Math.floor(Math.random() * 10000) - 5000,
});
}
}
return data;
};
// 组件挂载
onMounted(async () => {
@@ -136,3 +136,5 @@ export function formatTimePeriodDataForExport(data: any[]) {
}));
}
@@ -196,3 +196,6 @@ export const TABLE_CONFIG = {
};
@@ -366,3 +366,5 @@ class ProfitMarginReportServiceTest {
这样重构后的代码将更加清晰、可维护,并且性能更好。