Explorar o código

feat:新增下载文件浏览功能(开发中。。。)

Jerry Liang hai 8 meses
pai
achega
6cd8e328ff
Modificáronse 46 ficheiros con 2616 adicións e 276 borrados
  1. 2 0
      package.json
  2. 49 32
      pnpm-lock.yaml
  3. BIN=BIN
      service/fwh-toolbox-server
  4. 1 0
      service/repositories/repositories.go
  5. 2 2
      src/composable/comp/useFullCard.ts
  6. 12 0
      src/router/record.ts
  7. 30 2
      src/service/batchDownload/index.ts
  8. 99 0
      src/service/blobCache.ts
  9. 74 2
      src/service/fetchClient.ts
  10. 1 1
      src/service/serverReq.ts
  11. 70 3
      src/stores/user.ts
  12. 0 54
      src/utils/log-monaco/LogMonacoEditor.vue
  13. 0 0
      src/utils/log-monaco/decorations/errorDecoration.ts
  14. 0 5
      src/utils/log-monaco/decorations/index.ts
  15. 0 28
      src/utils/log-monaco/editor.ts
  16. 0 17
      src/utils/log-monaco/index.ts
  17. 0 5
      src/utils/log-monaco/languages/index.ts
  18. 0 43
      src/utils/log-monaco/languages/log.ts
  19. 0 18
      src/utils/log-monaco/themes/dark.ts
  20. 0 17
      src/utils/log-monaco/workers/setup.ts
  21. 64 0
      src/utils/monaco/components/LogMonacoEditor.vue
  22. 21 0
      src/utils/monaco/editor.ts
  23. 1 0
      src/utils/monaco/env.ts
  24. 60 0
      src/utils/monaco/index.ts
  25. 8 0
      src/utils/monaco/languages/index.ts
  26. 4 0
      src/utils/monaco/languages/log/definitions.ts
  27. 128 0
      src/utils/monaco/languages/log/lspLanguageFeatures.ts
  28. 35 0
      src/utils/monaco/languages/log/monarch.ts
  29. 563 0
      src/utils/monaco/languages/log/provider.ts
  30. 52 0
      src/utils/monaco/languages/log/register.ts
  31. 23 0
      src/utils/monaco/languages/log/worker/log.worker.ts
  32. 20 0
      src/utils/monaco/languages/log/worker/logMode.ts
  33. 63 0
      src/utils/monaco/languages/log/worker/logWorker.ts
  34. 84 0
      src/utils/monaco/languages/log/worker/workerManager.ts
  35. 25 0
      src/utils/monaco/themes/custom.ts
  36. 165 0
      src/utils/monaco/themes/index.ts
  37. 260 0
      src/utils/monaco/themes/internalThemes.ts
  38. 4 0
      src/utils/monaco/workers/editor.worker.d.ts
  39. 7 0
      src/utils/monaco/workers/editor.worker.start.d.ts
  40. 40 0
      src/utils/monaco/workers/setup.ts
  41. 226 0
      src/views/batch-download/component/fullscreenPreview.vue
  42. 157 26
      src/views/batch-download/component/viewPathDialog.vue
  43. 6 1
      src/views/batch-download/index.vue
  44. 3 2
      src/views/batch-download/page/viewLog.vue
  45. 39 18
      src/views/local-utils/code-diagnosis/index.vue
  46. 218 0
      src/views/test/index.vue

+ 2 - 0
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",

+ 49 - 32
pnpm-lock.yaml

@@ -80,6 +80,12 @@ importers:
       es-toolkit:
         specifier: ^1.39.8
         version: 1.40.0
+      idb:
+        specifier: ^8.0.3
+        version: 8.0.3
+      jwt-decode:
+        specifier: ^4.0.0
+        version: 4.0.0
       monaco-editor:
         specifier: ^0.55.1
         version: 0.55.1
