From f20f4e0f8e0b2a1ca603cfb4b9ff2b45f312071e Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 27 Jun 2026 20:01:17 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=88=9B=E4=BD=9C=E9=A1=B5?= =?UTF-8?q?=E7=B2=BE=E9=80=89=E4=BD=9C=E8=80=85=E5=92=8C=E7=80=91=E5=B8=83?= =?UTF-8?q?=E6=B5=81=E5=AE=BD=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 公开精选接口返回作者展示名和陶泥号 前端精选作者只使用公开展示字段,不再兜底内部用户 ID 约束精选瀑布流多列容器宽度,避免 release 桌面端横向撑宽 补充前后端回归测试和项目记忆文档 --- docs/project-memory/shared-memory/pitfalls.md | 18 ++- ...】server-rs与SpacetimeDB数据契约-2026-05-15.md | 2 +- ...作】创作主页与项目入口改版计划-2026-06-18.md | 4 +- .../crates/api-server/src/editor_project.rs | 132 +++++++++++++++++- .../CreationLandingView.test.tsx | 8 +- .../creationShowcaseModel.test.ts | 32 ++++- .../creation-home/creationShowcaseModel.ts | 9 +- src/index.css | 2 + .../image-editor/editorProjectClient.test.ts | 4 + .../image-editor/editorProjectClient.ts | 2 + 10 files changed, 188 insertions(+), 25 deletions(-) diff --git a/docs/project-memory/shared-memory/pitfalls.md b/docs/project-memory/shared-memory/pitfalls.md index ddea4931e..e12ac6eb7 100644 --- a/docs/project-memory/shared-memory/pitfalls.md +++ b/docs/project-memory/shared-memory/pitfalls.md @@ -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`。 + ## 画板外部生成排队超时不是失败 - 现象:画板发起付费图片生成后,前端弹出 `生成任务仍在队列中,请稍后刷新画布查看结果`,但后端任务仍在队列或执行中,后续可能正常完成。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 3c23c8a43..1153d4391 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -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` diff --git a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md index 347aca58c..b89de6f1a 100644 --- a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md +++ b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md @@ -63,7 +63,7 @@ `陶泥儿精选` 是页面底部的全站公开画布生成素材瀑布流,不承载玩法入口列表。瀑布流卡片按真实素材宽高设置预览比例,同一行允许出现不同高度卡片,不使用固定等高网格。创作入口配置仍继续来自 `/api/creation-entry/config`,供旧创作入口和具体 `/creation/` 工作台使用,但不作为本页精选区内容。 -精选内容只使用 `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 资源和假组合都不进入精选。 diff --git a/server-rs/crates/api-server/src/editor_project.rs b/server-rs/crates/api-server/src/editor_project.rs index 314d83be2..8f24996b1 100644 --- a/server-rs/crates/api-server/src/editor_project.rs +++ b/server-rs/crates/api-server/src/editor_project.rs @@ -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, + #[serde(skip_serializing_if = "Option::is_none")] + author_public_user_code: Option, image_src: String, object_key: Option, asset_object_id: Option, @@ -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, Option) { + 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, diff --git a/src/components/creation-home/CreationLandingView.test.tsx b/src/components/creation-home/CreationLandingView.test.tsx index 40eda8514..17b3c40fa 100644 --- a/src/components/creation-home/CreationLandingView.test.tsx +++ b/src/components/creation-home/CreationLandingView.test.tsx @@ -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, }, { diff --git a/src/components/creation-home/creationShowcaseModel.test.ts b/src/components/creation-home/creationShowcaseModel.test.ts index c4aee3a37..9d4586eba 100644 --- a/src/components/creation-home/creationShowcaseModel.test.ts +++ b/src/components/creation-home/creationShowcaseModel.test.ts @@ -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', diff --git a/src/components/creation-home/creationShowcaseModel.ts b/src/components/creation-home/creationShowcaseModel.ts index e7ace3a37..7585161c5 100644 --- a/src/components/creation-home/creationShowcaseModel.ts +++ b/src/components/creation-home/creationShowcaseModel.ts @@ -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: '素材包' }, diff --git a/src/index.css b/src/index.css index f23a69323..fe850a37e 100644 --- a/src/index.css +++ b/src/index.css @@ -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; } diff --git a/src/services/image-editor/editorProjectClient.test.ts b/src/services/image-editor/editorProjectClient.test.ts index 857bff53e..b1bbbcb33 100644 --- a/src/services/image-editor/editorProjectClient.test.ts +++ b/src/services/image-editor/editorProjectClient.test.ts @@ -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' }, diff --git a/src/services/image-editor/editorProjectClient.ts b/src/services/image-editor/editorProjectClient.ts index c280a4495..4a323a6da 100644 --- a/src/services/image-editor/editorProjectClient.ts +++ b/src/services/image-editor/editorProjectClient.ts @@ -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;