即时销售报表,毛利润报表
This commit is contained in:
1 parent
6fdc05c396
commit
c17731dd86
13 files changed
+2665
-329
No files matched your search
@@ -0,0 +1,90 @@
|
||||
import request from "/@/utils/request"
|
||||
|
||||
/**
|
||||
* 获取毛利润报表
|
||||
*/
|
||||
export function getProfitMarginReport(params?: object) {
|
||||
return request({
|
||||
url: "/supermarket/profitMarginReport/report",
|
||||
method: "get",
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品毛利润排名
|
||||
*/
|
||||
export function getProductProfitRanking(params?: object) {
|
||||
return request({
|
||||
url: "/supermarket/profitMarginReport/productProfitRanking",
|
||||
method: "get",
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类毛利润分析
|
||||
*/
|
||||
export function getCategoryProfitAnalysis(params?: object) {
|
||||
return request({
|
||||
url: "/supermarket/profitMarginReport/categoryProfitAnalysis",
|
||||
method: "get",
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取时段毛利润趋势
|
||||
*/
|
||||
export function getTimePeriodProfitTrend(params?: object) {
|
||||
return request({
|
||||
url: "/supermarket/profitMarginReport/timePeriodProfitTrend",
|
||||
method: "get",
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取高毛利润商品
|
||||
*/
|
||||
export function getHighProfitProducts(params?: object) {
|
||||
return request({
|
||||
url: "/supermarket/profitMarginReport/highProfitProducts",
|
||||
method: "get",
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取低毛利润/亏损商品
|
||||
*/
|
||||
export function getLowProfitProducts(params?: object) {
|
||||
return request({
|
||||
url: "/supermarket/profitMarginReport/lowProfitProducts",
|
||||
method: "get",
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取毛利润汇总信息
|
||||
*/
|
||||
export function getProfitSummary(params?: object) {
|
||||
return request({
|
||||
url: "/supermarket/profitMarginReport/profitSummary",
|
||||
method: "get",
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出毛利润报表
|
||||
*/
|
||||
export function exportProfitMarginReport(params?: object) {
|
||||
return request({
|
||||
url: "/supermarket/profitMarginReport/export",
|
||||
method: "post",
|
||||
params,
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,575 @@
|
||||
<template>
|
||||
<el-dialog
|
||||
:model-value="visible"
|
||||
:title="dialogTitle"
|
||||
width="80%"
|
||||
:before-close="handleClose"
|
||||
class="product-ranking-dialog"
|
||||
@update:model-value="handleClose"
|
||||
>
|
||||
<!-- 搜索和筛选区域 -->
|
||||
<div class="search-section">
|
||||
<el-form :model="searchForm" :inline="true" class="search-form">
|
||||
<el-form-item label="商品名称">
|
||||
<el-input
|
||||
v-model="searchForm.productName"
|
||||
placeholder="请输入商品名称"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="商品分类">
|
||||
<el-select
|
||||
v-model="searchForm.categoryId"
|
||||
placeholder="请选择分类"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
>
|
||||
<el-option
|
||||
v-for="category in categories"
|
||||
:key="category.id"
|
||||
:label="category.name"
|
||||
:value="category.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="库存状态">
|
||||
<el-select
|
||||
v-model="searchForm.stockStatus"
|
||||
placeholder="请选择状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="充足" value="SUFFICIENT" />
|
||||
<el-option label="不足" value="LOW" />
|
||||
<el-option label="缺货" value="OUT" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="handleSearch">
|
||||
<el-icon><Search /></el-icon>
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="handleReset">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<el-table
|
||||
:data="tableData"
|
||||
stripe
|
||||
v-loading="loading"
|
||||
@row-click="handleRowClick"
|
||||
class="ranking-table"
|
||||
height="400"
|
||||
>
|
||||
<el-table-column prop="ranking" label="排名" width="80" align="center" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getRankingTagType(row.ranking)"
|
||||
size="small"
|
||||
class="ranking-tag"
|
||||
>
|
||||
{{ row.ranking }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="productName" label="商品名称" min-width="200" show-overflow-tooltip fixed="left">
|
||||
<template #default="{ row }">
|
||||
<div class="product-info">
|
||||
<div class="product-name">{{ row.productName }}</div>
|
||||
<div class="product-code">{{ row.productCode }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="categoryName" label="分类" width="120" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="salesAmount" label="销售额" width="120" align="right" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(getNumericValue(row.salesAmount)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="salesQuantity" label="销量" width="100" align="right" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatNumber(getNumericValue(row.salesQuantity)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="salesCount" label="销售次数" width="100" align="right" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatNumber(getNumericValue(row.salesCount)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="averagePrice" label="平均单价" width="100" align="right" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(getNumericValue(row.averagePrice)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="stockQuantity" label="库存" width="100" align="right" sortable>
|
||||
<template #default="{ row }">
|
||||
{{ formatNumber(getNumericValue(row.stockQuantity)) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="stockStatus" label="库存状态" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag
|
||||
:type="getStockStatusType(row.stockStatus)"
|
||||
size="small"
|
||||
>
|
||||
{{ getStockStatusText(row.stockStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="growthRate" label="增长率" width="100" align="right" sortable>
|
||||
<template #default="{ row }">
|
||||
<span :class="getGrowthRateClass(getNumericValue(row.growthRate))">
|
||||
{{ getNumericValue(row.growthRate) > 0 ? '+' : '' }}{{ getNumericValue(row.growthRate) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="yearOverYearGrowthRate" label="同比" width="100" align="right" sortable>
|
||||
<template #default="{ row }">
|
||||
<span :class="getGrowthRateClass(getNumericValue(row.yearOverYearGrowthRate))">
|
||||
{{ getNumericValue(row.yearOverYearGrowthRate) > 0 ? '+' : '' }}{{ getNumericValue(row.yearOverYearGrowthRate) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="monthOverMonthGrowthRate" label="环比" width="100" align="right" sortable>
|
||||
<template #default="{ row }">
|
||||
<span :class="getGrowthRateClass(getNumericValue(row.monthOverMonthGrowthRate))">
|
||||
{{ getNumericValue(row.monthOverMonthGrowthRate) > 0 ? '+' : '' }}{{ getNumericValue(row.monthOverMonthGrowthRate) }}%
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="120" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="handleViewDetail(row)">
|
||||
查看详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="pagination-section">
|
||||
<el-pagination
|
||||
v-model:current-page="pagination.current"
|
||||
v-model:page-size="pagination.size"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="pagination.total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleClose">关闭</el-button>
|
||||
<el-button type="primary" @click="handleExport">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出数据
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup name="ProductRankingDialog">
|
||||
import { ref, reactive, computed, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import { Search, RefreshLeft, Download } from '@element-plus/icons-vue';
|
||||
import { getTopSellingProducts, getSlowSellingProducts } from '/@/api/supermarketerp/realtimeSalesReport';
|
||||
import { exportToCSV, formatProductDataForExport } from '../utils/exportUtils';
|
||||
|
||||
interface ProductRankingItem {
|
||||
productId: number;
|
||||
productName: string;
|
||||
productCode: string;
|
||||
categoryId?: string;
|
||||
categoryName?: string;
|
||||
salesAmount: number;
|
||||
salesQuantity: number;
|
||||
salesCount: number;
|
||||
averagePrice: number;
|
||||
ranking: number;
|
||||
stockQuantity: number;
|
||||
stockStatus: string;
|
||||
growthRate: number;
|
||||
yearOverYearGrowthRate?: number;
|
||||
monthOverMonthGrowthRate?: number;
|
||||
}
|
||||
|
||||
interface SearchForm {
|
||||
productName: string;
|
||||
categoryId: string;
|
||||
stockStatus: string;
|
||||
}
|
||||
|
||||
interface Pagination {
|
||||
current: number;
|
||||
size: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
visible: boolean;
|
||||
type: 'top' | 'slow';
|
||||
queryParams?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
visible: false,
|
||||
type: 'top',
|
||||
queryParams: () => ({})
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [];
|
||||
viewDetail: [item: ProductRankingItem];
|
||||
}>();
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false);
|
||||
const tableData = ref<ProductRankingItem[]>([]);
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive<SearchForm>({
|
||||
productName: '',
|
||||
categoryId: '',
|
||||
stockStatus: ''
|
||||
});
|
||||
|
||||
// 分页
|
||||
const pagination = reactive<Pagination>({
|
||||
current: 1,
|
||||
size: 20,
|
||||
total: 0
|
||||
});
|
||||
|
||||
// 分类数据
|
||||
const categories = ref([
|
||||
{ id: '1', name: '水果' },
|
||||
{ id: '2', name: '蔬菜' },
|
||||
{ id: '3', name: '肉类' },
|
||||
{ id: '4', name: '乳制品' },
|
||||
{ id: '5', name: '饮料' },
|
||||
{ id: '6', name: '零食' },
|
||||
{ id: '7', name: '日用品' }
|
||||
]);
|
||||
|
||||
// 计算属性
|
||||
const dialogTitle = computed(() => {
|
||||
return props.type === 'top' ? '畅销品排行榜' : '滞销品排行榜';
|
||||
});
|
||||
|
||||
// 监听弹窗显示状态
|
||||
watch(() => props.visible, (newVal) => {
|
||||
if (newVal) {
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
|
||||
// 加载数据
|
||||
const loadData = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {
|
||||
...props.queryParams,
|
||||
...searchForm,
|
||||
current: pagination.current,
|
||||
size: pagination.size
|
||||
};
|
||||
|
||||
let response;
|
||||
if (props.type === 'top') {
|
||||
response = await getTopSellingProducts(params);
|
||||
} else {
|
||||
response = await getSlowSellingProducts(params);
|
||||
}
|
||||
|
||||
if (response.code === 0 && response.data) {
|
||||
tableData.value = response.data.records || response.data || [];
|
||||
pagination.total = response.data.total || 0;
|
||||
} else {
|
||||
throw new Error(response.msg || '数据加载失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
ElMessage.error('数据加载失败');
|
||||
// 使用模拟数据
|
||||
tableData.value = getMockData();
|
||||
pagination.total = tableData.value.length;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 模拟数据
|
||||
const getMockData = (): ProductRankingItem[] => {
|
||||
const data: ProductRankingItem[] = [];
|
||||
const products = [
|
||||
{ name: '苹果', code: 'P001', category: '水果' },
|
||||
{ name: '牛奶', code: 'M002', category: '乳制品' },
|
||||
{ name: '面包', code: 'B003', category: '烘焙' },
|
||||
{ name: '矿泉水', code: 'W004', category: '饮料' },
|
||||
{ name: '鸡蛋', code: 'E005', category: '蛋类' },
|
||||
{ name: '香蕉', code: 'B006', category: '水果' },
|
||||
{ name: '酸奶', code: 'Y007', category: '乳制品' },
|
||||
{ name: '饼干', code: 'C008', category: '零食' },
|
||||
{ name: '可乐', code: 'K009', category: '饮料' },
|
||||
{ name: '橙子', code: 'O010', category: '水果' }
|
||||
];
|
||||
|
||||
for (let i = 0; i < 50; i++) {
|
||||
const product = products[i % products.length];
|
||||
const ranking = i + 1;
|
||||
data.push({
|
||||
productId: 1000 + i,
|
||||
productName: `${product.name}${i > 9 ? ` (${i + 1})` : ''}`,
|
||||
productCode: product.code.replace(/\d+/, String(i + 1).padStart(3, '0')),
|
||||
categoryId: String((i % 7) + 1),
|
||||
categoryName: product.category,
|
||||
salesAmount: Math.floor(Math.random() * 10000) + 1000,
|
||||
salesQuantity: Math.floor(Math.random() * 100) + 10,
|
||||
salesCount: Math.floor(Math.random() * 50) + 5,
|
||||
averagePrice: Math.floor(Math.random() * 100) + 10,
|
||||
ranking: ranking,
|
||||
stockQuantity: Math.floor(Math.random() * 200) + 10,
|
||||
stockStatus: ['SUFFICIENT', 'LOW', 'OUT'][Math.floor(Math.random() * 3)],
|
||||
growthRate: Math.floor(Math.random() * 40) - 20,
|
||||
yearOverYearGrowthRate: Math.floor(Math.random() * 30) - 15,
|
||||
monthOverMonthGrowthRate: Math.floor(Math.random() * 20) - 10
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 重置搜索
|
||||
const handleReset = () => {
|
||||
Object.assign(searchForm, {
|
||||
productName: '',
|
||||
categoryId: '',
|
||||
stockStatus: ''
|
||||
});
|
||||
pagination.current = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
pagination.size = size;
|
||||
pagination.current = 1;
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page: number) => {
|
||||
pagination.current = page;
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 行点击
|
||||
const handleRowClick = (item: ProductRankingItem) => {
|
||||
handleViewDetail(item);
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (item: ProductRankingItem) => {
|
||||
emit('viewDetail', item);
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
emit('close');
|
||||
};
|
||||
|
||||
// 导出数据
|
||||
const handleExport = () => {
|
||||
try {
|
||||
if (tableData.value.length === 0) {
|
||||
ElMessage.warning('没有数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
const exportData = formatProductDataForExport(tableData.value);
|
||||
const filename = `${dialogTitle.value}_${new Date().toISOString().slice(0, 10)}`;
|
||||
|
||||
exportToCSV(exportData, filename);
|
||||
ElMessage.success('数据导出成功');
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error);
|
||||
ElMessage.error('数据导出失败');
|
||||
}
|
||||
};
|
||||
|
||||
// 获取排名标签类型
|
||||
const getRankingTagType = (ranking: number) => {
|
||||
if (ranking <= 3) return 'danger';
|
||||
if (ranking <= 10) return 'warning';
|
||||
return 'primary';
|
||||
};
|
||||
|
||||
// 获取增长率样式类
|
||||
const getGrowthRateClass = (rate: number) => {
|
||||
if (rate > 0) return 'growth-up';
|
||||
if (rate < 0) return 'growth-down';
|
||||
return 'growth-stable';
|
||||
};
|
||||
|
||||
// 获取库存状态标签类型
|
||||
const getStockStatusType = (status: string) => {
|
||||
switch (status) {
|
||||
case 'SUFFICIENT':
|
||||
return 'success';
|
||||
case 'LOW':
|
||||
return 'warning';
|
||||
case 'OUT':
|
||||
return 'danger';
|
||||
default:
|
||||
return 'info';
|
||||
}
|
||||
};
|
||||
|
||||
// 获取库存状态文本
|
||||
const getStockStatusText = (status: string) => {
|
||||
switch (status) {
|
||||
case 'SUFFICIENT':
|
||||
return '充足';
|
||||
case 'LOW':
|
||||
return '不足';
|
||||
case 'OUT':
|
||||
return '缺货';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
};
|
||||
|
||||
// 格式化货币
|
||||
const formatCurrency = (value: number) => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
// 格式化数字
|
||||
const formatNumber = (value: number) => {
|
||||
return new Intl.NumberFormat('zh-CN').format(value);
|
||||
};
|
||||
|
||||
// 获取数值(处理复杂数据格式)
|
||||
const getNumericValue = (value: any) => {
|
||||
if (typeof value === 'number') {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (value.parsedValue !== undefined) {
|
||||
return value.parsedValue;
|
||||
}
|
||||
if (value.source !== undefined) {
|
||||
return parseFloat(value.source) || 0;
|
||||
}
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return parseFloat(value) || 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.product-ranking-dialog {
|
||||
.search-section {
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 6px;
|
||||
|
||||
.search-form {
|
||||
.el-form-item {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-table {
|
||||
.ranking-tag {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
.product-name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.product-code {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.growth-up {
|
||||
color: #67c23a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.growth-down {
|
||||
color: #f56c6c;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.growth-stable {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-section {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-dialog__body) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-table__row) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:deep(.el-table__row:hover) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
</style>
|
||||
@@ -337,3 +337,4 @@ const handleCurrentChange = (page: number) => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -189,3 +189,4 @@ defineExpose({
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -201,3 +201,4 @@ const trendClass = computed(() => {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
export { default as SalesChart } from './SalesChart.vue';
|
||||
export { default as SummaryCard } from './SummaryCard.vue';
|
||||
export { default as ProductRankingTable } from './ProductRankingTable.vue';
|
||||
|
||||
@@ -205,3 +205,4 @@ export const SUCCESS_MESSAGES = {
|
||||
DATA_EXPORTED: '数据导出成功',
|
||||
CACHE_REFRESHED: '缓存刷新成功',
|
||||
};
|
||||
|
||||
@@ -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,11 +415,20 @@
|
||||
</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 } from 'vue';
|
||||
import { ref, reactive, onMounted, nextTick, watch } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
import {
|
||||
Refresh,
|
||||
@@ -442,15 +451,21 @@ 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<NodeJS.Timeout | null>(null);
|
||||
const debounceTimer = ref<number | 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',
|
||||
@@ -464,16 +479,16 @@ const queryForm = reactive({
|
||||
});
|
||||
|
||||
// 高级选项显示状态
|
||||
const advancedOptionsVisible = ref([]);
|
||||
const advancedOptionsVisible = ref<string[]>([]);
|
||||
|
||||
// 数据
|
||||
const salesSummary = ref();
|
||||
const topSellingProducts = ref([]);
|
||||
const slowSellingProducts = ref([]);
|
||||
const timePeriodSales = ref([]);
|
||||
const customerAnalysis = ref();
|
||||
const salesTrend = ref([]);
|
||||
const expandedRows = 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[]>([]);
|
||||
|
||||
// 图表实例
|
||||
let salesTrendChartInstance: echarts.ECharts | null = null;
|
||||
@@ -492,9 +507,11 @@ 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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -596,20 +613,99 @@ 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(queryForm);
|
||||
const reportData = await getRealtimeSalesReport(processedParams);
|
||||
|
||||
if (reportData.code === 0 && reportData.data) {
|
||||
// 使用后端返回的数据
|
||||
salesSummary.value = reportData.data.salesSummary || getMockSalesSummary();
|
||||
salesSummary.value = reportData.data.salesSummary || {};
|
||||
topSellingProducts.value = reportData.data.topSellingProducts || [];
|
||||
slowSellingProducts.value = reportData.data.slowSellingProducts || [];
|
||||
timePeriodSales.value = reportData.data.timePeriodSales || [];
|
||||
customerAnalysis.value = reportData.data.customerAnalysis || getMockCustomerAnalysis();
|
||||
customerAnalysis.value = reportData.data.customerAnalysis || {};
|
||||
salesTrend.value = reportData.data.salesTrend || [];
|
||||
|
||||
ElMessage.success('数据加载成功');
|
||||
@@ -622,245 +718,31 @@ const loadData = async () => {
|
||||
renderCharts();
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error);
|
||||
// 使用模拟数据
|
||||
salesSummary.value = getMockSalesSummary();
|
||||
topSellingProducts.value = getMockTopProducts();
|
||||
slowSellingProducts.value = getMockSlowProducts();
|
||||
timePeriodSales.value = getMockTimePeriodSales();
|
||||
customerAnalysis.value = getMockCustomerAnalysis();
|
||||
salesTrend.value = []; // 空数组,不显示模拟数据
|
||||
// 清空数据
|
||||
salesSummary.value = {};
|
||||
topSellingProducts.value = [];
|
||||
slowSellingProducts.value = [];
|
||||
timePeriodSales.value = [];
|
||||
customerAnalysis.value = {};
|
||||
salesTrend.value = [];
|
||||
|
||||
await nextTick();
|
||||
renderCharts();
|
||||
|
||||
// 根据错误类型显示不同的提示信息
|
||||
if (error.message && error.message.includes('权限')) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
if (errorMessage.includes('权限')) {
|
||||
ElMessage.error('权限不足,无法访问即时销售报表');
|
||||
} else if (error.message && error.message.includes('网络')) {
|
||||
} else if (errorMessage.includes('网络')) {
|
||||
ElMessage.error('网络连接异常,请检查网络设置');
|
||||
} else {
|
||||
ElMessage.warning('数据加载失败,使用模拟数据展示');
|
||||
ElMessage.error('数据加载失败,请稍后重试');
|
||||
}
|
||||
} 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 },
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
// 刷新数据
|
||||
@@ -892,30 +774,48 @@ const refreshCharts = async () => {
|
||||
// 刷新时段数据
|
||||
const refreshTimePeriodData = async () => {
|
||||
try {
|
||||
const processedParams = processQueryParams(queryForm);
|
||||
const timePeriodData = await getTimePeriodSales({
|
||||
...queryForm,
|
||||
...processedParams,
|
||||
current: 1,
|
||||
size: 50,
|
||||
});
|
||||
timePeriodSales.value = timePeriodData.data?.records || getMockTimePeriodSales();
|
||||
timePeriodSales.value = timePeriodData.data?.records || [];
|
||||
ElMessage.success('时段数据刷新成功');
|
||||
} catch (error) {
|
||||
console.error('刷新时段数据失败:', error);
|
||||
timePeriodSales.value = getMockTimePeriodSales();
|
||||
ElMessage.warning('使用模拟数据');
|
||||
timePeriodSales.value = [];
|
||||
ElMessage.error('时段数据刷新失败');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 查看全部畅销品
|
||||
const viewAllTopProducts = () => {
|
||||
ElMessage.info('查看全部畅销品功能开发中...');
|
||||
};
|
||||
// 查看全部畅销品(已注释)
|
||||
// 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 viewAllSlowProducts = () => {
|
||||
ElMessage.info('查看全部滞销品功能开发中...');
|
||||
};
|
||||
|
||||
// 渲染销售趋势图表
|
||||
const renderSalesTrendChart = () => {
|
||||
@@ -926,9 +826,9 @@ const renderSalesTrendChart = () => {
|
||||
}
|
||||
|
||||
// 使用后端返回的销售趋势数据
|
||||
let xAxisData = [];
|
||||
let salesData = [];
|
||||
let countData = [];
|
||||
let xAxisData: string[] = [];
|
||||
let salesData: number[] = [];
|
||||
let countData: number[] = [];
|
||||
|
||||
if (salesTrend.value && salesTrend.value.length > 0) {
|
||||
// 使用后端返回的真实数据
|
||||
@@ -1020,28 +920,22 @@ const renderCustomerAnalysisChart = () => {
|
||||
}
|
||||
|
||||
// 使用后端数据结构
|
||||
let data = [];
|
||||
let data: any[] = [];
|
||||
if (customerAnalysis.value && customerAnalysis.value.valueRanges) {
|
||||
data = customerAnalysis.value.valueRanges.map(range => ({
|
||||
data = customerAnalysis.value.valueRanges.map((range: any) => ({
|
||||
name: range.rangeLabel,
|
||||
value: range.orderCount,
|
||||
percentage: range.percentage
|
||||
}));
|
||||
} else {
|
||||
// 模拟数据
|
||||
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 },
|
||||
];
|
||||
// 没有数据时显示空数组
|
||||
data = [];
|
||||
}
|
||||
|
||||
const option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
formatter: function(params: any) {
|
||||
return `${params.name}<br/>订单数: ${params.value}<br/>占比: ${params.data.percentage}%`;
|
||||
},
|
||||
},
|
||||
@@ -1171,68 +1065,6 @@ 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 () => {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 数据导出工具函数
|
||||
*/
|
||||
|
||||
// 导出为CSV格式
|
||||
export function exportToCSV(data: any[], filename: string, headers?: string[]) {
|
||||
if (!data || data.length === 0) {
|
||||
console.warn('没有数据可导出');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取表头
|
||||
const csvHeaders = headers || Object.keys(data[0]);
|
||||
|
||||
// 构建CSV内容
|
||||
let csvContent = '\uFEFF'; // 添加BOM以支持中文
|
||||
csvContent += csvHeaders.join(',') + '\n';
|
||||
|
||||
// 添加数据行
|
||||
data.forEach(row => {
|
||||
const values = csvHeaders.map(header => {
|
||||
let value = row[header];
|
||||
if (value === null || value === undefined) {
|
||||
value = '';
|
||||
}
|
||||
// 处理包含逗号的值
|
||||
if (typeof value === 'string' && value.includes(',')) {
|
||||
value = `"${value}"`;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
csvContent += values.join(',') + '\n';
|
||||
});
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
link.setAttribute('href', url);
|
||||
link.setAttribute('download', `${filename}.csv`);
|
||||
link.style.visibility = 'hidden';
|
||||
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
// 导出为Excel格式(需要xlsx库)
|
||||
export function exportToExcel(data: any[], filename: string, sheetName: string = 'Sheet1') {
|
||||
// 这里需要安装 xlsx 库: npm install xlsx
|
||||
// import * as XLSX from 'xlsx';
|
||||
|
||||
console.log('Excel导出功能需要安装xlsx库');
|
||||
console.log('安装命令: npm install xlsx');
|
||||
console.log('数据:', data);
|
||||
console.log('文件名:', filename);
|
||||
console.log('工作表名:', sheetName);
|
||||
|
||||
// 临时使用CSV导出
|
||||
exportToCSV(data, filename);
|
||||
}
|
||||
|
||||
// 格式化商品排名数据用于导出
|
||||
export function formatProductDataForExport(data: any[]) {
|
||||
return data.map((item, index) => ({
|
||||
排名: item.ranking || index + 1,
|
||||
商品名称: item.productName || '',
|
||||
商品编码: item.productCode || '',
|
||||
分类: item.categoryName || '',
|
||||
销售额: item.salesAmount || 0,
|
||||
销量: item.salesQuantity || 0,
|
||||
销售次数: item.salesCount || 0,
|
||||
平均单价: item.averagePrice || 0,
|
||||
库存: item.stockQuantity || 0,
|
||||
库存状态: getStockStatusText(item.stockStatus),
|
||||
增长率: `${item.growthRate || 0}%`,
|
||||
同比: `${item.yearOverYearGrowthRate || 0}%`,
|
||||
环比: `${item.monthOverMonthGrowthRate || 0}%`
|
||||
}));
|
||||
}
|
||||
|
||||
// 获取库存状态文本
|
||||
function getStockStatusText(status: string) {
|
||||
switch (status) {
|
||||
case 'SUFFICIENT':
|
||||
return '充足';
|
||||
case 'LOW':
|
||||
return '不足';
|
||||
case 'OUT':
|
||||
return '缺货';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化销售汇总数据用于导出
|
||||
export function formatSalesSummaryForExport(data: any) {
|
||||
return [{
|
||||
总销售额: data.totalSalesAmount || 0,
|
||||
总销售笔数: data.totalSalesCount || 0,
|
||||
平均客单价: data.averageCustomerValue || 0,
|
||||
商品种类数: data.productVarietyCount || 0,
|
||||
总销量: data.totalSalesQuantity || 0,
|
||||
销售增长率: `${data.salesGrowthRate || 0}%`,
|
||||
月度增长率: `${data.salesGrowthRateMonth || 0}%`,
|
||||
实时状态: getRealtimeStatusText(data.realtimeStatus)
|
||||
}];
|
||||
}
|
||||
|
||||
// 获取实时状态文本
|
||||
function getRealtimeStatusText(status: string) {
|
||||
switch (status) {
|
||||
case 'HIGH':
|
||||
return '高';
|
||||
case 'MEDIUM':
|
||||
return '中';
|
||||
case 'LOW':
|
||||
return '低';
|
||||
default:
|
||||
return '未知';
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化时段销售数据用于导出
|
||||
export function formatTimePeriodDataForExport(data: any[]) {
|
||||
return data.map(item => ({
|
||||
时段: item.timeLabel || '',
|
||||
销售额: item.salesAmount || 0,
|
||||
销售笔数: item.salesCount || 0,
|
||||
客单价: item.customerValue || 0,
|
||||
销量: item.salesQuantity || 0,
|
||||
增长率: `${item.growthRate || 0}%`
|
||||
}));
|
||||
}
|
||||
@@ -194,3 +194,4 @@ export const TABLE_CONFIG = {
|
||||
size: 'default' as const,
|
||||
showOverflowTooltip: true,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
# 毛利润报表业务代码整理
|
||||
|
||||
## 代码结构优化建议
|
||||
|
||||
### 1. 服务层重构
|
||||
|
||||
#### 1.1 将大方法拆分为小方法
|
||||
```java
|
||||
@Service
|
||||
public class ProfitMarginReportServiceImpl implements ProfitMarginReportService {
|
||||
|
||||
// 主入口方法 - 保持简洁
|
||||
@Override
|
||||
public ProfitMarginReportVO getProfitMarginReport(ProfitMarginReportQueryDTO queryDTO) {
|
||||
ProfitMarginReportVO reportVO = new ProfitMarginReportVO();
|
||||
reportVO.setReportTime(LocalDateTime.now());
|
||||
reportVO.setTimeRange(buildTimeRange(queryDTO));
|
||||
|
||||
// 并行获取各类数据
|
||||
CompletableFuture<ProfitSummaryVO> profitSummaryFuture =
|
||||
CompletableFuture.supplyAsync(() -> getProfitSummary(queryDTO));
|
||||
CompletableFuture<List<ProductProfitVO>> productRankingFuture =
|
||||
CompletableFuture.supplyAsync(() -> getProductProfitRankingData(queryDTO));
|
||||
CompletableFuture<List<CategoryProfitVO>> categoryAnalysisFuture =
|
||||
CompletableFuture.supplyAsync(() -> getCategoryProfitAnalysisData(queryDTO));
|
||||
CompletableFuture<List<TimePeriodProfitVO>> timeTrendFuture =
|
||||
CompletableFuture.supplyAsync(() -> getTimePeriodProfitTrendData(queryDTO));
|
||||
|
||||
try {
|
||||
reportVO.setProfitSummary(profitSummaryFuture.get());
|
||||
reportVO.setProductProfitRanking(productRankingFuture.get());
|
||||
reportVO.setCategoryProfitAnalysis(categoryAnalysisFuture.get());
|
||||
reportVO.setTimePeriodProfitTrend(timeTrendFuture.get());
|
||||
|
||||
// 基于商品排名数据生成高/低毛利润商品
|
||||
List<ProductProfitVO> allProducts = productRankingFuture.get();
|
||||
reportVO.setHighProfitProducts(filterHighProfitProducts(allProducts));
|
||||
reportVO.setLowProfitProducts(filterLowProfitProducts(allProducts));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("获取毛利润报表数据失败", e);
|
||||
throw new BusinessException("获取毛利润报表数据失败");
|
||||
}
|
||||
|
||||
return reportVO;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 数据查询优化
|
||||
```java
|
||||
@Component
|
||||
public class ProfitMarginDataQueryService {
|
||||
|
||||
/**
|
||||
* 获取销售明细数据 - 统一入口
|
||||
*/
|
||||
public List<SmErpInventoryRecordsItemEntity> getSalesItems(ProfitMarginReportQueryDTO queryDTO) {
|
||||
// 1. 先筛选商品ID(如果有商品筛选条件)
|
||||
Set<Long> filteredProductIds = getFilteredProductIds(queryDTO);
|
||||
|
||||
// 2. 查询销售明细
|
||||
return querySalesItems(queryDTO, filteredProductIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取商品信息
|
||||
*/
|
||||
public Map<Long, SmErpSpuEntity> getProductMap(Set<Long> productIds) {
|
||||
if (CollectionUtils.isEmpty(productIds)) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
List<SmErpSpuEntity> products = spuMapper.selectList(
|
||||
new LambdaQueryWrapper<SmErpSpuEntity>()
|
||||
.in(SmErpSpuEntity::getId, productIds)
|
||||
);
|
||||
|
||||
return products.stream()
|
||||
.collect(Collectors.toMap(SmErpSpuEntity::getId, product -> product));
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取分类信息
|
||||
*/
|
||||
public Map<Long, SmErpCategoryEntity> getCategoryMap(Set<Long> categoryIds) {
|
||||
if (CollectionUtils.isEmpty(categoryIds)) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
List<SmErpCategoryEntity> categories = categoryMapper.selectList(
|
||||
new LambdaQueryWrapper<SmErpCategoryEntity>()
|
||||
.in(SmErpCategoryEntity::getId, categoryIds)
|
||||
);
|
||||
|
||||
return categories.stream()
|
||||
.collect(Collectors.toMap(SmErpCategoryEntity::getId, category -> category));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 计算服务分离
|
||||
```java
|
||||
@Component
|
||||
public class ProfitCalculationService {
|
||||
|
||||
/**
|
||||
* 计算商品毛利润数据
|
||||
*/
|
||||
public ProductProfitVO calculateProductProfit(
|
||||
SmErpSpuEntity product,
|
||||
List<SmErpInventoryRecordsItemEntity> items,
|
||||
Map<Long, SmErpUnitEntity> unitMap,
|
||||
Map<Long, SmErpCategoryEntity> categoryMap) {
|
||||
|
||||
// 过滤补差价商品
|
||||
if (isPriceAdjustmentProduct(product.getProductName())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 计算销售数据
|
||||
BigDecimal salesAmount = calculateSalesAmount(items);
|
||||
double salesQuantity = calculateSalesQuantity(items);
|
||||
int salesCount = items.size();
|
||||
|
||||
// 计算成本数据
|
||||
BigDecimal costPrice = getActualCostPrice(product, items.get(0), unitMap);
|
||||
BigDecimal costAmount = costPrice.multiply(new BigDecimal(salesQuantity));
|
||||
|
||||
// 计算毛利润
|
||||
BigDecimal profitAmount = salesAmount.subtract(costAmount);
|
||||
BigDecimal marginRate = calculateMarginRate(salesAmount, profitAmount);
|
||||
|
||||
// 构建结果
|
||||
ProductProfitVO profitVO = new ProductProfitVO();
|
||||
profitVO.setProductId(product.getId());
|
||||
profitVO.setProductName(product.getProductName());
|
||||
profitVO.setProductCode(product.getProductCode());
|
||||
profitVO.setSalesAmount(salesAmount);
|
||||
profitVO.setCostAmount(costAmount);
|
||||
profitVO.setProfitAmount(profitAmount);
|
||||
profitVO.setMarginRate(marginRate);
|
||||
profitVO.setSalesQuantity(salesQuantity);
|
||||
profitVO.setSalesCount(salesCount);
|
||||
profitVO.setProfitStatus(determineProfitStatus(profitAmount));
|
||||
|
||||
// 设置分类信息
|
||||
if (product.getCategoryId() != null) {
|
||||
SmErpCategoryEntity category = categoryMap.get(product.getCategoryId());
|
||||
if (category != null) {
|
||||
profitVO.setCategoryId(category.getId());
|
||||
profitVO.setCategoryName(category.getName());
|
||||
}
|
||||
}
|
||||
|
||||
// 设置单位信息
|
||||
if (!items.isEmpty() && items.get(0).getUnitId() != null) {
|
||||
SmErpUnitEntity unit = unitMap.get(items.get(0).getUnitId());
|
||||
if (unit != null) {
|
||||
profitVO.setUnit(unit.getUnit());
|
||||
profitVO.setUnitName(unit.getUnit());
|
||||
}
|
||||
}
|
||||
|
||||
return profitVO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算毛利率
|
||||
*/
|
||||
private BigDecimal calculateMarginRate(BigDecimal salesAmount, BigDecimal profitAmount) {
|
||||
if (salesAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
return profitAmount.divide(salesAmount, 4, RoundingMode.HALF_UP)
|
||||
.multiply(new BigDecimal("100"))
|
||||
.setScale(2, RoundingMode.HALF_UP);
|
||||
}
|
||||
return BigDecimal.ZERO;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断利润状态
|
||||
*/
|
||||
private String determineProfitStatus(BigDecimal profitAmount) {
|
||||
if (profitAmount.compareTo(BigDecimal.ZERO) > 0) {
|
||||
return "profitable";
|
||||
} else if (profitAmount.compareTo(BigDecimal.ZERO) < 0) {
|
||||
return "loss";
|
||||
} else {
|
||||
return "break_even";
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 查询优化
|
||||
|
||||
#### 2.1 使用MyBatis-Plus的关联查询
|
||||
```java
|
||||
@Mapper
|
||||
public interface SmErpInventoryRecordsItemMapper extends BaseMapper<SmErpInventoryRecordsItemEntity> {
|
||||
|
||||
/**
|
||||
* 关联查询销售明细和主表信息
|
||||
*/
|
||||
@Select("SELECT item.*, record.inventory_date, record.create_time " +
|
||||
"FROM sm_erp_inventory_records_item item " +
|
||||
"INNER JOIN sm_erp_inventory_records record ON item.inventory_id = record.id " +
|
||||
"WHERE record.inventory_type = '5' AND record.order_type = '2' " +
|
||||
"AND record.inventory_date >= #{startDate} AND record.inventory_date <= #{endDate} " +
|
||||
"LIMIT #{limit}")
|
||||
List<SalesItemWithDateVO> selectSalesItemsWithDate(
|
||||
@Param("startDate") LocalDate startDate,
|
||||
@Param("endDate") LocalDate endDate,
|
||||
@Param("limit") int limit);
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 缓存优化
|
||||
```java
|
||||
@Service
|
||||
public class ProfitMarginCacheService {
|
||||
|
||||
@Cacheable(value = "productInfo", key = "#productIds")
|
||||
public Map<Long, SmErpSpuEntity> getProductMap(Set<Long> productIds) {
|
||||
// 查询逻辑
|
||||
}
|
||||
|
||||
@Cacheable(value = "categoryInfo", key = "#categoryIds")
|
||||
public Map<Long, SmErpCategoryEntity> getCategoryMap(Set<Long> categoryIds) {
|
||||
// 查询逻辑
|
||||
}
|
||||
|
||||
@Cacheable(value = "unitInfo", key = "#unitIds")
|
||||
public Map<Long, SmErpUnitEntity> getUnitMap(Set<Long> unitIds) {
|
||||
// 查询逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 配置优化
|
||||
|
||||
#### 3.1 分页配置
|
||||
```java
|
||||
@Configuration
|
||||
public class ProfitMarginConfig {
|
||||
|
||||
@Bean
|
||||
public PageInterceptor pageInterceptor() {
|
||||
PageInterceptor pageInterceptor = new PageInterceptor();
|
||||
Properties properties = new Properties();
|
||||
properties.setProperty("reasonable", "true");
|
||||
properties.setProperty("supportMethodsArguments", "true");
|
||||
properties.setProperty("returnPageInfo", "check");
|
||||
properties.setProperty("params", "count=countSql");
|
||||
pageInterceptor.setProperties(properties);
|
||||
return pageInterceptor;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 性能监控
|
||||
```java
|
||||
@Component
|
||||
@Aspect
|
||||
public class ProfitMarginPerformanceAspect {
|
||||
|
||||
@Around("execution(* com.bsd.pjcloud.supermarket.service.impl.ProfitMarginReportServiceImpl.*(..))")
|
||||
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
long startTime = System.currentTimeMillis();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
|
||||
try {
|
||||
Object result = joinPoint.proceed();
|
||||
long endTime = System.currentTimeMillis();
|
||||
long duration = endTime - startTime;
|
||||
|
||||
log.info("方法 {} 执行耗时: {}ms", methodName, duration);
|
||||
|
||||
// 如果执行时间超过阈值,记录警告
|
||||
if (duration > 5000) {
|
||||
log.warn("方法 {} 执行时间过长: {}ms", methodName, duration);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
long endTime = System.currentTimeMillis();
|
||||
log.error("方法 {} 执行失败,耗时: {}ms", methodName, endTime - startTime, e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 异常处理优化
|
||||
|
||||
#### 4.1 自定义异常
|
||||
```java
|
||||
public class ProfitMarginException extends BusinessException {
|
||||
public ProfitMarginException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public ProfitMarginException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2 统一异常处理
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class ProfitMarginExceptionHandler {
|
||||
|
||||
@ExceptionHandler(ProfitMarginException.class)
|
||||
public Result<Void> handleProfitMarginException(ProfitMarginException e) {
|
||||
log.error("毛利润报表异常", e);
|
||||
return Result.fail(e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(DataAccessException.class)
|
||||
public Result<Void> handleDataAccessException(DataAccessException e) {
|
||||
log.error("数据访问异常", e);
|
||||
return Result.fail("数据查询失败,请稍后重试");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 测试优化
|
||||
|
||||
#### 5.1 单元测试
|
||||
```java
|
||||
@SpringBootTest
|
||||
class ProfitMarginReportServiceTest {
|
||||
|
||||
@Autowired
|
||||
private ProfitMarginReportService profitMarginReportService;
|
||||
|
||||
@Test
|
||||
void testGetProfitMarginReport() {
|
||||
// 准备测试数据
|
||||
ProfitMarginReportQueryDTO queryDTO = new ProfitMarginReportQueryDTO();
|
||||
queryDTO.setQueryType("day");
|
||||
queryDTO.setStartDate(LocalDate.now().minusDays(7));
|
||||
queryDTO.setEndDate(LocalDate.now());
|
||||
|
||||
// 执行测试
|
||||
ProfitMarginReportVO result = profitMarginReportService.getProfitMarginReport(queryDTO);
|
||||
|
||||
// 验证结果
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getProfitSummary()).isNotNull();
|
||||
assertThat(result.getProductProfitRanking()).isNotEmpty();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6. 主要优化点总结
|
||||
|
||||
1. **方法拆分**: 将大方法拆分为职责单一的小方法
|
||||
2. **并行处理**: 使用CompletableFuture并行获取数据
|
||||
3. **缓存优化**: 对频繁查询的数据进行缓存
|
||||
4. **查询优化**: 使用关联查询减少数据库访问次数
|
||||
5. **异常处理**: 统一异常处理和错误信息
|
||||
6. **性能监控**: 添加性能监控和告警
|
||||
7. **测试覆盖**: 完善单元测试和集成测试
|
||||
|
||||
这样重构后的代码将更加清晰、可维护,并且性能更好。
|
||||
File diff suppressed because it is too large.
Load diff
@@ -111,7 +111,16 @@
|
||||
<el-input v-model="row.quantity" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="unitPrice" title="零售价">
|
||||
<vxe-column field="costPrice" title="成本价">
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
row.costPrice
|
||||
? displayPrice(row.costPrice)
|
||||
: displayPrice(row.erpUnitVos?.find((item: any) => item.id == row.unitId)?.costPrice)
|
||||
}}
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column field="retailPrice" title="零售价">
|
||||
<template #default="{ row }">
|
||||
{{
|
||||
row.retailPrice
|
||||
|
||||
Reference in new issue
Block a user