Bladeren bron

feat:完成dashboard界面开发

Jerry Liang 1 jaar geleden
bovenliggende
commit
850d92b2db

+ 1 - 0
package.json

@@ -26,6 +26,7 @@
     "@antv/g2": "^5.3.4",
     "@dotenvx/dotenvx": "^1.48.0",
     "@iconify-json/carbon": "^1.2.13",
+    "@iconify-json/circle-flags": "^1.2.10",
     "@iconify-json/fluent": "^1.2.28",
     "@iconify-json/hugeicons": "^1.2.6",
     "@iconify-json/mage": "^1.2.2",

+ 42 - 32
pnpm-lock.yaml

@@ -23,6 +23,9 @@ importers:
       '@iconify-json/carbon':
         specifier: ^1.2.13
         version: 1.2.14
+      '@iconify-json/circle-flags':
+        specifier: ^1.2.10
+        version: 1.2.10
       '@iconify-json/fluent':
         specifier: ^1.2.28
         version: 1.2.33
@@ -822,6 +825,9 @@ packages:
   '@iconify-json/carbon@1.2.14':
     resolution: {integrity: sha512-33u6uGiYJ79Dfp72peT+PBMcjxzi+NyJLpqYRX8pnw0zchsUW7Us2xecgvkWgD83KYcbe6hufyWlHFU9y7fb/Q==}
 
+  '@iconify-json/circle-flags@1.2.10':
+    resolution: {integrity: sha512-sZRxs689a281RtZvuAiKtV7pQHv8Tk0HkinSM7QvLgdLEK8xgGRCcbDvL09Rq+/KtemmsMzGhS9/qt+r3ca+Ug==}
+
   '@iconify-json/fluent@1.2.33':
     resolution: {integrity: sha512-boteYANiRo+txCGpxv0okpGUmAkmdsze+Gy061TpXx2N+VUFeGYMnmcoimRZWPOoJt7se+QF+Wv8DY3PV0otfg==}
 
