33ed85ba54
- 新增 ResourceAssetDeleteDialog:列出使用该素材的游戏版本(标识 + 创建时间),给出「把相关游戏版本一并删除」勾选,默认不勾 - ResourceClassificationPanel 删除入口改为先读 read_local_project_asset_references,再打开独立确认弹窗,不再用面板内二次确认态 - 确认删除时按勾选状态传 deleteReferencedVersions;未被任何版本引用时不显示连带删除勾选 - styles.css 补删除弹窗、版本列表与勾选行样式 - 重写并扩充 resourceClassificationPanel.test.tsx 的删除用例:确认弹窗才发起删除、未引用时不出现勾选、列出被引用版本且默认不勾、勾选后传 true、客户端拒绝时不报告删除
271 lines
8.6 KiB
TypeScript
271 lines
8.6 KiB
TypeScript
import { useState } from 'react';
|
||
|
||
import { PlatformActionButton } from '../../../../../packages/shared/src/components/PlatformActionButton';
|
||
import { PlatformSegmentedTabs } from '../../../../../packages/shared/src/components/PlatformSegmentedTabs';
|
||
import { PlatformTextField } from '../../../../../packages/shared/src/components/PlatformTextField';
|
||
import {
|
||
GAME_CREATION_APP_ASSET_CATEGORIES,
|
||
type GameCreationAppAssetCategory,
|
||
gameCreationAppAssetCategory,
|
||
type GameCreationAppAssetManifestEntry,
|
||
gameCreationAppAssetTags,
|
||
normalizeGameCreationAppAssetTags,
|
||
} from '../../../../../packages/shared/src/contracts/gameCreationApp';
|
||
import { ThemedModal } from '../../components/modal/ThemedModal';
|
||
import { resourceReferenceCategoryLabel } from '../../features/project-workspace/resourceReferences';
|
||
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[];
|
||
};
|
||
|
||
const RESOURCE_CLASSIFICATION_CATEGORY_OPTIONS =
|
||
GAME_CREATION_APP_ASSET_CATEGORIES.map((category) => ({
|
||
id: category,
|
||
label: resourceReferenceCategoryLabel(category),
|
||
}));
|
||
|
||
/** 标签草稿沿用写入路径的归一化边界,只按中英文逗号、顿号与换行切分。 */
|
||
function splitResourceClassificationTagsDraft(value: string) {
|
||
return normalizeGameCreationAppAssetTags(value.split(/[,,、\n]/u));
|
||
}
|
||
|
||
function resourceClassificationErrorMessage(error: unknown) {
|
||
if (typeof error === 'string' && error.trim()) return error;
|
||
if (error instanceof Error && error.message) return error.message;
|
||
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({
|
||
projectPath,
|
||
projectId,
|
||
asset,
|
||
onClose,
|
||
onSaved,
|
||
onDeleted,
|
||
}: ResourceClassificationPanelProps) {
|
||
const [category, setCategory] = useState<GameCreationAppAssetCategory>(() =>
|
||
gameCreationAppAssetCategory(asset),
|
||
);
|
||
const [tagsDraft, setTagsDraft] = useState(() =>
|
||
gameCreationAppAssetTags(asset).join('、'),
|
||
);
|
||
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);
|
||
|
||
async function saveResourceClassification() {
|
||
const invoke = window.__TAURI__?.core?.invoke;
|
||
if (!invoke) {
|
||
setError('资源分类与标签需要在客户端内保存');
|
||
return;
|
||
}
|
||
setSaving(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<UpdateLocalProjectResourceClassificationResult>(
|
||
'update_local_project_resource_classification',
|
||
{
|
||
input: {
|
||
projectPath,
|
||
expectedProjectId: projectId,
|
||
expectedProjectRevision: status.revision,
|
||
assetId: asset.id,
|
||
category,
|
||
tags: splitResourceClassificationTagsDraft(tagsDraft),
|
||
},
|
||
},
|
||
);
|
||
onSaved(result);
|
||
} catch (saveError) {
|
||
setError(resourceClassificationErrorMessage(saveError));
|
||
} finally {
|
||
setSaving(false);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 点删除先读引用信息,再打开独立确认面板。素材不可变:只摘掉 manifest 登记,磁盘文件保留。
|
||
*/
|
||
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);
|
||
}
|
||
}
|
||
|
||
return (
|
||
<ThemedModal
|
||
open
|
||
ariaLabel="资源分类与标签"
|
||
onClose={onClose}
|
||
panelClassName="game-approval-dialog game-resource-classification-dialog"
|
||
>
|
||
<header>
|
||
<div>
|
||
<h2>资源分类与标签</h2>
|
||
<p>{asset.localPath}</p>
|
||
</div>
|
||
<button type="button" aria-label="关闭资源分类与标签" onClick={onClose}>
|
||
×
|
||
</button>
|
||
</header>
|
||
<div className="game-resource-classification-body">
|
||
<PlatformSegmentedTabs
|
||
items={RESOURCE_CLASSIFICATION_CATEGORY_OPTIONS}
|
||
activeId={category}
|
||
onChange={setCategory}
|
||
columns="threeToSix"
|
||
gap="sm"
|
||
size="compact"
|
||
/>
|
||
<PlatformTextField
|
||
aria-label="资源标签"
|
||
placeholder="用、或逗号分隔标签"
|
||
value={tagsDraft}
|
||
onChange={(event) => setTagsDraft(event.currentTarget.value)}
|
||
/>
|
||
{error ? (
|
||
<p className="game-resource-classification-error" role="alert">
|
||
{error}
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
<footer>
|
||
<PlatformActionButton
|
||
tone="danger"
|
||
onClick={() => void openDeleteResourceDialog()}
|
||
disabled={saving || deleting || deleteDialogPreparing}
|
||
aria-label="删除资源"
|
||
>
|
||
删除
|
||
</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>
|
||
);
|
||
}
|