按服务器展示 SSH 密钥与登录命令

将 SSH 密钥主页面改为按部署服务器展示,增加密钥部署状态、指纹、测试时间、错误提示和服务器专属 SSH 命令,并优化响应式布局。
This commit is contained in:
xuans 2026-09-27 15:22:53 +08:00
parent 1aa1e52713
commit 2e2e804a49
3 changed files with 274 additions and 23 deletions

View File

@ -429,6 +429,150 @@ code {
background: #fff0f0;
color: #ba5555 !important;
}
.tag.neutral {
background: #eef2f7;
color: #63748c !important;
}
.ssh-server-list {
display: grid;
gap: 14px;
}
.ssh-server-card {
display: grid;
gap: 14px;
padding: 18px;
border: 1px solid #dfe7f1;
border-radius: 16px;
background: linear-gradient(135deg, #ffffff, #f8fbff);
}
.ssh-server-card-heading,
.ssh-orphaned-heading,
.ssh-server-command {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.ssh-server-meta {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
color: #718198;
font-size: 12px;
}
.ssh-server-details {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
}
.ssh-server-detail {
display: grid;
gap: 5px;
min-width: 0;
padding: 12px;
border: 1px solid #e4ebf4;
border-radius: 12px;
background: #fff;
color: #718198;
font-size: 12px;
line-height: 1.5;
}
.ssh-server-detail strong {
color: #30435f;
font-size: 13px;
}
.detail-label {
color: #8a98ab;
font-size: 11px;
letter-spacing: 0.04em;
}
.ssh-server-detail code,
.ssh-server-command code {
overflow-wrap: anywhere;
color: #345f94;
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 12px;
}
.inline-error {
margin: 0;
}
.ssh-server-command {
padding: 10px 12px;
border: 1px solid #d8e5f4;
border-radius: 12px;
background: #f3f8ff;
}
.ssh-server-command > div {
display: grid;
gap: 5px;
min-width: 0;
}
.ssh-orphaned-keys {
display: grid;
gap: 10px;
margin-top: 18px;
padding-top: 16px;
border-top: 1px solid #e7edf5;
}
.ssh-orphaned-heading > div {
display: grid;
gap: 3px;
}
.ssh-orphaned-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.ssh-orphaned-item {
display: inline-flex !important;
align-items: center;
gap: 8px;
padding: 7px 10px;
border: 1px solid #e1e8f1;
border-radius: 9px;
background: #fbfcfe;
color: #6f7f94;
font-size: 12px;
}
@media (max-width: 900px) {
.ssh-server-details {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 640px) {
.ssh-server-card-heading,
.ssh-orphaned-heading,
.ssh-server-command {
align-items: stretch;
flex-direction: column;
}
.ssh-server-details {
grid-template-columns: minmax(0, 1fr);
}
.ssh-server-command .inline-button {
align-self: flex-start;
}
}
.modal-backdrop {
position: fixed;
inset: 0;

View File

@ -516,6 +516,49 @@ export function useDashboard() {
return labels[value] || (value ? "未知" : "—");
}
function serverSSHKey(server) {
if (!server?.private_key_ref) return null;
return sshKeys.value.find((item) => item.reference === server.private_key_ref) || null;
}
function serverSSHKeyStatusLabel(server) {
const labels = {
not_configured: "未配置",
pending: "部署中",
deployed: "已部署并验证",
manual: "已配置,待验证",
failed: "部署失败,保留密码回退",
};
return labels[server?.ssh_key_status] || "未配置";
}
function serverSSHKeyStatusClass(server) {
if (server?.ssh_key_status === "deployed") return "success";
if (server?.ssh_key_status === "failed") return "danger";
if (server?.ssh_key_status === "pending") return "warning";
return "neutral";
}
function serverSSHCommand(server) {
if (!server?.address || !server?.username) return "—";
const port = Number(server.port || 22);
const portOption = port === 22 ? "" : ` -p ${port}`;
return `ssh${portOption} ${server.username}@${server.address}`;
}
function serverSSHKeyHint(server) {
if (server?.ssh_key_status === "deployed") {
return "Scert 已自动部署公钥并验证密钥登录,后续部署默认使用托管私钥。";
}
if (server?.ssh_key_status === "failed") {
return server.ssh_key_error || "公钥部署或密钥登录验证失败,当前仍使用密码回退。";
}
if (server?.auth_method === "private_key") {
return "当前使用已配置的私钥连接,尚未完成自动部署验证。";
}
return "保存服务器后,Scert 会先用密码部署公钥,再自动验证密钥登录。";
}
function formatDate(value) {
if (!value) return "尚未签发";
return new Intl.DateTimeFormat("zh-CN", {
@ -1318,6 +1361,11 @@ export function useDashboard() {
environmentLabels,
webServerLabels,
serverAuthLabel,
serverSSHKey,
serverSSHKeyStatusLabel,
serverSSHKeyStatusClass,
serverSSHCommand,
serverSSHKeyHint,
isEditing,
formTitle,
form,

View File

@ -58,6 +58,11 @@ const {
dnsProviders,
environmentLabels,
webServerLabels,
serverSSHKey,
serverSSHKeyStatusLabel,
serverSSHKeyStatusClass,
serverSSHCommand,
serverSSHKeyHint,
isEditing,
formTitle,
form,
@ -415,41 +420,92 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
<div class="panel-heading">
<div>
<p class="eyebrow">安全凭据</p>
<h2>SSH 私钥</h2>
<h2>服务器 SSH 密钥</h2>
</div>
<span class="muted">共 {{ sshKeys.length }} 个加密引用</span>
</div>
<div v-if="sshKeys.length === 0" class="empty-state">
<strong>还没有 SSH 私钥</strong
><span>私钥会加密保存,页面和接口只显示算法、指纹与安全引用。</span
><button class="primary-button" type="button" @click="openSSHKeyCreate">
添加 SSH 私钥
<button class="secondary-button" type="button" @click="openServerCreate">
添加部署服务器
</button>
</div>
<div v-else class="domain-list">
<article v-for="item in sshKeys" :key="item.id" class="domain-row">
<div class="domain-main">
<div v-if="servers.length === 0" class="empty-state">
<strong>还没有部署服务器</strong>
<span>添加服务器后,Scert 会自动生成密钥、部署公钥并验证密钥登录。</span>
<button class="primary-button" type="button" @click="openServerCreate">
添加部署服务器
</button>
</div>
<div v-else class="ssh-server-list">
<article v-for="item in servers" :key="item.id" class="ssh-server-card">
<div class="ssh-server-card-heading">
<div class="domain-title-line">
<strong>{{ item.name }}</strong
><span class="tag success">{{ item.algorithm }}</span>
<strong>{{ item.name }}</strong>
<span class="tag">{{ displayLabel(webServerLabels, item.web_server) }}</span>
<span :class="['tag', serverSSHKeyStatusClass(item)]">
{{ serverSSHKeyStatusLabel(item) }}
</span>
</div>
<div class="domain-meta">
<span class="key-fingerprint"
>公钥指纹:{{ item.fingerprint }}</span
><span>更新时间:{{ formatDate(item.updated_at) }}</span>
<div class="row-actions">
<button class="text-button" type="button" @click="openServerEdit(item)">
编辑
</button>
<button class="text-button danger-text" type="button" @click="deleteServer(item)">
删除
</button>
</div>
</div>
<div class="row-actions">
<div class="ssh-server-meta">
<span>{{ item.username }}@{{ item.address }}:{{ item.port }}</span>
<span>主机指纹:{{ item.host_key_fingerprint || "未填写" }}</span>
<span>认证:{{ serverAuthLabel(item) }}</span>
</div>
<div class="ssh-server-details">
<div class="ssh-server-detail">
<span class="detail-label">托管私钥</span>
<strong>{{ serverSSHKey(item)?.name || "尚未生成" }}</strong>
<code>{{ serverSSHKey(item)?.fingerprint || item.ssh_key_fingerprint || "—" }}</code>
</div>
<div class="ssh-server-detail">
<span class="detail-label">公钥部署</span>
<strong>{{ serverSSHKeyStatusLabel(item) }}</strong>
<span>{{ serverSSHKeyHint(item) }}</span>
</div>
<div class="ssh-server-detail">
<span class="detail-label">最近部署 / 测试</span>
<span>{{ formatDate(item.ssh_key_deployed_at) }} / {{ formatDate(item.ssh_key_tested_at) }}</span>
<span v-if="item.ssh_key_error" class="form-error inline-error">{{ item.ssh_key_error }}</span>
</div>
</div>
<div class="ssh-server-command">
<div>
<span class="detail-label">服务器 SSH 命令</span>
<code>{{ serverSSHCommand(item) }}</code>
</div>
<button
class="text-button danger-text"
class="inline-button"
type="button"
@click="deleteSSHKey(item)"
@click="copySSHKeyCommand(serverSSHCommand(item), '服务器 SSH 命令已复制')"
>
删除
复制命令
</button>
</div>
</article>
</div>
<div v-if="servers.length > 0 && sshKeys.length > 0" class="ssh-orphaned-keys">
<div class="ssh-orphaned-heading">
<div>
<span class="eyebrow">兼容入口</span>
<strong>未关联服务器的私钥</strong>
</div>
<button class="inline-button" type="button" @click="openSSHKeyCreate">
添加私钥
</button>
</div>
<div class="ssh-orphaned-list">
<span v-for="item in sshKeys.filter((key) => !servers.some((server) => server.private_key_ref === key.reference))" :key="item.id" class="ssh-orphaned-item">
{{ item.name }} · {{ item.fingerprint }}
<button class="text-button danger-text" type="button" @click="deleteSSHKey(item)">删除</button>
</span>
</div>
</div>
</section>
<section v-if="activeSection === 'servers'" class="panel server-panel">
@ -473,11 +529,14 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
<div class="domain-title-line">
<strong>{{ item.name }}</strong
><span class="tag">{{ displayLabel(webServerLabels, item.web_server) }}</span>
><span :class="['tag', serverSSHKeyStatusClass(item)]">
{{ serverSSHKeyStatusLabel(item) }}
</span>
</div>
<div class="domain-meta">
<span>{{ item.username }}@{{ item.address }}:{{ item.port }}</span
><span v-if="item.auth_method === 'private_key' && item.private_key_ref">私钥引用已配置</span
><span v-else>{{ serverAuthLabel(item) }}</span
><span>{{ serverAuthLabel(item) }}</span
><span>托管私钥:{{ serverSSHKey(item)?.name || "尚未生成" }}</span
><span>指纹:{{ item.host_key_fingerprint }}</span>
</div>
</div>