继续收口账号空态与运行态动作按钮

账号安全面板空态统一复用 PlatformEmptyState
登录入口不可用提示改为复用 PlatformEmptyState
RPG运行态底部与覆盖层动作统一委托 PlatformActionButton
背包工坊按钮回到共享 success tone
更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
2026-06-11 02:03:41 +08:00
parent 402b847c7f
commit 01c0028b87
10 changed files with 208 additions and 94 deletions

View File

@@ -2077,8 +2077,8 @@
- 决策:平台入口的创作前置泥点阻断提示只在 `platform-entry` 局部抽成 `src/components/platform-entry/PlatformDraftGenerationPointNoticeDialog.tsx`,并使用 `DraftGenerationPointNotice` union`insufficient-points` / `balance-load-failed`)承接业务真相;不要在 `common/` 再抽一个泛化 `BlockingNoticeDialog`,否则会把 `PlatformAcknowledgeStatusDialog` 的样式透传再包装一层而不缩小调用面。
- 决策:`PlatformAsyncStatePanel` 从 profile modal 扩展到作品架类白底 panel`CustomWorldCreationHub.tsx` 的作品架主体现在也统一走 `loadingState / emptyState / children` 三段 slot但 error + 重试继续留在业务层外侧不把共享组件扩成“banner + retry + content”全能状态机。后续白底作品架或列表 panel 若只是互斥的 `loading / empty / content`,优先直接复用这套骨架。
- 决策:`CopyFeedbackButton.tsx``actionSurface` 分支继续收口到 `PlatformActionButton``pill` 分支继续保留 `PlatformPillBadge` 风格;复制反馈按钮不再直接调用 `getPlatformActionButtonClassName` 手拼平台按钮基础 chrome。后续同类“复制状态机 + 平台动作按钮”组合优先直接复用 `CopyFeedbackButton`不要在业务页重新混写图标、文案、aria 和动作按钮 class。
- 决策:白底 / 暗色面板里的轻量空态和普通 CTA 继续向共享组件收口。`PuzzleResultView.tsx` 的缺草稿提示、`RpgCreationAssetDebugPanel.tsx` 的空诊断提示、`VisualNovelEntityGrid` 的空实体列表都改为 `PlatformEmptyState``Match3DResultView.tsx` 的引用素材列表直接复用 `PlatformAssetPickerGrid` 自己的空态;`AdventureEntityModal.tsx` 的私聊按钮、`InventoryPanel.tsx` 的锻造 / 合成按钮,以及 `RpgCreationRoleAssetStudioModalImpl.tsx``RpgCreationEntityEditorShared.tsx` 里的局部 `ActionButton` 包装层都改为委托 `PlatformActionButton surface="editorDark"`。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel" size="inline"`;暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`,若业务仍需 `stopPropagation`、tone 映射或局部排版,可保留薄包装层,但不要再直接写原生 `<button>` 基础 chrome。
- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx``npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx``npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx``npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
- 决策:白底 / 暗色面板里的轻量空态和普通 CTA 继续向共享组件收口。`PuzzleResultView.tsx` 的缺草稿提示、`RpgCreationAssetDebugPanel.tsx` 的空诊断提示、`VisualNovelEntityGrid` 的空实体列表`AccountModal.tsx` 里账号安全分区的“无安全限制 / 无登录设备 / 无操作记录”以及 `LoginScreen.tsx` 的“当前登录入口暂不可用”都改为 `PlatformEmptyState``Match3DResultView.tsx` 的引用素材列表直接复用 `PlatformAssetPickerGrid` 自己的空态;`AdventureEntityModal.tsx` 的私聊按钮、`InventoryPanel.tsx` 的锻造 / 合成按钮`RpgCreationRoleAssetStudioModalImpl.tsx``RpgCreationEntityEditorShared.tsx` 里的局部 `ActionButton` 包装层,以及 `RpgAdventurePanel.tsx` / `RpgAdventurePanelOverlays.tsx` 里标准 runtime CTA 都改为委托 `PlatformActionButton surface="editorDark"`。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel"`;暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`,若业务仍需 `stopPropagation`、tone 映射、运行态 icon 排版或局部字号,可保留薄包装层,但不要再直接写原生 `<button>` 基础 chrome。
- 验证方式:`npm run test -- src/components/common/PlatformAsyncStatePanel.test.tsx src/components/platform-entry/PlatformProfileReferralModal.test.tsx src/components/platform-entry/PlatformProfileWalletLedgerModal.test.tsx src/components/platform-entry/PlatformProfilePlayedWorksModal.test.tsx src/components/platform-entry/PlatformProfileTaskCenterModal.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/common/PlatformSegmentedTabs.test.tsx src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx``npm run test -- src/components/common/PlatformModalCloseButton.test.tsx src/components/PixelCloseButton.test.tsx src/components/CharacterChatModal.test.tsx src/components/MapModal.test.tsx``npm run test -- src/components/common/useMudPointConfirmController.test.tsx src/components/match3d-result/Match3DResultView.test.tsx src/components/unified-creation/workspaces/PuzzleCreationWorkspace.interaction.test.tsx src/components/unified-creation/workspaces/Match3DCreationWorkspace.interaction.test.tsx src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx src/components/platform-entry/PlatformProfileRechargeModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/rpg-creation-result/RpgCreationResultActionBar.test.tsx src/components/unified-creation/shared/PuzzleHistoryAssetPickerDialog.test.tsx src/components/puzzle-result/PuzzleResultView.test.tsx``npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx``npm run typecheck``npm run check:encoding``git diff --check`
## 2026-05-26 敲木鱼发布后作品架与推荐流刷新口径

