Files
webui4frpc/web/src/components/RemoteNode.vue
jianf 58906c5a81 feat: M2 HTTP/HTTPS 完善
- 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 验证通过
2026-08-17 12:59:12 +08:00

252 lines
5.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>