继续收口NPC弹窗与角色详情空态

统一 NPC 弹窗底部动作按钮与交易详情空态到共享组件
统一角色构筑详情空明细到 PlatformEmptyState
补充 PlatformUiKit 收口计划、共享决策记录与对应测试护栏
This commit is contained in:
2026-06-11 02:39:49 +08:00
parent ab5a0efe50
commit 84ded19f11
6 changed files with 116 additions and 64 deletions

View File

@@ -193,6 +193,28 @@ test('BuildContributionDetailPanel reuses dark PlatformSubpanel chrome', () => {
expect(attributeRow?.className).toContain('bg-black/25');
});
test('BuildContributionDetailPanel empty state reuses dark PlatformEmptyState chrome', () => {
const row: BuildContributionRow = {
label: '潮汐',
source: 'character',
fitScore: 0.72,
sourceCoefficient: 1,
bonusDelta: 0.12,
attributeSimilarities: {},
attributeWeights: {},
attributeContributions: {},
attributeModifierDeltas: {},
};
render(<BuildContributionDetailPanel row={row} attributes={[]} />);
const emptyState = screen.getByText('当前标签还没有可展示的属性适配明细。');
expect(emptyState.className).toContain('platform-empty-state');
expect(emptyState.className).toContain('border-dashed');
expect(emptyState.className).toContain('bg-black/20');
});
test('PlayerLevelProgress renders xp progress details', () => {
render(
<PlayerLevelProgress level={6} currentLevelXp={72} xpToNextLevel={120} />,

View File

@@ -352,14 +352,14 @@ export function BuildContributionDetailPanel({
))}
</div>
) : (
<PlatformSubpanel
surface="dark"
radius="xs"
padding="sm"
className="mt-4 px-4 py-3 text-sm leading-6 text-zinc-400"
<PlatformEmptyState
surface="editorDark"
size="compact"
tone="soft"
className="mt-4"
>
{emptyText}
</PlatformSubpanel>
</PlatformEmptyState>
)}
</PlatformSubpanel>
</div>

View File

@@ -260,7 +260,50 @@ test('NPC 弹窗空态复用暗色平台空态', () => {
});
const recruitIntro = screen.getByText('同行名额已满,需要先让一人离队。');
const tradeDetailEmptyState = screen.getByText(
'请选择一件物品,右侧会显示数量、价格与详情。',
);
expect(recruitIntro.className).toContain('platform-status-message');
expect(recruitIntro.className).toContain('border-amber-300/15');
expect(tradeDetailEmptyState.className).toContain('platform-empty-state');
expect(tradeDetailEmptyState.className).toContain('border-dashed');
});
test('NPC 弹窗标准 dark footer CTA 复用 PlatformActionButton', async () => {
const user = userEvent.setup();
const { unmount } = render(
<NpcModals gameState={createEmptyGameState()} npcUi={createEmptyNpcUi()} />,
);
const cancelButtons = screen.getAllByRole('button', { name: '取消' });
const tradeConfirmButton = screen.getByRole('button', { name: '确认购买' });
const giftConfirmButton = screen.getByRole('button', { name: '确认赠礼' });
const recruitConfirmButton = screen.getByRole('button', { name: '确认招募' });
const footerButtons = [
cancelButtons[0],
tradeConfirmButton,
cancelButtons[1],
giftConfirmButton,
cancelButtons[2],
recruitConfirmButton,
].filter((button): button is HTMLElement => Boolean(button));
expect(footerButtons).toHaveLength(6);
footerButtons.forEach((button) => {
expect(button.className).toContain('platform-action-button--editor-dark');
expect(button.className).toContain('rounded-2xl');
});
unmount();
render(<NpcModals gameState={createGameState()} npcUi={createNpcUi()} />);
await user.click(screen.getByRole('button', { name: /月壳/ }));
const closeButton = screen.getByRole('button', { name: '关闭' });
expect(closeButton.className).toContain('platform-action-button--editor-dark');
expect(closeButton.className).toContain('rounded-2xl');
});

View File

