From 58db9fa637a5712c12d4784d6f6791c433de9688 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 28 Aug 2026 21:35:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=8E=E5=8F=B0=E8=AF=A6=E6=83=85=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E5=B1=95=E7=A4=BA=E5=8E=9F=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 素材查询详情弹窗并列展示原图与当前结果 精选审核详情弹窗保留原有元信息并补充原图 管理快照与后台 DTO 传递源资源媒体字段 限制源资源字段仅在后台审核读取路径回填 更新后台素材预览的架构踩坑说明 --- apps/admin-web/src/api/adminApiTypes.ts | 9 ++++ .../src/components/AdminEditorAssetMedia.tsx | 24 +++++++++- .../src/pages/AdminEditorAssetQueryPage.tsx | 37 +++++++++++---- .../pages/AdminEditorShowcaseReviewPage.tsx | 45 ++++++++++++++----- apps/admin-web/src/styles/admin.css | 37 +++++++++++++++ docs/project-memory/shared-memory/pitfalls.md | 2 + server-rs/crates/api-server/src/admin.rs | 17 +++++++ .../crates/api-server/src/editor_project.rs | 4 ++ .../crates/shared-contracts/src/admin.rs | 9 ++++ .../src/active/mapper/editor_project.rs | 16 +++++++ .../src/mapper/editor_project.rs | 16 +++++++ .../admin_editor_asset_snapshot_type.rs | 4 ++ .../editor_showcase_asset_snapshot_type.rs | 4 ++ .../src/editor_project_storage.rs | 45 ++++++++++++++++--- 14 files changed, 241 insertions(+), 28 deletions(-) diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 38c4a8e10..bcd2ee1db 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -362,6 +362,10 @@ export interface AdminEditorAssetPayload { assetKind?: string | null; generationInputs?: Record | null; sourceResourceId?: string | null; + sourceImageSrc?: string | null; + sourceObjectKey?: string | null; + sourceAssetObjectId?: string | null; + sourceLabel?: string | null; thumbnailSrc?: string | null; generationCostMudPoints: number; createdAt: string; @@ -404,6 +408,11 @@ export interface AdminEditorShowcaseAssetPayload { model?: string | null; provider?: string | null; taskId?: string | null; + sourceResourceId?: string | null; + sourceImageSrc?: string | null; + sourceObjectKey?: string | null; + sourceAssetObjectId?: string | null; + sourceLabel?: string | null; assetKind?: string | null; generationInputs?: Record | null; thumbnailSrc?: string | null; diff --git a/apps/admin-web/src/components/AdminEditorAssetMedia.tsx b/apps/admin-web/src/components/AdminEditorAssetMedia.tsx index c2f6fba40..36c13586f 100644 --- a/apps/admin-web/src/components/AdminEditorAssetMedia.tsx +++ b/apps/admin-web/src/components/AdminEditorAssetMedia.tsx @@ -25,6 +25,11 @@ export interface AdminPreviewableEditorAsset { thumbnailSrc?: string | null; imageSequenceFrames?: AdminEditorImageSequenceFramePayload[] | null; imageSequenceDurationMs?: number | null; + sourceResourceId?: string | null; + sourceImageSrc?: string | null; + sourceObjectKey?: string | null; + sourceAssetObjectId?: string | null; + sourceLabel?: string | null; } export function AdminEditorAssetThumbnail({ @@ -70,6 +75,15 @@ export function AdminEditorAssetPreviewDialog({ token: string; onClose: () => void; }) { + const sourceEntry = entry.sourceImageSrc?.trim() + ? { + assetId: entry.sourceResourceId ?? `${entry.assetId}-source`, + label: entry.sourceLabel?.trim() || '原图', + imageSrc: entry.sourceImageSrc, + objectKey: entry.sourceObjectKey, + } + : null; + return (
+ {sourceEntry ? ( +
+

原图

+ +
+ ) : null} ); } -function AdminEditorAssetPreviewMedia({ +export function AdminEditorAssetPreviewMedia({ entry, token, }: { @@ -716,7 +736,7 @@ function isAdminImageSequenceFrameUrlUsable( ): cached is AdminImageSequenceFrameCacheEntry { return Boolean( cached?.resolvedUrl && - (cached.expiresAtMs === null || cached.expiresAtMs > Date.now()), + (cached.expiresAtMs === null || cached.expiresAtMs > Date.now()), ); } diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx index 53eb3676e..82f4d43e2 100644 --- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx +++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx @@ -16,6 +16,7 @@ import type { } from '../api/adminApiTypes'; import { AdminEditorAssetPreviewDialog, + AdminEditorAssetPreviewMedia, AdminEditorAssetThumbnail, } from '../components/AdminEditorAssetMedia'; import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton'; @@ -561,14 +562,34 @@ function AdminAssetDetailDialog({
- +
+ {entry.sourceImageSrc?.trim() ? ( +
+

原图

+ +
+ ) : null} +
+

当前结果

+ +
+
diff --git a/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx b/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx index fb90af61a..2cdef7086 100644 --- a/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx +++ b/apps/admin-web/src/pages/AdminEditorShowcaseReviewPage.tsx @@ -17,6 +17,7 @@ import type { } from '../api/adminApiTypes'; import { AdminEditorAssetPreviewDialog, + AdminEditorAssetPreviewMedia, AdminEditorAssetThumbnail, } from '../components/AdminEditorAssetMedia'; import { AdminUserReferenceButton } from '../components/AdminUserReferenceButton'; @@ -709,18 +710,38 @@ function AdminShowcaseDetailDialog({
- +
+ {entry.sourceImageSrc?.trim() ? ( +
+

原图

+ +
+ ) : null} +
+

当前结果

+ +
+
diff --git a/apps/admin-web/src/styles/admin.css b/apps/admin-web/src/styles/admin.css index e7610f4f0..719ecf714 100644 --- a/apps/admin-web/src/styles/admin.css +++ b/apps/admin-web/src/styles/admin.css @@ -1655,6 +1655,18 @@ button:disabled { width: min(100%, 860px); } +.admin-asset-query-source-preview { + margin: 0 20px 16px; + padding-bottom: 16px; + border-bottom: 1px solid #e2e8f0; +} + +.admin-asset-query-source-preview h4 { + margin: 0 0 8px; + color: #64748b; + font-size: 13px; +} + .admin-asset-query-prompt-dialog .admin-panel-heading > div, .admin-asset-query-detail-dialog .admin-panel-heading > div, .admin-asset-query-preview-dialog .admin-panel-heading > div { @@ -1683,6 +1695,27 @@ button:disabled { align-items: start; } +.admin-asset-query-detail-media { + display: grid; + gap: 14px; +} + +.admin-asset-query-detail-media-card { + display: grid; + gap: 7px; +} + +.admin-asset-query-detail-media-card h4 { + margin: 0; + color: #64748b; + font-size: 13px; +} + +.admin-asset-query-detail-media-card .admin-asset-query-preview-media { + width: 220px; + height: 220px; +} + .admin-asset-query-detail-thumb-button .admin-asset-query-thumb { width: 220px; height: 220px; @@ -2332,6 +2365,10 @@ button:disabled { justify-self: center; } + .admin-asset-query-detail-media { + justify-items: center; + } + .admin-dashboard-tabs { width: 100%; } diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index bdf1d0eb5..8b7018d39 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -548,6 +548,8 @@ ## 后台素材查询不要用 SQL 直查 editor_asset +- 后台审核与素材查询的图片预览若要显示像素化原图,应由后台 read model 在 `sourceResourceId` 关联的项目资源上预先透传原图媒体引用,再复用管理员换签;不要让 admin-web 直接查询私有 `editor_project_resource`。 + - 现象:后台“素材查询”报 `HTTP 400:no such table: editor_asset. If the table exists, it may be marked private.`。 - 原因:`editor_asset` 是私有 SpacetimeDB 表,后台 SQL / schema HTTP 查询面看不到私有表;即使 api-server 有后台身份,也不能把私有表当 Dashboard SQL 表直接查。 - 处理:后台素材查询走 `spacetime-module` 内的 `admin_list_editor_assets_and_return` procedure,由 `spacetime-client` typed facade 调用后再在 `api-server` 映射作者展示名和陶泥号。新增类似后台只读能力时,优先补窄 procedure / read model,不要复用 `fetch_admin_dashboard_rows` 直查私有源表。 diff --git a/server-rs/crates/api-server/src/admin.rs b/server-rs/crates/api-server/src/admin.rs index 5afb9ccf2..6a9a69ffe 100644 --- a/server-rs/crates/api-server/src/admin.rs +++ b/server-rs/crates/api-server/src/admin.rs @@ -1096,6 +1096,11 @@ fn admin_editor_showcase_asset_payload_from_record( model: record.model, provider: record.provider, task_id: record.task_id, + source_resource_id: record.source_resource_id, + source_image_src: record.source_image_src, + source_object_key: record.source_object_key, + source_asset_object_id: record.source_asset_object_id, + source_label: record.source_label, asset_kind, generation_inputs: sanitize_editor_generation_inputs(record.generation_inputs), thumbnail_src: record.thumbnail_src, @@ -1250,6 +1255,10 @@ fn admin_editor_asset_payload_from_record( asset_kind, generation_inputs: sanitize_editor_generation_inputs(record.generation_inputs), source_resource_id: record.source_resource_id, + source_image_src: record.source_image_src, + source_object_key: record.source_object_key, + source_asset_object_id: record.source_asset_object_id, + source_label: record.source_label, thumbnail_src: record.thumbnail_src, generation_cost_mud_points: record.generation_cost_mud_points, created_at: record.created_at, @@ -4136,6 +4145,10 @@ mod tests { provider: None, task_id: None, source_resource_id: None, + source_image_src: None, + source_object_key: None, + source_asset_object_id: None, + source_label: None, asset_kind: Some("character".to_string()), generation_inputs: None, thumbnail_src: None, @@ -4187,6 +4200,10 @@ mod tests { asset_kind: Some("character".to_string()), generation_inputs: None, source_resource_id: None, + source_image_src: None, + source_object_key: None, + source_asset_object_id: None, + source_label: None, thumbnail_src: None, generation_cost_mud_points, created_at: "2026-07-04T10:00:00Z".to_string(), diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 0e60b3ea0..d9f110905 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -14612,6 +14612,10 @@ mod tests { provider: None, task_id: None, source_resource_id: None, + source_image_src: None, + source_object_key: None, + source_asset_object_id: None, + source_label: None, asset_kind: None, generation_inputs: None, thumbnail_src: None, diff --git a/server-rs/crates/shared-contracts/src/admin.rs b/server-rs/crates/shared-contracts/src/admin.rs index 84264e72e..513cff7aa 100644 --- a/server-rs/crates/shared-contracts/src/admin.rs +++ b/server-rs/crates/shared-contracts/src/admin.rs @@ -247,6 +247,10 @@ pub struct AdminEditorAssetPayload { pub asset_kind: Option, pub generation_inputs: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub thumbnail_src: Option, pub generation_cost_mud_points: u64, pub created_at: String, @@ -295,6 +299,11 @@ pub struct AdminEditorShowcaseAssetPayload { pub model: Option, pub provider: Option, pub task_id: Option, + pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub asset_kind: Option, pub generation_inputs: Option, pub thumbnail_src: Option, diff --git a/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs b/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs index 17918226c..671d20708 100644 --- a/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/active/mapper/editor_project.rs @@ -166,6 +166,10 @@ pub struct AdminEditorAssetRecord { pub asset_kind: Option, pub generation_inputs: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub thumbnail_src: Option, pub generation_cost_mud_points: u64, pub created_at: String, @@ -198,6 +202,10 @@ pub struct EditorShowcaseAssetRecord { pub provider: Option, pub task_id: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub asset_kind: Option, pub generation_inputs: Option, pub thumbnail_src: Option, @@ -1496,6 +1504,10 @@ fn map_admin_editor_asset_snapshot( asset_kind: snapshot.asset_kind, generation_inputs, source_resource_id: snapshot.source_resource_id, + source_image_src: snapshot.source_image_src, + source_object_key: snapshot.source_object_key, + source_asset_object_id: snapshot.source_asset_object_id, + source_label: snapshot.source_label, thumbnail_src: snapshot.thumbnail_src, generation_cost_mud_points: snapshot.generation_cost_mud_points, created_at: format_timestamp_micros(snapshot.created_at_micros), @@ -1533,6 +1545,10 @@ fn map_editor_showcase_asset_snapshot( provider: snapshot.provider, task_id: snapshot.task_id, source_resource_id: snapshot.source_resource_id, + source_image_src: snapshot.source_image_src, + source_object_key: snapshot.source_object_key, + source_asset_object_id: snapshot.source_asset_object_id, + source_label: snapshot.source_label, asset_kind: snapshot.asset_kind, generation_inputs, thumbnail_src: snapshot.thumbnail_src, diff --git a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs index 7e9d39668..008b012f5 100644 --- a/server-rs/crates/spacetime-client/src/mapper/editor_project.rs +++ b/server-rs/crates/spacetime-client/src/mapper/editor_project.rs @@ -165,6 +165,10 @@ pub struct AdminEditorAssetRecord { pub asset_kind: Option, pub generation_inputs: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub thumbnail_src: Option, pub generation_cost_mud_points: u64, pub created_at: String, @@ -197,6 +201,10 @@ pub struct EditorShowcaseAssetRecord { pub provider: Option, pub task_id: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub asset_kind: Option, pub generation_inputs: Option, pub thumbnail_src: Option, @@ -1380,6 +1388,10 @@ fn map_editor_project_resource_snapshot( provider: snapshot.provider, task_id: snapshot.task_id, source_resource_id: snapshot.source_resource_id, + source_image_src: snapshot.source_image_src, + source_object_key: snapshot.source_object_key, + source_asset_object_id: snapshot.source_asset_object_id, + source_label: snapshot.source_label, asset_kind: snapshot.asset_kind, generation_inputs, public_showcase_enabled: snapshot.public_showcase_enabled, @@ -1532,6 +1544,10 @@ fn map_editor_showcase_asset_snapshot( provider: snapshot.provider, task_id: snapshot.task_id, source_resource_id: snapshot.source_resource_id, + source_image_src: snapshot.source_image_src, + source_object_key: snapshot.source_object_key, + source_asset_object_id: snapshot.source_asset_object_id, + source_label: snapshot.source_label, asset_kind: snapshot.asset_kind, generation_inputs, thumbnail_src: snapshot.thumbnail_src, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/admin_editor_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/admin_editor_asset_snapshot_type.rs index be01aa17c..3da4f3b11 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/admin_editor_asset_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/admin_editor_asset_snapshot_type.rs @@ -25,6 +25,10 @@ pub struct AdminEditorAssetSnapshot { pub asset_kind: Option, pub generation_inputs_json: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub thumbnail_src: Option, pub generation_cost_mud_points: u64, pub created_at_micros: i64, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_snapshot_type.rs index 9e7121394..ba0ebc35b 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/editor_showcase_asset_snapshot_type.rs @@ -23,6 +23,10 @@ pub struct EditorShowcaseAssetSnapshot { pub provider: Option, pub task_id: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub asset_kind: Option, pub generation_inputs_json: Option, pub thumbnail_src: Option, diff --git a/server-rs/crates/spacetime-module/src/editor_project_storage.rs b/server-rs/crates/spacetime-module/src/editor_project_storage.rs index 86eaae1f6..f56ede76e 100644 --- a/server-rs/crates/spacetime-module/src/editor_project_storage.rs +++ b/server-rs/crates/spacetime-module/src/editor_project_storage.rs @@ -879,6 +879,10 @@ pub struct AdminEditorAssetSnapshot { pub asset_kind: Option, pub generation_inputs_json: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub thumbnail_src: Option, pub generation_cost_mud_points: u64, pub created_at_micros: i64, @@ -912,6 +916,10 @@ pub struct EditorShowcaseAssetSnapshot { pub provider: Option, pub task_id: Option, pub source_resource_id: Option, + pub source_image_src: Option, + pub source_object_key: Option, + pub source_asset_object_id: Option, + pub source_label: Option, pub asset_kind: Option, pub generation_inputs_json: Option, pub thumbnail_src: Option, @@ -3217,9 +3225,13 @@ fn admin_list_editor_assets( Ok(groups .into_iter() .flat_map(|(_, group_task_id, assets)| { - assets - .into_iter() - .map(move |row| admin_asset_snapshot_from_row(row, group_task_id.clone())) + assets.into_iter().map(move |row| { + let source = row + .source_resource_id + .as_ref() + .and_then(|id| ctx.db.editor_project_resource().resource_id().find(id)); + admin_asset_snapshot_from_row(row, group_task_id.clone(), source.as_ref()) + }) }) .collect()) } @@ -6405,7 +6417,7 @@ fn admin_list_editor_showcase_assets( cursor.as_ref(), ) }) - .map(showcase_snapshot_from_row) + .map(|asset| showcase_snapshot_from_row_with_ctx(Some(ctx), asset)) .collect::>(); assets.sort_by(|left, right| { @@ -6436,7 +6448,7 @@ fn admin_review_editor_showcase_asset( if asset.review_status == EDITOR_SHOWCASE_STATUS_APPROVED && review_status == EDITOR_SHOWCASE_STATUS_APPROVED { - return Ok(showcase_snapshot_from_row(asset)); + return Ok(showcase_snapshot_from_row_with_ctx(Some(ctx), asset)); } return Err("只有待审核素材可以审核".to_string()); } @@ -7835,6 +7847,16 @@ fn asset_folder_snapshot_from_row(row: EditorAssetFolder) -> EditorAssetFolderSn } fn showcase_snapshot_from_row(row: EditorShowcaseAsset) -> EditorShowcaseAssetSnapshot { + showcase_snapshot_from_row_with_ctx(None, row) +} + +fn showcase_snapshot_from_row_with_ctx( + ctx: Option<&ReducerContext>, + row: EditorShowcaseAsset, +) -> EditorShowcaseAssetSnapshot { + let source = row.source_resource_id.as_ref().and_then(|id| { + ctx.and_then(|value| value.db.editor_project_resource().resource_id().find(id)) + }); EditorShowcaseAssetSnapshot { showcase_id: row.showcase_id, asset_id: row.asset_id, @@ -7852,6 +7874,12 @@ fn showcase_snapshot_from_row(row: EditorShowcaseAsset) -> EditorShowcaseAssetSn provider: row.provider, task_id: row.task_id, source_resource_id: row.source_resource_id, + source_image_src: source.as_ref().map(|value| value.image_src.clone()), + source_object_key: source.as_ref().and_then(|value| value.object_key.clone()), + source_asset_object_id: source + .as_ref() + .and_then(|value| value.asset_object_id.clone()), + source_label: source.as_ref().and_then(|value| value.prompt.clone()), asset_kind: row.asset_kind, generation_inputs_json: row.generation_inputs_json, thumbnail_src: row.thumbnail_src, @@ -8362,6 +8390,7 @@ fn asset_snapshot_from_row(ctx: &ReducerContext, row: EditorAsset) -> EditorAsse fn admin_asset_snapshot_from_row( row: EditorAsset, group_task_id: Option, + source: Option<&EditorProjectResource>, ) -> AdminEditorAssetSnapshot { AdminEditorAssetSnapshot { asset_id: row.asset_id, @@ -8382,6 +8411,10 @@ fn admin_asset_snapshot_from_row( asset_kind: row.asset_kind, generation_inputs_json: row.generation_inputs_json, source_resource_id: row.source_resource_id, + source_image_src: source.map(|value| value.image_src.clone()), + source_object_key: source.and_then(|value| value.object_key.clone()), + source_asset_object_id: source.and_then(|value| value.asset_object_id.clone()), + source_label: source.and_then(|value| value.prompt.clone()), thumbnail_src: row.thumbnail_src, generation_cost_mud_points: row.generation_cost_mud_points, created_at_micros: row.created_at.to_micros_since_unix_epoch(), @@ -20685,7 +20718,7 @@ mod tests { admin_editor_asset_group_task_id_with(&slice, &mut task_id_aliases, |_, _| { panic!("persisted group must not depend on project resources") }); - let snapshot = admin_asset_snapshot_from_row(slice, group_task_id); + let snapshot = admin_asset_snapshot_from_row(slice, group_task_id, None); assert_eq!(snapshot.task_id.as_deref(), Some(split_task_id)); assert_eq!(snapshot.group_task_id.as_deref(), Some(source_task_id));