前言

欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区 :https://harmonypc.csdn.net/

项目开源地址:https://AtomGit.com/lqjmac/ele_guidangliushuixian

桌面文件夹最常见的问题不是文件太多,而是很多文件“当时为什么留下”已经说不清了。

会议附件、临时截图、导出的表格、几版方案、压缩包和别人发来的补充材料,刚保存的时候都有用,过一段时间就会变成一堆难以判断的本地资料。

所以这个工具不做传统的文件夹美化,而是做成 归档流水线

它的核心目标是让一份资料经历“进入、判断、备注、归档、导出”这条路径。

适合处理的场景包括:

  • 项目结束后整理交付材料
  • 把桌面临时资料收口
  • 给保留文件补充原因
  • 导出一份归档说明给团队复盘

归档不是把文件移走,而是把文件为什么值得留下说清楚。

本文会从数据模型、页面结构、状态流转、Electron 桥接、Markdown 导出和构建检查几个角度拆解。

一、先把归档动作拆清楚

1.1 归档和删除不是一回事

很多整理工具把“归档”做得像“隐藏”。

但真实使用中,归档更像一种状态:这份资料已经有去处、有解释、有复查点。

动作 含义 是否可恢复 是否需要说明
删除 不再需要 通常不可恢复 不一定
隐藏 暂时不显示 可以恢复 不一定
归档 处理完成并保留 可以恢复 需要
待确认 暂时不能判断 可以继续处理 需要

这张表决定了按钮文案不能混在一起。

删除是破坏性动作,归档是流程动作。

1.2 第一版做轻流程

归档流水线第一版不急着做真实文件移动。

真实文件移动涉及目录权限、冲突处理、同名覆盖、失败回滚和批量任务。

第一版先把资料判断过程做顺:

  1. 新建资料条目
  2. 填写来源和保留原因
  3. 标记待归档、已入库或待确认
  4. 复制归档摘要
  5. 导出 Markdown 归档单

这条链路顺了以后,再接文件系统能力才更稳。

二、文件分工围绕归档流转

2.1 组件职责

归档工具的组件要围绕“资料如何流动”来拆。

文件 职责 归档场景里的作用
Home.vue 页面总装 组织收件栏、流水线和编辑区
ArchiveSidebar.vue 左侧入口 搜索、分类、统计、收口入口
ArchiveEditor.vue 编辑资料 来源、主题、说明、复查时间
ArchiveToolbar.vue 操作栏 新建、复制、导出、归档、删除
useArchiveItems.ts 状态层 本地保存、筛选、排序
useNativeBridge.ts 桥接层 剪贴板、文件保存、系统通知

如果项目里沿用通用组件名,也可以在文章里按业务职责解释。

关键是让读者知道每个文件服务哪一个归档动作。

2.2 页面和桥接要解耦

页面不要直接写文件保存细节。

它只需要调用 exportMarkdown,至于浏览器预览还是鸿蒙 Electron 运行时,由桥接层判断。

const { copyText, exportMarkdown, notify, isNativeRuntime } = useNativeBridge();

这种写法让页面更干净,也方便在浏览器里先调试界面。

三、页面结构图

3.1 归档流水线结构图

在这里插入图片描述

这张图表达的是资料从左侧收口进入,中间进入状态流转,右侧补充归档说明和复查信息。

3.2 为什么不是文件树

很多人做文件管理,第一反应是树形目录。

但归档流水线不以目录为核心,而以资料状态为核心。

区域 负责什么 不做什么
收件栏 接住散落资料 不做复杂目录树
流水线 展示待处理状态 不做审批系统
编辑区 写清保留原因 不只当备注框
工具栏 复制和导出 不堆无关按钮

这样用户打开后先看到的是“哪些资料还没处理”,而不是“文件夹层级有多深”。

四、数据模型要表达归档理由

4.1 核心字段

归档条目至少需要这些字段:

字段 说明 示例
id 唯一标识 archive-001
title 资料标题 五月验收截图包
source 资料来源 项目群附件
topic 归档主题 验收资料
state 处理状态 待归档
summary 保留摘要 用于验收复盘
highlights 关键说明 包含最终确认图
nextReview 复查时间 2026-05-30

这些字段让一份资料脱离原始文件名后仍然能被理解。

4.2 TypeScript 类型

export type ArchiveState = 'incoming' | 'stored' | 'checking';

export type ArchiveCategory =
  | 'inbox'
  | 'rule'
  | 'process'
  | 'reference';