@@ -1565,43 +1571,43 @@ packages:
     resolution: {integrity: sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260128.1':
-    resolution: {integrity: sha512-rPyH+d+M8NnYA9FOOB9xPtllB2jtDiB7vnLDGYVjE7y8j3XNMEHFYlwoZ5HLS2yLCgwmmcmEUgurJX94V9Iqeg==}
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260304.1':
+    resolution: {integrity: sha512-TnTUxYt+dShRSoeOldx7VlKoEG+bvPHnyPEBImlNc7c3WP0AHYyNHrNg6EbLbzkOorARtd06J3Vk+XYzkrRzZg==}
     cpu: [arm64]
     os: [darwin]
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260128.1':
-    resolution: {integrity: sha512-8zVZNktu8n6+kvWp9x6vb4J2uD51q58mB497ki0Fxf1q2WPTsPFjBUanFZc8K5hltmaoijF1214lN/KNmhVSqg==}
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260304.1':
+    resolution: {integrity: sha512-1nwXX1zbyYI3sDKdaR8NsBdM7LmE0J6OzVtlWgEJ/8YR7oC2/HY6/SfShF3DHHcEOHOFxRLbkJ9zVTJJspWLCw==}
     cpu: [x64]
     os: [darwin]
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260128.1':
-    resolution: {integrity: sha512-xcf3OW/bh6gA+urfG3ofHnfIBhKSqHtFolk2Akd/ZGLiUJik4mimhwlrqx9qus8tGFI3BepdCgfSNhW61fTrhw==}
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260304.1':
+    resolution: {integrity: sha512-cw+xqroXtsk/yVTKbelcPWMd6oZdET9kNWmigyc189KWwzOu2eq2EPXPQsrhEigq8O3j0xW0z3q2oqG+smOiXg==}
     cpu: [arm64]
     os: [linux]
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20260128.1':
-    resolution: {integrity: sha512-ZOKLNjuT5QFkbuJvS3cBTruV22gAllWRqhgFaMYIorT7WcTzEci2S5RfEz1NzYPYZeY4vJssMmr/O8wdPoMpMA==}
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20260304.1':
+    resolution: {integrity: sha512-TXZClCJVteK2f9gcI+I7o1Sxgq3qdMtraXOP9GZF8o0sKCLdDWENN8uORfZSeQv2qOJohcKvrrEz6LLSSngvEg==}
     cpu: [arm]
     os: [linux]
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20260128.1':
-    resolution: {integrity: sha512-qr9gTFYXCS83Sro+rBnw3fZIPRMBR9j/9FS5KtPRBOqn4jpiAR64CAE1NDHtTIBYwgPbpd6kvt8Ay5c+S4zxPQ==}
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20260304.1':
+    resolution: {integrity: sha512-EXufnN4PG0HYBHYbHXQXXRXtaQKuKBT3e6nxPhKnwpBBgy2MgWDIxzroTLvI9+SllhbJQzHNZOWiB+SU+KdCNw==}
     cpu: [x64]
     os: [linux]
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260128.1':
-    resolution: {integrity: sha512-8Ke5A+MNaiUqaX6nEDpXXmQqfQBBq4CtN6mGEQs5a5QCnyqcnjy13QlJuCxzRd/nvob7lWIxR6kz43Fdn8l5ig==}
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260304.1':
+    resolution: {integrity: sha512-Be9yyDDbT/PEdNlhG+NXT47fwuiIeN0+/9BkeRKkiLgzY8DqQIC9w5FRWmwAJ+9PVa2sKr5cjD1SpJDHGrPIrA==}
     cpu: [arm64]
     os: [win32]
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20260128.1':
-    resolution: {integrity: sha512-+r0hFY/fnQUY5z3lcVNWz5j2G2RBxkQVzRAvbQyl2esGt0scy/6dPeP/PrvHFADWoVJkV9DTSBhbmsJfL1inKw==}
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20260304.1':
+    resolution: {integrity: sha512-lg/w+rZ9NIUoqSsk2TbtDsqyD9nW0/rhTMYd14RFP7vuNijLrTbl7GPiMhFtMxaqCSOFapwbql7/3lU4BKHB6g==}
     cpu: [x64]
     os: [win32]
 
-  '@typescript/native-preview@7.0.0-dev.20260128.1':
-    resolution: {integrity: sha512-P7JZ3NpFdH1FqA8TbAKTsufOvykywFAW36H+3ZzUo9PRz/wmTPGkzn/CQ1QM2/YqxWwqLkU96CLeh8onIpj4nw==}
+  '@typescript/native-preview@7.0.0-dev.20260304.1':
+    resolution: {integrity: sha512-Xj0ZeHEy+yJ/bIg6psPwl0POvBf1j5u7IZAXsUqgvgWbMIvdM9JOGmhpifcj6j28LcXM6GTvXUoXwlatxJ73Qg==}
     hasBin: true
 
   '@umami/node@0.4.0':
@@ -3149,6 +3155,9 @@ packages:
     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==}
     engines: {node: '>=0.10.0'}
 