@@ -28,6 +28,7 @@ import {
getNineSliceStyle,
UI_CHROME,
} from '../uiAssets';
import { PlatformActionButton } from './common/PlatformActionButton';
import { PlatformDarkOptionCard } from './common/PlatformDarkOptionCard';
import { PlatformEmptyState } from './common/PlatformEmptyState';
import { PlatformPillBadge } from './common/PlatformPillBadge';
@@ -412,9 +413,14 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
</PlatformSubpanel>
</div>
) : (
<div className="px-2 py-8 text-center text-sm text-zinc-500">
<PlatformEmptyState
surface="editorDark"
size="compact"
tone="soft"
className="px-2 py-8 text-center"
>
</div>
</PlatformEmptyState>
)}
</PlatformSubpanel>
</div>
@@ -422,29 +428,23 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
</div>
<div className="flex items-center justify-end gap-3 border-t border-white/10 px-4 py-3 sm:px-5 sm:py-4">
<button
type="button"
<PlatformActionButton
surface="editorDark"
tone="secondary"
size="xs"
onClick={npcUi.closeTradeModal}
className="pixel-nine-slice pixel-pressable px-4 py-2 text-xs text-zinc-200"
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 14,
paddingY: 8,
})}
>
</button>
<button
type="button"
</PlatformActionButton>
<PlatformActionButton
surface="editorDark"
tone="primary"
size="xs"
disabled={!canConfirmTrade}
onClick={npcUi.confirmTrade}
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${canConfirmTrade ? 'text-white' : 'text-zinc-600'}`}
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 14,
paddingY: 8,
})}
>
{tradeMode === 'buy' ? '确认购买' : '确认出售'}
</button>
</PlatformActionButton>
</div>
</motion.div>
</motion.div>
@@ -560,17 +560,14 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
)}
<div className="flex justify-end">
<button
type="button"
<PlatformActionButton
surface="editorDark"
tone="secondary"
size="xs"
onClick={() => setTradeDetail(null)}
className="pixel-nine-slice pixel-pressable px-4 py-2 text-xs text-zinc-200"
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 14,
paddingY: 8,
})}
>
</button>
</PlatformActionButton>
</div>
</div>
</motion.div>
@@ -669,29 +666,23 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
</div>
<div className="flex justify-end gap-3 px-5 pb-5">
<button
type="button"
<PlatformActionButton
surface="editorDark"
tone="secondary"
size="xs"
onClick={npcUi.closeGiftModal}
className="pixel-nine-slice pixel-pressable px-4 py-2 text-xs text-zinc-200"
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 14,
paddingY: 8,
})}
>
</button>
<button
type="button"
</PlatformActionButton>
<PlatformActionButton
surface="editorDark"
tone="primary"
size="xs"
disabled={!activeGiftView?.canSubmit}
onClick={npcUi.confirmGift}
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${activeGiftView?.canSubmit ? 'text-white' : 'text-zinc-600'}`}
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 14,
paddingY: 8,
})}
>
</button>
</PlatformActionButton>
</div>
</motion.div>
</motion.div>
@@ -773,29 +764,23 @@ export function NpcModals({ gameState, npcUi }: NpcModalsProps) {
</div>
<div className="flex justify-end gap-3 px-5 pb-5">
<button
type="button"
<PlatformActionButton
surface="editorDark"
tone="secondary"
size="xs"
onClick={npcUi.closeRecruitModal}
className="pixel-nine-slice pixel-pressable px-4 py-2 text-xs text-zinc-200"
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 14,
paddingY: 8,
})}
>
</button>
<button
type="button"
</PlatformActionButton>
<PlatformActionButton
surface="editorDark"
tone="primary"
size="xs"
disabled={!npcUi.recruitModal.selectedReleaseNpcId}
onClick={npcUi.confirmRecruit}
className={`pixel-nine-slice pixel-pressable px-4 py-2 text-xs ${npcUi.recruitModal.selectedReleaseNpcId ? 'text-white' : 'text-zinc-600'}`}
style={getNineSliceStyle(UI_CHROME.choiceButton, {
paddingX: 14,
paddingY: 8,
})}
>
</button>
</PlatformActionButton>
</div>
</motion.div>
</motion.div>