鸿蒙PC:鸿蒙electron跨端框架PC影像巡检台实战:把图片管理做成可复核的本地工作流
欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区 :https://harmonypc.csdn.net/项目开源地址:https://AtomGit.com/lqjmac/ele-yingxiangxunjian很多图片管理工具只解决“看图”和“分类”,但在真实桌面工作里,图片经常不是单纯被欣赏,而是要被检查、标记、复核和交付。我把这个案例命名为影像巡检台,重点不是做一个普
前言
欢迎加入鸿蒙PC开发者社区,共同打造开发者工具生态:鸿蒙PC开发者社区 :https://harmonypc.csdn.net/
项目开源地址:https://AtomGit.com/lqjmac/ele-yingxiangxunjian
很多图片管理工具只解决“看图”和“分类”,但在真实桌面工作里,图片经常不是单纯被欣赏,而是要被检查、标记、复核和交付。
我把这个案例命名为 影像巡检台,重点不是做一个普通图库,而是把图片相关的判断过程沉淀下来。
它适合处理这些场景:
- 采样图片需要逐条复核
- 设计素材需要记录异常和来源
- 影像批次需要导出检查结论
- 本地桌面工具需要稳定运行、可复制、可导出
一个桌面图片工具要成立,不能只看缩略图够不够漂亮,还要看它能不能留下可追溯的判断记录。
本文会围绕 Vue3、Electron、鸿蒙 PC 运行环境、剪贴板、文件导出和构建检查展开。
相关技术栈可以参考 Electron 官方文档、Vue 官方文档、Vite 官方文档 和 TypeScript 文档。
一、先确定影像巡检台要解决什么
1.1 普通图库和巡检台的区别
图库关注的是浏览体验。
巡检台关注的是判断链路。
两者的差异可以先用一张表压住:
| 对比项 | 普通图库 | 影像巡检台 |
|---|---|---|
| 核心对象 | 图片文件 | 影像检查记录 |
| 主要动作 | 浏览、放大、收藏 | 标记、复核、导出 |
| 数据重点 | 文件路径、缩略图 | 批次、异常、结论 |
| 输出结果 | 看过即可 | 可交付的巡检摘要 |
这个差异会影响字段设计、页面结构和导出内容。
如果一开始没有把对象想清楚,后面很容易写成“左侧列表 + 右侧详情”的通用模板。
1.2 初始版本先做闭环
影像巡检台的初始版本不急着接入真实文件扫描。
原因很简单:真实文件扫描会引入路径权限、缩略图缓存、批量导入和性能优化。
如果主流程还没确定,就先做这些底层能力,容易把项目带散。
我先把闭环定成下面五步:
- 新建一条影像巡检记录
- 填写来源、异常类型和复核摘要
- 在列表中按状态和关键词找回
- 一键复制当前摘要
- 导出 Markdown 巡检记录
这五步跑通以后,再接真实图片文件会更稳。
初始版本不是功能少,而是先把最核心的业务路径打穿。
二、先看文件分工怎样服务巡检流程
2.1 文件职责表
| 文件 | 主要职责 | 影像巡检台里的关注点 |
|---|---|---|
| Home.vue | 页面总装 | 巡检列表、编辑区、工具栏组合 |
| useInspectItems.ts | 状态管理 | 本地保存、筛选、当前记录 |
| useNativeBridge.ts | 原生桥接 | 剪贴板、导出、通知 |
| electron/main.js | 主进程入口 | 窗口创建和本地页面加载 |
| electron/preload.js | 预加载桥 | 暴露安全 API |
| App.vue | 应用外壳 | 保证页面滚动和窗口填充 |
这种分工的好处是页面不直接碰底层能力。
页面只表达“我要复制”“我要导出”“我要通知”,具体是在浏览器预览还是鸿蒙运行时完成,由桥接层处理。
三、页面结构图
3.1 功能结构图

