feat: 控制台添加访问趋势统计

This commit is contained in:
ray
2024-07-01 00:35:11 +08:00
parent ab0d5c45b9
commit ad423da68c
14 changed files with 321 additions and 583 deletions

View File

@@ -1,202 +0,0 @@
<!-- 线 + 柱混合图 -->
<template>
<el-card>
<template #header>
<div class="title">
业绩柱状图
<el-tooltip effect="dark" content="点击试试下载" placement="bottom">
<i-ep-download class="download" @click="downloadEchart" />
</el-tooltip>
</div>
</template>
<div :id="id" :class="className" :style="{ height, width }"></div>
</el-card>
</template>
<script setup lang="ts">
import * as echarts from "echarts";
const props = defineProps({
id: {
type: String,
default: "barChart",
},
className: {
type: String,
default: "",
},
width: {
type: String,
default: "200px",
required: true,
},
height: {
type: String,
default: "200px",
required: true,
},
});
const options = {
grid: {
left: "2%",
right: "2%",
bottom: "10%",
containLabel: true,
},
tooltip: {
trigger: "axis",
axisPointer: {
type: "cross",
crossStyle: {
color: "#999",
},
},
},
legend: {
x: "center",
y: "bottom",
data: ["收入", "毛利润", "收入增长率", "利润增长率"],
textStyle: {
color: "#999",
},
},
xAxis: [
{
type: "category",
data: ["浙江", "北京", "上海", "广东", "深圳"],
axisPointer: {
type: "shadow",
},
},
],
yAxis: [
{
type: "value",
min: 0,
max: 10000,
interval: 2000,
axisLabel: {
formatter: "{value} ",
},
},
{
type: "value",
min: 0,
max: 100,
interval: 20,
axisLabel: {
formatter: "{value}%",
},
},
],
series: [
{
name: "收入",
type: "bar",
data: [7000, 7100, 7200, 7300, 7400],
barWidth: 20,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#83bff6" },
{ offset: 0.5, color: "#188df0" },
{ offset: 1, color: "#188df0" },
]),
},
},
{
name: "毛利润",
type: "bar",
data: [8000, 8200, 8400, 8600, 8800],
barWidth: 20,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#25d73c" },
{ offset: 0.5, color: "#1bc23d" },
{ offset: 1, color: "#179e61" },
]),
},
},
{
name: "收入增长率",
type: "line",
yAxisIndex: 1,
data: [60, 65, 70, 75, 80],
itemStyle: {
color: "#67C23A",
},
},
{
name: "利润增长率",
type: "line",
yAxisIndex: 1,
data: [70, 75, 80, 85, 90],
itemStyle: {
color: "#409EFF",
},
},
],
};
const downloadEchart = () => {
// 获取画布图表地址信息
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 chart = ref<any>("");
onMounted(() => {
// 图表初始化
chart.value = markRaw(
echarts.init(document.getElementById(props.id) as HTMLDivElement)
);
chart.value.setOption(options);
// 大小自适应
window.addEventListener("resize", () => {
chart.value.resize();
});
});
onActivated(() => {
if (chart.value) {
chart.value.resize();
}
});
</script>
<style lang="scss" scoped>
.title {
display: flex;
justify-content: space-between;
.download {
cursor: pointer;
&:hover {
color: #409eff;
}
}
}
</style>

View File

@@ -1,115 +0,0 @@
<!-- 漏斗图 -->
<template>
<div :id="id" :class="className" :style="{ height, width }"></div>
</template>
<script setup lang="ts">
import * as echarts from "echarts";
const props = defineProps({
id: {
type: String,
default: "funnelChart",
},
className: {
type: String,
default: "",
},
width: {
type: String,
default: "200px",
required: true,
},
height: {
type: String,
default: "200px",
required: true,
},
});
const options = {
title: {
show: true,
text: "订单线索转化漏斗图",
x: "center",
padding: 15,
textStyle: {
fontSize: 18,
fontStyle: "normal",
fontWeight: "bold",
color: "#337ecc",
},
},
grid: {
left: "2%",
right: "2%",
bottom: "10%",
containLabel: true,
},
legend: {
x: "center",
y: "bottom",
data: ["Show", "Click", "Visit", "Inquiry", "Order"],
},
series: [
{
name: "Funnel",
type: "funnel",
left: "20%",
top: 60,
bottom: 60,
width: "60%",
sort: "descending",
gap: 2,
label: {
show: true,
position: "inside",
},
labelLine: {
length: 10,
lineStyle: {
width: 1,
type: "solid",
},
},
itemStyle: {
borderColor: "#fff",
borderWidth: 1,
},
emphasis: {
label: {
fontSize: 20,
},
},
data: [
{ value: 60, name: "Visit" },
{ value: 40, name: "Inquiry" },
{ value: 20, name: "Order" },
{ value: 80, name: "Click" },
{ value: 100, name: "Show" },
],
},
],
};
const chart = ref<any>("");
onMounted(() => {
chart.value = markRaw(
echarts.init(document.getElementById(props.id) as HTMLDivElement)
);
chart.value.setOption(options);
window.addEventListener("resize", () => {
chart.value.resize();
});
});
onActivated(() => {
if (chart.value) {
chart.value.resize();
}
});
</script>

View File

@@ -1,89 +0,0 @@
<!-- 饼图 -->
<template>
<el-card>
<template #header> 产品分类饼图 </template>
<div :id="id" :class="className" :style="{ height, width }"></div>
</el-card>
</template>
<script setup lang="ts">
import * as echarts from "echarts";
const props = defineProps({
id: {
type: String,
default: "pieChart",
},
className: {
type: String,
default: "",
},
width: {
type: String,
default: "200px",
required: true,
},
height: {
type: String,
default: "200px",
required: true,
},
});
const options = {
grid: {
left: "2%",
right: "2%",
bottom: "10%",
containLabel: true,
},
legend: {
top: "bottom",
textStyle: {
color: "#999",
},
},
series: [
{
name: "Nightingale Chart",
type: "pie",
radius: [50, 130],
center: ["50%", "50%"],
roseType: "area",
itemStyle: {
borderRadius: 1,
color: function (params: any) {
//自定义颜色
const colorList = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C"];
return colorList[params.dataIndex];
},
},
data: [
{ value: 26, name: "家用电器" },
{ value: 27, name: "户外运动" },
{ value: 24, name: "汽车用品" },
{ value: 23, name: "手机数码" },
],
},
],
};
const chart = ref<any>("");
onMounted(() => {
chart.value = markRaw(
echarts.init(document.getElementById(props.id) as HTMLDivElement)
);
chart.value.setOption(options);
window.addEventListener("resize", () => {
chart.value.resize();
});
});
onActivated(() => {
if (chart.value) {
chart.value.resize();
}
});
</script>

View File

@@ -1,109 +0,0 @@
<!-- 雷达图 -->
<template>
<el-card>
<template #header> 订单状态雷达图 </template>
<div :id="id" :class="className" :style="{ height, width }"></div>
</el-card>
</template>
<script setup lang="ts">
import * as echarts from "echarts";
const props = defineProps({
id: {
type: String,
default: "radarChart",
},
className: {
type: String,
default: "",
},
width: {
type: String,
default: "200px",
required: true,
},
height: {
type: String,
default: "200px",
required: true,
},
});
const options = {
grid: {
left: "2%",
right: "2%",
bottom: "10%",
containLabel: true,
},
legend: {
x: "center",
y: "bottom",
data: ["预定数量", "下单数量", "发货数量"],
textStyle: {
color: "#999",
},
},
radar: {
// shape: 'circle',
radius: "60%",
indicator: [
{ name: "家用电器" },
{ name: "服装箱包" },
{ name: "运动户外" },
{ name: "手机数码" },
{ name: "汽车用品" },
{ name: "家具厨具" },
],
},
series: [
{
name: "Budget vs spending",
type: "radar",
itemStyle: {
borderRadius: 6,
color: function (params: any) {
//自定义颜色
const colorList = ["#409EFF", "#67C23A", "#E6A23C", "#F56C6C"];
return colorList[params.dataIndex];
},
},
data: [
{
value: [400, 400, 400, 400, 400, 400],
name: "预定数量",
},
{
value: [300, 300, 300, 300, 300, 300],
name: "下单数量",
},
{
value: [200, 200, 200, 200, 200, 200],
name: "发货数量",
},
],
},
],
};
const chart = ref<any>("");
onMounted(() => {
chart.value = markRaw(
echarts.init(document.getElementById(props.id) as HTMLDivElement)
);
chart.value.setOption(options);
window.addEventListener("resize", () => {
chart.value.resize();
});
});
onActivated(() => {
if (chart.value) {
chart.value.resize();
}
});
</script>

View File

@@ -0,0 +1,210 @@
<!-- 线 + 柱混合图 -->
<template>
<el-card>
<template #header>
<div class="flex-x-between">
<div class="flex-y-center">
访问趋势
<el-tooltip effect="dark" content="点击试试下载" placement="bottom">
<i-ep-download
class="cursor-pointer hover:color-#409eff ml-2"
@click="handleDownloadChart"
/>
</el-tooltip>
</div>
<el-radio-group
v-model="dataRange"
size="small"
@change="handleDateRangeChange"
>
<el-radio-button label="近7天" :value="1" />
<el-radio-button label="近30天" :value="2" />
</el-radio-group>
</div>
</template>
<div :id="id" :class="className" :style="{ height, width }"></div>
</el-card>
</template>
<script setup lang="ts">
import * as echarts from "echarts";
import StatsAPI, { VisitTrendVO, VisitTrendQuery } from "@/api/stats";
const dataRange = ref(1);
const chart: Ref<echarts.ECharts | null> = ref(null);
const props = defineProps({
id: {
type: String,
default: "VisitTrend",
},
className: {
type: String,
default: "",
},
width: {
type: String,
default: "200px",
required: true,
},
height: {
type: String,
default: "200px",
required: true,
},
});
/** 设置图表 */
const setChartOptions = (data: VisitTrendVO) => {
if (!chart.value) {
return;
}
const options = {
tooltip: {
trigger: "axis",
},
legend: {
data: ["浏览量(PV)", "IP"],
bottom: 0,
},
grid: {
left: "2%",
right: "7%",
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.3)",
},
smooth: true,
itemStyle: {
color: "#409EFF",
},
lineStyle: {
color: "#409EFF",
},
},
{
name: "IP",
type: "line",
data: data.ipList,
areaStyle: {
color: "rgba(103, 194, 58, 0.3)",
},
smooth: true,
itemStyle: {
color: "#67C23A",
},
lineStyle: {
color: "#67C23A",
},
},
],
};
chart.value.setOption(options);
};
// 计算日期范围
const calculateDateRange = () => {
const now = new Date();
const endDate = now.toISOString().split("T")[0];
const days = dataRange.value === 1 ? 7 : 30;
const startDate = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate() - days
)
.toISOString()
.split("T")[0];
return { startDate, endDate };
};
// 加载数据
const loadData = () => {
const { startDate, endDate } = calculateDateRange();
StatsAPI.getVisitTrend({
startDate,
endDate,
} as 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 = () => {
if (chart.value) {
chart.value.resize();
}
};
// 初始化图表
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>

View File

@@ -41,7 +41,7 @@
</el-card>
<!-- 数据卡片 -->
<el-row :gutter="10" class="mt-3">
<el-row :gutter="10" class="mt-5">
<el-col
:xs="24"
:sm="12"
@@ -55,7 +55,7 @@
<span class="text-[var(--el-text-color-secondary)]">{{
item.title
}}</span>
<el-tag :type="item.tagType">
<el-tag v-if="item.tagText" :type="item.tagType" size="small">
{{ item.tagText }}
</el-tag>
</div>
@@ -72,29 +72,16 @@
class="flex items-center justify-between mt-5 text-sm text-[var(--el-text-color-secondary)]"
>
<span> {{ item.dataDesc }} </span>
<span> {{ Math.round(item.count * 15) }} </span>
<span> {{ item.totalCount }} </span>
</div>
</el-card>
</el-col>
</el-row>
<!-- Echarts 图表 -->
<el-row :gutter="10" class="mt-3">
<el-col
:xs="24"
:sm="12"
:lg="8"
class="mb-2"
v-for="item in chartData"
:key="item"
>
<component
:is="chartComponent(item)"
:id="item"
height="400px"
width="100%"
class="bg-[var(--el-bg-color-overlay)]"
/>
<el-row :gutter="10" class="mt-5">
<el-col :xs="24" :span="24" class="mb-2">
<VisitTrend id="VisitTrend" width="100%" height="450px" />
</el-col>
</el-row>
</div>
@@ -130,37 +117,37 @@ const greetings = computed(() => {
const duration = 5000;
// 销售额
const amount = ref(0);
const amountOutput = useTransition(amount, {
// 在线用户数
const onlineUserCount = ref(0);
const onlineUserCountOutput = useTransition(onlineUserCount, {
duration: duration,
transition: TransitionPresets.easeOutExpo,
});
amount.value = 2000;
onlineUserCount.value = 1;
// 浏览量
const pvCount = ref(0);
const pvCountOutput = useTransition(pvCount, {
duration: duration,
transition: TransitionPresets.easeOutExpo,
});
pvCount.value = 2000;
// 访客数
const visitCount = ref(0);
const visitCountOutput = useTransition(visitCount, {
const uvCount = ref(0);
const uvCountOutput = useTransition(uvCount, {
duration: duration,
transition: TransitionPresets.easeOutExpo,
});
visitCount.value = 2000;
uvCount.value = 2000;
// IP数
const dauCount = ref(0);
const dauCountOutput = useTransition(dauCount, {
const ipCount = ref(0);
const ipCountOutput = useTransition(ipCount, {
duration: duration,
transition: TransitionPresets.easeOutExpo,
});
dauCount.value = 2000;
// 订单量
const orderCount = ref(0);
const orderCountOutput = useTransition(orderCount, {
duration: duration,
transition: TransitionPresets.easeOutExpo,
});
orderCount.value = 2000;
ipCount.value = 2000;
// 右上角数量
const statisticData = ref([
@@ -194,49 +181,65 @@ interface CardProp {
>;
tagText: string;
count: any;
totalCount: any;
dataDesc: string;
iconClass: string;
}
// 卡片数量
const cardData = ref<CardProp[]>([
{
title: "访客数",
title: "在线用户",
tagType: "success",
tagText: "",
count: visitCountOutput,
dataDesc: "总访客数",
tagText: "-",
count: onlineUserCountOutput,
totalCount: "3",
dataDesc: "总用户数",
iconClass: "visit",
},
{
title: "浏览量(PV)",
tagType: "primary",
tagText: "日",
count: pvCountOutput,
totalCount: 3000,
dataDesc: "总浏览量",
iconClass: "pv",
},
{
title: "访客数(UV)",
tagType: "danger",
tagText: "日",
count: uvCountOutput,
totalCount: 3000,
dataDesc: "总访客数",
iconClass: "uv",
},
{
title: "IP数",
tagType: "success",
tagText: "日",
count: dauCountOutput,
count: ipCountOutput,
totalCount: 3000,
dataDesc: "总IP数",
iconClass: "ip",
},
]);
// 通知公告数据
const notices = ref([
{
title: "销售额",
tagType: "primary",
tagText: "月",
count: amountOutput,
dataDesc: "总IP数",
iconClass: "money",
title: "系统更新",
content: "系统将于今晚22:00进行更新请提前保存好工作。",
},
{
title: "订单量",
tagType: "danger",
tagText: "季",
count: orderCountOutput,
dataDesc: "总订单量",
iconClass: "order",
title: "假期通知",
content: "国庆假期将于10月1日开始请提前做好工作安排。",
},
{
title: "紧急通知",
content: "请所有员工注意,明天将进行紧急疏散演练。",
},
]);
// 图表数据
const chartData = ref(["BarChart", "PieChart", "RadarChart"]);
const chartComponent = (item: string) => {
return defineAsyncComponent(() => import(`./components/${item}.vue`));
};
</script>
<style lang="scss" scoped>

View File

@@ -29,14 +29,19 @@
>
<el-table-column label="操作时间" prop="createTime" width="180" />
<el-table-column label="操作人" prop="operator" width="120" />
<el-table-column label="日志模块" prop="module" width="120" />
<el-table-column label="日志内容" prop="content" min-width="100" />
<el-table-column label="日志模块" prop="module" width="100" />
<el-table-column label="日志内容" prop="content" min-width="200" />
<el-table-column label="IP 地址" prop="ip" width="150" />
<el-table-column label="地区" prop="region" width="200" />
<el-table-column label="地区" prop="region" width="150" />
<el-table-column label="浏览器" prop="browser" width="150" />
<el-table-column label="终端系统" prop="os" width="300" />
<el-table-column
label="执行时间(毫秒)"
label="终端系统"
prop="os"
width="200"
show-overflow-tooltip
/>
<el-table-column
label="执行时间(ms)"
prop="executionTime"
width="150"
/>