{
+ onScrollLeftChange(event.currentTarget.scrollLeft);
+ }}
+ >
{chart.buckets.map((bucket) => (
@@ -450,6 +540,13 @@ function formatNumber(value: number) {
return new Intl.NumberFormat('zh-CN').format(value);
}
+function formatRateBasisPoints(value: number) {
+ return new Intl.NumberFormat('zh-CN', {
+ style: 'percent',
+ maximumFractionDigits: 2,
+ }).format(value / 10_000);
+}
+
function formatBeijingDateInput(date: Date) {
return new Intl.DateTimeFormat('en-CA', {
timeZone: 'Asia/Shanghai',
@@ -498,9 +595,7 @@ function buildWeekDateRange(dateValue: string) {
const weekday = date.getUTCDay() || 7;
const monday = new Date(date);
monday.setUTCDate(date.getUTCDate() - weekday + 1);
- const sunday = new Date(monday);
- sunday.setUTCDate(monday.getUTCDate() + 6);
- return { startDate: formatUtcDate(monday), endDate: formatUtcDate(sunday) };
+ return { startDate: formatUtcDate(monday), endDate: dateValue };
}
function buildMonthDateRange(dateValue: string) {
@@ -512,12 +607,9 @@ function buildMonthDateRange(dateValue: string) {
const firstDate = new Date(
Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), 1),
);
- const lastDate = new Date(
- Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + 1, 0),
- );
return {
startDate: formatUtcDate(firstDate),
- endDate: formatUtcDate(lastDate),
+ endDate: dateValue,
};
}
diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx
index c0cca000c..9d360b81c 100644
--- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx
+++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.test.tsx
@@ -148,6 +148,10 @@ const generatedAsset: AdminEditorAssetPayload = {
generationCostMudPoints: 12,
createdAt: '2026-07-04T10:00:00Z',
updatedAt: '2026-07-04T10:00:00Z',
+ generator: '角色抠图',
+ taskGenerator: '角色生成',
+ taskCostMudPoints: 12,
+ children: [],
};
beforeEach(() => {
@@ -245,6 +249,80 @@ test('后台素材查询不展示分类筛选和分类列', async () => {
expect(screen.queryByRole('columnheader', { name: '分类' })).toBeNull();
});
+test('后台素材查询将中间产物按真实类型逐行收纳在最终产物下', async () => {
+ const providerSource: AdminEditorAssetPayload = {
+ ...generatedAsset,
+ assetId: 'asset-source',
+ label: '角色形象生成原图',
+ objectKey:
+ 'generated-character-drafts/editor/character-images/provider-source-task-1.png',
+ imageSrc:
+ '/generated-character-drafts/editor/character-images/provider-source-task-1.png',
+ assetKind: 'character',
+ generator: '角色生图',
+ taskGenerator: '角色生成',
+ generationCostMudPoints: 12,
+ taskCostMudPoints: 12,
+ children: [],
+ };
+ const postprocessOutput: AdminEditorAssetPayload = {
+ ...generatedAsset,
+ assetId: 'asset-postprocess',
+ label: '角色抠图阶段产物',
+ objectKey:
+ 'generated-character-drafts/editor/character-images/background-removal-task-1.png',
+ imageSrc:
+ '/generated-character-drafts/editor/character-images/background-removal-task-1.png',
+ assetKind: 'character',
+ generator: '角色抠图',
+ taskGenerator: '角色生成',
+ generationCostMudPoints: 0,
+ taskCostMudPoints: 0,
+ children: [],
+ };
+ vi.mocked(listAdminEditorAssets).mockResolvedValueOnce({
+ entries: [
+ {
+ ...generatedAsset,
+ generationCostMudPoints: 0,
+ taskCostMudPoints: 12,
+ children: [providerSource, postprocessOutput],
+ },
+ ],
+ nextCursor: null,
+ });
+
+ render(
+
,
+ );
+
+ const finalRow = (await screen.findByText('角色形象 1')).closest('tr');
+ expect(finalRow).not.toBeNull();
+ expect(within(finalRow!).getByText('12 泥点')).toBeTruthy();
+ expect(screen.queryByText('角色形象生成原图')).toBeNull();
+ expect(screen.queryByText('角色抠图阶段产物')).toBeNull();
+
+ fireEvent.click(
+ within(finalRow!).getByRole('button', { name: '展开中间产物' }),
+ );
+
+ const sourceRow = (await screen.findByText('角色形象生成原图')).closest('tr');
+ const postprocessRow = screen.getByText('角色抠图阶段产物').closest('tr');
+ expect(sourceRow).not.toBeNull();
+ expect(postprocessRow).not.toBeNull();
+ expect(within(sourceRow!).getByText('角色生图')).toBeTruthy();
+ expect(within(sourceRow!).getByText('12 泥点')).toBeTruthy();
+ expect(within(postprocessRow!).getByText('角色抠图')).toBeTruthy();
+ expect(
+ within(postprocessRow!).getByText('本阶段不额外扣费'),
+ ).toBeTruthy();
+
+ fireEvent.click(within(sourceRow!).getByRole('button', { name: '详情' }));
+ const detail = await screen.findByRole('dialog', { name: '素材详情' });
+ expect(within(detail).getByText('角色生图')).toBeTruthy();
+ expect(within(detail).getByText('12 泥点')).toBeTruthy();
+});
+
test('后台素材查询缩略图使用 objectKey 换签后展示', async () => {
render(
,
diff --git a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx
index 5bb5d25fa..e23a00aae 100644
--- a/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx
+++ b/apps/admin-web/src/pages/AdminEditorAssetQueryPage.tsx
@@ -1,5 +1,5 @@
-import { Eye, FileText, RefreshCcw, X } from 'lucide-react';
-import type { ReactNode } from 'react';
+import { ChevronDown, ChevronRight, Eye, FileText, RefreshCcw, X } from 'lucide-react';
+import { Fragment, type ReactNode } from 'react';
import { useCallback, useEffect, useState } from 'react';
import {
@@ -44,6 +44,9 @@ export function AdminEditorAssetQueryPage({
useState
(null);
const [previewEntry, setPreviewEntry] =
useState(null);
+ const [expandedTaskIds, setExpandedTaskIds] = useState>(
+ () => new Set(),
+ );
const [promptPreview, setPromptPreview] = useState<{
title: string;
prompt: string;
@@ -149,7 +152,7 @@ export function AdminEditorAssetQueryPage({