摄影棚交付不只是将几张满意的照片列成清单。照片是否已经具备可长期使用的应用内副本、批注仍指向哪一个原图坐标、意见是否处理完成、当前版本和交付时看到的版本是否一致,都会影响接收方复查。前几篇围绕P001~P003建立了相册选片会话,P001白花原图为4000×3000,已经记录从(600,450)到(1800,1200)的矩形和三个原图像素点;接着把构图、色彩、细节意见与这些稳定标记关联,并以意见修订号处理文本与状态变化。

这一阶段要回答新的问题:当摄影师选择“准备交付”时,系统怎样判断哪些素材可以进入清单;如果P002缺少本地原图副本,P003还有待复核意见,是否应该阻止整个会话生成可用清单;接收方日后打开清单,又应凭什么确认其中的批注和原图属于同一个版本。解决思路是逐资产核查、给出具体阻塞原因、冻结合格资产的版本快照。冻结清单是一份可复核的交付意图,不改变系统图库原件,也不擅自把待处理意见改为完成。

摄影选片交付前的资产及批注核查

清单与活动工作区不能共用同一份可变对象

选片会话中的PhotoEntry表示当前可继续编辑的资产,Annotation表示原图像素空间内的点和矩形,ReviewNote表示面向目标标记的处理意见。把它们当前的数组引用直接放进交付清单,后面只要移动一个点或把意见重新打开,过去那份交付记录就会随之变化。交付时应复制必要字段形成不可变快照,记录源资产修订、几何批注修订、意见修订及生成时刻;以后要修订清单,应明确产生新版本,而不是在旧快照上静默覆盖。

目录在前面的model/PhotoEntry.ets和model/ReviewNote.ets之外增加model/DeliveryAsset.ets、service/DeliveryValidator.ets、service/FreezeManifestService.ets和pages/DeliveryCheckPage.ets。交付检查仅在已选择的P001~P003范围内进行,不重新扫描设备媒体库。每张资产都有稳定assetId;工作副本存储在应用空间,交付记录引用经过内部规范化的副本标识,而不记录暴露原始相册URI的外部页面地址。

export interface DeliveryAsset {
  assetId: string;
  displayName: string;
  copyKey: string;
  copyReady: boolean;
  imageWidth: number;
  imageHeight: number;
  sourceRevision: number;
  geometryRevision: number;
  noteRevision: number;
}

export interface DeliveryMark {
  assetId: string;
  markId: string;
  kind: 'POINT' | 'RECT';
  x: number;
  y: number;
  width: number;
  height: number;
}

export interface DeliveryNote {
  noteId: string;
  assetId: string;
  targetId: string;
  state: 'OPEN' | 'IN_PROGRESS' | 'RESOLVED';
  noteRevision: number;
}

这里仍使用第六篇定义的稳定标记身份:point-1、point-2和region-main。一个坐标点已经移到新的位置,意见依然引用同一个targetId,不应把坐标字符串当作关联键。检查不需要读取完整原图像素或分析花卉内容;它关注的是副本可用性、像素坐标范围、目标引用和意见状态。这些条件足以覆盖最常见的“清单看起来齐全,打开后找不到批注”的交付缺陷。

每张资产应给出可解释的阻塞原因

P001的本地工作副本已经建立,三个点与一个矩形都有确定的原图坐标,当前意见在人工整理后可以进入交付候选;P002若仍停留在选择器URI阶段、尚未写入工作副本,就需要提示“原图副本待补”;P003若还有意见处于OPEN或IN_PROGRESS,则提示“意见待处理”。这三个状态是清单候选数据中的不同业务情形,不应简单用一个“无效图片”标签概括。

核查函数把错误收集成数组,让页面能够逐项展示。对矩形检查宽高均大于零并落在原图范围,对点检查坐标在边界内;如果有意见引用已删除的标记,就把该意见列为待修复对象。资产修订号必须为正数,避免把尚未建档的对象混进已冻结列表。为保证文档可重复检查,验证函数只读取参数并返回结果,不在其中执行文件写入或弹窗操作。

export interface DeliveryCheck {
  assetId: string;
  ready: boolean;
  reasons: string[];
}

