后台详情弹窗展示原图
Project CI / Frontend tests (push) Successful in 4m30s
Project CI / Repository checks (push) Successful in 5m13s
Project CI / Native shell tests (push) Failing after 7m38s
Project CI / Backend tests (push) Failing after 6m11s

素材查询详情弹窗并列展示原图与当前结果

精选审核详情弹窗保留原有元信息并补充原图

管理快照与后台 DTO 传递源资源媒体字段

限制源资源字段仅在后台审核读取路径回填

更新后台素材预览的架构踩坑说明
This commit is contained in:
2026-08-28 21:35:36 +08:00
parent dcf01f5681
commit 58db9fa637
14 changed files with 241 additions and 28 deletions
+9
View File
@@ -362,6 +362,10 @@ export interface AdminEditorAssetPayload {
assetKind?: string | null;
generationInputs?: Record<string, unknown> | 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<string, unknown> | null;
thumbnailSrc?: string | null;
@@ -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 (
<div className="admin-confirm-backdrop" role="presentation">
<section
@@ -91,13 +105,19 @@ export function AdminEditorAssetPreviewDialog({
<X size={17} aria-hidden="true" />
</button>
</div>
{sourceEntry ? (
<div className="admin-asset-query-source-preview">
<h4></h4>
<AdminEditorAssetPreviewMedia entry={sourceEntry} token={token} />
</div>
) : null}
<AdminEditorAssetPreviewMedia entry={entry} token={token} />
</section>
</div>
);
}
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()),
);
}
@@ -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({
</button>
</div>
<div className="admin-asset-query-detail-layout">
<button
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
title="预览素材"
type="button"
onClick={() => onPreview(entry)}
>
<AdminEditorAssetThumbnail entry={entry} token={token} />
</button>
<div className="admin-asset-query-detail-media">
{entry.sourceImageSrc?.trim() ? (
<div className="admin-asset-query-detail-media-card">
<h4></h4>
<AdminEditorAssetPreviewMedia
entry={{
assetId:
entry.sourceResourceId ?? `${entry.assetId}-source`,
label: entry.sourceLabel?.trim() || '原图',
imageSrc: entry.sourceImageSrc,
objectKey: entry.sourceObjectKey,
}}
token={token}
/>
</div>
) : null}
<div className="admin-asset-query-detail-media-card">
<h4></h4>
<button
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
title="预览素材"
type="button"
onClick={() => onPreview(entry)}
>
<AdminEditorAssetThumbnail entry={entry} token={token} />
</button>
</div>
</div>
<dl className="admin-info-list admin-detail-list">
<AdminInfoItem label="作者">
<div className="admin-inline-identity">
@@ -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({
</button>
</div>
<div className="admin-asset-query-detail-layout">
<button
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
title="预览素材"
type="button"
onClick={() => onPreview(entry)}
>
<AdminEditorAssetThumbnail
entry={entry}
token={token}
altPrefix="精选素材"
/>
</button>
<div className="admin-asset-query-detail-media">
{entry.sourceImageSrc?.trim() ? (
<div className="admin-asset-query-detail-media-card">
<h4></h4>
<AdminEditorAssetPreviewMedia
entry={{
assetId:
entry.sourceResourceId ?? `${entry.assetId}-source`,
label: entry.sourceLabel?.trim() || '原图',
imageSrc: entry.sourceImageSrc,
objectKey: entry.sourceObjectKey,
}}
token={token}
/>
</div>
) : null}
<div className="admin-asset-query-detail-media-card">
<h4></h4>
<button
className="admin-asset-query-thumb-button admin-asset-query-detail-thumb-button"
title="预览素材"
type="button"
onClick={() => onPreview(entry)}
>
<AdminEditorAssetThumbnail
entry={entry}
token={token}
altPrefix="精选素材"
/>
</button>
</div>
</div>
<dl className="admin-info-list admin-detail-list">
<AdminInfoItem label="作者">
<div className="admin-inline-identity">
+37
View File
@@ -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%;
}
@@ -548,6 +548,8 @@
## 后台素材查询不要用 SQL 直查 editor_asset
- 后台审核与素材查询的图片预览若要显示像素化原图,应由后台 read model 在 `sourceResourceId` 关联的项目资源上预先透传原图媒体引用,再复用管理员换签;不要让 admin-web 直接查询私有 `editor_project_resource`
- 现象:后台“素材查询”报 `HTTP 400no 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` 直查私有源表。
+17
View File
@@ -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(),
@@ -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,
@@ -247,6 +247,10 @@ pub struct AdminEditorAssetPayload {
pub asset_kind: Option<String>,
pub generation_inputs: Option<Value>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub created_at: String,
@@ -295,6 +299,11 @@ pub struct AdminEditorShowcaseAssetPayload {
pub model: Option<String>,
pub provider: Option<String>,
pub task_id: Option<String>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<Value>,
pub thumbnail_src: Option<String>,
@@ -166,6 +166,10 @@ pub struct AdminEditorAssetRecord {
pub asset_kind: Option<String>,
pub generation_inputs: Option<serde_json::Value>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub created_at: String,
@@ -198,6 +202,10 @@ pub struct EditorShowcaseAssetRecord {
pub provider: Option<String>,
pub task_id: Option<String>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<serde_json::Value>,
pub thumbnail_src: Option<String>,
@@ -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,
@@ -165,6 +165,10 @@ pub struct AdminEditorAssetRecord {
pub asset_kind: Option<String>,
pub generation_inputs: Option<serde_json::Value>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub created_at: String,
@@ -197,6 +201,10 @@ pub struct EditorShowcaseAssetRecord {
pub provider: Option<String>,
pub task_id: Option<String>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs: Option<serde_json::Value>,
pub thumbnail_src: Option<String>,
@@ -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,
@@ -25,6 +25,10 @@ pub struct AdminEditorAssetSnapshot {
pub asset_kind: Option<String>,
pub generation_inputs_json: Option<String>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub created_at_micros: i64,
@@ -23,6 +23,10 @@ pub struct EditorShowcaseAssetSnapshot {
pub provider: Option<String>,
pub task_id: Option<String>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs_json: Option<String>,
pub thumbnail_src: Option<String>,
@@ -879,6 +879,10 @@ pub struct AdminEditorAssetSnapshot {
pub asset_kind: Option<String>,
pub generation_inputs_json: Option<String>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub thumbnail_src: Option<String>,
pub generation_cost_mud_points: u64,
pub created_at_micros: i64,
@@ -912,6 +916,10 @@ pub struct EditorShowcaseAssetSnapshot {
pub provider: Option<String>,
pub task_id: Option<String>,
pub source_resource_id: Option<String>,
pub source_image_src: Option<String>,
pub source_object_key: Option<String>,
pub source_asset_object_id: Option<String>,
pub source_label: Option<String>,
pub asset_kind: Option<String>,
pub generation_inputs_json: Option<String>,
pub thumbnail_src: Option<String>,
@@ -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::<Vec<_>>();
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<String>,
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));