import gradio as gr import requests import webbrowser from typing import List, Dict, Any class Hero: def __init__(self, data: Dict[str, Any]): self.heroId = data.get("heroId") self.name = data.get("name") self.alias = data.get("alias") self.title = data.get("title") self.roles = data.get("roles", []) self.keywords = data.get("keywords") @property def image_url(self): return f"https://game.gtimg.cn/images/lol/act/img/champion/{self.alias}.png" class HeroList: def __init__(self, data: Dict[str, Any]): self.hero = [Hero(hero_data) for hero_data in data.get("hero", [])] def get_hero_data() -> HeroList: url = "https://game.gtimg.cn/images/lol/act/img/js/heroList/hero_list.js" response = requests.get(url) data = response.json() return HeroList(data) def translate_role(role: str) -> str: role_map = { "fighter": "战士", "tank": "坦克", "mage": "法师", "assassin": "刺客", "marksman": "射手", "support": "辅助", } return role_map.get(role, role) hero_list = None all_heroes = [] selected_heroes = [] def load_heroes(): global hero_list, all_heroes if not all_heroes: hero_list = get_hero_data() all_heroes = hero_list.hero for hero in all_heroes: hero.roles = [translate_role(role) for role in hero.roles] return all_heroes # 应用启动时预加载英雄数据 # load_heroes() def get_heroes_by_role(role: str): if not all_heroes: load_heroes() return [hero for hero in all_heroes if role in hero.roles] def search_heroes(query: str): if not all_heroes: load_heroes() query = query.lower() if not query: return [] return [ hero for hero in all_heroes if query in hero.title.lower() or query in hero.name.lower() or (hero.keywords and query in hero.keywords.lower()) ] def select_hero(hero_alias: str): global selected_heroes if not all_heroes: load_heroes() hero = next((h for h in all_heroes if h.alias == hero_alias), None) if hero and len(selected_heroes) < 10 and hero not in selected_heroes: selected_heroes.append(hero) return get_selected_heroes_display() def remove_hero(hero_alias: str): global selected_heroes selected_heroes = [h for h in selected_heroes if h.alias != hero_alias] return get_selected_heroes_display() def clear_selection(): global selected_heroes selected_heroes = [] return get_selected_heroes_display() def get_selected_heroes_display(): if not selected_heroes: return "", "" team1 = selected_heroes[:5] team2 = selected_heroes[5:10] team1_str = "\n".join([f"{h.title}({h.name})" for h in team1]) team2_str = "\n".join([f"{h.title}({h.name})" for h in team2]) return team1_str, team2_str def open_hero_detail(hero_id: str): url = f"https://101.qq.com/#/hero-detail?heroid={hero_id}&tab=equipment" # 使用 JavaScript 在新标签页打开链接 return f"已打开英雄详情: {url}" def open_metasrc(hero_alias: str): url = f"https://www.metasrc.com/lol/mayhem/build/{hero_alias.lower()}" # 使用 JavaScript 在新标签页打开链接 return f"已打开 MetaSrc: {url}" def open_tier_list(): url = "https://www.metasrc.com/lol/urf/tier-list" # 使用 JavaScript 在新标签页打开链接 return f"已打开排行榜: {url}" def create_hero_card(hero: Hero): with gr.Column(scale=1, min_width=140, elem_classes=["hero-card"]): gr.HTML( value=f'', elem_classes=["hero-image-wrapper"] ) gr.Textbox( value=f"{hero.title}\n({hero.name})", lines=2, interactive=False, text_align="center", show_label=False, elem_classes=["hero-name"] ) gr.Textbox( value=",".join(hero.roles), lines=1, interactive=False, text_align="center", show_label=False, elem_classes=["hero-roles"] ) with gr.Row(elem_classes=["hero-buttons"]): gr.Button("详情", variant="primary").click( fn=lambda x: f"已打开英雄详情", inputs=[gr.Textbox(value=hero.heroId, visible=False)], outputs=[gr.Textbox(visible=False)], js=f"() => window.open('https://101.qq.com/#/hero-detail?heroid={hero.heroId}&tab=equipment', '_blank')", ) gr.Button("出装", variant="secondary").click( fn=lambda x: f"已打开 MetaSrc", inputs=[gr.Textbox(value=hero.alias, visible=False)], outputs=[gr.Textbox(visible=False)], js=f"() => window.open('https://www.metasrc.com/lol/mayhem/build/{hero.alias.lower()}', '_blank')", ) def create_hero_tab(tab_name: str): with gr.Tab(tab_name): # 直接创建英雄卡片,get_heroes_by_role 会自动加载英雄数据 heroes = get_heroes_by_role(tab_name) # 使用CSS Grid布局 with gr.Row(elem_classes=["hero-grid"]): for hero in heroes: create_hero_card(hero) role_tabs = ["战士", "法师", "刺客", "坦克", "射手", "辅助"] with gr.Blocks( title="ChatGptLoL - Python Gradio版本", css=""" /* 全局样式 */ :root { --primary-color: #0066cc; --secondary-color: #0099ff; --accent-color: #ff6600; --background-color: #f0f2f5; --card-background: #ffffff; --text-color: #333333; --text-light: #666666; --border-radius: 12px; --box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); --box-shadow-hover: 0 8px 15px rgba(0, 0, 0, 0.15); --transition: all 0.3s ease; --transition-fast: all 0.2s ease; --transition-slow: all 0.5s ease; } body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: var(--background-color); color: var(--text-color); line-height: 1.6; } /* 禁用所有 Textarea 滚动条 */ textarea { overflow: hidden !important; resize: none !important; } .gradio-container { max-width: 1400px !important; margin: 0 auto; padding: 20px; } /* 标题样式 */ h1 { text-align: center; color: var(--primary-color); margin-bottom: 30px !important; font-weight: 700; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1); } /* 标签页样式 */ .tabs { margin-bottom: 30px; } .tab-button { font-weight: 600; padding: 10px 20px; border-radius: var(--border-radius) var(--border-radius) 0 0; transition: var(--transition); } .tab-button:hover { background-color: var(--secondary-color) !important; color: white !important; } .tab-content { background-color: var(--card-background); border-radius: 0 var(--border-radius) var(--border-radius) var(--border-radius); padding: 20px; box-shadow: var(--box-shadow); } /* 搜索区域 */ #search-hero-input { width: 100% !important; margin: 0 auto 0px auto !important; background-color: var(--card-background) !important; border: 2px solid var(--secondary-color) !important; border-radius: var(--border-radius) !important; --spacing-sm: 0px !important; } #search-hero-input textarea { width: 100% !important; background-color: transparent !important; } /* 搜索结果容器 */ #search-results-html { background-color: transparent !important; box-shadow: none !important; padding: 0 !important; width: 100% !important; margin-top: 0 !important; margin-bottom: 0 !important; --spacing-sm: 0px !important; } /* 移除Gradio生成的灰色背景 - 搜索框的父容器 */ #search-hero-input.parent { background: transparent !important; box-shadow: none !important; border: none !important; } /* 移除Gradio生成的灰色背景 - 搜索结果的父容器 */ #search-results-html.parent { background: transparent !important; box-shadow: none !important; border: none !important; } /* 直接选择搜索框的父容器 */ .gradio-container > div:has(#search-hero-input) { background: transparent !important; box-shadow: none !important; border: none !important; } /* 直接选择搜索结果的父容器 */ .gradio-container > div:has(#search-results-html) { background: transparent !important; box-shadow: none !important; border: none !important; } /* 移除特定Svelte类的灰色背景 */ div.svelte-633qhp { background: transparent !important; box-shadow: none !important; border: none !important; } /* 英雄网格布局 */ .hero-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 16px; padding: 10px; max-height: 600px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: var(--secondary-color) #f1f1f1; } /* WebKit 浏览器滚动条样式 */ .hero-grid::-webkit-scrollbar { width: 8px; } .hero-grid::-webkit-scrollbar-track { background: #f1f1f1; border-radius: 4px; } .hero-grid::-webkit-scrollbar-thumb { background: var(--secondary-color); border-radius: 4px; } .hero-grid::-webkit-scrollbar-thumb:hover { background: var(--primary-color); } /* 英雄卡片样式 */ .hero-card { background-color: var(--card-background); border-radius: var(--border-radius); padding: 15px; box-shadow: var(--box-shadow); transition: var(--transition); text-align: center; display: flex; flex-direction: column; align-items: center; position: relative; overflow: hidden; } .hero-card::before { content: ''; position: absolute; top: 0; left: -100%; width: 100%; height: 100%; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); transition: var(--transition-slow); } .hero-card:hover { transform: translateY(-5px) scale(1.02); box-shadow: var(--box-shadow-hover); } .hero-card:hover::before { left: 100%; } /* 英雄图片样式 */ .hero-image { border-radius: 12px !important; border: 2px solid #c8c8c8 !important; margin-bottom: 10px !important; margin-left: auto !important; margin-right: auto !important; display: block !important; width: 90px !important; height: 90px !important; object-fit: cover !important; } /* 英雄名称样式 */ .hero-name { font-weight: 600 !important; color: var(--primary-color) !important; margin-bottom: 5px !important; overflow: hidden !important; text-overflow: ellipsis !important; white-space: nowrap !important; } /* 英雄名称 Textbox 禁用滚动 */ .hero-name textarea { overflow: hidden !important; resize: none !important; } /* 英雄角色样式 */ .hero-roles { font-size: 12px !important; color: var(--text-light) !important; margin-bottom: 10px !important; overflow: hidden !important; white-space: nowrap !important; } /* 英雄角色 Textbox 禁用滚动 */ .hero-roles textarea { overflow: hidden !important; resize: none !important; } /* 英雄按钮样式 */ .hero-buttons { display: flex; gap: 8px; width: 100%; margin-top: auto; } .hero-buttons .gr-button { flex: 1; font-size: 12px !important; padding: 6px 10px !important; } /* 按钮样式 */ .gr-button { border-radius: 8px !important; font-weight: 600 !important; transition: var(--transition) !important; padding: 8px 16px !important; position: relative; overflow: hidden; } .gr-button:hover { transform: translateY(-2px) !important; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1) !important; } .gr-button:active { transform: translateY(0) !important; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1) !important; } .gr-button::before { content: ''; position: absolute; top: 50%; left: 50%; width: 0; height: 0; background: rgba(255, 255, 255, 0.3); border-radius: 50%; transform: translate(-50%, -50%); transition: var(--transition-fast); } .gr-button:active::before { width: 300px; height: 300px; } /* 滚动按钮 */ .scroll-buttons { position: fixed; bottom: 30px; right: 30px; z-index: 1000; display: flex; flex-direction: column; gap: 10px; } .scroll-buttons .gr-button { background-color: var(--primary-color) !important; color: white !important; border: none !important; padding: 12px 16px !important; border-radius: 50% !important; min-width: 50px !important; height: 50px !important; display: flex !important; align-items: center !important; justify-content: center !important; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); } .scroll-buttons .gr-button:hover { background-color: var(--secondary-color) !important; transform: translateY(-3px) !important; } /* 功能按钮区域 */ .action-buttons { display: flex; gap: 15px; justify-content: center; margin: 20px 0; padding: 0 20px; } /* 队伍显示区域 */ .team-display { background-color: var(--card-background); border-radius: var(--border-radius); padding: 20px; box-shadow: var(--box-shadow); margin: 20px 0; gap: 30px; } .team-textbox { background-color: #f8f9fa !important; border: 1px solid var(--secondary-color) !important; border-radius: var(--border-radius) !important; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif !important; font-size: 14px !important; line-height: 1.5 !important; } /* 响应式设计 */ @media (max-width: 768px) { .gradio-container { padding: 10px; } h1 { font-size: 1.5rem !important; margin-bottom: 20px !important; } .hero-grid { grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); gap: 12px; padding: 10px; max-height: 500px; } .hero-card { padding: 12px; } .hero-image { width: 90px !important; height: 90px !important; } .hero-name { font-size: 12px !important; } .hero-roles { font-size: 10px !important; } .hero-buttons .gr-button { font-size: 10px !important; padding: 4px 8px !important; } .action-buttons { flex-direction: column; align-items: center; gap: 10px; } .action-buttons .gr-button { width: 100%; max-width: 200px; } .team-display { flex-direction: column; gap: 20px; padding: 15px; } .search-result-item { flex-direction: column; align-items: flex-start; gap: 12px; padding: 15px; } .search-result-image { width: 70px !important; height: 70px !important; } .search-result-title { font-size: 16px !important; } .search-result-name { font-size: 14px !important; } .search-result-roles { font-size: 12px !important; } .search-result-buttons { width: 100%; justify-content: space-between; } .search-result-button { flex: 1; font-size: 13px; padding: 8px 12px; } .scroll-buttons { bottom: 20px; right: 20px; } .scroll-buttons .gr-button { min-width: 40px !important; height: 40px !important; padding: 8px 12px !important; } } /* 搜索结果样式 */ .search-results { display: flex; flex-direction: column; gap: 16px; margin: 20px 0; padding: 0 10px; } .search-result-item { display: flex; align-items: center; gap: 20px; padding: 20px; background-color: var(--card-background); border-radius: var(--border-radius); box-shadow: var(--box-shadow); transition: var(--transition); } .search-result-item:hover { transform: translateY(-2px); box-shadow: 0 6px 12px rgba(0, 0, 0, 0.1); } .search-result-image { width: 90px; height: 90px; border-radius: 12px; object-fit: cover; border: 3px solid var(--secondary-color); flex-shrink: 0; } .search-result-info { flex: 1; } .search-result-title { font-weight: 700; font-size: 18px; color: var(--primary-color); margin-bottom: 6px; } .search-result-name { font-size: 16px; color: var(--text-light); margin-bottom: 6px; } .search-result-roles { font-size: 14px; color: var(--text-light); } .search-result-buttons { display: flex; gap: 12px; flex-shrink: 0; } .search-result-button { padding: 10px 20px; border: none; border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer; transition: var(--transition); min-width: 90px; text-align: center; } .search-result-button.primary { background-color: var(--primary-color); color: white; } .search-result-button.secondary { background-color: var(--secondary-color); color: white; } .search-result-button:hover { transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); } .search-empty { text-align: center; padding: 40px 20px; color: var(--text-light); background-color: var(--card-background); border-radius: var(--border-radius); box-shadow: var(--box-shadow); margin: 20px 0; } /* 中型屏幕响应式调整 */ @media (min-width: 769px) and (max-width: 1024px) { .hero-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 12px; } } """, head=""" """, ) as app: gr.Markdown("# ChatGptLoL - 英雄联盟英雄选择工具") for tab_name in role_tabs: create_hero_tab(tab_name) # # 功能按钮区域 # with gr.Row(elem_classes=["action-buttons"]): # clear_btn = gr.Button("清空选择", variant="secondary") # generate_btn = gr.Button("生成队伍", variant="primary") # tier_list_btn = gr.Button("排行资料", variant="secondary") # # 队伍显示区域 # with gr.Row(elem_classes=["team-display"]): # with gr.Column(): # gr.Markdown("**敌方队伍**") # team1_display = gr.Textbox(label="敌方英雄", lines=5, interactive=False, elem_classes=["team-textbox"]) # with gr.Column(): # gr.Markdown("**我方队伍**") # team2_display = gr.Textbox(label="我方英雄", lines=5, interactive=False, elem_classes=["team-textbox"]) # 搜索区域 search_query = gr.Textbox( label="搜索英雄", placeholder="输入英雄名称或关键词", elem_id="search-hero-input", ) search_results_html = gr.HTML(elem_id="search-results-html", visible=True) # 搜索功能防抖处理 import time last_search_time = 0 search_debounce_delay = 0.3 # 300ms防抖延迟 previous_results = "" # 存储上一次的搜索结果 def update_search_results(query): global last_search_time, previous_results current_time = time.time() # 检查是否在防抖延迟内 if current_time - last_search_time < search_debounce_delay: # 返回上一次的结果,不更新 return previous_results last_search_time = current_time results = search_heroes(query) if not results: previous_results = "
没有找到匹配的英雄
" return previous_results html_content = '
' for h in results: detail_url = ( f"https://101.qq.com/#/hero-detail?heroid={h.heroId}&tab=equipment" ) equipment_url = ( f"https://www.metasrc.com/lol/mayhem/build/{h.alias.lower()}" ) html_content += f"""
{h.title}
{h.name}
{''.join(h.roles)}
""" html_content += "
" previous_results = html_content return html_content search_query.change( fn=update_search_results, inputs=[search_query], outputs=[search_results_html] ) search_results = gr.Textbox( label="搜索结果", lines=10, interactive=False, visible=False ) # # 功能按钮事件处理 # clear_btn.click( # fn=clear_selection, # inputs=[], # outputs=[team1_display, team2_display] # ) # tier_list_btn.click( # fn=open_tier_list, # inputs=[], # outputs=[team1_display] # ) # # 生成队伍功能 # def generate_team(): # import random # if not all_heroes: # load_heroes() # # 随机选择10个英雄 # random_heroes = random.sample(all_heroes, min(10, len(all_heroes))) # global selected_heroes # selected_heroes = random_heroes # return get_selected_heroes_display() # generate_btn.click( # fn=generate_team, # inputs=[], # outputs=[team1_display, team2_display] # ) with gr.Row(elem_id="scroll-buttons", elem_classes="scroll-buttons"): gr.Button("↑", variant="primary", elem_classes=["scroll-top-btn"]).click( fn=lambda: None, inputs=[], outputs=[], js="() => window.scrollTo({top: 0, behavior: 'smooth'})" ) gr.Button("🔍", variant="secondary", elem_classes=["scroll-search-btn"]).click( fn=lambda: None, inputs=[], outputs=[], js='() => { const element = document.getElementById("search-hero-input"); if (element) { const rect = element.getBoundingClientRect(); window.scrollTo({top: window.pageYOffset + rect.top - 100, behavior: "smooth"}); } }' ) app.launch()