feat(character): 更新角色详情页面配装数据展示

- 添加平均属性和主流套装占比展示
-优化面板属性展示,增加最近更新时间
- 调整套装组合和神器数据展示方式
- 优化数据处理逻辑,提高页面加载速度
This commit is contained in:
hxt
2025-05-25 19:38:21 +08:00
parent f527462274
commit 21a3348e0e
15 changed files with 132 additions and 83 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

BIN
public/pic/eqset/sethit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -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 阵容列表

View File

@@ -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>
@@ -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>