style: 优化仪表盘托管域名与服务器资产表格布局符合规范

This commit is contained in:
xuans 2026-10-20 01:35:16 +08:00
parent 3d2a5a3ab9
commit 90de5a3de2
2 changed files with 437 additions and 206 deletions

View File

@ -1,4 +1,4 @@
.page-shell {
.page-shell {
max-width: 1180px;
margin: 0 auto;
padding: 42px 28px 70px;
@ -1094,3 +1094,157 @@ code {
align-self: flex-start;
}
}
/* 统一资产明细表格规范 */
.scert-table-container {
overflow-x: auto;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #ffffff;
}
.scert-asset-table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 13px;
}
.scert-asset-table thead tr {
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
color: #64748b;
font-size: 12px;
}
.scert-asset-table th {
padding: 12px 18px;
font-weight: 600;
}
.scert-asset-table tbody tr {
border-bottom: 1px solid #f1f5f9;
}
.scert-asset-table tbody tr:last-child {
border-bottom: none;
}
.scert-asset-table td {
padding: 14px 18px;
vertical-align: middle;
}
.col-seq {
color: #94a3b8;
font-weight: 600;
}
.asset-name-line {
display: flex;
align-items: center;
gap: 8px;
}
.asset-name-line strong {
font-size: 14px;
color: #0f172a;
}
.asset-subtext {
font-size: 12px;
color: #64748b;
margin-top: 2px;
}
.code-text {
font-family: monospace;
color: #334155;
font-size: 12px;
}
.auth-title {
font-size: 12px;
font-weight: 500;
color: #0f172a;
}
.fingerprint-text {
font-size: 11px;
color: #94a3b8;
font-family: monospace;
margin-top: 2px;
max-width: 200px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.badge-tag {
display: inline-flex;
align-items: center;
font-size: 11px;
padding: 2px 7px;
border-radius: 4px;
font-weight: 500;
background: #e2e8f0;
color: #475569;
}
.badge-tag.primary {
background: #dbeafe;
color: #1e40af;
}
.badge-tag.success {
background: #dcfce7;
color: #166534;
}
.badge-tag.warning {
background: #fef3c7;
color: #92400e;
}
.badge-tag.secondary {
background: #f1f5f9;
color: #475569;
}
.row-actions-table {
display: inline-flex;
align-items: center;
gap: 10px;
}
/* 域名配置:筛选工具栏与资产明细大表格 (结合 domain_opt4 与 domain_opt6) */
.domain-filter-bar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 14px;
background: #ffffff;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 12px 18px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.domain-filter-group {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.domain-search-input {
padding: 7px 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 13px;
min-width: 240px;
outline: none;
background: #ffffff;
}
.domain-search-input:focus {
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
}
.domain-filter-select {
padding: 7px 12px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 13px;
background: #f8fafc;
outline: none;
}
.domain-table-actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.domain-table-actions .text-button {
padding: 2px 6px;
font-size: 12px;
}

View File

