Преглед изворни кода

feat: 实现Cordova环境日志保存功能,支持文件系统本地存储

- 在src/log/logger.js中添加Cordova环境检测和动态日志写入器导入
- 新增src/log/cordova-log-writer.js,实现基于cordova-plugin-file的文件系统日志保存
- 支持2MB文件大小限制和自动轮转机制,文件命名格式为YYYY-MM-DD_XXX.log
- 集成到Android构建流程中,自动安装cordova-plugin-file插件
- 创建详细的使用文档src/log/cordova-log-writer.js.md
- 更新CHANGELOG.md记录新功能
- 版本号从1.9.0升级到1.10.0

改动文件:
- src/log/logger.js
- src/log/cordova-log-writer.js (新增)
- src/log/cordova-log-writer.js.md (新增)
- .build/build-android.js
- CHANGELOG.md
- package.json
dengdx пре 4 недеља
родитељ
комит
5634148912
6 измењених фајлова са 390 додато и 3 уклоњено
  1. 1 0
      .build/build-android.js
  2. 30 0
      CHANGELOG.md
  3. 1 1
      package.json
  4. 237 0
      src/log/cordova-log-writer.js
  5. 109 0
      src/log/cordova-log-writer.js.md
  6. 12 2
      src/log/logger.js

+ 1 - 0
.build/build-android.js

