Jerry Liang 1 жил өмнө
parent
commit
b03f62f039

+ 32 - 40
pnpm-lock.yaml

@@ -1507,51 +1507,43 @@ packages:
     resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250819.1':
-    resolution: {integrity: sha512-sZAKCg5lW10rg4REkm6Qv14+f2OVN4dX+AfznnxZp3iqj9am+0JCg67la3IVlhznY3jwvqJU6QK54vblK+my5A==}
-    engines: {node: '>=20.6.0'}
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250914.1':
+    resolution: {integrity: sha512-0Rvj+08RqtQKFBDJ2vu1ejIZccdvZYFwAg5j3b8Yffcyp8nIKKSd4iEr8Bf0sHw4n+76bJRf7MbgEcayZGEFvg==}
     cpu: [arm64]
     os: [darwin]
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20250819.1':
-    resolution: {integrity: sha512-EkcJWqnldvsEM59hNicAFGp0a3y4ebp60uJdlgxuOC3kFHffMHTUSxhuxySY4djKV9uei5RZR2O5s3XDrjcKoA==}
-    engines: {node: '>=20.6.0'}
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20250914.1':
+    resolution: {integrity: sha512-jZFqxney4PfLvODL6ywTrlbUggNoC/rN/mdSC1Tevlc3W3itvqlyMKEbUZ7LLzonc8phDWkL4qnwnsQcfu7mMg==}
     cpu: [x64]
     os: [darwin]
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20250819.1':
-    resolution: {integrity: sha512-nG07i0oVCISBVPqoHKPpBwYUg3XJhuo/QOScvTagJ4o1XS63HCZOldC0cu26ZyvlrgBaWmteVMzyaaIluwupNQ==}
-    engines: {node: '>=20.6.0'}
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20250914.1':
+    resolution: {integrity: sha512-MjicZpmJetOwaf1Hld/GMCtjFNk9GNxxyB4XcVXedq+q5E67ipCAJcu8Hz/p7MdhUiOzqM31mpMNxP0Kl1qtEA==}
     cpu: [arm64]
     os: [linux]
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20250819.1':
-    resolution: {integrity: sha512-yhfOqmxyscXYd6gkrvmDInVx5fntetIrIBHGw1ElNmnmlu5HgguXfEXLxYNMVnBRTzrHtHYPQ8jRd79X1uni+g==}
-    engines: {node: '>=20.6.0'}
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20250914.1':
+    resolution: {integrity: sha512-Lk0UrqLDzfbzUa5qiKeNTnOpuy08pZvnR2fqy+akuMCpsbI9YOUJxeG5e0NZ95Ca1lkCwJG5HZPyr7Ddgubz5w==}
     cpu: [arm]
     os: [linux]
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20250819.1':
-    resolution: {integrity: sha512-HrVUxN+VBDiiyi3xk4Ih323ZXAyLWu4CNlI1MGqazhOvahKUjTTvuyzdyTWI7Bzf25KcHIXqfyD0PTqQQRMnVw==}
-    engines: {node: '>=20.6.0'}
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20250914.1':
+    resolution: {integrity: sha512-R0mXfWBZpU+xs/X3Sqcpsb8Y3TcqlYOzmwczFz+K0yBIMXkEGklsYgnFho3ZmAXW7274sNq4xGA1D4+39oiryg==}
     cpu: [x64]
     os: [linux]
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20250819.1':
-    resolution: {integrity: sha512-TRSeuNIA3CI8BmZhioj+QBFdsywadJWKvdC/dhawaYJJuhwz4jbPD7lg/SA2gWihqDC7ZhPq6eqSgaxobW5/uw==}
-    engines: {node: '>=20.6.0'}
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20250914.1':
+    resolution: {integrity: sha512-4c2c2jV5PNbjAmZSbrMMRqcInGJUYSIVOJRVmHJJNZdjYR414whyiAcb8khZc/TbqhgznXYZHz9D8OvRBddpLA==}
     cpu: [arm64]
     os: [win32]
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20250819.1':
-    resolution: {integrity: sha512-LTLw0FtGmrSZ3mrERcDWJ1P7tvGMZAi5dhbwXBhGHiawX8kwWHZI3J+TqDnU2zrxL22164qsAee2LI4J2bwpNw==}
-    engines: {node: '>=20.6.0'}
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20250914.1':
+    resolution: {integrity: sha512-9+wfG73UAMtXkw6RxI5FZDD1PocTsnudmK2DMbYufE64sWDmIvIULL+NyttESK4/Kf8kVPebQJOd7Ue7ilFXJg==}
     cpu: [x64]
     os: [win32]
 
-  '@typescript/native-preview@7.0.0-dev.20250819.1':
-    resolution: {integrity: sha512-+gl0Sg+Ol5QibPacZziwHyImPtPzuYV1BF/n99LG6U9BAMOXoBamU+9Y5C+0gUdesmz5Ik9ZJpUuX4HTBzULig==}
-    engines: {node: '>=20.6.0'}
+  '@typescript/native-preview@7.0.0-dev.20250914.1':
+    resolution: {integrity: sha512-dcgUc4J7hGeXxKTMrDjxnBNShOKRKYmc+Lw6dSCkOML9LjeeTo+JnqQwcXhft60oC7YRyRyA/mnArYX+UrS/jA==}
     hasBin: true
 
   '@umami/node@0.4.0':
@@ -6204,36 +6196,36 @@ snapshots:
       '@typescript-eslint/types': 8.39.0
       eslint-visitor-keys: 4.2.1
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250819.1':
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20250914.1':
     optional: true
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20250819.1':
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20250914.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20250819.1':
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20250914.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20250819.1':
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20250914.1':
     optional: true
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20250819.1':
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20250914.1':
     optional: true
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20250819.1':
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20250914.1':
     optional: true
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20250819.1':
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20250914.1':
     optional: true
 
-  '@typescript/native-preview@7.0.0-dev.20250819.1':
+  '@typescript/native-preview@7.0.0-dev.20250914.1':
     optionalDependencies:
-      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20250819.1
-      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20250819.1
-      '@typescript/native-preview-linux-arm': 7.0.0-dev.20250819.1
-      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20250819.1
-      '@typescript/native-preview-linux-x64': 7.0.0-dev.20250819.1
-      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20250819.1
-      '@typescript/native-preview-win32-x64': 7.0.0-dev.20250819.1
+      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20250914.1
+      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20250914.1
+      '@typescript/native-preview-linux-arm': 7.0.0-dev.20250914.1
+      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20250914.1
+      '@typescript/native-preview-linux-x64': 7.0.0-dev.20250914.1
+      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20250914.1
+      '@typescript/native-preview-win32-x64': 7.0.0-dev.20250914.1
 
   '@umami/node@0.4.0': {}
 
@@ -9863,7 +9855,7 @@ snapshots:
 
   vue-vine-tsc@1.7.0(typescript@5.8.3):
     dependencies:
-      '@typescript/native-preview': 7.0.0-dev.20250819.1
+      '@typescript/native-preview': 7.0.0-dev.20250914.1
       '@volar/language-core': 2.4.22
       '@volar/typescript': 2.4.22
       '@vue-vine/language-service': 1.7.0(typescript@5.8.3)

+ 7 - 1
service/controllers/controllers.go

@@ -1,3 +1,9 @@
 package controllers
 
-
+// Public
+type ResPage[T any] struct {
+	Data     []T  `json:"data"`
+	PageSize uint `json:"pageSize"`
+	PageNum  uint `json:"pageNum"`
+	Total    uint `json:"total"`
+}

+ 43 - 336
service/controllers/sitesAnalysisControllers.go

@@ -2,204 +2,58 @@ package controllers
 
 import (
 	"context"
-	"encoding/json"
-	"errors"
 	"fwh-toolbox-server/database"
-	"fwh-toolbox-server/ext/api/fwh_pro"
 	"time"
 
 	"go.mongodb.org/mongo-driver/bson"
-	"go.mongodb.org/mongo-driver/mongo"
+	"go.mongodb.org/mongo-driver/bson/primitive"
 	"go.mongodb.org/mongo-driver/mongo/options"
 )
 
