继续收口轻量共享按钮

将 PlatformTagEditor 标签删除入口改为复用共享图标按钮

将角色选择页重复返回按钮收口到共享暗色动作按钮壳

补充 PlatformUiKit 收口计划与共享决策记录
This commit is contained in:
2026-06-11 04:25:59 +08:00
parent a8012109ae
commit 1b89611c9a
6 changed files with 58 additions and 26 deletions

View File

@@ -61,6 +61,7 @@
- 2026-06-11 追加:`PlatformDarkModalFooter` 不只收动作按钮区,也继续覆盖纯内容 footer`CompanionCampModal.tsx` 底部“营地气氛”区域已改成 `layout="content"` + `padding="roomy"` 的共享 footer frame保留原有文案和卡片布局不再单独手写 `border-t border-white/10 px-5 py-4`
- 2026-06-11 追加:`PlatformProfileModalShell` 继续补齐标准 footer 插槽,直接透传 `UnifiedModal.footer``footerClassName``RpgEntryHomeView.tsx` 的昵称修改弹窗已改成标准 profile footer不再把双按钮动作区手写在 body 末尾。后续个人中心里同类“表单内容 + 底部双按钮”弹窗优先走壳层 footer 接法。
- 2026-06-11 追加:`PlatformProfileModalShell` 的标准 footer 接法继续扩展到单 CTA 表单收尾;`PlatformProfileRewardCodeRedeemModal.tsx` 的兑换按钮已迁到壳层 footerbody 只保留输入和反馈消息。`PlatformAsyncStatePanel` 同日继续扩展到 `PlatformAssetPickerGrid``VisualNovelSavePanel.tsx``AccountModal.tsx` 的账号安全三个子区块;其中公共素材网格继续把 `error` banner 放在状态壳外层,保持错误提示可与加载态或内容并存的原语义。
- 2026-06-11 追加:按钮层继续补齐轻量漏网项。`PlatformTagEditor.tsx` 的标签 chip 删除入口已改成紧凑 `PlatformIconButton`,保留透明背景和原 chip 高度;`RpgEntryCharacterSelectView.tsx` 的两处“返回”按钮统一沉到局部 `CharacterSelectBackButton`,底层委托 `PlatformActionButton surface="editorDark"`。同日 `GenerationProgressHero.tsx` 新增 `GenerationHeaderBackButton``CustomWorldGenerationView.tsx``BarkBattleGeneratingView.tsx` 已开始复用这套暖色生成页返回入口骨架;后续同类轻量返回按钮与 chip 删除按钮优先继续沿共享按钮 + 薄包装的方向推进。
- 2026-06-09 追加:通用输入 Composer 的上传参考图、发送和移除参考图已迁移到 `PlatformIconButton`;图标上传仍使用 `asChild="label"` 保留 label + file input 语义,公共组件会自动写入隐藏文本,确保内嵌 file input 继承可访问名称。
- 2026-06-10 追加creation-agent composer 的上传文档 / 上传参考图入口使用 `PlatformIconButton` 默认 `platformIcon`;工作台只保留动态 label、title、busy 状态和 picker 回调,发送按钮继续保留主题色动作布局。验证命令:`npm run test -- src/components/creation-agent/CreationAgentWorkspace.test.tsx src/components/common/PlatformIconButton.test.tsx`
- 2026-06-10 追加:作品详情顶部返回 / 分享和封面轮播上一张 / 下一张入口使用 `PlatformIconButton variant="platformIcon"`;详情页保留原 `platform-work-detail__*` 局部 class 控制位置和尺寸,点赞、复制三态等专用动作暂不迁移。验证命令:`npm run test -- src/components/platform-entry/PlatformWorkDetailView.test.tsx src/components/common/PlatformIconButton.test.tsx`

View File

