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(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(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( '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( '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( '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 (

资源分类与标签

{asset.localPath}

setTagsDraft(event.currentTarget.value)} /> {error ? (

{error}

) : null}
void openDeleteResourceDialog()} disabled={saving || deleting || deleteDialogPreparing} aria-label="删除资源" > 删除 取消 void saveResourceClassification()} disabled={saving || deleting} > 保存
{deleteDialogOpen ? ( setDeleteDialogOpen(false)} onConfirm={() => void confirmDeleteResource()} /> ) : null}
); }