Jelajahi Sumber

Merge branch 'sg-20260127' of FWH/FranklinWH-Toolbox into master

合并sg-20260127的开发内容
调试了文件下载功能,并添加的文件预览部分功能
jerry 8 bulan lalu
induk
melakukan
cf01677a61
48 mengubah file dengan 3011 tambahan dan 338 penghapusan
  1. 3 1
      package.json
  2. 237 228
      pnpm-lock.yaml
  3. 26 0
      service/controllers/batchDownloadControllers.go
  4. 3 0
      service/domain/batchDownloadDomain.go
  5. 26 0
      service/repositories/mongo/batchDownloadRepo.go
  6. 1 0
      service/repositories/repositories.go
  7. 4 0
      service/routes/batchDownloadRoutes.go
  8. 10 0
      service/services/batchDownloadServices.go
  9. 35 0
      src/assets/data/alarmCode.ts
  10. 2 2
      src/composable/comp/useFullCard.ts
  11. 17 0
      src/router/record.ts
  12. 37 1
      src/service/batchDownload/index.ts
  13. 1 0
      src/service/batchDownload/interface.ts
  14. 99 0
      src/service/blobCache.ts
  15. 75 5
      src/service/fetchClient.ts
  16. 1 1
      src/service/serverReq.ts
  17. 70 3
      src/stores/user.ts
  18. 1 0
      src/types/components.d.ts
  19. 64 0
      src/utils/monaco/components/LogMonacoEditor.vue
  20. 21 0
      src/utils/monaco/editor.ts
  21. 1 0
      src/utils/monaco/env.ts
  22. 60 0
      src/utils/monaco/index.ts
  23. 8 0
      src/utils/monaco/languages/index.ts
  24. 4 0
      src/utils/monaco/languages/log/definitions.ts
  25. 128 0
      src/utils/monaco/languages/log/lspLanguageFeatures.ts
  26. 35 0
      src/utils/monaco/languages/log/monarch.ts
  27. 563 0
      src/utils/monaco/languages/log/provider.ts
  28. 52 0
      src/utils/monaco/languages/log/register.ts
  29. 23 0
      src/utils/monaco/languages/log/worker/log.worker.ts
  30. 20 0
      src/utils/monaco/languages/log/worker/logMode.ts
  31. 63 0
      src/utils/monaco/languages/log/worker/logWorker.ts
  32. 84 0
      src/utils/monaco/languages/log/worker/workerManager.ts
  33. 25 0
      src/utils/monaco/themes/custom.ts
  34. 165 0
      src/utils/monaco/themes/index.ts
  35. 260 0
      src/utils/monaco/themes/internalThemes.ts
  36. 4 0
      src/utils/monaco/workers/editor.worker.d.ts
  37. 7 0
      src/utils/monaco/workers/editor.worker.start.d.ts
  38. 40 0
      src/utils/monaco/workers/setup.ts
  39. 226 0
      src/views/batch-download/component/fullscreenPreview.vue
  40. 157 26
      src/views/batch-download/component/viewPathDialog.vue
  41. 19 3
      src/views/batch-download/index.vue
  42. 19 4
      src/views/batch-download/page/viewLog.vue
  43. 3 0
      src/views/local-utils/code-diagnosis/func/codeAnalysis.ts
  44. 54 0
      src/views/local-utils/code-diagnosis/index.vue
  45. 6 10
      src/views/local-utils/log-357-parser/func/parseData.ts
  46. 29 50
      src/views/local-utils/log-357-parser/func/parseScheduling.ts
  47. 218 0
      src/views/test/index.vue
  48. 5 4
      vite.config.ts

+ 3 - 1
package.json

@@ -45,6 +45,8 @@
     "chroma-js": "^3.1.2",
     "dayjs": "^1.11.18",
     "es-toolkit": "^1.39.8",
+    "idb": "^8.0.3",
+    "jwt-decode": "^4.0.0",
     "monaco-editor": "^0.55.1",
     "monaco-editor-vue3": "^1.0.5",
     "naive-ui": "^2.42.0",
@@ -86,7 +88,7 @@
     "unocss": "^66.3.3",
     "unplugin-auto-import": "^20.0.0",
     "unplugin-vue-components": "^28.8.0",
-    "vite": "npm:rolldown-vite@^7.0.9",
+    "vite": "npm:rolldown-vite@^7.3.1",
     "vite-plugin-compression": "^0.5.1",
     "vite-plugin-html": "^3.2.2",
     "vite-plugin-vue-devtools": "^7.7.7",

File diff ditekan karena terlalu besar
+ 237 - 228
pnpm-lock.yaml


+ 26 - 0
service/controllers/batchDownloadControllers.go

@@ -3,6 +3,7 @@
  * /batchDownload/manage/list - GET
  * /batchDownload/manage/config - POST 创建新配置
  * /batchDownload/manage/description/{id} - POST 更新任务说明
+ * /batchDownload/manage/owner/{id} - POST 设置owner
  * /batchDownload/manage/config/{id} - DELETE 删除配置
  * /batchDownload/data/{id} - GET 获取指定ID的数据包下载地址
  * /batchDownload/manage/path - GET 获取指定ID的文件目录结构
@@ -96,6 +97,31 @@ func UpdateBatchDownloadDescription(c *fiber.Ctx) error {
 	return Success(c, body)
 }
 
