前言

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

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

素材整理不只是把文件放进文件夹。

做设计、写文章、做活动页、整理产品参考时,真正容易丢的是“当时为什么觉得这份素材有用”。

所以这个案例做成 素材情绪板

它不像后台管理工具,也不像严肃的文件系统,而更像一张可以持续贴素材、写判断、留线索的桌面板。

它适合这些场景:

  • 收集视觉参考和配色方向
  • 记录文案片段和情绪关键词
  • 给素材补充适用场景
  • 把可复用线索导出到项目文档

素材的价值不只在文件本身,也在它被收藏时的判断和语境。

本文会从创作场景出发,拆解素材模型、情绪板布局、Vue 状态管理、Electron 桥接和导出能力。

一、素材情绪板先解决什么问题

1.1 素材管理和情绪板的差别

传统素材管理偏“存储”。

情绪板偏“唤起”。

对比项 素材管理器 素材情绪板
核心目标 找到文件 找回灵感和判断
常见字段 文件名、标签、路径 来源、情绪、关键词、适用场景
使用节奏 查找和下载 收集、筛选、复用
输出内容 文件或链接 素材说明和复用线索

这就是为什么这个工具不急着做成资源管理器。

第一版更应该把素材线索写清楚。

1.2 不做大而全

素材类工具很容易加功能:

  • 图片上传
  • 拖拽排序
  • 标签云
  • 多画板
  • 云同步

这些都不错,但第一版先聚焦一条主线:

  1. 收进一条素材线索
  2. 标记素材类型和可用状态
  3. 写下摘要、关键词和高亮
  4. 复制下一步可用的素材说明
  5. 导出 Markdown 素材卡

这条主线能跑通,工具就已经能帮助创作。

二、用文件分工承接创作面板

2.1 组件职责

素材情绪板需要的不是复杂后台组件,而是轻量创作面板。

文件 职责 素材场景里的说明
Home.vue 页面总装 组织素材栏、画板和编辑区
MaterialSidebar.vue 收纳栏 搜索、分类、统计、新建
MaterialBoard.vue 素材面板 置顶素材和素材卡片
MaterialEditor.vue 编辑区 来源、主题、关键词、摘要
useMaterials.ts 状态层 本地保存、筛选、排序
useNativeBridge.ts 桥接层 复制、导出、通知

如果项目内部仍然使用通用命名,也可以在文章里用业务职责解释清楚。

代码结构服务的是可维护性,不是表面命名。

2.2 状态层不要塞进组件

素材筛选、排序、本地保存、当前选中项都应放在 composable 中。

组件只负责展示和派发事件。

const {
  visibleMaterials,
  currentMaterial,
  createMaterial,
  updateMaterial,
  togglePinned,
  toggleArchived,
} = useMaterials();

这样页面会更像“组装面板”,而不是一整个大组件。

三、页面结构图

3.1 情绪板结构图

在这里插入图片描述

这张图对应素材情绪板的基本结构:左侧收纳素材,中间展示可复用卡片,右侧保留判断和关联线索。

3.2 页面气质要轻一点

素材情绪板不适合做得像报表后台。

它应该更接近一张创作前的桌面板。

区域 作用 视觉建议
收纳栏 快速进入分类 简洁、少说明
置顶区 放近期要用素材 更醒目
卡片区 扫描素材线索 允许轻微错落
编辑区 写判断和摘要 保持可读

页面可以有一点暖色,但不要过度装饰。

技术文章里也要说明为什么这样设计,而不是只贴 CSS。

四、数据模型要保留素材语境

4.1 核心字段

素材条目不是文件索引。

它要保留素材为什么可用。

字段 含义 示例
title 素材标题 活动主视觉参考
source 来源 设计稿、网站、项目复盘
topic 主题 低饱和配色
category 类型 视觉素材、情绪线索
state 状态 待整理、可复用、待确认
keywords 关键词 留白、轻量、温暖
summary 摘要 适合作为卡片页氛围参考
highlights 高亮 标题区和按钮层级清楚

字段越贴近创作动作,工具越不像空壳。

4.2 TypeScript 类型

export type MaterialState = 'draft' | 'reusable' | 'checking';

export type MaterialCategory =
  | 'visual'
  | 'mood'
  | 'action'
  | 'reference';

