272 lines
11 KiB
TypeScript
272 lines
11 KiB
TypeScript
// The exported code uses Tailwind CSS. Install Tailwind CSS in your dev environment to ensure all styles work.
|
|
|
|
import React, { useState, useEffect, useRef } from "react";
|
|
import { useNavigate } from 'react-router-dom';
|
|
import * as EpicApi from '@/api/index';
|
|
|
|
const ELEMENTS = [
|
|
{ key: 'all', label: 'All', img: null },
|
|
{ key: 'fire', label: 'Fire', img: '/pic/element/fire.png' },
|
|
{ key: 'ice', label: 'Ice', img: '/pic/element/ice.png' },
|
|
{ key: 'wind', label: 'Wind', img: '/pic/element/wind.png' },
|
|
{ key: 'light', label: 'Light', img: '/pic/element/light.png' },
|
|
{ key: 'dark', label: 'Dark', img: '/pic/element/dark.png' },
|
|
];
|
|
const ROLES = [
|
|
{ key: 'all', label: 'All', img: null },
|
|
{ key: 'knight', label: 'Knight', img: '/pic/role/knight.png' },
|
|
{ key: 'warrior', label: 'Warrior', img: '/pic/role/warrior.png' },
|
|
{ key: 'assassin', label: 'Thief', img: '/pic/role/assassin.png' },
|
|
{ key: 'ranger', label: 'Ranger', img: '/pic/role/ranger.png' },
|
|
{ key: 'mage', label: 'Mage', img: '/pic/role/mage.png' },
|
|
{ key: 'manauser', label: 'Soul Weaver', img: '/pic/role/manauser.png' },
|
|
];
|
|
const STAR_OPTIONS = [
|
|
{ label: "All Stars", value: 0 },
|
|
{ label: "3 Stars", value: 3 },
|
|
{ label: "4 Stars", value: 4 },
|
|
{ label: "5 Stars", value: 5 },
|
|
];
|
|
|
|
const ROLE_LABELS: Record<string, string> = {
|
|
knight: '骑士',
|
|
warrior: '战士',
|
|
assassin: '盗贼',
|
|
ranger: '射手',
|
|
mage: '魔导师',
|
|
manauser: '精灵师', // 你可以自行修改为合适的中文
|
|
all: '全部',
|
|
};
|
|
|
|
const Characters: React.FC = () => {
|
|
const [selectedStars, setSelectedStars] = useState<number>(0);
|
|
const [selectedElement, setSelectedElement] = useState<string>("all");
|
|
const [selectedRole, setSelectedRole] = useState<string>("all");
|
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
|
const [heroes, setHeroes] = useState<any[]>([]);
|
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
const [dropdownOptions, setDropdownOptions] = useState<any[]>([]);
|
|
const [dropdownLoading, setDropdownLoading] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
const navigate = useNavigate();
|
|
|
|
// 拉取英雄数据
|
|
useEffect(() => {
|
|
EpicApi.getHeroList().then(setHeroes);
|
|
}, []);
|
|
|
|
// // 拉取 team-list 下拉数据(只请求一次)
|
|
// useEffect(() => {
|
|
// setDropdownLoading(true);
|
|
// EpicApi.getGvgTeamList([]).then((data) => {
|
|
// // 合并防守和进攻英雄,去重
|
|
// const allHeroes = [
|
|
// ...data.flatMap((item: any) => item.defenseHeroInfos || []),
|
|
// ...data.flatMap((item: any) => item.attackHeroInfos || []),
|
|
// ];
|
|
// const unique = Array.from(new Map(allHeroes.map(h => [h.heroCode, h])).values());
|
|
// setDropdownOptions(unique);
|
|
// setDropdownLoading(false);
|
|
// });
|
|
// }, []);
|
|
|
|
// 过滤逻辑
|
|
const filteredHeroes = heroes.filter((hero) => {
|
|
if (searchTerm &&
|
|
!hero.heroName.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
|
!(hero.nickName && hero.nickName.toLowerCase().includes(searchTerm.toLowerCase()))
|
|
) return false;
|
|
if (selectedStars && hero.stars !== selectedStars) return false;
|
|
if (selectedElement !== 'all' && hero.attribute !== selectedElement) return false;
|
|
if (selectedRole !== 'all' && hero.role !== selectedRole) return false;
|
|
return true;
|
|
});
|
|
|
|
// 星级渲染
|
|
const renderStars = (count: number) => (
|
|
<div className="flex items-center gap-1">
|
|
{Array(count).fill(0).map((_, i) => (
|
|
<img key={i} src="/pic/star.png" alt="star" className="w-5 h-5 inline-block" />
|
|
))}
|
|
</div>
|
|
);
|
|
|
|
// 属性图标
|
|
const getElementIcon = (attribute: string) => {
|
|
if (!attribute) return null;
|
|
return <img src={`/pic/element/${attribute}.png`} alt={attribute} className="w-7 h-7" />;
|
|
};
|
|
// 职业图标
|
|
const getRoleIcon = (role: string) => {
|
|
if (!role) return null;
|
|
return <img src={`/pic/role/${role}.png`} alt={role} className="w-7 h-7" />;
|
|
};
|
|
|
|
// 下拉选项过滤
|
|
const filteredDropdownOptions = dropdownOptions.filter((hero: any) => {
|
|
if (!searchTerm) return true;
|
|
return hero.heroName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
(hero.nickName && hero.nickName.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
});
|
|
|
|
// 点击下拉选项
|
|
const handleDropdownSelect = (hero: any) => {
|
|
setSearchTerm(hero.heroName);
|
|
setDropdownOpen(false);
|
|
};
|
|
|
|
// 关闭下拉
|
|
useEffect(() => {
|
|
const handleClick = (e: MouseEvent) => {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
|
|
setDropdownOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClick);
|
|
return () => document.removeEventListener('mousedown', handleClick);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#1A1412] text-white font-sans relative">
|
|
{/* 背景装饰 */}
|
|
<div className="absolute inset-0 bg-gradient-to-b from-[#1A1412] via-[#2A211E] to-[#1A1412]"></div>
|
|
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative py-24">
|
|
<h1 className="text-4xl font-bold text-center mb-8">
|
|
<span className="bg-clip-text text-transparent bg-gradient-to-r from-[#E6B17E] via-[#C17F59] to-[#A66D4F]">
|
|
All Epic Seven Heroes
|
|
</span>
|
|
</h1>
|
|
|
|
{/* 搜索组件 */}
|
|
<div className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] p-4 rounded-lg mb-8 border border-[#C17F59]/30 backdrop-blur-sm">
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
|
<div className="relative" ref={dropdownRef}>
|
|
<div
|
|
className="bg-[#1A1412] border border-[#C17F59]/30 px-4 py-2 rounded-md text-sm w-full text-[#E6B17E] placeholder-[#9B8579] focus:border-[#C17F59] focus:outline-none flex items-center cursor-pointer"
|
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
|
>
|
|
<span className="flex-1">{searchTerm || '选择英雄名称/昵称'}</span>
|
|
<span className={`ml-2 transition-transform ${dropdownOpen ? 'rotate-180' : ''}`}>▼</span>
|
|
</div>
|
|
{dropdownOpen && (
|
|
<div className="absolute left-0 right-0 mt-1 bg-[#1A1412] border border-[#C17F59]/30 rounded-md shadow-lg max-h-60 overflow-y-auto z-50">
|
|
<input
|
|
type="text"
|
|
value={searchTerm}
|
|
onChange={e => setSearchTerm(e.target.value)}
|
|
placeholder="输入英雄名称或昵称搜索..."
|
|
className="w-full px-3 py-2 bg-[#1A1412] text-[#E6B17E] border-b border-[#C17F59]/30 rounded-t-md focus:outline-none focus:border-[#C17F59]"
|
|
autoFocus
|
|
/>
|
|
{dropdownLoading ? (
|
|
<div className="p-4 text-center text-[#9B8579]">加载中...</div>
|
|
) : (
|
|
filteredDropdownOptions.length > 0 ? filteredDropdownOptions.map((hero: any) => (
|
|
<div
|
|
key={hero.heroCode}
|
|
className="px-4 py-2 cursor-pointer hover:bg-[#2A211E] flex items-center gap-2"
|
|
onClick={() => handleDropdownSelect(hero)}
|
|
>
|
|
<img src={hero.headImgUrl} alt={hero.heroName} className="w-6 h-6 rounded-full" />
|
|
<span>{hero.heroName}</span>
|
|
{hero.nickName && <span className="text-[#9B8579] text-sm">({hero.nickName})</span>}
|
|
</div>
|
|
)) : <div className="p-4 text-center text-[#9B8579]">无匹配结果</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-2 h-10">
|
|
<div className="flex flex-wrap gap-1">
|
|
{STAR_OPTIONS.map((star) => (
|
|
<button
|
|
key={star.label}
|
|
className={`px-4 py-2 rounded-md cursor-pointer whitespace-nowrap !rounded-button border ${selectedStars === star.value ? "bg-gradient-to-r from-[#C17F59] to-[#A66D4F] text-[#1A1412] border-[#C17F59]" : "bg-[#1A1412] hover:bg-[#2A211E] text-[#E6B17E] border-[#C17F59]/30"}`}
|
|
onClick={() => setSelectedStars(star.value)}
|
|
>
|
|
{star.label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="mb-2 h-10">
|
|
<div className="flex flex-wrap gap-1 items-center">
|
|
{ELEMENTS.map((el) => (
|
|
<button
|
|
key={el.key}
|
|
className={`px-4 py-2 rounded-md flex items-center gap-2 cursor-pointer whitespace-nowrap !rounded-button border ${selectedElement === el.key ? "bg-gradient-to-r from-[#C17F59] to-[#A66D4F] text-[#1A1412] border-[#C17F59]" : "bg-[#1A1412] hover:bg-[#2A211E] text-[#E6B17E] border-[#C17F59]/30"}`}
|
|
onClick={() => setSelectedElement(el.key)}
|
|
>
|
|
{el.img ? <img src={el.img} alt={el.key} className="w-6 h-6" /> : 'All'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="h-10">
|
|
<div className="flex flex-wrap gap-1 items-center">
|
|
{ROLES.map((role) => (
|
|
<button
|
|
key={role.key}
|
|
className={`px-4 py-2 rounded-md flex items-center gap-2 cursor-pointer whitespace-nowrap !rounded-button border ${selectedRole === role.key ? "bg-gradient-to-r from-[#C17F59] to-[#A66D4F] text-[#1A1412] border-[#C17F59]" : "bg-[#1A1412] hover:bg-[#2A211E] text-[#E6B17E] border-[#C17F59]/30"}`}
|
|
onClick={() => setSelectedRole(role.key)}
|
|
>
|
|
{role.img ? <img src={role.img} alt={role.key} className="w-6 h-6" /> : 'All'}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-6">
|
|
{filteredHeroes.map((hero) => (
|
|
<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}`)}
|
|
>
|
|
{/* 头像+属性图标 */}
|
|
<div className="relative mr-4 flex-shrink-0">
|
|
<img
|
|
src={hero.headImgUrl}
|
|
alt={hero.heroName}
|
|
className="w-16 h-16 rounded-full border-2 border-[#C17F59] object-cover object-center"
|
|
/>
|
|
<div className="absolute -top-2 -right-2">
|
|
{getElementIcon(hero.attribute)}
|
|
</div>
|
|
</div>
|
|
{/* 右侧信息 */}
|
|
<div className="flex flex-col justify-center flex-1 min-w-0">
|
|
<div className="flex items-center mb-1">
|
|
<span className="text-[16px] font-bold text-[#E6B17E]">{hero.heroName}</span>
|
|
</div>
|
|
<div className="flex items-center mb-1">
|
|
{renderStars(hero.stars)}
|
|
</div>
|
|
<div className="flex items-center text-[#C17F59] text-base font-medium">
|
|
{hero.role && (
|
|
<>
|
|
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-[#E6B17E] mr-2">
|
|
<img src={`/pic/role/${hero.role}.png`} alt={hero.role} className="w-6 h-6" />
|
|
</span>
|
|
<span className="align-middle">{ROLE_LABELS[hero.role] || hero.role}</span>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Characters;
|