即时销售报表
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