收口前端平台组件库能力

新增 PlatformUiKit 通用弹窗、按钮、状态、空态、媒体、表单和标签等公共组件
迁移结果页、创作工作台、认证入口、RPG 暗色面板和运行态弹窗的重复 UI chrome
补充组件测试、页面回归测试、技术文档和 Hermes 共享决策记录
This commit is contained in:
2026-06-10 10:24:18 +08:00
parent a4ee6ff698
commit 1ad25e30f8
226 changed files with 23364 additions and 7825 deletions

View File

@@ -1,6 +1,7 @@
import { resolveRoleCombatStats } from '../data/attributeCombat';
import { getAttributeSlotValue } from '../data/attributeResolver';
import {
type BuildContributionAttributeRow,
type BuildDamageBreakdown,
formatBuildContributionPercent,
getBuildContributionQualityLabel,
@@ -21,6 +22,10 @@ import {
getSkillDeliveryLabel,
getSkillStyleLabel,
} from './CharacterInfoHelpers';
import { PlatformEmptyState } from './common/PlatformEmptyState';
import { PlatformPillBadge } from './common/PlatformPillBadge';
import type { PlatformPillBadgeTone } from './common/platformPillBadgeModel';
import { PlatformSubpanel } from './common/PlatformSubpanel';
export function StatusRow({
label,
@@ -68,28 +73,34 @@ export function CharacterIdentityBadges({
roleTone?: 'amber' | 'sky' | 'rose' | 'emerald' | 'zinc';
className?: string;
}) {
const roleClass =
const roleBadgeTone: PlatformPillBadgeTone =
roleTone === 'amber'
? 'border-amber-300/20 bg-amber-500/10 text-amber-100'
? 'darkAmber'
: roleTone === 'rose'
? 'border-rose-300/20 bg-rose-500/10 text-rose-100'
? 'darkRose'
: roleTone === 'emerald'
? 'border-emerald-300/20 bg-emerald-500/10 text-emerald-100'
? 'darkEmerald'
: roleTone === 'zinc'
? 'border-white/10 bg-black/20 text-zinc-200'
: 'border-sky-300/20 bg-sky-500/10 text-sky-100';
? 'darkNeutral'
: 'darkSky';
return (
<div className={`flex flex-wrap items-center gap-2 ${className}`.trim()}>
<span
className={`rounded-full border px-2.5 py-1 text-[10px] tracking-[0.16em] ${roleClass}`}
<PlatformPillBadge
tone={roleBadgeTone}
size="xxs"
className="tracking-[0.16em]"
>
{roleLabel}
</span>
</PlatformPillBadge>
{levelText ? (
<span className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1 text-[10px] tracking-[0.16em] text-zinc-200">
<PlatformPillBadge
tone="darkNeutral"
size="xxs"
className="tracking-[0.16em]"
>
{levelText}
</span>
</PlatformPillBadge>
) : null}
</div>
);
@@ -112,10 +123,7 @@ export function PlayerLevelProgress({
const ratio =
safeXpToNextLevel <= 0
? 1
: Math.max(
0,
Math.min(1, safeCurrentLevelXp / safeXpToNextLevel),
);
: Math.max(0, Math.min(1, safeCurrentLevelXp / safeXpToNextLevel));
return (
<div className={className}>
@@ -150,9 +158,9 @@ export function CharacterSkillsList({
}) {
if (skills.length === 0) {
return (
<div className="rounded-lg border border-white/5 bg-black/20 px-3 py-3 text-sm text-zinc-500">
<PlatformEmptyState surface="editorDark" size="compact" tone="soft">
{emptyText}
</div>
</PlatformEmptyState>
);
}
@@ -164,9 +172,13 @@ export function CharacterSkillsList({
<>
<div className="flex items-center justify-between gap-2">
<div className="font-semibold text-white">{skill.name}</div>
<span className="rounded-full border border-white/10 bg-white/6 px-2 py-0.5 text-[10px] text-zinc-100">
<PlatformPillBadge
tone="darkSoft"
size="xxs"
className="px-2 py-0.5"
>
{getSkillDeliveryLabel(skill)}
</span>
</PlatformPillBadge>
</div>
<div className="mt-2 grid grid-cols-2 gap-2 text-[11px] text-zinc-400">
<div>{skill.damage}</div>
@@ -182,24 +194,32 @@ export function CharacterSkillsList({
if (onSelectSkill) {
return (
<button
<PlatformSubpanel
as="button"
key={skillRenderId}
type="button"
onClick={() => onSelectSkill(skillRenderId)}
className="rounded-xl border border-white/8 bg-black/20 px-3 py-3 text-left text-sm text-zinc-300 transition-colors hover:border-sky-300/25 hover:bg-sky-500/8"
surface="dark"
radius="xs"
padding="sm"
className="text-left text-sm text-zinc-300 transition-colors hover:border-sky-300/25 hover:bg-sky-500/8"
>
{content}
</button>
</PlatformSubpanel>
);
}
return (
<div
<PlatformSubpanel
as="div"
key={skillRenderId}
className="rounded-lg border border-white/5 bg-black/20 px-3 py-3 text-sm text-zinc-300"
surface="dark"
radius="xs"
padding="sm"
className="border-white/5 bg-black/20 text-sm text-zinc-300"
>
{content}
</div>
</PlatformSubpanel>
);
})}
</div>
@@ -220,7 +240,13 @@ export function MultiplierContributionList({
);
return (
<div className="space-y-3 rounded-xl border border-sky-400/12 bg-sky-500/6 px-3 py-3">
<PlatformSubpanel
as="div"
surface="darkSky"
radius="xs"
padding="sm"
className="space-y-3"
>
<div className="flex flex-col items-start gap-1 text-[10px] uppercase tracking-[0.16em] text-sky-100/80 sm:flex-row sm:items-center sm:justify-between sm:gap-3">
<span></span>
<span className="text-[9px] leading-4 text-zinc-400 sm:text-[10px]">
@@ -251,10 +277,91 @@ export function MultiplierContributionList({
))}
</div>
) : (
<span 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="px-2">
</span>
</PlatformPillBadge>
)}
</PlatformSubpanel>
);
}
/**
* 角色构筑标签详情面板。
* 统一承接队伍面板和实体详情弹窗里的标签概览、属性加成与空明细外壳。
*/
export function BuildContributionDetailPanel({
row,
attributes,
emptyText = '当前标签还没有可展示的属性适配明细。',
}: {
row: ContributionRow;
attributes: BuildContributionAttributeRow[];
emptyText?: string;
}) {
return (
<div className="grid gap-4 lg:grid-cols-[minmax(0,18rem)_minmax(0,1fr)]">
<div className="space-y-4">
<PlatformSubpanel
surface="dark"
radius="md"
padding="md"
className="px-4 py-4"
style={getContributionVisualStyle(row.bonusDelta)}
>
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<div className="text-[10px] uppercase tracking-[0.16em] text-current/70">
</div>
<div className="mt-2 text-sm font-semibold">{row.label}</div>
</div>
<div className="rounded-xl border border-current/15 bg-black/25 px-3 py-2 text-right">
<div className="text-[11px] tracking-[0.14em] text-current/70">
{getBuildContributionQualityLabel(row.bonusDelta)}
</div>
<div className="mt-1 text-sm font-semibold">
{formatBuildContributionPercent(row.bonusDelta)}
</div>
</div>
</div>
</PlatformSubpanel>
</div>
<PlatformSubpanel surface="dark" radius="md" padding="md">
<div className="text-[10px] uppercase tracking-[0.16em] text-zinc-500">
</div>
{attributes.length > 0 ? (
<div className="mt-4 grid gap-3 sm:grid-cols-2">
{attributes.map((attribute) => (
<PlatformSubpanel
key={`${row.label}-${attribute.slotId}`}
surface="dark"
radius="xs"
padding="sm"
className="px-4 py-3"
>
<div className="flex items-center justify-between gap-3 text-sm text-zinc-200">
<span>{attribute.label}</span>
<span className="font-semibold text-white">
{formatBuildContributionPercent(attribute.modifierDelta)}
</span>
</div>
</PlatformSubpanel>
))}
</div>
) : (
<PlatformSubpanel
surface="dark"
radius="xs"
padding="sm"
className="mt-4 px-4 py-3 text-sm leading-6 text-zinc-400"
>
{emptyText}
</PlatformSubpanel>
)}
</PlatformSubpanel>
</div>
);
}