优化首页托管域名分页布局
将首页信息卡片移动到托管域名列表上方,增加证书状态列表和每页条数分页,并修正正常状态显示。
This commit is contained in:
parent
040ad2ed49
commit
a0b1f745c9
@ -145,9 +145,40 @@ h2 {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
.overview-domain-heading {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin: 24px 0 12px;
|
||||
}
|
||||
.overview-domain-heading h3 {
|
||||
margin: 0 0 5px;
|
||||
color: #32405a;
|
||||
font-size: 17px;
|
||||
}
|
||||
.overview-page-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
color: #7a879d;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.overview-page-size select {
|
||||
min-width: 86px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #bdcde0;
|
||||
border-radius: 9px;
|
||||
color: #32405a;
|
||||
background: #fff;
|
||||
font: inherit;
|
||||
font-weight: 700;
|
||||
}
|
||||
.overview-domain-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.3fr) minmax(130px, 0.8fr) minmax(180px, 1fr);
|
||||
grid-template-columns: minmax(0, 1.25fr) minmax(115px, 0.7fr) minmax(130px, 0.8fr) minmax(180px, 1fr);
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
padding: 15px 16px;
|
||||
@ -168,9 +199,38 @@ h2 {
|
||||
.overview-domain-field strong {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.overview-domain-main small {
|
||||
color: #7a879d;
|
||||
font-size: 12px;
|
||||
}
|
||||
.overview-domain-field strong {
|
||||
font-size: 14px;
|
||||
}
|
||||
.overview-certificate-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 27px;
|
||||
}
|
||||
.overview-pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin-top: 14px;
|
||||
color: #7a879d;
|
||||
font-size: 13px;
|
||||
}
|
||||
.overview-pagination-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #52617a;
|
||||
font-weight: 700;
|
||||
}
|
||||
.overview-pagination-actions .secondary-button {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.overview-details-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
|
||||
@ -22,6 +22,9 @@ export function useDashboard() {
|
||||
const settings = ref(null);
|
||||
const dnsProviderCatalog = ref([]);
|
||||
const domainServers = ref({});
|
||||
const domainPage = ref(1);
|
||||
const domainPageSize = ref(5);
|
||||
const domainPageSizeOptions = [5, 10, 15, 30];
|
||||
const loading = ref(true);
|
||||
const saving = ref(false);
|
||||
const error = ref("");
|
||||
@ -79,6 +82,30 @@ export function useDashboard() {
|
||||
);
|
||||
let previousBodyOverflow = "";
|
||||
|
||||
const domainPageCount = computed(() =>
|
||||
Math.max(1, Math.ceil(domains.value.length / domainPageSize.value)),
|
||||
);
|
||||
const paginatedDomains = computed(() => {
|
||||
const start = (domainPage.value - 1) * domainPageSize.value;
|
||||
return domains.value.slice(start, start + domainPageSize.value);
|
||||
});
|
||||
const domainPageStart = computed(() =>
|
||||
domains.value.length === 0 ? 0 : (domainPage.value - 1) * domainPageSize.value + 1,
|
||||
);
|
||||
const domainPageEnd = computed(() =>
|
||||
Math.min(domainPage.value * domainPageSize.value, domains.value.length),
|
||||
);
|
||||
|
||||
watch(domainPageSize, () => {
|
||||
domainPage.value = 1;
|
||||
});
|
||||
|
||||
watch(domains, () => {
|
||||
if (domainPage.value > domainPageCount.value) {
|
||||
domainPage.value = domainPageCount.value;
|
||||
}
|
||||
});
|
||||
|
||||
watch(hasOpenModal, (isOpen) => {
|
||||
if (isOpen) {
|
||||
previousBodyOverflow = document.body.style.overflow;
|
||||
@ -180,6 +207,7 @@ export function useDashboard() {
|
||||
const domainStatusLabels = {
|
||||
pending: "等待申请",
|
||||
active: "正常",
|
||||
valid: "正常",
|
||||
error: "错误",
|
||||
};
|
||||
const httpsModeLabels = { direct: "核心服务 HTTPS", proxy: "反向代理 HTTPS" };
|
||||
@ -486,13 +514,21 @@ export function useDashboard() {
|
||||
}
|
||||
|
||||
function statusClass(value) {
|
||||
return value === "active" || value === "success"
|
||||
return value === "active" || value === "valid" || value === "success"
|
||||
? "success"
|
||||
: value === "error" || value === "failed"
|
||||
? "danger"
|
||||
: "warning";
|
||||
}
|
||||
|
||||
function previousDomainPage() {
|
||||
domainPage.value = Math.max(1, domainPage.value - 1);
|
||||
}
|
||||
|
||||
function nextDomainPage() {
|
||||
domainPage.value = Math.min(domainPageCount.value, domainPage.value + 1);
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
editingId.value = "";
|
||||
form.value = createEmptyForm();
|
||||
@ -1192,6 +1228,15 @@ export function useDashboard() {
|
||||
clearDNSCredentials,
|
||||
testDNS,
|
||||
domainServers,
|
||||
domainPage,
|
||||
domainPageSize,
|
||||
domainPageSizeOptions,
|
||||
domainPageCount,
|
||||
paginatedDomains,
|
||||
domainPageStart,
|
||||
domainPageEnd,
|
||||
previousDomainPage,
|
||||
nextDomainPage,
|
||||
loading,
|
||||
saving,
|
||||
error,
|
||||
|
||||
@ -701,6 +701,58 @@ test("dashboardLayout.replacesHeroWithBasicInformationSummary", async () => {
|
||||
assert.match(stylesheet, /\.overview-domain-row\s*\{[^}]*grid-template-columns:/);
|
||||
});
|
||||
|
||||
test("dashboardLayout.paginatesHostedDomainsWithConfiguredPageSizes", async (t) => {
|
||||
const { state } = await mountComposable(t, useDashboard, async () => Response.json({ items: [] }));
|
||||
state.domains.value = Array.from({ length: 16 }, (_, index) => ({
|
||||
id: `domain-${index + 1}`,
|
||||
name: `domain-${index + 1}.example.test`,
|
||||
}));
|
||||
await nextTick();
|
||||
|
||||
assert.deepEqual(state.domainPageSizeOptions, [5, 10, 15, 30]);
|
||||
assert.equal(state.domainPageCount.value, 4);
|
||||
assert.equal(state.paginatedDomains.value.length, 5);
|
||||
assert.equal(state.domainPageStart.value, 1);
|
||||
assert.equal(state.domainPageEnd.value, 5);
|
||||
|
||||
state.nextDomainPage();
|
||||
assert.equal(state.domainPage.value, 2);
|
||||
assert.equal(state.paginatedDomains.value[0].id, "domain-6");
|
||||
|
||||
state.domainPageSize.value = 10;
|
||||
await nextTick();
|
||||
assert.equal(state.domainPageCount.value, 2);
|
||||
assert.equal(state.paginatedDomains.value.length, 10);
|
||||
|
||||
state.domainPage.value = 2;
|
||||
state.domainPageSize.value = 5;
|
||||
await nextTick();
|
||||
assert.equal(state.domainPage.value, 1);
|
||||
state.domainPage.value = 2;
|
||||
state.nextDomainPage();
|
||||
state.nextDomainPage();
|
||||
assert.equal(state.domainPage.value, 4);
|
||||
state.nextDomainPage();
|
||||
assert.equal(state.domainPage.value, 4);
|
||||
state.previousDomainPage();
|
||||
assert.equal(state.domainPage.value, 3);
|
||||
assert.equal(state.statusClass("valid"), "success");
|
||||
assert.equal(state.displayLabel(state.domainStatusLabels, "valid"), "正常");
|
||||
});
|
||||
|
||||
test("dashboardLayout.movesOverviewDetailsAboveHostedDomainList", async () => {
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
const template = await readFile(new URL("./Dashboard.vue", import.meta.url), "utf8");
|
||||
const detailsIndex = template.indexOf('class="overview-details-grid"');
|
||||
const listHeadingIndex = template.indexOf('class="overview-domain-heading"');
|
||||
assert.ok(detailsIndex >= 0);
|
||||
assert.ok(listHeadingIndex >= 0);
|
||||
assert.ok(detailsIndex < listHeadingIndex);
|
||||
assert.match(template, /v-model\.number="domainPageSize"/);
|
||||
assert.match(template, /上一页/);
|
||||
assert.match(template, /下一页/);
|
||||
});
|
||||
|
||||
test("dashboardLayout.usesSharedTooltipForUserManagementDescription", async () => {
|
||||
const { readFile } = await import("node:fs/promises");
|
||||
const template = await readFile(new URL("../UserManagement/UserManagement.vue", import.meta.url), "utf8");
|
||||
|
||||
@ -22,6 +22,15 @@ const {
|
||||
settings,
|
||||
settingsForm,
|
||||
domainServers,
|
||||
domainPage,
|
||||
domainPageSize,
|
||||
domainPageSizeOptions,
|
||||
domainPageCount,
|
||||
paginatedDomains,
|
||||
domainPageStart,
|
||||
domainPageEnd,
|
||||
previousDomainPage,
|
||||
nextDomainPage,
|
||||
loading,
|
||||
saving,
|
||||
error,
|
||||
@ -210,37 +219,6 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
|
||||
<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>
|
||||
<div class="overview-details-grid">
|
||||
<article class="overview-detail-card">
|
||||
<span>服务版本</span>
|
||||
@ -264,6 +242,82 @@ const { activeSection, activeLabel, sections, selectSection } = useDashboardNavi
|
||||
<strong>{{ domains.length }} 个</strong>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="overview-domain-heading">
|
||||
<div>
|
||||
<h3>当前托管域名</h3>
|
||||
<span class="muted">证书状态、到期时间和部署服务器</span>
|
||||
</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>{{ displayLabel(certificateTypeLabels, item.certificate_type) }}</small>
|
||||
</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>
|
||||
|
||||
</div>
|
||||
|
||||
@ -212,6 +212,17 @@
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.overview-domain-heading,
|
||||
.overview-pagination {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.overview-pagination-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.overview-details-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user