+  idb@8.0.3:
+    resolution: {integrity: sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==}
+
   ignore@5.3.2:
     resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
     engines: {node: '>= 4'}
@@ -3347,6 +3356,10 @@ packages:
   jsonfile@6.2.0:
     resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
 
+  jwt-decode@4.0.0:
+    resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==}
+    engines: {node: '>=18'}
+
   keyv@4.5.4:
     resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
 
@@ -6387,36 +6400,36 @@ snapshots:
       '@typescript-eslint/types': 8.46.1
       eslint-visitor-keys: 4.2.1
 
-  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260128.1':
+  '@typescript/native-preview-darwin-arm64@7.0.0-dev.20260304.1':
     optional: true
 
-  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260128.1':
+  '@typescript/native-preview-darwin-x64@7.0.0-dev.20260304.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260128.1':
+  '@typescript/native-preview-linux-arm64@7.0.0-dev.20260304.1':
     optional: true
 
-  '@typescript/native-preview-linux-arm@7.0.0-dev.20260128.1':
+  '@typescript/native-preview-linux-arm@7.0.0-dev.20260304.1':
     optional: true
 
-  '@typescript/native-preview-linux-x64@7.0.0-dev.20260128.1':
+  '@typescript/native-preview-linux-x64@7.0.0-dev.20260304.1':
     optional: true
 
-  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260128.1':
+  '@typescript/native-preview-win32-arm64@7.0.0-dev.20260304.1':
     optional: true
 
-  '@typescript/native-preview-win32-x64@7.0.0-dev.20260128.1':
+  '@typescript/native-preview-win32-x64@7.0.0-dev.20260304.1':
     optional: true
 
-  '@typescript/native-preview@7.0.0-dev.20260128.1':
+  '@typescript/native-preview@7.0.0-dev.20260304.1':
     optionalDependencies:
-      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260128.1
-      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260128.1
-      '@typescript/native-preview-linux-arm': 7.0.0-dev.20260128.1
-      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260128.1
-      '@typescript/native-preview-linux-x64': 7.0.0-dev.20260128.1
-      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260128.1
-      '@typescript/native-preview-win32-x64': 7.0.0-dev.20260128.1
+      '@typescript/native-preview-darwin-arm64': 7.0.0-dev.20260304.1
+      '@typescript/native-preview-darwin-x64': 7.0.0-dev.20260304.1
+      '@typescript/native-preview-linux-arm': 7.0.0-dev.20260304.1
+      '@typescript/native-preview-linux-arm64': 7.0.0-dev.20260304.1
+      '@typescript/native-preview-linux-x64': 7.0.0-dev.20260304.1
+      '@typescript/native-preview-win32-arm64': 7.0.0-dev.20260304.1
+      '@typescript/native-preview-win32-x64': 7.0.0-dev.20260304.1
 
   '@umami/node@0.4.0': {}
 
@@ -8243,6 +8256,8 @@ snapshots:
     dependencies:
       safer-buffer: 2.1.2
 
+  idb@8.0.3: {}
+
   ignore@5.3.2: {}
 
   ignore@7.0.5: {}
@@ -8408,6 +8423,8 @@ snapshots:
     optionalDependencies:
       graceful-fs: 4.2.11
 
+  jwt-decode@4.0.0: {}
+
   keyv@4.5.4:
     dependencies:
       json-buffer: 3.0.1
