feat: Add image generation for recent matches report

This commit is contained in:
Ching L 2025-03-05 21:54:29 +08:00
parent 2bae19643a
commit 9c8b360553
3 changed files with 270 additions and 1 deletions

17
dota.py
View File

@ -176,6 +176,23 @@ class Friend(BaseModel):
'timestamp': end_time,
'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

View File

@ -3,6 +3,7 @@ from jinja2 import Environment, FileSystemLoader
import utils
import json
import os
import datetime
class ImageGenerator:
@ -132,4 +133,84 @@ class ImageGenerator:
image_url = utils.upload_image(image_path, image_path)
os.remove(image_path)
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

View 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>