feat(character): 更新角色详情页面- 修改角色详情页面布局和样式
- 添加角色属性、职业等信息展示 - 更新技能和配装推荐模块 - 优化页面加载和错误处理逻辑
This commit is contained in:
@@ -128,7 +128,7 @@ const App: React.FC = () => {
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/characters" element={<Characters />} />
|
||||
<Route
|
||||
path="/character/:id"
|
||||
path="/character/:heroCode"
|
||||
element={<CharacterDetail character={mockCharacterData} />}
|
||||
/>
|
||||
<Route path="/news" element={<News />} />
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {Api} from "../utils/axios/config";
|
||||
import axios, {AxiosRequestConfig} from "axios";
|
||||
|
||||
// 查询参数接口
|
||||
export interface GvgTeamQueryParams {
|
||||
@@ -54,6 +53,37 @@ export interface Hero {
|
||||
headImgUrl: string;
|
||||
}
|
||||
|
||||
// 角色详情接口类型
|
||||
export interface HeroDetailResp {
|
||||
heroRespSimpleVO: {
|
||||
id: string;
|
||||
heroCode: string;
|
||||
heroName: string;
|
||||
nickName: string | null;
|
||||
headImgUrl: string;
|
||||
stars: number;
|
||||
role: string;
|
||||
attribute: string;
|
||||
};
|
||||
hero60AttributeVO: {
|
||||
cp: number;
|
||||
atk: number;
|
||||
hp: number;
|
||||
spd: number;
|
||||
def: number;
|
||||
chc: number;
|
||||
chd: number;
|
||||
dac: number;
|
||||
eff: number;
|
||||
efr: number;
|
||||
};
|
||||
}
|
||||
|
||||
// 查询角色详情
|
||||
export const getHeroDetail = async (heroCode: string): Promise<Response<HeroDetailResp>> => {
|
||||
return await Api.get<Response<HeroDetailResp>>(`/epic/hero/hero-detail?heroCode=${heroCode}`)
|
||||
};
|
||||
|
||||
// 查询 GVG 阵容列表
|
||||
export const getGvgTeamList = async (heroCodes?: string[]) => {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import React, {useEffect} from 'react';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {useParams} from 'react-router-dom';
|
||||
import * as EpicApi from '@/api/index';
|
||||
import {getHeroDetail} from "@/api/index";
|
||||
|
||||
export interface Skill {
|
||||
name: string;
|
||||
@@ -145,19 +148,82 @@ const artifactImgMap: Record<string, string> = {
|
||||
'伊赛丽亚的誓约': '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 ROLE_LABELS: Record<string, string> = {
|
||||
knight: '骑士',
|
||||
warrior: '战士',
|
||||
assassin: '盗贼',
|
||||
ranger: '射手',
|
||||
mage: '魔导师',
|
||||
manauser: '精灵师',
|
||||
all: '全部',
|
||||
};
|
||||
|
||||
// 属性图标映射
|
||||
const ATTR_ICON_MAP: Record<string, string> = {
|
||||
attack: '/pic/item/attr/cm_icon_stat_attack.png',
|
||||
defense: '/pic/item/attr/cm_icon_stat_defense.png',
|
||||
health: '/pic/item/attr/cm_icon_stat_health.png',
|
||||
speed: '/pic/item/attr/cm_icon_stat_speed.png',
|
||||
critChance: '/pic/item/attr/cm_icon_stat_crit_chance.png',
|
||||
critDamage: '/pic/item/attr/cm_icon_stat_crit_damage.png',
|
||||
effectiveness: '/pic/item/attr/cm_icon_stat_effectiveness.png',
|
||||
effectResistance: '/pic/item/attr/cm_icon_stat_effect_resistance.png',
|
||||
};
|
||||
|
||||
// 属性中文映射
|
||||
const ATTR_LABEL_MAP: Record<string, string> = {
|
||||
attack: '攻击',
|
||||
defense: '防御',
|
||||
health: '生命',
|
||||
speed: '速度',
|
||||
critChance: '暴击率',
|
||||
critDamage: '暴击伤害',
|
||||
effectiveness: '效果命中',
|
||||
effectResistance: '效果抗性',
|
||||
};
|
||||
|
||||
const CharacterDetail: React.FC = () => {
|
||||
const {heroCode} = useParams();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [heroDetail, setHeroDetail] = useState<EpicApi.HeroDetailResp['data'] | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!heroCode) return;
|
||||
setLoading(true);
|
||||
|
||||
EpicApi.getHeroDetail(heroCode)
|
||||
.then(res => {
|
||||
setHeroDetail(res);
|
||||
setError(null);
|
||||
})
|
||||
.catch(() => setError('获取角色详情失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [heroCode]);
|
||||
|
||||
// 星级渲染(详情页专用星星)
|
||||
const renderStars = (count: number) => {
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
{Array(count).fill(0).map((_, i) => (
|
||||
<img key={i} src="/pic/star.png" alt="star" className="w-6 h-6 inline-block"/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 属性图标
|
||||
const getElementIcon = (attribute: string) => {
|
||||
if (!attribute) return null;
|
||||
return <img src={`/pic/element/${attribute}.png`} alt={attribute} className="w-8 h-8"/>;
|
||||
};
|
||||
|
||||
if (loading) return <div className="text-center py-24 text-[#E6B17E]">加载中...</div>;
|
||||
if (error || !heroDetail) return <div className="text-center py-24 text-red-400">{error || '无数据'}</div>;
|
||||
|
||||
const {heroRespSimpleVO, hero60AttributeVO} = heroDetail;
|
||||
|
||||
const renderSkillType = (type: string) => {
|
||||
switch (type) {
|
||||
case 'None':
|
||||
@@ -180,18 +246,6 @@ 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">
|
||||
@@ -199,33 +253,50 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({character}) => {
|
||||
<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"/>
|
||||
className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg p-6 mb-12 border border-[#C17F59]/30 flex flex-col items-center relative">
|
||||
<div className="w-28 h-28 rounded-full border-4 border-[#C17F59] mb-3 relative">
|
||||
<img src={heroRespSimpleVO.headImgUrl} alt={heroRespSimpleVO.heroName} className="w-full h-full object-cover rounded-full" />
|
||||
{/* 属性图标 */}
|
||||
{heroRespSimpleVO.attribute && (
|
||||
<img
|
||||
src={`/pic/element/${heroRespSimpleVO.attribute}.png`}
|
||||
alt={heroRespSimpleVO.attribute}
|
||||
className="absolute -top-3 -right-3 w-10 h-10 z-10"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold text-[#E6B17E] mb-1">{heroRespSimpleVO.heroName}</h1>
|
||||
<div className="flex mb-1">{renderStars(heroRespSimpleVO.stars)}</div>
|
||||
{/* 职业展示 */}
|
||||
<div className="flex items-center text-[#C17F59] text-base font-medium">
|
||||
{heroRespSimpleVO.role && (
|
||||
<>
|
||||
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-[#E6B17E] mr-2">
|
||||
<img src={`/pic/role/${heroRespSimpleVO.role}.png`} alt={heroRespSimpleVO.role} className="w-6 h-6" />
|
||||
</span>
|
||||
<span className="align-middle">{ROLE_LABELS[heroRespSimpleVO.role] || heroRespSimpleVO.role}</span>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
<h2 className="text-lg font-bold mb-3 text-white">目 录</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>
|
||||
<a href="#base-stats" className="hover:text-[#E6B17E] transition">基础属性</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#builds" className="hover:text-[#E6B17E] transition">配装推荐</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#skills" className="hover:text-[#E6B17E] transition">技能</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#imprint" className="hover:text-[#E6B17E] transition">献身技能</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -237,24 +308,44 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({character}) => {
|
||||
<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>
|
||||
{hero60AttributeVO ? (
|
||||
<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>
|
||||
<img src={ATTR_ICON_MAP.attack} alt="攻击" className="w-6 h-6" />
|
||||
<span>攻击: {hero60AttributeVO.atk}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<i className="fas fa-heart text-[#C17F59]"></i>
|
||||
<span>Health: {character.baseStats.health}</span>
|
||||
<img src={ATTR_ICON_MAP.health} alt="生命" className="w-6 h-6" />
|
||||
<span>生命: {hero60AttributeVO.hp}</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>
|
||||
<img src={ATTR_ICON_MAP.defense} alt="防御" className="w-6 h-6" />
|
||||
<span>防御: {hero60AttributeVO.def}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<i className="fas fa-wind text-[#C17F59]"></i>
|
||||
<span>Speed: {character.baseStats.speed}</span>
|
||||
<img src={ATTR_ICON_MAP.speed} alt="速度" className="w-6 h-6" />
|
||||
<span>速度: {hero60AttributeVO.spd}</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src={ATTR_ICON_MAP.critChance} alt="暴击率" className="w-6 h-6" />
|
||||
<span>暴击率: {Math.round(hero60AttributeVO.chc * 100)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src={ATTR_ICON_MAP.critDamage} alt="暴击伤害" className="w-6 h-6" />
|
||||
<span>暴击伤害: {Math.round(hero60AttributeVO.chd * 100)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src={ATTR_ICON_MAP.effectiveness} alt="效果命中" className="w-6 h-6" />
|
||||
<span>效果命中: {Math.round(hero60AttributeVO.eff * 100)}%</span>
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src={ATTR_ICON_MAP.effectResistance} alt="效果抗性" className="w-6 h-6" />
|
||||
<span>效果抗性: {Math.round(hero60AttributeVO.efr * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<h3 className="text-lg font-semibold text-[#C17F59] mb-2 text-center">无数据</h3>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{/* 配装推荐模块 */}
|
||||
@@ -269,8 +360,8 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({character}) => {
|
||||
{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>
|
||||
<img src={ATTR_ICON_MAP[key] || ''} alt={key} className="w-6 h-6" />
|
||||
<span>{ATTR_LABEL_MAP[key] || key}</span>
|
||||
</div>
|
||||
<span className="font-bold text-[#E6B17E] ml-2">
|
||||
{value}{['critChance', 'critDamage', 'effectiveness', 'effectResistance'].includes(key) ? '%' : ''}
|
||||
@@ -343,8 +434,8 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({character}) => {
|
||||
{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>
|
||||
<img src={ATTR_ICON_MAP[key] || ''} alt={key} className="w-6 h-6" />
|
||||
<span>{ATTR_LABEL_MAP[key] || key}</span>
|
||||
</div>
|
||||
<span className="font-bold text-[#E6B17E] ml-2">
|
||||
{value}{['critChance', 'critDamage', 'effectiveness', 'effectResistance'].includes(key) ? '%' : ''}
|
||||
@@ -360,101 +451,36 @@ const CharacterDetail: React.FC<CharacterDetailProps> = ({character}) => {
|
||||
<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" />
|
||||
<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>
|
||||
<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" />
|
||||
<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" />
|
||||
<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
|
||||
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>
|
||||
)}
|
||||
className="text-[#E6B17E] font-bold text-base text-center mt-2">{build.artifact.name}</span>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@@ -228,7 +228,7 @@ const Characters: React.FC = () => {
|
||||
<div
|
||||
key={hero.id}
|
||||
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}`)}
|
||||
onClick={() => navigate(`/character/${hero.heroCode}`)}
|
||||
>
|
||||
{/* 头像+attr */}
|
||||
<div className="relative mr-4 flex-shrink-0">
|
||||
|
||||
Reference in New Issue
Block a user