|
413 | 413 | padding: 16px 32px 32px; |
414 | 414 | } |
415 | 415 |
|
| 416 | + .graph-stage { |
| 417 | + position: relative; |
| 418 | + min-width: 0; |
| 419 | + } |
| 420 | + |
| 421 | + .connection-layer { |
| 422 | + position: absolute; |
| 423 | + inset: 0; |
| 424 | + width: 100%; |
| 425 | + height: 100%; |
| 426 | + pointer-events: none; |
| 427 | + z-index: 0; |
| 428 | + overflow: visible; |
| 429 | + } |
| 430 | + |
| 431 | + .graph-line { |
| 432 | + fill: none; |
| 433 | + stroke: rgba(255, 255, 255, 0.22); |
| 434 | + stroke-width: 2.5; |
| 435 | + stroke-linecap: round; |
| 436 | + stroke-dasharray: 6 8; |
| 437 | + } |
| 438 | + |
| 439 | + .graph-line-primary { |
| 440 | + stroke: rgba(243, 87, 45, 0.68); |
| 441 | + } |
| 442 | + |
| 443 | + .graph-line-accent { |
| 444 | + stroke: rgba(101, 210, 123, 0.62); |
| 445 | + } |
| 446 | + |
416 | 447 | .lane-board { |
417 | 448 | display: grid; |
418 | 449 | grid-template-columns: repeat(4, minmax(280px, 1fr)); |
419 | 450 | gap: 20px; |
420 | 451 | min-width: 0; |
421 | 452 | align-items: start; |
| 453 | + position: relative; |
| 454 | + z-index: 1; |
422 | 455 | } |
423 | 456 |
|
424 | 457 | .lane { |
|
557 | 590 | padding: 14px; |
558 | 591 | display: grid; |
559 | 592 | gap: 10px; |
| 593 | + position: relative; |
560 | 594 | } |
561 | 595 |
|
562 | 596 | .group-card.active, |
|
712 | 746 | padding: 12px; |
713 | 747 | display: grid; |
714 | 748 | gap: 10px; |
| 749 | + position: relative; |
715 | 750 | } |
716 | 751 |
|
717 | 752 | .route-row { |
|
770 | 805 | color: var(--text-secondary); |
771 | 806 | } |
772 | 807 |
|
| 808 | + .graph-anchor-dot { |
| 809 | + position: absolute; |
| 810 | + top: 50%; |
| 811 | + width: 10px; |
| 812 | + height: 10px; |
| 813 | + border: 2px solid var(--accent); |
| 814 | + background: var(--bg-panel-soft); |
| 815 | + transform: translateY(-50%); |
| 816 | + box-shadow: 0 0 0 3px rgba(243, 87, 45, 0.12); |
| 817 | + } |
| 818 | + |
| 819 | + .graph-anchor-dot.left { |
| 820 | + left: -7px; |
| 821 | + } |
| 822 | + |
| 823 | + .graph-anchor-dot.right { |
| 824 | + right: -7px; |
| 825 | + } |
| 826 | + |
773 | 827 | .mapping-line, |
774 | 828 | .reason-line { |
775 | 829 | display: grid; |
|
1098 | 1152 | <div class="chain-bar" id="chain-bar" data-collaboration-id="RO.CHAIN"></div> |
1099 | 1153 |
|
1100 | 1154 | <div class="content-grid"> |
1101 | | - <section class="lane-board"> |
| 1155 | + <div class="graph-stage" id="graph-stage"> |
| 1156 | + <svg class="connection-layer" id="connection-layer" aria-hidden="true"></svg> |
| 1157 | + <section class="lane-board"> |
1102 | 1158 | <article class="lane active" data-lane="entry" data-collaboration-id="RO.LANE.ENTRY"> |
1103 | 1159 | <div class="lane-head"> |
1104 | 1160 | <div class="lane-head-top"> |
|
1114 | 1170 | </div> |
1115 | 1171 | </div> |
1116 | 1172 |
|
1117 | | - <div class="lane-section"> |
| 1173 | + <div class="lane-section" data-graph-anchor="entry"> |
| 1174 | + <span class="graph-anchor-dot right" aria-hidden="true"></span> |
1118 | 1175 | <div class="section-kicker">请求入口</div> |
1119 | 1176 | <div class="segmented"> |
1120 | 1177 | <button class="cli-token active" data-cli="codex" type="button">codex</button> |
|
1185 | 1242 | <span>结束</span> |
1186 | 1243 | <span class="locator">RO.LANE.END</span> |
1187 | 1244 | </div> |
1188 | | - <div class="lane-copy"> |
| 1245 | + <div class="lane-copy"> |
1189 | 1246 | 这里是链路收口区。确认当前请求会落到哪些账号, 走不走代理, 并允许直接发起测试。 |
1190 | 1247 | </div> |
1191 | 1248 | </div> |
1192 | 1249 |
|
1193 | 1250 | <div class="lane-section" id="end-list" data-collaboration-id="RO.LANE.END.LIST"></div> |
1194 | 1251 | </article> |
1195 | | - </section> |
| 1252 | + </section> |
| 1253 | + </div> |
1196 | 1254 |
|
1197 | 1255 | </div> |
1198 | 1256 |
|
|
1325 | 1383 | const defaultProxyBindings = Object.fromEntries(state.accounts.map((account) => [account.id, account.proxyId])); |
1326 | 1384 |
|
1327 | 1385 | const chainBar = document.getElementById("chain-bar"); |
| 1386 | + const graphStage = document.getElementById("graph-stage"); |
| 1387 | + const connectionLayer = document.getElementById("connection-layer"); |
1328 | 1388 | const groupList = document.getElementById("group-list"); |
1329 | 1389 | const proxyList = document.getElementById("proxy-list"); |
1330 | 1390 | const endList = document.getElementById("end-list"); |
1331 | 1391 | const copyToast = document.getElementById("copy-toast"); |
1332 | 1392 | let copyToastTimer = null; |
| 1393 | + let connectionFrame = null; |
1333 | 1394 |
|
1334 | 1395 | function toLocatorSegment(value) { |
1335 | 1396 | return String(value).replace(/[^a-zA-Z0-9]+/g, "-").replace(/^-+|-+$/g, "").toUpperCase(); |
|
1367 | 1428 | }); |
1368 | 1429 | } |
1369 | 1430 |
|
| 1431 | + function getAnchorPoint(name, side) { |
| 1432 | + const node = document.querySelector(`[data-graph-anchor="${name}"]`); |
| 1433 | + if (!node || !graphStage) return null; |
| 1434 | + const nodeRect = node.getBoundingClientRect(); |
| 1435 | + const stageRect = graphStage.getBoundingClientRect(); |
| 1436 | + const x = side === "left" ? nodeRect.left - stageRect.left : nodeRect.right - stageRect.left; |
| 1437 | + const y = nodeRect.top - stageRect.top + nodeRect.height / 2; |
| 1438 | + return { x, y }; |
| 1439 | + } |
| 1440 | + |
| 1441 | + function buildConnectionPath(from, to) { |
| 1442 | + const delta = Math.max(48, (to.x - from.x) * 0.35); |
| 1443 | + return `M ${from.x} ${from.y} C ${from.x + delta} ${from.y}, ${to.x - delta} ${to.y}, ${to.x} ${to.y}`; |
| 1444 | + } |
| 1445 | + |
| 1446 | + function renderConnections(computed) { |
| 1447 | + if (!graphStage || !connectionLayer) return; |
| 1448 | + |
| 1449 | + const activeAccounts = computed.filter((item) => item.active); |
| 1450 | + const destinations = [ |
| 1451 | + ...new Set(activeAccounts.map((account) => (account.proxyId ? `proxy-${account.proxyId}` : "proxy-direct"))), |
| 1452 | + ]; |
| 1453 | + const segments = []; |
| 1454 | + |
| 1455 | + const entryPoint = getAnchorPoint("entry", "right"); |
| 1456 | + const groupPointLeft = getAnchorPoint("group-selected", "left"); |
| 1457 | + const groupPointRight = getAnchorPoint("group-selected", "right"); |
| 1458 | + const endPoint = getAnchorPoint("end", "left"); |
| 1459 | + |
| 1460 | + if (entryPoint && groupPointLeft) { |
| 1461 | + segments.push(`<path d="${buildConnectionPath(entryPoint, groupPointLeft)}" class="graph-line graph-line-primary" />`); |
| 1462 | + } |
| 1463 | + |
| 1464 | + destinations.forEach((destination) => { |
| 1465 | + const destinationLeft = getAnchorPoint(destination, "left"); |
| 1466 | + const destinationRight = getAnchorPoint(destination, "right"); |
| 1467 | + if (groupPointRight && destinationLeft) { |
| 1468 | + segments.push(`<path d="${buildConnectionPath(groupPointRight, destinationLeft)}" class="graph-line" />`); |
| 1469 | + } |
| 1470 | + if (destinationRight && endPoint) { |
| 1471 | + segments.push(`<path d="${buildConnectionPath(destinationRight, endPoint)}" class="graph-line graph-line-accent" />`); |
| 1472 | + } |
| 1473 | + }); |
| 1474 | + |
| 1475 | + connectionLayer.innerHTML = segments.join(""); |
| 1476 | + } |
| 1477 | + |
1370 | 1478 | function computeAccounts() { |
1371 | 1479 | return state.accounts.map((account) => { |
1372 | 1480 | const groupSelected = account.group === state.activeGroupId; |
|
1419 | 1527 | const locatorId = `RO.GROUP.${toLocatorSegment(group.id)}`; |
1420 | 1528 | if (selected) cardClasses.push("unique"); |
1421 | 1529 | return ` |
1422 | | - <div class="${cardClasses.join(" ")}" data-collaboration-id="${locatorId}"> |
| 1530 | + <div |
| 1531 | + class="${cardClasses.join(" ")}" |
| 1532 | + data-collaboration-id="${locatorId}" |
| 1533 | + ${selected ? 'data-graph-anchor="group-selected"' : ""} |
| 1534 | + > |
| 1535 | + ${selected ? '<span class="graph-anchor-dot left" aria-hidden="true"></span><span class="graph-anchor-dot right" aria-hidden="true"></span>' : ""} |
1423 | 1536 | <div class="group-top"> |
1424 | 1537 | <div> |
1425 | 1538 | <div class="group-name">${group.label}</div> |
|
1521 | 1634 | `; |
1522 | 1635 |
|
1523 | 1636 | const directBlock = ` |
1524 | | - <div class="group-card" data-collaboration-id="RO.PROXY.DIRECT"> |
| 1637 | + <div class="group-card" data-collaboration-id="RO.PROXY.DIRECT" data-graph-anchor="proxy-direct"> |
| 1638 | + <span class="graph-anchor-dot left" aria-hidden="true"></span> |
| 1639 | + <span class="graph-anchor-dot right" aria-hidden="true"></span> |
1525 | 1640 | <div class="group-top"> |
1526 | 1641 | <div> |
1527 | 1642 | <div class="group-name">直连</div> |
|
1543 | 1658 | const linkedAccounts = activeAccounts.filter((account) => account.proxyId === proxy.id); |
1544 | 1659 | const locatorId = `RO.PROXY.${toLocatorSegment(proxy.id)}`; |
1545 | 1660 | return ` |
1546 | | - <div class="group-card ${linkedAccounts.length > 0 ? "active" : ""}" data-collaboration-id="${locatorId}"> |
| 1661 | + <div |
| 1662 | + class="group-card ${linkedAccounts.length > 0 ? "active" : ""}" |
| 1663 | + data-collaboration-id="${locatorId}" |
| 1664 | + data-graph-anchor="proxy-${proxy.id}" |
| 1665 | + > |
| 1666 | + <span class="graph-anchor-dot left" aria-hidden="true"></span> |
| 1667 | + <span class="graph-anchor-dot right" aria-hidden="true"></span> |
1547 | 1668 | <div class="group-top"> |
1548 | 1669 | <div> |
1549 | 1670 | <div class="group-name">${proxy.label}</div> |
|
1572 | 1693 | const proxyCount = activeAccounts.filter((item) => item.proxyId).length; |
1573 | 1694 |
|
1574 | 1695 | endList.innerHTML = ` |
1575 | | - <div class="outlet-block primary" data-collaboration-id="RO.END.TEST"> |
| 1696 | + <div class="outlet-block primary" data-collaboration-id="RO.END.TEST" data-graph-anchor="end"> |
| 1697 | + <span class="graph-anchor-dot left" aria-hidden="true"></span> |
1576 | 1698 | <div class="outlet-top"> |
1577 | 1699 | <div> |
1578 | 1700 | <div class="outlet-name">结束与测试</div> |
|
1673 | 1795 | renderEnd(computed); |
1674 | 1796 | renderLaneFocus(); |
1675 | 1797 | hydrateLocators(); |
| 1798 | + window.cancelAnimationFrame(connectionFrame); |
| 1799 | + connectionFrame = window.requestAnimationFrame(() => renderConnections(computed)); |
1676 | 1800 | } |
1677 | 1801 |
|
1678 | 1802 | document.addEventListener("click", async (event) => { |
|
1765 | 1889 | } |
1766 | 1890 | }); |
1767 | 1891 |
|
| 1892 | + window.addEventListener("resize", () => { |
| 1893 | + const computed = computeAccounts(); |
| 1894 | + window.cancelAnimationFrame(connectionFrame); |
| 1895 | + connectionFrame = window.requestAnimationFrame(() => renderConnections(computed)); |
| 1896 | + }); |
| 1897 | + |
1768 | 1898 | render(); |
1769 | 1899 | </script> |
1770 | 1900 | </body> |
|
0 commit comments