显示精选审核提交失败原因
统一素材精选审核提交资格判断 在素材菜单显示不可提交和后端拒绝原因 将编辑器业务拒绝映射为 400 并保留中文原因 补充精选审核原因展示测试和文档口径
This commit is contained in:
@@ -116,7 +116,7 @@ Tab:
|
||||
- 全部、角色、UI、音乐、音效、视频和美宣都必须来自用户素材库中已提交并通过审核的生成素材;不要求素材当前仍保留在某个项目资源中,已通过审核的快照在原素材删除后仍可保留公开展示。
|
||||
- 未登录用户也可读取公开精选;当没有公开画布生成资源时,不再用公开作品图片补充,只显示简洁空态。
|
||||
- 上传素材、公开作品图片、mock 资源和假组合都不进入精选。
|
||||
- 任意画布左侧素材列表中,单个素材右键打开素材菜单;原外置删除按钮移入该菜单,以文字 `删除` 展示。生成素材菜单内提供 `提交精选审核`,调用 `POST /api/editor/assets/{assetId}/showcase-submissions` 后进入 `pending` 状态;已提交、已通过或已拒绝的素材显示对应状态,不再显示默认打开的公开勾选项。
|
||||
- 任意画布左侧素材列表中,单个素材右键打开素材菜单;原外置删除按钮移入该菜单,以文字 `删除` 展示。生成素材菜单内提供 `提交精选审核`,调用 `POST /api/editor/assets/{assetId}/showcase-submissions` 后进入 `pending` 状态;已提交、已通过或已拒绝的素材显示对应状态,不再显示默认打开的公开勾选项。素材不可提交或后端拒绝提交时,菜单必须显示具体原因,例如非生成素材、素材尚未保存、没有媒体内容或审核未通过不能重复提交。
|
||||
- 后台新增纯素材查询页,读取账号级 `editor_asset` 中 `sourceType="generated"` 的素材,用于查看所有用户生成素材。该页只提供时间、用户 ID 和关键词查询,以及缩略图详情、提示词全文、生成成本、作者展示名和陶泥号查看;不提供分类筛选,也不承载审核、返还、展示开关或活动卡配置操作。
|
||||
- 不为了填满展示区创建假素材、假作者、假泥点成本或假组合关系。
|
||||
- 暂无真实数据的 Tab 保留 Tab 入口,但内容区显示简洁空态。
|
||||
|
||||
@@ -5088,6 +5088,11 @@ pub(crate) fn map_editor_project_error(error: SpacetimeClientError) -> AppError
|
||||
"message": message,
|
||||
}))
|
||||
}
|
||||
SpacetimeClientError::Procedure(message) => AppError::from_status(StatusCode::BAD_REQUEST)
|
||||
.with_details(json!({
|
||||
"provider": "editor-project",
|
||||
"message": message,
|
||||
})),
|
||||
SpacetimeClientError::Runtime(message) => AppError::from_status(StatusCode::BAD_REQUEST)
|
||||
.with_details(json!({
|
||||
"provider": "editor-project",
|
||||
@@ -5121,6 +5126,19 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::config::AppConfig;
|
||||
|
||||
#[test]
|
||||
fn editor_project_procedure_errors_preserve_business_reason_as_bad_request() {
|
||||
let error = map_editor_project_error(SpacetimeClientError::Procedure(
|
||||
"只有生成素材可以提交精选审核".to_string(),
|
||||
));
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST);
|
||||
assert_eq!(
|
||||
error.details().and_then(|details| details.get("message")),
|
||||
Some(&json!("只有生成素材可以提交精选审核")),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_editor_canvas_generation_completion(
|
||||
layers: Value,
|
||||
completion: &EditorCanvasGenerationCompletionRequest,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
renameAssetFolderById,
|
||||
replaceLocalAssetFolder,
|
||||
resolveAllAssetSelection,
|
||||
resolveAssetShowcaseSubmissionState,
|
||||
resolveAssetFolderIdFromPoint,
|
||||
resolveDefaultAssetFolder,
|
||||
resolvePreferredAssetUploadFolder,
|
||||
@@ -89,6 +90,58 @@ describe('ImageCanvasAssetLibraryModel', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('explains why assets cannot be submitted to showcase review', () => {
|
||||
expect(
|
||||
resolveAssetShowcaseSubmissionState(createAsset()).reason,
|
||||
).toBe('只有生成素材可以提交精选审核');
|
||||
expect(
|
||||
resolveAssetShowcaseSubmissionState(
|
||||
createAsset({ sourceType: 'generated', persisted: false }),
|
||||
).reason,
|
||||
).toBe('素材保存完成后才能提交精选审核');
|
||||
expect(
|
||||
resolveAssetShowcaseSubmissionState(
|
||||
createAsset({ sourceType: 'generated', src: '' }),
|
||||
).reason,
|
||||
).toBe('没有媒体内容的素材不能提交精选审核');
|
||||
expect(
|
||||
resolveAssetShowcaseSubmissionState(
|
||||
createAsset({
|
||||
sourceType: 'generated',
|
||||
showcaseReviewStatus: 'rejected',
|
||||
}),
|
||||
),
|
||||
).toMatchObject({
|
||||
canSubmit: false,
|
||||
label: '精选未通过',
|
||||
reason: '该素材审核未通过,不能重复提交',
|
||||
});
|
||||
});
|
||||
|
||||
it('allows persisted generated assets to be submitted and keeps prior backend errors visible', () => {
|
||||
expect(
|
||||
resolveAssetShowcaseSubmissionState(
|
||||
createAsset({ sourceType: 'generated' }),
|
||||
),
|
||||
).toMatchObject({
|
||||
canSubmit: true,
|
||||
label: '提交精选审核',
|
||||
reason: null,
|
||||
});
|
||||
expect(
|
||||
resolveAssetShowcaseSubmissionState(
|
||||
createAsset({
|
||||
sourceType: 'generated',
|
||||
showcaseSubmitError: '只有生成素材可以提交精选审核',
|
||||
}),
|
||||
),
|
||||
).toMatchObject({
|
||||
canSubmit: true,
|
||||
label: '提交精选审核',
|
||||
reason: '只有生成素材可以提交精选审核',
|
||||
});
|
||||
});
|
||||
|
||||
it('filters grouped assets by folder and asset search text', () => {
|
||||
const groupedAssets = groupAssetsByFolder(
|
||||
[
|
||||
|
||||
@@ -288,6 +288,82 @@ export function getSelectableAssets(assets: EditorAsset[]) {
|
||||
return assets.filter((asset) => asset.sourceKind === 'uploaded');
|
||||
}
|
||||
|
||||
export type AssetShowcaseSubmissionState = {
|
||||
canSubmit: boolean;
|
||||
label: string;
|
||||
reason: string | null;
|
||||
};
|
||||
|
||||
function normalizeAssetShowcaseReason(value: string | null | undefined) {
|
||||
return value?.trim() || null;
|
||||
}
|
||||
|
||||
export function resolveAssetShowcaseSubmissionState(
|
||||
asset: EditorAsset,
|
||||
): AssetShowcaseSubmissionState {
|
||||
const reviewStatus = asset.showcaseReviewStatus ?? null;
|
||||
|
||||
if (reviewStatus === 'pending') {
|
||||
return {
|
||||
canSubmit: false,
|
||||
label: '精选审核中',
|
||||
reason: '素材正在审核中,请等待审核结果',
|
||||
};
|
||||
}
|
||||
|
||||
if (reviewStatus === 'approved') {
|
||||
return {
|
||||
canSubmit: false,
|
||||
label:
|
||||
asset.showcaseDisplayEnabled === false
|
||||
? '已入选精选,暂未展示'
|
||||
: '已入选精选',
|
||||
reason:
|
||||
asset.showcaseDisplayEnabled === false
|
||||
? '素材已通过审核,运营后台开启展示后才会出现在前台'
|
||||
: '素材已经入选精选',
|
||||
};
|
||||
}
|
||||
|
||||
if (reviewStatus === 'rejected') {
|
||||
return {
|
||||
canSubmit: false,
|
||||
label: '精选未通过',
|
||||
reason: '该素材审核未通过,不能重复提交',
|
||||
};
|
||||
}
|
||||
|
||||
if (asset.sourceType !== 'generated') {
|
||||
return {
|
||||
canSubmit: false,
|
||||
label: '提交精选审核',
|
||||
reason: '只有生成素材可以提交精选审核',
|
||||
};
|
||||
}
|
||||
|
||||
if (!asset.persisted) {
|
||||
return {
|
||||
canSubmit: false,
|
||||
label: '提交精选审核',
|
||||
reason: '素材保存完成后才能提交精选审核',
|
||||
};
|
||||
}
|
||||
|
||||
if (!asset.src.trim()) {
|
||||
return {
|
||||
canSubmit: false,
|
||||
label: '提交精选审核',
|
||||
reason: '没有媒体内容的素材不能提交精选审核',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
canSubmit: true,
|
||||
label: '提交精选审核',
|
||||
reason: normalizeAssetShowcaseReason(asset.showcaseSubmitError),
|
||||
};
|
||||
}
|
||||
|
||||
export function areAllSelectableAssetsSelected(
|
||||
selectableAssets: EditorAsset[],
|
||||
selectedAssetIds: Set<string>,
|
||||
|
||||
@@ -371,6 +371,35 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
name: /提交精选审核/u,
|
||||
});
|
||||
expect((submitMenuItem as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(screen.getByText('只有生成素材可以提交精选审核')).toBeTruthy();
|
||||
|
||||
fireEvent.click(submitMenuItem);
|
||||
expect(submitAssetToShowcase).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows why generated assets cannot be submitted before they are saved', () => {
|
||||
const submitAssetToShowcase = vi.fn();
|
||||
renderAssetRow({
|
||||
asset: createAsset({
|
||||
sourceType: 'generated',
|
||||
persisted: false,
|
||||
}),
|
||||
submitAssetToShowcase,
|
||||
});
|
||||
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
|
||||
const submitMenuItem = screen.getByRole('menuitem', {
|
||||
name: /提交精选审核/u,
|
||||
});
|
||||
expect((submitMenuItem as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(screen.getByText('素材保存完成后才能提交精选审核')).toBeTruthy();
|
||||
|
||||
fireEvent.click(submitMenuItem);
|
||||
expect(submitAssetToShowcase).not.toHaveBeenCalled();
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
getDraggedAssetId,
|
||||
hasDataTransferType,
|
||||
} from './ImageCanvasEditorModel';
|
||||
import { resolveAssetShowcaseSubmissionState } from './ImageCanvasAssetLibraryModel';
|
||||
import type {
|
||||
AssetPointerDragState,
|
||||
EditorAsset,
|
||||
@@ -85,20 +86,10 @@ export function ImageCanvasAssetRowView({
|
||||
const isUploadingAsset = asset.uploadStatus === 'uploading';
|
||||
const isFailedUpload = asset.uploadStatus === 'failed';
|
||||
const showcaseReviewStatus = asset.showcaseReviewStatus ?? null;
|
||||
const canSubmitShowcaseAsset =
|
||||
asset.sourceType === 'generated' &&
|
||||
asset.persisted &&
|
||||
!showcaseReviewStatus;
|
||||
const showcaseMenuLabel =
|
||||
showcaseReviewStatus === 'pending'
|
||||
? '精选审核中'
|
||||
: showcaseReviewStatus === 'approved'
|
||||
? asset.showcaseDisplayEnabled === false
|
||||
? '已入选精选,暂未展示'
|
||||
: '已入选精选'
|
||||
: showcaseReviewStatus === 'rejected'
|
||||
? '精选未通过'
|
||||
: '提交精选审核';
|
||||
const showcaseSubmissionState = resolveAssetShowcaseSubmissionState(asset);
|
||||
const canSubmitShowcaseAsset = showcaseSubmissionState.canSubmit;
|
||||
const showcaseMenuLabel = showcaseSubmissionState.label;
|
||||
const showcaseMenuReason = showcaseSubmissionState.reason;
|
||||
|
||||
const closeMenu = useCallback(() => {
|
||||
setMenuPosition(null);
|
||||
@@ -460,16 +451,21 @@ export function ImageCanvasAssetRowView({
|
||||
role="menuitem"
|
||||
className="image-canvas-editor__asset-context-menu-toggle"
|
||||
disabled={!canSubmitShowcaseAsset}
|
||||
title={showcaseMenuReason ?? showcaseMenuLabel}
|
||||
onClick={() => {
|
||||
if (!canSubmitShowcaseAsset) {
|
||||
return;
|
||||
}
|
||||
submitAssetToShowcase(asset);
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
<span className="image-canvas-editor__asset-context-menu-label">
|
||||
{showcaseMenuLabel}
|
||||
<span>{showcaseMenuLabel}</span>
|
||||
{showcaseMenuReason ? (
|
||||
<span className="image-canvas-editor__asset-context-menu-reason">
|
||||
{showcaseMenuReason}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{showcaseReviewStatus === 'approved' ? (
|
||||
<span
|
||||
|
||||
@@ -54,6 +54,7 @@ export type EditorAsset = {
|
||||
showcaseReviewStatus?: string | null;
|
||||
showcaseDisplayEnabled?: boolean | null;
|
||||
showcaseLikeCount?: number | null;
|
||||
showcaseSubmitError?: string | null;
|
||||
assetKind?: CanvasAssetKind | null;
|
||||
generationInputs?: CanvasGenerationInputs | null;
|
||||
durationSeconds?: number;
|
||||
|
||||
@@ -117,6 +117,11 @@ function AssetLibraryHarness({
|
||||
)
|
||||
.join('|')}
|
||||
</span>
|
||||
<span data-testid="asset-submit-errors">
|
||||
{assetLibrary.assets
|
||||
.map((asset) => `${asset.id}:${asset.showcaseSubmitError ?? ''}`)
|
||||
.join('|')}
|
||||
</span>
|
||||
<span data-testid="active-upload-folder">
|
||||
{assetLibrary.activeUploadFolderId}
|
||||
</span>
|
||||
@@ -537,4 +542,48 @@ describe('useImageCanvasAssetLibrary', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the backend showcase rejection reason on the asset', async () => {
|
||||
submitEditorAssetShowcaseMock.mockRejectedValueOnce(
|
||||
new ApiClientError({
|
||||
message: '只有生成素材可以提交精选审核',
|
||||
status: 400,
|
||||
code: 'BAD_REQUEST',
|
||||
}),
|
||||
);
|
||||
loadEditorAssetLibraryMock.mockResolvedValueOnce({
|
||||
folders: [
|
||||
{
|
||||
folderId: 'project',
|
||||
label: '项目素材',
|
||||
sortOrder: 0,
|
||||
collapsed: false,
|
||||
systemDefault: true,
|
||||
},
|
||||
],
|
||||
assets: [
|
||||
createAssetSnapshot({
|
||||
assetId: 'asset-a',
|
||||
sourceType: 'generated',
|
||||
publicShowcaseEnabled: true,
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
render(<AssetLibraryHarness />);
|
||||
|
||||
await screen.findByText('素材A');
|
||||
act(() =>
|
||||
screen.getByRole('button', { name: 'submit asset to showcase' }).click(),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('asset-submit-errors').textContent).toBe(
|
||||
'asset-a:只有生成素材可以提交精选审核',
|
||||
);
|
||||
});
|
||||
expect(screen.getByTestId('assets').textContent).toBe(
|
||||
'asset-a:素材A:project:true',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
renameAssetById,
|
||||
renameAssetFolderById,
|
||||
replaceLocalAssetFolder,
|
||||
resolveAssetShowcaseSubmissionState,
|
||||
resolveAllAssetSelection,
|
||||
resolveDefaultAssetFolder,
|
||||
resolveAssetFolderIdFromPoint,
|
||||
@@ -63,6 +64,16 @@ function isEditorAuthError(error: unknown) {
|
||||
);
|
||||
}
|
||||
|
||||
function formatShowcaseSubmitError(error: unknown) {
|
||||
if (error instanceof ApiClientError && error.message.trim()) {
|
||||
return error.message.trim();
|
||||
}
|
||||
if (error instanceof Error && error.message.trim()) {
|
||||
return error.message.trim();
|
||||
}
|
||||
return '提交精选审核失败,请稍后重试';
|
||||
}
|
||||
|
||||
const ASSET_FOLDER_SELECTOR = '[data-asset-folder-id]';
|
||||
const ASSET_ITEM_SELECTOR = '[data-asset-id]';
|
||||
const ASSET_MARQUEE_BLOCKED_TARGET_SELECTOR =
|
||||
@@ -356,11 +367,7 @@ export function useImageCanvasAssetLibrary({
|
||||
|
||||
const submitAssetToShowcase = useCallback(
|
||||
(asset: EditorAsset) => {
|
||||
if (
|
||||
asset.sourceType !== 'generated' ||
|
||||
!asset.persisted ||
|
||||
asset.showcaseReviewStatus
|
||||
) {
|
||||
if (!resolveAssetShowcaseSubmissionState(asset).canSubmit) {
|
||||
return;
|
||||
}
|
||||
const previousAsset = asset;
|
||||
@@ -372,6 +379,7 @@ export function useImageCanvasAssetLibrary({
|
||||
publicShowcaseEnabled: false,
|
||||
showcaseReviewStatus: 'pending',
|
||||
showcaseDisplayEnabled: false,
|
||||
showcaseSubmitError: undefined,
|
||||
}
|
||||
: currentAsset,
|
||||
),
|
||||
@@ -391,15 +399,22 @@ export function useImageCanvasAssetLibrary({
|
||||
generationCostMudPoints:
|
||||
showcaseAsset.generationCostMudPoints ??
|
||||
currentAsset.generationCostMudPoints,
|
||||
showcaseSubmitError: undefined,
|
||||
}
|
||||
: currentAsset,
|
||||
),
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
.catch((error: unknown) => {
|
||||
const showcaseSubmitError = formatShowcaseSubmitError(error);
|
||||
setAssets((currentAssets) =>
|
||||
currentAssets.map((currentAsset) =>
|
||||
currentAsset.id === asset.id ? previousAsset : currentAsset,
|
||||
currentAsset.id === asset.id
|
||||
? {
|
||||
...previousAsset,
|
||||
showcaseSubmitError,
|
||||
}
|
||||
: currentAsset,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -8399,14 +8399,34 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
|
||||
.image-canvas-editor__asset-context-menu
|
||||
button.image-canvas-editor__asset-context-menu-toggle {
|
||||
min-height: 2.2rem;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding-top: 0.28rem;
|
||||
padding-bottom: 0.28rem;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.image-canvas-editor__asset-context-menu-label {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
gap: 0.12rem;
|
||||
line-height: 1.15;
|
||||
text-align: left;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.image-canvas-editor__asset-context-menu-reason {
|
||||
color: #64748b;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 680;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.image-canvas-editor__asset-context-menu button:disabled
|
||||
.image-canvas-editor__asset-context-menu-reason {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.image-canvas-editor__asset-context-menu-check {
|
||||
|
||||
Reference in New Issue
Block a user