feat(mbti): 替换 mbti 简单实现 sbti acgti 功能

This commit is contained in:
SXP-Simon
2026-04-13 21:43:25 +08:00
parent 06ea0e7812
commit ed85876b36
14 changed files with 702 additions and 175 deletions
@@ -621,6 +621,34 @@ class ConfigManager:
self._ensure_group("basic")["enable_analysis_reply"] = enabled
self.config.save_config()
def get_profile_display_mode(self) -> str:
"""获取人格标签展示模式。"""
mode = str(self._get_group("basic").get("profile_display_mode", "mbti")).lower()
if mode not in {"mbti", "sbti", "acgti"}:
return "mbti"
return mode
def get_profile_image_opacity(self) -> float:
"""获取人格背景图透明度。"""
value = self._get_group("basic").get("profile_image_opacity", 0.12)
try:
return max(0.0, min(1.0, float(value)))
except (TypeError, ValueError):
return 0.12
def get_profile_image_size_mode(self) -> str:
"""获取人格背景图尺寸模式。"""
mode = str(
self._get_group("basic").get("profile_image_size_mode", "contain")
).lower()
if mode not in {"contain", "cover"}:
return "contain"
return mode
def get_profile_mapping_config(self) -> str:
"""获取人格映射配置(JSON 文本)。"""
return str(self._get_group("basic").get("profile_mapping_config", "")).strip()
# ========== 群文件/群相册上传配置 ==========
def get_enable_group_file_upload(self) -> bool:
+206
View File
@@ -6,6 +6,7 @@
import asyncio
import base64
import html
import json
import os
import re
from dataclasses import asdict, is_dataclass
@@ -28,6 +29,63 @@ from .templates import HTMLTemplates
MAX_CONCURRENT_DOWNLOADS = 10
AVATAR_CACHE_EXPIRE_TIME = 259200
DEFAULT_PROFILE_MAPPING = {
"mbti": {
"INTJ": {"code": "INTJ", "name_zh": "建筑师"},
"INTP": {"code": "INTP", "name_zh": "逻辑学家"},
"ENTJ": {"code": "ENTJ", "name_zh": "指挥官"},
"ENTP": {"code": "ENTP", "name_zh": "辩论家"},
"INFJ": {"code": "INFJ", "name_zh": "提倡者"},
"INFP": {"code": "INFP", "name_zh": "调停者"},
"ENFJ": {"code": "ENFJ", "name_zh": "主人公"},
"ENFP": {"code": "ENFP", "name_zh": "竞选者"},
"ISTJ": {"code": "ISTJ", "name_zh": "物流师"},
"ISFJ": {"code": "ISFJ", "name_zh": "守卫者"},
"ESTJ": {"code": "ESTJ", "name_zh": "总经理"},
"ESTP": {"code": "ESTP", "name_zh": "企业家"},
"ISTP": {"code": "ISTP", "name_zh": "鉴赏家"},
"ISFP": {"code": "ISFP", "name_zh": "探险家"},
"ESFJ": {"code": "ESFJ", "name_zh": "执政官"},
"ESFP": {"code": "ESFP", "name_zh": "表演者"},
},
"sbti": {
"INTJ": {"code": "CTRL", "name_zh": "拿捏者", "asset_code": "CTRL"},
"INTP": {"code": "THIN-K", "name_zh": "思考者", "asset_code": "THIN-K"},
"ENTJ": {"code": "BOSS", "name_zh": "领导者", "asset_code": "BOSS"},
"ENTP": {"code": "JOKE-R", "name_zh": "小丑", "asset_code": "JOKE-R"},
"INFJ": {"code": "LOVE-R", "name_zh": "多情者", "asset_code": "LOVE-R"},
"INFP": {"code": "SOLO", "name_zh": "孤儿", "asset_code": "SOLO"},
"ENFJ": {"code": "THAN-K", "name_zh": "感恩者", "asset_code": "THAN-K"},
"ENFP": {"code": "GOGO", "name_zh": "行者", "asset_code": "GOGO"},
"ISTJ": {"code": "OH-NO", "name_zh": "哦不人", "asset_code": "OH-NO"},
"ISTP": {"code": "POOR", "name_zh": "贫困者", "asset_code": "POOR"},
"ESTJ": {"code": "SHIT", "name_zh": "愤世者", "asset_code": "SHIT"},
"ESTP": {"code": "WOC!", "name_zh": "握草人", "asset_code": "WOC"},
"ISFJ": {"code": "MUM", "name_zh": "妈妈", "asset_code": "MUM"},
"ISFP": {"code": "MALO", "name_zh": "吗喽", "asset_code": "MALO"},
"ESFJ": {"code": "ATM-er", "name_zh": "送钱者", "asset_code": "ATM-er"},
"ESFP": {"code": "SEXY", "name_zh": "尤物", "asset_code": "SEXY"},
},
"acgti": {
"INTJ": {"code": "MRTS-X", "name_zh": "Mortis"},
"INTP": {"code": "KNAN", "name_zh": "江户川柯南"},
"ENTJ": {"code": "SAKI", "name_zh": "丰川祥子"},
"ENTP": {"code": "CHKA", "name_zh": "藤原千花"},
"INFJ": {"code": "DLRS", "name_zh": "三角初华"},
"INFP": {"code": "BCHI", "name_zh": "后藤一里"},
"ENFJ": {"code": "YCYO", "name_zh": "月见八千代"},
"ENFP": {"code": "HTMK", "name_zh": "初音未来"},
"ISTJ": {"code": "MRTS", "name_zh": "若叶睦"},
"ISTP": {"code": "AYRE", "name_zh": "绫波丽"},
"ESTJ": {"code": "MIKT", "name_zh": "御坂美琴"},
"ESTP": {"code": "ASKA", "name_zh": "明日香"},
"ISFJ": {"code": "SOYO", "name_zh": "长崎爽世"},
"ISFP": {"code": "LTYI", "name_zh": "洛天依"},
"ESFJ": {"code": "ANON", "name_zh": "千早爱音"},
"ESFP": {"code": "FRNA", "name_zh": "芙宁娜"},
},
}
class ReportGenerator(IReportGenerator):
"""报告生成器"""
@@ -51,6 +109,148 @@ class ReportGenerator(IReportGenerator):
MAX_CONCURRENT_DOWNLOADS
)
self._avatar_session = None
self._profile_asset_manifest = self._load_profile_asset_manifest()
def _load_profile_asset_manifest(self) -> dict[str, dict]:
"""加载人格资源清单。"""
manifest_path = (
Path(__file__).resolve().parents[3]
/ "assets"
/ "profile_assets"
/ "manifest.json"
)
if not manifest_path.exists():
logger.warning(f"人格资源清单不存在: {manifest_path}")
return {"sbti": {}, "acgti": {}}
try:
raw = json.loads(manifest_path.read_text(encoding="utf-8-sig"))
except Exception as e:
logger.warning(f"加载人格资源清单失败: {e}")
return {"sbti": {}, "acgti": {}}
manifest: dict[str, dict] = {"sbti": {}, "acgti": {}}
for item in raw.get("sbti", []):
code = str(item.get("code", "")).strip()
if code:
manifest["sbti"][code] = item
for item in raw.get("acgti", []):
code = str(item.get("code", "")).strip()
if code:
manifest["acgti"][code] = item
return manifest
def _get_profile_mapping_overrides(self) -> dict[str, dict]:
"""解析用户配置的人格映射覆盖项。"""
raw = self.config_manager.get_profile_mapping_config()
if not raw:
return {}
try:
data = json.loads(raw)
if isinstance(data, dict):
return data
except Exception as e:
logger.warning(f"人格映射配置 JSON 解析失败,已回退到默认映射: {e}")
return {}
def _build_profile_image_from_manifest_pattern(
self, profile_mode: str, asset_code: str
) -> str:
"""当 manifest 缺少具体 code 时,根据已有资源路径模式推导图片地址。"""
system_manifest = self._profile_asset_manifest.get(profile_mode, {})
for item in system_manifest.values():
if not isinstance(item, dict):
continue
sample_code = str(item.get("code", "")).strip()
sample_file = str(item.get("file", "")).strip()
if not sample_code or not sample_file:
continue
code_token = f"/{sample_code}."
if code_token not in sample_file:
continue
return sample_file.replace(code_token, f"/{asset_code}.", 1)
return ""
def _get_manifest_profile_item_by_mbti(
self, profile_mode: str, mbti: str
) -> dict | None:
"""按 MBTI 从 manifest 中寻找可用资源。"""
normalized_mbti = str(mbti or "").strip().upper()
system_manifest = self._profile_asset_manifest.get(profile_mode, {})
for item in system_manifest.values():
if not isinstance(item, dict):
continue
item_mbti = str(item.get("mbti", "")).strip().upper()
if item_mbti == normalized_mbti:
return item
return None
def _resolve_profile_info(
self,
mbti: str,
profile_mode: str,
overrides: dict[str, dict],
) -> dict[str, str | float]:
"""根据当前展示模式解析人格标签展示信息。"""
normalized_mbti = str(mbti or "").strip().upper()
# 1. 基础信息获取:从默认映射或用户覆盖中获取核心属性
profile_defaults = DEFAULT_PROFILE_MAPPING.get(profile_mode, {})
base_info = dict(profile_defaults.get(normalized_mbti, {}))
# 用户覆盖优先级最高
user_override = overrides.get(profile_mode, {}).get(normalized_mbti, {})
if isinstance(user_override, dict):
base_info.update(user_override)
code = str(base_info.get("code", normalized_mbti)).strip() or normalized_mbti
name_zh = str(base_info.get("name_zh", "")).strip()
asset_code = str(base_info.get("asset_code", code)).strip() or code
image = str(base_info.get("image", "")).strip()
# 2. 图片与属性补全 (基于 manifest.json 可信源)
if not image:
system_manifest = self._profile_asset_manifest.get(profile_mode, {})
# A. 优先按 asset_code 索引
asset_item = system_manifest.get(asset_code)
if isinstance(asset_item, dict):
image = str(asset_item.get("file", "")).strip()
if not name_zh:
name_zh = str(asset_item.get("name", "")).strip()
# B. 对于 acgti 模式,如果没找到,尝试通过 MBTI 反查第一个匹配的资源
if not image and profile_mode == "acgti":
fallback_item = self._get_manifest_profile_item_by_mbti(
profile_mode, normalized_mbti
)
if isinstance(fallback_item, dict):
image = str(fallback_item.get("file", "")).strip()
if not name_zh:
name_zh = str(fallback_item.get("name", "")).strip()
if not code or code == normalized_mbti:
code = str(fallback_item.get("code", code)).strip()
# C. 最后底线:根据 manifest 路径模式推导
if not image:
image = self._build_profile_image_from_manifest_pattern(
profile_mode, asset_code
)
# 3. 构造显示文本 (Code + 中文名)
display = str(base_info.get("display", "")).strip()
if not display:
display = f"{code}{name_zh}" if name_zh else code
return {
"profile_mode": profile_mode,
"profile_code": code,
"profile_name_zh": name_zh,
"profile_display": display,
"profile_image": image,
"profile_image_opacity": self.config_manager.get_profile_image_opacity(),
"profile_image_size_mode": self.config_manager.get_profile_image_size_mode(),
}
@staticmethod
def _sanitize_path_component(name: str) -> str:
@@ -506,11 +706,16 @@ class ReportGenerator(IReportGenerator):
# 使用Jinja2模板构建用户称号HTML(批量渲染,包含头像)
max_user_titles = self.config_manager.get_max_user_titles()
titles_list = []
profile_mode = self.config_manager.get_profile_display_mode()
profile_mapping_overrides = self._get_profile_mapping_overrides()
for title in user_titles[:max_user_titles]:
# 获取用户头像
avatar_data = await self._get_user_avatar(
str(title.user_id), avatar_url_getter
)
profile_info = self._resolve_profile_info(
title.mbti, profile_mode, profile_mapping_overrides
)
title_data = {
"name": title.name,
"title": title.title,
@@ -518,6 +723,7 @@ class ReportGenerator(IReportGenerator):
"reason": title.reason,
"avatar_data": avatar_data,
}
title_data.update(profile_info)
titles_list.append(title_data)
titles_html = self.html_templates.render_template(
@@ -1,39 +1,42 @@
{% set title_emojis = [
'https://tc.ciallo.ccwu.cc/file/1775132813334_1774881267181_得意.gif',
'https://tc.ciallo.ccwu.cc/file/1775132808652_1774881267385_得意-1.gif',
'https://tc.ciallo.ccwu.cc/file/1775132804506_1774881263342_观察.gif',
'https://tc.ciallo.ccwu.cc/file/1775132811492_1774881264336_不要.gif'
] %}
{% set title_badge_colors = [
{'bg': 'linear-gradient(135deg, #ffcadf 0%, #ffc1cc 45%, #ffd9b8 100%)', 'text': '#b85c7b', 'shadow': 'rgba(255, 177, 203, 0.28)', 'border': 'rgba(255, 184, 210, 0.72)'},
{'bg': 'linear-gradient(135deg, #bfe7ff 0%, #c8dbff 48%, #d8ccff 100%)', 'text': '#5d71b8', 'shadow': 'rgba(168, 198, 255, 0.26)', 'border': 'rgba(173, 204, 255, 0.74)'},
{'bg': 'linear-gradient(135deg, #c7f3df 0%, #d3f1ff 50%, #e1ddff 100%)', 'text': '#4e8e88', 'shadow': 'rgba(161, 232, 205, 0.24)', 'border': 'rgba(178, 230, 214, 0.72)'},
{'bg': 'linear-gradient(135deg, #ffe5b7 0%, #ffd6c7 48%, #ffd7ef 100%)', 'text': '#b57752', 'shadow': 'rgba(255, 210, 163, 0.28)', 'border': 'rgba(255, 214, 178, 0.76)'}
] %}
{% for title in titles %}
{% set badge = title_badge_colors[loop.index0 % (title_badge_colors|length)] %}
<li class="item item-with-emoji">
<div class="item-main">
<div style="display: flex; align-items: center; gap: 15px;">
{% if title.avatar_data %}
<img src="{{ title.avatar_data }}"
alt="头像"
style="width: 50px; height: 50px; border-radius: 50%; object-fit: cover; border: 2px solid #667eea;">
{% endif %}
<div style="flex: 1;">
<div class="item-title">
{{ title.name }}
<span style="display: inline-flex; align-items: center; margin-left: 10px; padding: 5px 10px; border-radius: 999px; font-size: 0.78em; font-weight: 700; letter-spacing: 0.2px; color: {{ badge.text }}; background: {{ badge.bg }}; border: 1px solid {{ badge.border }}; box-shadow: 0 6px 14px {{ badge.shadow }}, inset 0 1px 0 rgba(255,255,255,0.55); vertical-align: middle;">
{{ title.title }}
</span>
</div>
<div class="item-content">
<p><strong>MBTI:</strong> {{ title.mbti }}</p>
<p><strong>称号理由:</strong> {{ title.reason }}</p>
</div>
</div>
</div>
</div>
<img class="item-emoji title-emoji" src="{{ title_emojis | random }}" alt="称号表情">
</li>
{% endfor %}
{% set title_emojis = [
'https://tc.ciallo.ccwu.cc/file/1775132813334_1774881267181_得意.gif',
'https://tc.ciallo.ccwu.cc/file/1775132808652_1774881267385_得意-1.gif',
'https://tc.ciallo.ccwu.cc/file/1775132804506_1774881263342_观察.gif',
'https://tc.ciallo.ccwu.cc/file/1775132811492_1774881264336_不要.gif'
] %}
{% set title_badge_colors = [
{'bg': 'linear-gradient(135deg, #ffcadf 0%, #ffc1cc 45%, #ffd9b8 100%)', 'text': '#b85c7b', 'shadow': 'rgba(255, 177, 203, 0.28)', 'border': 'rgba(255, 184, 210, 0.72)'},
{'bg': 'linear-gradient(135deg, #bfe7ff 0%, #c8dbff 48%, #d8ccff 100%)', 'text': '#5d71b8', 'shadow': 'rgba(168, 198, 255, 0.26)', 'border': 'rgba(173, 204, 255, 0.74)'},
{'bg': 'linear-gradient(135deg, #c7f3df 0%, #d3f1ff 50%, #e1ddff 100%)', 'text': '#4e8e88', 'shadow': 'rgba(161, 232, 205, 0.24)', 'border': 'rgba(178, 230, 214, 0.72)'},
{'bg': 'linear-gradient(135deg, #ffe5b7 0%, #ffd6c7 48%, #ffd7ef 100%)', 'text': '#b57752', 'shadow': 'rgba(255, 210, 163, 0.28)', 'border': 'rgba(255, 214, 178, 0.76)'}
] %}
{% for title in titles %}
{% set badge = title_badge_colors[loop.index0 % (title_badge_colors|length)] %}
<li class="item item-with-emoji" style="position: relative; overflow: hidden;">
<div class="item-main" style="position: relative; z-index: 1;">
<div style="display: flex; align-items: center; gap: 15px;">
{% if title.avatar_data %}
<img src="{{ title.avatar_data }}"
alt="头像"
style="width: 50px; height: 50px; border-radius: 50%; object-fit: cover; border: 2px solid #667eea;">
{% endif %}
<div style="flex: 1;">
<div class="item-title">
{{ title.name }}
<span style="display: inline-flex; align-items: center; margin-left: 10px; padding: 5px 10px; border-radius: 999px; font-size: 0.78em; font-weight: 700; letter-spacing: 0.2px; color: {{ badge.text }}; background: {{ badge.bg }}; border: 1px solid {{ badge.border }}; box-shadow: 0 6px 14px {{ badge.shadow }}, inset 0 1px 0 rgba(255,255,255,0.55); vertical-align: middle;">
{{ title.title }}
</span>
</div>
<div class="item-content">
<p><strong>人格标签:</strong> {{ title.profile_display or title.mbti }}</p>
<p><strong>称号理由:</strong> {{ title.reason }}</p>
</div>
</div>
</div>
</div>
{% if title.profile_image %}
<img src="{{ title.profile_image }}" alt="" style="position: absolute; right: 8px; bottom: 8px; width: 36%; max-height: 78%; object-fit: {{ title.profile_image_size_mode or 'contain' }}; opacity: {{ title.profile_image_opacity or 0.12 }}; pointer-events: none; z-index: 1;">
{% endif %}
<img class="item-emoji title-emoji" src="{{ title_emojis | random }}" alt="称号表情" style="z-index: 2;">
</li>
{% endfor %}
@@ -1,7 +1,7 @@
{% if titles %}
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px;">
{% for title in titles %}
<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="card-common" style="padding: 24px; margin-bottom: 0; border-top: 4px solid var(--miku-light); display: flex; flex-direction: column; gap: 15px; position: relative; overflow: hidden;">
<div style="display: flex; align-items: center; gap: 15px;">
<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);">
{% if title.avatar_data %}
@@ -16,8 +16,8 @@
<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>
{% if title.profile_display or 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.profile_display or title.mbti }}</span>
{% endif %}
</div>
</div>
@@ -26,7 +26,11 @@
{{ 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>
</svg>
</div>
{% if title.profile_image %}
<img src="{{ title.profile_image }}" alt="" style="position: absolute; right: 8px; bottom: 8px; width: 38%; max-height: 78%; object-fit: {{ title.profile_image_size_mode or 'contain' }}; opacity: {{ title.profile_image_opacity or 0.12 }}; pointer-events: none; z-index: 1;">
{% endif %}
</div>
{% endfor %}
</div>
@@ -3,7 +3,7 @@
<h2 class="section-title">群友称号</h2>
<div class="users-grid">
{% for title in titles %}
<div class="user-title">
<div class="user-title" style="position: relative; overflow: hidden;">
<div class="user-info">
{% if title.avatar_data %}
<img src="{{ title.avatar_data }}" class="user-avatar" alt="头像">
@@ -14,13 +14,16 @@
<div class="user-name">{{ title.name }}</div>
<div class="user-badges">
<div class="user-title-badge">{{ title.title }}</div>
<div class="user-mbti">{{ title.mbti }}</div>
<div class="user-mbti">{{ title.profile_display or title.mbti }}</div>
</div>
</div>
</div>
<div class="user-reason">{{ title.reason }}</div>
{% if title.profile_image %}
<img src="{{ title.profile_image }}" alt="" style="position: absolute; right: 8px; bottom: 8px; width: 38%; max-height: 82%; object-fit: {{ title.profile_image_size_mode or 'contain' }}; opacity: {{ title.profile_image_opacity or 0.12 }}; pointer-events: none; z-index: 1;">
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
@@ -1,7 +1,7 @@
{% if titles %}
<div class="user-grid">
{% for title in titles %}
<div class="user-card avoid-break">
<div class="user-card avoid-break" style="position: relative; overflow: hidden;">
<div class="user-avatar">
{% if title.avatar_data %}
<img src="{{ title.avatar_data }}" alt="{{ title.name }}"
@@ -15,12 +15,15 @@
<div class="user-info">
<h4>{{ title.name }}</h4>
<div style="margin-bottom: 10px;">
<span class="tag mbti">{{ title.mbti }}</span>
<span class="tag mbti">{{ title.profile_display or title.mbti }}</span>
<span class="tag title">{{ title.title }}</span>
</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); line-height: 1.4;">{{ title.reason }}</div>
</div>
{% if title.profile_image %}
<img src="{{ title.profile_image }}" alt="" style="position: absolute; right: 8px; bottom: 8px; width: 36%; max-height: 78%; object-fit: {{ title.profile_image_size_mode or 'contain' }}; opacity: {{ title.profile_image_opacity or 0.12 }}; pointer-events: none; z-index: 1;">
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
@@ -1,6 +1,6 @@
{% if titles %}
{% for title in titles %}
<div class="title-item">
<div class="title-item" style="position: relative; overflow: hidden;">
<div class="title-avatar-wrap">
{% if title.avatar_data %}
<img src="{{ title.avatar_data }}" alt="{{ title.name }}">
@@ -13,12 +13,15 @@
<div class="title-info">
<h4>{{ title.name }}</h4>
<div class="title-badge">
<span>{{ title.mbti }}</span>
<span>{{ title.profile_display or title.mbti }}</span>
<span style="color:var(--c-dec-3);">//</span>
<span>{{ title.title }}</span>
</div>
<div class="title-reason">{{ title.reason }}</div>
</div>
{% if title.profile_image %}
<img src="{{ title.profile_image }}" alt="" style="position: absolute; right: 8px; bottom: 8px; width: 35%; max-height: 80%; object-fit: {{ title.profile_image_size_mode or 'contain' }}; opacity: {{ title.profile_image_opacity or 0.12 }}; pointer-events: none; z-index: 1;">
{% endif %}
</div>
{% endfor %}
{% endif %}
{% endif %}
@@ -9,7 +9,7 @@
</div>
<div class="masonry-grid">
{% for title in titles %}
<div class="user-card avoid-break">
<div class="user-card avoid-break" style="position: relative; overflow: hidden;">
<div class="card-tape"></div>
<div class="user-header">
<div class="u-avatar">
@@ -26,15 +26,18 @@
<div class="u-name">{{ title.name }}</div>
<div class="badges">
<span class="badge title">{{ title.title }}</span>
<span class="badge mbti">{{ title.mbti }}</span>
<span class="badge mbti">{{ title.profile_display or title.mbti }}</span>
</div>
</div>
</div>
<div class="u-reason">
{{ title.reason }}
</div>
{% if title.profile_image %}
<img src="{{ title.profile_image }}" alt="" style="position: absolute; right: 8px; bottom: 8px; width: 38%; max-height: 80%; object-fit: {{ title.profile_image_size_mode or 'contain' }}; opacity: {{ title.profile_image_opacity or 0.12 }}; pointer-events: none; z-index: 1;">
{% endif %}
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
@@ -2,11 +2,14 @@
<div class="section">
<h2>群友称号</h2>
{% for item in titles %}
<div style="margin-bottom: 10px; border: 1px solid #eee; padding: 5px;">
<div style="margin-bottom: 10px; border: 1px solid #eee; padding: 5px; position: relative; overflow: hidden;">
<div style="font-weight: bold;">{{ item.name }} - {{ item.title }}</div>
<div style="font-size: 0.8em; background: #eee; display: inline-block; padding: 2px 5px; border-radius: 3px;">{{ item.mbti }}</div>
<div style="font-size: 0.9em; color: #555; margin-top: 5px;">{{ item.reason }}</div>
<div style="font-size: 0.8em; background: #eee; display: inline-block; padding: 2px 5px; border-radius: 3px; position: relative; z-index: 1;">{{ item.profile_display or item.mbti }}</div>
<div style="font-size: 0.9em; color: #555; margin-top: 5px; position: relative; z-index: 1;">{{ item.reason }}</div>
{% if item.profile_image %}
<img src="{{ item.profile_image }}" alt="" style="position: absolute; right: 4px; bottom: 4px; width: 36%; max-height: 90%; object-fit: {{ item.profile_image_size_mode or 'contain' }}; opacity: {{ item.profile_image_opacity or 0.12 }}; pointer-events: none; z-index: 2;">
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
@@ -1,7 +1,7 @@
{% if titles %}
<div class="sf-user-masonry">
{% for title in titles %}
<div class="sf-user-card avoid-break">
<div class="sf-user-card avoid-break" style="position: relative; overflow: hidden;">
<div class="sf-card-header">
<div class="sf-avatar-wrap">
{% if title.avatar_data %}
@@ -14,13 +14,16 @@
<div class="sf-user-info">
<h4 class="sf-user-name" style="margin: 0 0 5px; font-size: 1.3rem;">{{ title.name }}</h4>
<div class="sf-user-badges">
<span class="sf-badge mbti">{{ title.mbti }}</span>
<span class="sf-badge mbti">{{ title.profile_display or title.mbti }}</span>
<span class="sf-badge sf-title">{{ title.title }}</span>
</div>
</div>
</div>
<div class="sf-user-reason">{{ title.reason }}</div>
{% if title.profile_image %}
<img src="{{ title.profile_image }}" alt="" style="position: absolute; right: 8px; bottom: 8px; width: 36%; max-height: 78%; object-fit: {{ title.profile_image_size_mode or 'contain' }}; opacity: {{ title.profile_image_opacity or 0.12 }}; pointer-events: none; z-index: 1;">
{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}