465 lines
24 KiB
TypeScript
465 lines
24 KiB
TypeScript
import React, {useEffect} from 'react';
|
||
|
||
export interface Skill {
|
||
name: string;
|
||
type: 'None' | 'Passive' | 'Active';
|
||
soulBurn?: number;
|
||
description: string;
|
||
enhancements?: {
|
||
stars: number;
|
||
attack?: string;
|
||
health?: string;
|
||
defense?: string;
|
||
effectiveness?: string;
|
||
}[];
|
||
}
|
||
|
||
export interface ImprintConcentration {
|
||
type: string;
|
||
values: {
|
||
rank: string;
|
||
value: string;
|
||
}[];
|
||
}
|
||
|
||
interface CharacterDetailProps {
|
||
character: {
|
||
id: number;
|
||
name: string;
|
||
stars: number;
|
||
class: string;
|
||
element: string;
|
||
baseStats: {
|
||
attack: number;
|
||
health: number;
|
||
defense: number;
|
||
speed: number;
|
||
};
|
||
skills: Skill[];
|
||
imprintConcentration: ImprintConcentration[];
|
||
imageUrl: string;
|
||
};
|
||
}
|
||
|
||
// mock数据类型定义
|
||
interface GearSetRate {
|
||
setNames: string[]; // 每个set下可有1-3个套装
|
||
percent: number;
|
||
}
|
||
|
||
interface BuildStats {
|
||
attack: number;
|
||
defense: number;
|
||
health: number;
|
||
critChance: number;
|
||
critDamage: number;
|
||
effectiveness: number;
|
||
effectResistance: number;
|
||
speed: number;
|
||
}
|
||
|
||
interface BuildInfo {
|
||
stats: BuildStats;
|
||
gearSets: string[];
|
||
artifact: {
|
||
name: string;
|
||
img?: string;
|
||
};
|
||
}
|
||
|
||
// mock数据
|
||
const mockAverageStats: BuildStats = {
|
||
attack: 1887,
|
||
defense: 2136,
|
||
health: 17809,
|
||
critChance: 99,
|
||
critDamage: 267,
|
||
effectiveness: 1,
|
||
effectResistance: 2,
|
||
speed: 173,
|
||
};
|
||
|
||
const mockGearSetRates: GearSetRate[] = [
|
||
{setNames: ['免疫', '吸血'], percent: 40.37},
|
||
{setNames: ['暴击', '吸血', '速度'], percent: 28.7},
|
||
{setNames: ['生命'], percent: 7.4},
|
||
{setNames: ['命中', '防御'], percent: 5.1}, // 测试多余set不会显示
|
||
];
|
||
|
||
const mockBuilds: BuildInfo[] = [
|
||
{
|
||
stats: {
|
||
attack: 1769,
|
||
defense: 2156,
|
||
health: 19281,
|
||
critChance: 102,
|
||
critDamage: 273,
|
||
effectiveness: 22,
|
||
effectResistance: 0,
|
||
speed: 168,
|
||
},
|
||
gearSets: ['生命', '吸血', '吸血'],
|
||
artifact: {name: '伊赛丽亚的誓约', img: ''},
|
||
},
|
||
{
|
||
stats: {
|
||
attack: 1957,
|
||
defense: 2090,
|
||
health: 18369,
|
||
critChance: 100,
|
||
critDamage: 271,
|
||
effectiveness: 0,
|
||
effectResistance: 0,
|
||
speed: 178,
|
||
},
|
||
gearSets: ['暴击', '吸血'],
|
||
artifact: {name: '圣洁的遗物', img: ''},
|
||
},
|
||
{
|
||
stats: {
|
||
attack: 1894,
|
||
defense: 2030,
|
||
health: 19053,
|
||
critChance: 100,
|
||
critDamage: 272,
|
||
effectiveness: 0,
|
||
effectResistance: 0,
|
||
speed: 183,
|
||
},
|
||
gearSets: ['暴击', '吸血'],
|
||
artifact: {name: '伊赛丽亚的誓约', img: ''},
|
||
},
|
||
];
|
||
|
||
// 套装图标映射
|
||
const setIconMap: Record<string, string> = {
|
||
'Health': '/pic/item/set/Health.png',
|
||
'Immunity': '/pic/item/set/Immunity.png',
|
||
'生命': '/pic/item/set/Health.png',
|
||
'免疫': '/pic/item/set/Immunity.png',
|
||
};
|
||
|
||
// 神器图片映射
|
||
const artifactImgMap: Record<string, string> = {
|
||
'Elbris Ritual Sword': 'https://epic7db.com/images/artifacts/elbris-ritual-sword.webp',
|
||
'伊赛丽亚的誓约': 'https://epic7db.com/images/artifacts/elbris-ritual-sword.webp',
|
||
};
|
||
|
||
const CharacterDetail: React.FC<CharacterDetailProps> = ({character}) => {
|
||
useEffect(() => {
|
||
window.scrollTo({top: 0, behavior: 'auto'});
|
||
}, []);
|
||
|
||
const renderStars = (count: number) => {
|
||
return Array(count)
|
||
.fill(0)
|
||
.map((_, index) => (
|
||
<i key={index} className="text-yellow-400 text-lg">★</i>
|
||
));
|
||
};
|
||
|
||
const renderSkillType = (type: string) => {
|
||
switch (type) {
|
||
case 'None':
|
||
return <span className="bg-green-600 text-white text-xs px-2 py-1 rounded">None</span>;
|
||
case 'Passive':
|
||
return <span className="bg-green-600 text-white text-xs px-2 py-1 rounded">Passive</span>;
|
||
case 'Active':
|
||
return <span className="bg-blue-600 text-white text-xs px-2 py-1 rounded">Active</span>;
|
||
default:
|
||
return null;
|
||
}
|
||
};
|
||
|
||
const renderSoulBurn = (souls?: number) => {
|
||
if (!souls) return null;
|
||
return (
|
||
<span className="bg-teal-600 text-white text-xs px-2 py-1 rounded ml-2">
|
||
+{souls} souls
|
||
</span>
|
||
);
|
||
};
|
||
|
||
// 属性图标占位
|
||
const statIcons: Record<string, string> = {
|
||
attack: '⚔️',
|
||
defense: '🛡️',
|
||
health: '❤️',
|
||
critChance: '🎯',
|
||
critDamage: '💥',
|
||
effectiveness: '🎲',
|
||
effectResistance: '🚫',
|
||
speed: '💨',
|
||
};
|
||
|
||
return (
|
||
<div className="min-h-screen bg-[#1A1412] text-white font-sans">
|
||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex">
|
||
{/* 左侧:头像+导航 */}
|
||
<aside className="w-80 flex-shrink-0 pr-6 flex flex-col pt-12">
|
||
{/* 头像卡片 */}
|
||
<div
|
||
className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 mb-12 border border-[#C17F59]/30 flex flex-col items-center">
|
||
<div className="w-28 h-28 rounded-full overflow-hidden border-4 border-[#C17F59] mb-3">
|
||
<img src={character.imageUrl} alt={character.name} className="w-full h-full object-cover"/>
|
||
</div>
|
||
<h1 className="text-2xl font-bold text-[#E6B17E] mb-1">{character.name}</h1>
|
||
<div className="flex mb-1">{renderStars(character.stars)}</div>
|
||
<span className="text-[#9B8579] mb-2">{character.class}</span>
|
||
</div>
|
||
{/* 导航卡片 */}
|
||
<nav className="sticky top-12 z-30">
|
||
<div
|
||
className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 border border-[#C17F59]/30">
|
||
<h2 className="text-lg font-bold mb-3 text-white">Table of Contents</h2>
|
||
<ul className="space-y-2">
|
||
<li>
|
||
<a href="#base-stats" className="hover:text-[#E6B17E] transition">Base Stats</a>
|
||
</li>
|
||
<li>
|
||
<a href="#skills" className="hover:text-[#E6B17E] transition">Skills</a>
|
||
</li>
|
||
<li>
|
||
<a href="#imprint" className="hover:text-[#E6B17E] transition">Imprint
|
||
Concentration</a>
|
||
</li>
|
||
<li>
|
||
<a href="#builds" className="hover:text-[#E6B17E] transition">配装推荐</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</nav>
|
||
</aside>
|
||
|
||
{/* 右侧:详细内容 */}
|
||
<main className="flex-1 max-w-4xl mx-auto space-y-8">
|
||
{/* 基础属性 */}
|
||
<section id="base-stats"
|
||
className="scroll-mt-24 bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 border border-[#C17F59]/30 mt-12">
|
||
<h2 className="text-xl font-bold text-[#E6B17E] mb-4">六星满觉属性</h2>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
<div className="flex items-center space-x-2">
|
||
<i className="fas fa-sword text-[#C17F59]"></i>
|
||
<span>Attack: {character.baseStats.attack}</span>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<i className="fas fa-heart text-[#C17F59]"></i>
|
||
<span>Health: {character.baseStats.health}</span>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<i className="fas fa-shield-alt text-[#C17F59]"></i>
|
||
<span>Defense: {character.baseStats.defense}</span>
|
||
</div>
|
||
<div className="flex items-center space-x-2">
|
||
<i className="fas fa-wind text-[#C17F59]"></i>
|
||
<span>Speed: {character.baseStats.speed}</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
{/* 配装推荐模块 */}
|
||
<section id="builds" className="scroll-mt-24">
|
||
<h2 className="text-xl font-bold text-[#E6B17E] mb-4">角色配装推荐</h2>
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||
{/* 平均属性 */}
|
||
<div
|
||
className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 border border-[#C17F59]/30 flex flex-col gap-2 h-full justify-between">
|
||
<h3 className="text-lg font-semibold text-[#C17F59] mb-2">平均属性</h3>
|
||
<div className="divide-y divide-[#C17F59]/20">
|
||
{Object.entries(mockAverageStats).map(([key, value]) => (
|
||
<div key={key} className="flex items-center justify-between py-2">
|
||
<div className="flex items-center gap-2 min-w-[110px]">
|
||
<span>{statIcons[key] || '?'}</span>
|
||
<span className="capitalize">{key.replace(/([A-Z])/g, ' $1')}</span>
|
||
</div>
|
||
<span className="font-bold text-[#E6B17E] ml-2">
|
||
{value}{['critChance', 'critDamage', 'effectiveness', 'effectResistance'].includes(key) ? '%' : ''}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
{/* 主流套装占比 */}
|
||
<div
|
||
className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 border border-[#C17F59]/30 flex flex-col gap-2 h-full justify-between">
|
||
<h3 className="text-lg font-semibold text-[#C17F59] mb-2">主流套装占比</h3>
|
||
<div className="space-y-4">
|
||
{mockGearSetRates.slice(0, 3).map((set, idx) => (
|
||
<div key={idx} className="">
|
||
<div
|
||
className="bg-[#bcbcbc]/20 bg-[#C1BEB7]/30 px-4 py-2 font-bold text-[#222] text-base rounded-t"
|
||
style={{background: '#bcbcbc', color: '#222', opacity: 0.7}}>
|
||
Set {idx + 1} ({set.percent}% Use Rate)
|
||
</div>
|
||
<div className="flex flex-wrap gap-2 px-4 py-3 bg-[#181310] rounded-b">
|
||
{set.setNames.map((name, i) => (
|
||
<span key={i}
|
||
className="flex items-center gap-2 px-3 py-1 rounded-lg text-sm font-semibold"
|
||
style={{
|
||
background: '#23201c',
|
||
color: '#E6B17E',
|
||
fontFamily: 'inherit',
|
||
fontWeight: 600
|
||
}}>
|
||
{setIconMap[name] ? (
|
||
<img src={setIconMap[name]} alt={name}
|
||
className="w-8 h-8 mr-1"/>
|
||
) : (
|
||
<span
|
||
className="w-8 h-8 inline-block flex items-center justify-center text-[#C17F59]">?</span>
|
||
)}
|
||
<span>{name}</span>
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{/* 下方builds,两列布局 */}
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||
{mockBuilds.concat({
|
||
stats: {
|
||
attack: 1800,
|
||
defense: 2100,
|
||
health: 18500,
|
||
critChance: 101,
|
||
critDamage: 270,
|
||
effectiveness: 5,
|
||
effectResistance: 1,
|
||
speed: 175,
|
||
},
|
||
gearSets: ['速度', '命中'],
|
||
artifact: { name: 'Elbris Ritual Sword', img: '' },
|
||
}).map((build, idx) => (
|
||
<div key={idx}
|
||
className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 border border-[#C17F59]/30 flex flex-col gap-2">
|
||
<h3 className="text-lg font-semibold text-[#C17F59] mb-2">Build {idx + 1}</h3>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
{/* 左侧:属性 */}
|
||
<div className="space-y-0 divide-y divide-[#C17F59]/20">
|
||
<h4 className="font-bold text-[#E6B17E] mb-1 pb-2">属性</h4>
|
||
{Object.entries(build.stats).map(([key, value]) => (
|
||
<div key={key} className="flex items-center justify-between py-2">
|
||
<div className="flex items-center gap-2 min-w-[110px]">
|
||
<span>{statIcons[key] || '?'}</span>
|
||
<span className="capitalize">{key.replace(/([A-Z])/g, ' $1')}</span>
|
||
</div>
|
||
<span className="font-bold text-[#E6B17E] ml-2">
|
||
{value}{['critChance', 'critDamage', 'effectiveness', 'effectResistance'].includes(key) ? '%' : ''}
|
||
</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
{/* 右侧:套装图标+神器图片+神器名 */}
|
||
<div className="flex flex-col items-center justify-between h-full">
|
||
{/* 套装组合描述 */}
|
||
<h4 className="font-bold text-[#E6B17E] mb-2">套装组合</h4>
|
||
{/* 套装组合图标 */}
|
||
<div className="flex gap-3 mb-4">
|
||
{build.gearSets.map((set, i) => (
|
||
setIconMap[set] ? (
|
||
<img key={i} src={setIconMap[set]} alt={set} className="w-10 h-10" />
|
||
) : (
|
||
<span key={i} className="w-10 h-10 inline-flex items-center justify-center text-[#C17F59] text-2xl bg-[#23201c] rounded-full">?</span>
|
||
)
|
||
))}
|
||
</div>
|
||
{/* 神器图片 */}
|
||
<div className="flex-1 flex flex-col items-center justify-center">
|
||
{artifactImgMap[build.artifact.name] ? (
|
||
<img src={artifactImgMap[build.artifact.name]} alt={build.artifact.name} className="w-48 h48 rounded mb-2 object-contain" />
|
||
) : build.artifact.img ? (
|
||
<img src={build.artifact.img} alt={build.artifact.name} className="w-24 h-24 rounded mb-2 object-contain" />
|
||
) : (
|
||
<div className="w-24 h-24 rounded bg-[#2A211E] flex items-center justify-center text-3xl text-[#C17F59] mb-2">?</div>
|
||
)}
|
||
</div>
|
||
{/* 神器名称 */}
|
||
<span className="text-[#E6B17E] font-bold text-base text-center mt-2">{build.artifact.name}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</section>
|
||
|
||
{/* 技能列表 */}
|
||
<section id="skills" className="scroll-mt-24 space-y-6">
|
||
{character.skills.map((skill, index) => (
|
||
<div
|
||
key={index}
|
||
className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 border border-[#C17F59]/30"
|
||
>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-xl font-bold text-[#E6B17E]">{skill.name}</h3>
|
||
<div className="flex items-center">
|
||
{renderSkillType(skill.type)}
|
||
{renderSoulBurn(skill.soulBurn)}
|
||
</div>
|
||
</div>
|
||
<p className="text-[#9B8579] mb-4">{skill.description}</p>
|
||
{skill.enhancements && skill.enhancements.length > 0 && (
|
||
<div className="mt-4">
|
||
<h4 className="text-lg font-semibold text-[#E6B17E] mb-2">Skill
|
||
Enhancements</h4>
|
||
<div className="space-y-2">
|
||
{skill.enhancements.map((enhancement, idx) => (
|
||
<div key={idx} className="flex items-center space-x-4 text-sm">
|
||
<div className="flex">{renderStars(enhancement.stars)}</div>
|
||
<div className="flex flex-wrap gap-2">
|
||
{enhancement.attack && (
|
||
<span
|
||
className="text-[#9B8579]">Attack {enhancement.attack}</span>
|
||
)}
|
||
{enhancement.health && (
|
||
<span
|
||
className="text-[#9B8579]">Health {enhancement.health}</span>
|
||
)}
|
||
{enhancement.defense && (
|
||
<span
|
||
className="text-[#9B8579]">Defense {enhancement.defense}</span>
|
||
)}
|
||
{enhancement.effectiveness && (
|
||
<span
|
||
className="text-[#9B8579]">Effectiveness {enhancement.effectiveness}</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
))}
|
||
</section>
|
||
|
||
{/* 印记集中 */}
|
||
<section id="imprint"
|
||
className="scroll-mt-24 bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 border border-[#C17F59]/30">
|
||
<h2 className="text-xl font-bold text-[#E6B17E] mb-4">Imprint Concentration</h2>
|
||
{character.imprintConcentration.map((imprint, index) => (
|
||
<div key={index} className="mb-4">
|
||
<h3 className="text-lg font-semibold text-[#C17F59] mb-2">{imprint.type}</h3>
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||
{imprint.values.map((value, idx) => (
|
||
<div
|
||
key={idx}
|
||
className="flex items-center justify-between bg-[#1A1412] p-2 rounded border border-[#C17F59]/30"
|
||
>
|
||
<span className="text-[#E6B17E]">{value.rank}</span>
|
||
<span className="text-[#9B8579]">{value.value}</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</section>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default CharacterDetail;
|