继续收口账号空态与运行态动作按钮
账号安全面板空态统一复用 PlatformEmptyState 登录入口不可用提示改为复用 PlatformEmptyState RPG运行态底部与覆盖层动作统一委托 PlatformActionButton 背包工坊按钮回到共享 success tone 更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
@@ -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