Files
acme-auto/frontend/src/views/Domains.vue
T
sansenhoshi 226f6a76dd 改用element
增加acme多配置
2026-07-18 23:17:53 +08:00

280 lines
9.9 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>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 24px;">
<h2 style="font-size: 20px; font-weight: 700; margin: 0;">域名管理</h2>
<el-button type="primary" round :icon="Plus" @click="openCreate">新增域名</el-button>
</div>
<el-card>
<el-table :data="domains" stripe style="width: 100%;">
<el-table-column prop="domain" label="域名" width="240">
<template #default="{ row }">
<span style="font-weight: 600;">{{ row.domain }}</span>
</template>
</el-table-column>
<el-table-column label="服务器" width="120">
<template #default="{ row }">
<el-tag v-if="row.server_name" size="small">{{ row.server_name }}</el-tag>
<span v-else style="color: #c0c4cc;">-</span>
</template>
</el-table-column>
<el-table-column label="ACME 配置" width="120">
<template #default="{ row }">
<el-tag v-if="row.acme_config_name" type="info" size="small">{{ row.acme_config_name }}</el-tag>
<span v-else style="color: #c0c4cc;">默认</span>
</template>
</el-table-column>
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.deploy_success === true ? 'success' : row.deploy_success === false ? 'danger' : 'info'" size="small">
{{ row.deploy_success === true ? '成功' : row.deploy_success === false ? '失败' : '未部署' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="过期时间" width="170">
<template #default="{ row }">
<span :style="{ color: isExpiringSoon(row.expiry_date) ? '#f56c6c' : '#606266' }">
{{ row.expiry_date || '-' }}
</span>
</template>
</el-table-column>
<el-table-column label="版本" width="70">
<template #default="{ row }">{{ row.version }}</template>
</el-table-column>
<el-table-column label="操作" width="240" align="right">
<template #default="{ row }">
<div style="display: flex; align-items: center; gap: 4px; justify-content: flex-end;">
<el-button type="primary" link size="small" @click="openEdit(row)">编辑</el-button>
<el-button type="primary" link size="small" @click="openScript(row)">脚本</el-button>
<el-dropdown trigger="click" @command="(cmd) => handleCertAction(cmd, row)">
<el-button type="primary" link size="small">
证书 <el-icon class="el-icon--right"><ArrowDown /></el-icon>
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="fullchain">下载证书</el-dropdown-item>
<el-dropdown-item command="private">下载私钥</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-button type="danger" link size="small" @click="handleDelete(row)">删除</el-button>
</div>
</template>
</el-table-column>
</el-table>
</el-card>
<!-- 新增/编辑域名弹窗 -->
<el-dialog
v-model="formDialogVisible"
:title="isEdit ? '编辑域名' : '新增域名'"
width="500px"
destroy-on-close
>
<el-form :model="domainForm" label-width="100px">
<el-form-item label="域名">
<el-input v-model="domainForm.domain" placeholder="example.com 或 *.example.com" :disabled="isEdit" />
</el-form-item>
<el-form-item label="关联服务器">
<el-select v-model="domainForm.server_id" placeholder="选择服务器" style="width: 100%;" clearable>
<el-option v-for="s in serverList" :key="s.id" :label="s.name" :value="s.id" />
</el-select>
</el-form-item>
<el-form-item label="ACME 配置">
<el-select v-model="domainForm.acme_config_id" placeholder="使用默认配置" style="width: 100%;" clearable>
<el-option v-for="c in acmeConfigs" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button round @click="formDialogVisible = false">取消</el-button>
<el-button type="primary" round :loading="saving" @click="saveDomain">保存</el-button>
</template>
</el-dialog>
<!-- 部署脚本弹窗 -->
<el-dialog
v-model="scriptDialogVisible"
title="部署脚本"
width="680px"
destroy-on-close
>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
<span style="font-weight: 600; font-size: 16px;">{{ scriptDomain?.domain }}</span>
<el-tag v-if="scriptDomain?.server_name" type="info" size="small">{{ scriptDomain.server_name }}</el-tag>
</div>
<p style="color: #909399; font-size: 13px; margin-bottom: 16px;">
将以下命令添加到服务器的定时任务中实现证书自动拉取与部署
</p>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
<el-radio-group v-model="scriptOs" size="small" @change="loadScript">
<el-radio-button value="linux">Linux</el-radio-button>
<el-radio-button value="windows">Windows</el-radio-button>
</el-radio-group>
<el-button type="primary" round size="small" :icon="DocumentCopy" @click="copyScript">复制命令</el-button>
<el-button round size="small" :icon="Download" @click="downloadScript">下载脚本</el-button>
</div>
<el-input
v-model="scriptContent"
type="textarea"
:rows="12"
readonly
style="font-family: 'Courier New', monospace;"
/>
</el-dialog>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { Plus, ArrowDown, DocumentCopy, Download } from '@element-plus/icons-vue'
import { ElMessageBox, ElMessage } from 'element-plus'
import { getDomains, createDomain, updateDomain, deleteDomain, getServers } from '../api'
import api from '../api'
const domains = ref([])
const serverList = ref([])
const acmeConfigs = ref([])
// 域名表单弹窗
const formDialogVisible = ref(false)
const isEdit = ref(false)
const editId = ref(null)
const saving = ref(false)
const domainForm = ref({ domain: '', server_id: null, acme_config_id: null })
// 脚本弹窗
const scriptDialogVisible = ref(false)
const scriptDomain = ref(null)
const scriptOs = ref('linux')
const scriptContent = ref('')
const load = async () => {
const { data } = await getDomains()
domains.value = data
}
const loadServers = async () => {
const { data } = await getServers()
serverList.value = data
}
const loadAcmeConfigs = async () => {
const { data } = await api.get('/acme/configs')
acmeConfigs.value = data
}
const isExpiringSoon = (dateStr) => {
if (!dateStr) return false
const diff = (new Date(dateStr) - new Date()) / (1000 * 60 * 60 * 24)
return diff < 30
}
// ---- 域名表单 ----
const openCreate = () => {
isEdit.value = false
editId.value = null
domainForm.value = { domain: '', server_id: null, acme_config_id: null }
formDialogVisible.value = true
}
const openEdit = (row) => {
isEdit.value = true
editId.value = row.id
domainForm.value = { domain: row.domain, server_id: row.server_id, acme_config_id: row.acme_config_id }
formDialogVisible.value = true
}
const saveDomain = async () => {
saving.value = true
try {
if (isEdit.value) {
await updateDomain(editId.value, domainForm.value)
} else {
await createDomain(domainForm.value)
}
ElMessage.success('保存成功')
formDialogVisible.value = false
await load()
} catch (e) {
ElMessage.error(e.response?.data?.detail || '保存失败')
} finally {
saving.value = false
}
}
// ---- 脚本弹窗 ----
const openScript = async (row) => {
if (!row.server_name) {
ElMessage.warning('该域名未关联服务器,请先编辑关联服务器')
return
}
scriptDomain.value = row
scriptOs.value = 'linux'
scriptContent.value = ''
scriptDialogVisible.value = true
await loadScript()
}
const loadScript = async () => {
try {
const resp = await api.get('/script', {
params: {
domain: scriptDomain.value.domain,
server_name: scriptDomain.value.server_name,
os: scriptOs.value,
},
})
scriptContent.value = resp.data.script
} catch (e) {
scriptContent.value = '# 加载失败: ' + (e.response?.data?.detail || e.message)
}
}
const copyScript = () => {
navigator.clipboard.writeText(scriptContent.value).then(() => {
ElMessage.success('已复制到剪贴板')
})
}
const downloadScript = () => {
const ext = scriptOs.value === 'windows' ? '.ps1' : '.sh'
const blob = new Blob([scriptContent.value], { type: 'text/plain' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = `deploy-${scriptDomain.value.domain}${ext}`
link.click()
URL.revokeObjectURL(link.href)
}
// ---- 证书下载 ----
const handleCertAction = async (cmd, row) => {
try {
const { data } = await api.get(`/cert-download/${row.id}/${cmd}`, { responseType: 'blob' })
const blob = new Blob([data])
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = cmd === 'fullchain' ? `${row.domain}.pem` : `${row.domain}.key`
link.click()
URL.revokeObjectURL(link.href)
} catch (e) {
ElMessage.error(e.response?.data?.detail || '下载失败')
}
}
// ---- 删除 ----
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm(`确认删除域名 "${row.domain}"`, '确认删除', { type: 'warning' })
await deleteDomain(row.id)
await load()
} catch {}
}
onMounted(() => {
load()
loadServers()
loadAcmeConfigs()
})
</script>