工作台第二版

This commit is contained in:
xiaocp2009 2025-07-24 16:23:31 +08:00
parent 89cb192ded
commit 43df520b17
8 changed files with 812 additions and 9 deletions

View File

@ -26,3 +26,32 @@ export function getMktPieData(startDate: string,endDate: string) {
params: {startDate,endDate}
});
}
/** 获取汇总用户卡片数据 */
export function getSummaryCardData(startDate: string,endDate: string) {
return request<null>({
url: '/common/statistics/getSummaryCardData',
method: 'get',
params: {startDate,endDate}
});
}
/** 获取汇总用户line数据 */
export function getSummaryLineData(startDate: string,endDate: string) {
return request<null>({
url: '/common/statistics/getSummaryLineData',
method: 'get',
params: {startDate,endDate}
});
}
/** 获取汇总用户pie数据 */
export function getSummaryPieData(startDate: string,endDate: string) {
return request<null>({
url: '/common/statistics/getSummaryPieData',
method: 'get',
params: {startDate,endDate}
});
}

View File

@ -3,10 +3,13 @@ import { computed, onMounted, ref, watch } from 'vue';
import { useAppStore } from '@/store/modules/app';
import HeaderBanner from './modules/header-banner.vue';
import CardData from './modules/card-data.vue';
import CardDataSummary from './modules/card-data-summary.vue';
import CardDataMkt from './modules/card-data-mkt.vue';
import LineChart from './modules/line-chart.vue';
import LineChartSummary from './modules/line-chart-summary.vue';
import LineChartMkt from './modules/line-chart-mkt.vue';
import PieChart from './modules/pie-chart.vue';
import PieChartSummary from './modules/pie-chart-summary.vue';
import PieChartMkt from './modules/pie-chart-mkt.vue';
import ProjectNews from './modules/project-news.vue';
import CreativityBanner from './modules/creativity-banner.vue';
@ -74,7 +77,11 @@ onMounted(() => {
@date-change="handleDateChange"
@quick-change="setDateRange"
/>
<CardData v-else />
<CardDataSummary
v-else
:date-range="dateRange"
@date-change="handleDateChange"
@quick-change="setDateRange"/>
<!-- 线图bar图 -->
<NGrid :x-gap="gap" :y-gap="16" responsive="screen" item-responsive>
@ -84,7 +91,9 @@ onMounted(() => {
v-if="userInfo.user.userCategory === '0'"
:date-range="dateRange"
/>
<LineChart v-else />
<LineChartSummary
v-else
:date-range="dateRange"/>
</NCard>
</NGi>
<NGi span="24 s:24 m:10">
@ -93,7 +102,9 @@ onMounted(() => {
v-if="userInfo.user.userCategory === '0'"
:date-range="dateRange"
/>
<PieChart v-else />
<PieChartSummary
v-else
:date-range="dateRange"/>
</NCard>
</NGi>
</NGrid>

View File

@ -0,0 +1,306 @@
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { createReusableTemplate } from '@vueuse/core';
import { $t } from '@/locales';
import dayjs from 'dayjs';
import { getSummaryCardData } from '@/service/api/statistics/statistics'; // API
defineOptions({
name: 'CardDataSummary'
});
const props = defineProps<{
dateRange: [number, number];
}>();
const emit = defineEmits(['date-change', 'quick-change']);
//
const cardDataList = ref([
{
key: 'staffCount',
title: '总员工数',
value: 0,
unit: '',
color: {
start: '#ec4786',
end: '#b955a4'
},
icon: 'ant-design:bar-chart-outlined'
},
{
key: 'mpsCount',
title: '总计件量',
value: 0,
unit: '',
color: {
start: '#865ec0',
end: '#5144b4'
},
icon: 'ant-design:money-collect-outlined'
},
{
key: 'busiCount',
title: '总产品数',
value: 0,
unit: '',
color: {
start: '#56cdf3',
end: '#719de3'
},
icon: 'carbon:document-download'
},
{
key: 'customerCount',
title: '总客户量',
value: 0,
unit: '',
color: {
start: '#fcbc25',
end: '#f68057'
},
icon: 'ant-design:trademark-circle-outlined'
}
]);
const loading = ref(false);
const formattedDateRange = computed(() => {
if (!props.dateRange || props.dateRange.length !== 2) return '';
const format = 'YYYY-MM-DD';
const start = dayjs(props.dateRange[0]).format(format);
const end = dayjs(props.dateRange[1]).format(format);
return start === end ? `${start}` : `${start}${end}`;
});
// API
async function fetchSummaryData() {
try {
loading.value = true;
//
const startDate = dayjs(props.dateRange[0]).format('YYYY-MM-DD');
const endDate = dayjs(props.dateRange[1]).format('YYYY-MM-DD');
// API
const { error, data } = await getSummaryCardData(startDate, endDate);
if (error) {
throw new Error('Failed to fetch summary card data');
}
//
cardDataList.value = cardDataList.value.map(item => ({
...item,
value: data[item.key as keyof typeof data] || 0
}));
} catch (error) {
console.error('获取汇总卡片数据失败:', error);
// 0退
cardDataList.value.forEach(item => item.value = 0);
} finally {
loading.value = false;
}
}
//
const setDateRange = (type: 'today' | 'week' | 'month') => {
let start, end;
switch (type) {
case 'today':
start = dayjs().startOf('day');
end = dayjs().endOf('day');
break;
case 'week':
start = dayjs().startOf('week');
end = dayjs().endOf('week');
break;
case 'month':
start = dayjs().startOf('month');
end = dayjs().endOf('month');
break;
default:
start = dayjs().startOf('day');
end = dayjs().endOf('day');
}
emit('date-change', [start.valueOf(), end.valueOf()]);
};
//
const handleDatePickerChange = (range: [number, number] | null) => {
if (range && range.length === 2) {
emit('date-change', range);
}
};
//
watch(() => props.dateRange, fetchSummaryData, { immediate: true });
interface GradientBgProps {
gradientColor: string;
}
const [DefineGradientBg, GradientBg] = createReusableTemplate<GradientBgProps>();
function getGradientColor(color: { start: string; end: string }) {
return `linear-gradient(to bottom right, ${color.start}, ${color.end})`;
}
</script>
<template>
<NCard :bordered="false" size="small" class="card-wrapper">
<!-- 顶部工具栏 -->
<div class="date-selector-wrapper">
<div class="date-range-display">
<div class="title-container">
<span class="title">汇总数据概览</span>
<span class="value">{{ formattedDateRange }} </span>
</div>
</div>
<div class="date-controls">
<!-- 快速日期选项 -->
<div class="quick-date-buttons">
<NButton
size="small"
:type="dateRange[0] === dayjs().startOf('day').valueOf() ? 'primary' : 'default'"
@click="setDateRange('today')"
>
本日
</NButton>
<NButton
size="small"
:type="dateRange[0] === dayjs().startOf('week').valueOf() ? 'primary' : 'default'"
@click="setDateRange('week')"
>
本周
</NButton>
<NButton
size="small"
:type="dateRange[0] === dayjs().startOf('month').valueOf() ? 'primary' : 'default'"
@click="setDateRange('month')"
>
本月
</NButton>
</div>
<!-- 日期选择器 -->
<div class="date-picker-group">
<NDatePicker
:value="dateRange"
type="daterange"
clearable
@update:value="handleDatePickerChange"
class="date-picker"
/>
</div>
</div>
</div>
<!-- define component start: GradientBg -->
<DefineGradientBg v-slot="{ $slots, gradientColor }">
<div class="rd-8px px-16px pb-4px pt-8px text-white" :style="{ backgroundImage: gradientColor }">
<component :is="$slots.default" />
</div>
</DefineGradientBg>
<!-- define component end: GradientBg -->
<NGrid cols="s:1 m:2 l:4" responsive="screen" :x-gap="16" :y-gap="16">
<NGi v-for="item in cardDataList" :key="item.key">
<GradientBg :gradient-color="getGradientColor(item.color)" class="flex-1">
<h3 class="text-16px">{{ item.title }}</h3>
<div class="flex justify-between pt-12px">
<SvgIcon :icon="item.icon" class="text-32px" />
<CountTo
:prefix="item.unit"
:start-value="0"
:end-value="item.value"
class="text-30px text-white dark:text-dark"
/>
</div>
</GradientBg>
</NGi>
</NGrid>
</NCard>
</template>
<style scoped>
/* 日期选择器容器 - 与卡片风格统一 */
.date-selector-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
margin-bottom: 16px;
border-bottom: 1px dashed #eaecef;
}
/* 标题容器 */
.title-container {
display: flex;
align-items: center;
gap: 8px;
}
/* 标题样式 */
.title {
font-size: 16px;
font-weight: 600;
color: #1d2129;
letter-spacing: 0.5px;
}
/* 日期显示区域 */
.date-range-display {
display: flex;
flex-direction: column;
gap: 4px;
}
.date-range-display .value {
color: #1890ff;
font-weight: 500;
font-size: 13px;
margin-left: 26px;
}
/* 日期控制区域 */
.date-controls {
display: flex;
align-items: center;
gap: 16px;
}
/* 快速日期按钮组 */
.quick-date-buttons {
display: flex;
gap: 8px;
}
/* 日期选择器组 */
.date-picker-group {
display: flex;
align-items: center;
gap: 8px;
}
.date-picker {
width: 280px;
}
/* 卡片样式 */
.card-wrapper {
padding: 16px;
border-radius: 8px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.06);
}
/* 当前选中的快速按钮样式 */
.n-button--primary-type {
background-color: #1890ff;
color: white;
border-color: #1890ff;
}
</style>

View File

@ -0,0 +1,177 @@
<script setup lang="ts">
import { watch } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { useEcharts } from '@/hooks/common/echarts';
import { $t } from '@/locales';
import dayjs from 'dayjs';
import { getSummaryLineData } from '@/service/api/statistics/statistics'; // API
defineOptions({
name: 'LineChartSummary'
});
const props = defineProps<{
dateRange: [number, number];
}>();
const appStore = useAppStore();
// ECharts
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
}
},
legend: {
data: ['同比', '环比'] //
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: [] as string[]
},
yAxis: {
type: 'value'
},
series: [
{
color: '#8e9dff',
name: '同比', //
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#8e9dff'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [] as number[]
},
{
color: '#26deca',
name: '环比', //
type: 'line',
smooth: true,
stack: 'Total',
areaStyle: {
color: {
type: 'linear',
x: 0,
y: 0,
x2: 0,
y2: 1,
colorStops: [
{
offset: 0.25,
color: '#26deca'
},
{
offset: 1,
color: '#fff'
}
]
}
},
emphasis: {
focus: 'series'
},
data: [] as number[]
}
]
}));
// API
async function fetchTrendData() {
try {
//
const startDate = dayjs(props.dateRange[0]).format('YYYY-MM-DD');
const endDate = dayjs(props.dateRange[1]).format('YYYY-MM-DD');
// API
const { error, data } = await getSummaryLineData(startDate, endDate);
if (error) {
console.error('获取汇总趋势数据失败:', error);
return;
}
//
updateOptions(opts => {
// X
opts.xAxis.data = data.dates;
//
opts.series[0].data = data.yoyData;
//
opts.series[1].data = data.momData;
return opts;
});
} catch (err) {
console.error('获取汇总趋势数据时出错:', err);
}
}
//
function updateLocale() {
updateOptions((opts, factory) => {
const originOpts = factory();
opts.legend.data = originOpts.legend.data;
opts.series[0].name = originOpts.series[0].name;
opts.series[1].name = originOpts.series[1].name;
return opts;
});
}
//
watch(() => props.dateRange, () => {
fetchTrendData();
}, { immediate: true });
//
watch(
() => appStore.locale,
() => {
updateLocale();
}
);
</script>
<template>
<NCard :bordered="false" class="card-wrapper">
<div ref="domRef" class="h-360px overflow-hidden"></div>
</NCard>
</template>
<style scoped></style>

