From 14948c4357d0886777209b7500d6378c5a6396fd Mon Sep 17 00:00:00 2001
From: Soutfairy <1373664265@qq.com>
Date: Tue, 26 Nov 2024 11:35:23 +0800
Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=88=86=E6=9E=90?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 package-lock.json              |  63 ++++++++++++++++++
 package.json                   |   4 ++
 src/api/mock/index.ts          |  39 +++++++++++
 src/i18n/lang/en.ts            |   1 +
 src/i18n/lang/zh-cn.ts         |   1 +
 src/i18n/lang/zh-tw.ts         |   1 +
 src/router/route.ts            |  17 ++++-
 src/types/chartjs.d.ts         |   1 +
 src/views/statistics/index.vue | 118 +++++++++++++++++++++++++++++++++
 9 files changed, 244 insertions(+), 1 deletion(-)
 create mode 100644 src/api/mock/index.ts
 create mode 100644 src/types/chartjs.d.ts
 create mode 100644 src/views/statistics/index.vue

diff --git a/package-lock.json b/package-lock.json
index ef6bdfa..a7012ed 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
 				"@wangeditor/editor": "^5.1.23",
 				"@wangeditor/editor-for-vue": "^5.1.12",
 				"axios": "^1.6.8",
+				"chart.js": "^4.4.6",
 				"countup.js": "^2.8.0",
 				"cropperjs": "^1.6.1",
 				"echarts": "^5.5.0",
@@ -23,6 +24,7 @@
 				"js-table2excel": "^1.1.2",
 				"jsplumb": "^2.15.6",
 				"mitt": "^3.0.1",
+				"mockjs": "^1.1.0",
 				"nprogress": "^0.2.0",
 				"pinia": "^2.1.7",
 				"print-js": "^1.6.0",
@@ -39,6 +41,8 @@
 				"vue-router": "^4.3.0"
 			},
 			"devDependencies": {
+				"@types/chart.js": "^2.9.41",
+				"@types/mockjs": "^1.0.10",
 				"@types/node": "^20.11.28",
 				"@types/nprogress": "^0.2.3",
 				"@types/sortablejs": "^1.15.8",
@@ -412,6 +416,11 @@
 			"version": "1.4.15",
 			"license": "MIT"
 		},
+		"node_modules/@kurkle/color": {
+			"version": "0.3.4",
+			"resolved": "https://registry.npmmirror.com/@kurkle/color/-/color-0.3.4.tgz",
+			"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="
+		},
 		"node_modules/@nodelib/fs.scandir": {
 			"version": "2.1.5",
 			"dev": true,
@@ -472,6 +481,15 @@
 			"version": "0.0.7",
 			"license": "MIT"
 		},
+		"node_modules/@types/chart.js": {
+			"version": "2.9.41",
+			"resolved": "https://registry.npmmirror.com/@types/chart.js/-/chart.js-2.9.41.tgz",
+			"integrity": "sha512-3dvkDvueckY83UyUXtJMalYoH6faOLkWQoaTlJgB4Djde3oORmNP0Jw85HtzTuXyliUHcdp704s0mZFQKio/KQ==",
+			"dev": true,
+			"dependencies": {
+				"moment": "^2.10.2"
+			}
+		},
 		"node_modules/@types/estree": {
 			"version": "1.0.5",
 			"dev": true,
@@ -497,6 +515,12 @@
 				"@types/lodash": "*"
 			}
 		},
+		"node_modules/@types/mockjs": {
+			"version": "1.0.10",
+			"resolved": "https://registry.npmmirror.com/@types/mockjs/-/mockjs-1.0.10.tgz",
+			"integrity": "sha512-SXgrhajHG7boLv6oU93CcmdDm0HYRiceuz6b+7z+/2lCJPTWDv0V5YiwFHT2ejE4bQqgSXQiVPQYPWv7LGsK1g==",
+			"dev": true
+		},
 		"node_modules/@types/node": {
 			"version": "20.11.28",
 			"dev": true,
@@ -1245,6 +1269,17 @@
 				"url": "https://github.com/chalk/chalk?sponsor=1"
 			}
 		},
+		"node_modules/chart.js": {
+			"version": "4.4.6",
+			"resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.4.6.tgz",
+			"integrity": "sha512-8Y406zevUPbbIBA/HRk33khEmQPk5+cxeflWE/2rx1NJsjVWMPw/9mSP9rxHP5eqi6LNoPBVMfZHxbwLSgldYA==",
+			"dependencies": {
+				"@kurkle/color": "^0.3.0"
+			},
+			"engines": {
+				"pnpm": ">=8"
+			}
+		},
 		"node_modules/chokidar": {
 			"version": "3.5.3",
 			"dev": true,
@@ -1359,6 +1394,14 @@
 				"node": ">= 0.8"
 			}
 		},
