Explorar el Código

feat: 新增站点分析界面,但未完成。。。

Jerry Liang hace 1 año
padre
commit
b871f2ffd3

+ 433 - 0
service/controllers/sitesAnalysisControllers.go

@@ -0,0 +1,433 @@
+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/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"`
+}
+
+// 从数据库获取站点分析列表
+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
+}
+
+// 从数据库获取站点监控配置列表
+func GetSiteMonitorConfig() ([]SitesMonitorConfig, 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
+}
+
+// 从数据库获取监控名列表
+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) {
+	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 {
+		return nil, err
+	}
+
+	var configList []SitesMonitorConfig
+	configList = append(configList, cfg)
+
+	return configList, nil
+}
+
+// 插入一条站点监控配置到数据库
+func InsertSiteMonitorConfig(config SitesMonitorConfig) 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} // 按用户名匹配
+	update := bson.M{
+		"$set": config, // 如果存在就更新所有字段
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 更新一条站点监控配置到数据库
+func UpdateSiteMonitorConfig(name string, config SitesMonitorConfig) 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": config, // 如果存在就更新所有字段
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// 删除一条站点监控配置
+func DeleteSiteMonitorConfig(name string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	filter := bson.M{"name": name} // 删除条件
+	collection := database.GetCollection("SiteMonitorConfig")
+	_, err := collection.DeleteOne(ctx, filter)
+	if err != nil {
+		return err
+	}
+
+	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
+}

+ 1 - 0
service/routes/routes.go

@@ -70,6 +70,7 @@ func Setup(app *fiber.App) {
 	// api.Get("/keyAccounts/sunrun/abnormalList", controllers.GetUsers)
 	// …
 	SitesMonitorSetup(api)
+	SitesAnalysisSetup(api)
 
 	app.Get("/swagger/*", swagger.HandlerDefault)
 }

+ 101 - 0
service/routes/sitesAnalysisRoutes.go

@@ -0,0 +1,101 @@
+/*
+ * Sites Analysis API List
+ * /sitesAnalysis/manage/analysisList - GET
+ * /sitesAnalysis/manage/config - POST 创建新配置
+ * /sitesAnalysis/manage/config/{id} - GET & POST & DELETE 获取、更新、删除配置
+ * /sitesAnalysis/data/{id} - GET 获取指定ID的分析数据
+ */
+package routes
+
+import (
+	"fwh-toolbox-server/controllers"
+	"strconv"
+
+	"github.com/gofiber/fiber/v2"
+)
+
+func SitesAnalysisSetup(api fiber.Router) {
+	// 获取站点分析管理名称表
+	api.Get("/sitesAnalysis/manage/analysisList", func(c *fiber.Ctx) error {
+		listPage, err := controllers.GetSiteAnalysisManageList()
+		if err != nil {
+			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
+		}
+		return JSONResponse(c, 0, "success", listPage)
+	})
+
+	// 创建一个分析配置
+	api.Post("/sitesAnalysis/manage/config", func(c *fiber.Ctx) error {
+		var body controllers.SiteAnalysisConfig
+		if err := c.BodyParser(&body); err != nil {
+			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+				"code":    400,
+				"message": "Invalid request body",
+			})
+		}
+
+		if err := controllers.InsertSiteAnalysisConfig(body); err != nil {
+			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
+		}
+		return JSONResponse(c, 0, "success", body)
+	})
+
+	// 获取单个分析配置
+	api.Get("/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")
+		}
+		config, err := controllers.GetOneSiteAnalysisConfig(id)
+		if err != nil {
+			return JSONResponseNoData(c, 500, "Database read failed. (MongoDb)")
+		}
+		return JSONResponse(c, 0, "success", config)
+	})
+
+	// 更新一个分析配置
+	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")
+		}
+		var body controllers.SiteAnalysisConfig
+		if err := c.BodyParser(&body); err != nil {
+			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
+				"code":    400,
+				"message": "Invalid request body",
+			})
+		}
+
+		if err := controllers.UpdateSiteAnalysisConfig(id, body); err != nil {
+			return JSONResponseNoData(c, 500, "Database write failed. (MongoDb)")
+		}
+		return JSONResponse(c, 0, "success", body)
+	})
+
+	// 删除一个监控
+	api.Delete("/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")
+		}
+		err = controllers.DeleteSiteAnalysisConfig(id)
+		if err != nil {
+			return JSONResponseNoData(c, 500, "Database delete failed. (MongoDb)")
+		}
+		return JSONResponseNoData(c, 0, "success")
+	})
+
+	// 获取指定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)
+	})
+}

