我将从鸿蒙 Electron 本地文件操作的核心模块切入,先讲权限配置与路径处理基础,再详解文件读写、文件夹遍历的具体 API 用法,最后通过日志工具实战案例串联全流程,确保覆盖从权限申请到文件操作的完整实现。

<canvas_command><action>create</action><identifier>hongmeng-electron-file-operation-guide</identifier>

<title>鸿蒙 Electron 本地文件操作:读写文件、文件夹遍历、路径处理</title><genre>实战教程</genre><type>type/ddxml</type><content><title id="0">鸿蒙 Electron 本地文件操作:读写文件、文件夹遍历、路径处理</title><p id="1">本地文件操作是鸿蒙 Electron 应用的核心能力之一,无论是日志记录、配置保存还是数据导出,都离不开文件系统的交互。本文聚焦 Electron 原生 <code id="2">fs</code> 模块与鸿蒙文件系统的适配要点,从“权限配置→路径处理→文件读写→文件夹遍历”逐步展开,最后通过“本地日志记录工具”实战案例,完整实现文件操作全流程,解决鸿蒙环境下文件权限、路径兼容等核心问题。</p>

plaintext

&#x3C;callout id=&#x22;3&#x22; emoji=&#x22;&#x26A0;&#xFE0F;&#x22;&#x3E;
  &#x5173;&#x952E;&#x524D;&#x63D0;&#xFF1A;&#x9E3F;&#x8499; Electron &#x4E2D;&#x6587;&#x4EF6;&#x64CD;&#x4F5C;&#x4F9D;&#x8D56; Node.js &#x3C;code id=&#x22;4&#x22;&#x3E;fs&#x3C;/code&#x3E; &#x6A21;&#x5757;&#xFF08;&#x4E3B;&#x8FDB;&#x7A0B;&#xFF09;&#xFF0C;&#x9700;&#x63D0;&#x524D;&#x914D;&#x7F6E;&#x5B58;&#x50A8;&#x6743;&#x9650;&#xFF1B;&#x6E32;&#x67D3;&#x8FDB;&#x7A0B;&#x9700;&#x901A;&#x8FC7; IPC &#x901A;&#x4FE1;&#x8C03;&#x7528;&#xFF0C;&#x7981;&#x6B62;&#x76F4;&#x63A5;&#x64CD;&#x4F5C;&#x6587;&#x4EF6;&#x7CFB;&#x7EDF;&#x3002;
&#x3C;/callout&#x3E;

&#x3C;h1 id=&#x22;5&#x22;&#x3E;&#x4E00;&#x3001;&#x9E3F;&#x8499;&#x6587;&#x4EF6;&#x7CFB;&#x7EDF;&#x57FA;&#x7840;&#xFF1A;&#x6743;&#x9650;&#x4E0E;&#x8DEF;&#x5F84;&#x89C4;&#x8303;&#x3C;/h1&#x3E;
&#x3C;p id=&#x22;6&#x22;&#x3E;&#x9E3F;&#x8499;&#x7CFB;&#x7EDF;&#x5BF9;&#x6587;&#x4EF6;&#x8BBF;&#x95EE;&#x6709;&#x4E25;&#x683C;&#x7684;&#x6743;&#x9650;&#x7BA1;&#x63A7;&#x548C;&#x8DEF;&#x5F84;&#x89C4;&#x8303;&#xFF0C;&#x5F00;&#x53D1;&#x524D;&#x9700;&#x660E;&#x786E;&#x6838;&#x5FC3;&#x89C4;&#x5219;&#xFF0C;&#x907F;&#x514D;&#x64CD;&#x4F5C;&#x5931;&#x8D25;&#x3002;&#x3C;/p&#x3E;

