feat(value_rank): 社交关系图支持群友头像节点渲染
- 关系图节点优先使用 ContactManager 头像地址渲染,缺失头像自动回退昵称首字 - 新增 SVG clipPath 头像裁剪层与节点边框视觉策略,提升核心人物识别度 - 模板新增 __NODE_DEFS__ 占位符,支持头像裁剪定义注入
This commit is contained in:
@@ -1314,8 +1314,12 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
f'stroke="rgba(33, 150, 243, {opacity:.3f})" stroke-width="{stroke_width:.2f}" />'
|
||||
)
|
||||
|
||||
# 节点头像层拆分为 defs + body 两段:
|
||||
# 1. defs 内定义每个节点的裁剪路径,避免头像越界;
|
||||
# 2. body 里再引用 image/圆环/文案,便于模板层做结构化插槽替换。
|
||||
node_defs_parts: List[str] = []
|
||||
node_svg_parts: List[str] = []
|
||||
for uid in selected_nodes:
|
||||
for idx, uid in enumerate(selected_nodes, start=1):
|
||||
x, y = pos_map[uid]
|
||||
partner_count = len(partner_map.get(uid, set()))
|
||||
score = float(node_score_map.get(uid, 0.0))
|
||||
@@ -1323,11 +1327,39 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
node_radius = 18.0 + 24.0 * size_norm
|
||||
nick = cm.get_group_name(group_id, uid) or uid
|
||||
safe_nick = html.escape(str(nick))
|
||||
avatar_url = str(cm.get_head_image(uid) or "").strip()
|
||||
|
||||
# 使用“连接度越高环线越偏暖”的视觉策略,帮助快速识别核心节点。
|
||||
ring_color = "rgba(255, 152, 0, 0.95)" if size_norm >= 0.6 else "rgba(79, 123, 201, 0.95)"
|
||||
node_svg_parts.append(
|
||||
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{node_radius:.1f}" fill="rgba(255, 193, 7, 0.90)" '
|
||||
f'stroke="rgba(255, 152, 0, 0.95)" stroke-width="2.2"></circle>'
|
||||
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{node_radius + 2.2:.1f}" fill="rgba(255,255,255,0.92)" '
|
||||
f'stroke="{ring_color}" stroke-width="3.2"></circle>'
|
||||
)
|
||||
|
||||
if avatar_url:
|
||||
# 有头像时,使用 SVG clipPath 裁剪成圆形头像,既美观又保持节点尺寸可变。
|
||||
safe_avatar_url = html.escape(avatar_url, quote=True)
|
||||
clip_id = f"avatar_clip_{idx}"
|
||||
avatar_r = max(node_radius - 1.8, 8.0)
|
||||
node_defs_parts.append(
|
||||
f'<clipPath id="{clip_id}"><circle cx="{x:.1f}" cy="{y:.1f}" r="{avatar_r:.1f}" /></clipPath>'
|
||||
)
|
||||
node_svg_parts.append(
|
||||
f'<image href="{safe_avatar_url}" x="{x - avatar_r:.1f}" y="{y - avatar_r:.1f}" '
|
||||
f'width="{avatar_r * 2:.1f}" height="{avatar_r * 2:.1f}" clip-path="url(#{clip_id})" '
|
||||
f'preserveAspectRatio="xMidYMid slice"></image>'
|
||||
)
|
||||
else:
|
||||
# 无头像时回退为字符节点,保证图谱渲染完整可用。
|
||||
node_svg_parts.append(
|
||||
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{node_radius - 1.8:.1f}" fill="rgba(255, 193, 7, 0.90)"></circle>'
|
||||
)
|
||||
node_svg_parts.append(
|
||||
f'<text x="{x:.1f}" y="{y + 6:.1f}" text-anchor="middle" '
|
||||
f'font-size="{max(12, int(node_radius * 0.55))}" fill="#2F3B52" font-weight="700">'
|
||||
f'{html.escape(str(nick)[:1] or "?")}</text>'
|
||||
)
|
||||
|
||||
node_svg_parts.append(
|
||||
f'<text x="{x:.1f}" y="{y + node_radius + 22:.1f}" text-anchor="middle" '
|
||||
f'font-size="19" fill="#2F3B52" font-weight="700">{safe_nick}</text>'
|
||||
@@ -1367,6 +1399,7 @@ class ValueRankPlugin(MessagePluginInterface):
|
||||
"__GROUP_TITLE__": group_title,
|
||||
"__SUMMARY_TEXT__": summary_text,
|
||||
"__EDGE_SVG__": "".join(edge_svg_parts),
|
||||
"__NODE_DEFS__": "".join(node_defs_parts),
|
||||
"__NODE_SVG__": "".join(node_svg_parts),
|
||||
}
|
||||
for key, value in replace_map.items():
|
||||
|
||||
Reference in New Issue
Block a user