View File

@@ -256,7 +256,7 @@
19.3.32. 标准平台 modal header 的关闭入口继续统一到 `PlatformModalCloseButton variant="platformIcon"``PuzzleResultView.tsx` 的关卡详情 / 发布弹窗、`RpgCreationResultActionBar.tsx` 的发布检查弹窗,以及 `PuzzleHistoryAssetPickerDialog.tsx` 的历史素材弹窗已迁移。像素风 runtime、drawer collapse、玩法规则面板和运行态专属 overlay 继续保留本地 close 语义,不把 `PlatformModalCloseButton` 硬塞进非平台 modal header 场景。
19.3.33. `PlatformAsyncStatePanel` 从 profile modal 扩展到作品架:`CustomWorldCreationHub.tsx` 的作品架主体现在也统一通过 `loadingState / emptyState / children` 三个 slot 切换,保留外层 error + 重试提示不并入共享状态骨架。后续白底作品架或列表 panel 若只是互斥的 `loading / empty / content`,优先直接复用 `PlatformAsyncStatePanel`,不要再在业务 JSX 中重复拼 skeleton 和“当前筛选下没有内容”的分支。验证命令:`npx vitest run src/components/custom-world-home/CustomWorldCreationHub.test.tsx src/components/custom-world-home/CustomWorldCreationHub.interaction.test.tsx``npm run check:encoding`
19.3.34. `CopyFeedbackButton.tsx``actionSurface` 分支继续向共享按钮收口:带平台动作外观的复制按钮现在直接组合 `PlatformActionButton`,仅保留 `pill` 分支继续复用 `PlatformPillBadge` 风格。复制反馈按钮不再手动调用 `getPlatformActionButtonClassName` 拼平台按钮基础 chrome后续同类“复制状态机 + 平台动作按钮”组合也优先走 `CopyFeedbackButton + PlatformActionButton`不要在业务页或按钮组件里重新混写图标、文案、aria 和 class。验证命令`npm run test -- src/components/common/CopyFeedbackButton.test.tsx src/components/common/PlatformActionButton.test.tsx`
19.3.35. 白底 / 暗色面板里的轻量空态和普通 CTA 继续按共享组件收口:`PuzzleResultView.tsx` 的“还没有可编辑的拼图草稿”`RpgCreationAssetDebugPanel.tsx` 的“没有可诊断项”改为 `PlatformEmptyState``Match3DResultView.tsx` 的引用素材列表直接交给 `PlatformAssetPickerGrid` 自己处理空态`AdventureEntityModal.tsx` 的私聊按钮`InventoryPanel.tsx` 的锻造 / 合成按钮改为 `PlatformActionButton surface="editorDark"`,业务页只贴回 sky / emerald 局部皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel" size="inline"`暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`不要再手写基础按钮壳或空态面板 chrome。验证命令`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx``npm run check:encoding`
19.3.35. 白底 / 暗色面板里的轻量空态和普通 CTA 继续按共享组件收口:`PuzzleResultView.tsx` 的“还没有可编辑的拼图草稿”`RpgCreationAssetDebugPanel.tsx` 的“没有可诊断项”`VisualNovelEntityGrid` 的空实体列表、`AccountModal.tsx` 里账号安全分区的“无安全限制 / 无登录设备 / 无操作记录”以及 `LoginScreen.tsx` 的“当前登录入口暂不可用”都改为 `PlatformEmptyState``Match3DResultView.tsx` 的引用素材列表直接交给 `PlatformAssetPickerGrid` 自己处理空态`AdventureEntityModal.tsx` 的私聊按钮`InventoryPanel.tsx` 的锻造 / 合成按钮`RpgAdventurePanel.tsx` 底部 `队伍 / 背包 / 换一换 / 退出聊天` 按钮,以及 `RpgAdventurePanelOverlays.tsx` 里的“查看任务 / 保存并退出”都改为 `PlatformActionButton surface="editorDark"`,业务页只贴回局部 sky / emerald / runtime 皮肤。后续白底子面板里的只读空态优先使用 `PlatformEmptyState surface="subpanel"`暗色编辑 / 运行面板里的普通动作优先使用 `PlatformActionButton surface="editorDark"`若还需要 stopPropagation、局部字号或图标排版可保留薄包装层但不要再回退到原生 `<button>` 基础 chrome。验证命令`npm run test -- src/components/puzzle-result/PuzzleResultView.test.tsx src/components/rpg-creation-result/RpgCreationAssetDebugPanel.test.tsx src/components/AdventureEntityModal.test.tsx src/components/InventoryPanel.test.tsx src/components/auth/AccountModal.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.npcChat.test.tsx src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/common/PlatformActionButton.test.tsx``npm run check:encoding`
19.3.36. `VisualNovelEntityGrid` 的空态也继续收口到 `PlatformEmptyState surface="subpanel" size="inline"`;角色 / 场景 / 剧情阶段共用这一网格组件后,白底实体列表里的“暂无角色 / 暂无场景 / 暂无剧情阶段”等同构空态不再回退成 `PlatformSubpanel`。同时,`RpgCreationRoleAssetStudioModalImpl.tsx``RpgCreationEntityEditorShared.tsx` 保留局部 `ActionButton` 语义壳,但按钮本体已统一委托给 `PlatformActionButton surface="editorDark"`,只在包装层补最小的 `stopPropagation`、tone 映射和局部 class 适配。后续类似“暗色编辑器局部包装按钮”优先沿用这种薄包装模式,不再直接手写原生 `<button>` 基础 chrome。验证命令`npm run test -- src/components/visual-novel-result/VisualNovelResultView.test.tsx src/components/common/PlatformEmptyState.test.tsx src/components/rpg-creation-asset-studio/RpgCreationRoleAssetStudioModal.test.tsx src/components/CustomWorldEntityEditorModal.test.tsx src/components/common/PlatformActionButton.test.tsx``npm run check:encoding`
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 顶栏按钮壳。

