资源工作台「替换素材」候选窗接上预览管线,补齐候选排序、禁用态可读性与删除素材入口
- 「替换素材」候选卡缩略图接入资源卡同一条预览管线:新增 ResourcePreviewMedia,复用同一份预览身份、同一条队列与 LRU 缓存,不再只渲染类型占位图标 - 预览 hook 暴露 sweepVisiblePreviews:弹窗内容挂在 portal 下、不在资源画本 observer 的 root 子树内,改为在「弹窗打开」与「候选区滚动」两个几何变化点显式按同一套视口几何判据兜底放行,不新增第二条加载通路 - 候选排序抽出纯函数 sortResourceReplacementCandidates:可替换(无硬门禁失败)的排最前,其余按后端原名次跟随且仍渲染但禁用;判据与弹窗禁用状态同源,不会出现「排最前却点不动」 - 抽出纯函数 projectResourcesByManifestAssetId,让弹窗素材映射与候选缩略图共用同一份资源投影口径 - 共享选择弹窗的禁用态不再整卡压透明度:变灰只压在缩略图与名称上,「分类不同」这类禁用原因保持全对比度可读(网页端美术画布不传禁用原因,视觉逐字不变) - 资源卡选中工具条末位新增「删除素材」,前置共享工具条同一套分隔线与前面的非破坏性动作隔开,只在 manifestAssetId 存在时才渲染 - 删除流程抽成 useResourceAssetDeleteFlow(读引用信息 → ResourceAssetDeleteDialog 二次确认 → delete_local_project_asset 的 deleteReferencedVersions 三分支),工具条与面板共用同一份实现,没有第二套删除 - 「编辑素材标签」面板底部只留右下角一个「添加」:点它把输入框内容(含未按回车的尾巴)落成 pill 并按现有写入路径保存,分类仍原样回传落盘原值;删除素材/取消/保存标签三个按钮移除,关闭仍走头部 × - 标签 pill 内的删除标签按钮保持不变 - 更新与补充用例:候选排序、排序与禁用同源、候选缩略图真链路与滚动兜底放行、工具条删除入口位置与 IPC 载荷、连带删除分支、面板无删除入口与「添加」语义、禁用态可读性类名契约、工具条动作清单
This commit is contained in:
+46
-7
@@ -115,6 +115,51 @@ export function resourceReplacementAssetHints(
|
||||
return hints;
|
||||
}
|
||||
|
||||
/**
|
||||
* 候选排序:**可替换(无硬门禁失败)的排在最前,其余按原名次跟随**。
|
||||
*
|
||||
* 判据与弹窗的禁用状态同源(`resourceReplacementBlockedReason`),所以"排在最前"与
|
||||
* "点得动"永远是同一批条目 —— 不会出现排在最前却点不动的候选,也不会出现能点的候选被
|
||||
* 挤在不可替换项后面。同档内显式按原名次排(不依赖 `Array.prototype.sort` 的稳定性),
|
||||
* 保证任何引擎下结果一致。
|
||||
*
|
||||
* 不可替换项**不隐藏**:仍然渲染但禁用,把"为什么不能替换"留在界面上。
|
||||
*/
|
||||
export function sortResourceReplacementCandidates<
|
||||
Candidate extends LocalProjectVersionReplacementCandidate,
|
||||
>(candidates: readonly Candidate[]): Candidate[] {
|
||||
return candidates
|
||||
.map((candidate, index) => ({ candidate, index }))
|
||||
.sort((left, right) => {
|
||||
const leftBlocked =
|
||||
resourceReplacementBlockedReason(left.candidate) !== null;
|
||||
const rightBlocked =
|
||||
resourceReplacementBlockedReason(right.candidate) !== null;
|
||||
if (leftBlocked !== rightBlocked) return leftBlocked ? 1 : -1;
|
||||
return left.index - right.index;
|
||||
})
|
||||
.map((entry) => entry.candidate);
|
||||
}
|
||||
|
||||
/**
|
||||
* manifest 资产 id → 资源投影(同一资产生成多个投影时取第一个,与资源画布口径一致)。
|
||||
*
|
||||
* 抽成纯函数是为了让「候选条目 ↔ 资源投影 ↔ 预览身份」这条链在弹窗素材映射与宿主渲染处
|
||||
* 共用同一份口径,而不是各写一份 `forEach`。
|
||||
*/
|
||||
export function projectResourcesByManifestAssetId(
|
||||
resources: readonly ProjectResource[],
|
||||
): Map<string, ProjectResource> {
|
||||
const resourceByAssetId = new Map<string, ProjectResource>();
|
||||
for (const resource of resources) {
|
||||
const assetId = resource.manifestAssetId;
|
||||
if (assetId && !resourceByAssetId.has(assetId)) {
|
||||
resourceByAssetId.set(assetId, resource);
|
||||
}
|
||||
}
|
||||
return resourceByAssetId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 候选 → 弹窗素材。
|
||||
*
|
||||
@@ -126,13 +171,7 @@ export function resourceReplacementPickerAssets(
|
||||
resources: readonly ProjectResource[],
|
||||
candidates: readonly LocalProjectVersionReplacementCandidate[],
|
||||
): EditorAsset[] {
|
||||
const resourceByAssetId = new Map<string, ProjectResource>();
|
||||
for (const resource of resources) {
|
||||
const assetId = resource.manifestAssetId;
|
||||
if (assetId && !resourceByAssetId.has(assetId)) {
|
||||
resourceByAssetId.set(assetId, resource);
|
||||
}
|
||||
}
|
||||
const resourceByAssetId = projectResourcesByManifestAssetId(resources);
|
||||
return candidates.flatMap((candidate) => {
|
||||
const resource = resourceByAssetId.get(candidate.resourceId);
|
||||
if (!resource) return [];
|
||||
|
||||
Reference in New Issue
Block a user