mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 16:10:59 +08:00
feat: 优化上游设置
This commit is contained in:
@@ -64,8 +64,8 @@ type WebsiteUpdate struct {
|
||||
OpenBasedir bool `form:"open_basedir" json:"open_basedir"`
|
||||
|
||||
// 反向代理
|
||||
Upstreams map[string]types.Upstream `json:"upstreams"`
|
||||
Proxies []types.Proxy `json:"proxies"`
|
||||
Upstreams []types.Upstream `json:"upstreams"`
|
||||
Proxies []types.Proxy `json:"proxies"`
|
||||
}
|
||||
|
||||
type WebsiteUpdateRemark struct {
|
||||
|
||||
@@ -44,6 +44,6 @@ type WebsiteSetting struct {
|
||||
OpenBasedir bool `json:"open_basedir"`
|
||||
|
||||
// 反向代理
|
||||
Upstreams map[string]types.Upstream `json:"upstreams"`
|
||||
Proxies []types.Proxy `json:"proxies"`
|
||||
Upstreams []types.Upstream `json:"upstreams"`
|
||||
Proxies []types.Proxy `json:"proxies"`
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ func generateProxyConfig(proxy types.Proxy) string {
|
||||
}
|
||||
|
||||
// parseBalancerFiles 从 shared 目录解析所有负载均衡配置(Apache 的 upstream 等价物)
|
||||
func parseBalancerFiles(sharedDir string) (map[string]types.Upstream, error) {
|
||||
func parseBalancerFiles(sharedDir string) ([]types.Upstream, error) {
|
||||
entries, err := os.ReadDir(sharedDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -261,7 +261,7 @@ func parseBalancerFiles(sharedDir string) (map[string]types.Upstream, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upstreams := make(map[string]types.Upstream)
|
||||
var upstreams []types.Upstream
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
@@ -272,14 +272,13 @@ func parseBalancerFiles(sharedDir string) (map[string]types.Upstream, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := matches[2]
|
||||
filePath := filepath.Join(sharedDir, entry.Name())
|
||||
upstream, err := parseBalancerFile(filePath)
|
||||
upstream, err := parseBalancerFile(filePath, matches[2])
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if upstream != nil {
|
||||
upstreams[name] = *upstream
|
||||
upstreams = append(upstreams, *upstream)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +286,7 @@ func parseBalancerFiles(sharedDir string) (map[string]types.Upstream, error) {
|
||||
}
|
||||
|
||||
// parseBalancerFile 解析单个负载均衡配置文件
|
||||
func parseBalancerFile(filePath string) (*types.Upstream, error) {
|
||||
func parseBalancerFile(filePath string, name string) (*types.Upstream, error) {
|
||||
content, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -295,6 +294,7 @@ func parseBalancerFile(filePath string) (*types.Upstream, error) {
|
||||
|
||||
contentStr := string(content)
|
||||
upstream := &types.Upstream{
|
||||
Name: name,
|
||||
Servers: make(map[string]string),
|
||||
}
|
||||
|
||||
@@ -342,23 +342,22 @@ func parseBalancerFile(filePath string) (*types.Upstream, error) {
|
||||
}
|
||||
|
||||
// writeBalancerFiles 将负载均衡配置写入文件
|
||||
func writeBalancerFiles(sharedDir string, upstreams map[string]types.Upstream) error {
|
||||
func writeBalancerFiles(sharedDir string, upstreams []types.Upstream) error {
|
||||
// 删除现有的负载均衡配置文件
|
||||
if err := clearBalancerFiles(sharedDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新的配置文件
|
||||
num := 100
|
||||
for name, upstream := range upstreams {
|
||||
fileName := fmt.Sprintf("%03d-balancer-%s.conf", num, name)
|
||||
// 写入新的配置文件,保持顺序
|
||||
for i, upstream := range upstreams {
|
||||
num := 100 + i
|
||||
fileName := fmt.Sprintf("%03d-balancer-%s.conf", num, upstream.Name)
|
||||
filePath := filepath.Join(sharedDir, fileName)
|
||||
|
||||
content := generateBalancerConfig(name, upstream)
|
||||
content := generateBalancerConfig(upstream)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write balancer config: %w", err)
|
||||
}
|
||||
num++
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -391,12 +390,12 @@ func clearBalancerFiles(sharedDir string) error {
|
||||
}
|
||||
|
||||
// generateBalancerConfig 生成负载均衡配置内容
|
||||
func generateBalancerConfig(name string, upstream types.Upstream) string {
|
||||
func generateBalancerConfig(upstream types.Upstream) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("# Load balancer: %s\n", name))
|
||||
sb.WriteString(fmt.Sprintf("# Load balancer: %s\n", upstream.Name))
|
||||
sb.WriteString("<IfModule mod_proxy_balancer.c>\n")
|
||||
sb.WriteString(fmt.Sprintf(" <Proxy balancer://%s>\n", name))
|
||||
sb.WriteString(fmt.Sprintf(" <Proxy balancer://%s>\n", upstream.Name))
|
||||
|
||||
// 服务器列表
|
||||
for addr, options := range upstream.Servers {
|
||||
|
||||
@@ -679,13 +679,13 @@ func (v *ProxyVhost) ClearProxies() error {
|
||||
return clearProxyFiles(siteDir)
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) Upstreams() map[string]types.Upstream {
|
||||
func (v *ProxyVhost) Upstreams() []types.Upstream {
|
||||
sharedDir := filepath.Join(v.configDir, "shared")
|
||||
upstreams, _ := parseBalancerFiles(sharedDir)
|
||||
return upstreams
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) SetUpstreams(upstreams map[string]types.Upstream) error {
|
||||
func (v *ProxyVhost) SetUpstreams(upstreams []types.Upstream) error {
|
||||
sharedDir := filepath.Join(v.configDir, "shared")
|
||||
return writeBalancerFiles(sharedDir, upstreams)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/acepanel/panel/pkg/webserver/types"
|
||||
)
|
||||
@@ -15,7 +16,7 @@ import (
|
||||
var upstreamFilePattern = regexp.MustCompile(`^(\d{3})-(.+)\.conf$`)
|
||||
|
||||
// parseUpstreamFiles 从 shared 目录解析所有 upstream 配置
|
||||
func parseUpstreamFiles(sharedDir string) (map[string]types.Upstream, error) {
|
||||
func parseUpstreamFiles(sharedDir string) ([]types.Upstream, error) {
|
||||
entries, err := os.ReadDir(sharedDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
@@ -24,7 +25,7 @@ func parseUpstreamFiles(sharedDir string) (map[string]types.Upstream, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upstreams := make(map[string]types.Upstream)
|
||||
var upstreams []types.Upstream
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
@@ -40,14 +41,13 @@ func parseUpstreamFiles(sharedDir string) (map[string]types.Upstream, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := matches[2]
|
||||
filePath := filepath.Join(sharedDir, entry.Name())
|
||||
upstream, err := parseUpstreamFile(filePath, name)
|
||||
upstream, err := parseUpstreamFile(filePath, matches[2])
|
||||
if err != nil {
|
||||
continue // 跳过解析失败的文件
|
||||
}
|
||||
if upstream != nil {
|
||||
upstreams[name] = *upstream
|
||||
upstreams = append(upstreams, *upstream)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,9 @@ func parseUpstreamFile(filePath string, expectedName string) (*types.Upstream, e
|
||||
|
||||
blockContent := matches[2]
|
||||
upstream := &types.Upstream{
|
||||
Servers: make(map[string]string),
|
||||
Name: name,
|
||||
Servers: make(map[string]string),
|
||||
Resolver: []string{},
|
||||
}
|
||||
|
||||
// 解析负载均衡算法
|
||||
@@ -95,7 +97,8 @@ func parseUpstreamFile(filePath string, expectedName string) (*types.Upstream, e
|
||||
}
|
||||
|
||||
// 解析 server 指令
|
||||
serverPattern := regexp.MustCompile(`server\s+(\S+)(?:\s+([^;]+))?;`)
|
||||
// 匹配: server 127.0.0.1:8080; 或 server 127.0.0.1:8080 weight=5;
|
||||
serverPattern := regexp.MustCompile(`server\s+([^\s;]+)(?:\s+([^;]+))?;`)
|
||||
serverMatches := serverPattern.FindAllStringSubmatch(blockContent, -1)
|
||||
for _, sm := range serverMatches {
|
||||
addr := sm[1]
|
||||
@@ -112,27 +115,48 @@ func parseUpstreamFile(filePath string, expectedName string) (*types.Upstream, e
|
||||
upstream.Keepalive, _ = strconv.Atoi(km[1])
|
||||
}
|
||||
|
||||
// 解析 resolver
|
||||
resolverPattern := regexp.MustCompile(`resolver\s+([^;]+);`)
|
||||
if rm := resolverPattern.FindStringSubmatch(blockContent); rm != nil {
|
||||
parts := strings.Fields(rm[1])
|
||||
upstream.Resolver = parts
|
||||
}
|
||||
|
||||
// 解析 resolver_timeout
|
||||
resolverTimeoutPattern := regexp.MustCompile(`resolver_timeout\s+(\d+)([smh]?);`)
|
||||
if rtm := resolverTimeoutPattern.FindStringSubmatch(blockContent); rtm != nil {
|
||||
value, _ := strconv.Atoi(rtm[1])
|
||||
unit := rtm[2]
|
||||
switch unit {
|
||||
case "m":
|
||||
upstream.ResolverTimeout = time.Duration(value) * time.Minute
|
||||
case "h":
|
||||
upstream.ResolverTimeout = time.Duration(value) * time.Hour
|
||||
default:
|
||||
upstream.ResolverTimeout = time.Duration(value) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
return upstream, nil
|
||||
}
|
||||
|
||||
// writeUpstreamFiles 将 upstream 配置写入文件
|
||||
func writeUpstreamFiles(sharedDir string, upstreams map[string]types.Upstream) error {
|
||||
func writeUpstreamFiles(sharedDir string, upstreams []types.Upstream) error {
|
||||
// 删除现有的 upstream 配置文件
|
||||
if err := clearUpstreamFiles(sharedDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新的配置文件
|
||||
num := UpstreamStartNum
|
||||
for name, upstream := range upstreams {
|
||||
fileName := fmt.Sprintf("%03d-%s.conf", num, name)
|
||||
// 写入新的配置文件,保持顺序
|
||||
for i, upstream := range upstreams {
|
||||
num := UpstreamStartNum + i
|
||||
fileName := fmt.Sprintf("%03d-%s.conf", num, upstream.Name)
|
||||
filePath := filepath.Join(sharedDir, fileName)
|
||||
|
||||
content := generateUpstreamConfig(name, upstream)
|
||||
content := generateUpstreamConfig(upstream)
|
||||
if err := os.WriteFile(filePath, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write upstream config: %w", err)
|
||||
}
|
||||
num++
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -171,17 +195,25 @@ func clearUpstreamFiles(sharedDir string) error {
|
||||
}
|
||||
|
||||
// generateUpstreamConfig 生成 upstream 配置内容
|
||||
func generateUpstreamConfig(name string, upstream types.Upstream) string {
|
||||
func generateUpstreamConfig(upstream types.Upstream) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("# Upstream: %s\n", name))
|
||||
sb.WriteString(fmt.Sprintf("upstream %s {\n", name))
|
||||
sb.WriteString(fmt.Sprintf("# Upstream: %s\n", upstream.Name))
|
||||
sb.WriteString(fmt.Sprintf("upstream %s {\n", upstream.Name))
|
||||
|
||||
// 负载均衡算法
|
||||
if upstream.Algo != "" {
|
||||
sb.WriteString(fmt.Sprintf(" %s;\n", upstream.Algo))
|
||||
}
|
||||
|
||||
// resolver 配置
|
||||
if len(upstream.Resolver) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver %s;\n", strings.Join(upstream.Resolver, " ")))
|
||||
if upstream.ResolverTimeout > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver_timeout %ds;\n", int(upstream.ResolverTimeout.Seconds())))
|
||||
}
|
||||
}
|
||||
|
||||
// 服务器列表
|
||||
for addr, options := range upstream.Servers {
|
||||
if options != "" {
|
||||
|
||||
@@ -730,13 +730,13 @@ func (v *ProxyVhost) ClearProxies() error {
|
||||
return clearProxyFiles(siteDir)
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) Upstreams() map[string]types.Upstream {
|
||||
func (v *ProxyVhost) Upstreams() []types.Upstream {
|
||||
sharedDir := filepath.Join(v.configDir, "shared")
|
||||
upstreams, _ := parseUpstreamFiles(sharedDir)
|
||||
return upstreams
|
||||
}
|
||||
|
||||
func (v *ProxyVhost) SetUpstreams(upstreams map[string]types.Upstream) error {
|
||||
func (v *ProxyVhost) SetUpstreams(upstreams []types.Upstream) error {
|
||||
sharedDir := filepath.Join(v.configDir, "shared")
|
||||
return writeUpstreamFiles(sharedDir, upstreams)
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ type Proxy struct {
|
||||
|
||||
// Upstream 上游服务器配置
|
||||
type Upstream struct {
|
||||
Name string `form:"name" json:"name" validate:"required"` // 上游名称,如: "backend"
|
||||
Servers map[string]string `form:"servers" json:"servers" validate:"required"` // 上游服务器及配置,如: map["server1"] = "weight=5 resolve"
|
||||
Algo string `form:"algo" json:"algo"` // 负载均衡算法,如: "least_conn", "ip_hash"
|
||||
Keepalive int `form:"keepalive" json:"keepalive"` // 保持连接数,如: 32
|
||||
|
||||
@@ -133,9 +133,9 @@ type VhostProxy interface {
|
||||
ClearProxies() error
|
||||
|
||||
// Upstreams 取上游服务器配置
|
||||
Upstreams() map[string]Upstream
|
||||
Upstreams() []Upstream
|
||||
// SetUpstreams 设置上游服务器配置
|
||||
SetUpstreams(upstreams map[string]Upstream) error
|
||||
SetUpstreams(upstreams []Upstream) error
|
||||
// ClearUpstreams 清除所有上游服务器配置
|
||||
ClearUpstreams() error
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ const { data: setting, send: fetchSetting } = useRequest(website.config(Number(i
|
||||
php: 0,
|
||||
rewrite: '',
|
||||
open_basedir: false,
|
||||
upstreams: {},
|
||||
upstreams: [],
|
||||
proxies: []
|
||||
}
|
||||
})
|
||||
@@ -182,54 +182,48 @@ const hasArg = (args: string[], arg: string) => {
|
||||
}
|
||||
|
||||
// ========== Upstreams 相关 ==========
|
||||
const upstreamAlgoOptions = [
|
||||
{ label: $gettext('Round Robin (default)'), value: '' },
|
||||
{ label: 'least_conn', value: 'least_conn' },
|
||||
{ label: 'ip_hash', value: 'ip_hash' },
|
||||
{ label: 'hash', value: 'hash' },
|
||||
{ label: 'random', value: 'random' }
|
||||
]
|
||||
|
||||
// 添加新的上游
|
||||
const addUpstream = () => {
|
||||
const name = `backend_${Date.now()}`
|
||||
const name = `${setting.value.name.replace(/-/g, '_')}_upstream_${(setting.value.upstreams?.length || 0) + 1}`
|
||||
if (!setting.value.upstreams) {
|
||||
setting.value.upstreams = {}
|
||||
setting.value.upstreams = []
|
||||
}
|
||||
setting.value.upstreams[name] = {
|
||||
setting.value.upstreams.push({
|
||||
name,
|
||||
servers: {},
|
||||
algo: '',
|
||||
keepalive: 32
|
||||
}
|
||||
keepalive: 32,
|
||||
resolver: [],
|
||||
resolver_timeout: 5 * 1000000000 // 5秒,以纳秒为单位
|
||||
})
|
||||
}
|
||||
|
||||
// 删除上游
|
||||
const removeUpstream = (name: string) => {
|
||||
const removeUpstream = (index: number) => {
|
||||
if (setting.value.upstreams) {
|
||||
delete setting.value.upstreams[name]
|
||||
setting.value.upstreams.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名上游
|
||||
const renameUpstream = (oldName: string, newName: string) => {
|
||||
if (!setting.value.upstreams || oldName === newName) return
|
||||
if (setting.value.upstreams[newName]) {
|
||||
window.$message.error($gettext('Upstream name already exists'))
|
||||
return
|
||||
}
|
||||
const upstream = setting.value.upstreams[oldName]
|
||||
delete setting.value.upstreams[oldName]
|
||||
setting.value.upstreams[newName] = upstream
|
||||
}
|
||||
|
||||
// 为上游添加服务器
|
||||
const addServerToUpstream = (upstreamName: string) => {
|
||||
if (!setting.value.upstreams[upstreamName].servers) {
|
||||
setting.value.upstreams[upstreamName].servers = {}
|
||||
const addServerToUpstream = (index: number) => {
|
||||
const upstream = setting.value.upstreams[index]
|
||||
if (!upstream.servers) {
|
||||
upstream.servers = {}
|
||||
}
|
||||
setting.value.upstreams[upstreamName].servers[
|
||||
`127.0.0.1:${8080 + Object.keys(setting.value.upstreams[upstreamName].servers).length}`
|
||||
] = ''
|
||||
upstream.servers[`127.0.0.1:${8080 + Object.keys(upstream.servers).length}`] = ''
|
||||
}
|
||||
|
||||
// 更新上游超时时间值
|
||||
const updateUpstreamTimeoutValue = (upstream: any, value: number) => {
|
||||
const parsed = parseDuration(upstream.resolver_timeout)
|
||||
upstream.resolver_timeout = buildDuration(value, parsed.unit)
|
||||
}
|
||||
|
||||
// 更新上游超时时间单位
|
||||
const updateUpstreamTimeoutUnit = (upstream: any, unit: string) => {
|
||||
const parsed = parseDuration(upstream.resolver_timeout)
|
||||
upstream.resolver_timeout = buildDuration(parsed.value, unit)
|
||||
}
|
||||
|
||||
// ========== Proxies 相关 ==========
|
||||
@@ -245,7 +239,6 @@ const locationMatchTypes = [
|
||||
// 解析 location 字符串,返回匹配类型和表达式
|
||||
const parseLocation = (location: string): { type: string; expression: string } => {
|
||||
if (!location) return { type: '', expression: '/' }
|
||||
|
||||
// 精确匹配 =
|
||||
if (location.startsWith('= ')) {
|
||||
return { type: '=', expression: location.slice(2) }
|
||||
@@ -277,11 +270,9 @@ const buildLocation = (type: string, expression: string): string => {
|
||||
const extractHostFromUrl = (url: string): string => {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.host
|
||||
return urlObj.hostname
|
||||
} catch {
|
||||
// 如果不是有效的 URL,尝试简单提取
|
||||
const match = url.match(/^https?:\/\/([^/]+)/)
|
||||
return match ? match[1] : ''
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,92 +467,136 @@ const updateTimeoutUnit = (proxy: any, unit: string) => {
|
||||
</n-tab-pane>
|
||||
<n-tab-pane v-if="setting.type === 'proxy'" name="upstreams" :tab="$gettext('Upstreams')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{
|
||||
$gettext(
|
||||
'Upstreams define backend server groups for load balancing. You can reference an upstream in proxy settings using "http://upstream_name".'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<!-- 上游卡片列表 -->
|
||||
<n-card
|
||||
v-for="(upstream, name) in setting.upstreams"
|
||||
:key="name"
|
||||
:title="String(name)"
|
||||
closable
|
||||
@close="removeUpstream(String(name))"
|
||||
<draggable
|
||||
v-model="setting.upstreams"
|
||||
item-key="name"
|
||||
handle=".drag-handle"
|
||||
:animation="200"
|
||||
ghost-class="ghost-card"
|
||||
>
|
||||
<template #header>
|
||||
<n-input
|
||||
:value="String(name)"
|
||||
:placeholder="$gettext('Upstream name')"
|
||||
style="width: 300px"
|
||||
@blur="
|
||||
(e: FocusEvent) =>
|
||||
renameUpstream(String(name), (e.target as HTMLInputElement).value)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<n-form label-placement="left" label-width="auto">
|
||||
<n-form-item :label="$gettext('Load Balancing Algorithm')">
|
||||
<n-select
|
||||
v-model:value="upstream.algo"
|
||||
:options="upstreamAlgoOptions"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Keepalive Connections')">
|
||||
<n-input-number v-model:value="upstream.keepalive" :min="0" :max="1000" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Backend Servers')">
|
||||
<n-flex vertical :size="8" style="width: 100%">
|
||||
<n-flex
|
||||
v-for="(options, address) in upstream.servers"
|
||||
:key="String(address)"
|
||||
:size="8"
|
||||
align="center"
|
||||
>
|
||||
<template #item="{ element: upstream, index }">
|
||||
<n-card closable @close="removeUpstream(index)" style="margin-bottom: 16px">
|
||||
<template #header>
|
||||
<n-flex align="center" :size="8">
|
||||
<!-- 拖拽手柄 -->
|
||||
<div class="drag-handle" cursor-grab>
|
||||
<the-icon icon="mdi:drag" :size="20" />
|
||||
</div>
|
||||
<span>{{ $gettext('Upstream') }}</span>
|
||||
<n-input
|
||||
:value="String(address)"
|
||||
:placeholder="$gettext('Server address, e.g., 127.0.0.1:8080')"
|
||||
style="width: 250px"
|
||||
@blur="
|
||||
(e: FocusEvent) => {
|
||||
const newAddr = (e.target as HTMLInputElement).value
|
||||
const oldAddr = String(address)
|
||||
if (newAddr && newAddr !== oldAddr) {
|
||||
upstream.servers[newAddr] = upstream.servers[oldAddr]
|
||||
delete upstream.servers[oldAddr]
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<n-input
|
||||
:value="String(options)"
|
||||
:placeholder="$gettext('Options, e.g., weight=5 backup')"
|
||||
style="width: 300px"
|
||||
@update:value="(v: string) => (upstream.servers[String(address)] = v)"
|
||||
/>
|
||||
<n-button
|
||||
type="error"
|
||||
secondary
|
||||
v-model:value="upstream.name"
|
||||
:placeholder="$gettext('Upstream name')"
|
||||
size="small"
|
||||
@click="delete upstream.servers[String(address)]"
|
||||
>
|
||||
{{ $gettext('Remove') }}
|
||||
</n-button>
|
||||
style="width: 200px"
|
||||
/>
|
||||
</n-flex>
|
||||
<n-button dashed @click="addServerToUpstream(String(name))">
|
||||
{{ $gettext('Add Server') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</template>
|
||||
<n-form label-placement="left" label-width="140px">
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<n-form-item-gi :span="12" :label="$gettext('Load Balancing Algorithm')">
|
||||
<n-select
|
||||
v-model:value="upstream.algo"
|
||||
:options="[
|
||||
{ label: $gettext('Round Robin (default)'), value: '' },
|
||||
{ label: 'least_conn', value: 'least_conn' },
|
||||
{ label: 'ip_hash', value: 'ip_hash' },
|
||||
{ label: 'hash', value: 'hash' },
|
||||
{ label: 'random', value: 'random' }
|
||||
]"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" :label="$gettext('Keepalive Connections')">
|
||||
<n-input-number
|
||||
v-model:value="upstream.keepalive"
|
||||
:min="0"
|
||||
:max="1000"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" :label="$gettext('DNS Resolver')">
|
||||
<n-dynamic-tags
|
||||
v-model:value="upstream.resolver"
|
||||
:placeholder="$gettext('e.g., 8.8.8.8')"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi
|
||||
v-if="upstream.resolver?.length"
|
||||
:span="12"
|
||||
:label="$gettext('Resolver Timeout')"
|
||||
>
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
:value="parseDuration(upstream.resolver_timeout).value"
|
||||
:min="1"
|
||||
:max="3600"
|
||||
style="flex: 1"
|
||||
@update:value="
|
||||
(v: number | null) => updateUpstreamTimeoutValue(upstream, v ?? 5)
|
||||
"
|
||||
/>
|
||||
<n-select
|
||||
:value="parseDuration(upstream.resolver_timeout).unit"
|
||||
:options="timeUnitOptions"
|
||||
style="width: 100px"
|
||||
@update:value="(v: string) => updateUpstreamTimeoutUnit(upstream, v)"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
<n-form-item :label="$gettext('Backend Servers')">
|
||||
<n-flex vertical :size="8" style="width: 100%">
|
||||
<n-flex
|
||||
v-for="(options, address) in upstream.servers"
|
||||
:key="String(address)"
|
||||
:size="8"
|
||||
align="center"
|
||||
>
|
||||
<n-input
|
||||
:default-value="String(address)"
|
||||
:placeholder="$gettext('Server address, e.g., 127.0.0.1:8080')"
|
||||
style="flex: 1"
|
||||
@change="
|
||||
(newAddr: string) => {
|
||||
const oldAddr = String(address)
|
||||
if (newAddr && newAddr !== oldAddr) {
|
||||
upstream.servers[newAddr] = upstream.servers[oldAddr]
|
||||
delete upstream.servers[oldAddr]
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<n-input
|
||||
:value="String(options)"
|
||||
:placeholder="$gettext('Options, e.g., weight=5 backup')"
|
||||
style="flex: 1"
|
||||
@update:value="(v: string) => (upstream.servers[String(address)] = v)"
|
||||
/>
|
||||
<n-button
|
||||
type="error"
|
||||
secondary
|
||||
size="small"
|
||||
style="flex-shrink: 0"
|
||||
@click="delete upstream.servers[String(address)]"
|
||||
>
|
||||
{{ $gettext('Remove') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-button dashed size="small" @click="addServerToUpstream(index)">
|
||||
{{ $gettext('Add Server') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<n-empty v-if="!setting.upstreams || Object.keys(setting.upstreams).length === 0">
|
||||
<n-empty v-if="!setting.upstreams || setting.upstreams.length === 0">
|
||||
{{ $gettext('No upstreams configured') }}
|
||||
</n-empty>
|
||||
|
||||
<!-- 添加按钮 -->
|
||||
<n-button type="primary" dashed @click="addUpstream" mb-20>
|
||||
{{ $gettext('Add Upstream') }}
|
||||
|
||||
Reference in New Issue
Block a user