export interface ArchiveItem {
  id: string;
  title: string;
  source: string;
  topic: string;
  category: ArchiveCategory;
  state: ArchiveState;
  keywords: string;
  summary: string;
  highlights: string;
  content: string;
  nextReview: string;
  pinned: boolean;
  archived: boolean;
  createdAt: number;
  updatedAt: number;
}

这里保留 archived 字段,是为了区分“已入库状态”和“是否从活跃列表移走”。

五、状态文案要贴近资料整理

5.1 状态映射

const stateLabelMap: Record<ArchiveState, string> = {
  incoming: '待归档',
  stored: '已入库',
  checking: '待确认',
};

const categoryLabelMap: Record<ArchiveCategory, string> = {
  inbox: '待整理资料',
  rule: '归档规则',
  process: '流转过程',
  reference: '发布参考',
};

这组映射要贯穿列表、筛选、编辑器和导出。

否则用户会在不同区域看到不一致的表达。

5.2 状态优先级

归档流水线的优先级不是按创建时间简单倒序。

我更倾向于这样排:

  1. 待确认优先,因为它容易卡住后续归档
  2. 待归档其次,因为它还在活跃工作流里
  3. 已入库靠后,因为它已经完成
  4. 已隐藏或归档记录最后展示

这套排序会让用户打开应用后先处理最该处理的资料。

六、种子数据像真实桌面资料

6.1 示例数据

默认数据必须像真的从桌面整理出来。

export const seedArchiveItems: ArchiveItem[] = [
  {
    id: 'archive-acceptance-screenshots',
    title: '验收截图包整理',
    source: '项目群临时附件',
    topic: '验收材料',
    category: 'inbox',
    state: 'checking',
    keywords: '截图,验收,交付',
    summary: '截图包需要确认是否包含最终版本页面,确认后再入库。',
    highlights: '有两张截图命名不清楚,需要对照提交记录。',
    content: '资料来自项目群,需要补充版本说明和使用场景。',
    nextReview: '2026-05-30',
    pinned: true,
    archived: false,
    createdAt: Date.now() - 7200_000,
    updatedAt: Date.now(),
  },
];

这条记录能体现“来源不清、需要确认、暂不入库”的真实状态。

6.2 种子数据检查

种子数据至少要覆盖:

  • 待归档
  • 已入库
  • 待确认
  • 置顶资料
  • 已归档资料

这样才能检查筛选、排序、样式和导出。

七、本地存储保证资料不丢

7.1 读取逻辑

const STORAGE_KEY = 'guidang-liushuixian-items:v1';

function loadArchiveItems(): ArchiveItem[] {
  if (typeof window === 'undefined') return seedArchiveItems;

  try {
    const raw = window.localStorage.getItem(STORAGE_KEY);
    if (!raw) return seedArchiveItems;

    const parsed = JSON.parse(raw);
    return Array.isArray(parsed) ? parsed : seedArchiveItems;
  } catch {
    return seedArchiveItems;
  }
}

这段代码保持保守。

本地缓存损坏时回到默认数据,而不是让页面崩掉。

7.2 保存节奏

const archiveItems = ref<ArchiveItem[]>(loadArchiveItems());
const isSaving = ref(false);
const lastSavedAt = ref<number | null>(null);

function persistArchiveItems() {
  if (typeof window === 'undefined') return;
  window.localStorage.setItem(STORAGE_KEY, JSON.stringify(archiveItems.value));
  lastSavedAt.value = Date.now();
  isSaving.value = false;
}

编辑器频繁输入时,可以外面再包一层防抖。

本地优先工具一定要给用户“数据已经落盘”的信号。

八、筛选逻辑不能打断当前编辑

8.1 可见列表

const searchTerm = ref('');
const activeCategory = ref<'all' | ArchiveCategory>('all');
const showArchived = ref(false);

const visibleArchiveItems = computed(() => {
  const keyword = searchTerm.value.trim().toLowerCase();

  return archiveItems.value
    .filter(item => {
      if (!showArchived.value && item.archived) return false;
      if (activeCategory.value !== 'all' && item.category !== activeCategory.value) {
        return false;
      }
      if (!keyword) return true;
      return [
        item.title,
        item.source,
        item.topic,
        item.keywords,
        item.summary,
        item.highlights,
      ].join(' ').toLowerCase().includes(keyword);
    })
    .sort(sortArchiveItems);
});

用户可能按来源找,也可能按主题找。

搜索字段要覆盖真实记忆点。

8.2 当前条目兜底

