Parcourir la source

fix: improve dashboard navigation and domain detail layout

Codex il y a 1 mois
Parent
commit
fe52c78cba
3 fichiers modifiés avec 164 ajouts et 45 suppressions
  1. 115 43
      public/app.js
  2. 2 2
      public/index.html
  3. 47 0
      public/styles.css

+ 115 - 43
public/app.js

@@ -11,6 +11,7 @@ const state = {
   settings: null,
   selectedId: null,
   view: 'dashboard',
+  detailTab: 'dns',
   dnsTestResults: {},
   busy: false
 };
@@ -125,6 +126,7 @@ function bindEvents() {
     const item = event.target.closest('[data-domain-id]');
     if (!item) return;
     state.selectedId = Number(item.dataset.domainId);
+    state.detailTab = 'dns';
     setView('domains');
     render();
   });
@@ -782,8 +784,107 @@ function renderDetail() {
     </div>
     <div class="card-body detail-body">
       ${renderSetupOverview(domain, guide, records)}
-      ${guide.apply ? renderApplyResult(guide.apply) : ''}
       ${warnings.length ? renderWarnings(warnings) : ''}
+      ${renderDetailTabs()}
+      ${renderDetailTabContent(domain, guide, records)}
+    </div>
+  `;
+}
+
+function renderDetailTabs() {
+  const tabs = [
+    ['dns', 'DNS 配置', 'ti-dns'],
+    ['settings', '域名设置', 'ti-adjustments'],
+    ['send', '测试发送', 'ti-send'],
+    ['integrations', '接入方式', 'ti-plug-connected'],
+    ['events', '发送记录', 'ti-list-details']
+  ];
+  return `
+    <div class="detail-tabs" role="tablist">
+      ${tabs.map(([key, label, icon]) => `
+        <button class="${state.detailTab === key ? 'active' : ''}" data-action="detail-tab" data-tab="${key}" type="button">
+          <i class="ti ${icon}" aria-hidden="true"></i>
+          <span>${label}</span>
+        </button>
+      `).join('')}
+    </div>
+  `;
+}
+
+function renderDetailTabContent(domain, guide, records) {
+  if (state.detailTab === 'settings') {
+    return `
+      <section class="detail-tab-panel">
+        <div class="row row-cards">
+          <section class="col-lg-7">
+            <div class="card subpanel">
+              <div class="card-header"><h3 class="card-title">域名设置</h3></div>
+              <div class="card-body">${renderSettingsForm(domain)}</div>
+            </div>
+          </section>
+          <section class="col-lg-5">
+            <div class="card subpanel">
+              <div class="card-header"><h3 class="card-title">增强安全记录</h3></div>
+              <div class="card-body">${renderOptionalRecords(guide.optionalRecords || [])}</div>
+            </div>
+          </section>
+        </div>
+      </section>
+    `;
+  }
+  if (state.detailTab === 'send') {
+    return `
+      <section class="detail-tab-panel">
+        <div class="row row-cards">
+          <section class="col-lg-5">
+            <div class="card subpanel">
+              <div class="card-header"><h3 class="card-title">测试发送</h3></div>
+              <div class="card-body">${renderSendForm(domain)}</div>
+            </div>
+          </section>
+          <section class="col-lg-7">
+            <div class="card subpanel">
+              <div class="card-header"><h3 class="card-title">最近发送</h3></div>
+              <div class="card-body">${renderEvents(domain)}</div>
+            </div>
+          </section>
+        </div>
+      </section>
+    `;
+  }
+  if (state.detailTab === 'integrations') {
+    return `
+      <section class="detail-tab-panel">
+        <div class="row row-cards">
+          <section class="col-lg-6">
+            <div class="card subpanel api-box">
+              <div class="card-header"><h3 class="card-title">发送 API</h3></div>
+              <div class="card-body"><pre><code>${escapeHtml(apiExample(domain))}</code></pre></div>
+            </div>
+          </section>
+          <section class="col-lg-6">
+            <div class="card subpanel">
+              <div class="card-header"><h3 class="card-title">SMTP 发信</h3></div>
+              <div class="card-body">${renderSmtpBox(domain)}</div>
+            </div>
+          </section>
+        </div>
+      </section>
+    `;
+  }
+  if (state.detailTab === 'events') {
+    return `
+      <section class="detail-tab-panel">
+        <div class="card subpanel">
+          <div class="card-header"><h3 class="card-title">发送记录</h3></div>
+          <div class="card-body">${renderEvents(domain, 20)}</div>
+        </div>
+      </section>
+    `;
+  }
+  return `
+    <section class="detail-tab-panel">
+      ${guide.apply ? renderApplyResult(guide.apply) : ''}
       <div class="row row-cards">
         <section class="col-lg-7">
           <div class="card subpanel">
@@ -791,55 +892,17 @@ function renderDetail() {
               <h3 class="card-title">配置引导</h3>
               <div class="card-actions"><button class="btn btn-sm btn-outline-secondary" data-action="copy-all-dns" type="button">复制全部</button></div>
             </div>
-            <div class="card-body">
-              ${renderDnsGuide(records)}
-            </div>
+            <div class="card-body">${renderDnsGuide(records)}</div>
           </div>
         </section>
         <section class="col-lg-5">
-          <div class="card subpanel">
-            <div class="card-header"><h3 class="card-title">域名设置</h3></div>
-            <div class="card-body">${renderSettingsForm(domain)}</div>
-          </div>
-        </section>
-      </div>
-      <div class="row row-cards">
-        <section class="col-lg-6">
           <div class="card subpanel">
             <div class="card-header"><h3 class="card-title">当前 DNS</h3></div>
             <div class="card-body">${renderLiveDns(guide.live)}</div>
           </div>
         </section>
-        <section class="col-lg-6">
-          <div class="card subpanel">
-            <div class="card-header"><h3 class="card-title">测试发送</h3></div>
-            <div class="card-body">${renderSendForm(domain)}</div>
-          </div>
-        </section>
       </div>
-      <div class="row row-cards">
-        <section class="col-lg-6">
-          <div class="card subpanel api-box">
-            <div class="card-header"><h3 class="card-title">发送 API</h3></div>
-            <div class="card-body"><pre><code>${escapeHtml(apiExample(domain))}</code></pre></div>
-          </div>
-        </section>
-        <section class="col-lg-6">
-          <div class="card subpanel">
-            <div class="card-header"><h3 class="card-title">SMTP 发信</h3></div>
-            <div class="card-body">${renderSmtpBox(domain)}</div>
-          </div>
-        </section>
-      </div>
-      <section class="card subpanel">
-        <div class="card-header"><h3 class="card-title">最近发送</h3></div>
-        <div class="card-body">${renderEvents(domain)}</div>
-      </section>
-      <section class="card subpanel">
-        <div class="card-header"><h3 class="card-title">增强安全记录</h3></div>
-        <div class="card-body">${renderOptionalRecords(guide.optionalRecords || [])}</div>
-      </section>
-    </div>
+    </section>
   `;
 }
 
@@ -1003,8 +1066,8 @@ function renderSendForm(domain) {
   `;
 }
 
-function renderEvents(domain) {
-  const events = state.events.filter((event) => event.domain === domain.domain).slice(0, 8);
+function renderEvents(domain, limit = 8) {
+  const events = state.events.filter((event) => event.domain === domain.domain).slice(0, limit);
   if (!events.length) return '<p class="text-secondary">暂无发送记录</p>';
   return `<div class="event-list">${events.map((event) => `<div class="event-row"><div class="item-line"><strong>${escapeHtml(event.subject)}</strong>${badge({ className: event.status === 'queued' ? 'ok' : 'failed', label: event.status })}</div><span class="text-secondary">${escapeHtml(event.sender)} -> ${escapeHtml((event.recipients || []).join(', '))}</span><span class="text-secondary">${escapeHtml(formatDate(event.createdAt))}</span></div>`).join('')}</div>`;
 }
@@ -1161,6 +1224,7 @@ async function handleGlobalClick(event) {
   if (viewButton && !event.target.closest('[data-domain-id]')) {
     event.preventDefault();
     setView(viewButton.dataset.view);
+    render();
     return;
   }
   const copyButton = event.target.closest('[data-copy]');
@@ -1179,6 +1243,7 @@ async function handleGlobalClick(event) {
   if (action === 'check') return checkSelected();
   if (action === 'apply-dns') return applyDnsSelected();
   if (action === 'copy-all-dns') return copyAllDns();
+  if (action === 'detail-tab') return selectDetailTab(actionTarget.dataset.tab);
   if (action === 'rotate-dkim') return rotateDkim();
   if (action === 'delete-domain') return deleteSelected();
   if (action === 'edit-dns') return editDnsCredential(id);
@@ -1197,6 +1262,13 @@ async function handleDetailSubmit(event) {
   if (form.dataset.form === 'admin-settings') return saveAdminSettings(form);
 }
 
+function selectDetailTab(tab) {
+  const allowed = ['dns', 'settings', 'send', 'integrations', 'events'];
+  if (!allowed.includes(tab)) return;
+  state.detailTab = tab;
+  renderDetail();
+}
+
 async function checkSelected() {
   const id = state.selectedId;
   if (!id) return;

+ 2 - 2
public/index.html

@@ -6,7 +6,7 @@
     <title>MailHub</title>
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0/dist/css/tabler.min.css">
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3.31.0/dist/tabler-icons.min.css">
-    <link rel="stylesheet" href="/styles.css">
+    <link rel="stylesheet" href="/styles.css?v=20260708-tabs">
   </head>
   <body>
     <div class="page">
@@ -375,6 +375,6 @@
 
     <div class="toast-host" id="toastHost"></div>
     <script src="https://cdn.jsdelivr.net/npm/@tabler/core@1.0.0/dist/js/tabler.min.js"></script>
-    <script src="/app.js" type="module"></script>
+    <script src="/app.js?v=20260708-tabs" type="module"></script>
   </body>
 </html>

+ 47 - 0
public/styles.css

@@ -647,6 +647,53 @@ body {
     #ffffff;
 }
 
+.detail-body {
+  gap: 0.9rem;
+}
+
+.detail-tabs {
+  background: #eef4fb;
+  border: 1px solid #d7e1ed;
+  border-radius: 8px;
+  display: flex;
+  gap: 0.35rem;
+  overflow-x: auto;
+  padding: 0.35rem;
+}
+
+.detail-tabs button {
+  align-items: center;
+  background: transparent;
+  border: 1px solid transparent;
+  border-radius: 7px;
+  color: #475569;
+  display: inline-flex;
+  flex: 0 0 auto;
+  font: inherit;
+  font-weight: 700;
+  gap: 0.4rem;
+  min-height: 38px;
+  padding: 0 0.75rem;
+  white-space: nowrap;
+}
+
+.detail-tabs button:hover {
+  background: rgba(255, 255, 255, 0.68);
+  color: var(--mh-ink);
+}
+
+.detail-tabs button.active {
+  background: #ffffff;
+  border-color: #cbd5e1;
+  box-shadow: 0 8px 22px rgba(15, 23, 42, 0.08);
+  color: var(--mh-blue);
+}
+
+.detail-tab-panel {
+  display: grid;
+  gap: 1rem;
+}
+
 .console-hero {
   align-items: stretch;
   background: