优化首页基本信息布局

移除首页宣传横幅,新增用户、域名、状态、到期时间和部署服务器概览,并补充布局回归测试。
This commit is contained in:
xuans 2026-09-25 11:40:29 +08:00
parent 0a9172f639
commit 475058ed4b
4 changed files with 162 additions and 55 deletions

View File

@ -65,28 +65,6 @@ h2 {
cursor: wait;
opacity: 0.6;
}
.hero-card {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 250px;
margin: 36px 0 18px;
padding: 36px 42px;
border-radius: 28px;
color: #fff;
background: linear-gradient(120deg, #182b5b, #315aaa);
box-shadow: 0 20px 45px rgba(42, 75, 145, 0.2);
}
.hero-card p {
max-width: 650px;
margin-bottom: 0;
color: #d5e1ff;
line-height: 1.8;
}
.status-label {
font-size: 13px;
font-weight: 800;
}
.status-dot {
display: inline-block;
width: 9px;
@ -99,13 +77,6 @@ h2 {
background: #5ee0a0;
box-shadow: 0 0 0 5px rgba(94, 224, 160, 0.15);
}
.hero-mark {
display: block;
width: 138px;
height: 138px;
object-fit: contain;
filter: drop-shadow(0 16px 24px rgba(0, 0, 0, 0.18));
}
.error-banner,
.notice-banner {
margin: 18px 0;
@ -122,6 +93,84 @@ h2 {
color: #24734b;
background: #effbf4;
}
.overview-panel {
margin: 28px 0 18px;
}
.overview-summary-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 12px;
margin-bottom: 16px;
}
.overview-summary-card,
.overview-domain-row {
border: 1px solid #e2e8f2;
border-radius: 14px;
background: #fbfcfe;
}
.overview-summary-card {
display: grid;
gap: 7px;
min-width: 0;
padding: 16px;
}
.overview-summary-card > span,
.overview-domain-field > span,
.overview-summary-card small,
.overview-domain-field small {
color: #7a879d;
font-size: 12px;
}
.overview-summary-card strong {
min-width: 0;
overflow-wrap: anywhere;
font-size: 20px;
}
.overview-summary-card small,
.overview-domain-field small {
line-height: 1.45;
}
.overview-status {
display: flex;
align-items: center;
gap: 7px;
white-space: nowrap;
font-size: 17px !important;
}
.overview-status .status-dot {
flex: 0 0 auto;
margin-right: 0;
}
.overview-domain-list {
display: grid;
gap: 10px;
}
.overview-domain-row {
display: grid;
grid-template-columns: minmax(0, 1.3fr) minmax(130px, 0.8fr) minmax(180px, 1fr);
align-items: center;
gap: 18px;
padding: 15px 16px;
}
.overview-domain-main,
.overview-domain-field {
display: grid;
gap: 6px;
min-width: 0;
}
.overview-domain-main {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 9px;
}
.overview-domain-main strong,
.overview-domain-field strong {
overflow-wrap: anywhere;
}
.overview-domain-field strong {
font-size: 14px;
}
.metrics-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
@ -562,12 +611,9 @@ code {
.topbar-actions button {
flex: 1 1 calc(50% - 10px);
}
.hero-card {
align-items: flex-start;
padding: 28px;
}
.hero-mark {
display: none;
.overview-summary-grid,
.overview-domain-row {
grid-template-columns: 1fr;
}
.metrics-grid {
grid-template-columns: repeat(2, 1fr);

View File

@ -11,9 +11,8 @@ function dnsVerificationTimeoutFor(providerID) {
}
export function useDashboard() {
const scertLogo = "/Scert.png";
const status = ref(null);
const currentUser = ref(null);
const domains = ref([]);
const servers = ref([]);
const sshKeys = ref([]);
@ -1079,6 +1078,7 @@ export function useDashboard() {
try {
const [
statusResponse,
authResponse,
domainResponse,
serverResponse,
sshKeyResponse,
@ -1089,6 +1089,7 @@ export function useDashboard() {
dnsProviderResponse,
] = await Promise.all([
fetchAdmin("/api/v1/status"),
fetchAdmin("/api/v1/auth/session"),
fetchAdmin("/api/v1/domains"),
fetchAdmin("/api/v1/servers"),
fetchAdmin("/api/v1/ssh-keys"),
@ -1113,6 +1114,7 @@ export function useDashboard() {
throw new Error(await responseError(failedResponse, "读取管理数据失败"));
}
status.value = await statusResponse.json();
currentUser.value = await authResponse.json();
domains.value = (await domainResponse.json()).items || [];
servers.value = (await serverResponse.json()).items || [];
sshKeys.value = (await sshKeyResponse.json()).items || [];
@ -1160,8 +1162,8 @@ export function useDashboard() {
});
return {
scertLogo,
status,
currentUser,
domains,
servers,
sshKeys,

View File

@ -576,10 +576,23 @@ test("dashboardLayout.keepsActivityListsBoundedAndKeyboardAccessible", async ()
assert.match(stylesheet, /\.record-list:focus-visible/);
});
test("dashboardLayout.replacesHeroWithBasicInformationSummary", async () => {
const { readFile } = await import("node:fs/promises");
const template = await readFile(new URL("./Dashboard.vue", import.meta.url), "utf8");
const stylesheet = await readFile(new URL("./Dashboard.css", import.meta.url), "utf8");
assert.doesNotMatch(template, /class="hero-card"/);
assert.match(template, /class="panel overview-panel"/);
assert.match(template, /当前用户/);
assert.match(template, /到期时间/);
assert.match(template, /部署服务器/);
assert.match(stylesheet, /\.overview-summary-grid\s*\{[^}]*grid-template-columns: repeat\(3/);
assert.match(stylesheet, /\.overview-domain-row\s*\{[^}]*grid-template-columns:/);
});
test("dashboardLayout.usesSharedTooltipForUserManagementDescription", async () => {
const { readFile } = await import("node:fs/promises");
const template = await readFile(new URL("../UserManagement/UserManagement.vue", import.meta.url), "utf8");
assert.match(template, /import HelpTip from "\.\.\/components\/HelpTip\/HelpTip.vue";/);
assert.match(template, /<HelpTip>[\s\S]*?<\/HelpTip>/);
assert.doesNotMatch(template, /<p class="user-management-description">/);
});
});

View File

@ -9,8 +9,8 @@ import { useDashboard } from "./Dashboard.js";
const dashboard = useDashboard();
const {
scertLogo,
status,
currentUser,
domains,
servers,
sshKeys,
@ -153,24 +153,70 @@ const {
</div>
</header>
<section class="hero-card">
<div>
<span class="status-dot" :class="{ online: status }"></span>
<span class="status-label">{{
status ? "核心服务运行中" : "等待连接核心服务"
}}</span>
<h2>让证书续期和部署变得可追踪</h2>
<p>
统一管理单域名、泛域名和 SAN 证书,当前已接入 lego、DNS
验证、自动续期、远程部署与部署后校验流程。
</p>
</div>
<img class="hero-mark" :src="scertLogo" alt="Scert 标志" />
</section>
<p v-if="error" class="error-banner">{{ error }}</p>
<p v-if="notice" class="notice-banner">{{ notice }}</p>
<section class="panel overview-panel">
<div class="panel-heading">
<div>
<p class="eyebrow">首页概览</p>
<h2>基本信息</h2>
</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 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 domains" :key="item.id" class="overview-domain-row">
<div class="overview-domain-main">
<strong>{{ item.name }}</strong>
<span class="tag" :class="statusClass(item.status)">
{{ displayLabel(domainStatusLabels, item.status) }}
</span>
</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>
</section>
<section class="metrics-grid">
<article class="metric-card">
<span>服务版本</span><strong>{{ status?.version || "—" }}</strong>