资源工作台「替换素材」候选窗接上预览管线,补齐候选排序、禁用态可读性与删除素材入口
- 「替换素材」候选卡缩略图接入资源卡同一条预览管线:新增 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:
+24
-127
@@ -13,28 +13,12 @@ import {
|
||||
normalizeGameCreationAppAssetTags,
|
||||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||||
import { ThemedModal } from '../../components/modal/ThemedModal';
|
||||
import {
|
||||
ResourceAssetDeleteDialog,
|
||||
type ResourceAssetReferenceVersion,
|
||||
} from './ResourceAssetDeleteDialog';
|
||||
|
||||
type UpdateLocalProjectResourceClassificationResult = {
|
||||
asset: GameCreationAppAssetManifestEntry;
|
||||
committedProjectRevision: number;
|
||||
};
|
||||
|
||||
type DeleteLocalProjectAssetResult = {
|
||||
assetId: string;
|
||||
localPath: string;
|
||||
committedProjectRevision: number;
|
||||
fileRetained: boolean;
|
||||
};
|
||||
|
||||
type ReadLocalProjectAssetReferencesResult = {
|
||||
assetId: string;
|
||||
versions: ResourceAssetReferenceVersion[];
|
||||
};
|
||||
|
||||
/**
|
||||
* 标签草稿沿用写入路径的归一化边界,只按中英文逗号、顿号与换行切分。
|
||||
* 与输入框旧的"整段逗号分隔文本"口径完全一致,改动只是把结果换成逐个可删的 pill。
|
||||
@@ -71,19 +55,12 @@ function resourceClassificationErrorMessage(error: unknown) {
|
||||
return '保存素材标签失败';
|
||||
}
|
||||
|
||||
function resourceDeleteErrorMessage(error: unknown) {
|
||||
if (typeof error === 'string' && error.trim()) return error;
|
||||
if (error instanceof Error && error.message) return error.message;
|
||||
return '删除资源失败';
|
||||
}
|
||||
|
||||
type ResourceClassificationPanelProps = {
|
||||
projectPath: string;
|
||||
projectId: string;
|
||||
asset: GameCreationAppAssetManifestEntry;
|
||||
onClose: () => void;
|
||||
onSaved: (result: UpdateLocalProjectResourceClassificationResult) => void;
|
||||
onDeleted: (result: DeleteLocalProjectAssetResult) => void;
|
||||
};
|
||||
|
||||
export function ResourceClassificationPanel({
|
||||
@@ -92,7 +69,6 @@ export function ResourceClassificationPanel({
|
||||
asset,
|
||||
onClose,
|
||||
onSaved,
|
||||
onDeleted,
|
||||
}: ResourceClassificationPanelProps) {
|
||||
/**
|
||||
* 分类取值优先级由落盘 `category` + `kind` 派生决定,本面板不再提供手动设置入口。
|
||||
@@ -111,17 +87,9 @@ export function ResourceClassificationPanel({
|
||||
);
|
||||
const [tagDraft, setTagDraft] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogPreparing, setDeleteDialogPreparing] = useState(false);
|
||||
const [deleteReferencedVersions, setDeleteReferencedVersions] =
|
||||
useState(false);
|
||||
const [referencedVersions, setReferencedVersions] = useState<
|
||||
readonly ResourceAssetReferenceVersion[]
|
||||
>([]);
|
||||
const [deleting, setDeleting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
/** 回车 / 逗号 / 顿号都按同一口径切分;输入框仍是同一套提交方式。 */
|
||||
/** 回车 / 逗号 / 顿号都按同一口径切分:把草稿落成 pill。 */
|
||||
function commitTagDraft() {
|
||||
if (!tagDraft.trim()) return;
|
||||
setTags((current) =>
|
||||
@@ -134,7 +102,12 @@ export function ResourceClassificationPanel({
|
||||
setTags((current) => current.filter((item) => item !== tag));
|
||||
}
|
||||
|
||||
async function saveResourceClassification() {
|
||||
/**
|
||||
* 保存标签:`tagsToSave` 由调用方给出(「添加」把输入框里还未落成 pill 的尾巴一起并入)。
|
||||
*/
|
||||
async function saveResourceClassification(
|
||||
tagsToSave: readonly string[],
|
||||
): Promise<void> {
|
||||
const invoke = window.__TAURI__?.core?.invoke;
|
||||
if (!invoke) {
|
||||
setError('编辑素材标签需要在客户端内保存');
|
||||
@@ -150,9 +123,6 @@ export function ResourceClassificationPanel({
|
||||
if (!Number.isSafeInteger(status.revision) || status.revision < 0) {
|
||||
throw new Error('项目 revision 无效');
|
||||
}
|
||||
// 输入框里还没按回车 / 逗号落成 pill 的尾巴也要一起保存,
|
||||
// 沿用"整段文本在保存时统一切分"的既有行为,不让用户白输入。
|
||||
const tagsToSave = mergeResourceClassificationTagDraft(tags, tagDraft);
|
||||
const result =
|
||||
await invoke<UpdateLocalProjectResourceClassificationResult>(
|
||||
'update_local_project_resource_classification',
|
||||
@@ -177,66 +147,16 @@ export function ResourceClassificationPanel({
|
||||
}
|
||||
|
||||
/**
|
||||
* 点删除先读引用信息,再打开独立确认面板。素材不可变:只摘掉 manifest 登记,磁盘文件保留。
|
||||
* 底部唯一的「添加」:把输入框里的内容(**含没按回车的尾巴**)落成 pill,然后按同一写入路径保存。
|
||||
*
|
||||
* 落 pill 是即时反馈,保存是本次动作的语义本身 —— 拆成"先落 pill 再等用户去点保存"要求
|
||||
* 用户理解两步,而这里只有一步。
|
||||
*/
|
||||
async function openDeleteResourceDialog() {
|
||||
const invoke = window.__TAURI__?.core?.invoke;
|
||||
if (!invoke) {
|
||||
setError('删除资源需要在客户端内执行');
|
||||
return;
|
||||
}
|
||||
setDeleteDialogPreparing(true);
|
||||
setError(null);
|
||||
try {
|
||||
const references = await invoke<ReadLocalProjectAssetReferencesResult>(
|
||||
'read_local_project_asset_references',
|
||||
{ input: { projectPath, assetId: asset.id } },
|
||||
);
|
||||
setReferencedVersions(references.versions);
|
||||
setDeleteReferencedVersions(false);
|
||||
setDeleteDialogOpen(true);
|
||||
} catch (referenceError) {
|
||||
setError(resourceDeleteErrorMessage(referenceError));
|
||||
} finally {
|
||||
setDeleteDialogPreparing(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmDeleteResource() {
|
||||
const invoke = window.__TAURI__?.core?.invoke;
|
||||
if (!invoke) {
|
||||
setError('删除资源需要在客户端内执行');
|
||||
return;
|
||||
}
|
||||
setDeleting(true);
|
||||
setError(null);
|
||||
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: asset.id,
|
||||
deleteReferencedVersions,
|
||||
},
|
||||
},
|
||||
);
|
||||
setDeleteDialogOpen(false);
|
||||
onDeleted(result);
|
||||
} catch (deleteError) {
|
||||
setError(resourceDeleteErrorMessage(deleteError));
|
||||
} finally {
|
||||
setDeleting(false);
|
||||
}
|
||||
async function addTagDraftAndSave() {
|
||||
const tagsToSave = mergeResourceClassificationTagDraft(tags, tagDraft);
|
||||
setTags(tagsToSave);
|
||||
setTagDraft('');
|
||||
await saveResourceClassification(tagsToSave);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -304,41 +224,18 @@ export function ResourceClassificationPanel({
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
<footer>
|
||||
{/*
|
||||
底部只留一个「添加」:它 = 落 pill + 保存。删除素材挪到资源卡选中工具条
|
||||
(破坏性操作与它要改的对象放在一起),标签自身的删除按钮仍在每个 pill 内部。
|
||||
*/}
|
||||
<footer className="game-resource-classification-footer">
|
||||
<PlatformActionButton
|
||||
tone="danger"
|
||||
onClick={() => void openDeleteResourceDialog()}
|
||||
disabled={saving || deleting || deleteDialogPreparing}
|
||||
aria-label="删除资源"
|
||||
onClick={() => void addTagDraftAndSave()}
|
||||
disabled={saving}
|
||||
>
|
||||
删除
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
onClick={onClose}
|
||||
disabled={saving || deleting}
|
||||
>
|
||||
取消
|
||||
</PlatformActionButton>
|
||||
<PlatformActionButton
|
||||
onClick={() => void saveResourceClassification()}
|
||||
disabled={saving || deleting}
|
||||
>
|
||||
保存标签
|
||||
添加
|
||||
</PlatformActionButton>
|
||||
</footer>
|
||||
{deleteDialogOpen ? (
|
||||
<ResourceAssetDeleteDialog
|
||||
open
|
||||
localPath={asset.localPath}
|
||||
referencedVersions={referencedVersions}
|
||||
deleteReferencedVersions={deleteReferencedVersions}
|
||||
deleting={deleting}
|
||||
onChangeDeleteReferencedVersions={setDeleteReferencedVersions}
|
||||
onClose={() => setDeleteDialogOpen(false)}
|
||||
onConfirm={() => void confirmDeleteResource()}
|
||||
/>
|
||||
) : null}
|
||||
</ThemedModal>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user