回收站模块 Cordova 与 OpenHarmony 混合开发实战
摘要:回收站模块采用Cordova框架与OpenHarmony原生能力结合,实现"软删除"机制。当用户删除记录时,数据被移至回收站表而非直接删除,支持后续恢复操作。回收站页面提供记录浏览、搜索、筛选功能,用户可单条或批量恢复记录,也可彻底删除或清空回收站。该设计通过IndexedDB存储回收数据,并记录操作日志,在保障数据安全的同时降低误操作风险,实现了完整的数据恢复与审计链路
欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。

📌 概述
回收站模块用于管理被删除的记录,是数据安全和误删恢复的重要保障。该模块集成了 Cordova 框架与 OpenHarmony 原生能力,实现了“软删除”机制:业务页面不直接物理删除数据,而是将记录移动到回收站表,只有在回收站中执行“彻底删除”时才真正移除。用户可以在回收站中浏览、搜索、按类型筛选被删除的喝茶记录,并支持单条或批量恢复,还可以一键清空回收站。通过这一设计,应用在保持易用体验的同时,大幅降低了误操作带来的数据风险。
🔗 完整流程
第一步:删除动作转向回收站
在记录列表、编辑页面等业务模块,当用户点击“删除”按钮时,并不会立即从主表中删除数据,而是走一条“移动到回收站”的流程:前端通过 db.moveToTrash(record) 将记录插入回收站表,并在主业务表中标记为已删除或直接删除。与此同时,通过 Cordova 调用 OpenHarmony 原生插件,在日志中记录本次删除操作,包括记录 ID、删除时间、来源页面等,方便后续审计和问题排查。用户此时会看到“已移入回收站”的提示,而不是“已彻底删除”,在心理上也更安全。
第二步:回收站列表浏览与筛选
当用户进入“回收站”页面时,前端会从 IndexedDB 的 trash 表中加载所有软删除的记录,并按删除时间倒序排列,最近删除的记录排在最上方。用户可以通过搜索框按茶叶名称、产地、备注等快速过滤,也可以通过下拉框筛选删除来源(如“记录列表删除”“批量删除”“导入覆盖”等)。列表中每一条记录会标出原始类型、删除时间和删除原因,辅助用户判断是否需要恢复。
第三步:恢复或彻底删除
在回收站页面,用户可以对单条记录执行“恢复”操作,也可以勾选多条记录进行批量恢复。恢复时,系统会将记录重新写回业务表(如 records),并从 trash 表中移除对应条目。若用户选择“彻底删除”,则会在前端弹出二次确认提示,确认后才调用 db.deleteFromTrash(id) 和原生层的真实删除逻辑。所有恢复和彻底删除操作同样会被记录到 OpenHarmony 的原生日志中,形成完整的操作审计链路。
🔧 Web 代码实现
回收站页面 HTML 结构
<div id="trash-page" class="page">
<div class="page-header">
<h1>回收站</h1>
<button class="btn btn-danger" onclick="clearTrash()">清空回收站</button>
</div>
<div class="trash-toolbar">
<input type="text" id="trash-search" class="search-box" placeholder="搜索被删除记录...">
<select id="trash-source-filter" onchange="filterTrashBySource()">
<option value="">全部来源</option>
<option value="record-list">记录列表</option>
<option value="bulk-delete">批量删除</option>
<option value="import-overwrite">导入覆盖</option>
</select>
</div>
<div class="trash-actions-bar">
<button class="btn btn-secondary" onclick="selectAllTrash(true)">全选</button>
<button class="btn btn-secondary" onclick="selectAllTrash(false)">全不选</button>
<button class="btn btn-primary" onclick="restoreSelected()">恢复选中</button>
<button class="btn btn-danger" onclick="deleteSelected()">彻底删除选中</button>
</div>
<div id="trash-list" class="trash-list">
<!-- 回收站记录项动态生成 -->
</div>
</div>
这段 HTML 定义了回收站页面的基本结构:顶部是标题和“清空回收站”按钮,中间是搜索和来源筛选工具栏,下方是批量操作工具条,最底部是动态渲染的回收站记录列表。通过 trash-search 输入框可以按关键字过滤被删除记录,trash-source-filter 下拉框则用于按删除来源分类查看。批量操作按钮配合复选框可以一次性恢复或彻底删除多条记录,从而提高效率。
回收站列表渲染与搜索
let allTrashRecords = [];
let filteredTrashRecords = [];
async function renderTrashPage() {
try {
allTrashRecords = await db.getTrashRecords();
filteredTrashRecords = [...allTrashRecords];
renderTrashList();
bindTrashEvents();
} catch (error) {
console.error('Failed to load trash:', error);
showToast('加载回收站失败', 'error');
}
}
function renderTrashList() {
const container = document.getElementById('trash-list');
container.innerHTML = '';
if (filteredTrashRecords.length === 0) {
container.innerHTML = '<div class="no-data"><p>回收站为空</p></div>';
return;
}
filteredTrashRecords.forEach(item => {
const el = document.createElement('div');
el.className = 'trash-item';
el.dataset.id = item.id;
const deletedAt = new Date(item.deletedAt).toLocaleString('zh-CN');
el.innerHTML = `
<label class="trash-checkbox">
<input type="checkbox" class="trash-select" data-id="${item.id}">
<span></span>
</label>
<div class="trash-info">
<div class="trash-title">${item.data.teaType || '未知茶叶'}</div>
<div class="trash-meta">
<span>删除时间: ${deletedAt}</span>
<span>来源: ${item.source || '未知'}</span>
</div>
${item.data.notes ? `<div class="trash-notes">${item.data.notes}</div>` : ''}
</div>
<div class="trash-actions">
<button class="btn-icon" onclick="restoreOne(${item.id})" title="恢复">↩</button>
<button class="btn-icon" onclick="deleteOne(${item.id})" title="彻底删除">🗑️</button>
</div>
`;
container.appendChild(el);
});
}
function bindTrashEvents() {
const searchInput = document.getElementById('trash-search');
if (searchInput && !searchInput._bound) {
searchInput._bound = true;
searchInput.addEventListener('input', e => {
const keyword = e.target.value.toLowerCase();
filteredTrashRecords = allTrashRecords.filter(item => {
const data = item.data || {};
const text = `${data.teaType || ''} ${data.origin || ''} ${data.notes || ''}`.toLowerCase();
return text.includes(keyword);
});
renderTrashList();
});
}
}
这里首先通过 db.getTrashRecords() 读取所有回收站记录,保存在 allTrashRecords 中,再复制一份到 filteredTrashRecords 用于搜索和筛选。renderTrashList() 根据当前过滤结果渲染 DOM,每条记录包括复选框、基本信息和单条操作按钮。bindTrashEvents() 为搜索输入框绑定事件监听,在用户输入关键字时动态过滤 filteredTrashRecords 并重新渲染列表。这样就实现了前端即时搜索,无需额外的数据库查询开销。
恢复与彻底删除逻辑
async function restoreOne(id) {
const record = allTrashRecords.find(r => r.id === id);
if (!record) return;
try {
await db.restoreFromTrash(id);
if (window.cordova) {
cordova.exec(null, null, 'TeaLogger', 'logEvent', [
'trash_restore_one', { id, source: record.source }
]);
}
showToast('记录已恢复', 'success');
renderTrashPage();
} catch (error) {
console.error('Failed to restore record:', error);
showToast('恢复失败', 'error');
}
}
async function deleteOne(id) {
if (!confirm('确定要彻底删除这条记录吗?此操作不可恢复。')) {
return;
}
try {
await db.deleteFromTrash(id);
if (window.cordova) {
cordova.exec(null, null, 'TeaLogger', 'logEvent', [
'trash_delete_one', { id }
]);
}
showToast('记录已彻底删除', 'success');
renderTrashPage();
} catch (error) {
console.error('Failed to delete record:', error);
showToast('删除失败', 'error');
}
}
function getSelectedTrashIds() {
const checkboxes = document.querySelectorAll('.trash-select:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.dataset.id, 10));
}
async function restoreSelected() {
const ids = getSelectedTrashIds();
if (ids.length === 0) {
showToast('请先选择要恢复的记录', 'info');
return;
}
for (const id of ids) {
await db.restoreFromTrash(id);
}
if (window.cordova) {
cordova.exec(null, null, 'TeaLogger', 'logEvent', [
'trash_restore_batch', { count: ids.length }
]);
}
showToast(`已恢复 ${ids.length} 条记录`, 'success');
renderTrashPage();
}
async function deleteSelected() {
const ids = getSelectedTrashIds();
if (ids.length === 0) {
showToast('请先选择要删除的记录', 'info');
return;
}
if (!confirm(`确定要彻底删除选中的 ${ids.length} 条记录吗?此操作不可恢复。`)) {
return;
}
for (const id of ids) {
await db.deleteFromTrash(id);
}
if (window.cordova) {
cordova.exec(null, null, 'TeaLogger', 'logEvent', [
'trash_delete_batch', { count: ids.length }
]);
}
showToast(`已彻底删除 ${ids.length} 条记录`, 'success');
renderTrashPage();
}
async function clearTrash() {
if (!confirm('确定要清空整个回收站吗?此操作不可恢复。')) {
return;
}
try {
await db.clearTrash();
if (window.cordova) {
cordova.exec(null, null, 'TeaLogger', 'logEvent', [
'trash_clear_all', { count: allTrashRecords.length }
]);
}
showToast('回收站已清空', 'success');
renderTrashPage();
} catch (error) {
console.error('Failed to clear trash:', error);
showToast('清空失败', 'error');
}
}
这里分别实现了单条恢复、单条彻底删除、批量恢复、批量彻底删除和清空回收站的逻辑。restoreOne() 和 deleteOne() 针对单条记录进行操作,批量函数则先通过 getSelectedTrashIds() 获取选中的 ID,再在循环中逐条调用数据库 API。每一次重要操作都会通过 cordova.exec 调用 TeaLogger 原生插件记录日志,形成可追踪的操作审计链。针对不可恢复的操作(例如彻底删除、清空回收站),都通过 confirm() 弹窗进行二次确认,降低误操作风险。
🔌 OpenHarmony 原生代码(ArkTS)
回收站数据结构与恢复实现
// entry/src/main/ets/plugins/TrashManager.ets
import { relationalStore } from '@kit.ArkData';
export class TrashManager {
private store: relationalStore.RdbStore;
async init(store: relationalStore.RdbStore) {
this.store = store;
const sql = `
CREATE TABLE IF NOT EXISTS trash (
id INTEGER PRIMARY KEY AUTOINCREMENT,
entity_type TEXT NOT NULL,
entity_id INTEGER,
data TEXT NOT NULL,
source TEXT,
deleted_at TEXT NOT NULL
)
`;
await this.store.executeSql(sql);
}
async moveToTrash(entityType: string, entityId: number, data: object, source: string): Promise<number> {
const values: relationalStore.ValuesBucket = {
entity_type: entityType,
entity_id: entityId,
data: JSON.stringify(data),
source: source,
deleted_at: new Date().toISOString()
};
return await this.store.insert('trash', values);
}
async restoreRecord(trashId: number): Promise<void> {
const predicates = new relationalStore.RdbPredicates('trash');
predicates.equalTo('id', trashId);
const resultSet = await this.store.query(predicates);
if (!resultSet.goToFirstRow()) {
resultSet.close();
return;
}
const entityType = resultSet.getString(resultSet.getColumnIndex('entity_type'));
const dataJson = resultSet.getString(resultSet.getColumnIndex('data'));
const data = JSON.parse(dataJson);
resultSet.close();
if (entityType === 'record') {
await this.restoreTeaRecord(data);
}
await this.store.delete(predicates);
}
private async restoreTeaRecord(data: Record<string, unknown>): Promise<void> {
const values: relationalStore.ValuesBucket = {
tea_type: data['teaType'],
origin: data['origin'],
price: data['price'],
rating: data['rating'],
notes: data['notes'],
created_at: data['createdAt'] ?? new Date().toISOString()
};
await this.store.insert('tea_records', values);
}
}
这个 ArkTS 原生插件 TrashManager 负责在关系型数据库中维护 trash 表,并提供移动到回收站和从回收站恢复的能力。moveToTrash() 会将原始记录序列化为 JSON 存到 data 字段,并记录实体类型、原始 ID、删除来源和删除时间。restoreRecord() 先查出回收站记录,根据 entity_type 决定调用哪种恢复方法(这里以 record 类型为例),恢复成功后再删除对应的 trash 表项,避免重复恢复。通过这种方式,可以在原生层保证数据的原子性和一致性。
📝 总结
回收站模块是整个喝茶记录应用的“安全网”,它通过 软删除 + 回收站存储 + 原生日志审计 的组合,大幅降低了用户误删数据的风险。Web 层负责提供清晰的列表展示、搜索筛选和批量操作入口,原生 ArkTS 层则负责真正的数据落盘、恢复和彻底删除逻辑。通过 TrashManager、TeaLogger 等插件,Cordova 与 OpenHarmony 之间形成了一条稳定的数据安全通路。你在日后的其他项目中(例如财务应用、笔记应用、观影记录应用)也可以复用这套设计:将所有“删除”统一收口到回收站模块,再配合原生层的类型化恢复逻辑,就能在保持良好体验的前提下,最大限度保护用户数据不被意外丢失。
更多推荐



所有评论(0)