refactor(characters): 重构角色页面并集成 Ant Design Select 组件
- 移除自定义下拉菜单,替换为 Ant Design Select 组件 - 添加英雄选择功能,支持搜索和筛选 -优化过滤逻辑,提高页面性能- 调整样式以匹配项目主题
This commit is contained in:
@@ -10,6 +10,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
|
"antd": "^5.26.3",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
|||||||
@@ -1,9 +1,75 @@
|
|||||||
// The exported code uses Tailwind CSS. Install Tailwind CSS in your dev environment to ensure all styles work.
|
// 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 React, {useEffect, useState} from "react";
|
||||||
import {useNavigate} from 'react-router-dom';
|
import {useNavigate} from 'react-router-dom';
|
||||||
|
import {Select} from 'antd';
|
||||||
|
import type {Hero} from '@/api/index';
|
||||||
import * as EpicApi from '@/api/index';
|
import * as EpicApi from '@/api/index';
|
||||||
|
|
||||||
|
// 扩展 Hero 类型以包含所需的属性
|
||||||
|
type ExtendedHero = Hero & {
|
||||||
|
stars: number;
|
||||||
|
role: string;
|
||||||
|
attribute: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 自定义样式覆盖 Ant Design 默认样式
|
||||||
|
const selectStyles = `
|
||||||
|
.ant-select {
|
||||||
|
background-color: #1A1412 !important;
|
||||||
|
border-color: #C17F59 !important;
|
||||||
|
color: #E6B17E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select .ant-select-selector {
|
||||||
|
background-color: #1A1412 !important;
|
||||||
|
border-color: #C17F59 !important;
|
||||||
|
color: #E6B17E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-focused .ant-select-selector {
|
||||||
|
border-color: #C17F59 !important;
|
||||||
|
box-shadow: 0 0 0 2px rgba(193, 127, 89, 0.2) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-dropdown {
|
||||||
|
background-color: #1A1412 !important;
|
||||||
|
border-color: #C17F59 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-item {
|
||||||
|
background-color: #1A1412 !important;
|
||||||
|
color: #E6B17E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-item-option-selected {
|
||||||
|
background-color: #2A211E !important;
|
||||||
|
color: #E6B17E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-item-option-active {
|
||||||
|
background-color: #2A211E !important;
|
||||||
|
color: #E6B17E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selection-placeholder {
|
||||||
|
color: #9B8579 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-selection-item {
|
||||||
|
color: #E6B17E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-arrow {
|
||||||
|
color: #E6B17E !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-select-clear {
|
||||||
|
background-color: #1A1412 !important;
|
||||||
|
color: #E6B17E !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
const ELEMENTS = [
|
const ELEMENTS = [
|
||||||
{ key: 'all', label: 'All', img: null },
|
{ key: 'all', label: 'All', img: null },
|
||||||
{ key: 'fire', label: 'Fire', img: '/pic/element/fire.png' },
|
{ key: 'fire', label: 'Fire', img: '/pic/element/fire.png' },
|
||||||
@@ -39,16 +105,12 @@ const ROLE_LABELS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Characters: React.FC = () => {
|
const Characters: React.FC = () => {
|
||||||
const [allHeroes, setAllHeroes] = useState<any[]>([]);
|
const [allHeroes, setAllHeroes] = useState<ExtendedHero[]>([]);
|
||||||
const [selectedStars, setSelectedStars] = useState<number>(0);
|
const [selectedStars, setSelectedStars] = useState<number>(0);
|
||||||
const [selectedElement, setSelectedElement] = useState<string>("all");
|
const [selectedElement, setSelectedElement] = useState<string>("all");
|
||||||
const [selectedRole, setSelectedRole] = useState<string>("all");
|
const [selectedRole, setSelectedRole] = useState<string>("all");
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const [heroes, setHeroes] = useState<any[]>([]);
|
const [heroes, setHeroes] = useState<ExtendedHero[]>([]);
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
||||||
const [dropdownOptions, setDropdownOptions] = useState<any[]>([]);
|
|
||||||
const [dropdownLoading, setDropdownLoading] = useState(false);
|
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -56,26 +118,13 @@ const Characters: React.FC = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
EpicApi.getHeroList().then(data => {
|
EpicApi.getHeroList().then(data => {
|
||||||
// console.log('Heroes:', data);
|
// console.log('Heroes:', data);
|
||||||
setHeroes(data);
|
// 类型转换,假设 API 返回的数据包含所需的所有属性
|
||||||
setAllHeroes(data);
|
const extendedData = data as ExtendedHero[];
|
||||||
|
setHeroes(extendedData);
|
||||||
|
setAllHeroes(extendedData);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// // 拉取 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) => {
|
const filteredHeroes = heroes.filter((hero) => {
|
||||||
if (searchTerm &&
|
if (searchTerm &&
|
||||||
@@ -108,32 +157,15 @@ const Characters: React.FC = () => {
|
|||||||
return <img src={`/pic/role/${role}.png`} alt={role} className="w-7 h-7" />;
|
return <img src={`/pic/role/${role}.png`} alt={role} className="w-7 h-7" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 下拉选项过滤
|
// 处理英雄选择
|
||||||
const filteredDropdownOptions = allHeroes.filter((hero: any) => {
|
const handleHeroSelect = (value: string) => {
|
||||||
if (!searchTerm) return true;
|
setSearchTerm(value);
|
||||||
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 (
|
return (
|
||||||
<div className="min-h-screen bg-[#1A1412] text-white font-sans relative">
|
<div className="min-h-screen bg-[#1A1412] text-white font-sans relative">
|
||||||
|
{/* 注入自定义样式 */}
|
||||||
|
<style dangerouslySetInnerHTML={{ __html: selectStyles }} />
|
||||||
{/* 背景装饰 */}
|
{/* 背景装饰 */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-[#1A1412] via-[#2A211E] to-[#1A1412]"></div>
|
<div className="absolute inset-0 bg-gradient-to-b from-[#1A1412] via-[#2A211E] to-[#1A1412]"></div>
|
||||||
|
|
||||||
@@ -147,42 +179,28 @@ const Characters: React.FC = () => {
|
|||||||
{/* 搜索组件 */}
|
{/* 搜索组件 */}
|
||||||
<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="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="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
|
||||||
<div className="relative" ref={dropdownRef}>
|
<Select
|
||||||
<div
|
showSearch
|
||||||
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"
|
placeholder="选择英雄名称/昵称"
|
||||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
value={searchTerm || undefined}
|
||||||
>
|
onChange={handleHeroSelect}
|
||||||
<span className="flex-1">{searchTerm || '选择英雄名称/昵称'}</span>
|
onSearch={setSearchTerm}
|
||||||
<span className={`ml-2 transition-transform ${dropdownOpen ? 'rotate-180' : ''}`}>▼</span>
|
filterOption={(input, option) => {
|
||||||
</div>
|
const heroName = option?.value as string;
|
||||||
{dropdownOpen && (
|
return heroName.toLowerCase().includes(input.toLowerCase());
|
||||||
<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-100">
|
}}
|
||||||
<input
|
options={allHeroes.map(hero => ({
|
||||||
type="text"
|
value: hero.heroName,
|
||||||
value={searchTerm}
|
label: (
|
||||||
onChange={e => setSearchTerm(e.target.value)}
|
<div className="flex items-center gap-2">
|
||||||
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" />
|
<img src={hero.headImgUrl} alt={hero.heroName} className="w-6 h-6 rounded-full" />
|
||||||
<span>{hero.heroName}</span>
|
<span>{hero.heroName}</span>
|
||||||
{hero.nickName && <span className="text-[#9B8579] text-sm">({hero.nickName})</span>}
|
{hero.nickName && <span className="text-[#9B8579] text-sm">({hero.nickName})</span>}
|
||||||
</div>
|
</div>
|
||||||
)) : <div className="p-4 text-center text-[#9B8579]">无匹配结果</div>
|
)
|
||||||
)}
|
}))}
|
||||||
</div>
|
className="w-full"
|
||||||
)}
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-2 h-10">
|
<div className="mb-2 h-10">
|
||||||
|
|||||||
Reference in New Issue
Block a user