View File

@ -0,0 +1,122 @@
<script setup lang="ts">
import { watch } from 'vue';
import { useAppStore } from '@/store/modules/app';
import { useEcharts } from '@/hooks/common/echarts';
import { $t } from '@/locales';
import dayjs from 'dayjs';
import { getSummaryPieData } from '@/service/api/statistics/statistics'; // API
defineOptions({
name: 'PieChartSummary'
});
const props = defineProps<{
dateRange: [number, number];
}>();
const appStore = useAppStore();
// ECharts
const { domRef, updateOptions } = useEcharts(() => ({
tooltip: {
trigger: 'item'
},
legend: {
bottom: '1%',
left: 'center',
itemStyle: {
borderWidth: 0
}
},
series: [
{
color: ['#5da8ff', '#8e9dff', '#fedc69', '#26deca'],
name: '业务类别',
type: 'pie',
radius: ['45%', '75%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 1
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '12'
}
},
labelLine: {
show: false
},
data: [] as { name: string; value: number }[]
}
]
}));
// API
async function fetchPieData() {
try {
//
const startDate = dayjs(props.dateRange[0]).format('YYYY-MM-DD');
const endDate = dayjs(props.dateRange[1]).format('YYYY-MM-DD');
// API
const { error, data } = await getSummaryPieData(startDate, endDate);
if (error) {
console.error('获取汇总饼图数据失败:', error);
return;
}
//
updateOptions(opts => {
opts.series[0].data = data.map(item => ({
name: item.name,
value: item.value
}));
return opts;
});
} catch (err) {
console.error('获取汇总饼图数据时出错:', err);
}
}
//
function updateLocale() {
updateOptions((opts, factory) => {
const originOpts = factory();
//
opts.series[0].name = originOpts.series[0].name;
// fetchPieData
return opts;
});
}
//
watch(() => props.dateRange, () => {
fetchPieData();
}, { immediate: true });
//
watch(
() => appStore.locale,
() => {
updateLocale();
}
);
</script>
<template>
<NCard :bordered="false" class="card-wrapper">
<div ref="domRef" class="h-360px overflow-hidden"></div>
</NCard>
</template>
<style scoped></style>

