合并 feat/agc-resource-replace:把「替换素材」接进资源工作台
- 解冲突 project/manifest.rs:保留版本写入公共体 write_manifest_locked_with_version_guard(版本只追加 + 绑定改写放行),并把 schema 校验并入该公共体,两侧语义都不丢 - 解冲突 shared-contracts:取当前分支的读显示口径 game_creation_app_asset_effective_category(含与 TS 交叉钉住的 EFFECTIVE_CATEGORY_CONTRACT 矩阵),同步改写 version_resource_replacement 对旧函数名的调用 - 解冲突 index.tsx:两处均为纯 import 追加,两侧都保留 - 解冲突 decision-log 与技术方案文档:两侧条目都是追加,全部保留 - 随之进入本分支:版本绑定改写窄放行通道、直接替换命令与其兼容性判据、资源卡工具条「替换素材」入口
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { Check, ImageIcon, Music, Search, Video } from 'lucide-react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { type ReactNode, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { PlatformActionButton } from '../../../packages/shared/src/components/PlatformActionButton';
|
||||
import { PlatformResourceFilterBar } from '../../../packages/shared/src/components/PlatformResourceFilterBar';
|
||||
import { PlatformStatusMessage } from '../../../packages/shared/src/components/PlatformStatusMessage';
|
||||
import { UnifiedModal } from '../common/UnifiedModal';
|
||||
import type { EditorAsset } from './ImageCanvasEditorTypes';
|
||||
import {
|
||||
@@ -20,6 +21,41 @@ type ImageCanvasProjectAssetPickerDialogProps = {
|
||||
selectedAssetIds: readonly string[];
|
||||
onCancel: () => void;
|
||||
onConfirm: (assetIds: string[]) => void;
|
||||
/**
|
||||
* 单选模式:点击即整组替换当前选择,不再渲染「已选」chip 行。
|
||||
* 默认 `false`,网页端美术画布的参考图多选行为逐字不变。
|
||||
*/
|
||||
singleSelect?: boolean;
|
||||
/**
|
||||
* 被禁用的素材 id → 用户可见的禁用原因。默认空,即所有素材都可选。
|
||||
*
|
||||
* 禁用项仍然渲染(不隐藏):隐藏会让用户以为"素材不存在",而真实原因是它不可替换。
|
||||
*/
|
||||
assetBlockedReasons?: Readonly<Record<string, string>>;
|
||||
/**
|
||||
* 可选素材 id → 非阻断提示(例如"格式与源素材不同")。默认空,即不显示任何提示。
|
||||
*
|
||||
* 与 `assetBlockedReasons` 的区别:提示不改变可点性,只把差异说清楚。
|
||||
*/
|
||||
assetHints?: Readonly<Record<string, string>>;
|
||||
/**
|
||||
* 素材缩略图渲染器。默认 `undefined` → 沿用 `<img src={thumbnailSrc || src}>`。
|
||||
*
|
||||
* 宿主(如 AGC 资源工作台)没有同步 `src` 时必须传它:AGC 的预览读取走带 scope 的
|
||||
* 原生调度器 + Blob URL,弹窗内取不到,直接给空 `src` 会挂破图。
|
||||
*/
|
||||
renderAssetMedia?: (asset: EditorAsset) => ReactNode;
|
||||
/**
|
||||
* 选择对象的中文名词,用于拼弹窗标题与可访问名称。默认「参考图」,
|
||||
* 即网页端美术画布的现有文案逐字不变。
|
||||
*/
|
||||
selectionNoun?: string;
|
||||
/**
|
||||
* 宿主的失败原因(例如后端拒绝了这次替换)。默认 `undefined` → 不渲染。
|
||||
*
|
||||
* 用于在弹窗内说明"为什么这次操作没成功",而不是静默关闭弹窗让用户以为成功了。
|
||||
*/
|
||||
errorMessage?: string | null;
|
||||
};
|
||||
|
||||
function assetIcon(asset: EditorAsset) {
|
||||
@@ -46,6 +82,12 @@ export function ImageCanvasProjectAssetPickerDialog({
|
||||
selectedAssetIds,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
singleSelect = false,
|
||||
assetBlockedReasons,
|
||||
assetHints,
|
||||
renderAssetMedia,
|
||||
selectionNoun = '参考图',
|
||||
errorMessage,
|
||||
}: ImageCanvasProjectAssetPickerDialogProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [category, setCategory] = useState<ProjectAssetPickerCategory>('all');
|
||||
@@ -76,20 +118,24 @@ export function ImageCanvasProjectAssetPickerDialog({
|
||||
});
|
||||
|
||||
function toggleAsset(assetId: string) {
|
||||
setSelection((current) =>
|
||||
current.includes(assetId)
|
||||
if (assetBlockedReasons?.[assetId]) return;
|
||||
setSelection((current) => {
|
||||
if (singleSelect) {
|
||||
return current.includes(assetId) ? [] : [assetId];
|
||||
}
|
||||
return current.includes(assetId)
|
||||
? current.filter((item) => item !== assetId)
|
||||
: [...current, assetId],
|
||||
);
|
||||
: [...current, assetId];
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<UnifiedModal
|
||||
open={open}
|
||||
title="选择参考图"
|
||||
title={`选择${selectionNoun}`}
|
||||
size="lg"
|
||||
portalTheme="light"
|
||||
closeLabel="关闭选择参考图"
|
||||
closeLabel={`关闭选择${selectionNoun}`}
|
||||
onClose={onCancel}
|
||||
panelClassName="image-canvas-editor__project-asset-picker"
|
||||
bodyClassName="image-canvas-editor__project-asset-picker-body"
|
||||
@@ -118,7 +164,7 @@ export function ImageCanvasProjectAssetPickerDialog({
|
||||
<PlatformActionButton
|
||||
type="button"
|
||||
size="sm"
|
||||
aria-label="确认选择参考图"
|
||||
aria-label={`确认选择${selectionNoun}`}
|
||||
onClick={() => onConfirm(selection)}
|
||||
>
|
||||
确认
|
||||
@@ -126,13 +172,26 @@ export function ImageCanvasProjectAssetPickerDialog({
|
||||
</>
|
||||
}
|
||||
>
|
||||
{selectedAssets.length > 0 ? (
|
||||
<div className="mb-3 flex flex-wrap gap-1.5" aria-label="已选参考图">
|
||||
{errorMessage ? (
|
||||
<PlatformStatusMessage
|
||||
tone="error"
|
||||
size="xs"
|
||||
className="mb-3"
|
||||
role="alert"
|
||||
>
|
||||
{errorMessage}
|
||||
</PlatformStatusMessage>
|
||||
) : null}
|
||||
{selectedAssets.length > 0 && !singleSelect ? (
|
||||
<div
|
||||
className="mb-3 flex flex-wrap gap-1.5"
|
||||
aria-label={`已选${selectionNoun}`}
|
||||
>
|
||||
{selectedAssets.map((asset) => (
|
||||
<button
|
||||
key={asset.id}
|
||||
type="button"
|
||||
aria-label={`取消参考图${asset.label}`}
|
||||
aria-label={`取消${selectionNoun}${asset.label}`}
|
||||
className="platform-category-chip gap-1 px-2 text-xs"
|
||||
onClick={() => toggleAsset(asset.id)}
|
||||
>
|
||||
@@ -163,28 +222,39 @@ export function ImageCanvasProjectAssetPickerDialog({
|
||||
<div
|
||||
className="grid grid-cols-2 gap-2 sm:grid-cols-3 lg:grid-cols-4"
|
||||
role="listbox"
|
||||
aria-label="参考图素材"
|
||||
aria-multiselectable
|
||||
aria-label={`${selectionNoun}素材`}
|
||||
aria-multiselectable={!singleSelect}
|
||||
>
|
||||
{visibleAssets.map((asset) => {
|
||||
const selected = selection.includes(asset.id);
|
||||
const blockedReason = assetBlockedReasons?.[asset.id] ?? null;
|
||||
const hint = blockedReason
|
||||
? null
|
||||
: (assetHints?.[asset.id] ?? null);
|
||||
return (
|
||||
<button
|
||||
key={asset.id}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={selected}
|
||||
aria-label={`选择参考图${asset.label}`}
|
||||
aria-label={`选择${selectionNoun}${asset.label}`}
|
||||
disabled={blockedReason !== null}
|
||||
title={blockedReason ?? hint ?? undefined}
|
||||
className={[
|
||||
'relative flex min-h-[7.5rem] flex-col overflow-hidden rounded-[0.9rem] border text-left transition',
|
||||
selected
|
||||
? 'border-[var(--platform-accent)] bg-white shadow-sm'
|
||||
: 'border-[var(--platform-subpanel-border)] bg-white/70 hover:bg-white',
|
||||
].join(' ')}
|
||||
blockedReason !== null ? 'cursor-not-allowed opacity-60' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
onClick={() => toggleAsset(asset.id)}
|
||||
>
|
||||
<span className="flex h-20 w-full items-center justify-center overflow-hidden bg-white/60">
|
||||
{projectAssetPickerCategory(asset) === 'image' ? (
|
||||
{renderAssetMedia ? (
|
||||
renderAssetMedia(asset)
|
||||
) : projectAssetPickerCategory(asset) === 'image' ? (
|
||||
<img
|
||||
src={asset.thumbnailSrc || asset.src}
|
||||
alt=""
|
||||
@@ -205,6 +275,15 @@ export function ImageCanvasProjectAssetPickerDialog({
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
{blockedReason !== null ? (
|
||||
<span className="block px-2 pb-1.5 text-[0.6875rem] leading-snug text-[var(--platform-text-base)]">
|
||||
{blockedReason}
|
||||
</span>
|
||||
) : hint !== null ? (
|
||||
<span className="block px-2 pb-1.5 text-[0.6875rem] leading-snug text-[var(--platform-text-base)]">
|
||||
{hint}
|
||||
</span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user