export function checkDelivery(
  asset: DeliveryAsset, marks: DeliveryMark[], notes: DeliveryNote[]
): DeliveryCheck {
  const reasons: string[] = [];
  if (!asset.copyReady || !asset.copyKey) {
    reasons.push('原图副本待补');
  }
  if (asset.imageWidth <= 0 || asset.imageHeight <= 0 ||
      asset.sourceRevision < 1) {
    reasons.push('原图元信息待核查');
  }
  const ownMarks = marks.filter((m: DeliveryMark) => m.assetId === asset.assetId);
  for (const m of ownMarks) {
    const valid = m.x >= 0 && m.y >= 0 &&
      m.x + m.width <= asset.imageWidth &&
      m.y + m.height <= asset.imageHeight &&
      (m.kind === 'POINT' || (m.width > 0 && m.height > 0));
    if (!valid) { reasons.push(`标记${m.markId}越界`); }
  }
  const markIds = ownMarks.map((m: DeliveryMark) => m.markId);
  for (const note of notes.filter((n: DeliveryNote) => n.assetId === asset.assetId)) {
    if (!markIds.includes(note.targetId)) {
      reasons.push(`意见${note.noteId}找不到目标`);
    }
    if (note.state !== 'RESOLVED') {
      reasons.push(`意见${note.noteId}待处理`);
    }
  }
  return { assetId: asset.assetId, ready: reasons.length === 0, reasons };
}

对于POINT类型,宽高按零保存,此时x、y须满足原图范围;若项目约定坐标可以取到图像最右侧边界,应进一步按所选像素采样规则决定是否允许等于图像宽高。矩形的拖拽方向在第三篇已经归一化,核查阶段不重复猜测两个角点的先后关系。数据格式升级时,应先迁移旧版字段再核查,否则可能把旧数据缺少的新字段误判成用户尚未完成的编辑。

冻结时复制业务版本,拒绝混合快照

通过核查后,还不能直接把几张不同时间加载的记录拼成清单。假设在核查P001后,摄影师又将note-102重新打开;若冻结动作读取的还是此前的已解决状态,就会得到一个自相矛盾的交付包。清单服务应该取得一次明确的源快照,比较资产、几何和意见的版本三元组,校验通过后复制这些字段并分配新的清单版本。涉及共享后台时,最好由后台用版本条件检查和提交,客户端仅展示确认的快照结果。

冻结记录可包含资产显示序号,但序号只控制清单排版,不替代assetId。每次生成的条目按照会话中的既定显示顺序排列;若用户重新排序,应产生新清单修订,不要改变已经冻结的版本。下面的业务函数不依赖系统接口,对输入数组采用拷贝和排序,保证以后展开旧清单仍能按同一顺序找到对应资产。

export interface FrozenLine {
  assetId: string;
  order: number;
  copyKey: string;
  sourceRevision: number;
  geometryRevision: number;
  noteRevision: number;
}
export interface DeliveryManifest {
  manifestId: string;
  revision: number;
  createdAt: number;
  lines: FrozenLine[];
}

export function buildManifest(
  manifestId: string, revision: number, assets: DeliveryAsset[],
  checks: DeliveryCheck[], now: number
): DeliveryManifest {
  const readyIds = checks.filter((c: DeliveryCheck) => c.ready)
    .map((c: DeliveryCheck) => c.assetId);
  const lines: FrozenLine[] = assets
    .filter((a: DeliveryAsset) => readyIds.includes(a.assetId))
    .map((a: DeliveryAsset, index: number): FrozenLine => ({
      assetId: a.assetId, order: index + 1, copyKey: a.copyKey,
      sourceRevision: a.sourceRevision,
      geometryRevision: a.geometryRevision,
      noteRevision: a.noteRevision
    }));
  return { manifestId, revision, createdAt: now, lines };
}

buildManifest()负责组装已经核查通过的候选行;实际持久化前还需重新确认数据版本未发生改变,否则给出“资产在核查后更新”的明确提示。清单只有P001时可以只冻结P001,并把P002与P003列入待补清单;也可以由业务规则要求整套会话都合格后才能整体冻结,两种策略都需要显示具体结果。这里选择“允许合格资产先冻结”,避免另外两张素材迟迟未完成时阻断已经整理好的P001。

交付核查服务与清单界面的开发环境

清单页面保持当前选择与检查状态分离

