Files
epic-ui/src/pages/CharacterDetail.tsx
hxt 5cf5373696 feat(CharacterDetail): 添加角色配装推荐功能
- 新增配装推荐模块,包括平均属性、主流套装占比和具体Builds
- 添加属性图标和套装图标映射
- 实现角色基础属性展示
- 优化技能列表和印记集中展示
2025-05-24 11:01:41 +08:00

465 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;