即时销售报表
This commit is contained in:
1 parent
ca3c0b2a13
commit
45c2bd0e88
7 files changed
+2623
No files matched your search
@@ -0,0 +1,339 @@
|
||||
<template>
|
||||
<el-card class="ranking-table-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span class="card-title">{{ title }}</span>
|
||||
<div class="card-actions">
|
||||
<el-button size="small" @click="refreshData">
|
||||
<el-icon><Refresh /></el-icon>
|
||||
刷新
|
||||
</el-button>
|
||||
<el-button size="small" @click="viewAll">
|
||||
查看全部
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table
|
||||
:data="data"
|
||||
stripe
|
||||
:loading="loading"
|
||||
@row-click="handleRowClick"
|
||||
class="ranking-table"
|
||||
>
|
||||
<el-table-column prop="ranking" label="排名" width="60" align="center">
|
||||
<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="150" show-overflow-tooltip>
|
||||
<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="100" show-overflow-tooltip />
|
||||
|
||||
<el-table-column prop="salesAmount" label="销售额" width="120" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="amount-cell">
|
||||
<div class="amount-value">{{ formatCurrency(row.salesAmount) }}</div>
|
||||
<div v-if="row.yearOverYearGrowthRate !== undefined" class="growth-rate">
|
||||
<el-icon
|
||||
v-if="row.yearOverYearGrowthRate > 0"
|
||||
class="growth-icon up"
|
||||
>
|
||||
<ArrowUp />
|
||||
</el-icon>
|
||||
<el-icon
|
||||
v-else-if="row.yearOverYearGrowthRate < 0"
|
||||
class="growth-icon down"
|
||||
>
|
||||
<ArrowDown />
|
||||
</el-icon>
|
||||
<span
|
||||
:class="getGrowthRateClass(row.yearOverYearGrowthRate)"
|
||||
>
|
||||
{{ Math.abs(row.yearOverYearGrowthRate).toFixed(1) }}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="salesQuantity" label="销量" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatNumber(row.salesQuantity) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="salesCount" label="笔数" width="80" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatNumber(row.salesCount) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="averagePrice" label="均价" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
{{ formatCurrency(row.averagePrice) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="marketShare" label="市场份额" width="100" align="right">
|
||||
<template #default="{ row }">
|
||||
<div class="market-share">
|
||||
<div class="share-value">{{ (row.marketShare * 100).toFixed(1) }}%</div>
|
||||
<div class="share-bar">
|
||||
<div
|
||||
class="share-progress"
|
||||
:style="{ width: (row.marketShare * 100) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="80" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="small"
|
||||
text
|
||||
@click.stop="viewDetail(row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div v-if="showPagination" class="pagination-wrapper">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Refresh, ArrowUp, ArrowDown } from '@element-plus/icons-vue';
|
||||
|
||||
interface ProductRankingItem {
|
||||
productId: string;
|
||||
productName: string;
|
||||
productCode: string;
|
||||
categoryName: string;
|
||||
salesAmount: number;
|
||||
salesQuantity: number;
|
||||
salesCount: number;
|
||||
averagePrice: number;
|
||||
ranking: number;
|
||||
marketShare: number;
|
||||
yearOverYearGrowthRate?: number;
|
||||
monthOverMonthGrowthRate?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
data: ProductRankingItem[];
|
||||
loading?: boolean;
|
||||
showPagination?: boolean;
|
||||
total?: number;
|
||||
pageSize?: number;
|
||||
currentPage?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
loading: false,
|
||||
showPagination: false,
|
||||
total: 0,
|
||||
pageSize: 10,
|
||||
currentPage: 1,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
refresh: [];
|
||||
viewAll: [];
|
||||
viewDetail: [item: ProductRankingItem];
|
||||
rowClick: [item: ProductRankingItem];
|
||||
pageChange: [page: number, size: number];
|
||||
}>();
|
||||
|
||||
// 获取排名标签类型
|
||||
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 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 refreshData = () => {
|
||||
emit('refresh');
|
||||
};
|
||||
|
||||
// 查看全部
|
||||
const viewAll = () => {
|
||||
emit('viewAll');
|
||||
};
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (item: ProductRankingItem) => {
|
||||
emit('viewDetail', item);
|
||||
};
|
||||
|
||||
// 行点击
|
||||
const handleRowClick = (item: ProductRankingItem) => {
|
||||
emit('rowClick', item);
|
||||
};
|
||||
|
||||
// 分页大小变化
|
||||
const handleSizeChange = (size: number) => {
|
||||
emit('pageChange', props.currentPage, size);
|
||||
};
|
||||
|
||||
// 当前页变化
|
||||
const handleCurrentChange = (page: number) => {
|
||||
emit('pageChange', page, props.pageSize);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ranking-table-card {
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.card-title {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.card-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-table {
|
||||
.ranking-tag {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
.product-name {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.product-code {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.amount-cell {
|
||||
.amount-value {
|
||||
font-weight: 500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.growth-rate {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
|
||||
.growth-icon {
|
||||
margin-right: 2px;
|
||||
|
||||
&.up {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.down {
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
.growth-up {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.growth-down {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.growth-stable {
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.market-share {
|
||||
.share-value {
|
||||
font-weight: 500;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.share-bar {
|
||||
height: 4px;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.share-progress {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #409eff, #67c23a);
|
||||
border-radius: 2px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-wrapper {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div ref="chartContainer" class="sales-chart" :style="{ height: height + 'px' }"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
interface Props {
|
||||
data: any[];
|
||||
type: 'line' | 'bar' | 'pie' | 'area';
|
||||
height?: number;
|
||||
title?: string;
|
||||
xAxisData?: string[];
|
||||
series?: any[];
|
||||
options?: any;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
height: 300,
|
||||
type: 'line',
|
||||
data: () => [],
|
||||
xAxisData: () => [],
|
||||
series: () => [],
|
||||
options: () => ({}),
|
||||
});
|
||||
|
||||
const chartContainer = ref<HTMLElement>();
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (!chartContainer.value) return;
|
||||
|
||||
chartInstance = echarts.init(chartContainer.value);
|
||||
updateChart();
|
||||
};
|
||||
|
||||
// 更新图表
|
||||
const updateChart = () => {
|
||||
if (!chartInstance) return;
|
||||
|
||||
const option = getChartOption();
|
||||
chartInstance.setOption(option, true);
|
||||
};
|
||||
|
||||
// 获取图表配置
|
||||
const getChartOption = () => {
|
||||
const baseOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
...props.options,
|
||||
};
|
||||
|
||||
switch (props.type) {
|
||||
case 'line':
|
||||
return {
|
||||
...baseOption,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.xAxisData,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: props.series.map(item => ({
|
||||
type: 'line',
|
||||
smooth: true,
|
||||
...item,
|
||||
})),
|
||||
};
|
||||
|
||||
case 'bar':
|
||||
return {
|
||||
...baseOption,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.xAxisData,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: props.series.map(item => ({
|
||||
type: 'bar',
|
||||
...item,
|
||||
})),
|
||||
};
|
||||
|
||||
case 'pie':
|
||||
return {
|
||||
...baseOption,
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c} ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
series: [{
|
||||
name: props.title || '数据',
|
||||
type: 'pie',
|
||||
radius: '50%',
|
||||
data: props.data,
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
shadowBlur: 10,
|
||||
shadowOffsetX: 0,
|
||||
shadowColor: 'rgba(0, 0, 0, 0.5)',
|
||||
},
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
case 'area':
|
||||
return {
|
||||
...baseOption,
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: props.xAxisData,
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
},
|
||||
series: props.series.map(item => ({
|
||||
type: 'line',
|
||||
areaStyle: {},
|
||||
smooth: true,
|
||||
...item,
|
||||
})),
|
||||
};
|
||||
|
||||
default:
|
||||
return baseOption;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听数据变化
|
||||
watch(() => props.data, () => {
|
||||
nextTick(() => {
|
||||
updateChart();
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
watch(() => props.series, () => {
|
||||
nextTick(() => {
|
||||
updateChart();
|
||||
});
|
||||
}, { deep: true });
|
||||
|
||||
// 监听窗口大小变化
|
||||
const handleResize = () => {
|
||||
if (chartInstance) {
|
||||
chartInstance.resize();
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (chartInstance) {
|
||||
chartInstance.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
// 暴露方法
|
||||
defineExpose({
|
||||
updateChart,
|
||||
getChartInstance: () => chartInstance,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.sales-chart {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<el-card class="summary-card" :class="cardClass" shadow="never">
|
||||
<div class="card-content">
|
||||
<div class="card-icon" :class="iconClass">
|
||||
<el-icon :size="iconSize">
|
||||
<component :is="icon" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-title">{{ title }}</div>
|
||||
<div class="card-value">{{ formattedValue }}</div>
|
||||
<div v-if="subtitle" class="card-subtitle">{{ subtitle }}</div>
|
||||
<div v-if="trend !== undefined" class="card-trend" :class="trendClass">
|
||||
<el-icon v-if="trend > 0"><ArrowUp /></el-icon>
|
||||
<el-icon v-else-if="trend < 0"><ArrowDown /></el-icon>
|
||||
<span>{{ Math.abs(trend).toFixed(2) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { ArrowUp, ArrowDown } from '@element-plus/icons-vue';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
value: number | string;
|
||||
subtitle?: string;
|
||||
trend?: number;
|
||||
icon: string;
|
||||
type?: 'primary' | 'success' | 'warning' | 'danger' | 'info';
|
||||
format?: 'currency' | 'number' | 'percent';
|
||||
iconSize?: number;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'primary',
|
||||
format: 'number',
|
||||
iconSize: 24,
|
||||
});
|
||||
|
||||
// 格式化值
|
||||
const formattedValue = computed(() => {
|
||||
if (typeof props.value === 'string') {
|
||||
return props.value;
|
||||
}
|
||||
|
||||
switch (props.format) {
|
||||
case 'currency':
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency: 'CNY',
|
||||
}).format(props.value);
|
||||
|
||||
case 'percent':
|
||||
return `${props.value.toFixed(2)}%`;
|
||||
|
||||
case 'number':
|
||||
default:
|
||||
return new Intl.NumberFormat('zh-CN').format(props.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 卡片样式类
|
||||
const cardClass = computed(() => {
|
||||
return `summary-card--${props.type}`;
|
||||
});
|
||||
|
||||
// 图标样式类
|
||||
const iconClass = computed(() => {
|
||||
return `card-icon--${props.type}`;
|
||||
});
|
||||
|
||||
// 趋势样式类
|
||||
const trendClass = computed(() => {
|
||||
if (props.trend === undefined) return '';
|
||||
if (props.trend > 0) return 'trend-up';
|
||||
if (props.trend < 0) return 'trend-down';
|
||||
return 'trend-stable';
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.summary-card {
|
||||
height: 120px;
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&--primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
|
||||
.card-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: linear-gradient(135deg, #43e97b 0%, #38f9d7 100%);
|
||||
color: white;
|
||||
|
||||
.card-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
|
||||
.card-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
||||
color: white;
|
||||
|
||||
.card-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
||||
color: white;
|
||||
|
||||
.card-icon {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.card-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
.card-icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.card-info {
|
||||
flex: 1;
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
font-size: 12px;
|
||||
opacity: 0.8;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
&.trend-up {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.trend-down {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
&.trend-stable {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.el-icon {
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* 即时销售报表组件导出
|
||||
*/
|
||||
|
||||
export { default as SalesChart } from './SalesChart.vue';
|
||||
export { default as SummaryCard } from './SummaryCard.vue';
|
||||
export { default as ProductRankingTable } from './ProductRankingTable.vue';
|
||||
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* 即时销售报表常量定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* 查询类型枚举
|
||||
*/
|
||||
export enum QueryType {
|
||||
HOUR = 'hour',
|
||||
DAY = 'day',
|
||||
MONTH = 'month',
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询类型选项
|
||||
*/
|
||||
export const QUERY_TYPE_OPTIONS = [
|
||||
{ label: '按小时', value: QueryType.HOUR },
|
||||
{ label: '按天', value: QueryType.DAY },
|
||||
{ label: '按月', value: QueryType.MONTH },
|
||||
];
|
||||
|
||||
/**
|
||||
* 图表类型枚举
|
||||
*/
|
||||
export enum ChartType {
|
||||
LINE = 'line',
|
||||
BAR = 'bar',
|
||||
PIE = 'pie',
|
||||
AREA = 'area',
|
||||
}
|
||||
|
||||
/**
|
||||
* 趋势类型枚举
|
||||
*/
|
||||
export enum TrendType {
|
||||
UP = 'up',
|
||||
DOWN = 'down',
|
||||
STABLE = 'stable',
|
||||
}
|
||||
|
||||
/**
|
||||
* 排名类型枚举
|
||||
*/
|
||||
export enum RankingType {
|
||||
TOP_SELLING = 'topSelling',
|
||||
SLOW_SELLING = 'slowSelling',
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间间隔配置
|
||||
*/
|
||||
export const TIME_INTERVALS = {
|
||||
[QueryType.HOUR]: 60, // 分钟
|
||||
[QueryType.DAY]: 24, // 小时
|
||||
[QueryType.MONTH]: 30, // 天
|
||||
};
|
||||
|
||||
/**
|
||||
* 分页配置
|
||||
*/
|
||||
export const PAGINATION_CONFIG = {
|
||||
pageSizes: [10, 20, 50, 100],
|
||||
defaultPageSize: 10,
|
||||
defaultCurrentPage: 1,
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格列配置
|
||||
*/
|
||||
export const TABLE_COLUMNS = {
|
||||
PRODUCT_RANKING: [
|
||||
{ prop: 'ranking', label: '排名', width: 60, align: 'center' },
|
||||
{ prop: 'productName', label: '商品名称', minWidth: 150 },
|
||||
{ prop: 'categoryName', label: '分类', width: 100 },
|
||||
{ prop: 'salesAmount', label: '销售额', width: 120, align: 'right' },
|
||||
{ prop: 'salesQuantity', label: '销量', width: 100, align: 'right' },
|
||||
{ prop: 'salesCount', label: '笔数', width: 80, align: 'right' },
|
||||
{ prop: 'averagePrice', label: '均价', width: 100, align: 'right' },
|
||||
{ prop: 'marketShare', label: '市场份额', width: 100, align: 'right' },
|
||||
],
|
||||
TIME_PERIOD_SALES: [
|
||||
{ prop: 'timePeriodName', label: '时段', width: 120 },
|
||||
{ prop: 'salesAmount', label: '销售额', width: 120, align: 'right' },
|
||||
{ prop: 'salesCount', label: '销售笔数', width: 100, align: 'right' },
|
||||
{ prop: 'salesQuantity', label: '销售数量', width: 100, align: 'right' },
|
||||
{ prop: 'averageCustomerPrice', label: '平均客单价', width: 120, align: 'right' },
|
||||
{ prop: 'customerCount', label: '客户数', width: 80, align: 'right' },
|
||||
{ prop: 'productVarietyCount', label: '商品种类', width: 100, align: 'right' },
|
||||
{ prop: 'yearOverYearSalesAmount', label: '同比销售额', width: 120, align: 'right' },
|
||||
{ prop: 'monthOverMonthSalesAmount', label: '环比销售额', width: 120, align: 'right' },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* 图表配置
|
||||
*/
|
||||
export const CHART_OPTIONS = {
|
||||
SALES_TREND: {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
},
|
||||
legend: {
|
||||
data: ['销售额', '销售笔数'],
|
||||
},
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
},
|
||||
yAxis: [
|
||||
{
|
||||
type: 'value',
|
||||
name: '销售额(元)',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
type: 'value',
|
||||
name: '销售笔数',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
CUSTOMER_ANALYSIS: {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{a} <br/>{b}: {c}% ({d}%)',
|
||||
},
|
||||
legend: {
|
||||
orient: 'vertical',
|
||||
left: 'left',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 权限配置
|
||||
*/
|
||||
export const PERMISSIONS = {
|
||||
VIEW: 'supermarket_realtimeSalesReport_view',
|
||||
EDIT: 'supermarket_realtimeSalesReport_edit',
|
||||
EXPORT: 'supermarket_realtimeSalesReport_export',
|
||||
};
|
||||
|
||||
/**
|
||||
* 默认查询条件
|
||||
*/
|
||||
export const DEFAULT_QUERY = {
|
||||
queryType: QueryType.DAY,
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
storeId: '',
|
||||
categoryId: '',
|
||||
productId: '',
|
||||
topLimit: 10,
|
||||
current: 1,
|
||||
size: 10,
|
||||
};
|
||||
|
||||
/**
|
||||
* 刷新间隔配置(毫秒)
|
||||
*/
|
||||
export const REFRESH_INTERVALS = {
|
||||
REALTIME: 60000, // 1分钟
|
||||
NORMAL: 300000, // 5分钟
|
||||
SLOW: 900000, // 15分钟
|
||||
};
|
||||
|
||||
/**
|
||||
* 导出配置
|
||||
*/
|
||||
export const EXPORT_CONFIG = {
|
||||
FORMATS: ['xlsx', 'pdf', 'csv'],
|
||||
DEFAULT_FORMAT: 'xlsx',
|
||||
MAX_ROWS: 10000,
|
||||
};
|
||||
|
||||
/**
|
||||
* 缓存配置
|
||||
*/
|
||||
export const CACHE_CONFIG = {
|
||||
TTL: 300000, // 5分钟
|
||||
KEY_PREFIX: 'realtime_sales_report_',
|
||||
};
|
||||
|
||||
/**
|
||||
* 错误消息
|
||||
*/
|
||||
export const ERROR_MESSAGES = {
|
||||
LOAD_DATA_FAILED: '加载数据失败',
|
||||
REFRESH_DATA_FAILED: '刷新数据失败',
|
||||
EXPORT_DATA_FAILED: '导出数据失败',
|
||||
INVALID_QUERY_PARAMS: '查询参数无效',
|
||||
NO_DATA: '暂无数据',
|
||||
NETWORK_ERROR: '网络错误,请检查网络连接',
|
||||
};
|
||||
|
||||
/**
|
||||
* 成功消息
|
||||
*/
|
||||
export const SUCCESS_MESSAGES = {
|
||||
DATA_LOADED: '数据加载成功',
|
||||
DATA_REFRESHED: '数据刷新成功',
|
||||
DATA_EXPORTED: '数据导出成功',
|
||||
CACHE_REFRESHED: '缓存刷新成功',
|
||||
};
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,196 @@
|
||||
/**
|
||||
* 即时销售报表工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 格式化货币
|
||||
* @param value 数值
|
||||
* @param currency 货币类型
|
||||
* @returns 格式化后的货币字符串
|
||||
*/
|
||||
export const formatCurrency = (value: number, currency: string = 'CNY'): string => {
|
||||
return new Intl.NumberFormat('zh-CN', {
|
||||
style: 'currency',
|
||||
currency,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param value 数值
|
||||
* @returns 格式化后的数字字符串
|
||||
*/
|
||||
export const formatNumber = (value: number): string => {
|
||||
return new Intl.NumberFormat('zh-CN').format(value);
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param value 数值
|
||||
* @param decimals 小数位数
|
||||
* @returns 格式化后的百分比字符串
|
||||
*/
|
||||
export const formatPercent = (value: number, decimals: number = 2): string => {
|
||||
return `${(value * 100).toFixed(decimals)}%`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期时间
|
||||
* @param date 日期
|
||||
* @param format 格式
|
||||
* @returns 格式化后的日期字符串
|
||||
*/
|
||||
export const formatDateTime = (date: Date | string, format: string = 'YYYY-MM-DD HH:mm:ss'): string => {
|
||||
const d = new Date(date);
|
||||
const year = d.getFullYear();
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const hours = String(d.getHours()).padStart(2, '0');
|
||||
const minutes = String(d.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(d.getSeconds()).padStart(2, '0');
|
||||
|
||||
return format
|
||||
.replace('YYYY', String(year))
|
||||
.replace('MM', month)
|
||||
.replace('DD', day)
|
||||
.replace('HH', hours)
|
||||
.replace('mm', minutes)
|
||||
.replace('ss', seconds);
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取趋势样式类
|
||||
* @param rate 变化率
|
||||
* @returns 样式类名
|
||||
*/
|
||||
export const getTrendClass = (rate: number): string => {
|
||||
if (rate > 0) return 'trend-up';
|
||||
if (rate < 0) return 'trend-down';
|
||||
return 'trend-stable';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取排名标签类型
|
||||
* @param ranking 排名
|
||||
* @returns 标签类型
|
||||
*/
|
||||
export const getRankingTagType = (ranking: number): 'danger' | 'warning' | 'primary' => {
|
||||
if (ranking <= 3) return 'danger';
|
||||
if (ranking <= 10) return 'warning';
|
||||
return 'primary';
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算增长率
|
||||
* @param current 当前值
|
||||
* @param previous 之前值
|
||||
* @returns 增长率
|
||||
*/
|
||||
export const calculateGrowthRate = (current: number, previous: number): number => {
|
||||
if (previous === 0) return current > 0 ? 100 : 0;
|
||||
return ((current - previous) / previous) * 100;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成随机数据(用于演示)
|
||||
* @param min 最小值
|
||||
* @param max 最大值
|
||||
* @returns 随机数
|
||||
*/
|
||||
export const generateRandomData = (min: number, max: number): number => {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成时间序列数据
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @param interval 间隔(分钟)
|
||||
* @returns 时间序列数组
|
||||
*/
|
||||
export const generateTimeSeries = (startTime: Date, endTime: Date, interval: number = 60): string[] => {
|
||||
const times: string[] = [];
|
||||
const current = new Date(startTime);
|
||||
|
||||
while (current <= endTime) {
|
||||
times.push(formatDateTime(current, 'HH:mm'));
|
||||
current.setMinutes(current.getMinutes() + interval);
|
||||
}
|
||||
|
||||
return times;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取默认查询时间范围
|
||||
* @param type 查询类型
|
||||
* @returns 查询时间范围
|
||||
*/
|
||||
export const getDefaultTimeRange = (type: 'hour' | 'day' | 'month' = 'day') => {
|
||||
const now = new Date();
|
||||
const startTime = new Date(now);
|
||||
|
||||
switch (type) {
|
||||
case 'hour':
|
||||
startTime.setHours(now.getHours() - 24);
|
||||
break;
|
||||
case 'day':
|
||||
startTime.setDate(now.getDate() - 7);
|
||||
break;
|
||||
case 'month':
|
||||
startTime.setMonth(now.getMonth() - 6);
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
startTime: formatDateTime(startTime),
|
||||
endTime: formatDateTime(now),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 颜色配置
|
||||
*/
|
||||
export const CHART_COLORS = {
|
||||
primary: '#409EFF',
|
||||
success: '#67C23A',
|
||||
warning: '#E6A23C',
|
||||
danger: '#F56C6C',
|
||||
info: '#909399',
|
||||
gradient: {
|
||||
blue: ['#667eea', '#764ba2'],
|
||||
green: ['#43e97b', '#38f9d7'],
|
||||
orange: ['#f093fb', '#f5576c'],
|
||||
purple: ['#4facfe', '#00f2fe'],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 图表配置
|
||||
*/
|
||||
export const CHART_CONFIG = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: {
|
||||
type: 'cross',
|
||||
},
|
||||
},
|
||||
grid: {
|
||||
left: '3%',
|
||||
right: '4%',
|
||||
bottom: '3%',
|
||||
containLabel: true,
|
||||
},
|
||||
legend: {
|
||||
data: [],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 表格配置
|
||||
*/
|
||||
export const TABLE_CONFIG = {
|
||||
stripe: true,
|
||||
border: false,
|
||||
size: 'default' as const,
|
||||
showOverflowTooltip: true,
|
||||
};
|
||||
Reference in new issue
Block a user