feat(character): 更新角色详情页面配装数据展示
- 添加平均属性和主流套装占比展示 -优化面板属性展示,增加最近更新时间 - 调整套装组合和神器数据展示方式 - 优化数据处理逻辑,提高页面加载速度
BIN
public/pic/eqset/setcounter.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setcritical.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setdefense.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setdestruction.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/sethealth.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/sethit.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setimmunity.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setinjury.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setlifesteal.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setpenetration.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setresist.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/setspeed.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/pic/eqset/settorrent.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -77,11 +77,40 @@ export interface HeroDetailResp {
|
||||
eff: number;
|
||||
efr: number;
|
||||
};
|
||||
heroSetAvgVO: {
|
||||
atk: number;
|
||||
hp: number;
|
||||
spd: number;
|
||||
def: number;
|
||||
chc: number;
|
||||
chd: number;
|
||||
dac: number;
|
||||
eff: number;
|
||||
efr: number;
|
||||
};
|
||||
heroSetPercentVOS: {
|
||||
setName: string;
|
||||
percent: number;
|
||||
}[];
|
||||
heroSetShows: {
|
||||
cp: number;
|
||||
atk: number;
|
||||
hp: number;
|
||||
spd: number;
|
||||
def: number;
|
||||
chc: number;
|
||||
chd: number;
|
||||
dac: number;
|
||||
eff: number;
|
||||
efr: number;
|
||||
hds: string;
|
||||
ctr: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// 查询角色详情
|
||||
export const getHeroDetail = async (heroCode: string): Promise<Response<HeroDetailResp>> => {
|
||||
return await Api.get<Response<HeroDetailResp>>(`/epic/hero/hero-detail?heroCode=${heroCode}`)
|
||||
export const getHeroDetail = async (heroCode: string): Promise<HeroDetailResp> => {
|
||||
return await Api.get<HeroDetailResp>(`/epic/hero/hero-detail?heroCode=${heroCode}`)
|
||||
};
|
||||
|
||||
// 查询 GVG 阵容列表
|
||||
|
||||
@@ -136,10 +136,19 @@ const mockBuilds: BuildInfo[] = [
|
||||
|
||||
// 套装图标映射
|
||||
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',
|
||||
'生命': '/pic/eqset/sethealth.png',
|
||||
'免疫': '/pic/eqset/setimmunity.png',
|
||||
'速度': '/pic/eqset/setspeed.png',
|
||||
'穿透': '/pic/eqset/setpenetration.png',
|
||||
'反击': '/pic/eqset/setcounter.png',
|
||||
'命中': '/pic/eqset/sethit.png',
|
||||
'激流': '/pic/eqset/settorrent.png',
|
||||
'吸血': '/pic/eqset/setlifesteal.png',
|
||||
'抵抗': '/pic/eqset/setresist.png',
|
||||
'防御': '/pic/eqset/setdefense.png',
|
||||
'破灭': '/pic/eqset/setdestruction.png',
|
||||
'暴击': '/pic/eqset/setcritical.png',
|
||||
'伤口': '/pic/eqset/setinjury.png',
|
||||
};
|
||||
|
||||
// 神器图片映射
|
||||
@@ -183,22 +192,34 @@ const ATTR_LABEL_MAP: Record<string, string> = {
|
||||
effectResistance: '效果抗性',
|
||||
};
|
||||
|
||||
// 属性映射(API字段到显示字段的映射)
|
||||
const ATTR_KEY_MAP: Record<string, string> = {
|
||||
atk: 'attack',
|
||||
def: 'defense',
|
||||
hp: 'health',
|
||||
spd: 'speed',
|
||||
chc: 'critChance',
|
||||
chd: 'critDamage',
|
||||
eff: 'effectiveness',
|
||||
efr: '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);
|
||||
const [heroDetail, setHeroDetail] = useState<EpicApi.HeroDetailResp | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!heroCode) return;
|
||||
setLoading(true);
|
||||
|
||||
EpicApi.getHeroDetail(heroCode)
|
||||
.then(res => {
|
||||
setHeroDetail(res);
|
||||
.then(data => {
|
||||
setHeroDetail(data);
|
||||
setError(null);
|
||||
})
|
||||
.catch(() => setError('获取角色详情失败'))
|
||||
.catch(err => setError(err.message || '获取角色详情失败'))
|
||||
.finally(() => setLoading(false));
|
||||
}, [heroCode]);
|
||||
|
||||
@@ -222,7 +243,19 @@ const CharacterDetail: React.FC = () => {
|
||||
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 {heroRespSimpleVO, hero60AttributeVO, heroSetAvgVO, heroSetPercentVOS, heroSetShows} = heroDetail;
|
||||
|
||||
// 格式化数字,保留一位小数,如果是整数则只显示整数
|
||||
const formatNumber = (num: number | string) => {
|
||||
const numValue = typeof num === 'string' ? parseFloat(num) : num;
|
||||
if (Number.isInteger(numValue)) return numValue.toString();
|
||||
return numValue.toFixed(1).replace(/\.0$/, '');
|
||||
};
|
||||
|
||||
// 格式化百分比,保留一位小数
|
||||
const formatPercent = (num: number) => {
|
||||
return (num * 100).toFixed(1);
|
||||
};
|
||||
|
||||
const renderSkillType = (type: string) => {
|
||||
switch (type) {
|
||||
@@ -255,7 +288,8 @@ const CharacterDetail: React.FC = () => {
|
||||
<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 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" />
|
||||
<img src={heroRespSimpleVO.headImgUrl} alt={heroRespSimpleVO.heroName}
|
||||
className="w-full h-full object-cover rounded-full"/>
|
||||
{/* 属性图标 */}
|
||||
{heroRespSimpleVO.attribute && (
|
||||
<img
|
||||
@@ -271,10 +305,13 @@ const CharacterDetail: React.FC = () => {
|
||||
<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
|
||||
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>
|
||||
<span
|
||||
className="align-middle">{ROLE_LABELS[heroRespSimpleVO.role] || heroRespSimpleVO.role}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -311,35 +348,35 @@ const CharacterDetail: React.FC = () => {
|
||||
{hero60AttributeVO ? (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<img src={ATTR_ICON_MAP.attack} alt="攻击" className="w-6 h-6" />
|
||||
<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">
|
||||
<img src={ATTR_ICON_MAP.health} alt="生命" className="w-6 h-6" />
|
||||
<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">
|
||||
<img src={ATTR_ICON_MAP.defense} alt="防御" className="w-6 h-6" />
|
||||
<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">
|
||||
<img src={ATTR_ICON_MAP.speed} alt="速度" className="w-6 h-6" />
|
||||
<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" />
|
||||
<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" />
|
||||
<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" />
|
||||
<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" />
|
||||
<img src={ATTR_ICON_MAP.effectResistance} alt="效果抗性" className="w-6 h-6"/>
|
||||
<span>效果抗性: {Math.round(hero60AttributeVO.efr * 100)}%</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,37 +390,38 @@ const CharacterDetail: React.FC = () => {
|
||||
<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">
|
||||
<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]) => (
|
||||
{Object.entries(heroSetAvgVO).map(([key, value]) => {
|
||||
const displayKey = ATTR_KEY_MAP[key];
|
||||
if (!displayKey) return null;
|
||||
return (
|
||||
<div key={key} className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center gap-2 min-w-[110px]">
|
||||
<img src={ATTR_ICON_MAP[key] || ''} alt={key} className="w-6 h-6" />
|
||||
<span>{ATTR_LABEL_MAP[key] || key}</span>
|
||||
<img src={ATTR_ICON_MAP[displayKey]} alt={displayKey} className="w-6 h-6"/>
|
||||
<span>{ATTR_LABEL_MAP[displayKey]}</span>
|
||||
</div>
|
||||
<span className="font-bold text-[#E6B17E] ml-2">
|
||||
{value}{['critChance', 'critDamage', 'effectiveness', 'effectResistance'].includes(key) ? '%' : ''}
|
||||
{formatNumber(value)}{['critChance', 'critDamage', 'effectiveness', 'effectResistance'].includes(displayKey) ? '%' : ''}
|
||||
</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">
|
||||
<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) => (
|
||||
{heroSetPercentVOS.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"
|
||||
<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)
|
||||
套装 {idx + 1} ({formatPercent(set.percent)}% 使用比例)
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2 px-4 py-3 bg-[#181310] rounded-b">
|
||||
{set.setNames.map((name, i) => (
|
||||
{set.setName.split(',').map((name, i) => (
|
||||
<span key={i}
|
||||
className="flex items-center gap-2 px-3 py-1 rounded-lg text-sm font-semibold"
|
||||
style={{
|
||||
@@ -396,8 +434,7 @@ const CharacterDetail: React.FC = () => {
|
||||
<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 className="w-8 h-8 inline-block flex items-center justify-center text-[#C17F59]">?</span>
|
||||
)}
|
||||
<span>{name}</span>
|
||||
</span>
|
||||
@@ -410,38 +447,32 @@ const CharacterDetail: React.FC = () => {
|
||||
</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) => (
|
||||
{heroSetShows.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="flex items-center gap-2">
|
||||
<h3 className="text-lg font-semibold text-[#C17F59]">面板 {idx + 1}</h3>
|
||||
<span className="text-sm text-white/70">最近更新:{build.ctr}</span>
|
||||
</div>
|
||||
<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]) => (
|
||||
{Object.entries(build).map(([key, value]) => {
|
||||
const displayKey = ATTR_KEY_MAP[key];
|
||||
if (!displayKey || key === 'cp') return null;
|
||||
return (
|
||||
<div key={key} className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center gap-2 min-w-[110px]">
|
||||
<img src={ATTR_ICON_MAP[key] || ''} alt={key} className="w-6 h-6" />
|
||||
<span>{ATTR_LABEL_MAP[key] || key}</span>
|
||||
<img src={ATTR_ICON_MAP[displayKey]} alt={displayKey} className="w-6 h-6"/>
|
||||
<span>{ATTR_LABEL_MAP[displayKey]}</span>
|
||||
</div>
|
||||
<span className="font-bold text-[#E6B17E] ml-2">
|
||||
{value}{['critChance', 'critDamage', 'effectiveness', 'effectResistance'].includes(key) ? '%' : ''}
|
||||
{formatNumber(value)}{['critChance', 'critDamage', 'effectiveness', 'effectResistance'].includes(displayKey) ? '%' : ''}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* 右侧:套装图标+神器图片+神器名 */}
|
||||
<div className="flex flex-col items-center justify-between h-full">
|
||||
@@ -449,7 +480,7 @@ const CharacterDetail: React.FC = () => {
|
||||
<h4 className="font-bold text-[#E6B17E] mb-2">套装组合</h4>
|
||||
{/* 套装组合图标 */}
|
||||
<div className="flex gap-3 mb-4">
|
||||
{build.gearSets.map((set, i) => (
|
||||
{build.hds?.split(',').map((set: string, i: number) => (
|
||||
setIconMap[set] ? (
|
||||
<img key={i} src={setIconMap[set]} alt={set}
|
||||
className="w-10 h-10"/>
|
||||
@@ -461,21 +492,10 @@ const CharacterDetail: React.FC = () => {
|
||||
</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 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>
|
||||
<span className="text-[#E6B17E] font-bold text-base text-center mt-2">暂无神器数据</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||