mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 00:47:57 +00:00
- store: Local 增加 customDomains/subdomain/locations/hostHeaderRewrite/httpHeaders/basicAuth;Remote 增加 vhostHttpPort;migrate() 补列;CRUD 读写(JSOm 编解码) - render: http/https proxy 输出 customDomains/subdomain/locations/hostHeaderRewrite/httpHeaders/basicAuth - main: renderRemote 透传 M2 字段(customDomains 逗号拆分 splitCSV) - web: LocalNode 折叠 HTTP 路由高级区;RemoteNode vhostHTTPPort;StatusView 显示 HTTP 访问端口 - 测试: TestRenderHTTPAdvanced / TestRenderHTTPSUsesCustomDomains;端到端 PUT canvas→config 验证通过
252 lines
5.7 KiB
Vue
252 lines
5.7 KiB
Vue
<template>
|
||
<div class="remote-node" :class="{ selected, conflicted }">
|
||
<!-- target handle (left) receives connections from locals -->
|
||
<Handle type="target" :position="Position.Left" />
|
||
|
||
<div class="node-head">
|
||
<span class="node-icon">⬤</span>
|
||
<input v-model="nameField" class="name-input" />
|
||
<label class="enabled">
|
||
<input v-model="enabledField" type="checkbox" />
|
||
启用
|
||
</label>
|
||
<button
|
||
class="del-btn"
|
||
title="删除"
|
||
@click.stop="emit('remove', props.data.name)"
|
||
>
|
||
×
|
||
</button>
|
||
</div>
|
||
|
||
<div class="fields">
|
||
<label>IP <input v-model="ipField" /></label>
|
||
<label>端口 <input v-model.number="portField" type="number" /></label>
|
||
<label>令牌 <input v-model="tokenField" /></label>
|
||
<label>URL <input v-model="urlField" /></label>
|
||
</div>
|
||
|
||
<div class="adv">
|
||
<button class="adv-toggle" @click.stop="showAdv = !showAdv">
|
||
{{ showAdv ? '▾ 收起高级' : '▸ 高级' }}
|
||
</button>
|
||
<div v-if="showAdv" class="adv-body">
|
||
<label
|
||
>传输协议
|
||
<select v-model="transportProtocolField">
|
||
<option value="">tcp(默认)</option>
|
||
<option value="quic">quic</option>
|
||
<option value="kcp">kcp</option>
|
||
<option value="websocket">websocket</option>
|
||
</select>
|
||
</label>
|
||
<label class="adv-check">
|
||
<input v-model="transportTlsField" type="checkbox" /> TLS 传输
|
||
</label>
|
||
<label>TLS SNI <input v-model="transportTlsServerNameField" placeholder="frps.local" /></label>
|
||
<label>连接池 <input v-model.number="transportPoolField" type="number" min="0" /></label>
|
||
<label class="adv-sep">HTTP 访问端口 (frps vhostHTTPPort)</label>
|
||
<label>vhostHTTPPort <input v-model.number="vhostHttpPortField" type="number" min="0" /></label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { Handle, Position } from '@vue-flow/core'
|
||
import { computed, ref } from 'vue'
|
||
import type { CanvasRemote } from '../types'
|
||
|
||
const props = defineProps<{
|
||
data: CanvasRemote
|
||
selected?: boolean
|
||
conflicted?: boolean
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'update:data', data: CanvasRemote): void
|
||
(e: 'remove', name: string): void
|
||
}>()
|
||
|
||
const field = <K extends keyof CanvasRemote>(key: K) =>
|
||
computed({
|
||
get: () => props.data[key],
|
||
set: (val: CanvasRemote[K]) => {
|
||
emit('update:data', { ...props.data, [key]: val })
|
||
},
|
||
})
|
||
|
||
const nameField = field('name')
|
||
const ipField = field('ip')
|
||
const portField = field('port')
|
||
const tokenField = field('token')
|
||
const urlField = field('url')
|
||
const enabledField = field('enabled')
|
||
const transportProtocolField = field('transportProtocol')
|
||
const transportTlsField = field('transportTls')
|
||
const transportTlsServerNameField = field('transportTlsServerName')
|
||
const transportPoolField = field('transportPool')
|
||
const vhostHttpPortField = field('vhostHttpPort')
|
||
const showAdv = ref(false)
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.remote-node {
|
||
min-width: 200px;
|
||
border-radius: 14px 14px 4px 14px;
|
||
background: #fff3e0;
|
||
border: 2px solid #ff9800;
|
||
box-shadow: 0 2px 8px rgba(255, 152, 0, 0.18);
|
||
padding: 8px 10px;
|
||
|
||
&.selected {
|
||
border-color: #ffd54f;
|
||
box-shadow: 0 0 0 3px rgba(255, 213, 79, 0.45);
|
||
}
|
||
|
||
&.conflicted {
|
||
border-color: #f5222d;
|
||
box-shadow: 0 0 0 3px rgba(245, 34, 45, 0.4);
|
||
animation: conflict-pulse 1.2s ease-in-out infinite;
|
||
}
|
||
}
|
||
|
||
@keyframes conflict-pulse {
|
||
0%,
|
||
100% {
|
||
box-shadow: 0 0 0 3px rgba(245, 34, 45, 0.4);
|
||
}
|
||
50% {
|
||
box-shadow: 0 0 0 5px rgba(245, 34, 45, 0.7);
|
||
}
|
||
}
|
||
|
||
.node-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
|
||
.node-icon {
|
||
color: #ff9800;
|
||
font-size: 16px;
|
||
}
|
||
}
|
||
|
||
.name-input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
border: none;
|
||
background: transparent;
|
||
font-weight: 600;
|
||
font-size: 14px;
|
||
color: #e65100;
|
||
&:focus {
|
||
outline: 1px solid #ff9800;
|
||
}
|
||
}
|
||
|
||
.enabled {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 2px;
|
||
font-size: 10px;
|
||
color: #ef6c00;
|
||
}
|
||
|
||
.del-btn {
|
||
border: none;
|
||
background: transparent;
|
||
color: #ffb74d;
|
||
font-size: 16px;
|
||
cursor: pointer;
|
||
line-height: 1;
|
||
&:hover {
|
||
color: #c62828;
|
||
}
|
||
}
|
||
|
||
.fields {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin-top: 6px;
|
||
|
||
label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 11px;
|
||
color: #ef6c00;
|
||
|
||
input {
|
||
flex: 1;
|
||
min-width: 0;
|
||
border: 1px solid #ffcc80;
|
||
border-radius: 6px;
|
||
padding: 2px 6px;
|
||
font-size: 12px;
|
||
background: #fff;
|
||
color: #333;
|
||
&:focus {
|
||
outline: none;
|
||
border-color: #ff9800;
|
||
}
|
||
}
|
||
|
||
input[type='number'] {
|
||
width: 64px;
|
||
flex: 0 0 64px;
|
||
}
|
||
}
|
||
|
||
.adv {
|
||
margin-top: 6px;
|
||
.adv-toggle {
|
||
border: 1px dashed #ffcc80;
|
||
background: transparent;
|
||
color: #ef6c00;
|
||
font-size: 11px;
|
||
border-radius: 6px;
|
||
padding: 2px 8px;
|
||
cursor: pointer;
|
||
}
|
||
.adv-body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 4px;
|
||
margin-top: 6px;
|
||
padding: 6px;
|
||
background: rgba(255, 152, 0, 0.06);
|
||
border-radius: 8px;
|
||
|
||
label {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 11px;
|
||
color: #ef6c00;
|
||
|
||
input,
|
||
select {
|
||
flex: 1;
|
||
min-width: 0;
|
||
border: 1px solid #ffcc80;
|
||
border-radius: 6px;
|
||
padding: 2px 6px;
|
||
font-size: 12px;
|
||
background: #fff;
|
||
color: #333;
|
||
&:focus {
|
||
outline: none;
|
||
border-color: #ff9800;
|
||
}
|
||
}
|
||
}
|
||
.adv-check {
|
||
font-size: 11px;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|