export interface MaterialItem {
  id: string;
  title: string;
  source: string;
  topic: string;
  category: MaterialCategory;
  state: MaterialState;
  keywords: string;
  summary: string;
  highlights: string;
  related: string;
  content: string;
  pinned: boolean;
  archived: boolean;
  createdAt: number;
  updatedAt: number;
}

related 字段很重要。

很多素材不是独立有用,而是和另一个项目、页面、配色或文案一起产生价值。

五、文案映射要有创作味道

5.1 类型和状态

const categoryLabelMap: Record<MaterialCategory, string> = {
  visual: '视觉素材',
  mood: '情绪线索',
  action: '整理动作',
  reference: '参考资料',
};

const stateLabelMap: Record<MaterialState, string> = {
  draft: '待整理',
  reusable: '可复用',
  checking: '待确认',
};

这些文案要贯穿整个工具。

不要一会儿叫素材,一会儿叫资料,一会儿又叫条目。

5.2 状态含义

状态 含义 适合的动作
待整理 刚收进来,还没判断 补摘要、补关键词
可复用 已经明确用途 复制说明、导出
待确认 权限、尺寸或上下文不清 继续复核

把状态含义写清楚,用户才知道下一步怎么处理。

六、默认素材要像真的收集过

6.1 示例数据

export const seedMaterials: MaterialItem[] = [
  {
    id: 'material-soft-dashboard',
    title: '低饱和数据卡片参考',
    source: '产品官网截图',
    topic: '轻量工作台视觉',
    category: 'visual',
    state: 'reusable',
    keywords: '低饱和,卡片,留白,柔和阴影',
    summary: '适合作为桌面工具首页的数据卡片参考。',
    highlights: '标题层级清楚,按钮不抢主内容注意力。',
    related: '可用于归档工具、模板工具首页视觉。',
    content: '整体色彩克制,适合信息密度较高但不想显得压迫的工具页面。',
    pinned: true,
    archived: false,
    createdAt: Date.now() - 5400_000,
    updatedAt: Date.now(),
  },
];

这条素材不是“素材1”,而是有来源、有主题、有用途。

6.2 种子内容标准

好的默认素材应该满足:

  1. 一眼知道它能用在哪里
  2. 摘要能直接复制给别人
  3. 高亮能说明为什么留下
  4. 关键词能帮助再次找回
  5. 关联能提示后续组合

情绪板的默认数据越真实,读者越容易理解字段为什么这样设计。

七、本地保存让素材可回访

7.1 读取数据

const STORAGE_KEY = 'sucai-qingxuban-materials:v1';

function loadMaterials(): MaterialItem[] {
  if (typeof window === 'undefined') return seedMaterials;

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

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

本地保存不用一开始做得很重。

但必须稳定。

7.2 创建素材

function createMaterial() {
  const now = Date.now();
  const item: MaterialItem = {
    id: `material-${now}-${Math.random().toString(16).slice(2)}`,
    title: '新的素材线索',
    source: '',
    topic: '',
    category: 'visual',
    state: 'draft',
    keywords: '',
    summary: '',
    highlights: '',
    related: '',
    content: '',
    pinned: false,
    archived: false,
    createdAt: now,
    updatedAt: now,
  };

  materials.value = [item, ...materials.value];
  currentMaterialId.value = item.id;
  schedulePersist();
}

默认分类设为视觉素材,是为了降低新建成本。

用户可以再切换类型。

八、筛选要支持灵感回找

8.1 搜索范围

const searchTerm = ref('');
const activeCategory = ref<'all' | MaterialCategory>('all');

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

  return materials.value
    .filter(item => {
      if (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,
        item.related,
      ].join(' ').toLowerCase().includes(keyword);
    })
    .sort(sortMaterials);
});

素材回找经常不是按标题。

用户更可能记得一个颜色、一个语气、一个项目名。

8.2 排序规则

function sortMaterials(a: MaterialItem, b: MaterialItem) {
  if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;

  const order: Record<MaterialState, number> = {
    reusable: 0,
    checking: 1,
    draft: 2,
  };

  if (a.state !== b.state) {
    return order[a.state] - order[b.state];
  }

  return b.updatedAt - a.updatedAt;
}

可复用素材靠前,符合情绪板的使用习惯。

