refactor: ♻️ eCharts 调整为按需自动导入瘦身打包体积
This commit is contained in:
76
src/components/ECharts/index.vue
Normal file
76
src/components/ECharts/index.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<!--
|
||||
* 基于 ECharts 的 Vue3 图表组件
|
||||
* 版权所有 © 2021-present 有来开源组织
|
||||
*
|
||||
* 开源协议:https://opensource.org/licenses/MIT
|
||||
* 项目地址:https://gitee.com/youlaiorg/vue3-element-admin
|
||||
*
|
||||
* 在使用时,请保留此注释,感谢您对开源的支持!
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div ref="chartRef" :style="{ width, height }"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts/core";
|
||||
import { CanvasRenderer } from "echarts/renderers";
|
||||
import { BarChart, LineChart, PieChart } from "echarts/charts";
|
||||
import { GridComponent, TooltipComponent, LegendComponent } from "echarts/components";
|
||||
|
||||
import { useResizeObserver } from "@vueuse/core";
|
||||
|
||||
// 按需注册组件
|
||||
echarts.use([
|
||||
CanvasRenderer,
|
||||
BarChart,
|
||||
LineChart,
|
||||
PieChart,
|
||||
GridComponent,
|
||||
TooltipComponent,
|
||||
LegendComponent,
|
||||
]);
|
||||
|
||||
const props = defineProps<{
|
||||
options: echarts.EChartsCoreOption;
|
||||
width?: string;
|
||||
height?: string;
|
||||
}>();
|
||||
|
||||
const chartRef = ref<HTMLDivElement | null>(null);
|
||||
let chartInstance: echarts.ECharts | null = null;
|
||||
|
||||
// 初始化图表
|
||||
const initChart = () => {
|
||||
if (chartRef.value) {
|
||||
chartInstance = echarts.init(chartRef.value);
|
||||
if (props.options) {
|
||||
chartInstance.setOption(props.options);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 监听尺寸变化,自动调整
|
||||
useResizeObserver(chartRef, () => {
|
||||
chartInstance?.resize();
|
||||
});
|
||||
|
||||
// 监听 options 变化,更新图表
|
||||
watch(
|
||||
() => props.options,
|
||||
(newOptions) => {
|
||||
if (chartInstance && newOptions) {
|
||||
chartInstance.setOption(newOptions);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => initChart());
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
chartInstance?.dispose();
|
||||
});
|
||||
</script>
|
||||
1
src/types/components.d.ts
vendored
1
src/types/components.d.ts
vendored
@@ -13,6 +13,7 @@ declare module "vue" {
|
||||
CURD: (typeof import("./../components/CURD/index.vue"))["default"];
|
||||
Dict: (typeof import("./../components/Dict/index.vue"))["default"];
|
||||
DictLabel: (typeof import("./../components/Dict/DictLabel.vue"))["default"];
|
||||
ECharts: (typeof import("./../components/ECharts/index.vue"))["default"];
|
||||
ElBacktop: (typeof import("element-plus/es"))["ElBacktop"];
|
||||
ElBreadcrumb: (typeof import("element-plus/es"))["ElBreadcrumb"];
|
||||
ElBreadcrumbItem: (typeof import("element-plus/es"))["ElBreadcrumbItem"];
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
<!-- 线 + 柱混合图 -->
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="flex-x-between">
|
||||
<div class="flex-y-center">
|
||||
访问趋势
|
||||
<el-tooltip effect="dark" content="点击试试下载" placement="bottom">
|
||||
<el-icon
|
||||
class="cursor-pointer hover:color-#4080FF ml-1"
|
||||
name="el-icon-download"
|
||||
@click="handleDownloadChart"
|
||||
>
|
||||
<Download />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
|
||||
<el-radio-group v-model="recentDaysRange" size="small" @change="handleDateRangeChange">
|
||||
<el-radio-button label="近7天" :value="7" />
|
||||
<el-radio-button label="近30天" :value="30" />
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div :id="id" :style="{ height, width }" />
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import * as echarts from "echarts";
|
||||
import LogAPI, { VisitTrendVO } from "@/api/system/log";
|
||||
import { dayjs } from "element-plus";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: "VisitTrend",
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: "100%",
|
||||
},
|
||||
height: {
|
||||
type: String,
|
||||
default: "500px",
|
||||
},
|
||||
});
|
||||
|
||||
// 日期范围
|
||||
const recentDaysRange = ref(7);
|
||||
// 图表对象
|
||||
const chart: Ref<echarts.ECharts | null> = ref(null);
|
||||
|
||||
// 图表配置
|
||||
const setChartOptions = (data: VisitTrendVO) => {
|
||||
if (!chart.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
legend: {
|
||||
data: ["浏览量(PV)", "访客数(UV)"],
|
||||
bottom: 0,
|
||||
},
|
||||
grid: {
|
||||
left: "1%",
|
||||
right: "5%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: data.dates,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: "dashed",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "浏览量(PV)",
|
||||
type: "line",
|
||||
data: data.pvList,
|
||||
areaStyle: {
|
||||
color: "rgba(64, 158, 255, 0.1)",
|
||||
},
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: "#4080FF",
|
||||
},
|
||||
lineStyle: {
|
||||
color: "#4080FF",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "访客数(UV)",
|
||||
type: "line",
|
||||
data: data.ipList,
|
||||
areaStyle: {
|
||||
color: "rgba(103, 194, 58, 0.1)",
|
||||
},
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: "#67C23A",
|
||||
},
|
||||
lineStyle: {
|
||||
color: "#67C23A",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
chart.value.setOption(options);
|
||||
};
|
||||
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
const endDate = new Date();
|
||||
const startDate = dayjs()
|
||||
.subtract(recentDaysRange.value - 1, "day")
|
||||
.toDate();
|
||||
|
||||
const visitTrendQuery = {
|
||||
startDate: dayjs(startDate).format("YYYY-MM-DD"),
|
||||
endDate: dayjs(endDate).format("YYYY-MM-DD"),
|
||||
};
|
||||
|
||||
LogAPI.getVisitTrend(visitTrendQuery).then((data) => {
|
||||
setChartOptions(data);
|
||||
});
|
||||
};
|
||||
|
||||
// 日期范围变化
|
||||
const handleDateRangeChange = () => {
|
||||
loadData();
|
||||
};
|
||||
|
||||
// 下载图表
|
||||
const handleDownloadChart = () => {
|
||||
if (!chart.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取画布图表地址信息
|
||||
const img = new Image();
|
||||
img.src = chart.value.getDataURL({
|
||||
type: "png",
|
||||
pixelRatio: 1,
|
||||
backgroundColor: "#fff",
|
||||
});
|
||||
// 当图片加载完成后,生成 URL 并下载
|
||||
img.onload = () => {
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (ctx) {
|
||||
ctx.drawImage(img, 0, 0, img.width, img.height);
|
||||
const link = document.createElement("a");
|
||||
link.download = "访问趋势.png";
|
||||
link.href = canvas.toDataURL("image/png", 0.9);
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
// 窗口大小变化时,重置图表大小
|
||||
const handleResize = () => {
|
||||
setTimeout(() => {
|
||||
if (chart.value) {
|
||||
chart.value.resize();
|
||||
}
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 初始化图表
|
||||
onMounted(() => {
|
||||
chart.value = markRaw(echarts.init(document.getElementById(props.id) as HTMLDivElement));
|
||||
loadData();
|
||||
// 监听窗口大小变化
|
||||
window.addEventListener("resize", handleResize);
|
||||
});
|
||||
|
||||
onActivated(() => {
|
||||
handleResize();
|
||||
});
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -8,7 +8,7 @@
|
||||
<el-col :span="18" :xs="24">
|
||||
<div class="flex-x-start">
|
||||
<img
|
||||
class="wh-80px rounded-full"
|
||||
class="w80px h80px rounded-full"
|
||||
:src="userStore.userInfo.avatar + '?imageView2/1/w/80/h/80'"
|
||||
/>
|
||||
<div class="ml-5">
|
||||
@@ -116,7 +116,11 @@
|
||||
<div class="flex-y-center">
|
||||
<span class="text-lg">{{ visitStatsData.todayUvCount }}</span>
|
||||
<span
|
||||
:class="['text-xs', 'ml-2', getGrowthRateClass(visitStatsData.uvGrowthRate)]"
|
||||
:class="[
|
||||
'text-xs',
|
||||
'ml-2',
|
||||
computeGrowthRateClass(visitStatsData.uvGrowthRate),
|
||||
]"
|
||||
>
|
||||
<el-icon>
|
||||
<Top v-if="visitStatsData.uvGrowthRate > 0" />
|
||||
@@ -172,7 +176,11 @@
|
||||
<div class="flex-y-center">
|
||||
<span class="text-lg">{{ visitStatsData.todayPvCount }}</span>
|
||||
<span
|
||||
:class="['text-xs', 'ml-2', getGrowthRateClass(visitStatsData.pvGrowthRate)]"
|
||||
:class="[
|
||||
'text-xs',
|
||||
'ml-2',
|
||||
computeGrowthRateClass(visitStatsData.pvGrowthRate),
|
||||
]"
|
||||
>
|
||||
<el-icon>
|
||||
<Top v-if="visitStatsData.pvGrowthRate > 0" />
|
||||
@@ -197,7 +205,18 @@
|
||||
<el-row :gutter="10" class="mt-5">
|
||||
<!-- 访问趋势统计图 -->
|
||||
<el-col :xs="24" :span="16">
|
||||
<VisitTrend id="VisitTrend" height="400px" />
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="flex-x-between">
|
||||
<span>访问趋势</span>
|
||||
<el-radio-group v-model="visitTrendDateRange" size="small">
|
||||
<el-radio-button label="近7天" :value="7" />
|
||||
<el-radio-button label="近30天" :value="30" />
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<ECharts :options="visitTrendChartOptions" height="400px" />
|
||||
</el-card>
|
||||
</el-col>
|
||||
<!-- 通知公告 -->
|
||||
<el-col :xs="24" :span="8">
|
||||
@@ -206,7 +225,7 @@
|
||||
<div class="flex-x-between">
|
||||
<div class="flex-y-center">通知公告</div>
|
||||
<el-link type="primary">
|
||||
<span class="text-xs" @click="handleViewMoreNotice">查看更多</span>
|
||||
<span class="text-xs" @click="navigateToNoticePage">查看更多</span>
|
||||
<el-icon class="text-xs"><ArrowRight /></el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
@@ -218,7 +237,7 @@
|
||||
<el-text truncated class="!mx-2 flex-1 !text-xs !text-gray">
|
||||
{{ item.title }}
|
||||
</el-text>
|
||||
<el-link @click="handleOpenNoticeDetail(item.id)">
|
||||
<el-link @click="openNoticeDetail(item.id)">
|
||||
<el-icon class="text-sm"><View /></el-icon>
|
||||
</el-link>
|
||||
</div>
|
||||
@@ -236,38 +255,44 @@ defineOptions({
|
||||
name: "Dashboard",
|
||||
inheritAttrs: false,
|
||||
});
|
||||
import VisitTrend from "./components/visit-trend.vue";
|
||||
|
||||
import { dayjs } from "element-plus";
|
||||
import router from "@/router";
|
||||
|
||||
import LogAPI, { VisitStatsVO } from "@/api/system/log";
|
||||
import LogAPI, { VisitStatsVO, VisitTrendVO } from "@/api/system/log";
|
||||
import NoticeAPI, { NoticePageVO } from "@/api/system/notice";
|
||||
|
||||
import { useUserStore } from "@/store/modules/user";
|
||||
import { formatGrowthRate } from "@/utils";
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const noticeDetailRef = ref();
|
||||
|
||||
// 当前通知公告列表
|
||||
const notices = ref<NoticePageVO[]>([]);
|
||||
|
||||
const userStore = useUserStore();
|
||||
const date: Date = new Date();
|
||||
// 当前时间(用于计算问候语)
|
||||
const currentDate = new Date();
|
||||
|
||||
// 问候语:根据当前小时返回不同问候语
|
||||
const greetings = computed(() => {
|
||||
const hours = date.getHours();
|
||||
const hours = currentDate.getHours();
|
||||
const nickname = userStore.userInfo.nickname;
|
||||
if (hours >= 6 && hours < 8) {
|
||||
return "晨起披衣出草堂,轩窗已自喜微凉🌅!";
|
||||
} else if (hours >= 8 && hours < 12) {
|
||||
return "上午好," + userStore.userInfo.nickname + "!";
|
||||
return `上午好,${nickname}!`;
|
||||
} else if (hours >= 12 && hours < 18) {
|
||||
return "下午好," + userStore.userInfo.nickname + "!";
|
||||
return `下午好,${nickname}!`;
|
||||
} else if (hours >= 18 && hours < 24) {
|
||||
return "晚上好," + userStore.userInfo.nickname + "!";
|
||||
return `晚上好,${nickname}!`;
|
||||
} else {
|
||||
return "偷偷向银河要了一把碎星,只等你闭上眼睛撒入你的梦中,晚安🌛!";
|
||||
}
|
||||
});
|
||||
|
||||
// 访客统计数据加载状态
|
||||
const visitStatsLoading = ref(true);
|
||||
// 访客统计数据
|
||||
const visitStatsData = ref<VisitStatsVO>({
|
||||
todayUvCount: 0,
|
||||
uvGrowthRate: 0,
|
||||
@@ -277,8 +302,15 @@ const visitStatsData = ref<VisitStatsVO>({
|
||||
totalPvCount: 0,
|
||||
});
|
||||
|
||||
// 加载访问统计数据
|
||||
const loadVisitStatsData = async () => {
|
||||
// 访问趋势日期范围(单位:天)
|
||||
const visitTrendDateRange = ref(7);
|
||||
// 访问趋势图表配置
|
||||
const visitTrendChartOptions = ref();
|
||||
|
||||
/**
|
||||
* 获取访客统计数据
|
||||
*/
|
||||
const fetchVisitStatsData = () => {
|
||||
LogAPI.getVisitStats()
|
||||
.then((data) => {
|
||||
visitStatsData.value = data;
|
||||
@@ -288,12 +320,102 @@ const loadVisitStatsData = async () => {
|
||||
});
|
||||
};
|
||||
|
||||
// 根据增长率获取样式
|
||||
const getGrowthRateClass = (growthRate?: number): string => {
|
||||
/**
|
||||
* 获取访问趋势数据,并更新图表配置
|
||||
*/
|
||||
const fetchVisitTrendData = () => {
|
||||
const startDate = dayjs()
|
||||
.subtract(visitTrendDateRange.value - 1, "day")
|
||||
.toDate();
|
||||
const endDate = new Date();
|
||||
|
||||
LogAPI.getVisitTrend({
|
||||
startDate: dayjs(startDate).format("YYYY-MM-DD"),
|
||||
endDate: dayjs(endDate).format("YYYY-MM-DD"),
|
||||
}).then((data) => {
|
||||
updateVisitTrendChartOptions(data);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 更新访问趋势图表的配置项
|
||||
*
|
||||
* @param data - 访问趋势数据
|
||||
*/
|
||||
const updateVisitTrendChartOptions = (data: VisitTrendVO) => {
|
||||
console.log("Updating visit trend chart options");
|
||||
|
||||
visitTrendChartOptions.value = {
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
},
|
||||
legend: {
|
||||
data: ["浏览量(PV)", "访客数(UV)"],
|
||||
bottom: 0,
|
||||
},
|
||||
grid: {
|
||||
left: "1%",
|
||||
right: "5%",
|
||||
bottom: "10%",
|
||||
containLabel: true,
|
||||
},
|
||||
xAxis: {
|
||||
type: "category",
|
||||
data: data.dates,
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: {
|
||||
type: "dashed",
|
||||
},
|
||||
},
|
||||
},
|
||||
series: [
|
||||
{
|
||||
name: "浏览量(PV)",
|
||||
type: "line",
|
||||
data: data.pvList,
|
||||
areaStyle: {
|
||||
color: "rgba(64, 158, 255, 0.1)",
|
||||
},
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: "#4080FF",
|
||||
},
|
||||
lineStyle: {
|
||||
color: "#4080FF",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "访客数(UV)",
|
||||
type: "line",
|
||||
data: data.ipList,
|
||||
areaStyle: {
|
||||
color: "rgba(103, 194, 58, 0.1)",
|
||||
},
|
||||
smooth: true,
|
||||
itemStyle: {
|
||||
color: "#67C23A",
|
||||
},
|
||||
lineStyle: {
|
||||
color: "#67C23A",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据增长率计算对应的 CSS 类名
|
||||
*
|
||||
* @param growthRate - 增长率数值
|
||||
*/
|
||||
const computeGrowthRateClass = (growthRate?: number): string => {
|
||||
if (!growthRate) {
|
||||
return "color-[--el-color-info]";
|
||||
}
|
||||
|
||||
if (growthRate > 0) {
|
||||
return "color-[--el-color-danger]";
|
||||
} else if (growthRate < 0) {
|
||||
@@ -303,25 +425,45 @@ const getGrowthRateClass = (growthRate?: number): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const loadMyNotice = () => {
|
||||
/**
|
||||
* 获取当前用户的通知公告数据
|
||||
*/
|
||||
const fetchMyNotices = () => {
|
||||
NoticeAPI.getMyNoticePage({ pageNum: 1, pageSize: 10 }).then((data) => {
|
||||
notices.value = data.list;
|
||||
});
|
||||
};
|
||||
|
||||
// 查看更多
|
||||
function handleViewMoreNotice() {
|
||||
/**
|
||||
* 跳转至通知公告详情页面(查看更多通知)
|
||||
*/
|
||||
function navigateToNoticePage() {
|
||||
router.push({ path: "/myNotice" });
|
||||
}
|
||||
|
||||
// 打开通知公告
|
||||
function handleOpenNoticeDetail(id: string) {
|
||||
/**
|
||||
* 打开指定通知详情
|
||||
*
|
||||
* @param id - 通知 ID
|
||||
*/
|
||||
function openNoticeDetail(id: string) {
|
||||
noticeDetailRef.value.openNotice(id);
|
||||
}
|
||||
|
||||
// 监听访问趋势日期范围的变化,重新获取趋势数据
|
||||
watch(
|
||||
() => visitTrendDateRange.value,
|
||||
(newVal) => {
|
||||
console.log("Visit trend date range changed:", newVal);
|
||||
fetchVisitTrendData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 组件挂载后加载访客统计数据和通知公告数据
|
||||
onMounted(() => {
|
||||
loadVisitStatsData();
|
||||
loadMyNotice();
|
||||
fetchVisitStatsData();
|
||||
fetchMyNotices();
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user