fix(frontend): polish cert dialog and wire upload API

Prefer drag-and-drop for crt/key with PEM paste collapsed; rename
primary actions; add HTTP-01 prechecks. Upload posts to
POST /system/tls/certificate with cert_source from status.
This commit is contained in:
Codex committed 2026-10-26 11:40:11 +08:00
1 parent df3e119099
commit a9bc68cd28
3 files changed
+69 -12

No files matched your search

+7 -1
View File
@@ -972,7 +972,13 @@
"http01Hint": "Let’s Encrypt HTTP-01. Port 80 must be reachable. Auto-renewal status appears in the list.", "http01Hint": "Let’s Encrypt HTTP-01. Port 80 must be reachable. Auto-renewal status appears in the list.",
"certKeyRequired": "Certificate and private key are required", "certKeyRequired": "Certificate and private key are required",
"certUploaded": "Certificate uploaded", "certUploaded": "Certificate uploaded",
"uploadApiPending": "Upload API is not ready yet (POST /system/tls/certificate). Use HTTP-01 for now." "uploadApiPending": "Upload API is not ready yet (POST /system/tls/certificate). Use HTTP-01 for now.",
"dropHint": "Drop a file here, or click to choose",
"pastePem": "Or paste PEM text",
"importCert": "Import certificate",
"startIssue": "Start issuance",
"http01CheckDns": "DNS already points to this public entry",
"http01CheckPort": "Issuance port is reachable (usually 80, or matches entry port)"
}, },
"feishuSync": { "feishuSync": {
"title": "Feishu directory", "title": "Feishu directory",
+7 -1
View File
@@ -972,7 +972,13 @@
"http01Hint": "使用 Let’s Encrypt HTTP-01 验证;需公网可访问 80 端口。过期前将尝试自动续期,失败时列表会显示状态。", "http01Hint": "使用 Let’s Encrypt HTTP-01 验证;需公网可访问 80 端口。过期前将尝试自动续期,失败时列表会显示状态。",
"certKeyRequired": "请填写证书与私钥", "certKeyRequired": "请填写证书与私钥",
"certUploaded": "证书已上传", "certUploaded": "证书已上传",
"uploadApiPending": "上传证书接口尚未就绪,请先由后端落地 POST /system/tls/certificate,或改用 HTTP-01 申请。" "uploadApiPending": "上传证书接口尚未就绪,请先由后端落地 POST /system/tls/certificate,或改用 HTTP-01 申请。",
"dropHint": "拖拽文件到此处,或点击选择",
"pastePem": "或粘贴 PEM 文本",
"importCert": "导入证书",
"startIssue": "开始申请",
"http01CheckDns": "DNS 已指向本机公网入口",
"http01CheckPort": "申请用端口可达(通常为 80,或与入口配置一致)"
}, },
"feishuSync": { "feishuSync": {
"title": "飞书组织与成员", "title": "飞书组织与成员",
@@ -138,16 +138,42 @@
<template v-if="certForm.method === 'upload'"> <template v-if="certForm.method === 'upload'">
<el-form label-position="top" style="margin-top:16px"> <el-form label-position="top" style="margin-top:16px">
<el-form-item :label="$t('tls.certPem')" required> <el-form-item :label="$t('tls.certPem')" required>
<el-input v-model="certForm.certPem" type="textarea" :rows="6" :placeholder="$t('tls.certPemPlaceholder')" /> <el-upload
<el-upload :show-file-list="false" accept=".crt,.pem,.cer" :before-upload="(f) => readPemFile(f, 'certPem')"> drag
<el-button class="upload-btn" style="width:100%;margin-top:8px">{{ $t('tls.uploadCertFile') }}</el-button> class="pem-drop"
:show-file-list="false"
accept=".crt,.pem,.cer"
:before-upload="(f) => readPemFile(f, 'certPem')"
>
<div class="pem-drop-inner">
<div class="pem-drop-title">{{ certForm.certFileName || $t('tls.uploadCertFile') }}</div>
<div class="pem-drop-hint">{{ $t('tls.dropHint') }}</div>
</div>
</el-upload> </el-upload>
<el-collapse style="margin-top:8px">
<el-collapse-item :title="$t('tls.pastePem')" name="cert">
<el-input v-model="certForm.certPem" type="textarea" :rows="5" :placeholder="$t('tls.certPemPlaceholder')" />
</el-collapse-item>
</el-collapse>
</el-form-item> </el-form-item>
<el-form-item :label="$t('tls.keyPem')" required> <el-form-item :label="$t('tls.keyPem')" required>
<el-input v-model="certForm.keyPem" type="textarea" :rows="6" :placeholder="$t('tls.keyPemPlaceholder')" /> <el-upload
<el-upload :show-file-list="false" accept=".key,.pem" :before-upload="(f) => readPemFile(f, 'keyPem')"> drag
<el-button class="upload-btn" style="width:100%;margin-top:8px">{{ $t('tls.uploadKeyFile') }}</el-button> class="pem-drop"
:show-file-list="false"
accept=".key,.pem"
:before-upload="(f) => readPemFile(f, 'keyPem')"
>
<div class="pem-drop-inner">
<div class="pem-drop-title">{{ certForm.keyFileName || $t('tls.uploadKeyFile') }}</div>
<div class="pem-drop-hint">{{ $t('tls.dropHint') }}</div>
</div>
</el-upload> </el-upload>
<el-collapse style="margin-top:8px">
<el-collapse-item :title="$t('tls.pastePem')" name="key">
<el-input v-model="certForm.keyPem" type="textarea" :rows="5" :placeholder="$t('tls.keyPemPlaceholder')" />
</el-collapse-item>
</el-collapse>
</el-form-item> </el-form-item>
</el-form> </el-form>
</template> </template>
@@ -160,13 +186,19 @@
<el-form-item :label="$t('tls.applyEmail')"> <el-form-item :label="$t('tls.applyEmail')">
<el-input v-model.trim="certForm.email" placeholder="admin@example.com" /> <el-input v-model.trim="certForm.email" placeholder="admin@example.com" />
</el-form-item> </el-form-item>
<el-alert type="info" :closable="false" :title="$t('tls.http01Hint')" /> <el-alert type="info" :closable="false" :title="$t('tls.http01Hint')" style="margin-bottom:12px" />
<ul class="http01-checklist">
<li>{{ $t('tls.http01CheckDns') }}</li>
<li>{{ $t('tls.http01CheckPort') }}</li>
</ul>
</el-form> </el-form>
</template> </template>
<template #footer> <template #footer>
<el-button @click="certDialog = false">{{ $t('common.cancel') }}</el-button> <el-button @click="certDialog = false">{{ $t('common.cancel') }}</el-button>
<el-button type="primary" :loading="certSubmitting" @click="submitCert">{{ $t('common.submit') }}</el-button> <el-button type="primary" :loading="certSubmitting" @click="submitCert">
{{ certForm.method === 'upload' ? $t('tls.importCert') : $t('tls.startIssue') }}
</el-button>
</template> </template>
<pre v-if="nginxActivateLog" ref="nginxActivateLogView" class="nginx-preview" role="log" aria-live="polite" style="margin-top:12px">{{ nginxActivateLog }}</pre> <pre v-if="nginxActivateLog" ref="nginxActivateLogView" class="nginx-preview" role="log" aria-live="polite" style="margin-top:12px">{{ nginxActivateLog }}</pre>
</el-dialog> </el-dialog>
@@ -207,6 +239,8 @@ const certForm = reactive({
method: 'upload', method: 'upload',
certPem: '', certPem: '',
keyPem: '', keyPem: '',
certFileName: '',
keyFileName: '',
host: '', host: '',
email: '', email: '',
}) })
@@ -243,6 +277,8 @@ function resetCertForm() {
certForm.method = 'upload' certForm.method = 'upload'
certForm.certPem = '' certForm.certPem = ''
certForm.keyPem = '' certForm.keyPem = ''
certForm.certFileName = ''
certForm.keyFileName = ''
certForm.host = entry.host certForm.host = entry.host
certForm.email = '' certForm.email = ''
nginxActivateLog.value = '' nginxActivateLog.value = ''
@@ -255,7 +291,11 @@ function openAddCert() {
function readPemFile(file, field) { function readPemFile(file, field) {
const reader = new FileReader() const reader = new FileReader()
reader.onload = () => { certForm[field] = String(reader.result || '') } reader.onload = () => {
certForm[field] = String(reader.result || '')
if (field === 'certPem') certForm.certFileName = file.name
if (field === 'keyPem') certForm.keyFileName = file.name
}
reader.readAsText(file) reader.readAsText(file)
return false return false
} }
@@ -461,5 +501,10 @@ onMounted(() => { if (!auth.mustChange) refreshAll() })
transition: border-color .15s, box-shadow .15s; transition: border-color .15s, box-shadow .15s;
} }
.method-card.active { border-color: var(--el-color-primary); box-shadow: 0 0 0 1px var(--el-color-primary) inset; } .method-card.active { border-color: var(--el-color-primary); box-shadow: 0 0 0 1px var(--el-color-primary) inset; }
.upload-btn { color: var(--el-color-primary); border-style: dashed; } .pem-drop { width: 100%; }
.pem-drop :deep(.el-upload), .pem-drop :deep(.el-upload-dragger) { width: 100%; }
.pem-drop-inner { padding: 8px 0; }
.pem-drop-title { color: var(--el-color-primary); font-weight: 600; }
.pem-drop-hint { margin-top: 4px; font-size: 12px; color: var(--text-secondary); }
.http01-checklist { margin: 0; padding-left: 1.2em; color: var(--text-secondary); font-size: 13px; line-height: 1.7; }
</style> </style>