feat(api): 添加图片识别功能并优化阵容搜索

- 新增图片识别接口和相关功能
-重构阵容搜索逻辑,支持两组英雄同时搜索
- 优化用户界面,增加上传图片预览和示例图片提示
-调整布局和样式,提升用户体验
This commit is contained in:
hxt
2025-05-03 20:34:37 +08:00
parent 140e2b24c3
commit 063c9bdd54
4 changed files with 411 additions and 233 deletions

8
public/favicon.svg Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="32" height="32" rx="16" fill="#C17F59"/>
<path d="M16 8L20 12H12L16 8Z" fill="#E6B17E"/>
<path d="M16 24L12 20H20L16 24Z" fill="#E6B17E"/>
<path d="M8 16L12 12V20L8 16Z" fill="#E6B17E"/>
<path d="M24 16L20 20V12L24 16Z" fill="#E6B17E"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

BIN
public/tt.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 KiB

View File

@@ -1,80 +1,99 @@
import { Api } from "../utils/axios/config"; import {Api} from "../utils/axios/config";
import type { AxiosRequestConfig } from "axios"; import axios, {AxiosRequestConfig} from "axios";
// 查询参数接口 // 查询参数接口
export interface GvgTeamQueryParams { export interface GvgTeamQueryParams {
defenseHeroes?: string[]; defenseHeroes?: string[];
attackHeroes?: string[]; attackHeroes?: string[];
equipmentInfo?: string; equipmentInfo?: string;
artifacts?: string[]; artifacts?: string[];
battleStrategy?: string; battleStrategy?: string;
pageNum?: number; pageNum?: number;
pageSize?: number; pageSize?: number;
} }
// 英雄信息接口 // 英雄信息接口
export interface HeroInfo { export interface HeroInfo {
heroCode: string; heroCode: string;
headImgUrl: string; headImgUrl: string;
heroName: string; heroName: string;
} }
// 阵容数据接口 // 阵容数据接口
export interface GvgTeam { export interface GvgTeam {
id: number; id: number;
defenseHeroes: string[]; defenseHeroes: string[];
defenseHeroInfos: HeroInfo[]; defenseHeroInfos: HeroInfo[];
attackHeroes: string[]; attackHeroes: string[];
attackHeroInfos: HeroInfo[]; attackHeroInfos: HeroInfo[];
equipmentInfo: string; equipmentInfo: string;
artifacts: string; artifacts: string;
battleStrategy: string; battleStrategy: string;
prerequisites: string; prerequisites: string;
importantNotes: string; importantNotes: string;
} }
// 分页响应接口 // 分页响应接口
export interface PageResult<T> { export interface PageResult<T> {
total: number; total: number;
list: T[]; list: T[];
} }
export interface Response<T> { export interface Response<T> {
code: number; code: number;
data: T; data: T;
msg: string; msg: string;
} }
// 英雄列表接口 // 英雄列表接口
export interface Hero { export interface Hero {
id: number; id: number;
heroCode: string; heroCode: string;
heroName: string; heroName: string;
nickName: string; nickName: string;
headImgUrl: string; headImgUrl: string;
} }
// 查询 GVG 阵容列表 // 查询 GVG 阵容列表
export const getGvgTeamList = async (heroCodes?: string[]) => { export const getGvgTeamList = async (heroCodes?: string[]) => {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (heroCodes && heroCodes.length > 0) { if (heroCodes && heroCodes.length > 0) {
params.append('heroCodes', heroCodes.join(',')); params.append('heroCodes', heroCodes.join(','));
} }
const response = await Api.get<GvgTeam[]>(`/epic/hero/team-list?${params.toString()}`); const response = await Api.get<GvgTeam[]>(`/epic/hero/team-list?${params.toString()}`);
return response; return response;
}; };
// 获取 GVG 阵容详情 // 获取 GVG 阵容详情
export const getGvgTeamDetail = async (id: number) => { export const getGvgTeamDetail = async (id: number) => {
const response = await Api.get<Response<GvgTeam>>(`/api/gvg/team/${id}`); const response = await Api.get<Response<GvgTeam>>(`/api/gvg/team/${id}`);
if (response.code === 0) { if (response.code === 0) {
return response.data; return response.data;
} }
throw new Error(response.msg || "获取阵容详情失败"); throw new Error(response.msg || "获取阵容详情失败");
}; };
// 查询所有英雄列表 // 查询所有英雄列表
export const getHeroList = async () => { export const getHeroList = async () => {
const response = await Api.get<Hero[]>("/epic/hero/list-all"); const response = await Api.get<Hero[]>("/epic/hero/list-all");
return response; return response;
};
export interface ImageRecognitionResult {
heroNames: string[]; // 6个有序的角色名称前3个为一组后3个为一组
}
// 图片识别接口
export const recognizeHeroesFromImage = async (file: File): Promise<ImageRecognitionResult> => {
try {
const response = await Api.upload<string[]>('/epic/hero/recognize', file);
console.log(response)
if (response && Array.isArray(response)) {
return { heroNames: response };
}
throw new Error("图片识别失败");
} catch (error) {
console.error('Failed to recognize heroes from image:', error);
throw error;
}
}; };