&#x3C;h2 id=&#x22;7&#x22;&#x3E;1.1 &#x6587;&#x4EF6;&#x6743;&#x9650;&#x914D;&#x7F6E;&#xFF08;config.json&#xFF09;&#x3C;/h2&#x3E;
&#x3C;p id=&#x22;8&#x22;&#x3E;&#x6587;&#x4EF6;&#x64CD;&#x4F5C;&#x9700;&#x58F0;&#x660E;&#x5B58;&#x50A8;&#x6743;&#x9650;&#xFF0C;&#x5728;&#x9879;&#x76EE;&#x6839;&#x76EE;&#x5F55; &#x3C;code id=&#x22;9&#x22;&#x3E;config.json&#x3C;/code&#x3E; &#x7684; &#x3C;code id=&#x22;10&#x22;&#x3E;reqPermissions&#x3C;/code&#x3E; &#x8282;&#x70B9;&#x6DFB;&#x52A0;&#x4EE5;&#x4E0B;&#x914D;&#x7F6E;&#xFF1A;&#x3C;/p&#x3E;
&#x3C;pre id=&#x22;11&#x22;&#x3E;&#x3C;code id=&#x22;12&#x22; lang=&#x22;json&#x22;&#x3E;{
"module": {
  "reqPermissions": [
    {
      "name": "ohos.permission.READ_USER_STORAGE",
      "reason": "用于读取本地配置文件和日志信息",
      "usedScene": {
        "ability": ["MainAbility"],
        "when": "always"
      }
    },
    {
      "name": "ohos.permission.WRITE_USER_STORAGE",
      "reason": "用于记录日志和保存配置数据",
      "usedScene": {
        "ability": ["MainAbility"],
        "when": "always"
      }
    }
  ]
}
 

plaintext

&#x3C;h2 id=&#x22;13&#x22;&#x3E;1.2 &#x9E3F;&#x8499;&#x6587;&#x4EF6;&#x8DEF;&#x5F84;&#x89C4;&#x8303;&#x3C;/h2&#x3E;
&#x3C;p id=&#x22;14&#x22;&#x3E;&#x9E3F;&#x8499;&#x7CFB;&#x7EDF;&#x5212;&#x5206;&#x4E86;&#x4E0D;&#x540C;&#x7684;&#x6587;&#x4EF6;&#x5B58;&#x50A8;&#x533A;&#x57DF;&#xFF0C;&#x63A8;&#x8350;&#x4F7F;&#x7528;&#x4EE5;&#x4E0B;&#x8DEF;&#x5F84;&#x8FDB;&#x884C;&#x6587;&#x4EF6;&#x64CD;&#x4F5C;&#xFF08;&#x907F;&#x514D;&#x8BBF;&#x95EE;&#x654F;&#x611F;&#x76EE;&#x5F55;&#xFF09;&#xFF1A;&#x3C;/p&#x3E;
&#x3C;table id=&#x22;15&#x22;&#x3E;
  &#x3C;thead id=&#x22;16&#x22;&#x3E;
    &#x3C;tr id=&#x22;17&#x22;&#x3E;
      &#x3C;th id=&#x22;18&#x22;&#x3E;&#x8DEF;&#x5F84;&#x7C7B;&#x578B;&#x3C;/th&#x3E;
      &#x3C;th id=&#x22;19&#x22;&#x3E;&#x83B7;&#x53D6;&#x65B9;&#x5F0F;&#xFF08;Electron API&#xFF09;&#x3C;/th&#x3E;
      &#x3C;th id=&#x22;20&#x22;&#x3E;&#x9002;&#x7528;&#x573A;&#x666F;&#x3C;/th&#x3E;
      &#x3C;th id=&#x22;21&#x22;&#x3E;&#x6743;&#x9650;&#x8981;&#x6C42;&#x3C;/th&#x3E;
    &#x3C;/tr&#x3E;
  &#x3C;/thead&#x3E;
  &#x3C;tbody id=&#x22;22&#x22;&#x3E;
    &#x3C;tr id=&#x22;23&#x22;&#x3E;
      &#x3C;td id=&#x22;24&#x22;&#x3E;&#x5E94;&#x7528;&#x6570;&#x636E;&#x76EE;&#x5F55;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;25&#x22;&#x3E;&#x3C;code id=&#x22;26&#x22;&#x3E;app.getPath(&#x27;userData&#x27;)&#x3C;/code&#x3E;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;27&#x22;&#x3E;&#x914D;&#x7F6E;&#x6587;&#x4EF6;&#x3001;&#x65E5;&#x5FD7;&#x6587;&#x4EF6;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;28&#x22;&#x3E;&#x9ED8;&#x8BA4;&#x6743;&#x9650;&#xFF08;&#x65E0;&#x9700;&#x52A8;&#x6001;&#x7533;&#x8BF7;&#xFF09;&#x3C;/td&#x3E;
    &#x3C;/tr&#x3E;
    &#x3C;tr id=&#x22;29&#x22;&#x3E;
      &#x3C;td id=&#x22;30&#x22;&#x3E;&#x6587;&#x6863;&#x76EE;&#x5F55;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;31&#x22;&#x3E;&#x3C;code id=&#x22;32&#x22;&#x3E;app.getPath(&#x27;documents&#x27;)&#x3C;/code&#x3E;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;33&#x22;&#x3E;&#x7528;&#x6237;&#x6587;&#x6863;&#x3001;&#x5BFC;&#x51FA;&#x6570;&#x636E;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;34&#x22;&#x3E;&#x9700; WRITE_USER_STORAGE &#x6743;&#x9650;&#x3C;/td&#x3E;
    &#x3C;/tr&#x3E;
    &#x3C;tr id=&#x22;35&#x22;&#x3E;
      &#x3C;td id=&#x22;36&#x22;&#x3E;&#x4E34;&#x65F6;&#x76EE;&#x5F55;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;37&#x22;&#x3E;&#x3C;code id=&#x22;38&#x22;&#x3E;app.getPath(&#x27;temp&#x27;)&#x3C;/code&#x3E;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;39&#x22;&#x3E;&#x4E34;&#x65F6;&#x6587;&#x4EF6;&#x3001;&#x7F13;&#x5B58;&#x6570;&#x636E;&#x3C;/td&#x3E;
      &#x3C;td id=&#x22;40&#x22;&#x3E;&#x9ED8;&#x8BA4;&#x6743;&#x9650;&#x3C;/td&#x3E;
    &#x3C;/tr&#x3E;
  &#x3C;/tbody&#x3E;
