增加web模块
This commit is contained in:
parent
1053ea7697
commit
0362a9c3ab
36
README.md
36
README.md
@ -61,6 +61,32 @@ python main.py --help
|
||||
|
||||
输出结果会显示所有可用的子命令和选项。
|
||||
|
||||
### Web界面(新增)
|
||||
|
||||
运行以下命令启动Web界面:
|
||||
|
||||
```bash
|
||||
python main.py web
|
||||
```
|
||||
|
||||
默认情况下,Web服务器将在`http://localhost:5000`上运行。您可以使用以下参数自定义服务器配置:
|
||||
|
||||
- `--host`: 指定服务器主机地址,默认为"0.0.0.0"(允许所有网络接口访问)
|
||||
- `--port`: 指定服务器端口,默认为5000
|
||||
- `--dev`: 启用开发模式(开启调试功能,自动重载代码)
|
||||
|
||||
例如:
|
||||
```bash
|
||||
python main.py web --host 127.0.0.1 --port 8080 --dev
|
||||
```
|
||||
|
||||
Web界面提供以下功能:
|
||||
- 通过滑动条调整基础权重(羁绊等级权重、羁绊数量权重、棋子费用权重)
|
||||
- 为每个具体的羁绊和棋子设置自定义权重
|
||||
- 指定必选羁绊和必选棋子
|
||||
- 一键生成最佳阵容并可视化显示
|
||||
- 支持筛选显示已激活的羁绊和棋子
|
||||
|
||||
### 数据提供模块演示
|
||||
|
||||
运行以下命令启动数据提供模块的演示程序:
|
||||
@ -160,6 +186,16 @@ TFT-Strategist/
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── api.py # 编程接口
|
||||
│ │ └── cli.py # 命令行界面
|
||||
│ ├── web/ # Web界面模块 (新增)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── app.py # Flask应用
|
||||
│ │ ├── static/ # 静态资源
|
||||
│ │ │ ├── css/ # CSS样式
|
||||
│ │ │ │ └── style.css
|
||||
│ │ │ └── js/ # JavaScript脚本
|
||||
│ │ │ └── main.js
|
||||
│ │ └── templates/ # HTML模板
|
||||
│ │ └── index.html
|
||||
│ ├── __init__.py
|
||||
│ ├── data_provider_demo.py # 数据提供模块演示
|
||||
│ ├── recommendation_demo.py # 阵容推荐模块演示
|
||||
|
16
main.py
16
main.py
@ -12,6 +12,7 @@ from src.recommendation_demo import main as recommendation_demo
|
||||
from src.interface.cli import main as cli_main
|
||||
from src.scoring.scoring_system import TeamScorer
|
||||
from src.test_scoring import main as test_scoring
|
||||
from src.web import run_server # 导入Web模块
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
@ -45,6 +46,12 @@ def main():
|
||||
scoring_parser = subparsers.add_parser("scoring", help="评分模块测试")
|
||||
scoring_parser.add_argument("--config", type=str, help="指定配置文件路径")
|
||||
|
||||
# Web界面 (新增)
|
||||
web_parser = subparsers.add_parser("web", help="启动Web界面")
|
||||
web_parser.add_argument("--host", type=str, default="0.0.0.0", help="服务器主机地址")
|
||||
web_parser.add_argument("--port", type=int, default=5000, help="服务器端口")
|
||||
web_parser.add_argument("--dev", action="store_true", help="开发模式")
|
||||
|
||||
# 解析命令行参数
|
||||
args = parser.parse_args()
|
||||
|
||||
@ -79,6 +86,15 @@ def main():
|
||||
# 否则使用默认配置文件
|
||||
test_scoring()
|
||||
|
||||
elif args.command == "web":
|
||||
logger.info("启动Web界面")
|
||||
# 调用Web服务器
|
||||
run_server(
|
||||
host=args.host,
|
||||
port=args.port,
|
||||
dev_mode=args.dev
|
||||
)
|
||||
|
||||
else:
|
||||
parser.print_help()
|
||||
|
||||
|
@ -3,4 +3,7 @@ pandas>=1.2.4
|
||||
pyyaml>=6.0
|
||||
pytest>=6.2.5
|
||||
numpy>=1.20.3
|
||||
tqdm>=4.62.3
|
||||
tqdm>=4.62.3
|
||||
flask>=2.0.1
|
||||
flask-cors>=3.0.10
|
||||
waitress>=2.1.2
|
@ -145,7 +145,9 @@ class DataQueryAPI:
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 职业数据,如果不存在则返回None
|
||||
"""
|
||||
return self._job_cache.get(job_id)
|
||||
# 确保job_id是字符串
|
||||
job_id_str = str(job_id)
|
||||
return self._job_cache.get(job_id_str)
|
||||
|
||||
def get_race_by_id(self, race_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
@ -157,7 +159,9 @@ class DataQueryAPI:
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 特质数据,如果不存在则返回None
|
||||
"""
|
||||
return self._race_cache.get(race_id)
|
||||
# 确保race_id是字符串
|
||||
race_id_str = str(race_id)
|
||||
return self._race_cache.get(race_id_str)
|
||||
|
||||
def get_chess_by_id(self, chess_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
@ -169,7 +173,9 @@ class DataQueryAPI:
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 棋子数据,如果不存在则返回None
|
||||
"""
|
||||
return self._chess_cache.get(chess_id)
|
||||
# 确保chess_id是字符串
|
||||
chess_id_str = str(chess_id)
|
||||
return self._chess_cache.get(chess_id_str)
|
||||
|
||||
def get_job_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
@ -319,13 +325,16 @@ class DataQueryAPI:
|
||||
Returns:
|
||||
Optional[Dict[str, Any]]: 羁绊数据,如果不存在则返回None
|
||||
"""
|
||||
# 确保synergy_id是字符串
|
||||
synergy_id_str = str(synergy_id)
|
||||
|
||||
# 先在职业中查找
|
||||
job = self.get_job_by_id(synergy_id)
|
||||
job = self.get_job_by_id(synergy_id_str)
|
||||
if job:
|
||||
return job
|
||||
|
||||
# 再在特质中查找
|
||||
return self.get_race_by_id(synergy_id)
|
||||
return self.get_race_by_id(synergy_id_str)
|
||||
|
||||
def get_synergy_by_name(self, name: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
@ -355,13 +364,16 @@ class DataQueryAPI:
|
||||
Returns:
|
||||
List[Dict[str, Any]]: 棋子数据列表
|
||||
"""
|
||||
# 确保synergy_id是字符串
|
||||
synergy_id_str = str(synergy_id)
|
||||
|
||||
# 先在职业中查找
|
||||
chess_list = self.get_chess_by_job(synergy_id)
|
||||
chess_list = self.get_chess_by_job(synergy_id_str)
|
||||
if chess_list:
|
||||
return chess_list
|
||||
|
||||
# 再在特质中查找
|
||||
return self.get_chess_by_race(synergy_id)
|
||||
return self.get_chess_by_race(synergy_id_str)
|
||||
|
||||
def get_synergies_of_chess(self, chess_id: str) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
@ -418,7 +430,9 @@ class DataQueryAPI:
|
||||
Returns:
|
||||
Dict[str, str]: 羁绊等级信息,键为等级,值为效果描述
|
||||
"""
|
||||
synergy = self.get_synergy_by_id(synergy_id)
|
||||
# 确保synergy_id是字符串
|
||||
synergy_id_str = str(synergy_id)
|
||||
synergy = self.get_synergy_by_id(synergy_id_str)
|
||||
if not synergy:
|
||||
return {}
|
||||
|
||||
|
@ -60,19 +60,22 @@ class TeamComposition:
|
||||
|
||||
# 确定各羁绊激活的等级
|
||||
for synergy_id, count in self.synergy_counts.items():
|
||||
synergy = api.get_synergy_by_id(synergy_id)
|
||||
# 确保synergy_id是字符串
|
||||
synergy_id_str = str(synergy_id)
|
||||
synergy = api.get_synergy_by_id(synergy_id_str)
|
||||
if not synergy:
|
||||
logger.warning(f"未找到羁绊: {{'id': {synergy_id}}}")
|
||||
continue
|
||||
|
||||
# 确定激活的等级
|
||||
levels = api.get_synergy_levels(synergy_id)
|
||||
levels = api.get_synergy_levels(synergy_id_str)
|
||||
active_levels = []
|
||||
|
||||
for level_str, effect in levels.items():
|
||||
level = int(level_str)
|
||||
if count >= level:
|
||||
active_level = {
|
||||
'id': synergy_id,
|
||||
'id': synergy_id_str,
|
||||
'name': synergy.get('name', ''),
|
||||
'level': level,
|
||||
'count': count,
|
||||
@ -149,7 +152,7 @@ class RecommendationEngine:
|
||||
if isinstance(chess_info, dict) and 'name' in chess_info:
|
||||
chess = self.api.get_chess_by_name(chess_info['name'])
|
||||
elif isinstance(chess_info, dict) and 'id' in chess_info:
|
||||
chess = self.api.get_chess_by_id(chess_info['id'])
|
||||
chess = self.api.get_chess_by_id(str(chess_info['id']))
|
||||
else:
|
||||
chess = self.api.get_chess_by_name(str(chess_info))
|
||||
|
||||
@ -172,7 +175,7 @@ class RecommendationEngine:
|
||||
synergy = self.api.get_synergy_by_name(synergy_info['name'])
|
||||
min_level = synergy_info.get('level', 1)
|
||||
elif isinstance(synergy_info, dict) and 'id' in synergy_info:
|
||||
synergy = self.api.get_synergy_by_id(synergy_info['id'])
|
||||
synergy = self.api.get_synergy_by_id(str(synergy_info['id']))
|
||||
min_level = synergy_info.get('level', 1)
|
||||
else:
|
||||
synergy = self.api.get_synergy_by_name(str(synergy_info))
|
||||
|
8
src/web/__init__.py
Normal file
8
src/web/__init__.py
Normal file
@ -0,0 +1,8 @@
|
||||
"""
|
||||
云顶之弈阵容推荐器 - Web模块
|
||||
提供Web界面,允许用户通过浏览器调整权重进行阵容推荐
|
||||
"""
|
||||
|
||||
from src.web.app import create_app, run_server
|
||||
|
||||
__all__ = ['create_app', 'run_server']
|
168
src/web/app.py
Normal file
168
src/web/app.py
Normal file
@ -0,0 +1,168 @@
|
||||
"""
|
||||
云顶之弈阵容推荐器 - Web应用
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
from flask_cors import CORS
|
||||
from waitress import serve
|
||||
|
||||
from src.data_provider import DataQueryAPI
|
||||
from src.recommendation import RecommendationEngine
|
||||
from src.scoring.scoring_system import TeamScorer, ScoringConfig
|
||||
from src.scoring.config_loader import ConfigLoader
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
logger = logging.getLogger("TFT-Strategist-Web")
|
||||
|
||||
def create_app():
|
||||
"""创建Flask应用实例"""
|
||||
app = Flask(__name__,
|
||||
static_folder='static',
|
||||
template_folder='templates')
|
||||
|
||||
# 启用CORS,允许跨域请求
|
||||
CORS(app)
|
||||
|
||||
# 加载数据
|
||||
data_api = DataQueryAPI()
|
||||
engine = RecommendationEngine(api=data_api)
|
||||
config_loader = ConfigLoader()
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
"""首页"""
|
||||
# 获取所有羁绊和棋子信息,用于前端展示
|
||||
all_jobs = data_api.get_all_jobs()
|
||||
all_races = data_api.get_all_races()
|
||||
all_synergies = all_jobs + all_races
|
||||
all_chess = data_api.get_all_chess()
|
||||
|
||||
# 获取配置文件中的权重信息
|
||||
weights_config = config_loader.load_config()
|
||||
|
||||
return render_template('index.html',
|
||||
synergies=all_synergies,
|
||||
chess=all_chess,
|
||||
base_weights=weights_config.get('base_weights', {}),
|
||||
synergy_weights=weights_config.get('synergy_weights', {}),
|
||||
chess_weights=weights_config.get('chess_weights', {}))
|
||||
|
||||
@app.route('/api/recommend', methods=['POST'])
|
||||
def recommend():
|
||||
"""阵容推荐API"""
|
||||
try:
|
||||
# 获取用户提交的参数
|
||||
data = request.json
|
||||
population = data.get('population', 9)
|
||||
num_results = data.get('num_results', 3)
|
||||
|
||||
# 获取用户配置的权重
|
||||
base_weights = data.get('base_weights', {})
|
||||
synergy_weights = data.get('synergy_weights', {})
|
||||
chess_weights = data.get('chess_weights', {})
|
||||
|
||||
# 获取必选棋子和必选羁绊
|
||||
required_chess_ids = data.get('required_chess', [])
|
||||
required_synergy_ids = data.get('required_synergies', [])
|
||||
|
||||
# 将ID转换为字典格式,确保将ID转换为字符串
|
||||
required_chess = [{'id': str(chess_id)} for chess_id in required_chess_ids]
|
||||
required_synergies = [{'id': str(synergy_id)} for synergy_id in required_synergy_ids]
|
||||
|
||||
# 创建评分配置
|
||||
scoring_config = ScoringConfig(
|
||||
synergy_level_weight=base_weights.get('synergy_level_weight', 1.0),
|
||||
synergy_count_weight=base_weights.get('synergy_count_weight', 0.5),
|
||||
chess_cost_weight=base_weights.get('chess_cost_weight', 0.1),
|
||||
synergy_weights=synergy_weights,
|
||||
chess_weights=chess_weights
|
||||
)
|
||||
|
||||
# 创建评分系统
|
||||
scorer = TeamScorer(config=scoring_config)
|
||||
|
||||
# 调用推荐引擎生成阵容
|
||||
teams = engine.recommend_team(
|
||||
population=population,
|
||||
required_synergies=required_synergies,
|
||||
required_chess=required_chess,
|
||||
max_results=num_results
|
||||
)
|
||||
|
||||
# 将结果转换为JSON
|
||||
results = []
|
||||
for team in teams:
|
||||
# 获取阵容信息
|
||||
chess_list = []
|
||||
for chess_obj in team.chess_list:
|
||||
chess_id = chess_obj.get('chessId')
|
||||
if chess_id:
|
||||
chess_list.append({
|
||||
'id': chess_id,
|
||||
'name': chess_obj.get('displayName', ''),
|
||||
'cost': chess_obj.get('price', 0),
|
||||
'image': f"chess_{chess_id}.png" # 假设有对应图片
|
||||
})
|
||||
|
||||
# 获取激活的羁绊
|
||||
active_synergies = {}
|
||||
|
||||
# 合并job和race的激活羁绊
|
||||
for synergy_type in ['job', 'race']:
|
||||
for synergy_info in team.synergy_levels.get(synergy_type, []):
|
||||
synergy_id = synergy_info.get('id')
|
||||
if synergy_id:
|
||||
# 取最高等级
|
||||
if synergy_id not in active_synergies or active_synergies[synergy_id] < synergy_info.get('level', 0):
|
||||
active_synergies[synergy_id] = synergy_info.get('level', 0)
|
||||
|
||||
# 转换为前端需要的格式
|
||||
active_synergies_list = []
|
||||
for synergy_id, level in active_synergies.items():
|
||||
synergy_info = data_api.get_synergy_by_id(synergy_id)
|
||||
if synergy_info:
|
||||
active_synergies_list.append({
|
||||
'id': synergy_id,
|
||||
'name': synergy_info.get('name', ''),
|
||||
'level': level,
|
||||
'description': synergy_info.get('description', ''),
|
||||
'image': f"synergy_{synergy_id}.png" # 假设有对应图片
|
||||
})
|
||||
|
||||
results.append({
|
||||
'chess_list': chess_list,
|
||||
'active_synergies': active_synergies_list,
|
||||
'score': team.score
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'results': results
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("推荐过程中发生错误")
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}), 500
|
||||
|
||||
return app
|
||||
|
||||
def run_server(host='0.0.0.0', port=5000, dev_mode=False):
|
||||
"""运行Web服务器"""
|
||||
app = create_app()
|
||||
logger.info(f"启动Web服务器: {'开发模式' if dev_mode else '生产模式'}")
|
||||
|
||||
if dev_mode:
|
||||
# 开发模式,使用Flask内置服务器
|
||||
app.run(host=host, port=port, debug=True)
|
||||
else:
|
||||
# 生产模式,使用waitress
|
||||
serve(app, host=host, port=port)
|
77
src/web/static/css/style.css
Normal file
77
src/web/static/css/style.css
Normal file
@ -0,0 +1,77 @@
|
||||
/* 云顶之弈阵容推荐器样式 */
|
||||
|
||||
/* 滑块样式 */
|
||||
.weight-slider {
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
margin-top: 5px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.noUi-connect {
|
||||
background: #4f46e5; /* Indigo-600 */
|
||||
}
|
||||
|
||||
.noUi-handle {
|
||||
height: 16px !important;
|
||||
width: 16px !important;
|
||||
border-radius: 50%;
|
||||
background: #4f46e5;
|
||||
box-shadow: none;
|
||||
border: 2px solid #ffffff;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.noUi-handle:before,
|
||||
.noUi-handle:after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 棋子卡片样式 */
|
||||
.chess-card {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 0.375rem;
|
||||
background-color: #f9fafb;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.chess-card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 羁绊标签样式 */
|
||||
.synergy-tag {
|
||||
background-color: #e0e7ff;
|
||||
color: #4338ca;
|
||||
border-radius: 9999px;
|
||||
padding: 0.25rem 0.75rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 棋子标识 */
|
||||
.chess-cost-1 { border-left: 3px solid #94a3b8; } /* 1费 - 灰色 */
|
||||
.chess-cost-2 { border-left: 3px solid #65a30d; } /* 2费 - 绿色 */
|
||||
.chess-cost-3 { border-left: 3px solid #2563eb; } /* 3费 - 蓝色 */
|
||||
.chess-cost-4 { border-left: 3px solid #7e22ce; } /* 4费 - 紫色 */
|
||||
.chess-cost-5 { border-left: 3px solid #f59e0b; } /* 5费 - 金色 */
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
.team-result {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* 响应式调整 */
|
||||
@media (max-width: 768px) {
|
||||
.chess-list {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
414
src/web/static/js/main.js
Normal file
414
src/web/static/js/main.js
Normal file
@ -0,0 +1,414 @@
|
||||
// 云顶之弈阵容推荐器 - 前端脚本
|
||||
|
||||
// 页面加载完成后执行
|
||||
$(document).ready(function() {
|
||||
// 初始化基础权重滑块
|
||||
initBasicWeightSliders();
|
||||
|
||||
// 初始化羁绊权重滑块
|
||||
initSynergyWeightSliders();
|
||||
|
||||
// 初始化棋子权重滑块
|
||||
initChessWeightSliders();
|
||||
|
||||
// 监听人口数量变化
|
||||
$('#population').on('input', function() {
|
||||
$('#population-value').text($(this).val());
|
||||
});
|
||||
|
||||
// 监听推荐结果数量变化
|
||||
$('#num-results').on('input', function() {
|
||||
$('#num-results-value').text($(this).val());
|
||||
});
|
||||
|
||||
// 添加必要羁绊
|
||||
$('#add-required-synergy').on('click', function() {
|
||||
const synergySelect = $('#required-synergy-select');
|
||||
const synergyId = synergySelect.val();
|
||||
const synergyName = synergySelect.find('option:selected').text();
|
||||
|
||||
if (!synergyId) return;
|
||||
|
||||
// 检查是否已添加
|
||||
if ($(`#required-synergy-${synergyId}`).length > 0) return;
|
||||
|
||||
// 添加到列表
|
||||
const synergyItem = $(`
|
||||
<div id="required-synergy-${synergyId}" class="flex items-center justify-between bg-indigo-50 p-2 rounded" data-synergy-id="${synergyId}">
|
||||
<span class="text-gray-800">${synergyName}</span>
|
||||
<button class="remove-synergy text-red-500 hover:text-red-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$('#required-synergies-list').append(synergyItem);
|
||||
|
||||
// 重置选择框
|
||||
synergySelect.val('');
|
||||
});
|
||||
|
||||
// 移除必要羁绊
|
||||
$(document).on('click', '.remove-synergy', function() {
|
||||
$(this).closest('div').remove();
|
||||
});
|
||||
|
||||
// 添加必选棋子
|
||||
$('#add-required-chess').on('click', function() {
|
||||
const chessSelect = $('#required-chess-select');
|
||||
const chessId = chessSelect.val();
|
||||
const chessName = chessSelect.find('option:selected').text();
|
||||
|
||||
if (!chessId) return;
|
||||
|
||||
// 检查是否已添加
|
||||
if ($(`#required-chess-${chessId}`).length > 0) return;
|
||||
|
||||
// 添加到列表
|
||||
const chessItem = $(`
|
||||
<div id="required-chess-${chessId}" class="flex items-center justify-between bg-indigo-50 p-2 rounded" data-chess-id="${chessId}">
|
||||
<span class="text-gray-800">${chessName}</span>
|
||||
<button class="remove-chess text-red-500 hover:text-red-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
`);
|
||||
|
||||
$('#required-chess-list').append(chessItem);
|
||||
|
||||
// 重置选择框
|
||||
chessSelect.val('');
|
||||
});
|
||||
|
||||
// 移除必选棋子
|
||||
$(document).on('click', '.remove-chess', function() {
|
||||
$(this).closest('div').remove();
|
||||
});
|
||||
|
||||
// 生成阵容
|
||||
$('#generate-btn').on('click', function() {
|
||||
generateTeam();
|
||||
});
|
||||
|
||||
// 仅显示已激活羁绊
|
||||
$('#show-only-active').on('change', function() {
|
||||
if ($(this).is(':checked')) {
|
||||
// 隐藏所有羁绊权重项
|
||||
$('.synergy-weight-item').hide();
|
||||
// 显示已激活羁绊项
|
||||
$('.synergy-weight-item.active').show();
|
||||
} else {
|
||||
// 显示所有羁绊权重项
|
||||
$('.synergy-weight-item').show();
|
||||
}
|
||||
});
|
||||
|
||||
// 仅显示已激活棋子
|
||||
$('#show-only-active-chess').on('change', function() {
|
||||
if ($(this).is(':checked')) {
|
||||
// 隐藏所有棋子权重项
|
||||
$('.chess-weight-item').hide();
|
||||
// 显示已激活棋子项
|
||||
$('.chess-weight-item.active').show();
|
||||
} else {
|
||||
// 显示所有棋子权重项
|
||||
$('.chess-weight-item').show();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 初始化基础权重滑块
|
||||
*/
|
||||
function initBasicWeightSliders() {
|
||||
// 羁绊等级权重
|
||||
const synergyLevelSlider = document.getElementById('synergy-level-weight-slider');
|
||||
noUiSlider.create(synergyLevelSlider, {
|
||||
start: [1.0],
|
||||
connect: [true, false],
|
||||
step: 0.1,
|
||||
range: {
|
||||
'min': [0.0],
|
||||
'max': [3.0]
|
||||
}
|
||||
});
|
||||
|
||||
// 更新羁绊等级权重值
|
||||
synergyLevelSlider.noUiSlider.on('update', function(values, handle) {
|
||||
$('#synergy-level-weight-value').text(parseFloat(values[handle]).toFixed(1));
|
||||
});
|
||||
|
||||
// 羁绊数量权重
|
||||
const synergyCountSlider = document.getElementById('synergy-count-weight-slider');
|
||||
noUiSlider.create(synergyCountSlider, {
|
||||
start: [0.5],
|
||||
connect: [true, false],
|
||||
step: 0.1,
|
||||
range: {
|
||||
'min': [0.0],
|
||||
'max': [3.0]
|
||||
}
|
||||
});
|
||||
|
||||
// 更新羁绊数量权重值
|
||||
synergyCountSlider.noUiSlider.on('update', function(values, handle) {
|
||||
$('#synergy-count-weight-value').text(parseFloat(values[handle]).toFixed(1));
|
||||
});
|
||||
|
||||
// 棋子费用权重
|
||||
const chessCountSlider = document.getElementById('chess-cost-weight-slider');
|
||||
noUiSlider.create(chessCountSlider, {
|
||||
start: [0.1],
|
||||
connect: [true, false],
|
||||
step: 0.1,
|
||||
range: {
|
||||
'min': [0.0],
|
||||
'max': [1.0]
|
||||
}
|
||||
});
|
||||
|
||||
// 更新棋子费用权重值
|
||||
chessCountSlider.noUiSlider.on('update', function(values, handle) {
|
||||
$('#chess-cost-weight-value').text(parseFloat(values[handle]).toFixed(1));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化羁绊权重滑块
|
||||
*/
|
||||
function initSynergyWeightSliders() {
|
||||
$('.synergy-weight-item').each(function() {
|
||||
const slider = $(this).find('.synergy-weight-slider')[0];
|
||||
const valueElement = $(this).find('.synergy-weight-value');
|
||||
const initialValue = parseFloat(valueElement.text());
|
||||
|
||||
noUiSlider.create(slider, {
|
||||
start: [initialValue],
|
||||
connect: [true, false],
|
||||
step: 0.1,
|
||||
range: {
|
||||
'min': [0.0],
|
||||
'max': [3.0]
|
||||
}
|
||||
});
|
||||
|
||||
// 更新权重值
|
||||
slider.noUiSlider.on('update', function(values, handle) {
|
||||
valueElement.text(parseFloat(values[handle]).toFixed(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化棋子权重滑块
|
||||
*/
|
||||
function initChessWeightSliders() {
|
||||
$('.chess-weight-item').each(function() {
|
||||
const slider = $(this).find('.chess-weight-slider')[0];
|
||||
const valueElement = $(this).find('.chess-weight-value');
|
||||
const initialValue = parseFloat(valueElement.text());
|
||||
|
||||
noUiSlider.create(slider, {
|
||||
start: [initialValue],
|
||||
connect: [true, false],
|
||||
step: 0.1,
|
||||
range: {
|
||||
'min': [0.0],
|
||||
'max': [3.0]
|
||||
}
|
||||
});
|
||||
|
||||
// 更新权重值
|
||||
slider.noUiSlider.on('update', function(values, handle) {
|
||||
valueElement.text(parseFloat(values[handle]).toFixed(1));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成阵容
|
||||
*/
|
||||
function generateTeam() {
|
||||
// 显示加载状态
|
||||
$('#loading').removeClass('hidden');
|
||||
$('#results-container').empty();
|
||||
|
||||
// 获取基础参数
|
||||
const population = parseInt($('#population').val());
|
||||
const numResults = parseInt($('#num-results').val());
|
||||
|
||||
// 获取基础权重
|
||||
const synergyLevelWeight = parseFloat($('#synergy-level-weight-value').text());
|
||||
const synergyCountWeight = parseFloat($('#synergy-count-weight-value').text());
|
||||
const chessCountWeight = parseFloat($('#chess-cost-weight-value').text());
|
||||
|
||||
// 获取羁绊权重
|
||||
const synergyWeights = {};
|
||||
$('.synergy-weight-item').each(function() {
|
||||
const synergyId = $(this).data('synergy-id');
|
||||
const weight = parseFloat($(this).find('.synergy-weight-value').text());
|
||||
synergyWeights[synergyId] = weight;
|
||||
});
|
||||
|
||||
// 获取棋子权重
|
||||
const chessWeights = {};
|
||||
$('.chess-weight-item').each(function() {
|
||||
const chessId = $(this).data('chess-id');
|
||||
const weight = parseFloat($(this).find('.chess-weight-value').text());
|
||||
chessWeights[chessId] = weight;
|
||||
});
|
||||
|
||||
// 获取必选羁绊
|
||||
const requiredSynergies = [];
|
||||
$('#required-synergies-list > div').each(function() {
|
||||
requiredSynergies.push($(this).data('synergy-id'));
|
||||
});
|
||||
|
||||
// 获取必选棋子
|
||||
const requiredChess = [];
|
||||
$('#required-chess-list > div').each(function() {
|
||||
requiredChess.push($(this).data('chess-id'));
|
||||
});
|
||||
|
||||
// 构建请求体
|
||||
const requestData = {
|
||||
population: population,
|
||||
num_results: numResults,
|
||||
base_weights: {
|
||||
synergy_level_weight: synergyLevelWeight,
|
||||
synergy_count_weight: synergyCountWeight,
|
||||
chess_cost_weight: chessCountWeight
|
||||
},
|
||||
synergy_weights: synergyWeights,
|
||||
chess_weights: chessWeights,
|
||||
required_synergies: requiredSynergies,
|
||||
required_chess: requiredChess
|
||||
};
|
||||
|
||||
// 发送请求
|
||||
$.ajax({
|
||||
url: '/api/recommend',
|
||||
type: 'POST',
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(requestData),
|
||||
success: function(response) {
|
||||
// 隐藏加载状态
|
||||
$('#loading').addClass('hidden');
|
||||
|
||||
if (response.status === 'success') {
|
||||
// 渲染结果
|
||||
renderResults(response.results);
|
||||
|
||||
// 更新羁绊和棋子激活状态
|
||||
updateActiveStatus(response.results);
|
||||
} else {
|
||||
// 显示错误信息
|
||||
$('#results-container').html(`<div class="text-red-600 p-4">生成阵容失败: ${response.message}</div>`);
|
||||
}
|
||||
},
|
||||
error: function(xhr, status, error) {
|
||||
// 隐藏加载状态
|
||||
$('#loading').addClass('hidden');
|
||||
|
||||
// 显示错误信息
|
||||
$('#results-container').html(`<div class="text-red-600 p-4">请求失败: ${error}</div>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染阵容结果
|
||||
* @param {Array} results 阵容结果列表
|
||||
*/
|
||||
function renderResults(results) {
|
||||
const container = $('#results-container');
|
||||
container.empty();
|
||||
|
||||
results.forEach((result, index) => {
|
||||
// 克隆结果模板
|
||||
const template = document.getElementById('team-result-template');
|
||||
const teamElement = $(template.content.cloneNode(true));
|
||||
|
||||
// 设置阵容编号和评分
|
||||
teamElement.find('.team-number').text(index + 1);
|
||||
teamElement.find('.team-score').text(result.score.toFixed(1));
|
||||
|
||||
// 渲染棋子列表
|
||||
const chessList = teamElement.find('.chess-list');
|
||||
result.chess_list.forEach(chess => {
|
||||
const chessCard = $(`
|
||||
<div class="chess-card p-2 chess-cost-${chess.cost}">
|
||||
<div class="font-medium text-sm">${chess.name}</div>
|
||||
<div class="text-xs text-gray-500">${chess.cost}费</div>
|
||||
</div>
|
||||
`);
|
||||
chessList.append(chessCard);
|
||||
});
|
||||
|
||||
// 渲染羁绊列表
|
||||
const synergyList = teamElement.find('.synergy-list');
|
||||
result.active_synergies.forEach(synergy => {
|
||||
const synergyTag = $(`
|
||||
<div class="synergy-tag">
|
||||
<span>${synergy.name}</span>
|
||||
<span class="ml-1 text-indigo-700">(${synergy.level})</span>
|
||||
</div>
|
||||
`);
|
||||
synergyList.append(synergyTag);
|
||||
});
|
||||
|
||||
// 添加到结果容器
|
||||
container.append(teamElement);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新羁绊和棋子的激活状态
|
||||
* @param {Array} results 阵容结果列表
|
||||
*/
|
||||
function updateActiveStatus(results) {
|
||||
// 重置激活状态
|
||||
$('.synergy-weight-item').removeClass('active');
|
||||
$('.chess-weight-item').removeClass('active');
|
||||
|
||||
// 收集所有被激活的羁绊和棋子
|
||||
const activeSynergies = new Set();
|
||||
const activeChess = new Set();
|
||||
|
||||
results.forEach(result => {
|
||||
// 添加激活的羁绊
|
||||
result.active_synergies.forEach(synergy => {
|
||||
activeSynergies.add(synergy.id);
|
||||
});
|
||||
|
||||
// 添加激活的棋子
|
||||
result.chess_list.forEach(chess => {
|
||||
activeChess.add(chess.id);
|
||||
});
|
||||
});
|
||||
|
||||
// 设置羁绊激活状态
|
||||
activeSynergies.forEach(synergyId => {
|
||||
$(`.synergy-weight-item[data-synergy-id="${synergyId}"]`).addClass('active');
|
||||
});
|
||||
|
||||
// 设置棋子激活状态
|
||||
activeChess.forEach(chessId => {
|
||||
$(`.chess-weight-item[data-chess-id="${chessId}"]`).addClass('active');
|
||||
});
|
||||
|
||||
// 如果勾选了"仅显示已激活",则更新显示状态
|
||||
if ($('#show-only-active').is(':checked')) {
|
||||
$('.synergy-weight-item').hide();
|
||||
$('.synergy-weight-item.active').show();
|
||||
}
|
||||
|
||||
if ($('#show-only-active-chess').is(':checked')) {
|
||||
$('.chess-weight-item').hide();
|
||||
$('.chess-weight-item.active').show();
|
||||
}
|
||||
}
|
194
src/web/templates/index.html
Normal file
194
src/web/templates/index.html
Normal file
@ -0,0 +1,194 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>云顶之弈阵容推荐器</title>
|
||||
<!-- 使用TailwindCSS CDN -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<!-- jQuery -->
|
||||
<script src="https://unpkg.com/jquery@3.6.0/dist/jquery.min.js"></script>
|
||||
<!-- noUiSlider和其依赖 -->
|
||||
<link rel="stylesheet" href="https://unpkg.com/nouislider@15.5.1/dist/nouislider.min.css">
|
||||
<script src="https://unpkg.com/nouislider@15.5.1/dist/nouislider.min.js"></script>
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
|
||||
</head>
|
||||
<body class="bg-gray-100 min-h-screen">
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<h1 class="text-3xl font-bold text-center text-indigo-600 mb-8">云顶之弈阵容推荐器</h1>
|
||||
|
||||
<!-- 主要内容区 -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- 左侧权重设置区 -->
|
||||
<div class="lg:col-span-1 bg-white rounded-lg shadow-md p-6">
|
||||
<h2 class="text-xl font-semibold mb-4 text-indigo-700">基础设置</h2>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 mb-2">人口数量</label>
|
||||
<div class="flex items-center">
|
||||
<input type="range" id="population" min="1" max="10" value="9"
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<span id="population-value" class="ml-2 text-gray-700">9</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 mb-2">推荐结果数量</label>
|
||||
<div class="flex items-center">
|
||||
<input type="range" id="num-results" min="1" max="10" value="3"
|
||||
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer">
|
||||
<span id="num-results-value" class="ml-2 text-gray-700">3</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold mb-4 mt-6 text-indigo-700">基础权重</h2>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 mb-2">羁绊等级权重</label>
|
||||
<div id="synergy-level-weight-slider" class="weight-slider"></div>
|
||||
<div class="flex justify-between mt-1">
|
||||
<span class="text-xs text-gray-500">0.0</span>
|
||||
<span id="synergy-level-weight-value" class="text-xs text-gray-700">1.0</span>
|
||||
<span class="text-xs text-gray-500">3.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-700 mb-2">羁绊数量权重</label>
|
||||
<div id="synergy-count-weight-slider" class="weight-slider"></div>
|
||||
<div class="flex justify-between mt-1">
|
||||
<span class="text-xs text-gray-500">0.0</span>
|
||||
<span id="synergy-count-weight-value" class="text-xs text-gray-700">0.5</span>
|
||||
<span class="text-xs text-gray-500">3.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<label class="block text-gray-700 mb-2">棋子费用权重</label>
|
||||
<div id="chess-cost-weight-slider" class="weight-slider"></div>
|
||||
<div class="flex justify-between mt-1">
|
||||
<span class="text-xs text-gray-500">0.0</span>
|
||||
<span id="chess-cost-weight-value" class="text-xs text-gray-700">0.1</span>
|
||||
<span class="text-xs text-gray-500">1.0</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 必要羁绊设置 -->
|
||||
<h2 class="text-xl font-semibold mb-4 mt-6 text-indigo-700">必要羁绊设置</h2>
|
||||
<div class="mb-6">
|
||||
<select id="required-synergy-select" class="w-full p-2 border border-gray-300 rounded mb-2">
|
||||
<option value="">-- 选择羁绊 --</option>
|
||||
{% for synergy in synergies %}
|
||||
<option value="{{ synergy.jobId or synergy.raceId }}">{{ synergy.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="add-required-synergy" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700">
|
||||
添加必要羁绊
|
||||
</button>
|
||||
<div id="required-synergies-list" class="mt-2 space-y-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- 必选棋子设置 -->
|
||||
<h2 class="text-xl font-semibold mb-4 mt-6 text-indigo-700">必选棋子设置</h2>
|
||||
<div class="mb-6">
|
||||
<select id="required-chess-select" class="w-full p-2 border border-gray-300 rounded mb-2">
|
||||
<option value="">-- 选择棋子 --</option>
|
||||
{% for c in chess %}
|
||||
<option value="{{ c.chessId }}">{{ c.displayName }} ({{ c.price }}费)</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="add-required-chess" class="bg-indigo-600 text-white px-4 py-2 rounded hover:bg-indigo-700">
|
||||
添加必选棋子
|
||||
</button>
|
||||
<div id="required-chess-list" class="mt-2 space-y-2"></div>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold mb-4 mt-6 text-indigo-700">生成阵容</h2>
|
||||
<button id="generate-btn" class="w-full bg-indigo-600 text-white px-4 py-3 rounded-lg hover:bg-indigo-700 transition-colors">
|
||||
生成最佳阵容
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 中间自定义权重设置区 -->
|
||||
<div class="lg:col-span-1 bg-white rounded-lg shadow-md p-6 h-[800px] overflow-y-auto">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h2 class="text-xl font-semibold text-indigo-700">羁绊权重设置</h2>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="show-only-active" class="mr-2">
|
||||
<label for="show-only-active" class="text-sm text-gray-700">仅显示已激活</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="synergy-weights-container" class="space-y-4">
|
||||
{% for synergy_id, weight in synergy_weights.items() %}
|
||||
<div class="synergy-weight-item" data-synergy-id="{{ synergy_id }}">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="text-gray-700">{{ synergy_id }}</label>
|
||||
<span class="synergy-weight-value text-xs text-gray-700">{{ weight }}</span>
|
||||
</div>
|
||||
<div class="synergy-weight-slider"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-8 mb-4">
|
||||
<h2 class="text-xl font-semibold text-indigo-700">棋子权重设置</h2>
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="show-only-active-chess" class="mr-2">
|
||||
<label for="show-only-active-chess" class="text-sm text-gray-700">仅显示已激活</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="chess-weights-container" class="space-y-4">
|
||||
{% for chess_id, weight in chess_weights.items() %}
|
||||
<div class="chess-weight-item" data-chess-id="{{ chess_id }}">
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="text-gray-700">{{ chess_id }}</label>
|
||||
<span class="chess-weight-value text-xs text-gray-700">{{ weight }}</span>
|
||||
</div>
|
||||
<div class="chess-weight-slider"></div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧结果展示区 -->
|
||||
<div class="lg:col-span-1 bg-white rounded-lg shadow-md p-6 h-[800px] overflow-y-auto">
|
||||
<h2 class="text-xl font-semibold mb-4 text-indigo-700">阵容推荐结果</h2>
|
||||
<div id="loading" class="hidden">
|
||||
<div class="flex justify-center items-center h-60">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-t-2 border-b-2 border-indigo-500"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="results-container" class="space-y-8"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 结果模板 -->
|
||||
<template id="team-result-template">
|
||||
<div class="team-result p-4 bg-gray-50 rounded-lg">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h3 class="text-lg font-semibold text-gray-800">阵容 #<span class="team-number"></span></h3>
|
||||
<span class="text-sm bg-indigo-100 text-indigo-800 px-2 py-1 rounded">
|
||||
评分: <span class="team-score"></span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<h4 class="text-md font-medium text-gray-700 mb-2">棋子列表</h4>
|
||||
<div class="chess-list grid grid-cols-3 sm:grid-cols-3 md:grid-cols-3 gap-2"></div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-md font-medium text-gray-700 mb-2">激活羁绊</h4>
|
||||
<div class="synergy-list space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- JavaScript -->
|
||||
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user