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 验证通过
This commit is contained in:
2026-08-17 12:59:12 +08:00
parent 07f25ea067
commit 58906c5a81
11 changed files with 323 additions and 116 deletions

View File

@ -46,6 +46,15 @@
<label>连接池 <input v-model.number="poolCountField" type="number" min="0" /></label>
<label>元数据(JSON) <input v-model="metadatasField" placeholder='{"env":"prod"}' /></label>
<label>注解(JSON) <input v-model="annotationsField" placeholder='{"owner":"ops"}' /></label>
<div class="adv-sep">HTTP/HTTPS 路由</div>
<label>域名(逗号分隔) <input v-model="customDomainsField" placeholder="app.example.com" /></label>
<label>子域 <input v-model="subdomainField" placeholder="app (需 frps subdomain_host)" /></label>
<label>路径路由(JSON) <input v-model="locationsField" placeholder='["/api","/admin"]' /></label>
<label>Host 改写 <input v-model="hostHeaderRewriteField" placeholder="backend.internal" /></label>
<label>请求头(JSON) <input v-model="httpHeadersField" placeholder='{"X-Custom":"val"}' /></label>
<label>BasicAuth 用户 <input v-model="basicAuthUserField" placeholder="user" /></label>
<label>BasicAuth 密码 <input v-model="basicAuthPasswordField" type="password" placeholder="pass" /></label>
</div>
</div>
</div>
@ -114,6 +123,44 @@ const annotationsField = computed({
emit('update:data', { ...props.data, annotations: parsed })
},
})
const customDomainsField = field('customDomains')
const subdomainField = field('subdomain')
const hostHeaderRewriteField = field('hostHeaderRewrite')
const basicAuthUserField = field('basicAuthUser')
const basicAuthPasswordField = field('basicAuthPassword')
// locations is an array shown as JSON in the UI
const locationsField = computed({
get: () => JSON.stringify(props.data.locations ?? []),
set: (v: string) => {
let parsed: string[] = []
if (v.trim()) {
try {
const arr = JSON.parse(v)
if (Array.isArray(arr)) parsed = arr.map(String)
else return
} catch {
return
}
}
emit('update:data', { ...props.data, locations: parsed })
},
})
// httpHeaders is a map shown as JSON
const httpHeadersField = computed({
get: () => JSON.stringify(props.data.httpHeaders ?? {}),
set: (v: string) => {
let parsed: Record<string, string> = {}
if (v.trim()) {
try {
parsed = JSON.parse(v)
} catch {
return
}
}
emit('update:data', { ...props.data, httpHeaders: parsed })
},
})
</script>
<style scoped lang="scss">