修复陶泥儿精选点赞刷新状态
Project CI / Repository checks (pull_request) Successful in 1m20s
Project CI / Frontend tests (pull_request) Successful in 2m59s
Project CI / Backend tests (pull_request) Successful in 4m7s
Project CI / Native shell tests (pull_request) Successful in 14m46s

新增 viewer 点赞读写投影并保留服务身份鉴权
公开接口返回权威 viewerLiked 并禁止个性化响应缓存
前端刷新和账号切换后重载点赞状态并丢弃过期响应
删除旧点赞返回 procedure 与 facade 并同步生成绑定和文档
补充点赞持久化、鉴权、竞态及失败状态测试
This commit is contained in:
2026-08-10 13:20:47 +08:00
parent bcff8f1957
commit 95fa252933
26 changed files with 1154 additions and 123 deletions
@@ -679,6 +679,7 @@ describe('CreationLandingView', () => {
toggleEditorShowcaseAssetLikeMock.mockResolvedValueOnce({
showcaseId: 'showcase-character',
likeCount: 4,
viewerLiked: true,
});
renderCreationLanding();
@@ -712,6 +713,287 @@ describe('CreationLandingView', () => {
).toBeTruthy();
});
it('restores the server-provided liked state after remount', async () => {
const likedResource = {
resourceId: 'resource-character',
projectId: 'editor-showcase',
showcaseId: 'showcase-character',
label: '角色英雄',
imageSrc: 'data:image/png;base64,character',
width: 512,
height: 512,
sourceType: 'generated',
prompt: 'character hero prompt',
showcaseCategory: 'characters',
likeCount: 4,
viewerLiked: true,
};
listEditorProjectsMock.mockResolvedValue([]);
listPublicEditorProjectResourcesMock.mockResolvedValue([likedResource]);
const firstRender = renderCreationLanding();
expect(
await screen.findByRole('button', { name: '取消点赞:角色英雄' }),
).toBeTruthy();
firstRender.unmount();
renderCreationLanding();
expect(
await screen.findByRole('button', { name: '取消点赞:角色英雄' }),
).toBeTruthy();
expect(listPublicEditorProjectResourcesMock).toHaveBeenCalledTimes(2);
});
it('waits for the server before changing like state and count', async () => {
const user = userEvent.setup();
let resolveLike!: (value: {
showcaseId: string;
likeCount: number;
viewerLiked: boolean;
}) => void;
const likeResult = new Promise<{
showcaseId: string;
likeCount: number;
viewerLiked: boolean;
}>((resolve) => {
resolveLike = resolve;
});
listEditorProjectsMock.mockResolvedValueOnce([]);
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
{
resourceId: 'resource-character',
projectId: 'editor-showcase',
showcaseId: 'showcase-character',
label: '角色英雄',
imageSrc: 'data:image/png;base64,character',
width: 512,
height: 512,
sourceType: 'generated',
prompt: 'character hero prompt',
likeCount: 3,
viewerLiked: false,
},
]);
toggleEditorShowcaseAssetLikeMock.mockReturnValueOnce(likeResult);
renderCreationLanding();
const likeButton = await screen.findByRole('button', {
name: '点赞:角色英雄',
});
await user.click(likeButton);
expect((likeButton as HTMLButtonElement).disabled).toBe(true);
expect(likeButton.getAttribute('aria-busy')).toBe('true');
expect(screen.getByRole('button', { name: '点赞:角色英雄' })).toBeTruthy();
expect(within(likeButton).getByText('3')).toBeTruthy();
await act(async () => {
resolveLike({
showcaseId: 'showcase-character',
likeCount: 4,
viewerLiked: true,
});
await likeResult;
});
expect(
await screen.findByRole('button', { name: '取消点赞:角色英雄' }),
).toBeTruthy();
expect(screen.getByText('4')).toBeTruthy();
});
it('preserves the prior like state and announces a write failure', async () => {
const user = userEvent.setup();
listEditorProjectsMock.mockResolvedValueOnce([]);
listPublicEditorProjectResourcesMock.mockResolvedValueOnce([
{
resourceId: 'resource-character',
projectId: 'editor-showcase',
showcaseId: 'showcase-character',
label: '角色英雄',
imageSrc: 'data:image/png;base64,character',
width: 512,
height: 512,
sourceType: 'generated',
prompt: 'character hero prompt',
likeCount: 3,
viewerLiked: false,
},
]);
toggleEditorShowcaseAssetLikeMock.mockRejectedValueOnce(
new Error('write failed'),
);
renderCreationLanding();
await user.click(
await screen.findByRole('button', { name: '点赞:角色英雄' }),
);
expect((await screen.findByRole('alert')).textContent).toContain(
'点赞失败,请重试',
);
expect(
screen.getByRole('button', { name: '点赞:角色英雄' }),
).toBeTruthy();
expect(screen.getByText('3')).toBeTruthy();
});
it('does not fall back to an anonymous showcase read while auth is recovering', async () => {
listEditorProjectsMock.mockResolvedValue([]);
const recoveringAuth = createAuthValue({ canAccessProtectedData: false });
const readyAuth = createAuthValue({ canAccessProtectedData: true });
const result = renderCreationLanding({ authValue: recoveringAuth });
await act(async () => Promise.resolve());
expect(listPublicEditorProjectResourcesMock).not.toHaveBeenCalled();
result.rerender(
<AuthUiContext.Provider value={readyAuth}>
<CreationLandingView
onOpenProject={vi.fn()}
onOpenProjects={vi.fn()}
/>
</AuthUiContext.Provider>,
);
await waitFor(() =>
expect(listPublicEditorProjectResourcesMock).toHaveBeenCalledWith({
viewer: 'authenticated',
}),
);
});
it('ignores an old account showcase response after an account switch', async () => {
let resolveOldPage!: (value: unknown[]) => void;
const oldPage = new Promise<unknown[]>((resolve) => {
resolveOldPage = resolve;
});
listEditorProjectsMock.mockResolvedValue([]);
listPublicEditorProjectResourcesMock
.mockReturnValueOnce(oldPage)
.mockResolvedValueOnce([
{
resourceId: 'new-account-resource',
projectId: 'editor-showcase',
label: '新账号素材',
imageSrc: 'data:image/png;base64,new',
width: 512,
height: 512,
sourceType: 'generated',
prompt: 'new account',
viewerLiked: false,
},
]);
const result = renderCreationLanding();
await waitFor(() =>
expect(listPublicEditorProjectResourcesMock).toHaveBeenCalledTimes(1),
);
const nextAuth = createAuthValue({
user: {
...createAuthValue().user!,
id: 'user-2',
publicUserCode: '100002',
},
});
result.rerender(
<AuthUiContext.Provider value={nextAuth}>
<CreationLandingView
onOpenProject={vi.fn()}
onOpenProjects={vi.fn()}
/>
</AuthUiContext.Provider>,
);
expect(await screen.findByText('新账号素材')).toBeTruthy();
await act(async () => {
resolveOldPage([
{
resourceId: 'old-account-resource',
projectId: 'editor-showcase',
label: '旧账号素材',
imageSrc: 'data:image/png;base64,old',
width: 512,
height: 512,
sourceType: 'generated',
prompt: 'old account',
viewerLiked: true,
},
]);
await oldPage;
});
expect(screen.queryByText('旧账号素材')).toBeNull();
expect(screen.getByText('新账号素材')).toBeTruthy();
});
it('ignores an old account like response after an account switch', async () => {
const user = userEvent.setup();
let resolveOldLike!: (value: {
showcaseId: string;
likeCount: number;
viewerLiked: boolean;
}) => void;
const oldLike = new Promise<{
showcaseId: string;
likeCount: number;
viewerLiked: boolean;
}>((resolve) => {
resolveOldLike = resolve;
});
const resource = {
resourceId: 'resource-character',
projectId: 'editor-showcase',
showcaseId: 'showcase-character',
label: '角色英雄',
imageSrc: 'data:image/png;base64,character',
width: 512,
height: 512,
sourceType: 'generated',
prompt: 'character hero prompt',
viewerLiked: false,
};
listEditorProjectsMock.mockResolvedValue([]);
listPublicEditorProjectResourcesMock
.mockResolvedValueOnce([{ ...resource, likeCount: 3 }])
.mockResolvedValueOnce([{ ...resource, likeCount: 10 }]);
toggleEditorShowcaseAssetLikeMock.mockReturnValueOnce(oldLike);
const result = renderCreationLanding();
await user.click(
await screen.findByRole('button', { name: '点赞:角色英雄' }),
);
const nextAuth = createAuthValue({
user: {
...createAuthValue().user!,
id: 'user-2',
publicUserCode: '100002',
},
});
result.rerender(
<AuthUiContext.Provider value={nextAuth}>
<CreationLandingView
onOpenProject={vi.fn()}
onOpenProjects={vi.fn()}
/>
</AuthUiContext.Provider>,
);
const newAccountLikeButton = await screen.findByRole('button', {
name: '点赞:角色英雄',
});
expect(within(newAccountLikeButton).getByText('10')).toBeTruthy();
await act(async () => {
resolveOldLike({
showcaseId: 'showcase-character',
likeCount: 4,
viewerLiked: true,
});
await oldLike;
});
expect(
screen.getByRole('button', { name: '点赞:角色英雄' }),
).toBeTruthy();
expect(within(newAccountLikeButton).getByText('10')).toBeTruthy();
});
it('does not show a like action in the featured asset preview modal', async () => {
const user = userEvent.setup();
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
@@ -1064,6 +1346,7 @@ describe('CreationLandingView', () => {
await waitFor(() =>
expect(listPublicEditorProjectResourcesMock).toHaveBeenLastCalledWith({
cursor: 'cursor-page-2',
viewer: 'authenticated',
}),
);
expect(listPublicEditorProjectResourcesMock).toHaveBeenCalledTimes(2);
@@ -15,6 +15,7 @@ import {
type EditorProjectResourceListPage,
type EditorProjectResourceSnapshot,
type EditorProjectSnapshot,
type EditorPublicShowcaseResourceSnapshot,
type EditorShowcaseCampaignSnapshot,
listEditorProjects,
listPublicEditorProjectResources,
@@ -165,13 +166,22 @@ function normalizeShowcaseResourcePage(
page: EditorProjectResourceListPage | EditorProjectResourceSnapshot[],
): EditorProjectResourceListPage {
return Array.isArray(page)
? { resources: page, nextCursor: null, campaign: null }
? {
resources: page.map((resource) => ({
...resource,
viewerLiked:
(resource as Partial<EditorPublicShowcaseResourceSnapshot>)
.viewerLiked === true,
})),
nextCursor: null,
campaign: null,
}
: page;
}
function mergeShowcaseResources(
current: EditorProjectResourceSnapshot[],
incoming: EditorProjectResourceSnapshot[],
current: EditorPublicShowcaseResourceSnapshot[],
incoming: EditorPublicShowcaseResourceSnapshot[],
) {
const seenResourceIds = new Set(
current.map((resource) => resource.resourceId),
@@ -835,6 +845,13 @@ export function CreationLandingView({
const prefersReducedMotion = usePrefersReducedMotion();
const authUi = useAuthUi();
const isAuthenticated = Boolean(authUi?.user);
const showcaseViewerUserId = authUi?.user?.id.trim() || null;
const canLoadShowcaseViewer =
!showcaseViewerUserId || Boolean(authUi?.canAccessProtectedData);
const showcaseViewerKey = showcaseViewerUserId
? `user:${showcaseViewerUserId}`
: 'anonymous';
const showcaseRequestScope = `${showcaseViewerKey}:${canLoadShowcaseViewer ? 'ready' : 'recovering'}`;
const normalizedSearchKeyword = searchKeyword.trim().toLocaleLowerCase();
const [recentProjects, setRecentProjects] = useState<EditorProjectSnapshot[]>(
[],
@@ -845,7 +862,7 @@ export function CreationLandingView({
const [isCreatingProject, setIsCreatingProject] = useState(false);
const [projectError, setProjectError] = useState<string | null>(null);
const [projectShowcaseResources, setProjectShowcaseResources] = useState<
EditorProjectResourceSnapshot[]
EditorPublicShowcaseResourceSnapshot[]
>([]);
const [showcaseCampaign, setShowcaseCampaign] =
useState<EditorShowcaseCampaignSnapshot | null>(null);
@@ -865,17 +882,27 @@ export function CreationLandingView({
const [focusedShowcaseSequenceCardId, setFocusedShowcaseSequenceCardId] =
useState<string | null>(null);
const [selectedShowcaseIndex, setSelectedShowcaseIndex] = useState(0);
const [likedShowcaseIds, setLikedShowcaseIds] = useState<Set<string>>(
() => new Set(),
);
const [pendingLikeShowcaseIds, setPendingLikeShowcaseIds] = useState<
Set<string>
>(() => new Set());
const [showcaseLikeError, setShowcaseLikeError] = useState<string | null>(
null,
);
const [isPendingFeatureDialogOpen, setIsPendingFeatureDialogOpen] =
useState(false);
const showcaseLoadMoreRef = useRef<HTMLDivElement | null>(null);
const showcaseLoadingMoreCursorRef = useRef<string | null>(null);
const showcaseFailedLoadMoreCursorRef = useRef<string | null>(null);
const showcaseRequestStateRef = useRef({
scope: showcaseRequestScope,
generation: 0,
});
if (showcaseRequestStateRef.current.scope !== showcaseRequestScope) {
showcaseRequestStateRef.current = {
scope: showcaseRequestScope,
generation: showcaseRequestStateRef.current.generation + 1,
};
}
const refreshRecentProjects = useCallback(() => {
if (!isAuthenticated) {
@@ -910,21 +937,44 @@ export function CreationLandingView({
refreshRecentProjects();
}, [refreshRecentProjects]);
const refreshShowcaseResources = useCallback(() => {
useEffect(() => {
const requestState = { ...showcaseRequestStateRef.current };
const isCurrentRequest = () =>
showcaseRequestStateRef.current.scope === requestState.scope &&
showcaseRequestStateRef.current.generation === requestState.generation;
setIsLoadingShowcase(true);
setIsLoadingMoreShowcase(false);
showcaseLoadingMoreCursorRef.current = null;
showcaseFailedLoadMoreCursorRef.current = null;
setProjectShowcaseResources([]);
setShowcaseCampaign(null);
setShowcaseNextCursor(null);
setAssetError(null);
setShowcaseLoadMoreError(null);
listPublicEditorProjectResources()
setShowcaseLikeError(null);
setPendingLikeShowcaseIds(new Set());
setSelectedShowcaseItem(null);
if (!canLoadShowcaseViewer) {
return;
}
listPublicEditorProjectResources({
viewer: showcaseViewerUserId ? 'authenticated' : 'anonymous',
})
.then((page) => {
if (!isCurrentRequest()) {
return;
}
const normalizedPage = normalizeShowcaseResourcePage(page);
setProjectShowcaseResources(normalizedPage.resources);
setShowcaseCampaign(normalizedPage.campaign ?? null);
setShowcaseNextCursor(normalizedPage.nextCursor);
})
.catch((error: unknown) => {
if (!isCurrentRequest()) {
return;
}
setProjectShowcaseResources([]);
setShowcaseCampaign(null);
setShowcaseNextCursor(null);
@@ -933,12 +983,12 @@ export function CreationLandingView({
error instanceof Error ? error.message : '读取陶泥儿精选失败',
);
})
.finally(() => setIsLoadingShowcase(false));
}, []);
useEffect(() => {
refreshShowcaseResources();
}, [refreshShowcaseResources]);
.finally(() => {
if (isCurrentRequest()) {
setIsLoadingShowcase(false);
}
});
}, [canLoadShowcaseViewer, showcaseRequestScope, showcaseViewerUserId]);
const loadMoreShowcaseResources = useCallback(
(options: { force?: boolean } = {}) => {
@@ -956,10 +1006,20 @@ export function CreationLandingView({
return;
}
showcaseLoadingMoreCursorRef.current = cursor;
const requestState = { ...showcaseRequestStateRef.current };
const isCurrentRequest = () =>
showcaseRequestStateRef.current.scope === requestState.scope &&
showcaseRequestStateRef.current.generation === requestState.generation;
setIsLoadingMoreShowcase(true);
setShowcaseLoadMoreError(null);
listPublicEditorProjectResources({ cursor })
listPublicEditorProjectResources({
cursor,
viewer: showcaseViewerUserId ? 'authenticated' : 'anonymous',
})
.then((page) => {
if (!isCurrentRequest()) {
return;
}
const normalizedPage = normalizeShowcaseResourcePage(page);
setProjectShowcaseResources((current) =>
mergeShowcaseResources(current, normalizedPage.resources),
@@ -969,19 +1029,30 @@ export function CreationLandingView({
showcaseFailedLoadMoreCursorRef.current = null;
})
.catch((error: unknown) => {
if (!isCurrentRequest()) {
return;
}
showcaseFailedLoadMoreCursorRef.current = cursor;
setShowcaseLoadMoreError(
error instanceof Error ? error.message : '读取陶泥儿精选失败',
);
})
.finally(() => {
if (!isCurrentRequest()) {
return;
}
if (showcaseLoadingMoreCursorRef.current === cursor) {
showcaseLoadingMoreCursorRef.current = null;
}
setIsLoadingMoreShowcase(false);
});
},
[isLoadingMoreShowcase, isLoadingShowcase, showcaseNextCursor],
[
isLoadingMoreShowcase,
isLoadingShowcase,
showcaseNextCursor,
showcaseViewerUserId,
],
);
useEffect(() => {
@@ -1056,19 +1127,19 @@ export function CreationLandingView({
[createProject],
);
const updateShowcaseLikeCount = useCallback(
(showcaseId: string, likeCount: number) => {
const updateShowcaseViewerState = useCallback(
(showcaseId: string, likeCount: number, viewerLiked: boolean) => {
setProjectShowcaseResources((current) =>
current.map((resource) =>
resource.showcaseId === showcaseId ||
resource.resourceId === showcaseId
? { ...resource, likeCount }
? { ...resource, likeCount, viewerLiked }
: resource,
),
);
setSelectedShowcaseItem((current) =>
current?.showcaseId === showcaseId
? { ...current, likeCount }
? { ...current, likeCount, viewerLiked }
: current,
);
},
@@ -1089,44 +1160,38 @@ export function CreationLandingView({
return;
}
const nextLiked = !likedShowcaseIds.has(showcaseId);
const previousLikeCount = item.likeCount ?? 0;
const optimisticLikeCount = Math.max(
0,
previousLikeCount + (nextLiked ? 1 : -1),
);
const nextLiked = item.viewerLiked !== true;
const requestState = { ...showcaseRequestStateRef.current };
const requestViewerKey = showcaseViewerKey;
const isCurrentRequest = () =>
showcaseRequestStateRef.current.scope === requestState.scope &&
showcaseRequestStateRef.current.generation === requestState.generation &&
requestViewerKey === showcaseViewerKey;
setShowcaseLikeError(null);
setPendingLikeShowcaseIds((current) => new Set(current).add(showcaseId));
setLikedShowcaseIds((current) => {
const next = new Set(current);
if (nextLiked) {
next.add(showcaseId);
} else {
next.delete(showcaseId);
}
return next;
});
updateShowcaseLikeCount(showcaseId, optimisticLikeCount);
toggleEditorShowcaseAssetLike(showcaseId, nextLiked)
.then((record) => {
updateShowcaseLikeCount(
if (!isCurrentRequest()) {
return;
}
updateShowcaseViewerState(
showcaseId,
record.likeCount ?? optimisticLikeCount,
record.likeCount ?? item.likeCount ?? 0,
record.viewerLiked,
);
})
.catch(() => {
setLikedShowcaseIds((current) => {
const next = new Set(current);
if (nextLiked) {
next.delete(showcaseId);
} else {
next.add(showcaseId);
}
return next;
});
updateShowcaseLikeCount(showcaseId, previousLikeCount);
if (isCurrentRequest()) {
setShowcaseLikeError(
nextLiked ? '点赞失败,请重试' : '取消点赞失败,请重试',
);
}
})
.finally(() => {
if (!isCurrentRequest()) {
return;
}
setPendingLikeShowcaseIds((current) => {
const next = new Set(current);
next.delete(showcaseId);
@@ -1134,7 +1199,12 @@ export function CreationLandingView({
});
});
},
[authUi, likedShowcaseIds, pendingLikeShowcaseIds, updateShowcaseLikeCount],
[
authUi,
pendingLikeShowcaseIds,
showcaseViewerKey,
updateShowcaseViewerState,
],
);
const showcaseItems = useMemo(
@@ -1227,6 +1297,11 @@ export function CreationLandingView({
return (
<>
{showcaseLikeError ? (
<PlatformStatusMessage tone="error" surface="platform" role="alert">
{showcaseLikeError}
</PlatformStatusMessage>
) : null}
<div
ref={showcaseMasonryRef}
className="creation-landing__asset-waterfall"
@@ -1235,9 +1310,7 @@ export function CreationLandingView({
>
{visibleShowcaseItems.map((item) => {
const showcaseId = item.showcaseId?.trim();
const isLiked = Boolean(
showcaseId && likedShowcaseIds.has(showcaseId),
);
const isLiked = item.viewerLiked === true;
const isLikePending = Boolean(
showcaseId && pendingLikeShowcaseIds.has(showcaseId),
);
@@ -1302,6 +1375,7 @@ export function CreationLandingView({
: 'creation-landing__asset-like-button'
}
aria-label={`${isLiked ? '取消点赞' : '点赞'}:${item.label}`}
aria-busy={isLikePending}
disabled={isLikePending}
onClick={() => handleToggleShowcaseLike(item)}
>
@@ -44,6 +44,7 @@ export type ShowcaseAssetItem = {
author: string;
cost: string;
likeCount?: number | null;
viewerLiked?: boolean;
showcaseId?: string | null;
campaign?: boolean;
};
@@ -983,6 +984,7 @@ function toLibraryShowcaseItem(
author: resolveGroupAuthor(sortedAssets, fallbackAuthorName),
cost: resolveGroupCost(sortedAssets),
likeCount: normalizeNumber(assetRecord(primaryAsset).likeCount),
viewerLiked: assetRecord(primaryAsset).viewerLiked === true,
showcaseId: normalizeText(assetRecord(primaryAsset).showcaseId),
};
}
@@ -282,6 +282,7 @@ describe('editorProjectClient', () => {
],
imageSequenceDurationMs: 6_000,
publicShowcaseEnabled: true,
viewerLiked: false,
},
],
nextCursor: 'cursor-next',
@@ -306,6 +307,7 @@ describe('editorProjectClient', () => {
expect(page.resources[0]?.authorPublicUserCode).toBe('SY-00000042');
expect(page.resources[0]?.imageSequenceFrames).toHaveLength(2);
expect(page.resources[0]?.imageSequenceDurationMs).toBe(6_000);
expect(page.resources[0]?.viewerLiked).toBe(false);
expect(page.nextCursor).toBe('cursor-next');
expect(page.campaign?.title).toBe('活动精选');
expect(page.campaign?.imageWidth).toBe(900);
@@ -348,6 +350,34 @@ describe('editorProjectClient', () => {
);
});
it('loads viewer-specific showcase state with authentication enabled', async () => {
requestJsonMock.mockResolvedValueOnce({
resources: [
{
resourceId: 'showcase-1',
projectId: 'editor-showcase',
imageSrc: '/showcase-1.png',
width: 512,
height: 512,
sourceType: 'generated',
viewerLiked: true,
},
],
nextCursor: null,
});
const page = await listPublicEditorProjectResources({
viewer: 'authenticated',
});
expect(page.resources[0]?.viewerLiked).toBe(true);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/showcase/resources',
{ method: 'GET' },
'读取陶泥儿精选素材失败',
);
});
it('loads an explicit project by id', async () => {
requestJsonMock.mockResolvedValueOnce({
project: {
@@ -706,6 +736,7 @@ describe('editorProjectClient', () => {
reviewStatus: 'approved',
displayEnabled: true,
likeCount: 8,
viewerLiked: true,
generationCostMudPoints: 20,
refundMudPoints: 10,
},
@@ -717,6 +748,7 @@ describe('editorProjectClient', () => {
);
expect(showcaseAsset.likeCount).toBe(8);
expect(showcaseAsset.viewerLiked).toBe(true);
expect(requestJsonMock).toHaveBeenCalledWith(
'/api/editor/showcase/assets/showcase-asset-1/likes',
expect.objectContaining({
@@ -257,6 +257,11 @@ export type EditorProjectResourceSnapshot = {
updatedAt?: string;
};
export type EditorPublicShowcaseResourceSnapshot =
EditorProjectResourceSnapshot & {
viewerLiked: boolean;
};
export type EditorAssetFolderSnapshot = {
folderId: string;
label: string;
@@ -798,13 +803,13 @@ type EditorProjectResourceResponse = {
};
type EditorProjectResourceListResponse = {
resources: EditorProjectResourceSnapshot[];
resources: EditorPublicShowcaseResourceSnapshot[];
nextCursor?: string | null;
campaign?: EditorShowcaseCampaignSnapshot | null;
};
export type EditorProjectResourceListPage = {
resources: EditorProjectResourceSnapshot[];
resources: EditorPublicShowcaseResourceSnapshot[];
nextCursor: string | null;
campaign?: EditorShowcaseCampaignSnapshot | null;
};
@@ -829,6 +834,10 @@ type EditorShowcaseAssetResponse = {
showcaseAsset: EditorProjectResourceSnapshot;
};
type EditorShowcaseAssetViewerResponse = {
showcaseAsset: EditorPublicShowcaseResourceSnapshot;
};
type EditorImageGenerationResponse = EditorImageGenerationResult;
type EditorBackgroundRemovalResponse = EditorBackgroundRemovalResult;
type EditorPixelArtSnapResponse = EditorPixelArtSnapResult;
@@ -856,8 +865,10 @@ export async function listEditorProjects() {
export async function listPublicEditorProjectResources({
cursor,
viewer = 'anonymous',
}: {
cursor?: string | null;
viewer?: 'anonymous' | 'authenticated';
} = {}): Promise<EditorProjectResourceListPage> {
const params = new URLSearchParams();
if (cursor?.trim()) {
@@ -866,17 +877,24 @@ export async function listPublicEditorProjectResources({
const url = params.toString()
? `${EDITOR_SHOWCASE_RESOURCE_API}?${params.toString()}`
: EDITOR_SHOWCASE_RESOURCE_API;
const response = await requestJson<EditorProjectResourceListResponse>(
url,
{ method: 'GET' },
'读取陶泥儿精选素材失败',
{
skipAuth: true,
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,
},
);
const response =
viewer === 'authenticated'
? await requestJson<EditorProjectResourceListResponse>(
url,
{ method: 'GET' },
'读取陶泥儿精选素材失败',
)
: await requestJson<EditorProjectResourceListResponse>(
url,
{ method: 'GET' },
'读取陶泥儿精选素材失败',
{
skipAuth: true,
skipRefresh: true,
notifyAuthStateChange: false,
clearAuthOnUnauthorized: false,
},
);
return {
resources: response.resources,
nextCursor: response.nextCursor?.trim() || null,
@@ -1097,7 +1115,7 @@ export async function toggleEditorShowcaseAssetLike(
showcaseId: string,
liked: boolean,
) {
const response = await requestJson<EditorShowcaseAssetResponse>(
const response = await requestJson<EditorShowcaseAssetViewerResponse>(
`${EDITOR_SHOWCASE_ASSET_API_BASE}/${encodeURIComponent(showcaseId)}/likes`,
jsonRequest('POST', { liked }),
liked ? '点赞精选素材失败' : '取消点赞精选素材失败',