@@ -10148,7 +10165,7 @@ snapshots:
 
   vue-vine-tsc@1.7.13(typescript@5.8.3):
     dependencies:
-      '@typescript/native-preview': 7.0.0-dev.20260128.1
+      '@typescript/native-preview': 7.0.0-dev.20260304.1
       '@volar/language-core': 2.4.20
       '@volar/typescript': 2.4.20
       '@vue-vine/language-service': 1.7.13(typescript@5.8.3)

BIN=BIN
service/fwh-toolbox-server


+ 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

+ 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(() => {

+ 12 - 0
src/router/record.ts

@@ -429,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,
+    },
+  },
 ]

+ 30 - 2
src/service/batchDownload/index.ts

@@ -10,6 +10,7 @@
 // /batchDownload/manage/fileDownload - GET 下载指定文件
 
 import http from '@/service/serverReq'
+import { blobCache } from '@/service/blobCache'
 import type {
   BatchDownloadFilter,
   BatchDownloadItemPage,
@@ -62,7 +63,7 @@ export const updateBatchDownloadDescription = async (id: string, description: st
 // 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})
+  const res = await http.post(url, { owner })
 }
 
 // DELETE 删除指定id配置
@@ -98,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,
@@ -106,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
+}

+ 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()

+ 74 - 2
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)
     }
 
@@ -289,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) || []

+ 0 - 54
src/utils/log-monaco/LogMonacoEditor.vue

@@ -1,54 +0,0 @@
-<script setup lang="ts">
-import { onMounted, onBeforeUnmount, ref, watch } from 'vue'
-import * as monaco from 'monaco-editor'
-import { initMonaco } from '.'
-import { createEditor, createModel } from './editor'
-import { LOG_LANGUAGE_ID } from './languages/log'
-
-const props = defineProps<{
-  modelValue: string
-}>()
-
-const emit = defineEmits<{
-  (e: 'update:modelValue', v: string): void
-}>()
-
-const container = ref<HTMLDivElement>()
-
-let editor: monaco.editor.IStandaloneCodeEditor
-let model: monaco.editor.ITextModel
-
-onMounted(() => {
-  initMonaco()
-
-  model = createModel(props.modelValue, LOG_LANGUAGE_ID)
-
-  editor = createEditor(container.value!, {
-    model,
-    theme: 'log-dark',
-    readOnly: false
-  })
-
-  editor.onDidChangeModelContent(() => {
-    emit('update:modelValue', model.getValue())
-  })
-})
-
-watch(
-  () => props.modelValue,
-  v => {
-    if (v !== model?.getValue()) {
-      model?.setValue(v)
-    }
-  }
-)
-
-onBeforeUnmount(() => {
-  editor?.dispose()
-  model?.dispose()
-})
-</script>
-
-<template>
-  <div ref="container" style="width: 100%; height: 100%" />
-</template>

+ 0 - 0
src/utils/log-monaco/decorations/errorDecoration.ts


+ 0 - 5
src/utils/log-monaco/decorations/index.ts

@@ -1,5 +0,0 @@
-
-
-export function registerDecorations() {
-
-}

+ 0 - 28
src/utils/log-monaco/editor.ts

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

+ 0 - 17
src/utils/log-monaco/index.ts

@@ -1,17 +0,0 @@
-import * as monaco from 'monaco-editor'
-import { setupMonacoWorkers } from './workers/setup'
-import { registerDarkTheme } from './themes/dark'
-import { registerLanguages } from './languages'
-
-let initialized = false
-
-export function initMonaco() {
-  if (initialized) return monaco
-  initialized = true
-
-  setupMonacoWorkers()
-  registerDarkTheme()
-  registerLanguages()
-
-  return monaco
-}

+ 0 - 5
src/utils/log-monaco/languages/index.ts

@@ -1,5 +0,0 @@
-import { registerLogLanguage } from './log'
-
-export function registerLanguages() {
-  registerLogLanguage()
-}

+ 0 - 43
src/utils/log-monaco/languages/log.ts

