按服务器展示 SSH 密钥与登录命令
将 SSH 密钥主页面改为按部署服务器展示,增加密钥部署状态、指纹、测试时间、错误提示和服务器专属 SSH 命令,并优化响应式布局。
This commit is contained in:
parent
1aa1e52713
commit
2e2e804a49
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user