修复创作页精选作者和瀑布流宽度
公开精选接口返回作者展示名和陶泥号 前端精选作者只使用公开展示字段,不再兜底内部用户 ID 约束精选瀑布流多列容器宽度,避免 release 桌面端横向撑宽 补充前后端回归测试和项目记忆文档
This commit is contained in:
@@ -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: '素材包' },
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user