收口平台报告弹窗

新增 PlatformReportDialog 公共可复制报告弹窗组件
将 PlatformErrorDialog 与 PlatformTaskCompletionDialog 改为薄包装
同步更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
2026-06-10 15:50:11 +08:00
parent ebf181d53b
commit 7411b9a435
6 changed files with 189 additions and 113 deletions

View File

@@ -0,0 +1,59 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen, waitFor, within } from '@testing-library/react';
import { afterEach, expect, test, vi } from 'vitest';
import * as clipboardService from '../../services/clipboard';
import { PlatformReportDialog } from './PlatformReportDialog';
vi.mock('../../services/clipboard', () => ({
copyTextToClipboard: vi.fn(),
}));
afterEach(() => {
vi.clearAllMocks();
});
test('renders report fields and copies the joined report lines', async () => {
vi.mocked(clipboardService.copyTextToClipboard).mockResolvedValue(true);
render(
<PlatformReportDialog
open
title="统一报告"
onClose={() => {}}
copyIdleLabel="复制内容"
fields={[
{ label: '来源', value: '拼图草稿 puzzle-session-1' },
{ label: '状态', value: '已完成', multiline: true },
]}
/>,
);
const dialog = screen.getByRole('dialog', { name: '统一报告' });
expect(within(dialog).getByText('拼图草稿 puzzle-session-1')).toBeTruthy();
expect(within(dialog).getByText('已完成')).toBeTruthy();
fireEvent.click(within(dialog).getByRole('button', { name: '复制内容' }));
expect(clipboardService.copyTextToClipboard).toHaveBeenCalledWith(
['来源:拼图草稿 puzzle-session-1', '状态:已完成'].join('\n'),
);
await waitFor(() => {
expect(within(dialog).getByRole('button', { name: '已复制' })).toBeTruthy();
});
});
test('does not render report fields when closed', () => {
render(
<PlatformReportDialog
open={false}
title="统一报告"
onClose={() => {}}
copyIdleLabel="复制内容"
fields={[]}
/>,
);
expect(screen.queryByRole('dialog', { name: '统一报告' })).toBeNull();
});

View File

@@ -0,0 +1,87 @@
import { useEffect, useMemo } from 'react';
import { CopyFeedbackButton } from './CopyFeedbackButton';
import { PlatformInfoBlock } from './PlatformInfoBlock';
import { UnifiedModal } from './UnifiedModal';
import { useCopyFeedback } from './useCopyFeedback';
export type PlatformReportDialogField = {
label: string;
value: string;
multiline?: boolean;
};
type PlatformReportDialogProps = {
open: boolean;
title: string;
onClose: () => void;
copyIdleLabel: string;
fields: readonly PlatformReportDialogField[];
overlayClassName?: string;
panelClassName?: string;
};
function buildPlatformReportText(fields: readonly PlatformReportDialogField[]) {
return fields.map((field) => `${field.label}${field.value}`).join('\n');
}
export function PlatformReportDialog({
open,
title,
onClose,
copyIdleLabel,
fields,
overlayClassName = 'platform-theme platform-theme--light !items-center',
panelClassName = 'platform-remap-surface rounded-[1.5rem]',
}: PlatformReportDialogProps) {
const { copyState, copyText, resetCopyState } = useCopyFeedback();
const reportText = useMemo(() => buildPlatformReportText(fields), [fields]);
useEffect(() => {
resetCopyState();
}, [open, reportText, resetCopyState]);
const copyReport = () => {
if (!reportText) {
return;
}
void copyText(reportText);
};
return (
<UnifiedModal
open={open}
title={title}
onClose={onClose}
size="sm"
overlayClassName={overlayClassName}
panelClassName={panelClassName}
bodyClassName="space-y-3 px-4 py-4 sm:px-5 sm:py-5"
footerClassName="justify-end px-4 py-4 sm:px-5"
footer={
<CopyFeedbackButton
state={copyState}
onClick={copyReport}
disabled={!reportText}
idleLabel={copyIdleLabel}
actionSurface="platform"
actionFullWidth
className="sm:w-auto"
/>
}
>
{open
? fields.map((field) => (
<PlatformInfoBlock
key={`${field.label}-${field.value}`}
label={field.label}
multiline={field.multiline}
>
{field.value}
</PlatformInfoBlock>
))
: null}
</UnifiedModal>
);
}

View File

@@ -1,9 +1,4 @@
import { useEffect, useMemo } from 'react';
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
import { PlatformInfoBlock } from '../common/PlatformInfoBlock';
import { UnifiedModal } from '../common/UnifiedModal';
import { useCopyFeedback } from '../common/useCopyFeedback';
import { PlatformReportDialog } from '../common/PlatformReportDialog';
export type PlatformErrorDialogPayload = {
source: string;
@@ -17,10 +12,6 @@ type PlatformErrorDialogProps = {
panelClassName?: string;
};
function buildPlatformErrorReport(error: PlatformErrorDialogPayload) {
return [`来源:${error.source}`, `错误:${error.message}`].join('\n');
}
function isBlacklistedPlatformError(error: PlatformErrorDialogPayload | null) {
// 中文注释:入口关闭是平台开关状态,不作为全局错误弹窗打扰用户。
return Boolean(error?.message.includes('creation_entry_disabled'));
@@ -32,57 +23,31 @@ export function PlatformErrorDialog({
overlayClassName = 'platform-theme platform-theme--light !items-center',
panelClassName = 'platform-remap-surface rounded-[1.5rem]',
}: PlatformErrorDialogProps) {
const { copyState, copyText, resetCopyState } = useCopyFeedback();
const dialogError = isBlacklistedPlatformError(error) ? null : error;
const reportText = useMemo(
() => (dialogError ? buildPlatformErrorReport(dialogError) : ''),
[dialogError],
);
useEffect(() => {
resetCopyState();
}, [dialogError?.source, dialogError?.message, resetCopyState]);
const copyError = () => {
if (!reportText) {
return;
}
void copyText(reportText);
};
return (
<UnifiedModal
<PlatformReportDialog
open={Boolean(dialogError)}
title="发生错误"
onClose={onClose}
size="sm"
copyIdleLabel="复制报错"
fields={
dialogError
? [
{
label: '来源',
value: dialogError.source,
},
{
label: '错误',
value: dialogError.message,
multiline: true,
},
]
: []
}
overlayClassName={overlayClassName}
panelClassName={panelClassName}
bodyClassName="space-y-3 px-4 py-4 sm:px-5 sm:py-5"
footerClassName="justify-end px-4 py-4 sm:px-5"
footer={
<CopyFeedbackButton
state={copyState}
onClick={copyError}
disabled={!reportText}
idleLabel="复制报错"
actionSurface="platform"
actionFullWidth
className="sm:w-auto"
/>
}
>
{dialogError ? (
<>
<PlatformInfoBlock label="来源">
{dialogError.source}
</PlatformInfoBlock>
<PlatformInfoBlock label="错误" multiline>
{dialogError.message}
</PlatformInfoBlock>
</>
) : null}
</UnifiedModal>
/>
);
}

View File

@@ -1,9 +1,4 @@
import { useEffect, useMemo } from 'react';
import { CopyFeedbackButton } from '../common/CopyFeedbackButton';
import { PlatformInfoBlock } from '../common/PlatformInfoBlock';
import { UnifiedModal } from '../common/UnifiedModal';
import { useCopyFeedback } from '../common/useCopyFeedback';
import { PlatformReportDialog } from '../common/PlatformReportDialog';
export type PlatformTaskCompletionDialogPayload = {
source: string;
@@ -17,70 +12,35 @@ type PlatformTaskCompletionDialogProps = {
panelClassName?: string;
};
function buildPlatformTaskCompletionReport(
completion: PlatformTaskCompletionDialogPayload,
) {
return [`来源:${completion.source}`, `状态:${completion.message}`].join(
'\n',
);
}
export function PlatformTaskCompletionDialog({
completion,
onClose,
overlayClassName = 'platform-theme platform-theme--light !items-center',
panelClassName = 'platform-remap-surface rounded-[1.5rem]',
}: PlatformTaskCompletionDialogProps) {
const { copyState, copyText, resetCopyState } = useCopyFeedback();
const reportText = useMemo(
() => (completion ? buildPlatformTaskCompletionReport(completion) : ''),
[completion],
);
useEffect(() => {
resetCopyState();
}, [completion?.source, completion?.message, resetCopyState]);
const copyCompletion = () => {
if (!reportText) {
return;
}
void copyText(reportText);
};
return (
<UnifiedModal
<PlatformReportDialog
open={Boolean(completion)}
title="生成完成"
onClose={onClose}
size="sm"
copyIdleLabel="复制内容"
fields={
completion
? [
{
label: '来源',
value: completion.source,
},
{
label: '状态',
value: completion.message,
multiline: true,
},
]
: []
}
overlayClassName={overlayClassName}
panelClassName={panelClassName}
bodyClassName="space-y-3 px-4 py-4 sm:px-5 sm:py-5"
footerClassName="justify-end px-4 py-4 sm:px-5"
footer={
<CopyFeedbackButton
state={copyState}
onClick={copyCompletion}
disabled={!reportText}
idleLabel="复制内容"
actionSurface="platform"
actionFullWidth
className="sm:w-auto"
/>
}
>
{completion ? (
<>
<PlatformInfoBlock label="来源">
{completion.source}
</PlatformInfoBlock>
<PlatformInfoBlock label="状态" multiline>
{completion.message}
</PlatformInfoBlock>
</>
) : null}
</UnifiedModal>
/>
);
}