记录精选活动卡图片宽高

后台上传活动卡图片时读取并保存原始宽高

隐藏活动卡 Object Key 输入,重复上传替换图片引用

公开精选按活动图宽高设置卡片比例

同步 SpacetimeDB schema、bindings 和后端数据契约文档
This commit is contained in:
2026-07-06 21:00:21 +08:00
parent ab61390121
commit be7f0375e5
18 changed files with 226 additions and 53 deletions
+72
View File
@@ -403,6 +403,7 @@ export async function uploadAdminEditorShowcaseCampaignImage(
file: File,
): Promise<AdminUploadedEditorShowcaseCampaignImage> {
const contentType = resolveAdminImageContentType(file);
const dimensions = await readAdminImageFileDimensions(file);
const response = await request<AdminCreateEditorShowcaseCampaignImageUploadTicketResponse>(
'/admin/api/editor-showcase/campaign/image-upload-ticket',
{
@@ -420,6 +421,8 @@ export async function uploadAdminEditorShowcaseCampaignImage(
return {
imageSrc: objectKey ? `/${objectKey}` : response.upload.legacyPublicPath,
imageObjectKey: objectKey,
imageWidth: dimensions.imageWidth,
imageHeight: dimensions.imageHeight,
legacyPublicPath: response.upload.legacyPublicPath,
};
}
@@ -607,6 +610,75 @@ function resolveAdminImageContentType(file: File) {
return declaredType || 'application/octet-stream';
}
function normalizeAdminImageDimensions(width: number, height: number) {
const imageWidth = Math.round(width);
const imageHeight = Math.round(height);
if (
!Number.isFinite(imageWidth) ||
!Number.isFinite(imageHeight) ||
imageWidth <= 0 ||
imageHeight <= 0
) {
return null;
}
return { imageWidth, imageHeight };
}
async function readAdminImageFileDimensions(file: File) {
if (typeof createImageBitmap === 'function') {
try {
const bitmap = await createImageBitmap(file);
const dimensions = normalizeAdminImageDimensions(
bitmap.width,
bitmap.height,
);
bitmap.close();
if (dimensions) {
return dimensions;
}
} catch {
// Fall back to HTMLImageElement decoding below.
}
}
if (
typeof Image === 'undefined' ||
typeof URL === 'undefined' ||
typeof URL.createObjectURL !== 'function'
) {
throw new Error('读取活动卡图片尺寸失败,请重新选择图片');
}
return new Promise<{ imageWidth: number; imageHeight: number }>(
(resolve, reject) => {
const objectUrl = URL.createObjectURL(file);
const image = new Image();
const cleanup = () => {
if (typeof URL.revokeObjectURL === 'function') {
URL.revokeObjectURL(objectUrl);
}
};
image.onload = () => {
cleanup();
const dimensions = normalizeAdminImageDimensions(
image.naturalWidth || image.width,
image.naturalHeight || image.height,
);
if (dimensions) {
resolve(dimensions);
return;
}
reject(new Error('读取活动卡图片尺寸失败,请重新选择图片'));
};
image.onerror = () => {
cleanup();
reject(new Error('读取活动卡图片尺寸失败,请重新选择图片'));
};
image.src = objectUrl;
},
);
}
function buildAdminDirectUploadFormData(
upload: AdminDirectUploadTicketPayload,
file: File,
+6
View File
@@ -458,6 +458,8 @@ export interface AdminEditorShowcaseCampaignPayload {
title: string;
imageSrc: string;
imageObjectKey?: string | null;
imageWidth?: number | null;
imageHeight?: number | null;
prompt: string;
author: string;
costText: string;
@@ -473,6 +475,8 @@ export interface AdminUpsertEditorShowcaseCampaignRequest {
title: string;
imageSrc: string;
imageObjectKey?: string | null;
imageWidth?: number | null;
imageHeight?: number | null;
prompt: string;
author: string;
costText: string;
@@ -500,6 +504,8 @@ export interface AdminCreateEditorShowcaseCampaignImageUploadTicketResponse {
export interface AdminUploadedEditorShowcaseCampaignImage {
imageSrc: string;
imageObjectKey: string;
imageWidth: number;
imageHeight: number;
legacyPublicPath: string;
}
@@ -91,6 +91,8 @@ beforeEach(() => {
title: '活动卡',
imageSrc: '/campaign.png',
imageObjectKey: null,
imageWidth: null,
imageHeight: null,
prompt: '活动提示词',
author: '官方',
costText: '12 泥点',
@@ -125,6 +127,8 @@ beforeEach(() => {
title: '新活动卡',
imageSrc: '/new-campaign.png',
imageObjectKey: null,
imageWidth: null,
imageHeight: null,
prompt: '新活动提示词',
author: '官方',
costText: '6 泥点',
@@ -135,6 +139,8 @@ beforeEach(() => {
imageSrc: '/generated-character-drafts/editor/showcase-campaign/card.png',
imageObjectKey:
'generated-character-drafts/editor/showcase-campaign/card.png',
imageWidth: 900,
imageHeight: 1200,
legacyPublicPath:
'/generated-character-drafts/editor/showcase-campaign/card.png',
});
@@ -291,7 +297,7 @@ test('后台精选审核可以切换展示和保存活动卡', async () => {
});
});
test('后台精选活动卡可以上传图片并保存 objectKey', async () => {
test('后台精选活动卡上传图片时替换图片引用并隐藏 objectKey', async () => {
render(
<AdminEditorShowcaseReviewPage
token="admin-token"
@@ -311,8 +317,45 @@ test('后台精选活动卡可以上传图片并保存 objectKey', async () => {
file,
);
});
expect(await screen.findByDisplayValue('/generated-character-drafts/editor/showcase-campaign/card.png')).toBeTruthy();
expect(screen.getByDisplayValue('generated-character-drafts/editor/showcase-campaign/card.png')).toBeTruthy();
expect(
await screen.findByDisplayValue(
'/generated-character-drafts/editor/showcase-campaign/card.png',
),
).toBeTruthy();
expect(
screen.queryByDisplayValue(
'generated-character-drafts/editor/showcase-campaign/card.png',
),
).toBeNull();
vi.mocked(uploadAdminEditorShowcaseCampaignImage).mockResolvedValueOnce({
imageSrc:
'/generated-character-drafts/editor/showcase-campaign/replacement.png',
imageObjectKey:
'generated-character-drafts/editor/showcase-campaign/replacement.png',
imageWidth: 1024,
imageHeight: 1536,
legacyPublicPath:
'/generated-character-drafts/editor/showcase-campaign/replacement.png',
});
const replacementFile = new File(['replacement-bytes'], 'replacement.png', {
type: 'image/png',
});
fireEvent.change(screen.getByLabelText('上传活动卡图片'), {
target: { files: [replacementFile] },
});
await waitFor(() => {
expect(uploadAdminEditorShowcaseCampaignImage).toHaveBeenCalledWith(
'admin-token',
replacementFile,
);
});
expect(
await screen.findByDisplayValue(
'/generated-character-drafts/editor/showcase-campaign/replacement.png',
),
).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '保存活动卡' }));
@@ -321,9 +364,11 @@ test('后台精选活动卡可以上传图片并保存 objectKey', async () => {
'admin-token',
expect.objectContaining({
imageSrc:
'/generated-character-drafts/editor/showcase-campaign/card.png',
'/generated-character-drafts/editor/showcase-campaign/replacement.png',
imageObjectKey:
'generated-character-drafts/editor/showcase-campaign/card.png',
'generated-character-drafts/editor/showcase-campaign/replacement.png',
imageWidth: 1024,
imageHeight: 1536,
}),
);
});
@@ -70,6 +70,8 @@ export function AdminEditorShowcaseReviewPage({
author: '',
costText: '',
imageObjectKey: null,
imageWidth: null,
imageHeight: null,
});
const [isSavingCampaign, setIsSavingCampaign] = useState(false);
const [isUploadingCampaignImage, setIsUploadingCampaignImage] =
@@ -202,6 +204,8 @@ export function AdminEditorShowcaseReviewPage({
title: campaignDraft.title,
imageSrc: campaignDraft.imageSrc,
imageObjectKey: campaignDraft.imageObjectKey ?? null,
imageWidth: campaignDraft.imageWidth ?? null,
imageHeight: campaignDraft.imageHeight ?? null,
prompt: campaignDraft.prompt,
author: campaignDraft.author,
costText: campaignDraft.costText,
@@ -228,6 +232,8 @@ export function AdminEditorShowcaseReviewPage({
...current,
imageSrc: upload.imageSrc,
imageObjectKey: upload.imageObjectKey,
imageWidth: upload.imageWidth,
imageHeight: upload.imageHeight,
}));
} catch (error: unknown) {
handlePageError(error, onUnauthorized, setErrorMessage);
@@ -514,6 +520,8 @@ export function AdminEditorShowcaseReviewPage({
...current,
imageSrc: event.target.value,
imageObjectKey: null,
imageWidth: null,
imageHeight: null,
}))
}
/>
@@ -538,25 +546,6 @@ export function AdminEditorShowcaseReviewPage({
</button>
</div>
</label>
<label className="admin-field">
<span>Object Key</span>
<input
value={campaignDraft.imageObjectKey ?? ''}
onChange={(event) =>
setCampaignDraft((current) => {
const imageObjectKey =
event.target.value.trim().replace(/^\/+/u, '') || null;
return {
...current,
imageObjectKey,
imageSrc: imageObjectKey
? `/${imageObjectKey}`
: current.imageSrc,
};
})
}
/>
</label>
<label className="admin-field">
<span></span>
<input
@@ -516,7 +516,7 @@ npm run check:server-rs-ddd
- Rust 结构体:`EditorShowcaseCampaignConfig`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:`陶泥儿精选` 首位固定活动卡配置表,当前使用固定 `config_id = global`。后台可配置启用状态、标题、图片地址、图片 OSS `image_object_key`提示词、作者和成本文案;上传按钮通过后台受控上传票据把图片写入 OSS,保存时同时落 `image_src` `image_object_key`。公开精选接口只在启用时返回该配置,前端优先用 `image_object_key` 走签名读地址展示。
- 说明:`陶泥儿精选` 首位固定活动卡配置表,当前使用固定 `config_id = global`。后台可配置启用状态、标题、图片地址、提示词、作者和成本文案;上传按钮通过后台受控上传票据把图片写入 OSS,保存时同时落 `image_src`、内部图片 OSS `image_object_key``image_width``image_height`。公开精选接口只在启用时返回该配置,前端优先用 `image_object_key` 走签名读地址展示,并按记录的图片宽高决定活动卡比例
- 索引:主键 `config_id`
### `inventory_slot`
+27 -27
View File
@@ -19,17 +19,16 @@ use serde::Deserialize;
use serde_json::{Map, Value};
use shared_contracts::admin::{
AdminCreateEditorShowcaseCampaignImageUploadTicketRequest,
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse,
AdminDirectUploadTicketPayload,
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
AdminDashboardBreakdownRowPayload, AdminDashboardChartBucketPayload,
AdminDashboardChartPayload, AdminDashboardMetricsPayload, AdminDashboardOperationMetricPayload,
AdminDashboardOperationsPayload, AdminDashboardQuery, AdminDashboardRangePayload,
AdminDashboardResponse, AdminDatabaseOverviewPayload, AdminDatabaseTableListResponse,
AdminDatabaseTableRowPayload, AdminDatabaseTableRowsQuery, AdminDatabaseTableRowsResponse,
AdminDatabaseTableStatPayload, AdminDebugHeaderInput, AdminDebugHttpRequest,
AdminDebugHttpResponse, AdminEditorAssetListQuery, AdminEditorAssetListResponse,
AdminEditorAssetPayload, AdminEditorShowcaseAssetPayload, AdminEditorShowcaseAssetResponse,
AdminCreateEditorShowcaseCampaignImageUploadTicketResponse, AdminCreationEntryConfigResponse,
AdminCreationEntryTypeConfigPayload, AdminDashboardBreakdownRowPayload,
AdminDashboardChartBucketPayload, AdminDashboardChartPayload, AdminDashboardMetricsPayload,
AdminDashboardOperationMetricPayload, AdminDashboardOperationsPayload, AdminDashboardQuery,
AdminDashboardRangePayload, AdminDashboardResponse, AdminDatabaseOverviewPayload,
AdminDatabaseTableListResponse, AdminDatabaseTableRowPayload, AdminDatabaseTableRowsQuery,
AdminDatabaseTableRowsResponse, AdminDatabaseTableStatPayload, AdminDebugHeaderInput,
AdminDebugHttpRequest, AdminDebugHttpResponse, AdminDirectUploadTicketPayload,
AdminEditorAssetListQuery, AdminEditorAssetListResponse, AdminEditorAssetPayload,
AdminEditorShowcaseAssetPayload, AdminEditorShowcaseAssetResponse,
AdminEditorShowcaseCampaignPayload, AdminEditorShowcaseCampaignResponse,
AdminEditorShowcaseDisplayRequest, AdminEditorShowcaseListQuery,
AdminEditorShowcaseListResponse, AdminEditorShowcaseReviewRequest, AdminLoginRequest,
@@ -41,9 +40,7 @@ use shared_contracts::admin::{
AdminUpsertEditorShowcaseCampaignRequest, AdminUpsertPublicWorkInteractionConfigRequest,
AdminWorkVisibilityListResponse,
};
use shared_contracts::assets::{
CreateDirectUploadTicketRequest, DirectUploadObjectAccess,
};
use shared_contracts::assets::{CreateDirectUploadTicketRequest, DirectUploadObjectAccess};
use shared_contracts::creation_entry_config::{
encode_unified_creation_spec_response, validate_unified_creation_spec_for_play,
};
@@ -587,6 +584,8 @@ pub async fn admin_upsert_editor_showcase_campaign(
title: payload.title,
image_src: payload.image_src,
image_object_key: payload.image_object_key,
image_width: payload.image_width,
image_height: payload.image_height,
prompt: payload.prompt,
author: payload.author,
cost_text: payload.cost_text,
@@ -620,14 +619,11 @@ pub async fn admin_create_editor_showcase_campaign_image_upload_ticket(
);
}
if payload.content_length == 0 {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST).with_message("图片文件不能为空")
);
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("图片文件不能为空"));
}
if payload.content_length > ADMIN_EDITOR_SHOWCASE_CAMPAIGN_IMAGE_MAX_BYTES {
return Err(
AppError::from_status(StatusCode::BAD_REQUEST)
.with_message("活动卡图片不能超过 20MB")
AppError::from_status(StatusCode::BAD_REQUEST).with_message("活动卡图片不能超过 20MB")
);
}
@@ -650,7 +646,10 @@ pub async fn admin_create_editor_showcase_campaign_image_upload_ticket(
"asset_kind".to_string(),
"editor_showcase_campaign_image".to_string(),
),
("source".to_string(), "admin_editor_showcase_campaign".to_string()),
(
"source".to_string(),
"admin_editor_showcase_campaign".to_string(),
),
]),
max_size_bytes: Some(payload.content_length),
expire_seconds: None,
@@ -662,9 +661,11 @@ pub async fn admin_create_editor_showcase_campaign_image_upload_ticket(
let payload_value = value.get("data").cloned().unwrap_or(value);
let response: shared_contracts::assets::CreateDirectUploadTicketResponse =
serde_json::from_value(payload_value).map_err(|error| {
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(serde_json::json!({
"message": format!("活动卡上传凭证响应解析失败:{error}"),
}))
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(
serde_json::json!({
"message": format!("活动卡上传凭证响应解析失败:{error}"),
}),
)
})?;
Ok(json_success_body(
Some(&request_context),
@@ -799,6 +800,8 @@ fn admin_editor_showcase_campaign_payload_from_record(
title: record.title,
image_src: record.image_src,
image_object_key: record.image_object_key,
image_width: record.image_width,
image_height: record.image_height,
prompt: record.prompt,
author: record.author,
cost_text: record.cost_text,
@@ -821,10 +824,7 @@ fn admin_direct_upload_ticket_payload_from_asset_upload(
upload.form_fields.credential,
),
("x-oss-date".to_string(), upload.form_fields.date),
(
"x-oss-signature".to_string(),
upload.form_fields.signature,
),
("x-oss-signature".to_string(), upload.form_fields.signature),
(
"success_action_status".to_string(),
upload.form_fields.success_action_status,
@@ -618,6 +618,8 @@ pub struct EditorShowcaseCampaignPayload {
title: String,
image_src: String,
image_object_key: Option<String>,
image_width: Option<u32>,
image_height: Option<u32>,
prompt: String,
author: String,
cost_text: String,
@@ -3352,6 +3354,8 @@ fn editor_showcase_campaign_payload_from_record(
title: record.title,
image_src: record.image_src,
image_object_key: record.image_object_key,
image_width: record.image_width,
image_height: record.image_height,
prompt: record.prompt,
author: record.author,
cost_text: record.cost_text,
@@ -257,6 +257,8 @@ pub struct AdminEditorShowcaseCampaignPayload {
pub title: String,
pub image_src: String,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
pub prompt: String,
pub author: String,
pub cost_text: String,
@@ -276,6 +278,8 @@ pub struct AdminUpsertEditorShowcaseCampaignRequest {
pub title: String,
pub image_src: String,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
pub prompt: String,
pub author: String,
pub cost_text: String,
@@ -181,6 +181,8 @@ pub struct EditorShowcaseCampaignConfigRecord {
pub title: String,
pub image_src: String,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
pub prompt: String,
pub author: String,
pub cost_text: String,
@@ -428,6 +430,8 @@ pub struct EditorShowcaseCampaignConfigUpsertRecordInput {
pub cost_text: String,
pub updated_at_micros: i64,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
}
impl From<EditorProjectCreateRecordInput> for crate::module_bindings::EditorProjectCreateInput {
@@ -786,6 +790,8 @@ impl From<EditorShowcaseCampaignConfigUpsertRecordInput>
cost_text: input.cost_text,
updated_at_micros: input.updated_at_micros,
image_object_key: input.image_object_key,
image_width: input.image_width,
image_height: input.image_height,
}
}
}
@@ -1218,6 +1224,8 @@ fn map_editor_showcase_campaign_config_snapshot(
title: snapshot.title,
image_src: snapshot.image_src,
image_object_key: snapshot.image_object_key,
image_width: snapshot.image_width,
image_height: snapshot.image_height,
prompt: snapshot.prompt,
author: snapshot.author,
cost_text: snapshot.cost_text,
@@ -16,6 +16,8 @@ pub struct EditorShowcaseCampaignConfigSnapshot {
pub updated_by_admin_user_id: Option<String>,
pub updated_at_micros: i64,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
}
impl __sdk::InModule for EditorShowcaseCampaignConfigSnapshot {
@@ -17,6 +17,8 @@ pub struct EditorShowcaseCampaignConfig {
pub updated_by_admin_user_id: Option<String>,
pub updated_at: __sdk::Timestamp,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
}
impl __sdk::InModule for EditorShowcaseCampaignConfig {
@@ -38,6 +40,8 @@ pub struct EditorShowcaseCampaignConfigCols {
__sdk::__query_builder::Col<EditorShowcaseCampaignConfig, Option<String>>,
pub updated_at: __sdk::__query_builder::Col<EditorShowcaseCampaignConfig, __sdk::Timestamp>,
pub image_object_key: __sdk::__query_builder::Col<EditorShowcaseCampaignConfig, Option<String>>,
pub image_width: __sdk::__query_builder::Col<EditorShowcaseCampaignConfig, Option<u32>>,
pub image_height: __sdk::__query_builder::Col<EditorShowcaseCampaignConfig, Option<u32>>,
}
impl __sdk::__query_builder::HasCols for EditorShowcaseCampaignConfig {
@@ -57,6 +61,8 @@ impl __sdk::__query_builder::HasCols for EditorShowcaseCampaignConfig {
),
updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"),
image_object_key: __sdk::__query_builder::Col::new(table_name, "image_object_key"),
image_width: __sdk::__query_builder::Col::new(table_name, "image_width"),
image_height: __sdk::__query_builder::Col::new(table_name, "image_height"),
}
}
}
@@ -16,6 +16,8 @@ pub struct EditorShowcaseCampaignConfigUpsertInput {
pub cost_text: String,
pub updated_at_micros: i64,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
}
impl __sdk::InModule for EditorShowcaseCampaignConfigUpsertInput {
@@ -207,6 +207,10 @@ pub struct EditorShowcaseCampaignConfig {
updated_at: Timestamp,
#[default(None::<String>)]
image_object_key: Option<String>,
#[default(None::<u32>)]
image_width: Option<u32>,
#[default(None::<u32>)]
image_height: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
@@ -466,6 +470,8 @@ pub struct EditorShowcaseCampaignConfigSnapshot {
pub updated_by_admin_user_id: Option<String>,
pub updated_at_micros: i64,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
@@ -621,6 +627,8 @@ pub struct EditorShowcaseCampaignConfigUpsertInput {
pub cost_text: String,
pub updated_at_micros: i64,
pub image_object_key: Option<String>,
pub image_width: Option<u32>,
pub image_height: Option<u32>,
}
#[derive(Clone, Debug, PartialEq, SpacetimeType)]
@@ -2202,7 +2210,10 @@ fn upsert_editor_showcase_campaign_config(
let title = input.title.trim().to_string();
let image_object_key = normalize_optional(input.image_object_key)
.map(|value| value.trim_start_matches('/').to_string());
let image_src = normalize_campaign_image_src(input.image_src.as_str(), image_object_key.as_deref());
let image_src =
normalize_campaign_image_src(input.image_src.as_str(), image_object_key.as_deref());
let image_width = input.image_width.filter(|value| *value > 0);
let image_height = input.image_height.filter(|value| *value > 0);
if input.enabled {
if title.is_empty() {
return Err("editor_showcase_campaign_config.title 不能为空".to_string());
@@ -2238,6 +2249,8 @@ fn upsert_editor_showcase_campaign_config(
updated_by_admin_user_id: Some(admin_user_id),
updated_at: now,
image_object_key,
image_width,
image_height,
});
ctx.db
.editor_showcase_campaign_config()
@@ -2713,6 +2726,8 @@ fn showcase_campaign_config_snapshot_from_row(
updated_by_admin_user_id: row.updated_by_admin_user_id,
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
image_object_key: row.image_object_key,
image_width: row.image_width,
image_height: row.image_height,
}
}
@@ -1271,6 +1271,12 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
object
.entry("image_object_key".to_string())
.or_insert(serde_json::Value::Null);
object
.entry("image_width".to_string())
.or_insert(serde_json::Value::Null);
object
.entry("image_height".to_string())
.or_insert(serde_json::Value::Null);
}
}
if table_name == "big_fish_creation_session" {
@@ -83,6 +83,8 @@ describe('creationShowcaseModel', () => {
enabled: true,
title: '活动精选',
imageSrc: '/campaign.png',
imageWidth: 900,
imageHeight: 1200,
prompt: '活动提示词',
author: '运营',
costText: '50泥点',
@@ -108,6 +110,10 @@ describe('creationShowcaseModel', () => {
campaign: true,
cost: '50泥点',
});
expect(items[0]?.previews[0]).toMatchObject({
width: 900,
height: 1200,
});
expect(items[1]).toMatchObject({
id: 'asset:asset-1',
label: '审核通过素材',
@@ -805,6 +805,8 @@ function campaignToShowcaseItem(
src: imageSrc,
objectKey: objectKey || null,
mediaType: 'image',
width: campaign.imageWidth ?? undefined,
height: campaign.imageHeight ?? undefined,
},
],
prompt: campaign.prompt.trim() || UNKNOWN_META_VALUE,
@@ -239,6 +239,8 @@ describe('editorProjectClient', () => {
enabled: true,
title: '活动精选',
imageSrc: '/campaign.png',
imageWidth: 900,
imageHeight: 1200,
prompt: '活动提示词',
author: '运营',
costText: '50泥点',
@@ -254,6 +256,8 @@ describe('editorProjectClient', () => {
expect(page.resources[0]?.authorPublicUserCode).toBe('SY-00000042');
expect(page.nextCursor).toBe('cursor-next');
expect(page.campaign?.title).toBe('活动精选');
expect(page.campaign?.imageWidth).toBe(900);
expect(page.campaign?.imageHeight).toBe(1200);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/showcase/resources',
{ method: 'GET' },
@@ -139,6 +139,8 @@ export type EditorShowcaseCampaignSnapshot = {
title: string;
imageSrc: string;
imageObjectKey?: string | null;
imageWidth?: number | null;
imageHeight?: number | null;
prompt: string;
author: string;
costText: string;