function ensureSelectedArchiveItem() {
  const exists = archiveItems.value.some(item => item.id === currentArchiveId.value);
  if (exists) return;

  currentArchiveId.value =
    archiveItems.value.find(item => !item.archived)?.id ??
    archiveItems.value[0]?.id ??
    '';
}

筛选或删除后不要让编辑区出现不可解释的空白。

这个兜底能让体验稳定很多。

九、流水线列表要突出状态

9.1 卡片内容

归档卡片不应该只显示标题。

<template>
  <button class="archive-card" :class="item.state" @click="$emit('select', item.id)">
    <span class="state-badge">{{ stateLabelMap[item.state] }}</span>
    <strong>{{ item.title || '未命名资料' }}</strong>
    <span class="source-line">{{ item.source || '未设置来源' }}</span>
    <p>{{ excerpt(item.summary || item.content) }}</p>
  </button>
</template>

状态、来源、摘要都要露出来。

这样用户扫列表时能判断先处理哪条。

9.2 摘要截断

function excerpt(value: string) {
  return value.trim().replace(/\s+/g, ' ').slice(0, 92) || '暂无归档说明';
}

摘要不是越长越好。

列表层只负责提示,不负责承载完整内容。

十、编辑器要写清保留依据

10.1 表单结构

<template>
  <article class="archive-editor">
    <input
      class="title-input"
      :value="item.title"
      placeholder="资料标题"
      @input="updateField('title', $event)"
    />

    <div class="meta-grid">
      <label>
        <span>资料来源</span>
        <input :value="item.source" @input="updateField('source', $event)" />
      </label>

      <label>
        <span>归档主题</span>
        <input :value="item.topic" @input="updateField('topic', $event)" />
      </label>
    </div>

    <textarea
      class="content-input"
      :value="item.content"
      placeholder="写清这份资料为什么保留、归到哪里、后面谁会用"
      @input="updateField('content', $event)"
    />
  </article>
</template>

归档说明要能回答三个问题:

  1. 为什么保留
  2. 放到哪里
  3. 后续谁会用

10.2 字段更新

function updateArchiveItem(id: string, patch: Partial<ArchiveItem>) {
  archiveItems.value = archiveItems.value.map(item =>
    item.id === id
      ? { ...item, ...patch, updatedAt: Date.now() }
      : item
  );

  schedulePersist();
}

统一入口更新可以确保 updatedAt 和保存逻辑不会漏。

十一、归档动作要和删除分开

11.1 切换归档状态

function toggleArchived(id: string) {
  const target = archiveItems.value.find(item => item.id === id);
  if (!target) return;

  updateArchiveItem(id, {
    archived: !target.archived,
    pinned: target.archived ? target.pinned : false,
  });
}

归档后取消置顶,是为了避免隐藏资料仍占据重点位。

11.2 删除动作

function deleteArchiveItem(id: string) {
  archiveItems.value = archiveItems.value.filter(item => item.id !== id);
  ensureSelectedArchiveItem();
  schedulePersist();
}

删除可以做二次确认。

不过初始版本如果定位为个人工具,也可以先保证按钮样式明显区分。

十二、复制摘要服务流转

12.1 复制逻辑

async function handleCopyArchiveSummary() {
  if (!currentArchiveItem.value) return;

  const text =
    currentArchiveItem.value.summary ||
    currentArchiveItem.value.highlights ||
    currentArchiveItem.value.title;

  const ok = await copyText(text);

  if (ok) {
    showFeedback('归档摘要已复制');
    await notify('归档流水线', '当前归档摘要已经复制到剪贴板');
  }
}

复制摘要适合发到聊天、日报或项目文档里。

它不需要把整篇内容都复制走。

12.2 剪贴板能力

Electron 里可以使用 clipboard API

浏览器预览里可以使用 Clipboard API

桥接层统一封装后,页面就不用关心差异。

十三、导出 Markdown 形成归档单

13.1 导出结构

function buildArchiveMarkdown(item: ArchiveItem) {
  return [
    `# ${item.title || '未命名归档资料'}`,
    '',
    `- 类型:${categoryLabelMap[item.category]}`,
    `- 状态:${stateLabelMap[item.state]}`,
    `- 来源:${item.source || '未设置'}`,
    `- 主题:${item.topic || '未设置'}`,
    `- 关键词:${item.keywords || '未设置'}`,
    `- 下次复查:${item.nextReview || '未设置'}`,
    '',
    '## 归档摘要',
    '',
    item.summary || '暂无摘要',
    '',
    '## 保留依据',
    '',
    item.content || '暂无说明',
    '',
    '## 关键说明',
    '',
    item.highlights || '暂无高亮',
  ].join('\n');
}

