82426b1941
新增侧栏「宝塔面板」菜单、代理 API 与连接配置页;通过 SSH 读写子机 api.json 自动写入凭据,5 分钟后台重试,含审计修复、检测缓存与并发锁。 Co-authored-by: Cursor <cursoragent@cursor.com>
99 lines
3.3 KiB
Vue
99 lines
3.3 KiB
Vue
<template>
|
||
<BtPanelPageShell title="宝塔 · SSL 证书" subtitle="站点证书查看 / 上传 / Let's Encrypt(C15)">
|
||
<template #default>
|
||
<v-data-table :headers="headers" :items="rows" :loading="loading" density="compact">
|
||
<template #item.ssl="{ item }">
|
||
<span>{{ formatSsl(item) }}</span>
|
||
</template>
|
||
<template #item.actions="{ item }">
|
||
<v-btn size="x-small" variant="text" @click="applyLe(item)">申请 LE</v-btn>
|
||
</template>
|
||
</v-data-table>
|
||
|
||
<v-expansion-panels class="mt-4" variant="accordion">
|
||
<v-expansion-panel title="上传证书(SetSSL)">
|
||
<v-expansion-panel-text>
|
||
<v-text-field v-model="upload.siteName" label="网站名" density="compact" class="mb-2" />
|
||
<v-textarea v-model="upload.csr" label="证书 PEM" rows="4" class="mb-2" />
|
||
<v-textarea v-model="upload.key" label="私钥 PEM" rows="4" class="mb-2" />
|
||
<v-btn color="primary" :loading="loading" @click="uploadSsl">上传</v-btn>
|
||
</v-expansion-panel-text>
|
||
</v-expansion-panel>
|
||
</v-expansion-panels>
|
||
</template>
|
||
</BtPanelPageShell>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { reactive, ref } from 'vue'
|
||
import BtPanelPageShell from '@/components/btpanel/BtPanelPageShell.vue'
|
||
import { btApplySsl, btSetSsl, btSslSites } from '@/api/btpanel'
|
||
import { useBtPanelPageLoad } from '@/composables/btpanel/useBtPanelPageLoad'
|
||
|
||
defineOptions({ name: 'BtPanelSslPage' })
|
||
|
||
interface SiteRow {
|
||
id: number
|
||
name: string
|
||
ssl?: { subject?: string; notAfter?: string; endtime?: string } | number
|
||
}
|
||
|
||
const loading = ref(false)
|
||
const rows = ref<SiteRow[]>([])
|
||
const upload = reactive({ siteName: '', key: '', csr: '' })
|
||
const headers = [
|
||
{ title: 'ID', key: 'id', width: 70 },
|
||
{ title: '网站', key: 'name' },
|
||
{ title: 'SSL', key: 'ssl' },
|
||
{ title: '操作', key: 'actions', sortable: false, width: 100 },
|
||
]
|
||
|
||
const { serverId, run } = useBtPanelPageLoad(async (id) => {
|
||
loading.value = true
|
||
try {
|
||
const res = await btSslSites(id)
|
||
rows.value = (res.data ?? []) as SiteRow[]
|
||
} catch (e: unknown) {
|
||
window.$snackbar?.(e instanceof Error ? e.message : '加载失败', 'error')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
})
|
||
|
||
function formatSsl(item: SiteRow) {
|
||
if (!item.ssl || item.ssl === -1) return '无'
|
||
if (typeof item.ssl === 'object') return item.ssl.subject || item.ssl.notAfter || '已配置'
|
||
return String(item.ssl)
|
||
}
|
||
|
||
async function applyLe(item: SiteRow) {
|
||
const sid = serverId.value
|
||
if (!sid) return
|
||
loading.value = true
|
||
try {
|
||
await btApplySsl(sid, { domains: item.name, id: item.id, auth_type: 'http' })
|
||
window.$snackbar?.('已提交申请', 'success')
|
||
await run()
|
||
} catch (e: unknown) {
|
||
window.$snackbar?.(e instanceof Error ? e.message : '申请失败', 'error')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function uploadSsl() {
|
||
const sid = serverId.value
|
||
if (!sid || !upload.siteName) return
|
||
loading.value = true
|
||
try {
|
||
await btSetSsl(sid, { siteName: upload.siteName, key: upload.key, csr: upload.csr })
|
||
window.$snackbar?.('证书已上传', 'success')
|
||
await run()
|
||
} catch (e: unknown) {
|
||
window.$snackbar?.(e instanceof Error ? e.message : '上传失败', 'error')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
</script>
|