mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 00:47:57 +00:00
fix: 完善同事未完成的修改 — LAN 地址检测/画布删除撤销/连线删除按钮/Del 键/注释修正
- cmd/webui4frpc/main.go: 新增 primaryLANIPv4() 检测内网 LAN 地址, 解决三台内网主机间集群发现失败 (reachableAddr 只回退到 hostname) - internal/httpapi/handlers.go: applyCanvas 新增拓扑差异检测, 画布删除连线时自动 RevokeTask 避免集群残留 - web/src/api.ts: 新增 addLink / deleteLink API 封装 - web/src/components/PortEdge.vue: × 删除按钮移到端口标签右上角 (绝对定位) - web/src/views/CanvasView.vue: 启用 Del/Backspace 键删除选中连线 (确认对话框 + 走 onDeleteEdge 逻辑), 修正注释 - embed dist 同步
This commit is contained in:
@ -72,6 +72,30 @@ export const api = {
|
||||
method: "DELETE",
|
||||
}),
|
||||
|
||||
// Add a single link (POST /links). The canvas save path (saveCanvas) does a
|
||||
// wholesale link replace, so this is for incremental single-link adds from
|
||||
// other surfaces (e.g. a forwards list). For a cluster (non-localOnly)
|
||||
// forward the backend also submits a ring task so the owning node claims it.
|
||||
addLink: (link: {
|
||||
local: string;
|
||||
remote: string;
|
||||
remotePort: number;
|
||||
group?: string;
|
||||
}) =>
|
||||
request<import("./types").Link>(`/api/manager/links`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(link),
|
||||
}),
|
||||
|
||||
// Delete a single link by id. When present in the canvas save payload,
|
||||
// links are replaced wholesale, so the typical path is "remove from
|
||||
// canvas edges + save". This endpoint is kept for explicit removal
|
||||
// (e.g. delete from a forwards list) — it also revokes cluster tasks
|
||||
// for the removed forward.
|
||||
deleteLink: (id: number) =>
|
||||
request<void>(`/api/manager/links/${id}`, { method: "DELETE" }),
|
||||
|
||||
// Per-forward start/stop (forwards page). local-only forwards toggle the
|
||||
// local frpc worker; cluster forwards submit/revoke via the ring.
|
||||
forwardStart: (local: string, remote: string, remotePort: number) =>
|
||||
|
||||
@ -34,6 +34,12 @@
|
||||
@pointerdown.stop
|
||||
@click.stop="emit('edit-group', { edgeId: props.id })"
|
||||
>{{ groupName }}</span>
|
||||
<span
|
||||
class="port-delete"
|
||||
@pointerdown.stop
|
||||
@click.stop="emit('delete-edge', { edgeId: props.id })"
|
||||
title="删除连线"
|
||||
>×</span>
|
||||
</div>
|
||||
</EdgeLabelRenderer>
|
||||
</g>
|
||||
@ -60,6 +66,7 @@ const emit = defineEmits<{
|
||||
): void
|
||||
(e: 'toggle-disabled', payload: { edgeId: string }): void
|
||||
(e: 'edit-group', payload: { edgeId: string }): void
|
||||
(e: 'delete-edge', payload: { edgeId: string }): void
|
||||
}>()
|
||||
|
||||
// User-dragged offset relative to the curve midpoint, initialized from the
|
||||
@ -331,6 +338,7 @@ function midpointOf(path: string): { x: number; y: number } {
|
||||
}
|
||||
|
||||
.port-label {
|
||||
position: relative;
|
||||
background: color-mix(in srgb, var(--w4f-card-solid) 90%, transparent);
|
||||
color: $color-text-primary;
|
||||
border: 1px solid $color-border-light;
|
||||
@ -383,5 +391,29 @@ function midpointOf(path: string): { x: number; y: number } {
|
||||
cursor: pointer;
|
||||
transition: background $transition-fast;
|
||||
}
|
||||
|
||||
.port-delete {
|
||||
position: absolute;
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
margin-left: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
background: color-mix(in srgb, var(--el-color-danger) 16%, transparent);
|
||||
color: var(--el-color-danger);
|
||||
cursor: pointer;
|
||||
transition: background $transition-fast, color $transition-fast;
|
||||
&:hover {
|
||||
background: var(--el-color-danger);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -65,6 +65,7 @@
|
||||
@label-drag="onLabelDrag"
|
||||
@toggle-disabled="onToggleDisabled"
|
||||
@edit-group="onEditGroup"
|
||||
@delete-edge="onDeleteEdge"
|
||||
/>
|
||||
</template>
|
||||
</VueFlow>
|
||||
@ -466,6 +467,61 @@ const editEdge = (edge: Edge) => {
|
||||
)
|
||||
}
|
||||
|
||||
// onDeleteEdge removes an edge from the canvas after a confirm dialog.
|
||||
// The removed link is persisted on the next save: applyCanvas (PUT /canvas)
|
||||
// replaces links wholesale and revokes any cluster task for links that
|
||||
// disappeared. The × badge on the edge label is the quick entry; the
|
||||
// Delete/Backspace key (keydown handler below) removes SELECTED edges.
|
||||
const onDeleteEdge = async ({ edgeId }: { edgeId: string }) => {
|
||||
const e = edges.value.find((x) => x.id === edgeId)
|
||||
if (!e) return
|
||||
try {
|
||||
await ElMessageBox.confirm('确认删除该连线?删除后需点击「保存配置」生效。', '删除连线', {
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
} catch {
|
||||
return // cancelled
|
||||
}
|
||||
removeEdgesSilently([edgeId])
|
||||
}
|
||||
|
||||
// onCanvasKeydown handles Delete/Backspace to drop all SELECTED edges after a
|
||||
// single shared confirmation (VueFlow's own delete-key handling stays disabled
|
||||
// via :delete-key-code="null" because we must keep removal behind the same
|
||||
// confirm dialog; nodes have their own delete buttons).
|
||||
const onCanvasKeydown = async (ev: KeyboardEvent) => {
|
||||
if (ev.key !== 'Delete' && ev.key !== 'Backspace') return
|
||||
const tag = (ev.target as HTMLElement | null)?.tagName
|
||||
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return
|
||||
if (!canWrite.value) return
|
||||
const selectedIds = edges.value
|
||||
.filter((x): x is Edge & { selected?: boolean } => Boolean((x as Edge & { selected?: boolean }).selected))
|
||||
.map((x) => x.id)
|
||||
if (selectedIds.length === 0) return
|
||||
ev.preventDefault()
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除选中的 ${selectedIds.length} 条连线?删除后需点击「保存配置」生效。`,
|
||||
'删除连线',
|
||||
{ confirmButtonText: '删除', cancelButtonText: '取消', type: 'warning' },
|
||||
)
|
||||
} catch {
|
||||
return // cancelled
|
||||
}
|
||||
removeEdgesSilently(selectedIds)
|
||||
}
|
||||
|
||||
// removeEdgesSilently drops the given edge ids from the canvas without its own
|
||||
// prompt (the callers already confirmed). Marks the canvas dirty.
|
||||
const removeEdgesSilently = (ids: string[]) => {
|
||||
const gone = new Set(ids)
|
||||
edges.value = edges.value.filter((x) => !gone.has(x.id))
|
||||
assignLayers()
|
||||
dirty.value = true
|
||||
}
|
||||
|
||||
// onEdgeLabelClick finds the edge by id and opens its port editor.
|
||||
const onEdgeLabelClick = ({ edgeId }: { edgeId: string }) => {
|
||||
const e = edges.value.find((x) => x.id === edgeId)
|
||||
@ -815,8 +871,10 @@ onMounted(() => {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-theme'],
|
||||
})
|
||||
window.addEventListener('keydown', onCanvasKeydown)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', onCanvasKeydown)
|
||||
themeObserver?.disconnect()
|
||||
themeObserver = null
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user