@@ -266,6 +266,8 @@
19.3.40. `PlatformNavigableListItem` 继续从桌面首页扩展到 profile 设置行:`src/components/platform-entry/PlatformProfilePrimitives.tsx` 里的 `ProfileSettingsRow` 现已统一委托共享 `button + leading + trailing` 骨架,保留本地 `platform-profile-settings-row` class 承接行间分隔、icon 胶囊和字号微调。后续 profile / 账户中心里同类“左图标标题 + 右箭头”的轻量导航行,优先直接复用 `PlatformNavigableListItem`,不要再回退成原生 `<button>` 手写布局。验证命令:`npx vitest run src/components/platform-entry/PlatformProfilePrimitives.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3.41. `PlatformAsyncStatePanel` 继续补齐首页分类分支:`RpgEntryHomeView.tsx` 的移动端“发现 -> 分类”、桌面发现页“分类”以及桌面首页“作品分类”模块都改成共享状态壳承接外层 `loading / empty / content` 切换,分类控制条与排序按钮继续保留在内容 slot 中;筛选后无结果的“当前筛选下没有作品。”也统一改由内层 `PlatformAsyncStatePanel` 切换,不再在三处 JSX 中各自手写空态分支。后续同类“外层数据可用性 + 内层筛选空态”面板优先沿用这套双层状态壳,不要回退成嵌套 ternary。验证命令`npx vitest run src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3.42. `PlatformAsyncStatePanel` 继续从首页扩展到公共素材网格、runtime 面板和账号子区块:`PlatformAssetPickerGrid` 现已统一用共享状态壳承接 `loading / empty / content`,但继续把 `error` banner 留在外层,以保持“错误提示可与内容或加载态并存”的原语义;`VisualNovelSavePanel.tsx` 的存档列表,以及 `AccountModal.tsx` 里的“安全状态 / 当前登录设备 / 账号操作记录”三个子区块也都改成各自使用 `PlatformAsyncStatePanel`。后续白底列表、素材选择器或账号子面板若只是标准互斥异步状态,优先按这三种接法复用共享状态壳,不再把读取态和空态分支手写回组件内部。验证命令:`npx vitest run src/components/common/PlatformAssetPickerCard.test.tsx src/components/visual-novel-runtime/VisualNovelRuntimePanels.emptyState.test.tsx src/components/auth/AccountModal.test.tsx src/components/platform-entry/PlatformProfileRewardCodeRedeemModal.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3.43. 轻量按钮漏网继续向共享按钮收口:`PlatformTagEditor.tsx` 的标签 chip 删除入口已改成紧凑 `PlatformIconButton`,保留 `label="删除标签 ${tag}"`、透明背景和原 chip 高度,不再手写裸 `<button>``RpgEntryCharacterSelectView.tsx` 两处重复的“返回”按钮已统一沉到文件内 `CharacterSelectBackButton`,底层委托 `PlatformActionButton surface="editorDark"`,保留原有暗色视觉与文案。后续同类“局部 chip 删除按钮”优先先用 `PlatformIconButton` 压缩尺寸和视觉;暗色轻量返回 / 返回上一级 CTA 则优先用 `PlatformActionButton surface="editorDark"` 包一层局部 helper不再复制原生 `<button>` class。验证命令`npx vitest run src/components/common/PlatformTagEditor.test.tsx src/components/rpg-entry/RpgEntryCharacterSelectView.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3.44. 暖色生成页顶部返回入口开始沉淀共享壳:`GenerationProgressHero.tsx` 新增 `GenerationHeaderBackButton`,统一承接 `ArrowLeft + 文案 + 透明背景` 的暖色生成页返回按钮骨架,并底层复用 `PlatformIconButton variant="darkMini"``CustomWorldGenerationView.tsx``BarkBattleGeneratingView.tsx` 已接入,继续保留各自 `backLabel`、禁用态和局部暖色文字样式。后续同类生成页、等待页或暖色 hero 顶栏若只是“左箭头 + 返回文案”的轻量返回入口,优先复用这个小组件,不再各自手写 `ArrowLeft`、透明按钮背景和字号间距。验证命令:`npx vitest run src/components/CustomWorldGenerationView.test.tsx src/components/bark-battle-creation/BarkBattleGeneratingView.test.tsx src/components/common/PlatformIconButton.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
19.3. creative-agent 首页的侧边栏菜单、账号入口、开启新对话、我的创作、首页激励 CTA 和 prompt suggestion 按钮迁移到 `PlatformIconButton` / `PlatformActionButton`;首页继续保留 `creative-agent-home__*` 本地 class 承接透明顶栏、抽屉和品牌化胶囊视觉,不把视觉回收和语义收口绑成一次大改。`Beta` 徽标和历史记录纯文本行暂保留本地实现,等出现更多同构轻量列表行后再评估是否抽新的共享 row primitive。
19.4. 大鱼吃小鱼结果页 hero 的返回入口迁移到 `PlatformIconButton variant="darkMini"`,测试 / 发布动作迁移到 `PlatformActionButton surface="editorDark"`;结果页只保留测试运行、发布提交和文案状态语义,不再手写 hero 顶栏按钮壳。
19.4.1. 大鱼吃小鱼结果页的发布失败弹层迁移到 `src/components/common/PlatformStatusDialog.tsx``PlatformStatusDialog` 补充自定义图标、可访问标签和动作按钮样式透传后,`BigFishResultView` 不再保留 `BigFishResultErrorModal` 内联的 `UnifiedConfirmDialog + PlatformIconBadge` 组合。结果页只保留失败文案和关闭回调,发布失败的状态图标、遮罩、白底面板和“知道了”主动作统一由共享状态弹层承接。验证命令:`npm run test -- src/components/common/PlatformStatusDialog.test.tsx src/components/big-fish-result/BigFishResultView.test.tsx``npm run typecheck`

View File

@@ -23,7 +23,18 @@ test('renders tags and removes a tag', () => {
/>,
);
fireEvent.click(screen.getByRole('button', { name: '删除标签 山海' }));
const removeButton = screen.getByRole('button', { name: '删除标签 山海' });
expect(removeButton.className).toContain('platform-icon-button');
expect(removeButton.className).toContain('h-3.5');
expect(removeButton.className).toContain('w-3.5');
expect(removeButton.className).toContain('border-0');
expect(removeButton.className).toContain('bg-transparent');
expect(removeButton.className).toContain('p-0');
expect(removeButton.className).toContain('opacity-70');
expect(removeButton.getAttribute('title')).toBe('删除标签');
fireEvent.click(removeButton);
expect(onChange).toHaveBeenCalledWith(['机关']);
});

View File

@@ -131,20 +131,18 @@ export function PlatformTagEditor({
].join(' ')}
>
{tag}
<button
type="button"
<PlatformIconButton
disabled={disabled}
label={`删除标签 ${tag}`}
title="删除标签"
onClick={() =>
onChange(
normalizedTags.filter((currentTag) => currentTag !== tag),
)
}
className="rounded-full opacity-70 transition hover:opacity-100 disabled:opacity-45"
aria-label={`删除标签 ${tag}`}
title="删除标签"
>
<X className="h-3.5 w-3.5" />
</button>
className="h-3.5 w-3.5 border-0 bg-transparent p-0 opacity-70 shadow-none transition hover:translate-y-0 hover:bg-transparent disabled:opacity-45"
icon={<X className="h-3.5 w-3.5" />}
/>
</span>
))}
{normalizedTags.length <= 0 ? (

View File

@@ -103,6 +103,7 @@ afterEach(() => {
test('custom world character selection stays stable when character ids are empty', async () => {
const user = userEvent.setup();
const handleBack = vi.fn();
const handleConfirm = vi.fn();
const consoleErrorSpy = vi
.spyOn(console, 'error')
@@ -201,11 +202,21 @@ test('custom world character selection stays stable when character ids are empty
],
},
} as unknown as CustomWorldProfile}
onBack={() => {}}
onBack={handleBack}
onConfirm={handleConfirm}
/>,
);
const backButton = screen.getByRole('button', {name: '返回'});
expect(backButton.className).toContain('platform-action-button--editor-dark');
expect(backButton.className).toContain('rounded-full');
expect(backButton.className).toContain('px-3');
expect(backButton.className).toContain('py-1.5');
expect(backButton.className).toContain('text-[11px]');
await user.click(backButton);
expect(handleBack).toHaveBeenCalledTimes(1);
expect(screen.getByText(/:/u)).toBeTruthy();
expect(screen.queryByText(/:/u)).toBeNull();
@@ -235,7 +246,9 @@ test('custom world character selection stays stable when character ids are empty
expect(duplicateKeyCalls).toHaveLength(0);
});
test('custom world character selection falls back instead of rendering a blank screen when profile characters are malformed', () => {
test('custom world character selection falls back instead of rendering a blank screen when profile characters are malformed', async () => {
const user = userEvent.setup();
const handleBack = vi.fn();
vi.spyOn(console, 'warn').mockImplementation(() => undefined);
vi.mocked(buildCustomWorldPlayableCharacters).mockImplementation(() => {
throw new TypeError('profile.playableNpcs is not iterable');
@@ -268,7 +281,7 @@ test('custom world character selection falls back instead of rendering a blank s
],
},
} as unknown as CustomWorldProfile}
onBack={() => {}}
onBack={handleBack}
onConfirm={() => {}}
/>,
);
@@ -276,4 +289,7 @@ test('custom world character selection falls back instead of rendering a blank s
expect(screen.getByText('选择你的角色')).toBeTruthy();
expect(screen.getAllByText('兜底侠').length).toBeGreaterThan(0);
expect(screen.getByRole('button', { name: //u })).toBeTruthy();
await user.click(screen.getByRole('button', {name: '返回'}));
expect(handleBack).toHaveBeenCalledTimes(1);
});

View File

@@ -20,6 +20,7 @@ import {
import { getNineSliceStyle, UI_CHROME } from '../../uiAssets';
import { CharacterAnimator } from '../CharacterAnimator';
import { CharacterDetailModal } from '../CharacterDetailModal';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import { CharacterDraftModal } from '../SelectionCustomizationModals';
@@ -215,6 +216,21 @@ function getCharacterCardStyle(index: number, progress: number) {
};
}
function CharacterSelectBackButton({onBack}: {onBack: () => void}) {
return (
<PlatformActionButton
surface="editorDark"
tone="ghost"
size="xxs"
shape="pill"
onClick={onBack}
className="px-3 py-1.5 text-[11px]"
>
</PlatformActionButton>
);
}
export function RpgEntryCharacterSelectView({
worldType,
customWorldProfile,
@@ -344,13 +360,7 @@ export function RpgEntryCharacterSelectView({
if (!selectedCharacter || !selectedCharacterMeta) {
return (
<div className="flex h-full min-h-0 flex-col items-center justify-center gap-4 text-center">
<button
type="button"
onClick={onBack}
className="rounded-full border border-white/10 bg-black/18 px-3 py-1.5 text-[11px] text-zinc-300 transition-colors hover:text-white"
>
</button>
<CharacterSelectBackButton onBack={onBack} />
<div className="text-sm text-zinc-300"></div>
</div>
);
@@ -360,13 +370,7 @@ export function RpgEntryCharacterSelectView({
<>
<div className="flex h-full min-h-0 flex-col">
<div className="mb-3 flex justify-start">
<button
type="button"
onClick={onBack}
className="rounded-full border border-white/10 bg-black/18 px-3 py-1.5 text-[11px] text-zinc-300 transition-colors hover:text-white"
>
</button>
<CharacterSelectBackButton onBack={onBack} />
</div>
<div className="mb-4 text-center">
<div className="text-2xl font-black text-white sm:text-[2rem]"></div>