@@ -1550,43 +1556,43 @@ packages:
     resolution: {integrity: sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251017.1':
-    resolution: {integrity: sha512-P2Anvpgb1TOxP1jsPuJlde+ipkETWMfn0UGiydP8332ctq7dS/3cfUzM52PFiH5kx9zoa6vqv+IW1gaSUBPtmw==}
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251019.1':
+    resolution: {integrity: sha512-GecLPUXgaptUiBrpuLhKwsxsckJ/rBA1e9pY2HdFx+mIWze1FTUiXu0It6EcFbQ2IZCMke1WuZZz18Bo4lftwA==}
     cpu: [arm64]
     os: [darwin]
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20251017.1':
-    resolution: {integrity: sha512-9KW10N8eZZTvj2+egam0JxMo5ajO/QB8uuotiiepihl1q4Xi4ExNukWyg4+FhA56x4yMpABaIuvykKd099ubmA==}
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20251019.1':
+    resolution: {integrity: sha512-/XTRfbZW+BKvxC0XwoRp21UXdQOAEUwTf/T1OMs797HLfl1EbBiCp2UK+boYFwDw/5WP18i5bYEHkzx34wUTaA==}
     cpu: [x64]
     os: [darwin]
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20251017.1':
-    resolution: {integrity: sha512-0DOI/Fae+VP2otbmWpx0gxeTQABxDgZnDJbyptea43CCHGNnXttMufwegiHjs1Uv79fLX16jdhtBdTiROOZg0A==}
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20251019.1':
+    resolution: {integrity: sha512-wJR4FDSvOBqtNIZ3SxXk72LfMMPdx69VXpavSgoyZY9Xkf7Wr6uNpKTmwzX/fOjQBhpHo/6ctiSjB/t3uVKKSQ==}
     cpu: [arm64]
     os: [linux]
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20251017.1':
-    resolution: {integrity: sha512-Qed6PFwq9KxDydS+5Ov3YnuDzLO88HWyQtt7bnE2e7VJ6aW06pVNxjxgLLgZQpAlQV8TMAg2NtfonEDt+NPSBQ==}
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20251019.1':
+    resolution: {integrity: sha512-m0dBydey0T9ToLVbB1e4keK2hSLqJPOT5RMQH9plxibU89Ry4ueON6yGvJgO4La0LqVyk5RqLSkuHyLGFHmevA==}
     cpu: [arm]
     os: [linux]
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20251017.1':
-    resolution: {integrity: sha512-wN2T28rwlGLgfIPqG4e3cy2wPO4VuVTpBH127P8UsBRQYaAIiGAv1BM7gsRjlnXBmVUGOXQ8LsxFpI4Un/YB1A==}
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20251019.1':
+    resolution: {integrity: sha512-xS6qSkZEKw/kw95+K/1xe/3ivx+M8bu5rrySSi+lZmvk2Og19pTmvyW4ec9ojleoYGrU3E6oAcCI+mbcO+KVKg==}
     cpu: [x64]
     os: [linux]
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20251017.1':
-    resolution: {integrity: sha512-6phF6+qXp7im/ymFCsOPSkH83o0uL2wUUpAdg44YluuLmfh7DbT1D6UTg4uZYMYY25nElZ27fZIgFjzCSmJbbQ==}
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20251019.1':
+    resolution: {integrity: sha512-vJvkjZEN6GRH+Y3atQO5t8WGGjqgnHzwU0ZP+4oqYJl7G6sNiqRw23lHedZxZF6Iav+lE6SPMAhVbz97LlvkbQ==}
     cpu: [arm64]
     os: [win32]
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20251017.1':
-    resolution: {integrity: sha512-xvSSet89ZLiz+Z3nU227xQ/9/H7REukqSSPpaPBdu/+oUv07MQ993JwO69CG1katDMMEABfoJJ7D7EzQAi8iMw==}
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20251019.1':
+    resolution: {integrity: sha512-GMYGYxRHIX/+hFn7SGj9LMh4CLm90ZByGH4BvgKvwJGEctkYtOB6wXJUvQMo5koel7pDY1Yy3uChiexuG11biQ==}
     cpu: [x64]
     os: [win32]
 
-  '@typescript/native-preview@7.0.0-dev.20251017.1':
-    resolution: {integrity: sha512-awfunrvMGHxKdkqTRbcsrLQMaVkAAZWR0BrlOOJiy+JRnE/I/uhGBQ9oLvvUr1bGt4QnkXz3mybCdn2pFgSy5g==}
+  '@typescript/native-preview@7.0.0-dev.20251019.1':
+    resolution: {integrity: sha512-ytCPJouuNmJyGjZwSFg/v0Ugkn/52drU5HymW1p0l6dU+iHuTIaZSKfHFWETJxQVwyyYqNxxvC0QMxTDfwPlGQ==}
     hasBin: true
 
   '@umami/node@0.4.0':
@@ -5777,6 +5783,10 @@ snapshots:
     dependencies:
       '@iconify/types': 2.0.0
 
+  '@iconify-json/circle-flags@1.2.10':
+    dependencies:
+      '@iconify/types': 2.0.0
+
   '@iconify-json/fluent@1.2.33':
     dependencies:
       '@iconify/types': 2.0.0
@@ -6376,36 +6386,36 @@ snapshots:
       '@typescript-eslint/types': 8.46.1
       eslint-visitor-keys: 4.2.1
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251017.1':
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20251019.1':
     optional: true
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20251017.1':
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20251019.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20251017.1':
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20251019.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20251017.1':
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20251019.1':
     optional: true
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20251017.1':
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20251019.1':
     optional: true
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20251017.1':
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20251019.1':
     optional: true
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20251017.1':
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20251019.1':
     optional: true
 
-  '@typescript/native-preview@7.0.0-dev.20251017.1':
+  '@typescript/native-preview@7.0.0-dev.20251019.1':
     optionalDependencies:
-      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20251017.1
-      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20251017.1
-      '@typescript/native-preview-linux-arm': 7.0.0-dev.20251017.1
-      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20251017.1
-      '@typescript/native-preview-linux-x64': 7.0.0-dev.20251017.1
-      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20251017.1
-      '@typescript/native-preview-win32-x64': 7.0.0-dev.20251017.1
+      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20251019.1
+      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20251019.1
+      '@typescript/native-preview-linux-arm': 7.0.0-dev.20251019.1
+      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20251019.1
+      '@typescript/native-preview-linux-x64': 7.0.0-dev.20251019.1
+      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20251019.1
+      '@typescript/native-preview-win32-x64': 7.0.0-dev.20251019.1
 
   '@umami/node@0.4.0': {}
 
@@ -10117,7 +10127,7 @@ snapshots:
 
   vue-vine-tsc@1.7.13(typescript@5.8.3):
     dependencies:
-      '@typescript/native-preview': 7.0.0-dev.20251017.1
+      '@typescript/native-preview': 7.0.0-dev.20251019.1
       '@volar/language-core': 2.4.20
       '@volar/typescript': 2.4.20
       '@vue-vine/language-service': 1.7.13(typescript@5.8.3)

+ 9 - 0
service/domain/fwhProDomain.go

@@ -40,3 +40,12 @@ type EventAlarmItem struct {
 	StartTime     string  `json:"startTime"` // 事件或告警的开始时间
 	EndTime       *string `json:"endTime"`   // 告警结束时间,事件没有结束时间为null,告警枚举结束时间则未结束
 }
+
+type ShutdownRecord struct {
+	GatewayId    string `json:"gatewayId"`    // SN
+	ShutdownType int    `json:"shutdownType"` // 1-系统停机
+	Country      string `json:"country"`      // 国家
+	Province     string `json:"province"`     // 州/省
+	ActiveTime   string `json:"activeTime"`   // 激活时间
+	DeviceTime   string `json:"deviceTime"`   // 告警时的设备时间 (Shutdown开始时间)
+}

+ 30 - 0
service/domain/shutdownStatisticsDomain.go

@@ -0,0 +1,30 @@
+package domain
+
+import (
+	"go.mongodb.org/mongo-driver/bson/primitive"
+)
+
+type ShutdownStatisticsStatus string // 'error' | 'complete'
+
+// ShutdownStatisticsStatus枚举
+const (
+	ShutdownStatisticsError    ShutdownStatisticsStatus = "error"
+	ShutdownStatisticsComplete ShutdownStatisticsStatus = "complete"
+)
+
+type TaskShutdownStatistics struct {
+	Id   primitive.ObjectID `bson:"_id" json:"id"`
+	Date string             `bson:"date" json:"date"`
+}
+
+type ShutdownStatisticsItem struct {
+	Id      *primitive.ObjectID               `bson:"_id,omitempty" json:"id,omitempty"`
+	Date    string                            `bson:"date" json:"date"`
+	Status  ShutdownStatisticsStatus          `bson:"status" json:"status"`
+	SiteMap map[string]ShutdownStatisticsInfo `bson:"siteMap" json:"siteMap"` // SN: Info
+}
+
+type ShutdownStatisticsInfo struct {
+	SiteAnalysisId   *primitive.ObjectID `bson:"siteAnalysisId" json:"siteAnalysisId"`
+	ShutdownTimeList []string            `bson:"shutdownTimeList" json:"shutdownTimeList"`
+}

+ 0 - 10
service/domain/sitesAnalysisDomain.go

@@ -4,16 +4,6 @@ import (
 	"go.mongodb.org/mongo-driver/bson/primitive"
 )
 
-type Progress string // 'pending' | 'running' | 'error' | 'complete'
-
-// Progress枚举
-const (
-	ProgressPending  Progress = "pending"
-	ProgressRunning  Progress = "running"
-	ProgressError    Progress = "error"
-	ProgressComplete Progress = "complete"
-)
-
 type SiteAnalysisInfo struct {
 	Id          primitive.ObjectID `bson:"_id" json:"id"`
 	Sn          string             `bson:"sn" json:"sn"`

+ 11 - 0
service/domain/taskQueueDomain.go

@@ -0,0 +1,11 @@
+package domain
+
+type Progress string // 'pending' | 'running' | 'error' | 'complete'
+
+// Progress枚举
+const (
+	ProgressPending  Progress = "pending"
+	ProgressRunning  Progress = "running"
+	ProgressError    Progress = "error"
+	ProgressComplete Progress = "complete"
+)

BIN
service/fwh-toolbox-server


+ 82 - 0
service/jobs/statisticsShutdown.go

@@ -0,0 +1,82 @@
+package jobs
+
+import (
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/repositories"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.uber.org/zap"
+)
+
+func StatisticsShutdown(date string, data []domain.ShutdownRecord, analysisFunc func(sn string, date string) (*primitive.ObjectID, error)) error {
+	_, err := time.Parse(time.DateOnly, date)
+	if err != nil {
+		zap.L().Error("StatisticsShutdown日期参数错误", zap.String("date", date))
+		return err
+	}
+
+	item, err := statisticsShutdown(date, data, analysisFunc)
+	if err != nil {
+		repositories.InsertStatisticsShutdown(domain.ShutdownStatisticsItem{
+			Date:   date,
+			Status: domain.ShutdownStatisticsError,
+		})
+		return err
+	}
+	repositories.InsertStatisticsShutdown(*item)
+	return nil
+}
+
+// 分析统计告警关机站点
+// analysisFunc为触发站点分析任务的回调函数
+func statisticsShutdown(date string, data []domain.ShutdownRecord, analysisFunc func(sn string, date string) (*primitive.ObjectID, error)) (*domain.ShutdownStatisticsItem, error) {
+	siteMap := make(map[string]domain.ShutdownStatisticsInfo)
+
+	for _, d := range data {
+		sn := d.GatewayId
+		activeTime := d.ActiveTime
+		deviceTime := d.DeviceTime
+
+		// 解析为 time.Time
+		timeActive, err := time.Parse(time.DateTime, activeTime)
+		if err != nil {
+			zap.L().Error("ActiveTime解析错误", zap.String("ActiveTime", activeTime))
+			return nil, err
+		}
+		timeAlarm, err := time.Parse(time.DateTime, deviceTime)
+		if err != nil {
+			zap.L().Error("DeviceTime解析错误", zap.String("DeviceTime", deviceTime))
+			return nil, err
+		}
+
+		// 如果告警发生在激活之前,则不统计
+		if timeAlarm.Before(timeActive) {
+			continue
+		}
+
+		info := siteMap[sn]
+		if info.SiteAnalysisId == nil {
+			// 触发一次站点分析
+			var siteAnalysisId *primitive.ObjectID = nil
+			analysisId, err := analysisFunc(sn, date)
+			if err != nil {
+				zap.L().Error("自动触发站点分析失败", zap.String("err", err.Error()))
+			} else {
+				siteAnalysisId = analysisId
+			}
+
+			info.SiteAnalysisId = siteAnalysisId
+		}
+		info.ShutdownTimeList = append(info.ShutdownTimeList, deviceTime)
+		siteMap[sn] = info
+	}
+
+	item := domain.ShutdownStatisticsItem{
+		Date:    date,
+		Status:  domain.ShutdownStatisticsComplete,
+		SiteMap: siteMap,
+	}
+
+	return &item, nil
+}

+ 8 - 0
service/repositories/api/fwh_pro/fwh_public.go

@@ -1,3 +1,11 @@
 package fwh_pro
 
 var REQ_PAGE_SIZE uint64 = 1000
+
+var (
+	America    int = 2
+	Australia  int = 3
+	Canada     int = 5
+	NewZealand int = 6
+	Barbados   int = 51
+)

+ 67 - 0
service/repositories/api/fwh_pro/fwh_shutdownList.go

@@ -0,0 +1,67 @@
+package fwh_pro
+
+import (
+	"encoding/json"
+	"fwh-toolbox-server/domain"
+	"net/url"
+	"strconv"
+)
+
+type ShutdownListResponse struct {
+	Code int              `json:"code"`
+	Msg  string           `json:"msg"`
+	Data ShutdownListData `json:"data"`
+}
+
+type ShutdownListData struct {
+	Current int                     `json:"current"` // 当前页
+	Pages   int                     `json:"pages"`   // 总页数
+	Size    int                     `json:"size"`    // 每页最大条数
+	Total   int64                   `json:"total"`   // 总条数
+	Records []domain.ShutdownRecord `json:"records"` // 记录列表
+}
+
+// 查询Shutdown站点列表 one page
+func querySiteShutdownListOnePage(date string, country int, page int) ([]byte, error) {
+	// 构造 query 参数
+	var params url.Values
+	if country <= 0 {
+		params.Add("countryId", "") // 0-ALL
+	} else {
+		params.Add("countryId", strconv.Itoa(country)) // 2-美国 3-澳大利亚 5-加拿大 6-新西兰 51-巴巴多斯
+	}
+	params.Add("date", date)
+	params.Add("timeType", strconv.Itoa(1)) // 0-通讯时间;1-设备时间
+	params.Add("pageSize", strconv.FormatUint(REQ_PAGE_SIZE, 10))
+	params.Add("pageNum", strconv.Itoa(page))
+	params.Add("land", "ZH_CN")
+
+	return FwhHttpGet("https://energy.franklinwh.com/api-inspect/fault/listShutdownGateway", params)
+}
+
+// 查询Shutdown站点列表
+func QuerySiteShutdownList(date string, country int) ([]domain.ShutdownRecord, error) {
+	var total int64 = 0
+	var page int = 0
+	var records []domain.ShutdownRecord = []domain.ShutdownRecord{}
+
+	for total == 0 || REQ_PAGE_SIZE*uint64(page) < uint64(total) {
+		page++
+		resByte, err := querySiteShutdownListOnePage(date, country, page)
+		if err != nil {
+			return nil, err
+		}
+
+		var shutdownListRes ShutdownListResponse
+		// 反序列化(unmarshal)
+		jsonErr := json.Unmarshal(resByte, &shutdownListRes)
+		if jsonErr != nil {
+			return nil, jsonErr
+		}
+
+		total = int64(shutdownListRes.Data.Total)
+		records = append(records, shutdownListRes.Data.Records...)
+	}
+
+	return records, nil
+}

+ 67 - 0
service/repositories/mongo/statisticsShutdownRepo.go

@@ -0,0 +1,67 @@
+package mongo
+
+import (
+	"context"
+	"fwh-toolbox-server/database"
+	"fwh-toolbox-server/domain"
+	"time"
+
+	"go.mongodb.org/mongo-driver/bson"
+	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/mongo/options"
+)
+
+// 从数据库获取Shutdown统计数据
+func GetStatisticsShutdown(date string) (*domain.ShutdownStatisticsItem, error) {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("StatisticsShutdown")
+
+	filter := bson.M{}
+	// 条件
+	if date != "" {
+		filter["date"] = date
+	}
+
+	findOptions := options.FindOne().
+		SetProjection(bson.M{
+			"_id":      1,
+			"date":     1,
+			"siteMap":  1,
+			"alarmMap": 1,
+		})
+
+	var item domain.ShutdownStatisticsItem
+	err := collection.FindOne(ctx, filter, findOptions).Decode(&item)
+	if err != nil {
+		return nil, err
+	}
+
+	return &item, nil
+}
+
+// 插入或更新一条Shutdown统计数据到数据库
+func InsertStatisticsShutdown(data domain.ShutdownStatisticsItem) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("StatisticsShutdown")
+
+	// 创建日期字段唯一索引
+	collection.Indexes().CreateOne(ctx, mongo.IndexModel{
+		Keys:    bson.M{"date": 1},
+		Options: options.Index().SetUnique(true),
+	})
+
+	filter := bson.M{"date": data.Date}
+	update := bson.M{"$set": bson.M{"siteMap": data.SiteMap}}
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}

+ 6 - 0
service/repositories/repositories.go

@@ -30,6 +30,8 @@ var QueryAlarmSnapshotInfo = fwh_pro.QueryAlarmSnapshotInfo
 
 var QueryMqttLogListOnTimeRange = fwh_pro.QueryMqttLogListOnTimeRange
 
+var QuerySiteShutdownList = fwh_pro.QuerySiteShutdownList
+
 /*************** MONGO ***************/
 // Xlog
 var GetXlogManageList = mongo.GetXlogManageList
@@ -56,6 +58,10 @@ var InsertSiteAnalysisData = mongo.InsertSiteAnalysisData
 var InsertSiteAnalysisTimeline = mongo.InsertSiteAnalysisTimeline
 var InsertSiteAnalysisRuntime = mongo.InsertSiteAnalysisRuntime
 
+// StatisticsShutdown
+var GetStatisticsShutdown = mongo.GetStatisticsShutdown
+var InsertStatisticsShutdown = mongo.InsertStatisticsShutdown
+
 /*************** PGSQL ***************/
 
 // Alarm Snapshot

+ 32 - 9
service/scheduler/scheduler.go

@@ -1,16 +1,39 @@
 package scheduler
 
 import (
-  "github.com/robfig/cron/v3"
-  "log"
+	"log"
+
+	"github.com/robfig/cron/v3"
 )
 
 func Init() *cron.Cron {
-  c := cron.New()
-  c.AddFunc("@every 1h", func() {
-    log.Println("每小时任务执行")
-    // do job
-  })
-  c.Start()
-  return c
+	c := cron.New()
+
+	c.AddFunc("@every 1h", func() {
+		log.Println("每小时任务执行")
+		// do job
+	})
+
+	// 每天 14:00 执行一次
+	// robfig/cron 默认支持 6 字段:秒 分 时 日 月 星期
+	// "0 0 14 * * *" 秒=0, 分=0, 时=14,每天执行
+	// c.AddFunc("0 0 14 * * *", func() {
+	// 	log.Println("每天 14:00 执行Shutdown统计")
+
+	// 	// 获取当前时间
+	// 	now := time.Now()
+
+	// 	// 计算前天(往前两天)
+	// 	beforeYesterday := now.AddDate(0, 0, -2)
+
+	// 	// 格式化为字符串(按 yyyy-MM-dd)
+	// 	beforeYesterdayStr := beforeYesterday.Format(time.DateOnly)
+
+	// 	taskQueue.EnqueueShutdownStatisticsTask(domain.TaskShutdownStatistics{
+	// 		Date: beforeYesterdayStr,
+	// 	})
+	// })
+
+	c.Start()
+	return c
 }

+ 22 - 1
service/taskQueue/taskQueue.go

@@ -78,7 +78,7 @@ func startTaskQueueWorker() {
 				}
 
 				switch base.Type {
-				case TaskTypeSiteAnalysis:
+				case TaskTypeSiteAnalysis: // 站点分析任务
 					var payload TaskPayload[domain.TaskSiteAnalysis]
 					if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
 						ackTask(TaskStreamInfo{
@@ -99,6 +99,27 @@ func startTaskQueueWorker() {
 						}, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
 						continue
 					}
+				case TaskTypeShutdownStatistics: // Shutdown统计任务
+					var payload TaskPayload[domain.TaskShutdownStatistics]
+					if err := json.Unmarshal([]byte(rawPayload), &payload); err != nil {
+						ackTask(TaskStreamInfo{
+							Ctx:        ctx,
+							StreamName: streamName,
+							GroupName:  groupName,
+							ID:         id,
+						}, false, fmt.Errorf("unmarshal payload error - %s", err.Error()))
+						continue
+					}
+					logger.Std.Printf("Processing task %s: %+v\n", id, payload)
+					if err := handleTaskAlarmStatistics(ctx, payload.Payload); err != nil {
+						ackTask(TaskStreamInfo{
+							Ctx:        ctx,
+							StreamName: streamName,
+							GroupName:  groupName,
+							ID:         id,
+						}, false, fmt.Errorf("handle task SiteAnalysis error - %s", err.Error()))
+						continue
+					}
 				default:
 					logger.Std.Println("Unknown task type:", base.Type)
 				}

+ 92 - 0
service/taskQueue/taskShutdownStatistics.go

@@ -0,0 +1,92 @@
+package taskQueue
+
+import (
+	"context"
+	"fmt"
+	"fwh-toolbox-server/domain"
+	"fwh-toolbox-server/jobs"
+	"fwh-toolbox-server/logger"
+	"fwh-toolbox-server/repositories"
+
+	"go.mongodb.org/mongo-driver/bson/primitive"
+	"go.uber.org/zap"
+)
+
+var (
+	TaskTypeShutdownStatistics = "task_ShutdownStatistics"
+)
+
+func handleTaskAlarmStatistics(ctx context.Context, payload domain.TaskShutdownStatistics) error {
+	fmt.Printf("开始执行任务 %v\n", payload)
+	err := executeTaskAlarmStatistics(ctx, payload)
+	// 记录任务执行结果
+	if err != nil {
+		fmt.Printf("任务执行失败 %s\n", err.Error())
+	} else {
+		fmt.Println("任务执行成功")
+	}
+
+	return err
+}
+
+func analysisShutdownSite(sn string, date string) (*primitive.ObjectID, error) {
+	writeConfig := domain.SiteAnalysisConfigWrite{
+		Sn:          sn,
+		Description: fmt.Sprintf("Shutdown站点分析 %s", date),
+		TimePeriod:  [2]string{date, date},
+		Options: domain.SiteAnalysisOptions{
+			EnableTimeline: true,
+			EnableRuntime:  true,
+		},
+	}
+
+	id, err := repositories.InsertSiteAnalysisConfig(writeConfig)
+	if err != nil {
+		return nil, err
+	}
+
+	EnqueueSiteAnalysisTask(domain.TaskSiteAnalysis{
+		Id:         id,
+		Sn:         writeConfig.Sn,
+		TimePeriod: writeConfig.TimePeriod,
+		Options: domain.SiteAnalysisOptions{
+			EnableTimeline: writeConfig.Options.EnableTimeline,
+			EnableRuntime:  writeConfig.Options.EnableRuntime,
+		},
+	})
+
+	return &id, nil
+}
+
+var executeTaskAlarmStatistics = func(ctx context.Context, payload domain.TaskShutdownStatistics) error {
+	executeDate := payload.Date
+
+	records, err := repositories.QuerySiteShutdownList(executeDate, 0) // 请求所有设备
+	if err != nil {
+		return err
+	}
+
+	// 分析告警统计数据,并将结果写入数据库
+	fmt.Println("分析告警统计数据,并将结果写入数据库")
+	err = jobs.StatisticsShutdown(executeDate, records, analysisShutdownSite)
+	if err != nil {
+		zap.L().Error("告警统计数据分析失败", zap.String("date", payload.Date))
+		return err
+	}
+
+	return nil
+}
+
+func EnqueueShutdownStatisticsTask(payload domain.TaskShutdownStatistics) error {
+	p := TaskPayload[domain.TaskShutdownStatistics]{
+		TaskPayloadBase: TaskPayloadBase{
+			Type: TaskTypeShutdownStatistics,
+		},
+		Payload: payload,
+	}
+	if err := EnqueueTask[domain.TaskShutdownStatistics](p); err != nil {
+		logger.Std.Printf("EnqueueShutdownStatisticsTask - %s : %s\n", payload.Id, err.Error())
+		return err
+	}
+	return nil
+}

+ 0 - 0
src/service/dashboard/index.ts


+ 16 - 0
src/service/dashboard/interface.ts

@@ -0,0 +1,16 @@
+export interface ShutdownStatisticsInfo {
+  date: string // 当前数据日期
+  dayTotal: ShutdownStatisticsDayItem // 日总数
+  usTotal: ShutdownStatisticsDayItem // 北美日总数
+  auTotal: ShutdownStatisticsDayItem // 澳洲日总数
+}
+
+export interface ShutdownStatisticsDayItem {
+  count: number
+  growth: number | undefined // 增长量 增长(+)/下降(-)
+  weekly: number[] // 周数据
+
+  monthCount: number
+  monthGrowth: number | undefined // 增长量 增长(+)/下降(-)
+  year: number[] // 年数据
+}

+ 1 - 0
src/types/components.d.ts

@@ -23,6 +23,7 @@ declare module 'vue' {
     NEllipsis: typeof import('naive-ui')['NEllipsis']
     NFormItem: typeof import('naive-ui')['NFormItem']
     NInput: typeof import('naive-ui')['NInput']
+    NNumberAnimation: typeof import('naive-ui')['NNumberAnimation']
     Noise: typeof import('./../components/Noise.vue')['default']
     NPagination: typeof import('naive-ui')['NPagination']
     NPerformantEllipsis: typeof import('naive-ui')['NPerformantEllipsis']

+ 92 - 0
src/views/dashboard/component/infoCard.vue

@@ -0,0 +1,92 @@
+<template>
+  <div class="flex items-center justify-between gap-x-4 overflow-hidden rounded bg-[var(--card-color)] p-6 shadow-xs duration-300 transition-ease-in-out">
+    <div class="flex-1">
+      <span class="text-sm font-medium text-neutral-450">{{ props.data.title }}</span>
+      <div class="mt-1 mb-1.5 flex gap-x-4 text-2xl text-neutral-700 dark:text-neutral-400">
+        <NNumberAnimation :to="props.data.value" show-separator :precision="props.data.precision" />
+      </div>
+      <div class="flex items-center">
+        <div v-if="props.data.growth !== undefined" class="flex items-center gap-x-0.5 rounded-xs px-1.5 py-0.5 text-xs duration-300" :class="props.data.percentage > 0
+          ? 'bg-green-100 text-green-600 dark:bg-green-500/20 dark:text-green-400'
+          : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
+          ">
+          <span :class="(props.data.growth > 0) ? 'i-ph:arrow-up' : 'i-ph:arrow-down'"></span>
+          <span class="font-medium">{{ Math.abs(props.data.growth) }}</span>
+        </div>
+        <span v-if="props.data.description !== undefined" class="ml-2 text-neutral-500 dark:text-neutral-400">{{ props.data.description }}</span>
+      </div>
+    </div>
+    <div>
+      <div class="grid place-items-center rounded-full p-3" :class="iconBgClass">
+        <span class="size-7" :class="iconClass" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { NNumberAnimation } from 'naive-ui'
+
+type BackgroundColors = 'indigo' | 'blue' | 'emerald' | 'orange'
+
+export interface InfoCardData {
+  title: string
+  value: number
+  growth?: number | undefined // 上升(+)/下降(-)数量
+  iconClass: string
+  backgroundColor: BackgroundColors
+  precision: number // 显示精度 N 位小数
+  description?: string
+}
+
+const props = defineProps<{
+  data: InfoCardData
+}>()
+
+const iconClass = computed(() => {
+  let iconColorClass = ''
+  switch (props.data.backgroundColor) {
+  case 'indigo':
+    iconColorClass = 'text-indigo-50 dark:text-indigo-150'
+    break;
+  case 'blue':
+    iconColorClass = 'text-blue-50 dark:text-blue-150'
+    break;
+  case 'emerald':
+    iconColorClass = 'text-emerald-50 dark:text-emerald-150'
+    break;
+  case 'orange':
+    iconColorClass = 'text-orange-50 dark:text-orange-150'
+    break;
+
+  default: // 默认indigo
+    iconColorClass = 'text-indigo-50 dark:text-indigo-150'
+    break;
+  }
+  return props.data.iconClass + ' ' + iconColorClass
+})
+
+const iconBgClass = computed(() => {
+  let iconBgColorClass = ''
+  switch (props.data.backgroundColor) {
+  case 'indigo':
+    iconBgColorClass = 'text-indigo-500/5 bg-indigo-400 ring-4 ring-indigo-200 duration-300 dark:bg-indigo-650 dark:ring-indigo-500/30'
+    break;
+  case 'blue':
+    iconBgColorClass = 'text-blue-500/5 bg-blue-400 ring-4 ring-blue-200 duration-300 dark:bg-blue-650 dark:ring-blue-500/30'
+    break;
+  case 'emerald':
+    iconBgColorClass = 'text-emerald-500/5 bg-emerald-400 ring-4 ring-emerald-200 duration-300 dark:bg-emerald-650 dark:ring-emerald-500/30'
+    break;
+  case 'orange':
+    iconBgColorClass = 'text-orange-500/5 bg-orange-400 ring-4 ring-orange-200 duration-300 dark:bg-orange-650 dark:ring-orange-500/30'
+    break;
+
+  default: // 默认indigo
+    iconBgColorClass = 'text-indigo-500/5 bg-indigo-400 ring-4 ring-indigo-200 duration-300 dark:bg-indigo-650 dark:ring-indigo-500/30'
+    break;
+  }
+  return iconBgColorClass
+})
+
+</script>

+ 343 - 280
src/views/dashboard/index.vue

@@ -1,39 +1,79 @@
 <script setup lang="ts">
-import chroma from 'chroma-js'
-import { Chart } from '@antv/g2';
-import { NNumberAnimation } from 'naive-ui'
+import { Chart, type G2Spec, type Mark, type Composition, type TooltipTitle } from '@antv/g2';
 import { onMounted, watch, ref, computed, onUnmounted } from 'vue'
 
+import InfoCard, { type InfoCardData } from './component/infoCard.vue'
+
 import { usePersonalization } from '@/composable/usePersonalization'
 import thc from '@/utils/themeColor'
 
+import type { ShutdownStatisticsInfo } from '@/service/dashboard/interface'
+import { it } from 'node:test';
+
 defineOptions({
   name: 'Dashboard',
 })
 
 const { isDark, color } = usePersonalization()
 
-const cardList = ref(generateCardData())
+// const cardList = ref(generateCardData())
+const cardList = computed<InfoCardData>(() => {
+  const date = new Date(shutdownStatisticsInfo.value.date);
+  // const year = date.getFullYear();
+  const month = date.getMonth() + 1; // getMonth() 返回 0-11,需要 +1
+  // const day = date.getDate();
 
-const revenueChart = ref<HTMLDivElement | null>(null)
-let revenueChartInstance: Chart | null = null
-let revenueChartResizeHandler: (() => void) | null = null
+  const dayTotal = shutdownStatisticsInfo.value.dayTotal
+  const usTotal = shutdownStatisticsInfo.value.usTotal
+  const auTotal = shutdownStatisticsInfo.value.auTotal
 
-const revenueBarChart = ref<HTMLDivElement | null>(null)
-let revenueBarChartInstance: Chart | null = null
-let revenueBarChartResizeHandler: (() => void) | null = null
+  return [
+    {
+      title: 'Shutdown总数',
+      value: dayTotal.count,
+      growth: dayTotal.growth,
+      iconClass: 'i-ph:smiley-x-eyes-bold',
+      backgroundColor: 'indigo',
+      precision: 0
+    },
+    {
+      title: '北美地区总数',
+      value: usTotal.count,
+      growth: usTotal.growth,
+      iconClass: 'i-circle-flags:us',
+      backgroundColor: 'indigo',
+      precision: 0
+    },
+    {
+      title: '澳洲地区总数',
+      value: auTotal.count,
+      growth: auTotal.growth,
+      iconClass: 'i-circle-flags:au',
+      backgroundColor: 'indigo',
+      precision: 0
+    },
+    {
+      title: `${month}月总数`,
+      value: dayTotal.monthCount,
+      growth: dayTotal.monthGrowth,
+      iconClass: 'i-ph:calendar-dots-bold',
+      backgroundColor: 'indigo',
+      precision: 0
+    },
+  ]
+})
 
-const revenueBarChart2 = ref<HTMLDivElement | null>(null)
-let revenueBarChart2Instance: Chart | null = null
-let revenueBarChart2ResizeHandler: (() => void) | null = null
+const shutdownWeeklyChart = ref<HTMLDivElement | null>(null)
+let shutdownWeeklyChartInstance: Chart | null = null
+let shutdownWeeklyChartResizeHandler: (() => void) | null = null
 
-const monthlyRadarChart = ref<HTMLDivElement | null>(null)
-let monthlyRadarChartInstance: Chart | null = null
-let monthlyRadarChartResizeHandler: (() => void) | null = null
+const shutdownYearChart = ref<HTMLDivElement | null>(null)
+let shutdownYearChartInstance: Chart | null = null
+let shutdownYearChartResizeHandler: (() => void) | null = null
 
-const highestRevenueChart = ref<HTMLDivElement | null>(null)
-let highestRevenueChartInstance: Chart | null = null
-let highestRevenueChartResizeHandler: (() => void) | null = null
+// const shutdownYearChart2 = ref<HTMLDivElement | null>(null)
+// let shutdownYearChart2Instance: Chart | null = null
+// let shutdownYearChart2ResizeHandler: (() => void) | null = null
 
 const CHART_CONFIG = {
   MONTHS: Array.from({ length: 12 }, (_, i) => `${i + 1}月`),
@@ -69,61 +109,6 @@ const getBusinessLinesConfig = () => [
 
 const barChartSelectedLegend = ref(getBusinessLinesConfig()[0].name)
 
-function generateCardData() {
-  const now = new Date()
-  const currentMonth = now.getMonth() + 1
-
-  const baseUserCount = 10000 + Math.floor(Math.random() * 5000)
-  const todayVisits = Math.floor(
-    5000 + Math.random() * 8000 + Math.sin((now.getHours() / 24) * Math.PI) * 2000,
-  )
-  const monthlySales = Math.floor(1500000 + Math.random() * 1000000 + (currentMonth / 12) * 500000)
-  const pendingOrders = Math.floor(150 + Math.random() * 200)
-
-  return [
-    {
-      title: '用户总数',
-      value: baseUserCount,
-      percentage: parseFloat((3.2 + Math.random() * 4).toFixed(2)),
-      iconClass: 'i-ph:users-bold text-indigo-50 dark:text-indigo-150',
-      iconBgClass:
-        'text-indigo-500/5 bg-indigo-400 ring-4 ring-indigo-200 duration-300 dark:bg-indigo-650 dark:ring-indigo-500/30',
-      precision: 0,
-      description: `${currentMonth}月新增 ${Math.floor(100 + Math.random() * 200)} 人`,
-    },
-    {
-      title: '今日访问',
-      value: todayVisits,
-      percentage: parseFloat((-2 + Math.random() * 20).toFixed(2)),
-      iconClass: 'i-ph:eye-bold text-blue-50 dark:text-blue-150',
-      iconBgClass:
-        'text-blue-500/5 bg-blue-400 ring-4 ring-blue-200 duration-300 dark:bg-blue-650 dark:ring-blue-500/30',
-      precision: 0,
-      description: '较昨日变化',
-    },
-    {
-      title: `${currentMonth}月销售额`,
-      value: monthlySales,
-      percentage: parseFloat((5 + Math.random() * 10).toFixed(2)),
-      iconClass: 'i-ph:currency-dollar-bold text-emerald-50 dark:text-emerald-150',
-      iconBgClass:
-        'text-emerald-500/5 bg-emerald-400 ring-4 ring-emerald-200 duration-300 dark:bg-emerald-650 dark:ring-emerald-500/30',
-      precision: 2,
-      description: '本月累计收入',
-    },
-    {
-      title: '待处理订单',
-      value: pendingOrders,
-      percentage: parseFloat((-8 + Math.random() * 6).toFixed(2)),
-      iconClass: 'i-ph:shopping-cart-bold text-orange-50 dark:text-orange-150',
-      iconBgClass:
-        'text-orange-500/5 bg-orange-400 ring-4 ring-orange-200 duration-300 dark:bg-orange-650 dark:ring-orange-500/30',
-      precision: 0,
-      description: '需要及时处理',
-    },
-  ]
-}
-
 const generateRandomData = (
   baseMin: number,
   baseMax: number,
@@ -215,242 +200,335 @@ const chartDataManager = {
   },
 }
 
-function initRevenueChart() {
-  if (!revenueChart.value) return
-
-  // 初始化图表实例
-  const chart = new Chart({
-    container: revenueChart.value,
-  });
-
-  // 设置数据
-  const data = businessLinesWithData.value.map((line, idx) => ({
-    name: line.name,
-    data: line.data,
-    color: line.color,
-  }));
-
-  chart.data(data)
-
-  revenueChartInstance = chart
-  // revenueChartResizeHandler = () => chart.resize()
-  // window.addEventListener('resize', revenueChartResizeHandler, { passive: true })
+interface ShutdownWeeklyChartData {
+  type: '总数' | '北美' | '澳洲'
+  date: string
+  value: number
 }
 
-function initRevenueBarChart() {
-  if (!revenueBarChart.value) return
+function initShutdownWeeklyChart() {
+  if (!shutdownWeeklyChart.value) return
 
   // 初始化图表实例
   const chart = new Chart({
-    container: revenueBarChart.value,
+    container: shutdownWeeklyChart.value,
+    height: 340,
   });
 
-  revenueBarChartInstance = chart
-  // revenueBarChartResizeHandler = () => chart.resize()
-  // window.addEventListener('resize', revenueBarChartResizeHandler, { passive: true })
+  const data = shutdownStatisticsInfo.value
+
+  // 生成日期与数值对应数据
+  const weeklyDateList = Array.from<void, string>({ length: 7 }, (_, i) => {
+    const baseDate = new Date(data.date)
+    const date = new Date(data.date)
+    date.setDate(baseDate.getDate() - (6 - i)) // 倒推 6 天
+    const dateStr = date.toISOString().slice(0, 10)
+    return dateStr
+  })
+
+  chart.options({
+    type: 'view',
+    autoFit: true,
+    title: {
+      title: "Shutdown周曲线",
+      align: 'center'
+    },
+    data: {
+      type: 'inline',
+      value: data,
+      transform: [{
+        type: 'custom',
+        callback: (data: ShutdownStatisticsInfo): ShutdownWeeklyChartData[] => {
+          let chartData: ShutdownWeeklyChartData[] = []
+          data.dayTotal.weekly.map((item, idx) => {
+            chartData.push({
+              type: '总数', // 日总数据
+              date: weeklyDateList[idx] ?? "",
+              value: item
+            })
+          })
+          data.usTotal.weekly.map((item, idx) => {
+            chartData.push({
+              type: '北美', // 北美总数据
+              date: weeklyDateList[idx] ?? "",
+              value: item
+            })
+          })
+          data.auTotal.weekly.map((item, idx) => {
+            chartData.push({
+              type: '澳洲', // 澳洲总数据
+              date: weeklyDateList[idx] ?? "",
+              value: item
+            })
+          })
+          return chartData
+        }
+      }]
+    },
+    encode: {
+      x: 'date',
+      y: 'value',
+      color: 'type',
+      shape: 'smooth' // 圆滑曲线
+    },
+    axis: {
+      x: false,
+      y: {
+        title: false
+      }
+    },
+    legend: {
+      color: {
+        itemMarker: 'rect'
+      }
+    },
+    children: [
+      {
+        type: 'area',
+        style: { fill: 'l(270) 0:#ffffff 0.9:#7ec2f3 1:#1890ff', fillOpacity: 0.3 },
+        animate: { enter: { type: 'fadeIn' as const, duration: 1000 } }
+      },
+      {
+        type: 'line',
+        style: { strokeWidth: 2 },
+        animate: { enter: { type: 'pathIn' as const, duration: 1500, delay: 100, easing: 'out-cubic' as const } },
+        tooltip: false
+      }
+    ]
+  })
+
+  chart.render()
+
+  shutdownWeeklyChartInstance = chart
+  shutdownWeeklyChartResizeHandler = () => chart.resize()
+  window.addEventListener('resize', shutdownWeeklyChartResizeHandler, { passive: true })
 }
 
-function initRevenueBarChart2() {
-  if (!revenueBarChart2.value) return
-
-  // 初始化图表实例
-  const chart = new Chart({
-    container: revenueBarChart2.value,
-  });
-
-  revenueBarChart2Instance = chart
-  // revenueBarChart2ResizeHandler = () => chart.resize()
-  // window.addEventListener('resize', revenueBarChart2ResizeHandler, { passive: true })
+interface ShutdownYearChartData {
+  type: '总数' | '北美' | '澳洲'
+  month: string
+  value: number
 }
 
-function initMonthlyRadarChart() {
-  if (!monthlyRadarChart.value) return
-
-  const now = new Date()
-  const currentMonth = now.getMonth()
+// 记录当前选择的Legend
+let currentLegend = '总数'
 
-  const currentMonthData = chartDataManager.getCurrentMonthData(currentMonth)
+function initShutdownYearChart() {
+  if (!shutdownYearChart.value) return
 
   // 初始化图表实例
   const chart = new Chart({
-    container: monthlyRadarChart.value,
+    container: shutdownYearChart.value
   });
 
-  monthlyRadarChartInstance = chart
-  // monthlyRadarChartResizeHandler = () => chart.resize()
-  // window.addEventListener('resize', monthlyRadarChartResizeHandler, { passive: true })
-}
-
-function initHighestRevenueChart() {
-  if (!highestRevenueChart.value) return
-
-  const highestLine = chartDataManager.getHighestRevenueLine()
-  const lowestLine = chartDataManager.getLowestRevenueLine()
-
-  const chartData = [
-    {
-      legendName: '最高',
-      legendValue: 'max',
-      businessName: highestLine.name,
-      color: highestLine.color,
-      data: highestLine.data,
+  const data = shutdownStatisticsInfo.value
+
+  chart.options({
+    type: 'interval',
+    autoFit: true,
+    data: {
+      type: 'inline',
+      value: data,
+      transform: [{
+        type: 'custom',
+        callback: (data: ShutdownStatisticsInfo): ShutdownYearChartData[] => {
+          let chartData: ShutdownYearChartData[] = []
+          data.dayTotal.year.map((item, idx) => {
+            if (idx >= 12) return
+            chartData.push({
+              type: '总数', // 月总数据
+              month: `${idx + 1}月`,
+              value: item
+            })
+          })
+          data.usTotal.year.map((item, idx) => {
+            if (idx >= 12) return
+            chartData.push({
+              type: '北美', // 北美总数据
+              month: `${idx + 1}月`,
+              value: item
+            })
+          })
+          data.auTotal.year.map((item, idx) => {
+            if (idx >= 12) return
+            chartData.push({
+              type: '澳洲', // 澳洲总数据
+              month: `${idx + 1}月`,
+              value: item
+            })
+          })
+          console.log({ chartData })
+          return chartData
+        }
+      }]
     },
-    {
-      legendName: '最低',
-      legendValue: 'min',
-      businessName: lowestLine.name,
-      color: lowestLine.color,
-      data: lowestLine.data,
+    encode: {
+      x: 'month',
+      y: 'value',
+      color: 'type',
     },
-  ]
-
-  // 初始化图表实例
-  const chart = new Chart({
-    container: highestRevenueChart.value,
-  });
+    axis: {
+      x: { title: false },
+      y: { title: false }
+    },
+    legend: {
+      color: {
+        itemMarker: 'rect',
+        defaultSelect: [currentLegend]
+      }
+    },
+    tooltip: {
+      title: (d: ShutdownYearChartData): TooltipTitle => `${d.type} - ${d.month}`
+    }
+  })
 
-  const legendSelected: Record<string, boolean> = { max: true, min: false }
+  chart.render()
 
-  highestRevenueChartInstance = chart
-  // highestRevenueChartResizeHandler = () => chart.resize()
-  // window.addEventListener('resize', highestRevenueChartResizeHandler, { passive: true })
-}
+  const setLegend = (legends: string[]) => {
+    chart.emit('legend:filter', {
+      data: { channel: 'color', values: legends },
+    });
+  }
 
-onMounted(() => {
-  initRevenueChart()
-  initRevenueBarChart()
-  initRevenueBarChart2()
-  initMonthlyRadarChart()
-  initHighestRevenueChart()
-})
 
-onUnmounted(() => {
-  if (revenueChartInstance) {
-    if (revenueChartResizeHandler) {
-      window.removeEventListener('resize', revenueChartResizeHandler)
-      revenueChartResizeHandler = null
+  // 监听图例点击事件 - 当用户通过图例进行筛选时触发
+  chart.on('legend:filter', (e) => {
+    const { nativeEvent, data } = e;
+    if (!nativeEvent) return;
+    const selectLegends = data.values as string[]
+    if (selectLegends.length === 0) {
+      setLegend([currentLegend])
+    } else if (selectLegends.length > 1) {
+      const legends = (data.values as string[]).filter(item => item !== currentLegend)
+      currentLegend = legends[0]
+      setLegend(legends)
     }
-    revenueChartInstance.destroy()
-    revenueChartInstance = null
-  }
+  });
 
-  if (revenueBarChartInstance) {
-    if (revenueBarChartResizeHandler) {
-      window.removeEventListener('resize', revenueBarChartResizeHandler)
-      revenueBarChartResizeHandler = null
-    }
-    revenueBarChartInstance.destroy()
-    revenueBarChartInstance = null
-  }
+  shutdownYearChartInstance = chart
+  shutdownYearChartResizeHandler = () => chart.resize()
+  window.addEventListener('resize', shutdownYearChartResizeHandler, { passive: true })
+}
 
-  if (revenueBarChart2Instance) {
-    if (revenueBarChart2ResizeHandler) {
-      window.removeEventListener('resize', revenueBarChart2ResizeHandler)
-      revenueBarChart2ResizeHandler = null
-    }
-    revenueBarChart2Instance.destroy()
-    revenueBarChart2Instance = null
+// function initShutdownYearChart2() {
+//   if (!shutdownYearChart2.value) return
+
+//   // 初始化图表实例
+//   const chart = new Chart({
+//     container: shutdownYearChart2.value,
+//   });
+
+//   shutdownYearChart2Instance = chart
+//   shutdownYearChart2ResizeHandler = () => chart.resize()
+//   window.addEventListener('resize', shutdownYearChart2ResizeHandler, { passive: true })
+// }
+
+// 模拟数据
+const shutdownStatisticsInfo = ref<ShutdownStatisticsInfo>({
+  date: "2025-10-16",
+  dayTotal: {
+    count: 687,
+    growth: 14,
+    weekly: [0, 0, 456, 234, 103, 43, 100],
+
+    monthCount: 6800,
+    monthGrowth: 340,
+    year: [0, 0, 0, 0, 0, 0, 0, 5867, 3245, 6780, 3400, 5800],
+  },
+  usTotal: {
+    count: 580,
+    growth: 32,
+    weekly: [0, 0, 234, 123, 98, 23, 55],
+
+    monthCount: 6800,
+    monthGrowth: 340,
+    year: [0, 0, 0, 0, 0, 0, 0, 5867, 3245, 6780, 3400, 5800],
+  },
+  auTotal: {
+    count: 120,
+    growth: 9,
+    weekly: [0, 0, 44, 21, 5, 12, 36],
+
+    monthCount: 6800,
+    monthGrowth: 340,
+    year: [0, 0, 0, 0, 0, 0, 0, 5867, 3245, 6780, 3400, 5800],
   }
+})
 
-  if (monthlyRadarChartInstance) {
-    if (monthlyRadarChartResizeHandler) {
-      window.removeEventListener('resize', monthlyRadarChartResizeHandler)
-      monthlyRadarChartResizeHandler = null
-    }
-    monthlyRadarChartInstance.destroy()
-    monthlyRadarChartInstance = null
-  }
+onMounted(() => {
+  // TODO 请求数据
 
-  if (highestRevenueChartInstance) {
-    if (highestRevenueChartResizeHandler) {
-      window.removeEventListener('resize', highestRevenueChartResizeHandler)
-      highestRevenueChartResizeHandler = null
-    }
-    highestRevenueChartInstance.destroy()
-    highestRevenueChartInstance = null
-  }
+  initShutdownWeeklyChart()
+  initShutdownYearChart()
+  // initShutdownYearChart2()
 })
 
-watch([isDark, color], () => {
-  if (revenueChartInstance) {
-    if (revenueChartResizeHandler) {
-      window.removeEventListener('resize', revenueChartResizeHandler)
-      revenueChartResizeHandler = null
+onUnmounted(() => {
+  if (shutdownWeeklyChartInstance) {
+    if (shutdownWeeklyChartResizeHandler) {
+      window.removeEventListener('resize', shutdownWeeklyChartResizeHandler)
+      shutdownWeeklyChartResizeHandler = null
     }
-    revenueChartInstance.destroy()
-    revenueChartInstance = null
+    shutdownWeeklyChartInstance.destroy()
+    shutdownWeeklyChartInstance = null
   }
 
-  if (revenueBarChartInstance) {
-    if (revenueBarChartResizeHandler) {
-      window.removeEventListener('resize', revenueBarChartResizeHandler)
-      revenueBarChartResizeHandler = null
+  if (shutdownYearChartInstance) {
+    if (shutdownYearChartResizeHandler) {
+      window.removeEventListener('resize', shutdownYearChartResizeHandler)
+      shutdownYearChartResizeHandler = null
     }
-    revenueBarChartInstance.destroy()
-    revenueBarChartInstance = null
+    shutdownYearChartInstance.destroy()
+    shutdownYearChartInstance = null
   }
 
-  if (revenueBarChart2Instance) {
-    if (revenueBarChart2ResizeHandler) {
-      window.removeEventListener('resize', revenueBarChart2ResizeHandler)
-      revenueBarChart2ResizeHandler = null
-    }
-    revenueBarChart2Instance.destroy()
-    revenueBarChart2Instance = null
-  }
+  // if (shutdownYearChart2Instance) {
+  //   if (shutdownYearChart2ResizeHandler) {
+  //     window.removeEventListener('resize', shutdownYearChart2ResizeHandler)
+  //     shutdownYearChart2ResizeHandler = null
+  //   }
+  //   shutdownYearChart2Instance.destroy()
+  //   shutdownYearChart2Instance = null
+  // }
+})
 
-  if (monthlyRadarChartInstance) {
-    if (monthlyRadarChartResizeHandler) {
-      window.removeEventListener('resize', monthlyRadarChartResizeHandler)
-      monthlyRadarChartResizeHandler = null
+watch([isDark, color], () => {
+  if (shutdownWeeklyChartInstance) {
+    if (shutdownWeeklyChartResizeHandler) {
+      window.removeEventListener('resize', shutdownWeeklyChartResizeHandler)
+      shutdownWeeklyChartResizeHandler = null
     }
-    monthlyRadarChartInstance.destroy()
-    monthlyRadarChartInstance = null
+    shutdownWeeklyChartInstance.destroy()
+    shutdownWeeklyChartInstance = null
   }
 
-  if (highestRevenueChartInstance) {
-    if (highestRevenueChartResizeHandler) {
-      window.removeEventListener('resize', highestRevenueChartResizeHandler)
-      highestRevenueChartResizeHandler = null
+  if (shutdownYearChartInstance) {
+    if (shutdownYearChartResizeHandler) {
+      window.removeEventListener('resize', shutdownYearChartResizeHandler)
+      shutdownYearChartResizeHandler = null
     }
-    highestRevenueChartInstance.destroy()
-    highestRevenueChartInstance = null
+    shutdownYearChartInstance.destroy()
+    shutdownYearChartInstance = null
   }
 
-  initRevenueChart()
-  initRevenueBarChart()
-  initRevenueBarChart2()
-  initMonthlyRadarChart()
-  initHighestRevenueChart()
+  // if (shutdownYearChart2Instance) {
+  //   if (shutdownYearChart2ResizeHandler) {
+  //     window.removeEventListener('resize', shutdownYearChart2ResizeHandler)
+  //     shutdownYearChart2ResizeHandler = null
+  //   }
+  //   shutdownYearChart2Instance.destroy()
+  //   shutdownYearChart2Instance = null
+  // }
+
+  initShutdownWeeklyChart()
+  initShutdownYearChart()
+  // initShutdownYearChart2()
 })
 </script>
 <template>
   <div class="space-y-4 p-4">
-    <!-- <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
-      <div v-for="item in cardList" :key="item.title"
-        class="flex items-center justify-between gap-x-4 overflow-hidden rounded bg-[var(--card-color)] p-6 shadow-xs duration-300 transition-ease-in-out">
-        <div class="flex-1">
-          <span class="text-sm font-medium text-neutral-450">{{ item.title }}</span>
-          <div class="mt-1 mb-1.5 flex gap-x-4 text-2xl text-neutral-700 dark:text-neutral-400">
-            <NNumberAnimation :to="item.value" show-separator :precision="item.precision" />
-          </div>
-          <div class="flex items-center">
-            <div class="flex items-center gap-x-0.5 rounded-xs px-1.5 py-0.5 text-xs duration-300" :class="item.percentage > 0
-              ? 'bg-green-100 text-green-600 dark:bg-green-500/20 dark:text-green-400'
-              : 'bg-rose-100 text-rose-600 dark:bg-rose-500/20 dark:text-rose-400'
-              ">
-              <span :class="item.percentage > 0 ? 'i-ph:arrow-up' : 'i-ph:arrow-down'"></span>
-              <span class="font-medium">{{ Math.abs(item.percentage) }}%</span>
-            </div>
-            <span class="ml-2 text-neutral-500 dark:text-neutral-400">{{ item.description }}</span>
-          </div>
-        </div>
-        <div>
-          <div class="grid place-items-center rounded-full p-3" :class="item.iconBgClass">
-            <span class="size-7" :class="item.iconClass" />
-          </div>
-        </div>
-      </div>
+    <div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-4">
+      <InfoCard v-for="item in cardList" :key="item.title" :data="item" />
     </div>
 
     <div class="grid grid-cols-1 gap-4 overflow-hidden lg:grid-cols-12">
@@ -458,32 +536,17 @@ watch([isDark, color], () => {
         <div
           class="rounded bg-[var(--card-color)] px-5 pt-5 pb-4.5 shadow-xs transition-background-color-300 transition-ease-in-out"
           style="height: 400px">
-          <div ref="revenueChart" class="h-full" />
+          <div ref="shutdownWeeklyChart" class="h-full" />
         </div>
       </div>
       <div class="col-span-1 lg:col-span-4">
         <div
           class="flex flex-col rounded bg-[var(--card-color)] px-5 pt-5 pb-4.5 shadow-xs transition-[background-color]-300 transition-ease-in-out"
           style="height: 400px">
-          <div ref="revenueBarChart" class="flex-1" />
-          <div ref="revenueBarChart2" style="height: 150px" />
+          <div ref="shutdownYearChart" class="flex-1" />
+          <!-- <div ref="shutdownYearChart2" style="height: 150px" /> -->
         </div>
       </div>
     </div>
-    <div class="grid grid-cols-1 gap-4 overflow-hidden lg:grid-cols-12">
-      <div class="col-span-1 lg:col-span-5">
-        <div
-          class="rounded bg-[var(--card-color)] px-5 pt-5 pb-3 shadow-xs transition-background-color-300 transition-ease-in-out"
-          style="height: 340px">
-          <div ref="monthlyRadarChart" class="h-full" />
-        </div>
-      </div>
-      <div class="col-span-1 lg:col-span-7">
-        <div class="rounded bg-[var(--card-color)] p-5 shadow-xs transition-background-color-300 transition-ease-in-out"
-          style="height: 340px; position: relative">
-          <div ref="highestRevenueChart" class="h-full" />
-        </div>
-      </div>
-    </div> -->
   </div>
 </template>