&#x3C;/table&#x3E;

&#x3C;callout id=&#x22;41&#x22; emoji=&#x22;&#x1F4A1;&#x22;&#x3E;
  &#x907F;&#x5751;&#x70B9;&#xFF1A;&#x9E3F;&#x8499;&#x7CFB;&#x7EDF;&#x7981;&#x6B62;&#x76F4;&#x63A5;&#x8BBF;&#x95EE;&#x6839;&#x76EE;&#x5F55;&#xFF08;&#x3C;code id=&#x22;42&#x22;&#x3E;/&#x3C;/code&#x3E;&#xFF09;&#x548C;&#x7CFB;&#x7EDF;&#x76EE;&#x5F55;&#xFF08;&#x3C;code id=&#x22;43&#x22;&#x3E;/system&#x3C;/code&#x3E;&#xFF09;&#xFF0C;&#x4F18;&#x5148;&#x4F7F;&#x7528; Electron &#x63D0;&#x4F9B;&#x7684;&#x8DEF;&#x5F84; API &#x83B7;&#x53D6;&#x5408;&#x6CD5;&#x8DEF;&#x5F84;&#xFF0C;&#x907F;&#x514D;&#x786C;&#x7F16;&#x7801;&#x8DEF;&#x5F84;&#x5BFC;&#x81F4;&#x8DE8;&#x8BBE;&#x5907;&#x517C;&#x5BB9;&#x95EE;&#x9898;&#x3002;
&#x3C;/callout&#x3E;