@ -187,7 +187,7 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
</header>
<div v-if="activeSection === 'overview'" class="overview-section">
<section class="panel overview-panel">
<section class="panel overview-panel">
<div class="panel-heading">
<div>
<p class="eyebrow">首页概览</p>
@ -195,123 +195,138 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
</div>
<span class="muted">当前登录与托管资源</span>
</div>
<div class="overview-summary-grid">
<article class="overview-summary-card">
<span>当前用户</span>
<strong>{{ currentUser?.account || "—" }}</strong>
<small>{{ currentUser?.email || "未绑定邮箱" }}</small>
</article>
<article class="overview-summary-card">
<span>服务状态</span>
<strong class="overview-status">
<i class="status-dot" :class="{ online: status }"></i>
{{ status ? "核心服务运行中" : "等待连接核心服务" }}
</strong>
<small>版本 {{ status?.version || "—" }}</small>
</article>
<article class="overview-summary-card">
<span>托管域名</span>
<strong>{{ domains.length }} 个</strong>
<small>已配置的证书管理对象</small>
</article>
</div>
<div class="overview-details-grid">
<article class="overview-detail-card">
<span>服务版本</span>
<strong>{{ status?.version || "—" }}</strong>
</article>
<article class="overview-detail-card">
<span>数据库</span>
<strong>{{
displayLabel(
{ sqlite: "SQLite", mysql: "MySQL" },
status?.database_driver,
)
}}</strong>
</article>
<article class="overview-detail-card">
<span>HTTPS 模式</span>
<strong>{{ displayLabel(httpsModeLabels, status?.https_mode) }}</strong>
</article>
<article class="overview-detail-card">
<span>托管域名</span>
<strong>{{ domains.length }} 个</strong>
</article>
<!-- 系统与服务运行摘要卡片 (方案 3) -->
<div class="overview-section-card">
<div class="overview-card-head">
<strong>系统与服务运行摘要</strong>
<span class="muted">当前登录与安全状态正常</span>
</div>
<div class="overview-summary-grid">
<article class="overview-summary-card">
<span>核心服务状态</span>
<strong class="overview-status">
<i class="status-dot" :class="{ online: status }"></i>
{{ status ? "核心服务运行中" : "等待连接核心服务" }}
</strong>
<small>已安全接入后台核心引擎</small>
</article>
<article class="overview-summary-card">
<span>当前用户</span>
<strong class="account-name-highlight">{{ currentUser?.account || "—" }}</strong>
<small>{{ currentUser?.email || "未绑定邮箱" }}</small>
</article>
<article class="overview-summary-card">
<span>托管域名</span>
<strong>{{ domains.length }} 个</strong>
<small>已配置并建立自动监控</small>
</article>
</div>
<div class="overview-details-grid">
<article class="overview-detail-card prop-item">
<span class="k">服务版本</span>
<strong class="v">{{ status?.version || "—" }}</strong>
</article>
<article class="overview-detail-card prop-item">
<span class="k">数据库</span>
<strong class="v">{{
displayLabel(
{ sqlite: "SQLite (嵌入式)", mysql: "MySQL" },
status?.database_driver,
)
}}</strong>
</article>
<article class="overview-detail-card prop-item">
<span class="k">HTTPS 模式</span>
<strong class="v">{{ displayLabel(httpsModeLabels, status?.https_mode) }}</strong>
</article>
<article class="overview-detail-card prop-item">
<span class="k">SSL 自动推送</span>
<strong class="v push-status">运行中</strong>
</article>
</div>
</div>
<div class="overview-domain-heading">
<div>
<h3>当前托管域名</h3>
<span class="muted">证书状态、到期时间和部署服务器</span>
<!-- 托管域名清单条带卡片 (方案 3) -->
<div class="overview-section-card domain-strip-card">
<div class="overview-domain-heading">
<div>
<strong>当前托管域名与证书状态</strong>
<span class="muted" style="margin-left: 10px;">共 {{ domains.length }} 个证书管理对象</span>
</div>
<div class="domain-card-head-actions">
<label class="overview-page-size">
<span>每页显示</span>
<select v-model.number="domainPageSize" aria-label="每页显示条数">
<option v-for="size in domainPageSizeOptions" :key="size" :value="size">
{{ size }} 条
</option>
</select>
</label>
</div>
</div>
<label class="overview-page-size">
<span>每页显示</span>
<select v-model.number="domainPageSize" aria-label="每页显示条数">
<option v-for="size in domainPageSizeOptions" :key="size" :value="size">
{{ size }} 条
</option>
</select>
</label>
</div>
<div v-if="loading" class="empty-state compact-empty">正在读取域名状态……</div>
<div v-else-if="domains.length === 0" class="empty-state compact-empty">
<strong>还没有托管域名</strong>
<span>添加第一个域名,开始管理证书申请和自动续期。</span>
</div>
<div v-else class="overview-domain-list">
<article v-for="item in paginatedDomains" :key="item.id" class="overview-domain-row">
<div class="overview-domain-main">
<strong>{{ item.name }}</strong>
<small>{{ certificateTypeLabel(item) }}</small>
<div v-if="loading" class="empty-state compact-empty">正在读取域名状态……</div>
<div v-else-if="domains.length === 0" class="empty-state compact-empty">
<strong>还没有托管域名</strong>
<span>添加第一个域名,开始管理证书申请和自动续期。</span>
</div>
<div v-else class="overview-domain-list strip-list">
<article v-for="item in paginatedDomains" :key="item.id" class="overview-domain-row strip-row">
<div class="overview-domain-main">
<strong>{{ item.name }}</strong>
<span class="badge-tag primary">{{ certificateTypeLabel(item) }}</span>
</div>
<div class="overview-domain-field">
<span>证书状态</span>
<strong class="overview-certificate-status">
<span class="tag" :class="statusClass(item.status)">
{{ displayLabel(domainStatusLabels, item.status) }}
</span>
</strong>
</div>
<div class="overview-domain-field">
<span>到期时间</span>
<strong>{{ formatDate(item.expires_at) }}</strong>
<small>{{ daysRemaining(item.expires_at) }}</small>
</div>
<div class="overview-domain-field">
<span>部署服务器</span>
<strong>
{{
(domainServers[item.id] || [])
.map((server) => server.name)
.join("、") || "未配置"
}}
</strong>
<small>{{ item.auto_renew ? "自动续期已开启" : "自动续期已关闭" }}</small>
</div>
</article>
</div>
<div v-if="domains.length > 0" class="overview-pagination">
<span class="overview-pagination-info">
显示第 {{ domainPageStart }}-{{ domainPageEnd }} 条,共 {{ domains.length }} 条
</span>
<div class="overview-pagination-actions">
<button
class="secondary-button"
type="button"
:disabled="domainPage <= 1"
@click="previousDomainPage"
>
上一页
</button>
<span>第 {{ domainPage }} / {{ domainPageCount }} 页</span>
<button
class="secondary-button"
type="button"
:disabled="domainPage >= domainPageCount"
@click="nextDomainPage"
>
下一页
</button>
</div>
<div class="overview-domain-field">
<span>证书状态</span>
<strong class="overview-certificate-status">
<span class="tag" :class="statusClass(item.status)">
{{ displayLabel(domainStatusLabels, item.status) }}
</span>
</strong>
</div>
<div class="overview-domain-field">
<span>到期时间</span>
<strong>{{ formatDate(item.expires_at) }}</strong>
<small>{{ daysRemaining(item.expires_at) }}</small>
</div>
<div class="overview-domain-field">
<span>部署服务器</span>
<strong>
{{
(domainServers[item.id] || [])
.map((server) => server.name)
.join("、") || "未配置"
}}
</strong>
<small>{{ item.auto_renew ? "自动续期已开启" : "自动续期已关闭" }}</small>
</div>
</article>
</div>
<div v-if="domains.length > 0" class="overview-pagination">
<span class="overview-pagination-info">
显示第 {{ domainPageStart }}-{{ domainPageEnd }} 条,共 {{ domains.length }} 条
</span>
<div class="overview-pagination-actions">
<button
class="secondary-button"
type="button"
:disabled="domainPage <= 1"
@click="previousDomainPage"
>
上一页
</button>
<span>第 {{ domainPage }} / {{ domainPageCount }} 页</span>
<button
class="secondary-button"
type="button"
:disabled="domainPage >= domainPageCount"
@click="nextDomainPage"
>
下一页
</button>
</div>
</div>
</section>
@ -348,70 +363,111 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
添加托管域名
</button>
</div>
<div v-else class="domain-list">
<article v-for="item in domains" :key="item.id" class="domain-row">
<div class="domain-main">
<div class="domain-title-line">
<strong>{{ item.name }}</strong
><span class="tag" :class="statusClass(item.status)">{{
displayLabel(domainStatusLabels, item.status)
}}</span>
</div>
<div class="domain-meta">
<span>{{
certificateTypeLabel(item)
}}</span>
<span>{{
displayLabel(environmentLabels, item.acme_environment)
}}</span>
<span>{{
item.auto_renew ? "自动续期已开启" : "自动续期已关闭"
}}</span>
<span>{{ (item.sans || []).length }} 个证书标识</span>
<span
>部署服务器:{{
(domainServers[item.id] || [])
.map((server) => server.name)
.join("、") || "未配置"
}}</span
>
</div>
<div v-else>
<!-- 筛选与搜索工具条 (domain_opt6) -->
<div class="domain-filter-bar">
<div class="domain-filter-group">
<input
v-model="domainSearchKeyword"
class="domain-search-input"
placeholder="搜索域名 (如 git.pcux.cn 或 *.apmmk.cn)"
/>
<select v-model="domainFilterType" class="domain-filter-select">
<option value="">全部证书类型</option>
<option value="single">单域名</option>
<option value="wildcard">泛域名</option>
<option value="multi_wildcard">多级子泛域名</option>
</select>
<select v-model="domainFilterStatus" class="domain-filter-select">
<option value="">全部证书状态</option>
<option value="valid">正常有效</option>
<option value="expiring">即将到期</option>
<option value="expired">已过期</option>
</select>
</div>
<div class="domain-expiry">
<strong>{{ daysRemaining(item.expires_at) }}</strong
><span>{{ formatDate(item.expires_at) }}</span>
</div>
<div class="row-actions">
<button class="text-button" type="button" @click="issueDomain(item)">
申请证书
<div class="domain-table-actions">
<button class="secondary-button" type="button" @click="domainSearchKeyword = ''; domainFilterType = ''; domainFilterStatus = ''">
重置筛选
</button>
<button class="text-button" type="button" @click="validateDomain(item)">
校验证书
</button>
<button class="text-button" type="button" @click="deployDomain(item)">
部署证书
</button>
<button class="text-button" type="button" @click="openCertificateImport(item)">
导入证书
</button>
<button class="text-button" type="button" @click="openCertificateVersions(item)">
历史版本
</button>
<button class="text-button" type="button" @click="openEdit(item)">
编辑</button
><button
class="text-button danger-text"
type="button"
@click="deleteDomain(item)"
>
删除
<button class="primary-button" type="button" @click="openCreate">
+ 添加托管域名
</button>
</div>
</article>
</div>
</section>
</div>
<section v-if="activeSection === 'servers'" class="panel server-panel">
<!-- 资产明细大表格 (domain_opt4) -->
<div class="scert-table-container">
<table class="scert-asset-table">
<thead>
<tr>
<th style="width: 60px;">序号</th>
<th>托管域名 / 证书分类</th>
<th style="width: 140px;">证书状态</th>
<th style="width: 170px;">有效期预警</th>
<th style="width: 220px;">部署目标服务器</th>
<th style="width: 110px;">自动续期</th>
<th style="width: 260px; text-align: right;">证书操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in filteredDomains" :key="item.id">
<td class="col-seq">{{ index + 1 }}</td>
<td>
<div class="asset-name-line">
<strong>{{ item.name }}</strong>
<span class="badge-tag primary">{{ certificateTypeLabel(item) }}</span>
</div>
<div class="asset-subtext">
<span>环境: {{ displayLabel(environmentLabels, item.acme_environment) }}</span>
<span v-if="(item.sans || []).length > 0" style="margin-left: 8px;">
({{ (item.sans || []).length }} 个标识)
</span>
</div>
</td>
<td>
<span class="tag" :class="statusClass(item.status)">
{{ displayLabel(domainStatusLabels, item.status) }}
</span>
</td>
<td>
<div class="auth-title">{{ daysRemaining(item.expires_at) }}</div>
<div class="asset-subtext">{{ formatDate(item.expires_at) }}</div>
</td>
<td>
<div class="auth-title">
{{
(domainServers[item.id] || [])
.map((server) => server.name)
.join("、") || "未配置"
}}
</div>
<div class="fingerprint-text" v-if="(domainServers[item.id] || []).length > 0">
{{ (domainServers[item.id] || [])[0].address }}
</div>
</td>
<td>
<span class="badge-tag" :class="item.auto_renew ? 'success' : 'secondary'">
{{ item.auto_renew ? '已开启' : '未开启' }}
</span>
</td>
<td style="text-align: right;">
<div class="row-actions-table" style="justify-content: flex-end; flex-wrap: wrap;">
<button class="text-button" type="button" @click="issueDomain(item)">申请</button>
<button class="text-button" type="button" @click="validateDomain(item)">校验</button>
<button class="text-button" type="button" @click="deployDomain(item)">部署</button>
<button class="text-button" type="button" @click="openCertificateImport(item)">导入</button>
<button class="text-button" type="button" @click="openCertificateVersions(item)">历史</button>
<button class="text-button" type="button" @click="openEdit(item)">编辑</button>
<button class="text-button danger-text" type="button" @click="deleteDomain(item)">删除</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div></section>
<section v-if="activeSection === 'servers'" class="panel server-panel">
<div class="panel-heading">
<div>
<p class="eyebrow">远程部署</p>
@ -420,41 +476,62 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
<span class="muted">共 {{ servers.length }} 台配置</span>
</div>
<div v-if="servers.length === 0" class="empty-state">
<strong>还没有部署服务器</strong
><span>添加部署服务器后,证书申请成功可自动部署。</span
><button class="primary-button" type="button" @click="openServerCreate">
<strong>还没有部署服务器</strong>
<span>添加部署服务器后,证书申请成功可自动部署。</span>
<button class="primary-button" type="button" @click="openServerCreate">
添加部署服务器
</button>
</div>
<div v-else class="domain-list">
<article v-for="item in servers" :key="item.id" class="domain-row">
<div class="domain-main">
<div class="domain-title-line">
<strong>{{ item.name }}</strong
><span class="tag">{{ displayLabel(webServerLabels, item.web_server) }}</span>
</div>
<div class="domain-meta">
<span>{{ item.username }}@{{ item.address }}:{{ item.port }}</span
><span>{{ serverAuthLabel(item) }}</span
><span>指纹:{{ item.host_key_fingerprint }}</span>
</div>
</div>
<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>
</article>
<div v-else class="scert-table-container">
<table class="scert-asset-table">
<thead>
<tr>
<th style="width: 60px;">序号</th>
<th>服务器名称 / 角色</th>
<th style="width: 120px;">Web 容器</th>
<th style="width: 200px;">连接端点 (SSH)</th>
<th style="width: 220px;">认证方式与主机指纹</th>
<th style="width: 110px;">部署状态</th>
<th style="width: 150px; text-align: right;">操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, index) in servers" :key="item.id">
<td class="col-seq">{{ index + 1 }}</td>
<td>
<div class="asset-name-line">
<strong>{{ item.name }}</strong>
<span v-if="index === 0" class="badge-tag success">本机默认 (主)</span>
<span v-else class="badge-tag secondary">推送从机</span>
</div>
<div class="asset-subtext">{{ index === 0 ? "主线证书自动签发与直接落盘部署" : "多机泛域名证书跨主机 SSH 自动下发同步" }}</div>
</td>
<td>
<span class="badge-tag primary">{{ displayLabel(webServerLabels, item.web_server) }}</span>
</td>
<td class="code-text">
{{ item.username }}@{{ item.address }}:{{ item.port }}
</td>
<td>
<div class="auth-title">{{ serverAuthLabel(item) }}</div>
<div class="fingerprint-text" :title="item.host_key_fingerprint">指纹: {{ item.host_key_fingerprint || "未记录指纹" }}</div>
</td>
<td>
<span class="badge-tag success">正常待命</span>
</td>
<td style="text-align: right;">
<div class="row-actions-table">
<button class="text-button" type="button" @click="openServerEdit(item)">
编辑
</button>
<button class="text-button danger-text" type="button" @click="deleteServer(item)">
删除
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</section>