View File

@@ -1,4 +1,4 @@
import React, {useState, useEffect} from "react"; import React, {useState, useEffect, useRef} from "react";
import * as EpicApi from '@/api/index'; import * as EpicApi from '@/api/index';
import { Character } from '@/api/types'; import { Character } from '@/api/types';
@@ -7,9 +7,15 @@ const Lineup: React.FC = () => {
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [heroes, setHeroes] = useState<EpicApi.Hero[]>([]); const [heroes, setHeroes] = useState<EpicApi.Hero[]>([]);
const [selectedHeroes, setSelectedHeroes] = useState<EpicApi.Hero[]>([]); const [selectedHeroes1, setSelectedHeroes1] = useState<EpicApi.Hero[]>([]);
const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [selectedHeroes2, setSelectedHeroes2] = useState<EpicApi.Hero[]>([]);
const [searchInput, setSearchInput] = useState(""); const [isDropdownOpen1, setIsDropdownOpen1] = useState(false);
const [isDropdownOpen2, setIsDropdownOpen2] = useState(false);
const [searchInput1, setSearchInput1] = useState("");
const [searchInput2, setSearchInput2] = useState("");
const [isUploading, setIsUploading] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
const [previewImage, setPreviewImage] = useState<string | null>('/tt.png');
// Fetch heroes data // Fetch heroes data
useEffect(() => { useEffect(() => {
@@ -30,38 +36,43 @@ const Lineup: React.FC = () => {
handleSearch(); handleSearch();
}, []); }, []);
const handleHeroSelect = (hero: EpicApi.Hero) => { const handleHeroSelect = (hero: EpicApi.Hero, group: number) => {
if (selectedHeroes.length < 3 && !selectedHeroes.find(h => h.id === hero.id)) { if (group === 1) {
setSelectedHeroes([...selectedHeroes, hero]); if (selectedHeroes1.length < 3 && !selectedHeroes1.find(h => h.id === hero.id)) {
setSearchInput(""); setSelectedHeroes1([...selectedHeroes1, hero]);
setIsDropdownOpen(false); setSearchInput1("");
} setIsDropdownOpen1(false);
}; }
} else {
const handleHeroRemove = (heroId: number) => { if (selectedHeroes2.length < 3 && !selectedHeroes2.find(h => h.id === hero.id)) {
setSelectedHeroes(selectedHeroes.filter(h => h.id !== heroId)); setSelectedHeroes2([...selectedHeroes2, hero]);
}; setSearchInput2("");
setIsDropdownOpen2(false);
const handleSearch = async () => {
if (selectedHeroes.length === 0) {
// If no heroes selected, fetch all lineups
setLoading(true);
try {
const data = await EpicApi.getGvgTeamList([]);
setLineups(data);
setError(null);
} catch (err) {
console.error('Failed to fetch lineups:', err);
setError(err instanceof Error ? err.message : '获取阵容数据失败,请稍后再试');
} finally {
setLoading(false);
} }
return;
} }
};
const handleHeroRemove = (heroId: number, group: number) => {
if (group === 1) {
setSelectedHeroes1(selectedHeroes1.filter(h => h.id !== heroId));
} else {
setSelectedHeroes2(selectedHeroes2.filter(h => h.id !== heroId));
}
};
const handleSearch = async (group?: number) => {
setLoading(true); setLoading(true);
try { try {
const heroCodes = selectedHeroes.map(hero => hero.heroCode); let heroCodes: string[] = [];
if (group === 1) {
heroCodes = selectedHeroes1.map(hero => hero.heroCode);
} else if (group === 2) {
heroCodes = selectedHeroes2.map(hero => hero.heroCode);
} else {
// 如果未指定组,使用第一组的英雄
heroCodes = selectedHeroes1.map(hero => hero.heroCode);
}
const data = await EpicApi.getGvgTeamList(heroCodes); const data = await EpicApi.getGvgTeamList(heroCodes);
setLineups(data); setLineups(data);
setError(null); setError(null);
@@ -73,22 +84,188 @@ const Lineup: React.FC = () => {
} }
}; };
const handleReset = () => { const handleReset = (group: number) => {
setSelectedHeroes([]); if (group === 1) {
setSearchInput(""); setSelectedHeroes1([]);
setLineups([]); setSearchInput1("");
setError(null); } else {
handleSearch(); // Fetch all lineups after reset setSelectedHeroes2([]);
setSearchInput2("");
}
handleSearch(group);
}; };
const filteredHeroes = heroes.filter(hero => { const filteredHeroes = (searchInput: string) => heroes.filter(hero => {
const searchLower = searchInput.toLowerCase(); const searchLower = searchInput.toLowerCase();
return hero.heroName.toLowerCase().includes(searchLower) || return hero.heroName.toLowerCase().includes(searchLower) ||
(hero.nickName && hero.nickName.toLowerCase().includes(searchLower)); (hero.nickName && hero.nickName.toLowerCase().includes(searchLower));
}); });
const handleImageUpload = async (file: File) => {
setIsUploading(true);
try {
// 创建预览图片URL
const previewUrl = URL.createObjectURL(file);
setPreviewImage(previewUrl);
const result = await EpicApi.recognizeHeroesFromImage(file);
// 更新第一组英雄,保持顺序
const heroes1 = result.heroNames.slice(0, 3).map(name => {
const hero = heroes.find(h => h.heroName === name);
return hero;
}).filter((hero): hero is EpicApi.Hero => hero !== undefined);
setSelectedHeroes1(heroes1);
// 更新第二组英雄,保持顺序
const heroes2 = result.heroNames.slice(3, 6).map(name => {
const hero = heroes.find(h => h.heroName === name);
return hero;
}).filter((hero): hero is EpicApi.Hero => hero !== undefined);
setSelectedHeroes2(heroes2);
// 自动触发搜索
// handleSearch();
} catch (error) {
console.error('Failed to process image:', error);
setError('图片识别失败,请重试');
} finally {
setIsUploading(false);
}
};
// 清理预览图片URL
useEffect(() => {
return () => {
if (previewImage) {
URL.revokeObjectURL(previewImage);
}
};
}, [previewImage]);
const handlePaste = async (e: React.ClipboardEvent) => {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
const file = items[i].getAsFile();
if (file) {
await handleImageUpload(file);
}
break;
}
}
};
const handleFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
await handleImageUpload(file);
}
};
const renderHeroSearch = (group: number) => {
const selectedHeroes = group === 1 ? selectedHeroes1 : selectedHeroes2;
const isDropdownOpen = group === 1 ? isDropdownOpen1 : isDropdownOpen2;
const searchInput = group === 1 ? searchInput1 : searchInput2;
const setSearchInput = group === 1 ? setSearchInput1 : setSearchInput2;
const setIsDropdownOpen = group === 1 ? setIsDropdownOpen1 : setIsDropdownOpen2;
return (
<div className="relative flex gap-2">
<div
className="bg-[#1A1412] border border-[#C17F59]/30 px-4 py-2 rounded-md text-sm w-[32rem] text-[#E6B17E] cursor-pointer flex items-center justify-between h-12"
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
>
<div className="flex flex-wrap gap-2 flex-1 items-center">
{selectedHeroes.length > 0 ? (
selectedHeroes.map((hero) => (
<div
key={hero.id}
className="flex items-center gap-2 px-2 py-1 bg-[#2A211E] rounded-md"
>
<span>{hero.heroName}</span>
<button
onClick={(e) => {
e.stopPropagation();
handleHeroRemove(hero.id, group);
}}
className="text-[#C17F59] hover:text-[#E6B17E]"
>
×
</button>
</div>
))
) : (
<span className="text-[#9B8579]">3</span>
)}
</div>
<span className={`transform transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''} ml-2`}>
</span>
</div>
<button
className="bg-[#1A1412] border border-[#C17F59]/30 px-4 py-2 rounded-md text-sm text-[#E6B17E] hover:bg-[#2A211E] hover:border-[#C17F59] transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
onClick={() => handleSearch(group)}
disabled={selectedHeroes.length === 0}
>
</button>
<button
className="bg-[#1A1412] border border-[#C17F59]/30 px-4 py-2 rounded-md text-sm text-[#E6B17E] hover:bg-[#2A211E] hover:border-[#C17F59] transition-colors duration-200 whitespace-nowrap"
onClick={() => handleReset(group)}
>
</button>
{isDropdownOpen && (
<div className="absolute top-full left-0 mt-1 bg-[#1A1412] border border-[#C17F59]/30 rounded-md shadow-lg max-h-60 overflow-y-auto z-50 w-[32rem]">
<div className="p-2 border-b border-[#C17F59]/30">
<input
type="text"
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)}
placeholder="输入英雄名称或昵称搜索..."
className="w-full px-3 py-2 bg-[#1A1412] text-[#E6B17E] border border-[#C17F59]/30 rounded-md focus:outline-none focus:border-[#C17F59]"
onClick={(e) => e.stopPropagation()}
/>
</div>
{filteredHeroes(searchInput).map((hero) => (
<div
key={hero.id}
className={`px-4 py-2 cursor-pointer ${
selectedHeroes.find(h => h.id === hero.id)
? "bg-[#2A211E] text-[#E6B17E]"
: "text-[#E6B17E] hover:bg-[#2A211E]"
} ${selectedHeroes.length >= 3 && !selectedHeroes.find(h => h.id === hero.id) ? "opacity-50 cursor-not-allowed" : ""}`}
onClick={() => {
if (selectedHeroes.length < 3 || selectedHeroes.find(h => h.id === hero.id)) {
handleHeroSelect(hero, group);
}
}}
>
<div className="flex items-center gap-2">
<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>
))}
</div>
)}
</div>
);
};
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" onPaste={handlePaste}>
{/* 背景装饰 */} {/* 背景装饰 */}
<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>
@@ -115,101 +292,65 @@ const Lineup: React.FC = () => {
<span></span> <span></span>
</div> </div>
{/* 英雄多选下拉框 */} <div className="flex gap-6">
<div className="relative flex gap-2"> <div className="flex-1">
<div {/* 图片上传区域 */}
className="bg-[#1A1412] border border-[#C17F59]/30 px-4 py-2 rounded-md text-sm w-[32rem] text-[#E6B17E] cursor-pointer flex items-center justify-between h-12" <div className="flex items-center gap-4 mb-4">
onClick={() => setIsDropdownOpen(!isDropdownOpen)} <div className="flex-1">
> <div className="flex items-center gap-2">
<div className="flex flex-wrap gap-2 flex-1 items-center"> <input
{selectedHeroes.length > 0 ? ( type="file"
selectedHeroes.map((hero) => ( ref={fileInputRef}
<div onChange={handleFileChange}
key={hero.id} accept="image/*"
className="flex items-center gap-2 px-2 py-1 bg-[#2A211E] rounded-md" className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="bg-[#1A1412] border border-[#C17F59]/30 px-4 py-2 rounded-md text-sm text-[#E6B17E] hover:bg-[#2A211E] hover:border-[#C17F59] transition-colors duration-200 whitespace-nowrap"
> >
<span>{hero.heroName}</span>
<button </button>
onClick={(e) => { <span className="text-[#9B8579] text-sm">
e.stopPropagation(); (Ctrl+V)
handleHeroRemove(hero.id); </span>
}} </div>
className="text-[#C17F59] hover:text-[#E6B17E]" </div>
>
×
</button>
</div>
))
) : (
<span className="text-[#9B8579]">3</span>
)}
</div> </div>
<span className={`transform transition-transform duration-200 ${isDropdownOpen ? 'rotate-180' : ''} ml-2`}>
</span>
</div>
<button
className="bg-[#1A1412] border border-[#C17F59]/30 px-4 py-2 rounded-md text-sm text-[#E6B17E] hover:bg-[#2A211E] hover:border-[#C17F59] transition-colors duration-200 disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap"
onClick={handleSearch}
disabled={selectedHeroes.length === 0}
>
</button>
<button {/* 第一组搜索 */}
className="bg-[#1A1412] border border-[#C17F59]/30 px-4 py-2 rounded-md text-sm text-[#E6B17E] hover:bg-[#2A211E] hover:border-[#C17F59] transition-colors duration-200 whitespace-nowrap" <div className="mb-4">
onClick={handleReset} <h3 className="text-[#E6B17E] mb-2"> 1</h3>
> {renderHeroSearch(1)}
</div>
</button>
{/* 第二组搜索 */}
{isDropdownOpen && ( <div>
<div className="absolute top-full left-0 mt-1 bg-[#1A1412] border border-[#C17F59]/30 rounded-md shadow-lg max-h-60 overflow-y-auto z-50 w-[32rem]"> <h3 className="text-[#E6B17E] mb-2"> 2</h3>
<div className="p-2 border-b border-[#C17F59]/30"> {renderHeroSearch(2)}
<input </div>
type="text" </div>
value={searchInput}
onChange={(e) => setSearchInput(e.target.value)} {/* 图片预览区域 */}
placeholder="输入英雄名称或昵称搜索..." {previewImage && (
className="w-full px-3 py-2 bg-[#1A1412] text-[#E6B17E] border border-[#C17F59]/30 rounded-md focus:outline-none focus:border-[#C17F59]" <div className="w-64 flex-shrink-0">
onClick={(e) => e.stopPropagation()} <h3 className="text-[#E6B17E] mb-2"></h3>
<div className="relative aspect-square rounded-lg overflow-hidden border border-[#C17F59]/30">
<img
src={previewImage}
alt="预览图片"
className="w-full h-full object-contain"
/> />
</div> </div>
{filteredHeroes.map((hero) => ( {/* 示例图片提示 */}
<div {previewImage === '/tt.png'}
key={hero.id}
className={`px-4 py-2 cursor-pointer ${
selectedHeroes.find(h => h.id === hero.id)
? "bg-[#2A211E] text-[#E6B17E]"
: "text-[#E6B17E] hover:bg-[#2A211E]"
} ${selectedHeroes.length >= 3 && !selectedHeroes.find(h => h.id === hero.id) ? "opacity-50 cursor-not-allowed" : ""}`}
onClick={() => {
if (selectedHeroes.length < 3 || selectedHeroes.find(h => h.id === hero.id)) {
handleHeroSelect(hero);
}
}}
>
<div className="flex items-center gap-2">
<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>
))}
</div> </div>
)} )}
</div> </div>
</div> </div>
{/* 加载状态 */} {/* 加载状态 */}
{loading && ( {(loading || isUploading) && (
<div className="flex justify-center items-center py-12"> <div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#E6B17E]"></div> <div className="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-[#E6B17E]"></div>
</div> </div>
@@ -225,82 +366,92 @@ const Lineup: React.FC = () => {
{/* 阵容列表 */} {/* 阵容列表 */}
{!loading && ( {!loading && (
<div className="space-y-4"> <div className="space-y-4">
{lineups.map((lineup) => ( {lineups.length > 0 ? (
<div lineups.map((lineup) => (
key={lineup.id} <div
className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg overflow-hidden border border-[#C17F59]/30 hover:border-[#C17F59] transition-all duration-300 transform hover:-translate-y-1 hover:shadow-xl hover:shadow-[#C17F59]/10 backdrop-blur-sm" key={lineup.id}
> className="bg-gradient-to-br from-[#2A211E] to-[#1A1412] rounded-lg overflow-hidden border border-[#C17F59]/30 hover:border-[#C17F59] transition-all duration-300 transform hover:-translate-y-1 hover:shadow-xl hover:shadow-[#C17F59]/10 backdrop-blur-sm"
<div className="p-6"> >
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4"> <div className="p-6">
{/* 防守阵容 */} <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
<div> {/* 防守阵容 */}
<h4 className="text-lg font-semibold text-[#E6B17E] mb-4"></h4> <div>
<div className="flex flex-wrap gap-4"> <h4 className="text-lg font-semibold text-[#E6B17E] mb-4"></h4>
{lineup.defenseHeroInfos.map((hero, index) => ( <div className="flex flex-wrap gap-4">
<div key={index} className="flex flex-col items-center group relative"> {lineup.defenseHeroInfos.map((hero, index) => (
<img <div key={index} className="flex flex-col items-center group relative">
src={hero.headImgUrl} <img
alt={hero.heroName} src={hero.headImgUrl}
className="w-16 h-16 rounded-full border-2 border-[#C17F59]/30 hover:border-[#C17F59] transition-colors duration-300" alt={hero.heroName}
/> className="w-16 h-16 rounded-full border-2 border-[#C17F59]/30 hover:border-[#C17F59] transition-colors duration-300"
<div className="absolute bottom-full mb-2 px-2 py-1 bg-[#1A1412] text-[#E6B17E] text-sm rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap border border-[#C17F59]/30"> />
{hero.heroName} <div className="absolute bottom-full mb-2 px-2 py-1 bg-[#1A1412] text-[#E6B17E] text-sm rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap border border-[#C17F59]/30">
{hero.heroName}
</div>
</div> </div>
</div> ))}
))} </div>
</div>
{/* 进攻阵容 */}
<div>
<h4 className="text-lg font-semibold text-[#E6B17E] mb-4"></h4>
<div className="flex flex-wrap gap-4">
{lineup.attackHeroInfos.map((hero, index) => (
<div key={index} className="flex flex-col items-center group relative">
<img
src={hero.headImgUrl}
alt={hero.heroName}
className="w-16 h-16 rounded-full border-2 border-[#C17F59]/30 hover:border-[#C17F59] transition-colors duration-300"
/>
<div className="absolute bottom-full mb-2 px-2 py-1 bg-[#1A1412] text-[#E6B17E] text-sm rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap border border-[#C17F59]/30">
{hero.heroName}
</div>
</div>
))}
</div>
</div> </div>
</div> </div>
{/* 进攻阵容 */} <div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
<div> {/* 装备信息 */}
<h4 className="text-lg font-semibold text-[#E6B17E] mb-4"></h4> <div>
<div className="flex flex-wrap gap-4"> <h4 className="text-lg font-semibold text-[#E6B17E] mb-2"></h4>
{lineup.attackHeroInfos.map((hero, index) => ( <p className="text-[#9B8579] whitespace-pre-line">{lineup.equipmentInfo.replace(/[^\S\n]/g, '')}</p>
<div key={index} className="flex flex-col items-center group relative"> </div>
<img
src={hero.headImgUrl} {/* 神器信息 */}
alt={hero.heroName} <div>
className="w-16 h-16 rounded-full border-2 border-[#C17F59]/30 hover:border-[#C17F59] transition-colors duration-300" <h4 className="text-lg font-semibold text-[#E6B17E] mb-2"></h4>
/> <p className="text-[#9B8579] whitespace-pre-line">{lineup.artifacts.replace(/[^\S\n]/g, '')}</p>
<div className="absolute bottom-full mb-2 px-2 py-1 bg-[#1A1412] text-[#E6B17E] text-sm rounded-md opacity-0 group-hover:opacity-100 transition-opacity duration-200 whitespace-nowrap border border-[#C17F59]/30">
{hero.heroName}
</div>
</div>
))}
</div> </div>
</div> </div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 装备信息 */} {/* 前置条件 */}
<div> <div>
<h4 className="text-lg font-semibold text-[#E6B17E] mb-2"></h4> <h4 className="text-lg font-semibold text-[#E6B17E] mb-2"></h4>
<p className="text-[#9B8579] whitespace-pre-line">{lineup.equipmentInfo.replace(/[^\S\n]/g, '')}</p> <p className="text-[#9B8579] whitespace-pre-line">{lineup.prerequisites.replace(/[^\S\n]/g, '')}</p>
</div> </div>
{/* 神器信息 */} {/* 重要提示 */}
<div> <div>
<h4 className="text-lg font-semibold text-[#E6B17E] mb-2"></h4> <h4 className="text-lg font-semibold text-[#E6B17E] mb-2"></h4>
<p className="text-[#9B8579] whitespace-pre-line">{lineup.artifacts.replace(/[^\S\n]/g, '')}</p> <p className="text-[#9B8579] whitespace-pre-line">{lineup.importantNotes.replace(/[^\S\n]/g, '')}</p>
</div> </div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* 前置条件 */}
<div>
<h4 className="text-lg font-semibold text-[#E6B17E] mb-2"></h4>
<p className="text-[#9B8579] whitespace-pre-line">{lineup.prerequisites.replace(/[^\S\n]/g, '')}</p>
</div>
{/* 重要提示 */}
<div>
<h4 className="text-lg font-semibold text-[#E6B17E] mb-2"></h4>
<p className="text-[#9B8579] whitespace-pre-line">{lineup.importantNotes.replace(/[^\S\n]/g, '')}</p>
</div> </div>
</div> </div>
</div> </div>
))
) : (
<div className="flex flex-col items-center justify-center py-12 text-[#9B8579]">
<svg className="w-16 h-16 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-lg"></p>
<p className="text-sm mt-2"></p>
</div> </div>
))} )}
</div> </div>
)} )}
</div> </div>