View File

@@ -98,7 +98,7 @@ test('背包工坊材料需求状态复用暗色平台胶囊标签', () => {
expect(recipePanel?.className).toContain('bg-black/25');
expect(forgeButton.className).toContain('platform-action-button--editor-dark');
expect(forgeButton.className).toContain('rounded-lg');
expect(forgeButton.className).toContain('bg-emerald-500/10');
expect(forgeButton.className).toContain('bg-emerald-400');
expect(forgeButton.className).toContain('disabled:bg-black/20');
});

View File

@@ -219,7 +219,7 @@ export function InventoryPanel({
</div>
<PlatformActionButton
surface="editorDark"
tone="ghost"
tone="success"
size="xxs"
disabled={
!recipe.canCraft ||
@@ -235,7 +235,7 @@ export function InventoryPanel({
setSelectedItem(null);
}
}}
className="rounded-lg border-emerald-400/30 bg-emerald-500/10 text-emerald-100 hover:bg-emerald-500/20 disabled:border-white/8 disabled:bg-black/20 disabled:text-zinc-500 disabled:opacity-100"
className="rounded-lg disabled:border-white/8 disabled:bg-black/20 disabled:text-zinc-500 disabled:opacity-100"
>
{forgeActionKey === recipe.id
? '制作中...'

View File

@@ -434,6 +434,33 @@ test('legacy nested section requests now open the merged account panel', () => {
expect(within(accountDialog).getByText('操作记录')).toBeTruthy();
});
test('account panel empty shells reuse PlatformEmptyState subpanel chrome', async () => {
const user = userEvent.setup();
renderAccountModal();
await user.click(screen.getByRole('button', { name: /账号与安全/ }));
const accountDialog = screen.getByRole('dialog', { name: '账号信息' });
const emptyStateMessages = [
'当前没有生效中的安全限制。',
'暂无可展示的登录设备。',
'暂无账号操作记录。',
];
for (const message of emptyStateMessages) {
const shell = findNearestClassName(
within(accountDialog).getByText(message),
'platform-empty-state',
);
expect(shell?.className).toContain('rounded-[1rem]');
expect(shell?.className).toContain('bg-white/74');
expect(shell?.className).toContain('px-4');
expect(shell?.className).toContain('py-3');
}
});
test('current merged session group hides kick action and shows count', async () => {
const user = userEvent.setup();

View File

@@ -16,6 +16,7 @@ import type {
AuthUser,
} from '../../services/authService';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
@@ -164,6 +165,19 @@ function SettingsEntryCard({
);
}
// 中文注释:账号安全子面板里的空态与轻量加载态共用同一层白底外壳,避免重复拼 flat subpanel 样式。
function AccountSubpanelState({ children }: { children: ReactNode }) {
return (
<PlatformEmptyState
surface="subpanel"
size="compact"
className="py-3 text-center"
>
{children}
</PlatformEmptyState>
);
}
function OverlayPanel({
eyebrow,
title,
@@ -729,15 +743,9 @@ export function AccountModal({
<div className="mt-3 grid gap-2.5">
{loadingRiskBlocks ? (
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
padding="none"
className="px-4 py-3 text-sm text-[var(--platform-text-soft)]"
>
<AccountSubpanelState>
...
</PlatformSubpanel>
</AccountSubpanelState>
) : riskBlocks.length > 0 ? (
riskBlocks.map((block) => (
<PlatformStatusMessage
@@ -772,15 +780,9 @@ export function AccountModal({
</PlatformStatusMessage>
))
) : (
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
padding="none"
className="px-4 py-3 text-sm text-[var(--platform-text-soft)]"
>
<AccountSubpanelState>
</PlatformSubpanel>
</AccountSubpanelState>
)}
</div>
</PlatformSubpanel>
@@ -812,15 +814,9 @@ export function AccountModal({
<div className="mt-3 grid gap-2.5">
{loadingSessions ? (
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
padding="none"
className="px-4 py-3 text-sm text-[var(--platform-text-soft)]"
>
<AccountSubpanelState>
...
</PlatformSubpanel>
</AccountSubpanelState>
) : sessions.length > 0 ? (
sessions.map((session) => {
const isRevoking = revokingSessionIds.includes(
@@ -885,15 +881,9 @@ export function AccountModal({
);
})
) : (
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
padding="none"
className="px-4 py-3 text-sm text-[var(--platform-text-soft)]"
>
<AccountSubpanelState>
</PlatformSubpanel>
</AccountSubpanelState>
)}
</div>
</PlatformSubpanel>
@@ -925,15 +915,9 @@ export function AccountModal({
<div className="mt-3 grid gap-2.5">
{loadingAuditLogs ? (
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
padding="none"
className="px-4 py-3 text-sm text-[var(--platform-text-soft)]"
>
<AccountSubpanelState>
...
</PlatformSubpanel>
</AccountSubpanelState>
) : auditLogs.length > 0 ? (
auditLogs.map((log) => (
<PlatformSubpanel
@@ -961,15 +945,9 @@ export function AccountModal({
</PlatformSubpanel>
))
) : (
<PlatformSubpanel
as="div"
surface="flat"
radius="sm"
padding="none"
className="px-4 py-3 text-sm text-[var(--platform-text-soft)]"
>
<AccountSubpanelState>
</PlatformSubpanel>
</AccountSubpanelState>
)}
</div>
</PlatformSubpanel>

View File

@@ -16,11 +16,11 @@ import {
readStoredLegalConsent,
} from '../common/legalDocuments';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformFieldLabel } from '../common/PlatformFieldLabel';
import { PlatformModalCloseButton } from '../common/PlatformModalCloseButton';
import { PlatformSegmentedTabs } from '../common/PlatformSegmentedTabs';
import { PlatformStatusMessage } from '../common/PlatformStatusMessage';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { PlatformTextField } from '../common/PlatformTextField';
import { CaptchaChallengeField } from './CaptchaChallengeField';
@@ -374,14 +374,13 @@ export function LoginScreen({
!phoneLoginEnabled &&
!wechatLoginEnabled &&
!miniProgramRuntime ? (
<PlatformSubpanel
as="div"
radius="sm"
padding="none"
className="px-4 py-4 text-sm text-[var(--platform-text-base)]"
<PlatformEmptyState
surface="subpanel"
size="compact"
className="px-4 py-4"
>
</PlatformSubpanel>
</PlatformEmptyState>
) : null}
</div>
)}

View File

@@ -205,3 +205,95 @@ test('adventure panel shows current act label without fixed hostile chat turns',
expect(html).toContain('1/3');
expect(html).not.toContain('剩余交谈');
});
test('adventure panel bottom runtime actions reuse editor dark action buttons in npc chat mode', () => {
const currentStory: StoryMoment = {
text: '你们暂时放慢了语气。',
displayMode: 'dialogue',
dialogue: [
{ speaker: 'player', text: '那我们换个说法继续。' },
{ speaker: 'npc', speakerName: '柳无声', text: '你先说,我听着。' },
],
options: [],
npcChatState: {
npcId: 'npc-liu',
npcName: '柳无声',
turnCount: 1,
customInputPlaceholder: '输入你想对 TA 说的话',
},
};
const html = renderToStaticMarkup(
<RpgAdventurePanel
aiError={null}
currentStory={currentStory}
isLoading={false}
displayedOptions={[]}
hideOptions={false}
canRefreshOptions
onRefreshOptions={() => undefined}
onChoice={() => undefined}
onSubmitNpcChatInput={() => true}
onExitNpcChat={() => true}
onOpenCharacter={() => undefined}
onOpenInventory={() => undefined}
playerCharacter={createCharacter()}
worldType={WorldType.WUXIA}
quests={[]}
questUi={{
acknowledgeQuestCompletion: () => undefined,
claimQuestReward: () => null,
}}
npcChatQuestOfferUi={{
replacePendingOffer: () => false,
abandonPendingOffer: () => false,
acceptPendingOffer: () => null,
}}
goalStack={{
northStarGoal: null,
activeGoal: null,
immediateStepGoal: null,
supportGoals: [],
}}
goalPulse={null}
onDismissGoalPulse={() => undefined}
battleRewardUi={{
reward: null,
dismiss: () => undefined,
}}
playerHp={100}
playerMaxHp={100}
playerMana={20}
playerMaxMana={20}
playerSkillCooldowns={{}}
inBattle={false}
currentNpcBattleMode={null}
statistics={{
playTimeMs: 0,
hostileNpcsDefeated: 0,
questsAccepted: 0,
questsCompleted: 0,
questsTurnedIn: 0,
itemsUsed: 0,
scenesTraveled: 0,
currentSceneName: '竹林古道',
playerCurrency: 0,
inventoryItemCount: 0,
inventoryStackCount: 0,
activeCompanionCount: 0,
rosterCompanionCount: 0,
}}
musicVolume={0.6}
onMusicVolumeChange={() => undefined}
onSaveAndExit={() => undefined}
/>,
);
expect(html).toContain('platform-action-button--editor-dark');
expect(html).toContain('bg-black/20');
expect(html).toContain('bg-rose-500/10');
expect(html).toContain('打开队伍');
expect(html).toContain('打开背包');
expect(html).toContain('换一换选项');
expect(html).toContain('退出聊天');
});

View File

@@ -51,6 +51,7 @@ import {
UI_CHROME,
} from '../../uiAssets';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import { PlatformQuantityBadge } from '../common/PlatformQuantityBadge';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
@@ -60,6 +61,9 @@ import { PixelIcon } from '../PixelIcon';
const BATTLE_OPTION_ROW_MIN_HEIGHT = 58;
const BATTLE_OPTION_ROW_GAP = 6;
const DEFAULT_BATTLE_VISIBLE_OPTION_COUNT = 4;
const RPG_RUNTIME_DARK_CTA_CLASS_NAME =
'h-8 shrink-0 gap-1.5 rounded-md px-2 py-0 text-xs font-normal';
const RPG_RUNTIME_DARK_GHOST_CTA_CLASS_NAME = `${RPG_RUNTIME_DARK_CTA_CLASS_NAME} text-zinc-300 hover:text-white`;
export interface RpgAdventurePanelProps {
aiError: string | null;
@@ -913,63 +917,73 @@ function RpgAdventureChoiceSection(props: {
{/* 让底部操作区整体更贴近屏幕底部,同时把上方聊天区腾出更稳定的展示高度。 */}
<div className="mb-2 flex flex-wrap items-center justify-between gap-2">
<div className="flex min-w-0 flex-wrap items-center gap-2">
<button
type="button"
<PlatformActionButton
onClick={onOpenCharacter}
aria-label="打开队伍"
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-white/10 bg-black/20 px-2 text-zinc-300 transition-colors hover:text-white"
surface="editorDark"
tone="ghost"
size="xxs"
className={RPG_RUNTIME_DARK_GHOST_CTA_CLASS_NAME}
>
<PixelIcon src={TAB_ICONS.character.active} className="h-4 w-4" />
<span className="text-xs leading-none"></span>
</button>
<button
type="button"
</PlatformActionButton>
<PlatformActionButton
onClick={onOpenInventory}
aria-label="打开背包"
className="inline-flex h-8 shrink-0 items-center gap-1.5 rounded-md border border-white/10 bg-black/20 px-2 text-zinc-300 transition-colors hover:text-white"
surface="editorDark"
tone="ghost"
size="xxs"
className={RPG_RUNTIME_DARK_GHOST_CTA_CLASS_NAME}
>
<PixelIcon src={TAB_ICONS.inventory.active} className="h-4 w-4" />
<span className="text-xs leading-none"></span>
</button>
</PlatformActionButton>
</div>
<div className="flex shrink-0 items-center gap-2">
{isNpcChatMode && canRefreshOptions && !shouldHideChoiceUi ? (
<button
type="button"
<PlatformActionButton
onClick={onRefreshOptions}
aria-label="换一换选项"
className="inline-flex h-8 shrink-0 items-center gap-1.5 self-start rounded-md border border-white/10 bg-black/20 px-2 text-zinc-300 transition-colors hover:text-white"
surface="editorDark"
tone="ghost"
size="xxs"
className={`${RPG_RUNTIME_DARK_GHOST_CTA_CLASS_NAME} self-start`}
>
<PixelIcon
src={CHROME_ICONS.refreshOptions}
className="h-4 w-4"
/>
<span className="text-xs leading-none"></span>
</button>
</PlatformActionButton>
) : !isNpcChatMode && canRefreshOptions && !shouldHideChoiceUi ? (
<button
type="button"
<PlatformActionButton
onClick={onRefreshOptions}
aria-label="换一换选项"
className="inline-flex h-8 shrink-0 items-center gap-1.5 self-start rounded-md border border-white/10 bg-black/20 px-2 text-zinc-300 transition-colors hover:text-white"
surface="editorDark"
tone="ghost"
size="xxs"
className={`${RPG_RUNTIME_DARK_GHOST_CTA_CLASS_NAME} self-start`}
>
<PixelIcon
src={CHROME_ICONS.refreshOptions}
className="h-4 w-4"
/>
<span className="text-xs leading-none"></span>
</button>
</PlatformActionButton>
) : null}
{isNpcChatMode ? (
<button
type="button"
<PlatformActionButton
onClick={() => onExitNpcChat?.()}
aria-label="退出聊天"
className="inline-flex h-8 shrink-0 items-center gap-1.5 self-start rounded-md border border-rose-300/20 bg-rose-500/10 px-2 text-rose-100 transition-colors hover:bg-rose-500/15"
surface="editorDark"
tone="danger"
size="xxs"
className={`${RPG_RUNTIME_DARK_CTA_CLASS_NAME} self-start`}
>
<span className="text-xs leading-none">退</span>
</button>
</PlatformActionButton>
) : null}
</div>
</div>

View File

@@ -45,6 +45,7 @@ import {
getNineSliceStyle,
UI_CHROME,
} from '../../uiAssets';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformEmptyState } from '../common/PlatformEmptyState';
import { PlatformPillBadge } from '../common/PlatformPillBadge';
import type { PlatformPillBadgeTone } from '../common/platformPillBadgeModel';
@@ -1027,16 +1028,19 @@ export function RpgAdventurePanelOverlays({
<div className="flex items-center justify-end gap-2 border-t border-white/10 px-4 py-3 sm:px-5">
{goalStack.activeGoal?.sourceKind === 'quest' ||
goalStack.immediateStepGoal?.sourceKind === 'quest' ? (
<button
type="button"
<PlatformActionButton
surface="editorDark"
tone="ghost"
size="xxs"
shape="pill"
onClick={() => {
closeGoalPanel();
setIsQuestPanelOpen(true);
}}
className="rounded-full border border-white/12 bg-black/20 px-3 py-1.5 text-[11px] text-zinc-200 transition hover:text-white"
className="border-white/12 py-1.5 text-[11px]"
>
</button>
</PlatformActionButton>
) : null}
<button
type="button"
@@ -1140,8 +1144,12 @@ export function RpgAdventurePanelOverlays({
</div>
</button>
<button
type="button"
<PlatformActionButton
surface="editorDark"
tone="danger"
size="md"
align="start"
fullWidth
disabled={saveAndExitDisabled}
onClick={() => {
if (saveAndExitDisabled) return;
@@ -1149,13 +1157,9 @@ export function RpgAdventurePanelOverlays({
setIsSettingsPanelOpen(false);
onSaveAndExit();
}}
className={`w-full rounded-2xl border px-4 py-3 text-left transition ${
saveAndExitDisabled
? 'border-white/8 bg-black/20 text-zinc-500'
: 'border-rose-300/18 bg-rose-500/10 text-white hover:border-rose-300/30'
}`}
className="border-rose-300/18 text-white hover:border-rose-300/30 disabled:border-white/8 disabled:bg-black/20 disabled:text-zinc-500 disabled:opacity-100"
>
<div className="flex items-center justify-between gap-3">
<div className="flex w-full items-center justify-between gap-3">
<div>
<div className="text-sm font-semibold">退</div>
<div className="mt-1 text-[11px] text-zinc-400">
@@ -1164,7 +1168,7 @@ export function RpgAdventurePanelOverlays({
</div>
<LogOut className="h-4 w-4" />
</div>
</button>
</PlatformActionButton>
{saveAndExitDisabled && (
<PlatformEmptyState