feat(CharacterDetail): 添加角色配装推荐功能
- 新增配装推荐模块,包括平均属性、主流套装占比和具体Builds - 添加属性图标和套装图标映射 - 实现角色基础属性展示 - 优化技能列表和印记集中展示
BIN
public/pic/item/attr/cm_icon_stat_attack.png
Normal file
|
After Width: | Height: | Size: 619 B |
BIN
public/pic/item/attr/cm_icon_stat_crit_chance.png
Normal file
|
After Width: | Height: | Size: 738 B |
BIN
public/pic/item/attr/cm_icon_stat_crit_damage.png
Normal file
|
After Width: | Height: | Size: 699 B |
BIN
public/pic/item/attr/cm_icon_stat_defense.png
Normal file
|
After Width: | Height: | Size: 660 B |
BIN
public/pic/item/attr/cm_icon_stat_effect_resistance.png
Normal file
|
After Width: | Height: | Size: 693 B |
BIN
public/pic/item/attr/cm_icon_stat_effectiveness.png
Normal file
|
After Width: | Height: | Size: 828 B |
BIN
public/pic/item/attr/cm_icon_stat_health.png
Normal file
|
After Width: | Height: | Size: 595 B |
BIN
public/pic/item/attr/cm_icon_stat_speed.png
Normal file
|
After Width: | Height: | Size: 574 B |
BIN
public/pic/item/set/Health.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/pic/item/set/Immunity.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -41,6 +41,110 @@ interface CharacterDetailProps {
|
||||
};
|
||||
}
|
||||
|
||||
// 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'});
|
||||
@@ -76,13 +180,26 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({ character }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// 属性图标占位
|
||||
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="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>
|
||||
@@ -92,7 +209,8 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({ character }) => {
|
||||
</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">
|
||||
<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>
|
||||
@@ -102,7 +220,11 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({ character }) => {
|
||||
<a href="#skills" className="hover:text-[#E6B17E] transition">Skills</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#imprint" className="hover:text-[#E6B17E] transition">Imprint Concentration</a>
|
||||
<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>
|
||||
@@ -112,8 +234,9 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({ character }) => {
|
||||
{/* 右侧:详细内容 */}
|
||||
<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">Base Stats</h2>
|
||||
<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>
|
||||
@@ -134,6 +257,134 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({ character }) => {
|
||||
</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) => (
|
||||
@@ -151,23 +402,28 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({ character }) => {
|
||||
<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>
|
||||
<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>
|
||||
<span
|
||||
className="text-[#9B8579]">Attack {enhancement.attack}</span>
|
||||
)}
|
||||
{enhancement.health && (
|
||||
<span className="text-[#9B8579]">Health {enhancement.health}</span>
|
||||
<span
|
||||
className="text-[#9B8579]">Health {enhancement.health}</span>
|
||||
)}
|
||||
{enhancement.defense && (
|
||||
<span className="text-[#9B8579]">Defense {enhancement.defense}</span>
|
||||
<span
|
||||
className="text-[#9B8579]">Defense {enhancement.defense}</span>
|
||||
)}
|
||||
{enhancement.effectiveness && (
|
||||
<span className="text-[#9B8579]">Effectiveness {enhancement.effectiveness}</span>
|
||||
<span
|
||||
className="text-[#9B8579]">Effectiveness {enhancement.effectiveness}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -180,7 +436,8 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({ character }) => {
|
||||
</section>
|
||||
|
||||
{/* 印记集中 */}
|
||||
<section id="imprint" className="scroll-mt-24 bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 border border-[#C17F59]/30">
|
||||
<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">
|
||||
|
||||
@@ -92,7 +92,7 @@ const Characters: React.FC = () => {
|
||||
</div>
|
||||
);
|
||||
|
||||
// 属性图标
|
||||
// attr
|
||||
const getElementIcon = (attribute: string) => {
|
||||
if (!attribute) return null;
|
||||
return <img src={`/pic/element/${attribute}.png`} alt={attribute} className="w-7 h-7" />;
|
||||
@@ -230,7 +230,7 @@ const Characters: React.FC = () => {
|
||||
className="flex items-center bg-[#23201B] rounded-xl border border-[#C17F59]/30 hover:border-[#C17F59] transition-all duration-300 shadow-md px-4 py-3 min-h-[96px] max-h-[96px] cursor-pointer backdrop-blur-sm"
|
||||
onClick={() => navigate(`/character/${hero.id}`)}
|
||||
>
|
||||
{/* 头像+属性图标 */}
|
||||
{/* 头像+attr */}
|
||||
<div className="relative mr-4 flex-shrink-0">
|
||||
<img
|
||||
src={hero.headImgUrl}
|
||||
|
||||