+		"node_modules/commander": {
+			"version": "12.1.0",
+			"resolved": "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz",
+			"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
+			"engines": {
+				"node": ">=18"
+			}
+		},
 		"node_modules/compute-scroll-into-view": {
 			"version": "1.0.20",
 			"license": "MIT"
@@ -2538,6 +2581,26 @@
 				"mkdirp": "bin/cmd.js"
 			}
 		},
+		"node_modules/mockjs": {
+			"version": "1.1.0",
+			"resolved": "https://registry.npmmirror.com/mockjs/-/mockjs-1.1.0.tgz",
+			"integrity": "sha512-eQsKcWzIaZzEZ07NuEyO4Nw65g0hdWAyurVol1IPl1gahRwY+svqzfgfey8U8dahLwG44d6/RwEzuK52rSa/JQ==",
+			"dependencies": {
+				"commander": "*"
+			},
+			"bin": {
+				"random": "bin/random"
+			}
+		},
+		"node_modules/moment": {
+			"version": "2.30.1",
+			"resolved": "https://registry.npmmirror.com/moment/-/moment-2.30.1.tgz",
+			"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+			"dev": true,
+			"engines": {
+				"node": "*"
+			}
+		},
 		"node_modules/ms": {
 			"version": "2.1.2",
 			"dev": true,
diff --git a/package.json b/package.json
index 554989b..69d9952 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
 		"@wangeditor/editor": "^5.1.23",
 		"@wangeditor/editor-for-vue": "^5.1.12",
 		"axios": "^1.6.8",
+		"chart.js": "^4.4.6",
 		"countup.js": "^2.8.0",
 		"cropperjs": "^1.6.1",
 		"echarts": "^5.5.0",
@@ -24,6 +25,7 @@
 		"js-table2excel": "^1.1.2",
 		"jsplumb": "^2.15.6",
 		"mitt": "^3.0.1",
+		"mockjs": "^1.1.0",
 		"nprogress": "^0.2.0",
 		"pinia": "^2.1.7",
 		"print-js": "^1.6.0",
@@ -40,6 +42,8 @@
 		"vue-router": "^4.3.0"
 	},
 	"devDependencies": {
+		"@types/chart.js": "^2.9.41",
+		"@types/mockjs": "^1.0.10",
 		"@types/node": "^20.11.28",
 		"@types/nprogress": "^0.2.3",
 		"@types/sortablejs": "^1.15.8",
diff --git a/src/api/mock/index.ts b/src/api/mock/index.ts
new file mode 100644
index 0000000..3209170
--- /dev/null
+++ b/src/api/mock/index.ts
@@ -0,0 +1,39 @@
+// src/api/mock/index.ts
+import { Random } from 'mockjs';
+
+interface StatisticsData {
+  todayVisits: number;
+  newUsers: number;
+  currentMonthSales: number;
+  currentMonthOrders: number;
+}
+
+interface YearlySalesData {
+  monthlySales: number[];
+}
+
+// 模拟统计数据接口
+export const getStatisticsData = async (): Promise<StatisticsData> => {
+  return new Promise((resolve) => {
+    // 模拟 API 延迟
+    setTimeout(() => {
+      resolve({
+        todayVisits: Random.integer(500, 1500),
+        newUsers: Random.integer(20, 100),
+        currentMonthSales: Random.integer(3000, 15000),
+        currentMonthOrders: Random.integer(100, 500),
+      });
+    }, 1000);
+  });
+};
+
+// 模拟年度销售数据接口
+export const getYearlySalesData = async (): Promise<YearlySalesData> => {
+  return new Promise((resolve) => {
+    // 模拟 API 延迟
+    setTimeout(() => {
+      const monthlySales = Array.from({ length: 12 }, () => Random.integer(5000, 20000));
+      resolve({ monthlySales });
+    }, 1000);
+  });
+};
diff --git a/src/i18n/lang/en.ts b/src/i18n/lang/en.ts
index 8aaf6c5..dd58fb4 100644
--- a/src/i18n/lang/en.ts
+++ b/src/i18n/lang/en.ts
@@ -13,6 +13,7 @@ export default {
 		article: 'article',
 		message: 'message',
 		order: 'order',
+		statistics: 'statistics',
 		articleDetail: 'articleDetail',
 		addArticle: 'addArticle',
 		editArticle: 'editArticle',
diff --git a/src/i18n/lang/zh-cn.ts b/src/i18n/lang/zh-cn.ts
index 0b603ba..1552702 100644
--- a/src/i18n/lang/zh-cn.ts
+++ b/src/i18n/lang/zh-cn.ts
@@ -13,6 +13,7 @@ export default {
 		article: '文章管理',
 		message: '留言管理',
 		order: '订单管理',
+		statistics: '统计分析',
 		articleDetail: '文章详情',
 		addArticle: '新增文章',
 		editArticle: '编辑文章',
diff --git a/src/i18n/lang/zh-tw.ts b/src/i18n/lang/zh-tw.ts
index eca98f1..31483d0 100644
--- a/src/i18n/lang/zh-tw.ts
+++ b/src/i18n/lang/zh-tw.ts
@@ -13,6 +13,7 @@ export default {
 		article: '文章管理',
 		message: '聯言管理',
 		order: '訂單管理',
+		statistics: '統計分析',
 		articleDetail: '文章詳情',
 		addArticle: '文章新增',
 		editArticle: '文章編輯',
diff --git a/src/router/route.ts b/src/router/route.ts
index 9555dfe..8e32e4c 100644
--- a/src/router/route.ts
+++ b/src/router/route.ts
@@ -117,7 +117,7 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 							isLink: '',
 							isHide: false,
 							isKeepAlive: true,
-							isAffix: false,
+							isAffix: false,	
 							isIframe: false,
 							roles: ['admin'],
 							icon: 'iconfont icon-icon-',
@@ -295,6 +295,21 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
 					icon: 'iconfont icon-shuju', 
 				},
 			},
+			{
+				path: '/statistics',
+				name: 'statistics',
+				component: () => import('/@/views/statistics/index.vue'),
+				meta: {
+					title: 'message.router.statistics',
+					isLink: '',
+					isHide: false,
+					isKeepAlive: true,
+					isAffix: false,
+					isIframe: false,
+					roles: ['admin', 'common'],
+					icon: 'iconfont icon-ico_shuju', 
+				},
+			},
 			{
 				path: '/video',
 				name: 'video',
diff --git a/src/types/chartjs.d.ts b/src/types/chartjs.d.ts
new file mode 100644
index 0000000..7c60267
--- /dev/null
+++ b/src/types/chartjs.d.ts
@@ -0,0 +1 @@
+declare module 'chart.js';
diff --git a/src/views/statistics/index.vue b/src/views/statistics/index.vue
new file mode 100644
index 0000000..12f435c
--- /dev/null
+++ b/src/views/statistics/index.vue
@@ -0,0 +1,118 @@
+<template>
+  <div class="statistics">
+    <h1>统计分析</h1>
+    <div class="cards">
+      <div class="card" v-for="(item, index) in statistics" :key="index">
+        <h2>{{ item.title }}</h2>
+        <p class="val">{{ item.value }}</p>
+      </div>
+    </div>
+    <div class="chart">
+      <h2>全年销售额分析图</h2>
+      <canvas ref="salesChart"></canvas>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { ref, onMounted } from 'vue';
+import { Chart, registerables } from 'chart.js';
+import { getStatisticsData, getYearlySalesData } from '/@/api/mock'; 
+
+// 定义统计项的类型
+interface Statistic {
+  title: string;
+  value: string | number;
+}
+
+Chart.register(...registerables);
+
+export default {
+  name: 'Statistics',
+
+  setup() {
+    const statistics = ref<Statistic[]>([]); 
+      const salesChart = ref<HTMLCanvasElement | null>(null);
+
+    const fetchStatistics = async () => {
+      const data = await getStatisticsData();
+      statistics.value = [
+        { title: '今日访问量', value: '+' + data.todayVisits },
+        { title: '新增用户', value: '+' + data.newUsers },
+        { title: '当月销售额', value: data.currentMonthSales },
+        { title: '当月订单量', value: data.currentMonthOrders },
+      ];
+    };
+
+    const fetchYearlySales = async () => {
+      if (salesChart.value) {
+        const ctx = salesChart.value.getContext('2d');
+        const data = await getYearlySalesData();
+
+        new Chart(ctx!, {
+          type: 'line',
+          data: {
+            labels: [
+              '一月', '二月', '三月', '四月', '五月', '六月',
+              '七月', '八月', '九月', '十月', '十一月', '十二月'
+            ],
+            datasets: [{
+              label: '全年销售额',
+              data: data.monthlySales,
+              borderColor: 'rgba(75, 192, 192, 1)',
+              backgroundColor: 'rgba(75, 192, 192, 0.2)',
+              borderWidth: 1,
+            }]
+          },
+          options: {
+            responsive: true,
+            scales: {
+              y: {
+                beginAtZero: true
+              }
+            }
+          }
+        });
+      }
+    };
+
+    onMounted(async () => {
+      await fetchStatistics();
+      await fetchYearlySales();
+    });
+
+    return {
+      statistics,
+      salesChart
+    };
+  }
+};
+</script>
+
+<style scoped>
+.val {
+    margin-top: 10px;
+}
+
+.statistics {
+  padding: 20px;
+}
+
+.cards {
+  display: flex;
+  gap: 20px;
+  margin-bottom: 20px;
+}
+
+.card {
+  flex: 1;
+  border: 1px solid #ccc;
+  padding: 20px;
+  border-radius: 5px;
+  text-align: center;
+}
+
+.chart {
+  margin-top: 20px;
+}
+</style>