继续收口账号空态与运行态动作按钮
账号安全面板空态统一复用 PlatformEmptyState 登录入口不可用提示改为复用 PlatformEmptyState RPG运行态底部与覆盖层动作统一委托 PlatformActionButton 背包工坊按钮回到共享 success tone 更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
@@ -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 敲木鱼发布后作品架与推荐流刷新口径
|
||||
|
||||
|
||||
@@ -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 顶栏按钮壳。
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
? '制作中...'
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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('退出聊天');
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user