这份归档单能解释资料的来源、状态和保留原因。

13.2 保存文件名

function getArchiveFileName(item: ArchiveItem) {
  return `${safeFileName(item.title || '归档资料')}.md`;
}

文件名不建议带太多状态信息。

状态已经写在 Markdown 内容里。

十四、主进程保证本地页面加载

14.1 BrowserWindow 配置

const { app, BrowserWindow } = require('electron');
const path = require('path');

function createWindow() {
  const win = new BrowserWindow({
    width: 1225,
    height: 850,
    minWidth: 980,
    minHeight: 720,
    title: '归档流水线',
    webPreferences: {
      preload: path.join(__dirname, 'preload.js'),
      contextIsolation: true,
      nodeIntegration: false,
    },
  });

  win.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
}

app.whenReady().then(createWindow);

这里要注意 dist/index.html 是否进入最终资源目录。

如果路径错了,用户看到的就是白屏或加载失败。

14.2 资源检查

npm run build
test -f dist/index.html
find dist/assets -type f | sort

构建产物检查应该成为发布前固定动作。

十五、样式要像一条流水线

15.1 主布局样式

归档流水线适合使用稳重的纸张色和清晰边框。

.archive-app {
  min-height: 100%;
  display: grid;
  grid-template-columns: 300px minmax(0, 1fr);
  background: #f3efe6;
  color: #27313d;
}

.archive-board {
  min-height: 0;
  overflow: auto;
  padding: 20px;
}

.archive-card {
  border: 1px solid #d7c8ae;
  border-radius: 8px;
  background: #fffaf0;
  padding: 14px;
}

它不需要太多装饰。

重点是让资料状态和归档说明读起来清楚。

15.2 状态徽标

.state-badge {
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 8px;
  border-radius: 999px;
  font-size: 12px;
  font-weight: 700;
}

.archive-card.checking .state-badge {
  background: #fff1c2;
  color: #8a5a00;
}

状态徽标要小而明确。

不要抢走标题和摘要的注意力。

十六、滚动和窗口体验

16.1 内容滚动

归档说明可能很长,外层必须允许滚动。

html,
body,
#app {
  height: 100%;
  margin: 0;
}

.app-shell {
  height: 100vh;
  min-height: 0;
  overflow: hidden;
}

.app-content {
  height: 100%;
  min-height: 0;
  overflow: auto;
}

如果没有 min-height: 0,Flex 或 Grid 内部滚动经常会失效。

16.2 原生窗口栏

归档工具不需要自己做一套窗口按钮。

使用原生窗口栏更稳,最小化、最大化、关闭都由系统处理。

业务页面只负责内容。

十七、发布前验证

17.1 功能验证

我会按下面清单检查:

  1. 应用标题显示为归档流水线
  2. 新建资料后默认进入待归档状态
  3. 分类筛选不会导致编辑区异常空白
  4. 复制摘要可以写入剪贴板
  5. 导出 Markdown 包含来源和保留依据
  6. 已归档资料可以隐藏和恢复
  7. 删除不会误删其他资料

这些动作覆盖了主要归档链路。

17.2 发布检查表

检查项 结果 说明
标题一致 通过 页面、窗口和导出都叫归档流水线
图片存在 通过 结构图可显示
代码块完整 通过 覆盖类型、状态、组件、导出
资源链接 通过 保留社区、文档和 Electron
投票引导 通过 文末保留互动引导

技术文章写到最后,一定要回到“读者能不能照着检查”。

十八、后续扩展方向

18.1 文件系统能力

后续可以接真实文件处理:

  • 选择文件夹
  • 批量导入文件清单
  • 移动到归档目录
  • 同名文件冲突提示
  • 失败任务重试

这些能力可以逐步接,不要一次全塞进去。

18.2 更强的归档规则

规则层可以继续增强:

  1. 按文件类型生成建议分类
  2. 按关键词推荐归档主题
  3. 按项目名自动生成目录
  4. 导出归档报告
  5. 增加批量状态修改

做这些之前,先确保当前的单条归档链路足够稳定。

总结

归档流水线把文件整理从“移动文件”改成“解释资料为什么留下”。

它通过状态建模、本地保存、筛选排序、复制摘要和 Markdown 导出,把桌面资料整理做成一条可以追踪的轻流程。

后续如果要增强,我会先接文件选择和批量导入,再逐步扩展真实移动、规则推荐和归档报告。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