统一音频素材封面缩略图
新增音频素材统一封面图。 素材库、创作主页精选和后台缩略图改为使用音频封面。 补充音频封面展示回归测试。
This commit is contained in:
@@ -1,14 +1,20 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {fireEvent, render, screen, waitFor, within} from '@testing-library/react';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getAdminAssetReadUrl,
|
||||
listAdminEditorAssets,
|
||||
} from '../api/adminApiClient';
|
||||
import type {AdminEditorAssetPayload} from '../api/adminApiTypes';
|
||||
import {AdminEditorAssetQueryPage} from './AdminEditorAssetQueryPage';
|
||||
import type { AdminEditorAssetPayload } from '../api/adminApiTypes';
|
||||
import { AdminEditorAssetQueryPage } from './AdminEditorAssetQueryPage';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
getAdminAssetReadUrl: vi.fn(),
|
||||
@@ -34,7 +40,7 @@ const generatedAsset: AdminEditorAssetPayload = {
|
||||
provider: 'character',
|
||||
taskId: 'task-1',
|
||||
assetKind: 'character',
|
||||
generationInputs: {style: 'clay'},
|
||||
generationInputs: { style: 'clay' },
|
||||
sourceResourceId: 'resource-1',
|
||||
thumbnailSrc: null,
|
||||
generationCostMudPoints: 12,
|
||||
@@ -60,10 +66,7 @@ beforeEach(() => {
|
||||
|
||||
test('后台素材查询展示作者昵称和陶泥号', async () => {
|
||||
render(
|
||||
<AdminEditorAssetQueryPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('作者昵称')).toBeTruthy();
|
||||
@@ -73,25 +76,22 @@ test('后台素材查询展示作者昵称和陶泥号', async () => {
|
||||
|
||||
test('后台素材查询按用户、搜索和时间调用查询接口', async () => {
|
||||
render(
|
||||
<AdminEditorAssetQueryPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
await screen.findByRole('img', {name: '素材:角色形象 1'});
|
||||
await screen.findByRole('img', { name: '素材:角色形象 1' });
|
||||
|
||||
fireEvent.change(screen.getByLabelText('用户 ID'), {
|
||||
target: {value: 'user-1'},
|
||||
target: { value: 'user-1' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('搜索'), {
|
||||
target: {value: '陶泥角色'},
|
||||
target: { value: '陶泥角色' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('开始时间'), {
|
||||
target: {value: '2026-07-01'},
|
||||
target: { value: '2026-07-01' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('结束时间'), {
|
||||
target: {value: '2026-07-04'},
|
||||
target: { value: '2026-07-04' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -107,26 +107,20 @@ test('后台素材查询按用户、搜索和时间调用查询接口', async ()
|
||||
|
||||
test('后台素材查询不展示分类筛选和分类列', async () => {
|
||||
render(
|
||||
<AdminEditorAssetQueryPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('角色形象 1')).toBeTruthy();
|
||||
expect(screen.queryByLabelText('分类')).toBeNull();
|
||||
expect(screen.queryByRole('columnheader', {name: '分类'})).toBeNull();
|
||||
expect(screen.queryByRole('columnheader', { name: '分类' })).toBeNull();
|
||||
});
|
||||
|
||||
test('后台素材查询缩略图使用 objectKey 换签后展示', async () => {
|
||||
render(
|
||||
<AdminEditorAssetQueryPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
const image = await screen.findByRole('img', {name: '素材:角色形象 1'});
|
||||
const image = await screen.findByRole('img', { name: '素材:角色形象 1' });
|
||||
|
||||
await waitFor(() => {
|
||||
expect(image.getAttribute('src')).toBe(
|
||||
@@ -139,6 +133,32 @@ test('后台素材查询缩略图使用 objectKey 换签后展示', async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('后台素材查询音频素材使用统一封面缩略图', async () => {
|
||||
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
|
||||
entries: [
|
||||
{
|
||||
...generatedAsset,
|
||||
assetId: 'asset-audio-1',
|
||||
label: '胜利音效',
|
||||
imageSrc: '/generated-editor-audios/sfx.mp3',
|
||||
objectKey: 'generated-editor-audios/sfx.mp3',
|
||||
assetKind: 'sound-effect',
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
const image = await screen.findByRole('img', { name: '素材:胜利音效' });
|
||||
expect(image.getAttribute('src')).toBe(
|
||||
'/creation-home/audio-asset-cover.svg',
|
||||
);
|
||||
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('后台素材查询格式化微秒时间文本', async () => {
|
||||
vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
|
||||
entries: [
|
||||
@@ -152,10 +172,7 @@ test('后台素材查询格式化微秒时间文本', async () => {
|
||||
});
|
||||
|
||||
render(
|
||||
<AdminEditorAssetQueryPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
expect(await screen.findByText('角色形象 1')).toBeTruthy();
|
||||
@@ -164,15 +181,12 @@ test('后台素材查询格式化微秒时间文本', async () => {
|
||||
|
||||
test('后台素材查询可查看素材详情', async () => {
|
||||
render(
|
||||
<AdminEditorAssetQueryPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', {name: '详情'}));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '详情' }));
|
||||
|
||||
const dialog = screen.getByRole('dialog', {name: '素材详情'});
|
||||
const dialog = screen.getByRole('dialog', { name: '素材详情' });
|
||||
expect(within(dialog).getByText('asset-1')).toBeTruthy();
|
||||
expect(within(dialog).getByText('1024 x 1024')).toBeTruthy();
|
||||
expect(within(dialog).getByText('12 泥点')).toBeTruthy();
|
||||
@@ -181,20 +195,21 @@ test('后台素材查询可查看素材详情', async () => {
|
||||
|
||||
test('后台素材查询可打开弹窗查看完整提示词', async () => {
|
||||
render(
|
||||
<AdminEditorAssetQueryPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
<AdminEditorAssetQueryPage token="admin-token" onUnauthorized={vi.fn()} />,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', {name: '完整提示词内容'}));
|
||||
fireEvent.click(
|
||||
await screen.findByRole('button', { name: '完整提示词内容' }),
|
||||
);
|
||||
|
||||
const dialog = screen.getByRole('dialog', {name: '完整提示词'});
|
||||
const dialog = screen.getByRole('dialog', { name: '完整提示词' });
|
||||
expect(within(dialog).getByText('完整提示词内容')).toBeTruthy();
|
||||
|
||||
fireEvent.click(within(dialog).getByRole('button', {name: '关闭完整提示词'}));
|
||||
fireEvent.click(
|
||||
within(dialog).getByRole('button', { name: '关闭完整提示词' }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('dialog', {name: '完整提示词'})).toBeNull();
|
||||
expect(screen.queryByRole('dialog', { name: '完整提示词' })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import {Eye, FileText, RefreshCcw, X} from 'lucide-react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import { Eye, FileText, RefreshCcw, X } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
getAdminAssetReadUrl,
|
||||
listAdminEditorAssets,
|
||||
} from '../api/adminApiClient';
|
||||
import type {AdminAssetReadUrlResponse} from '../api/adminApiClient';
|
||||
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminEditorAssetListQuery,
|
||||
AdminEditorAssetPayload,
|
||||
} from '../api/adminApiTypes';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminEditorAssetQueryPageProps {
|
||||
token: string;
|
||||
@@ -19,6 +19,7 @@ interface AdminEditorAssetQueryPageProps {
|
||||
}
|
||||
|
||||
const ADMIN_ASSET_READ_EXPIRE_SECONDS = 300;
|
||||
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
|
||||
|
||||
export function AdminEditorAssetQueryPage({
|
||||
token,
|
||||
@@ -274,10 +275,11 @@ export function AdminEditorAssetQueryPage({
|
||||
);
|
||||
}
|
||||
|
||||
function AdminAssetThumbnail({entry}: {entry: AdminEditorAssetPayload}) {
|
||||
function AdminAssetThumbnail({ entry }: { entry: AdminEditorAssetPayload }) {
|
||||
const isAudio = isAdminAudioAsset(entry);
|
||||
const imageSrc = useAdminResolvedAssetImageSrc(
|
||||
entry.thumbnailSrc || entry.imageSrc,
|
||||
entry.objectKey,
|
||||
isAudio ? AUDIO_ASSET_COVER_SRC : entry.thumbnailSrc || entry.imageSrc,
|
||||
isAudio ? null : entry.objectKey,
|
||||
);
|
||||
const alt = `素材:${entry.label || entry.assetId}`;
|
||||
|
||||
@@ -288,6 +290,16 @@ function AdminAssetThumbnail({entry}: {entry: AdminEditorAssetPayload}) {
|
||||
);
|
||||
}
|
||||
|
||||
function isAdminAudioAsset(entry: AdminEditorAssetPayload) {
|
||||
const assetKind = entry.assetKind?.trim() ?? '';
|
||||
return (
|
||||
assetKind === 'sound-effect' ||
|
||||
assetKind === 'background-music' ||
|
||||
assetKind === 'editor_uploaded_audio' ||
|
||||
/\.(?:mp3|wav|m4a|aac|ogg)(?:$|[?#])/iu.test(entry.imageSrc.trim())
|
||||
);
|
||||
}
|
||||
|
||||
function AdminAssetDetailDialog({
|
||||
entry,
|
||||
onClose,
|
||||
@@ -334,7 +346,9 @@ function AdminAssetDetailDialog({
|
||||
{entry.generationCostMudPoints} 泥点
|
||||
</AdminInfoItem>
|
||||
<AdminInfoItem label="模型">{entry.model || '-'}</AdminInfoItem>
|
||||
<AdminInfoItem label="Provider">{entry.provider || '-'}</AdminInfoItem>
|
||||
<AdminInfoItem label="Provider">
|
||||
{entry.provider || '-'}
|
||||
</AdminInfoItem>
|
||||
<AdminInfoItem label="Task ID">{entry.taskId || '-'}</AdminInfoItem>
|
||||
<AdminInfoItem label="Object Key">
|
||||
{entry.objectKey || '-'}
|
||||
@@ -463,7 +477,8 @@ function mergeAssetEntries(
|
||||
[...current, ...incoming].forEach((entry) => byId.set(entry.assetId, entry));
|
||||
return [...byId.values()].sort(
|
||||
(left, right) =>
|
||||
parseAdminTimestamp(right.createdAt) - parseAdminTimestamp(left.createdAt) ||
|
||||
parseAdminTimestamp(right.createdAt) -
|
||||
parseAdminTimestamp(left.createdAt) ||
|
||||
right.assetId.localeCompare(left.assetId),
|
||||
);
|
||||
}
|
||||
@@ -505,13 +520,13 @@ function parseAdminTimestamp(value: string | null | undefined) {
|
||||
|
||||
function authorDisplayName(entry: AdminEditorAssetPayload) {
|
||||
return (
|
||||
entry.authorDisplayName?.trim() ||
|
||||
entry.authorPublicUserCode?.trim() ||
|
||||
'-'
|
||||
entry.authorDisplayName?.trim() || entry.authorPublicUserCode?.trim() || '-'
|
||||
);
|
||||
}
|
||||
|
||||
function formatGenerationInputs(value: Record<string, unknown> | null | undefined) {
|
||||
function formatGenerationInputs(
|
||||
value: Record<string, unknown> | null | undefined,
|
||||
) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import {fireEvent, render, screen, waitFor, within} from '@testing-library/react';
|
||||
import {beforeEach, expect, test, vi} from 'vitest';
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getAdminAssetReadUrl,
|
||||
@@ -11,8 +17,8 @@ import {
|
||||
updateAdminEditorShowcaseDisplay,
|
||||
upsertAdminEditorShowcaseCampaign,
|
||||
} from '../api/adminApiClient';
|
||||
import type {AdminEditorShowcaseAssetPayload} from '../api/adminApiTypes';
|
||||
import {AdminEditorShowcaseReviewPage} from './AdminEditorShowcaseReviewPage';
|
||||
import type { AdminEditorShowcaseAssetPayload } from '../api/adminApiTypes';
|
||||
import { AdminEditorShowcaseReviewPage } from './AdminEditorShowcaseReviewPage';
|
||||
|
||||
vi.mock('../api/adminApiClient', () => ({
|
||||
getAdminAssetReadUrl: vi.fn(),
|
||||
@@ -40,7 +46,7 @@ const pendingShowcaseAsset: AdminEditorShowcaseAssetPayload = {
|
||||
provider: 'character',
|
||||
taskId: 'task-1',
|
||||
assetKind: 'character',
|
||||
generationInputs: {style: 'clay'},
|
||||
generationInputs: { style: 'clay' },
|
||||
generationCostMudPoints: 12,
|
||||
refundMudPoints: 6,
|
||||
reviewStatus: 'pending',
|
||||
@@ -169,6 +175,36 @@ test('后台精选审核格式化微秒时间并显示素材名', async () => {
|
||||
expect(screen.queryByText('1783231493.573727Z')).toBeNull();
|
||||
});
|
||||
|
||||
test('后台精选审核音频素材使用统一封面缩略图', async () => {
|
||||
vi.mocked(listAdminEditorShowcaseAssets).mockResolvedValueOnce({
|
||||
entries: [
|
||||
{
|
||||
...pendingShowcaseAsset,
|
||||
showcaseId: 'showcase-audio-1',
|
||||
assetId: 'asset-audio-1',
|
||||
label: '胜利音效',
|
||||
imageSrc: '/generated-editor-audios/sfx.mp3',
|
||||
objectKey: 'generated-editor-audios/sfx.mp3',
|
||||
assetKind: 'sound-effect',
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
});
|
||||
|
||||
render(
|
||||
<AdminEditorShowcaseReviewPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
const image = await screen.findByRole('img', { name: '精选素材:胜利音效' });
|
||||
expect(image.getAttribute('src')).toBe(
|
||||
'/creation-home/audio-asset-cover.svg',
|
||||
);
|
||||
expect(getAdminAssetReadUrl).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('后台精选审核可以通过素材并查看完整提示词', async () => {
|
||||
render(
|
||||
<AdminEditorShowcaseReviewPage
|
||||
@@ -177,11 +213,10 @@ test('后台精选审核可以通过素材并查看完整提示词', async () =>
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.change(
|
||||
await screen.findByLabelText('审核备注:角色形象 1'),
|
||||
{target: {value: '质量通过'}},
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', {name: '通过'}));
|
||||
fireEvent.change(await screen.findByLabelText('审核备注:角色形象 1'), {
|
||||
target: { value: '质量通过' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '通过' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(reviewAdminEditorShowcaseAsset).toHaveBeenCalledWith('admin-token', {
|
||||
@@ -191,8 +226,8 @@ test('后台精选审核可以通过素材并查看完整提示词', async () =>
|
||||
});
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByRole('button', {name: '完整提示词内容'}));
|
||||
const dialog = screen.getByRole('dialog', {name: '完整提示词'});
|
||||
fireEvent.click(screen.getByRole('button', { name: '完整提示词内容' }));
|
||||
const dialog = screen.getByRole('dialog', { name: '完整提示词' });
|
||||
expect(within(dialog).getByText('完整提示词内容')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -209,7 +244,7 @@ test('后台精选审核可以切换展示和保存活动卡', async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(await screen.findByRole('button', {name: '隐藏'}));
|
||||
fireEvent.click(await screen.findByRole('button', { name: '隐藏' }));
|
||||
await waitFor(() => {
|
||||
expect(updateAdminEditorShowcaseDisplay).toHaveBeenCalledWith(
|
||||
'admin-token',
|
||||
@@ -222,15 +257,15 @@ test('后台精选审核可以切换展示和保存活动卡', async () => {
|
||||
});
|
||||
|
||||
fireEvent.change(await screen.findByLabelText('标题'), {
|
||||
target: {value: '新活动卡'},
|
||||
target: { value: '新活动卡' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('图片地址'), {
|
||||
target: {value: '/new-campaign.png'},
|
||||
target: { value: '/new-campaign.png' },
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('成本文案'), {
|
||||
target: {value: '6 泥点'},
|
||||
target: { value: '6 泥点' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', {name: '保存活动卡'}));
|
||||
fireEvent.click(screen.getByRole('button', { name: '保存活动卡' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(upsertAdminEditorShowcaseCampaign).toHaveBeenCalledWith(
|
||||
@@ -271,7 +306,7 @@ test('后台精选审核已通过素材可以设置精选分类', async () => {
|
||||
);
|
||||
|
||||
fireEvent.change(await screen.findByLabelText('精选分类:角色形象 2'), {
|
||||
target: {value: 'ui'},
|
||||
target: { value: 'ui' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -307,7 +342,7 @@ test('后台精选审核清空精选分类时自动隐藏素材', async () => {
|
||||
);
|
||||
|
||||
fireEvent.change(await screen.findByLabelText('精选分类:角色形象 2'), {
|
||||
target: {value: ''},
|
||||
target: { value: '' },
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Eye, FileText, RefreshCcw, X} from 'lucide-react';
|
||||
import type {ReactNode} from 'react';
|
||||
import {useEffect, useState} from 'react';
|
||||
import { Eye, FileText, RefreshCcw, X } from 'lucide-react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
getAdminAssetReadUrl,
|
||||
@@ -10,13 +10,13 @@ import {
|
||||
updateAdminEditorShowcaseDisplay,
|
||||
upsertAdminEditorShowcaseCampaign,
|
||||
} from '../api/adminApiClient';
|
||||
import type {AdminAssetReadUrlResponse} from '../api/adminApiClient';
|
||||
import type { AdminAssetReadUrlResponse } from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminEditorShowcaseAssetPayload,
|
||||
AdminEditorShowcaseCampaignPayload,
|
||||
AdminEditorShowcaseListQuery,
|
||||
} from '../api/adminApiTypes';
|
||||
import {handlePageError} from './pageUtils';
|
||||
import { handlePageError } from './pageUtils';
|
||||
|
||||
interface AdminEditorShowcaseReviewPageProps {
|
||||
token: string;
|
||||
@@ -24,20 +24,21 @@ interface AdminEditorShowcaseReviewPageProps {
|
||||
}
|
||||
|
||||
const ADMIN_SHOWCASE_READ_EXPIRE_SECONDS = 300;
|
||||
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
|
||||
|
||||
const showcaseCategoryOptions = [
|
||||
{value: 'packs', label: '素材包'},
|
||||
{value: 'characters', label: '角色'},
|
||||
{value: 'ui', label: 'UI'},
|
||||
{value: 'music', label: '音乐'},
|
||||
{value: 'marketing', label: '美宣'},
|
||||
{ value: 'packs', label: '素材包' },
|
||||
{ value: 'characters', label: '角色' },
|
||||
{ value: 'ui', label: 'UI' },
|
||||
{ value: 'music', label: '音乐' },
|
||||
{ value: 'marketing', label: '美宣' },
|
||||
];
|
||||
|
||||
const reviewStatusOptions = [
|
||||
{value: '', label: '全部'},
|
||||
{value: 'pending', label: '待审核'},
|
||||
{value: 'approved', label: '已通过'},
|
||||
{value: 'rejected', label: '已拒绝'},
|
||||
{ value: '', label: '全部' },
|
||||
{ value: 'pending', label: '待审核' },
|
||||
{ value: 'approved', label: '已通过' },
|
||||
{ value: 'rejected', label: '已拒绝' },
|
||||
];
|
||||
|
||||
export function AdminEditorShowcaseReviewPage({
|
||||
@@ -314,9 +315,7 @@ export function AdminEditorShowcaseReviewPage({
|
||||
</button>
|
||||
<small>{entry.label || '-'}</small>
|
||||
</td>
|
||||
<td>
|
||||
{formatDateTime(entry.submittedAt)}
|
||||
</td>
|
||||
<td>{formatDateTime(entry.submittedAt)}</td>
|
||||
<td>
|
||||
{authorDisplayName(entry)}
|
||||
<small>{entry.authorPublicUserCode?.trim() || '-'}</small>
|
||||
@@ -342,7 +341,9 @@ export function AdminEditorShowcaseReviewPage({
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<span className={reviewStatusClassName(entry.reviewStatus)}>
|
||||
<span
|
||||
className={reviewStatusClassName(entry.reviewStatus)}
|
||||
>
|
||||
{reviewStatusLabel(entry.reviewStatus)}
|
||||
</span>
|
||||
{entry.reviewStatus === 'approved' ? (
|
||||
@@ -589,7 +590,11 @@ function AdminShowcaseThumbnail({
|
||||
}: {
|
||||
entry: AdminEditorShowcaseAssetPayload;
|
||||
}) {
|
||||
const imageSrc = useAdminResolvedAssetImageSrc(entry.imageSrc, entry.objectKey);
|
||||
const isAudio = isAdminShowcaseAudioAsset(entry);
|
||||
const imageSrc = useAdminResolvedAssetImageSrc(
|
||||
isAudio ? AUDIO_ASSET_COVER_SRC : entry.imageSrc,
|
||||
isAudio ? null : entry.objectKey,
|
||||
);
|
||||
const alt = `精选素材:${entry.label || entry.showcaseId}`;
|
||||
|
||||
return imageSrc ? (
|
||||
@@ -599,6 +604,16 @@ function AdminShowcaseThumbnail({
|
||||
);
|
||||
}
|
||||
|
||||
function isAdminShowcaseAudioAsset(entry: AdminEditorShowcaseAssetPayload) {
|
||||
const assetKind = entry.assetKind?.trim() ?? '';
|
||||
return (
|
||||
assetKind === 'sound-effect' ||
|
||||
assetKind === 'background-music' ||
|
||||
assetKind === 'editor_uploaded_audio' ||
|
||||
/\.(?:mp3|wav|m4a|aac|ogg)(?:$|[?#])/iu.test(entry.imageSrc.trim())
|
||||
);
|
||||
}
|
||||
|
||||
function AdminShowcaseDetailDialog({
|
||||
entry,
|
||||
onClose,
|
||||
@@ -659,7 +674,9 @@ function AdminShowcaseDetailDialog({
|
||||
</AdminInfoItem>
|
||||
<AdminInfoItem label="点赞数">{entry.likeCount}</AdminInfoItem>
|
||||
<AdminInfoItem label="模型">{entry.model || '-'}</AdminInfoItem>
|
||||
<AdminInfoItem label="Provider">{entry.provider || '-'}</AdminInfoItem>
|
||||
<AdminInfoItem label="Provider">
|
||||
{entry.provider || '-'}
|
||||
</AdminInfoItem>
|
||||
<AdminInfoItem label="Task ID">{entry.taskId || '-'}</AdminInfoItem>
|
||||
<AdminInfoItem label="Object Key">
|
||||
{entry.objectKey || '-'}
|
||||
@@ -869,13 +886,13 @@ function reviewStatusClassName(value: string) {
|
||||
|
||||
function authorDisplayName(entry: AdminEditorShowcaseAssetPayload) {
|
||||
return (
|
||||
entry.authorDisplayName?.trim() ||
|
||||
entry.authorPublicUserCode?.trim() ||
|
||||
'-'
|
||||
entry.authorDisplayName?.trim() || entry.authorPublicUserCode?.trim() || '-'
|
||||
);
|
||||
}
|
||||
|
||||
function formatGenerationInputs(value: Record<string, unknown> | null | undefined) {
|
||||
function formatGenerationInputs(
|
||||
value: Record<string, unknown> | null | undefined,
|
||||
) {
|
||||
if (!value) {
|
||||
return '-';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 256 256"
|
||||
role="img"
|
||||
aria-label="音频素材"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="#dc7f58"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="16"
|
||||
>
|
||||
<path d="M92 185V35l152-24v149" />
|
||||
<path d="M92 87l152-24" />
|
||||
<ellipse cx="53" cy="207" rx="41" ry="24" transform="rotate(4 53 207)" />
|
||||
<ellipse cx="201" cy="193" rx="41" ry="24" transform="rotate(-4 201 193)" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 475 B |
@@ -125,12 +125,10 @@ function installIntersectionObserverMock() {
|
||||
return observers;
|
||||
}
|
||||
|
||||
function triggerIntersection(
|
||||
observer: {
|
||||
callback: IntersectionObserverCallback;
|
||||
instance: IntersectionObserver;
|
||||
},
|
||||
) {
|
||||
function triggerIntersection(observer: {
|
||||
callback: IntersectionObserverCallback;
|
||||
instance: IntersectionObserver;
|
||||
}) {
|
||||
act(() => {
|
||||
observer.callback(
|
||||
[{ isIntersecting: true } as IntersectionObserverEntry],
|
||||
@@ -157,15 +155,15 @@ describe('CreationLandingView', () => {
|
||||
|
||||
renderCreationLanding();
|
||||
|
||||
expect(
|
||||
screen.getByRole('main', { name: '陶泥儿创作主页' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('main', { name: '陶泥儿创作主页' })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('heading', {
|
||||
name: '陶泥儿 - 开启全民精品游戏创作',
|
||||
}),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('登录即送100泥点,可以免费制作50个素材')).toBeTruthy();
|
||||
expect(
|
||||
screen.getByText('登录即送100泥点,可以免费制作50个素材'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('heading', { name: '创作工具' })).toBeTruthy();
|
||||
expect(screen.getByText('游戏视觉规范')).toBeTruthy();
|
||||
expect(screen.getByRole('heading', { name: '陶泥儿精选' })).toBeTruthy();
|
||||
@@ -193,7 +191,9 @@ describe('CreationLandingView', () => {
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
expect(await screen.findByRole('heading', { name: '最近项目' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: '最近项目' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('最新项目')).toBeTruthy();
|
||||
expect(screen.getByText('旧项目')).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '查看全部' }));
|
||||
@@ -302,10 +302,7 @@ describe('CreationLandingView', () => {
|
||||
|
||||
render(
|
||||
<AuthUiContext.Provider value={createAuthValue()}>
|
||||
<CreationLandingView
|
||||
onOpenProject={vi.fn()}
|
||||
onOpenProjects={vi.fn()}
|
||||
/>
|
||||
<CreationLandingView onOpenProject={vi.fn()} onOpenProjects={vi.fn()} />
|
||||
</AuthUiContext.Provider>,
|
||||
);
|
||||
|
||||
@@ -375,9 +372,13 @@ describe('CreationLandingView', () => {
|
||||
const assetCard = await screen.findByText('角色英雄');
|
||||
const card = assetCard.closest('.creation-landing__asset-card');
|
||||
expect(card).toBeTruthy();
|
||||
expect(within(card as HTMLElement).getByText('character hero prompt')).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).queryByText('user-internal-author'),
|
||||
).toBeNull();
|
||||
expect(within(card as HTMLElement).getByText('20泥点')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -500,18 +501,56 @@ describe('CreationLandingView', () => {
|
||||
expect(within(dialog).getByText('生成总成本')).toBeTruthy();
|
||||
expect(within(dialog).getByText('60泥点')).toBeTruthy();
|
||||
expect(within(dialog).getByText('创作者A')).toBeTruthy();
|
||||
expect(
|
||||
within(dialog).getByLabelText('素材缩略图导航'),
|
||||
).toBeTruthy();
|
||||
expect(within(dialog).getByLabelText('素材缩略图导航')).toBeTruthy();
|
||||
|
||||
await user.click(within(dialog).getByRole('button', { name: '查看待机动画' }));
|
||||
await user.click(
|
||||
within(dialog).getByRole('button', { name: '查看待机动画' }),
|
||||
);
|
||||
expect(
|
||||
within(dialog).getByRole('button', { name: '查看待机动画' }).getAttribute(
|
||||
'aria-current',
|
||||
),
|
||||
within(dialog)
|
||||
.getByRole('button', { name: '查看待机动画' })
|
||||
.getAttribute('aria-current'),
|
||||
).toBe('true');
|
||||
});
|
||||
|
||||
it('uses the unified audio cover image for featured audio assets', async () => {
|
||||
const user = userEvent.setup();
|
||||
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
|
||||
{
|
||||
resourceId: 'resource-music',
|
||||
projectId: 'project-newest',
|
||||
label: '背景音乐',
|
||||
imageSrc: '/generated-editor-audios/music.mp3',
|
||||
objectKey: 'generated-editor-audios/music.mp3',
|
||||
width: 1,
|
||||
height: 1,
|
||||
sourceType: 'generated',
|
||||
prompt: '舒缓背景音乐',
|
||||
actualPrompt: null,
|
||||
model: 'audio1.0',
|
||||
provider: 'ark',
|
||||
taskId: 'task-audio-1',
|
||||
assetKind: 'background-music',
|
||||
priceMudPoints: 5,
|
||||
},
|
||||
]);
|
||||
|
||||
renderCreationLanding();
|
||||
|
||||
await user.click(screen.getByRole('tab', { name: '音乐' }));
|
||||
const card = (await screen.findByText('背景音乐')).closest(
|
||||
'.creation-landing__asset-card',
|
||||
);
|
||||
expect(card).toBeTruthy();
|
||||
const image = within(card as HTMLElement).getByRole('img', {
|
||||
name: '背景音乐',
|
||||
});
|
||||
expect(image.getAttribute('src')).toBe(
|
||||
'/creation-home/audio-asset-cover.svg',
|
||||
);
|
||||
});
|
||||
|
||||
it('uses Taonier featured as the generated project resource waterfall instead of creation entries', async () => {
|
||||
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
|
||||
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
|
||||
@@ -564,9 +603,9 @@ describe('CreationLandingView', () => {
|
||||
expect(await screen.findByText('素材包 A')).toBeTruthy();
|
||||
expect(screen.getByText('素材包 B')).toBeTruthy();
|
||||
expect(screen.queryByText('上传素材')).toBeNull();
|
||||
expect(
|
||||
screen.getByLabelText('用户素材瀑布流').className,
|
||||
).toContain('creation-landing__asset-waterfall');
|
||||
expect(screen.getByLabelText('用户素材瀑布流').className).toContain(
|
||||
'creation-landing__asset-waterfall',
|
||||
);
|
||||
const firstPreview = container.querySelector(
|
||||
'.creation-landing__asset-preview',
|
||||
) as HTMLElement | null;
|
||||
@@ -665,7 +704,9 @@ describe('CreationLandingView', () => {
|
||||
await waitFor(() => expect(observers).toHaveLength(1));
|
||||
triggerIntersection(observers[0]!);
|
||||
|
||||
expect(await screen.findByRole('button', { name: '读取更多素材失败,点击重试' })).toBeTruthy();
|
||||
expect(
|
||||
await screen.findByRole('button', { name: '读取更多素材失败,点击重试' }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('素材包 A')).toBeTruthy();
|
||||
expect(screen.getByLabelText('用户素材瀑布流')).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -61,8 +61,7 @@ const CREATION_HOME_ASSETS = {
|
||||
projectNew: '/creation-home/project-new.png',
|
||||
} as const;
|
||||
|
||||
const SHOWCASE_LIKE_ICON_SRC =
|
||||
'/creation-home/showcase-like-thumb.png';
|
||||
const SHOWCASE_LIKE_ICON_SRC = '/creation-home/showcase-like-thumb.png';
|
||||
|
||||
const CREATION_FEATURES: CreationFeatureItem[] = [
|
||||
{
|
||||
@@ -137,7 +136,9 @@ function mergeShowcaseResources(
|
||||
current: EditorProjectResourceSnapshot[],
|
||||
incoming: EditorProjectResourceSnapshot[],
|
||||
) {
|
||||
const seenResourceIds = new Set(current.map((resource) => resource.resourceId));
|
||||
const seenResourceIds = new Set(
|
||||
current.map((resource) => resource.resourceId),
|
||||
);
|
||||
return [
|
||||
...current,
|
||||
...incoming.filter((resource) => {
|
||||
@@ -189,10 +190,17 @@ function ShowcaseAudioPreview({
|
||||
const { resolvedUrl } = useResolvedAssetReadUrl(preview.src, {
|
||||
objectKey: preview.objectKey,
|
||||
});
|
||||
const coverSrc =
|
||||
preview.coverSrc?.trim() || '/creation-home/audio-asset-cover.svg';
|
||||
|
||||
return (
|
||||
<div className="creation-landing__asset-audio-preview">
|
||||
<Music2 aria-hidden="true" />
|
||||
<img
|
||||
alt={preview.label}
|
||||
className="creation-landing__asset-audio-cover"
|
||||
draggable={false}
|
||||
src={coverSrc}
|
||||
/>
|
||||
{controls && resolvedUrl ? (
|
||||
<audio controls preload="metadata" src={resolvedUrl} />
|
||||
) : null}
|
||||
@@ -271,11 +279,7 @@ function ShowcaseVideoPreview({
|
||||
);
|
||||
}
|
||||
|
||||
function ShowcaseBundlePreview({
|
||||
item,
|
||||
}: {
|
||||
item: ShowcaseAssetItem;
|
||||
}) {
|
||||
function ShowcaseBundlePreview({ item }: { item: ShowcaseAssetItem }) {
|
||||
const visiblePreviews = item.previews.slice(0, 4);
|
||||
const primaryPreview = visiblePreviews[0];
|
||||
|
||||
@@ -396,7 +400,9 @@ function CreationShowcaseModal({
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{isLikePending ? '处理中' : isLiked ? '已点赞' : '点赞'}</span>
|
||||
<span>
|
||||
{isLikePending ? '处理中' : isLiked ? '已点赞' : '点赞'}
|
||||
</span>
|
||||
</PlatformActionButton>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -418,7 +424,8 @@ function CreationShowcaseModal({
|
||||
onClick={() => onSelectIndex(index)}
|
||||
>
|
||||
<span className="creation-landing__showcase-thumb-media">
|
||||
{preview.mediaType === 'image' ? (
|
||||
{preview.mediaType === 'image' ||
|
||||
preview.mediaType === 'audio' ? (
|
||||
<ShowcasePreview preview={preview} variant="thumb" />
|
||||
) : (
|
||||
getPreviewIcon(preview)
|
||||
@@ -455,9 +462,13 @@ export function CreationLandingView({
|
||||
>([]);
|
||||
const [showcaseCampaign, setShowcaseCampaign] =
|
||||
useState<EditorShowcaseCampaignSnapshot | null>(null);
|
||||
const [showcaseNextCursor, setShowcaseNextCursor] = useState<string | null>(null);
|
||||
const [showcaseNextCursor, setShowcaseNextCursor] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [assetError, setAssetError] = useState<string | null>(null);
|
||||
const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState<string | null>(null);
|
||||
const [showcaseLoadMoreError, setShowcaseLoadMoreError] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [activeShowcaseTab, setActiveShowcaseTab] =
|
||||
useState<ShowcaseTabId>('packs');
|
||||
const [selectedShowcaseItem, setSelectedShowcaseItem] =
|
||||
@@ -499,9 +510,7 @@ export function CreationLandingView({
|
||||
}
|
||||
const message =
|
||||
error instanceof Error ? error.message : '读取最近项目失败';
|
||||
setProjectError(
|
||||
message,
|
||||
);
|
||||
setProjectError(message);
|
||||
})
|
||||
.finally(() => setIsLoadingProjects(false));
|
||||
}, [authUi, isAuthenticated]);
|
||||
@@ -540,47 +549,58 @@ export function CreationLandingView({
|
||||
refreshShowcaseResources();
|
||||
}, [refreshShowcaseResources]);
|
||||
|
||||
const loadMoreShowcaseResources = useCallback((options: { force?: boolean } = {}) => {
|
||||
const cursor = showcaseNextCursor;
|
||||
if (!cursor || isLoadingShowcase || isLoadingMoreShowcase) {
|
||||
return;
|
||||
}
|
||||
if (showcaseLoadingMoreCursorRef.current === cursor) {
|
||||
return;
|
||||
}
|
||||
if (!options.force && showcaseFailedLoadMoreCursorRef.current === cursor) {
|
||||
return;
|
||||
}
|
||||
showcaseLoadingMoreCursorRef.current = cursor;
|
||||
setIsLoadingMoreShowcase(true);
|
||||
setShowcaseLoadMoreError(null);
|
||||
listPublicEditorProjectResources({ cursor })
|
||||
.then((page) => {
|
||||
const normalizedPage = normalizeShowcaseResourcePage(page);
|
||||
setProjectShowcaseResources((current) =>
|
||||
mergeShowcaseResources(current, normalizedPage.resources),
|
||||
);
|
||||
setShowcaseCampaign((current) => normalizedPage.campaign ?? current);
|
||||
setShowcaseNextCursor(normalizedPage.nextCursor);
|
||||
showcaseFailedLoadMoreCursorRef.current = null;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
showcaseFailedLoadMoreCursorRef.current = cursor;
|
||||
setShowcaseLoadMoreError(
|
||||
error instanceof Error ? error.message : '读取陶泥儿精选失败',
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (showcaseLoadingMoreCursorRef.current === cursor) {
|
||||
showcaseLoadingMoreCursorRef.current = null;
|
||||
}
|
||||
setIsLoadingMoreShowcase(false);
|
||||
});
|
||||
}, [isLoadingMoreShowcase, isLoadingShowcase, showcaseNextCursor]);
|
||||
const loadMoreShowcaseResources = useCallback(
|
||||
(options: { force?: boolean } = {}) => {
|
||||
const cursor = showcaseNextCursor;
|
||||
if (!cursor || isLoadingShowcase || isLoadingMoreShowcase) {
|
||||
return;
|
||||
}
|
||||
if (showcaseLoadingMoreCursorRef.current === cursor) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!options.force &&
|
||||
showcaseFailedLoadMoreCursorRef.current === cursor
|
||||
) {
|
||||
return;
|
||||
}
|
||||
showcaseLoadingMoreCursorRef.current = cursor;
|
||||
setIsLoadingMoreShowcase(true);
|
||||
setShowcaseLoadMoreError(null);
|
||||
listPublicEditorProjectResources({ cursor })
|
||||
.then((page) => {
|
||||
const normalizedPage = normalizeShowcaseResourcePage(page);
|
||||
setProjectShowcaseResources((current) =>
|
||||
mergeShowcaseResources(current, normalizedPage.resources),
|
||||
);
|
||||
setShowcaseCampaign((current) => normalizedPage.campaign ?? current);
|
||||
setShowcaseNextCursor(normalizedPage.nextCursor);
|
||||
showcaseFailedLoadMoreCursorRef.current = null;
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
showcaseFailedLoadMoreCursorRef.current = cursor;
|
||||
setShowcaseLoadMoreError(
|
||||
error instanceof Error ? error.message : '读取陶泥儿精选失败',
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
if (showcaseLoadingMoreCursorRef.current === cursor) {
|
||||
showcaseLoadingMoreCursorRef.current = null;
|
||||
}
|
||||
setIsLoadingMoreShowcase(false);
|
||||
});
|
||||
},
|
||||
[isLoadingMoreShowcase, isLoadingShowcase, showcaseNextCursor],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const element = showcaseLoadMoreRef.current;
|
||||
if (!element || !showcaseNextCursor || isLoadingShowcase || isLoadingMoreShowcase) {
|
||||
if (
|
||||
!element ||
|
||||
!showcaseNextCursor ||
|
||||
isLoadingShowcase ||
|
||||
isLoadingMoreShowcase
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (showcaseFailedLoadMoreCursorRef.current === showcaseNextCursor) {
|
||||
@@ -608,28 +628,31 @@ export function CreationLandingView({
|
||||
showcaseNextCursor,
|
||||
]);
|
||||
|
||||
const createProject = useCallback((tool?: CreationFeatureTool) => {
|
||||
if (authUi && !authUi.user) {
|
||||
authUi.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setIsCreatingProject(true);
|
||||
setProjectError(null);
|
||||
createEditorProject()
|
||||
.then((project) =>
|
||||
onOpenProject(project.projectId, tool ? { tool } : { guide: true }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (isUnauthorizedError(error)) {
|
||||
authUi?.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setProjectError(
|
||||
error instanceof Error ? error.message : '创建项目失败',
|
||||
);
|
||||
})
|
||||
.finally(() => setIsCreatingProject(false));
|
||||
}, [authUi, onOpenProject]);
|
||||
const createProject = useCallback(
|
||||
(tool?: CreationFeatureTool) => {
|
||||
if (authUi && !authUi.user) {
|
||||
authUi.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setIsCreatingProject(true);
|
||||
setProjectError(null);
|
||||
createEditorProject()
|
||||
.then((project) =>
|
||||
onOpenProject(project.projectId, tool ? { tool } : { guide: true }),
|
||||
)
|
||||
.catch((error: unknown) => {
|
||||
if (isUnauthorizedError(error)) {
|
||||
authUi?.openLoginModal(() => createProject(tool));
|
||||
return;
|
||||
}
|
||||
setProjectError(
|
||||
error instanceof Error ? error.message : '创建项目失败',
|
||||
);
|
||||
})
|
||||
.finally(() => setIsCreatingProject(false));
|
||||
},
|
||||
[authUi, onOpenProject],
|
||||
);
|
||||
|
||||
const handleFeatureClick = useCallback(
|
||||
(feature: CreationFeatureItem) => {
|
||||
@@ -646,13 +669,16 @@ export function CreationLandingView({
|
||||
(showcaseId: string, likeCount: number) => {
|
||||
setProjectShowcaseResources((current) =>
|
||||
current.map((resource) =>
|
||||
resource.showcaseId === showcaseId || resource.resourceId === showcaseId
|
||||
resource.showcaseId === showcaseId ||
|
||||
resource.resourceId === showcaseId
|
||||
? { ...resource, likeCount }
|
||||
: resource,
|
||||
),
|
||||
);
|
||||
setSelectedShowcaseItem((current) =>
|
||||
current?.showcaseId === showcaseId ? { ...current, likeCount } : current,
|
||||
current?.showcaseId === showcaseId
|
||||
? { ...current, likeCount }
|
||||
: current,
|
||||
);
|
||||
},
|
||||
[],
|
||||
@@ -783,7 +809,10 @@ export function CreationLandingView({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="creation-landing__asset-waterfall" aria-label="用户素材瀑布流">
|
||||
<div
|
||||
className="creation-landing__asset-waterfall"
|
||||
aria-label="用户素材瀑布流"
|
||||
>
|
||||
{showcaseItems.map((item) => {
|
||||
const showcaseId = item.showcaseId?.trim();
|
||||
const isLiked = Boolean(
|
||||
@@ -830,7 +859,11 @@ export function CreationLandingView({
|
||||
disabled={isLikePending}
|
||||
onClick={() => handleToggleShowcaseLike(item)}
|
||||
>
|
||||
<img src={SHOWCASE_LIKE_ICON_SRC} alt="" aria-hidden="true" />
|
||||
<img
|
||||
src={SHOWCASE_LIKE_ICON_SRC}
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span>{item.likeCount ?? 0}</span>
|
||||
</button>
|
||||
) : null}
|
||||
@@ -1015,7 +1048,11 @@ export function CreationLandingView({
|
||||
<h2>陶泥儿精选</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="creation-landing__tabs" role="tablist" aria-label="用户素材分类">
|
||||
<div
|
||||
className="creation-landing__tabs"
|
||||
role="tablist"
|
||||
aria-label="用户素材分类"
|
||||
>
|
||||
{SHOWCASE_TABS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
|
||||
@@ -458,6 +458,9 @@ describe('creationShowcaseModel', () => {
|
||||
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]?.previews[0]?.mediaType).toBe('audio');
|
||||
expect(items[0]?.previews[0]?.coverSrc).toBe(
|
||||
'/creation-home/audio-asset-cover.svg',
|
||||
);
|
||||
expect(items[0]?.author).toBe('当前用户');
|
||||
expect(items[0]?.cost).toBe('5泥点');
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ export type ShowcaseAssetPreview = {
|
||||
id: string;
|
||||
label: string;
|
||||
src: string;
|
||||
coverSrc?: string | null;
|
||||
objectKey?: string | null;
|
||||
mediaType: ShowcaseAssetMediaType;
|
||||
width?: number;
|
||||
@@ -55,6 +56,7 @@ type ShowcaseAssetGroup = {
|
||||
};
|
||||
|
||||
const UNKNOWN_META_VALUE = '-';
|
||||
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
|
||||
|
||||
const SHOWCASE_COST_KEYS = [
|
||||
'priceMudPoints',
|
||||
@@ -63,7 +65,10 @@ const SHOWCASE_COST_KEYS = [
|
||||
'mudPointCost',
|
||||
] as const;
|
||||
|
||||
const SHOWCASE_AUTHOR_KEYS = ['authorDisplayName', 'authorPublicUserCode'] as const;
|
||||
const SHOWCASE_AUTHOR_KEYS = [
|
||||
'authorDisplayName',
|
||||
'authorPublicUserCode',
|
||||
] as const;
|
||||
|
||||
export const SHOWCASE_TABS: Array<{ id: ShowcaseTabId; label: string }> = [
|
||||
{ id: 'packs', label: '素材包' },
|
||||
@@ -183,7 +188,10 @@ function projectResourceToShowcaseAsset(
|
||||
|
||||
return {
|
||||
...record,
|
||||
assetId: resource.assetId?.trim() || resource.showcaseId?.trim() || resource.resourceId,
|
||||
assetId:
|
||||
resource.assetId?.trim() ||
|
||||
resource.showcaseId?.trim() ||
|
||||
resource.resourceId,
|
||||
folderId: `project:${resource.projectId}`,
|
||||
label: fallbackLabel,
|
||||
imageSrc: resource.imageSrc,
|
||||
@@ -216,7 +224,9 @@ function normalizeShowcaseCategory(value: string): ShowcaseTabId | null {
|
||||
}
|
||||
|
||||
function resolveAssetShowcaseCategory(asset: EditorAssetSnapshot) {
|
||||
return normalizeShowcaseCategory(normalizeText(assetRecord(asset).showcaseCategory));
|
||||
return normalizeShowcaseCategory(
|
||||
normalizeText(assetRecord(asset).showcaseCategory),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeShowcaseMediaRef(value: string | null | undefined) {
|
||||
@@ -307,11 +317,15 @@ function findSpecReference(asset: EditorAssetSnapshot) {
|
||||
}
|
||||
|
||||
function findCharacterReference(asset: EditorAssetSnapshot) {
|
||||
return findReference(asset, (reference) => /角色图片|角色图/u.test(reference.title));
|
||||
return findReference(asset, (reference) =>
|
||||
/角色图片|角色图/u.test(reference.title),
|
||||
);
|
||||
}
|
||||
|
||||
function findUiDesignReference(asset: EditorAssetSnapshot) {
|
||||
return findReference(asset, (reference) => /UI设计图|UI 图|UI图片/u.test(reference.title));
|
||||
return findReference(asset, (reference) =>
|
||||
/UI设计图|UI 图|UI图片/u.test(reference.title),
|
||||
);
|
||||
}
|
||||
|
||||
function isAssetKind(asset: EditorAssetSnapshot, kinds: readonly string[]) {
|
||||
@@ -336,7 +350,9 @@ function isVideoAsset(asset: EditorAssetSnapshot) {
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAssetMediaType(asset: EditorAssetSnapshot): ShowcaseAssetMediaType {
|
||||
function resolveAssetMediaType(
|
||||
asset: EditorAssetSnapshot,
|
||||
): ShowcaseAssetMediaType {
|
||||
if (isAudioAsset(asset)) {
|
||||
return 'audio';
|
||||
}
|
||||
@@ -435,11 +451,16 @@ function mergeAssetAliasGroups(
|
||||
|
||||
function sortGroupAssets(assets: EditorAssetSnapshot[]) {
|
||||
return [...assets].sort((left, right) => {
|
||||
const leftRank = isSpecAsset(left) || isCharacterAsset(left) || isAssetKind(left, ['ui-design'])
|
||||
? 0
|
||||
: 1;
|
||||
const leftRank =
|
||||
isSpecAsset(left) ||
|
||||
isCharacterAsset(left) ||
|
||||
isAssetKind(left, ['ui-design'])
|
||||
? 0
|
||||
: 1;
|
||||
const rightRank =
|
||||
isSpecAsset(right) || isCharacterAsset(right) || isAssetKind(right, ['ui-design'])
|
||||
isSpecAsset(right) ||
|
||||
isCharacterAsset(right) ||
|
||||
isAssetKind(right, ['ui-design'])
|
||||
? 0
|
||||
: 1;
|
||||
if (leftRank !== rightRank) {
|
||||
@@ -454,7 +475,12 @@ function groupPackAssets(assets: EditorAssetSnapshot[]) {
|
||||
assets.forEach((asset) => {
|
||||
const specReference = findSpecReference(asset);
|
||||
if (specReference) {
|
||||
addAssetToGroup(groups, getReferenceKey(specReference), asset, specReference.label);
|
||||
addAssetToGroup(
|
||||
groups,
|
||||
getReferenceKey(specReference),
|
||||
asset,
|
||||
specReference.label,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (isSpecAsset(asset)) {
|
||||
@@ -466,7 +492,10 @@ function groupPackAssets(assets: EditorAssetSnapshot[]) {
|
||||
const specAssets = assets.filter(isSpecAsset);
|
||||
specAssets.forEach((asset) => {
|
||||
const group = mergeAssetAliasGroups(groups, asset, asset.label);
|
||||
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
|
||||
if (
|
||||
group &&
|
||||
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
|
||||
) {
|
||||
group.assets.unshift(asset);
|
||||
}
|
||||
});
|
||||
@@ -500,12 +529,20 @@ function groupCharacterAssets(assets: EditorAssetSnapshot[]) {
|
||||
);
|
||||
return;
|
||||
}
|
||||
addAssetToGroup(groups, `standalone:${asset.assetId}`, asset, asset.label);
|
||||
});
|
||||
addAssetToGroup(
|
||||
groups,
|
||||
`standalone:${asset.assetId}`,
|
||||
asset,
|
||||
asset.label,
|
||||
);
|
||||
});
|
||||
const characterAssets = assets.filter(isCharacterAsset);
|
||||
characterAssets.forEach((asset) => {
|
||||
const group = mergeAssetAliasGroups(groups, asset, asset.label);
|
||||
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
|
||||
if (
|
||||
group &&
|
||||
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
|
||||
) {
|
||||
group.assets.unshift(asset);
|
||||
}
|
||||
});
|
||||
@@ -518,23 +555,43 @@ function groupCharacterAssets(assets: EditorAssetSnapshot[]) {
|
||||
function groupUiAssets(assets: EditorAssetSnapshot[]) {
|
||||
const groups = new Map<string, ShowcaseAssetGroup>();
|
||||
assets
|
||||
.filter((asset) => isUiAsset(asset) || /\bui\b|界面|图标|icon/u.test(getAssetHaystack(asset)))
|
||||
.filter(
|
||||
(asset) =>
|
||||
isUiAsset(asset) ||
|
||||
/\bui\b|界面|图标|icon/u.test(getAssetHaystack(asset)),
|
||||
)
|
||||
.forEach((asset) => {
|
||||
if (isAssetKind(asset, ['ui-design'])) {
|
||||
addAssetToGroup(groups, getSelfAssetKey(asset), asset, asset.label);
|
||||
return;
|
||||
}
|
||||
const uiReference = findUiDesignReference(asset) ?? findSpecReference(asset);
|
||||
const uiReference =
|
||||
findUiDesignReference(asset) ?? findSpecReference(asset);
|
||||
if (uiReference) {
|
||||
addAssetToGroup(groups, getReferenceKey(uiReference), asset, uiReference.label);
|
||||
addAssetToGroup(
|
||||
groups,
|
||||
getReferenceKey(uiReference),
|
||||
asset,
|
||||
uiReference.label,
|
||||
);
|
||||
return;
|
||||
}
|
||||
addAssetToGroup(groups, `standalone:${asset.assetId}`, asset, asset.label);
|
||||
});
|
||||
const uiDesignAssets = assets.filter((asset) => isAssetKind(asset, ['ui-design']));
|
||||
addAssetToGroup(
|
||||
groups,
|
||||
`standalone:${asset.assetId}`,
|
||||
asset,
|
||||
asset.label,
|
||||
);
|
||||
});
|
||||
const uiDesignAssets = assets.filter((asset) =>
|
||||
isAssetKind(asset, ['ui-design']),
|
||||
);
|
||||
uiDesignAssets.forEach((asset) => {
|
||||
const group = mergeAssetAliasGroups(groups, asset, asset.label);
|
||||
if (group && !group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)) {
|
||||
if (
|
||||
group &&
|
||||
!group.assets.some((groupAsset) => groupAsset.assetId === asset.assetId)
|
||||
) {
|
||||
group.assets.unshift(asset);
|
||||
}
|
||||
});
|
||||
@@ -588,13 +645,18 @@ function resolveAssetPrompt(asset: EditorAssetSnapshot) {
|
||||
|
||||
const generationFields = asset.generationInputs?.fields ?? [];
|
||||
const firstPromptField =
|
||||
generationFields.find((field) => /提示词|描述|设定|prompt/u.test(field.title)) ??
|
||||
generationFields[0];
|
||||
generationFields.find((field) =>
|
||||
/提示词|描述|设定|prompt/u.test(field.title),
|
||||
) ?? generationFields[0];
|
||||
return firstPromptField?.value?.trim() || UNKNOWN_META_VALUE;
|
||||
}
|
||||
|
||||
function resolveGroupPrompt(assets: EditorAssetSnapshot[]) {
|
||||
return assets.map(resolveAssetPrompt).find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE;
|
||||
return (
|
||||
assets
|
||||
.map(resolveAssetPrompt)
|
||||
.find((value) => value !== UNKNOWN_META_VALUE) ?? UNKNOWN_META_VALUE
|
||||
);
|
||||
}
|
||||
|
||||
function resolveAssetAuthor(
|
||||
@@ -672,7 +734,12 @@ function inferAssetCost(asset: EditorAssetSnapshot) {
|
||||
| '480p'
|
||||
| '720p'
|
||||
| '1080p',
|
||||
normalizeInt(asset.durationSeconds, DEFAULT_VIDEO_DURATION_SECONDS, 4, 15),
|
||||
normalizeInt(
|
||||
asset.durationSeconds,
|
||||
DEFAULT_VIDEO_DURATION_SECONDS,
|
||||
4,
|
||||
15,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (asset.sourceType === 'generated' || kind) {
|
||||
@@ -715,12 +782,14 @@ function resolveGroupCost(assets: EditorAssetSnapshot[]) {
|
||||
}
|
||||
|
||||
function toAssetPreview(asset: EditorAssetSnapshot): ShowcaseAssetPreview {
|
||||
const mediaType = resolveAssetMediaType(asset);
|
||||
return {
|
||||
id: asset.assetId,
|
||||
label: asset.label,
|
||||
src: asset.imageSrc,
|
||||
coverSrc: mediaType === 'audio' ? AUDIO_ASSET_COVER_SRC : null,
|
||||
objectKey: asset.objectKey,
|
||||
mediaType: resolveAssetMediaType(asset),
|
||||
mediaType,
|
||||
width: asset.width,
|
||||
height: asset.height,
|
||||
};
|
||||
@@ -731,7 +800,9 @@ function toLibraryShowcaseItem(
|
||||
fallbackAuthorName?: string | null,
|
||||
): ShowcaseAssetItem | null {
|
||||
const sortedAssets = sortGroupAssets(group.assets);
|
||||
const previews = sortedAssets.map(toAssetPreview).filter((preview) => preview.src.trim());
|
||||
const previews = sortedAssets
|
||||
.map(toAssetPreview)
|
||||
.filter((preview) => preview.src.trim());
|
||||
if (!previews.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -27,25 +27,30 @@ vi.mock('../common/PlatformMediaFrame', () => ({
|
||||
}) => {
|
||||
const imageSrc =
|
||||
src?.trim() ||
|
||||
(objectKey?.trim() ? `https://signed.example.com/${objectKey.trim()}` : '');
|
||||
(objectKey?.trim()
|
||||
? `https://signed.example.com/${objectKey.trim()}`
|
||||
: '');
|
||||
return (
|
||||
<div className="platform-media-frame">
|
||||
{imageSrc ? (
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
data-object-key={objectKey?.trim() || undefined}
|
||||
/>
|
||||
) : null}
|
||||
{previewOverlay}
|
||||
</div>
|
||||
<img
|
||||
src={imageSrc}
|
||||
alt={alt}
|
||||
data-object-key={objectKey?.trim() || undefined}
|
||||
/>
|
||||
) : null}
|
||||
{previewOverlay}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
useResolvedAssetReadUrlMock.mockImplementation(
|
||||
(source: string | null | undefined, options?: { objectKey?: string | null }) => ({
|
||||
(
|
||||
source: string | null | undefined,
|
||||
options?: { objectKey?: string | null },
|
||||
) => ({
|
||||
resolvedUrl: options?.objectKey
|
||||
? `https://signed.example.com/${options.objectKey}`
|
||||
: (source ?? ''),
|
||||
@@ -138,9 +143,7 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
const onDownloadAsset = vi.fn();
|
||||
renderAssetRow({ asset, onDownloadAsset });
|
||||
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '下载素材账号素材A' }),
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载素材账号素材A' }));
|
||||
|
||||
expect(onDownloadAsset).toHaveBeenCalledWith(asset);
|
||||
});
|
||||
@@ -154,20 +157,26 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
const onDownloadAsset = vi.fn();
|
||||
renderAssetRow({ asset, addAssetLayer, onDownloadAsset });
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
expect(screen.getByRole('menu')).toBeTruthy();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '下载素材账号素材A' }));
|
||||
expect(onDownloadAsset).toHaveBeenCalledWith(asset);
|
||||
expect(screen.queryByRole('menu')).toBeNull();
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
fireEvent.click(screen.getByRole('button', { name: '添加账号素材A' }));
|
||||
expect(addAssetLayer).toHaveBeenCalledWith(asset);
|
||||
expect(screen.queryByRole('menu')).toBeNull();
|
||||
@@ -199,7 +208,9 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
fireEvent.keyDown(input, { key: 'Enter' });
|
||||
|
||||
expect(input.className).toContain('platform-text-field');
|
||||
expect(input.className).toContain('image-canvas-editor__asset-rename-input');
|
||||
expect(input.className).toContain(
|
||||
'image-canvas-editor__asset-rename-input',
|
||||
);
|
||||
expect(commitAssetRename).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ id: 'asset-1' }),
|
||||
);
|
||||
@@ -226,7 +237,7 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
).toBe('42');
|
||||
});
|
||||
|
||||
it('uses icon thumbnails for audio assets instead of rendering media as images', () => {
|
||||
it('uses the audio cover thumbnail for audio assets', () => {
|
||||
renderAssetRow({
|
||||
asset: createAsset({
|
||||
label: '胜利音效.mp3',
|
||||
@@ -236,13 +247,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
});
|
||||
|
||||
expect(screen.getByText('音频')).toBeTruthy();
|
||||
expect(screen.queryByRole('img', { name: '素材:胜利音效.mp3' })).toBeNull();
|
||||
const image = screen.getByRole('img', { name: '素材:胜利音效.mp3' });
|
||||
expect(image.getAttribute('src')).toBe(
|
||||
'/creation-home/audio-asset-cover.svg',
|
||||
);
|
||||
expect(
|
||||
document.querySelector('.image-canvas-editor__asset-media-overlay'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
document.querySelector('.image-canvas-editor__media-preview--audio'),
|
||||
).toBeTruthy();
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('uses a stable visual preview for video asset cards without image thumbnails', () => {
|
||||
@@ -250,13 +261,16 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
asset: createAsset({
|
||||
label: '生成视频.mp4',
|
||||
src: '/generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
objectKey: 'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
objectKey:
|
||||
'generated-character-drafts/editor/asset-library/video/demo.mp4',
|
||||
mediaType: 'video',
|
||||
}),
|
||||
});
|
||||
|
||||
expect(screen.getByText('视频')).toBeTruthy();
|
||||
expect(screen.queryByRole('img', { name: '素材:生成视频.mp4' })).toBeNull();
|
||||
expect(
|
||||
screen.queryByRole('img', { name: '素材:生成视频.mp4' }),
|
||||
).toBeNull();
|
||||
const video = screen.getByLabelText(
|
||||
'视频素材预览:生成视频.mp4',
|
||||
) as HTMLVideoElement;
|
||||
@@ -315,10 +329,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
screen.queryByRole('button', { name: '删除素材账号素材A' }),
|
||||
).toBeNull();
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
|
||||
const submitMenuItem = screen.getByRole('menuitem', {
|
||||
name: /提交精选审核/u,
|
||||
@@ -327,10 +344,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
fireEvent.click(submitMenuItem);
|
||||
expect(submitAssetToShowcase).toHaveBeenCalledWith(asset);
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
fireEvent.click(screen.getByRole('menuitem', { name: '删除' }));
|
||||
expect(deleteUploadedAsset).toHaveBeenCalledWith(asset);
|
||||
});
|
||||
@@ -339,10 +359,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
const submitAssetToShowcase = vi.fn();
|
||||
renderAssetRow({ submitAssetToShowcase });
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
|
||||
const submitMenuItem = screen.getByRole('menuitem', {
|
||||
name: /提交精选审核/u,
|
||||
@@ -360,10 +383,13 @@ describe('ImageCanvasAssetRowView', () => {
|
||||
const submitAssetToShowcase = vi.fn();
|
||||
renderAssetRow({ asset, submitAssetToShowcase });
|
||||
|
||||
fireEvent.contextMenu(screen.getByRole('button', { name: '添加账号素材A' }), {
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
});
|
||||
fireEvent.contextMenu(
|
||||
screen.getByRole('button', { name: '添加账号素材A' }),
|
||||
{
|
||||
clientX: 120,
|
||||
clientY: 80,
|
||||
},
|
||||
);
|
||||
|
||||
const submitMenuItem = screen.getByRole('menuitem', {
|
||||
name: /提交精选审核/u,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Check, Download, Music2, Pencil, Video, X } from 'lucide-react';
|
||||
import { Check, Download, Pencil, Video, X } from 'lucide-react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import type { Dispatch, SetStateAction } from 'react';
|
||||
|
||||
@@ -24,6 +24,8 @@ import {
|
||||
getCanvasMediaPreviewMarker,
|
||||
} from './ImageCanvasMediaModel';
|
||||
|
||||
const AUDIO_ASSET_COVER_SRC = '/creation-home/audio-asset-cover.svg';
|
||||
|
||||
export type ImageCanvasAssetRowViewProps = {
|
||||
asset: EditorAsset;
|
||||
assetPointerDragRef: { current: AssetPointerDragState | null };
|
||||
@@ -38,7 +40,10 @@ export type ImageCanvasAssetRowViewProps = {
|
||||
setAssetPointerDrag: Dispatch<SetStateAction<AssetPointerDragState | null>>;
|
||||
setSelectedAssetIds: Dispatch<SetStateAction<Set<string>>>;
|
||||
updateAssetMoveDropFolder: (folderId: string | null) => void;
|
||||
addUploadedFiles: (files: FileList | File[], options?: UploadFilesOptions) => void;
|
||||
addUploadedFiles: (
|
||||
files: FileList | File[],
|
||||
options?: UploadFilesOptions,
|
||||
) => void;
|
||||
moveAssetToFolder: (assetId: string, folderId: string) => void;
|
||||
startRenamingAsset: (asset: EditorAsset) => void;
|
||||
commitAssetRename: (asset: EditorAsset) => void;
|
||||
@@ -81,7 +86,9 @@ export function ImageCanvasAssetRowView({
|
||||
const isFailedUpload = asset.uploadStatus === 'failed';
|
||||
const showcaseReviewStatus = asset.showcaseReviewStatus ?? null;
|
||||
const canSubmitShowcaseAsset =
|
||||
asset.sourceType === 'generated' && asset.persisted && !showcaseReviewStatus;
|
||||
asset.sourceType === 'generated' &&
|
||||
asset.persisted &&
|
||||
!showcaseReviewStatus;
|
||||
const showcaseMenuLabel =
|
||||
showcaseReviewStatus === 'pending'
|
||||
? '精选审核中'
|
||||
@@ -153,13 +160,7 @@ export function ImageCanvasAssetRowView({
|
||||
? '视频'
|
||||
: `${asset.width} x ${asset.height}`;
|
||||
const mediaOverlay =
|
||||
asset.mediaType === 'audio' ? (
|
||||
<div
|
||||
className={`image-canvas-editor__asset-media-overlay ${mediaPreviewClassName}`}
|
||||
>
|
||||
<Music2 className="h-5 w-5" />
|
||||
</div>
|
||||
) : asset.mediaType === 'video' ? (
|
||||
asset.mediaType === 'video' ? (
|
||||
<div
|
||||
className={`image-canvas-editor__asset-media-overlay ${mediaPreviewClassName}`}
|
||||
>
|
||||
@@ -279,9 +280,7 @@ export function ImageCanvasAssetRowView({
|
||||
rowClassName={[
|
||||
'image-canvas-editor__asset-row',
|
||||
isUploadingAsset ? 'image-canvas-editor__asset-row--uploading' : '',
|
||||
isFailedUpload
|
||||
? 'image-canvas-editor__asset-row--upload-failed'
|
||||
: '',
|
||||
isFailedUpload ? 'image-canvas-editor__asset-row--upload-failed' : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
@@ -291,12 +290,18 @@ export function ImageCanvasAssetRowView({
|
||||
imageSrc={
|
||||
asset.mediaType === 'image'
|
||||
? asset.src
|
||||
: (asset.thumbnailSrc ?? '')
|
||||
: asset.mediaType === 'audio'
|
||||
? AUDIO_ASSET_COVER_SRC
|
||||
: (asset.thumbnailSrc ?? '')
|
||||
}
|
||||
objectKey={asset.mediaType === 'image' ? asset.objectKey : undefined}
|
||||
previewNode={videoPreview}
|
||||
titleNode={
|
||||
isUploadingAsset || isFailedUpload ? <span>{asset.label}</span> : titleNode
|
||||
isUploadingAsset || isFailedUpload ? (
|
||||
<span>{asset.label}</span>
|
||||
) : (
|
||||
titleNode
|
||||
)
|
||||
}
|
||||
actions={actions}
|
||||
draggable={!isRenaming && !isUploadingAsset && !isFailedUpload}
|
||||
|
||||
+132
-60
@@ -1585,7 +1585,9 @@ html[data-mobile-keyboard-open='true'] #root {
|
||||
background: rgba(255, 252, 248, 0.97);
|
||||
box-shadow: 0 18px 36px rgba(74, 34, 15, 0.18);
|
||||
pointer-events: none;
|
||||
transform: translateX(calc(-50% + var(--floating-feedback-contact-offset, 0px)));
|
||||
transform: translateX(
|
||||
calc(-50% + var(--floating-feedback-contact-offset, 0px))
|
||||
);
|
||||
}
|
||||
|
||||
.floating-feedback-entry-contact-panel img {
|
||||
@@ -3183,8 +3185,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(15, 23, 42, 0.06), transparent 56%),
|
||||
background: linear-gradient(135deg, rgba(15, 23, 42, 0.06), transparent 56%),
|
||||
#f3f4f6;
|
||||
}
|
||||
|
||||
@@ -3397,7 +3398,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
overflow: auto;
|
||||
background:
|
||||
linear-gradient(rgba(255, 250, 244, 0.56), rgba(255, 250, 244, 0.56)),
|
||||
url('/creation-home/background-clay-workspace.png') center top / 64rem 64rem repeat,
|
||||
url('/creation-home/background-clay-workspace.png') center top / 64rem 64rem
|
||||
repeat,
|
||||
linear-gradient(180deg, #fff9f3 0%, #fffdf9 52%, #fff8f0 100%);
|
||||
color: var(--creation-ink);
|
||||
}
|
||||
@@ -3434,8 +3436,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
margin: 0;
|
||||
max-width: 62rem;
|
||||
color: var(--creation-ink);
|
||||
font-family:
|
||||
"Songti SC", "STSong", "Noto Serif SC", "SimSun", serif;
|
||||
font-family: 'Songti SC', 'STSong', 'Noto Serif SC', 'SimSun', serif;
|
||||
font-size: clamp(2.35rem, 4.25vw, 3.85rem);
|
||||
font-weight: 950;
|
||||
letter-spacing: 0;
|
||||
@@ -3512,8 +3513,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
top: 14%;
|
||||
width: 58%;
|
||||
aspect-ratio: 4 / 3;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(75, 181, 170, 0.24), transparent 52%),
|
||||
background: linear-gradient(135deg, rgba(75, 181, 170, 0.24), transparent 52%),
|
||||
#ffffff;
|
||||
}
|
||||
|
||||
@@ -3522,8 +3522,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
top: 28%;
|
||||
width: 34%;
|
||||
aspect-ratio: 1;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(246, 171, 76, 0.24), transparent 56%),
|
||||
background: linear-gradient(135deg, rgba(246, 171, 76, 0.24), transparent 56%),
|
||||
#ffffff;
|
||||
}
|
||||
|
||||
@@ -3532,8 +3531,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
bottom: 10%;
|
||||
width: 36%;
|
||||
aspect-ratio: 16 / 9;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(129, 140, 248, 0.2), transparent 56%),
|
||||
background: linear-gradient(135deg, rgba(129, 140, 248, 0.2), transparent 56%),
|
||||
#ffffff;
|
||||
}
|
||||
|
||||
@@ -3585,9 +3583,16 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
min-height: 9.55rem;
|
||||
border: 1px solid rgba(220, 189, 163, 0.78);
|
||||
border-radius: 1.12rem;
|
||||
background:
|
||||
radial-gradient(circle at 21% 42%, rgba(222, 130, 66, 0.12), transparent 9rem),
|
||||
linear-gradient(135deg, rgba(255, 254, 251, 0.92), rgba(255, 247, 238, 0.82));
|
||||
background: radial-gradient(
|
||||
circle at 21% 42%,
|
||||
rgba(222, 130, 66, 0.12),
|
||||
transparent 9rem
|
||||
),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 254, 251, 0.92),
|
||||
rgba(255, 247, 238, 0.82)
|
||||
);
|
||||
padding: 1.05rem 1.3rem;
|
||||
color: inherit;
|
||||
font: inherit;
|
||||
@@ -3646,7 +3651,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
border-radius: 999px;
|
||||
background: rgba(112, 62, 32, 0.22);
|
||||
filter: blur(0.16rem);
|
||||
content: "";
|
||||
content: '';
|
||||
}
|
||||
|
||||
.creation-landing__feature-art {
|
||||
@@ -3961,15 +3966,25 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
place-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(244, 187, 129, 0.4), rgba(255, 247, 236, 0.92)),
|
||||
radial-gradient(circle at 30% 24%, rgba(255, 255, 255, 0.9), transparent 42%);
|
||||
color: #9a5428;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(244, 187, 129, 0.4),
|
||||
rgba(255, 247, 236, 0.92)
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 30% 24%,
|
||||
rgba(255, 255, 255, 0.9),
|
||||
transparent 42%
|
||||
);
|
||||
}
|
||||
|
||||
.creation-landing__asset-audio-preview svg {
|
||||
width: 2.6rem;
|
||||
height: 2.6rem;
|
||||
.creation-landing__asset-audio-cover {
|
||||
display: block;
|
||||
width: min(48%, 11rem);
|
||||
min-width: 3.2rem;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.creation-landing__asset-audio-preview audio {
|
||||
@@ -4054,8 +4069,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
min-height: 0;
|
||||
grid-template-rows: minmax(0, 1fr) auto;
|
||||
overflow: hidden;
|
||||
background:
|
||||
radial-gradient(circle at 18% 12%, rgba(186, 117, 65, 0.18), transparent 32%),
|
||||
background: radial-gradient(
|
||||
circle at 18% 12%,
|
||||
rgba(186, 117, 65, 0.18),
|
||||
transparent 32%
|
||||
),
|
||||
#100d0b;
|
||||
}
|
||||
|
||||
@@ -4376,7 +4394,10 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
--platform-button-primary-fill,
|
||||
linear-gradient(135deg, #df7f40, #b95d3a)
|
||||
);
|
||||
--image-canvas-brand-text-on-fill: var(--platform-button-primary-text, #fffaf5);
|
||||
--image-canvas-brand-text-on-fill: var(
|
||||
--platform-button-primary-text,
|
||||
#fffaf5
|
||||
);
|
||||
--image-canvas-brand-border: rgba(204, 117, 76, 0.34);
|
||||
--image-canvas-brand-border-strong: rgba(199, 101, 61, 0.64);
|
||||
--image-canvas-brand-border-soft: rgba(226, 203, 184, 0.82);
|
||||
@@ -4881,8 +4902,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
.image-canvas-editor__media-preview {
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
background:
|
||||
radial-gradient(circle at 25% 24%, rgba(255, 255, 255, 0.22), transparent 0.72rem),
|
||||
background: radial-gradient(
|
||||
circle at 25% 24%,
|
||||
rgba(255, 255, 255, 0.22),
|
||||
transparent 0.72rem
|
||||
),
|
||||
linear-gradient(135deg, rgba(15, 23, 42, 0.84), rgba(20, 83, 45, 0.58));
|
||||
}
|
||||
|
||||
@@ -4895,8 +4919,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
}
|
||||
|
||||
.image-canvas-editor__media-preview--audio::before {
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(103, 232, 249, 0.34) 0 0.18rem,
|
||||
transparent 0.18rem 0.52rem
|
||||
@@ -4905,8 +4928,12 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
}
|
||||
|
||||
.image-canvas-editor__media-preview--video::before {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(15, 23, 42, 0.42) 0 14%, transparent 14% 86%, rgba(15, 23, 42, 0.42) 86%),
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(15, 23, 42, 0.42) 0 14%,
|
||||
transparent 14% 86%,
|
||||
rgba(15, 23, 42, 0.42) 86%
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
0deg,
|
||||
rgba(248, 250, 252, 0.34) 0 0.2rem,
|
||||
@@ -5840,7 +5867,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
rgba(250, 245, 255, 0.94),
|
||||
transparent 30%
|
||||
),
|
||||
linear-gradient(135deg, rgba(250, 245, 255, 0.84), rgba(243, 232, 255, 0.72));
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(250, 245, 255, 0.84),
|
||||
rgba(243, 232, 255, 0.72)
|
||||
);
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-frame--character.image-canvas-editor__generation-frame--focused {
|
||||
@@ -5853,9 +5884,16 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-frame--character-animation {
|
||||
background:
|
||||
radial-gradient(circle at 50% 36%, rgba(255, 247, 237, 0.96), transparent 32%),
|
||||
linear-gradient(135deg, rgba(255, 247, 237, 0.92), rgba(254, 215, 170, 0.74));
|
||||
background: radial-gradient(
|
||||
circle at 50% 36%,
|
||||
rgba(255, 247, 237, 0.96),
|
||||
transparent 32%
|
||||
),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(255, 247, 237, 0.92),
|
||||
rgba(254, 215, 170, 0.74)
|
||||
);
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-frame--character-animation.image-canvas-editor__generation-frame--focused {
|
||||
@@ -5868,8 +5906,12 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
}
|
||||
|
||||
.image-canvas-editor__generation-frame--video {
|
||||
background:
|
||||
linear-gradient(90deg, rgba(15, 23, 42, 0.12) 10%, transparent 10% 90%, rgba(15, 23, 42, 0.12) 90%),
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(15, 23, 42, 0.12) 10%,
|
||||
transparent 10% 90%,
|
||||
rgba(15, 23, 42, 0.12) 90%
|
||||
),
|
||||
linear-gradient(135deg, rgba(239, 246, 255, 0.9), rgba(219, 234, 254, 0.82));
|
||||
}
|
||||
|
||||
@@ -5884,8 +5926,7 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
|
||||
.image-canvas-editor__generation-frame--audio-sound-effect,
|
||||
.image-canvas-editor__generation-frame--audio-background-music {
|
||||
background:
|
||||
repeating-linear-gradient(
|
||||
background: repeating-linear-gradient(
|
||||
90deg,
|
||||
rgba(79, 70, 229, 0.14) 0 0.22rem,
|
||||
transparent 0.22rem 0.58rem
|
||||
@@ -6091,8 +6132,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
|
||||
.image-canvas-editor__generation-frame--quick-edit {
|
||||
border-color: var(--image-canvas-brand-border);
|
||||
background:
|
||||
linear-gradient(135deg, rgba(204, 117, 76, 0.12), rgba(240, 203, 169, 0.12)),
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
rgba(204, 117, 76, 0.12),
|
||||
rgba(240, 203, 169, 0.12)
|
||||
),
|
||||
rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
@@ -6109,8 +6153,11 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
|
||||
.image-canvas-editor__generation-frame--publication {
|
||||
border-color: rgba(236, 72, 153, 0.28);
|
||||
background:
|
||||
radial-gradient(circle at 30% 20%, rgba(244, 114, 182, 0.18), transparent 38%),
|
||||
background: radial-gradient(
|
||||
circle at 30% 20%,
|
||||
rgba(244, 114, 182, 0.18),
|
||||
transparent 38%
|
||||
),
|
||||
linear-gradient(135deg, rgba(236, 72, 153, 0.12), rgba(251, 146, 60, 0.08)),
|
||||
rgba(255, 247, 252, 0.78);
|
||||
}
|
||||
@@ -6311,7 +6358,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
.image-canvas-editor__floating-toolbar button.image-canvas-editor__floating-toolbar-text-button {
|
||||
.image-canvas-editor__floating-toolbar
|
||||
button.image-canvas-editor__floating-toolbar-text-button {
|
||||
width: auto;
|
||||
min-width: 0;
|
||||
gap: 0.35rem;
|
||||
@@ -8779,8 +8827,11 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
z-index: 999;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--image-canvas-brand-accent);
|
||||
background:
|
||||
linear-gradient(90deg, rgba(204, 117, 76, 0.2) 1px, transparent 1px),
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(204, 117, 76, 0.2) 1px,
|
||||
transparent 1px
|
||||
),
|
||||
linear-gradient(rgba(204, 117, 76, 0.2) 1px, transparent 1px);
|
||||
background-size: 33.333% 33.333%;
|
||||
box-shadow:
|
||||
@@ -9335,7 +9386,8 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.image-canvas-editor__floating-toolbar button.image-canvas-editor__floating-toolbar-text-button {
|
||||
.image-canvas-editor__floating-toolbar
|
||||
button.image-canvas-editor__floating-toolbar-text-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@@ -9632,8 +9684,11 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
}
|
||||
|
||||
.platform-mobile-home-welcome-overlay {
|
||||
background:
|
||||
radial-gradient(circle at 50% 18%, rgba(255, 254, 252, 0.54), transparent 32%),
|
||||
background: radial-gradient(
|
||||
circle at 50% 18%,
|
||||
rgba(255, 254, 252, 0.54),
|
||||
transparent 32%
|
||||
),
|
||||
rgba(61, 31, 16, 0.24);
|
||||
}
|
||||
|
||||
@@ -9642,8 +9697,11 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
overflow: hidden;
|
||||
border-radius: 1.65rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.68);
|
||||
background:
|
||||
radial-gradient(circle at 18% 0%, rgba(240, 203, 169, 0.28), transparent 38%),
|
||||
background: radial-gradient(
|
||||
circle at 18% 0%,
|
||||
rgba(240, 203, 169, 0.28),
|
||||
transparent 38%
|
||||
),
|
||||
var(--platform-modal-fill);
|
||||
box-shadow:
|
||||
0 24px 58px rgba(112, 57, 30, 0.2),
|
||||
@@ -12521,7 +12579,8 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
padding: 0 !important;
|
||||
background:
|
||||
linear-gradient(rgba(255, 250, 244, 0.72), rgba(255, 250, 244, 0.72)),
|
||||
url('/creation-home/background-clay-workspace.png') center top / 64rem 64rem repeat,
|
||||
url('/creation-home/background-clay-workspace.png') center top / 64rem 64rem
|
||||
repeat,
|
||||
linear-gradient(180deg, #fffaf4 0%, #fffdf9 52%, #fff8f0 100%);
|
||||
box-shadow: 0 24px 58px rgba(112, 57, 30, 0.08);
|
||||
}
|
||||
@@ -12656,7 +12715,7 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
width: 1px;
|
||||
height: 1.66rem;
|
||||
background: rgba(217, 190, 166, 0.82);
|
||||
content: "";
|
||||
content: '';
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
@@ -12691,7 +12750,9 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.platform-desktop-shell--workbench .platform-desktop-topbar button.platform-desktop-search {
|
||||
.platform-desktop-shell--workbench
|
||||
.platform-desktop-topbar
|
||||
button.platform-desktop-search {
|
||||
width: auto;
|
||||
max-width: none !important;
|
||||
min-width: 7.35rem;
|
||||
@@ -12702,12 +12763,17 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
gap: 0.38rem !important;
|
||||
}
|
||||
|
||||
.platform-desktop-shell--workbench .platform-desktop-topbar button.platform-desktop-search:hover {
|
||||
.platform-desktop-shell--workbench
|
||||
.platform-desktop-topbar
|
||||
button.platform-desktop-search:hover {
|
||||
border-color: rgba(214, 184, 159, 0.52);
|
||||
background: rgba(255, 250, 244, 0.56);
|
||||
}
|
||||
|
||||
.platform-desktop-shell--workbench .platform-desktop-topbar button.platform-desktop-search > span:first-child {
|
||||
.platform-desktop-shell--workbench
|
||||
.platform-desktop-topbar
|
||||
button.platform-desktop-search
|
||||
> span:first-child {
|
||||
width: 2.06rem;
|
||||
height: 2.06rem;
|
||||
}
|
||||
@@ -12750,12 +12816,16 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.platform-desktop-shell--workbench .platform-desktop-rail__button--active .platform-desktop-rail__icon-shell {
|
||||
.platform-desktop-shell--workbench
|
||||
.platform-desktop-rail__button--active
|
||||
.platform-desktop-rail__icon-shell {
|
||||
background: #fff2e7;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.platform-desktop-shell--workbench .platform-desktop-rail__button--active .platform-desktop-rail__icon {
|
||||
.platform-desktop-shell--workbench
|
||||
.platform-desktop-rail__button--active
|
||||
.platform-desktop-rail__icon {
|
||||
color: #c7653d;
|
||||
}
|
||||
|
||||
@@ -12795,7 +12865,9 @@ button.image-canvas-editor__reference-chip:disabled {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.platform-desktop-shell--workbench .platform-desktop-rail__button--active .platform-desktop-rail__label {
|
||||
.platform-desktop-shell--workbench
|
||||
.platform-desktop-rail__button--active
|
||||
.platform-desktop-rail__label {
|
||||
color: #c7653d;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user