初始仓库迁移
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-04 23:57:06 +08:00
parent 80986b790d
commit c49c64896a
18446 changed files with 532435 additions and 2 deletions

View File

@@ -0,0 +1,898 @@
import { type ReactNode, useDeferredValue, useEffect, useMemo, useState } from 'react';
import { PRESET_CHARACTERS } from '../data/characterPresets';
import { getInventoryItemValue } from '../data/economy';
import { validateItemOverrides } from '../data/editorValidation';
import { getEquipmentSlotFromItem, getEquipmentSlotLabel } from '../data/equipmentEffects';
import { isInventoryItemUsable, resolveInventoryItemUseEffect } from '../data/inventoryEffects';
import {
applyItemCatalogOverride,
buildItemCatalogFromAssetPaths,
createInventoryItemFromCatalogEntry,
ITEM_CATALOG_API_PATH,
ITEM_CATEGORY_OPTIONS,
ITEM_OVERRIDES_API_PATH,
} from '../data/itemCatalog';
import { fetchJson, saveJsonObject } from '../editor/shared/jsonClient';
import { SectionCard as Section } from '../editor/shared/SectionCard';
import { type ItemCatalogOverride, type ItemRarity, type TimedBuildBuff,WorldType } from '../types';
import { PixelIcon } from './PixelIcon';
const ITEM_PREVIEW_CHARACTER = PRESET_CHARACTERS[0] ?? null;
const LIST_PREVIEW_LIMIT = 240;
type ItemCatalogAssetResponse = {
assetPaths: string[];
};
const RARITY_OPTIONS: ItemRarity[] = ['common', 'uncommon', 'rare', 'epic', 'legendary'];
const RARITY_LABELS: Record<ItemRarity, string> = {
common: '普通',
uncommon: '不普通',
rare: '稀有',
epic: '史诗',
legendary: '传奇',
};
function arraysEqual(left: string[], right: string[]) {
if (left.length !== right.length) return false;
return left.every((value, index) => value === right[index]);
}
function parseTagsInput(value: string) {
return [...new Set(
value
.split(/[\n,]/u)
.map(tag => tag.trim())
.filter(Boolean),
)];
}
function tagsInputValue(tags: string[]) {
return tags.join(', ');
}
function parseBuildBuffLines(
value: string,
sourceType: TimedBuildBuff['sourceType'],
sourceId: string,
) {
return value
.split('\n')
.map(line => line.trim())
.filter(Boolean)
.map((line, index) => {
const [namePart, tagsPart, durationPart] = line.split('|').map(part => part.trim());
const tags = parseTagsInput(tagsPart ?? '');
const durationTurns = Math.max(1, Number(durationPart ?? '1') || 1);
return {
id: `${sourceId}-buff-${index + 1}`,
sourceType,
sourceId,
name: namePart || `${sourceId}-buff-${index + 1}`,
tags,
durationTurns,
} satisfies TimedBuildBuff;
})
.filter(buff => buff.tags.length > 0);
}
function buildBuffLinesValue(buffs: TimedBuildBuff[] | null | undefined) {
return (buffs ?? [])
.map(buff => `${buff.name}|${buff.tags.join(',')}|${buff.durationTurns}`)
.join('\n');
}
function Label({ children }: { children: ReactNode }) {
return <div className="mb-1 text-xs font-medium text-zinc-300">{children}</div>;
}
function TextInput({
value,
onChange,
placeholder,
disabled = false,
}: {
value: string;
onChange: (value: string) => void;
placeholder?: string;
disabled?: boolean;
}) {
return (
<input
value={value}
onChange={event => onChange(event.target.value)}
placeholder={placeholder}
disabled={disabled}
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white outline-none transition focus:border-emerald-400/40 disabled:cursor-not-allowed disabled:opacity-60"
/>
);
}
function TextArea({
value,
onChange,
rows = 4,
}: {
value: string;
onChange: (value: string) => void;
rows?: number;
}) {
return (
<textarea
rows={rows}
value={value}
onChange={event => onChange(event.target.value)}
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm leading-relaxed text-white outline-none transition focus:border-emerald-400/40"
/>
);
}
function Select({
value,
onChange,
options,
}: {
value: string;
onChange: (value: string) => void;
options: Array<{ label: string; value: string }>;
}) {
return (
<select
value={value}
onChange={event => onChange(event.target.value)}
className="w-full rounded-lg border border-white/10 bg-black/30 px-3 py-2 text-sm text-white outline-none transition focus:border-emerald-400/40"
>
{options.map(option => (
<option key={`${option.value}-${option.label}`} value={option.value}>
{option.label}
</option>
))}
</select>
);
}
export function ItemCatalogEditor() {
const [assetPaths, setAssetPaths] = useState<string[]>([]);
const [overrideMap, setOverrideMap] = useState<Record<string, ItemCatalogOverride>>({});
const [selectedItemId, setSelectedItemId] = useState('');
const [searchText, setSearchText] = useState('');
const [categoryFilter, setCategoryFilter] = useState('ALL');
const [rarityFilter, setRarityFilter] = useState<'ALL' | ItemRarity>('ALL');
const [previewWorld, setPreviewWorld] = useState<WorldType>(WorldType.WUXIA);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [loadError, setLoadError] = useState<string | null>(null);
const [saveMessage, setSaveMessage] = useState<string | null>(null);
const deferredSearchText = useDeferredValue(searchText);
useEffect(() => {
let disposed = false;
const load = async () => {
setIsLoading(true);
setLoadError(null);
try {
const [catalogResponse, overridesResponse] = await Promise.all([
fetchJson<ItemCatalogAssetResponse>(ITEM_CATALOG_API_PATH),
fetchJson<Record<string, ItemCatalogOverride>>(ITEM_OVERRIDES_API_PATH),
]);
if (disposed) return;
const nextAssetPaths = catalogResponse.assetPaths ?? [];
setAssetPaths(nextAssetPaths);
setOverrideMap(overridesResponse ?? {});
setSelectedItemId(current => current || (buildItemCatalogFromAssetPaths(nextAssetPaths)[0]?.id ?? ''));
} catch (error) {
if (disposed) return;
setLoadError(error instanceof Error ? error.message : '物品目录加载失败');
} finally {
if (!disposed) {
setIsLoading(false);
}
}
};
void load();
return () => {
disposed = true;
};
}, []);
const baseItems = useMemo(
() => buildItemCatalogFromAssetPaths(assetPaths),
[assetPaths],
);
const baseItemMap = useMemo(
() => new Map(baseItems.map(item => [item.id, item])),
[baseItems],
);
const effectiveItems = useMemo(
() => baseItems.map(item => applyItemCatalogOverride(item, overrideMap[item.id])),
[baseItems, overrideMap],
);
const filteredItems = useMemo(() => {
const query = deferredSearchText.trim().toLowerCase();
return effectiveItems.filter(item => {
if (categoryFilter !== 'ALL' && item.category !== categoryFilter) return false;
if (rarityFilter !== 'ALL' && item.rarity !== rarityFilter) return false;
if (!query) return true;
const haystack = [
item.name,
item.category,
item.rarity,
item.description,
item.sourcePath,
...item.tags,
].join(' ').toLowerCase();
return haystack.includes(query);
});
}, [categoryFilter, deferredSearchText, effectiveItems, rarityFilter]);
const visibleItems = useMemo(
() => filteredItems.slice(0, LIST_PREVIEW_LIMIT),
[filteredItems],
);
useEffect(() => {
if (!effectiveItems.length) {
setSelectedItemId('');
return;
}
if (!selectedItemId || !baseItemMap.has(selectedItemId)) {
setSelectedItemId(effectiveItems[0]?.id ?? '');
}
}, [baseItemMap, effectiveItems, selectedItemId]);
const selectedBaseItem = selectedItemId ? baseItemMap.get(selectedItemId) ?? null : null;
const selectedItem = selectedBaseItem
? applyItemCatalogOverride(selectedBaseItem, overrideMap[selectedBaseItem.id])
: null;
const selectedOverride = selectedItemId ? overrideMap[selectedItemId] ?? null : null;
const previewInventoryItem = useMemo(
() => selectedItem ? createInventoryItemFromCatalogEntry(selectedItem, 1, previewWorld) : null,
[previewWorld, selectedItem],
);
const worldProfile = selectedItem?.worldProfiles?.[previewWorld] ?? null;
const previewUseEffect = useMemo(
() => (previewInventoryItem && ITEM_PREVIEW_CHARACTER)
? resolveInventoryItemUseEffect(previewInventoryItem, ITEM_PREVIEW_CHARACTER)
: null,
[previewInventoryItem],
);
const previewEquipmentSlot = useMemo(
() => previewInventoryItem
? getEquipmentSlotFromItem(previewInventoryItem)
: null,
[previewInventoryItem],
);
const updateSelectedOverride = <K extends keyof ItemCatalogOverride>(
key: K,
value: ItemCatalogOverride[K],
) => {
if (!selectedBaseItem) return;
setOverrideMap(current => {
const nextOverride = {
...(current[selectedBaseItem.id] ?? {}),
[key]: value,
};
const normalizedOverride: ItemCatalogOverride = {...nextOverride};
if ((normalizedOverride.name ?? selectedBaseItem.name) === selectedBaseItem.name) {
delete normalizedOverride.name;
}
if ((normalizedOverride.category ?? selectedBaseItem.category) === selectedBaseItem.category) {
delete normalizedOverride.category;
}
if ((normalizedOverride.rarity ?? selectedBaseItem.rarity) === selectedBaseItem.rarity) {
delete normalizedOverride.rarity;
}
if ((normalizedOverride.description ?? selectedBaseItem.description) === selectedBaseItem.description) {
delete normalizedOverride.description;
}
if (
normalizedOverride.tags &&
arraysEqual(normalizedOverride.tags, selectedBaseItem.tags)
) {
delete normalizedOverride.tags;
}
const hasOverride = Object.keys(normalizedOverride).length > 0;
if (!hasOverride) {
const { [selectedBaseItem.id]: _removed, ...rest } = current;
return rest;
}
return {
...current,
[selectedBaseItem.id]: normalizedOverride,
};
});
};
const updateSelectedStatProfileField = (
key: 'maxHpBonus' | 'maxManaBonus' | 'outgoingDamageBonus' | 'incomingDamageMultiplier',
value: number,
) => {
if (!selectedItem) return;
const nextProfile = {
...(selectedItem.statProfile ?? {}),
[key]: value,
};
updateSelectedOverride('statProfile', nextProfile);
};
const updateSelectedUseProfileField = (
key: 'hpRestore' | 'manaRestore' | 'cooldownReduction',
value: number,
) => {
if (!selectedItem) return;
const nextProfile = {
...(selectedItem.useProfile ?? {}),
[key]: value,
buildBuffs: selectedItem.useProfile?.buildBuffs ?? [],
};
updateSelectedOverride('useProfile', nextProfile);
};
const updateSelectedUseProfileBuffs = (value: string) => {
if (!selectedItem) return;
const nextProfile = {
...(selectedItem.useProfile ?? {}),
hpRestore: selectedItem.useProfile?.hpRestore ?? 0,
manaRestore: selectedItem.useProfile?.manaRestore ?? 0,
cooldownReduction: selectedItem.useProfile?.cooldownReduction ?? 0,
buildBuffs: parseBuildBuffLines(value, 'item', selectedItem.id),
};
updateSelectedOverride('useProfile', nextProfile);
};
const updateSelectedBuildProfileField = (
key: 'role' | 'setId' | 'setName' | 'pieceName' | 'forgeRank',
value: string | number,
) => {
if (!selectedItem) return;
const nextProfile = {
...(selectedItem.buildProfile ?? { role: '', tags: [], synergy: [], craftTags: [], forgeRank: 0 }),
[key]: value,
tags: selectedItem.buildProfile?.tags ?? [],
synergy: selectedItem.buildProfile?.synergy ?? [],
craftTags: selectedItem.buildProfile?.craftTags ?? [],
};
updateSelectedOverride('buildProfile', nextProfile);
};
const updateSelectedBuildProfileTags = (key: 'tags' | 'synergy' | 'craftTags', value: string) => {
if (!selectedItem) return;
const nextProfile = {
...(selectedItem.buildProfile ?? { role: '', tags: [], synergy: [], craftTags: [], forgeRank: 0 }),
role: selectedItem.buildProfile?.role ?? '',
tags: key === 'tags' ? parseTagsInput(value) : (selectedItem.buildProfile?.tags ?? []),
synergy: key === 'synergy' ? parseTagsInput(value) : (selectedItem.buildProfile?.synergy ?? []),
craftTags: key === 'craftTags' ? parseTagsInput(value) : (selectedItem.buildProfile?.craftTags ?? []),
forgeRank: selectedItem.buildProfile?.forgeRank ?? 0,
};
updateSelectedOverride('buildProfile', nextProfile);
};
const resetSelectedOverride = () => {
if (!selectedItemId) return;
setOverrideMap(current => {
const { [selectedItemId]: _removed, ...rest } = current;
return rest;
});
};
const handleSave = async () => {
const validationErrors = validateItemOverrides(
overrideMap,
baseItems.map(item => item.id),
);
if (validationErrors.length > 0) {
setSaveMessage(validationErrors.slice(0, 3).join(' | '));
setTimeout(() => setSaveMessage(null), 5000);
return;
}
setIsSaving(true);
setSaveMessage(null);
try {
await saveJsonObject(ITEM_OVERRIDES_API_PATH, overrideMap as Record<string, unknown>);
setSaveMessage('物品覆盖已保存到 src/data/itemOverrides.json。');
setTimeout(() => setSaveMessage(null), 5000);
} catch (error) {
setSaveMessage(error instanceof Error ? error.message : '保存失败');
setTimeout(() => setSaveMessage(null), 5000);
} finally {
setIsSaving(false);
}
};
const categoryOptions = [
{ label: '全部分类', value: 'ALL' },
...ITEM_CATEGORY_OPTIONS.map(category => ({ label: category, value: category })),
];
const rarityOptions = [
{ label: '全部稀有度', value: 'ALL' },
...RARITY_OPTIONS.map(rarity => ({ label: rarity, value: rarity })),
];
if (isLoading) {
return (
<div className="rounded-2xl border border-white/10 bg-black/20 p-6 text-sm text-zinc-300">
...
</div>
);
}
if (loadError) {
return (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 p-6 text-sm text-rose-100">
{loadError}
</div>
);
}
return (
<div className="grid gap-6 xl:grid-cols-[360px_1fr_420px]">
<Section title="物品列表" description="基于 public/Icons 下的全部 png 素材自动构建物品目录,可按名称、路径、分类和稀有度筛选。">
<div className="grid gap-3">
<div>
<Label></Label>
<TextInput
value={searchText}
onChange={setSearchText}
placeholder="按名称、路径、标签搜索"
/>
</div>
<div className="grid gap-3 md:grid-cols-2">
<div>
<Label></Label>
<Select value={categoryFilter} onChange={setCategoryFilter} options={categoryOptions} />
</div>
<div>
<Label></Label>
<Select value={rarityFilter} onChange={value => setRarityFilter(value as 'ALL' | ItemRarity)} options={rarityOptions} />
</div>
</div>
</div>
<div className="mt-4 rounded-xl border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-zinc-400">
{effectiveItems.length} {filteredItems.length} {Math.min(visibleItems.length, LIST_PREVIEW_LIMIT)}
</div>
<div className="mt-4 max-h-[70vh] space-y-2 overflow-y-auto pr-1 scrollbar-hide">
{visibleItems.map(item => {
const selected = item.id === selectedItemId;
const overridden = Boolean(overrideMap[item.id]);
return (
<button
key={item.id}
type="button"
onClick={() => setSelectedItemId(item.id)}
className={`flex w-full items-center gap-3 rounded-xl border px-3 py-2 text-left transition ${
selected
? 'border-emerald-400/40 bg-emerald-500/10'
: 'border-white/8 bg-black/20 hover:border-white/15'
}`}
>
<div className="flex h-12 w-12 shrink-0 items-center justify-center rounded-lg border border-white/10 bg-black/35">
<PixelIcon src={item.iconSrc} className="h-9 w-9" />
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-white">{item.name}</div>
<div className="mt-1 truncate text-[10px] text-zinc-500">{item.sourcePath}</div>
<div className="mt-1 flex flex-wrap gap-1.5">
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-0.5 text-[10px] text-zinc-300">
{item.category}
</span>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-0.5 text-[10px] text-zinc-300">
{item.rarity}
</span>
{overridden && (
<span className="rounded-full border border-amber-400/20 bg-amber-500/10 px-2 py-0.5 text-[10px] text-amber-100">
</span>
)}
</div>
</div>
</button>
);
})}
</div>
</Section>
<Section title="物品预览" description="这里会实时预览当前素材构建出的物品效果,包括图标、系统推断结果以及一张背包卡片。">
{selectedItem ? (
<div className="space-y-5">
<div className="grid gap-4 lg:grid-cols-[220px_1fr]">
<div className="flex min-h-[240px] items-center justify-center rounded-2xl border border-white/10 bg-[radial-gradient(circle_at_top,rgba(255,255,255,0.12),transparent_45%),linear-gradient(180deg,#171a22,#0d1016)] p-6">
<PixelIcon src={selectedItem.iconSrc} className="h-40 w-40" />
</div>
<div className="space-y-3 rounded-2xl border border-white/10 bg-black/20 p-4">
<div>
<div className="text-xs uppercase tracking-[0.2em] text-zinc-500">{selectedItem.category}</div>
<div className="mt-1 text-xl font-semibold text-white">{selectedItem.name}</div>
<div className="mt-1 text-xs text-zinc-500">{selectedItem.sourcePath}</div>
</div>
<div className="max-w-[12rem]">
<Label></Label>
<Select
value={previewWorld}
onChange={value => setPreviewWorld(value as WorldType)}
options={[
{ label: '武侠', value: WorldType.WUXIA },
{ label: '仙侠', value: WorldType.XIANXIA },
]}
/>
</div>
<div className="flex flex-wrap gap-2">
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-1 text-[11px] text-zinc-200">
: {RARITY_LABELS[selectedItem.rarity]}
</span>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-1 text-[11px] text-zinc-200">
: {getInventoryItemValue(previewInventoryItem!)}
</span>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-1 text-[11px] text-zinc-200">
使: {isInventoryItemUsable(previewInventoryItem!) ? '是' : '否'}
</span>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-1 text-[11px] text-zinc-200">
: {previewEquipmentSlot ? getEquipmentSlotLabel(previewEquipmentSlot) : '否'}
</span>
<span className="rounded-full border border-white/10 bg-white/[0.04] px-2 py-1 text-[11px] text-zinc-200">
: {selectedItem.worldAffinity === 'neutral' ? '中立' : selectedItem.worldAffinity === 'wuxia' ? '武侠' : selectedItem.worldAffinity === 'xianxia' ? '仙侠' : '中立'}
</span>
</div>
<p className="text-sm leading-relaxed text-zinc-300">{selectedItem.description}</p>
{worldProfile && (
<div className="rounded-xl border border-white/10 bg-white/[0.03] px-3 py-3">
<div className="text-[11px] uppercase tracking-[0.18em] text-zinc-500">
{previewWorld === WorldType.WUXIA ? '武侠命名' : '仙侠命名'}
</div>
<div className="mt-1 text-sm font-semibold text-white">{worldProfile.name}</div>
<div className="mt-2 text-sm leading-relaxed text-zinc-300">{worldProfile.description}</div>
</div>
)}
<div className="flex flex-wrap gap-2">
{selectedItem.tags.length > 0 ? selectedItem.tags.map(tag => (
<span
key={`${selectedItem.id}-${tag}`}
className="rounded-full border border-white/10 bg-black/25 px-2 py-1 text-[10px] text-zinc-300"
>
{tag}
</span>
)) : (
<span className="rounded-full border border-white/10 bg-black/25 px-2 py-1 text-[10px] text-zinc-300">
</span>
)}
</div>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-3">
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.2em] text-zinc-500"></div>
<div className="space-y-1 text-sm text-zinc-300">
<div>: {selectedItem.statProfile?.maxHpBonus ?? 0}</div>
<div>: {selectedItem.statProfile?.maxManaBonus ?? 0}</div>
<div>: {selectedItem.statProfile?.outgoingDamageBonus ?? 0}</div>
<div>: {selectedItem.statProfile?.incomingDamageMultiplier ?? 1}</div>
</div>
</div>
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.2em] text-zinc-500">使</div>
{selectedItem.useProfile ? (
<div className="space-y-1 text-sm text-zinc-300">
<div>: {selectedItem.useProfile.hpRestore ?? 0}</div>
<div>: {selectedItem.useProfile.manaRestore ?? 0}</div>
<div>: {selectedItem.useProfile.cooldownReduction ?? 0}</div>
<div>: {(selectedItem.useProfile.buildBuffs ?? []).map(buff => buff.name).join(' / ') || '无'}</div>
</div>
) : (
<div className="text-sm text-zinc-500">使</div>
)}
</div>
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
<div className="mb-2 text-xs font-semibold uppercase tracking-[0.2em] text-zinc-500"> / </div>
{selectedItem.buildProfile ? (
<div className="space-y-1 text-sm text-zinc-300">
<div>: {selectedItem.buildProfile.role}</div>
<div>: {selectedItem.buildProfile.setName ?? '无'}</div>
<div>: {selectedItem.buildProfile.pieceName ?? '独立'}</div>
<div>{(selectedItem.buildProfile.synergy ?? []).join(' / ') || '无'}</div>
<div>: {(selectedItem.buildProfile.craftTags ?? []).join(' / ') || '无'}</div>
<div>: {selectedItem.buildProfile.forgeRank ?? 0}</div>
</div>
) : (
<div className="text-sm text-zinc-500"></div>
)}
</div>
</div>
<div className="rounded-2xl border border-white/10 bg-black/20 p-4">
<div className="mb-3 text-xs font-semibold uppercase tracking-[0.2em] text-zinc-500"></div>
<div className="flex items-center gap-4 rounded-2xl border border-white/10 bg-black/25 p-4">
<div className="relative flex h-24 w-24 shrink-0 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.04]">
<PixelIcon src={selectedItem.iconSrc} className="h-14 w-14" />
<div className="absolute bottom-2 right-2 rounded-full border border-black/30 bg-black/65 px-1.5 py-0.5 text-[10px] font-semibold text-white">
1
</div>
</div>
<div className="min-w-0 flex-1 space-y-2">
<div className="text-base font-semibold text-white">{selectedItem.name}</div>
<div className="text-sm text-zinc-400">{selectedItem.category} / {RARITY_LABELS[selectedItem.rarity]}</div>
{previewUseEffect && (
<div className="text-sm text-zinc-300">
HP +{previewUseEffect.hpRestore} / MP +{previewUseEffect.manaRestore} / CD -{previewUseEffect.cooldownReduction}
</div>
)}
{!previewUseEffect && (
<div className="text-sm text-zinc-400">
使
</div>
)}
</div>
</div>
</div>
</div>
) : (
<div className="rounded-xl border border-white/10 bg-black/20 px-4 py-6 text-sm text-zinc-400">
</div>
)}
</Section>
<Section title="物品字段" description="编辑当前物品的覆盖字段。未修改的字段不会写入 override重置后会恢复自动生成值。">
{selectedBaseItem && selectedItem ? (
<div className="space-y-4">
<div>
<Label> ID</Label>
<TextInput value={selectedItem.id} onChange={() => undefined} disabled />
</div>
<div>
<Label></Label>
<TextInput value={selectedItem.sourcePath} onChange={() => undefined} disabled />
</div>
<div>
<Label></Label>
<TextInput
value={selectedItem.name}
onChange={value => updateSelectedOverride('name', value)}
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div>
<Label></Label>
<Select
value={selectedItem.category}
onChange={value => updateSelectedOverride('category', value)}
options={ITEM_CATEGORY_OPTIONS.map(category => ({ label: category, value: category }))}
/>
</div>
<div>
<Label></Label>
<Select
value={selectedItem.rarity}
onChange={value => updateSelectedOverride('rarity', value as ItemRarity)}
options={RARITY_OPTIONS.map(rarity => ({ label: RARITY_LABELS[rarity], value: rarity }))}
/>
</div>
</div>
<div>
<Label></Label>
<TextArea
value={tagsInputValue(selectedItem.tags)}
onChange={value => updateSelectedOverride('tags', parseTagsInput(value))}
rows={4}
/>
<div className="mt-1 text-xs text-zinc-500"></div>
</div>
<div>
<Label></Label>
<TextArea
value={selectedItem.description}
onChange={value => updateSelectedOverride('description', value)}
rows={5}
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div>
<Label>HP </Label>
<TextInput
value={String(selectedItem.statProfile?.maxHpBonus ?? 0)}
onChange={value => updateSelectedStatProfileField('maxHpBonus', Number(value) || 0)}
/>
</div>
<div>
<Label>MP </Label>
<TextInput
value={String(selectedItem.statProfile?.maxManaBonus ?? 0)}
onChange={value => updateSelectedStatProfileField('maxManaBonus', Number(value) || 0)}
/>
</div>
<div>
<Label></Label>
<TextInput
value={String(selectedItem.statProfile?.outgoingDamageBonus ?? 0)}
onChange={value => updateSelectedStatProfileField('outgoingDamageBonus', Number(value) || 0)}
/>
</div>
<div>
<Label></Label>
<TextInput
value={String(selectedItem.statProfile?.incomingDamageMultiplier ?? 1)}
onChange={value => updateSelectedStatProfileField('incomingDamageMultiplier', Number(value) || 1)}
/>
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div>
<Label>使 HP</Label>
<TextInput
value={String(selectedItem.useProfile?.hpRestore ?? 0)}
onChange={value => updateSelectedUseProfileField('hpRestore', Number(value) || 0)}
/>
</div>
<div>
<Label>使 MP</Label>
<TextInput
value={String(selectedItem.useProfile?.manaRestore ?? 0)}
onChange={value => updateSelectedUseProfileField('manaRestore', Number(value) || 0)}
/>
</div>
<div>
<Label>使</Label>
<TextInput
value={String(selectedItem.useProfile?.cooldownReduction ?? 0)}
onChange={value => updateSelectedUseProfileField('cooldownReduction', Number(value) || 0)}
/>
</div>
</div>
<div>
<Label>使 Build Buff|1,2|</Label>
<TextArea
value={buildBuffLinesValue(selectedItem.useProfile?.buildBuffs)}
onChange={updateSelectedUseProfileBuffs}
rows={4}
/>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div>
<Label></Label>
<TextInput
value={selectedItem.buildProfile?.role ?? ''}
onChange={value => updateSelectedBuildProfileField('role', value)}
/>
</div>
<div>
<Label> ID</Label>
<TextInput
value={selectedItem.buildProfile?.setId ?? ''}
onChange={value => updateSelectedBuildProfileField('setId', value)}
/>
</div>
<div>
<Label></Label>
<TextInput
value={selectedItem.buildProfile?.setName ?? ''}
onChange={value => updateSelectedBuildProfileField('setName', value)}
/>
</div>
<div>
<Label></Label>
<TextInput
value={selectedItem.buildProfile?.pieceName ?? ''}
onChange={value => updateSelectedBuildProfileField('pieceName', value)}
/>
</div>
<div>
<Label></Label>
<TextInput
value={String(selectedItem.buildProfile?.forgeRank ?? 0)}
onChange={value => updateSelectedBuildProfileField('forgeRank', Number(value) || 0)}
/>
</div>
</div>
<div>
<Label></Label>
<TextArea
value={tagsInputValue(selectedItem.buildProfile?.tags ?? [])}
onChange={value => updateSelectedBuildProfileTags('tags', value)}
rows={3}
/>
</div>
<div>
<Label></Label>
<TextArea
value={tagsInputValue(selectedItem.buildProfile?.synergy ?? [])}
onChange={value => updateSelectedBuildProfileTags('synergy', value)}
rows={3}
/>
</div>
<div>
<Label></Label>
<TextArea
value={tagsInputValue(selectedItem.buildProfile?.craftTags ?? [])}
onChange={value => updateSelectedBuildProfileTags('craftTags', value)}
rows={3}
/>
</div>
<div className="rounded-xl border border-white/10 bg-white/[0.03] px-3 py-2 text-xs text-zinc-400">
{selectedOverride ? '该物品有覆盖字段,保存后会写入 itemOverrides.json。' : '当前全部字段都在使用自动生成值。'}
</div>
<div className="flex flex-wrap items-center gap-3">
<button
type="button"
onClick={handleSave}
disabled={isSaving}
className="rounded-lg bg-emerald-500 px-4 py-2 text-sm font-medium text-black transition hover:bg-emerald-400 disabled:cursor-not-allowed disabled:opacity-60"
>
{isSaving ? '保存中...' : '保存物品覆盖'}
</button>
<button
type="button"
onClick={resetSelectedOverride}
disabled={!selectedOverride}
className={`rounded-lg border px-4 py-2 text-sm transition ${
selectedOverride
? 'border-white/15 bg-black/20 text-white hover:border-white/30'
: 'border-white/8 bg-black/20 text-zinc-500'
}`}
>
</button>
{saveMessage && <div className="text-xs text-zinc-400">{saveMessage}</div>}
</div>
</div>
) : (
<div className="rounded-xl border border-white/10 bg-black/20 px-4 py-6 text-sm text-zinc-400">
</div>
)}
</Section>
</div>
);
}