-type StateConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type SocConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type APowerConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type AlarmConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type VersionConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type TariffsConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type OthersConfig struct {
-	Enable bool `bson:"enable" json:"enable"`
-}
-
-type MonitorPageConfig struct {
-	State   StateConfig   `bson:"state" json:"state"`
-	Soc     SocConfig     `bson:"soc" json:"soc"`
-	APower  APowerConfig  `bson:"aPower" json:"aPower"`
-	Alarm   AlarmConfig   `bson:"alarm" json:"alarm"`
-	Version VersionConfig `bson:"version" json:"version"`
-	Tariffs TariffsConfig `bson:"tariffs" json:"tariffs"`
-	Others  OthersConfig  `bson:"others" json:"others"`
-}
-
-type SitesMonitorConfig struct {
-	Name        string             `bson:"name" json:"name"`
-	Description string             `bson:"description" json:"description"`
-	SiteList    []string           `bson:"siteList" json:"siteList"`
-	PageConfig  *MonitorPageConfig `bson:"pageConfig,omitempty" json:"pageConfig,omitempty"`
-}
-
-type SiteInfoPara struct {
-	SiteList []string `json:"siteList"`
-	PageSize uint     `json:"pageSize"`
-	PageNum  uint     `json:"pageNum"`
-}
-
-type SiteInfo struct {
-	Id               string  `json:"id"`
-	Name             *string `json:"name"`
-	FhpSn            string  `json:"fhpSn"`
-	ActiveStatus     int     `json:"activeStatus"`
-	ActiveTime       string  `json:"activeTime"`
-	Country          string  `json:"country"`
-	Province         string  `json:"province"`
-	City             string  `json:"city"`
-	OnOffStatus      int     `json:"onOffStatus"`
-	RealSysHdVersion string  `json:"realSysHdVersion"`
-	RunStatus        int     `json:"runStatus"`
-	Version          string  `json:"version"`
-}
-
-type SiteInfoResponse struct {
-	Code   int        `json:"code"`
-	Msg    string     `json:"msg"`
-	Result []SiteInfo `json:"result"`
-}
-
-type WorkModeInfo struct {
-	Id                   uint     `json:"id"`
-	OldIndex             uint     `json:"oldIndex"`
-	Name                 string   `json:"name"`
-	Soc                  float32  `json:"soc"`
-	MaxSoc               float32  `json:"maxSoc"`
-	MinSoc               float32  `json:"minSoc"`
-	DischargeDepthSoc    *float32 `json:"dischargeDepthSoc"`
-	WorkMode             uint     `json:"workMode"`
-	EnergyIncentivesType uint     `json:"energyIncentivesType"`
-	ElectricityType      uint     `json:"electricityType"`
-}
-
-type SiteModeInfo struct {
-	CurrendId uint           `json:"currendId"`
-	List      []WorkModeInfo `json:"list"`
-}
-
-type SiteModeInfoResponse struct {
-	Code    int          `json:"code"`
-	Msg     string       `json:"msg"`
-	Result  SiteModeInfo `json:"result"`
-	Success bool         `json:"success"`
-}
-
-type SitePowerFlow struct {
-	Index           uint      `json:"index"`           // 非当天为0,当天为实时数量
-	DeviceTimeArray []string  `json:"deviceTimeArray"` // 设备本地时间,格式 “2025-08-18 00:00:00”
-	PowerFhpArray   []float32 `json:"powerFhpArray"`
-	PowerGenArray   []float32 `json:"powerGenArray"`
-	PowerLoadArray  []float32 `json:"powerLoadArray"`
-	PowerSunArray   []float32 `json:"powerSunArray"`
-	PowerUtiArray   []float32 `json:"powerUtiArray"`
-	SocArray        []float32 `json:"socArray"`
-	RunStatusArray  []uint    `json:"runStatusArray"` // 运行状态,0:待机;1:充电;2:放电;3:故障
-	KwhTotalArray   []float32 `json:"kwhTotalArray"`  // 总可用电量,应该是根据SOC推算出来的。
-}
-
-type SitePowerFlowResponse struct {
-	Code   int           `json:"code"`
-	Msg    string        `json:"msg"`
-	Result SitePowerFlow `json:"result"`
-}
-
-type SiteAlarmList struct {
-	Id              uint   `json:"id"`
-	GatewayId       string `json:"gatewayId"`
-	AlarmEqSn       string `json:"alarmEqSn"`
-	AlarmCode       string `json:"alarmCode"`
-	CommonAlarmCode string `json:"commonAlarmCode"`
-	Level           int    `json:"level"`
-	Time            string `json:"time"`
-	LogName         string `json:"logName"`
-	CommonLogName   string `json:"commonLogName"`
-	LogNameEn       string `json:"logNameEn"`
-	CommonLogNameEn string `json:"commonLogNameEn"`
-}
-
-type SiteAlarmListResponse struct {
-	Code    int             `json:"code"`
-	Message string          `json:"message"`
-	Result  []SiteAlarmList `json:"result"`
-	Success bool            `json:"success"`
-	Total   uint            `json:"total"`
-}
-
 type Progress string // 'pending' | 'running' | 'error' | 'complete'
 
 type SiteAnalysisInfo struct {
-	Id          uint64   `bson:"id" json:"id"`
-	Sn          string   `bson:"sn" json:"sn"`
-	Description string   `bson:"description" json:"description"`
-	Item        []string `bson:"item" json:"item"`
-	Progress    Progress `bson:"progress" json:"progress"`
+	Id          primitive.ObjectID `bson:"_id" json:"id"`
+	Sn          string             `bson:"sn" json:"sn"`
+	Description string             `bson:"description" json:"description"`
+	Item        []string           `bson:"item" json:"item"`
+	Progress    Progress           `bson:"progress" json:"progress"`
 }
 
