fix(HatsuneMiku): 完善

This commit is contained in:
SXP-Simon
2026-03-28 21:18:50 +08:00
parent ae96ea9c93
commit 4200631f17
12 changed files with 579 additions and 343 deletions
+3
View File
@@ -28,3 +28,6 @@ scripts/output_all/debug_scrapbook_test_group_*.pdf
scripts/output_all/debug_simple_test_group_*.pdf scripts/output_all/debug_simple_test_group_*.pdf
scripts/output_all/debug_spring_festival_test_group_*.pdf scripts/output_all/debug_spring_festival_test_group_*.pdf
scripts/data/avatar/cache.db scripts/data/avatar/cache.db
debug_miku.html
scripts/output_all/debug_HatsuneMiku_test_group_*.pdf
scripts/output/mock_report_test_group_mock_*.pdf
+10 -4
View File
@@ -16,6 +16,10 @@
<a href="https://t.me/AstrBotPluginGroupDailyAnalysis"><img src="https://fastly.jsdelivr.net/gh/SXP-Simon/astrbot_plugin_qq_group_daily_analysis@main/assets/TG.png" width="300" alt="Telegram Group"></a> <a href="https://t.me/AstrBotPluginGroupDailyAnalysis"><img src="https://fastly.jsdelivr.net/gh/SXP-Simon/astrbot_plugin_qq_group_daily_analysis@main/assets/TG.png" width="300" alt="Telegram Group"></a>
</td> </td>
</tr> </tr>
<tr>
<td style="border: none;" align="center"><b>QQ群</b></td>
<td style="border: none;" align="center"><b>Telegram群</b></td>
</tr>
</table> </table>
@@ -365,10 +369,11 @@ src/infrastructure/reporting/templates/your_theme_name/
PDF 调试模板命令: PDF 调试模板命令:
```bash ```bash
# docker # 生成所有主题的 PDF (推荐)
docker exec -it astrbot python data/plugins/astrbot_plugin_qq_group_daily_analysis/scripts/mock_pdf_gen.py uv run --with playwright --with diskcache scripts\debug_all_pdf_themes.py
# uv
uv run --with playwright --with diskcache data\plugins\astrbot_plugin_qq_group_daily_analysis\scripts\mock_pdf_gen.py # 生成指定主题的 PDF (例如:HatsuneMiku ,可以修改内部代码占位解决)
uv run --with playwright --with diskcache scripts\mock_pdf_gen.py
``` ```
Image 模板调试: Image 模板调试:
@@ -401,6 +406,7 @@ uv run scripts\debug_render.py -t retro_futurism -o debug_retro.html
uv run scripts\debug_render.py -t format -o debug_format.html uv run scripts\debug_render.py -t format -o debug_format.html
uv run scripts\debug_render.py -t simple -o debug_simple.html uv run scripts\debug_render.py -t simple -o debug_simple.html
uv run scripts\debug_render.py -t spring_festival -o debug_spring.html uv run scripts\debug_render.py -t spring_festival -o debug_spring.html
uv run scripts\debug_render.py -t HatsuneMiku -o debug_miku.html
``` ```
+1
View File
@@ -69,6 +69,7 @@
"options": [ "options": [
"scrapbook", "scrapbook",
"retro_futurism", "retro_futurism",
"HatsuneMiku",
"hack", "hack",
"spring_festival", "spring_festival",
"simple", "simple",
+1
View File
@@ -226,6 +226,7 @@ async def main():
"simple", "simple",
"spring_festival", "spring_festival",
"format", "format",
"HatsuneMiku",
] ]
for theme in themes: for theme in themes:
await generate_theme(theme) await generate_theme(theme)
+1 -1
View File
@@ -87,7 +87,7 @@ class MockConfigManager:
return 5 return 5
def get_report_template(self): def get_report_template(self):
return "scrapbook" return "HatsuneMiku"
def get_enable_user_card(self): def get_enable_user_card(self):
return True return True
@@ -1,6 +1,6 @@
<div class="card-common" style="padding: 30px 20px 15px 20px;"> <div style="display: flex; flex-direction: column; gap: 12px; width: 100%; min-width: 600px;">
<div style="display: flex; align-items: flex-end; justify-content: space-between; height: 180px; gap: 4px;"> {# 柱状图主体层:确保底部对齐 #}
{# 定义一个从 葱绿 -> 薄荷 -> 鹅黄 -> 蜜桃粉 的完美过渡色板 #} <div style="display: flex; align-items: flex-end; justify-content: space-between; height: 160px; gap: 4px; padding-bottom: 5px; border-bottom: 2px solid rgba(57, 197, 187, 0.1);">
{% set chart_colors = [ {% set chart_colors = [
'#66D1C7', '#6FD3C6', '#79D5C4', '#85D8C3', '#93DAC1', '#A3DDBF', '#66D1C7', '#6FD3C6', '#79D5C4', '#85D8C3', '#93DAC1', '#A3DDBF',
'#B4E0BD', '#C7E2BA', '#DBE5B8', '#EFE8B5', '#FFEAB3', '#FFDF9E', '#B4E0BD', '#C7E2BA', '#DBE5B8', '#EFE8B5', '#FFEAB3', '#FFDF9E',
@@ -10,10 +10,7 @@
{% for item in chart_data %} {% for item in chart_data %}
{% set h = item.percentage | float %} {% set h = item.percentage | float %}
{# 使用 loop.index0 从色板中依次取色,形成从左到右的全局渐变 #}
{% set bar_color = chart_colors[loop.index0 % 24] %} {% set bar_color = chart_colors[loop.index0 % 24] %}
{% set bar_opacity = '1' %} {% set bar_opacity = '1' %}
{% if item.count == 0 %} {% if item.count == 0 %}
{% set h = '4' %} {% set h = '4' %}
@@ -21,22 +18,27 @@
{% set bar_color = 'var(--miku-blue)' %} {% set bar_color = 'var(--miku-blue)' %}
{% endif %} {% endif %}
<div style="flex: 1; display: flex; flex-direction: column; align-items: center; height: 100%; justify-content: flex-end; position: relative;"> <div style="flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: flex-end; height: 100%;">
{% if item.count > 0 %} {% if item.count > 0 %}
{# 数字去掉背景框,颜色与柱子保持一致,视觉更通透 #} <span style="font-family: var(--font-num); font-size: 0.75rem; color: {{ bar_color }}; margin-bottom: 4px; font-weight: 800;">{{ item.count }}</span>
<span style="font-family: var(--font-num); font-size: 0.8rem; color: {{ bar_color }}; margin-bottom: 6px; font-weight: bold; text-shadow: 0 1px 2px rgba(0,0,0,0.02);">{{ item.count }}</span>
{% endif %} {% endif %}
{# 柱状体 #}
<div style="width: 100%; height: {{ h }}%; <div style="width: 100%; height: {{ h }}%;
background: {{ bar_color }}; background: {{ bar_color }};
border-radius: 4px 4px 1px 1px; border-radius: 4px 4px 1px 1px;
box-shadow: 0 2px 8px rgba(0,0,0,0.05); opacity: {{ bar_opacity }};
transition: height 0.3s ease; transition: height 0.3s ease;"></div>
opacity: {{ bar_opacity }};"></div> </div>
{% endfor %}
{# X轴标签 #} </div>
<span style="margin-top: 10px; font-family: var(--font-num); font-size: 0.75rem; color: var(--text-sub); font-weight: bold;">{{ "%02d"|format(item.hour) }}时</span>
{# X轴标签层:独立容器确保水平对齐且不换行 #}
<div style="display: flex; justify-content: space-between; gap: 4px;">
{% for item in chart_data %}
<div style="flex: 1; text-align: center;">
<span style="font-family: var(--font-num); font-size: 0.7rem; color: var(--text-sub); font-weight: bold; white-space: nowrap; display: inline-block;">
{{ "%02d"|format(item.hour) }}{% if loop.index0 % 4 == 0 or loop.last %}时{% endif %}
</span>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,7 +5,8 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>群聊日常分析看板 · {{current_date}}</title> <title>群聊日常分析看板 · {{current_date}}</title>
<link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=Noto+Sans+SC:wght@400;500;700&family=Share+Tech+Mono&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=ZCOOL+KuaiLe&family=Noto+Sans+SC:wght@400;500;700&family=Outfit:wght@400;600;800&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<script src="https://unpkg.com/lucide@latest"></script>
<style> <style>
:root { :root {
--miku-base: #39C5BB; --miku-base: #39C5BB;
@@ -24,7 +25,8 @@
--radius: 12px; --radius: 12px;
--font-main: 'Noto Sans SC', sans-serif; --font-main: 'Noto Sans SC', sans-serif;
--font-title: 'ZCOOL KuaiLe', cursive; --font-title: 'ZCOOL KuaiLe', cursive;
--font-num: 'Share Tech Mono', monospace; --font-num: 'Outfit', sans-serif;
--font-mono: 'Share Tech Mono', monospace;
--border-miku-light: rgba(57, 197, 187, 0.15); --border-miku-light: rgba(57, 197, 187, 0.15);
--border-miku-gradient: linear-gradient(90deg, var(--miku-dark) 0%, var(--miku-base) 100%); --border-miku-gradient: linear-gradient(90deg, var(--miku-dark) 0%, var(--miku-base) 100%);
@@ -104,8 +106,9 @@
.stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 30px; position: relative; z-index: 1; } .stats-row { display: grid; grid-template-columns: repeat(4, 1fr); gap: 15px; margin-bottom: 30px; position: relative; z-index: 1; }
.stat-box { background: rgba(255, 255, 255, 0.95); border-radius: var(--radius); padding: 18px; border: 1px solid var(--border-miku-light); box-shadow: 0 4px 12px rgba(57, 197, 187, 0.05); text-align: center; } .stat-box { background: rgba(255, 255, 255, 0.95); border-radius: var(--radius); padding: 18px; border: 1px solid var(--border-miku-light); box-shadow: 0 4px 12px rgba(57, 197, 187, 0.05); text-align: center; }
.stat-num { font-family: var(--font-num); font-size: 2.2rem; color: var(--text-main); font-weight: bold; } .stat-num { font-family: var(--font-num); font-size: 2.2rem; color: var(--text-main); font-weight: 800; }
.stat-txt { font-size: 0.85rem; color: var(--text-sub); margin-top: 5px; font-weight: bold;} .stat-txt { font-size: 0.85rem; color: var(--text-sub); margin-top: 5px; font-weight: bold;}
.stat-icon { color: var(--miku-base); width: 18px; height: 18px; margin-bottom: 2px; }
.active-peak-block { .active-peak-block {
grid-column: span 12; grid-column: span 12;
@@ -125,11 +128,11 @@
} }
.active-peak-block::after { .active-peak-block::after {
content: "MIKU"; position: absolute; right: 15px; top: 0; bottom: 0; display: flex; align-items: center; font-family: var(--font-num); font-size: 160px; line-height: 0.73; font-weight: 900; font-style: italic; color: rgba(255, 255, 255, 0.2); z-index: 0; pointer-events: none; letter-spacing: -3px; content: "MIKU"; position: absolute; right: 15px; top: 0; bottom: 0; display: flex; align-items: center; font-family: var(--font-mono); font-size: 160px; line-height: 0.73; font-weight: 900; font-style: italic; color: rgba(255, 255, 255, 0.2); z-index: 0; pointer-events: none; letter-spacing: -3px;
} }
.peak-label { font-size: 1.1rem; font-weight: bold; text-shadow: 0 1px 2px rgba(0,0,0,0.1); position: relative; z-index: 2;} .peak-label { font-size: 1rem; font-weight: bold; text-shadow: 0 1px 2px rgba(0,0,0,0.1); position: relative; z-index: 2; display: flex; align-items: center; gap: 8px;}
.peak-time-text { font-family: var(--font-num); font-size: 2.8rem; font-weight: bold; margin-top: 8px; text-shadow: 0 2px 4px rgba(0,0,0,0.1); letter-spacing: 2px; position: relative; z-index: 2;} .peak-time-text { font-family: var(--font-num); font-size: 2.8rem; font-weight: 800; margin-top: 4px; text-shadow: 0 2px 4px rgba(0,0,0,0.1); letter-spacing: 1px; position: relative; z-index: 2;}
.peak-image-container { height: 100%; width: auto; max-width: 45%; display: flex; align-items: center; justify-content: flex-end; padding: 10px 0; position: relative; z-index: 1; } .peak-image-container { height: 100%; width: auto; max-width: 45%; display: flex; align-items: center; justify-content: flex-end; padding: 10px 0; position: relative; z-index: 1; }
.peak-image-container img { height: 100%; width: auto; object-fit: contain; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.15); } .peak-image-container img { height: 100%; width: auto; object-fit: contain; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.15); }
@@ -142,15 +145,14 @@
.footer { .footer {
margin-top: 50px; margin-top: 50px;
background: rgba(224, 242, 241, 0.65); background: #e0f2f1;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(57, 197, 187, 0.3); border: 1px solid rgba(57, 197, 187, 0.3);
border-radius: var(--radius); border-radius: var(--radius);
padding: 20px 30px; padding: 20px 30px;
position: relative; position: relative;
z-index: 1; z-index: 1;
box-shadow: 0 8px 20px rgba(57, 197, 187, 0.08); box-shadow: 0 8px 20px rgba(57, 197, 187, 0.08);
page-break-inside: avoid;
} }
.footer-top { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed rgba(57, 197, 187, 0.3); padding-bottom: 15px; margin-bottom: 15px; } .footer-top { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed rgba(57, 197, 187, 0.3); padding-bottom: 15px; margin-bottom: 15px; }
@@ -223,29 +225,29 @@
<div class="stats-row"> <div class="stats-row">
<div class="stat-box"> <div class="stat-box">
<div style="display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 8px;"> <div style="display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 5px;">
<div style="width: 4px; height: 14px; background: var(--miku-base); border-radius: 2px;"></div> <i data-lucide="message-square" class="stat-icon"></i>
<div class="stat-txt" style="margin: 0;">今日消息数</div> <div class="stat-txt" style="margin: 0;">今日消息数</div>
</div> </div>
<div class="stat-num">{{message_count}}</div> <div class="stat-num">{{message_count}}</div>
</div> </div>
<div class="stat-box"> <div class="stat-box">
<div style="display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 8px;"> <div style="display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 5px;">
<div style="width: 4px; height: 14px; background: var(--miku-base); border-radius: 2px;"></div> <i data-lucide="users" class="stat-icon"></i>
<div class="stat-txt" style="margin: 0;">活跃成员数</div> <div class="stat-txt" style="margin: 0;">活跃成员数</div>
</div> </div>
<div class="stat-num">{{participant_count}}</div> <div class="stat-num">{{participant_count}}</div>
</div> </div>
<div class="stat-box"> <div class="stat-box">
<div style="display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 8px;"> <div style="display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 5px;">
<div style="width: 4px; height: 14px; background: var(--miku-base); border-radius: 2px;"></div> <i data-lucide="smile" class="stat-icon"></i>
<div class="stat-txt" style="margin: 0;">表情互动</div> <div class="stat-txt" style="margin: 0;">表情互动</div>
</div> </div>
<div class="stat-num">{{emoji_count}}</div> <div class="stat-num">{{emoji_count}}</div>
</div> </div>
<div class="stat-box"> <div class="stat-box">
<div style="display: flex; align-items: center; justify-content: center; gap: 8px; margin-bottom: 8px;"> <div style="display: flex; align-items: center; justify-content: center; gap: 6px; margin-bottom: 5px;">
<div style="width: 4px; height: 14px; background: var(--miku-base); border-radius: 2px;"></div> <i data-lucide="type" class="stat-icon"></i>
<div class="stat-txt" style="margin: 0;">今日字数累计</div> <div class="stat-txt" style="margin: 0;">今日字数累计</div>
</div> </div>
<div class="stat-num">{{total_characters}}</div> <div class="stat-num">{{total_characters}}</div>
@@ -254,7 +256,10 @@
<div class="active-peak-block"> <div class="active-peak-block">
<div> <div>
<div class="peak-label">峰值活跃时间段</div> <div class="peak-label">
<i data-lucide="trending-up" style="width: 20px; height: 20px;"></i>
峰值活跃时间段
</div>
<div class="peak-time-text">{{most_active_period}}</div> <div class="peak-time-text">{{most_active_period}}</div>
</div> </div>
@@ -279,17 +284,37 @@
<div class="footer"> <div class="footer">
<div class="footer-top"> <div class="footer-top">
<div class="footer-repo"> <div class="footer-repo">
Full repository path / <a href="#">SXP-Simon/astrbot_plugin_qq_group_daily_analysis</a> <svg viewBox="0 0 24 24" fill="var(--miku-dark)" style="width: 18px; height: 18px; vertical-align: middle; margin-right: 6px;">
<path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.042-1.416-4.042-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.652.242 2.873.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/>
</svg>
<a href="https://github.com/SXP-Simon/astrbot_plugin_qq_group_daily_analysis" target="_blank">SXP-Simon/astrbot_plugin_qq_group_daily_analysis</a>
</div> </div>
<div class="token-data"> <div class="token-data">
Token usage: {{total_tokens}} <span class="token-sub">( P: {{prompt_tokens}} | C: {{completion_tokens}} )</span> <svg viewBox="0 0 24 24" stroke="var(--text-sub)" stroke-width="2" fill="none" style="width: 14px; height: 14px; vertical-align: middle; margin-right: 4px;">
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect>
<path d="M9 9h6v6H9z"></path><path d="M9 1v3"></path><path d="M15 1v3"></path><path d="M9 20v3"></path><path d="M15 20v3"></path><path d="M20 9h3"></path><path d="M20 15h3"></path><path d="M1 9h3"></path><path d="M1 15h3"></path>
</svg>
Token usage: <span style="font-weight: 800;">{{total_tokens}}</span> <span class="token-sub">( P: {{prompt_tokens}} | C: {{completion_tokens}} )</span>
</div> </div>
</div> </div>
<div class="footer-bottom"> <div class="footer-bottom">
<div class="footer-time">{{current_datetime}}</div> <div class="footer-time">
<div class="footer-author">Template by shiitin</div> <svg viewBox="0 0 24 24" stroke="var(--text-sub)" stroke-width="2" fill="none" style="width: 14px; height: 14px; vertical-align: middle; margin-right: 4px;">
<circle cx="12" cy="12" r="10"></circle><polyline points="12 6 12 12 16 14"></polyline>
</svg>
Generated at {{current_datetime}}
</div>
<div class="footer-author">
<svg viewBox="0 0 24 24" fill="#ff85a2" style="width: 14px; height: 14px; vertical-align: middle; margin-right: 4px;">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
Template by shiitin
</div>
</div> </div>
</div> </div>
</div> </div>
<script>
lucide.createIcons();
</script>
</body> </body>
</html> </html>
@@ -1,9 +1,4 @@
{% if quotes %} {% if quotes %}
<div style="margin-bottom: 40px;">
<div class="section-header">
<div class="v-bar"></div>
<div class="section-title-text">每日回响</div>
</div>
{% for quote in quotes %} {% for quote in quotes %}
<div style="margin-bottom: 25px; display: flex; flex-direction: column; align-items: {% if loop.index0 is even %}flex-start{% else %}flex-end{% endif %};"> <div style="margin-bottom: 25px; display: flex; flex-direction: column; align-items: {% if loop.index0 is even %}flex-start{% else %}flex-end{% endif %};">
<div style="display: flex; gap: 15px; max-width: 85%; {% if loop.index0 is odd %}flex-direction: row-reverse;{% endif %}"> <div style="display: flex; gap: 15px; max-width: 85%; {% if loop.index0 is odd %}flex-direction: row-reverse;{% endif %}">
@@ -19,8 +14,11 @@
<div style="display: inline-block; background: var(--bg-page-base); padding: 12px 18px; border-radius: 12px; border-left: 4px solid var(--miku-pink-light); text-align: left;"> <div style="display: inline-block; background: var(--bg-page-base); padding: 12px 18px; border-radius: 12px; border-left: 4px solid var(--miku-pink-light); text-align: left;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;"> <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 6px;">
<span style="font-size: 1.1rem;">🎵</span> <svg viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" fill="none" style="width: 18px; height: 18px; color: var(--miku-dark);">
<span style="font-weight: bold; color: var(--miku-dark); font-size: 0.95rem;">Miku 锐评</span> <path d="M9 18V5l12-2v13"/>
<circle cx="6" cy="18" r="3"/>
<circle cx="18" cy="16" r="3"/>
</svg> <span style="font-weight: bold; color: var(--miku-dark); font-size: 0.95rem;">Miku 锐评</span>
</div> </div>
<div style="font-size: 0.9rem; color: var(--text-sub); line-height: 1.6;">{{ quote.reason | safe }}</div> <div style="font-size: 0.9rem; color: var(--text-sub); line-height: 1.6;">{{ quote.reason | safe }}</div>
</div> </div>
@@ -28,5 +26,4 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div>
{% endif %} {% endif %}
File diff suppressed because one or more lines are too long
@@ -1,29 +1,33 @@
{% if titles %} {% if titles %}
<div style="margin-bottom: 40px;"> <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px;">
<div class="section-header"> {% for title in titles %}
<div class="v-bar"></div> <div class="card-common" style="padding: 24px; margin-bottom: 0; border-top: 4px solid var(--miku-light); display: flex; flex-direction: column; gap: 15px;">
<div class="section-title-text">群友画像</div> <div style="display: flex; align-items: center; gap: 15px;">
</div> <div style="width: 60px; height: 60px; border-radius: 50%; border: 3px solid white; box-shadow: 0 4px 10px rgba(57,197,187,0.2); overflow: hidden; flex-shrink: 0; background: var(--bg-page-base);">
<div style="column-count: 2; column-gap: 15px;"> {% if title.avatar_data %}
{% for title in titles %} <img src="{{ title.avatar_data }}" style="width: 100%; height: 100%; object-fit: cover;">
<div class="card-common" style="break-inside: avoid; padding: 20px; margin-bottom: 15px; border-top: 3px solid var(--miku-light);"> {% else %}
<div style="display: flex; align-items: center; gap: 15px; margin-bottom: 15px;"> <div style="width: 100%; height: 100%; background: var(--miku-blue); display: flex; align-items: center; justify-content: center; color: white; font-weight: bold;">
<div style="width: 55px; height: 55px; border-radius: 50%; border: 3px solid white; box-shadow: 0 0 0 2px var(--miku-light); overflow: hidden; flex-shrink: 0; background: var(--bg-page-base);"> {{ title.name[0] if title.name else '?' }}
{% if title.avatar_data %}<img src="{{ title.avatar_data }}" style="width: 100%; height: 100%; object-fit: cover;">{% endif %}
</div>
<div>
<div style="font-weight: bold; font-size: 1.1rem; color: var(--text-main);">{{ title.name }}</div>
<div style="display: flex; gap: 6px; margin-top: 6px;">
<span style="background: var(--miku-blue); color: var(--miku-dark); font-size: 0.75rem; padding: 3px 8px; border-radius: 6px; font-weight: bold;">{{ title.title }}</span>
<span style="background: var(--miku-pink-light); color: #fff; font-size: 0.75rem; padding: 3px 8px; border-radius: 6px; font-family: var(--font-num); font-weight: bold;">{{ title.mbti }}</span>
</div> </div>
</div> {% endif %}
</div> </div>
<div style="background: var(--bg-page-base); padding: 12px 15px; border-radius: 8px; font-size: 0.95rem; color: var(--text-sub); line-height: 1.6; border: 1px dashed var(--border-miku-light);"> <div style="flex: 1; overflow: hidden;">
{{ title.reason }} <div style="font-weight: 800; font-size: 1.15rem; color: var(--accent-dark); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{ title.name }}</div>
<div style="display: flex; gap: 6px; margin-top: 6px;">
<span style="background: var(--miku-blue); color: var(--miku-dark); font-size: 0.7rem; padding: 2px 8px; border-radius: 6px; font-weight: 800; border: 1px solid rgba(57,197,187,0.3);">{{ title.title }}</span>
{% if title.mbti %}
<span style="background: var(--miku-pink-light); color: white; font-size: 0.7rem; padding: 2px 8px; border-radius: 6px; font-family: var(--font-num); font-weight: 800; box-shadow: 0 2px 5px rgba(255,107,149,0.2);">{{ title.mbti }}</span>
{% endif %}
</div>
</div> </div>
</div> </div>
{% endfor %} <div style="background: var(--bg-page-base); padding: 15px; border-radius: 12px; font-size: 0.95rem; color: var(--text-sub); line-height: 1.6; border: 1px dashed var(--border-miku-light); position: relative; flex: 1;">
{{ title.reason }}
<svg viewBox="0 0 24 24" fill="var(--miku-base)" style="position: absolute; right: 8px; bottom: 8px; width: 16px; height: 16px; opacity: 0.2;">
<path d="M3 21c3 0 7-1 7-8V5c0-1.25-.75-2-2-2H4c-1.25 0-2 .75-2 2v5c0 1.25.75 2 2 2h3c0 4-4 6-4 6Zm11 0c3 0 7-1 7-8V5c0-1.25-.75-2-2-2h-4c-1.25 0-2 .75-2 2v5c0 1.25.75 2 2 2h3c0 4-4 6-4 6Z"/>
</svg> </div>
</div> </div>
{% endfor %}
</div> </div>
{% endif %} {% endif %}