@@ -507,6 +507,7 @@ function buildAndroidApk() {
   const cordovaJsDst = path.join(dstDir, 'cordova.js');
   fs.copyFileSync(cordovaJsSrc, cordovaJsDst);
   console.log('✅ 已复制 cordova.js 到 www 目录');
+  execSync('npx cordova plugin add cordova-plugin-file', { cwd: cordovaPrjDir, stdio: 'inherit' });
   // 4. 安装插件 :  为了本地保存网络配置等数据
   execSync('npx cordova plugin add cordova-plugin-nativestorage', { cwd: cordovaPrjDir, stdio: 'inherit' });
 

+ 30 - 0
CHANGELOG.md

@@ -5,6 +5,36 @@
 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
 版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
 
+## [1.10.0] - 2025-12-15 20:27
+
+### 新增 (Added)
+- **Cordova环境日志保存功能** ([src/log/](src/log/))
+  - 实现Cordova环境下使用文件系统保存日志,支持本地离线存储
+  - 新增CordovaLogWriter类,支持2MB文件大小限制和自动轮转机制
+  - 文件命名格式:`YYYY-MM-DD_XXX.log`,XXX从000开始递增
+  - 集成到主日志系统中,支持动态环境检测和写入器选择
+  - 自动等待Cordova deviceready事件,确保文件系统可用性
+  - 完善的错误处理,避免应用崩溃
+
+**核心功能:**
+- 文件大小轮转:超过2MB自动创建新文件,避免单文件过大
+- 异步写入:不阻塞应用主线程,提升用户体验
+- 跨平台兼容:支持Android和iOS的文件系统路径
+- 错误容忍:文件操作失败时只记录警告,不抛出异常
+
+**技术实现:**
+- 使用cordova-plugin-file API进行文件操作
+- 单例模式确保只有一个日志写入器实例
+- 动态导入避免在非Cordova环境下加载相关代码
+
+**改动文件:**
+- src/log/logger.js - 添加Cordova环境检测和日志写入逻辑
+- src/log/cordova-log-writer.js (新增) - Cordova日志写入器实现
+- src/log/cordova-log-writer.js.md (新增) - 详细使用文档
+- .build/build-android.js - 自动安装cordova-plugin-file插件
+
+---
+
 ## [1.9.0] - 2025-12-15 18:40
 
 ### 新增 (Added)

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "zsis",
-  "version": "1.9.0",
+  "version": "1.10.0",
   "private": true,
   "description": "医学成像系统",
   "main": "main.js",

+ 237 - 0
src/log/cordova-log-writer.js

@@ -0,0 +1,237 @@
+/**
+ * Cordova 日志写入器
+ * 使用 cordova-plugin-file 将日志保存到设备本地文件系统
+ */
+export class CordovaLogWriter {
+  constructor() {
+    this.logFile = null;
+    this.isReady = false;
+    this.initPromise = this.initialize();
+  }
+
+  // 文件大小限制:2MB
+  static get MAX_SIZE() {
+    return 2 * 1024 * 1024;
+  }
+
+  /**
+   * 初始化日志写入器
+   * 获取文件系统并创建日志文件
+   */
+  async initialize() {
+    try {
+      // 等待Cordova就绪
+      if (!window.cordova) {
+        throw new Error('Cordova not available');
+      }
+
+      // 等待deviceready事件(如果还没触发)
+      if (!this.isDeviceReady()) {
+        await this.waitForDeviceReady();
+      }
+
+      // 获取应用数据目录的文件系统
+      const fs = await this.getFileSystem();
+
+      // 查找当前可写的日志文件(考虑大小限制)
+      this.logFile = await this.findCurrentLogFile(fs);
+      this.isReady = true;
+
+      console.log('Cordova日志写入器初始化完成');
+    } catch (error) {
+      console.error('Cordova日志初始化失败:', error);
+      // 不抛出错误,避免阻塞应用启动
+    }
+  }
+
+  /**
+   * 检查设备是否就绪
+   */
+  isDeviceReady() {
+    return document.readyState === 'complete' &&
+           window.cordova &&
+           (typeof device !== 'undefined');
+  }
+
+  /**
+   * 等待Cordova deviceready事件
+   */
+  waitForDeviceReady() {
+    return new Promise((resolve) => {
+      if (this.isDeviceReady()) {
+        resolve();
+        return;
+      }
+
+      const onDeviceReady = () => {
+        document.removeEventListener('deviceready', onDeviceReady);
+        resolve();
+      };
+
+      document.addEventListener('deviceready', onDeviceReady, false);
+
+      // 超时保护(30秒)
+      setTimeout(() => {
+        document.removeEventListener('deviceready', onDeviceReady);
+        resolve(); // 即使超时也继续
+      }, 30000);
+    });
+  }
+
+  /**
+   * 获取文件系统
+   */
+  getFileSystem() {
+    return new Promise((resolve, reject) => {
+      window.resolveLocalFileSystemURL(
+        cordova.file.dataDirectory,
+        (fs) => resolve(fs),
+        (error) => reject(error)
+      );
+    });
+  }
+
+  /**
+   * 生成日志文件名(按日期,支持索引)
+   */
+  getLogFileName(index = 0) {
+    const date = new Date().toISOString().slice(0, 10);
+    const paddedIndex = index.toString().padStart(3, '0');
+    return `${date}_${paddedIndex}.log`;
+  }
+
+  /**
+   * 查找当前可写的日志文件(考虑文件大小限制)
+   */
+  async findCurrentLogFile(fs) {
+    let index = 0;
+    while (true) {
+      const fileName = this.getLogFileName(index);
+      try {
+        const fileEntry = await this.getLogFile(fs, fileName);
+        const fileInfo = await this.getFileInfo(fileEntry);
+        if (fileInfo.size < CordovaLogWriter.MAX_SIZE) {
+          return fileEntry; // 文件存在且大小合适
+        }
+        index++; // 文件已满,尝试下一个索引
+      } catch {
+        // 文件不存在,直接使用
+        return await this.getLogFile(fs, fileName);
+      }
+    }
+  }
+
+  /**
+   * 获取文件信息(用于检查大小)
+   */
+  async getFileInfo(fileEntry) {
+    return new Promise((resolve, reject) => {
+      fileEntry.file(
+        (file) => resolve(file),
+        (error) => reject(error)
+      );
+    });
+  }
+
+  /**
+   * 获取或创建日志文件
+   */
+  async getLogFile(fs, fileName) {
+    return new Promise((resolve, reject) => {
+      fs.getFile(
+        fileName,
+        { create: true, exclusive: false },
+        (fileEntry) => resolve(fileEntry),
+        (error) => reject(error)
+      );
+    });
+  }
+
+  /**
+   * 写入日志(支持文件轮转)
+   */
+  async writeLog(level, msg) {
+    await this.initPromise;
+
+    if (!this.isReady) {
+      console.warn('Cordova日志写入器未就绪,跳过日志写入');
+      return;
+    }
+
+    // 检查当前文件是否已满,如果是则切换到新文件
+    await this.ensureCurrentFileWritable();
+
+    if (!this.logFile) {
+      console.warn('无法获取可写日志文件,跳过日志写入');
+      return;
+    }
+
+    const line = `[${new Date().toISOString()}] [${level}] ${msg}\n`;
+
+    return new Promise((resolve) => {
+      this.logFile.createWriter(
+        (writer) => {
+          try {
+            writer.seek(writer.length); // 移动到文件末尾进行追加
+            writer.write(line);
+            resolve();
+          } catch (error) {
+            console.error('写入日志失败:', error);
+            resolve(); // 不抛出错误,避免阻塞应用
+          }
+        },
+        (error) => {
+          console.error('创建文件写入器失败:', error);
+          resolve(); // 不抛出错误,避免阻塞应用
+        }
+      );
+    });
+  }
+
+  /**
+   * 确保当前文件可写(检查大小,必要时轮转)
+   */
+  async ensureCurrentFileWritable() {
+    if (!this.logFile) {
+      return;
+    }
+
+    try {
+      const fileInfo = await this.getFileInfo(this.logFile);
+      if (fileInfo.size >= CordovaLogWriter.MAX_SIZE) {
+        // 当前文件已满,切换到新文件
+        const fs = await this.getFileSystem();
+        this.logFile = await this.findCurrentLogFile(fs);
+      }
+    } catch (error) {
+      console.warn('检查文件大小时出错:', error);
+      // 出错时尝试重新获取文件系统
+      try {
+        const fs = await this.getFileSystem();
+        this.logFile = await this.findCurrentLogFile(fs);
+      } catch (fsError) {
+        console.error('重新获取文件系统失败:', fsError);
+      }
+    }
+  }
+
+  /**
+   * 获取日志文件路径(用于调试)
+   */
+  async getLogFilePath() {
+    await this.initPromise;
+    if (this.logFile) {
+      return this.logFile.toURL();
+    }
+    return null;
+  }
+}
+
+// 导出单例实例
+let cordovaWriterInstance = null;
+export function getCordovaLogWriter() {
+  if (!cordovaWriterInstance) {
+    cordovaWriterInstance = new CordovaLogWriter();
+  }
+  return cordovaWriterInstance;
+}

+ 109 - 0
src/log/cordova-log-writer.js.md

@@ -0,0 +1,109 @@
+# Cordova 日志写入器
+
+使用 `cordova-plugin-file` 将日志保存到 Cordova 应用的本地文件系统。
+
+## 功能特性
+
+- **本地存储**:日志保存在设备本地,不依赖网络连接
+- **按日期轮转**:每天自动创建新的日志文件
+- **异步写入**:不阻塞应用主线程
+- **错误容忍**:写入失败时不抛出异常,避免影响应用运行
+- **Cordova就绪检测**:自动等待 `deviceready` 事件
+
+## 文件位置和轮转
+
+日志文件保存在应用的数据目录中:
+
+- **Android**: `/data/data/{app-package-name}/files/YYYY-MM-DD_XXX.log`
+- **iOS**: `Documents/YYYY-MM-DD_XXX.log` (应用沙盒内)
+
+### 文件轮转机制
+
+- **大小限制**: 每个日志文件最大2MB
+- **命名规则**: `YYYY-MM-DD_XXX.log`,其中XXX是从000开始的索引
+- **自动轮转**: 当文件超过2MB时,自动创建新的索引文件
+- **按日期分组**: 每天重置索引,从000开始
+
+## 使用方法
+
+### 1. 安装插件
+
+```bash
+cordova plugin add cordova-plugin-file
+```
+
+### 2. 自动集成
+
+当应用运行在Cordova环境中时,日志系统会自动检测并使用Cordova日志写入器:
+
+```javascript
+import { logger } from './src/log/logger.js';
+
+// 这些日志调用会自动保存到本地文件
+logger.log('应用启动');
+logger.error('发生错误', error);
+logger.warn('警告信息');
+```
+
+### 3. 日志格式
+
+```
+[2025-12-15T07:29:09.123Z] [level] 消息内容
+```
+
+## 实现细节
+
+### 初始化流程
+
+1. 检测Cordova环境可用性
+2. 等待 `deviceready` 事件(如果未触发)
+3. 获取应用数据目录的文件系统
+4. 创建或打开当天的日志文件
+
+### 错误处理
+
+- 所有文件操作失败时只记录警告,不抛出异常
+- 超时保护:初始化等待最多30秒
+- 降级策略:如果文件系统不可用,仍可在控制台看到日志
+
+### 单例模式
+
+使用单例模式确保只有一个日志写入器实例,避免重复初始化和资源浪费。
+
+## 调试方法
+
+### 查看日志文件路径
+
+```javascript
+import { getCordovaLogWriter } from './src/log/cordova-log-writer.js';
+
+const writer = getCordovaLogWriter();
+const path = await writer.getLogFilePath();
+console.log('日志文件路径:', path);
+```
+
+### 检查写入器状态
+
+```javascript
+console.log('写入器就绪:', writer.isReady);
+```
+
+## 注意事项
+
+1. **权限要求**:确保应用有文件系统访问权限
+2. **存储空间**:定期清理旧日志文件,避免占用过多存储空间
+3. **性能影响**:频繁日志写入可能影响应用性能,建议在生产环境中控制日志级别
+4. **隐私考虑**:日志可能包含敏感信息,注意数据保护要求
+
+## 故障排除
+
+### 日志不写入文件
+
+1. 检查 `cordova-plugin-file` 是否正确安装
+2. 确认应用在Cordova环境中运行
+3. 检查设备存储权限
+4. 查看控制台是否有初始化错误信息
+
+### 文件路径问题
+
+不同平台的路径表示可能不同,使用 `getLogFilePath()` 方法获取实际路径进行调试。

+ 12 - 2
src/log/logger.js

@@ -3,13 +3,17 @@ const isElectron = () =>
   window.electronAPI &&
   typeof window.electronAPI.writeLog === 'function';
 
+// 检测是否在 Cordova 环境中
+const isCordova = () =>
+  typeof window !== 'undefined' && !!window.cordova;
+
 // 检测是否在 Cypress 测试环境中
 const isTestEnvironment = () =>
   typeof window !== 'undefined' && window.Cypress;
 
 // 检测是否在浏览器环境中
 const isBrowser = () =>
-  typeof window !== 'undefined' && !isElectron() && !isTestEnvironment();
+  typeof window !== 'undefined' && !isElectron() && !isCordova() && !isTestEnvironment();
 
 //避免obj是递归引用导致崩溃
 function safeStringify(obj) {
@@ -44,8 +48,14 @@ function proxyLog(level) {
 
     if (isElectron()) {                      // ② Electron 环境
       window.electronAPI.writeLog(level, msg).catch(() => {});
+    } else if (isCordova()) {                // ③ Cordova 环境
+      // 动态导入Cordova日志写入器,避免在非Cordova环境下加载
+      import('./cordova-log-writer.js').then(({ getCordovaLogWriter }) => {
+        const writer = getCordovaLogWriter();
+        writer.writeLog(level, msg).catch(() => {});
+      }).catch(() => {});
     } else if (typeof fetch !== 'undefined') {
-      // // ③ 纯 Web 或 pkg-static:发一条异步 POST /log,不阻塞、不报错
+      // //  纯 Web 或 pkg-static:发一条异步 POST /log,不阻塞、不报错
       // fetch('/log', {
       //   method: 'POST',
       //   headers: { 'Content-Type': 'application/json' },