-// 从数据库获取站点分析列表
-func GetSiteAnalysisList() ([]SiteAnalysisInfo, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-	cursor, err := collection.Find(ctx, bson.M{}) // 空条件,查全部
-	if err != nil {
-		return nil, err
-	}
-	defer cursor.Close(ctx)
-
-	var configList []SitesMonitorConfig
-	for cursor.Next(ctx) {
-		var cfg SitesMonitorConfig
-		if err := cursor.Decode(&cfg); err != nil {
-			return nil, err
-		}
-		configList = append(configList, cfg)
-	}
-
-	return configList, nil
+type SiteAnalysisConfig struct {
+	Id                  primitive.ObjectID `bson:"_id,omitempty" json:"id,omitempty"`
+	Sn                  string             `bson:"sn" json:"sn"`
+	Description         string             `bson:"description" json:"description"`
+	Item                []string           `bson:"item" json:"item"`
+	Progress            Progress           `bson:"progress" json:"progress"`
+	TimePeriod          [2]string          `bson:"timePeriod" json:"timePeriod"`
+	EnableEventAndAlarm bool               `bson:"enableEventAndAlarm" json:"enableEventAndAlarm"`
+	EnableCloudLog      bool               `bson:"enableCloudLog" json:"enableCloudLog"`
+	EnableSiteLog       bool               `bson:"enableSiteLog" json:"enableSiteLog"`
 }
 
-// 从数据库获取站点监控配置列表
-func GetSiteMonitorConfig() ([]SitesMonitorConfig, error) {
+// 从数据库获取站点分析列表
+func GetSiteAnalysisManageList() ([]SiteAnalysisInfo, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
-	collection := database.GetCollection("SiteMonitorConfig")
-	cursor, err := collection.Find(ctx, bson.M{}) // 空条件,查全部
+	collection := database.GetCollection("SiteAnalysisConfig")
+	cursor, err := collection.Find(ctx, bson.M{}, options.Find().SetProjection(bson.M{
+		// 配置需要的字段
+		"_id":         1, // (默认会返回)
+		"sn":          1,
+		"description": 1,
+		"item":        1,
+		"progress":    1,
+	})) // 空条件,查全部
 	if err != nil {
 		return nil, err
 	}
 	defer cursor.Close(ctx)
 
-	var configList []SitesMonitorConfig
+	var configList []SiteAnalysisInfo
 	for cursor.Next(ctx) {
-		var cfg SitesMonitorConfig
+		var cfg SiteAnalysisInfo
 		if err := cursor.Decode(&cfg); err != nil {
 			return nil, err
 		}
@@ -209,68 +63,30 @@ func GetSiteMonitorConfig() ([]SitesMonitorConfig, error) {
 	return configList, nil
 }
 
-// 从数据库获取监控名列表
-func GetSiteMonitorNameList() ([]string, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-	cursor, err := collection.Find(
-		ctx,
-		bson.M{}, // 空条件,查全部
-		options.Find().SetProjection(bson.M{
-			"name": 1, // 只要 name 字段
-			"_id":  0, // 不要 _id 字段(默认会返回)
-		}),
-	)
-	if err != nil {
-		return nil, err
-	}
-	defer cursor.Close(ctx)
-
-	var nameList []string
-	for cursor.Next(ctx) {
-		var result struct {
-			Name string `bson:"name"`
-		}
-		if err := cursor.Decode(&result); err != nil {
-			return nil, err
-		}
-		nameList = append(nameList, result.Name)
-	}
-
-	return nameList, nil
-}
-
-// 从数据库获取一条站点监控配置
-func GetOneSiteMonitorConfig(name string) ([]SitesMonitorConfig, error) {
+// 从数据库获取一条站点分析配置
+func GetSiteAnalysisConfig(id primitive.ObjectID) (*SitesMonitorConfig, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
 	collection := database.GetCollection("SiteMonitorConfig")
 	var cfg SitesMonitorConfig
-	err := collection.FindOne(ctx, bson.M{"name": name}).Decode(&cfg)
-	if err == mongo.ErrNoDocuments {
-		return []SitesMonitorConfig{}, nil
-	} else if err != nil {
+	err := collection.FindOne(ctx, bson.M{"_id": id}).Decode(&cfg)
+	if err != nil {
 		return nil, err
 	}
 
-	var configList []SitesMonitorConfig
-	configList = append(configList, cfg)
-
-	return configList, nil
+	return &cfg, nil
 }
 
-// 插入一条站点监控配置到数据库
-func InsertSiteMonitorConfig(config SitesMonitorConfig) error {
+// 插入一条站点分析配置到数据库
+func InsertSiteAnalysisConfig(config SiteAnalysisConfig) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
 	collection := database.GetCollection("SiteMonitorConfig")
 
 	// _, err := collection.InsertOne(ctx, config)
-	filter := bson.M{"name": config.Name} // 按用户名匹配
+	filter := bson.M{"_id": config.Id} // 按用户名匹配
 	update := bson.M{
 		"$set": config, // 如果存在就更新所有字段
 	}
@@ -285,15 +101,15 @@ func InsertSiteMonitorConfig(config SitesMonitorConfig) error {
 	return nil
 }
 
-// 更新一条站点监控配置到数据库
-func UpdateSiteMonitorConfig(name string, config SitesMonitorConfig) error {
+// 更新一条站点分析配置到数据库
+func UpdateSiteAnalysisConfig(id primitive.ObjectID, config SiteAnalysisConfig) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
 	collection := database.GetCollection("SiteMonitorConfig")
 
 	// _, err := collection.InsertOne(ctx, config)
-	filter := bson.M{"name": name} // 按用户名匹配
+	filter := bson.M{"_id": id} // 按用户名匹配
 	update := bson.M{
 		"$set": config, // 如果存在就更新所有字段
 	}
@@ -308,12 +124,12 @@ func UpdateSiteMonitorConfig(name string, config SitesMonitorConfig) error {
 	return nil
 }
 
-// 删除一条站点监控配置
-func DeleteSiteMonitorConfig(name string) error {
+// 删除一条站点分析配置
+func DeleteSiteAnalysisConfig(id primitive.ObjectID) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 	defer cancel()
 
-	filter := bson.M{"name": name} // 删除条件
+	filter := bson.M{"_id": id} // 删除条件
 	collection := database.GetCollection("SiteMonitorConfig")
 	_, err := collection.DeleteOne(ctx, filter)
 	if err != nil {
@@ -322,112 +138,3 @@ func DeleteSiteMonitorConfig(name string) error {
 
 	return nil
 }
-
-// 更新一条站点监控页面配置到数据库
-func UpdateSiteMonitorPageConfig(name string, config MonitorPageConfig) error {
-	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
-	defer cancel()
-
-	collection := database.GetCollection("SiteMonitorConfig")
-
-	// _, err := collection.InsertOne(ctx, config)
-	filter := bson.M{"name": name} // 按用户名匹配
-	update := bson.M{
-		"$set": bson.M{
-			"pageConfig": config, // 更新页面配置字段
-		},
-	}
-
-	opts := options.Update().SetUpsert(true)
-
-	_, err := collection.UpdateOne(ctx, filter, update, opts)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// 从fwh_pro获取站点信息表
-func QuerySiteInfoList(para SiteInfoPara) (SiteInfoResponse, error) {
-	resByte, err := fwh_pro.QuerySiteInfoList(para.SiteList, para.PageSize, para.PageNum)
-	if err != nil {
-		return SiteInfoResponse{}, err
-	}
-	var siteInfoRes SiteInfoResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &siteInfoRes)
-	if jsonErr != nil {
-		return SiteInfoResponse{}, jsonErr
-	}
-
-	if siteInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SiteInfoResponse{}, errors.New("certification expired")
-	}
-
-	return siteInfoRes, nil
-}
-
-// 从fwh_pro获取站点工作模式信息
-func QuerySiteModeInfo(site string) (SiteModeInfoResponse, error) {
-	resByte, err := fwh_pro.QuerySiteModeInfo(site)
-	if err != nil {
-		return SiteModeInfoResponse{}, err
-	}
-	var siteModeInfoRes SiteModeInfoResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &siteModeInfoRes)
-	if jsonErr != nil {
-		return SiteModeInfoResponse{}, jsonErr
-	}
-
-	if siteModeInfoRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SiteModeInfoResponse{}, errors.New("certification expired")
-	}
-
-	return siteModeInfoRes, nil
-}
-
-// 从fwh_pro获取站点功率流
-func QuerySitePowerFlow(site string, day time.Time) (SitePowerFlowResponse, error) {
-	resByte, err := fwh_pro.QuerySitePowerFlow(site, day)
-	if err != nil {
-		return SitePowerFlowResponse{}, err
-	}
-	var sitePowerFlowRes SitePowerFlowResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &sitePowerFlowRes)
-	if jsonErr != nil {
-		return SitePowerFlowResponse{}, jsonErr
-	}
-
-	if sitePowerFlowRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SitePowerFlowResponse{}, errors.New("certification expired")
-	}
-
-	return sitePowerFlowRes, nil
-}
-
-// 从fwh_pro获取站点告警列表
-func QuerySiteAlarmList(site string) (SiteAlarmListResponse, error) {
-	resByte, err := fwh_pro.QuerySiteAlarmList(site)
-	if err != nil {
-		return SiteAlarmListResponse{}, err
-	}
-	var siteAlarmListRes SiteAlarmListResponse
-	// 反序列化(unmarshal)
-	jsonErr := json.Unmarshal(resByte, &siteAlarmListRes)
-	if jsonErr != nil {
-		return SiteAlarmListResponse{}, jsonErr
-	}
-
-	if siteAlarmListRes.Code == 401 {
-		// TODO 尝试重新登录
-		return SiteAlarmListResponse{}, errors.New("certification expired")
-	}
-
-	return siteAlarmListRes, nil
-}

+ 46 - 0
service/controllers/xlogControllers.go

@@ -0,0 +1,46 @@
+package controllers
+
+import (
+	"fmt"
+	"fwh-toolbox-server/networkEvents"
+)
+
+type XlogManageInfo = networkEvents.SiteInfo
+type XlogManagePage = ResPage[XlogManageInfo]
+
+func GetXlogManageList(page, size uint) XlogManagePage {
+	infoList := networkEvents.GetConnectList()
+	infoListLen := uint(len(infoList))
+	resPage := XlogManagePage{
+		Data:     []XlogManageInfo{},
+		PageNum:  page,
+		PageSize: size,
+		Total:    infoListLen,
+	}
+
+	if infoListLen == 0 {
+		return resPage
+	}
+
+	if page < 1 {
+		return resPage
+	}
+
+	startIndex := (page - 1) * size
+	endIndex := page * size
+	if endIndex > infoListLen {
+		endIndex = infoListLen
+	}
+
+	if endIndex <= startIndex {
+		return resPage
+	}
+
+	resPage.Data = infoList[startIndex:endIndex]
+	return resPage
+}
+
+func SendXlogCmd(uuid, cmd string) (string, error) {
+	fmt.Printf("Send xlog cmd: %s - %s\n", uuid, cmd)
+	return networkEvents.SendXlogCmdDate(uuid, []byte(cmd))
+}

+ 2 - 0
service/go.mod

@@ -40,6 +40,7 @@ require (
 	github.com/jinzhu/inflection v1.0.0 // indirect
 	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
+	github.com/kavu/go_reuseport v1.5.0 // indirect
 	github.com/klauspost/compress v1.17.9 // indirect
 	github.com/mailru/easyjson v0.7.6 // indirect
 	github.com/mattn/go-colorable v0.1.13 // indirect
@@ -56,6 +57,7 @@ require (
 	github.com/subosito/gotenv v1.6.0 // indirect
 	github.com/swaggo/files/v2 v2.0.2 // indirect
 	github.com/swaggo/swag v1.16.4 // indirect
+	github.com/tidwall/evio v1.0.8 // indirect
 	github.com/valyala/bytebufferpool v1.0.0 // indirect
 	github.com/valyala/fasthttp v1.51.0 // indirect
 	github.com/valyala/tcplisten v1.0.0 // indirect

+ 4 - 0
service/go.sum

@@ -60,6 +60,8 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
 github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
 github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
+github.com/kavu/go_reuseport v1.5.0 h1:UNuiY2OblcqAtVDE8Gsg1kZz8zbBWg907sP1ceBV+bk=
+github.com/kavu/go_reuseport v1.5.0/go.mod h1:CG8Ee7ceMFSMnx/xr25Vm0qXaj2Z4i5PWoUx+JZ5/CU=
 github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
 github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -125,6 +127,8 @@ github.com/swaggo/files/v2 v2.0.2 h1:Bq4tgS/yxLB/3nwOMcul5oLEUKa877Ykgz3CJMVbQKU
 github.com/swaggo/files/v2 v2.0.2/go.mod h1:TVqetIzZsO9OhHX1Am9sRf9LdrFZqoK49N37KON/jr0=
 github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
 github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
+github.com/tidwall/evio v1.0.8 h1:+M7lh83rL4KwEObDGtXP3J1wE5utH80LeaAhrKCGVfE=
+github.com/tidwall/evio v1.0.8/go.mod h1:MJhRp4iVVqx/n/5mJk77oKmSABVhC7yYykcJiKaFYYw=
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=

+ 12 - 0
service/main.go

@@ -1,6 +1,7 @@
 package main
 
 import (
+	"fmt"
 	"fwh-toolbox-server/cache"
 	"fwh-toolbox-server/config"
 	"fwh-toolbox-server/database"
@@ -11,6 +12,8 @@ import (
 	"fwh-toolbox-server/routes"
 	"fwh-toolbox-server/scheduler"
 
+	"fwh-toolbox-server/networkEvents"
+
 	"github.com/gofiber/fiber/v2"
 	"github.com/gofiber/swagger"
 	"go.uber.org/zap"
@@ -33,5 +36,14 @@ func main() {
 	app.Get("/swagger/*", swagger.HandlerDefault)
 
 	routes.Setup(app)
+
+	// 启动第一个监听
+	// go func() {
+	go func() {
+		if err := networkEvents.InitEvio(); err != nil {
+			fmt.Println(err.Error())
+		}
+	}()
+
 	app.Listen("localhost:" + cfg.AppPort)
 }

+ 176 - 0
service/networkEvents/evioEvents.go

@@ -0,0 +1,176 @@
+package networkEvents
+
+import (
+	"context"
+	"fmt"
+	"fwh-toolbox-server/utils"
+	"sync"
+	"time"
+
+	"github.com/tidwall/evio"
+)
+
+type SiteInfo struct {
+	Sn   string `json:"sn"`
+	Uuid string `json:"uuid"`
+}
+
+var connMap sync.Map // key - string UUID, value - evio.Conn
+
+type ConnectContext = utils.ConnectContext
+
+func InitEvio() error {
+	var events evio.Events
+	events.Opened = EventsOpened
+	events.Closed = EventsClosed
+	events.Data = EventsData
+	if err := evio.Serve(events, "tcp://0.0.0.0:9099"); err != nil {
+		return err
+	}
+	return nil
+}
+
+func EventsOpened(c evio.Conn) (out []byte, opts evio.Options, action evio.Action) {
+	connectContext := &ConnectContext{
+		UUID:              "",
+		SN:                "",
+		XlogPackageParser: nil,
+		DoneChan:          make(chan string, 1), // 缓冲避免阻塞
+	}
+	connectContext.XlogPackageParser = utils.NewXlogPackageParser(connectContext)
+	connectContext.SendBuffer.Store([]byte(nil))
+	c.SetContext(connectContext)
+
+	// connMap.Store(c.RemoteAddr().String(), c) // 保存连接
+	// xlogPackageParserMap.Store(c.RemoteAddr().String(), utils.NewXlogPackageParser())
+	opts = evio.Options{}
+	action = evio.None
+	return
+}
+
+func EventsClosed(c evio.Conn, err error) (action evio.Action) {
+	if connectContext := c.Context().(*ConnectContext); connectContext != nil {
+		uuid := connectContext.UUID
+		connMap.Delete(uuid)
+		fmt.Println("断开连接端口 UUID: ", uuid)
+	}
+
+	// xlogPackageParserMap.Delete(c.RemoteAddr().String())
+	action = evio.None
+	return
+}
+
+func EventsData(c evio.Conn, in []byte) (out []byte, action evio.Action) {
+	connectContext, ok := c.Context().(*ConnectContext)
+	if !ok || connectContext == nil {
+		fmt.Println("EventsData Context invalid.")
+		fmt.Println(connectContext)
+		action = evio.None
+		return
+	}
+
+	uuid := connectContext.UUID
+	fmt.Println("接收数据 UUID: ", uuid)
+
+	out = nil
+
+	if in == nil {
+		// 处理数据发送事件
+		buffer := connectContext.SendBuffer.Swap([]byte(nil)).([]byte)
+		if len(buffer) > 0 {
+			out = buffer
+		}
+	} else {
+		// 处理数据接收事件
+		parser := connectContext.XlogPackageParser
+		if !parser.Handshake {
+			data, err := parser.ParseXlogHandshakePackage(in)
+			if err != nil {
+				fmt.Println(err)
+				fmt.Println("握手失败")
+			} else {
+				out, err = parser.CreateXlogHandshakePackage()
+				if err != nil {
+					fmt.Println(err)
+					fmt.Println("握手失败")
+				} else {
+					parser.Handshake = true
+
+					connectContext.UUID = string(data.DevUuid[:])
+					connMap.Store(string(data.DevUuid[:]), c) // 保存连接
+
+					fmt.Println("握手校验")
+					fmt.Println(data.NewIv)
+				}
+			}
+		} else {
+			parser.PreparserXlogPackage(in)
+		}
+	}
+
+	action = evio.None
+	return
+}
+
+func GetConnectList() []SiteInfo {
+	infoList := make([]SiteInfo, 0)
+	connMap.Range(func(key, value any) bool {
+		conn := value.(evio.Conn)
+		ctx := conn.Context().(*ConnectContext)
+		infoList = append(infoList, SiteInfo{
+			Sn:   ctx.SN,
+			Uuid: key.(string),
+		})
+		return true
+	})
+	return infoList
+}
+
+func SendXlogCmdDate(uuid string, data []byte) (string, error) {
+	var err error
+	// connMap.Range(func(key, value any) bool {
+	// 	fmt.Printf("%v => %v\n", key, value)
+	// 	return true // 返回 true 继续迭代
+	// })
+	if val, ok := connMap.Load(uuid); ok {
+		c := val.(evio.Conn)
+		connectContext, ok := c.Context().(*ConnectContext)
+		if !ok || connectContext == nil {
+			err = fmt.Errorf("send xlog cmd filed : Context missing, UUID: %s", uuid)
+			return "", err
+		}
+
+		buffer, _ := connectContext.SendBuffer.Load().([]byte)
+		if buffer != nil {
+			err = fmt.Errorf("send xlog cmd filed : Data sending, UUID: %s", uuid)
+			return "", err
+		}
+
+		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+		defer cancel()
+
+		// 创建一个带缓冲的 done channel
+		done := make(chan string, 1)
+		connectContext.DoneChan = done
+
+		// 存储要发送的数据
+		cmdPackage, err := connectContext.XlogPackageParser.CreateXlogCmdPackage(append([]byte(connectContext.UUID)[len(uuid)-8:], data...))
+		if err != nil {
+			return "", err
+		}
+		connectContext.SendBuffer.Store(cmdPackage)
+		// 唤醒 evio,让它触发 Data 事件去真正写数据
+		c.Wake()
+
+		// 等待结果或超时
+		select {
+		case result := <-done:
+			return result, nil
+		case <-ctx.Done():
+			return "", fmt.Errorf("send xlog cmd timeout, UUID: %s, err: %v", uuid, ctx.Err())
+		}
+	} else {
+		err = fmt.Errorf("send xlog cmd filed : Connect missing, UUID: %s", uuid)
+	}
+	return "", err
+}

+ 1 - 0
service/routes/routes.go

@@ -71,6 +71,7 @@ func Setup(app *fiber.App) {
 	// …
 	SitesMonitorSetup(api)
 	SitesAnalysisSetup(api)
+	XlogSetup(api)
 
 	app.Get("/swagger/*", swagger.HandlerDefault)
 }

+ 31 - 23
service/routes/sitesAnalysisRoutes.go

@@ -9,9 +9,9 @@ package routes
 
 import (
 	"fwh-toolbox-server/controllers"
-	"strconv"
 
 	"github.com/gofiber/fiber/v2"
+	"go.mongodb.org/mongo-driver/bson/primitive"
 )
 
 func SitesAnalysisSetup(api fiber.Router) {
@@ -42,11 +42,14 @@ func SitesAnalysisSetup(api fiber.Router) {
 
 	// 获取单个分析配置
 	api.Get("/sitesAnalysis/manage/config/:id", func(c *fiber.Ctx) error {
-		id, err := strconv.ParseUint(c.Params("id"), 10, 64)
+		id := c.Params("id")
+
+		objID, err := primitive.ObjectIDFromHex(id)
 		if err != nil {
-			return JSONResponseNoData(c, 400, "Invalid request id")
+			return JSONResponseNoData(c, 400, "Invalid request para :id")
 		}
-		config, err := controllers.GetOneSiteAnalysisConfig(id)
+
+		config, err := controllers.GetSiteAnalysisConfig(objID)
 		if err != nil {
 			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
 		}
@@ -55,10 +58,7 @@ func SitesAnalysisSetup(api fiber.Router) {
 
 	// 更新一个分析配置
 	api.Post("/sitesAnalysis/manage/config/:id", func(c *fiber.Ctx) error {
-		id, err := strconv.ParseUint(c.Params("id"), 10, 64)
-		if err != nil {
-			return JSONResponseNoData(c, 400, "Invalid request id")
-		}
+		id := c.Params("id")
 		var body controllers.SiteAnalysisConfig
 		if err := c.BodyParser(&body); err != nil {
 			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
@@ -67,7 +67,12 @@ func SitesAnalysisSetup(api fiber.Router) {
 			})
 		}
 
-		if err := controllers.UpdateSiteAnalysisConfig(id, body); err != nil {
+		objID, err := primitive.ObjectIDFromHex(id)
+		if err != nil {
+			return JSONResponseNoData(c, 400, "Invalid request para :id")
+		}
+
+		if err := controllers.UpdateSiteAnalysisConfig(objID, body); err != nil {
 			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
 		}
 		return JSONResponse(c, 0, "success", body)
@@ -75,11 +80,14 @@ func SitesAnalysisSetup(api fiber.Router) {
 
 	// 删除一个监控
 	api.Delete("/sitesAnalysis/manage/config/:id", func(c *fiber.Ctx) error {
-		id, err := strconv.ParseUint(c.Params("id"), 10, 64)
+		id := c.Params("id")
+
+		objID, err := primitive.ObjectIDFromHex(id)
 		if err != nil {
-			return JSONResponseNoData(c, 400, "Invalid request id")
+			return JSONResponseNoData(c, 400, "Invalid request para :id")
 		}
-		err = controllers.DeleteSiteAnalysisConfig(id)
+
+		err = controllers.DeleteSiteAnalysisConfig(objID)
 		if err != nil {
 			return JSONResponseNoData(c, 500, "Database delete failed. (MongoDb)")
 		}
@@ -87,15 +95,15 @@ func SitesAnalysisSetup(api fiber.Router) {
 	})
 
 	// 获取指定ID的分析数据
-	api.Get("/sitesAnalysis/data/:id", func(c *fiber.Ctx) error {
-		id, err := strconv.ParseUint(c.Params("id"), 10, 64)
-		if err != nil {
-			return JSONResponseNoData(c, 400, "Invalid request id")
-		}
-		data, err := controllers.GetSiteAnalysisData(id)
-		if err != nil {
-			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
-		}
-		return JSONResponse(c, 0, "success", data)
-	})
+	// api.Get("/sitesAnalysis/data/:id", func(c *fiber.Ctx) error {
+	// 	id, err := strconv.ParseUint(c.Params("id"), 10, 64)
+	// 	if err != nil {
+	// 		return JSONResponseNoData(c, 400, "Invalid request id")
+	// 	}
+	// 	data, err := controllers.GetSiteAnalysisData(id)
+	// 	if err != nil {
+	// 		return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
+	// 	}
+	// 	return JSONResponse(c, 0, "success", data)
+	// })
 }

+ 51 - 0
service/routes/xlogRoutes.go

@@ -0,0 +1,51 @@
+/*
+ * XLog API List
+ * /xlog/manage/siteList - GET
+ * /xlog/terminal/{uuid} - POST 发送终端命令
+ */
+package routes
+
+import (
+	"fmt"
+	"fwh-toolbox-server/controllers"
+	"strconv"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+func XlogSetup(api fiber.Router) {
+	// 获取站点分析管理名称表
+	api.Get("/xlog/manage/siteList", func(c *fiber.Ctx) error {
+		pageInt, _ := strconv.Atoi(c.Query("pageNum", "1"))
+		sizeInt, _ := strconv.Atoi(c.Query("pageSize", "20"))
+
+		if pageInt <= 0 || sizeInt <= 0 {
+			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+				"code":    400,
+				"message": "Invalid request para",
+			})
+		}
+
+		listPage := controllers.GetXlogManageList(uint(pageInt), uint(sizeInt))
+		return JSONResponse(c, 0, "success", listPage)
+	})
+
+	// 发送命令
+	api.Post("/xlog/terminal/:uuid", func(c *fiber.Ctx) error {
+		uuid := c.Params("uuid")
+		fmt.Println(uuid)
+		var body string
+		if err := c.BodyParser(&body); err != nil {
+			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+				"code":    400,
+				"message": "Invalid request body",
+			})
+		}
+
+		data, err := controllers.SendXlogCmd(uuid, body)
+		if err != nil {
+			return JSONResponseNoData(c, 500, err.Error())
+		}
+		return JSONResponse(c, 0, "success", data)
+	})
+}

+ 141 - 0
service/utils/xlog_encrypt.go

@@ -0,0 +1,141 @@
+package utils
+
+import (
+	"crypto/aes"
+	"crypto/cipher"
+	"errors"
+	"fmt"
+)
+
+// 外部禁止使用此结构体创建实例,否则会出错
+type XlogEncrypt struct {
+	AesIv []byte // 长度必须为aes.BlockSize
+}
+
+var aes_key = []byte("12369854712564897564325467553685") // 32 字节 = 256 位
+var aes_iv = []byte("5123688955621365")
+
+// 必须用函数创建实例,否则会出错
+func NewXlogEncrypt() XlogEncrypt {
+	xe := XlogEncrypt{
+		AesIv: aes_iv,
+	}
+	return xe
+}
+
+func (x *XlogEncrypt) SetNewIv(newIv []byte) error {
+	if len(newIv) != aes.BlockSize {
+		return fmt.Errorf("IV length must be 16 bytes, got %d", len(newIv))
+	}
+
+	x.AesIv = newIv
+	return nil
+}
+
+// PKCS7 填充
+func PKCS7Pad(data []byte, blockSize int) []byte {
+	padLen := blockSize - len(data)%blockSize
+	pad := make([]byte, padLen)
+	for i := range pad {
+		pad[i] = byte(padLen)
+	}
+	return append(data, pad...)
+}
+
+// PKCS7 去填充
+func PKCS7Unpad(data []byte, blockSize int) ([]byte, error) {
+	length := len(data)
+	if length == 0 {
+		return nil, errors.New("data is empty")
+	}
+	if length%blockSize != 0 {
+		return nil, errors.New("data is not a multiple of block size")
+	}
+	padLen := int(data[length-1])
+	if padLen == 0 || padLen > blockSize {
+		return nil, errors.New("invalid padding")
+	}
+	for i := 0; i < padLen; i++ {
+		if data[length-1-i] != byte(padLen) {
+			return nil, errors.New("invalid padding")
+		}
+	}
+	return data[:length-padLen], nil
+}
+
+// AES-256-CBC 加密
+func Aes256CbcEncrypt(plainText, iv []byte) ([]byte, error) {
+	if len(aes_key) != 32 {
+		return nil, errors.New("key length must be 32 bytes")
+	}
+	if len(iv) != aes.BlockSize {
+		return nil, errors.New("IV length must be 16 bytes")
+	}
+
+	block, err := aes.NewCipher(aes_key)
+	if err != nil {
+		return nil, err
+	}
+
+	// PKCS7 填充
+	padded := PKCS7Pad(plainText, aes.BlockSize)
+
+	cipherText := make([]byte, len(padded))
+	mode := cipher.NewCBCEncrypter(block, iv)
+	mode.CryptBlocks(cipherText, padded)
+
+	return cipherText, nil
+}
+
+// AES-256-CBC 解密
+func Aes256CbcDecrypt(cipherText []byte, iv []byte) ([]byte, error) {
+	if len(aes_key) != 32 {
+		return nil, errors.New("key length must be 32 bytes")
+	}
+	if len(iv) != aes.BlockSize {
+		return nil, errors.New("IV length must be 16 bytes")
+	}
+
+	block, err := aes.NewCipher(aes_key)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(cipherText)%aes.BlockSize != 0 {
+		return nil, fmt.Errorf("cipherText is not a multiple of the block size")
+	}
+
+	mode := cipher.NewCBCDecrypter(block, iv)
+	plaintext := make([]byte, len(cipherText))
+	mode.CryptBlocks(plaintext, cipherText)
+
+	// 去掉 PKCS7 填充
+	plaintext, err = PKCS7Unpad(plaintext, aes.BlockSize)
+	if err != nil {
+		return nil, err
+	}
+
+	return plaintext, nil
+}
+
+// 加密数据
+func (x XlogEncrypt) EncryptData(data []byte) ([]byte, error) {
+	fmt.Println("NewIv")
+	fmt.Println(x.AesIv)
+	encrypted, err := Aes256CbcEncrypt(data, x.AesIv)
+	if err != nil {
+		return nil, err
+	}
+
+	return encrypted, nil
+}
+
+// 解密数据
+func (x XlogEncrypt) DecryptData(data []byte) ([]byte, error) {
+	decrypted, err := Aes256CbcDecrypt(data, x.AesIv)
+	if err != nil {
+		return nil, err
+	}
+
+	return decrypted, nil
+}

+ 274 - 0
service/utils/xlog_package.go

@@ -0,0 +1,274 @@
+package utils
+
+import (
+	"bytes"
+	"encoding/binary"
+	"errors"
+	"fmt"
+	"strings"
+)
+
+const (
+	// Magic word
+	_MAGIC_XLOG_SYSTEM_ uint32 = 0x99A3B417 // ARM的日志发送给PC
+
+	// CmdType 命令类型 mainCmd(8bit) + subCmd(8bit)
+	// mainCmd
+	_XLOG_CMD_LOG_TO_PC_        uint8 = 0  // 心跳内容, 内容为"[x]\n"
+	_XLOG_CMD_REPORT_BASICINFO_ uint8 = 1  // 上报sn + mac, 结构为 XlogSiteBaseInfo
+	_XLOG_CMD_REPLY_BASICINFO_  uint8 = 2  // 回复sn + mac,暂不回复
+	_XLOG_P2P_EXEC_BASH_CMD_    uint8 = 64 // pc    -> SERNAME_UB_SERVER -> xlogv3, 执行远程命令
+	_XLOG_P2P_BASH_CMD_RESULT_  uint8 = 65 // xlogv3-> SERNAME_UB_SERVER -> pc      返回远程命令执行结果
+)
+
+func NewXlogPackageParser(ctx *ConnectContext) *XlogPackageParser {
+	return &XlogPackageParser{
+		crypt:        NewXlogEncrypt(),
+		ctx:          ctx,
+		cmdResultStr: "",
+		Handshake:    false,
+		Buffer:       make([]byte, 0),
+	}
+}
+
+// 解析设备握手包
+func (parser *XlogPackageParser) ParseXlogHandshakePackage(raw []byte) (*XlogHandshakePackageData, error) {
+	reader := bytes.NewReader(raw)
+
+	// 数据长度校验
+	if len(raw) != 72 {
+		return nil, fmt.Errorf("invalid handshake packet: data length error, expected 72, got %d", len(raw))
+	}
+
+	// 解析数据结构
+	var p XlogHandshakePackage
+	if err := binary.Read(reader, binary.LittleEndian, &p); err != nil {
+		return nil, err
+	}
+
+	// 校验数据头
+	if p.MagicWord != _MAGIC_XLOG_SYSTEM_ {
+		return nil, errors.New("invalid magic")
+	}
+	if p.CmdLen != uint32(len(raw)-8) {
+		return nil, fmt.Errorf("invalid handshake packet: data length error, CmdLen: %d", p.CmdLen)
+	}
+
+	// 解密数据
+	handshakeData, err := parser.crypt.DecryptData(p.EncryptData[:])
+	if err != nil {
+		return nil, err
+	}
+
+	// 数据长度校验
+	if len(handshakeData) != (16 + 36) {
+		return nil, fmt.Errorf("invalid handshake packet: data length error, expected 52, got %d", len(handshakeData))
+	}
+
+	// 解析握手数据结构
+	var h XlogHandshakePackageData
+	reader = bytes.NewReader(handshakeData)
+	if err := binary.Read(reader, binary.LittleEndian, &h); err != nil {
+		return nil, err
+	}
+
+	fmt.Println(string(bytes.TrimRight(h.DevUuid[:], "\x00")))
+
+	// 更新IV
+	if err := parser.crypt.SetNewIv(h.NewIv[:]); err != nil {
+		return nil, err
+	}
+
+	return &h, nil
+}
+
+// 预解析数据包,数据提取,提取一个完整数据包
+func (parser *XlogPackageParser) PreparserXlogPackage(raw []byte) {
+	// 累积数据
+	parser.Buffer = append(parser.Buffer, raw...)
+	buffer := parser.Buffer
+
+	for {
+		// 不够一个包头和长度字段,继续收
+		if len(buffer) < 8 {
+			return
+		}
+
+		// 查找合法包头
+		header := binary.LittleEndian.Uint32(buffer[:4])
+		if header != _MAGIC_XLOG_SYSTEM_ {
+			// 丢掉 1 字节,继续找包头
+			buffer = buffer[1:]
+			continue
+		}
+
+		// 读取长度
+		length := binary.LittleEndian.Uint32(buffer[4:8])
+		totalLen := 8 + int(length)
+
+		// 数据是否完整
+		if len(buffer) < totalLen {
+			break
+		}
+
+		// 提取完整包
+		packet := buffer[:totalLen]
+
+		// 异步处理
+		go parser.ParseXlogPackage(packet)
+
+		// 移除已处理的数据
+		parser.Buffer = buffer[totalLen:]
+		buffer = parser.Buffer
+	}
+}
+
+// 处理设备心跳信息
+func (parser XlogPackageParser) handleHeartbeat(raw []byte) {
+	fmt.Printf("%s - 接收到心跳:%s", parser.ctx.UUID, string(raw))
+}
+
+// 处理设备基本信息
+func (parser XlogPackageParser) handleSiteBaseInfo(raw []byte) {
+	// 解析基本信息
+	infoReader := bytes.NewReader(raw)
+	var info XlogSiteBaseInfo
+	if err := binary.Read(infoReader, binary.LittleEndian, &info); err != nil {
+		fmt.Printf("handleSiteBaseInfo failed: %s\n", err)
+		return
+	}
+
+	// 更新SN
+	parser.ctx.SN = string(bytes.TrimRight(info.SN[:], "\x00"))
+	fmt.Printf("update SN: %s\n", parser.ctx.SN)
+	fmt.Println("握手成功")
+}
+
+// 处理命令执行结果
+func (parser *XlogPackageParser) handleCmdResult(raw []byte) {
+	resStr := string(raw[:])
+	if strings.HasPrefix(resStr, "\n***above is result of:") {
+		parser.cmdResultStr = parser.cmdResultStr + resStr
+		select {
+		case parser.ctx.DoneChan <- parser.cmdResultStr:
+			// 成功写入结果
+			fmt.Printf("%s - 回传结果:%s\n", parser.ctx.UUID, parser.cmdResultStr)
+		default:
+			// channel 已经满(说明调用方可能已经超时退出了)
+			fmt.Printf("%s - DoneChan已满或超时。\n", parser.ctx.UUID)
+		}
+	} else {
+		parser.cmdResultStr = resStr
+	}
+}
+
+// 解析设备数据包
+func (parser *XlogPackageParser) ParseXlogPackage(raw []byte) {
+	// 数据长度校验
+	headerPackageSize := binary.Size(XlogPackageHeader{})
+	if len(raw) < headerPackageSize {
+		fmt.Printf("invalid packet: data length error, expected >= %d, got %d\n", headerPackageSize, len(raw))
+		return
+	}
+
+	headerRaw := raw[:headerPackageSize] // 非加密头部
+	headerReader := bytes.NewReader(headerRaw)
+
+	// 解析头部
+	var h XlogPackageHeader
+	if err := binary.Read(headerReader, binary.LittleEndian, &h); err != nil {
+		fmt.Printf("XlogPackage header parsing error: %s\n", err.Error())
+		return
+	}
+
+	if len(raw) > headerPackageSize {
+		encryptRaw := raw[headerPackageSize:] // 加密数据
+
+		// 解密Payload
+		bodyRaw, err := parser.crypt.DecryptData(encryptRaw)
+		if err != nil {
+			fmt.Println("DecryptData error: " + err.Error())
+			return
+		}
+
+		// 判断包类型
+		switch h.MainCmd {
+		case _XLOG_CMD_LOG_TO_PC_:
+			parser.handleHeartbeat(bodyRaw)
+		case _XLOG_CMD_REPORT_BASICINFO_:
+			parser.handleSiteBaseInfo(bodyRaw)
+		case _XLOG_P2P_BASH_CMD_RESULT_:
+			parser.handleCmdResult(bodyRaw)
+		}
+
+	}
+}
+
+// 创建握手响应包
+func (parser XlogPackageParser) CreateXlogHandshakePackage() ([]byte, error) {
+	encryptData, err := parser.crypt.EncryptData([]byte("0123456789abcdef"))
+	if err != nil {
+		return nil, err
+	}
+
+	buf := new(bytes.Buffer)
+	// 按小端序写入,先写头部
+	if err := binary.Write(buf, binary.LittleEndian, _MAGIC_XLOG_SYSTEM_); err != nil {
+		return nil, fmt.Errorf("failed to write XlogHandshakeResponsePackage magic word: %w", err)
+	}
+	if err := binary.Write(buf, binary.LittleEndian, uint32(32)); err != nil {
+		return nil, fmt.Errorf("failed to write XlogHandshakeResponsePackage magic word: %w", err)
+	}
+	// 再写 EncryptData
+	if _, err := buf.Write(encryptData); err != nil {
+		return nil, fmt.Errorf("failed to write XlogHandshakeResponsePackage EncryptData: %w", err)
+	}
+
+	return buf.Bytes(), nil
+}
+
+// 创建发送命令数据包
+func (parser XlogPackageParser) CreateXlogCmdPackage(payload []byte) ([]byte, error) {
+	var dstServiceName [12]byte
+	copy(dstServiceName[:], "Xlog")
+	h := XlogPackageHeader{
+		MagicWord:      _MAGIC_XLOG_SYSTEM_,
+		CmdLen:         0,
+		MainCmd:        _XLOG_P2P_EXEC_BASH_CMD_,
+		SubCmd:         0,
+		ControlBitData: 0,
+		DstServiceName: dstServiceName,
+		ToBeSendLen:    0,
+		IgnoredLen:     0,
+		UpTime:         0,
+	}
+
+	encryptData := []byte{}
+	if len(payload) > 0 {
+		encrypted, err := parser.crypt.EncryptData(payload)
+		if err != nil {
+			return nil, err
+		}
+		encryptData = encrypted
+	}
+
+	// 36是XlogPackageHeader字节数,8是MagicWord + CmdLen字节数
+	h.CmdLen = uint32(36 + len(encryptData) - 8)
+
+	p := XlogPackage{
+		Header:         h,
+		EncryptPayload: encryptData,
+	}
+
+	buf := new(bytes.Buffer)
+	// 按小端序写入,先写头部
+	if err := binary.Write(buf, binary.LittleEndian, p.Header); err != nil {
+		return nil, fmt.Errorf("failed to write XlogPackage header: %w", err)
+	}
+	// 再写 payload
+	if _, err := buf.Write(p.EncryptPayload); err != nil {
+		return nil, fmt.Errorf("failed to write XlogPackage payload: %w", err)
+	}
+
+	return buf.Bytes(), nil
+}

+ 62 - 0
service/utils/xlog_types.go

@@ -0,0 +1,62 @@
+package utils
+
+import (
+	"sync/atomic"
+)
+
+type ConnectContext struct {
+	UUID              string
+	SN                string
+	XlogPackageParser *XlogPackageParser
+	SendBuffer        atomic.Value
+	DoneChan          chan string // 用来回传结果的 channel
+}
+
+// Site基本信息
+type XlogSiteBaseInfo struct {
+	SN  [48]byte
+	MAC [20]byte
+}
+
+// EncryptData解密后类型
+type XlogHandshakePackageData struct {
+	NewIv   [16]byte // 新的IV,用于后续通讯
+	DevUuid [36]byte // 设备UUID
+}
+
+type XlogHandshakePackage struct {
+	MagicWord   uint32   // Magic word
+	CmdLen      uint32   // 数据长度 = XlogPackage总长度 - 8(MagicWord长度)
+	EncryptData [64]byte //
+}
+
+type XlogHandshakeResponsePackage struct {
+	MagicWord   uint32   // Magic word
+	EncryptData [64]byte //
+}
+
+type XlogPackageHeader struct {
+	MagicWord      uint32   // Magic word
+	CmdLen         uint32   // 数据长度 = XlogPackage总长度 - 8(MagicWord长度)
+	MainCmd        uint8    // 命令类型 CmdType - MainCmd
+	SubCmd         uint8    // 命令类型 CmdType - SubCmd
+	ControlBitData uint16   // 控制位数据
+	DstServiceName [12]byte // 目标服务名称
+	ToBeSendLen    uint32   // 发送这包之前,已经缓存的数据,这个值通常要是0,表示网络非常好,没有缓存数据
+	IgnoredLen     uint32   // 忽略的字节数
+	UpTime         uint32
+}
+
+type XlogPackage struct {
+	Header         XlogPackageHeader
+	EncryptPayload []byte // 数据
+}
+
+// 必须用构造函数NewXlogPackageParser创建
+type XlogPackageParser struct {
+	crypt        XlogEncrypt
+	ctx          *ConnectContext
+	cmdResultStr string
+	Handshake    bool   // 连接是否已握手
+	Buffer       []byte // 接收数据缓存buffer,数据可能不完整,需预处理
+}

+ 105 - 0
src/composable/comp/useFullCard.ts

@@ -0,0 +1,105 @@
+import { isString, debounce } from 'es-toolkit'
+import { isElement } from 'es-toolkit/compat'
+import { isRef, nextTick, onMounted, reactive, ref, onBeforeUnmount } from 'vue'
+
+import type { ComponentPublicInstance, Ref } from 'vue'
+
+type MaybeHTMLElement = HTMLElement | Ref<HTMLElement | null> | string | null
+
+interface FullCardOptions {
+  parentContainer?: MaybeHTMLElement
+  parentWrap?: MaybeHTMLElement
+}
+
+type ElementKey = 'container' | 'wrap' | 'card'
+
+export function useFullCard<T extends ComponentPublicInstance>(
+  target: Ref<T | null>,
+  options: FullCardOptions = {},
+) {
+  const { parentContainer = '.main-container', parentWrap = '.main-wrap' } = options
+
+  let resizeObserver: ResizeObserver | null = null
+
+  const height = ref<number | undefined>(0)
+
+  const elementMap = reactive<Record<ElementKey, HTMLElement | null>>({
+    container: null,
+    wrap: null,
+    card: null,
+  })
+
+  function updateMaxHeight() {
+    const { container, wrap, card } = elementMap
+
+    if (!container || !wrap || !card) {
+      return
+    }
+
+    const containerHeight = container.offsetHeight
+    const wrapHeight = wrap.offsetHeight
+    const cardHeight = card.offsetHeight
+
+    height.value =
+      (containerHeight < wrapHeight
+        ? cardHeight - (wrapHeight - containerHeight)
+        : cardHeight + containerHeight - wrapHeight) - 1
+  }
+
+  const debounceUpdateMaxHeight = debounce(() => {
+    updateMaxHeight()
+  }, 300)
+
+  function getParentElement(element: MaybeHTMLElement) {
+    if (isString(element)) return elementMap.card?.closest<HTMLElement>(element) ?? null
+    if (isRef(element)) return element.value
+    if (isElement(element)) return element
+    return null
+  }
+
+  function initTableResizeObserver() {
+    if (!target.value) {
+      height.value = undefined
+      return
+    }
+
+    elementMap.card = (target.value?.$el as HTMLElement) ?? null
+
+    nextTick(() => {
+      elementMap.container = getParentElement(parentContainer)
+      elementMap.wrap = getParentElement(parentWrap)
+
+      const missingKeys = Object.entries(elementMap)
+        .filter((value) => value[1] == null)
+        .map(([key]) => key)
+
+      if (missingKeys.length) {
+        throw new Error(`Missing or null elements: ${missingKeys.join(', ')}`)
+      }
+
+      resizeObserver = new ResizeObserver((entries) => {
+        for (const entry of entries) {
+          const { height } = entry.contentRect
+          if (height) debounceUpdateMaxHeight()
+        }
+      })
+
+      if (elementMap.container) {
+        resizeObserver.observe(elementMap.container)
+      }
+    })
+  }
+
+  onMounted(() => {
+    initTableResizeObserver()
+  })
+
+  onBeforeUnmount(() => {
+    resizeObserver?.disconnect()
+  })
+
+  return {
+    height,
+    updateMaxHeight,
+  }
+}

+ 1 - 1
src/main.ts

@@ -26,7 +26,7 @@ async function setupApp() {
   await router.isReady()
 
   console.log({ env: import.meta.env })
-  console.log({ routes: router.options.routes })
+  console.log({ routes: router.getRoutes() })
 
   if (window.loaderElement) {
     window.loaderElement.remove()

+ 31 - 3
src/router/record.ts

@@ -25,7 +25,7 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
     component: 'sites-monitor/manage',
   },
   {
-    path: 'sites-monitor/:name?',
+    path: 'sites-monitor/:name',
     name: 'sitesMonitor',
     icon: 'i-carbon:cloud-monitoring',
     label: '站点监控',
@@ -33,7 +33,7 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
       componentName: 'SitesMonitor',
       showTab: true,
       enableMultiTab: true,
-      renderTabLabel({ id, name }) {
+      renderTabLabel({ name }) {
         return `站点监控${name ? '-' + name : ''}`
       },
     },
@@ -60,7 +60,7 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
   //     componentName: 'SitesAnalysis',
   //     showTab: true,
   //     enableMultiTab: true,
-  //     renderTabLabel({ id, name }) {
+  //     renderTabLabel({ name }) {
   //       return `站点分析${name ? '-' + name : ''}`
   //     },
   //   },
@@ -266,6 +266,34 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
   //   },
   //   component: 'error-page/404',
   // },
+  {
+    path: 'xlog-manage',
+    name: 'xlogManage',
+    icon: 'i-carbon:terminal',
+    label: 'XLOG管理',
+    meta: {
+      componentName: 'XlogManage',
+      showTab: true,
+    },
+    component: 'xlog/manage',
+  },
+  {
+    path: 'xlog-terminal/:uuid',
+    name: 'xlogTerminal',
+    icon: 'i-carbon:terminal',
+    label: 'XLOG',
+    meta: {
+      componentName: 'XlogTerminal',
+      showTab: true,
+      enableMultiTab: true,
+      renderTabLabel({ uuid }) {
+        // TODO 后续考虑如何显示SN
+        return `XLOG${uuid ? '-' + uuid : ''}`
+      },
+    },
+    component: 'xlog/index',
+    show: false,
+  },
   {
     path: '/about',
     key: 'about',

+ 2 - 2
src/service/sitesAnalysis/interface.ts

@@ -6,7 +6,7 @@ export interface AnalysisListFilter {
 }
 
 export interface AnalysisConfig {
-  id: number
+  id: string
   sn: string
   description: string
   timePeriod: [string, string]
@@ -17,7 +17,7 @@ export interface AnalysisConfig {
 }
 
 export interface AnalysisItemInfo {
-  id: number
+  id: string
   sn: string
   description: string
   item: string[]

+ 35 - 0
src/service/xlog/index.ts

@@ -0,0 +1,35 @@
+// API List
+// /xlog/manage/siteList - GET
+// /xlog/terminal/{uuid} - POST 发送终端命令
+
+import http from '@/service/serverReq'
+import type { XlogSiteItemInfoFilter, XlogSiteItemInfoPage } from './interface'
+
+const API_List = {
+  manageList: '/xlog/manage/siteList',
+  xlogCmdSend: '/xlog/terminal/{uuid}',
+}
+
+// GET xlog管理表
+export const getManageList = async (
+  pageSize: number,
+  pageNum: number,
+  filter?: XlogSiteItemInfoFilter,
+): Promise<XlogSiteItemInfoPage> => {
+  const res = await http.get<XlogSiteItemInfoPage>(API_List.manageList, {
+    params: {
+      pageSize,
+      pageNum,
+      sn: filter ? filter.sn : undefined,
+      uuid: filter ? filter.uuid : undefined,
+    },
+  })
+  return res
+}
+
+// POST 发送终端命令
+export const sendXlogCmd = async (uuid: string, cmd: string): Promise<string> => {
+  const url = API_List.xlogCmdSend.replace('{uuid}', encodeURIComponent(uuid))
+  const res = await http.post<string>(url, cmd)
+  return res
+}

+ 13 - 0
src/service/xlog/interface.ts

@@ -0,0 +1,13 @@
+import type { ResPage } from '../interface'
+
+export interface XlogSiteItemInfoFilter {
+  sn?: string
+  uuid?: string
+}
+
+export interface XlogSiteItemInfo {
+  sn: string
+  uuid: string
+}
+
+export interface XlogSiteItemInfoPage extends ResPage<XlogSiteItemInfo[]> {}

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

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

+ 110 - 0
src/views/xlog/component/xlogCommand.vue

@@ -0,0 +1,110 @@
+<template>
+  <div class="flex flex-col space-y-4">
+    <div class="flex flex-1 gap-4 h-full overflow-hidden">
+      <div class="w-1/3 overflow-auto h-full">
+        <NButtonGroup vertical class="w-full">
+          <NButton class="w-full justify-start px-4" v-for="(cmd, index) in cmdList" :key="index"
+            @click="handleListClick(cmd)">
+            {{ index }} - {{ cmd.send }}
+          </NButton>
+        </NButtonGroup>
+      </div>
+      <NLog :log="logStr" class="flex-1 overflow-auto h-full" :style="{ height: null }"></NLog>
+    </div>
+    <div class="flex items-center space-x-2 mt-auto">
+      <NInput v-model:value="inputCmd" @update:value="handleInputChange" @keyup.enter="handleSendCmd"
+        @keyup.up="handleInputUp" @keyup.down="handleInputDown">
+        <template #prefix>
+          <i class="i-ph:terminal-bold" />
+        </template>
+      </NInput>
+      <NButton type="primary" :disabled="disabledSendCmd" :loading="loadingSendCmd" @click="handleSendCmd">
+        Send
+      </NButton>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { NButton, NInput, NLog, NButtonGroup } from 'naive-ui'
+import { sendXlogCmd } from '@/service/xlog';
+
+const { uuid, sn } = defineProps<{
+  uuid: string,
+  sn: string
+}>()
+
+let inputCmdIndex = -1
+let inputCmdTemp = ""
+const inputCmd = ref<string>("")
+const logStr = ref<string>("")
+const cmdList = ref<{
+  send: string,
+  recv: string
+}[]>([])
+
+const disabledSendCmd = ref<boolean>(true)
+const loadingSendCmd = ref<boolean>(false)
+const handleSendCmd = async () => {
+  loadingSendCmd.value = true
+  const res = await sendXlogCmd(uuid, inputCmd.value)
+  cmdList.value.push({
+    send: inputCmd.value,
+    recv: res
+  })
+  loadingSendCmd.value = false
+}
+
+const handleInputChange = (value: string) => {
+  if (value.length === 0 && !disabledSendCmd.value) {
+    disabledSendCmd.value = true
+  } else if (value.length > 0 && disabledSendCmd.value) {
+    disabledSendCmd.value = false
+  }
+
+  if (inputCmdIndex !== -1) {
+    inputCmdIndex = -1
+  }
+}
+
+const handleInputUp = (e: KeyboardEvent) => {
+  if (inputCmdIndex === -1) {
+    inputCmdTemp = inputCmd.value
+    inputCmdIndex = cmdList.value.length - 1
+    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ""
+  } else if (inputCmdIndex > 0) {
+    inputCmdIndex = inputCmdIndex - 1
+    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ""
+  }
+}
+
+const handleInputDown = (e: KeyboardEvent) => {
+  if (inputCmdIndex === -1) {
+    return
+  } else if (inputCmdIndex === cmdList.value.length - 1) {
+    inputCmdIndex = -1
+    inputCmd.value = inputCmdTemp
+  } else if (inputCmdIndex < cmdList.value.length - 1) {
+    inputCmdIndex = inputCmdIndex + 1
+    inputCmd.value = cmdList.value.at(inputCmdIndex)?.send ?? ""
+  }
+}
+
+const handleListClick = (cmd: {
+  send: string,
+  recv: string
+}) => {
+  logStr.value = cmd.recv
+}
+
+onMounted(() => {
+
+
+  // 更新log显示
+  const lastCmd = cmdList.value.at(-1)
+  if (lastCmd) {
+    logStr.value = lastCmd.recv
+  }
+})
+
+</script>

+ 36 - 0
src/views/xlog/index.vue

@@ -0,0 +1,36 @@
+<template>
+  <div class="main-wrap flex flex-col gap-y-2 p-4">
+    <NAlert type="info" closable @after-leave="updateMaxHeight">
+      模拟xlog终端 - {{ uuid }} - {{ sn }}
+    </NAlert>
+    <div :style="cardHeight">
+      <NCard ref="mainCardRef" class="h-full" content-class="h-full">
+        <div class="h-full">
+          <XlogCommand class="h-full" ref="xlogCommandRef" :uuid="uuid" :sn="sn" />
+        </div>
+      </NCard>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { NCard, NAlert } from 'naive-ui'
+import { useRoute } from 'vue-router'
+import XlogCommand from './component/xlogCommand.vue'
+import { useFullCard } from '@/composable/comp/useFullCard'
+
+const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
+const xlogCommandRef = useTemplateRef<InstanceType<typeof XlogCommand>>('xlogCommandRef')
+const { height, updateMaxHeight } = useFullCard(mainCardRef)
+
+const cardHeight = computed(() => {
+  console.log({ height: height.value })
+  return { height: (height.value === undefined || height.value === 0 ? 480 : height.value) + 'px' }
+})
+
+const route = useRoute()
+// 获取 SN & UUID
+const sn = route.query.sn as string
+const uuid = route.params.uuid as string
+
+</script>

+ 242 - 0
src/views/xlog/manage.vue

@@ -0,0 +1,242 @@
+<template>
+  <div class="main-wrap flex flex-col gap-y-2 p-4">
+    <NCard>
+      <div class="mb-2 flex gap-x-4">
+        <NForm ref="formRef" :model="form" inline label-placement="left">
+          <NFormItem label="SN" path="sn">
+            <NInput v-model:value="form.sn" style="width: 250px" clearable />
+          </NFormItem>
+          <NFormItem label="UUID" path="uuid">
+            <NInput v-model:value="form.uuid" style="width: 250px" clearable />
+          </NFormItem>
+        </NForm>
+        <div class="flex gap-2">
+          <NButton disabled type="success" @click="createXlogConnect()">
+            <template #icon>
+              <span class="i-ph:plus-circle" />
+            </template>
+            新增xlog
+          </NButton>
+          <NButton type="info" @click="handleQueryClick" :loading="isRequestLoading" :disabled="isRequestLoading">
+            <template #icon>
+              <span class="i-ph:magnifying-glass" />
+            </template>
+            查询
+          </NButton>
+          <NButton type="warning" @click="resetForm">
+            <template #icon>
+              <span class="i-ph:arrow-clockwise" />
+            </template>
+            重置
+          </NButton>
+        </div>
+      </div>
+      <div>
+        <NDataTable ref="dataTableRef" v-model:checked-row-keys="checkedRowKeys" :remote="true" :min-height="166.6"
+          :max-height="maxHeight" size="small" :columns="tableColumns" :data="tableData" :row-key="tableRowKey"
+          :loading="isRequestLoading" striped :single-line="false" :row-props="rowProps" />
+        <div class="mt-3 flex items-end justify-between">
+          <div class="flex items-center gap-x-3">
+            <span>已选择&nbsp;{{ checkedRowKeys.length }}&nbsp; 条</span>
+          </div>
+          <NPagination v-bind="tablePagination" :prefix="paginationPrefix" />
+        </div>
+      </div>
+    </NCard>
+    <NDropdown placement="bottom-start" trigger="manual" v-bind="dropdownOptions" :show="showDropdown" />
+  </div>
+</template>
+
+<script setup lang="tsx">
+import { h } from 'vue'
+import { useRouter } from 'vue-router'
+import { NButton, NDataTable, NForm, NNumberAnimation } from 'naive-ui'
+import type { DataTableColumns, PaginationProps, DropdownProps } from 'naive-ui'
+import type { XlogSiteItemInfo } from '@/service/xlog/interface'
+import { getManageList, sendXlogCmd } from '@/service/xlog'
+
+import { useDataTable } from '@/composable/comp/useDataTable'
+import { useResettableReactive } from '@/composable/useResettable'
+
+const router = useRouter()
+
+const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
+const dataTableRef = useTemplateRef<InstanceType<typeof NDataTable>>('dataTableRef')
+const { maxHeight } = useDataTable(dataTableRef)
+
+const isRequestLoading = ref(false)
+const showDropdown = ref(false)
+const checkedRowKeys = ref<Array<number | string>>([])
+const contextmenuUUID = ref<string | undefined>(undefined)
+
+interface XlogSiteFilterForm extends Partial<XlogSiteItemInfo> { }
+
+const [form, , resetForm] = useResettableReactive<XlogSiteFilterForm>({
+  sn: '',
+  uuid: '',
+})
+
+const handleQueryClick = () => {
+  // 请求查询数据
+  handleListQuery()
+}
+
+const prevUserListTotal = ref(0)
+
+const paginationPrefix: PaginationProps['prefix'] = (info) => {
+  const { itemCount } = info
+  return (
+    itemCount && (
+      <div>
+        <span>总&nbsp; </span>
+        < NNumberAnimation
+          from={prevUserListTotal.value}
+          to={itemCount}
+          onFinish={() => {
+            prevUserListTotal.value = itemCount
+          }}
+        />
+        <span>&nbsp;条 </span>
+      </div>
+    )
+  )
+}
+
+const dropdownOptions = reactive<DropdownProps>({
+  x: 0,
+  y: 0,
+  options: [
+    {
+      label: () => <span class='text-green-500'> 查看 </span>,
+      key: 'view',
+    },
+  ],
+  onClickoutside: () => {
+    showDropdown.value = false
+  },
+  onSelect: (key: string) => {
+    showDropdown.value = false
+    if (contextmenuUUID.value === undefined) return
+    if (key === 'view') {
+      itemView(contextmenuUUID.value, "")
+    }
+  },
+})
+
+const createXlogConnect = () => {
+  // TODO 创建xlog连接
+}
+
+const tableRowKey = (rowData: XlogSiteItemInfo) => {
+  return rowData.sn
+}
+
+function createColumns(): DataTableColumns<XlogSiteItemInfo> {
+  return [
+    {
+      type: 'selection',
+      fixed: 'left'
+    },
+    {
+      title: 'SN',
+      key: 'sn',
+      width: 200,
+    },
+    {
+      title: 'UUID',
+      key: 'uuid',
+    },
+    {
+      title: '操作',
+      key: 'operate',
+      width: 80,
+      fixed: 'right',
+      render(row) {
+        return h('div', { class: 'flex justify-center gap-4 p-x-4' }, [
+          h(NButton, {
+            type: "success",
+            strong: true,
+            size: 'small',
+            onClick: () => itemView(row.uuid, "")
+          }, {
+            default: () => '查看'
+          })
+        ])
+      }
+    }
+  ]
+}
+
+// 表格右键菜单
+function rowProps(row: XlogSiteItemInfo) {
+  return {
+    onContextmenu: (e: MouseEvent) => {
+      e.preventDefault()
+      showDropdown.value = false
+      nextTick().then(() => {
+        contextmenuUUID.value = row.uuid
+        showDropdown.value = true
+        dropdownOptions.x = e.clientX
+        dropdownOptions.y = e.clientY
+      })
+    },
+  }
+}
+
+const tableData = ref<XlogSiteItemInfo[]>([])
+const tableColumns = createColumns()
+const tablePagination = reactive<PaginationProps>({
+  page: 1,
+  pageSize: 10,
+  showSizePicker: true,
+  pageSizes: [10, 20, 50, 100],
+  itemCount: 0,
+  showQuickJumper: true,
+  showQuickJumpDropdown: true,
+  onUpdatePage: (page: number) => {
+    tablePagination.page = page
+    handleListQuery()
+  },
+  onUpdatePageSize: (pageSize: number) => {
+    tablePagination.pageSize = pageSize
+    tablePagination.page = 1
+    handleListQuery()
+  },
+})
+
+const itemView = (uuid: string, sn: string) => {
+  router.push({
+    path: `/xlog-terminal/${uuid}`,
+    query: { sn },
+  })
+}
+
+interface XlogSiteListFilter {
+  sn?: string
+  uuid?: string
+}
+
+const queryXlogSiteList = async (pageSize: number, pageNum: number, filter?: XlogSiteListFilter) => {
+  const siteList = await getManageList(pageSize, pageNum, filter)
+  tableData.value = siteList.data
+  tablePagination.itemCount = siteList.total
+}
+
+const handleListQuery = () => {
+  const pageSize = tablePagination.pageSize ?? 10
+  const page = tablePagination.page ?? 1
+
+  // 处理过滤条件
+  const filter = {
+    sn: form.sn,
+    uuid: form.uuid
+  }
+
+  queryXlogSiteList(pageSize, page, filter)
+}
+
+onMounted(() => {
+  handleListQuery()
+})
+
+</script>