记录精选活动卡图片宽高
后台上传活动卡图片时读取并保存原始宽高 隐藏活动卡 Object Key 输入,重复上传替换图片引用 公开精选按活动图宽高设置卡片比例 同步 SpacetimeDB schema、bindings 和后端数据契约文档
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
@@ -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 {
|
||||
|
||||
+6
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user