&#x3C;h1 id=&#x22;44&#x22;&#x3E;&#x4E8C;&#x3001;&#x6838;&#x5FC3;&#x64CD;&#x4F5C;&#xFF1A;&#x6587;&#x4EF6;&#x8BFB;&#x5199;&#x4E0E;&#x8DEF;&#x5F84;&#x5904;&#x7406;&#x3C;/h1&#x3E;
&#x3C;p id=&#x22;45&#x22;&#x3E;Electron &#x4E3B;&#x8FDB;&#x7A0B;&#x901A;&#x8FC7; Node.js &#x3C;code id=&#x22;46&#x22;&#x3E;fs/promises&#x3C;/code&#x3E; &#x6A21;&#x5757;&#xFF08;&#x5F02;&#x6B65;&#x975E;&#x963B;&#x585E;&#xFF09;&#x5B9E;&#x73B0;&#x6587;&#x4EF6;&#x64CD;&#x4F5C;&#xFF0C;&#x7ED3;&#x5408;&#x9E3F;&#x8499;&#x8DEF;&#x5F84;&#x89C4;&#x8303;&#xFF0C;&#x4EE5;&#x4E0B;&#x662F;&#x6838;&#x5FC3;&#x64CD;&#x4F5C;&#x7684;&#x5177;&#x4F53;&#x5B9E;&#x73B0;&#x3002;&#x3C;/p&#x3E;

&#x3C;h2 id=&#x22;47&#x22;&#x3E;2.1 &#x8DEF;&#x5F84;&#x5904;&#x7406;&#x5DE5;&#x5177;&#x51FD;&#x6570;&#x3C;/h2&#x3E;
&#x3C;p id=&#x22;48&#x22;&#x3E;&#x5C01;&#x88C5;&#x8DEF;&#x5F84;&#x5904;&#x7406;&#x5DE5;&#x5177;&#xFF0C;&#x89E3;&#x51B3;&#x9E3F;&#x8499;&#x4E0D;&#x540C;&#x8BBE;&#x5907;&#x8DEF;&#x5F84;&#x5DEE;&#x5F02;&#x95EE;&#x9898;&#xFF1A;&#x3C;/p&#x3E;
&#x3C;pre id=&#x22;49&#x22;&#x3E;&#x3C;code id=&#x22;50&#x22; lang=&#x22;typescript&#x22;&#x3E;// src/main/utils/pathUtil.ts
import { app } from '@hmos/electron';
import path from 'path';

/**
 * 获取应用专属日志目录
 * @returns 日志目录绝对路径
 */
export function getLogDirPath(): string {
  const userDataPath = app.getPath('userData');
  return path.join(userDataPath, 'logs');
}

/**
 * 获取指定日期的日志文件路径
 * @param date 日期(默认当天)
 * @returns 日志文件绝对路径
 */
export function getLogFilePath(date: Date = new Date()): string {
  const logDir = getLogDirPath();
  const dateStr = date.toISOString().split('T')[0]; // 格式:YYYY-MM-DD
  const logFileName = `app-${dateStr}.log`;
  return path.join(logDir, logFileName);
}

/**
 * 确保目录存在(不存在则创建)
 * @param dirPath 目录路径
 */
export async function ensureDirExists(dirPath: string): Promise<void> {
  const fs = await import('fs/promises');
  try {
    await fs.access(dirPath);
  } catch {
    await fs.mkdir(dirPath, { recursive: true });
  }
}

// src/main/utils/fileUtil.ts
import { ensureDirExists, getLogFilePath, getLogDirPath } from './pathUtil';

/**
 * 写入日志内容到文件
 * @param content 日志内容
 * @param level 日志级别(info/warn/error)
 */
export async function writeLog(content: string, level: 'info' | 'warn' | 'error' = 'info'): Promise<void> {
  const fs = await import('fs/promises');
  await ensureDirExists(getLogDirPath());
  
  const timestamp = new Date().toISOString();
  const logContent = `[${timestamp}] [${level.toUpperCase()}] ${content}\n`;
  
  await fs.appendFile(getLogFilePath(), logContent, 'utf8');
}

/**
 * 读取指定日期的日志文件内容
 * @param date 日期(默认当天)
 * @returns 日志内容数组(按行分割)
 */
