topo-engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -0
- package/dist/style.css +494 -0
- package/dist/topo-engine.cjs +28416 -0
- package/dist/topo-engine.cjs.map +1 -0
- package/dist/topo-engine.js +28416 -0
- package/dist/topo-engine.js.map +1 -0
- package/dist/topo-engine.umd.cjs +28418 -0
- package/dist/topo-engine.umd.cjs.map +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# 配电台区单线图拓扑成图引擎
|
|
2
|
+
|
|
3
|
+
基于 **Vue3 + AntV G6 v5 + Vite(Node)** 的配电台区单线图拓扑成图引擎。
|
|
4
|
+
解析国网 PMS 台区拓扑 JSON,在 Web 端以 **深色 SCADA / SVG 单线图风格** 渲染**横平竖直、0 交叉**的树状拓扑图。
|
|
5
|
+
|
|
6
|
+
## 功能特性
|
|
7
|
+
|
|
8
|
+
- 🔍 **完整的拓扑查询 API** - 节点查询、路径查找、邻居关系等
|
|
9
|
+
- 🎨 **丰富的样式控制** - 节点样式、边样式、高亮标注
|
|
10
|
+
- ✨ **动画效果** - 节点脉动、边流动动画
|
|
11
|
+
- 📝 **文字标注** - 附加文字、用户名显示控制
|
|
12
|
+
- 🃏 **卡片盒** - 节点信息弹窗
|
|
13
|
+
- 🔗 **连接线/拓扑线** - 动态连接线、拓扑高亮线
|
|
14
|
+
- 🎧 **事件系统** - 节点点击、悬停等事件监听
|
|
15
|
+
- 💾 **导出功能** - PNG 导出、数据导出
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
# npm
|
|
21
|
+
npm install topo-engine
|
|
22
|
+
|
|
23
|
+
# yarn
|
|
24
|
+
yarn add topo-engine
|
|
25
|
+
|
|
26
|
+
# pnpm
|
|
27
|
+
pnpm add topo-engine
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 快速开始
|
|
31
|
+
|
|
32
|
+
### 1. 作为 Vue 组件使用
|
|
33
|
+
|
|
34
|
+
```vue
|
|
35
|
+
<template>
|
|
36
|
+
<TopoGraph
|
|
37
|
+
:data-url="dataUrl"
|
|
38
|
+
@node-select="onNodeSelect"
|
|
39
|
+
@loaded="onLoaded"
|
|
40
|
+
/>
|
|
41
|
+
</template>
|
|
42
|
+
|
|
43
|
+
<script setup>
|
|
44
|
+
import { ref } from 'vue';
|
|
45
|
+
import { TopoGraph } from 'topo-engine';
|
|
46
|
+
|
|
47
|
+
const dataUrl = ref('/data/topology.json');
|
|
48
|
+
|
|
49
|
+
const onNodeSelect = (payload) => {
|
|
50
|
+
console.log('选中节点:', payload.node);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const onLoaded = ({ stats, title }) => {
|
|
54
|
+
console.log('加载完成:', title, stats);
|
|
55
|
+
};
|
|
56
|
+
</script>
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. 作为独立 API 使用
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
import { TopoApi } from 'topo-engine';
|
|
63
|
+
|
|
64
|
+
// 假设你已经有一个 G6 Graph 实例
|
|
65
|
+
const graph = new Graph({ /* ... */ });
|
|
66
|
+
|
|
67
|
+
// 加载数据后创建 TopoApi 实例
|
|
68
|
+
const topoApi = new TopoApi(graph, model, layout, nodeByIdMap);
|
|
69
|
+
|
|
70
|
+
// 使用 API
|
|
71
|
+
const nodes = topoApi.getAllNodes();
|
|
72
|
+
const path = topoApi.getPath('node1', 'node2');
|
|
73
|
+
topoApi.highlightPath(['node1', 'node2', 'node3']);
|
|
74
|
+
topoApi.setEdgeFlow('node1', 'node2', { color: '#FF0000' });
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 运行
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npm install # 安装依赖
|
|
81
|
+
npm run dev # 开发服务器 → http://127.0.0.1:5173/
|
|
82
|
+
npm run build # 生产构建 → dist/
|
|
83
|
+
npm run build:lib # 构建库文件
|
|
84
|
+
npm run preview # 预览生产构建
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 数据
|
|
88
|
+
|
|
89
|
+
输入:`public/data/上屋胡2台区单线图.json`(源文件 `上屋胡2台区单线图.json` 的副本,UTF-8)。
|
|
90
|
+
|
|
91
|
+
数据结构:
|
|
92
|
+
|
|
93
|
+
- `data.nodes[]` — 设备/线路节点:`{ id, psrType, symbolId, name, psrId, status }`
|
|
94
|
+
- `data.relationships[]` — 有向边:`{ from, to }`(电源 → 负荷,103 节点 102 边构成树)
|
|
95
|
+
- `data.stations[]` — 台区根(低压配电箱),`contain` 列出箱内设备 psrId
|
|
96
|
+
|
|
97
|
+
psrType → 图元映射(见 `src/engine/parser.js`):
|
|
98
|
+
|
|
99
|
+
| psrType | 设备 | 图元样式 |
|
|
100
|
+
| ------- | ---- | -------- |
|
|
101
|
+
| 0302 | 配电变压器 | 橙圆 + 红圈(SVG 2030020) |
|
|
102
|
+
| 3301 | 开关/熔丝 | 红色矩形(4030010 / 2100010) |
|
|
103
|
+
| 3303 | 低压母线 | 白色短粗线 |
|
|
104
|
+
| 3202 | 电缆终端头 | 红色三角(3030010) |
|
|
105
|
+
| 310100000 | 低压导线 | 青色连接点 |
|
|
106
|
+
| 3112 | 计量箱 | 黑底白框 "JX"(5040010) |
|
|
107
|
+
| 3218000 | 低压用户接入点 | 白圈 "J"(5010030) |
|
|
108
|
+
| 32500000 | 低压配电箱(台区) | — |
|
|
109
|
+
|
|
110
|
+
## 代码结构
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
src/
|
|
114
|
+
engine/
|
|
115
|
+
parser.js # JSON → 图模型(建树、找根、短名提取、类型映射)
|
|
116
|
+
layout.js # ★ 主干交替式正交布局引擎(0 交叉)+ 数学校验器
|
|
117
|
+
orthEdge.js # ★ 自定义正交折线边(直接用布局引擎的完整点序列绘制)
|
|
118
|
+
canvasNode.js # ★ 自定义 canvas 图元节点(@antv/g 原生图形,与边同场景图)
|
|
119
|
+
symbols.js # SVG 风格图元规格(尺寸/配色,供 canvasNode 与图例使用)
|
|
120
|
+
graphData.js # 模型 → G6 数据(节点/边样式、显式正交点序列)
|
|
121
|
+
components/
|
|
122
|
+
TopoGraph.vue # G6 图组件(布局调用、自定义边/节点、tooltip、点击联动)
|
|
123
|
+
SidePanel.vue # 设备详情 / 台区概览
|
|
124
|
+
LegendBar.vue # 图例
|
|
125
|
+
App.vue # 布局与数据加载
|
|
126
|
+
style.css
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## 渲染要点(强参考 sample/*.svg)
|
|
130
|
+
|
|
131
|
+
- 黑色画布背景(`#050505`,对照 SVG `BackGround_Layer` 的 `rgb(0,0,0)`)
|
|
132
|
+
- 导线双色:干线白色、支线灰色(对照 SVG 的 `rgb(255,255,255)` / `rgb(128,128,128)`)
|
|
133
|
+
- 图元配色与几何逐项对照 sample SVG 的 `<symbol>` 定义
|
|
134
|
+
- **正交布局(0 交叉,见 `src/engine/layout.js`)—— 单向直走式**:
|
|
135
|
+
- 主干 = 从根沿**最长路径**前进(每步选到最深叶子距离最大的子节点),主干横向铺开
|
|
136
|
+
- 主干节点的其余子节点为侧分支,交替挂下方/上方,带内按内容盒 x 排布
|
|
137
|
+
- 侧分支 = **垂直列式(选定方向后不拐弯)**:分支内每步同样选最长路径子节点**同列垂直直走**,
|
|
138
|
+
其余子节点独占新列(同样垂直直走),列内递归同规则——整张图只有横向主干 + 垂直列,无方向交替拐弯
|
|
139
|
+
- 边路由:同向链 = 直线;跨列/跨带 = L 形(垂直段 + 水平带线,带线统一高度)
|
|
140
|
+
- 数学验证:`assertOrthogonal`(全线段横平竖直)+ `checkZeroCrossing`(0 交叉、0 压线、0 节点重叠)
|
|
141
|
+
- 实测:102 条边端点全部落在节点边界、0 非正交段;分支链节点同列直走(如 K02 链 5 节点 x 完全一致)
|
|
142
|
+
- 交互:滚轮缩放、拖拽平移、悬停 tooltip(设备/名称/编码/状态)、点击选中(金色虚框 + 右侧详情)
|
|
143
|
+
|
|
144
|
+
## 布局参数(`TopoGraph.vue` → `computeOrthogonalLayout`)
|
|
145
|
+
|
|
146
|
+
| 参数 | 默认 | 含义 |
|
|
147
|
+
| ---- | ---- | ---- |
|
|
148
|
+
| nodeGap | 26 | 主干相邻节点边界间距 |
|
|
149
|
+
| band | 50 | 主干轴线到分支带(带线)的距离 |
|
|
150
|
+
| packGap | 22 | 分支带内兄弟子树间距 |
|
|
151
|
+
| sideGap | 40 | 相邻主干节点分支带之间的隔离间距 |
|
|
152
|
+
| margin | 40 | 画布外框留白 |
|
|
153
|
+
|
|
154
|
+
## 备注
|
|
155
|
+
|
|
156
|
+
- 布局由自研引擎显式给定节点坐标与边路径点(`orth-polyline` 边直接使用完整点序列,
|
|
157
|
+
首尾即真实边界端点,绕开 G6 内置边对 HTML 节点失效的 `getIntersectPoint`)。
|
|
158
|
+
- **节点为自定义 canvas 节点 `svg-symbol`**(`canvasNode.js`):用 @antv/g 原生图形绘制图元,
|
|
159
|
+
与边同在场景图、同一相机 —— 规避 G6 HTML 节点(DOM 覆盖层)不跟随相机变换导致的
|
|
160
|
+
fitView/缩放后节点与边错位问题(实测:HTML 节点 DOM 定位停留在世界坐标、无缩放平移)。
|
|
161
|
+
- key shape 为透明 Rect(`fill: rgba(255,255,255,0.01)`,alpha=0 不参与命中测试),
|
|
162
|
+
图元/标签/选中高亮框作为其子元素绘制。
|
|
163
|
+
- 图元采用 G6 `html` 节点 + 内联 SVG 字符串渲染,文字(宋体)清晰且与 SVG 风格一致。
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
|
|
2
|
+
.topo-wrap[data-v-1f2e13bc] {
|
|
3
|
+
position: relative;
|
|
4
|
+
width: 100%;
|
|
5
|
+
height: 100%;
|
|
6
|
+
overflow: hidden;
|
|
7
|
+
background: #050505;
|
|
8
|
+
}
|
|
9
|
+
.topo-container[data-v-1f2e13bc] {
|
|
10
|
+
width: 100%;
|
|
11
|
+
height: 100%;
|
|
12
|
+
}
|
|
13
|
+
.topo-toolbar[data-v-1f2e13bc] {
|
|
14
|
+
position: absolute;
|
|
15
|
+
top: 10px;
|
|
16
|
+
left: 12px;
|
|
17
|
+
display: flex;
|
|
18
|
+
align-items: center;
|
|
19
|
+
gap: 12px;
|
|
20
|
+
padding: 6px 12px;
|
|
21
|
+
background: rgba(20, 20, 20, 0.88);
|
|
22
|
+
border: 1px solid #333;
|
|
23
|
+
border-radius: 6px;
|
|
24
|
+
font-size: 13px;
|
|
25
|
+
color: #ddd;
|
|
26
|
+
z-index: 10;
|
|
27
|
+
}
|
|
28
|
+
.topo-title[data-v-1f2e13bc] {
|
|
29
|
+
color: #fff;
|
|
30
|
+
font-weight: 600;
|
|
31
|
+
}
|
|
32
|
+
.topo-meta[data-v-1f2e13bc] {
|
|
33
|
+
color: #9aa;
|
|
34
|
+
}
|
|
35
|
+
.btn[data-v-1f2e13bc] {
|
|
36
|
+
margin-left: 4px;
|
|
37
|
+
padding: 3px 10px;
|
|
38
|
+
border: 1px solid #555;
|
|
39
|
+
border-radius: 4px;
|
|
40
|
+
background: #222;
|
|
41
|
+
color: #eee;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
font-size: 12px;
|
|
44
|
+
}
|
|
45
|
+
.btn[data-v-1f2e13bc]:hover {
|
|
46
|
+
border-color: #00c8ff;
|
|
47
|
+
color: #00c8ff;
|
|
48
|
+
}
|
|
49
|
+
.topo-loading[data-v-1f2e13bc],
|
|
50
|
+
.topo-error[data-v-1f2e13bc] {
|
|
51
|
+
position: absolute;
|
|
52
|
+
inset: 0;
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
justify-content: center;
|
|
56
|
+
color: #aaa;
|
|
57
|
+
font-size: 14px;
|
|
58
|
+
z-index: 5;
|
|
59
|
+
background: rgba(5, 5, 5, 0.7);
|
|
60
|
+
}
|
|
61
|
+
.topo-error[data-v-1f2e13bc] {
|
|
62
|
+
color: #ff6b6b;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.panel[data-v-f9af862d] {
|
|
66
|
+
width: 300px;
|
|
67
|
+
height: 100%;
|
|
68
|
+
background: rgba(16, 16, 18, 0.96);
|
|
69
|
+
border-left: 1px solid #2c2c30;
|
|
70
|
+
display: flex;
|
|
71
|
+
flex-direction: column;
|
|
72
|
+
color: #ddd;
|
|
73
|
+
}
|
|
74
|
+
.panel-head[data-v-f9af862d] {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
justify-content: space-between;
|
|
78
|
+
padding: 12px 14px;
|
|
79
|
+
font-size: 14px;
|
|
80
|
+
font-weight: 600;
|
|
81
|
+
color: #fff;
|
|
82
|
+
border-bottom: 1px solid #2c2c30;
|
|
83
|
+
}
|
|
84
|
+
.panel-close[data-v-f9af862d] {
|
|
85
|
+
border: none;
|
|
86
|
+
background: none;
|
|
87
|
+
color: #888;
|
|
88
|
+
font-size: 18px;
|
|
89
|
+
cursor: pointer;
|
|
90
|
+
line-height: 1;
|
|
91
|
+
}
|
|
92
|
+
.panel-close[data-v-f9af862d]:hover {
|
|
93
|
+
color: #fff;
|
|
94
|
+
}
|
|
95
|
+
.panel-body[data-v-f9af862d] {
|
|
96
|
+
padding: 14px;
|
|
97
|
+
overflow-y: auto;
|
|
98
|
+
flex: 1;
|
|
99
|
+
}
|
|
100
|
+
.kv[data-v-f9af862d] {
|
|
101
|
+
display: flex;
|
|
102
|
+
gap: 10px;
|
|
103
|
+
margin-bottom: 10px;
|
|
104
|
+
font-size: 13px;
|
|
105
|
+
word-break: break-all;
|
|
106
|
+
}
|
|
107
|
+
.kv .k[data-v-f9af862d] {
|
|
108
|
+
color: #8a8a96;
|
|
109
|
+
flex: none;
|
|
110
|
+
width: 64px;
|
|
111
|
+
}
|
|
112
|
+
.kv .v[data-v-f9af862d] {
|
|
113
|
+
color: #eee;
|
|
114
|
+
}
|
|
115
|
+
.mono[data-v-f9af862d] {
|
|
116
|
+
font-family: Consolas, monospace;
|
|
117
|
+
font-size: 12px;
|
|
118
|
+
color: #9ecbff;
|
|
119
|
+
}
|
|
120
|
+
.ok[data-v-f9af862d] {
|
|
121
|
+
color: #6fdc8c;
|
|
122
|
+
}
|
|
123
|
+
.warn[data-v-f9af862d] {
|
|
124
|
+
color: #ffb454;
|
|
125
|
+
}
|
|
126
|
+
.neighbor-section[data-v-f9af862d] {
|
|
127
|
+
margin-top: 16px;
|
|
128
|
+
border-top: 1px solid #2c2c30;
|
|
129
|
+
padding-top: 12px;
|
|
130
|
+
}
|
|
131
|
+
.neighbor-title[data-v-f9af862d] {
|
|
132
|
+
font-size: 13px;
|
|
133
|
+
font-weight: 600;
|
|
134
|
+
color: #fff;
|
|
135
|
+
margin-bottom: 6px;
|
|
136
|
+
}
|
|
137
|
+
.neighbor-list[data-v-f9af862d] {
|
|
138
|
+
display: flex;
|
|
139
|
+
flex-direction: column;
|
|
140
|
+
gap: 4px;
|
|
141
|
+
}
|
|
142
|
+
.neighbor-item[data-v-f9af862d] {
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
gap: 3px;
|
|
146
|
+
padding: 5px 8px;
|
|
147
|
+
background: #1c1c22;
|
|
148
|
+
border: 1px solid #2c2c34;
|
|
149
|
+
border-radius: 4px;
|
|
150
|
+
cursor: pointer;
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
}
|
|
153
|
+
.neighbor-item[data-v-f9af862d]:hover {
|
|
154
|
+
border-color: #00c8ff;
|
|
155
|
+
background: #12222e;
|
|
156
|
+
}
|
|
157
|
+
.n-main[data-v-f9af862d] {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 8px;
|
|
161
|
+
min-width: 0;
|
|
162
|
+
}
|
|
163
|
+
.n-type[data-v-f9af862d] {
|
|
164
|
+
flex: none;
|
|
165
|
+
color: #00c8ff;
|
|
166
|
+
font-size: 11px;
|
|
167
|
+
}
|
|
168
|
+
.n-name[data-v-f9af862d] {
|
|
169
|
+
color: #ddd;
|
|
170
|
+
overflow: hidden;
|
|
171
|
+
text-overflow: ellipsis;
|
|
172
|
+
white-space: nowrap;
|
|
173
|
+
}
|
|
174
|
+
.n-psr[data-v-f9af862d] {
|
|
175
|
+
display: flex;
|
|
176
|
+
align-items: center;
|
|
177
|
+
gap: 6px;
|
|
178
|
+
min-width: 0;
|
|
179
|
+
}
|
|
180
|
+
.n-psr-id[data-v-f9af862d] {
|
|
181
|
+
font-family: Consolas, monospace;
|
|
182
|
+
font-size: 11px;
|
|
183
|
+
color: #9ecbff;
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
text-overflow: ellipsis;
|
|
186
|
+
white-space: nowrap;
|
|
187
|
+
flex: 1;
|
|
188
|
+
user-select: text;
|
|
189
|
+
-webkit-user-select: text;
|
|
190
|
+
}
|
|
191
|
+
.copy-btn[data-v-f9af862d] {
|
|
192
|
+
flex: none;
|
|
193
|
+
border: 1px solid #3a3a44;
|
|
194
|
+
border-radius: 3px;
|
|
195
|
+
background: #26262e;
|
|
196
|
+
color: #9ecbff;
|
|
197
|
+
font-size: 11px;
|
|
198
|
+
line-height: 1;
|
|
199
|
+
padding: 2px 5px;
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
}
|
|
202
|
+
.copy-btn[data-v-f9af862d]:hover {
|
|
203
|
+
border-color: #00c8ff;
|
|
204
|
+
color: #fff;
|
|
205
|
+
background: #12222e;
|
|
206
|
+
}
|
|
207
|
+
.neighbor-empty[data-v-f9af862d] {
|
|
208
|
+
font-size: 12px;
|
|
209
|
+
color: #666;
|
|
210
|
+
padding: 4px 0;
|
|
211
|
+
}
|
|
212
|
+
.stat-grid[data-v-f9af862d] {
|
|
213
|
+
display: grid;
|
|
214
|
+
grid-template-columns: 1fr 1fr;
|
|
215
|
+
gap: 10px;
|
|
216
|
+
margin-bottom: 16px;
|
|
217
|
+
}
|
|
218
|
+
.stat[data-v-f9af862d] {
|
|
219
|
+
background: #1c1c22;
|
|
220
|
+
border: 1px solid #2c2c34;
|
|
221
|
+
border-radius: 6px;
|
|
222
|
+
padding: 10px;
|
|
223
|
+
text-align: center;
|
|
224
|
+
}
|
|
225
|
+
.stat-num[data-v-f9af862d] {
|
|
226
|
+
font-size: 20px;
|
|
227
|
+
font-weight: 700;
|
|
228
|
+
color: #00c8ff;
|
|
229
|
+
}
|
|
230
|
+
.stat-label[data-v-f9af862d] {
|
|
231
|
+
font-size: 12px;
|
|
232
|
+
color: #9a9aa6;
|
|
233
|
+
margin-top: 2px;
|
|
234
|
+
}
|
|
235
|
+
.hint[data-v-f9af862d] {
|
|
236
|
+
font-size: 12px;
|
|
237
|
+
color: #777;
|
|
238
|
+
line-height: 1.7;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.distance-top10[data-v-936b875b] {
|
|
242
|
+
width: 340px;
|
|
243
|
+
height: 100%;
|
|
244
|
+
background: rgba(12, 12, 15, 0.98);
|
|
245
|
+
border-left: 1px solid #1e1e24;
|
|
246
|
+
display: flex;
|
|
247
|
+
flex-direction: column;
|
|
248
|
+
color: #ddd;
|
|
249
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ====== 头部 ====== */
|
|
253
|
+
.panel-head[data-v-936b875b] {
|
|
254
|
+
display: flex;
|
|
255
|
+
align-items: center;
|
|
256
|
+
justify-content: space-between;
|
|
257
|
+
padding: 14px 16px;
|
|
258
|
+
background: linear-gradient(180deg, rgba(20, 20, 26, 1) 0%, rgba(14, 14, 18, 1) 100%);
|
|
259
|
+
border-bottom: 1px solid #1e1e24;
|
|
260
|
+
}
|
|
261
|
+
.head-left[data-v-936b875b] {
|
|
262
|
+
display: flex;
|
|
263
|
+
align-items: center;
|
|
264
|
+
gap: 8px;
|
|
265
|
+
}
|
|
266
|
+
.head-icon[data-v-936b875b] {
|
|
267
|
+
font-size: 16px;
|
|
268
|
+
}
|
|
269
|
+
.head-title[data-v-936b875b] {
|
|
270
|
+
font-size: 14px;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
color: #f0f0f0;
|
|
273
|
+
letter-spacing: 0.5px;
|
|
274
|
+
}
|
|
275
|
+
.panel-close[data-v-936b875b] {
|
|
276
|
+
border: none;
|
|
277
|
+
background: rgba(255, 255, 255, 0.06);
|
|
278
|
+
color: #666;
|
|
279
|
+
font-size: 16px;
|
|
280
|
+
cursor: pointer;
|
|
281
|
+
line-height: 1;
|
|
282
|
+
width: 24px;
|
|
283
|
+
height: 24px;
|
|
284
|
+
border-radius: 4px;
|
|
285
|
+
display: flex;
|
|
286
|
+
align-items: center;
|
|
287
|
+
justify-content: center;
|
|
288
|
+
transition: all 0.15s ease;
|
|
289
|
+
}
|
|
290
|
+
.panel-close[data-v-936b875b]:hover {
|
|
291
|
+
background: rgba(255, 255, 255, 0.12);
|
|
292
|
+
color: #fff;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/* ====== 内容区 ====== */
|
|
296
|
+
.panel-body[data-v-936b875b] {
|
|
297
|
+
padding: 12px;
|
|
298
|
+
overflow-y: auto;
|
|
299
|
+
flex: 1;
|
|
300
|
+
}
|
|
301
|
+
.panel-body[data-v-936b875b]::-webkit-scrollbar {
|
|
302
|
+
width: 4px;
|
|
303
|
+
}
|
|
304
|
+
.panel-body[data-v-936b875b]::-webkit-scrollbar-track {
|
|
305
|
+
background: transparent;
|
|
306
|
+
}
|
|
307
|
+
.panel-body[data-v-936b875b]::-webkit-scrollbar-thumb {
|
|
308
|
+
background: #333;
|
|
309
|
+
border-radius: 2px;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/* ====== 空状态 ====== */
|
|
313
|
+
.empty-state[data-v-936b875b] {
|
|
314
|
+
display: flex;
|
|
315
|
+
flex-direction: column;
|
|
316
|
+
align-items: center;
|
|
317
|
+
justify-content: center;
|
|
318
|
+
padding: 48px 0;
|
|
319
|
+
color: #555;
|
|
320
|
+
}
|
|
321
|
+
.empty-icon[data-v-936b875b] {
|
|
322
|
+
font-size: 32px;
|
|
323
|
+
margin-bottom: 12px;
|
|
324
|
+
opacity: 0.5;
|
|
325
|
+
}
|
|
326
|
+
.empty-text[data-v-936b875b] {
|
|
327
|
+
font-size: 13px;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* ====== 列表 ====== */
|
|
331
|
+
.distance-list[data-v-936b875b] {
|
|
332
|
+
display: flex;
|
|
333
|
+
flex-direction: column;
|
|
334
|
+
gap: 6px;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/* ====== 卡片 ====== */
|
|
338
|
+
.distance-item[data-v-936b875b] {
|
|
339
|
+
display: flex;
|
|
340
|
+
gap: 12px;
|
|
341
|
+
padding: 10px 12px;
|
|
342
|
+
background: rgba(255, 255, 255, 0.02);
|
|
343
|
+
border: 1px solid rgba(255, 255, 255, 0.04);
|
|
344
|
+
border-radius: 8px;
|
|
345
|
+
transition: all 0.2s ease;
|
|
346
|
+
cursor: default;
|
|
347
|
+
}
|
|
348
|
+
.distance-item[data-v-936b875b]:hover {
|
|
349
|
+
background: rgba(0, 200, 255, 0.04);
|
|
350
|
+
border-color: rgba(0, 200, 255, 0.15);
|
|
351
|
+
}
|
|
352
|
+
.distance-item.top-3[data-v-936b875b] {
|
|
353
|
+
background: rgba(0, 200, 255, 0.03);
|
|
354
|
+
border-color: rgba(0, 200, 255, 0.08);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ====== 排名 ====== */
|
|
358
|
+
.rank-col[data-v-936b875b] {
|
|
359
|
+
flex-shrink: 0;
|
|
360
|
+
padding-top: 2px;
|
|
361
|
+
}
|
|
362
|
+
.rank-badge[data-v-936b875b] {
|
|
363
|
+
width: 22px;
|
|
364
|
+
height: 22px;
|
|
365
|
+
border-radius: 6px;
|
|
366
|
+
display: flex;
|
|
367
|
+
align-items: center;
|
|
368
|
+
justify-content: center;
|
|
369
|
+
font-size: 11px;
|
|
370
|
+
font-weight: 700;
|
|
371
|
+
color: #fff;
|
|
372
|
+
}
|
|
373
|
+
.rank-gold[data-v-936b875b] {
|
|
374
|
+
background: linear-gradient(135deg, #f6b93b, #e58e26);
|
|
375
|
+
box-shadow: 0 2px 8px rgba(246, 185, 59, 0.3);
|
|
376
|
+
}
|
|
377
|
+
.rank-silver[data-v-936b875b] {
|
|
378
|
+
background: linear-gradient(135deg, #a4b0be, #8395a7);
|
|
379
|
+
box-shadow: 0 2px 8px rgba(164, 176, 190, 0.25);
|
|
380
|
+
}
|
|
381
|
+
.rank-bronze[data-v-936b875b] {
|
|
382
|
+
background: linear-gradient(135deg, #e58e26, #d35400);
|
|
383
|
+
box-shadow: 0 2px 8px rgba(229, 142, 38, 0.25);
|
|
384
|
+
}
|
|
385
|
+
.rank-normal[data-v-936b875b] {
|
|
386
|
+
background: rgba(255, 255, 255, 0.08);
|
|
387
|
+
color: #888;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* ====== 信息列 ====== */
|
|
391
|
+
.info-col[data-v-936b875b] {
|
|
392
|
+
flex: 1;
|
|
393
|
+
min-width: 0;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* ====== 节点行 ====== */
|
|
397
|
+
.node-row[data-v-936b875b] {
|
|
398
|
+
display: flex;
|
|
399
|
+
align-items: center;
|
|
400
|
+
gap: 6px;
|
|
401
|
+
margin-bottom: 8px;
|
|
402
|
+
}
|
|
403
|
+
.node-tag[data-v-936b875b] {
|
|
404
|
+
font-size: 12px;
|
|
405
|
+
padding: 2px 8px;
|
|
406
|
+
border-radius: 4px;
|
|
407
|
+
max-width: 110px;
|
|
408
|
+
overflow: hidden;
|
|
409
|
+
text-overflow: ellipsis;
|
|
410
|
+
white-space: nowrap;
|
|
411
|
+
cursor: pointer;
|
|
412
|
+
transition: all 0.15s ease;
|
|
413
|
+
}
|
|
414
|
+
.node-tag.parent[data-v-936b875b] {
|
|
415
|
+
background: rgba(111, 220, 140, 0.1);
|
|
416
|
+
color: #6fdc8c;
|
|
417
|
+
border: 1px solid rgba(111, 220, 140, 0.15);
|
|
418
|
+
}
|
|
419
|
+
.node-tag.parent[data-v-936b875b]:hover {
|
|
420
|
+
background: rgba(111, 220, 140, 0.18);
|
|
421
|
+
border-color: rgba(111, 220, 140, 0.3);
|
|
422
|
+
}
|
|
423
|
+
.node-tag.child[data-v-936b875b] {
|
|
424
|
+
background: rgba(0, 200, 255, 0.08);
|
|
425
|
+
color: #00c8ff;
|
|
426
|
+
border: 1px solid rgba(0, 200, 255, 0.12);
|
|
427
|
+
}
|
|
428
|
+
.node-tag.child[data-v-936b875b]:hover {
|
|
429
|
+
background: rgba(0, 200, 255, 0.15);
|
|
430
|
+
border-color: rgba(0, 200, 255, 0.25);
|
|
431
|
+
}
|
|
432
|
+
.link-icon[data-v-936b875b] {
|
|
433
|
+
color: #444;
|
|
434
|
+
font-size: 12px;
|
|
435
|
+
flex-shrink: 0;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/* ====== 进度条行 ====== */
|
|
439
|
+
.bar-row[data-v-936b875b] {
|
|
440
|
+
display: flex;
|
|
441
|
+
align-items: center;
|
|
442
|
+
gap: 10px;
|
|
443
|
+
}
|
|
444
|
+
.bar-track[data-v-936b875b] {
|
|
445
|
+
flex: 1;
|
|
446
|
+
height: 4px;
|
|
447
|
+
background: rgba(255, 255, 255, 0.04);
|
|
448
|
+
border-radius: 2px;
|
|
449
|
+
overflow: hidden;
|
|
450
|
+
}
|
|
451
|
+
.bar-fill[data-v-936b875b] {
|
|
452
|
+
height: 100%;
|
|
453
|
+
background: linear-gradient(90deg, #00c8ff, #0070ff);
|
|
454
|
+
border-radius: 2px;
|
|
455
|
+
transition: width 0.4s ease;
|
|
456
|
+
}
|
|
457
|
+
.top-3 .bar-fill[data-v-936b875b] {
|
|
458
|
+
background: linear-gradient(90deg, #00e5ff, #00c8ff, #0070ff);
|
|
459
|
+
}
|
|
460
|
+
.distance-num[data-v-936b875b] {
|
|
461
|
+
flex-shrink: 0;
|
|
462
|
+
display: flex;
|
|
463
|
+
align-items: baseline;
|
|
464
|
+
gap: 2px;
|
|
465
|
+
min-width: 48px;
|
|
466
|
+
justify-content: flex-end;
|
|
467
|
+
}
|
|
468
|
+
.num[data-v-936b875b] {
|
|
469
|
+
font-size: 14px;
|
|
470
|
+
font-weight: 700;
|
|
471
|
+
color: #00c8ff;
|
|
472
|
+
font-variant-numeric: tabular-nums;
|
|
473
|
+
}
|
|
474
|
+
.top-3 .num[data-v-936b875b] {
|
|
475
|
+
color: #00e5ff;
|
|
476
|
+
}
|
|
477
|
+
.unit[data-v-936b875b] {
|
|
478
|
+
font-size: 10px;
|
|
479
|
+
color: #555;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* ====== 响应式 ====== */
|
|
483
|
+
@media (max-width: 768px) {
|
|
484
|
+
.distance-top10[data-v-936b875b] {
|
|
485
|
+
width: 100%;
|
|
486
|
+
height: auto;
|
|
487
|
+
max-height: 320px;
|
|
488
|
+
border-left: none;
|
|
489
|
+
border-top: 1px solid #1e1e24;
|
|
490
|
+
}
|
|
491
|
+
.node-row[data-v-936b875b] {
|
|
492
|
+
flex-wrap: wrap;
|
|
493
|
+
}
|
|
494
|
+
}
|