九、Vue 页面像一张创作桌

9.1 页面骨架

<template>
  <section class="material-app">
    <MaterialSidebar
      :summary="summary"
      v-model:search-term="searchTerm"
      @create="createMaterial"
      @set-category="setCategory"
    />

    <main class="moodboard">
      <MaterialToolbar
        :has-material="Boolean(currentMaterial)"
        @copy="handleCopy"
        @export="handleExport"
        @pin="handlePin"
      />

      <MaterialBoard
        :materials="visibleMaterials"
        :current-id="currentMaterialId"
        @select="selectMaterial"
      />
    </main>

    <MaterialEditor
      v-if="currentMaterial"
      :material="currentMaterial"
      @update="updateMaterial"
    />
  </section>
</template>

这个结构把收纳、展示和编辑分成三个角色。

读者照着看也比较容易迁移。

9.2 反馈提示

const feedback = ref('');

function showFeedback(message: string) {
  feedback.value = message;
  window.setTimeout(() => {
    feedback.value = '';
  }, 2200);
}

情绪板的反馈不需要打断用户。

短提示就够了。

十、编辑器保留判断

10.1 编辑区结构

<template>
  <article class="material-editor">
    <input
      class="title-input"
      :value="material.title"
      placeholder="素材标题"
      @input="updateField('title', $event)"
    />

    <label>
      <span>素材来源</span>
      <input :value="material.source" @input="updateField('source', $event)" />
    </label>

    <label>
      <span>情绪关键词</span>
      <textarea :value="material.keywords" @input="updateField('keywords', $event)" />
    </label>

    <label>
      <span>为什么留下它</span>
      <textarea :value="material.summary" @input="updateField('summary', $event)" />
    </label>
  </article>
</template>

这里的重点不是“正文”,而是“为什么留下它”。

这会让工具更像创作辅助,而不是资料录入表。

10.2 关键词拆分

const keywordItems = computed(() => {
  if (!currentMaterial.value?.keywords.trim()) return [];

  return currentMaterial.value.keywords
    .split(/[,\n,]/)
    .map(item => item.trim())
    .filter(Boolean);
});

关键词拆成标签后,右侧信息区会更有情绪板的感觉。

十一、复制动作复制素材说明

11.1 复制逻辑

async function handleCopyMaterial() {
  if (!currentMaterial.value) return;

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

  const ok = await copyText(text);

  if (ok) {
    showFeedback('素材说明已复制');
    await notify('素材情绪板', '当前素材说明已经复制到剪贴板');
  }
}

素材工具复制出去的内容应该是可读说明。

直接复制标题通常不够用。

11.2 剪贴板差异

可以参考 Electron clipboardMDN Clipboard API

在桌面端,剪贴板是非常高频的流转入口。

所以它要被单独封装,不要写散。

十二、导出 Markdown 做成素材卡

12.1 导出内容

function buildMaterialMarkdown(material: MaterialItem) {
  return [
    `# ${material.title || '未命名素材'}`,
    '',
    `- 类型:${categoryLabelMap[material.category]}`,
    `- 状态:${stateLabelMap[material.state]}`,
    `- 来源:${material.source || '未设置'}`,
    `- 主题:${material.topic || '未设置'}`,
    `- 关键词:${material.keywords || '未设置'}`,
    '',
    '## 这份素材为什么值得留下',
    '',
    material.summary || '暂无摘要',
    '',
    '## 可复用线索',
    '',
    material.highlights || '暂无高亮',
    '',
    '## 关联参考',
    '',
    material.related || '暂无关联',
    '',
    '## 备注',
    '',
    material.content || '暂无正文',
  ].join('\n');
}

导出后更像一张素材卡。

它可以直接放进项目复盘或设计说明。

12.2 文件名处理

