diff --git a/frontend/src/views/Dashboard/Dashboard.css b/frontend/src/views/Dashboard/Dashboard.css index 0d2e990..622d4b0 100644 --- a/frontend/src/views/Dashboard/Dashboard.css +++ b/frontend/src/views/Dashboard/Dashboard.css @@ -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); diff --git a/frontend/src/views/Dashboard/Dashboard.js b/frontend/src/views/Dashboard/Dashboard.js index d6b916c..ef0a6e5 100644 --- a/frontend/src/views/Dashboard/Dashboard.js +++ b/frontend/src/views/Dashboard/Dashboard.js @@ -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, diff --git a/frontend/src/views/Dashboard/Dashboard.test.js b/frontend/src/views/Dashboard/Dashboard.test.js index aa4b675..b3420c4 100644 --- a/frontend/src/views/Dashboard/Dashboard.test.js +++ b/frontend/src/views/Dashboard/Dashboard.test.js @@ -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"); diff --git a/frontend/src/views/Dashboard/Dashboard.vue b/frontend/src/views/Dashboard/Dashboard.vue index 72700f5..6c6339e 100644 --- a/frontend/src/views/Dashboard/Dashboard.vue +++ b/frontend/src/views/Dashboard/Dashboard.vue @@ -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 已配置的证书管理对象 -