View File

@ -2,9 +2,7 @@ package org.dromara.statistics.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.statistics.domain.vo.MktCardVo;
import org.dromara.statistics.domain.vo.MktLineVo;
import org.dromara.statistics.domain.vo.MktPieVo;
import org.dromara.statistics.domain.vo.*;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@ -14,9 +12,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.*;
/**
* 统计分析控制器
@ -129,4 +125,121 @@ public class StatisticsController {
return R.ok(data);
}
// 汇总卡片数据接口
@GetMapping("/getSummaryCardData")
public R<Map<String, Integer>> getSummaryCardData(String startDate, String endDate) {
System.out.println("获取汇总卡片数据: " + startDate + "" + endDate);
// 计算日期范围天数
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
long days = ChronoUnit.DAYS.between(start, end) + 1;
// 根据天数调整比例 (最大不超过2倍)
double multiplier = Math.min(1 + days / 30.0, 2.0);
// 创建模拟数据
Map<String, Integer> data = new HashMap<>();
// 基础值
int[] baseValues = {400, 4000, 16, 66};
String[] keys = {"staffCount", "mpsCount", "busiCount", "customerCount"};
Random random = new Random();
for (int i = 0; i < keys.length; i++) {
// 基础值乘以倍数并添加随机波动
int value = (int) Math.round(baseValues[i] * multiplier * (0.9 + 0.2 * random.nextDouble()));
data.put(keys[i], value);
}
return R.ok(data);
}
// 汇总趋势数据接口 (同比环比)
@GetMapping("/getSummaryLineData")
public R<SummaryLineVo> getSummaryLineData(String startDate, String endDate) {
System.out.println("获取汇总趋势数据: " + startDate + "" + endDate);
// 解析日期范围
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
// 生成日期列表
List<String> dates = new ArrayList<>();
List<Double> yoyData = new ArrayList<>(); // 同比数据
List<Double> momData = new ArrayList<>(); // 环比数据
// 随机数生成器
Random random = new Random();
double baseValue = 5000 + random.nextInt(3000); // 基础值范围5000-8000
// 遍历日期范围
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
// 添加格式化日期 (MM/dd)
dates.add(date.format(DateTimeFormatter.ofPattern("MM/dd")));
// 生成模拟数据 - 基于基础值波动
double dailyBase = baseValue * (0.9 + 0.2 * random.nextDouble());
// 同比数据较去年同期的变化
double yoyValue = dailyBase * (0.8 + 0.4 * random.nextDouble());
// 环比数据较前一日的变化
double momValue = dailyBase * (0.85 + 0.3 * random.nextDouble());
yoyData.add(Math.round(yoyValue * 100.0) / 100.0);
momData.add(Math.round(momValue * 100.0) / 100.0);
// 更新基础值用于下一天
baseValue = dailyBase * (0.95 + 0.1 * random.nextDouble());
}
// 创建返回对象
SummaryLineVo summaryLineVo = new SummaryLineVo();
summaryLineVo.setDates(dates);
summaryLineVo.setYoyData(yoyData);
summaryLineVo.setMomData(momData);
return R.ok(summaryLineVo);
}
// 汇总饼图数据接口
@GetMapping("/getSummaryPieData")
public R<List<SummaryPieVo>> getSummaryPieData(String startDate, String endDate) {
System.out.println("获取汇总饼图数据: " + startDate + "" + endDate);
// 创建模拟数据
List<SummaryPieVo> data = new ArrayList<>();
// 产品类型及其基础值
String[] productTypes = {"智E通", "社保类", "公积金", "代收付"};
int[] baseValues = {20, 10, 40, 30};
// 计算日期范围天数
LocalDate start = LocalDate.parse(startDate);
LocalDate end = LocalDate.parse(endDate);
long days = ChronoUnit.DAYS.between(start, end) + 1;
// 根据天数调整比例 (最大不超过2倍)
double multiplier = Math.min(1 + days / 30.0, 2.0);
// 生成模拟数据
Random random = new Random();
for (int i = 0; i < productTypes.length; i++) {
SummaryPieVo item = new SummaryPieVo();
item.setName(productTypes[i]);
// 基础值乘以倍数并添加随机波动
int value = (int) Math.round(baseValues[i] * multiplier * (0.9 + 0.2 * random.nextDouble()));
item.setValue(value);
data.add(item);
}
return R.ok(data);
}
}

View File

@ -0,0 +1,26 @@
package org.dromara.statistics.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 营销线图数据VO
*/
@Data
public class SummaryLineVo {
/**
* 日期列表 (格式: MM/dd)
*/
private List<String> dates;
/**
* 同比数据 (与去年同期比较)
*/
private List<Double> yoyData;
/**
* 环比数据 (与前一日比较)
*/
private List<Double> momData;
}

View File

@ -0,0 +1,19 @@
package org.dromara.statistics.domain.vo;
import lombok.Data;
/**
* 营销线图数据VO
*/
@Data
public class SummaryPieVo {
/**
* 产品名称
*/
private String name;
/**
* 产品数量/占比值
*/
private Integer value;
}