- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听) - 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP) - 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点) - 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
256 lines
6.8 KiB
Vue
256 lines
6.8 KiB
Vue
<template>
|
|
<div class="provider-form">
|
|
<div class="provider-form__head">
|
|
<span class="provider-form__title">{{ editing ? '编辑服务商' : '添加服务商' }}</span>
|
|
</div>
|
|
|
|
<div class="provider-form__field">
|
|
<label class="provider-form__label">DNS 服务商</label>
|
|
<div class="provider-select">
|
|
<span class="provider-logo provider-logo--md">
|
|
<img :src="providerIcon(form.provider)" :alt="providerLabel(form.provider)" />
|
|
</span>
|
|
<n-select
|
|
v-model:value="form.provider"
|
|
:options="providerOptions"
|
|
:disabled="!!editing"
|
|
size="small"
|
|
class="provider-select__input"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="form.provider === 'dnspod'" class="provider-form__field">
|
|
<label class="provider-form__label">Token ID</label>
|
|
<n-input v-model:value="form.api_token_id" placeholder="DNSPod ID" size="small" />
|
|
</div>
|
|
|
|
<div class="provider-form__field">
|
|
<label class="provider-form__label provider-secret-label">
|
|
<span>{{ credentialLabel }}</span>
|
|
<n-tag v-if="editing?.has_token" size="small" type="success" :bordered="false">
|
|
{{ CONFIGURED_SECRET_TAG }}
|
|
</n-tag>
|
|
</label>
|
|
<n-input
|
|
v-model:value="form.api_token"
|
|
type="password"
|
|
show-password-on="click"
|
|
:placeholder="credentialPlaceholder"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="form.provider === 'alidns' || form.provider === 'tencentcloud' || form.provider === 'volcengine'" class="provider-form__field">
|
|
<label class="provider-form__label provider-secret-label">
|
|
<span>{{ secretLabel }}</span>
|
|
<n-tag v-if="editing?.has_token" size="small" type="success" :bordered="false">
|
|
{{ CONFIGURED_SECRET_TAG }}
|
|
</n-tag>
|
|
</label>
|
|
<n-input
|
|
v-model:value="form.api_secret"
|
|
type="password"
|
|
show-password-on="click"
|
|
:placeholder="editing?.has_token ? CONFIGURED_SECRET_PLACEHOLDER : secretPlaceholder"
|
|
size="small"
|
|
/>
|
|
</div>
|
|
|
|
<div class="provider-form__switches">
|
|
<div class="settings-switch">
|
|
<span>IPv4</span>
|
|
<n-switch v-model:value="form.ipv4_enabled" size="small" />
|
|
</div>
|
|
<div class="settings-switch">
|
|
<span>IPv6</span>
|
|
<n-switch v-model:value="form.ipv6_enabled" size="small" />
|
|
</div>
|
|
<div class="settings-switch">
|
|
<span>启用</span>
|
|
<n-switch v-model:value="form.enabled" size="small" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="provider-form__actions">
|
|
<n-button size="small" quaternary @click="emit('cancel')">取消</n-button>
|
|
<n-button size="small" :loading="testing" @click="emit('test')">测试连接</n-button>
|
|
<n-button size="small" type="primary" :loading="saving" @click="emit('save')">保存</n-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import { NButton, NInput, NSelect, NSwitch, NTag } from 'naive-ui'
|
|
import { CONFIGURED_SECRET_PLACEHOLDER, CONFIGURED_SECRET_TAG } from '../constants/secretField'
|
|
import aliyunIcon from '../assets/brand/dns/aliyun.png'
|
|
import cloudflareIcon from '../assets/brand/dns/cloudflare.png'
|
|
import dnspodIcon from '../assets/brand/dns/dnspod.png'
|
|
import tencentcloudIcon from '../assets/brand/dns/tencentcloud.png'
|
|
import volcengineIcon from '../assets/brand/dns/volcengine.png'
|
|
import type { DDNSConfig } from '../api/types'
|
|
|
|
export type ProviderForm = {
|
|
provider: string
|
|
api_token: string
|
|
api_token_id: string
|
|
api_secret: string
|
|
ipv4_enabled: boolean
|
|
ipv6_enabled: boolean
|
|
enabled: boolean
|
|
custom_ipv4: string
|
|
custom_ipv6: string
|
|
}
|
|
|
|
const props = defineProps<{
|
|
form: ProviderForm
|
|
editing?: DDNSConfig | null
|
|
saving?: boolean
|
|
testing?: boolean
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
save: []
|
|
test: []
|
|
cancel: []
|
|
}>()
|
|
|
|
const providerOptions = [
|
|
{ label: 'Cloudflare', value: 'cloudflare' },
|
|
{ label: 'DNSPod', value: 'dnspod' },
|
|
{ label: '阿里云 DNS', value: 'alidns' },
|
|
{ label: '腾讯云 DNS', value: 'tencentcloud' },
|
|
{ label: '火山引擎 DNS', value: 'volcengine' },
|
|
]
|
|
|
|
const providerMap: Record<string, { label: string; icon: string }> = {
|
|
cloudflare: { label: 'Cloudflare', icon: cloudflareIcon },
|
|
dnspod: { label: 'DNSPod', icon: dnspodIcon },
|
|
alidns: { label: '阿里云 DNS', icon: aliyunIcon },
|
|
tencentcloud: { label: '腾讯云 DNS', icon: tencentcloudIcon },
|
|
volcengine: { label: '火山引擎 DNS', icon: volcengineIcon },
|
|
}
|
|
|
|
const credentialLabel = computed(() => {
|
|
if (props.form.provider === 'alidns' || props.form.provider === 'volcengine') return 'AccessKey ID'
|
|
if (props.form.provider === 'tencentcloud') return 'SecretId'
|
|
if (props.form.provider === 'dnspod') return 'Token'
|
|
return 'API Token'
|
|
})
|
|
|
|
const secretLabel = computed(() => {
|
|
if (props.form.provider === 'tencentcloud') return 'SecretKey'
|
|
return 'AccessKey Secret'
|
|
})
|
|
|
|
const secretPlaceholder = computed(() => {
|
|
if (props.form.provider === 'tencentcloud') return '腾讯云 SecretKey'
|
|
return 'AccessKey Secret'
|
|
})
|
|
|
|
const credentialPlaceholder = computed(() => {
|
|
if (props.editing?.has_token) return CONFIGURED_SECRET_PLACEHOLDER
|
|
if (props.form.provider === 'alidns' || props.form.provider === 'volcengine') return 'AccessKey ID'
|
|
if (props.form.provider === 'tencentcloud') return '腾讯云 SecretId'
|
|
if (props.form.provider === 'dnspod') return 'DNSPod Token'
|
|
return 'Cloudflare API Token'
|
|
})
|
|
|
|
function providerLabel(v: string) {
|
|
return providerMap[v]?.label ?? v
|
|
}
|
|
|
|
function providerIcon(v: string) {
|
|
return providerMap[v]?.icon ?? ''
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.provider-form__head {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.provider-form__title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--fonu-text);
|
|
}
|
|
|
|
.provider-form__field {
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.provider-form__label {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-size: 12px;
|
|
color: var(--fonu-text-secondary);
|
|
}
|
|
|
|
.provider-select {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
}
|
|
|
|
.provider-select__input {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.provider-logo {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
flex-shrink: 0;
|
|
background: #fff;
|
|
border: 1px solid var(--fonu-border);
|
|
}
|
|
|
|
.provider-logo--md {
|
|
width: 32px;
|
|
height: 32px;
|
|
}
|
|
|
|
.provider-logo img {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.provider-form__switches {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin: 8px 0 12px;
|
|
}
|
|
|
|
.settings-switch {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
font-size: 13px;
|
|
color: var(--fonu-text);
|
|
}
|
|
|
|
.provider-form__actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 6px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.provider-secret-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
</style>
|