收口前端平台组件库能力

新增 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

@@ -14,6 +14,9 @@ import {
NarrativeQaReport,
WorldType,
} from '../types';
import { PlatformPillBadge } from './common/PlatformPillBadge';
import { PlatformStatusMessage } from './common/PlatformStatusMessage';
import { PlatformSubpanel } from './common/PlatformSubpanel';
import {
InventoryItemDetailModal,
InventoryItemGrid,
@@ -83,7 +86,10 @@ export function InventoryPanel({
[playerCharacter, playerInventory, serverInventoryItems, worldType],
);
const documentItems = useMemo(
() => inventoryItems.filter((item) => item.category === '文书' || item.tags.includes('document')),
() =>
inventoryItems.filter(
(item) => item.category === '文书' || item.tags.includes('document'),
),
[inventoryItems],
);
@@ -97,43 +103,65 @@ export function InventoryPanel({
/>
{documentItems.length > 0 && (
<div className="mt-4 rounded-2xl border border-white/10 bg-black/20 p-4">
<PlatformSubpanel
surface="dark"
radius="sm"
padding="md"
className="mt-4"
>
<div className="mb-2 text-xs uppercase tracking-[0.2em] text-zinc-500">
</div>
<div className="space-y-2">
{documentItems.map((item) => (
<button
<PlatformSubpanel
as="button"
key={item.id}
type="button"
onClick={() => setSelectedItem(item)}
className="w-full rounded-xl border border-white/8 bg-black/20 px-3 py-2 text-left transition hover:border-white/15"
surface="dark"
radius="xs"
padding="row"
className="w-full text-left transition hover:border-white/15"
>
<div className="text-sm font-semibold text-white">{item.name}</div>
<div className="text-sm font-semibold text-white">
{item.name}
</div>
<div className="mt-1 text-xs text-zinc-400">
{item.description || '记录着当前线程的阶段性线索。'}
</div>
</button>
</PlatformSubpanel>
))}
</div>
</div>
</PlatformSubpanel>
)}
{(narrativeCodex.length > 0 || narrativeQaReport) && (
<div className="mt-4 rounded-2xl border border-white/10 bg-black/20 p-4">
<PlatformSubpanel
surface="dark"
radius="sm"
padding="md"
className="mt-4"
>
<div className="mb-2 text-xs uppercase tracking-[0.2em] text-zinc-500">
</div>
{narrativeQaReport && (
<div className="mb-3 rounded-xl border border-amber-400/18 bg-amber-500/8 px-3 py-2 text-xs text-amber-100/85">
<PlatformStatusMessage
tone="warning"
surface="editorDark"
size="xs"
className="mb-3"
>
QA{narrativeQaReport.summary}
</div>
</PlatformStatusMessage>
)}
<div className="space-y-3">
{narrativeCodex.slice(0, 3).map((section) => (
<div
<PlatformSubpanel
key={section.id}
className="rounded-xl border border-white/8 bg-black/20 p-3"
surface="dark"
radius="xs"
padding="sm"
>
<div className="text-sm font-semibold text-white">
{section.title}
@@ -147,13 +175,18 @@ export function InventoryPanel({
</div>
))}
</div>
</div>
</PlatformSubpanel>
))}
</div>
</div>
</PlatformSubpanel>
)}
<div className="mt-4 rounded-2xl border border-white/10 bg-black/20 p-4">
<PlatformSubpanel
surface="dark"
radius="sm"
padding="md"
className="mt-4"
>
<div className="mb-2 flex items-center justify-between gap-3 text-xs uppercase tracking-[0.2em] text-zinc-500">
<span></span>
<span className="text-emerald-200/80">
@@ -162,9 +195,11 @@ export function InventoryPanel({
</div>
<div className="space-y-3">
{forgeRecipes.map((recipe) => (
<div
<PlatformSubpanel
key={recipe.id}
className="rounded-xl border border-white/8 bg-black/20 p-3"
surface="dark"
radius="xs"
padding="sm"
>
<div className="flex items-start justify-between gap-3">
<div className="min-w-0">
@@ -211,19 +246,22 @@ export function InventoryPanel({
</button>
</div>
<div className="mt-2 flex flex-wrap gap-2">
{recipe.requirements.map((requirement) => (
<span
key={`${recipe.id}-${requirement.id}`}
className={`rounded-full border px-2 py-1 text-[10px] ${
requirement.owned >= requirement.quantity
? 'border-emerald-400/20 bg-emerald-500/10 text-emerald-100'
: 'border-white/10 bg-black/20 text-zinc-400'
}`}
>
{requirement.label} {requirement.owned}/
{requirement.quantity}
</span>
))}
{recipe.requirements.map((requirement) => {
const isRequirementMet =
requirement.owned >= requirement.quantity;
return (
<PlatformPillBadge
key={`${recipe.id}-${requirement.id}`}
tone={isRequirementMet ? 'darkEmerald' : 'darkNeutral'}
size="xxs"
className={`px-2 ${isRequirementMet ? '' : 'text-zinc-400'}`}
>
{requirement.label} {requirement.owned}/
{requirement.quantity}
</PlatformPillBadge>
);
})}
</div>
{(!recipe.canCraft || !recipe.action.enabled) &&
(recipe.disabledReason || recipe.action.reason) && (
@@ -231,10 +269,10 @@ export function InventoryPanel({
{recipe.disabledReason ?? recipe.action.reason}
</div>
)}
</div>
</PlatformSubpanel>
))}
</div>
</div>
</PlatformSubpanel>
</div>
<InventoryItemDetailModal