export async function readLogFile(date: Date = new Date()): Promise<string[]> {
  const fs = await import('fs/promises');
  const logFilePath = getLogFilePath(date);
  
  try {
    const content = await fs.readFile(logFilePath, 'utf8');
    return content.split(/\r?\n/).filter(line => line.trim() !== '');
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
      return [`日志文件不存在(${logFilePath})`];
    }
    throw new Error(`读取日志失败:${(error as Error).message}`);
  }
}

// src/main/utils/fileUtil.ts(续)
import { getLogDirPath } from './pathUtil';
 
// src/main/index.ts
import { app, BrowserWindow, ipcMain } from '@hmos/electron';
import path from 'path';
import { writeLog, readLogFile, listLogFiles } from './utils/fileUtil';
import { getLogFilePath } from './utils/pathUtil';

// 创建窗口逻辑(省略)
function createWindow() { /* ... */ }

// 监听写入日志请求
ipcMain.handle('write-log', async (_, content: string, level: string) => {
  await writeLog(content, level as 'info' | 'warn' | 'error');
  return { success: true };
});

// 监听读取日志请求
ipcMain.handle('read-log', async (_, dateStr: string) => {
  const date = new Date(dateStr);
  if (isNaN(date.getTime())) {
    throw new Error('无效的日期格式');
  }
  const content = await readLogFile(date);
  return { success: true, data: content };
});

// 监听日志文件列表请求
ipcMain.handle('list-log-files', async () => {
  const files = await listLogFiles();
  return { success: true, data: files };
});

// 监听删除日志文件请求
ipcMain.handle('delete-log-file', async (_, filePath: string) => {
  const fs = await import('fs/promises');
  try {
    await fs.unlink(filePath);
    return { success: true };
  } catch (error) {
    return { success: false, message: (error as Error).message };
  }
});

// 应用启动逻辑(省略)
app.whenReady().then(createWindow);
 

四、实战案例:本地日志记录工具

基于上述功能实现一个完整的日志工具,包含日志写入、日志查看、日志管理三大功能,使用 Vue 3 开发渲染界面。

4.1 日志写入组件(LogWriter.vue)

