285df5a12f
- Explicitly set selectedCredId in onCredSelected - Separate credential validation from manual input validation
339 lines
10 KiB
Vue
339 lines
10 KiB
Vue
<template>
|
||
<div class="cert-create">
|
||
<h2 class="page-title">申请新证书</h2>
|
||
|
||
<el-card>
|
||
<el-form
|
||
ref="formRef"
|
||
:model="form"
|
||
:rules="rules"
|
||
label-width="140px"
|
||
style="max-width: 620px"
|
||
>
|
||
<el-form-item label="域名" prop="domain">
|
||
<el-input v-model="form.domain" placeholder="example.com" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="邮箱" prop="email">
|
||
<el-input v-model="form.email" placeholder="admin@example.com" />
|
||
</el-form-item>
|
||
|
||
<el-form-item label="ACME 提供商" prop="provider">
|
||
<el-radio-group v-model="form.provider">
|
||
<el-radio value="letsencrypt">Let's Encrypt</el-radio>
|
||
<el-radio value="zerossl">ZeroSSL</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<el-form-item label="验证方式" prop="challenge_type">
|
||
<el-radio-group v-model="form.challenge_type" @change="onChallengeChange">
|
||
<el-radio value="dns">DNS-01(推荐)</el-radio>
|
||
<el-radio value="http">HTTP-01(需80端口)</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<!-- DNS mode: credential or manual -->
|
||
<template v-if="form.challenge_type === 'dns'">
|
||
<el-form-item label="使用方式">
|
||
<el-radio-group v-model="credMode">
|
||
<el-radio value="credential">选择已有凭证</el-radio>
|
||
<el-radio value="manual">手动输入</el-radio>
|
||
</el-radio-group>
|
||
</el-form-item>
|
||
|
||
<!-- Select existing credential -->
|
||
<template v-if="credMode === 'credential'">
|
||
<el-form-item label="选择凭证">
|
||
<el-select v-model="selectedCredId" placeholder="选择已保存的 DNS 凭证" style="width: 100%" @change="onCredSelected">
|
||
<el-option
|
||
v-for="cred in credentials"
|
||
:key="cred.id"
|
||
:label="cred.name + ' (' + typeLabel(cred.type) + ')'"
|
||
:value="cred.id"
|
||
/>
|
||
</el-select>
|
||
<div class="form-tip" v-if="credentials.length === 0">
|
||
暂无凭证,请先在 <router-link to="/credentials">DNS 凭证</router-link> 中添加
|
||
</div>
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- Manual input -->
|
||
<template v-if="credMode === 'manual'">
|
||
<el-form-item label="DNS 提供商" prop="dns_provider">
|
||
<el-select v-model="form.dns_provider" placeholder="选择 DNS 提供商" style="width: 100%" @change="onProviderChange">
|
||
<el-option label="阿里云 DNS (Aliyun)" value="alidns" />
|
||
<el-option label="Cloudflare" value="cloudflare" />
|
||
<el-option label="DNSPod" value="dnspod" />
|
||
</el-select>
|
||
</el-form-item>
|
||
|
||
<!-- Aliyun DNS Config -->
|
||
<template v-if="form.dns_provider === 'alidns'">
|
||
<el-form-item label="AccessKey ID">
|
||
<el-input v-model="manualConfig.ali_key" placeholder="Aliyun AccessKey ID" />
|
||
</el-form-item>
|
||
<el-form-item label="AccessKey Secret">
|
||
<el-input v-model="manualConfig.ali_secret" type="password" show-password placeholder="Aliyun AccessKey Secret" />
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- Cloudflare Config -->
|
||
<template v-if="form.dns_provider === 'cloudflare'">
|
||
<el-form-item label="API Token">
|
||
<el-input v-model="manualConfig.cf_api_token" type="password" show-password placeholder="Cloudflare API Token" />
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- DNSPod Config -->
|
||
<template v-if="form.dns_provider === 'dnspod'">
|
||
<el-form-item label="ID">
|
||
<el-input v-model="manualConfig.dnspod_id" placeholder="DNSPod ID" />
|
||
</el-form-item>
|
||
<el-form-item label="Key">
|
||
<el-input v-model="manualConfig.dnspod_key" type="password" show-password placeholder="DNSPod Key" />
|
||
</el-form-item>
|
||
</template>
|
||
|
||
<!-- Save as credential option -->
|
||
<el-form-item v-if="form.dns_provider">
|
||
<el-checkbox v-model="saveAsCredential">保存为新凭证</el-checkbox>
|
||
</el-form-item>
|
||
<el-form-item v-if="saveAsCredential && form.dns_provider" label="凭证名称">
|
||
<el-input v-model="newCredName" placeholder="例如:阿里云生产环境" />
|
||
</el-form-item>
|
||
</template>
|
||
</template>
|
||
|
||
<el-form-item label="自动续期">
|
||
<el-switch v-model="form.auto_renew" />
|
||
</el-form-item>
|
||
|
||
<el-form-item v-if="form.auto_renew" label="提前续期天数">
|
||
<el-input-number v-model="form.renew_days" :min="1" :max="60" />
|
||
</el-form-item>
|
||
|
||
<el-form-item>
|
||
<el-button type="primary" @click="handleSubmit" :loading="submitting">
|
||
<el-icon><Plus /></el-icon> 提交申请
|
||
</el-button>
|
||
<el-button @click="$router.push('/certificates')">取消</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</el-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import { certApi, credApi } from '../api'
|
||
import { ElMessage } from 'element-plus'
|
||
import { useRouter } from 'vue-router'
|
||
|
||
interface Credential {
|
||
id: number
|
||
name: string
|
||
type: string
|
||
masked: string
|
||
}
|
||
|
||
const router = useRouter()
|
||
const formRef = ref()
|
||
const submitting = ref(false)
|
||
const credentials = ref<Credential[]>([])
|
||
|
||
// Credential mode: select existing or manual input
|
||
const credMode = ref<'credential' | 'manual'>('credential')
|
||
const selectedCredId = ref<number | null>(null)
|
||
const saveAsCredential = ref(false)
|
||
const newCredName = ref('')
|
||
|
||
const form = reactive({
|
||
domain: '',
|
||
email: '',
|
||
provider: 'letsencrypt',
|
||
challenge_type: 'dns',
|
||
dns_provider: 'alidns',
|
||
auto_renew: true,
|
||
renew_days: 30,
|
||
})
|
||
|
||
const manualConfig = reactive({
|
||
ali_key: '',
|
||
ali_secret: '',
|
||
cf_api_token: '',
|
||
dnspod_id: '',
|
||
dnspod_key: '',
|
||
})
|
||
|
||
const rules = {
|
||
domain: [{ required: true, message: '请输入域名', trigger: 'blur' }],
|
||
email: [
|
||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||
{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' },
|
||
],
|
||
}
|
||
|
||
const typeLabel = (type: string) => {
|
||
const labels: Record<string, string> = {
|
||
alidns: '阿里云 DNS',
|
||
cloudflare: 'Cloudflare',
|
||
dnspod: 'DNSPod',
|
||
}
|
||
return labels[type] || type
|
||
}
|
||
|
||
const loadCredentials = async () => {
|
||
try {
|
||
const res = await credApi.list()
|
||
credentials.value = res.data
|
||
} catch {
|
||
// Ignore errors loading credentials
|
||
}
|
||
}
|
||
|
||
const onChallengeChange = () => {
|
||
// Reset DNS config when switching challenge type
|
||
Object.keys(manualConfig).forEach(k => (manualConfig as any)[k] = '')
|
||
selectedCredId.value = null
|
||
credMode.value = 'credential'
|
||
saveAsCredential.value = false
|
||
newCredName.value = ''
|
||
}
|
||
|
||
const onProviderChange = () => {
|
||
Object.keys(manualConfig).forEach(k => (manualConfig as any)[k] = '')
|
||
}
|
||
|
||
const onCredSelected = (credId: number) => {
|
||
// Just store the selection, backend resolves DNS config
|
||
selectedCredId.value = credId
|
||
}
|
||
|
||
const buildDnsConfigJson = (): string => {
|
||
switch (form.dns_provider) {
|
||
case 'alidns':
|
||
return JSON.stringify({
|
||
ali_key: manualConfig.ali_key,
|
||
ali_secret: manualConfig.ali_secret,
|
||
})
|
||
case 'cloudflare':
|
||
return JSON.stringify({
|
||
cf_api_token: manualConfig.cf_api_token,
|
||
})
|
||
case 'dnspod':
|
||
return JSON.stringify({
|
||
dnspod_id: manualConfig.dnspod_id,
|
||
dnspod_key: manualConfig.dnspod_key,
|
||
})
|
||
default:
|
||
return '{}'
|
||
}
|
||
}
|
||
|
||
const handleSubmit = async () => {
|
||
const valid = await formRef.value.validate().catch(() => false)
|
||
if (!valid) return
|
||
|
||
// Validate manual fields
|
||
if (form.challenge_type === 'dns' && credMode.value === 'manual') {
|
||
if (!form.dns_provider) {
|
||
ElMessage.warning('请选择 DNS 提供商')
|
||
return
|
||
}
|
||
// Check required fields
|
||
if (form.dns_provider === 'alidns' && (!manualConfig.ali_key || !manualConfig.ali_secret)) {
|
||
ElMessage.warning('请填写阿里云 AccessKey')
|
||
return
|
||
}
|
||
if (form.dns_provider === 'cloudflare' && !manualConfig.cf_api_token) {
|
||
ElMessage.warning('请填写 Cloudflare API Token')
|
||
return
|
||
}
|
||
if (form.dns_provider === 'dnspod' && (!manualConfig.dnspod_id || !manualConfig.dnspod_key)) {
|
||
ElMessage.warning('请填写 DNSPod 凭证')
|
||
return
|
||
}
|
||
|
||
if (saveAsCredential.value && !newCredName.value) {
|
||
ElMessage.warning('请输入凭证名称')
|
||
return
|
||
}
|
||
}
|
||
|
||
submitting.value = true
|
||
try {
|
||
const payload: any = {
|
||
domain: form.domain,
|
||
email: form.email,
|
||
provider: form.provider,
|
||
challenge_type: form.challenge_type,
|
||
auto_renew: form.auto_renew,
|
||
renew_days: form.renew_days,
|
||
}
|
||
|
||
if (form.challenge_type === 'dns') {
|
||
if (credMode.value === 'credential') {
|
||
if (!selectedCredId.value) {
|
||
ElMessage.warning('请选择一个 DNS 凭证')
|
||
submitting.value = false
|
||
return
|
||
}
|
||
// Use existing credential
|
||
payload.credential_id = selectedCredId.value
|
||
} else if (credMode.value === 'manual') {
|
||
// Manual input
|
||
payload.dns_provider = form.dns_provider
|
||
payload.dns_config = buildDnsConfigJson()
|
||
|
||
// Optionally save as new credential
|
||
if (saveAsCredential.value && newCredName.value) {
|
||
await credApi.create({
|
||
name: newCredName.value,
|
||
type: form.dns_provider,
|
||
data: payload.dns_config,
|
||
})
|
||
}
|
||
} else {
|
||
ElMessage.warning('请选择 DNS 凭证或选择手动输入')
|
||
submitting.value = false
|
||
return
|
||
}
|
||
}
|
||
|
||
await certApi.create(payload)
|
||
ElMessage.success('证书申请已提交,后台处理中')
|
||
router.push('/certificates')
|
||
} catch (e: any) {
|
||
ElMessage.error('提交失败: ' + (e.response?.data?.error || e.message))
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
|
||
onMounted(loadCredentials)
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-title {
|
||
margin-bottom: 20px;
|
||
font-size: 24px;
|
||
color: #1a1a2e;
|
||
}
|
||
|
||
.form-tip {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 4px;
|
||
}
|
||
|
||
.form-tip a {
|
||
color: #1890ff;
|
||
text-decoration: none;
|
||
}
|
||
|
||
.form-tip a:hover {
|
||
text-decoration: underline;
|
||
}
|
||
</style>
|