diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index 3014f539e..e692bba45 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -3944,7 +3944,8 @@ iframe.preview-frame { stroke-width: 2px; } -.game-resource-dependency-edge--task.is-highlighted .game-resource-dependency-trunk { +.game-resource-dependency-edge--task.is-highlighted + .game-resource-dependency-trunk { stroke-width: 2.4px; } @@ -4092,38 +4093,16 @@ iframe.preview-frame { font-size: 9px; } -.game-resource-focus-layer { - position: fixed; - inset: 0; - z-index: 240; - pointer-events: none; -} - .game-resource-focus { - position: fixed; - left: 50%; - top: 50%; display: grid; grid-template-rows: auto minmax(0, 1fr); - width: min(520px, calc(100vw - 108px)); - max-height: min(620px, calc(100dvh - 190px)); + width: 100%; + min-width: 0; + min-height: 0; overflow: hidden; - border: 1px solid #dda07e; - border-radius: 16px; - background: rgb(255 251 247 / 97%); + background: #fffdfa; color: #563b31; - box-shadow: 0 20px 42px rgb(84 49 34 / 18%); outline: 0; - pointer-events: auto; - transform: translate(-50%, -50%); -} - -.game-resource-focus[style] { - transform: none; -} - -.game-resource-focus--art { - width: min(760px, calc(100vw - 140px)); } .game-resource-focus-titlebar { @@ -4132,16 +4111,9 @@ iframe.preview-frame { justify-content: space-between; gap: 12px; min-height: 58px; - padding: 10px 12px 10px 14px; + padding: 10px 14px 10px 16px; border-bottom: 1px solid #ead8cf; background: #fff8f3; - cursor: grab; - touch-action: none; - user-select: none; -} - -.game-resource-focus-titlebar:active { - cursor: grabbing; } .game-resource-focus-heading { @@ -4151,6 +4123,17 @@ iframe.preview-frame { min-width: 0; } +.game-resource-focus-heading > span:last-child { + display: grid; + min-width: 0; +} + +.game-resource-focus-heading small { + color: #a27764; + font-size: 9px; + font-weight: 700; +} + .game-resource-focus-icon { display: grid; width: 36px; @@ -4176,26 +4159,12 @@ iframe.preview-frame { align-content: start; gap: 6px; min-height: 0; - padding: 14px 16px 16px; + padding: 18px 20px 24px; overflow: auto; overscroll-behavior: contain; scrollbar-gutter: stable; } -.game-resource-focus span, -.game-resource-focus small { - overflow-wrap: anywhere; -} - -.game-resource-focus span { - font-size: 11px; -} - -.game-resource-focus small { - color: #92776c; - font-size: 10px; -} - .game-resource-focus-close { display: grid; width: 28px; @@ -4210,10 +4179,43 @@ iframe.preview-frame { cursor: pointer; } +.game-resource-focus-metadata { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px 18px; + margin: 0 0 8px; + padding: 12px 14px; + border: 1px solid #eaded8; + border-radius: 12px; + background: #fff; +} + +.game-resource-focus-metadata > div { + display: grid; + grid-template-columns: 72px minmax(0, 1fr); + gap: 8px; + min-width: 0; +} + +.game-resource-focus-metadata dt, +.game-resource-focus-metadata dd { + margin: 0; + overflow-wrap: anywhere; + font-size: 10px; +} + +.game-resource-focus-metadata dt { + color: #a08377; +} + +.game-resource-focus-metadata dd { + color: #5d4339; +} + .game-resource-image-preview { position: relative; display: grid; - height: min(420px, calc(100dvh - 360px)); + height: min(460px, calc(100dvh - 330px)); min-height: 260px; margin-bottom: 8px; overflow: hidden; @@ -4251,13 +4253,13 @@ iframe.preview-frame { .game-resource-document-body { height: max-content; - min-height: 120px; + min-height: 220px; margin-top: 6px; padding: 14px; border-radius: 10px; background: #f8f2ee; - color: #9e8579; - font-size: 11px; + color: #765c51; + font-size: 12px; line-height: 1.6; overflow: hidden; overflow-wrap: anywhere; diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index 0186d1110..41bdd78ea 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -19,7 +19,6 @@ import { import { type CSSProperties, memo, - type PointerEvent as ReactPointerEvent, type ReactNode, useCallback, useEffect, @@ -36,7 +35,6 @@ import type { GameCreationAppManifest, GameCreationAppPreviewState, ProjectResourceCanvasLayoutMode, - ProjectResourceCanvasSection, } from '../../../../../packages/shared/src/contracts/gameCreationApp'; import { LocalGamePreviewFrame, @@ -53,43 +51,25 @@ import { } from './resourceDependencyGraphModel'; import { ResourceDependencyOverlay } from './ResourceDependencyOverlay'; import { useProjectResourceCanvasLayout } from './useProjectResourceCanvasLayout'; +import { + type ProjectAgentResultSummary, + type ProjectAttachmentResult, + type ProjectResource, + type ProjectResourceCategory, + type ProjectVersionResourceSummary, + projectResourcesFromReadModels, +} from './resourceProjectionModel'; -type AttachmentResult = { - fileName: string; - mediaType: string; - localPath?: string; - status: 'imported' | 'failed'; - error?: string; -}; +export type { + ProjectAgentResultSummary, + ProjectVersionResourceSummary, +} from './resourceProjectionModel'; -type ResourceCategory = ProjectResourceCanvasSection; +type ResourceCategory = ProjectResourceCategory; type ResourceSortMode = ProjectResourceCanvasLayoutMode; type WorkbenchMode = 'resources' | 'run'; type ApprovalMode = 'strict' | 'risk' | 'none'; -type Point = { - x: number; - y: number; -}; - -type ProjectResource = { - id: string; - category: ResourceCategory; - subtype: string; - label: string; - path: string; - mediaType: string; - sourceLabel: string; - taskTitle: string | null; - manifestAssetId: string | null; - producerTaskId: string | null; - externalResourceId: string | null; - referenceResourceIds: string[]; - dependencies: string[]; - dependencyDepth: number; - content?: string; -}; - type LocalProjectImagePreview = { path: string; mediaType: string; @@ -107,15 +87,6 @@ type ImagePreviewState = } | { status: 'failed'; resourceId: string; error: string }; -export type ProjectAgentResultSummary = { - agentId: string; - runId: string; - label: string; - title: string; - content: string; - updatedAt: number; -}; - export type ProjectAgentRuntimeSummary = { group: GameCreationAppAgentGroup; label: string; @@ -130,6 +101,7 @@ export type ProjectAgentRuntimeSummary = { const emptyProjectAgentRuntimeSummaries: ProjectAgentRuntimeSummary[] = []; const emptyProjectAgentResults: ProjectAgentResultSummary[] = []; +const emptyProjectVersions: ProjectVersionResourceSummary[] = []; const RESOURCE_DEPENDENCY_VISUAL_GUTTER = 64; type AgentSummary = ProjectAgentRuntimeSummary; @@ -138,12 +110,13 @@ export type ProjectDevelopmentViewProps = { projectName: string; projectPath: string; manifest: GameCreationAppManifest; - attachments: AttachmentResult[]; + attachments: ProjectAttachmentResult[]; recentRunStatus: string | null; recentRunStopReason: string | null; preview?: GameCreationAppPreviewState | null; agentRuntimeSummaries?: ProjectAgentRuntimeSummary[]; agentResults?: ProjectAgentResultSummary[]; + projectVersions?: ProjectVersionResourceSummary[]; supervisor: ReactNode; onHomeOpen: () => void; onProjectsOpen: () => void; @@ -196,36 +169,6 @@ const approvalOptions: Array<{ }, ]; -function fileName(path: string) { - return path.split(/[\\/]/).filter(Boolean).pop() || path; -} - -function categoryFromResource(path: string, mediaType: string) { - const normalizedPath = path.toLowerCase(); - const normalizedMediaType = mediaType.toLowerCase(); - if ( - normalizedMediaType.startsWith('audio/') || - /\.(mp3|wav|ogg|m4a|aac|flac)$/u.test(normalizedPath) - ) { - return 'audio' as const; - } - if ( - normalizedMediaType.startsWith('image/') || - normalizedMediaType.startsWith('video/') || - /\.(png|jpe?g|webp|gif|svg|mp4|webm)$/u.test(normalizedPath) - ) { - return 'art' as const; - } - if ( - normalizedMediaType.includes('json') || - normalizedMediaType.startsWith('text/') || - /\.(md|txt|json|ya?ml|toml)$/u.test(normalizedPath) - ) { - return 'document' as const; - } - return 'version' as const; -} - function isRasterImageResource(resource: ProjectResource) { const mediaType = resource.mediaType.toLowerCase(); return ( @@ -264,125 +207,6 @@ function imagePreviewErrorMessage(error: unknown) { return '图片暂时无法读取,请关闭后重试'; } -function resourcesFromProject( - manifest: GameCreationAppManifest, - attachments: AttachmentResult[], - agentResults: ProjectAgentResultSummary[], -) { - const taskById = new Map(manifest.tasks.map((task) => [task.id, task])); - const resources: ProjectResource[] = []; - - for (const task of manifest.tasks) { - if (task.status !== 'completed') { - continue; - } - for (const path of task.artifacts) { - const category = categoryFromResource(path, ''); - resources.push({ - id: `task:${task.id}:${path}`, - category, - subtype: 'task-artifact', - label: fileName(path), - path, - mediaType: category === 'document' ? '项目文档' : '项目产物', - sourceLabel: '任务产物', - taskTitle: task.title, - manifestAssetId: null, - producerTaskId: task.id, - externalResourceId: null, - referenceResourceIds: [], - dependencies: task.dependencies, - dependencyDepth: 0, - }); - } - } - - for (const asset of manifest.assets) { - const isPendingUiPrototype = - asset.kind === 'ui-prototype' && - taskById.get('design-foundation')?.status !== 'completed'; - resources.push({ - id: `asset:${asset.id}`, - category: categoryFromResource(asset.localPath, asset.mediaType), - subtype: asset.kind, - label: `${fileName(asset.localPath)}${ - isPendingUiPrototype ? '(待视觉验收)' : '' - }`, - path: asset.localPath, - mediaType: asset.mediaType, - sourceLabel: - isPendingUiPrototype && asset.source.kind === 'canvas' - ? '画板 · 候选界面图' - : asset.source.kind === 'canvas' - ? '画板' - : asset.source.kind === 'generated' - ? 'Agent 生成' - : '用户上传', - taskTitle: null, - manifestAssetId: asset.id, - producerTaskId: null, - externalResourceId: asset.source.resourceId ?? null, - referenceResourceIds: asset.source.referenceResourceIds ?? [], - dependencies: [], - dependencyDepth: 0, - }); - } - - for (const attachment of attachments) { - if (attachment.status !== 'imported' || !attachment.localPath) { - continue; - } - resources.push({ - id: `attachment:${attachment.localPath}`, - category: categoryFromResource( - attachment.localPath, - attachment.mediaType, - ), - subtype: 'attachment', - label: attachment.fileName, - path: attachment.localPath, - mediaType: attachment.mediaType || '未知媒体类型', - sourceLabel: '用户上传', - taskTitle: null, - manifestAssetId: null, - producerTaskId: null, - externalResourceId: null, - referenceResourceIds: [], - dependencies: [], - dependencyDepth: 0, - }); - } - - for (const result of agentResults) { - resources.push({ - id: `agent-result:${result.agentId}:${result.runId}`, - category: 'document', - subtype: 'agent-result', - label: result.title, - path: `专业 Agent 文本回执 · ${result.label}`, - mediaType: 'Agent 历史文本回执', - sourceLabel: `历史成果 · ${result.label}`, - taskTitle: null, - manifestAssetId: null, - producerTaskId: taskById.has(result.agentId) ? result.agentId : null, - externalResourceId: null, - referenceResourceIds: [], - dependencies: [], - dependencyDepth: 0, - content: result.content, - }); - } - - const uniqueByPath = new Map(); - for (const resource of resources) { - const existing = uniqueByPath.get(resource.path); - if (!existing || resource.id.startsWith('asset:')) { - uniqueByPath.set(resource.path, resource); - } - } - return Array.from(uniqueByPath.values()); -} - function summarizeAgent( manifest: GameCreationAppManifest, group: AgentSummary['group'], @@ -477,6 +301,7 @@ export default function ProjectDevelopmentView({ preview: previewOverride, agentRuntimeSummaries = emptyProjectAgentRuntimeSummaries, agentResults = emptyProjectAgentResults, + projectVersions = emptyProjectVersions, supervisor, }: ProjectDevelopmentViewProps) { const [mode, setMode] = useState('resources'); @@ -485,27 +310,23 @@ export default function ProjectDevelopmentView({ const [selectedResourceId, setSelectedResourceId] = useState( null, ); + const [focusedResourceId, setFocusedResourceId] = useState( + null, + ); const [approvalMode, setApprovalMode] = useState('strict'); const [approvalDialogOpen, setApprovalDialogOpen] = useState(false); const [approvalNotice, setApprovalNotice] = useState(''); const [showAllAgentGroups, setShowAllAgentGroups] = useState(false); const [runPlaying, setRunPlaying] = useState(false); const [activeSlice, setActiveSlice] = useState(0); - const [resourceDialogPosition, setResourceDialogPosition] = - useState(null); const [imagePreview, setImagePreview] = useState({ status: 'idle', resourceId: null, }); - const workbenchRef = useRef(null); - const stageRef = useRef(null); - const dockRef = useRef(null); - const resourceDialogRef = useRef(null); - const resourceDialogDragRef = useRef<{ - pointerId: number; - offsetX: number; - offsetY: number; - } | null>(null); + const resourceCanvasRef = useRef(null); + const resourceFocusRef = useRef(null); + const resourceListScrollRef = useRef({ left: 0, top: 0 }); + const restoreResourceListScrollRef = useRef(false); const preview = previewOverride ?? manifest.preview ?? null; const embeddedPreviewUrl = resolveEmbeddedPreviewUrl(preview); @@ -515,8 +336,14 @@ export default function ProjectDevelopmentView({ (task) => task.id === 'code-prototype' && task.status === 'completed', ); const projectedResources = useMemo( - () => resourcesFromProject(manifest, attachments, agentResults), - [agentResults, attachments, manifest], + () => + projectResourcesFromReadModels( + manifest, + attachments, + agentResults, + projectVersions, + ), + [agentResults, attachments, manifest, projectVersions], ); const resourceGraphInputs = useMemo( () => @@ -529,11 +356,7 @@ export default function ProjectDevelopmentView({ ); const resourceGraphScopeKey = useMemo( () => - JSON.stringify([ - projectPath, - manifest.projectId, - resourceGraphInputs, - ]), + JSON.stringify([projectPath, manifest.projectId, resourceGraphInputs]), [manifest.projectId, projectPath, resourceGraphInputs], ); const [resourceGraphState, setResourceGraphState] = useState<{ @@ -615,10 +438,9 @@ export default function ProjectDevelopmentView({ resourceGraphState.scopeKey === resourceGraphScopeKey; const resourceGraphReady = resourceGraphScopeMatches && resourceGraphState.status === 'ready'; - const resourceGraph = - resourceGraphReady - ? resourceGraphState.graph - : EMPTY_PROJECT_RESOURCE_GRAPH; + const resourceGraph = resourceGraphReady + ? resourceGraphState.graph + : EMPTY_PROJECT_RESOURCE_GRAPH; const resourceGraphInitializationReady = sortMode !== 'dependency' || (resourceGraphScopeMatches && @@ -658,8 +480,7 @@ export default function ProjectDevelopmentView({ mode: sortMode, resources, initializationReady: resourceGraphInitializationReady, - rederiveAutomaticPositions: - sortMode === 'dependency' && resourceGraphReady, + rederiveAutomaticPositions: sortMode === 'dependency' && resourceGraphReady, }); const resourcePositionById = useMemo( () => @@ -699,9 +520,7 @@ export default function ProjectDevelopmentView({ new Map( categoryOrder.map((category) => [ category, - visibleResources.filter( - (resource) => resource.category === category, - ), + visibleResources.filter((resource) => resource.category === category), ]), ), [visibleResources], @@ -732,8 +551,10 @@ export default function ProjectDevelopmentView({ ); const selectedResource = resources.find((resource) => resource.id === selectedResourceId) ?? null; - const selectedResourceIsImage = Boolean( - selectedResource && isRasterImageResource(selectedResource), + const focusedResource = + resources.find((resource) => resource.id === focusedResourceId) ?? null; + const focusedResourceIsImage = Boolean( + focusedResource && isRasterImageResource(focusedResource), ); const hasRegisteredArtImageAssets = manifest.assets.some( (asset) => @@ -770,32 +591,40 @@ export default function ProjectDevelopmentView({ const currentApprovalLabel = approvalOptions.find((option) => option.id === approvalMode)?.label ?? '严格审批'; + const FocusIcon = focusedResource + ? categoryIcons[focusedResource.category] + : FileText; useEffect(() => { if (embeddedPreviewUrl) { + setFocusedResourceId(null); setMode('run'); } }, [embeddedPreviewUrl]); useEffect(() => { setSelectedResourceId(null); + setFocusedResourceId(null); + resourceListScrollRef.current = { left: 0, top: 0 }; + restoreResourceListScrollRef.current = false; }, [projectPath]); useEffect(() => { - if (!selectedResourceId) { + if (!focusedResourceId) { return undefined; } function closeOnEscape(event: KeyboardEvent) { if (event.key === 'Escape') { - setSelectedResourceId(null); + restoreResourceListScrollRef.current = true; + setFocusedResourceId(null); } } window.addEventListener('keydown', closeOnEscape); return () => window.removeEventListener('keydown', closeOnEscape); - }, [selectedResourceId]); + }, [focusedResourceId]); useEffect(() => { - if (!selectedResource || !selectedResourceIsImage) { + if (!focusedResource || !focusedResourceIsImage) { setImagePreview({ status: 'idle', resourceId: null }); return undefined; } @@ -803,23 +632,23 @@ export default function ProjectDevelopmentView({ if (!invoke) { setImagePreview({ status: 'failed', - resourceId: selectedResource.id, + resourceId: focusedResource.id, error: '图片预览需要在客户端内打开', }); return undefined; } let cancelled = false; - setImagePreview({ status: 'loading', resourceId: selectedResource.id }); + setImagePreview({ status: 'loading', resourceId: focusedResource.id }); void invoke('read_local_project_image_preview', { projectPath, - relativePath: selectedResource.path, + relativePath: focusedResource.path, }) .then((preview) => { if (!cancelled) { setImagePreview({ status: 'loaded', - resourceId: selectedResource.id, + resourceId: focusedResource.id, preview, }); } @@ -828,7 +657,7 @@ export default function ProjectDevelopmentView({ if (!cancelled) { setImagePreview({ status: 'failed', - resourceId: selectedResource.id, + resourceId: focusedResource.id, error: imagePreviewErrorMessage(error), }); } @@ -836,142 +665,65 @@ export default function ProjectDevelopmentView({ return () => { cancelled = true; }; - }, [projectPath, selectedResource, selectedResourceIsImage]); - - const clampResourceDialogPosition = useCallback((x: number, y: number) => { - const dialog = resourceDialogRef.current; - const workbench = workbenchRef.current; - const dock = dockRef.current; - if (!dialog) { - return { x, y }; - } - - const dialogRect = dialog.getBoundingClientRect(); - const workbenchRect = workbench?.getBoundingClientRect(); - const dockRect = dock?.getBoundingClientRect(); - const viewportWidth = document.documentElement.clientWidth; - const viewportHeight = document.documentElement.clientHeight; - const minX = Math.max(12, (workbenchRect?.left ?? 0) + 12); - const maxRight = Math.min( - viewportWidth - 12, - (workbenchRect?.right ?? viewportWidth) - 12, - ); - const minY = Math.max(12, (workbenchRect?.top ?? 0) + 12); - const maxBottom = Math.min( - viewportHeight - 12, - dockRect ? dockRect.top - 12 : viewportHeight - 12, - ); - const maxX = Math.max(minX, maxRight - dialogRect.width); - const maxY = Math.max(minY, maxBottom - dialogRect.height); - - return { - x: Math.min(Math.max(x, minX), maxX), - y: Math.min(Math.max(y, minY), maxY), - }; - }, []); + }, [focusedResource, focusedResourceIsImage, projectPath]); useLayoutEffect(() => { - if (!selectedResource) { - setResourceDialogPosition(null); + if (focusedResource) { + resourceFocusRef.current?.focus({ preventScroll: true }); return; } - const dialog = resourceDialogRef.current; - const stage = stageRef.current; - if (!dialog || !stage) { + if (!restoreResourceListScrollRef.current) { return; } - const dialogRect = dialog.getBoundingClientRect(); - const stageRect = stage.getBoundingClientRect(); - setResourceDialogPosition( - clampResourceDialogPosition( - stageRect.left + (stageRect.width - dialogRect.width) / 2, - stageRect.top + (stageRect.height - dialogRect.height) / 2, - ), - ); - dialog.focus({ preventScroll: true }); - }, [clampResourceDialogPosition, selectedResource]); - - useEffect(() => { - if (!selectedResource) { - return undefined; + const canvas = resourceCanvasRef.current; + if (canvas) { + canvas.scrollLeft = resourceListScrollRef.current.left; + canvas.scrollTop = resourceListScrollRef.current.top; + restoreResourceListScrollRef.current = false; } - function clampOnResize() { - setResourceDialogPosition((current) => - current ? clampResourceDialogPosition(current.x, current.y) : current, - ); - } - window.addEventListener('resize', clampOnResize); - return () => window.removeEventListener('resize', clampOnResize); - }, [clampResourceDialogPosition, selectedResource]); + }, [focusedResource]); const handleResourceSelect = useCallback((resourceId: string) => { + const canvas = resourceCanvasRef.current; + if (canvas) { + resourceListScrollRef.current = { + left: canvas.scrollLeft, + top: canvas.scrollTop, + }; + } setSelectedResourceId(resourceId); + setFocusedResourceId(resourceId); }, []); - function handleResourceDialogPointerDown( - event: ReactPointerEvent, - ) { - if (event.button !== 0 || (event.target as HTMLElement).closest('button')) { - return; - } - const dialogRect = resourceDialogRef.current?.getBoundingClientRect(); - if (!dialogRect) { - return; - } - resourceDialogDragRef.current = { - pointerId: event.pointerId, - offsetX: event.clientX - dialogRect.left, - offsetY: event.clientY - dialogRect.top, - }; - event.currentTarget.setPointerCapture(event.pointerId); - event.preventDefault(); - } - - function handleResourceDialogPointerMove( - event: ReactPointerEvent, - ) { - const drag = resourceDialogDragRef.current; - if (!drag || drag.pointerId !== event.pointerId) { - return; - } - setResourceDialogPosition( - clampResourceDialogPosition( - event.clientX - drag.offsetX, - event.clientY - drag.offsetY, - ), - ); - } - - function handleResourceDialogPointerEnd( - event: ReactPointerEvent, - ) { - if (resourceDialogDragRef.current?.pointerId !== event.pointerId) { - return; - } - resourceDialogDragRef.current = null; - if (event.currentTarget.hasPointerCapture(event.pointerId)) { - event.currentTarget.releasePointerCapture(event.pointerId); - } + function closeResourceFocus() { + restoreResourceListScrollRef.current = true; + setFocusedResourceId(null); } function showRunView() { if (!runAvailable) { return; } + setFocusedResourceId(null); setMode('run'); } return (
@@ -999,7 +751,7 @@ export default function ProjectDevelopmentView({
- {mode === 'resources' ? ( + {mode === 'resources' && !focusedResource ? ( <>
- {!runAvailable ? ( + {!runAvailable && !focusedResource ? (

) : null} - {mode === 'resources' ? ( + {mode === 'resources' && focusedResource ? ( +

+
+ + + + + {categoryLabels[focusedResource.category]} + + {focusedResource.label} + + + + +
+
+ {focusedResourceIsImage ? ( +
+ {imagePreview.status === 'loaded' && + imagePreview.resourceId === focusedResource.id ? ( + {`${focusedResource.label} + setImagePreview({ + status: 'failed', + resourceId: focusedResource.id, + error: '图片内容无法解码,请重新生成或替换该资源', + }) + } + /> + ) : imagePreview.status === 'failed' && + imagePreview.resourceId === focusedResource.id ? ( +

{imagePreview.error}

+ ) : ( +

正在载入图片…

+ )} +
+ ) : null} +
+
+
资源路径
+
{focusedResource.path}
+
+
+
资源类型
+
{focusedResource.mediaType}
+
+
+
资源来源
+
{focusedResource.sourceLabel}
+
+ {focusedResource.taskTitle ? ( +
+
来源任务
+
{focusedResource.taskTitle}
+
+ ) : null} + {focusedResource.version ? ( + <> +
+
项目修订
+
{focusedResource.version.projectRevision}
+
+
+
父版本
+
+ {focusedResource.version.parentVersionId ?? + '首个版本'} +
+
+ + ) : null} +
+ {focusedResource.category === 'document' ? ( +
+ + {focusedResource.content ?? '文档内容尚未载入'} + +
+ ) : null} +
+
+ ) : mode === 'resources' ? (
) : null}
-