@@ -1,43 +0,0 @@
-import * as monaco from 'monaco-editor'
-
-export const LOG_LANGUAGE_ID = 'log'
-
-export function registerLogLanguage() {
-  monaco.languages.register({ id: LOG_LANGUAGE_ID })
-
-  monaco.languages.setMonarchTokensProvider(LOG_LANGUAGE_ID, {
-    tokenizer: {
-      root: [
-        // 时间戳
-        [/\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?/, 'log.timestamp'],
-
-        // level
-        [/\b(\[INFO\]|\[Info\]|\[info\]|\[I\])\b/, 'log.info'],
-        [/\b(\[WARN\]|\[Warn\]|\[warn\]|\[WARNING\]|\[W\])\b/, 'log.warn'],
-        [/\b(\[ERROR\]|\[Error\]|\[error\]|\[E\])\b/, 'log.error'],
-        [/\b(\[DEBUG\]|\[Debug\]|\[debug\]|\[D\])\b/, 'log.debug'],
-
-        // 数字
-        [/\b\d+\b/, 'number'],
-
-        // 路径
-        [/(\/[a-zA-Z0-9._-]+)+/, 'string']
-      ]
-    }
-  })
-
-  // 悬浮提示(可选)
-  monaco.languages.registerHoverProvider(LOG_LANGUAGE_ID, {
-    provideHover(model, position) {
-      const word = model.getWordAtPosition(position)
-      if (!word) return null
-
-      if (word.word.toUpperCase() === 'ERROR') {
-        return {
-          contents: [{ value: '**ERROR** 日志级别' }]
-        }
-      }
-      return null
-    }
-  })
-}

+ 0 - 18
src/utils/log-monaco/themes/dark.ts

@@ -1,18 +0,0 @@
-import * as monaco from 'monaco-editor'
-
-export function registerDarkTheme() {
-  monaco.editor.defineTheme('log-dark', {
-    base: 'vs-dark',
-    inherit: true,
-    rules: [
-      { token: 'log.info', foreground: '4FC1FF' },
-      { token: 'log.warn', foreground: 'D7BA7D' },
-      { token: 'log.error', foreground: 'F44747' },
-      { token: 'log.debug', foreground: 'B5CEA8' },
-      { token: 'log.timestamp', foreground: '808080' }
-    ],
-    colors: {
-      'editor.background': '#0f0f0f'
-    }
-  })
-}

+ 0 - 17
src/utils/log-monaco/workers/setup.ts

@@ -1,17 +0,0 @@
-export function setupMonacoWorkers() {
-  self.MonacoEnvironment = {
-    getWorker(_, label) {
-      if (label === 'json') {
-        return new Worker(
-          new URL('monaco-editor/esm/vs/language/json/json.worker', import.meta.url),
-          { type: 'module' }
-        )
-      }
-
-      return new Worker(
-        new URL('monaco-editor/esm/vs/editor/editor.worker', import.meta.url),
-        { type: 'module' }
-      )
-    }
-  }
-}

+ 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>

+ 6 - 1
src/views/batch-download/index.vue

@@ -389,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'
+    }
   })
 }
 

+ 3 - 2
src/views/batch-download/page/viewLog.vue

@@ -1,13 +1,14 @@
 <template>
   <div style="height: 400px;">
-    <CodeEditor v-model:value="code" language="javascript" theme="vs-dark" :options="editorOptions" @editor-did-mount="editorDidMount" />
+    <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 type { EditorOptions } from 'monaco-editor-vue3';
-import type { editor }  from 'monaco-editor'
+import type { editor } from 'monaco-editor'
 import { rules } from 'unocss/preset-wind4';
 
 const monacoEditor = await import('monaco-editor')

+ 39 - 18
src/views/local-utils/code-diagnosis/index.vue

@@ -2,32 +2,53 @@
   <NDynamicTags v-model:value="alarmCodes" :render-tag="renderAlarmCode">
     <template #input="{ submit, deactivate }">
       <NAutoComplete :options="computedAlarmCode" size="small" placeholder="告警码"
-        @select="(value) => submit(value)" @blur="deactivate" />
+        @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="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"
-    />
+    <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 } from "naive-ui"
+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>

+ 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>