feat: Add image generation for recent matches report
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
a2de839a6d
commit
2b70813fb3
17
dota.py
17
dota.py
@ -176,6 +176,23 @@ class Friend(BaseModel):
|
|||||||
'timestamp': end_time,
|
'timestamp': end_time,
|
||||||
'url': f"https://www.opendota.com/matches/{match_['match_id']}",
|
'url': f"https://www.opendota.com/matches/{match_['match_id']}",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# 生成图片报告
|
||||||
|
try:
|
||||||
|
image_url = image_generator.generate_recent_matches_image(name, matches[:limit])
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成最近比赛报告图片失败: {str(e)}")
|
||||||
|
image_url = None
|
||||||
|
|
||||||
|
# 如果成功生成了图片,添加到第一个embed中
|
||||||
|
if image_url:
|
||||||
|
data['embeds'].append({
|
||||||
|
'image': {
|
||||||
|
'url': image_url
|
||||||
|
},
|
||||||
|
'color': 3447003 # 蓝色
|
||||||
|
})
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ from jinja2 import Environment, FileSystemLoader
|
|||||||
import utils
|
import utils
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
|
||||||
class ImageGenerator:
|
class ImageGenerator:
|
||||||
@ -133,3 +134,83 @@ class ImageGenerator:
|
|||||||
os.remove(image_path)
|
os.remove(image_path)
|
||||||
return image_url
|
return image_url
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def generate_recent_matches_image(self, player_name, matches):
|
||||||
|
"""
|
||||||
|
生成玩家最近比赛的图片报告
|
||||||
|
|
||||||
|
Args:
|
||||||
|
player_name: 玩家名称
|
||||||
|
matches: 比赛数据列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 上传后的图片URL
|
||||||
|
"""
|
||||||
|
# 处理比赛数据
|
||||||
|
processed_matches = []
|
||||||
|
|
||||||
|
for match in matches:
|
||||||
|
# 获取英雄图片
|
||||||
|
hero_id = str(match['hero_id'])
|
||||||
|
hero_img = f"https://cdn.dota2.com/apps/dota2/images/heroes/{self.heroes_data[hero_id]['name'].replace('npc_dota_hero_', '')}_full.png"
|
||||||
|
|
||||||
|
# 格式化时间
|
||||||
|
end_time = datetime.datetime.fromtimestamp(match['end_time']).strftime('%Y-%m-%d %H:%M')
|
||||||
|
|
||||||
|
# 格式化时长
|
||||||
|
duration_hms = utils.convert_seconds_to_hms(match['duration'])
|
||||||
|
duration_formatted = f"{duration_hms[0]}:{duration_hms[1]:02d}:{duration_hms[2]:02d}"
|
||||||
|
|
||||||
|
# 获取英雄中文名
|
||||||
|
hero_name = utils.get_hero_chinese_name(self.heroes_data[hero_id]['name'])
|
||||||
|
|
||||||
|
processed_match = {
|
||||||
|
'win': match['win'],
|
||||||
|
'kills': match['kills'],
|
||||||
|
'deaths': match['deaths'],
|
||||||
|
'assists': match['assists'],
|
||||||
|
'hero_img': hero_img,
|
||||||
|
'hero_name': hero_name,
|
||||||
|
'duration_formatted': duration_formatted,
|
||||||
|
'end_time_formatted': end_time,
|
||||||
|
'party_size': match['party_size'],
|
||||||
|
'average_rank': match['average_rank']
|
||||||
|
}
|
||||||
|
|
||||||
|
processed_matches.append(processed_match)
|
||||||
|
|
||||||
|
# 渲染模板
|
||||||
|
template = self.env.get_template('recent_matches.html')
|
||||||
|
html_content = template.render(
|
||||||
|
player_name=player_name,
|
||||||
|
matches=processed_matches
|
||||||
|
)
|
||||||
|
|
||||||
|
# 使用Playwright生成图片
|
||||||
|
with sync_playwright() as playwright:
|
||||||
|
browser = playwright.chromium.launch()
|
||||||
|
page = browser.new_page()
|
||||||
|
page.set_content(html_content)
|
||||||
|
page.set_viewport_size({"width": 800, "height": 800})
|
||||||
|
|
||||||
|
# 等待内容完全加载
|
||||||
|
page.wait_for_timeout(1000)
|
||||||
|
|
||||||
|
# 调整截图高度以适应内容
|
||||||
|
body_height = page.evaluate('document.body.scrollHeight')
|
||||||
|
body_width = page.evaluate('document.body.offsetWidth')
|
||||||
|
page.set_viewport_size({"width": body_width, "height": body_height})
|
||||||
|
|
||||||
|
# 截图
|
||||||
|
image_path = f"recent_matches_{player_name.replace(' ', '_')}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}.png"
|
||||||
|
page.screenshot(path=image_path, full_page=True)
|
||||||
|
browser.close()
|
||||||
|
|
||||||
|
# 上传图片
|
||||||
|
image_url = utils.upload_image(image_path)
|
||||||
|
|
||||||
|
# 删除本地文件
|
||||||
|
os.remove(image_path)
|
||||||
|
|
||||||
|
return image_url
|
||||||
|
|
||||||
BIN
match_report.png
BIN
match_report.png
Binary file not shown.
|
Before Width: | Height: | Size: 91 KiB |
171
templates/recent_matches.html
Normal file
171
templates/recent_matches.html
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<style>
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
width: auto;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: #1a1a1a;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: "Segoe UI", Arial, sans-serif;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 15px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
width: calc(100% - 24px);
|
||||||
|
}
|
||||||
|
.header-title {
|
||||||
|
font-size: 1.3em;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.matches-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.match-card {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
/* 胜利/失败指示条 */
|
||||||
|
.match-card::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 5px;
|
||||||
|
}
|
||||||
|
.match-card.win::before {
|
||||||
|
background: linear-gradient(to right, #4caf50, #8bc34a);
|
||||||
|
}
|
||||||
|
.match-card.lose::before {
|
||||||
|
background: linear-gradient(to right, #f44336, #ff9800);
|
||||||
|
}
|
||||||
|
.match-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.match-time {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #aaaaaa;
|
||||||
|
}
|
||||||
|
.match-details {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.hero-img {
|
||||||
|
width: 60px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin-right: 10px;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.match-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.kda {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
.match-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #dddddd;
|
||||||
|
}
|
||||||
|
.match-duration,
|
||||||
|
.match-party,
|
||||||
|
.match-rank {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.match-duration::before {
|
||||||
|
content: "️";
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
.match-party::before {
|
||||||
|
content: "";
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
.match-rank::before {
|
||||||
|
content: "";
|
||||||
|
margin-right: 3px;
|
||||||
|
}
|
||||||
|
.win-text {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
.lose-text {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div class="header-title">{{ player_name }}的最近比赛</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="matches-container">
|
||||||
|
{% for match in matches %}
|
||||||
|
<div class="match-card {% if match.win %}win{% else %}lose{% endif %}">
|
||||||
|
<div class="match-header">
|
||||||
|
<div
|
||||||
|
class="match-result {% if match.win %}win-text{% else %}lose-text{% endif %}"
|
||||||
|
>
|
||||||
|
{% if match.win %}胜利{% else %}失败{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="match-time">{{ match.end_time_formatted }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="match-details">
|
||||||
|
<img
|
||||||
|
class="hero-img"
|
||||||
|
src="{{ match.hero_img }}"
|
||||||
|
alt="{{ match.hero_name }}"
|
||||||
|
/>
|
||||||
|
<div class="match-stats">
|
||||||
|
<div class="kda">
|
||||||
|
{{ match.kills }} / {{ match.deaths }} / {{ match.assists }}
|
||||||
|
</div>
|
||||||
|
<div class="match-info">
|
||||||
|
<div class="match-duration">{{ match.duration_formatted }}</div>
|
||||||
|
{% if match.party_size %}
|
||||||
|
<div class="match-party">
|
||||||
|
{% if match.party_size == 1 %} 单排 {% else %} {{
|
||||||
|
match.party_size }}黑 {% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %} {% if match.average_rank %}
|
||||||
|
<div class="match-rank">{{ match.average_rank }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user