这张图对应影像巡检台的核心结构:左侧负责检索和状态入口,中间负责巡检记录,右侧负责复核信息和导出摘要。
3.2 三栏不是装饰
影像巡检台采用 状态入口 + 巡检列表 + 复核编辑区 的结构。
它不是为了把页面做复杂,而是为了让用户从“找问题”自然走到“写结论”。
| 区域 | 用户动作 | 设计重点 |
|---|---|---|
| 左侧入口 | 搜索、切换状态、查看统计 | 入口要短,不放长说明 |
| 中间列表 | 扫描异常记录 | 卡片要突出状态和批次 |
| 右侧编辑 | 写复核结论 | 字段要有足够上下文 |
这种布局在 PC 窗口里比较自然。
宽屏时三栏同时展示,窄一点时保持内容区域可滚动。
四、字段模型要服务巡检判断
4.1 核心字段设计
影像巡检台不能只保存 title 和 content。
它至少要知道图片来自哪里、现在是什么状态、为什么需要复查。
| 字段 | 含义 | 页面位置 |
|---|---|---|
| id | 巡检记录唯一标识 | 状态层 |
| title | 影像记录标题 | 列表和编辑区 |
| batch | 采集批次或来源 | 编辑区 |
| issueType | 异常类型 | 列表和编辑区 |
| state | 当前巡检状态 | 筛选和状态徽标 |
| summary | 巡检摘要 | 复制和导出 |
| highlights | 关键画面特征 | 右侧信息区 |
| nextReview | 复核时间 | 复核提示 |
字段不是为了显得完整,而是让每条影像记录能独立说明问题。
4.2 TypeScript 类型
export type InspectState = 'pending' | 'confirmed' | 'reviewing';
export type InspectCategory =
| 'sample'
| 'abnormal'
| 'workflow'
| 'standard';
export interface InspectItem {
id: string;
title: string;
batch: string;
issueType: string;
category: InspectCategory;
state: InspectState;
keywords: string;
summary: string;
highlights: string;
content: string;
nextReview: string;
pinned: boolean;
archived: boolean;
createdAt: number;
updatedAt: number;
}
这里把 state 和 category 拆开,是为了避免状态和分类互相污染。
分类描述它是什么资料,状态描述它处理到哪一步。
五、状态命名要让用户看懂
5.1 显示文案不要直接暴露枚举
代码里可以使用英文枚举,但界面上要用业务语言。
const stateLabelMap: Record<InspectState, string> = {
pending: '待巡检',
confirmed: '已确认',
reviewing: '需复核',
};
const categoryLabelMap: Record<InspectCategory, string> = {
sample: '待检影像',
abnormal: '异常线索',
workflow: '复核流程',
standard: '采样规范',
};
这段映射会出现在列表、编辑区、导出内容和筛选入口中。
统一它们可以避免同一状态在不同位置出现不同叫法。
5.2 状态颜色不要过多
状态色只保留三种即可:
- 待巡检:提示还有任务未处理
- 已确认:说明记录可以流转
- 需复核:提醒用户优先查看
过多颜色会让影像巡检台像监控大屏,但这个工具本质上还是个人桌面工作台。
六、种子数据要像真实巡检记录
6.1 为什么不能写测试数据
如果默认数据写成“图片1”“图片2”,页面看起来会很空。
真实一点的种子数据能同时测试列表、摘要、关键词、状态和导出。
export const seedInspectItems: InspectItem[] = [
{
id: 'inspect-glass-reflection',
title: '玻璃反光区域复核',
batch: 'A-2026-05-23',
issueType: '反光干扰',
category: 'abnormal',
state: 'reviewing',
keywords: '反光,边缘,遮挡',
summary: '样本右上角出现连续反光,需要复核是否影响边缘识别。',
highlights: '高亮区域集中在右上角,建议补拍同角度样本。',
content: '采样环境中存在强光源,右上角反光覆盖目标边缘。',
nextReview: '2026-05-25',
pinned: true,
archived: false,
createdAt: Date.now() - 3600_000,
updatedAt: Date.now(),
},
];
这条数据能让界面一打开就知道工具在做什么。
6.2 默认数据的写作标准
我给默认记录定了三条标准:
- 标题要能看出问题
- 摘要要能被直接复制
- 高亮要能提示下一步动作
这样默认数据既能服务演示,也能服务测试。
七、本地保存不要和页面混在一起
7.1 读取逻辑
本地工具最基本的要求是重启后数据还在。
这里可以用 localStorage 做初始版本的数据持久化。
const STORAGE_KEY = 'yingxiang-xunjian-items:v1';
function loadItems(): InspectItem[] {
if (typeof window === 'undefined') {
return seedInspectItems;
}
try {
const raw = window.localStorage.getItem(STORAGE_KEY);
if (!raw) return seedInspectItems;
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed : seedInspectItems;
} catch {
return seedInspectItems;
}
}
这段代码的重点是兜底。
本地缓存一旦被手动改坏,应用也不应该直接白屏。
7.2 保存逻辑
const items = ref<InspectItem[]>(loadItems());
function persistItems() {
if (typeof window === 'undefined') return;
window.localStorage.setItem(STORAGE_KEY, JSON.stringify(items.value));
}
保存动作可以配合防抖。
编辑器输入频繁,不能每敲一个字都同步写入。
桌面端应用的“已保存”反馈很重要,它会直接影响用户敢不敢把真实资料放进去。
八、筛选排序要突出待处理影像
8.1 搜索字段
影像巡检台的搜索不只看标题。
批次、异常类型、关键词、摘要都应该参与搜索。
const searchText = ref('');
const activeState = ref<'all' | InspectState>('all');
const filteredItems = computed(() => {
const keyword = searchText.value.trim().toLowerCase();
return items.value
.filter(item => {
if (activeState.value !== 'all' && item.state !== activeState.value) {
return false;
}
if (!keyword) return true;
return [
item.title,
item.batch,
item.issueType,
item.keywords,
item.summary,
item.highlights,
].join(' ').toLowerCase().includes(keyword);
})
.sort(sortInspectItems);
});
这个筛选逻辑能覆盖真实查找方式。
用户可能记得批次,也可能只记得“反光”。
8.2 排序规则
function sortInspectItems(a: InspectItem, b: InspectItem) {
if (a.archived !== b.archived) return a.archived ? 1 : -1;
if (a.pinned !== b.pinned) return a.pinned ? -1 : 1;
if (a.state !== b.state) {
const order: Record<InspectState, number> = {
reviewing: 0,
pending: 1,
confirmed: 2,
};
return order[a.state] - order[b.state];
}
return b.updatedAt - a.updatedAt;
}
排序不是技术细节,而是产品判断。
需要复核的记录应该靠前,已归档的记录应该沉下去。
九、Vue 页面负责组织工作台
9.1 Home.vue 的骨架
Home.vue 不应该承担所有逻辑。
它更适合做页面编排,把状态层、工具栏、列表和编辑器连起来。
<template>
<section class="inspect-workbench">
<aside class="inspect-rail">
<InspectSidebar
:items="filteredItems"
:summary="summary"
v-model:search-text="searchText"
@create="createItem"
@select="selectItem"
/>
</aside>
<main class="inspect-main">
<InspectToolbar
:has-item="Boolean(currentItem)"
@copy="handleCopy"
@export="handleExport"
/>
<InspectEditor
v-if="currentItem"
:item="currentItem"
@update="updateCurrentItem"
/>
</main>
</section>
</template>
这段模板的目标是清楚。
数据和动作都从状态层来,页面负责摆放。
9.2 反馈信息
复制、导出、保存都要有反馈。
桌面应用如果没有反馈,用户很难判断动作是否成功。
const feedback = ref('');
function showFeedback(message: string) {
feedback.value = message;
window.clearTimeout(feedbackTimer);
feedbackTimer = window.setTimeout(() => {
feedback.value = '';
}, 2200);
}
轻量反馈比弹窗更适合高频操作。
十、编辑区要承接复核结论
10.1 编辑字段布局
编辑区不能只放一个 textarea。
巡检记录需要结构化字段。
<template>
<article class="inspect-editor">
<input
class="title-input"
:value="item.title"
placeholder="输入影像记录标题"
@input="emitField('title', $event)"
/>
<div class="meta-grid">
<label>
<span>采集批次</span>
<input :value="item.batch" @input="emitField('batch', $event)" />
</label>
<label>
<span>异常类型</span>
<input :value="item.issueType" @input="emitField('issueType', $event)" />
</label>
</div>
<textarea
class="content-input"
:value="item.content"
placeholder="记录复核依据、画面问题和下一步处理动作"
@input="emitField('content', $event)"
/>
</article>
</template>
标题、批次、异常类型和正文分开后,导出时也更好组织。
10.2 输入事件处理
function emitField(field: keyof InspectItem, event: Event) {
const target = event.target as HTMLInputElement | HTMLTextAreaElement;
emit('update', {
id: props.item.id,
patch: {
[field]: target.value,
},
});
}
这里不要在子组件里直接改状态。
单向数据流能让保存和撤销都更容易扩展。
十一、复制动作要复制有价值的内容
11.1 摘要优先
影像巡检台的复制按钮不应该默认复制标题。
用户点击复制,通常是要把结论发给别人。
async function handleCopy() {
if (!currentItem.value) return;
const text =
currentItem.value.summary ||
currentItem.value.highlights ||
currentItem.value.title;
const ok = await copyText(text);
if (ok) {
showFeedback('巡检摘要已复制');
await notify('影像巡检台', '当前巡检摘要已经复制到剪贴板');
}
}
这个顺序很实用:
- 有摘要就复制摘要
- 没摘要就复制关键高亮
- 再没有才复制标题
11.2 剪贴板文档
剪贴板能力可以参考 MDN Clipboard API。
在 Electron 环境中,也可以参考 Electron clipboard。
鸿蒙 Electron 运行时可以通过预加载脚本统一封装,避免页面直接判断环境。
十二、导出 Markdown 要像巡检单
12.1 导出内容结构
导出的 Markdown 不是备份,而是交付记录。
function buildInspectMarkdown(item: InspectItem) {
return [
`# ${item.title || '未命名影像巡检记录'}`,
'',
`- 分类:${categoryLabelMap[item.category]}`,
`- 状态:${stateLabelMap[item.state]}`,
`- 批次:${item.batch || '未设置'}`,
`- 异常类型:${item.issueType || '未设置'}`,
`- 关键词:${item.keywords || '未设置'}`,
`- 下次复核:${item.nextReview || '未设置'}`,
'',
'## 巡检摘要',
'',
item.summary || '暂无摘要',
'',
'## 关键高亮',
'',
item.highlights || '暂无高亮',
'',
'## 复核记录',
'',
item.content || '暂无正文',
].join('\n');
}
这样导出的内容可以直接贴进项目文档或验收记录。
12.2 文件名处理
function safeFileName(value: string) {
return (value.trim() || '影像巡检记录')
.replace(/[\\/:*?"<>|]/g, '-')
.slice(0, 80);
}
文件名一定要做非法字符处理。
否则在不同系统之间保存时会出现奇怪问题。
十三、原生桥接层负责环境差异
13.1 统一 API
页面层不要散落一堆 window.electronAPI 判断。
桥接层统一输出方法:
export function useNativeBridge() {
const isNativeRuntime = computed(() => Boolean(window.ohosElectron));
async function copyText(text: string) {
if (!text.trim()) return false;
if (window.ohosElectron?.clipboard) {
return window.ohosElectron.clipboard.writeText(text);
}
await navigator.clipboard.writeText(text);
return true;
}
return {
isNativeRuntime,
copyText,
exportMarkdown,
notify,
};
}
这层写稳以后,其他桌面工具也能复用。
13.2 预加载脚本暴露能力
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('ohosElectron', {
clipboard: {
writeText: text => ipcRenderer.invoke('clipboard:write-text', text),
},
file: {
saveMarkdown: payload => ipcRenderer.invoke('file:save-markdown', payload),
},
notification: {
show: payload => ipcRenderer.invoke('notification:show', payload),
},
});
预加载层只暴露必要能力。
这比把 Node 能力直接丢给页面安全得多。
十四、主进程加载要避免白屏
14.1 本地 dist 路径
鸿蒙 Electron 打包后,页面应加载本地 dist/index.html。
const path = require('path');
const { app, BrowserWindow } = require('electron');
function createWindow() {
const win = new BrowserWindow({
width: 1225,
height: 850,
title: '影像巡检台',
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
contextIsolation: true,
nodeIntegration: false,
},
});
const indexPath = path.join(__dirname, '..', 'dist', 'index.html');
win.loadFile(indexPath, {
query: {
route: '/',
windowLabel: 'main',
},
});
}
app.whenReady().then(createWindow);
如果日志里出现 ERR_FILE_NOT_FOUND,优先检查 HAP 包内是否真的存在 resources/app/dist/index.html。
14.2 构建检查命令
npm run build
test -f dist/index.html
find dist -maxdepth 2 -type f | sort
这些命令很基础,但能快速排除资源没有进入包的问题。
十五、滚动和窗口控制要分清责任
15.1 页面滚动
PC 应用里,窗口高度经常变化。
如果外层没有设置好 min-height: 0,内部滚动很容易失效。
html,
body,
#app {
width: 100%;
height: 100%;
margin: 0;
}
.app-container {
width: 100%;
height: 100vh;
min-height: 0;
display: grid;
grid-template-rows: minmax(0, 1fr);
overflow: hidden;
}
.app-content {
min-height: 0;
overflow: auto;
}
这个写法让窗口保持满屏,内容区自己滚动。
15.2 窗口按钮
最小化、最大化、关闭应尽量交给原生窗口栏。
如果业务页面自己做窗口按钮,就要同时处理拖拽区域、双击最大化、焦点状态和异常退出。
对于这个工具,原生窗口栏更稳定。
十六、样式要突出可读性
16.1 主视觉
影像巡检台适合使用偏冷静的深蓝灰和浅色内容区。
它不是影集,也不是设计展板。
它需要的是稳定、耐看和高信息密度。
.inspect-workbench {
min-height: 100%;
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
background: #eef3f8;
color: #223047;
}
.inspect-card {
border: 1px solid #d6dee8;
background: #ffffff;
border-radius: 8px;
padding: 14px;
}
.inspect-card.reviewing {
border-color: #d66a38;
box-shadow: inset 4px 0 0 #d66a38;
}
状态色只用在关键位置。
16.2 卡片摘要
function excerpt(value: string) {
return value.trim().replace(/\s+/g, ' ').slice(0, 96) || '暂无巡检摘要';
}
摘要截断看起来简单,但列表能否扫读,很大程度取决于它。
十七、发布前检查清单
17.1 功能检查
发布前我会按下面顺序过一遍:
- 打开应用后标题显示为影像巡检台
- 新建记录后能立即进入编辑区
- 搜索批次、异常类型、关键词都能命中
- 复制摘要能写入剪贴板
- 导出 Markdown 能保留巡检元信息
- 页面内容超过窗口高度时可以滚动
- 关闭窗口不会误删本地数据
这些检查比单纯看页面截图可靠。
17.2 构建检查
| 检查项 | 期望结果 | 备注 |
|---|---|---|
| dist/index.html | 存在 | 避免白屏 |
| assets 文件 | 存在 | CSS 和 JS 进入包 |
| 标题文案 | 影像巡检台 | 不串到其他工具 |
| 剪贴板 | 可复制 | 桥接层可用 |
| 页面滚动 | 可滚动 | 长内容不截断 |
如果 HAP 日志出现资源加载错误,应先看构建产物,再看运行时路径。
十八、可以继续扩展的方向
18.1 真实图片能力
影像巡检台后续可以接入真实图片能力:
- 文件选择器导入图片
- 缩略图缓存
- 批量批次管理
- 图片元信息读取
- 标注区域截图
这些能力可以参考 MDN File API 和 Electron dialog。
18.2 工程化增强
工程层还可以补这些能力:
- 用 IndexedDB 替代 localStorage
- 给导出记录加入模板
- 给状态变更加入历史记录
- 加入键盘快捷键
- 增加批量归档
IndexedDB 可以参考 MDN IndexedDB。
总结
影像巡检台的重点不是把图片显示出来,而是把图片背后的判断过程做成可复核的桌面工作流。
通过字段建模、状态筛选、剪贴板复制、Markdown 导出和鸿蒙 Electron 本地加载,这个工具可以完成从记录到交付的一条闭环。
如果后续要继续扩展,我会先接真实图片导入和缩略图缓存,再考虑批量处理和标注能力。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 鸿蒙PC开发者社区:https://harmonypc.csdn.net/
- OpenHarmony 文档:https://docs.openharmony.cn/
- Electron 官方文档:https://www.electronjs.org/docs/latest/
更多推荐




所有评论(0)