+ 27 - 0
src/router/record.ts

@@ -40,6 +40,33 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
     component: 'sites-monitor/index',
     show: false,
   },
+  // {
+  //   path: 'sites-analysis-manage',
+  //   name: 'sitesAnalysisManage',
+  //   icon: 'i-carbon:data-analytics',
+  //   label: '站点分析管理',
+  //   meta: {
+  //     componentName: 'SitesAnalysisManage',
+  //     showTab: true,
+  //   },
+  //   component: 'sites-analysis/manage',
+  // },
+  // {
+  //   path: 'sites-analysis/:name?',
+  //   name: 'sitesAnalysis',
+  //   icon: 'i-carbon:data-analytics',
+  //   label: '站点分析',
+  //   meta: {
+  //     componentName: 'SitesAnalysis',
+  //     showTab: true,
+  //     enableMultiTab: true,
+  //     renderTabLabel({ id, name }) {
+  //       return `站点分析${name ? '-' + name : ''}`
+  //     },
+  //   },
+  //   component: 'sites-analysis/index',
+  //   show: false,
+  // },
   {
     path: 'data-show',
     name: 'dataShow',

+ 0 - 140
src/service/fetchClient copy.ts

@@ -1,140 +0,0 @@
-// fetchClient.ts
-import { TaskQueue } from './taskQueue'
-
-export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
-
-export interface FetchOptions extends RequestInit {
-  baseURL?: string
-  timeout?: number // 毫秒
-  params?: Record<string, any> // URL 查询参数
-  data?: any // body 数据
-  signal?: AbortSignal // 可选外部传入的 signal
-  queue?: TaskQueue // 指定请求队列
-  key?: string // 用于实现请求去重
-}
-
-export type Interceptor<T> = (data: T) => T | Promise<T>
-
-export class FetchClient {
-  private baseURL: string = ''
-  private defaultTimeout = 10000
-  private requestInterceptor?: Interceptor<RequestInit>
-  private responseInterceptor?: Interceptor<any>
-
-  constructor(baseURL = '', timeout = 10000) {
-    this.baseURL = baseURL
-    this.defaultTimeout = timeout
-  }
-
-  updateBaseUrl(baseURL: string) {
-    this.baseURL = baseURL
-  }
-
-  updateTimeout(timeout: number) {
-    this.defaultTimeout = timeout
-  }
-
-  setRequestInterceptor(interceptor: Interceptor<RequestInit>) {
-    this.requestInterceptor = interceptor
-  }
-
-  setResponseInterceptor(interceptor: Interceptor<any>) {
-    this.responseInterceptor = interceptor
-  }
-
-  async request<T = any>(
-    url: string,
-    method: HttpMethod = 'GET',
-    options: FetchOptions = {},
-    queue?: TaskQueue, // 指定请求队列
-    key?: string, // 用于实现请求去重
-  ): Promise<T> {
-    const {
-      baseURL = this.baseURL,
-      timeout = this.defaultTimeout,
-      params,
-      data,
-      headers = {},
-      signal,
-    } = options
-
-    // 构造完整 URL + 查询参数
-    let fullURL = baseURL + url
-    if (params) {
-      const query = new URLSearchParams(params).toString()
-      fullURL += fullURL.includes('?') ? '&' + query : '?' + query
-    }
-
-    // Abort 控制
-    const controller = new AbortController()
-    const timeoutId = setTimeout(() => controller.abort(), timeout)
-    const mergedSignal = signal ?? controller.signal
-
-    // 构造请求配置
-    const config: RequestInit = {
-      method,
-      headers: {
-        'Content-Type': 'application/json',
-        ...headers,
-      },
-      signal: mergedSignal,
-    }
-
-    if (data) {
-      config.body = JSON.stringify(data)
-    }
-
-    // 请求拦截器
-    const finalConfig = this.requestInterceptor ? await this.requestInterceptor(config) : config
-
-    try {
-      let promise: Promise<Response>
-      if (queue !== undefined) {
-        promise = queue.add(() => fetch(fullURL, finalConfig), key)
-      } else {
-        promise = fetch(fullURL, finalConfig)
-      }
-      
-      const res = await promise
-      clearTimeout(timeoutId)
-
-      if (!res.ok) {
-        const errorBody = await res.text()
-        throw new Error(`HTTP ${res.status}: ${errorBody}`)
-      }
-
-      const contentType = res.headers.get('Content-Type') || ''
-      let result: any
-      if (contentType.includes('application/json')) {
-        result = await res.json()
-      } else {
-        result = await res.text()
-      }
-
-      return this.responseInterceptor ? await this.responseInterceptor(result) : result
-    } catch (err: any) {
-      if (err.name === 'AbortError') {
-        throw new Error('Request timeout')
-      }
-      throw err
-    }
-  }
-
-  get<T = any>(url: string, options?: FetchOptions) {
-    return this.request<T>(url, 'GET', options, options?.queue, options?.key)
-  }
-
-  post<T = any>(url: string, data?: any, options?: FetchOptions) {
-    return this.request<T>(url, 'POST', { ...options, data }, options?.queue, options?.key)
-  }
-
-  put<T = any>(url: string, data?: any, options?: FetchOptions) {
-    return this.request<T>(url, 'PUT', { ...options, data }, options?.queue, options?.key)
-  }
-
-  delete<T = any>(url: string, options?: FetchOptions) {
-    return this.request<T>(url, 'DELETE', options, options?.queue, options?.key)
-  }
-}
-
-// export const http = new FetchClient('/api', 10000) // 默认 baseURL 和超时

+ 6 - 0
src/service/interface.ts

@@ -0,0 +1,6 @@
+export interface ResPage<T> {
+  data: T
+  pageSize: number
+  pageNum: number
+  total: number
+}

+ 69 - 0
src/service/sitesAnalysis/index.ts

@@ -0,0 +1,69 @@
+// API List
+// /sitesAnalysis/manage/analysisList - GET
+// /sitesAnalysis/manage/config - POST 创建新配置
+// /sitesAnalysis/manage/config/{id} - GET & POST & DELETE 获取、更新、删除配置
+// /sitesAnalysis/data/{id} - GET 获取指定ID的分析数据
+
+import http from '@/service/serverReq'
+import type {
+  AnalysisListFilter,
+  AnalysisItemInfoPage,
+  AnalysisConfig,
+  AnalysisResultData,
+} from './interface'
+
+const API_List = {
+  manageList: '/sitesAnalysis/manage/analysisList',
+  manageConfig: '/sitesAnalysis/manage/config',
+  manageConfigWithID: '/sitesAnalysis/manage/config/{id}',
+  analysisData: '/sitesAnalysis/data/{id}',
+}
+
+// GET 分析管理表
+export const getManageList = async (
+  pageSize: number,
+  pageNum: number,
+  filter?: AnalysisListFilter,
+): Promise<AnalysisItemInfoPage> => {
+  const res = await http.get<AnalysisItemInfoPage>(API_List.manageList, {
+    params: {
+      pageSize,
+      pageNum,
+      sn: filter ? filter.sn : undefined,
+      progress: filter ? filter.progress : undefined,
+    },
+  })
+  return res
+}
+
+// POST 创建配置
+export const createManageConfig = async (config: AnalysisConfig) => {
+  const url = API_List.manageConfig
+  const res = await http.post(url, config)
+}
+
+// GET 指定id的配置
+export const getManageConfig = async (id: number): Promise<AnalysisConfig> => {
+  const url = API_List.manageConfigWithID.replace('{id}', encodeURIComponent(id))
+  const res = await http.get<AnalysisConfig>(url)
+  return res
+}
+
+// POST 更新指定id配置
+export const updateManageConfig = async (id: number, config: AnalysisConfig) => {
+  const url = API_List.manageConfigWithID.replace('{id}', encodeURIComponent(id))
+  const res = await http.post(url, config)
+}
+
+// DELETE 删除指定id配置
+export const deleteManageConfig = async (id: number) => {
+  const url = API_List.manageConfigWithID.replace('{id}', encodeURIComponent(id))
+  const res = await http.delete(url)
+}
+
+// GET 获取分析数据
+export const getAnalysisData = async (id: number): Promise<AnalysisResultData> => {
+  const url = API_List.analysisData.replace('{id}', encodeURIComponent(id))
+  const res = await http.get<AnalysisResultData>(url)
+  return res
+}

+ 37 - 0
src/service/sitesAnalysis/interface.ts

@@ -0,0 +1,37 @@
+import type { ResPage } from '../interface'
+
+export interface AnalysisListFilter {
+  sn?: string
+  progress?: 'pending' | 'running' | 'error' | 'complete'
+}
+
+export interface AnalysisConfig {
+  id: number
+  sn: string
+  description: string
+  timePeriod: [string, string]
+
+  enableEventAndAlarm: boolean
+  enableCloudLog: boolean
+  enableSiteLog: boolean
+}
+
+export interface AnalysisItemInfo {
+  id: number
+  sn: string
+  description: string
+  item: string[]
+  progress: 'pending' | 'running' | 'error' | 'complete'
+}
+
+export interface AnalysisItemInfoPage extends ResPage<AnalysisItemInfo[]> {}
+
+export interface AnalysisData {
+  // TODO 待补充
+}
+
+export interface AnalysisResultData {
+  result: boolean
+  message: string
+  data?: AnalysisData
+}

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

@@ -15,15 +15,23 @@ declare module 'vue' {
     NAlert: typeof import('naive-ui')['NAlert']
     NButton: typeof import('naive-ui')['NButton']
     NCard: typeof import('naive-ui')['NCard']
+    NCol: typeof import('naive-ui')['NCol']
+    NCollapse: typeof import('naive-ui')['NCollapse']
     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition']
+    NDatePicker: typeof import('naive-ui')['NDatePicker']
     NDivider: typeof import('naive-ui')['NDivider']
     NDropdown: typeof import('naive-ui')['NDropdown']
+    NFormItem: typeof import('naive-ui')['NFormItem']
+    NInput: typeof import('naive-ui')['NInput']
     Noise: typeof import('./../components/Noise.vue')['default']
+    NPagination: typeof import('naive-ui')['NPagination']
     NPerformantEllipsis: typeof import('naive-ui')['NPerformantEllipsis']
     NPopover: typeof import('naive-ui')['NPopover']
+    NRow: typeof import('naive-ui')['NRow']
     NSelect: typeof import('naive-ui')['NSelect']
     NSpace: typeof import('naive-ui')['NSpace']
     NSpin: typeof import('naive-ui')['NSpin']
+    NStatistic: typeof import('naive-ui')['NStatistic']
     NTag: typeof import('naive-ui')['NTag']
     NThing: typeof import('naive-ui')['NThing']
     NTimeline: typeof import('naive-ui')['NTimeline']

+ 11 - 1
src/utils/extend/analysisNBT.ts

@@ -7,6 +7,8 @@ interface CsvRow {
   id: number
   sn: string
   rates: string
+  selfConsumptionID: number
+  soc: number
   description: string
   '9-5(Weekday)': string
   '9-6(Weekend)': string
@@ -94,12 +96,18 @@ const createNbtResult = async (siteList: string[]): Promise<CsvRow[]> => {
     let modeInfo = siteModeDataStore.siteModeInfoGet(site)
     let modeName: string | undefined
     modeInfo ??= await siteModeDataStore.querySiteModeInfo(site)
+    let selfConsumptionID: number = 0
+    let soc: number = 20
     if (modeInfo !== undefined) {
       const currentId = modeInfo.currendId
       modeInfo.list.forEach((item) => {
         if (item.id === currentId) {
           modeName = `${item.name}`
         }
+        if (item.name === '自发自用') {
+          selfConsumptionID = item.id
+          soc = item.soc
+        }
       })
     }
 
@@ -109,6 +117,8 @@ const createNbtResult = async (siteList: string[]): Promise<CsvRow[]> => {
       id: index + 1,
       sn: site,
       rates: modeName ?? 'unknown',
+      selfConsumptionID,
+      soc,
       description: res.description,
       '9-5(Weekday)': res.analysis5,
       '9-6(Weekend)': res.analysis6,
@@ -122,7 +132,7 @@ export const DownloadNbtCsv = async (siteList: string[]) => {
   downloadCSV(
     '9月份NBT放电结果.csv',
     toCSV(data, {
-      headers: ['id', 'sn', 'rates', 'description', '9-5(Weekday)', '9-6(Weekend)'], // 指定列顺序(可选)
+      headers: ['id', 'sn', 'rates', 'selfConsumptionID', 'soc'], // 指定列顺序(可选)
       includeHeader: true,
     }),
   )

+ 115 - 0
src/views/sites-analysis/ModalData.vue

@@ -0,0 +1,115 @@
+<script setup lang="ts">
+import { computed, onMounted, useTemplateRef } from 'vue'
+import { NForm, NFormItem, NInput, NButton, NCollapseItem, NSwitch } from 'naive-ui'
+import type { FormRules } from 'naive-ui'
+
+import { useResettableReactive } from '@/composable/useResettable'
+
+import type { AnalysisConfig } from '@/service/sitesAnalysis/interface'
+
+interface AnalysisConfigForm extends AnalysisConfig { }
+
+const { id } = defineProps<{
+  id?: number
+}>()
+
+const formRef = useTemplateRef<InstanceType<typeof NForm>>('formRef')
+
+const emits = defineEmits<{
+  (e: 'submit'): void
+  (e: 'cancel'): void
+}>()
+
+const [form, setUserForm, reset, updateInitialState] = useResettableReactive<AnalysisConfigForm>({
+  id: 0,
+  sn: '',
+  description: '',
+  timePeriod: ["", ""],
+
+  enableEventAndAlarm: false,
+  enableCloudLog: false,
+  enableSiteLog: false
+})
+
+const handleAllowInput = (value: string) => {
+  // 如果输入不匹配 URL 允许的字符,去掉不合法字符
+  const res = /\W/.test(value)
+  return !res
+};
+
+const rules: FormRules = {
+  sn: { required: true, message: '请输入站点SN', trigger: ['input'] },
+  timePeriod: { required: true, message: '请选择时间范围', trigger: ['input'] },
+}
+
+const handleSubmitClick = () => {
+  formRef.value?.validate().then((valid) => {
+    if (valid) {
+      emits('submit')
+    }
+  })
+}
+
+onMounted(() => {
+  if (id !== undefined) {
+    // 请求表单数据
+
+    // 填入表单数据
+    // setUserForm({ ...data })
+    // 设置表单数据重置状态
+    // updateInitialState({
+    //   name: data.name ?? '',
+    //   description: data.description ?? '',
+    //   siteList: data.siteList ?? ''
+    // })
+  }
+})
+</script>
+<template>
+  <div class="p-2">
+    <NForm :model="form" :rules="rules" label-placement="left" style="width: 100%" ref="formRef" label-width="88px"
+      size="small">
+      <NFormItem label="SN" path="sn">
+        <NInput v-model:value="form.sn" :allow-input="handleAllowInput" />
+      </NFormItem>
+      <NFormItem label="描述" path="description">
+        <NInput type="textarea" v-model:value="form.description" />
+      </NFormItem>
+      <NFormItem label="时间" path="timePeriod">
+        <NDatePicker v-model:formatted-value="form.timePeriod" type="datetimerange" clearable />
+      </NFormItem>
+
+      <NCollapse :trigger-areas="['arrow']">
+        <NCollapseItem :disable="!form.enableEventAndAlarm">
+          <template #header-extra>
+            <NSwitch v-model:value="form.enableEventAndAlarm" size="small" />
+          </template>
+          <div>事件&告警</div>
+        </NCollapseItem>
+        <NCollapseItem :disable="!form.enableCloudLog">
+          <template #header-extra>
+            <NSwitch v-model:value="form.enableCloudLog" size="small" />
+          </template>
+          <div>云通讯日志</div>
+        </NCollapseItem>
+        <NCollapseItem :disable="!form.enableSiteLog">
+          <template #header-extra>
+            <NSwitch v-model:value="form.enableSiteLog" size="small" />
+          </template>
+          <div>设备日志</div>
+        </NCollapseItem>
+      </NCollapse>
+    </NForm>
+    <div class="flex justify-end gap-x-4">
+      <NButton secondary @click="emits('cancel')">
+        取消
+      </NButton>
+      <NButton secondary type="warning" @click="reset">
+        重置
+      </NButton>
+      <NButton type="success" @click="handleSubmitClick">
+        确定
+      </NButton>
+    </div>
+  </div>
+</template>

+ 0 - 0
src/views/sites-analysis/interface.ts


+ 349 - 0
src/views/sites-analysis/manage.vue

@@ -0,0 +1,349 @@
+<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="进度" path="progress">
+            <NSelect v-model:value="form.progress" :options="progressOptions" style="width: 120px" clearable />
+          </NFormItem>
+        </NForm>
+        <div class="flex gap-2">
+          <NButton type="success" @click="createOrEditAnalysis()">
+            <template #icon>
+              <span class="i-ph:plus-circle" />
+            </template>
+            新增数据
+          </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 { NButton, NDataTable, NForm, NNumberAnimation, NEllipsis } from 'naive-ui'
+import type { DataTableColumns, PaginationProps, DropdownProps } from 'naive-ui'
+import ModalData from './ModalData.vue'
+import type { AnalysisConfig, AnalysisItemInfo } from '@/service/sitesAnalysis/interface'
+
+import { useDataTable } from '@/composable/comp/useDataTable'
+import { useResettableReactive } from '@/composable/useResettable'
+
+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 contextmenuID = ref<number | undefined>(undefined)
+
+interface AnalysisFilterForm extends Partial<AnalysisItemInfo> { }
+
+const [form, , resetForm] = useResettableReactive<AnalysisFilterForm>({
+  sn: '',
+  progress: undefined,
+})
+
+const message = useMessage()
+const modal = useModal()
+
+const progressOptions = [
+  { label: 'pending', value: 'pending' },
+  { label: 'running', value: 'running' },
+  { label: 'error', value: 'error' },
+  { label: 'complete', value: 'complete' },
+]
+
+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',
+    },
+    {
+      label: () => <span class='text-blue-500'> 编辑 </span>,
+      key: 'edit',
+    },
+    {
+      label: () => <span class='text-red-500'> 删除 </span>,
+      key: 'delete',
+    },
+  ],
+  onClickoutside: () => {
+    showDropdown.value = false
+  },
+  onSelect: (key: string) => {
+    showDropdown.value = false
+    if (contextmenuID.value === undefined) return
+    if (key === 'view') {
+      itemView(contextmenuID.value)
+    } else if (key === 'edit') {
+      itemEdit(contextmenuID.value)
+    } else if (key === 'delete') {
+      itemDelete(contextmenuID.value)
+    }
+  },
+})
+
+const createOrEditAnalysis = (id?: number) => {
+  const title = id === undefined ? '新增监控' : '编辑编辑'
+  const isNew = id === undefined
+
+  const handleSubmitClick = () => {
+    // 重新查询数据
+    handleListQuery()
+
+    if (isNew) {
+      message.success('创建成功')
+    } else {
+      message.success('更新成功')
+    }
+
+    m.destroy()
+  }
+
+  const handleCancelClick = () => {
+    m.destroy()
+  }
+
+  const m = modal.create({
+    title,
+    preset: 'card',
+    draggable: true,
+    style: {
+      width: '500px',
+    },
+    content: () => (
+      <ModalData
+        id={id}
+        onSubmit={handleSubmitClick}
+        onCancel={handleCancelClick}
+      />
+    ),
+  })
+}
+
+const tableRowKey = (rowData: AnalysisItemInfo) => {
+  return rowData.sn
+}
+
+function createColumns(): DataTableColumns<AnalysisItemInfo> {
+  return [
+    {
+      type: 'selection',
+      fixed: 'left'
+    },
+    {
+      title: 'SN',
+      key: 'sn',
+      width: 200,
+      fixed: 'left'
+    },
+    {
+      title: '描述',
+      key: 'description'
+    },
+    {
+      title: '项目',
+      key: 'item',
+      // render(row) {
+      //   return h(NMarquee, { "auto-fill": true }, {
+      //     default: () => h('div', { class: 'flex gap-4 p-x-4' }, {
+      //       default: () => row.item.map((itemName) =>
+      //         h(NTag, { type: "info" }, { default: () => itemName })
+      //       )
+      //     })
+      //   })
+      // }
+      render(row) {
+        return h(NEllipsis, {}, {
+          default: () => row.item.join(', ')
+        })
+      }
+    },
+    {
+      title: '进度',
+      key: 'progress',
+      width: 100,
+      fixed: 'right',
+    },
+    {
+      title: '操作',
+      key: 'operate',
+      width: 200,
+      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.id)
+          }, {
+            default: () => '查看'
+          }),
+          h(NButton, {
+            type: "info",
+            strong: true,
+            size: 'small',
+            onClick: () => itemEdit(row.id)
+          }, {
+            default: () => '编辑'
+          }),
+          h(NButton, {
+            type: "error",
+            strong: true,
+            size: 'small',
+            onClick: () => itemDelete(row.id)
+          }, {
+            default: () => '删除'
+          })
+        ])
+      }
+    }
+  ]
+}
+
+// 表格右键菜单
+function rowProps(row: AnalysisItemInfo) {
+  return {
+    onContextmenu: (e: MouseEvent) => {
+      e.preventDefault()
+      showDropdown.value = false
+      nextTick().then(() => {
+        contextmenuID.value = row.id
+        showDropdown.value = true
+        dropdownOptions.x = e.clientX
+        dropdownOptions.y = e.clientY
+      })
+    },
+  }
+}
+
+const tableData = ref<AnalysisItemInfo[]>(Array.from({ length: 46 }).map((_, index) => ({
+  id: index,
+  sn: `Edward King ${index}`,
+  description: "",
+  item: ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'],
+  progress: 'pending',
+})))
+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 = (id: number) => {
+
+}
+
+const itemEdit = (id: number) => {
+  createOrEditAnalysis(id)
+}
+
+const itemDelete = (id: number) => {
+
+}
+
+interface AnalysisListFilter {
+  sn?: string
+  progress?: 'pending' | 'running' | 'error' | 'complete'
+}
+
+const querySitesAnalysisList = (pageSize: number, pageNum: number, filter?: AnalysisListFilter) => {
+
+}
+
+const updateSiteAnalysisConfig = (data: AnalysisConfig) => {
+
+}
+
+const deleteSiteFromList = (id: number) => {
+
+}
+
+const handleListQuery = () => {
+  const pageSize = tablePagination.pageSize ?? 10
+  const page = tablePagination.page ?? 1
+
+  // 处理过滤条件
+  const filter = {
+    sn: form.sn,
+    progress: form.progress
+  }
+
+  querySitesAnalysisList(pageSize, page, filter)
+}
+
+</script>