收口前端平台组件库能力
新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件 迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome 补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
@@ -5,8 +5,14 @@ import { getCharacterById } from '../data/characterPresets';
|
||||
import { MAX_COMPANIONS } from '../data/npcInteractions';
|
||||
import { Character, CompanionState } from '../types';
|
||||
import { getNineSliceStyle, UI_CHROME } from '../uiAssets';
|
||||
import { PlatformActionButton } from './common/PlatformActionButton';
|
||||
import { PlatformDarkOptionCard } from './common/PlatformDarkOptionCard';
|
||||
import { PlatformEmptyState } from './common/PlatformEmptyState';
|
||||
import { PlatformMediaFrame } from './common/PlatformMediaFrame';
|
||||
import { PlatformPillBadge } from './common/PlatformPillBadge';
|
||||
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
|
||||
import { PlatformSubpanel } from './common/PlatformSubpanel';
|
||||
import { PixelCloseButton } from './PixelCloseButton';
|
||||
import { ResolvedAssetImage } from './ResolvedAssetImage';
|
||||
|
||||
interface CompanionCampModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -26,9 +32,13 @@ type CompanionCardData = {
|
||||
|
||||
function StatusPill({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-full border border-white/10 bg-black/20 px-2 py-1 text-[10px] text-zinc-300">
|
||||
<PlatformPillBadge
|
||||
tone="darkNeutral"
|
||||
size="xxs"
|
||||
className="font-normal text-zinc-300"
|
||||
>
|
||||
{label} {value}
|
||||
</div>
|
||||
</PlatformPillBadge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -149,7 +159,13 @@ export function CompanionCampModal({
|
||||
</div>
|
||||
|
||||
<div className="grid min-h-0 flex-1 gap-4 overflow-y-auto p-5 lg:grid-cols-[1.05fr_0.95fr] lg:overflow-hidden">
|
||||
<section className="rounded-2xl border border-white/10 bg-black/18 p-4 lg:min-h-0 lg:overflow-y-auto">
|
||||
<PlatformSubpanel
|
||||
as="section"
|
||||
surface="dark"
|
||||
radius="sm"
|
||||
padding="md"
|
||||
className="lg:min-h-0 lg:overflow-y-auto"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white">当前队伍</div>
|
||||
@@ -160,28 +176,39 @@ export function CompanionCampModal({
|
||||
<StatusPill label="出战" value={`${companions.length}/${MAX_COMPANIONS}`} />
|
||||
</div>
|
||||
{inBattle && (
|
||||
<div className="mb-3 rounded-xl border border-amber-400/20 bg-amber-500/10 px-3 py-2 text-xs text-amber-100">
|
||||
<PlatformStatusMessage
|
||||
tone="warning"
|
||||
surface="editorDark"
|
||||
size="xs"
|
||||
className="mb-3"
|
||||
>
|
||||
战斗中无法调整编组。
|
||||
</div>
|
||||
</PlatformStatusMessage>
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{activeCompanionCards.length > 0 ? activeCompanionCards.map(({ companion, character }) => {
|
||||
const selectedForSwap = selectedSwapNpcId === companion.npcId;
|
||||
return (
|
||||
<div
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
key={companion.npcId}
|
||||
className={`rounded-xl border px-3 py-3 ${selectedForSwap ? 'border-sky-400/40 bg-sky-500/10' : 'border-white/8 bg-black/20'}`}
|
||||
data-testid={`active-companion-card-${companion.npcId}`}
|
||||
surface={selectedForSwap ? 'darkSky' : 'dark'}
|
||||
radius="xs"
|
||||
padding="md"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-white/10 bg-black/25">
|
||||
<ResolvedAssetImage
|
||||
src={character.portrait}
|
||||
alt={character.name}
|
||||
className="h-full w-full scale-125 object-contain"
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
</div>
|
||||
<PlatformMediaFrame
|
||||
src={character.portrait}
|
||||
alt={character.name}
|
||||
fallbackLabel={character.name}
|
||||
aspect="square"
|
||||
surface="editorDark"
|
||||
className="h-16 w-16 shrink-0 rounded-xl"
|
||||
imageClassName="h-full w-full scale-125 object-contain"
|
||||
imageProps={{ style: { imageRendering: 'pixelated' } }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-semibold text-white">{character.name}</div>
|
||||
<div className="text-[10px] tracking-[0.18em] text-zinc-500">{character.title}</div>
|
||||
@@ -193,34 +220,48 @@ export function CompanionCampModal({
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
<PlatformDarkOptionCard
|
||||
disabled={inBattle}
|
||||
onClick={() => setSelectedSwapNpcId(companion.npcId)}
|
||||
className={`rounded-lg border px-3 py-2 text-xs ${selectedForSwap ? 'border-sky-400/30 bg-sky-500/15 text-sky-100' : 'border-white/10 bg-white/5 text-zinc-200'} ${inBattle ? 'opacity-50' : ''}`}
|
||||
selected={selectedForSwap}
|
||||
tone="sky"
|
||||
radius="sm"
|
||||
padding="sm"
|
||||
className="text-xs"
|
||||
>
|
||||
设为替换位
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
</PlatformDarkOptionCard>
|
||||
<PlatformActionButton
|
||||
surface="editorDark"
|
||||
tone="secondary"
|
||||
size="xs"
|
||||
disabled={inBattle}
|
||||
onClick={() => onBenchCompanion(companion.npcId)}
|
||||
className={`rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-xs text-zinc-200 ${inBattle ? 'opacity-50' : ''}`}
|
||||
>
|
||||
转入后备
|
||||
</button>
|
||||
</PlatformActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}) : (
|
||||
<div className="rounded-xl border border-dashed border-white/10 bg-black/20 px-4 py-6 text-sm text-zinc-400">
|
||||
<PlatformEmptyState
|
||||
surface="editorDark"
|
||||
size="inline"
|
||||
className="rounded-xl py-6 font-normal text-zinc-400"
|
||||
>
|
||||
当前没有已出战的同行者。
|
||||
</div>
|
||||
</PlatformEmptyState>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</PlatformSubpanel>
|
||||
|
||||
<section className="rounded-2xl border border-white/10 bg-black/18 p-4 lg:min-h-0 lg:overflow-y-auto">
|
||||
<PlatformSubpanel
|
||||
as="section"
|
||||
surface="dark"
|
||||
radius="sm"
|
||||
padding="md"
|
||||
className="lg:min-h-0 lg:overflow-y-auto"
|
||||
>
|
||||
<div className="mb-3 flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white">后备队伍</div>
|
||||
@@ -235,16 +276,25 @@ export function CompanionCampModal({
|
||||
{reserveCompanionCards.length > 0 ? reserveCompanionCards.map(({ companion, character }) => {
|
||||
const needsSwap = companions.length >= MAX_COMPANIONS;
|
||||
return (
|
||||
<div key={companion.npcId} className="rounded-xl border border-white/8 bg-black/20 px-3 py-3">
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
key={companion.npcId}
|
||||
data-testid={`reserve-companion-card-${companion.npcId}`}
|
||||
surface="dark"
|
||||
radius="xs"
|
||||
padding="md"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-16 w-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-white/10 bg-black/25">
|
||||
<ResolvedAssetImage
|
||||
src={character.portrait}
|
||||
alt={character.name}
|
||||
className="h-full w-full scale-125 object-contain"
|
||||
style={{ imageRendering: 'pixelated' }}
|
||||
/>
|
||||
</div>
|
||||
<PlatformMediaFrame
|
||||
src={character.portrait}
|
||||
alt={character.name}
|
||||
fallbackLabel={character.name}
|
||||
aspect="square"
|
||||
surface="editorDark"
|
||||
className="h-16 w-16 shrink-0 rounded-xl"
|
||||
imageClassName="h-full w-full scale-125 object-contain"
|
||||
imageProps={{ style: { imageRendering: 'pixelated' } }}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="text-sm font-semibold text-white">{character.name}</div>
|
||||
<div className="text-[10px] tracking-[0.18em] text-zinc-500">{character.title}</div>
|
||||
@@ -255,39 +305,46 @@ export function CompanionCampModal({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
<PlatformActionButton
|
||||
surface="editorDark"
|
||||
tone={inBattle || (needsSwap && !selectedSwapNpcId) ? 'ghost' : 'success'}
|
||||
size="xs"
|
||||
fullWidth
|
||||
disabled={inBattle || (needsSwap && !selectedSwapNpcId)}
|
||||
onClick={() => onActivateCompanion(companion.npcId, needsSwap ? selectedSwapNpcId : null)}
|
||||
className={`mt-3 w-full rounded-lg border px-3 py-2 text-xs ${
|
||||
inBattle || (needsSwap && !selectedSwapNpcId)
|
||||
? 'border-white/6 bg-black/20 text-zinc-500'
|
||||
: 'border-emerald-400/20 bg-emerald-500/10 text-emerald-100'
|
||||
}`}
|
||||
className="mt-3"
|
||||
>
|
||||
{needsSwap ? '换入队伍' : '编入队伍'}
|
||||
</button>
|
||||
</div>
|
||||
</PlatformActionButton>
|
||||
</PlatformSubpanel>
|
||||
);
|
||||
}) : (
|
||||
<div className="rounded-xl border border-dashed border-white/10 bg-black/20 px-4 py-6 text-sm text-zinc-400">
|
||||
<PlatformEmptyState
|
||||
surface="editorDark"
|
||||
size="inline"
|
||||
className="rounded-xl py-6 font-normal text-zinc-400"
|
||||
>
|
||||
当前还没有后备同行者。
|
||||
</div>
|
||||
</PlatformEmptyState>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</PlatformSubpanel>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-white/10 px-5 py-4">
|
||||
<div className="mb-3 text-xs font-bold text-white">营地气氛</div>
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
{campMoments.map((moment, index) => (
|
||||
<div
|
||||
<PlatformSubpanel
|
||||
as="div"
|
||||
key={`camp-moment-${index}-${moment}`}
|
||||
className="rounded-xl border border-white/8 bg-black/18 px-4 py-3 text-sm leading-relaxed text-zinc-300"
|
||||
surface="dark"
|
||||
radius="xs"
|
||||
padding="md"
|
||||
className="text-sm leading-relaxed text-zinc-300"
|
||||
>
|
||||
{moment}
|
||||
</div>
|
||||
</PlatformSubpanel>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user