Showing
1 changed file
with
892 additions
and
846 deletions
| @@ -6,377 +6,425 @@ | @@ -6,377 +6,425 @@ | ||
| 6 | <title>知识图谱可视化 - BettaFish</title> | 6 | <title>知识图谱可视化 - BettaFish</title> |
| 7 | <!-- Vis.js --> | 7 | <!-- Vis.js --> |
| 8 | <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script> | 8 | <script src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"></script> |
| 9 | + <!-- Google Fonts --> | ||
| 10 | + <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&display=swap" rel="stylesheet"> | ||
| 9 | <style> | 11 | <style> |
| 10 | :root { | 12 | :root { |
| 11 | - --primary-color: #4F46E5; | ||
| 12 | - --primary-light: #818CF8; | ||
| 13 | - --bg-color: #0F172A; | ||
| 14 | - --card-bg: #1E293B; | ||
| 15 | - --text-color: #F1F5F9; | ||
| 16 | - --text-muted: #94A3B8; | 13 | + --bg-color: #0f172a; |
| 14 | + --sidebar-bg: #1e293b; | ||
| 15 | + --card-bg: #1e293b; | ||
| 17 | --border-color: #334155; | 16 | --border-color: #334155; |
| 18 | - --success-color: #10B981; | ||
| 19 | - --warning-color: #F59E0B; | ||
| 20 | - --error-color: #EF4444; | 17 | + --primary-color: #6366f1; |
| 18 | + --primary-hover: #4f46e5; | ||
| 19 | + --text-main: #f8fafc; | ||
| 20 | + --text-sec: #94a3b8; | ||
| 21 | + --success: #10b981; | ||
| 22 | + --warning: #f59e0b; | ||
| 23 | + --error: #ef4444; | ||
| 24 | + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); | ||
| 25 | + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); | ||
| 26 | + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1); | ||
| 21 | } | 27 | } |
| 22 | 28 | ||
| 23 | * { | 29 | * { |
| 24 | margin: 0; | 30 | margin: 0; |
| 25 | padding: 0; | 31 | padding: 0; |
| 26 | box-sizing: border-box; | 32 | box-sizing: border-box; |
| 33 | + outline: none; | ||
| 27 | } | 34 | } |
| 28 | 35 | ||
| 29 | body { | 36 | body { |
| 30 | - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; | 37 | + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
| 31 | background-color: var(--bg-color); | 38 | background-color: var(--bg-color); |
| 32 | - color: var(--text-color); | ||
| 33 | - min-height: 100vh; | 39 | + color: var(--text-main); |
| 40 | + height: 100vh; | ||
| 41 | + overflow: hidden; | ||
| 42 | + font-size: 14px; | ||
| 34 | } | 43 | } |
| 35 | 44 | ||
| 36 | - /* 顶部工具栏 */ | ||
| 37 | - .toolbar { | ||
| 38 | - position: fixed; | ||
| 39 | - top: 0; | ||
| 40 | - left: 0; | ||
| 41 | - right: 0; | ||
| 42 | - height: 60px; | ||
| 43 | - background: var(--card-bg); | ||
| 44 | - border-bottom: 1px solid var(--border-color); | 45 | + /* 布局容器 */ |
| 46 | + .app-container { | ||
| 45 | display: flex; | 47 | display: flex; |
| 46 | - align-items: center; | ||
| 47 | - padding: 0 20px; | ||
| 48 | - gap: 16px; | ||
| 49 | - z-index: 1000; | 48 | + height: 100vh; |
| 49 | + width: 100vw; | ||
| 50 | } | 50 | } |
| 51 | 51 | ||
| 52 | - .toolbar h1 { | ||
| 53 | - font-size: 1.25rem; | ||
| 54 | - font-weight: 600; | 52 | + /* 侧边栏 */ |
| 53 | + .sidebar { | ||
| 54 | + width: 320px; | ||
| 55 | + background: var(--sidebar-bg); | ||
| 56 | + border-right: 1px solid var(--border-color); | ||
| 55 | display: flex; | 57 | display: flex; |
| 56 | - align-items: center; | ||
| 57 | - gap: 8px; | 58 | + flex-direction: column; |
| 59 | + z-index: 20; | ||
| 60 | + transition: transform 0.3s ease; | ||
| 61 | + box-shadow: var(--shadow-lg); | ||
| 58 | } | 62 | } |
| 59 | 63 | ||
| 60 | - .toolbar h1 svg { | ||
| 61 | - width: 24px; | ||
| 62 | - height: 24px; | ||
| 63 | - color: var(--primary-color); | 64 | + .sidebar.collapsed { |
| 65 | + transform: translateX(-320px); | ||
| 66 | + position: absolute; | ||
| 67 | + height: 100%; | ||
| 64 | } | 68 | } |
| 65 | 69 | ||
| 66 | - .toolbar-divider { | ||
| 67 | - width: 1px; | ||
| 68 | - height: 30px; | ||
| 69 | - background: var(--border-color); | 70 | + .sidebar-header { |
| 71 | + padding: 20px; | ||
| 72 | + border-bottom: 1px solid var(--border-color); | ||
| 73 | + display: flex; | ||
| 74 | + align-items: center; | ||
| 75 | + justify-content: space-between; | ||
| 70 | } | 76 | } |
| 71 | 77 | ||
| 72 | - .btn { | 78 | + .app-title { |
| 79 | + font-size: 1.1rem; | ||
| 80 | + font-weight: 600; | ||
| 73 | display: flex; | 81 | display: flex; |
| 74 | align-items: center; | 82 | align-items: center; |
| 75 | - gap: 6px; | ||
| 76 | - padding: 8px 16px; | ||
| 77 | - border: 1px solid var(--border-color); | ||
| 78 | - border-radius: 6px; | ||
| 79 | - background: transparent; | ||
| 80 | - color: var(--text-color); | ||
| 81 | - cursor: pointer; | ||
| 82 | - font-size: 0.875rem; | ||
| 83 | - transition: all 0.2s; | 83 | + gap: 10px; |
| 84 | + color: var(--text-main); | ||
| 84 | } | 85 | } |
| 85 | 86 | ||
| 86 | - .btn:hover { | ||
| 87 | - background: var(--primary-color); | ||
| 88 | - border-color: var(--primary-color); | 87 | + .app-title svg { |
| 88 | + color: var(--primary-color); | ||
| 89 | + width: 24px; | ||
| 90 | + height: 24px; | ||
| 89 | } | 91 | } |
| 90 | 92 | ||
| 91 | - .btn-primary { | ||
| 92 | - background: var(--primary-color); | ||
| 93 | - border-color: var(--primary-color); | 93 | + /* 侧边栏内容区 */ |
| 94 | + .sidebar-content { | ||
| 95 | + flex: 1; | ||
| 96 | + overflow-y: auto; | ||
| 97 | + padding: 0; | ||
| 94 | } | 98 | } |
| 95 | 99 | ||
| 96 | - .btn svg { | ||
| 97 | - width: 16px; | ||
| 98 | - height: 16px; | 100 | + .section { |
| 101 | + padding: 20px; | ||
| 102 | + border-bottom: 1px solid var(--border-color); | ||
| 99 | } | 103 | } |
| 100 | 104 | ||
| 101 | - .search-box { | ||
| 102 | - flex: 1; | ||
| 103 | - max-width: 400px; | 105 | + .section-title { |
| 106 | + font-size: 0.75rem; | ||
| 107 | + font-weight: 600; | ||
| 108 | + text-transform: uppercase; | ||
| 109 | + letter-spacing: 0.05em; | ||
| 110 | + color: var(--text-sec); | ||
| 111 | + margin-bottom: 12px; | ||
| 112 | + display: flex; | ||
| 113 | + align-items: center; | ||
| 114 | + justify-content: space-between; | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + /* 搜索框 */ | ||
| 118 | + .search-wrapper { | ||
| 104 | position: relative; | 119 | position: relative; |
| 120 | + margin-bottom: 10px; | ||
| 105 | } | 121 | } |
| 106 | 122 | ||
| 107 | - .search-box input { | 123 | + .search-input { |
| 108 | width: 100%; | 124 | width: 100%; |
| 109 | - padding: 8px 16px 8px 40px; | 125 | + background: rgba(15, 23, 42, 0.5); |
| 110 | border: 1px solid var(--border-color); | 126 | border: 1px solid var(--border-color); |
| 111 | - border-radius: 6px; | ||
| 112 | - background: var(--bg-color); | ||
| 113 | - color: var(--text-color); | ||
| 114 | - font-size: 0.875rem; | 127 | + padding: 10px 36px 10px 12px; |
| 128 | + border-radius: 8px; | ||
| 129 | + color: var(--text-main); | ||
| 130 | + font-size: 0.9rem; | ||
| 131 | + transition: all 0.2s; | ||
| 115 | } | 132 | } |
| 116 | 133 | ||
| 117 | - .search-box input:focus { | ||
| 118 | - outline: none; | 134 | + .search-input:focus { |
| 119 | border-color: var(--primary-color); | 135 | border-color: var(--primary-color); |
| 136 | + box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2); | ||
| 120 | } | 137 | } |
| 121 | 138 | ||
| 122 | - .search-box svg { | 139 | + .search-icon { |
| 123 | position: absolute; | 140 | position: absolute; |
| 124 | - left: 12px; | 141 | + right: 12px; |
| 125 | top: 50%; | 142 | top: 50%; |
| 126 | transform: translateY(-50%); | 143 | transform: translateY(-50%); |
| 127 | - width: 16px; | ||
| 128 | - height: 16px; | ||
| 129 | - color: var(--text-muted); | ||
| 130 | - } | ||
| 131 | - | ||
| 132 | - .search-group { | ||
| 133 | - display: flex; | ||
| 134 | - align-items: center; | ||
| 135 | - gap: 10px; | ||
| 136 | - flex: 1; | ||
| 137 | - max-width: 560px; | 144 | + color: var(--text-sec); |
| 145 | + pointer-events: none; | ||
| 138 | } | 146 | } |
| 139 | 147 | ||
| 140 | - .search-actions { | 148 | + .search-controls { |
| 141 | display: flex; | 149 | display: flex; |
| 150 | + gap: 8px; | ||
| 142 | align-items: center; | 151 | align-items: center; |
| 143 | - gap: 6px; | 152 | + margin-top: 8px; |
| 144 | } | 153 | } |
| 145 | 154 | ||
| 146 | .search-status { | 155 | .search-status { |
| 147 | - min-width: 44px; | ||
| 148 | - text-align: center; | ||
| 149 | font-size: 0.8rem; | 156 | font-size: 0.8rem; |
| 150 | - color: var(--text-muted); | 157 | + color: var(--text-sec); |
| 158 | + margin-left: auto; | ||
| 151 | } | 159 | } |
| 152 | 160 | ||
| 153 | - /* 统计信息 */ | ||
| 154 | - .stats { | 161 | + /* 过滤器 */ |
| 162 | + .filter-list { | ||
| 155 | display: flex; | 163 | display: flex; |
| 156 | - gap: 16px; | ||
| 157 | - margin-left: auto; | 164 | + flex-direction: column; |
| 165 | + gap: 8px; | ||
| 158 | } | 166 | } |
| 159 | 167 | ||
| 160 | - .stat-item { | 168 | + .filter-item { |
| 161 | display: flex; | 169 | display: flex; |
| 162 | align-items: center; | 170 | align-items: center; |
| 163 | - gap: 6px; | ||
| 164 | - font-size: 0.875rem; | 171 | + padding: 8px 12px; |
| 172 | + background: rgba(15, 23, 42, 0.3); | ||
| 173 | + border-radius: 6px; | ||
| 174 | + cursor: pointer; | ||
| 175 | + transition: background 0.2s; | ||
| 176 | + border: 1px solid transparent; | ||
| 165 | } | 177 | } |
| 166 | 178 | ||
| 167 | - .stat-item .label { | ||
| 168 | - color: var(--text-muted); | 179 | + .filter-item:hover { |
| 180 | + background: rgba(15, 23, 42, 0.6); | ||
| 181 | + border-color: var(--border-color); | ||
| 169 | } | 182 | } |
| 170 | 183 | ||
| 171 | - .stat-item .value { | ||
| 172 | - font-weight: 600; | ||
| 173 | - color: var(--primary-light); | 184 | + .filter-item input { |
| 185 | + display: none; | ||
| 174 | } | 186 | } |
| 175 | 187 | ||
| 176 | - /* 左侧面板 */ | ||
| 177 | - .sidebar { | ||
| 178 | - position: fixed; | ||
| 179 | - top: 60px; | ||
| 180 | - left: 0; | ||
| 181 | - width: 300px; | ||
| 182 | - bottom: 0; | ||
| 183 | - background: var(--card-bg); | ||
| 184 | - border-right: 1px solid var(--border-color); | ||
| 185 | - overflow-y: auto; | ||
| 186 | - padding: 16px; | ||
| 187 | - transition: transform 0.3s; | ||
| 188 | - z-index: 100; | 188 | + .filter-item.active { |
| 189 | + background: rgba(99, 102, 241, 0.1); | ||
| 190 | + border-color: rgba(99, 102, 241, 0.3); | ||
| 189 | } | 191 | } |
| 190 | 192 | ||
| 191 | - .sidebar.collapsed { | ||
| 192 | - transform: translateX(-100%); | 193 | + /* 未选中时灰显彩点 */ |
| 194 | + .filter-item:not(.active) .filter-dot { | ||
| 195 | + background-color: var(--text-sec) !important; | ||
| 196 | + box-shadow: none; | ||
| 193 | } | 197 | } |
| 194 | - | ||
| 195 | - .sidebar h3 { | ||
| 196 | - font-size: 0.875rem; | ||
| 197 | - font-weight: 600; | ||
| 198 | - color: var(--text-muted); | ||
| 199 | - text-transform: uppercase; | ||
| 200 | - letter-spacing: 0.05em; | ||
| 201 | - margin-bottom: 12px; | 198 | + |
| 199 | + .filter-item:not(.active) .filter-name, | ||
| 200 | + .filter-item:not(.active) .filter-count { | ||
| 201 | + color: var(--text-sec); | ||
| 202 | + opacity: 0.7; | ||
| 202 | } | 203 | } |
| 203 | 204 | ||
| 204 | - .filter-group { | ||
| 205 | - margin-bottom: 20px; | 205 | + .filter-dot { |
| 206 | + width: 10px; | ||
| 207 | + height: 10px; | ||
| 208 | + border-radius: 50%; | ||
| 209 | + margin-right: 10px; | ||
| 210 | + box-shadow: 0 0 8px currentColor; | ||
| 206 | } | 211 | } |
| 207 | 212 | ||
| 208 | - .filter-item { | ||
| 209 | - display: flex; | ||
| 210 | - align-items: center; | ||
| 211 | - gap: 10px; | ||
| 212 | - padding: 8px 0; | ||
| 213 | - cursor: pointer; | 213 | + .filter-name { |
| 214 | + flex: 1; | ||
| 215 | + font-weight: 500; | ||
| 214 | } | 216 | } |
| 215 | 217 | ||
| 216 | - .filter-item input[type="checkbox"] { | ||
| 217 | - width: 16px; | ||
| 218 | - height: 16px; | ||
| 219 | - accent-color: var(--primary-color); | 218 | + .filter-count { |
| 219 | + font-size: 0.75rem; | ||
| 220 | + color: var(--text-sec); | ||
| 221 | + background: rgba(255, 255, 255, 0.1); | ||
| 222 | + padding: 2px 6px; | ||
| 223 | + border-radius: 4px; | ||
| 220 | } | 224 | } |
| 221 | 225 | ||
| 222 | - .filter-item .color-dot { | ||
| 223 | - width: 12px; | ||
| 224 | - height: 12px; | ||
| 225 | - border-radius: 50%; | 226 | + /* 节点详情卡片 */ |
| 227 | + .node-details { | ||
| 228 | + display: none; | ||
| 229 | + animation: fadeIn 0.3s ease; | ||
| 226 | } | 230 | } |
| 227 | 231 | ||
| 228 | - .filter-item .count { | ||
| 229 | - margin-left: auto; | ||
| 230 | - font-size: 0.75rem; | ||
| 231 | - color: var(--text-muted); | 232 | + .detail-card { |
| 233 | + background: rgba(15, 23, 42, 0.3); | ||
| 234 | + border-radius: 8px; | ||
| 235 | + padding: 16px; | ||
| 236 | + border: 1px solid var(--border-color); | ||
| 232 | } | 237 | } |
| 233 | 238 | ||
| 234 | - /* 节点详情 */ | ||
| 235 | - .node-detail { | ||
| 236 | - margin-top: 20px; | ||
| 237 | - padding-top: 20px; | ||
| 238 | - border-top: 1px solid var(--border-color); | 239 | + .detail-header { |
| 240 | + margin-bottom: 12px; | ||
| 241 | + padding-bottom: 12px; | ||
| 242 | + border-bottom: 1px solid rgba(255, 255, 255, 0.1); | ||
| 239 | } | 243 | } |
| 240 | 244 | ||
| 241 | - .node-detail .detail-title { | 245 | + .detail-title { |
| 246 | + font-size: 1rem; | ||
| 242 | font-weight: 600; | 247 | font-weight: 600; |
| 248 | + color: var(--primary-color); | ||
| 249 | + margin-bottom: 4px; | ||
| 250 | + word-break: break-all; | ||
| 251 | + } | ||
| 252 | + | ||
| 253 | + .detail-badge { | ||
| 254 | + display: inline-block; | ||
| 255 | + font-size: 0.7rem; | ||
| 256 | + padding: 2px 8px; | ||
| 257 | + border-radius: 12px; | ||
| 258 | + background: var(--border-color); | ||
| 259 | + color: var(--text-sec); | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + .prop-row { | ||
| 243 | margin-bottom: 8px; | 263 | margin-bottom: 8px; |
| 244 | - color: var(--primary-light); | ||
| 245 | } | 264 | } |
| 246 | 265 | ||
| 247 | - .node-detail .detail-type { | 266 | + .prop-label { |
| 248 | font-size: 0.75rem; | 267 | font-size: 0.75rem; |
| 249 | - color: var(--text-muted); | ||
| 250 | - margin-bottom: 12px; | 268 | + color: var(--text-sec); |
| 269 | + margin-bottom: 2px; | ||
| 251 | } | 270 | } |
| 252 | 271 | ||
| 253 | - .node-detail .detail-props { | ||
| 254 | - font-size: 0.875rem; | 272 | + .prop-value { |
| 273 | + font-size: 0.85rem; | ||
| 274 | + color: var(--text-main); | ||
| 275 | + word-break: break-word; | ||
| 276 | + line-height: 1.4; | ||
| 255 | } | 277 | } |
| 256 | 278 | ||
| 257 | - .node-detail .prop-item { | ||
| 258 | - padding: 6px 0; | ||
| 259 | - border-bottom: 1px solid var(--border-color); | 279 | + /* 统计数据 */ |
| 280 | + .stats-grid { | ||
| 281 | + display: grid; | ||
| 282 | + grid-template-columns: 1fr 1fr; | ||
| 283 | + gap: 12px; | ||
| 260 | } | 284 | } |
| 261 | 285 | ||
| 262 | - .node-detail .prop-key { | ||
| 263 | - color: var(--text-muted); | ||
| 264 | - font-size: 0.75rem; | 286 | + .stat-box { |
| 287 | + background: rgba(15, 23, 42, 0.3); | ||
| 288 | + padding: 12px; | ||
| 289 | + border-radius: 8px; | ||
| 290 | + text-align: center; | ||
| 291 | + border: 1px solid var(--border-color); | ||
| 265 | } | 292 | } |
| 266 | 293 | ||
| 267 | - .node-detail .prop-value { | ||
| 268 | - margin-top: 2px; | ||
| 269 | - word-break: break-all; | 294 | + .stat-value { |
| 295 | + font-size: 1.25rem; | ||
| 296 | + font-weight: 700; | ||
| 297 | + color: var(--text-main); | ||
| 270 | } | 298 | } |
| 271 | 299 | ||
| 272 | - /* 图谱容器 */ | ||
| 273 | - .graph-container { | ||
| 274 | - position: fixed; | ||
| 275 | - top: 60px; | ||
| 276 | - left: 300px; | ||
| 277 | - right: 0; | ||
| 278 | - bottom: 0; | ||
| 279 | - transition: left 0.3s; | 300 | + .stat-label { |
| 301 | + font-size: 0.75rem; | ||
| 302 | + color: var(--text-sec); | ||
| 303 | + margin-top: 4px; | ||
| 280 | } | 304 | } |
| 281 | 305 | ||
| 282 | - .graph-container.fullwidth { | ||
| 283 | - left: 0; | 306 | + /* 主画布区 */ |
| 307 | + .main-content { | ||
| 308 | + flex: 1; | ||
| 309 | + position: relative; | ||
| 310 | + background: var(--bg-color); | ||
| 311 | + overflow: hidden; | ||
| 284 | } | 312 | } |
| 285 | 313 | ||
| 286 | #network { | 314 | #network { |
| 287 | width: 100%; | 315 | width: 100%; |
| 288 | height: 100%; | 316 | height: 100%; |
| 289 | - background: var(--bg-color); | ||
| 290 | } | 317 | } |
| 291 | 318 | ||
| 292 | - /* 加载状态 */ | ||
| 293 | - .loading-overlay { | 319 | + /* 浮动工具栏 */ |
| 320 | + .floating-toolbar { | ||
| 294 | position: absolute; | 321 | position: absolute; |
| 295 | - top: 0; | ||
| 296 | - left: 0; | ||
| 297 | - right: 0; | ||
| 298 | - bottom: 0; | 322 | + background: var(--card-bg); |
| 323 | + border: 1px solid var(--border-color); | ||
| 324 | + border-radius: 8px; | ||
| 325 | + padding: 6px; | ||
| 299 | display: flex; | 326 | display: flex; |
| 327 | + gap: 4px; | ||
| 328 | + box-shadow: var(--shadow-lg); | ||
| 329 | + z-index: 10; | ||
| 330 | + } | ||
| 331 | + | ||
| 332 | + .top-right { | ||
| 333 | + top: 20px; | ||
| 334 | + right: 20px; | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + .bottom-right { | ||
| 338 | + bottom: 20px; | ||
| 339 | + right: 20px; | ||
| 300 | flex-direction: column; | 340 | flex-direction: column; |
| 301 | - align-items: center; | ||
| 302 | - justify-content: center; | ||
| 303 | - background: var(--bg-color); | ||
| 304 | - z-index: 500; | 341 | + } |
| 342 | + | ||
| 343 | + .bottom-left { | ||
| 344 | + bottom: 20px; | ||
| 345 | + left: 20px; | ||
| 346 | + /* 当侧边栏存在时,需要调整left */ | ||
| 347 | + left: 340px; | ||
| 348 | + transition: left 0.3s ease; | ||
| 305 | } | 349 | } |
| 306 | 350 | ||
| 307 | - .loading-spinner { | ||
| 308 | - width: 48px; | ||
| 309 | - height: 48px; | ||
| 310 | - border: 4px solid var(--border-color); | ||
| 311 | - border-top-color: var(--primary-color); | ||
| 312 | - border-radius: 50%; | ||
| 313 | - animation: spin 1s linear infinite; | 351 | + .sidebar.collapsed ~ .main-content .bottom-left { |
| 352 | + left: 20px; | ||
| 353 | + } | ||
| 354 | + | ||
| 355 | + .top-left-toggle { | ||
| 356 | + position: absolute; | ||
| 357 | + top: 20px; | ||
| 358 | + left: 20px; | ||
| 359 | + z-index: 30; | ||
| 360 | + display: none; /* 默认隐藏,折叠时显示 */ | ||
| 314 | } | 361 | } |
| 315 | 362 | ||
| 316 | - @keyframes spin { | ||
| 317 | - to { transform: rotate(360deg); } | 363 | + .sidebar.collapsed ~ .main-content .top-left-toggle { |
| 364 | + display: block; | ||
| 318 | } | 365 | } |
| 319 | 366 | ||
| 320 | - .loading-text { | ||
| 321 | - margin-top: 16px; | ||
| 322 | - color: var(--text-muted); | 367 | + /* 按钮样式 */ |
| 368 | + .btn-icon { | ||
| 369 | + width: 36px; | ||
| 370 | + height: 36px; | ||
| 371 | + display: flex; | ||
| 372 | + align-items: center; | ||
| 373 | + justify-content: center; | ||
| 374 | + border: 1px solid transparent; | ||
| 375 | + background: transparent; | ||
| 376 | + color: var(--text-sec); | ||
| 377 | + border-radius: 6px; | ||
| 378 | + cursor: pointer; | ||
| 379 | + transition: all 0.2s; | ||
| 323 | } | 380 | } |
| 324 | 381 | ||
| 325 | - /* 空状态 */ | ||
| 326 | - .empty-state { | ||
| 327 | - position: absolute; | ||
| 328 | - top: 50%; | ||
| 329 | - left: 50%; | ||
| 330 | - transform: translate(-50%, -50%); | ||
| 331 | - text-align: center; | ||
| 332 | - color: var(--text-muted); | 382 | + .btn-icon:hover { |
| 383 | + background: rgba(255, 255, 255, 0.1); | ||
| 384 | + color: var(--text-main); | ||
| 333 | } | 385 | } |
| 334 | 386 | ||
| 335 | - .empty-state svg { | ||
| 336 | - width: 64px; | ||
| 337 | - height: 64px; | ||
| 338 | - margin-bottom: 16px; | ||
| 339 | - opacity: 0.5; | 387 | + .btn-icon.primary { |
| 388 | + background: var(--primary-color); | ||
| 389 | + color: white; | ||
| 390 | + } | ||
| 391 | + | ||
| 392 | + .btn-icon.primary:hover { | ||
| 393 | + background: var(--primary-hover); | ||
| 340 | } | 394 | } |
| 341 | 395 | ||
| 342 | - /* 提示信息 */ | ||
| 343 | - .toast { | ||
| 344 | - position: fixed; | ||
| 345 | - bottom: 20px; | ||
| 346 | - right: 20px; | ||
| 347 | - padding: 12px 20px; | ||
| 348 | - background: var(--card-bg); | ||
| 349 | - border: 1px solid var(--border-color); | ||
| 350 | - border-radius: 8px; | ||
| 351 | - display: none; | ||
| 352 | - animation: slideIn 0.3s; | ||
| 353 | - z-index: 2000; | 396 | + .btn-icon:disabled { |
| 397 | + opacity: 0.5; | ||
| 398 | + cursor: not-allowed; | ||
| 354 | } | 399 | } |
| 355 | 400 | ||
| 356 | - @keyframes slideIn { | ||
| 357 | - from { | ||
| 358 | - transform: translateX(100%); | ||
| 359 | - opacity: 0; | ||
| 360 | - } | 401 | + .btn-sm { |
| 402 | + padding: 4px 12px; | ||
| 403 | + height: 32px; | ||
| 404 | + width: auto; | ||
| 405 | + font-size: 0.8rem; | ||
| 361 | } | 406 | } |
| 362 | 407 | ||
| 363 | /* 图例 */ | 408 | /* 图例 */ |
| 364 | - .legend { | ||
| 365 | - position: fixed; | ||
| 366 | - bottom: 20px; | ||
| 367 | - left: 320px; | ||
| 368 | - background: var(--card-bg); | 409 | + .legend-panel { |
| 410 | + background: rgba(30, 41, 59, 0.9); | ||
| 411 | + backdrop-filter: blur(8px); | ||
| 369 | border: 1px solid var(--border-color); | 412 | border: 1px solid var(--border-color); |
| 370 | border-radius: 8px; | 413 | border-radius: 8px; |
| 371 | - padding: 12px 16px; | ||
| 372 | - display: flex; | ||
| 373 | - gap: 16px; | ||
| 374 | - z-index: 100; | ||
| 375 | - transition: left 0.3s; | 414 | + padding: 12px; |
| 376 | } | 415 | } |
| 377 | 416 | ||
| 378 | - .legend.fullwidth { | ||
| 379 | - left: 20px; | 417 | + .legend-title { |
| 418 | + font-size: 0.75rem; | ||
| 419 | + font-weight: 600; | ||
| 420 | + color: var(--text-sec); | ||
| 421 | + margin-bottom: 8px; | ||
| 422 | + } | ||
| 423 | + | ||
| 424 | + .legend-items { | ||
| 425 | + display: flex; | ||
| 426 | + gap: 16px; | ||
| 427 | + flex-wrap: wrap; | ||
| 380 | } | 428 | } |
| 381 | 429 | ||
| 382 | .legend-item { | 430 | .legend-item { |
| @@ -384,729 +432,727 @@ | @@ -384,729 +432,727 @@ | ||
| 384 | align-items: center; | 432 | align-items: center; |
| 385 | gap: 6px; | 433 | gap: 6px; |
| 386 | font-size: 0.75rem; | 434 | font-size: 0.75rem; |
| 435 | + color: var(--text-main); | ||
| 387 | } | 436 | } |
| 388 | 437 | ||
| 389 | - .legend-item .dot { | ||
| 390 | - width: 10px; | ||
| 391 | - height: 10px; | 438 | + .legend-dot { |
| 439 | + width: 8px; | ||
| 440 | + height: 8px; | ||
| 441 | + border-radius: 2px; | ||
| 442 | + } | ||
| 443 | + | ||
| 444 | + /* 加载与空状态 */ | ||
| 445 | + .overlay-message { | ||
| 446 | + position: absolute; | ||
| 447 | + top: 0; | ||
| 448 | + left: 0; | ||
| 449 | + right: 0; | ||
| 450 | + bottom: 0; | ||
| 451 | + background: rgba(15, 17, 42, 0.8); | ||
| 452 | + backdrop-filter: blur(4px); | ||
| 453 | + display: flex; | ||
| 454 | + flex-direction: column; | ||
| 455 | + align-items: center; | ||
| 456 | + justify-content: center; | ||
| 457 | + z-index: 50; | ||
| 458 | + } | ||
| 459 | + | ||
| 460 | + .spinner { | ||
| 461 | + width: 40px; | ||
| 462 | + height: 40px; | ||
| 463 | + border: 3px solid rgba(99, 102, 241, 0.3); | ||
| 392 | border-radius: 50%; | 464 | border-radius: 50%; |
| 465 | + border-top-color: var(--primary-color); | ||
| 466 | + animation: spin 1s ease-in-out infinite; | ||
| 467 | + margin-bottom: 16px; | ||
| 468 | + } | ||
| 469 | + | ||
| 470 | + .empty-icon { | ||
| 471 | + width: 64px; | ||
| 472 | + height: 64px; | ||
| 473 | + color: var(--text-sec); | ||
| 474 | + opacity: 0.5; | ||
| 475 | + margin-bottom: 16px; | ||
| 393 | } | 476 | } |
| 394 | 477 | ||
| 395 | - /* 全屏模式 */ | ||
| 396 | - .fullscreen-btn { | 478 | + .toast { |
| 397 | position: fixed; | 479 | position: fixed; |
| 398 | bottom: 20px; | 480 | bottom: 20px; |
| 399 | - right: 20px; | 481 | + left: 50%; |
| 482 | + transform: translateX(-50%) translateY(100px); | ||
| 483 | + background: var(--card-bg); | ||
| 484 | + border: 1px solid var(--primary-color); | ||
| 485 | + color: var(--text-main); | ||
| 486 | + padding: 10px 24px; | ||
| 487 | + border-radius: 50px; | ||
| 488 | + box-shadow: var(--shadow-lg); | ||
| 400 | z-index: 100; | 489 | z-index: 100; |
| 490 | + transition: transform 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); | ||
| 491 | + font-weight: 500; | ||
| 492 | + display: flex; | ||
| 493 | + align-items: center; | ||
| 494 | + gap: 8px; | ||
| 495 | + } | ||
| 496 | + | ||
| 497 | + .toast.show { | ||
| 498 | + transform: translateX(-50%) translateY(0); | ||
| 401 | } | 499 | } |
| 402 | 500 | ||
| 403 | - /* 节点类型颜色 */ | ||
| 404 | - .color-topic { background-color: #EF4444; } | ||
| 405 | - .color-engine { background-color: #F59E0B; } | ||
| 406 | - .color-section { background-color: #10B981; } | ||
| 407 | - .color-search_query { background-color: #3B82F6; } | ||
| 408 | - .color-source { background-color: #8B5CF6; } | 501 | + @keyframes spin { |
| 502 | + to { transform: rotate(360deg); } | ||
| 503 | + } | ||
| 504 | + | ||
| 505 | + @keyframes fadeIn { | ||
| 506 | + from { opacity: 0; transform: translateY(5px); } | ||
| 507 | + to { opacity: 1; transform: translateY(0); } | ||
| 508 | + } | ||
| 509 | + | ||
| 510 | + /* 颜色定义 */ | ||
| 511 | + .color-topic { color: #EF4444; background-color: #EF4444; } | ||
| 512 | + .color-engine { color: #F59E0B; background-color: #F59E0B; } | ||
| 513 | + .color-section { color: #10B981; background-color: #10B981; } | ||
| 514 | + .color-search_query { color: #3B82F6; background-color: #3B82F6; } | ||
| 515 | + .color-source { color: #8B5CF6; background-color: #8B5CF6; } | ||
| 516 | + | ||
| 409 | </style> | 517 | </style> |
| 410 | </head> | 518 | </head> |
| 411 | <body> | 519 | <body> |
| 412 | - <!-- 顶部工具栏 --> | ||
| 413 | - <div class="toolbar"> | ||
| 414 | - <h1> | ||
| 415 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 416 | - <circle cx="12" cy="5" r="3"/> | ||
| 417 | - <circle cx="5" cy="19" r="3"/> | ||
| 418 | - <circle cx="19" cy="19" r="3"/> | ||
| 419 | - <line x1="12" y1="8" x2="5" y2="16"/> | ||
| 420 | - <line x1="12" y1="8" x2="19" y2="16"/> | ||
| 421 | - </svg> | ||
| 422 | - 知识图谱 | ||
| 423 | - </h1> | ||
| 424 | - | ||
| 425 | - <div class="toolbar-divider"></div> | ||
| 426 | - | ||
| 427 | - <button class="btn" id="toggleSidebar" title="切换侧边栏"> | ||
| 428 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 429 | - <rect x="3" y="3" width="18" height="18" rx="2"/> | ||
| 430 | - <line x1="9" y1="3" x2="9" y2="21"/> | ||
| 431 | - </svg> | ||
| 432 | - </button> | ||
| 433 | - | ||
| 434 | - <button class="btn" id="fitBtn" title="适应视图"> | ||
| 435 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 436 | - <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/> | ||
| 437 | - </svg> | ||
| 438 | - 适应 | ||
| 439 | - </button> | ||
| 440 | - | ||
| 441 | - <button class="btn" id="zoomInBtn" title="放大"> | ||
| 442 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 443 | - <circle cx="11" cy="11" r="8"/> | ||
| 444 | - <line x1="21" y1="21" x2="16.65" y2="16.65"/> | ||
| 445 | - <line x1="11" y1="8" x2="11" y2="14"/> | ||
| 446 | - <line x1="8" y1="11" x2="14" y2="11"/> | ||
| 447 | - </svg> | ||
| 448 | - </button> | ||
| 449 | - | ||
| 450 | - <button class="btn" id="zoomOutBtn" title="缩小"> | ||
| 451 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 452 | - <circle cx="11" cy="11" r="8"/> | ||
| 453 | - <line x1="21" y1="21" x2="16.65" y2="16.65"/> | ||
| 454 | - <line x1="8" y1="11" x2="14" y2="11"/> | ||
| 455 | - </svg> | ||
| 456 | - </button> | ||
| 457 | - | ||
| 458 | - <button class="btn" id="manualRefreshBtn" title="手动刷新图谱"> | ||
| 459 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 460 | - <polyline points="23 4 23 10 17 10"/> | ||
| 461 | - <polyline points="1 20 1 14 7 14"/> | ||
| 462 | - <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10"/> | ||
| 463 | - <path d="M20.49 15a9 9 0 0 1-14.85 3.36L1 14"/> | ||
| 464 | - </svg> | ||
| 465 | - 刷新 | ||
| 466 | - </button> | ||
| 467 | - | ||
| 468 | - <div class="search-group"> | ||
| 469 | - <div class="search-box"> | ||
| 470 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 471 | - <circle cx="11" cy="11" r="8"/> | ||
| 472 | - <line x1="21" y1="21" x2="16.65" y2="16.65"/> | ||
| 473 | - </svg> | ||
| 474 | - <input type="text" id="searchInput" placeholder="搜索节点..."> | 520 | + |
| 521 | + <div class="app-container"> | ||
| 522 | + <!-- 左侧边栏 --> | ||
| 523 | + <aside class="sidebar" id="sidebar"> | ||
| 524 | + <div class="sidebar-header"> | ||
| 525 | + <div class="app-title"> | ||
| 526 | + <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 527 | + <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path> | ||
| 528 | + <polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline> | ||
| 529 | + <line x1="12" y1="22.08" x2="12" y2="12"></line> | ||
| 530 | + </svg> | ||
| 531 | + <span>知识图谱</span> | ||
| 532 | + </div> | ||
| 533 | + <button class="btn-icon" id="toggleSidebar" title="收起侧边栏"> | ||
| 534 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 535 | + <path d="M15 18l-6-6 6-6"/> | ||
| 536 | + </svg> | ||
| 537 | + </button> | ||
| 475 | </div> | 538 | </div> |
| 476 | - <div class="search-actions"> | ||
| 477 | - <button class="btn" id="searchBtn" title="搜索">搜索</button> | ||
| 478 | - <button class="btn" id="searchPrevBtn" title="上一个">↑</button> | ||
| 479 | - <button class="btn" id="searchNextBtn" title="下一个">↓</button> | ||
| 480 | - <span class="search-status" id="searchStatus">0/0</span> | 539 | + |
| 540 | + <div class="sidebar-content"> | ||
| 541 | + <!-- 搜索区域 --> | ||
| 542 | + <div class="section"> | ||
| 543 | + <div class="section-title">搜索节点</div> | ||
| 544 | + <div class="search-wrapper"> | ||
| 545 | + <input type="text" id="searchInput" class="search-input" placeholder="输入关键词..."> | ||
| 546 | + <svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 547 | + <circle cx="11" cy="11" r="8"></circle> | ||
| 548 | + <line x1="21" y1="21" x2="16.65" y2="16.65"></line> | ||
| 549 | + </svg> | ||
| 550 | + </div> | ||
| 551 | + <div class="search-controls"> | ||
| 552 | + <button class="btn-icon btn-sm" id="searchPrevBtn" disabled title="上一个"> | ||
| 553 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"></polyline></svg> | ||
| 554 | + </button> | ||
| 555 | + <button class="btn-icon btn-sm" id="searchNextBtn" disabled title="下一个"> | ||
| 556 | + <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg> | ||
| 557 | + </button> | ||
| 558 | + <span class="search-status" id="searchStatus"></span> | ||
| 559 | + </div> | ||
| 560 | + </div> | ||
| 561 | + | ||
| 562 | + <!-- 节点详情 --> | ||
| 563 | + <div class="section node-details" id="nodeDetailPanel"> | ||
| 564 | + <div class="section-title"> | ||
| 565 | + <span>选定节点</span> | ||
| 566 | + <button class="btn-icon btn-sm" onclick="network.unselectAll(); hideNodeDetail();" title="取消选择"> | ||
| 567 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg> | ||
| 568 | + </button> | ||
| 569 | + </div> | ||
| 570 | + <div class="detail-card"> | ||
| 571 | + <div class="detail-header"> | ||
| 572 | + <div class="detail-title" id="detailTitle"></div> | ||
| 573 | + <span class="detail-badge" id="detailType"></span> | ||
| 574 | + </div> | ||
| 575 | + <div id="detailProps"></div> | ||
| 576 | + </div> | ||
| 577 | + </div> | ||
| 578 | + | ||
| 579 | + <!-- 过滤器 --> | ||
| 580 | + <div class="section"> | ||
| 581 | + <div class="section-title">显示节点</div> | ||
| 582 | + <div class="filter-list" id="filterList"> | ||
| 583 | + <!-- 动态生成 --> | ||
| 584 | + <label class="filter-item active"> | ||
| 585 | + <input type="checkbox" checked data-type="topic"> | ||
| 586 | + <span class="filter-dot color-topic"></span> | ||
| 587 | + <span class="filter-name">主题 (Topic)</span> | ||
| 588 | + <span class="filter-count" id="count-topic">0</span> | ||
| 589 | + </label> | ||
| 590 | + <label class="filter-item active"> | ||
| 591 | + <input type="checkbox" checked data-type="engine"> | ||
| 592 | + <span class="filter-dot color-engine"></span> | ||
| 593 | + <span class="filter-name">分析引擎 (Engine)</span> | ||
| 594 | + <span class="filter-count" id="count-engine">0</span> | ||
| 595 | + </label> | ||
| 596 | + <label class="filter-item active"> | ||
| 597 | + <input type="checkbox" checked data-type="section"> | ||
| 598 | + <span class="filter-dot color-section"></span> | ||
| 599 | + <span class="filter-name">报告段落 (Section)</span> | ||
| 600 | + <span class="filter-count" id="count-section">0</span> | ||
| 601 | + </label> | ||
| 602 | + <label class="filter-item active"> | ||
| 603 | + <input type="checkbox" checked data-type="search_query"> | ||
| 604 | + <span class="filter-dot color-search_query"></span> | ||
| 605 | + <span class="filter-name">搜索词 (Query)</span> | ||
| 606 | + <span class="filter-count" id="count-search_query">0</span> | ||
| 607 | + </label> | ||
| 608 | + <label class="filter-item active"> | ||
| 609 | + <input type="checkbox" checked data-type="source"> | ||
| 610 | + <span class="filter-dot color-source"></span> | ||
| 611 | + <span class="filter-name">数据来源 (Source)</span> | ||
| 612 | + <span class="filter-count" id="count-source">0</span> | ||
| 613 | + </label> | ||
| 614 | + </div> | ||
| 615 | + </div> | ||
| 616 | + | ||
| 617 | + <!-- 统计信息 --> | ||
| 618 | + <div class="section"> | ||
| 619 | + <div class="section-title">图谱统计</div> | ||
| 620 | + <div class="stats-grid"> | ||
| 621 | + <div class="stat-box"> | ||
| 622 | + <div class="stat-value" id="totalNodes">0</div> | ||
| 623 | + <div class="stat-label">节点总数</div> | ||
| 624 | + </div> | ||
| 625 | + <div class="stat-box"> | ||
| 626 | + <div class="stat-value" id="totalEdges">0</div> | ||
| 627 | + <div class="stat-label">关系总数</div> | ||
| 628 | + </div> | ||
| 629 | + </div> | ||
| 630 | + </div> | ||
| 481 | </div> | 631 | </div> |
| 482 | - </div> | ||
| 483 | - | ||
| 484 | - <div class="stats" id="statsContainer"> | ||
| 485 | - <div class="stat-item"> | ||
| 486 | - <span class="label">节点</span> | ||
| 487 | - <span class="value" id="nodeCount">0</span> | 632 | + </aside> |
| 633 | + | ||
| 634 | + <!-- 主画布 --> | ||
| 635 | + <main class="main-content"> | ||
| 636 | + <!-- 侧边栏展开按钮 --> | ||
| 637 | + <button class="btn-icon floating-toolbar top-left-toggle" id="expandSidebar" title="展开侧边栏"> | ||
| 638 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 639 | + <path d="M9 18l6-6-6-6"/> | ||
| 640 | + </svg> | ||
| 641 | + </button> | ||
| 642 | + | ||
| 643 | + <!-- 右上角工具栏 --> | ||
| 644 | + <div class="floating-toolbar top-right"> | ||
| 645 | + <button class="btn-icon" id="refreshBtn" title="刷新图谱"> | ||
| 646 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 647 | + <path d="M23 4v6h-6"></path><path d="M1 20v-6h6"></path> | ||
| 648 | + <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path> | ||
| 649 | + </svg> | ||
| 650 | + </button> | ||
| 651 | + <button class="btn-icon" id="fullscreenBtn" title="全屏模式"> | ||
| 652 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 653 | + <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2 2v-3M3 16v3a2 2 0 0 0 2 2h3"></path> | ||
| 654 | + </svg> | ||
| 655 | + </button> | ||
| 488 | </div> | 656 | </div> |
| 489 | - <div class="stat-item"> | ||
| 490 | - <span class="label">关系</span> | ||
| 491 | - <span class="value" id="edgeCount">0</span> | 657 | + |
| 658 | + <!-- 右下角缩放控制 --> | ||
| 659 | + <div class="floating-toolbar bottom-right"> | ||
| 660 | + <button class="btn-icon" id="zoomInBtn" title="放大"> | ||
| 661 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 662 | + <line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line> | ||
| 663 | + </svg> | ||
| 664 | + </button> | ||
| 665 | + <button class="btn-icon" id="zoomOutBtn" title="缩小"> | ||
| 666 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 667 | + <line x1="5" y1="12" x2="19" y2="12"></line> | ||
| 668 | + </svg> | ||
| 669 | + </button> | ||
| 670 | + <div style="height: 1px; background: var(--border-color); margin: 4px 2px;"></div> | ||
| 671 | + <button class="btn-icon" id="fitBtn" title="适应视图"> | ||
| 672 | + <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 673 | + <path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path> | ||
| 674 | + </svg> | ||
| 675 | + </button> | ||
| 492 | </div> | 676 | </div> |
| 493 | - </div> | ||
| 494 | - </div> | ||
| 495 | 677 | ||
| 496 | - <!-- 左侧面板 --> | ||
| 497 | - <div class="sidebar" id="sidebar"> | ||
| 498 | - <div class="filter-group"> | ||
| 499 | - <h3>节点类型</h3> | ||
| 500 | - <label class="filter-item"> | ||
| 501 | - <input type="checkbox" checked data-type="topic"> | ||
| 502 | - <span class="color-dot color-topic"></span> | ||
| 503 | - <span>主题</span> | ||
| 504 | - <span class="count" id="count-topic">0</span> | ||
| 505 | - </label> | ||
| 506 | - <label class="filter-item"> | ||
| 507 | - <input type="checkbox" checked data-type="engine"> | ||
| 508 | - <span class="color-dot color-engine"></span> | ||
| 509 | - <span>分析引擎</span> | ||
| 510 | - <span class="count" id="count-engine">0</span> | ||
| 511 | - </label> | ||
| 512 | - <label class="filter-item"> | ||
| 513 | - <input type="checkbox" checked data-type="section"> | ||
| 514 | - <span class="color-dot color-section"></span> | ||
| 515 | - <span>报告段落</span> | ||
| 516 | - <span class="count" id="count-section">0</span> | ||
| 517 | - </label> | ||
| 518 | - <label class="filter-item"> | ||
| 519 | - <input type="checkbox" checked data-type="search_query"> | ||
| 520 | - <span class="color-dot color-search_query"></span> | ||
| 521 | - <span>搜索关键词</span> | ||
| 522 | - <span class="count" id="count-search_query">0</span> | ||
| 523 | - </label> | ||
| 524 | - <label class="filter-item"> | ||
| 525 | - <input type="checkbox" checked data-type="source"> | ||
| 526 | - <span class="color-dot color-source"></span> | ||
| 527 | - <span>数据来源</span> | ||
| 528 | - <span class="count" id="count-source">0</span> | ||
| 529 | - </label> | ||
| 530 | - </div> | ||
| 531 | - | ||
| 532 | - <div class="node-detail" id="nodeDetail" style="display: none;"> | ||
| 533 | - <h3>节点详情</h3> | ||
| 534 | - <div class="detail-title" id="detailTitle"></div> | ||
| 535 | - <div class="detail-type" id="detailType"></div> | ||
| 536 | - <div class="detail-props" id="detailProps"></div> | ||
| 537 | - </div> | ||
| 538 | - </div> | 678 | + <!-- 左下角图例 --> |
| 679 | + <div class="floating-toolbar bottom-left legend-panel"> | ||
| 680 | + <div class="legend-title">图例</div> | ||
| 681 | + <div class="legend-items"> | ||
| 682 | + <div class="legend-item"><span class="legend-dot color-topic"></span>主题</div> | ||
| 683 | + <div class="legend-item"><span class="legend-dot color-engine"></span>引擎</div> | ||
| 684 | + <div class="legend-item"><span class="legend-dot color-section"></span>段落</div> | ||
| 685 | + <div class="legend-item"><span class="legend-dot color-search_query"></span>搜索词</div> | ||
| 686 | + <div class="legend-item"><span class="legend-dot color-source"></span>来源</div> | ||
| 687 | + </div> | ||
| 688 | + </div> | ||
| 539 | 689 | ||
| 540 | - <!-- 图谱容器 --> | ||
| 541 | - <div class="graph-container" id="graphContainer" data-report-id="{{ report_id | e if report_id else '' }}"> | ||
| 542 | - <div id="network"></div> | ||
| 543 | - | ||
| 544 | - <!-- 加载状态 --> | ||
| 545 | - <div class="loading-overlay" id="loadingOverlay"> | ||
| 546 | - <div class="loading-spinner"></div> | ||
| 547 | - <div class="loading-text">正在加载知识图谱...</div> | ||
| 548 | - </div> | ||
| 549 | - | ||
| 550 | - <!-- 空状态 --> | ||
| 551 | - <div class="empty-state" id="emptyState" style="display: none;"> | ||
| 552 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> | ||
| 553 | - <circle cx="12" cy="12" r="10"/> | ||
| 554 | - <path d="M8 15h8"/> | ||
| 555 | - <path d="M9 9h.01"/> | ||
| 556 | - <path d="M15 9h.01"/> | ||
| 557 | - </svg> | ||
| 558 | - <h3>暂无图谱数据</h3> | ||
| 559 | - <p>请先生成报告以创建知识图谱</p> | ||
| 560 | - </div> | ||
| 561 | - </div> | 690 | + <!-- 网络图容器 --> |
| 691 | + <div id="network" data-report-id="{{ report_id | e if report_id else '' }}"></div> | ||
| 562 | 692 | ||
| 563 | - <!-- 图例 --> | ||
| 564 | - <div class="legend" id="legend"> | ||
| 565 | - <div class="legend-item"> | ||
| 566 | - <span class="dot color-topic"></span> | ||
| 567 | - <span>主题</span> | ||
| 568 | - </div> | ||
| 569 | - <div class="legend-item"> | ||
| 570 | - <span class="dot color-engine"></span> | ||
| 571 | - <span>引擎</span> | ||
| 572 | - </div> | ||
| 573 | - <div class="legend-item"> | ||
| 574 | - <span class="dot color-section"></span> | ||
| 575 | - <span>段落</span> | ||
| 576 | - </div> | ||
| 577 | - <div class="legend-item"> | ||
| 578 | - <span class="dot color-search_query"></span> | ||
| 579 | - <span>搜索词</span> | ||
| 580 | - </div> | ||
| 581 | - <div class="legend-item"> | ||
| 582 | - <span class="dot color-source"></span> | ||
| 583 | - <span>来源</span> | ||
| 584 | - </div> | ||
| 585 | - </div> | 693 | + <!-- 加载遮罩 --> |
| 694 | + <div class="overlay-message" id="loadingOverlay"> | ||
| 695 | + <div class="spinner"></div> | ||
| 696 | + <div>正在构建知识图谱...</div> | ||
| 697 | + </div> | ||
| 586 | 698 | ||
| 587 | - <!-- 全屏按钮 --> | ||
| 588 | - <button class="btn fullscreen-btn" id="fullscreenBtn" title="全屏"> | ||
| 589 | - <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | ||
| 590 | - <path d="M8 3H5a2 2 0 00-2 2v3m18 0V5a2 2 0 00-2-2h-3m0 18h3a2 2 0 002-2v-3M3 16v3a2 2 0 002 2h3"/> | ||
| 591 | - </svg> | ||
| 592 | - </button> | 699 | + <!-- 空状态遮罩 --> |
| 700 | + <div class="overlay-message" id="emptyState" style="display: none;"> | ||
| 701 | + <svg class="empty-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"> | ||
| 702 | + <circle cx="12" cy="12" r="10"></circle> | ||
| 703 | + <line x1="12" y1="8" x2="12" y2="12"></line> | ||
| 704 | + <line x1="12" y1="16" x2="12.01" y2="16"></line> | ||
| 705 | + </svg> | ||
| 706 | + <h3>暂无图谱数据</h3> | ||
| 707 | + <p style="color: var(--text-sec); margin-top: 8px;">请生成报告以查看关联分析</p> | ||
| 708 | + <button class="btn-icon primary" style="margin-top: 20px; width: auto; padding: 0 20px;" onclick="loadGraphData({fromManual: true})"> | ||
| 709 | + 重试 | ||
| 710 | + </button> | ||
| 711 | + </div> | ||
| 712 | + </main> | ||
| 713 | + </div> | ||
| 593 | 714 | ||
| 594 | - <!-- 提示 --> | ||
| 595 | - <div class="toast" id="toast"></div> | 715 | + <!-- 消息提示 --> |
| 716 | + <div class="toast" id="toast"> | ||
| 717 | + <div id="toastIcon" style="display: flex; align-items: center;"></div> | ||
| 718 | + <span id="toastMsg">操作成功</span> | ||
| 719 | + </div> | ||
| 596 | 720 | ||
| 597 | <script> | 721 | <script> |
| 598 | - // 配置 | ||
| 599 | - const NODE_COLORS = { | ||
| 600 | - topic: '#EF4444', | ||
| 601 | - engine: '#F59E0B', | ||
| 602 | - section: '#10B981', | ||
| 603 | - search_query: '#3B82F6', | ||
| 604 | - source: '#8B5CF6' | ||
| 605 | - }; | ||
| 606 | - | ||
| 607 | - const NODE_SHAPES = { | ||
| 608 | - topic: 'star', | ||
| 609 | - engine: 'diamond', | ||
| 610 | - section: 'dot', | ||
| 611 | - search_query: 'triangle', | ||
| 612 | - source: 'square' | 722 | + // 配置常量 |
| 723 | + const NODE_CONFIG = { | ||
| 724 | + topic: { color: '#EF4444', shape: 'dot', size: 30, label: '主题' }, | ||
| 725 | + engine: { color: '#F59E0B', shape: 'diamond', size: 25, label: '引擎' }, | ||
| 726 | + section: { color: '#10B981', shape: 'dot', size: 15, label: '段落' }, | ||
| 727 | + search_query: { color: '#3B82F6', shape: 'triangle', size: 15, label: '搜索词' }, | ||
| 728 | + source: { color: '#8B5CF6', shape: 'square', size: 15, label: '来源' } | ||
| 613 | }; | 729 | }; |
| 614 | 730 | ||
| 615 | - // 全局变量 | 731 | + // 全局状态 |
| 616 | let network = null; | 732 | let network = null; |
| 617 | - let allNodes = []; | ||
| 618 | - let allEdges = []; | 733 | + let graphData = { nodes: [], edges: [] }; |
| 619 | let reportId = null; | 734 | let reportId = null; |
| 620 | - const graphContainer = document.getElementById('graphContainer'); | ||
| 621 | - if (graphContainer) { | ||
| 622 | - reportId = graphContainer.dataset.reportId || null; | ||
| 623 | - } | ||
| 624 | - let graphReady = false; | ||
| 625 | - let graphPollTimer = null; | ||
| 626 | - let graphSearchResults = []; | ||
| 627 | - let graphSearchIndex = -1; | ||
| 628 | - let graphSearchKeyword = ''; | ||
| 629 | - const GRAPH_POLL_INTERVAL = 4000; | 735 | + let isGraphReady = false; |
| 736 | + let pollTimer = null; | ||
| 737 | + | ||
| 738 | + // 搜索状态 | ||
| 739 | + let searchState = { | ||
| 740 | + keyword: '', | ||
| 741 | + results: [], | ||
| 742 | + currentIndex: -1 | ||
| 743 | + }; | ||
| 744 | + | ||
| 745 | + // DOM 元素 | ||
| 746 | + const els = { | ||
| 747 | + network: document.getElementById('network'), | ||
| 748 | + sidebar: document.getElementById('sidebar'), | ||
| 749 | + loading: document.getElementById('loadingOverlay'), | ||
| 750 | + empty: document.getElementById('emptyState'), | ||
| 751 | + stats: { | ||
| 752 | + nodes: document.getElementById('totalNodes'), | ||
| 753 | + edges: document.getElementById('totalEdges'), | ||
| 754 | + counts: {} | ||
| 755 | + }, | ||
| 756 | + search: { | ||
| 757 | + input: document.getElementById('searchInput'), | ||
| 758 | + prev: document.getElementById('searchPrevBtn'), | ||
| 759 | + next: document.getElementById('searchNextBtn'), | ||
| 760 | + status: document.getElementById('searchStatus') | ||
| 761 | + }, | ||
| 762 | + detail: { | ||
| 763 | + panel: document.getElementById('nodeDetailPanel'), | ||
| 764 | + title: document.getElementById('detailTitle'), | ||
| 765 | + type: document.getElementById('detailType'), | ||
| 766 | + props: document.getElementById('detailProps') | ||
| 767 | + } | ||
| 768 | + }; | ||
| 630 | 769 | ||
| 631 | // 初始化 | 770 | // 初始化 |
| 632 | document.addEventListener('DOMContentLoaded', () => { | 771 | document.addEventListener('DOMContentLoaded', () => { |
| 633 | - loadGraphData(); | ||
| 634 | - startGraphPolling(); | 772 | + // 获取 Report ID |
| 773 | + const container = document.getElementById('network'); | ||
| 774 | + if (container) reportId = container.dataset.reportId || null; | ||
| 775 | + | ||
| 776 | + initGraph(); | ||
| 635 | setupEventListeners(); | 777 | setupEventListeners(); |
| 778 | + startPolling(); | ||
| 636 | }); | 779 | }); |
| 637 | 780 | ||
| 638 | - // 加载图谱数据 | 781 | + // 初始化图谱 |
| 782 | + function initGraph() { | ||
| 783 | + loadGraphData(); | ||
| 784 | + } | ||
| 785 | + | ||
| 786 | + // 加载数据 | ||
| 639 | async function loadGraphData(options = {}) { | 787 | async function loadGraphData(options = {}) { |
| 640 | - const { fromPoll = false, fromManual = false, allowFallback = true } = options; | ||
| 641 | - // 仅在首次或未加载成功时展示大遮罩 | ||
| 642 | - if (!graphReady || !fromPoll) { | ||
| 643 | - showLoading(true); | ||
| 644 | - } | 788 | + const { fromPoll = false, fromManual = false } = options; |
| 645 | 789 | ||
| 790 | + if (!isGraphReady && !fromPoll) showLoading(true); | ||
| 791 | + | ||
| 646 | try { | 792 | try { |
| 647 | - const fetchGraph = async (id) => { | 793 | + const fetchUrl = async (id) => { |
| 648 | const url = id ? `/api/graph/${id}` : '/api/graph/latest'; | 794 | const url = id ? `/api/graph/${id}` : '/api/graph/latest'; |
| 649 | - const response = await fetch(url, { cache: 'no-store' }); | ||
| 650 | - const data = await response.json(); | ||
| 651 | - if (!response.ok || !data.success || !data.graph) { | ||
| 652 | - return null; | ||
| 653 | - } | ||
| 654 | - return data; | 795 | + const res = await fetch(url, { cache: 'no-store' }); |
| 796 | + return await res.json(); | ||
| 655 | }; | 797 | }; |
| 656 | 798 | ||
| 657 | - let data = await fetchGraph(reportId); | ||
| 658 | - let usedFallback = false; | ||
| 659 | - const allowLatestFallback = allowFallback && !reportId; | ||
| 660 | - if ((!data || !data.graph) && allowLatestFallback) { | ||
| 661 | - data = await fetchGraph(null); | ||
| 662 | - usedFallback = !!(data && data.graph); | ||
| 663 | - } | 799 | + // 尝试获取数据 |
| 800 | + let data = await fetchUrl(reportId); | ||
| 664 | 801 | ||
| 665 | - if (data && data.graph) { | ||
| 666 | - if (data.report_id) { | ||
| 667 | - reportId = data.report_id; | ||
| 668 | - } | ||
| 669 | - allNodes = data.graph.nodes; | ||
| 670 | - allEdges = data.graph.edges; | ||
| 671 | - | ||
| 672 | - updateStats(data.graph.stats); | ||
| 673 | - resetGraphSearchState(); | ||
| 674 | - renderGraph(); | ||
| 675 | - const input = document.getElementById('searchInput'); | ||
| 676 | - const currentKeyword = (input && input.value) ? input.value : ''; | ||
| 677 | - if (currentKeyword) { | ||
| 678 | - runGraphSearch(currentKeyword); | ||
| 679 | - } | ||
| 680 | - showLoading(false); | ||
| 681 | - showEmpty(false); | ||
| 682 | - graphReady = true; | ||
| 683 | - stopGraphPolling(); | ||
| 684 | - if (fromManual) { | ||
| 685 | - showToast(usedFallback ? '未找到指定图谱,已切换至最新版本' : '已刷新最新图谱'); | ||
| 686 | - } | ||
| 687 | - } else { | ||
| 688 | - showEmpty(true); | ||
| 689 | - graphReady = false; | ||
| 690 | - allNodes = []; | ||
| 691 | - allEdges = []; | ||
| 692 | - resetGraphSearchState(); | ||
| 693 | - updateStats({ | ||
| 694 | - total_nodes: 0, | ||
| 695 | - total_edges: 0, | ||
| 696 | - topic: 0, | ||
| 697 | - engine: 0, | ||
| 698 | - section: 0, | ||
| 699 | - search_query: 0, | ||
| 700 | - source: 0 | ||
| 701 | - }); | ||
| 702 | - if (network) { | ||
| 703 | - network.destroy(); | ||
| 704 | - network = null; | ||
| 705 | - } | ||
| 706 | - hideNodeDetail(); | ||
| 707 | - showLoading(false); | ||
| 708 | - if (fromManual) { | ||
| 709 | - showToast('未找到图谱数据'); | ||
| 710 | - } | 802 | + // 如果指定ID没有数据,尝试获取最新 |
| 803 | + if ((!data || !data.success || !data.graph) && !reportId) { | ||
| 804 | + data = await fetchUrl(null); | ||
| 711 | } | 805 | } |
| 712 | - } catch (error) { | ||
| 713 | - console.error('加载图谱失败:', error); | ||
| 714 | - showToast('加载图谱失败: ' + error.message); | ||
| 715 | - if (!graphReady) { | ||
| 716 | - showEmpty(true); | 806 | + |
| 807 | + if (data && data.success && data.graph) { | ||
| 808 | + handleGraphSuccess(data, fromManual); | ||
| 809 | + } else { | ||
| 810 | + handleGraphEmpty(fromManual); | ||
| 717 | } | 811 | } |
| 718 | - showLoading(false); | 812 | + } catch (err) { |
| 813 | + console.error("Graph load error:", err); | ||
| 814 | + if (fromManual) showToast('加载失败: ' + err.message, 'error'); | ||
| 815 | + if (!isGraphReady) showEmpty(true); | ||
| 816 | + } finally { | ||
| 817 | + if (!isGraphReady) showLoading(false); | ||
| 719 | } | 818 | } |
| 720 | } | 819 | } |
| 721 | 820 | ||
| 722 | - function startGraphPolling() { | ||
| 723 | - if (graphPollTimer) return; | ||
| 724 | - graphPollTimer = setInterval(() => { | ||
| 725 | - loadGraphData({ fromPoll: true }); | ||
| 726 | - }, GRAPH_POLL_INTERVAL); | 821 | + function handleGraphSuccess(data, showMessage) { |
| 822 | + const newNodes = data.graph.nodes || []; | ||
| 823 | + const newEdges = data.graph.edges || []; | ||
| 824 | + | ||
| 825 | + // 简单的Diff检查,避免频繁重绘 | ||
| 826 | + if (isGraphReady && newNodes.length === graphData.nodes.length && newEdges.length === graphData.edges.length) { | ||
| 827 | + if (showMessage) showToast('已是最新数据'); | ||
| 828 | + return; | ||
| 829 | + } | ||
| 830 | + | ||
| 831 | + graphData = { nodes: newNodes, edges: newEdges }; | ||
| 832 | + if (data.report_id) reportId = data.report_id; | ||
| 833 | + | ||
| 834 | + updateStats(data.graph.stats); | ||
| 835 | + renderNetwork(); | ||
| 836 | + | ||
| 837 | + // 恢复搜索状态 | ||
| 838 | + if (searchState.keyword) runSearch(searchState.keyword); | ||
| 839 | + | ||
| 840 | + isGraphReady = true; | ||
| 841 | + showEmpty(false); | ||
| 842 | + showLoading(false); | ||
| 843 | + stopPolling(); // 成功加载后停止轮询,或者可以继续轮询以获得实时更新 | ||
| 844 | + | ||
| 845 | + if (showMessage) showToast('图谱已更新'); | ||
| 727 | } | 846 | } |
| 728 | 847 | ||
| 729 | - function stopGraphPolling() { | ||
| 730 | - if (graphPollTimer) { | ||
| 731 | - clearInterval(graphPollTimer); | ||
| 732 | - graphPollTimer = null; | 848 | + function handleGraphEmpty(showMessage) { |
| 849 | + if (!isGraphReady) { | ||
| 850 | + showEmpty(true); | ||
| 851 | + // 清空数据 | ||
| 852 | + graphData = { nodes: [], edges: [] }; | ||
| 853 | + if (network) { | ||
| 854 | + network.destroy(); | ||
| 855 | + network = null; | ||
| 856 | + } | ||
| 857 | + updateStats({}); | ||
| 733 | } | 858 | } |
| 859 | + if (showMessage) showToast('暂无数据'); | ||
| 734 | } | 860 | } |
| 735 | 861 | ||
| 736 | - // 渲染图谱 | ||
| 737 | - function renderGraph() { | ||
| 738 | - const container = document.getElementById('network'); | ||
| 739 | - | ||
| 740 | - // 处理节点 | 862 | + // 渲染 Vis Network |
| 863 | + function renderNetwork() { | ||
| 741 | const visibleTypes = getVisibleTypes(); | 864 | const visibleTypes = getVisibleTypes(); |
| 742 | - const filteredNodes = allNodes.filter(n => visibleTypes.includes(n.group)); | ||
| 743 | - const filteredNodeIds = new Set(filteredNodes.map(n => n.id)); | ||
| 744 | - | ||
| 745 | - const nodes = new vis.DataSet(filteredNodes.map(node => ({ | ||
| 746 | - id: node.id, | ||
| 747 | - label: truncateLabel(node.label, 20), | ||
| 748 | - title: node.title, | ||
| 749 | - group: node.group, | ||
| 750 | - color: { | ||
| 751 | - background: NODE_COLORS[node.group] || '#6B7280', | ||
| 752 | - border: NODE_COLORS[node.group] || '#6B7280', | ||
| 753 | - highlight: { | ||
| 754 | - background: lightenColor(NODE_COLORS[node.group] || '#6B7280'), | ||
| 755 | - border: NODE_COLORS[node.group] || '#6B7280' | ||
| 756 | - } | ||
| 757 | - }, | ||
| 758 | - shape: NODE_SHAPES[node.group] || 'dot', | ||
| 759 | - size: node.group === 'topic' ? 30 : (node.group === 'engine' ? 25 : 15), | ||
| 760 | - font: { | ||
| 761 | - color: '#F1F5F9', | ||
| 762 | - size: 12 | ||
| 763 | - }, | ||
| 764 | - // 保存原始数据 | ||
| 765 | - _data: node | ||
| 766 | - }))); | ||
| 767 | - | ||
| 768 | - // 处理边 | ||
| 769 | - const edges = new vis.DataSet(allEdges | ||
| 770 | - .filter(e => filteredNodeIds.has(e.from) && filteredNodeIds.has(e.to)) | ||
| 771 | - .map(edge => ({ | ||
| 772 | - from: edge.from, | ||
| 773 | - to: edge.to, | ||
| 774 | - label: edge.label, | ||
| 775 | - arrows: edge.arrows || 'to', | ||
| 776 | - color: { | ||
| 777 | - color: '#475569', | ||
| 778 | - highlight: '#818CF8' | ||
| 779 | - }, | ||
| 780 | - font: { | ||
| 781 | - color: '#94A3B8', | ||
| 782 | - size: 10, | ||
| 783 | - strokeWidth: 0 | ||
| 784 | - }, | ||
| 785 | - smooth: { | ||
| 786 | - type: 'continuous' | ||
| 787 | - } | ||
| 788 | - })) | ||
| 789 | - ); | 865 | + const nodes = graphData.nodes |
| 866 | + .filter(n => visibleTypes.includes(n.group)) | ||
| 867 | + .map(n => ({ | ||
| 868 | + id: n.id, | ||
| 869 | + label: truncate(n.label, 20), | ||
| 870 | + group: n.group, | ||
| 871 | + title: n.title || n.label, // Tooltip | ||
| 872 | + _raw: n, // 保存原始数据 | ||
| 873 | + ...getNodeStyle(n.group) | ||
| 874 | + })); | ||
| 875 | + | ||
| 876 | + const nodeIds = new Set(nodes.map(n => n.id)); | ||
| 877 | + const edges = graphData.edges | ||
| 878 | + .filter(e => nodeIds.has(e.from) && nodeIds.has(e.to)) | ||
| 879 | + .map(e => ({ | ||
| 880 | + from: e.from, | ||
| 881 | + to: e.to, | ||
| 882 | + arrows: 'to', | ||
| 883 | + color: { color: '#475569', opacity: 0.6 }, | ||
| 884 | + width: 1 | ||
| 885 | + })); | ||
| 886 | + | ||
| 887 | + const data = { | ||
| 888 | + nodes: new vis.DataSet(nodes), | ||
| 889 | + edges: new vis.DataSet(edges) | ||
| 890 | + }; | ||
| 790 | 891 | ||
| 791 | - // 图谱配置 | ||
| 792 | const options = { | 892 | const options = { |
| 793 | nodes: { | 893 | nodes: { |
| 794 | borderWidth: 2, | 894 | borderWidth: 2, |
| 795 | - shadow: true | 895 | + shadow: true, |
| 896 | + font: { color: '#f8fafc', size: 12, face: 'Inter' } | ||
| 796 | }, | 897 | }, |
| 797 | edges: { | 898 | edges: { |
| 798 | - width: 1, | ||
| 799 | - shadow: true | 899 | + smooth: { type: 'continuous', roundness: 0.5 } |
| 800 | }, | 900 | }, |
| 801 | physics: { | 901 | physics: { |
| 802 | - enabled: true, | ||
| 803 | - solver: 'forceAtlas2Based', | ||
| 804 | - forceAtlas2Based: { | ||
| 805 | - gravitationalConstant: -100, | ||
| 806 | - centralGravity: 0.01, | ||
| 807 | - springLength: 150, | ||
| 808 | - springConstant: 0.08, | ||
| 809 | - damping: 0.5 | ||
| 810 | - }, | ||
| 811 | - stabilization: { | ||
| 812 | - enabled: true, | ||
| 813 | - iterations: 200 | ||
| 814 | - } | 902 | + stabilization: { enabled: true, iterations: 100 }, |
| 903 | + barnesHut: { gravitationalConstant: -2000, springConstant: 0.04, springLength: 95 } | ||
| 815 | }, | 904 | }, |
| 816 | interaction: { | 905 | interaction: { |
| 817 | hover: true, | 906 | hover: true, |
| 818 | - tooltipDelay: 100, | ||
| 819 | - zoomView: true, | ||
| 820 | - dragView: true | 907 | + tooltipDelay: 200, |
| 908 | + zoomView: true | ||
| 821 | } | 909 | } |
| 822 | }; | 910 | }; |
| 823 | 911 | ||
| 824 | - // 创建网络 | ||
| 825 | - network = new vis.Network(container, { nodes, edges }, options); | ||
| 826 | - | ||
| 827 | - // 节点点击事件 | ||
| 828 | - network.on('click', (params) => { | ||
| 829 | - if (params.nodes.length > 0) { | ||
| 830 | - const nodeId = params.nodes[0]; | ||
| 831 | - const node = allNodes.find(n => n.id === nodeId); | ||
| 832 | - if (node) { | ||
| 833 | - showNodeDetail(node); | 912 | + if (!network) { |
| 913 | + network = new vis.Network(els.network, data, options); | ||
| 914 | + | ||
| 915 | + // 事件绑定 | ||
| 916 | + network.on('click', params => { | ||
| 917 | + if (params.nodes.length) { | ||
| 918 | + const nodeId = params.nodes[0]; | ||
| 919 | + const node = graphData.nodes.find(n => n.id === nodeId); | ||
| 920 | + showNodeDetails(node); | ||
| 921 | + } else { | ||
| 922 | + hideNodeDetail(); | ||
| 834 | } | 923 | } |
| 835 | - } else { | ||
| 836 | - hideNodeDetail(); | ||
| 837 | - } | ||
| 838 | - }); | ||
| 839 | - | ||
| 840 | - // 稳定后适应视图 | ||
| 841 | - network.once('stabilizationIterationsDone', () => { | ||
| 842 | - network.fit({ animation: true }); | ||
| 843 | - }); | 924 | + }); |
| 844 | 925 | ||
| 845 | - // 如果已有搜索关键词,重新聚焦当前匹配;否则更新状态显示 | ||
| 846 | - if (graphSearchKeyword) { | ||
| 847 | - runGraphSearch(graphSearchKeyword); | 926 | + network.on('stabilizationIterationsDone', () => { |
| 927 | + // 初次加载完成也可以fit一下,但有时会跳动,可视情况开启 | ||
| 928 | + // network.fit(); | ||
| 929 | + }); | ||
| 848 | } else { | 930 | } else { |
| 849 | - updateGraphSearchStatus(); | 931 | + network.setData(data); |
| 850 | } | 932 | } |
| 851 | } | 933 | } |
| 852 | 934 | ||
| 853 | - // 显示节点详情 | ||
| 854 | - function showNodeDetail(node) { | ||
| 855 | - const detailPanel = document.getElementById('nodeDetail'); | ||
| 856 | - const titleEl = document.getElementById('detailTitle'); | ||
| 857 | - const typeEl = document.getElementById('detailType'); | ||
| 858 | - const propsEl = document.getElementById('detailProps'); | ||
| 859 | - | ||
| 860 | - titleEl.textContent = node.label; | ||
| 861 | - | ||
| 862 | - const typeLabels = { | ||
| 863 | - topic: '主题', | ||
| 864 | - engine: '分析引擎', | ||
| 865 | - section: '报告段落', | ||
| 866 | - search_query: '搜索关键词', | ||
| 867 | - source: '数据来源' | 935 | + function getNodeStyle(group) { |
| 936 | + const conf = NODE_CONFIG[group] || { color: '#94a3b8', shape: 'dot' }; | ||
| 937 | + return { | ||
| 938 | + color: { | ||
| 939 | + background: conf.color, | ||
| 940 | + border: conf.color, | ||
| 941 | + highlight: { background: lighten(conf.color, 20), border: conf.color }, | ||
| 942 | + hover: { background: lighten(conf.color, 20), border: conf.color } | ||
| 943 | + }, | ||
| 944 | + shape: conf.shape, | ||
| 945 | + size: conf.size | ||
| 868 | }; | 946 | }; |
| 869 | - typeEl.textContent = typeLabels[node.group] || node.group; | ||
| 870 | - | ||
| 871 | - // 显示属性 | ||
| 872 | - let propsHtml = ''; | ||
| 873 | - const props = node.properties || {}; | ||
| 874 | - for (const [key, value] of Object.entries(props)) { | ||
| 875 | - if (value) { | ||
| 876 | - propsHtml += ` | ||
| 877 | - <div class="prop-item"> | ||
| 878 | - <div class="prop-key">${key}</div> | ||
| 879 | - <div class="prop-value">${truncateText(String(value), 200)}</div> | ||
| 880 | - </div> | ||
| 881 | - `; | ||
| 882 | - } | ||
| 883 | - } | ||
| 884 | - propsEl.innerHTML = propsHtml || '<div class="prop-item">无附加属性</div>'; | ||
| 885 | - | ||
| 886 | - detailPanel.style.display = 'block'; | ||
| 887 | } | 947 | } |
| 888 | 948 | ||
| 889 | - // 隐藏节点详情 | ||
| 890 | - function hideNodeDetail() { | ||
| 891 | - document.getElementById('nodeDetail').style.display = 'none'; | ||
| 892 | - } | 949 | + // 交互逻辑 |
| 950 | + function setupEventListeners() { | ||
| 951 | + // 侧边栏切换 | ||
| 952 | + document.getElementById('toggleSidebar').addEventListener('click', () => { | ||
| 953 | + els.sidebar.classList.add('collapsed'); | ||
| 954 | + }); | ||
| 955 | + document.getElementById('expandSidebar').addEventListener('click', () => { | ||
| 956 | + els.sidebar.classList.remove('collapsed'); | ||
| 957 | + }); | ||
| 893 | 958 | ||
| 894 | - function resetGraphSearchState() { | ||
| 895 | - graphSearchResults = []; | ||
| 896 | - graphSearchIndex = -1; | ||
| 897 | - graphSearchKeyword = ''; | ||
| 898 | - updateGraphSearchStatus(); | ||
| 899 | - } | 959 | + // 搜索 |
| 960 | + els.search.input.addEventListener('input', (e) => runSearch(e.target.value)); | ||
| 961 | + els.search.prev.addEventListener('click', () => navSearch(-1)); | ||
| 962 | + els.search.next.addEventListener('click', () => navSearch(1)); | ||
| 900 | 963 | ||
| 901 | - function updateGraphSearchStatus() { | ||
| 902 | - const statusEl = document.getElementById('searchStatus'); | ||
| 903 | - const prevBtn = document.getElementById('searchPrevBtn'); | ||
| 904 | - const nextBtn = document.getElementById('searchNextBtn'); | ||
| 905 | - const hasResults = graphSearchResults.length > 0 && graphSearchIndex >= 0; | ||
| 906 | - if (statusEl) { | ||
| 907 | - statusEl.textContent = hasResults ? `${graphSearchIndex + 1}/${graphSearchResults.length}` : '0/0'; | ||
| 908 | - statusEl.style.visibility = hasResults ? 'visible' : 'hidden'; | ||
| 909 | - } | ||
| 910 | - if (prevBtn) prevBtn.disabled = !hasResults; | ||
| 911 | - if (nextBtn) nextBtn.disabled = !hasResults; | ||
| 912 | - } | 964 | + // 过滤器 |
| 965 | + document.querySelectorAll('.filter-item input').forEach(cb => { | ||
| 966 | + cb.addEventListener('change', () => { | ||
| 967 | + const parent = cb.closest('.filter-item'); | ||
| 968 | + if (cb.checked) parent.classList.add('active'); | ||
| 969 | + else parent.classList.remove('active'); | ||
| 970 | + renderNetwork(); | ||
| 971 | + }); | ||
| 972 | + }); | ||
| 913 | 973 | ||
| 914 | - function runGraphSearch(keyword) { | ||
| 915 | - if (!network) return; | ||
| 916 | - const term = (keyword || '').trim(); | ||
| 917 | - graphSearchKeyword = term; | 974 | + // 工具栏 |
| 975 | + document.getElementById('refreshBtn').addEventListener('click', () => loadGraphData({fromManual: true})); | ||
| 976 | + document.getElementById('zoomInBtn').addEventListener('click', () => network?.moveTo({scale: network.getScale() * 1.2, animation: true})); | ||
| 977 | + document.getElementById('zoomOutBtn').addEventListener('click', () => network?.moveTo({scale: network.getScale() / 1.2, animation: true})); | ||
| 978 | + document.getElementById('fitBtn').addEventListener('click', () => network?.fit({animation: true})); | ||
| 979 | + document.getElementById('fullscreenBtn').addEventListener('click', toggleFullScreen); | ||
| 980 | + } | ||
| 918 | 981 | ||
| 919 | - if (!term) { | ||
| 920 | - resetGraphSearchState(); | ||
| 921 | - network.selectNodes([]); | 982 | + // 搜索功能 |
| 983 | + function runSearch(keyword) { | ||
| 984 | + keyword = keyword.trim().toLowerCase(); | ||
| 985 | + searchState.keyword = keyword; | ||
| 986 | + | ||
| 987 | + if (!keyword) { | ||
| 988 | + searchState.results = []; | ||
| 989 | + searchState.currentIndex = -1; | ||
| 990 | + updateSearchUI(); | ||
| 991 | + if(network) network.unselectAll(); | ||
| 922 | return; | 992 | return; |
| 923 | } | 993 | } |
| 924 | 994 | ||
| 925 | - const lower = term.toLowerCase(); | ||
| 926 | - const nodesDataset = network.body && network.body.data && network.body.data.nodes ? network.body.data.nodes.get() : []; | ||
| 927 | - graphSearchResults = nodesDataset.filter(n => (n.label || '').toLowerCase().includes(lower)); | ||
| 928 | - graphSearchResults.sort((a, b) => { | ||
| 929 | - const aLabel = (a.label || '').toLowerCase(); | ||
| 930 | - const bLabel = (b.label || '').toLowerCase(); | ||
| 931 | - if (aLabel === bLabel) { | ||
| 932 | - return String(a.id).localeCompare(String(b.id), 'zh'); | ||
| 933 | - } | ||
| 934 | - return aLabel.localeCompare(bLabel, 'zh'); | ||
| 935 | - }); | ||
| 936 | - graphSearchIndex = graphSearchResults.length ? 0 : -1; | ||
| 937 | - | ||
| 938 | - if (!graphSearchResults.length) { | ||
| 939 | - network.selectNodes([]); | ||
| 940 | - hideNodeDetail(); | ||
| 941 | - updateGraphSearchStatus(); | ||
| 942 | - return; | 995 | + // 在当前显示的数据集中搜索 |
| 996 | + const visibleTypes = getVisibleTypes(); | ||
| 997 | + searchState.results = graphData.nodes | ||
| 998 | + .filter(n => visibleTypes.includes(n.group) && n.label.toLowerCase().includes(keyword)) | ||
| 999 | + .map(n => n.id); | ||
| 1000 | + | ||
| 1001 | + searchState.currentIndex = searchState.results.length ? 0 : -1; | ||
| 1002 | + updateSearchUI(); | ||
| 1003 | + | ||
| 1004 | + if (searchState.results.length > 0) { | ||
| 1005 | + focusNode(searchState.results[0]); | ||
| 943 | } | 1006 | } |
| 944 | - | ||
| 945 | - focusGraphSearchIndex(graphSearchIndex); | ||
| 946 | } | 1007 | } |
| 947 | 1008 | ||
| 948 | - function focusGraphSearchIndex(index) { | ||
| 949 | - if (!network || !graphSearchResults.length) return; | ||
| 950 | - const total = graphSearchResults.length; | ||
| 951 | - graphSearchIndex = ((index % total) + total) % total; | ||
| 952 | - const target = graphSearchResults[graphSearchIndex]; | ||
| 953 | - network.selectNodes([target.id]); | ||
| 954 | - network.focus(target.id, { animation: true, scale: 1.4 }); | ||
| 955 | - showNodeDetail(target._data || target); | ||
| 956 | - updateGraphSearchStatus(); | 1009 | + function navSearch(direction) { |
| 1010 | + if (!searchState.results.length) return; | ||
| 1011 | + const len = searchState.results.length; | ||
| 1012 | + searchState.currentIndex = (searchState.currentIndex + direction + len) % len; | ||
| 1013 | + focusNode(searchState.results[searchState.currentIndex]); | ||
| 1014 | + updateSearchUI(); | ||
| 957 | } | 1015 | } |
| 958 | 1016 | ||
| 959 | - function stepGraphSearch(delta) { | ||
| 960 | - if (!graphSearchResults.length) return; | ||
| 961 | - focusGraphSearchIndex(graphSearchIndex + delta); | 1017 | + function focusNode(nodeId) { |
| 1018 | + if (!network) return; | ||
| 1019 | + network.selectNodes([nodeId]); | ||
| 1020 | + network.focus(nodeId, { | ||
| 1021 | + scale: 1.2, | ||
| 1022 | + animation: true | ||
| 1023 | + }); | ||
| 1024 | + const node = graphData.nodes.find(n => n.id === nodeId); | ||
| 1025 | + if (node) showNodeDetails(node); | ||
| 962 | } | 1026 | } |
| 963 | 1027 | ||
| 964 | - // 更新统计 | ||
| 965 | - function updateStats(stats) { | ||
| 966 | - document.getElementById('nodeCount').textContent = stats.total_nodes || 0; | ||
| 967 | - document.getElementById('edgeCount').textContent = stats.total_edges || 0; | 1028 | + function updateSearchUI() { |
| 1029 | + const { results, currentIndex } = searchState; | ||
| 1030 | + const hasResults = results.length > 0; | ||
| 968 | 1031 | ||
| 969 | - // 更新各类型计数 | ||
| 970 | - document.getElementById('count-topic').textContent = stats.topic || 0; | ||
| 971 | - document.getElementById('count-engine').textContent = stats.engine || 0; | ||
| 972 | - document.getElementById('count-section').textContent = stats.section || 0; | ||
| 973 | - document.getElementById('count-search_query').textContent = stats.search_query || 0; | ||
| 974 | - document.getElementById('count-source').textContent = stats.source || 0; | ||
| 975 | - } | ||
| 976 | - | ||
| 977 | - // 获取可见类型 | ||
| 978 | - function getVisibleTypes() { | ||
| 979 | - const types = []; | ||
| 980 | - document.querySelectorAll('.filter-item input[type="checkbox"]').forEach(cb => { | ||
| 981 | - if (cb.checked) { | ||
| 982 | - types.push(cb.dataset.type); | 1032 | + els.search.prev.disabled = !hasResults; |
| 1033 | + els.search.next.disabled = !hasResults; | ||
| 1034 | + els.search.status.textContent = hasResults | ||
| 1035 | + ? `${currentIndex + 1} / ${results.length}` | ||
| 1036 | + : (searchState.keyword ? '无结果' : ''); | ||
| 1037 | + } | ||
| 1038 | + | ||
| 1039 | + // 详情面板 | ||
| 1040 | + function showNodeDetails(node) { | ||
| 1041 | + if (!node) return; | ||
| 1042 | + els.detail.panel.style.display = 'block'; | ||
| 1043 | + els.detail.title.textContent = node.label; | ||
| 1044 | + els.detail.type.textContent = (NODE_CONFIG[node.group] || {}).label || node.group; | ||
| 1045 | + | ||
| 1046 | + let html = ''; | ||
| 1047 | + const props = node.properties || {}; | ||
| 1048 | + if (Object.keys(props).length === 0) { | ||
| 1049 | + html = '<div class="prop-label" style="text-align:center; padding:10px;">暂无额外属性</div>'; | ||
| 1050 | + } else { | ||
| 1051 | + for (const [k, v] of Object.entries(props)) { | ||
| 1052 | + html += ` | ||
| 1053 | + <div class="prop-row"> | ||
| 1054 | + <div class="prop-label">${k}</div> | ||
| 1055 | + <div class="prop-value">${v}</div> | ||
| 1056 | + </div> | ||
| 1057 | + `; | ||
| 983 | } | 1058 | } |
| 984 | - }); | ||
| 985 | - return types; | 1059 | + } |
| 1060 | + els.detail.props.innerHTML = html; | ||
| 986 | } | 1061 | } |
| 987 | 1062 | ||
| 988 | - // 设置事件监听 | ||
| 989 | - function setupEventListeners() { | ||
| 990 | - // 侧边栏切换 | ||
| 991 | - document.getElementById('toggleSidebar').addEventListener('click', () => { | ||
| 992 | - const sidebar = document.getElementById('sidebar'); | ||
| 993 | - const container = document.getElementById('graphContainer'); | ||
| 994 | - const legend = document.getElementById('legend'); | ||
| 995 | - | ||
| 996 | - sidebar.classList.toggle('collapsed'); | ||
| 997 | - container.classList.toggle('fullwidth'); | ||
| 998 | - legend.classList.toggle('fullwidth'); | ||
| 999 | - }); | ||
| 1000 | - | ||
| 1001 | - // 适应视图 | ||
| 1002 | - document.getElementById('fitBtn').addEventListener('click', () => { | ||
| 1003 | - if (network) network.fit({ animation: true }); | ||
| 1004 | - }); | ||
| 1005 | - | ||
| 1006 | - // 放大 | ||
| 1007 | - document.getElementById('zoomInBtn').addEventListener('click', () => { | ||
| 1008 | - if (network) { | ||
| 1009 | - const scale = network.getScale() * 1.2; | ||
| 1010 | - network.moveTo({ scale, animation: true }); | ||
| 1011 | - } | ||
| 1012 | - }); | 1063 | + function hideNodeDetail() { |
| 1064 | + els.detail.panel.style.display = 'none'; | ||
| 1065 | + } | ||
| 1013 | 1066 | ||
| 1014 | - // 缩小 | ||
| 1015 | - document.getElementById('zoomOutBtn').addEventListener('click', () => { | ||
| 1016 | - if (network) { | ||
| 1017 | - const scale = network.getScale() / 1.2; | ||
| 1018 | - network.moveTo({ scale, animation: true }); | ||
| 1019 | - } | 1067 | + // 辅助功能 |
| 1068 | + function updateStats(stats = {}) { | ||
| 1069 | + els.stats.nodes.textContent = stats.total_nodes || 0; | ||
| 1070 | + els.stats.edges.textContent = stats.total_edges || 0; | ||
| 1071 | + | ||
| 1072 | + ['topic', 'engine', 'section', 'search_query', 'source'].forEach(type => { | ||
| 1073 | + const el = document.getElementById(`count-${type}`); | ||
| 1074 | + if (el) el.textContent = stats[type] || 0; | ||
| 1020 | }); | 1075 | }); |
| 1076 | + } | ||
| 1021 | 1077 | ||
| 1022 | - // 手动刷新 | ||
| 1023 | - const manualRefreshBtn = document.getElementById('manualRefreshBtn'); | ||
| 1024 | - if (manualRefreshBtn) { | ||
| 1025 | - manualRefreshBtn.addEventListener('click', () => { | ||
| 1026 | - loadGraphData({ fromManual: true }); | ||
| 1027 | - }); | ||
| 1028 | - } | 1078 | + function getVisibleTypes() { |
| 1079 | + return Array.from(document.querySelectorAll('.filter-item input:checked')) | ||
| 1080 | + .map(cb => cb.dataset.type); | ||
| 1081 | + } | ||
| 1029 | 1082 | ||
| 1030 | - // 全屏 | ||
| 1031 | - document.getElementById('fullscreenBtn').addEventListener('click', () => { | ||
| 1032 | - if (!document.fullscreenElement) { | ||
| 1033 | - document.documentElement.requestFullscreen(); | ||
| 1034 | - } else { | ||
| 1035 | - document.exitFullscreen(); | ||
| 1036 | - } | ||
| 1037 | - }); | 1083 | + function startPolling() { |
| 1084 | + if (pollTimer) return; | ||
| 1085 | + pollTimer = setInterval(() => loadGraphData({fromPoll: true}), 5000); | ||
| 1086 | + } | ||
| 1038 | 1087 | ||
| 1039 | - // 搜索 | ||
| 1040 | - const searchInput = document.getElementById('searchInput'); | ||
| 1041 | - const searchBtn = document.getElementById('searchBtn'); | ||
| 1042 | - const searchPrevBtn = document.getElementById('searchPrevBtn'); | ||
| 1043 | - const searchNextBtn = document.getElementById('searchNextBtn'); | ||
| 1044 | - if (searchInput) { | ||
| 1045 | - searchInput.addEventListener('keydown', (e) => { | ||
| 1046 | - if (e.key === 'Enter') { | ||
| 1047 | - runGraphSearch(searchInput.value); | ||
| 1048 | - } | ||
| 1049 | - }); | ||
| 1050 | - searchInput.addEventListener('input', () => { | ||
| 1051 | - if (!searchInput.value) { | ||
| 1052 | - resetGraphSearchState(); | ||
| 1053 | - if (network) network.selectNodes([]); | ||
| 1054 | - } | ||
| 1055 | - }); | ||
| 1056 | - } | ||
| 1057 | - if (searchBtn) { | ||
| 1058 | - searchBtn.addEventListener('click', () => runGraphSearch(searchInput ? searchInput.value : '')); | ||
| 1059 | - } | ||
| 1060 | - if (searchPrevBtn) { | ||
| 1061 | - searchPrevBtn.addEventListener('click', () => stepGraphSearch(-1)); | 1088 | + function stopPolling() { |
| 1089 | + if (pollTimer) { | ||
| 1090 | + clearInterval(pollTimer); | ||
| 1091 | + pollTimer = null; | ||
| 1062 | } | 1092 | } |
| 1063 | - if (searchNextBtn) { | ||
| 1064 | - searchNextBtn.addEventListener('click', () => stepGraphSearch(1)); | ||
| 1065 | - } | ||
| 1066 | - | ||
| 1067 | - // 筛选 | ||
| 1068 | - document.querySelectorAll('.filter-item input[type="checkbox"]').forEach(cb => { | ||
| 1069 | - cb.addEventListener('change', () => { | ||
| 1070 | - renderGraph(); | ||
| 1071 | - }); | ||
| 1072 | - }); | ||
| 1073 | } | 1093 | } |
| 1074 | 1094 | ||
| 1075 | - // 辅助函数 | ||
| 1076 | function showLoading(show) { | 1095 | function showLoading(show) { |
| 1077 | - document.getElementById('loadingOverlay').style.display = show ? 'flex' : 'none'; | 1096 | + els.loading.style.display = show ? 'flex' : 'none'; |
| 1078 | } | 1097 | } |
| 1079 | 1098 | ||
| 1080 | function showEmpty(show) { | 1099 | function showEmpty(show) { |
| 1081 | - document.getElementById('emptyState').style.display = show ? 'block' : 'none'; | 1100 | + els.empty.style.display = show ? 'flex' : 'none'; |
| 1101 | + if (show) els.loading.style.display = 'none'; | ||
| 1082 | } | 1102 | } |
| 1083 | 1103 | ||
| 1084 | - function showToast(message) { | 1104 | + function showToast(msg, type = 'info') { |
| 1085 | const toast = document.getElementById('toast'); | 1105 | const toast = document.getElementById('toast'); |
| 1086 | - toast.textContent = message; | ||
| 1087 | - toast.style.display = 'block'; | ||
| 1088 | - setTimeout(() => { | ||
| 1089 | - toast.style.display = 'none'; | ||
| 1090 | - }, 3000); | 1106 | + const iconContainer = document.getElementById('toastIcon'); |
| 1107 | + | ||
| 1108 | + document.getElementById('toastMsg').textContent = msg; | ||
| 1109 | + toast.style.borderColor = type === 'error' ? '#EF4444' : '#6366f1'; | ||
| 1110 | + | ||
| 1111 | + // 图标定义 | ||
| 1112 | + const icons = { | ||
| 1113 | + success: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>', | ||
| 1114 | + error: '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>' | ||
| 1115 | + }; | ||
| 1116 | + | ||
| 1117 | + // 如果是 error 则显示错号,否则显示对号 | ||
| 1118 | + const isError = type === 'error'; | ||
| 1119 | + iconContainer.innerHTML = isError ? icons.error : icons.success; | ||
| 1120 | + iconContainer.style.color = isError ? '#EF4444' : '#10B981'; | ||
| 1121 | + | ||
| 1122 | + toast.classList.add('show'); | ||
| 1123 | + setTimeout(() => toast.classList.remove('show'), 3000); | ||
| 1091 | } | 1124 | } |
| 1092 | 1125 | ||
| 1093 | - function truncateLabel(text, maxLen) { | ||
| 1094 | - if (!text) return ''; | ||
| 1095 | - return text.length > maxLen ? text.slice(0, maxLen) + '...' : text; | 1126 | + function toggleFullScreen() { |
| 1127 | + if (!document.fullscreenElement) { | ||
| 1128 | + document.documentElement.requestFullscreen(); | ||
| 1129 | + } else { | ||
| 1130 | + if (document.exitFullscreen) document.exitFullscreen(); | ||
| 1131 | + } | ||
| 1096 | } | 1132 | } |
| 1097 | 1133 | ||
| 1098 | - function truncateText(text, maxLen) { | ||
| 1099 | - if (!text) return ''; | ||
| 1100 | - return text.length > maxLen ? text.slice(0, maxLen) + '...' : text; | 1134 | + // Utils |
| 1135 | + function truncate(str, n) { | ||
| 1136 | + return (str && str.length > n) ? str.substr(0, n - 1) + '...' : str; | ||
| 1101 | } | 1137 | } |
| 1102 | 1138 | ||
| 1103 | - function lightenColor(color) { | ||
| 1104 | - // 简单的颜色变亮 | ||
| 1105 | - const hex = color.replace('#', ''); | ||
| 1106 | - const r = Math.min(255, parseInt(hex.slice(0, 2), 16) + 40); | ||
| 1107 | - const g = Math.min(255, parseInt(hex.slice(2, 4), 16) + 40); | ||
| 1108 | - const b = Math.min(255, parseInt(hex.slice(4, 6), 16) + 40); | ||
| 1109 | - return `rgb(${r}, ${g}, ${b})`; | 1139 | + function lighten(col, amt) { |
| 1140 | + var usePound = false; | ||
| 1141 | + if (col[0] == "#") { | ||
| 1142 | + col = col.slice(1); | ||
| 1143 | + usePound = true; | ||
| 1144 | + } | ||
| 1145 | + var num = parseInt(col, 16); | ||
| 1146 | + var r = (num >> 16) + amt; | ||
| 1147 | + if (r > 255) r = 255; | ||
| 1148 | + else if (r < 0) r = 0; | ||
| 1149 | + var b = ((num >> 8) & 0x00FF) + amt; | ||
| 1150 | + if (b > 255) b = 255; | ||
| 1151 | + else if (b < 0) b = 0; | ||
| 1152 | + var g = (num & 0x0000FF) + amt; | ||
| 1153 | + if (g > 255) g = 255; | ||
| 1154 | + else if (g < 0) g = 0; | ||
| 1155 | + return (usePound ? "#" : "") + (g | (b << 8) | (r << 16)).toString(16); | ||
| 1110 | } | 1156 | } |
| 1111 | </script> | 1157 | </script> |
| 1112 | </body> | 1158 | </body> |
-
Please register or login to post a comment