diff --git a/public/favicon.svg b/public/favicon.svg new file mode 100644 index 0000000..23f7bfc --- /dev/null +++ b/public/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/tt.png b/public/tt.png new file mode 100644 index 0000000..8faa340 Binary files /dev/null and b/public/tt.png differ diff --git a/src/api/index.ts b/src/api/index.ts index b20e72e..cc28330 100644 --- a/src/api/index.ts +++ b/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 { - total: number; - list: T[]; + total: number; + list: T[]; } export interface Response { - 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(`/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(`/epic/hero/team-list?${params.toString()}`); + return response; }; // 获取 GVG 阵容详情 export const getGvgTeamDetail = async (id: number) => { - const response = await Api.get>(`/api/gvg/team/${id}`); - if (response.code === 0) { - return response.data; - } - throw new Error(response.msg || "获取阵容详情失败"); + const response = await Api.get>(`/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("/epic/hero/list-all"); - return response; + const response = await Api.get("/epic/hero/list-all"); + return response; +}; + +export interface ImageRecognitionResult { + heroNames: string[]; // 6个有序的角色名称,前3个为一组,后3个为一组 +} + +// 图片识别接口 +export const recognizeHeroesFromImage = async (file: File): Promise => { + try { + const response = await Api.upload('/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; + } }; diff --git a/src/pages/Lineup.tsx b/src/pages/Lineup.tsx index 7811a92..ab3f11d 100644 --- a/src/pages/Lineup.tsx +++ b/src/pages/Lineup.tsx @@ -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(false); const [error, setError] = useState(null); const [heroes, setHeroes] = useState([]); - const [selectedHeroes, setSelectedHeroes] = useState([]); - const [isDropdownOpen, setIsDropdownOpen] = useState(false); - const [searchInput, setSearchInput] = useState(""); + const [selectedHeroes1, setSelectedHeroes1] = useState([]); + const [selectedHeroes2, setSelectedHeroes2] = 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(null); + const [previewImage, setPreviewImage] = useState('/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) => { + 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 ( +
+
setIsDropdownOpen(!isDropdownOpen)} + > +
+ {selectedHeroes.length > 0 ? ( + selectedHeroes.map((hero) => ( +
+ {hero.heroName} + +
+ )) + ) : ( + 选择防守英雄(最多3个) + )} +
+ + ▼ + +
+ + + + + + {isDropdownOpen && ( +
+
+ 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()} + /> +
+ {filteredHeroes(searchInput).map((hero) => ( +
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); + } + }} + > +
+ {hero.heroName} + {hero.heroName} + {hero.nickName && ( + ({hero.nickName}) + )} +
+
+ ))} +
+ )} +
+ ); + }; + return ( -
+
{/* 背景装饰 */}
@@ -115,101 +292,65 @@ const Lineup: React.FC = () => { 感谢分享
- {/* 英雄多选下拉框 */} -
-
setIsDropdownOpen(!isDropdownOpen)} - > -
- {selectedHeroes.length > 0 ? ( - selectedHeroes.map((hero) => ( -
+
+ {/* 图片上传区域 */} +
+
+
+ + -
- )) - ) : ( - 选择防守英雄(最多3个) - )} + 上传图片 + + + 或直接粘贴图片 (Ctrl+V),请参考右侧图片示例 + +
+
- - ▼ - -
- - - - - {isDropdownOpen && ( -
-
- 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()} + {/* 第一组搜索 */} +
+

防守阵容 1

+ {renderHeroSearch(1)} +
+ + {/* 第二组搜索 */} +
+

防守阵容 2

+ {renderHeroSearch(2)} +
+
+ + {/* 图片预览区域 */} + {previewImage && ( +
+

上传图片预览

+
+ 预览图片
- {filteredHeroes.map((hero) => ( -
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); - } - }} - > -
- {hero.heroName} - {hero.heroName} - {hero.nickName && ( - ({hero.nickName}) - )} -
-
- ))} + {/* 示例图片提示 */} + {previewImage === '/tt.png'}
)}
{/* 加载状态 */} - {loading && ( + {(loading || isUploading) && (
@@ -225,82 +366,92 @@ const Lineup: React.FC = () => { {/* 阵容列表 */} {!loading && (
- {lineups.map((lineup) => ( -
-
-
- {/* 防守阵容 */} -
-

防守阵容

-
- {lineup.defenseHeroInfos.map((hero, index) => ( -
- {hero.heroName} -
- {hero.heroName} + {lineups.length > 0 ? ( + lineups.map((lineup) => ( +
+
+
+ {/* 防守阵容 */} +
+

防守阵容

+
+ {lineup.defenseHeroInfos.map((hero, index) => ( +
+ {hero.heroName} +
+ {hero.heroName} +
-
- ))} + ))} +
+
+ + {/* 进攻阵容 */} +
+

进攻阵容

+
+ {lineup.attackHeroInfos.map((hero, index) => ( +
+ {hero.heroName} +
+ {hero.heroName} +
+
+ ))} +
- {/* 进攻阵容 */} -
-

进攻阵容

-
- {lineup.attackHeroInfos.map((hero, index) => ( -
- {hero.heroName} -
- {hero.heroName} -
-
- ))} +
+ {/* 装备信息 */} +
+

装备信息

+

{lineup.equipmentInfo.replace(/[^\S\n]/g, '')}

+
+ + {/* 神器信息 */} +
+

神器信息

+

{lineup.artifacts.replace(/[^\S\n]/g, '')}

-
-
- {/* 装备信息 */} -
-

装备信息

-

{lineup.equipmentInfo.replace(/[^\S\n]/g, '')}

-
+
+ {/* 前置条件 */} +
+

前置条件

+

{lineup.prerequisites.replace(/[^\S\n]/g, '')}

+
- {/* 神器信息 */} -
-

神器信息

-

{lineup.artifacts.replace(/[^\S\n]/g, '')}

-
-
- -
- {/* 前置条件 */} -
-

前置条件

-

{lineup.prerequisites.replace(/[^\S\n]/g, '')}

-
- - {/* 重要提示 */} -
-

重要提示

-

{lineup.importantNotes.replace(/[^\S\n]/g, '')}

+ {/* 重要提示 */} +
+

重要提示

+

{lineup.importantNotes.replace(/[^\S\n]/g, '')}

+
+ )) + ) : ( +
+ + + +

暂无对战阵容信息

+

请尝试其他英雄组合

- ))} + )}
)}