+// 设置一个批量数据的owner
+// /batchDownload/manage/owner/:id
+func UpdateBatchDownloadOwner(c *fiber.Ctx) error {
+	id := c.Params("id")
+	var body struct {
+		Owner []string `json:"owner"`
+	}
+	if err := c.BodyParser(&body); err != nil {
+		zap.L().Warn("Invalid request body", zap.String("err", err.Error()))
+		return Fail(c, "Invalid request body")
+	}
+
+	objID, err := primitive.ObjectIDFromHex(id)
+	if err != nil {
+		zap.L().Warn("Invalid request para :id", zap.String("err", err.Error()))
+		return Fail(c, "Invalid request para :id")
+	}
+
+	if err := services.UpdateBatchDownloadOwner(objID, body.Owner); err != nil {
+		zap.L().Warn("Database write failed. (MongoDb)", zap.String("err", err.Error()))
+		return Error(c, "Database write failed. (MongoDb)")
+	}
+	return Success(c, body)
+}
+
 // 删除一个批量数据下载数据
 // /batchDownload/manage/config/:id
 func DeleteBatchDownload(c *fiber.Ctx) error {

+ 3 - 0
service/domain/batchDownloadDomain.go

@@ -12,6 +12,7 @@ type BatchDownloadInfo struct {
 	Options     BatchDownloadOptions `bson:"options" json:"options"`
 	Progress    Progress             `bson:"progress" json:"progress"`
 	FailReasons []string             `bson:"failReasons" json:"failReasons"`
+	Owner       *[]string            `bson:"owner,omitempty" json:"owner,omitempty"`
 }
 
 type BatchDownloadOptions struct {
@@ -30,6 +31,7 @@ type BatchDownloadConfigWrite struct {
 	Description string               `bson:"description" json:"description"`
 	TimePeriod  [2]string            `bson:"timePeriod" json:"timePeriod"`
 	Options     BatchDownloadOptions `bson:"options"`
+	Owner       *[]string            `bson:"owner,omitempty" json:"owner,omitempty"`
 }
 
 type BatchDownloadConfig struct {
@@ -41,6 +43,7 @@ type BatchDownloadConfig struct {
 	Progress    Progress                     `bson:"progress" json:"progress"`
 	Data        map[string]BatchDownloadData `bson:"data" json:"data"` // map-key为SN
 	FailReasons []string                     `bson:"failReasons" json:"failReasons"`
+	Owner       *[]string                    `bson:"owner,omitempty" json:"owner,omitempty"`
 }
 
 type TaskBatchDownload struct {

+ 26 - 0
service/repositories/mongo/batchDownloadRepo.go

@@ -141,6 +141,32 @@ func UpdateBatchDownloadDescription(id primitive.ObjectID, desc string) error {
 	return nil
 }
 
+// 更新一条批量下载数据owner到数据库
+func UpdateBatchDownloadOwner(id primitive.ObjectID, owner []string) error {
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+
+	collection := database.GetCollection("BatchDownloadConfig")
+
+	// _, err := collection.InsertOne(ctx, config)
+	filter := bson.M{"_id": id} // 按ID匹配
+	update := bson.M{
+		// 如果存在就更新全部字段
+		"$set": bson.M{
+			"owner": owner,
+		},
+	}
+
+	opts := options.Update().SetUpsert(true)
+
+	_, err := collection.UpdateOne(ctx, filter, update, opts)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // 更新一条批量下载结果到数据库
 func UpdateBatchDownloadResult(id primitive.ObjectID, result domain.Progress, reasons []string) error {
 	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

+ 1 - 0
service/repositories/repositories.go

@@ -101,6 +101,7 @@ var GetBatchDownloadManageList = mongo.GetBatchDownloadManageList
 var GetBatchDownloadConfig = mongo.GetBatchDownloadConfig
 var InsertBatchDownloadConfig = mongo.InsertBatchDownloadConfig
 var UpdateBatchDownloadDescription = mongo.UpdateBatchDownloadDescription
+var UpdateBatchDownloadOwner = mongo.UpdateBatchDownloadOwner
 var UpdateBatchDownloadResult = mongo.UpdateBatchDownloadResult
 var DeleteBatchDownloadConfig = mongo.DeleteBatchDownloadConfig
 var InsertBatchDownloadData = mongo.InsertBatchDownloadData

+ 4 - 0
service/routes/batchDownloadRoutes.go

@@ -3,6 +3,7 @@
  * /batchDownload/manage/list - GET
  * /batchDownload/manage/config - POST 创建新配置
  * /batchDownload/manage/description/{id} - POST 更新任务说明
+ * /batchDownload/manage/owner/{id} - POST 设置owner
  * /batchDownload/manage/config/{id} - DELETE 删除配置
  * /batchDownload/data/{id} - GET 获取指定ID的数据包下载地址
  * /batchDownload/manage/path - GET 获取指定ID的文件目录结构
@@ -27,6 +28,9 @@ func BatchDownloadSetup(api fiber.Router) {
 	// 更新一个批量数据下载说明
 	api.Post("/batchDownload/manage/description/:id", controllers.UpdateBatchDownloadDescription)
 
+	// 设置一个批量数据的owner
+	api.Post("/batchDownload/manage/owner/:id", controllers.UpdateBatchDownloadOwner)
+
 	// 删除一个批量数据下载数据
 	api.Delete("/batchDownload/manage/config/:id", controllers.DeleteBatchDownload)
 

+ 10 - 0
service/services/batchDownloadServices.go

@@ -51,6 +51,16 @@ func UpdateBatchDownloadDescription(id primitive.ObjectID, desc string) error {
 	return nil
 }
 
+// 设置一个批量数据的owner
+func UpdateBatchDownloadOwner(id primitive.ObjectID, owner []string) error {
+	err := repositories.UpdateBatchDownloadOwner(id, owner)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
 // 删除一条批量数据下载配置
 func DeleteBatchDownloadConfig(id primitive.ObjectID) error {
 	err := repositories.DeleteBatchDownloadConfig(id)

+ 35 - 0
src/assets/data/alarmCode.ts

@@ -0,0 +1,35 @@
+const alarmCode: Record<string,{name:string}> = {
+    '1001': {
+      name: "电网继电器常闭故障"
+    },
+    '1002': {
+      name: "电网继电器常开故障"
+    },
+    '1003': {
+      name: "油机继电器常闭故障"
+    },
+    '1004': {
+      name: "油机继电器常开故障"
+    },
+    '1005': {
+      name: "EPO紧急停机"
+    },
+    '1006': {
+      name: "油机故障"
+    },
+    '1007': {
+      name: "备电故障"
+    },
+    '1008': {
+      name: "FHP总功率故障"
+    },
+    '1009': {
+      name: "DSP与ARM双向通信故障"
+    },
+    '100A': {
+      name: "光伏电表通信故障"
+    },
+    '100B': {
+      name: ""
+    }
+} as const

+ 2 - 2
src/composable/comp/useFullCard.ts

@@ -57,7 +57,7 @@ export function useFullCard<T extends ComponentPublicInstance>(
     return null
   }
 
-  function initTableResizeObserver() {
+  function initCardResizeObserver() {
     if (!target.value) {
       height.value = undefined
       return
@@ -91,7 +91,7 @@ export function useFullCard<T extends ComponentPublicInstance>(
   }
 
   onMounted(() => {
-    initTableResizeObserver()
+    initCardResizeObserver()
   })
 
   onBeforeUnmount(() => {

+ 17 - 0
src/router/record.ts

@@ -340,6 +340,11 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
         name: 'codeDiagnosis',
         icon: 'i-ph:pipe-wrench',
         label: '故障诊断',
+        meta: {
+          componentName: 'codeDiagnosis',
+          showTab: true,
+        },
+        component: 'local-utils/code-diagnosis/index',
       },
       {
         path: 'log-357-parser',
@@ -424,4 +429,16 @@ export const routeRecordRaw: MergeMenuMixedOptions[] = [
       enableMultiTab: true,
     },
   },
+  {
+    path: 'test',
+    key: 'test',
+    name: 'test',
+    icon: 'i-ph:test-tube-duotone',
+    label: '测试页',
+    component: 'test/index',
+    meta: {
+      showTab: true,
+      enableMultiTab: true,
+    },
+  },
 ]

+ 37 - 1
src/service/batchDownload/index.ts

@@ -2,6 +2,7 @@
 // /batchDownload/manage/list - GET
 // /batchDownload/manage/config - POST 创建新配置
 // /batchDownload/manage/description/{id} - POST 更新任务说明
+// /batchDownload/manage/owner/{id} - POST 设置数据的owner
 // /batchDownload/manage/config/{id} - DELETE 删除配置
 // /batchDownload/data/{id} - GET 获取指定ID的数据包下载地址
 // /batchDownload/manage/path - GET 获取指定ID的文件目录结构
@@ -9,6 +10,7 @@
 // /batchDownload/manage/fileDownload - GET 下载指定文件
 
 import http from '@/service/serverReq'
+import { blobCache } from '@/service/blobCache'
 import type {
   BatchDownloadFilter,
   BatchDownloadItemPage,
@@ -21,6 +23,7 @@ const API_List = {
   batchDownloadList: '/batchDownload/manage/list',
   createBatchDownload: '/batchDownload/manage/config',
   updateBatchDownloadDescription: '/batchDownload/manage/description/{id}',
+  updateBatchDownloadOwner: '/batchDownload/manage/owner/{id}',
   deleteBatchDownload: '/batchDownload/manage/config/{id}',
   getBatchDownloadPackage: '/batchDownload/data/{id}',
   getBatchDownloadPath: '/batchDownload/manage/path',
@@ -57,6 +60,12 @@ export const updateBatchDownloadDescription = async (id: string, description: st
   const res = await http.post(url, description)
 }
 
+// POST 设置数据的owner
+export const updateBatchDownloadOwner = async (id: string, owner: string[]) => {
+  const url = API_List.updateBatchDownloadOwner.replace('{id}', encodeURIComponent(id))
+  const res = await http.post(url, { owner })
+}
+
 // DELETE 删除指定id配置
 export const deleteBatchDownload = async (id: string) => {
   const url = API_List.deleteBatchDownload.replace('{id}', encodeURIComponent(id))
@@ -90,7 +99,7 @@ export const getBatchDownloadFileTree = async (file: string): Promise<BatchDownl
 }
 
 // GET 下载指定文件
-export const getBatchDownloadFilePath = async (filePath: string, innerPath?: string) => {
+export const downloadBatchDownloadFile = async (filePath: string, innerPath?: string) => {
   await http.download(API_List.getBatchDownloadFileDownload, undefined, {
     params: {
       file: filePath,
@@ -98,3 +107,30 @@ export const getBatchDownloadFilePath = async (filePath: string, innerPath?: str
     },
   })
 }
+
+// GET 流式读取指定文件
+export const getBatchDownloadFile = async (filePath: string, innerPath?: string) => {
+  const blobKey = innerPath
+    ? `BatchDownload:${filePath}&&${innerPath}`
+    : `BatchDownload:${filePath}`
+  const cacheBlob = await blobCache.get(blobKey)
+  if (cacheBlob === undefined) {
+    const fileBlob = await http.downloadToBlob(API_List.getBatchDownloadFileDownload, {
+      params: {
+        file: filePath,
+        inner: innerPath ?? '',
+      },
+    })
+    if (fileBlob !== undefined) {
+      await blobCache.putCache(
+        blobKey,
+        fileBlob,
+        1000 * 60 * 60, // 缓存 1 小时
+      )
+    }
+
+    return fileBlob
+  }
+
+  return cacheBlob
+}

+ 1 - 0
src/service/batchDownload/interface.ts

@@ -34,6 +34,7 @@ export interface BatchDownloadInfo {
   options: BatchDownloadOptions
   progress: string
   failReasons: string[]
+  owner?: string // 未设置则只有管理员能查看
 }
 
 export interface BatchDownloadStructNode {

+ 99 - 0
src/service/blobCache.ts

@@ -0,0 +1,99 @@
+import { openDB, type DBSchema, type IDBPDatabase } from 'idb'
+
+/**
+ * 定义数据库结构
+ */
+interface FileCacheDB extends DBSchema {
+  files: {
+    key: string
+    value: {
+      blob: Blob
+      type: string
+      size: number
+      createdAt: number
+      expiresAt?: number
+    }
+  }
+}
+
+class BlobCache {
+  private readonly dbPromise: Promise<IDBPDatabase<FileCacheDB>>
+
+  constructor() {
+    this.dbPromise = openDB<FileCacheDB>('blob-file-cache', 1, {
+      upgrade(db) {
+        if (!db.objectStoreNames.contains('files')) {
+          db.createObjectStore('files')
+        }
+      },
+    })
+  }
+
+  /**
+   * 放入缓存文件
+   */
+  async putCache(
+    key: string,
+    blob: Blob,
+    ttl?: number, // 毫秒
+  ): Promise<void> {
+    const db = await this.dbPromise
+
+    await db.put(
+      'files',
+      {
+        blob,
+        type: blob.type,
+        size: blob.size,
+        createdAt: Date.now(),
+        expiresAt: ttl ? Date.now() + ttl : undefined,
+      },
+      key,
+    )
+  }
+
+  /**
+   * 获取缓存
+   */
+  async get(key: string): Promise<Blob | undefined> {
+    const db = await this.dbPromise
+    const record = await db.get('files', key)
+
+    if (!record) return undefined
+
+    // 过期判断
+    if (record.expiresAt && record.expiresAt < Date.now()) {
+      await db.delete('files', key)
+      return undefined
+    }
+
+    return record.blob
+  }
+
+  /**
+   * 删除单个
+   */
+  async delete(key: string): Promise<void> {
+    const db = await this.dbPromise
+    await db.delete('files', key)
+  }
+
+  /**
+   * 清空所有
+   */
+  async clear(): Promise<void> {
+    const db = await this.dbPromise
+    await db.clear('files')
+  }
+
+  /**
+   * 获取缓存总大小
+   */
+  async getTotalSize(): Promise<number> {
+    const db = await this.dbPromise
+    const all = await db.getAll('files')
+    return all.reduce((sum, item) => sum + item.size, 0)
+  }
+}
+
+export const blobCache = new BlobCache()

+ 75 - 5
src/service/fetchClient.ts

@@ -241,8 +241,6 @@ export class FetchClient {
     let downloadName = filename
     if (!downloadName) {
       const disposition = res.headers.get('Content-Disposition') || ''
-      // const match = RegExp(/filename="?([^"]+)"?/).exec(disposition)
-      // downloadName = filename ?? match?.[1] ?? 'download'
       downloadName = this.parseFilenameFromContentDisposition(disposition)
     }
 
@@ -259,9 +257,7 @@ export class FetchClient {
       try {
         controller.abort() // 中断 fetch readable
       } catch {}
-      try {
-        writable.abort() // 中断写入
-      } catch {}
+      writable.abort().catch(() => {}) // 中断写入
 
       window.removeEventListener('beforeunload', stopDownload)
     }
@@ -291,6 +287,80 @@ export class FetchClient {
       window.removeEventListener('beforeunload', stopDownload)
     }
   }
+
+  async downloadToBlob(url: string, options?: FetchOptions): Promise<Blob | undefined> {
+    const { baseURL = this.baseURL, params, headers = {}, signal } = options || {}
+
+    // 构造完整 URL + 查询参数
+    let fullURL = baseURL + url
+    if (params) {
+      const query = new URLSearchParams(params).toString()
+      fullURL += fullURL.includes('?') ? '&' + query : '?' + query
+    }
+
+    // AbortController
+    const controller = new AbortController()
+    const mergedSignal = signal ?? controller.signal
+
+    // fetch config
+    const config: RequestInit = {
+      method: 'GET',
+      headers: {
+        ...headers,
+      },
+      signal: mergedSignal,
+    }
+
+    // 请求拦截器
+    const finalConfig = this.requestInterceptor ? await this.requestInterceptor(config) : config
+
+    // 发起请求
+    const res = await fetch(fullURL, finalConfig)
+    if (!res.ok) {
+      throw new Error(`Download failed with status ${res.status}`)
+    }
+
+    if (!res.body) throw new Error('No response body')
+
+    // 页面关闭 → 停止下载
+    const stopDownload = () => {
+      console.log('Page is closing → abort download')
+
+      try {
+        controller.abort() // 中断 fetch readable
+      } catch {}
+
+      window.removeEventListener('beforeunload', stopDownload)
+    }
+
+    window.addEventListener('beforeunload', stopDownload)
+
+    let blob: Blob | undefined
+    try {
+      const reader = res.body.getReader()
+      const chunks: Uint8Array[] = []
+
+      while (true) {
+        const { value, done } = await reader.read()
+        if (done) break
+
+        // 支持 abort
+        if (mergedSignal.aborted) throw new Error('Aborted')
+
+        if (value !== undefined) chunks.push(value)
+      }
+
+      // 合并所有块生成 Blob
+      const contentType = res.headers.get('Content-Type') ?? 'application/octet-stream'
+      blob = new Blob(chunks, { type: contentType })
+    } catch (err) {
+      console.warn('Download aborted:', err)
+    } finally {
+      window.removeEventListener('beforeunload', stopDownload)
+    }
+
+    return blob
+  }
 }
 
 // export const http = new FetchClient('/api', 10000) // 默认 baseURL 和超时

+ 1 - 1
src/service/serverReq.ts

@@ -64,7 +64,7 @@ async function handleTokenExpired(
 
 function handleRefreshFailed() {
   const userStore = useUserStore()
-  userStore.setToken(null)
+  userStore.setToken(undefined)
   pendingQueue = []
   isRefreshing = false
 

+ 70 - 3
src/stores/user.ts

@@ -9,6 +9,7 @@ import { routeRecordRaw } from '@/router/record'
 import type { MergeMenuMixedOptions } from '@/router/helper'
 import type { MenuProps } from 'naive-ui'
 import type { RouteRecordRaw } from 'vue-router'
+import { jwtDecode } from 'jwt-decode'
 
 interface User {
   avatar?: string
@@ -24,21 +25,87 @@ const DEFAULT_USER: User = {
   role: 'admin',
 }
 
+interface JwtPayload {
+  iss: string
+  id: string
+  sub: string
+  aud: string[] | string
+  name: string
+  displayName: string
+  nbf: number
+  iat: number
+  jti: string
+  exp: number // 过期时间
+  isAdmin: boolean
+  isDeleted: boolean
+  isForbidden: boolean
+  permissions: JwtPayloadPermission[]
+  roles: JwtPayloadRole[]
+}
+
+interface JwtPayloadPermission {
+  name: string
+  displayName: string
+  description: string
+  effect: string
+  isEnabled: boolean
+  state: string
+}
+
+interface JwtPayloadRole {
+  name: string
+  displayName: string
+  description: string
+  isEnabled: string
+  domains: string[]
+}
+
 export const useUserStore = defineStore('userStore', () => {
   const user = useStorage<User>('user', DEFAULT_USER)
 
-  const token = useStorage<string | null>('token', '')
+  const token = useStorage<string | undefined>('token', '')
 
   const menuList = ref<MenuProps['options']>([])
 
   const routeList = ref<RouteRecordRaw[]>([])
 
-  const setToken = (value: string | null) => {
+  const setToken = (value: string | undefined) => {
     token.value = value
   }
 
   async function resolveMenuList() {
     const res = await new Promise<MergeMenuMixedOptions[]>((resolve) => {
+      if (token.value !== undefined) {
+        const payload = jwtDecode<JwtPayload>(token.value)
+        if (payload.isAdmin) {
+          resolve(routeRecordRaw)
+        } else {
+          const allowedRoutes = payload.roles.map((role) => role.domains).flat()
+          const filterRoutes = (routeRecord: MergeMenuMixedOptions[]) => {
+            return routeRecord
+              .map((route): MergeMenuMixedOptions | undefined => {
+                const selfAllowed =
+                  !route.type && route.name && allowedRoutes.includes(route.name as string)
+                if (selfAllowed) return route
+
+                // 先递归处理 children
+                if (!('children' in route) || route.children === undefined) {
+                  return undefined
+                }
+                const result = { ...route }
+                result.children = filterRoutes(route.children)
+                return result
+              })
+              .filter((item): item is MergeMenuMixedOptions => Boolean(item))
+          }
+          // const filteredRoutes = routeRecordRaw.filter((route) => {
+          //   return !route.type && route.name && allowedRoutes.includes(route.name as string)
+          // })
+          resolve(filterRoutes(routeRecordRaw))
+        }
+      } else {
+        resolve([])
+      }
       // if (token.value?.includes('admin')) {
       //   resolve(routeRecordRaw)
       // } else {
@@ -49,7 +116,7 @@ export const useUserStore = defineStore('userStore', () => {
       //   resolve(filteredRoutes)
       // }
       // 暂时没有管理员权限
-      resolve(routeRecordRaw)
+      // resolve(routeRecordRaw)
     })
 
     menuList.value = resolveMenu(res) || []

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

@@ -13,6 +13,7 @@ declare module 'vue' {
     EmptyPlaceholder: typeof import('./../components/EmptyPlaceholder.vue')['default']
     HintHelp: typeof import('./../components/HintHelp.vue')['default']
     NAlert: typeof import('naive-ui')['NAlert']
+    NAutoComplete: typeof import('naive-ui')['NAutoComplete']
     NButton: typeof import('naive-ui')['NButton']
     NButtonGroup: typeof import('naive-ui')['NButtonGroup']
     NCard: typeof import('naive-ui')['NCard']

+ 64 - 0
src/utils/monaco/components/LogMonacoEditor.vue

@@ -0,0 +1,64 @@
+<script setup lang="ts">
+import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
+import type { MonacoEditor } from '..'
+import { LOG_LANGUAGE_ID } from '../languages/log/definitions'
+
+const props = defineProps<{
+  modelValue: string
+}>()
+
+const emit = defineEmits<{
+  'update:modelValue': [v: string]
+}>()
+
+const container = ref<HTMLDivElement>()
+
+let monacoEditor: MonacoEditor | undefined
+
+function openFind() {
+  monacoEditor?.editor.trigger('vue-button', 'actions.find', null)
+}
+
+onMounted(async () => {
+  // monaco必须动态加载,减少首屏包体积,提示加载速度
+  const monaco = await import('../index')
+  monacoEditor = await monaco.createMonacoEditor({
+    initValue: props.modelValue,
+    language: LOG_LANGUAGE_ID
+  }, {
+    el: container.value!,
+    options: {
+      readOnly: true
+    }
+  })
+
+  monacoEditor.editor.onDidChangeModelContent(() => {
+    if (monacoEditor !== undefined) {
+      if (monacoEditor.model.getValue() === props.modelValue) return
+      emit('update:modelValue', monacoEditor?.model.getValue())
+    }
+  })
+
+  watch(
+    () => props.modelValue,
+    v => {
+      if (v !== monacoEditor?.model.getValue()) {
+        monacoEditor?.model.setValue(v)
+      }
+    }
+  )
+})
+
+onBeforeUnmount(() => {
+  monacoEditor?.editor.dispose()
+  monacoEditor?.model.dispose()
+})
+
+defineExpose({
+  openFind
+})
+</script>
+
+<template>
+  <div ref="container" style="width: 100%; height: auto" />
+</template>

+ 21 - 0
src/utils/monaco/editor.ts

@@ -0,0 +1,21 @@
+import * as monaco from 'monaco-editor'
+
+export function createEditor(
+  el: HTMLElement,
+  options: monaco.editor.IStandaloneEditorConstructionOptions,
+) {
+  return monaco.editor.create(el, {
+    // 默认选项
+    automaticLayout: true,
+    minimap: { enabled: true },
+    scrollBeyondLastLine: false,
+    fontSize: 13,
+    // glyphMargin: true,
+    renderValidationDecorations: 'on',
+    ...options,
+  })
+}
+
+export function createModel(value: string, language: string, uri?: string) {
+  return monaco.editor.createModel(value, language, uri ? monaco.Uri.parse(uri) : undefined)
+}

+ 1 - 0
src/utils/monaco/env.ts

@@ -0,0 +1 @@
+import './workers/setup'

+ 60 - 0
src/utils/monaco/index.ts

@@ -0,0 +1,60 @@
+import './env'
+import * as monaco from 'monaco-editor'
+import { registerCustomTheme } from './themes'
+import { registerLanguages } from './languages'
+import { createEditor, createModel } from './editor'
+
+import { watchEffect, type WatchHandle } from 'vue'
+import { usePersonalization } from '@/composable/usePersonalization'
+
+let initialized = false
+
+const { isDark } = usePersonalization()
+let stopThemeWatch: WatchHandle | undefined
+
+function initMonaco() {
+  if (initialized) return
+  initialized = true
+
+  registerCustomTheme()
+  registerLanguages()
+
+  // 统一切换 Monaco 主题
+  stopThemeWatch ??= watchEffect(() => {
+    monaco.editor.setTheme(isDark.value ? 'custom-dark' : 'custom')
+  }, {})
+}
+
+export interface MonacoModelParam {
+  initValue: string
+  language: string
+  uri?: string
+}
+
+export interface MonacoEditorParam {
+  el: HTMLElement
+  options: monaco.editor.IStandaloneEditorConstructionOptions
+}
+
+export interface MonacoEditor {
+  model: monaco.editor.ITextModel
+  editor: monaco.editor.IStandaloneCodeEditor
+}
+
+export function createMonacoEditor(
+  model: MonacoModelParam,
+  editor: MonacoEditorParam,
+): MonacoEditor {
+  initMonaco()
+
+  const m = createModel(model.initValue, model.language, model.uri)
+  const e = createEditor(editor.el, {
+    model: m,
+    ...editor.options,
+  })
+
+  return {
+    model: m,
+    editor: e,
+  }
+}

+ 8 - 0
src/utils/monaco/languages/index.ts

@@ -0,0 +1,8 @@
+import { LOG_LANGUAGE_ID } from './log/definitions'
+import { registerLogLanguage } from './log/register'
+
+export const LanguageNames = { log: LOG_LANGUAGE_ID } as const
+
+export function registerLanguages() {
+  registerLogLanguage()
+}

+ 4 - 0
src/utils/monaco/languages/log/definitions.ts

@@ -0,0 +1,4 @@
+export const LOG_LANGUAGE_ID = 'log'
+export const LOG_EXTENSIONS = ['.log']
+export const LOG_ALIASES = ['Log', 'LOG', 'log']
+export const LOG_MIMETYPES = ['text/log']

+ 128 - 0
src/utils/monaco/languages/log/lspLanguageFeatures.ts

@@ -0,0 +1,128 @@
+import * as monaco from 'monaco-editor'
+import type { LogWorker } from './worker/logWorker'
+
+export type WorkerAccessor<T> = (...uris: monaco.Uri[]) => Promise<T>
+
+export abstract class Adapter {
+  constructor(protected _worker: (...uris: monaco.Uri[]) => Promise<LogWorker>) {}
+}
+
+//#region DiagnosticsAdapter
+
+export interface ILanguageWorkerWithDiagnostics {
+  doValidation(uri: string): Promise<monaco.editor.IMarkerData[]>
+}
+
+export class DiagnosticsAdapter<T extends ILanguageWorkerWithDiagnostics> {
+  protected readonly _disposables: monaco.IDisposable[] = []
+  private readonly _listener: { [uri: string]: monaco.IDisposable } = Object.create(null)
+
+  constructor(
+    private readonly _languageId: string,
+    protected readonly _worker: WorkerAccessor<T>,
+  ) {
+    const onModelAdd = (model: monaco.editor.IModel): void => {
+      let modeId = model.getLanguageId()
+      if (modeId !== this._languageId) {
+        return
+      }
+
+      let handle: number
+      this._listener[model.uri.toString()] = model.onDidChangeContent(() => {
+        window.clearTimeout(handle)
+        handle = window.setTimeout(() => this._doValidate(model.uri, modeId), 500)
+      })
+
+      this._doValidate(model.uri, modeId)
+    }
+
+    const onModelRemoved = (model: monaco.editor.IModel): void => {
+      monaco.editor.setModelMarkers(model, this._languageId, [])
+
+      let uriStr = model.uri.toString()
+      let listener = this._listener[uriStr]
+      if (listener) {
+        listener.dispose()
+        delete this._listener[uriStr]
+      }
+    }
+
+    this._disposables.push(monaco.editor.onDidCreateModel(onModelAdd))
+    this._disposables.push(monaco.editor.onWillDisposeModel(onModelRemoved))
+    this._disposables.push(
+      monaco.editor.onDidChangeModelLanguage((event) => {
+        onModelRemoved(event.model)
+        onModelAdd(event.model)
+      }),
+    )
+
+    this._disposables.push({
+      dispose: () => {
+        monaco.editor.getModels().forEach(onModelRemoved)
+        for (let key in this._listener) {
+          this._listener[key].dispose()
+        }
+      },
+    })
+
+    monaco.editor.getModels().forEach(onModelAdd)
+  }
+
+  public dispose(): void {
+    this._disposables.forEach((d) => d && d.dispose())
+    this._disposables.length = 0
+  }
+
+  private _doValidate(resource: monaco.Uri, languageId: string): void {
+    this._worker(resource)
+      .then((worker) => {
+        return worker.doValidation(resource.toString())
+      })
+      .then((diagnostics) => {
+        const markers = diagnostics
+        let model = monaco.editor.getModel(resource)
+        if (model && model.getLanguageId() === languageId) {
+          monaco.editor.setModelMarkers(model, 'LogWorker', markers)
+        }
+      })
+      .then(undefined, (err) => {
+        console.error(err)
+      })
+  }
+}
+
+// --- inlay hints ----
+
+export class InlayHintsAdapter extends Adapter implements monaco.languages.InlayHintsProvider {
+  public async provideInlayHints(
+    model: monaco.editor.ITextModel,
+    range: monaco.Range,
+    token: monaco.CancellationToken,
+  ): Promise<monaco.languages.InlayHintList | null> {
+    const resource = model.uri
+    const fileName = resource.toString()
+    const start = model.getOffsetAt({
+      lineNumber: range.startLineNumber,
+      column: range.startColumn,
+    })
+    const end = model.getOffsetAt({
+      lineNumber: range.endLineNumber,
+      column: range.endColumn,
+    })
+    const worker = await this._worker(resource)
+    if (model.isDisposed()) {
+      return null
+    }
+
+    const logHints = await worker.provideInlayHints(fileName, start, end)
+    const hints: monaco.languages.InlayHint[] = logHints.map((hint) => {
+      return {
+        ...hint,
+        label: hint.label,
+        position: hint.position,
+        kind: hint.kind,
+      }
+    })
+    return { hints, dispose: () => {} }
+  }
+}

+ 35 - 0
src/utils/monaco/languages/log/monarch.ts

@@ -0,0 +1,35 @@
+import * as monaco from 'monaco-editor'
+import { type MonacoEditor } from '../..'
+
+export const LogMonarchConfig: monaco.languages.IMonarchLanguage = {
+  tokenizer: {
+    root: [
+      // 时间戳
+      [/\[\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?\]/, 'tag.timestamp.log'],
+      [/\[\d{8},\d{2}:\d{2}:\d{2}\.\d{3}[+-]\d{4}\]/, 'tag.timestamp.log'],
+
+      // level
+      [/(\[INFO\]|\[Info\]|\[info\]|\[I\])/, 'tag.info.log'],
+      [/(\[WARN\]|\[Warn\]|\[warn\]|\[WARNING\]|\[W\])/, 'tag.warn.log'],
+      [/(\[ERROR\]|\[Error\]|\[error\]|\[E\])/, 'tag.error.log'],
+      [/(\[DEBUG\]|\[Debug\]|\[debug\]|\[D\])/, 'tag.debug.log'],
+
+      // 数字
+      [/\b\d+\b/, 'number'],
+
+      // 路径
+      [/(\/[a-zA-Z0-9._-]+)+/, 'string'],
+
+      // 标签
+      [/\[[^\s[\]]+\]/, 'tag'],
+    ],
+  },
+} as const
+
+let decorations: monaco.editor.IEditorDecorationsCollection | undefined = undefined
+
+export function handleUpdateValue(monacoEditor: MonacoEditor) {
+  const value = monacoEditor.model.getValue()
+
+  decorations = monacoEditor.editor.createDecorationsCollection([])
+}

+ 563 - 0
src/utils/monaco/languages/log/provider.ts

@@ -0,0 +1,563 @@
+import * as monaco from 'monaco-editor'
+import { LogMode } from './worker/logMode'
+import { LOG_LANGUAGE_ID } from './definitions'
+import { DiagnosticsAdapter, InlayHintsAdapter } from './lspLanguageFeatures'
+
+export function registerProvider(client: LogMode): monaco.IDisposable {
+  const providers: monaco.IDisposable[] = []
+
+  // 内容诊断(寻找错误日志、故障信息等)
+  providers.push(new DiagnosticsAdapter(LOG_LANGUAGE_ID, client.worker))
+
+  providers.push(
+    monaco.languages.registerHoverProvider(LOG_LANGUAGE_ID, {
+      provideHover: (model: monaco.editor.ITextModel, position: monaco.Position) => {
+        return {
+          contents: [{ value: 'Log file hover' }],
+        }
+      },
+    }),
+  )
+
+  // 注册内联类型提示或参数名提示(用于标注告警码说明等信息)
+  providers.push(
+    monaco.languages.registerInlayHintsProvider(
+      LOG_LANGUAGE_ID,
+      new InlayHintsAdapter(client.worker),
+    ),
+    // monaco.languages.registerInlayHintsProvider(LOG_LANGUAGE_ID, {
+    //   provideInlayHints: (model, range, token) => {
+    //     console.log({ model, range, token })
+    //     // kind说明
+    //     // monaco.languages.InlayHintKind.Type: 标注添加在后方
+    //     // monaco.languages.InlayHintKind.Parameter: 标注添加在前方
+    //     return {
+    //       hints: [
+    //         {
+    //           kind: monaco.languages.InlayHintKind.Type,
+    //           position: { column: 13, lineNumber: 4 },
+    //           label: `: Number`,
+    //         },
+    //         {
+    //           kind: monaco.languages.InlayHintKind.Type,
+    //           position: { column: 13, lineNumber: 2 },
+    //           label: `: Number`,
+    //         },
+    //         {
+    //           kind: monaco.languages.InlayHintKind.Type,
+    //           position: { column: 16, lineNumber: 2 },
+    //           label: `: Number`,
+    //           whitespaceBefore: true, // see difference between a and b parameter
+    //         },
+    //         {
+    //           kind: monaco.languages.InlayHintKind.Parameter,
+    //           position: { column: 18, lineNumber: 4 },
+    //           label: `a:`,
+    //         },
+    //         {
+    //           kind: monaco.languages.InlayHintKind.Parameter,
+    //           position: { column: 21, lineNumber: 4 },
+    //           label: `b:`,
+    //           whitespaceAfter: true, // similar to whitespaceBefore
+    //         },
+    //       ],
+    //       dispose: () => {},
+    //     }
+    //   },
+    // }),
+  )
+
+  // 注册文档结构解析器(用于展示日志中的关键数据)
+  providers.push(
+    monaco.languages.registerDocumentSymbolProvider(LOG_LANGUAGE_ID, {
+      provideDocumentSymbols: async (model, token) => {
+        console.log({ model, token })
+
+        // const markers = await client?.doValidation(model.uri.toString())
+
+        // 设置Marker
+        // monaco.editor.setModelMarkers(model, 'logProvider', markers ?? [])
+
+        // 返回解析结果
+        return [
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'File',
+            kind: 0,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Module',
+            kind: 1,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Namespace',
+            kind: 2,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Package',
+            kind: 3,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Class',
+            kind: 4,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Method',
+            kind: 5,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Property',
+            kind: 6,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Field',
+            kind: 7,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Constructor',
+            kind: 8,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Enum',
+            kind: 9,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Interface',
+            kind: 10,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Function',
+            kind: 11,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Variable',
+            kind: 12,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Constant',
+            kind: 13,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'String',
+            kind: 14,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Number',
+            kind: 15,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Boolean',
+            kind: 16,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Array',
+            kind: 17,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Object',
+            kind: 18,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Key',
+            kind: 19,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Null',
+            kind: 20,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'EnumMember',
+            kind: 21,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Struct',
+            kind: 22,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Event',
+            kind: 23,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'Operator',
+            kind: 24,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+          {
+            range: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+            name: 'TypeParameter',
+            kind: 25,
+            detail: '',
+            tags: [],
+            selectionRange: {
+              startLineNumber: 1,
+              startColumn: 1,
+              endLineNumber: 2,
+              endColumn: 1,
+            },
+          },
+        ]
+      },
+    }),
+  )
+
+  return {
+    dispose: () => {
+      while (providers.length) {
+        providers.pop()!.dispose()
+      }
+    },
+  }
+}

+ 52 - 0
src/utils/monaco/languages/log/register.ts

@@ -0,0 +1,52 @@
+import * as monaco from 'monaco-editor'
+import { LogMode } from './worker/logMode'
+import { LogMonarchConfig } from './monarch'
+import { registerProvider } from './provider'
+
+import * as Definitions from './definitions'
+
+let logMode: LogMode | null = null
+
+export interface LanguageServiceDefaults {
+  readonly languageId: string
+  readonly onDidChange: monaco.IEvent<LanguageServiceDefaults>
+}
+
+class LanguageServiceDefaultsImpl implements LanguageServiceDefaults {
+  private readonly _onDidChange: monaco.Emitter<LanguageServiceDefaults>
+
+  constructor(private readonly _languageId: string) {
+    this._onDidChange = new monaco.Emitter<LanguageServiceDefaults>()
+  }
+
+  get languageId(): string {
+    return this._languageId
+  }
+
+  get onDidChange(): monaco.IEvent<LanguageServiceDefaults> {
+    return this._onDidChange.event
+  }
+}
+
+export const logDefaults: LanguageServiceDefaults = new LanguageServiceDefaultsImpl(
+  Definitions.LOG_LANGUAGE_ID,
+)
+
+export function registerLogLanguage() {
+  monaco.languages.register({
+    id: Definitions.LOG_LANGUAGE_ID,
+    extensions: Definitions.LOG_EXTENSIONS,
+    aliases: Definitions.LOG_ALIASES,
+    mimetypes: Definitions.LOG_MIMETYPES,
+  })
+
+  // 注册Monarch
+  monaco.languages.setMonarchTokensProvider(Definitions.LOG_LANGUAGE_ID, LogMonarchConfig)
+
+  monaco.languages.onLanguage(Definitions.LOG_LANGUAGE_ID, () => {
+    if (logMode === null) {
+      logMode = new LogMode(new LanguageServiceDefaultsImpl(Definitions.LOG_LANGUAGE_ID))
+      registerProvider(logMode)
+    }
+  })
+}

+ 23 - 0
src/utils/monaco/languages/log/worker/log.worker.ts

@@ -0,0 +1,23 @@
+/// <reference lib="webworker" />
+
+import { initialize } from 'monaco-editor/esm/vs/editor/editor.worker.js'
+import { create as createLogWorker, type ICreateData } from './logWorker'
+import type { worker } from 'monaco-editor'
+
+self.onmessage = () => {
+  initialize((ctx: worker.IWorkerContext<{}>, createData: ICreateData) => {
+    return createLogWorker(ctx, createData)
+  })
+
+  // test: 保存原始 onmessage
+  const originalOnMessage = globalThis.onmessage
+
+  globalThis.onmessage = (event) => {
+    console.log('[DEBUG] Worker 收到消息:', event.data)
+
+    // 调用原来的处理逻辑
+    if (originalOnMessage) {
+      originalOnMessage.call(globalThis as unknown as Window, event)
+    }
+  }
+}

+ 20 - 0
src/utils/monaco/languages/log/worker/logMode.ts

@@ -0,0 +1,20 @@
+import * as monaco from 'monaco-editor'
+import type { LanguageServiceDefaults } from '../register'
+import { WorkerManager } from './workerManager'
+import type { LogWorker } from './logWorker'
+
+export class LogMode {
+  private readonly _client: WorkerManager
+
+  constructor(private readonly defaults: LanguageServiceDefaults) {
+    this._client = new WorkerManager(defaults)
+  }
+
+  dispose(): void {
+    this._client.dispose()
+  }
+
+  readonly worker = (...uris: monaco.Uri[]): Promise<LogWorker> => {
+    return this._client.getLanguageServiceWorker(...uris)
+  }
+}

+ 63 - 0
src/utils/monaco/languages/log/worker/logWorker.ts

@@ -0,0 +1,63 @@
+import type * as Monaco from 'monaco-editor'
+
+// 同Monaco.MarkerSeverity
+enum MarkerSeverity {
+  Hint = 1,
+  Info = 2,
+  Warning = 4,
+  Error = 8,
+}
+
+export class LogWorker {
+  constructor(
+    private readonly _ctx: Monaco.worker.IWorkerContext,
+    private readonly _createData: ICreateData,
+  ) {
+    console.log({ ctx: _ctx, createData: _createData })
+  }
+
+  private _getText(uri: string): string {
+    const model = this._ctx.getMirrorModels().find((m) => m.uri.toString() === uri)
+    return model?.getValue() ?? ''
+  }
+
+  // TODO 待实现,可实现告警码提示
+  async provideInlayHints(
+    uri: string,
+    start: number,
+    end: number,
+  ): Promise<readonly Monaco.languages.InlayHint[]> {
+    return []
+  }
+
+  async doValidation(uri: string): Promise<Monaco.editor.IMarkerData[]> {
+    const text = this._getText(uri)
+
+    const markers: Monaco.editor.IMarkerData[] = []
+
+    const lines = text.split('\n')
+
+    lines.forEach((line, index) => {
+      if (line.includes('[E]') || line.includes('[Error]') || line.includes('[ERROR]')) {
+        markers.push({
+          severity: MarkerSeverity.Error,
+          message: 'Error log detected',
+          startLineNumber: index + 1,
+          endLineNumber: index + 1,
+          startColumn: 1,
+          endColumn: line.length + 1,
+        })
+      }
+    })
+
+    return markers
+  }
+}
+
+export interface ICreateData {
+  languageId: string
+}
+
+export function create(ctx: Monaco.worker.IWorkerContext, createData: ICreateData): LogWorker {
+  return new LogWorker(ctx, createData)
+}

+ 84 - 0
src/utils/monaco/languages/log/worker/workerManager.ts

@@ -0,0 +1,84 @@
+import * as monaco from 'monaco-editor'
+import type { LanguageServiceDefaults } from '../register'
+import type { LogWorker, ICreateData } from './logWorker'
+import { LOG_LANGUAGE_ID } from '../definitions'
+
+const STOP_WHEN_IDLE_FOR = 2 * 60 * 1000 // 2min
+
+export class WorkerManager {
+  private readonly _defaults: LanguageServiceDefaults
+  private _worker: monaco.editor.MonacoWebWorker<LogWorker> | null = null
+  private _client: Promise<LogWorker> | null
+  private readonly _idleCheckInterval: number
+  private _lastUsedTime: number
+
+  constructor(defaults: LanguageServiceDefaults) {
+    this._defaults = defaults
+    this._worker = null
+    this._client = null
+    this._idleCheckInterval = window.setInterval(() => this._checkIfIdle(), 30 * 1000)
+    this._lastUsedTime = 0
+  }
+
+  private _stopWorker(): void {
+    if (this._worker) {
+      this._worker.dispose()
+      this._worker = null
+    }
+    this._client = null
+  }
+
+  dispose(): void {
+    clearInterval(this._idleCheckInterval)
+    this._stopWorker()
+  }
+
+  private _checkIfIdle(): void {
+    if (!this._worker) {
+      return
+    }
+    let timePassedSinceLastUsed = Date.now() - this._lastUsedTime
+    if (timePassedSinceLastUsed > STOP_WHEN_IDLE_FOR) {
+      this._stopWorker()
+    }
+  }
+
+  private _getClient(): Promise<LogWorker> {
+    this._lastUsedTime = Date.now()
+
+    if (!this._client) {
+      this._worker = monaco.createWebWorker<LogWorker>({
+        // module that exports the create() method and returns a `LogWorker` instance
+        moduleId: 'vs/language/log/logWorker',
+        createWorker: () => {
+          return new Worker(new URL('./log.worker?esm', import.meta.url), { type: 'module' })
+        },
+
+        label: LOG_LANGUAGE_ID,
+
+        // passed in to the create() method
+        createData: {
+          languageId: LOG_LANGUAGE_ID,
+        } satisfies ICreateData,
+      })
+
+      this._client = <Promise<LogWorker>>(<any>this._worker.getProxy())
+    }
+
+    return this._client
+  }
+
+  getLanguageServiceWorker(...resources: monaco.Uri[]): Promise<LogWorker> {
+    let _client: LogWorker
+    return this._getClient()
+      .then((client) => {
+        _client = client
+      })
+      .then((_) => {
+        if (this._worker) {
+          return this._worker.withSyncedResources(resources)
+        }
+      })
+      .then((_) => _client)
+  }
+}

+ 25 - 0
src/utils/monaco/themes/custom.ts

@@ -0,0 +1,25 @@
+import * as monaco from 'monaco-editor'
+
+export const custom_rules: {
+  light: monaco.editor.ITokenThemeRule[]
+  dark: monaco.editor.ITokenThemeRule[]
+} = {
+  light: [
+    { token: 'error', foreground: 'FF0000' },
+    { token: 'tag', foreground: 'CEA8C8' },
+    { token: 'tag.timestamp.log', foreground: '00AAFF' },
+    { token: 'tag.info.log', foreground: '00FF00' },
+    { token: 'tag.warn.log', foreground: 'FFFF00' },
+    { token: 'tag.error.log', foreground: 'FF0000' },
+    { token: 'tag.debug.log', foreground: 'B5CEA8' },
+  ],
+  dark: [
+    { token: 'error', foreground: 'FF0000' },
+    { token: 'tag', foreground: 'CEA8C8' },
+    { token: 'tag.timestamp.log', foreground: '00AAFF' },
+    { token: 'tag.info.log', foreground: '00FF00' },
+    { token: 'tag.warn.log', foreground: 'FFFF00' },
+    { token: 'tag.error.log', foreground: 'FF0000' },
+    { token: 'tag.debug.log', foreground: 'B5CEA8' },
+  ],
+} as const

+ 165 - 0
src/utils/monaco/themes/index.ts

@@ -0,0 +1,165 @@
+import * as monaco from 'monaco-editor'
+import { vs, vs_dark, hc_light, hc_black } from './internalThemes'
+import { custom_rules } from './custom'
+
+// vs theme Token
+// invalid:                 无效
+// emphasis:                强调(斜体)
+// strong:                  粗体(粗体)
+// variable:                变量
+// variable.predefined:     变量.预定义
+// constant:                常量
+// comment:                 注释
+// number:                  数字
+// number.hex:              数字.十六进制
+// regexp:                  正则表达式
+// annotation:              注解
+// type:                    类型
+// delimiter:               分隔符
+// delimiter.html:          分隔符.html
+// delimiter.xml:           分隔符.xml
+// tag:                     标签
+// tag.id.pug:              标签.id.pug
+// tag.class.pug:           标签.class.pug
+// meta.scss:               元数据.scss
+// metatag:                 元数据标签
+// metatag.content.html:    元数据标签.内容.html
+// metatag.html:            元数据标签.html
+// metatag.xml:             元数据标签.xml
+// metatag.php:             元数据标签.php
+// key:                     键
+// string.key.json:         字符串.键.json
+// string.value.json:       字符串.值.json
+// attribute.name:          属性.名
+// attribute.value:         属性.值
+// attribute.value.number:  属性.值.数值
+// attribute.value.unit:    属性.值.单位
+// attribute.value.html:    属性.值.html
+// attribute.value.xml:     属性.值.xml
+// string:                  字符串
+// string.html:             字符串.html
+// string.sql:              字符串.sql
+// string.yaml:             字符串.yaml
+// keyword:                 关键字
+// keyword.json:            关键字.json
+// keyword.flow:            关键字.flow
+// keyword.flow.scss:       关键字.flow.scss
+// operator.scss:           算子.scss
+// operator.sql:            算子.sql
+// operator.swift:          算子.swift
+// predefined.sql:          预定义.sql
+
+// vs-dark theme Token
+// invalid:                 无效
+// emphasis:                强调(斜体)
+// strong:                  粗体(粗体)
+// variable:                变量
+// variable.predefined:     变量.预定义
+// variable.parameter:      变量.参数
+// constant:                常量
+// comment:                 注释
+// number:                  数字
+// number.hex:              数字.十六进制
+// regexp:                  正则表达式
+// annotation:              注解
+// type:                    类型
+// delimiter:               分隔符
+// delimiter.html:          分隔符.html
+// delimiter.xml:           分隔符.xml
+// tag:                     标签
+// tag.id.pug:              标签.id.pug
+// tag.class.pug:           标签.class.pug
+// meta.scss:               元数据.scss
+// meta.tag:                元数据.标签
+// metatag:                 元数据标签
+// metatag.content.html:    元数据标签.内容.html
+// metatag.html:            元数据标签.html
+// metatag.xml:             元数据标签.xml
+// metatag.php:             元数据标签.php
+// key:                     键
+// string.key.json:         字符串.键.json
+// string.value.json:       字符串.值.json
+// attribute.name:          属性.名
+// attribute.value:         属性.值
+// attribute.value.number.css:  属性.值.数值.css
+// attribute.value.unit.css:    属性.值.单位.css
+// attribute.value.hex.css:     属性.值.hex.css
+// string:                  字符串
+// string.sql:              字符串.sql
+// keyword:                 关键字
+// keyword.flow:            关键字.flow
+// keyword.json:            关键字.json
+// keyword.flow.scss:       关键字.flow.scss
+// operator.scss:           算子.scss
+// operator.sql:            算子.sql
+// operator.swift:          算子.swift
+// predefined.sql:          预定义.sql
+
+interface IChildThemeData
+  extends Omit<monaco.editor.IStandaloneThemeData, 'base' | 'inherit' | 'encodedTokensColors'> {}
+
+function inheritTheme(
+  base: monaco.editor.BuiltinTheme,
+  themeData: IChildThemeData,
+): monaco.editor.IStandaloneThemeData {
+  let baseThemeData: monaco.editor.IStandaloneThemeData = vs
+  switch (base) {
+    case 'vs':
+      baseThemeData = vs
+      break
+    case 'vs-dark':
+      baseThemeData = vs_dark
+      break
+    case 'hc-black':
+      baseThemeData = hc_black
+      break
+    case 'hc-light':
+      baseThemeData = hc_light
+      break
+  }
+
+  // 合并rules
+  const ruleTokens = themeData.rules.map((r) => r.token)
+  const rules = [
+    ...baseThemeData.rules
+      .map((r) => {
+        if (ruleTokens.includes(r.token)) {
+          return undefined // 去除同名规则
+        } else {
+          return r
+        }
+      })
+      .filter((v) => v !== undefined),
+    ...themeData.rules,
+  ]
+
+  // 合并colors
+  const colors = {
+    ...baseThemeData.colors,
+    ...themeData.colors,
+  }
+
+  return {
+    base: baseThemeData.base,
+    inherit: false,
+    rules,
+    encodedTokensColors: baseThemeData.encodedTokensColors,
+    colors,
+  }
+}
+
+export function registerCustomTheme() {
+  monaco.editor.defineTheme('custom', {
+    base: 'vs',
+    inherit: true,
+    rules: [...custom_rules.light],
+    colors: {},
+  })
+
+  monaco.editor.defineTheme('custom-dark', {
+    base: 'vs-dark',
+    inherit: true,
+    rules: [...custom_rules.dark],
+    colors: {},
+  })
+}

+ 260 - 0
src/utils/monaco/themes/internalThemes.ts

@@ -0,0 +1,260 @@
+import * as monaco from 'monaco-editor'
+
+/* -------------------------------- Begin vs theme -------------------------------- */
+export const vs: monaco.editor.IStandaloneThemeData = {
+  base: 'vs',
+  inherit: false,
+  rules: [
+    { token: '', foreground: '000000', background: 'fffffe' },
+    { token: 'invalid', foreground: 'cd3131' },
+    { token: 'emphasis', fontStyle: 'italic' },
+    { token: 'strong', fontStyle: 'bold' },
+
+    { token: 'variable', foreground: '001188' },
+    { token: 'variable.predefined', foreground: '4864AA' },
+    { token: 'constant', foreground: 'dd0000' },
+    { token: 'comment', foreground: '008000' },
+    { token: 'number', foreground: '098658' },
+    { token: 'number.hex', foreground: '3030c0' },
+    { token: 'regexp', foreground: '800000' },
+    { token: 'annotation', foreground: '808080' },
+    { token: 'type', foreground: '008080' },
+
+    { token: 'delimiter', foreground: '000000' },
+    { token: 'delimiter.html', foreground: '383838' },
+    { token: 'delimiter.xml', foreground: '0000FF' },
+
+    { token: 'tag', foreground: '800000' },
+    { token: 'tag.id.pug', foreground: '4F76AC' },
+    { token: 'tag.class.pug', foreground: '4F76AC' },
+    { token: 'meta.scss', foreground: '800000' },
+    { token: 'metatag', foreground: 'e00000' },
+    { token: 'metatag.content.html', foreground: 'FF0000' },
+    { token: 'metatag.html', foreground: '808080' },
+    { token: 'metatag.xml', foreground: '808080' },
+    { token: 'metatag.php', fontStyle: 'bold' },
+
+    { token: 'key', foreground: '863B00' },
+    { token: 'string.key.json', foreground: 'A31515' },
+    { token: 'string.value.json', foreground: '0451A5' },
+
+    { token: 'attribute.name', foreground: 'FF0000' },
+    { token: 'attribute.value', foreground: '0451A5' },
+    { token: 'attribute.value.number', foreground: '098658' },
+    { token: 'attribute.value.unit', foreground: '098658' },
+    { token: 'attribute.value.html', foreground: '0000FF' },
+    { token: 'attribute.value.xml', foreground: '0000FF' },
+
+    { token: 'string', foreground: 'A31515' },
+    { token: 'string.html', foreground: '0000FF' },
+    { token: 'string.sql', foreground: 'FF0000' },
+    { token: 'string.yaml', foreground: '0451A5' },
+
+    { token: 'keyword', foreground: '0000FF' },
+    { token: 'keyword.json', foreground: '0451A5' },
+    { token: 'keyword.flow', foreground: 'AF00DB' },
+    { token: 'keyword.flow.scss', foreground: '0000FF' },
+
+    { token: 'operator.scss', foreground: '666666' },
+    { token: 'operator.sql', foreground: '778899' },
+    { token: 'operator.swift', foreground: '666666' },
+    { token: 'predefined.sql', foreground: 'C700C7' },
+  ],
+  colors: {
+    ['editor.background']: '#FFFFFE',
+    ['editor.foreground']: '#000000',
+    ['editor.inactiveSelectionBackground']: '#E5EBF1',
+    ['editorIndentGuide.background1']: '#D3D3D3',
+    ['editorIndentGuide.activeBackground1']: '#939393',
+    ['editor.selectionHighlightBackground']: '#ADD6FF4D',
+  },
+}
+/* -------------------------------- End vs theme -------------------------------- */
+
+/* -------------------------------- Begin vs-dark theme -------------------------------- */
+export const vs_dark: monaco.editor.IStandaloneThemeData = {
+  base: 'vs-dark',
+  inherit: false,
+  rules: [
+    { token: '', foreground: 'D4D4D4', background: '1E1E1E' },
+    { token: 'invalid', foreground: 'f44747' },
+    { token: 'emphasis', fontStyle: 'italic' },
+    { token: 'strong', fontStyle: 'bold' },
+
+    { token: 'variable', foreground: '74B0DF' },
+    { token: 'variable.predefined', foreground: '4864AA' },
+    { token: 'variable.parameter', foreground: '9CDCFE' },
+    { token: 'constant', foreground: '569CD6' },
+    { token: 'comment', foreground: '608B4E' },
+    { token: 'number', foreground: 'B5CEA8' },
+    { token: 'number.hex', foreground: '5BB498' },
+    { token: 'regexp', foreground: 'B46695' },
+    { token: 'annotation', foreground: 'cc6666' },
+    { token: 'type', foreground: '3DC9B0' },
+
+    { token: 'delimiter', foreground: 'DCDCDC' },
+    { token: 'delimiter.html', foreground: '808080' },
+    { token: 'delimiter.xml', foreground: '808080' },
+
+    { token: 'tag', foreground: '569CD6' },
+    { token: 'tag.id.pug', foreground: '4F76AC' },
+    { token: 'tag.class.pug', foreground: '4F76AC' },
+    { token: 'meta.scss', foreground: 'A79873' },
+    { token: 'meta.tag', foreground: 'CE9178' },
+    { token: 'metatag', foreground: 'DD6A6F' },
+    { token: 'metatag.content.html', foreground: '9CDCFE' },
+    { token: 'metatag.html', foreground: '569CD6' },
+    { token: 'metatag.xml', foreground: '569CD6' },
+    { token: 'metatag.php', fontStyle: 'bold' },
+
+    { token: 'key', foreground: '9CDCFE' },
+    { token: 'string.key.json', foreground: '9CDCFE' },
+    { token: 'string.value.json', foreground: 'CE9178' },
+
+    { token: 'attribute.name', foreground: '9CDCFE' },
+    { token: 'attribute.value', foreground: 'CE9178' },
+    { token: 'attribute.value.number.css', foreground: 'B5CEA8' },
+    { token: 'attribute.value.unit.css', foreground: 'B5CEA8' },
+    { token: 'attribute.value.hex.css', foreground: 'D4D4D4' },
+
+    { token: 'string', foreground: 'CE9178' },
+    { token: 'string.sql', foreground: 'FF0000' },
+
+    { token: 'keyword', foreground: '569CD6' },
+    { token: 'keyword.flow', foreground: 'C586C0' },
+    { token: 'keyword.json', foreground: 'CE9178' },
+    { token: 'keyword.flow.scss', foreground: '569CD6' },
+
+    { token: 'operator.scss', foreground: '909090' },
+    { token: 'operator.sql', foreground: '778899' },
+    { token: 'operator.swift', foreground: '909090' },
+    { token: 'predefined.sql', foreground: 'FF00FF' },
+  ],
+  colors: {
+    ['editor.background']: '#1E1E1E',
+    ['editor.foreground']: '#D4D4D4',
+    ['editor.inactiveSelectionBackground']: '#3A3D41',
+    ['editorIndentGuide.background1']: '#404040',
+    ['editorIndentGuide.activeBackground1']: '#707070',
+    ['editor.selectionHighlightBackground']: '#ADD6FF26',
+  },
+}
+/* -------------------------------- End vs-dark theme -------------------------------- */
+
+/* -------------------------------- Begin hc-black theme -------------------------------- */
+export const hc_black: monaco.editor.IStandaloneThemeData = {
+  base: 'hc-black',
+  inherit: false,
+  rules: [
+    { token: '', foreground: 'FFFFFF', background: '000000' },
+    { token: 'invalid', foreground: 'f44747' },
+    { token: 'emphasis', fontStyle: 'italic' },
+    { token: 'strong', fontStyle: 'bold' },
+
+    { token: 'variable', foreground: '1AEBFF' },
+    { token: 'variable.parameter', foreground: '9CDCFE' },
+    { token: 'constant', foreground: '569CD6' },
+    { token: 'comment', foreground: '608B4E' },
+    { token: 'number', foreground: 'FFFFFF' },
+    { token: 'regexp', foreground: 'C0C0C0' },
+    { token: 'annotation', foreground: '569CD6' },
+    { token: 'type', foreground: '3DC9B0' },
+
+    { token: 'delimiter', foreground: 'FFFF00' },
+    { token: 'delimiter.html', foreground: 'FFFF00' },
+
+    { token: 'tag', foreground: '569CD6' },
+    { token: 'tag.id.pug', foreground: '4F76AC' },
+    { token: 'tag.class.pug', foreground: '4F76AC' },
+    { token: 'meta', foreground: 'D4D4D4' },
+    { token: 'meta.tag', foreground: 'CE9178' },
+    { token: 'metatag', foreground: '569CD6' },
+    { token: 'metatag.content.html', foreground: '1AEBFF' },
+    { token: 'metatag.html', foreground: '569CD6' },
+    { token: 'metatag.xml', foreground: '569CD6' },
+    { token: 'metatag.php', fontStyle: 'bold' },
+
+    { token: 'key', foreground: '9CDCFE' },
+    { token: 'string.key', foreground: '9CDCFE' },
+    { token: 'string.value', foreground: 'CE9178' },
+
+    { token: 'attribute.name', foreground: '569CD6' },
+    { token: 'attribute.value', foreground: '3FF23F' },
+
+    { token: 'string', foreground: 'CE9178' },
+    { token: 'string.sql', foreground: 'FF0000' },
+
+    { token: 'keyword', foreground: '569CD6' },
+    { token: 'keyword.flow', foreground: 'C586C0' },
+
+    { token: 'operator.sql', foreground: '778899' },
+    { token: 'operator.swift', foreground: '909090' },
+    { token: 'predefined.sql', foreground: 'FF00FF' },
+  ],
+  colors: {
+    ['editor.background']: '#000000',
+    ['editor.foreground']: '#FFFFFF',
+    ['editorIndentGuide.background1']: '#FFFFFF',
+    ['editorIndentGuide.activeBackground1']: '#FFFFFF',
+  },
+}
+/* -------------------------------- End hc-black theme -------------------------------- */
+
+/* -------------------------------- Begin hc-light theme -------------------------------- */
+export const hc_light: monaco.editor.IStandaloneThemeData = {
+  base: 'hc-light',
+  inherit: false,
+  rules: [
+    { token: '', foreground: '292929', background: 'FFFFFF' },
+    { token: 'invalid', foreground: 'B5200D' },
+    { token: 'emphasis', fontStyle: 'italic' },
+    { token: 'strong', fontStyle: 'bold' },
+
+    { token: 'variable', foreground: '264F70' },
+    { token: 'variable.predefined', foreground: '4864AA' },
+    { token: 'constant', foreground: 'dd0000' },
+    { token: 'comment', foreground: '008000' },
+    { token: 'number', foreground: '098658' },
+    { token: 'number.hex', foreground: '3030c0' },
+    { token: 'regexp', foreground: '800000' },
+    { token: 'annotation', foreground: '808080' },
+    { token: 'type', foreground: '008080' },
+
+    { token: 'delimiter', foreground: '000000' },
+    { token: 'delimiter.html', foreground: '383838' },
+
+    { token: 'tag', foreground: '800000' },
+    { token: 'tag.id.pug', foreground: '4F76AC' },
+    { token: 'tag.class.pug', foreground: '4F76AC' },
+    { token: 'meta.scss', foreground: '800000' },
+    { token: 'metatag', foreground: 'e00000' },
+    { token: 'metatag.content.html', foreground: 'B5200D' },
+    { token: 'metatag.html', foreground: '808080' },
+    { token: 'metatag.xml', foreground: '808080' },
+    { token: 'metatag.php', fontStyle: 'bold' },
+
+    { token: 'key', foreground: '863B00' },
+    { token: 'string.key.json', foreground: 'A31515' },
+    { token: 'string.value.json', foreground: '0451A5' },
+
+    { token: 'attribute.name', foreground: '264F78' },
+    { token: 'attribute.value', foreground: '0451A5' },
+
+    { token: 'string', foreground: 'A31515' },
+    { token: 'string.sql', foreground: 'B5200D' },
+
+    { token: 'keyword', foreground: '0000FF' },
+    { token: 'keyword.flow', foreground: 'AF00DB' },
+
+    { token: 'operator.sql', foreground: '778899' },
+    { token: 'operator.swift', foreground: '666666' },
+    { token: 'predefined.sql', foreground: 'C700C7' },
+  ],
+  colors: {
+    ['editor.background']: '#FFFFFF',
+    ['editor.foreground']: '#292929',
+    ['editorIndentGuide.background1']: '#292929',
+    ['editorIndentGuide.activeBackground1']: '#292929',
+  },
+}
+/* -------------------------------- End hc-light theme -------------------------------- */

+ 4 - 0
src/utils/monaco/workers/editor.worker.d.ts

@@ -0,0 +1,4 @@
+declare module 'monaco-editor/esm/vs/editor/editor.worker.js' {
+  // TypeScript 中 Worker 初始化函数类型
+  export function initialize(factory: (ctx: any, createData: any) => any): void
+}

+ 7 - 0
src/utils/monaco/workers/editor.worker.start.d.ts

@@ -0,0 +1,7 @@
+declare module 'monaco-editor/esm/vs/editor/editor.worker.start.js' {
+  import type { worker } from 'monaco-editor'
+
+  export function start<THost extends object, TClient extends object>(
+    createClient: (ctx: worker.IWorkerContext<THost>) => TClient,
+  ): TClient
+}

+ 40 - 0
src/utils/monaco/workers/setup.ts

@@ -0,0 +1,40 @@
+globalThis.MonacoEnvironment = {
+  getWorker(_: any, label: string) {
+    console.log('Worker request label →', label)
+    if (label === 'json') {
+      return new Worker(
+        new URL('monaco-editor/esm/vs/language/json/json.worker.js', import.meta.url),
+        { type: 'module' },
+      )
+    } else if (label === 'css' || label === 'scss' || label === 'less') {
+      return new Worker(
+        new URL('monaco-editor/esm/vs/language/css/css.worker.js', import.meta.url),
+        {
+          type: 'module',
+        },
+      )
+    } else if (label === 'html' || label === 'handlebars' || label === 'razor') {
+      return new Worker(
+        new URL('monaco-editor/esm/vs/language/html/html.worker.js', import.meta.url),
+        { type: 'module' },
+      )
+    } else if (label === 'typescript' || label === 'javascript') {
+      return new Worker(
+        new URL('monaco-editor/esm/vs/language/typescript/ts.worker.js', import.meta.url),
+        { type: 'module' },
+      )
+    }
+
+    /* 自定义的语言worker */
+    // log
+    if (label === 'log') {
+      return new Worker(new URL('../languages/log/worker/log.worker.ts', import.meta.url), {
+        type: 'module',
+      })
+    }
+
+    return new Worker(new URL('monaco-editor/esm/vs/editor/editor.worker.js', import.meta.url), {
+      type: 'module',
+    })
+  },
+}

+ 226 - 0
src/views/batch-download/component/fullscreenPreview.vue

@@ -0,0 +1,226 @@
+<template>
+  <Teleport to="body">
+    <Transition enter-active-class="transition-all duration-200 ease-out" enter-from-class="opacity-0 translate-y-2"
+      enter-to-class="opacity-100 translate-y-0" leave-active-class="transition-all duration-150 ease-in"
+      leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 translate-y-2">
+      <div v-if="showFullscreen" class="fixed inset-0 z-9999 bg-black/60 flex items-center justify-center">
+        <!-- 内容容器 -->
+        <div class="relative w-95% h-95% bg-white rounded-xl shadow-xl flex flex-col overflow-hidden">
+          <div class="flex items-center gap-2 px-2 pt-2">
+            <NTooltip :show-arrow="false" placement="bottom-start" trigger="hover">
+              <template #trigger>
+                <NButton text :focusable="false" style="font-size: 18px">
+                  <span class="i-ph:info" />
+                </NButton>
+              </template>
+              <div>
+                <span class="font-bold">使用说明</span>
+                <span>搜索</span>
+              </div>
+
+            </NTooltip>
+            <NPopover placement="bottom-start" trigger="manual" :show="showFilterMenu">
+              <template #trigger>
+                <NButton text :focusable="false" style="font-size: 18px" @click="openFilterMenu">
+                  <span class="i-mdi:filter-cog-outline" />
+                </NButton>
+              </template>
+              <div>
+                <span>文本过滤条件,每个条件之间是或的关系,每个标签是与的关系。</span>
+                <NDivider style="margin: 8px 0;" />
+                <NDynamicInput v-model:value="filterConfig" :on-create="onCreateFilter">
+                  <template #create-button-default>
+                    新增过滤条件
+                  </template>
+                  <template #default="{ value }">
+                    <div style="display: flex; align-items: center; width: 100%">
+                      <NCheckbox v-model:checked="value.active" style="margin-right: 12px" />
+                      <NDynamicTags v-model:value="value.tags" />
+                    </div>
+                  </template>
+                </NDynamicInput>
+                <NDivider style="margin: 8px 0;" />
+                <div class="flex justify-end gap-4">
+                  <NButton class="px-3 py-1 border rounded" text :focusable="false" style="font-size: 18px"
+                    @click="closeFilterMenu">
+                    <span class="i-mdi:close" />
+                  </NButton>
+                  <NButton class="px-3 py-1 border rounded" text :focusable="false" style="font-size: 18px"
+                    @click="submitFilterMenu">
+                    <span class="i-mdi:check" />
+                  </NButton>
+                </div>
+              </div>
+            </NPopover>
+            <NButton text :focusable="false" style="font-size: 18px" class="transition-colors duration-200"
+              :class="cancelAllFilter ? 'text-green-500' : 'text-gray-400'" @click="() => {
+                cancelAllFilter = !cancelAllFilter
+                console.log({ cancelAllFilter })
+              }">
+              <span class="i-mdi:filter-off-outline" />
+            </NButton>
+            <NButton text :focusable="false" style="font-size: 18px">
+              <span class="i-ph:faders" />
+            </NButton>
+            <NButton text :focusable="false" style="font-size: 18px" @click="openSearch">
+              <span class="i-mdi:search" />
+            </NButton>
+            <!-- 靠右按钮 -->
+            <div class="ml-auto flex gap-2">
+              <NButton text :focusable="false" class="text-red-500" style="font-size: 18px"
+                @click="() => { showFullscreen = false }">
+                <span class="i-mdi:close-circle" />
+              </NButton>
+            </div>
+          </div>
+          <NDivider style="margin: 16px 0;" />
+          <LogMonacoEditor class="flex-1" ref="logMonacoEditorRef" v-model="editorTxt" />
+        </div>
+      </div>
+    </Transition>
+  </Teleport>
+</template>
+
+<script setup lang="ts">
+import { NButton, NDivider, NTooltip, NPopover, NDynamicInput, NCheckbox, NDynamicTags } from 'naive-ui'
+import LogMonacoEditor from '@/utils/monaco/components/LogMonacoEditor.vue'
+
+const showFullscreen = defineModel<boolean>('show', { required: true })
+
+const logMonacoEditorRef = useTemplateRef<InstanceType<typeof LogMonacoEditor>>('logMonacoEditorRef')
+
+// 禁用所有过滤
+const cancelAllFilter = ref(false)
+
+interface FilterConfigType {
+  active: boolean
+  tags: string[]
+}
+
+interface UserConfig {
+  filter: {
+    disable: boolean
+    config: FilterConfigType[]
+  }
+}
+
+const filterConfig = ref<FilterConfigType[]>([])
+const onCreateFilter = () => {
+  return {
+    active: true,
+    tags: []
+  }
+}
+
+const userConfig = reactive<UserConfig>({
+  filter: {
+    disable: false,
+    config: []
+  }
+})
+
+const showFilterMenu = ref(false)
+
+const openFilterMenu = () => {
+  showFilterMenu.value = true
+}
+
+const closeFilterMenu = () => {
+  showFilterMenu.value = false
+}
+
+const submitFilterMenu = () => {
+  showFilterMenu.value = false
+}
+
+const openSearch = () => {
+  logMonacoEditorRef.value?.openFind()
+}
+
+const editorTxt = ref<string>(`[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[E] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[ERROR] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+`)
+
+// setTimeout(() => {
+//   editorTxt.value = "[I] [20260105,22:12:05.914-0500]"
+// }, 10000);
+
+</script>

+ 157 - 26
src/views/batch-download/component/viewPathDialog.vue

@@ -1,17 +1,125 @@
 <template>
   <NTree block-line :data="dataRef" :on-load="handleLoad" expand-on-click />
+  <FullscreenPreview v-model:show="showFullscreenPreview" />
 </template>
 
 <script setup lang="ts">
-import { NTree, NButton } from 'naive-ui';
+import { type VNodeChild } from 'vue'
+import { useMessage, NTree, NButton } from 'naive-ui';
 import type { TreeOption } from 'naive-ui';
-import { getBatchDownloadFileTree, getBatchDownloadFilePath } from '@/service/batchDownload';
+import { getBatchDownloadFileTree, downloadBatchDownloadFile, getBatchDownloadFile } from '@/service/batchDownload';
 import type { BatchDownloadStructNode, BatchDownloadTarNode } from '@/service/batchDownload/interface';
+import FullscreenPreview from './fullscreenPreview.vue';
+
+const message = useMessage()
 
 const props = defineProps<{
   data: BatchDownloadStructNode[]
 }>()
 
+const showFullscreenPreview = ref(false)
+
+const fileSuffixSupport = {
+  package: [
+    '.tar.gz',
+  ],
+  file: [
+    '.html',
+    '.csv',
+    '.xlsx',
+    '.log',
+  ],
+  preview: [
+    '.html',
+    '.csv',
+    '.xlsx',
+    '.log',
+  ]
+}
+
+function endsWithIgnoreCase(str: string, suffix: string): boolean {
+  return str.toLowerCase().endsWith(suffix.toLowerCase())
+}
+
+const generateButtonFromFileSuffix = (filePath: string, innerPath?: string): (() => VNodeChild) | undefined => {
+  console.log({ filePath, innerPath })
+
+  const downloadButtonNode = h(
+    NButton,
+    {
+      text: true,
+      type: 'primary',
+      size: 'small',
+      onClick(e) {
+        e.stopPropagation()
+        downloadBatchDownloadFile(filePath, innerPath)
+      }
+    },
+    { default: () => '下载' }
+  )
+
+  const previewButtonNode = h(
+    NButton,
+    {
+      text: true,
+      type: 'info',
+      size: 'small',
+      onClick(e) {
+        e.stopPropagation()
+        previewFile(filePath, innerPath)
+      }
+    },
+    { default: () => '查看' }
+  )
+
+  let buttonNodes: VNodeChild[] = []
+  // 检查是否是压缩包
+  const packageSuffix = fileSuffixSupport.package.find((item, index, array) => endsWithIgnoreCase(filePath, item))
+  if (packageSuffix !== undefined) {
+    if (innerPath === undefined) {
+      // 压缩包,只能下载
+      buttonNodes.push(downloadButtonNode)
+    } else {
+      const innerFileSuffix = fileSuffixSupport.file.find((item, index, array) => endsWithIgnoreCase(innerPath, item))
+      if (innerFileSuffix === undefined) {
+        // 无法识别的文件类型
+        return undefined
+      }
+
+      // 能识别额的文件类型,可下载
+      buttonNodes.push(downloadButtonNode)
+
+      if (fileSuffixSupport.preview.includes(innerFileSuffix)) {
+        // 可查看的文件类型
+        buttonNodes.push(previewButtonNode)
+      }
+    }
+  } else {
+    const fileSuffix = fileSuffixSupport.file.find((item, index, array) => endsWithIgnoreCase(filePath, item))
+    if (fileSuffix === undefined) {
+      // 无法识别的文件类型
+      return undefined
+    }
+
+    // 能识别额的文件类型,可下载
+    buttonNodes.push(downloadButtonNode)
+
+    if (fileSuffixSupport.preview.includes(fileSuffix)) {
+      // 可查看的文件类型
+      buttonNodes.push(previewButtonNode)
+    }
+  }
+
+  return () =>
+    h(
+      'div',
+      { style: { display: 'flex', gap: '8px' } },
+      [
+        ...buttonNodes
+      ]
+    )
+}
+
 const transformStructNode = (data: BatchDownloadStructNode[], baseKey?: string): TreeOption[] => {
   return data.map<TreeOption>(d => {
     const newKey = !d.isFile && baseKey !== undefined ? baseKey + '/' + d.name : d.name
@@ -21,18 +129,7 @@ const transformStructNode = (data: BatchDownloadStructNode[], baseKey?: string):
       key: newKey,
       isLeaf: (d.children == null || d.children.length == 0) && !d.name.endsWith(".tar.gz"),
       children: d.children == null || d.children.length == 0 ? undefined : transformStructNode(d.children, newKey),
-      suffix: !d.isFile ? undefined : () => h(
-        NButton,
-        {
-          text: true,
-          type: 'primary',
-          onClick(e) {
-            e.stopPropagation()
-            getBatchDownloadFilePath(d.name)
-          },
-        },
-        { default: () => "下载" }
-      )
+      suffix: !d.isFile ? undefined : generateButtonFromFileSuffix(d.name)
     }
   })
 }
@@ -46,18 +143,7 @@ const transformTarNode = (data: BatchDownloadTarNode[], filePath: string): TreeO
       key: d.name,
       isLeaf: d.children == null || d.children.length == 0,
       children: d.children == null || d.children.length == 0 ? undefined : transformTarNode(d.children, filePath),
-      suffix: d.isDir ? undefined : () => h(
-        NButton,
-        {
-          text: true,
-          type: 'primary',
-          onClick(e) {
-            e.stopPropagation()
-            getBatchDownloadFilePath(filePath, d.path)
-          },
-        },
-        { default: () => "下载" }
-      )
+      suffix: d.isDir ? undefined : generateButtonFromFileSuffix(filePath, d.path)
     }
   })
 }
@@ -100,4 +186,49 @@ const handleLoad = (node: TreeOption) => {
   })
 }
 
+const previewFile = async (filePath: string, innerPath?: string) => {
+  const fileContent = await getBatchDownloadFile(filePath, innerPath)
+  if (fileContent === undefined) {
+    message.error("文件读取失败!")
+    return
+  }
+
+  const suffixWith = (suffix: string[]): boolean => {
+    return suffix.find((v) => innerPath ? endsWithIgnoreCase(innerPath, v) : endsWithIgnoreCase(filePath, v)) !== undefined
+  }
+
+  if (suffixWith(['.html'])) {
+    previewHtmlFile(fileContent)
+  } else if (suffixWith(['.csv', '.xlsx'])) {
+    previewExcelFile(fileContent)
+  } else if (suffixWith(['.log'])) {
+    previewLogFile(fileContent)
+  }
+}
+
+// html文件预览方法
+const previewHtmlFile = (blob: Blob) => {
+  const blobUrl = URL.createObjectURL(blob);
+
+  const a = document.createElement("a");
+  a.href = blobUrl;
+  a.target = "_blank";
+  a.click();
+
+  // 延迟释放(推荐)
+  setTimeout(() => {
+    URL.revokeObjectURL(blobUrl);
+  }, 10000);
+}
+
+// csv/xlsx文件预览方法
+const previewExcelFile = (blob: Blob) => {
+  showFullscreenPreview.value = true
+}
+
+// log文件预览方法
+const previewLogFile = (blob: Blob) => {
+  showFullscreenPreview.value = true
+}
+
 </script>

+ 19 - 3
src/views/batch-download/index.vue

@@ -66,7 +66,7 @@ import { NButton, NDataTable, NForm, NNumberAnimation, NInput, NEllipsis, NDrawe
 import type { DataTableColumns, PaginationProps, DropdownProps, EllipsisProps } from 'naive-ui'
 import { useDataTable } from '@/composable/comp/useDataTable'
 import { useResettableReactive } from '@/composable/useResettable'
-import { getBatchDownloadList, createBatchDownload as createBatchDownloadTask, getBatchDownloadPackage, getBatchDownloadPath } from '@/service/batchDownload'
+import { getBatchDownloadList, createBatchDownload as createBatchDownloadTask, getBatchDownloadPackage, getBatchDownloadPath, updateBatchDownloadOwner } from '@/service/batchDownload'
 import type { BatchDownloadFilter, BatchDownloadInfo, BatchDownloadConfig } from '@/service/batchDownload/interface'
 import ModalData from './ModalData.vue'
 import ViewPathDialog from './component/viewPathDialog.vue'
@@ -242,6 +242,14 @@ function createColumns(): DataTableColumns<BatchDownloadInfo> {
       key: 'progress',
       width: 100,
     },
+    {
+      title: '所有者',
+      key: 'owner',
+      render(rowData, rowIndex) {
+        if (rowData.owner === undefined) return 'admin'
+        return ['admin', ...rowData.owner].join(', ')
+      }
+    },
     {
       title: '任务结果',
       key: 'failReasons',
@@ -367,7 +375,10 @@ const itemViewInfo = (info: BatchDownloadInfo) => {
   const optionStr = "Options:\n" + powerFlowStr + '\n' + mqttLogsStr + '\n' + eventAlarmStr + '\n' + alarmSnapshotStr + '\n' + emsLogsStr + '\n' + faultRecordStr + '\n' + updateLogStr + '\n' + settingsStr
   const progressStr = `Progress: ${info.progress}`
   const failReasonsStr = "Fail Reasons:\n" + info.failReasons.map(fail => `\t${fail}`).join("\n")
-  const contextStr = idStr + "\n" + snStr + "\n" + descStr + "\n" + periodStr + "\n" + optionStr + "\n" + progressStr + "\n" + failReasonsStr
+  let contextStr = idStr + "\n" + snStr + "\n" + descStr + "\n" + periodStr + "\n" + optionStr + "\n" + progressStr + "\n" + failReasonsStr
+  if (info.owner !== undefined) {
+    contextStr = contextStr + "\n" + `[${['admin', ...info.owner].join(', ')}]`
+  }
 
   drawerTitle.value = "任务详情"
   drawerContext.value = contextStr
@@ -378,7 +389,12 @@ const itemViewPath = async (id: string) => {
   const pathList = await getBatchDownloadPath(id)
   dialog.info({
     title: "数据目录",
-    content: () => h(ViewPathDialog, { data: pathList })
+    content: () => h(ViewPathDialog, { data: pathList }),
+    style: {
+      minWidth: '600px',
+      width: 'fit-content',
+      maxWidth: '90vw'
+    }
   })
 }
 

+ 19 - 4
src/views/batch-download/page/viewLog.vue

@@ -1,20 +1,35 @@
 <template>
   <div style="height: 400px;">
-    <CodeEditor v-model:value="code" language="javascript" theme="vs-dark" :options="editorOptions" />
+    <CodeEditor v-model:value="code" language="javascript" theme="vs-dark" :options="editorOptions"
+      @editor-did-mount="editorDidMount" />
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref } from 'vue';
-import { CodeEditor } from 'monaco-editor-vue3';
+import type { EditorOptions } from 'monaco-editor-vue3';
+import type { editor } from 'monaco-editor'
+import { rules } from 'unocss/preset-wind4';
+
+const monacoEditor = await import('monaco-editor')
+
+// 改为动态载入,提升加载速度
+const CodeEditor = defineAsyncComponent(() =>
+  import('monaco-editor-vue3')
+);
+
+const editorDidMount = (editor: editor.IStandaloneCodeEditor) => {
+  editor
+}
 
 const code = ref(`function hello() {
   console.log('Hello, Monaco Editor Vue3!');
 }`);
 
-const editorOptions = {
+const editorOptions: EditorOptions = {
   fontSize: 14,
   minimap: { enabled: false },
-  automaticLayout: true
+  automaticLayout: true,
+
 };
 </script>

+ 3 - 0
src/views/local-utils/code-diagnosis/func/codeAnalysis.ts

@@ -0,0 +1,3 @@
+export function codeAnalysis(codes: string[]) {
+  
+}

+ 54 - 0
src/views/local-utils/code-diagnosis/index.vue

@@ -0,0 +1,54 @@
+<template>
+  <NDynamicTags v-model:value="alarmCodes" :render-tag="renderAlarmCode">
+    <template #input="{ submit, deactivate }">
+      <NAutoComplete :options="computedAlarmCode" size="small" placeholder="告警码"
+        @select="(value: string) => submit(value)" @blur="deactivate" />
+    </template>
+  </NDynamicTags>
+  <NSteps vertical :current="current as number" :status="currentStatus">
+    <NStep title="I Me Mine" description="All through the day, I me mine I me mine, I me mine" />
+    <NStep title="Let It Be" description="When I find myself in times of trouble Mother Mary comes to me" />
+    <NStep title="Break" />
+    <NStep title="Come Together" description="Here come old flat top He come grooving up slowly" />
+    <NStep title="Something" description="Something in the way she moves Attracts me like no other lover" />
+  </NSteps>
+</template>
+
+<script setup lang="ts">
+import { NDynamicTags, NSteps, NStep, NTag } from "naive-ui"
+import type { StepsProps } from 'naive-ui'
+
+const alarmCodes = ref([])
+
+function renderAlarmCode(tag: string, index: number) {
+  return h(
+    NTag,
+    {
+      type: index < 3 ? 'success' : 'error',
+      disabled: index > 3,
+      closable: true,
+      onClose: () => {
+        alarmCodes.value.splice(index, 1)
+      }
+    },
+    {
+      default: () => tag
+    }
+  )
+}
+
+const valueRef = ref('')
+const computedAlarmCode = computed(() => {
+  return ['@gmail.com', '@163.com', '@qq.com'].map((suffix) => {
+    const prefix = valueRef.value.split('@')[0]
+    return {
+      label: prefix + suffix,
+      value: prefix + suffix
+    }
+  })
+})
+
+const current = ref<number | undefined>(1)
+const currentStatus = ref<StepsProps['status']>('process')
+
+</script>

+ 6 - 10
src/views/local-utils/log-357-parser/func/parseData.ts

@@ -64,8 +64,8 @@ export interface TimeRangeSchedulingInfo {
   startTime: string // 开始时间 "06:30"
   endTime: string // 结束时间 "07:30"
   flag: number // 峰平谷 0:谷;1:平;2:峰;3:尖峰;4:低谷
-  name: string // 子策略名
-  describe: string // 描述
+  name?: string // 子策略名
+  describe?: string // 描述
   solarPriority?: BuildTuple<3, number> // 光伏电能优先级 [0]:市电, [1]:负载, [2]:FHP;优先级为0/1/2/3,可相同,若相同则表明需要按比例或者功率分配,0为不涉及,数字越大优先级越高
   loadPriority?: BuildTuple<3, number> // 负载用电优先级 可选,[0]:市电, [1]:光伏, [2]:FHP;优先级为0/1/2/3,可相同,若相同则表明需要按比例或者功率分配,0为不涉及,数字越大优先级越高
   gridDischargeMax?: number // 电池最大允许馈网功率 可选,默认不能馈网;0:不可馈网,-1:不限制
@@ -260,14 +260,10 @@ export function parse357Log(
           const flag = fieldCheck<'number'>(v, 'flag', 'number', errorCallback)
           if (flag === undefined) return false
 
-          const name = fieldCheck<'string'>(v, 'name', 'string', errorCallback)
-          if (name === undefined) return false
-
-          const describe = fieldCheck<'string'>(v, 'describe', 'string', errorCallback)
-          if (describe === undefined) return false
-
+          const name = fieldCheck<'string'>(v, 'name', 'string', errorCallback, true)
+          const describe = fieldCheck<'string'>(v, 'describe', 'string', errorCallback, true)
           const solarPriorityObject = fieldCheck<'array'>(
-            data,
+            v,
             'solar_priority',
             'array',
             errorCallback,
@@ -288,7 +284,7 @@ export function parse357Log(
               : (solarPriorityObject as BuildTuple<3, number> | undefined)
 
           const loadPriorityObject = fieldCheck<'array'>(
-            data,
+            v,
             'load_priority',
             'array',
             errorCallback,

+ 29 - 50
src/views/local-utils/log-357-parser/func/parseScheduling.ts

@@ -29,6 +29,7 @@ export type SchedulingType =
   | 'F/H'
   | 'G'
   | 'H'
+  | 'noPriority'
   | 'unknown'
 
 const SchedulingMap: Record<SchedulingType, string> = {
@@ -42,49 +43,45 @@ const SchedulingMap: Record<SchedulingType, string> = {
   'F/H': 'aPower prioritize home / aPower export to the grid(enable aPower export)',
   G: 'aPower charge from grid', // E 使能电网充电的版本
   H: 'aPower export to the grid', // F 使能aPower馈网的版本
+  noPriority: 'Self consumption (No priority)',
   unknown: 'unknown',
 }
 
+const SchedulingRules: {
+  match: [number, number, number, number, number, number]
+  type: SchedulingType
+}[] = [
+  { match: [1, 2, 0, 1, 3, 2], type: 'A' }, // Excess solar export to grid
+  { match: [1, 2, 0, 1, 2, 0], type: 'B' }, // aPower standby
+  { match: [1, 3, 2, 1, 2, 0], type: 'C' }, // Excess solar charge aPower
+  { match: [1, 3, 2, 1, 3, 2], type: 'D' }, // Self-consumption
+  { match: [1, 2, 3, 1, 2, 0], type: 'E/G' }, // aPower charge from solar
+  { match: [1, 2, 0, 1, 2, 3], type: 'F/H' }, // aPower prioritize home
+] as const
+
 export interface TimePeriodSchedulingBriefInfo {
   timePeriod: string // format: HH:MM - HH:MM
   brief: string
   priceColor: PriceColorType // 峰平谷颜色
 }
 
+function arrayEqual<T>(a: readonly T[], b: readonly T[]) {
+  return a.length === b.length && a.every((v, i) => v === b[i])
+}
+
 export function parseSchedulingBriefInfo(
   info: TimeRangeSchedulingInfo,
 ): TimePeriodSchedulingBriefInfo {
   const result: TimePeriodSchedulingBriefInfo = {
     timePeriod: `${info.startTime} - ${info.endTime}`,
     brief: ((): string => {
-      let schedulingType: SchedulingType = 'D' // 没有优先级时,默认 Self consumption
+      let schedulingType: SchedulingType = 'noPriority' // 没有优先级时,默认 Self consumption
 
       // 判断优先级
-      if (info.solarPriority != null && info.loadPriority != null) {
+      if (info.solarPriority !== undefined && info.loadPriority !== undefined) {
         const priority = [...info.solarPriority, ...info.loadPriority]
-        switch (priority) {
-          case [1, 2, 0, 1, 3, 2]: // A - Excess solar export to grid
-            schedulingType = 'A'
-            break
-          case [1, 2, 0, 1, 2, 0]: // B - aPower standby
-            schedulingType = 'B'
-            break
-          case [1, 3, 2, 1, 2, 0]: // C - Excess solar charge aPower
-            schedulingType = 'C'
-            break
-          case [1, 3, 2, 1, 3, 2]: // D - Self-consumption
-            schedulingType = 'D'
-            break
-          case [1, 2, 3, 1, 2, 0]: // E - aPower charge from solar
-            schedulingType = 'E/G'
-            break
-          case [1, 2, 0, 1, 2, 3]: // F - aPower prioritize home
-            schedulingType = 'F/H'
-            break
-          default:
-            schedulingType = 'unknown'
-            break
-        }
+        schedulingType =
+          SchedulingRules.find((rule) => arrayEqual(priority, rule.match))?.type ?? 'unknown'
       }
       return SchedulingMap[schedulingType]
     })(),
@@ -475,34 +472,13 @@ export function parseSchedulingInfo(info: TimeRangeSchedulingInfo): TimePeriodSc
       }
     })(),
     scheduling: ((): SchedulingType => {
-      let schedulingType: SchedulingType = 'D' // 没有优先级时,默认 Self consumption
+      let schedulingType: SchedulingType = 'noPriority' // 没有优先级时,默认 Self consumption
 
       // 判断优先级
-      if (info.solarPriority != null && info.loadPriority != null) {
+      if (info.solarPriority !== undefined && info.loadPriority !== undefined) {
         const priority = [...info.solarPriority, ...info.loadPriority]
-        switch (priority) {
-          case [1, 2, 0, 1, 3, 2]: // A - Excess solar export to grid
-            schedulingType = 'A'
-            break
-          case [1, 2, 0, 1, 2, 0]: // B - aPower standby
-            schedulingType = 'B'
-            break
-          case [1, 3, 2, 1, 2, 0]: // C - Excess solar charge aPower
-            schedulingType = 'C'
-            break
-          case [1, 3, 2, 1, 3, 2]: // D - Self-consumption
-            schedulingType = 'D'
-            break
-          case [1, 2, 3, 1, 2, 0]: // E - aPower charge from solar
-            schedulingType = 'E/G'
-            break
-          case [1, 2, 0, 1, 2, 3]: // F - aPower prioritize home
-            schedulingType = 'F/H'
-            break
-          default:
-            schedulingType = 'unknown'
-            break
-        }
+        schedulingType =
+          SchedulingRules.find((rule) => arrayEqual(priority, rule.match))?.type ?? 'unknown'
       }
       return schedulingType
     })(),
@@ -571,6 +547,7 @@ export function transformSchedulingType(type: SchedulingType): FlowChartConfig[]
       })
       break
     case 'D':
+    case 'noPriority': // 无优先级默认自发自用
       configList.push({
         schedulingName: SchedulingMap[type],
         schedulingTip: getSchedulingTip(type),
@@ -673,6 +650,8 @@ function getSchedulingTip(type: SchedulingType): string {
       return '(使能电网充电)光伏优先充电,多余供载,再剩余馈网\n充电不足市电补充'
     case 'H':
       return '(使能aPower馈网)aPower优先供载,不足光伏补充,再不足市电补充\n光伏供载剩余馈网\naPower满功率放电供载剩余馈网,光伏馈网'
+    case 'noPriority':
+      return '无优先级配置,默认自发自用'
     case 'unknown':
       return '未知调度'
   }

+ 218 - 0
src/views/test/index.vue

@@ -0,0 +1,218 @@
+<template>
+  <div class="main-wrap flex flex-col gap-y-4 p-4">
+    <NCard ref="mainCardRef" class="h-full" content-class="h-full flex  flex-col" :style="cardHeight">
+      <div class="flex items-center gap-2">
+        <NTooltip :show-arrow="false" placement="bottom-start" trigger="hover">
+          <template #trigger>
+            <NButton text :focusable="false" style="font-size: 18px">
+              <span class="i-ph:info" />
+            </NButton>
+          </template>
+          <div>
+            <span class="font-bold">使用说明</span>
+            <span>搜索</span>
+          </div>
+
+        </NTooltip>
+        <NPopover placement="bottom-start" trigger="manual" :show="showFilterMenu">
+          <template #trigger>
+            <NButton text :focusable="false" style="font-size: 18px" @click="openFilterMenu">
+              <span class="i-mdi:filter-cog-outline" />
+            </NButton>
+          </template>
+          <div>
+            <span>文本过滤条件,每个条件之间是或的关系,每个标签是与的关系。</span>
+            <NDivider style="margin: 8px 0;" />
+            <NDynamicInput v-model:value="filterConfig" :on-create="onCreateFilter">
+              <template #create-button-default>
+                新增过滤条件
+              </template>
+              <template #default="{ value }">
+                <div style="display: flex; align-items: center; width: 100%">
+                  <NCheckbox v-model:checked="value.active" style="margin-right: 12px" />
+                  <NDynamicTags v-model:value="value.tags" />
+                </div>
+              </template>
+            </NDynamicInput>
+            <NDivider style="margin: 8px 0;" />
+            <div class="flex justify-end gap-4">
+              <NButton class="px-3 py-1 border rounded" text :focusable="false" style="font-size: 18px"
+                @click="closeFilterMenu">
+                <span class="i-mdi:close" />
+              </NButton>
+              <NButton class="px-3 py-1 border rounded" text :focusable="false" style="font-size: 18px"
+                @click="submitFilterMenu">
+                <span class="i-mdi:check" />
+              </NButton>
+            </div>
+          </div>
+        </NPopover>
+        <NButton text :focusable="false" style="font-size: 18px" class="transition-colors duration-200"
+          :class="cancelAllFilter ? 'text-green-500' : 'text-gray-400'" @click="() => {
+            cancelAllFilter = !cancelAllFilter
+            console.log({ cancelAllFilter })
+          }">
+          <span class="i-mdi:filter-off-outline" />
+        </NButton>
+        <NButton text :focusable="false" style="font-size: 18px">
+          <span class="i-ph:faders" />
+        </NButton>
+        <NButton text :focusable="false" style="font-size: 18px" @click="openSearch">
+          <span class="i-mdi:search" />
+        </NButton>
+      </div>
+      <NDivider style="margin: 16px 0;" />
+      <LogMonacoEditor class="flex-1" ref="logMonacoEditorRef" v-model="editorTxt" />
+    </NCard>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { NCard, NButton, NDivider, NTooltip, NPopover, NDynamicInput, NCheckbox, NDynamicTags } from 'naive-ui'
+import { useFullCard } from '@/composable/comp/useFullCard'
+import LogMonacoEditor from '@/utils/monaco/components/LogMonacoEditor.vue'
+
+const mainCardRef = useTemplateRef<InstanceType<typeof NCard>>('mainCardRef')
+const { height } = useFullCard(mainCardRef)
+
+const logMonacoEditorRef = useTemplateRef<InstanceType<typeof LogMonacoEditor>>('logMonacoEditorRef')
+
+// 禁用所有过滤
+const cancelAllFilter = ref(false)
+
+interface FilterConfigType {
+  active: boolean
+  tags: string[]
+}
+
+interface UserConfig {
+  filter: {
+    disable: boolean
+    config: FilterConfigType[]
+  }
+}
+
+const cardHeight = computed(() => {
+  return { height: (height.value === undefined || height.value === 0 ? 480 : height.value) + 'px' }
+})
+
+const filterConfig = ref<FilterConfigType[]>([])
+const onCreateFilter = () => {
+  return {
+    active: true,
+    tags: []
+  }
+}
+
+const userConfig = reactive<UserConfig>({
+  filter: {
+    disable: false,
+    config: []
+  }
+})
+
+const showFilterMenu = ref(false)
+
+const openFilterMenu = () => {
+  showFilterMenu.value = true
+}
+
+const closeFilterMenu = () => {
+  showFilterMenu.value = false
+}
+
+const submitFilterMenu = () => {
+  showFilterMenu.value = false
+}
+
+const openSearch = () => {
+  logMonacoEditorRef.value?.openFind()
+}
+
+const editorTxt = ref<string>(`[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[E] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[ERROR] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:56.002-0500] [pv_manager] [CxMpptPvCtrl.cpp:976] get sun rise min=473, sunset min=1042
+[I] [20260105,22:11:56.723-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:58.252-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:11:59.790-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:00.397-0500] [mppt#>10] [pv_manager] [CxMpptPvCtrl.cpp:829] loadpwr=0,exptPwr=0,pvPowr=0,mpptpv=0,m_sysMaxUsePwr=0,m_agateMaxUsePwr=0
+[I] [20260105,22:12:00.744-0500] [black_start] [CxBlackStart.cpp:626] BSCurState = BS_STANDBY, DSPState=5,maxDchSoc=69.300003,aPower i=0,PEstate=3,BMSState=5,soc=69.300003, maxChgPwr=7995,maxDchPwr=11500,bmSleep=0
+[I] [20260105,22:12:01.321-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:02.853-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:04.384-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+[I] [20260105,22:12:05.914-0500] [energy_manager] [CxPowerAllocationSupGen.cpp:307] mode = 0, gridPw = 0, solarPw = 0 loadPv=0 linePv=0 fphpw = 0, genpw=0, ld = 0,appLimpwr=0,tolAllocPwr=0, tolPw = 0, type=2
+`)
+
+// setTimeout(() => {
+//   editorTxt.value = "[I] [20260105,22:12:05.914-0500]"
+// }, 10000);
+
+</script>

+ 5 - 4
vite.config.ts

@@ -52,8 +52,9 @@ export default defineConfig(({ mode }) => {
     },
     build: {
       sourcemap: true, // ✅ 生成 source map
-      // rolldownOptions: {
-      //   output: {
+      rolldownOptions: {
+        output: {
+          
       //     advancedChunks: {
       //       includeDependenciesRecursively: true,
       //       minSize: 50 * 1024, // 全局最小 50KB
@@ -103,8 +104,8 @@ export default defineConfig(({ mode }) => {
       //         },
       //       ],
       //     },
-      //   },
-      // },
+        },
+      },
     },
   }
 })

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini