收口平台报告弹窗

新增 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

@@ -106,6 +106,7 @@
- 2026-06-10 追加:`CopyFeedbackButton` 支持 `actionAppearance="pill"``CopyCodeButton` 透传同一入口,并复用 `platformPillBadgeModel.ts``getPlatformPillBadgeClassName` 视觉 chrome可点击复制 / 分享胶囊 chip 不再在业务 JSX 中手写 `platform-pill`RPG 世界详情作品号复制 / 分享入口和抓大鹅批量新增 / 重生成物品名称预览已迁移。
- 2026-06-10 追加:平台作品详情页主题标签使用 `PlatformPillBadge tone="neutralSolid" size="sm"`,作品号复制按钮使用 `CopyCodeButton actionAppearance="pill" actionPillTone="neutralSolid" actionPillSize="sm"`;详情页只保留标签映射、作品号复制状态和顶部外边距,不再手写 `platform-work-detail__chip / code` 基础 chrome。验证命令`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformPillBadge.test.tsx src/components/common/CopyCodeButton.test.tsx`
- 2026-06-10 追加:平台作品详情页分享复制反馈使用 `PlatformStatusMessage surface="platform"`,按 `shareState` 映射 `success / error`;详情页保留 `useCopyFeedback` 状态机和文案,不再让失败态复用成功 toast chrome。验证命令`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformStatusMessage.test.tsx`
- 2026-06-10 追加:平台错误弹窗和生成完成弹窗的“字段展示 + 复制整段报告”能力统一收口到 `src/components/common/PlatformReportDialog.tsx``PlatformErrorDialog``PlatformTaskCompletionDialog` 只保留标题、字段语义和错误黑名单过滤,不再各自组合 `UnifiedModal``PlatformInfoBlock``CopyFeedbackButton``useCopyFeedback`。验证命令:`npm run test -- src/components/common/PlatformReportDialog.test.tsx src/components/platform-entry/PlatformErrorDialog.test.tsx src/components/platform-entry/PlatformTaskCompletionDialog.test.tsx`
- 2026-06-10 追加:`PlatformPillBadge` 支持 `darkSoft` / `darkNeutral` / `darkSky` / `darkEmerald` / `darkAmber` / `darkRose` 暗色 tone用于 RPG 暗色弹窗和角色详情里的纯展示 chip角色身份 / 等级、技能列表出手方式、技能详情方式 / 风格 / 状态标签、地图节点方向标签、地图场景切换方向标签和营地编组状态数值已迁移。暗色动作按钮、runtime HUD、属性加成动态 pill 和按钮内部消耗 chip 暂不直接套静态 badge。
- 2026-06-10 追加:背景故事已解锁 / 需好感状态和好感等级 badge 也使用 `PlatformPillBadge``dark*` tone好感进度时间轴刻度、runtime HUD 和带点击卡片视觉的标签仍保留专用布局。
- 2026-06-10 追加RPG 角色资产工作室动作列表的生成中 / 已生成 / 待生成状态 chip 直接使用 `PlatformPillBadge``darkAmber` / `darkEmerald` / `darkNeutral` tone父弹窗不再维护本地 `StatusBadge` 浅封装,动作生成按钮仍保留工作室专用暗色按钮布局。

View File

@@ -29,6 +29,7 @@
- 新增 `src/components/common/PlatformPillBadge.tsx` 作为平台胶囊状态标签 Module统一承载结果页、作品卡和配置摘要里的单个状态 / 标签 chip。
- 新增 `src/components/common/PlatformProgressBar.tsx` 作为平台进度条 Module统一承载 `progressbar` 语义、`platform-progress-track` 壳、填充宽度、最小可见宽度、未知进度语义、条内覆盖层和局部主题色。
- 新增 `src/components/common/PlatformInfoBlock.tsx` 作为平台只读信息块 Module统一承载弹窗和详情页中的短标签、白底内容壳、单行 / 多行正文排版。
- 新增 `src/components/common/PlatformReportDialog.tsx` 作为平台可复制报告弹窗 Module统一承载来源 / 状态 / 错误这类字段块展示、报告拼装、复制反馈按钮和标准 footer。
- 新增 `src/components/common/PlatformSubpanel.tsx` 作为平台白底子面板 Module统一承载结果页、创作工作台和普通白底面板内的小型列表卡片里的 `platform-subpanel` / flat 外壳、标题行、右侧动作区、圆角、响应式内边距和交互态。
- 新增 `src/components/common/PlatformMediaFrame.tsx` 作为平台媒体预览框 Module统一承载图片源、fallback 图、fallback 文案、固定比例、surface 和可选 overlay。
- 新增 `src/components/common/PlatformMediaTileGrid.tsx` 作为平台媒体缩略格网格 Module统一承载结果页里同尺寸素材 tile 的列数、间距、白底容器、圆角、边框、图片和 fallback 格。
@@ -184,6 +185,7 @@
18.4. 平台白底分段 Tab / 二选一 / 四选一配置项迁移到 `PlatformSegmentedTabs`;拼图结果页、抓大鹅结果页、抓大鹅素材配置、抓大鹅创作 / 结果页难度选择、视觉小说结果页和 creative-agent 模板确认弹窗已先迁移。后续同类控件只传选项、当前 id、变更回调、列数、尺寸、调性和外壳形态不再在业务 JSX 中重复容器边框、`bg-white/62`、选中态和 `aria-pressed`
18.4.1. `PlatformSegmentedTabs` 支持 `semantics="tabs"``tone="underline"``size="tab"``columns="one"`,用于承接认证入口短信 / 密码登录切换这类真实 Tab 语义;业务页不再维护本地 `LoginTabButton``role="tab"``aria-selected` 和下划线选中态。
18.5. 平台只读信息块迁移到 `PlatformInfoBlock`;错误弹窗和生成完成弹窗的来源、错误和状态展示、分享弹窗正文,以及汪汪声浪预览卡场景 / 形象 / 难度 / 声浪信息行已先迁移。后续弹窗、详情页或预览卡里只是展示短标签 + 只读正文,或无标签纯只读正文时,优先使用该 Module横向信息行通过 `labelClassName` / `valueClassName` 保留标签和值排版,不在业务 JSX 中重复白底信息块 chrome。
18.5.1. 平台来源 / 状态 / 错误这类可复制报告弹窗迁移到 `PlatformReportDialog``PlatformErrorDialog``PlatformTaskCompletionDialog` 已先迁移,业务弹窗只保留标题、字段语义和黑名单过滤,不再重复维护 `UnifiedModal``CopyFeedbackButton``useCopyFeedback`、报告拼装和 `PlatformInfoBlock` footer 组合。后续同类“字段展示 + 复制整段报告”弹窗优先复用该 Module。
18.6. 平台统计小卡和轻量状态 chip 迁移到 `PlatformStatGrid`;拼消消结果页素材摘要、方洞结果页封面状态 chip、抓大鹅结果页难度摘要、creative-agent 模板确认摘要和自定义世界实体目录世界页档案规模已先迁移。后续结果页里只表达数值 / 标签摘要时,优先传 `items`、列数、密度、surface 和 label/value 顺序,不再在业务 JSX 中重复手写统计卡 chrome。
18.6.1. 平台普通进度条迁移到 `PlatformProgressBar`creation-agent 主进度 / operation banner、RPG 结果页生成提示、RPG 实体目录生成中提示、开场 CG 生成占位、拼图关卡画面生成进度、生成页当前步骤线性进度、抓大鹅批量物品素材生成进度和自定义世界生成选择弹窗进度提示已先迁移。creation-agent operation banner 的状态外壳也迁移到 `PlatformStatusMessage surface="platform" remapSurface`,避免业务 JSX 继续组合 `platform-remap-surface platform-banner``platform-banner--*`。后续生成进度、素材进度或实体目录进度只保留进度值、显示文案、主题色、必要覆盖层和业务状态,不再重复写 `role="progressbar"``platform-progress-track`、fill 宽度和最小可见宽度计算;未知进度用 `indeterminate`。生成页环形总进度继续保留 `GenerationProgressHero` 专用 SVG。
18.6.2. 平台单个胶囊状态 / 标签 chip 迁移到 `PlatformPillBadge`;宝贝识物结果页发布状态、主题标签与占位资源 overlay宝贝识物 / 拼图 / 抓大鹅 / 视觉小说工作台 BETA chip、汪汪声浪轻配置 chip、汪汪声浪结果页草稿 chip、汪汪声浪预览 VS chip、敲木鱼结果页飘字 chip、creative-agent 顶部阶段 / 过程计数 / 条目 meta chip、通用音频输入面板限制标签、自定义世界实体目录批量选择 / 生成中 / 开局 CG / 可扮演角色元信息 badge、RPG 首页公开作品卡 / 搜索结果 / 充值商品 / 移动端创建入口 / 桌面发现区 chip、RPG 世界详情静态元信息 chip、平台作品详情主题标签、RPG 角色身份 / 等级 / 技能出手方式 / 技能详情与状态标签 / 背景故事解锁状态 / 好感等级 / 角色资产工作室动作状态 / 角色编辑技能动作状态 / 角色资源应用状态 / 场景角色选择状态 / 地标当前连接状态 / 地图节点当前状态 / 地图节点方向标签 / 地图场景切换方向标签 / 作品封面来源状态 / 开局物品标签、NPC 交易数量 / 赠礼好感和背包工坊材料需求等暗色展示 chip、抓大鹅批量新增 / 批量重生成物品名称预览 chip、抓大鹅 / RPG / 拼图 / 方洞结果页自动保存状态、抓大鹅结果页当前难度 badge、拼图结果页关卡生成中 overlay / 列表 badge、大鱼吃小鱼结果页终局 / 关卡元信息 / 发布校验成功 badge、RPG 开发资产诊断数量 / 加载状态 badge、RPG 发布弹窗封面来源 badge、账号弹窗主题状态 / 会话数量 / 设备状态 badge、汪汪声浪生成页和通用生成页右上状态 badge、创作类型弹层锁定 badge、通用创作图片面板提交按钮内泥点消耗标签以及个人中心泥点账单余额、玩过总时长和玩过作品类型 chip 已先迁移。后续只表达一个状态、标签、分类 chip 或按钮内消耗小胶囊时使用该 Module不在业务 JSX 中重复拼 `rounded-full border bg-* text-* px-* py-*`;个人中心玫瑰色 chip 使用 `tone="profile"` / `tone="profileAccent"`RPG 暗色展示 chip 使用 `dark*` tone密集目录元信息用 `size="xxs"`,平台白底柔和状态使用 `tone="muted"`,实心中性详情标签使用 `tone="neutralSolid"`,按钮内浅色叠层使用 `tone="lightOverlay"`,多项统计摘要继续使用 `PlatformStatGrid`。可点击复制 / 分享胶囊 chip 继续由 `CopyCodeButton` / `CopyFeedbackButton` 管复制状态,并通过 `actionAppearance="pill"` 复用 `PlatformPillBadge` chrome。
@@ -219,6 +221,8 @@
## 验证
- `npm run test -- src/components/common/PlatformReportDialog.test.tsx src/components/platform-entry/PlatformErrorDialog.test.tsx`
- `npm run test -- src/components/common/PlatformReportDialog.test.tsx src/components/platform-entry/PlatformTaskCompletionDialog.test.tsx`
- `npm run test -- src/components/common/UnifiedConfirmDialog.test.tsx`
- `npm run test -- src/components/common/useCopyFeedback.test.tsx`
- `npm run test -- src/components/common/CopyFeedbackButton.test.tsx`

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>
/>
);
}