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

账号安全面板空态统一复用 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

@@ -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