d1581892d7
- 素材重命名(跨范围契约):`rename_local_project_asset` 现在要求 `expectedProjectId` + `expectedProjectRevision`,前端按删除 / 分类保存同一套 CAS 口径补齐——先 `get_local_game_project_revision` 读当前 revision 再连同项目身份提交,不用可能过期的本地缓存值 - 失败文案统一:新增 `projectAssetCommandErrorMessage`,把 `project-identity-conflict` / `project-revision-conflict` 翻成用户可读中文,重命名 / 删除 / 分类保存三条链路共用一份映射,其余错误原样透出 - index.tsx:资源面板下载按 `resolveDownloadablePanelEntries` 过滤,版本卡这类合成 path 的条目不再进入落盘链路 - index.tsx:`characterAnimationPanel` 纳入画布浮层开关判据(抽 `resolveResourceCanvasFloatingPanelOpen`),「生成动画」面板与快速编辑 / 信息浮层共用同一条「点外部 / Esc 关闭」规则 - index.tsx:去掉 `ResourceBookScene` 上重复的 `onWheel`,滚轮只由 manager 上的原生 `passive:false` 监听处理,平移 / 缩放不再被翻倍 - index.tsx:快速编辑的源资源在层 id 命中不到时回落到已正规化的 `asset:<id>`,重试不再静默什么都不做;取不到时给出可见失败 - ResourceCanvasPanelView.tsx:下载按钮按可下载条数判可用、不再在上传中显示下载转圈;上传中禁掉 × / Esc / 遮罩三条关闭路径;预览占位文案抽 helper - GameRunVersionPicker.tsx:portal 菜单在滚动 / 缩放后重算位置;外部点击判定沿用共享 `useImageCanvasFloatingOptionDismiss` + `menuRef` 边界(review 该条已修,保留现状并补位置用例) - ResourceCanvasGenerationPanelView.tsx:`submitting` 改 `finally` 收回,成功路径不再永久锁住面板 - ResourceAssetDeleteDialog.tsx / ResourceRenameDialog.tsx / ResourceClassificationPanel.tsx:在飞时 `closeOnEscape` / `closeOnBackdrop` 与头部 × 一起挡住 - resourceEditModel.ts / resourceCanvasToolbarModel.ts / useProjectResourceCardPreviews.ts / projectResourceLiveUpdateModel.ts:抽 `resourceBaseName`、`canonicalProjectedResourceMediaType` 参数收窄去掉强转、删未使用参数、补「IO 失败会 reject」的签名说明 - resourceCanvasHistoryModel.ts:`resourceCanvasSnapshotsEqual` 不再比较不可恢复的 `manuallyPlaced`,消除「压进历史但撤销是空操作」 - resourceCanvasSectionMapping.ts:删掉从未接线、且注释与实现相矛盾的 `LEGACY_RESOURCE_CANVAS_SECTION_FALLBACK` - resourceCanvasChrome.css:`.game-resource-panel-grid` 补 `flex: 1 1 auto; min-height: 0`,卡片网格成为真正的滚动区 - 用例:重命名 CAS 载荷与读序、CAS 拒绝文案、版本卡下载门禁、上传中关闭路径、浮层判据、菜单跟随滚动、分区映射
172 lines
5.4 KiB
TypeScript
172 lines
5.4 KiB
TypeScript
import { useCallback, useState } from 'react';
|
||
|
||
import { projectAssetCommandErrorMessage } from './projectAssetCommandErrorMessage';
|
||
import type { ResourceAssetReferenceVersion } from './ResourceAssetDeleteDialog';
|
||
|
||
/**
|
||
* `delete_local_project_asset` 的返回:素材不可变,删除只摘掉 manifest 登记,磁盘文件保留。
|
||
*/
|
||
export type DeleteLocalProjectAssetResult = {
|
||
assetId: string;
|
||
localPath: string;
|
||
committedProjectRevision: number;
|
||
fileRetained: boolean;
|
||
};
|
||
|
||
type ReadLocalProjectAssetReferencesResult = {
|
||
assetId: string;
|
||
versions: ResourceAssetReferenceVersion[];
|
||
};
|
||
|
||
/** 待删除的素材:`assetId` 是 manifest 资产 id,`localPath` 只用于确认面板的副标题。 */
|
||
export type ResourceAssetDeleteTarget = {
|
||
assetId: string;
|
||
localPath: string;
|
||
};
|
||
|
||
type UseResourceAssetDeleteFlowInput = {
|
||
projectPath: string;
|
||
projectId: string;
|
||
/**
|
||
* 失败原因出口(读引用失败 / 删除失败 / 不在客户端内)。
|
||
*
|
||
* 宿主自己决定显示在哪里:面板内联成 `role="alert"`,资源卡工具条落到工作台提示条。
|
||
* 删除流程本身不持有错误态,避免出现"两处各显示半句失败原因"。
|
||
*/
|
||
onError: (message: string) => void;
|
||
onDeleted: (result: DeleteLocalProjectAssetResult) => void;
|
||
};
|
||
|
||
/**
|
||
* 删除失败原因:项目身份 / 版本 CAS 拒绝翻成用户可读中文,其余原样透出。
|
||
* 与重命名、分类保存共用同一份映射(`projectAssetCommandErrorMessage`)。
|
||
*/
|
||
export function resourceDeleteErrorMessage(error: unknown) {
|
||
return projectAssetCommandErrorMessage(error, '删除资源失败');
|
||
}
|
||
|
||
/**
|
||
* 素材删除流程:读引用信息 → 二次确认 → 落盘删除。
|
||
*
|
||
* 「编辑素材标签」面板与资源卡选中工具条**共用这一份实现**(同一个原生命令、同一个二次确认
|
||
* 面板、同一套 `deleteReferencedVersions` 三分支),所以再没有第二份删除实现可以分叉。
|
||
*/
|
||
export function useResourceAssetDeleteFlow({
|
||
projectPath,
|
||
projectId,
|
||
onError,
|
||
onDeleted,
|
||
}: UseResourceAssetDeleteFlowInput) {
|
||
const [target, setTarget] = useState<ResourceAssetDeleteTarget | null>(null);
|
||
const [dialogOpen, setDialogOpen] = useState(false);
|
||
const [preparing, setPreparing] = useState(false);
|
||
const [deleting, setDeleting] = useState(false);
|
||
const [referencedVersions, setReferencedVersions] = useState<
|
||
readonly ResourceAssetReferenceVersion[]
|
||
>([]);
|
||
const [deleteReferencedVersions, setDeleteReferencedVersions] =
|
||
useState(false);
|
||
|
||
/**
|
||
* 点删除先读引用信息,再打开独立确认面板;这一步不写盘。
|
||
*/
|
||
const requestDelete = useCallback(
|
||
async (next: ResourceAssetDeleteTarget) => {
|
||
const invoke = window.__TAURI__?.core?.invoke;
|
||
if (!invoke) {
|
||
onError('删除资源需要在客户端内执行');
|
||
return;
|
||
}
|
||
setPreparing(true);
|
||
setTarget(next);
|
||
try {
|
||
const references = await invoke<ReadLocalProjectAssetReferencesResult>(
|
||
'read_local_project_asset_references',
|
||
{ input: { projectPath, assetId: next.assetId } },
|
||
);
|
||
setReferencedVersions(references.versions);
|
||
setDeleteReferencedVersions(false);
|
||
setDialogOpen(true);
|
||
} catch (referenceError) {
|
||
setTarget(null);
|
||
onError(resourceDeleteErrorMessage(referenceError));
|
||
} finally {
|
||
setPreparing(false);
|
||
}
|
||
},
|
||
[onError, projectPath],
|
||
);
|
||
|
||
const confirmDelete = useCallback(async () => {
|
||
const invoke = window.__TAURI__?.core?.invoke;
|
||
if (!invoke) {
|
||
onError('删除资源需要在客户端内执行');
|
||
return;
|
||
}
|
||
if (!target) return;
|
||
setDeleting(true);
|
||
try {
|
||
const status = await invoke<{ revision: number }>(
|
||
'get_local_game_project_revision',
|
||
{ projectPath },
|
||
);
|
||
if (!Number.isSafeInteger(status.revision) || status.revision < 0) {
|
||
throw new Error('项目 revision 无效');
|
||
}
|
||
const result = await invoke<DeleteLocalProjectAssetResult>(
|
||
'delete_local_project_asset',
|
||
{
|
||
input: {
|
||
projectPath,
|
||
expectedProjectId: projectId,
|
||
expectedProjectRevision: status.revision,
|
||
assetId: target.assetId,
|
||
deleteReferencedVersions,
|
||
},
|
||
},
|
||
);
|
||
setDialogOpen(false);
|
||
setTarget(null);
|
||
onDeleted(result);
|
||
} catch (deleteError) {
|
||
onError(resourceDeleteErrorMessage(deleteError));
|
||
} finally {
|
||
setDeleting(false);
|
||
}
|
||
}, [
|
||
deleteReferencedVersions,
|
||
onDeleted,
|
||
onError,
|
||
projectId,
|
||
projectPath,
|
||
target,
|
||
]);
|
||
|
||
const closeDeleteDialog = useCallback(() => {
|
||
setDialogOpen(false);
|
||
setTarget(null);
|
||
}, []);
|
||
|
||
return {
|
||
requestDelete,
|
||
/** 读引用信息在途:入口按钮据此禁用,避免连点发出两次确认面板。 */
|
||
preparing,
|
||
deleting,
|
||
/**
|
||
* 现有确认面板的现成入参。没有待删除目标时为 `null`,宿主直接不渲染 `ResourceAssetDeleteDialog`。
|
||
*/
|
||
deleteDialogProps: target
|
||
? {
|
||
open: dialogOpen,
|
||
localPath: target.localPath,
|
||
referencedVersions,
|
||
deleteReferencedVersions,
|
||
deleting,
|
||
onChangeDeleteReferencedVersions: setDeleteReferencedVersions,
|
||
onClose: closeDeleteDialog,
|
||
onConfirm: () => void confirmDelete(),
|
||
}
|
||
: null,
|
||
};
|
||
}
|