mirror of
https://github.com/Nezumi-2711/astrbot_plugin_qq_group_daily_analysis.git
synced 2026-09-22 13:38:43 +00:00
feat(mbti): 替换 mbti 简单实现 sbti acgti 功能
This commit is contained in:
@@ -33,3 +33,4 @@ scripts/output_all/debug_HatsuneMiku_test_group_*.pdf
|
||||
scripts/output/mock_report_test_group_mock_*.pdf
|
||||
astrbot-lark-group-daily-analysis-main/**
|
||||
.ace-tool/
|
||||
debug_atri.html
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,246 @@
|
||||
{
|
||||
"sbti": [
|
||||
{
|
||||
"code": "ATM-er",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/ATM-er.png"
|
||||
},
|
||||
{
|
||||
"code": "BOSS",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/BOSS.png"
|
||||
},
|
||||
{
|
||||
"code": "CTRL",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/CTRL.png"
|
||||
},
|
||||
{
|
||||
"code": "DEAD",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/DEAD.png"
|
||||
},
|
||||
{
|
||||
"code": "Dior-s",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/Dior-s.jpg"
|
||||
},
|
||||
{
|
||||
"code": "DRUNK",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/DRUNK.png"
|
||||
},
|
||||
{
|
||||
"code": "FAKE",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/FAKE.png"
|
||||
},
|
||||
{
|
||||
"code": "FUCK",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/FUCK.png"
|
||||
},
|
||||
{
|
||||
"code": "GOGO",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/GOGO.png"
|
||||
},
|
||||
{
|
||||
"code": "HHHH",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/HHHH.png"
|
||||
},
|
||||
{
|
||||
"code": "IMFW",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/IMFW.png"
|
||||
},
|
||||
{
|
||||
"code": "IMSB",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/IMSB.png"
|
||||
},
|
||||
{
|
||||
"code": "JOKE-R",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/JOKE-R.jpg"
|
||||
},
|
||||
{
|
||||
"code": "LOVE-R",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/LOVE-R.png"
|
||||
},
|
||||
{
|
||||
"code": "MALO",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/MALO.png"
|
||||
},
|
||||
{
|
||||
"code": "MONK",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/MONK.png"
|
||||
},
|
||||
{
|
||||
"code": "MUM",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/MUM.png"
|
||||
},
|
||||
{
|
||||
"code": "OH-NO",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/OH-NO.png"
|
||||
},
|
||||
{
|
||||
"code": "OJBK",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/OJBK.png"
|
||||
},
|
||||
{
|
||||
"code": "POOR",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/POOR.png"
|
||||
},
|
||||
{
|
||||
"code": "SEXY",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/SEXY.png"
|
||||
},
|
||||
{
|
||||
"code": "SHIT",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/SHIT.png"
|
||||
},
|
||||
{
|
||||
"code": "SOLO",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/SOLO.png"
|
||||
},
|
||||
{
|
||||
"code": "THAN-K",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/THAN-K.png"
|
||||
},
|
||||
{
|
||||
"code": "THIN-K",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/THIN-K.png"
|
||||
},
|
||||
{
|
||||
"code": "WOC",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/WOC.png"
|
||||
},
|
||||
{
|
||||
"code": "ZZZZ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/sbti/ZZZZ.png"
|
||||
}
|
||||
],
|
||||
"acgti": [
|
||||
{
|
||||
"code": "CIRN",
|
||||
"name": "琪露诺",
|
||||
"mbti": "ESTP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/CIRN.png"
|
||||
},
|
||||
{
|
||||
"code": "MIKT",
|
||||
"name": "御坂美琴",
|
||||
"mbti": "ESTJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/MIKT.png"
|
||||
},
|
||||
{
|
||||
"code": "MADK",
|
||||
"name": "鹿目圆",
|
||||
"mbti": "INFP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/MADK.png"
|
||||
},
|
||||
{
|
||||
"code": "FRNA",
|
||||
"name": "芙宁娜",
|
||||
"mbti": "ESFP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/FRNA.png"
|
||||
},
|
||||
{
|
||||
"code": "GGGG-A",
|
||||
"name": "高松灯",
|
||||
"mbti": "INFP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/GGGG-A.png"
|
||||
},
|
||||
{
|
||||
"code": "ANON",
|
||||
"name": "千早爱音",
|
||||
"mbti": "ESFJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/ANON.png"
|
||||
},
|
||||
{
|
||||
"code": "BAFE",
|
||||
"name": "要乐奈",
|
||||
"mbti": "ISTP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/BAFE.png"
|
||||
},
|
||||
{
|
||||
"code": "SOYO",
|
||||
"name": "长崎爽世",
|
||||
"mbti": "ISFJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/SOYO.png"
|
||||
},
|
||||
{
|
||||
"code": "RIKI",
|
||||
"name": "椎名立希",
|
||||
"mbti": "ESTJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/RIKI.png"
|
||||
},
|
||||
{
|
||||
"code": "SAKI",
|
||||
"name": "丰川祥子",
|
||||
"mbti": "ENTJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/SAKI.png"
|
||||
},
|
||||
{
|
||||
"code": "MRTS",
|
||||
"name": "若叶睦",
|
||||
"mbti": "ISTJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/MRTS.png"
|
||||
},
|
||||
{
|
||||
"code": "MRTS-X",
|
||||
"name": "Mortis",
|
||||
"mbti": "INTJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/MRTS-X.png"
|
||||
},
|
||||
{
|
||||
"code": "DLRS",
|
||||
"name": "三角初华 / Doloris",
|
||||
"mbti": "INFJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/DLRS.png"
|
||||
},
|
||||
{
|
||||
"code": "TMRS",
|
||||
"name": "八幡海铃 / Timoris",
|
||||
"mbti": "ISTJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/TMRS.png"
|
||||
},
|
||||
{
|
||||
"code": "AMRS",
|
||||
"name": "祐天寺若麦 / Amoris",
|
||||
"mbti": "ENTP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/AMRS.png"
|
||||
},
|
||||
{
|
||||
"code": "DINA",
|
||||
"name": "嘉然",
|
||||
"mbti": "ENFP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/DINA.png"
|
||||
},
|
||||
{
|
||||
"code": "TAFI",
|
||||
"name": "永雏塔菲",
|
||||
"mbti": "ENTP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/TAFI.png"
|
||||
},
|
||||
{
|
||||
"code": "NEUR",
|
||||
"name": "Neuro-sama",
|
||||
"mbti": "ENTP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/NEUR.png"
|
||||
},
|
||||
{
|
||||
"code": "EVIL",
|
||||
"name": "Evil Neuro",
|
||||
"mbti": "INTJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/EVIL.png"
|
||||
},
|
||||
{
|
||||
"code": "RINN",
|
||||
"name": "镜音铃",
|
||||
"mbti": "ESFP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/RINN.png"
|
||||
},
|
||||
{
|
||||
"code": "LTYI",
|
||||
"name": "洛天依",
|
||||
"mbti": "ISFP",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/LTYI.png"
|
||||
},
|
||||
{
|
||||
"code": "YCYO",
|
||||
"name": "月见八千代",
|
||||
"mbti": "ENFJ",
|
||||
"file": "https://fastly.jsdelivr.net/gh/SXP-Simon/profile_assets@main/acgti/characters/YCYO.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
+98
-112
@@ -64,8 +64,17 @@ from src.infrastructure.reporting.generators import ReportGenerator # noqa: E40
|
||||
|
||||
|
||||
class MockConfigManager:
|
||||
def __init__(self, template_name: str = "scrapbook") -> None:
|
||||
def __init__(
|
||||
self,
|
||||
template_name: str = "scrapbook",
|
||||
profile_mode: str = "mbti",
|
||||
profile_image_opacity: float = 0.20,
|
||||
profile_mapping_config: str = "",
|
||||
) -> None:
|
||||
self.template_name = template_name
|
||||
self.profile_mode = profile_mode
|
||||
self.profile_image_opacity = profile_image_opacity
|
||||
self.profile_mapping_config = profile_mapping_config
|
||||
|
||||
def get_report_template(self) -> str:
|
||||
return self.template_name
|
||||
@@ -74,16 +83,16 @@ class MockConfigManager:
|
||||
return 8
|
||||
|
||||
def get_max_user_titles(self) -> int:
|
||||
return 8
|
||||
return 16
|
||||
|
||||
def get_max_golden_quotes(self) -> int:
|
||||
return 8
|
||||
|
||||
def get_pdf_output_dir(self) -> str:
|
||||
return "data/pdf"
|
||||
def get_html_output_dir(self) -> str:
|
||||
return "data/html"
|
||||
|
||||
def get_pdf_filename_format(self) -> str:
|
||||
return "report_{group_id}_{date}.pdf"
|
||||
def get_html_filename_format(self) -> str:
|
||||
return "report_{group_id}_{date}.html"
|
||||
|
||||
def get_enable_user_card(self) -> bool:
|
||||
return True
|
||||
@@ -101,6 +110,21 @@ class MockConfigManager:
|
||||
def get_llm_max_concurrent(self) -> int:
|
||||
return 2
|
||||
|
||||
def get_profile_display_mode(self) -> str:
|
||||
return self.profile_mode
|
||||
|
||||
def get_profile_image_opacity(self) -> float:
|
||||
return self.profile_image_opacity
|
||||
|
||||
def get_profile_image_size_mode(self) -> str:
|
||||
return "contain"
|
||||
|
||||
def get_profile_mapping_config(self) -> str:
|
||||
return self.profile_mapping_config
|
||||
|
||||
def get_html_base_url(self) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
async def mock_get_user_avatar(user_id: str) -> str:
|
||||
# Return a known avatar URL for testing
|
||||
@@ -108,10 +132,15 @@ async def mock_get_user_avatar(user_id: str) -> str:
|
||||
|
||||
|
||||
async def debug_render(
|
||||
template_name: str, output_file: str = "debug_output.html"
|
||||
template_name: str,
|
||||
output_file: str = "debug_output.html",
|
||||
profile_mode: str = "mbti",
|
||||
) -> None:
|
||||
# 1. Setup Mock Data
|
||||
config_manager = MockConfigManager(template_name)
|
||||
config_manager = MockConfigManager(
|
||||
template_name=template_name,
|
||||
profile_mode=profile_mode,
|
||||
)
|
||||
|
||||
# 2. Mock Analysis Result using Data Models
|
||||
stats = GroupStatistics(
|
||||
@@ -177,81 +206,48 @@ async def debug_render(
|
||||
SummaryTopic(
|
||||
topic="新出的3A大作测评",
|
||||
contributors=["周八", "吴九"],
|
||||
detail="[123456789] 分享了最新游戏的通关体验,讨论了画面表现和剧情走向。",
|
||||
),
|
||||
SummaryTopic(
|
||||
topic="新出的3A大作测评",
|
||||
contributors=["周八", "吴九"],
|
||||
detail="[123456789] 分享了最新游戏的通关体验,讨论了画面表现和剧情走向。",
|
||||
),
|
||||
SummaryTopic(
|
||||
topic="新出的3A大作测评",
|
||||
contributors=["周八", "吴九"],
|
||||
detail="[123456789] 分享了最新游戏的通关体验,讨论了画面表现和剧情走向。",
|
||||
),
|
||||
SummaryTopic(
|
||||
topic="新出的3A大作测评",
|
||||
contributors=["周八", "吴九"],
|
||||
detail="[123456789] 分享了最新游戏的通关体验,讨论了画面表现和剧情走向。",
|
||||
),
|
||||
SummaryTopic(
|
||||
topic="新出的3A大作测评",
|
||||
contributors=["周八", "吴九"],
|
||||
detail="[123456789] 分享了最新游戏的通关体验,讨论了画面表现和剧情走向。",
|
||||
detail="[200000000] 开始尝试新的游戏,发现 INTJ 类型的玩家在策略游戏中非常吃香。",
|
||||
),
|
||||
]
|
||||
|
||||
user_titles = [
|
||||
UserTitle(
|
||||
name="张三",
|
||||
user_id="123456789",
|
||||
title="代码收割机",
|
||||
mbti="INTJ",
|
||||
reason="在短短一小时内提交了10个PR,效率惊人。",
|
||||
),
|
||||
UserTitle(
|
||||
name="李四",
|
||||
user_id="987654321",
|
||||
title="群聊气氛组",
|
||||
mbti="ENFP",
|
||||
reason="总能精准接住每一个冷笑话,让群里充满快活的气息。",
|
||||
),
|
||||
UserTitle(
|
||||
name="潜水员",
|
||||
user_id="112233445",
|
||||
title="深夜潜水员",
|
||||
mbti="INFP",
|
||||
reason="总是在凌晨三点出没,留下几句深奥的话语后消失。",
|
||||
),
|
||||
UserTitle(
|
||||
name="潜水员",
|
||||
user_id="112233445",
|
||||
title="深夜潜水员",
|
||||
mbti="INFP",
|
||||
reason="总是在凌晨三点出没,留下几句深奥的话语后消失。",
|
||||
),
|
||||
UserTitle(
|
||||
name="潜水员",
|
||||
user_id="112233445",
|
||||
title="深夜潜水员",
|
||||
mbti="INFP",
|
||||
reason="总是在凌晨三点出没,留下几句深奥的话语后消失。",
|
||||
),
|
||||
UserTitle(
|
||||
name="潜水员",
|
||||
user_id="112233445",
|
||||
title="深夜潜水员",
|
||||
mbti="INFP",
|
||||
reason="总是在凌晨三点出没,留下几句深奥的话语后消失。",
|
||||
),
|
||||
UserTitle(
|
||||
name="潜水员",
|
||||
user_id="112233445",
|
||||
title="深夜潜水员",
|
||||
mbti="INFP",
|
||||
reason="总是在凌晨三点出没,留下几句深奥的话语后消失。",
|
||||
),
|
||||
mbti_types = [
|
||||
"INTJ",
|
||||
"INTP",
|
||||
"ENTJ",
|
||||
"ENTP",
|
||||
"INFJ",
|
||||
"INFP",
|
||||
"ENFJ",
|
||||
"ENFP",
|
||||
"ISTJ",
|
||||
"ISFJ",
|
||||
"ESTJ",
|
||||
"ESTP",
|
||||
"ISTP",
|
||||
"ISFP",
|
||||
"ESFJ",
|
||||
"ESFP",
|
||||
]
|
||||
user_titles = []
|
||||
user_analysis = {
|
||||
"123456789": {"nickname": "张三"},
|
||||
"987654321": {"nickname": "李四"},
|
||||
"112233445": {"nickname": "潜水员"},
|
||||
}
|
||||
|
||||
for i, mbti in enumerate(mbti_types):
|
||||
uid = str(200000000 + i)
|
||||
uname = f"用户_{mbti}"
|
||||
user_titles.append(
|
||||
UserTitle(
|
||||
name=uname,
|
||||
user_id=uid,
|
||||
title=f"{mbti} 王者",
|
||||
mbti=mbti,
|
||||
reason=f"在日常交流中表现出极强的 {mbti} 特征,获得了大家的认可。",
|
||||
)
|
||||
)
|
||||
user_analysis[uid] = {"nickname": uname}
|
||||
|
||||
golden_quotes = [
|
||||
GoldenQuote(
|
||||
@@ -269,31 +265,7 @@ async def debug_render(
|
||||
GoldenQuote(
|
||||
content="PHP是世界上最好的语言!",
|
||||
sender="王五",
|
||||
reason="引发了长达3小时的群聊大讨论",
|
||||
user_id="112233445",
|
||||
),
|
||||
GoldenQuote(
|
||||
content="PHP是世界上最好的语言!",
|
||||
sender="王五",
|
||||
reason="引发了长达3小时的群聊大讨论",
|
||||
user_id="112233445",
|
||||
),
|
||||
GoldenQuote(
|
||||
content="PHP是世界上最好的语言!",
|
||||
sender="王五",
|
||||
reason="引发了长达3小时的群聊大讨论",
|
||||
user_id="112233445",
|
||||
),
|
||||
GoldenQuote(
|
||||
content="PHP是世界上最好的语言!",
|
||||
sender="王五",
|
||||
reason="引发了长达3小时的群聊大讨论",
|
||||
user_id="112233445",
|
||||
),
|
||||
GoldenQuote(
|
||||
content="PHP是世界上最好的语言!",
|
||||
sender="王五",
|
||||
reason="引发了长达3小时的群聊大讨论",
|
||||
reason="[200000001] 表示强烈赞同,并引发了后续的长篇大论。",
|
||||
user_id="112233445",
|
||||
),
|
||||
]
|
||||
@@ -305,15 +277,11 @@ async def debug_render(
|
||||
"statistics": stats,
|
||||
"topics": topics,
|
||||
"user_titles": user_titles,
|
||||
"user_analysis": {
|
||||
"123456789": {"nickname": "张三"},
|
||||
"987654321": {"nickname": "李四"},
|
||||
"112233445": {"nickname": "潜水员"},
|
||||
},
|
||||
"user_analysis": user_analysis,
|
||||
"chat_quality_review": stats.chat_quality_review,
|
||||
"analysis_date": "2026年02月11日",
|
||||
"group_id": "123456",
|
||||
"group_name": "测试群组",
|
||||
"group_name": "插件逻辑调试群",
|
||||
}
|
||||
|
||||
# 3. Initialize Generator
|
||||
@@ -321,6 +289,16 @@ async def debug_render(
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
generator = ReportGenerator(config_manager, data_dir)
|
||||
|
||||
# Mock avatar cache to avoid KeyError in debug mode
|
||||
class MockCache(dict):
|
||||
def __getitem__(self, key):
|
||||
return self.get(key, "")
|
||||
|
||||
def set(self, key, value, expire=None):
|
||||
self[key] = value
|
||||
|
||||
generator._avatar_cache = MockCache()
|
||||
|
||||
# 4. Prepare Render Data
|
||||
# Note: _prepare_render_data handles converting Entities to template-friendly dicts
|
||||
render_payload = await generator._prepare_render_data(
|
||||
@@ -340,7 +318,7 @@ async def debug_render(
|
||||
await generator.close()
|
||||
|
||||
print(
|
||||
f"Successfully rendered template '{template_name}' to {output_path.absolute()}"
|
||||
f"Successfully rendered template '{template_name}' in mode '{profile_mode}' to {output_path.absolute()}"
|
||||
)
|
||||
print("You can now open this file with your browser to debug your HTML/CSS.")
|
||||
|
||||
@@ -363,9 +341,17 @@ def main() -> None:
|
||||
default="debug_output.html",
|
||||
help="Output HTML file path (default: debug_output.html)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-m",
|
||||
"--mode",
|
||||
type=str,
|
||||
default="mbti",
|
||||
choices=["mbti", "sbti", "acgti"],
|
||||
help="Profile display mode to render (default: mbti)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
asyncio.run(debug_render(args.template, args.output))
|
||||
asyncio.run(debug_render(args.template, args.output, args.mode))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
] %}
|
||||
{% 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">
|
||||
<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 }}"
|
||||
@@ -28,12 +28,15 @@
|
||||
</span>
|
||||
</div>
|
||||
<div class="item-content">
|
||||
<p><strong>MBTI:</strong> {{ title.mbti }}</p>
|
||||
<p><strong>人格标签:</strong> {{ title.profile_display or title.mbti }}</p>
|
||||
<p><strong>称号理由:</strong> {{ title.reason }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<img class="item-emoji title-emoji" src="{{ title_emojis | random }}" alt="称号表情">
|
||||
{% 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,11 +14,14 @@
|
||||
<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>
|
||||
|
||||
@@ -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,11 +15,14 @@
|
||||
<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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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,13 +26,16 @@
|
||||
<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>
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
<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>
|
||||
|
||||
@@ -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,12 +14,15 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user