优化社交关系图视觉风格并加入轻科幻元素

This commit is contained in:
liuwei
2026-04-27 09:51:31 +08:00
parent f7a5096b3d
commit d2c766eec9
2 changed files with 133 additions and 21 deletions

View File

@@ -1462,6 +1462,7 @@ class ValueRankPlugin(MessagePluginInterface):
cm = ContactManager.get_instance() cm = ContactManager.get_instance()
strongest_uid = selected_nodes[0] if selected_nodes else "" strongest_uid = selected_nodes[0] if selected_nodes else ""
edge_defs_parts: List[str] = []
edge_svg_parts: List[str] = [] edge_svg_parts: List[str] = []
# 边数字标签只给“最重要的少数边”: # 边数字标签只给“最重要的少数边”:
# 1. 全边标数字时,图面会被 0/1、1/0 这类噪声淹没; # 1. 全边标数字时,图面会被 0/1、1/0 这类噪声淹没;
@@ -1509,11 +1510,29 @@ class ValueRankPlugin(MessagePluginInterface):
ax, ay = pos_map[a] ax, ay = pos_map[a]
bx, by = pos_map[b] bx, by = pos_map[b]
normalized = max(0.12, min(score / max_edge_score, 1.0)) normalized = max(0.12, min(score / max_edge_score, 1.0))
stroke_width = 1.2 + 6.2 * normalized stroke_width = 1.35 + 6.5 * normalized
opacity = 0.14 + 0.42 * normalized glow_width = stroke_width + 3.6
opacity = 0.20 + 0.48 * normalized
glow_opacity = 0.10 + 0.22 * normalized
edge_gradient_id = f"edge_gradient_{edge_idx}"
start_color = "#50D6FF"
end_color = "#FFBB5C" if (a == strongest_uid or b == strongest_uid) else "#6C92FF"
edge_defs_parts.append(
f'<linearGradient id="{edge_gradient_id}" gradientUnits="userSpaceOnUse" '
f'x1="{ax:.1f}" y1="{ay:.1f}" x2="{bx:.1f}" y2="{by:.1f}">'
f'<stop offset="0%" stop-color="{start_color}" stop-opacity="0.96"></stop>'
f'<stop offset="100%" stop-color="{end_color}" stop-opacity="0.90"></stop>'
f'</linearGradient>'
)
edge_svg_parts.append( edge_svg_parts.append(
f'<line x1="{ax:.1f}" y1="{ay:.1f}" x2="{bx:.1f}" y2="{by:.1f}" ' f'<line x1="{ax:.1f}" y1="{ay:.1f}" x2="{bx:.1f}" y2="{by:.1f}" '
f'stroke="rgba(33, 150, 243, {opacity:.3f})" stroke-width="{stroke_width:.2f}" />' f'stroke="rgba(84, 214, 255, {glow_opacity:.3f})" stroke-width="{glow_width:.2f}" '
f'stroke-linecap="round" filter="url(#edgeGlow)" />'
)
edge_svg_parts.append(
f'<line x1="{ax:.1f}" y1="{ay:.1f}" x2="{bx:.1f}" y2="{by:.1f}" '
f'stroke="url(#{edge_gradient_id})" stroke-opacity="{opacity:.3f}" '
f'stroke-width="{stroke_width:.2f}" stroke-linecap="round" />'
) )
if (a, b) not in labeled_edge_keys: if (a, b) not in labeled_edge_keys:
continue continue
@@ -1541,8 +1560,9 @@ class ValueRankPlugin(MessagePluginInterface):
edge_label_parts.append( edge_label_parts.append(
f'<g transform="translate({label_x:.1f},{label_y:.1f}) rotate({angle_deg:.1f})">' f'<g transform="translate({label_x:.1f},{label_y:.1f}) rotate({angle_deg:.1f})">'
f'<rect x="-{label_width / 2:.1f}" y="-11" width="{label_width:.1f}" height="22" ' f'<rect x="-{label_width / 2:.1f}" y="-11" width="{label_width:.1f}" height="22" '
f'rx="7" ry="7" fill="rgba(255,255,255,0.84)"></rect>' f'rx="7" ry="7" fill="rgba(16,38,74,0.68)" stroke="rgba(84,214,255,0.34)" stroke-width="1.0" '
f'<text x="0" y="4" text-anchor="middle" font-size="11" fill="#4E5F7D">{safe_label}</text>' f'filter="url(#panelSoftGlow)"></rect>'
f'<text x="0" y="4" text-anchor="middle" font-size="11" fill="#EAF7FF">{safe_label}</text>'
f'</g>' f'</g>'
) )
@@ -1566,10 +1586,20 @@ class ValueRankPlugin(MessagePluginInterface):
ring_idx = int(node_meta_map.get(uid, {}).get("ring_idx", 0)) ring_idx = int(node_meta_map.get(uid, {}).get("ring_idx", 0))
# 使用“连接度越高环线越偏暖”的视觉策略,帮助快速识别核心节点。 # 使用“连接度越高环线越偏暖”的视觉策略,帮助快速识别核心节点。
ring_color = "rgba(255, 152, 0, 0.95)" if size_norm >= 0.6 else "rgba(79, 123, 201, 0.95)" ring_color = "rgba(255, 176, 58, 0.96)" if size_norm >= 0.6 else "rgba(74, 141, 255, 0.96)"
glow_color = "rgba(255, 205, 111, 0.30)" if size_norm >= 0.6 else "rgba(84, 214, 255, 0.26)"
orbit_color = "rgba(255, 211, 130, 0.74)" if size_norm >= 0.6 else "rgba(113, 228, 255, 0.72)"
node_svg_parts.append( node_svg_parts.append(
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{node_radius + 2.2:.1f}" fill="rgba(255,255,255,0.92)" ' f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{node_radius + 7.4:.1f}" fill="{glow_color}" '
f'stroke="{ring_color}" stroke-width="3.2"></circle>' f'filter="url(#nodeGlow)"></circle>'
)
node_svg_parts.append(
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{node_radius + 2.8:.1f}" fill="rgba(255,255,255,0.88)" '
f'stroke="{ring_color}" stroke-width="3.4"></circle>'
)
node_svg_parts.append(
f'<circle cx="{x:.1f}" cy="{y:.1f}" r="{node_radius + 5.0:.1f}" fill="none" '
f'stroke="{orbit_color}" stroke-width="1.2" stroke-dasharray="7 5" stroke-linecap="round" opacity="0.78"></circle>'
) )
if avatar_url: if avatar_url:
@@ -1613,7 +1643,7 @@ class ValueRankPlugin(MessagePluginInterface):
] ]
crown_points_text = " ".join([f"{px:.1f},{py:.1f}" for px, py in crown_points]) crown_points_text = " ".join([f"{px:.1f},{py:.1f}" for px, py in crown_points])
node_svg_parts.append( node_svg_parts.append(
f'<g>' f'<g filter="url(#crownGlow)">'
f'<rect x="{x - 15.0:.1f}" y="{crown_y + 18.0:.1f}" width="30.0" height="5.5" rx="2.8" ry="2.8" ' f'<rect x="{x - 15.0:.1f}" y="{crown_y + 18.0:.1f}" width="30.0" height="5.5" rx="2.8" ry="2.8" '
f'fill="rgba(255, 183, 3, 0.98)" stroke="rgba(168, 104, 0, 0.85)" stroke-width="1.0"></rect>' f'fill="rgba(255, 183, 3, 0.98)" stroke="rgba(168, 104, 0, 0.85)" stroke-width="1.0"></rect>'
f'<polygon points="{crown_points_text}" fill="rgba(255, 202, 40, 0.98)" ' f'<polygon points="{crown_points_text}" fill="rgba(255, 202, 40, 0.98)" '
@@ -1636,11 +1666,12 @@ class ValueRankPlugin(MessagePluginInterface):
badge_y = y + node_radius * 0.55 - badge_height / 2.0 badge_y = y + node_radius * 0.55 - badge_height / 2.0
node_svg_parts.append( node_svg_parts.append(
f'<rect x="{badge_x:.1f}" y="{badge_y:.1f}" width="{badge_width:.1f}" height="{badge_height:.1f}" ' f'<rect x="{badge_x:.1f}" y="{badge_y:.1f}" width="{badge_width:.1f}" height="{badge_height:.1f}" '
f'rx="10" ry="10" fill="rgba(255,255,255,0.95)" stroke="{ring_color}" stroke-width="1.9"></rect>' f'rx="10" ry="10" fill="rgba(17, 41, 79, 0.84)" stroke="{ring_color}" stroke-width="1.6" '
f'filter="url(#panelSoftGlow)"></rect>'
) )
node_svg_parts.append( node_svg_parts.append(
f'<text x="{badge_x + badge_width / 2.0:.1f}" y="{badge_y + 13.2:.1f}" text-anchor="middle" ' f'<text x="{badge_x + badge_width / 2.0:.1f}" y="{badge_y + 13.2:.1f}" text-anchor="middle" '
f'font-size="{badge_font_size}" fill="#2F3B52" font-weight="700">{badge_text}</text>' f'font-size="{badge_font_size}" fill="#F4FBFF" font-weight="700">{badge_text}</text>'
) )
# 名字固定放在头像正下方,避免沿径向排布带来的“漂移感”: # 名字固定放在头像正下方,避免沿径向排布带来的“漂移感”:
@@ -1657,11 +1688,12 @@ class ValueRankPlugin(MessagePluginInterface):
label_box_y = title_y - 18.0 label_box_y = title_y - 18.0
node_svg_parts.append( node_svg_parts.append(
f'<rect x="{label_box_x:.1f}" y="{label_box_y:.1f}" width="{label_width:.1f}" height="{label_height:.1f}" ' f'<rect x="{label_box_x:.1f}" y="{label_box_y:.1f}" width="{label_width:.1f}" height="{label_height:.1f}" '
f'rx="12" ry="12" fill="rgba(255,255,255,0.78)"></rect>' f'rx="12" ry="12" fill="rgba(14, 33, 66, 0.66)" stroke="rgba(84,214,255,0.24)" stroke-width="1.0" '
f'filter="url(#panelSoftGlow)"></rect>'
) )
node_svg_parts.append( node_svg_parts.append(
f'<text x="{title_x:.1f}" y="{title_y:.1f}" text-anchor="{anchor}" ' f'<text x="{title_x:.1f}" y="{title_y:.1f}" text-anchor="{anchor}" '
f'font-size="14.5" fill="#2F3B52" font-weight="700">{safe_nick}</text>' f'font-size="14.5" fill="#F2FAFF" font-weight="700">{safe_nick}</text>'
) )
group_title = html.escape(ContactManager.get_instance().get_nickname(group_id) or group_id) group_title = html.escape(ContactManager.get_instance().get_nickname(group_id) or group_id)
@@ -1693,6 +1725,7 @@ class ValueRankPlugin(MessagePluginInterface):
"__HEIGHT__": str(height), "__HEIGHT__": str(height),
"__GROUP_TITLE__": group_title, "__GROUP_TITLE__": group_title,
"__SUMMARY_TEXT__": summary_text, "__SUMMARY_TEXT__": summary_text,
"__EDGE_DEFS__": "".join(edge_defs_parts),
"__EDGE_SVG__": "".join(edge_svg_parts), "__EDGE_SVG__": "".join(edge_svg_parts),
"__EDGE_LABELS__": "".join(edge_label_parts), "__EDGE_LABELS__": "".join(edge_label_parts),
"__NODE_DEFS__": "".join(node_defs_parts), "__NODE_DEFS__": "".join(node_defs_parts),

View File

@@ -3,9 +3,18 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<style> <style>
:root {
--panel-blue: #16325f;
--panel-cyan: #52d7ff;
--panel-ice: #e8f6ff;
--panel-amber: #ffb347;
}
body { body {
margin: 0; margin: 0;
background: linear-gradient(135deg, #f7fbff 0%, #f2f7ff 45%, #fff8ed 100%); background:
radial-gradient(circle at 18% 18%, rgba(86, 227, 255, 0.22) 0%, rgba(86, 227, 255, 0.00) 28%),
radial-gradient(circle at 84% 16%, rgba(255, 195, 108, 0.18) 0%, rgba(255, 195, 108, 0.00) 26%),
linear-gradient(135deg, #eef7ff 0%, #edf5ff 44%, #fef7ec 100%);
font-family: "Microsoft YaHei", "PingFang SC", sans-serif; font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
} }
.card { .card {
@@ -16,22 +25,28 @@
} }
.title { .title {
font-size: 38px; font-size: 38px;
color: #1f2d46; color: #1c2b52;
font-weight: 800; font-weight: 800;
letter-spacing: 1px; letter-spacing: 1px;
text-shadow: 0 0 18px rgba(82, 215, 255, 0.14);
} }
.subtitle { .subtitle {
margin-top: 8px; margin-top: 8px;
font-size: 17px; font-size: 17px;
color: #5f6f8a; color: #5c708f;
} }
.graph-wrap { .graph-wrap {
margin-top: 18px; margin-top: 18px;
border-radius: 18px; border-radius: 24px;
background: rgba(255, 255, 255, 0.82); background:
border: 1px solid rgba(207, 221, 246, 0.9); linear-gradient(180deg, rgba(255,255,255,0.92) 0%, rgba(243,250,255,0.88) 54%, rgba(255,249,240,0.90) 100%);
box-shadow: 0 8px 24px rgba(58, 82, 130, 0.12); border: 1px solid rgba(157, 221, 255, 0.72);
box-shadow:
0 10px 32px rgba(34, 60, 108, 0.10),
0 0 0 1px rgba(255,255,255,0.42) inset,
0 0 48px rgba(82, 215, 255, 0.10);
overflow: hidden; overflow: hidden;
position: relative;
} }
.legend { .legend {
margin-top: 14px; margin-top: 14px;
@@ -48,10 +63,74 @@
<div class="subtitle">__SUMMARY_TEXT__</div> <div class="subtitle">__SUMMARY_TEXT__</div>
<div class="graph-wrap"> <div class="graph-wrap">
<svg width="__WIDTH__" height="__HEIGHT__" viewBox="0 0 __WIDTH__ __HEIGHT__"> <svg width="__WIDTH__" height="__HEIGHT__" viewBox="0 0 __WIDTH__ __HEIGHT__">
<rect x="0" y="0" width="__WIDTH__" height="__HEIGHT__" fill="rgba(247,251,255,0.72)"></rect>
<defs> <defs>
<linearGradient id="panelBgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#f8fdff"></stop>
<stop offset="56%" stop-color="#edf7ff"></stop>
<stop offset="100%" stop-color="#fff8ef"></stop>
</linearGradient>
<radialGradient id="panelGlowLeft" cx="20%" cy="18%" r="36%">
<stop offset="0%" stop-color="#52D7FF" stop-opacity="0.42"></stop>
<stop offset="100%" stop-color="#52D7FF" stop-opacity="0"></stop>
</radialGradient>
<radialGradient id="panelGlowRight" cx="84%" cy="15%" r="30%">
<stop offset="0%" stop-color="#FFC16C" stop-opacity="0.26"></stop>
<stop offset="100%" stop-color="#FFC16C" stop-opacity="0"></stop>
</radialGradient>
<pattern id="techGrid" width="32" height="32" patternUnits="userSpaceOnUse">
<path d="M 32 0 L 0 0 0 32" fill="none" stroke="rgba(110,168,214,0.10)" stroke-width="1"></path>
<circle cx="0" cy="0" r="1.4" fill="rgba(82,215,255,0.16)"></circle>
</pattern>
<filter id="edgeGlow" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="3.2" result="blur"></feGaussianBlur>
<feMerge>
<feMergeNode in="blur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<filter id="nodeGlow" x="-80%" y="-80%" width="260%" height="260%">
<feGaussianBlur stdDeviation="6" result="blur"></feGaussianBlur>
<feColorMatrix
in="blur"
type="matrix"
values="1 0 0 0 0
0 1 0 0 0
0 0 1 0 0
0 0 0 0.36 0"></feColorMatrix>
<feMerge>
<feMergeNode></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<filter id="panelSoftGlow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="2.2" result="blur"></feGaussianBlur>
<feMerge>
<feMergeNode in="blur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
<filter id="crownGlow" x="-60%" y="-60%" width="220%" height="220%">
<feGaussianBlur stdDeviation="2.6" result="blur"></feGaussianBlur>
<feMerge>
<feMergeNode in="blur"></feMergeNode>
<feMergeNode in="SourceGraphic"></feMergeNode>
</feMerge>
</filter>
__EDGE_DEFS__
__NODE_DEFS__ __NODE_DEFS__
</defs> </defs>
<rect x="0" y="0" width="__WIDTH__" height="__HEIGHT__" fill="url(#panelBgGradient)"></rect>
<rect x="0" y="0" width="__WIDTH__" height="__HEIGHT__" fill="url(#techGrid)"></rect>
<ellipse cx="250" cy="160" rx="240" ry="140" fill="url(#panelGlowLeft)"></ellipse>
<ellipse cx="1280" cy="150" rx="220" ry="126" fill="url(#panelGlowRight)"></ellipse>
<path d="M46 54 L126 54" stroke="rgba(84,212,255,0.78)" stroke-width="3" stroke-linecap="round"></path>
<path d="M46 54 L46 134" stroke="rgba(84,212,255,0.34)" stroke-width="2" stroke-linecap="round"></path>
<path d="M1412 54 L1492 54" stroke="rgba(255,190,98,0.56)" stroke-width="3" stroke-linecap="round"></path>
<path d="M1492 54 L1492 134" stroke="rgba(255,190,98,0.26)" stroke-width="2" stroke-linecap="round"></path>
<path d="M46 1066 L126 1066" stroke="rgba(84,212,255,0.42)" stroke-width="3" stroke-linecap="round"></path>
<path d="M46 986 L46 1066" stroke="rgba(84,212,255,0.20)" stroke-width="2" stroke-linecap="round"></path>
<path d="M1412 1066 L1492 1066" stroke="rgba(255,190,98,0.38)" stroke-width="3" stroke-linecap="round"></path>
<path d="M1492 986 L1492 1066" stroke="rgba(255,190,98,0.18)" stroke-width="2" stroke-linecap="round"></path>
__EDGE_SVG__ __EDGE_SVG__
__EDGE_LABELS__ __EDGE_LABELS__
__NODE_SVG__ __NODE_SVG__