界面需要同时展示“当前选中的候选资产”和“经核查可交付的资产”。它们并不相等:P002可以继续处于选中状态,却因为原图副本缺失而暂不能冻结。用户点击阻塞原因,应进入对应的原图副本页或意见页;返回后重新读取相关源修订,再刷新该行的核查结论。列表键使用assetId,不用当前索引,避免照片重新排序后提示被贴到另一张花卉缩略图上。

@Entry
@Component
struct DeliveryCheckPage {
  @State private checks: DeliveryCheck[] = [];
  @State private busy: boolean = false;
  @State private selectedAssetId: string = '';
  @State private warning: string = '';
  private service: DeliveryService = createDeliveryService();

  aboutToAppear(): void { this.refresh(); }

  private async refresh(): Promise<void> {
    if (this.busy) { return; }
    this.busy = true;
    try {
      this.checks = await this.service.checkCurrentSession();
      this.warning = '';
    } catch (error) {
      this.warning = '清单核查受阻,请重新获取';
    } finally {
      this.busy = false;
    }
  }

  build() {
    Column({ space: 12 }) {
      Text('摄影棚 · 交付核查').fontSize(23)
      Text(`可交付 ${this.checks.filter((c: DeliveryCheck) => c.ready).length} / ${this.checks.length}`)
      if (this.warning) { Text(this.warning).fontColor(Color.Orange) }
      List() {
        ForEach(this.checks, (item: DeliveryCheck) => {
          ListItem() {
            Column({ space: 4 }) {
              Text(item.assetId).fontSize(18)
              Text(item.ready ? '可交付' : item.reasons.join('、'))
            }.padding(12).width('100%')
          }.onClick(() => { this.selectedAssetId = item.assetId; })
        }, (item: DeliveryCheck) => item.assetId)
      }.layoutWeight(1)
      Button('重新核查').enabled(!this.busy)
        .onClick(() => this.refresh())
    }.height('100%').padding(16)
  }
}

DeliveryService和createDeliveryService()是项目内的服务抽象,分别读取选片会话、工作副本和批注模型,再调用前面的核查函数。该页面用于交付前检查,真正的“冻结”按钮可以在确认页上附带本次可交付资产数量及各自版本,防止人员误以为三个素材都已经进入清单。对于尺寸不足、目标缺失或意见重新打开的资产,提供跳转修复入口比一个不可操作的错误提示更有价值。

轻量索引的恢复与交付检查路径

清单正文适合由独立业务存储维护,不宜直接把几张大图转换成字符串放进Preferences。Preferences可以保存最近打开的manifestId、筛选条件与显示顺序这些小型界面线索;在同一应用进程里按键读取,写入后等待flush()完成。交付清单的真实内容和图像文件仍由应用沙箱或业务仓储服务保存,恢复页面时先核对版本,再显示上次选中的资产。后续生成交换文件时,也应以已冻结版本为输入,而不是重新从活动编辑数组临时拼接。

import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';

export async function saveDeliveryView(
  context: common.UIAbilityContext, manifestId: string,
  selectedAssetId: string
): Promise<void> {
  const store = await preferences.getPreferences(context,
    { name: 'photo_delivery_view' });
  await store.put('last_manifest', manifestId);
  await store.put('last_asset', selectedAssetId);
  await store.flush();
}

调试可构造三个固定业务状态:P001原图副本和批注目标完整、意见已整理;P002缺少工作副本;P003有一条待处理意见。逐项检查返回原因是否可解释;把P001的一个点移动到图外,观察是否被检测;删除region-main但保留引用它的意见,检查关联失败;冻结前修改P001的意见修订号,观察版本比较能否拒绝旧快照;调整显示顺序后,检查既有清单的assetId及版本是否保持原样。HiLog可记录资产数量、通过数量、阻塞原因类别和清单修订号,避免写出原图外部URI和完整批注意见文本。

P001至P003交付核查与待处理原因的手机界面

摄影素材交付的可靠性来自清楚区分“已选中”“已经核查”“可以冻结”和“已经交付”四个阶段。原图副本、批注位置、意见状态与源修订保持各自身份,才可以在以后查看交付清单时找到一致的内容。P001可以形成独立的合格候选,P002与P003则保留明确的补齐入口。下一阶段能够在这份冻结清单之上设计导出格式与交付归档,不必再临时猜测哪些批注属于哪个照片版本。

参考资料:ArkUI列表创建、Preferences接口、ArkTS语言概述。

Logo

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

更多推荐