<template>
  <div class="log-writer-card">
    <h3 class="card-title">日志写入测试</h3>
    <textarea
      v-model="logContent"
      placeholder="请输入日志内容..."
      class="log-input"
    ></textarea>
    <div class="log-level-selector">
      <label>日志级别:</label>
      <select v-model="selectedLevel" class="level-select">
        <option value="info">Info</option>
        <option value="warn">Warn</option>
        <option value="error">Error</option>
      </select>
    </div>
    <button @click="submitLog" class="submit-btn">写入日志</button>
    <div class="status-msg" v-if="statusMsg" :class="statusType">
      {{ statusMsg }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const logContent = ref('');
const selectedLevel = ref('info');
const statusMsg = ref('');
const statusType = ref('success');

async function submitLog() {
  if (!logContent.value.trim()) {
    statusMsg.value = '日志内容不能为空';
    statusType.value = 'error';
    return;
  }

  try {
    await window.fileApi.writeLog(logContent.value, selectedLevel.value);
    statusMsg.value = '日志写入成功!';
    statusType.value = 'success';
    logContent.value = ''; // 清空输入框
  } catch (error) {
    statusMsg.value = `写入失败:${(error as Error).message}`;
    statusType.value = 'error';
  }

  // 3 秒后清空状态提示
  setTimeout(() => (statusMsg.value = ''), 3000);
}
</script>

<style scoped>
.log-writer-card {
  border: 1px solid #eee;
  border-radius: 0.8rem;
  padding: 1.5rem;
  margin-bottom: 2rem;
}

.card-title {
  margin: 0 0 1rem 0;
  color: #007dff;
}

.log-input {
  width: 100%;
  height: 120px;
  padding: 0.8rem;
  border: 1px solid #ddd;
  border-radius: 0.4rem;
  resize: none;
  margin-bottom: 1rem;
}

.log-level-selector {
  margin-bottom: 1rem;
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

.level-select {
  padding: 0.4rem;
  border: 1px solid #ddd;
  border-radius: 0.4rem;
}

.submit-btn {
  background-color: #007dff;
  color: white;
  border: none;
  border-radius: 0.4rem;
  padding: 0.8rem 1.5rem;
  cursor: pointer;
}

.submit-btn:hover {
  background-color: #0066cc;
}

.status-msg {
  margin-top: 1rem;
  padding: 0.8rem;
  border-radius: 0.4rem;
}

.success {
  background-color: #e8f5e9;
  color: #2e7d32;
}

.error {
  background-color: #ffebee;
  color: #c62828;
}
</style>

## 4.2 日志查看组件(LogViewer.vue)

```vue
<template>
  <div class="log-viewer-card">
    <h3 class="card-title">日志查看</h3>
    <div class="date-selector">
      <label>选择日期:</label>
      <input type="date" v-model="selectedDate" class="date-input" />
      <button @click="loadLog" class="load-btn">加载日志</button>
    </div>
    <div class="log-content" v-if="logContent.length">
      <div 
        v-for="(line, index) in logContent"
        :key="index"
        class="log-line"
        :class="getLogLevelClass(line)"
      >
        {{ line }}
      </div>
    </div>
    <div class="empty-msg" v-if="logContent.length === 0 && loaded">
      暂无日志数据
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

const selectedDate = ref(new Date().toISOString().split('T')[0]);
const logContent = ref<string[]>([]);
const loaded = ref(false);

async function loadLog() {
  try {
    const result = await window.fileApi.readLog(selectedDate.value);
    logContent.value = result.data || [];
    loaded.value = true;
  } catch (error) {
    logContent.value = [`加载失败:${(error as Error).message}`];
    loaded.value = true;
  }
}

// 根据日志内容判断级别并返回样式类
function getLogLevelClass(line: string): string {
  if (line.includes('[ERROR]')) return 'log-error';
  if (line.includes('[WARN]')) return 'log-warn';
  return 'log-info';
}
</script>

<style scoped>
.log-viewer-card {
  border: 1px solid #eee;
  border-radius: 0.8rem;
  padding: 1.5rem;
  margin-bottom: 2rem;
}

.date-selector {
  margin-bottom: 1rem;
  display: flex;
  align-items: center;
  gap: 0.8rem;
}

.date-input {
  padding: 0.4rem;
  border: 1px solid #ddd;
  border-radius: 0.4rem;
}

.load-btn {
  background-color: #007dff;
  color: white;
  border: none;
  border-radius: 0.4rem;
  padding: 0.4rem 1rem;
  cursor: pointer;
}

.log-content {
  background-color: #f9f9f9;
  padding: 1rem;
  border-radius: 0.4rem;
  max-height: 400px;
  overflow-y: auto;
}

.log-line {
  padding: 0.2rem 0;
  border-bottom: 1px solid #eee;
  font-family: monospace;
  font-size: 0.9rem;
}

.log-info {
  color: #333;
}

.log-warn {
  color: #f57c00;
}

.log-error {
  color: #d32f2f;
}

.empty-msg {
  text-align: center;
  padding: 2rem;
  color: #999;
}
</style>


## 4.3 日志管理组件(LogManager.vue)

```vue
<template>
  <div class="log-manager-card">
    <h3 class="card-title">日志文件管理</h3>
    <button @click="refreshLogList" class="refresh-btn">刷新列表</button>
    
    <table class="log-file-table" v-if="logFiles.length">
      <thead>
        <tr>
          <th>文件名</th>
          <th>大小(KB)</th>
          <th>创建时间</th>
          <th>操作</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="file in logFiles" :key="file.path">
          <td>{{ file.name }}</td>
          <td>{{ (file.size / 1024).toFixed(2) }}</td>
          <td>{{ new Date(file.createTime).toLocaleString() }}</td>
          <td>
            <button @click="deleteLogFile(file.path)" class="delete-btn">删除</button>
          </td>
        </tr>
      </tbody>
    </table>
    
    <div class="empty-msg" v-if="logFiles.length === 0 && loaded">暂无日志文件</div>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

const logFiles = ref<Array<{ name: string; path: string; size: number; createTime: number }>>([]);
const loaded = ref(false);

async function refreshLogList() {
  try {
    const result = await window.fileApi.listLogFiles();
    logFiles.value = result.data || [];
    loaded.value = true;
  } catch (error) {
    alert(`获取日志列表失败:${(error as Error).message}`);
  }
}

async function deleteLogFile(filePath: string) {
  if (!confirm('确定要删除该日志文件吗?')) return;
  try {
    const result = await window.fileApi.deleteLogFile(filePath);
    if (result.success) {
      refreshLogList();
      alert('删除成功!');
    } else {
      alert(`删除失败:${result.message}`);
    }
  } catch (error) {
    alert(`删除异常:${(error as Error).message}`);
  }
}

onMounted(() => refreshLogList());
</script>

<style scoped>
.log-manager-card {
  border: 1px solid #eee;
  border-radius: 0.8rem;
  padding: 1.5rem;
}

.refresh-btn {
  background-color: #007dff;
  color: white;
  border: none;
  border-radius: 0.4rem;
  padding: 0.4rem 1rem;
  cursor: pointer;
  margin-bottom: 1rem;
}

.log-file-table {
  width: 100%;
  border-collapse: collapse;
}

.log-file-table th,
.log-file-table td {
  border: 1px solid #ddd;
  padding: 0.8rem;
  text-align: left;
}

.log-file-table th {
  background-color: #f5f5f5;
}

.delete-btn {
  background-color: #d32f2f;
  color: white;
  border: none;
  border-radius: 0.4rem;
  padding: 0.4rem 0.8rem;
  cursor: pointer;
}

.empty-msg {
  text-align: center;
  padding: 2rem;
  color: #999;
}
</style>

## 4.4 主页面整合(LogTool.vue)

```vue
<template>
  <div class="log-tool-container">
    <h2 class="page-title">鸿蒙 Electron 日志工具</h2>
    <LogWriter />
    <LogViewer />
    <LogManager />
  </div>
</template>

<script setup lang="ts">
import LogWriter from './LogWriter.vue'
import LogViewer from './LogViewer.vue'
import LogManager from './LogManager.vue'
</script>

<style scoped>
.log-tool-container {
  width: 90vw;
  max-width: 1000px;
  margin: 2rem auto;
  padding: 1rem;
}

.page-title {
  text-align: center;
  color: #333;
  margin-bottom: 2rem;
}
</style>


# 五、鸿蒙文件操作避坑指南

| 问题现象 | 根本原因 | 解决方案 |
|---------|---------|---------|
| 写入文件提示"permission denied" | 未声明 WRITE_USER_STORAGE 权限,或访问系统敏感目录 | 1. 配置 config.json 权限;2. 使用 app.getPath('userData') 等合法路径 |
| 读取文件显示乱码 | 未指定编码格式(默认 buffer) | 读取时添加编码参数:`readFile(path, 'utf8')` |
| 创建目录失败 | 未使用递归创建(父目录不存在) | 添加 `{ recursive: true }` 参数:`mkdir(path, { recursive: true })` |
| 路径拼接错误(跨设备) | 使用字符串拼接路径(如 `path + '/file'`) | 使用 path.join() 拼接路径:`path.join(dir, 'file')` |
| 大文件读取卡顿 | 一次性读取整个文件内容 | 使用流(stream)分段读取:`fs.createReadStream(path)` |

# 六、总结:鸿蒙文件操作核心要点

鸿蒙 Electron 本地文件操作需遵循"权限合规、路径规范、异步操作"三大原则:

1. **权限先行**:所有文件操作前必须在 config.json 声明对应权限,危险权限需动态申请;
2. **路径安全**:优先使用 Electron 提供的路径 API,避免硬编码路径,确保跨设备兼容;
3. **异步优先**:使用 fs/promises 异步 API,避免阻塞主进程;

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