修复创作页精选作者和瀑布流宽度

公开精选接口返回作者展示名和陶泥号

前端精选作者只使用公开展示字段,不再兜底内部用户 ID

约束精选瀑布流多列容器宽度,避免 release 桌面端横向撑宽

补充前后端回归测试和项目记忆文档
This commit is contained in:
2026-06-27 20:01:17 +08:00
parent bb51c7e0d2
commit f20f4e0f8e
10 changed files with 188 additions and 25 deletions
+13 -5
View File
@@ -38,14 +38,22 @@
- 验证:`creationShowcaseModel.test.ts` 覆盖同源同媒体副本只展示原件;`ImageCanvasEditorAssetsIntegration.test.tsx` 覆盖拖拽生成素材到画布时继续提交 `sourceResourceId``cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 确认后端资源复用 / 精选过滤逻辑可编译。
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs``src/components/creation-home/creationShowcaseModel.ts``src/components/image-editor/ImageCanvasEditorAssetsIntegration.test.tsx`
## 陶泥儿精选作者丢失先查公开资源 owner 字段
## 陶泥儿精选作者丢失先查公开作者展示字段
- 现象:`/creation``陶泥儿精选` 卡片和预览弹窗中,素材下方作者 id 消失只显示占位。
- 原因:精选数据源来自公开 `editor_project_resource` 快照;如果 SpacetimeDB read model、`spacetime-client` mapper 或 `api-server` payload 任一层漏传 `owner_user_id` / `ownerUserId`,前端作者兜底就没有真实值可显示
- 处理:`EditorProjectResourceSnapshot``EditorProjectResourceRecord``EditorProjectResourcePayload` 需要一路保留 owner 字段;前端 `creationShowcaseModel` 在没有作者昵称字段时读取 `ownerUserId` 作为作者兜底
- 验证:`creationShowcaseModel.test.ts` 覆盖 owner id 作者兜底`editorProjectClient.test.ts` 覆盖公开精选接口客户端保留 `ownerUserId`;后端改 read model运行 `npm run spacetime:generate``cargo check -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml``npm run check:spacetime-schema`
- 现象:`/creation``陶泥儿精选` 卡片和预览弹窗中,素材下方作者消失只显示占位,或错误显示内部用户 ID
- 原因:精选数据源来自公开 `editor_project_resource` 快照;如果 SpacetimeDB read model、`spacetime-client` mapper 或 `api-server` payload 任一层漏传 `authorDisplayName` / `display_name``authorPublicUserCode` / 陶泥号,前端没有可展示的公开作者字段。`owner_user_id` / `ownerUserId` / `user_id` 是内部归属字段,不是公开作者兜底。
- 处理:`EditorProjectResourceSnapshot``EditorProjectResourceRecord``EditorProjectResourcePayload` 需要一路保留公开作者展示字段;前端 `creationShowcaseModel` 优先显示 `authorDisplayName` / `display_name`,没有展示名时显示 `authorPublicUserCode` / 陶泥号,绝不能兜底到内部 `ownerUserId` / `user_id`
- 验证:`creationShowcaseModel.test.ts` 覆盖展示名优先、陶泥号兜底和内部 owner/user id 不展示`editorProjectClient.test.ts` 覆盖公开精选接口客户端保留公开作者字段;若改动 SpacetimeDB read model,再运行 `npm run spacetime:generate``cargo check -p spacetime-client -p api-server --manifest-path server-rs/Cargo.toml``npm run check:spacetime-schema`
- 关联:`server-rs/crates/spacetime-module/src/editor_project_storage.rs``server-rs/crates/spacetime-client/src/mapper/editor_project.rs``server-rs/crates/api-server/src/editor_project.rs``src/components/creation-home/creationShowcaseModel.ts`
## 陶泥儿精选瀑布流变宽先查 multi-column 容器宽度
- 现象:release `/creation` 桌面端精选卡片明显变宽,第三列被裁到屏幕外,页面内部可横向滑动;dev 看起来正常。
- 原因:精选瀑布流使用 `column-count`,当它作为 grid item 时如果没有显式 `width: 100%` / `min-width: 0`Chrome 会用多列内容的 intrinsic width 反向撑开 grid track。线上实测 1920 视口下 section 为 `1296px`waterfall 被撑到约 `2048px`,单卡宽约 `672px`
- 处理:保留 multi-column 瀑布流时,`.creation-landing__asset-waterfall` 必须显式约束 `width: 100%``min-width: 0`;不要只看单张图片天然尺寸或改卡片宽度。
- 验证:Playwright / CSSOM 检查 `.creation-landing__section``.creation-landing__asset-waterfall`、首张 `.creation-landing__asset-card``getBoundingClientRect()`waterfall 宽度应等于 section 宽度。
- 关联:`src/index.css``src/components/creation-home/CreationLandingView.tsx`
## 画板外部生成排队超时不是失败
- 现象:画板发起付费图片生成后,前端弹出 `生成任务仍在队列中,请稍后刷新画布查看结果`,但后端任务仍在队列或执行中,后续可能正常完成。
@@ -468,7 +468,7 @@ npm run check:server-rs-ddd
- Rust 结构体:`EditorProjectResource`
- 源码:`server-rs/crates/spacetime-module/src/editor_project_storage.rs`
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind``generation_inputs_json``public_showcase_enabled``asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复;`public_showcase_enabled` 控制该生成资源是否进入 `/creation``陶泥儿精选`,新增和旧行默认 `true`,但通过 `source_resource_id` 指向同一 owner 源资源且媒体引用相同的画布副本默认不公开。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot``source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 `editor_canvas.layers_json` 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。公开精选读取走 `GET /api/editor/showcase/resources`,只返回 `source_type = generated`、图片源非空且 `public_showcase_enabled = true` 的全站资源,并跳过同源同媒体副本,按 `created_at` 倒序取前 36 条;公开响应必须透传 `ownerUserId`,供精选卡片在缺少作者昵称时显示作者 id。公开开关修改走登录鉴权 `PATCH /api/editor/project-resources/{resource_id}/showcase`,只允许资源 owner 更新。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。
- 说明:图片画布工程资源元数据表,保存已经放入某个 project 画布的上传 / 生成图片资源快照、OSS 引用、尺寸、来源类型、prompt、provider、task、源资源关系、`asset_kind``generation_inputs_json``public_showcase_enabled``asset_kind` 标记角色、图标、UI 设计图、视频、音频等素材类别;`generation_inputs_json` 保存用户可见生成输入快照,供图片信息页刷新后恢复;`public_showcase_enabled` 控制该生成资源是否进入 `/creation``陶泥儿精选`,新增和旧行默认 `true`,但通过 `source_resource_id` 指向同一 owner 源资源且媒体引用相同的画布副本默认不公开。图片 / 图标 / UI 提取等生成 BFF 在请求携带 `project_id` 时负责创建该表记录并把 resource 快照返回前端;前端只保存布局引用,不能把同一生成结果再次作为正式业务真相写入。项目封面快照也落在该表,使用 `asset_kind = project-cover-snapshot``source_type = uploaded` 和私有 OSS / asset object 引用,代表画布当前视口栅格化后的静态封面;项目列表和创作主页最近项目只读取最新封面快照资源,不在列表页根据 `editor_canvas.layers_json` 临时拼画布。从账号级素材库把同一生成素材拖回同一项目画布时,后端优先复用同项目内同源同媒体资源,避免每个图层实例都插入新的资源行。公开精选读取走 `GET /api/editor/showcase/resources`,只返回 `source_type = generated`、图片源非空且 `public_showcase_enabled = true` 的全站资源,并跳过同源同媒体副本,按 `created_at` 倒序取前 36 条;公开响应的作者展示字段优先提供 `authorDisplayName` / `display_name`,没有展示名时提供 `authorPublicUserCode` / 陶泥号作为展示兜底,前端公开展示绝不能兜底到内部 `ownerUserId` / `user_id`。公开开关修改走登录鉴权 `PATCH /api/editor/project-resources/{resource_id}/showcase`,只允许资源 owner 更新。账号级素材删除不级联删除该表,避免历史画布丢图。`editor_canvas.layers_json` 只保存图层几何、层级、分组、资源引用和生成器对象;新写入不再把素材生成输入快照作为图层布局真相保存,旧布局字段只作为兼容兜底读取。
- 索引:`by_editor_project_resource_project_id``by_editor_project_resource_owner_user_id`
### `editor_asset_folder`
@@ -63,7 +63,7 @@
`陶泥儿精选` 是页面底部的全站公开画布生成素材瀑布流,不承载玩法入口列表。瀑布流卡片按真实素材宽高设置预览比例,同一行允许出现不同高度卡片,不使用固定等高网格。创作入口配置仍继续来自 `/api/creation-entry/config`,供旧创作入口和具体 `/creation/<play>` 工作台使用,但不作为本页精选区内容。
精选内容只使用 `editor_project_resource``sourceType="generated"``public_showcase_enabled = true` 的项目资源,代表用户通过图片画布生成、已经落入某个项目并允许公开展示的素材。账号级 `editor_asset` 只表示跨项目素材库,不单独作为精选来源;上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。从项目素材把同一个生成素材再次拖入画布时,只是新增图层实例或复用同源同媒体项目资源,不应额外生成新的精选候选;历史上已经产生的同源同媒体副本也需要在精选读取时跳过。公开 BFF 必须返回资源 `ownerUserId`,前端在没有作者昵称字段时用该 id 作为作者兜底。若现有数据缺少提示词、作者标识或成本字段,v1 显示保守占位,不伪造内容。
精选内容只使用 `editor_project_resource``sourceType="generated"``public_showcase_enabled = true` 的项目资源,代表用户通过图片画布生成、已经落入某个项目并允许公开展示的素材。账号级 `editor_asset` 只表示跨项目素材库,不单独作为精选来源;上传素材、公开作品图片和 `mock_generated` 资源都不进入精选。从项目素材把同一个生成素材再次拖入画布时,只是新增图层实例或复用同源同媒体项目资源,不应额外生成新的精选候选;历史上已经产生的同源同媒体副本也需要在精选读取时跳过。公开 BFF 必须返回资源作者公开展示字段:优先 `authorDisplayName` / `display_name`,没有展示名时兜底 `authorPublicUserCode` / 陶泥号;前端展示绝不能兜底到内部 `ownerUserId` / `user_id`。若现有数据缺少提示词、作者公开标识或成本字段,v1 显示保守占位,不伪造内容。
Tab
@@ -100,7 +100,7 @@ Tab
数据来源:
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_project_resource` 快照,快照包含 `ownerUserId` 作为作者展示兜底;前端只保留 `sourceType="generated"``publicShowcaseEnabled !== false` 的画布生成资源;若某条资源通过 `sourceResourceId` 指回同一 owner 的源资源且媒体引用相同,则视为拖拽画布副本,不进入精选。
- 读取公开 BFF `GET /api/editor/showcase/resources` 返回的全站公开 `editor_project_resource` 快照,快照包含 `authorDisplayName` / `display_name``authorPublicUserCode` / 陶泥号用于作者展示;前端只保留 `sourceType="generated"``publicShowcaseEnabled !== false` 的画布生成资源;若某条资源通过 `sourceResourceId` 指回同一 owner 的源资源且媒体引用相同,则视为拖拽画布副本,不进入精选。作者展示优先展示名,没有展示名时展示陶泥号,绝不能展示内部 `ownerUserId` / `user_id`
- 素材包、角色、UI、音乐、音效、视频和美宣都必须来自已存在项目资源;素材库里尚未放入任何项目的账号级素材不展示。
- 未登录用户也可读取公开精选;当没有公开画布生成资源时,不再用公开作品图片补充,只显示简洁空态。
- 上传素材、公开作品图片、mock 资源和假组合都不进入精选。
@@ -65,6 +65,7 @@ use crate::{
platform_errors::map_oss_error,
request_context::RequestContext,
state::AppState,
work_author::{ORPHAN_WORK_AUTHOR_PUBLIC_USER_CODE, ORPHAN_WORK_OWNER_USER_ID},
};
pub(crate) const EDITOR_PROJECT_ID_PREFIX: &str = "editor-project-";
@@ -478,6 +479,10 @@ pub struct EditorProjectResourcePayload {
resource_id: String,
project_id: String,
owner_user_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
author_display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
author_public_user_code: Option<String>,
image_src: String,
object_key: Option<String>,
asset_object_id: Option<String>,
@@ -616,7 +621,7 @@ pub async fn list_public_editor_project_resources(
.map_err(map_editor_project_error)?;
let resources = resources
.into_iter()
.map(editor_project_resource_payload_from_record)
.map(|record| editor_project_resource_public_payload_from_record(&state, record))
.collect();
Ok(json_success_body(
@@ -2432,6 +2437,8 @@ pub(crate) fn editor_project_resource_payload_from_record(
resource_id: record.resource_id,
project_id: record.project_id,
owner_user_id: record.owner_user_id,
author_display_name: None,
author_public_user_code: None,
image_src,
object_key: record.object_key,
asset_object_id: record.asset_object_id,
@@ -2454,6 +2461,38 @@ pub(crate) fn editor_project_resource_payload_from_record(
}
}
fn editor_project_resource_public_payload_from_record(
state: &AppState,
record: EditorProjectResourceRecord,
) -> EditorProjectResourcePayload {
let mut payload = editor_project_resource_payload_from_record(record);
let (display_name, public_user_code) =
resolve_editor_project_resource_public_author(state, payload.owner_user_id.as_str());
payload.author_display_name = display_name;
payload.author_public_user_code = public_user_code;
payload
}
fn resolve_editor_project_resource_public_author(
state: &AppState,
owner_user_id: &str,
) -> (Option<String>, Option<String>) {
let Some(owner_user_id) = normalize_optional_string(Some(owner_user_id.to_string())) else {
return (None, Some(ORPHAN_WORK_AUTHOR_PUBLIC_USER_CODE.to_string()));
};
if owner_user_id == ORPHAN_WORK_OWNER_USER_ID {
return (None, Some(ORPHAN_WORK_AUTHOR_PUBLIC_USER_CODE.to_string()));
}
match state.auth_user_service().get_user_by_id(&owner_user_id) {
Ok(Some(user)) => (
normalize_optional_string(Some(user.display_name)),
normalize_optional_string(Some(user.public_user_code)),
),
Ok(None) | Err(_) => (None, Some(ORPHAN_WORK_AUTHOR_PUBLIC_USER_CODE.to_string())),
}
}
pub(crate) fn editor_asset_library_payload_from_record(
record: EditorAssetLibraryRecord,
) -> EditorAssetLibraryPayload {
@@ -3867,12 +3906,91 @@ mod tests {
assert_eq!(value["ownerUserId"], json!("user-author-1"));
}
#[test]
fn editor_project_resource_public_payload_uses_display_author_fields() {
let state = AppState::new(AppConfig::default()).expect("state should build");
state
.auth_user_service()
.ensure_orphan_work_owner_user("user-author-1", "author-1", "作者昵称", "SY-00000042")
.expect("author should be inserted");
let payload = editor_project_resource_public_payload_from_record(
&state,
EditorProjectResourceRecord {
resource_id: "resource-1".to_string(),
project_id: "project-1".to_string(),
owner_user_id: "user-author-1".to_string(),
asset_object_id: None,
image_src: "/generated-character-drafts/editor/spec.png".to_string(),
object_key: None,
width: 512,
height: 512,
source_type: "generated".to_string(),
prompt: None,
actual_prompt: None,
model: None,
provider: None,
task_id: None,
source_resource_id: None,
asset_kind: None,
generation_inputs: None,
public_showcase_enabled: true,
created_at: "2026-06-23T00:00:00.000Z".to_string(),
updated_at: "2026-06-23T00:00:00.000Z".to_string(),
},
);
let value = serde_json::to_value(payload).expect("payload should serialize");
assert_eq!(value["ownerUserId"], json!("user-author-1"));
assert_eq!(value["authorDisplayName"], json!("作者昵称"));
assert_eq!(value["authorPublicUserCode"], json!("SY-00000042"));
}
#[test]
fn editor_project_resource_public_payload_falls_back_to_public_user_code() {
let state = AppState::new(AppConfig::default()).expect("state should build");
let payload = editor_project_resource_public_payload_from_record(
&state,
EditorProjectResourceRecord {
resource_id: "resource-1".to_string(),
project_id: "project-1".to_string(),
owner_user_id: "missing-user".to_string(),
asset_object_id: None,
image_src: "/generated-character-drafts/editor/spec.png".to_string(),
object_key: None,
width: 512,
height: 512,
source_type: "generated".to_string(),
prompt: None,
actual_prompt: None,
model: None,
provider: None,
task_id: None,
source_resource_id: None,
asset_kind: None,
generation_inputs: None,
public_showcase_enabled: true,
created_at: "2026-06-23T00:00:00.000Z".to_string(),
updated_at: "2026-06-23T00:00:00.000Z".to_string(),
},
);
let value = serde_json::to_value(payload).expect("payload should serialize");
assert_eq!(value["ownerUserId"], json!("missing-user"));
assert!(value.get("authorDisplayName").is_none());
assert_eq!(value["authorPublicUserCode"], json!("SY-00000000"));
}
#[test]
fn editor_payload_media_sanitizer_replaces_inline_layer_src_from_resource() {
let resources = vec![EditorProjectResourcePayload {
resource_id: "resource-1".to_string(),
project_id: "project-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: None,
author_public_user_code: None,
image_src: "/generated-character-drafts/editor/spec.png".to_string(),
object_key: Some("generated-character-drafts/editor/spec.png".to_string()),
asset_object_id: Some("asset-object-1".to_string()),
@@ -3945,6 +4063,8 @@ mod tests {
resource_id: "resource-spec".to_string(),
project_id: "project-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: None,
author_public_user_code: None,
image_src: "/generated-character-drafts/editor/spec.png".to_string(),
object_key: Some("generated-character-drafts/editor/spec.png".to_string()),
asset_object_id: Some("asset-object-spec".to_string()),
@@ -4013,6 +4133,8 @@ mod tests {
resource_id: "editor-resource-character".to_string(),
project_id: "editor-project-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: None,
author_public_user_code: None,
image_src: "/generated-character-drafts/editor/character.png".to_string(),
object_key: Some("generated-character-drafts/editor/character.png".to_string()),
asset_object_id: Some("asset-object-character".to_string()),
@@ -4502,6 +4624,8 @@ mod tests {
resource_id: "editor-resource-character".to_string(),
project_id: "editor-project-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: None,
author_public_user_code: None,
image_src: "data:image/png;base64,character".to_string(),
object_key: Some("generated-character-drafts/editor/character.png".to_string()),
asset_object_id: Some("asset-object-1".to_string()),
@@ -4603,6 +4727,8 @@ mod tests {
resource_id: "editor-resource-character".to_string(),
project_id: "editor-project-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: None,
author_public_user_code: None,
image_src: "data:image/png;base64,character".to_string(),
object_key: None,
asset_object_id: None,
@@ -4685,6 +4811,8 @@ mod tests {
resource_id: "editor-resource-character".to_string(),
project_id: "editor-project-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: None,
author_public_user_code: None,
image_src: "data:image/png;base64,character".to_string(),
object_key: None,
asset_object_id: None,
@@ -4808,6 +4936,8 @@ mod tests {
resource_id: "editor-resource-character".to_string(),
project_id: "editor-project-1".to_string(),
owner_user_id: "user-1".to_string(),
author_display_name: None,
author_public_user_code: None,
image_src: "data:image/png;base64,character".to_string(),
object_key: None,
asset_object_id: None,
@@ -235,7 +235,9 @@ describe('CreationLandingView', () => {
provider: 'provider',
taskId: 'task-1',
assetKind: 'character',
authorName: '创作者A',
ownerUserId: 'user-internal-author',
authorDisplayName: '创作者A',
authorPublicUserCode: 'SY-00000042',
priceMudPoints: 20,
},
]);
@@ -249,6 +251,7 @@ describe('CreationLandingView', () => {
expect(card).toBeTruthy();
expect(within(card as HTMLElement).getByText('character hero prompt')).toBeTruthy();
expect(within(card as HTMLElement).getByText('创作者A')).toBeTruthy();
expect(within(card as HTMLElement).queryByText('user-internal-author')).toBeNull();
expect(within(card as HTMLElement).getByText('20泥点')).toBeTruthy();
});
@@ -270,7 +273,8 @@ describe('CreationLandingView', () => {
provider: 'provider',
taskId: 'task-1',
assetKind: 'character',
authorName: '创作者A',
authorDisplayName: '创作者A',
authorPublicUserCode: 'SY-00000042',
priceMudPoints: 20,
},
{
@@ -76,21 +76,41 @@ describe('creationShowcaseModel', () => {
expect(items[0]?.label).toBe('公开素材');
});
it('uses the public project resource owner user id as showcase author fallback', () => {
it('uses public author fields without falling back to internal owner user id', () => {
const items = buildCreationShowcaseItems({
activeTab: 'characters',
projectResources: [
createResource({
resourceId: 'owner-resource',
label: '作者兜底素材',
resourceId: 'display-author-resource',
label: '展示名作者素材',
assetKind: 'character',
ownerUserId: 'user-author-1',
authorDisplayName: '作者昵称',
authorPublicUserCode: 'SY-00000001',
}),
createResource({
resourceId: 'code-author-resource',
label: '陶泥号作者素材',
assetKind: 'character',
ownerUserId: 'user-author-2',
authorPublicUserCode: 'SY-00000002',
}),
createResource({
resourceId: 'owner-only-resource',
label: '内部id素材',
assetKind: 'character',
ownerUserId: 'user-author-3',
authorName: 'user-author-3',
ownerName: 'user-author-3',
}),
],
});
expect(items).toHaveLength(1);
expect(items[0]?.author).toBe('user-author-1');
expect(items.map((item) => item.author)).toEqual([
'作者昵称',
'SY-00000002',
'-',
]);
});
it('deduplicates canvas copies that point back to the same generated resource', () => {
@@ -137,7 +157,7 @@ describe('creationShowcaseModel', () => {
assetKind: 'spec',
prompt: '规范提示词',
priceMudPoints: 5,
authorName: '创作者A',
authorDisplayName: '创作者A',
}),
createResource({
resourceId: 'character-1',
@@ -62,14 +62,7 @@ const SHOWCASE_COST_KEYS = [
'mudPointCost',
] as const;
const SHOWCASE_AUTHOR_KEYS = [
'authorName',
'creatorName',
'ownerLabel',
'ownerName',
'authorDisplayName',
'ownerUserId',
] as const;
const SHOWCASE_AUTHOR_KEYS = ['authorDisplayName', 'authorPublicUserCode'] as const;
export const SHOWCASE_TABS: Array<{ id: ShowcaseTabId; label: string }> = [
{ id: 'packs', label: '素材包' },
+2
View File
@@ -3771,6 +3771,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
}
.creation-landing__asset-waterfall {
width: 100%;
min-width: 0;
column-count: 3;
column-gap: 0.92rem;
}
@@ -221,6 +221,8 @@ describe('editorProjectClient', () => {
resourceId: 'resource-public-1',
projectId: 'editor-project-1',
ownerUserId: 'user-public-author',
authorDisplayName: '公开作者',
authorPublicUserCode: 'SY-00000042',
label: '公开角色素材',
imageSrc: '/generated-editor-assets/public.png',
width: 512,
@@ -236,6 +238,8 @@ describe('editorProjectClient', () => {
expect(resources).toHaveLength(1);
expect(resources[0]?.resourceId).toBe('resource-public-1');
expect(resources[0]?.ownerUserId).toBe('user-public-author');
expect(resources[0]?.authorDisplayName).toBe('公开作者');
expect(resources[0]?.authorPublicUserCode).toBe('SY-00000042');
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/showcase/resources',
{ method: 'GET' },
@@ -60,6 +60,8 @@ export type EditorProjectResourceSnapshot = {
resourceId: string;
projectId: string;
ownerUserId?: string | null;
authorDisplayName?: string | null;
authorPublicUserCode?: string | null;
imageSrc: string;
objectKey?: string | null;
assetObjectId?: string | null;