function safeMaterialFileName(value: string) {
  return (value.trim() || '素材卡')
    .replace(/[\\/:*?"<>|]/g, '-')
    .slice(0, 80);
}

保存文件名要干净。

不要把太多字段塞到文件名里。

十三、主进程加载本地页面

13.1 BrowserWindow 设置

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

function createWindow() {
  const win = new BrowserWindow({
    width: 1225,
    height: 850,
    minWidth: 960,
    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);

桌面端标题要和应用主题一致。

不要让运行窗口还显示旧名字。

13.2 构建检查

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

这个检查可以快速发现前端资源是否生成。

如果 HAP 包内没有 dist/index.html,运行时必然加载失败。

十四、样式要区别于后台工具

14.1 情绪板主题

.material-app {
  min-height: 100%;
  display: grid;
  grid-template-columns: 280px minmax(0, 1fr) 360px;
  background: #f6efe7;
  color: #28313a;
}

.moodboard-card {
  border: 1px solid #ead7c4;
  border-radius: 8px;
  background: #fffaf4;
  padding: 14px;
  box-shadow: 8px 8px 0 rgba(40, 49, 58, 0.08);
}

.moodboard-card.active {
  border-color: #28313a;
  box-shadow: 8px 8px 0 #f06f52;
}

这套样式带一点手工板感,但仍然保持克制。

14.2 标签样式

.keyword-chip {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  padding: 0 8px;
  border-radius: 999px;
  background: #ffe1d4;
  color: #8a3f2c;
  font-size: 12px;
  font-weight: 700;
}

标签是素材情绪板的重要视觉元素。

它能让用户快速抓到风格线索。

十五、滚动和响应式处理

15.1 三栏滚动

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

.moodboard,
.material-editor,
.material-sidebar {
  min-height: 0;
  overflow: auto;
}

三栏布局最容易出的问题是中间或右侧不能滚动。

每一栏都要明确滚动边界。

15.2 窗口缩小时

@media (max-width: 1100px) {
  .material-app {
    grid-template-columns: 260px minmax(0, 1fr);
  }

  .material-editor {
    grid-column: 1 / -1;
  }
}

PC 应用不等于永远大窗口。

用户可能会把它和浏览器、文档并排打开。

十六、原生桥接封装

16.1 预加载脚本

const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('desktopBridge', {
  copyText: text => ipcRenderer.invoke('copy-text', text),
  saveMarkdown: payload => ipcRenderer.invoke('save-markdown', payload),
  showNotification: payload => ipcRenderer.invoke('show-notification', payload),
});

预加载脚本只暴露必要能力。

这样比把 Node 能力直接打开更安全。

16.2 页面调用

async function exportCurrentMaterial() {
  if (!currentMaterial.value) return;

  const markdown = buildMaterialMarkdown(currentMaterial.value);
  const result = await exportMarkdown(currentMaterial.value.title, markdown);

  if (result.ok) {
    showFeedback('素材卡已导出');
  }
}

页面只关心结果,不关心底层保存方式。

十七、发布前检查

17.1 功能检查

发布前检查下面这些点:

  1. 标题显示为素材情绪板
  2. 默认素材不是空占位
  3. 关键词可以被搜索
  4. 标签展示不会撑破卡片
  5. 复制素材说明可用
  6. 导出 Markdown 像素材卡
  7. 三栏内容都能滚动

这些检查能覆盖创作型桌面工具的主要风险。

17.2 文章发布检查

检查项 结果 说明
图片存在 通过 结构图能显示
表格数量 通过 覆盖对比、字段、检查
代码块 通过 覆盖类型、状态、组件、导出
链接数量 通过 正文和资源区包含有效链接
总结和投票 通过 文末保留引导

技术文章不是代码贴得越多越好,而是要让读者知道每段代码为什么出现。

十八、后续增强方向

18.1 素材导入能力

后续可以逐步增加:

  • 图片文件导入
  • 拖拽上传
  • 缩略图缓存
  • 颜色提取
  • 多画板分组

真实文件处理可以参考 MDN File API

18.2 创作辅助能力

更进一步可以做:

  1. 根据关键词推荐相关素材
  2. 生成项目素材包
  3. 导出设计说明
  4. 按主题生成情绪板视图
  5. 支持素材归档和恢复

这些能力都建立在素材字段已经稳定的基础上。

总结

素材情绪板的重点不是存更多素材,而是保留素材被收藏时的语境。

通过素材类型、可用状态、关键词、摘要、高亮、复制和导出,桌面端可以变成一个轻量创作准备台。

后续如果继续扩展,我会优先做真实图片导入和缩略图缓存,再考虑颜色分析和多画板。

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


相关资源:

Logo

一站式 AI 云服务平台

更多推荐