feat(api): 添加图片识别功能并优化阵容搜索
- 新增图片识别接口和相关功能 -重构阵容搜索逻辑,支持两组英雄同时搜索 - 优化用户界面,增加上传图片预览和示例图片提示 -调整布局和样式,提升用户体验
This commit is contained in:
8
public/favicon.svg
Normal file
8
public/favicon.svg
Normal 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
BIN
public/tt.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 250 KiB |
109
src/api/index.ts
109
src/api/index.ts
@@ -1,80 +1,99 @@
|
||||
import { Api } from "../utils/axios/config";
|
||||
import type { AxiosRequestConfig } from "axios";
|
||||
import {Api} from "../utils/axios/config";
|
||||
import axios, {AxiosRequestConfig} from "axios";
|
||||
|
||||
// 查询参数接口
|
||||
export interface GvgTeamQueryParams {
|
||||
defenseHeroes?: string[];
|
||||
attackHeroes?: string[];
|
||||
equipmentInfo?: string;
|
||||
artifacts?: string[];
|
||||
battleStrategy?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
defenseHeroes?: string[];
|
||||
attackHeroes?: string[];
|
||||
equipmentInfo?: string;
|
||||
artifacts?: string[];
|
||||
battleStrategy?: string;
|
||||
pageNum?: number;
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
// 英雄信息接口
|
||||
export interface HeroInfo {
|
||||
heroCode: string;
|
||||
headImgUrl: string;
|
||||
heroName: string;
|
||||
heroCode: string;
|
||||
headImgUrl: string;
|
||||
heroName: string;
|
||||
}
|
||||
|
||||
// 阵容数据接口
|
||||
export interface GvgTeam {
|
||||
id: number;
|
||||
defenseHeroes: string[];
|
||||
defenseHeroInfos: HeroInfo[];
|
||||
attackHeroes: string[];
|
||||
attackHeroInfos: HeroInfo[];
|
||||
equipmentInfo: string;
|
||||
artifacts: string;
|
||||
battleStrategy: string;
|
||||
prerequisites: string;
|
||||
importantNotes: string;
|
||||
id: number;
|
||||
defenseHeroes: string[];
|
||||
defenseHeroInfos: HeroInfo[];
|
||||
attackHeroes: string[];
|
||||
attackHeroInfos: HeroInfo[];
|
||||
equipmentInfo: string;
|
||||
artifacts: string;
|
||||
battleStrategy: string;
|
||||
prerequisites: string;
|
||||
importantNotes: string;
|
||||
}
|
||||
|
||||
// 分页响应接口
|
||||
export interface PageResult<T> {
|
||||
total: number;
|
||||
list: T[];
|
||||
total: number;
|
||||
list: T[];
|
||||
}
|
||||
|
||||
export interface Response<T> {
|
||||
code: number;
|
||||
data: T;
|
||||
msg: string;
|
||||
code: number;
|
||||
data: T;
|
||||
msg: string;
|
||||
}
|
||||
|
||||
// 英雄列表接口
|
||||
export interface Hero {
|
||||
id: number;
|
||||
heroCode: string;
|
||||
heroName: string;
|
||||
nickName: string;
|
||||
headImgUrl: string;
|
||||
id: number;
|
||||
heroCode: string;
|
||||
heroName: string;
|
||||
nickName: string;
|
||||
headImgUrl: string;
|
||||
}
|
||||
|
||||
// 查询 GVG 阵容列表
|
||||
export const getGvgTeamList = async (heroCodes?: string[]) => {
|
||||
const params = new URLSearchParams();
|
||||
if (heroCodes && heroCodes.length > 0) {
|
||||
params.append('heroCodes', heroCodes.join(','));
|
||||
}
|
||||
const response = await Api.get<GvgTeam[]>(`/epic/hero/team-list?${params.toString()}`);
|
||||
return response;
|
||||
const params = new URLSearchParams();
|
||||
if (heroCodes && heroCodes.length > 0) {
|
||||
params.append('heroCodes', heroCodes.join(','));
|
||||
}
|
||||
const response = await Api.get<GvgTeam[]>(`/epic/hero/team-list?${params.toString()}`);
|
||||
return response;
|
||||
};
|
||||
|
||||
// 获取 GVG 阵容详情
|
||||
export const getGvgTeamDetail = async (id: number) => {
|
||||
const response = await Api.get<Response<GvgTeam>>(`/api/gvg/team/${id}`);
|
||||
if (response.code === 0) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error(response.msg || "获取阵容详情失败");
|
||||
const response = await Api.get<Response<GvgTeam>>(`/api/gvg/team/${id}`);
|
||||
if (response.code === 0) {
|
||||
return response.data;
|
||||
}
|
||||
throw new Error(response.msg || "获取阵容详情失败");
|
||||
};
|
||||
|
||||
// 查询所有英雄列表
|
||||
export const getHeroList = async () => {
|
||||
const response = await Api.get<Hero[]>("/epic/hero/list-all");
|
||||
return response;
|
||||
const response = await Api.get<Hero[]>("/epic/hero/list-all");
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useState, useEffect} from "react";
|
||||
import React, {useState, useEffect, useRef} from "react";
|
||||
import * as EpicApi from '@/api/index';
|
||||
import { Character } from '@/api/types';
|
||||
|
||||
@@ -7,9 +7,15 @@ const Lineup: React.FC = () => {
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [heroes, setHeroes] = useState<EpicApi.Hero[]>([]);
|
||||
const [selectedHeroes, setSelectedHeroes] = useState<EpicApi.Hero[]>([]);
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
const [searchInput, setSearchInput] = useState("");
|
||||
const [selectedHeroes1, setSelectedHeroes1] = useState<EpicApi.Hero[]>([]);
|
||||
const [selectedHeroes2, setSelectedHeroes2] = useState<EpicApi.Hero[]>([]);
|
||||
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
|
||||
useEffect(() => {
|
||||
@@ -30,38 +36,43 @@ const Lineup: React.FC = () => {
|
||||
handleSearch();
|
||||
}, []);
|
||||
|
||||
const handleHeroSelect = (hero: EpicApi.Hero) => {
|
||||
if (selectedHeroes.length < 3 && !selectedHeroes.find(h => h.id === hero.id)) {
|
||||
setSelectedHeroes([...selectedHeroes, hero]);
|
||||
setSearchInput("");
|
||||
setIsDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleHeroRemove = (heroId: number) => {
|
||||
setSelectedHeroes(selectedHeroes.filter(h => h.id !== heroId));
|
||||
};
|
||||
|
||||
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);
|
||||
const handleHeroSelect = (hero: EpicApi.Hero, group: number) => {
|
||||
if (group === 1) {
|
||||
if (selectedHeroes1.length < 3 && !selectedHeroes1.find(h => h.id === hero.id)) {
|
||||
setSelectedHeroes1([...selectedHeroes1, hero]);
|
||||
setSearchInput1("");
|
||||
setIsDropdownOpen1(false);
|
||||
}
|
||||
} else {
|
||||
if (selectedHeroes2.length < 3 && !selectedHeroes2.find(h => h.id === hero.id)) {
|
||||
setSelectedHeroes2([...selectedHeroes2, hero]);
|
||||
setSearchInput2("");
|
||||
setIsDropdownOpen2(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);
|
||||
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);
|
||||
setLineups(data);
|
||||
setError(null);
|
||||
@@ -73,22 +84,188 @@ const Lineup: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setSelectedHeroes([]);
|
||||
setSearchInput("");
|
||||
setLineups([]);
|
||||
setError(null);
|
||||
handleSearch(); // Fetch all lineups after reset
|
||||
const handleReset = (group: number) => {
|
||||
if (group === 1) {
|
||||
setSelectedHeroes1([]);
|
||||
setSearchInput1("");
|
||||
} else {
|
||||
setSelectedHeroes2([]);
|
||||
setSearchInput2("");
|
||||
}
|
||||
handleSearch(group);
|
||||
};
|
||||
|
||||
const filteredHeroes = heroes.filter(hero => {
|
||||
const filteredHeroes = (searchInput: string) => heroes.filter(hero => {
|
||||
const searchLower = searchInput.toLowerCase();
|
||||
return hero.heroName.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 (
|
||||
<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>
|
||||
|
||||
@@ -115,101 +292,65 @@ const Lineup: React.FC = () => {
|
||||
<span>感谢分享</span>
|
||||
</div>
|
||||
|
||||
{/* 英雄多选下拉框 */}
|
||||
<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"
|
||||
<div className="flex gap-6">
|
||||
<div className="flex-1">
|
||||
{/* 图片上传区域 */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={handleFileChange}
|
||||
accept="image/*"
|
||||
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
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleHeroRemove(hero.id);
|
||||
}}
|
||||
className="text-[#C17F59] hover:text-[#E6B17E]"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className="text-[#9B8579]">选择防守英雄(最多3个)</span>
|
||||
)}
|
||||
上传图片
|
||||
</button>
|
||||
<span className="text-[#9B8579] text-sm">
|
||||
或直接粘贴图片 (Ctrl+V),请参考右侧图片示例
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 第一组搜索 */}
|
||||
<div className="mb-4">
|
||||
<h3 className="text-[#E6B17E] mb-2">防守阵容 1</h3>
|
||||
{renderHeroSearch(1)}
|
||||
</div>
|
||||
|
||||
{/* 第二组搜索 */}
|
||||
<div>
|
||||
<h3 className="text-[#E6B17E] mb-2">防守阵容 2</h3>
|
||||
{renderHeroSearch(2)}
|
||||
</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"
|
||||
onClick={handleReset}
|
||||
>
|
||||
重置
|
||||
</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()}
|
||||
{/* 图片预览区域 */}
|
||||
{previewImage && (
|
||||
<div className="w-64 flex-shrink-0">
|
||||
<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>
|
||||
{filteredHeroes.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);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
))}
|
||||
{/* 示例图片提示 */}
|
||||
{previewImage === '/tt.png'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 加载状态 */}
|
||||
{loading && (
|
||||
{(loading || isUploading) && (
|
||||
<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>
|
||||
@@ -225,82 +366,92 @@ const Lineup: React.FC = () => {
|
||||
{/* 阵容列表 */}
|
||||
{!loading && (
|
||||
<div className="space-y-4">
|
||||
{lineups.map((lineup) => (
|
||||
<div
|
||||
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>
|
||||
<h4 className="text-lg font-semibold text-[#E6B17E] mb-4">防守阵容</h4>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{lineup.defenseHeroInfos.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}
|
||||
{lineups.length > 0 ? (
|
||||
lineups.map((lineup) => (
|
||||
<div
|
||||
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>
|
||||
<h4 className="text-lg font-semibold text-[#E6B17E] mb-4">防守阵容</h4>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{lineup.defenseHeroInfos.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>
|
||||
<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>
|
||||
<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 className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-4">
|
||||
{/* 装备信息 */}
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
{/* 神器信息 */}
|
||||
<div>
|
||||
<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>
|
||||
</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-2">装备信息</h4>
|
||||
<p className="text-[#9B8579] whitespace-pre-line">{lineup.equipmentInfo.replace(/[^\S\n]/g, '')}</p>
|
||||
</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.artifacts.replace(/[^\S\n]/g, '')}</p>
|
||||
</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>
|
||||
<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 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>
|
||||
|
||||
Reference in New Issue
Block a user