配电柜现场资料板在上一章解决了“拖的是哪张卡、可以放进哪一栏”的问题,实际接入附件时,新的矛盾很快出现:照片有JPEG与PNG,巡查报告是PDF,接线图可能是PDF也可能是图片,维修人员还会选中一个视频和一个表格。它们可以同时出现在文件选择结果中,却不该因为看起来都带文件名,就被统一投到“设备照片”栏。更麻烦的是,文件扩展名、页面看到的类型文字、内容描述和当前是否还能读取资源,并不总是相互一致。

这里继续沿用现场资料板工程,三个主栏仍为“待整理”“设备照片”“巡查记录”,板ID使用site-east,卡片保持itemId、boardId、columnId、kind、revision等固定字段。本篇只补充附件层attachmentId、displayName、declaredMime、sizeBytes、sourceRef、availability等信息。附件与资料卡片分开保存:一张卡片可关联数个附件,附件名称改动不能让卡片身份变化。第三篇才处理悬停取消和拖拽中途清理。

资料板多附件元信息与过滤流程

为什么不直接看文件名后缀

维修人员有时会把照片重命名为“配电柜检查.pdf”,也可能将扫描形成的PDF报告改成“.jpg”以便识别。在文件预览阶段用后缀画一个图标问题不大,在业务接收阶段把后缀当作信任依据则会出错:目标区允许图片,却接收到了无法按图片解析的内容。另一种常见错误是处理image/*时把image/svg+xml与JPEG统一交给缩略图组件,而实际处理链、资源大小限制及安全要求并不相同。

附件进入资料板应分三步:先保留来源给出的文件类型线索,再按应用支持清单做归类,最后由目标栏的接收规则作决定。文件名、MIME与统一数据类型描述符都能提供线索,却有各自用途;当来源不可信或格式不在清单内,应标记“待确认”,不要通过扩展名强行升级权限。华为的UTD预置类型列表解释了为什么相同数据可能具有不同的MIME、扩展名和统一类型标识。实际使用UDMF前,还应核对应用接收路径、数据访问授权与相关系统接口的版本要求。

为了把类型过滤和文件读取分开,业务域先用一个显式白名单处理已知的MIME。它不去读取文件字节,也不把一个可访问的URI自动变成长期有效的路径。下面包含文件名净化、字节大小检查和四种常见类型归类,未知类型留给页面展示拒绝原因。

const assert = require('node:assert/strict');
const SUPPORTED = new Map([
  ['image/jpeg','photo'], ['image/png','photo'],
  ['application/pdf','report'], ['text/plain','report']
]);
function describeAttachment(raw) {
  const mime = String(raw.mime || '').toLowerCase().trim();
  const size = Number(raw.sizeBytes);
  if (!Number.isSafeInteger(size) || size < 0) throw new Error('附件大小非法');
  const name = String(raw.displayName || '').replace(/[\\/\x00-\x1f]/g,'_').trim();
  if (!name || name.length > 120) throw new Error('附件名称不合法');
  return Object.freeze({ attachmentId:String(raw.attachmentId),
    displayName:name, mime, sizeBytes:size,
    kind:SUPPORTED.get(mime) || 'unknown',
    availability: raw.available === true ? 'available' : 'needs-check' });
}
const image = describeAttachment({ attachmentId:'ATT-PHOTO-1',
  displayName:'配电柜现场.jpg',mime:'image/jpeg',sizeBytes:2800000,available:true });
assert.equal(image.kind, 'photo');
assert.equal(describeAttachment({ attachmentId:'ATT-X',displayName:'报告.jpg',
  mime:'application/octet-stream',sizeBytes:800,available:true }).kind,'unknown');

归类清单是产品自己的接收策略,并不等同于平台支持的完整类型集合。比如业务允许PDF和纯文本进入“巡查记录”,但不允许电子表格和视频直接归档;如果以后扩展维护日志,需要同时扩展预览、索引与权限处理链,而不是在白名单里追加一个字符串就宣布完成。附件真实内容的安全检查应在获得受控访问能力后独立进行,类型字段与签名不匹配时停止后续处理。

一次选择多个附件,接收结果应该逐项可解释

多选以后页面不能用一个布尔值代表所有附件“投放成功”。假设选中了配电柜现场JPEG、现场接线PNG以及巡查记录PDF,目标列为“设备照片”,前两个可接收,PDF应保留在待整理区域,并给出“目标栏不接收报告”的原因。若采用静默丢弃,用户会认为三个文件都已经归档;若把整个批次全部拒绝,现场人员又得重新选择两张有效图片。资料板采用“逐项判定、批次提交一份快照”的策略,既能清楚表达部分成功,又能防止提交过程中出现同一文件在两列同时存在。

投放前再次确认boardId和当前列规则非常必要。用户在文件选择窗口停留几分钟,期间可能切到另一个工地;图片虽然还是图片,旧site-east板上的会话却不应在新工地生效。来源卡片的revision也应在提交时核对,避免上一页面的列移动覆盖较新的分类。标记可接收,只说明类型规则通过,仍须检查文件大小、当前可访问状态和卡片归属。

接收规则写成明确的函数,可以直接用于触控投放、鼠标拖拽和无障碍操作入口。它只返回业务决策,不涉及系统事件API,因此测试时能稳定覆盖拒绝理由。

const assert = require('node:assert/strict');
const COLUMN_MIME = {
  inbox: new Set(['image/jpeg','image/png','application/pdf','text/plain']),
  photos: new Set(['image/jpeg','image/png']),
  reports: new Set(['application/pdf','text/plain'])
};
function decideDrop(file, targetColumn, boardId, activeBoardId) {
  if (boardId !== activeBoardId) return { ok:false, reason:'资料板已切换' };
  if (file.availability !== 'available') return { ok:false, reason:'来源需重新确认' };
  if (file.sizeBytes > 20*1024*1024) return { ok:false, reason:'附件超过本栏大小策略' };
  if (!COLUMN_MIME[targetColumn]?.has(file.mime))
    return { ok:false, reason:'目标栏不接收该类型' };
  return { ok:true, reason:'可接收' };
}
const meta = { mime:'application/pdf',sizeBytes:3200000,availability:'available' };
assert.equal(decideDrop(meta,'photos','site-east','site-east').ok,false);
assert.equal(decideDrop(meta,'reports','site-east','site-east').ok,true);

大小上限是此资料板的产品策略,不能当作系统文件限制。更重要的是sourceRef:读取授权可能随应用重启、源文件移动或用户撤销而变化。列表中看到过缩略图,不能据此认为以后任何时候都可以重新读取原始字节。资料卡片持久层保存受控引用和显示所需元信息,临时读取句柄在文件处理完成后应关闭,引用过期时显示“重新关联”入口;相关生命周期将在第六篇细化。

开发环境中的附件元信息和目标栏规则

文件筛选不能破坏上一章的卡片身份

第一篇确立的itemId用于标识资料卡片,临时sessionId用于标识一次拖拽。第二篇加入attachmentId以后,三者的分工应更清楚:选中一个文件创建附件元信息,并不必然立刻创建资料卡;同一个附件可以在一张卡片中作为修订版替换,但不应因为文件名相同而覆盖另一个附件。要给同名文件去重,需要比较受控内容指纹或明确的来源标识,并将用户选择的“替换”动作记录下来。

批量添加时,可以先计算成功与失败清单,再一次性提交新卡片快照。下列业务函数对attachmentId去重、保持文件选择顺序,并把不被接收的文件连同原因返回给界面。它没有在接收判断阶段修改原有资料项,因而取消添加不会使旧卡片消失。

const assert = require('node:assert/strict');
function prepareBatch(files, column, boardId, activeBoard, decide) {
  const seen = new Set(), accepted = [], rejected = [];
  for (const file of files) {
    if (seen.has(file.attachmentId)) {
      rejected.push({ id:file.attachmentId, reason:'重复选择' });
      continue;
    }
    seen.add(file.attachmentId);
    const verdict = decide(file, column, boardId, activeBoard);
    if (!verdict.ok) rejected.push({ id:file.attachmentId, reason:verdict.reason });
    else accepted.push({ id:file.attachmentId, columnId:column,
      mime:file.mime, sizeBytes:file.sizeBytes });
  }
  return { accepted, rejected };
}
const accept = f => ({ ok:f.mime.startsWith('image/'),reason:'目标栏不接收该类型' });
const files = [{attachmentId:'P1',mime:'image/jpeg',sizeBytes:5},
  {attachmentId:'R1',mime:'application/pdf',sizeBytes:3},
  {attachmentId:'P1',mime:'image/jpeg',sizeBytes:5}];
const prepared = prepareBatch(files,'photos','site-east','site-east',accept);
assert.deepEqual(prepared.accepted.map(x=>x.id),['P1']);
assert.equal(prepared.rejected.length,2);

图片与文档的缩略图只是用户界面的便利信息,不能作为真实数据类型的依据。筛选条中切换“图片/报告/图纸”时,应保持原文件选中状态;用户可以暂时看不到被筛掉的PDF,但重新切回“全部”后仍能知道它尚未被投放。若目标栏在拖拽过程中变更接收规则,结束投放前还要重新判定,不能依赖悬停阶段留下的绿色边框。数据层应保存接受名单和拒绝名单,让批次结果可复核。

手机端附件类型筛选与拒绝理由

遇到“看得见但拖不过去”,沿四条线索查

第一条是类型线索:文件后缀、来源报告的MIME和UDT/UTD类型映射是否一致,未知类型是否被错误认作图片。第二条是来源访问:sourceRef是否仍有效,是否需要用户重新选择,预览成功和再次读取是否使用相同授权条件。第三条是目标语义:板ID、目标列白名单和大小策略是否在投放结束时再次判断。第四条是持久化:附件记录、关联卡片及投放结果是否以同一版业务快照发布,失败项是否保留可解释原因。

涉及拖入拖出时,可对照华为跨设备拖拽开发指导了解拖拽事件和数据接收角色,再根据实际应用内场景及项目SDK能力选择事件接入方式;跨设备约束不可直接移植成应用内文件处理规则。对统一类型标识,应以UTD类型列表为基准做明确映射。

附件接入的重点不是支持越多扩展名越好,而是让每一次归类有理由、每一份引用有来源、每一个失败项有去向。下一篇处理拖拽取消和悬停反馈时,可以继续使用此处的元信息对象与接收结果,不需要为了视觉高亮去提前修改资料卡片。

Logo

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

更多推荐