浏览代码

feat(admin): polish management UI

AI-Co-Authored-By: Codex
chendeben 1 月之前
父节点
当前提交
8fa980a0ef

+ 41 - 14
admin-web/src/app.tsx

@@ -3,24 +3,51 @@ import type { ReactNode } from 'react';
 
 const theme: ThemeConfig = {
   token: {
-    colorPrimary: '#168acd',
-    colorInfo: '#168acd',
-    colorSuccess: '#238636',
-    colorWarning: '#b7791f',
-    colorError: '#cf222e',
-    colorBgLayout: '#f5f7fa',
-    colorText: '#17212b',
-    colorTextSecondary: '#52606d',
-    borderRadius: 6,
+    colorPrimary: '#168fce',
+    colorInfo: '#168fce',
+    colorSuccess: '#168a5b',
+    colorWarning: '#b36b13',
+    colorError: '#d14343',
+    colorBgLayout: '#f3f7fb',
+    colorBgContainer: '#ffffff',
+    colorText: '#162131',
+    colorTextSecondary: '#5b6b7d',
+    colorBorder: '#d9e3ec',
+    colorBorderSecondary: '#e7edf3',
+    borderRadius: 10,
+    borderRadiusLG: 14,
+    controlHeight: 40,
+    controlHeightLG: 44,
+    boxShadowSecondary: '0 16px 40px rgba(34, 57, 79, 0.12)',
     fontFamily:
       '-apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif',
   },
   components: {
-    Layout: { siderBg: '#17212b', headerBg: '#ffffff' },
-    Table: { headerBg: '#f6f8fa', rowHoverBg: '#f0f7fb' },
-    Menu: { darkItemBg: '#17212b', darkItemSelectedBg: '#168acd' },
-    Button: { controlHeight: 36 },
-    Card: { borderRadiusLG: 6 },
+    Layout: { siderBg: '#111c2b', headerBg: '#ffffff' },
+    Table: {
+      headerBg: '#f6f9fc',
+      headerColor: '#3b4a5b',
+      rowHoverBg: '#f0f8fc',
+      borderColor: '#e7edf3',
+    },
+    Menu: {
+      darkItemBg: '#111c2b',
+      darkItemColor: '#9fb0c3',
+      darkItemHoverBg: 'rgba(43, 156, 216, 0.12)',
+      darkItemSelectedBg: 'rgba(43, 156, 216, 0.2)',
+      darkItemSelectedColor: '#ffffff',
+      itemBorderRadius: 10,
+      itemMarginInline: 10,
+    },
+    Button: {
+      controlHeight: 40,
+      borderRadius: 10,
+      fontWeight: 600,
+      primaryShadow: '0 8px 18px rgba(22, 143, 206, 0.2)',
+    },
+    Card: { borderRadiusLG: 14 },
+    Tabs: { inkBarColor: '#168fce', itemSelectedColor: '#167fb7' },
+    Select: { optionSelectedBg: '#e8f5fb' },
   },
 };
 

+ 27 - 6
admin-web/src/components/Page.tsx

@@ -2,15 +2,20 @@ import type { ReactNode } from 'react';
 
 export function PageHeader({
   title,
+  description,
   actions,
 }: {
   title: string;
+  description?: string;
   actions?: ReactNode;
 }) {
   return (
     <div className="page-toolbar">
-      <h1 className="page-title">{title}</h1>
-      {actions ? <div>{actions}</div> : null}
+      <div className="page-heading">
+        <h1 className="page-title">{title}</h1>
+        {description ? <p className="page-description">{description}</p> : null}
+      </div>
+      {actions ? <div className="page-actions">{actions}</div> : null}
     </div>
   );
 }
@@ -39,11 +44,27 @@ export function Surface({
   );
 }
 
-export function Metric({ label, value }: { label: string; value: number | string }) {
+export function Metric({
+  label,
+  value,
+  icon,
+  hint,
+  tone = 'default',
+}: {
+  label: string;
+  value: number | string;
+  icon?: ReactNode;
+  hint?: string;
+  tone?: 'default' | 'info' | 'success' | 'warning' | 'danger';
+}) {
   return (
-    <div className="metric">
-      <div className="metric-label">{label}</div>
-      <div className="metric-value">{value}</div>
+    <div className={`metric metric--${tone}`}>
+      <div className="metric-copy">
+        <div className="metric-label">{label}</div>
+        <div className="metric-value">{value}</div>
+        {hint ? <div className="metric-hint">{hint}</div> : null}
+      </div>
+      {icon ? <div className="metric-icon" aria-hidden="true">{icon}</div> : null}
     </div>
   );
 }

+ 374 - 40
admin-web/src/global.less

@@ -1,6 +1,20 @@
 :root {
   color-scheme: light;
   font-synthesis: none;
+  --color-brand: #168fce;
+  --color-brand-strong: #107ab2;
+  --color-brand-soft: #e8f5fb;
+  --color-ink: #162131;
+  --color-muted: #5b6b7d;
+  --color-canvas: #f3f7fb;
+  --color-surface: rgba(255, 255, 255, 0.96);
+  --color-border: #dce5ed;
+  --color-success: #168a5b;
+  --color-warning: #b36b13;
+  --color-danger: #d14343;
+  --shadow-surface: 0 1px 2px rgba(34, 57, 79, 0.04), 0 10px 30px rgba(34, 57, 79, 0.06);
+  --shadow-raised: 0 12px 28px rgba(22, 84, 118, 0.12);
+  --motion-standard: 180ms ease-out;
 }
 
 * {
@@ -18,8 +32,16 @@ body,
 body {
   min-width: 320px;
   overflow-x: hidden;
-  background: #f5f7fa;
-  color: #17212b;
+  background:
+    radial-gradient(circle at 88% 0%, rgba(85, 190, 230, 0.1), transparent 28rem),
+    var(--color-canvas);
+  color: var(--color-ink);
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
+  line-height: 1.5;
+}
+
+::selection {
+  background: rgba(22, 143, 206, 0.2);
 }
 
 button,
@@ -28,33 +50,63 @@ a,
   touch-action: manipulation;
 }
 
+button:focus-visible,
+a:focus-visible,
+[role='button']:focus-visible {
+  outline: 3px solid rgba(22, 143, 206, 0.28);
+  outline-offset: 2px;
+}
+
 .page-content {
   width: 100%;
-  max-width: 1600px;
+  max-width: 1440px;
   margin: 0 auto;
+  padding-bottom: 48px;
+  animation: page-enter 220ms ease-out both;
 }
 
 .page-toolbar {
   display: flex;
   flex-wrap: wrap;
-  align-items: center;
+  align-items: flex-start;
   justify-content: space-between;
-  gap: 12px;
-  margin-bottom: 16px;
+  gap: 16px;
+  margin-bottom: 22px;
+}
+
+.page-heading {
+  min-width: 0;
 }
 
 .page-title {
   margin: 0;
-  font-size: 22px;
-  line-height: 1.35;
-  font-weight: 650;
+  color: var(--color-ink);
+  font-size: 28px;
+  line-height: 1.25;
+  font-weight: 720;
+  letter-spacing: -0.02em;
+}
+
+.page-description {
+  max-width: 720px;
+  margin: 7px 0 0;
+  color: var(--color-muted);
+  font-size: 14px;
+}
+
+.page-actions {
+  display: flex;
+  min-height: 40px;
+  align-items: center;
 }
 
 .surface {
   overflow: hidden;
-  background: #fff;
-  border: 1px solid #d8dee4;
-  border-radius: 6px;
+  background: var(--color-surface);
+  border: 1px solid var(--color-border);
+  border-radius: 14px;
+  box-shadow: var(--shadow-surface);
+  transition: border-color var(--motion-standard), box-shadow var(--motion-standard);
 }
 
 .surface-header {
@@ -63,61 +115,129 @@ a,
   align-items: center;
   justify-content: space-between;
   gap: 12px;
-  min-height: 52px;
-  padding: 10px 16px;
-  border-bottom: 1px solid #eaeef2;
+  min-height: 58px;
+  padding: 12px 20px;
+  border-bottom: 1px solid #e7edf3;
 }
 
 .surface-title {
   margin: 0;
+  color: #26384a;
   font-size: 16px;
   line-height: 1.4;
-  font-weight: 600;
+  font-weight: 650;
 }
 
 .surface-body {
-  padding: 16px;
+  padding: 20px;
 }
 
-.metric-grid {
+.metric-grid,
+.metrics-grid {
   display: grid;
   grid-template-columns: repeat(4, minmax(0, 1fr));
-  gap: 12px;
-  margin-bottom: 16px;
+  gap: 16px;
+  margin-bottom: 20px;
 }
 
 .metric {
-  min-height: 108px;
-  padding: 16px;
-  background: #fff;
-  border: 1px solid #d8dee4;
-  border-radius: 6px;
+  --metric-accent: var(--color-brand);
+  position: relative;
+  display: flex;
+  min-width: 0;
+  min-height: 122px;
+  align-items: flex-start;
+  justify-content: space-between;
+  gap: 12px;
+  overflow: hidden;
+  padding: 20px;
+  background: linear-gradient(145deg, #ffffff 0%, #fbfdff 100%);
+  border: 1px solid var(--color-border);
+  border-radius: 14px;
+  box-shadow: var(--shadow-surface);
+  transition: transform var(--motion-standard), border-color var(--motion-standard), box-shadow var(--motion-standard);
+}
+
+.metric::before {
+  position: absolute;
+  inset: 0 auto 0 0;
+  width: 4px;
+  background: var(--metric-accent);
+  content: '';
+}
+
+.metric:hover {
+  border-color: rgba(22, 143, 206, 0.3);
+  box-shadow: var(--shadow-raised);
+  transform: translateY(-2px);
+}
+
+.metric--success {
+  --metric-accent: var(--color-success);
+}
+
+.metric--warning {
+  --metric-accent: var(--color-warning);
+}
+
+.metric--danger {
+  --metric-accent: var(--color-danger);
+}
+
+.metric--default {
+  --metric-accent: #8190a2;
+}
+
+.metric-copy {
+  min-width: 0;
 }
 
 .metric-label {
-  color: #52606d;
+  color: var(--color-muted);
   font-size: 14px;
+  font-weight: 520;
 }
 
 .metric-value {
-  margin-top: 12px;
-  font-size: 28px;
-  line-height: 1;
-  font-weight: 650;
+  margin-top: 10px;
+  overflow-wrap: anywhere;
+  color: var(--color-ink);
+  font-size: 30px;
+  line-height: 1.1;
+  font-weight: 720;
   font-variant-numeric: tabular-nums;
+  letter-spacing: -0.025em;
+}
+
+.metric-hint {
+  margin-top: 7px;
+  color: #7a8999;
+  font-size: 12px;
+}
+
+.metric-icon {
+  display: grid;
+  width: 42px;
+  height: 42px;
+  flex: 0 0 42px;
+  place-items: center;
+  color: var(--metric-accent);
+  background: color-mix(in srgb, var(--metric-accent) 11%, white);
+  border-radius: 12px;
 }
 
 .split-grid {
   display: grid;
   grid-template-columns: minmax(0, 1.35fr) minmax(320px, 0.65fr);
-  gap: 16px;
+  gap: 20px;
 }
 
 .rule-group {
   min-width: 0;
   padding: 14px 14px 0;
-  background: #f7f9fb;
-  border-left: 3px solid #1677ff;
+  background: #f6f9fc;
+  border-left: 3px solid var(--color-brand);
+  border-radius: 0 10px 10px 0;
 }
 
 .rule-group-title {
@@ -139,6 +259,7 @@ a,
 .table-wrap {
   width: 100%;
   overflow: hidden;
+  border-radius: 10px;
 }
 
 .workspace-tab-select {
@@ -150,11 +271,172 @@ a,
 }
 
 .ant-form-item-label > label {
-  font-weight: 500;
+  color: #33465a !important;
+  font-weight: 600;
+}
+
+.ant-form-item-extra,
+.ant-form-item-explain {
+  margin-top: 5px;
+  line-height: 1.45;
+}
+
+.ant-btn,
+.ant-input,
+.ant-input-number,
+.ant-input-affix-wrapper,
+.ant-select-selector {
+  transition: border-color var(--motion-standard), box-shadow var(--motion-standard), background var(--motion-standard) !important;
+}
+
+.ant-btn:not(.ant-btn-link):not(.ant-btn-text) {
+  box-shadow: 0 1px 2px rgba(34, 57, 79, 0.05);
+}
+
+.ant-btn-primary:not(:disabled) {
+  background: linear-gradient(135deg, #1c9bd8, #147fb9);
+}
+
+.ant-btn-primary:not(:disabled):hover {
+  background: linear-gradient(135deg, #168fce, #106fa4) !important;
+  box-shadow: 0 8px 18px rgba(22, 143, 206, 0.22);
+}
+
+.ant-input:hover,
+.ant-input-affix-wrapper:hover,
+.ant-input-number:hover,
+.ant-select:not(.ant-select-disabled):hover .ant-select-selector {
+  border-color: rgba(22, 143, 206, 0.56) !important;
+}
+
+.ant-table-wrapper .ant-table-container {
+  overflow: hidden;
+  border-radius: 10px;
+}
+
+.ant-table-wrapper .ant-table-thead > tr > th {
+  color: #3b4a5b;
+  font-size: 13px;
+  font-weight: 650;
+}
+
+.ant-table-wrapper .ant-table-tbody > tr > td {
+  border-bottom-color: #edf1f5;
+}
+
+.ant-tabs .ant-tabs-nav {
+  margin-bottom: 16px;
+}
+
+.ant-tabs .ant-tabs-tab {
+  min-height: 44px;
+  padding-inline: 4px;
+  font-weight: 550;
+}
+
+.ant-alert {
+  border-radius: 12px;
+}
+
+.ant-tag {
+  margin-inline-end: 6px;
+  padding-inline: 7px;
+  border-radius: 6px;
+  line-height: 22px;
+}
+
+.ant-descriptions-bordered .ant-descriptions-view {
+  overflow: hidden;
+  border-radius: 10px;
+}
+
+.ant-modal .ant-modal-content,
+.ant-drawer .ant-drawer-content {
+  border: 1px solid rgba(220, 229, 237, 0.8);
+  box-shadow: 0 24px 64px rgba(22, 33, 49, 0.18);
+}
+
+.admin-shell .ant-layout-header {
+  border-bottom: 1px solid rgba(220, 229, 237, 0.82);
+  box-shadow: 0 1px 12px rgba(34, 57, 79, 0.045);
+  backdrop-filter: blur(14px);
+}
+
+.admin-shell .ant-pro-sider {
+  border-right: 1px solid rgba(255, 255, 255, 0.04);
+  box-shadow: 8px 0 26px rgba(10, 22, 36, 0.1);
+}
+
+.admin-shell .ant-menu-dark {
+  background: transparent !important;
+}
+
+.admin-shell .ant-menu-item {
+  min-height: 44px;
+  margin-block: 4px;
+}
+
+.admin-shell .ant-pro-layout-content {
+  padding-block: 28px 48px;
+}
+
+.brand-mark {
+  display: grid;
+  width: 34px;
+  height: 34px;
+  place-items: center;
+  color: #ffffff;
+  background: linear-gradient(145deg, #32b5eb, #168fce);
+  border: 1px solid rgba(255, 255, 255, 0.24);
+  border-radius: 11px;
+  box-shadow: 0 8px 18px rgba(22, 143, 206, 0.25);
+}
+
+.nav-button {
+  min-height: 40px;
+}
+
+.bot-selector .ant-select-selector {
+  background: #f7fafc !important;
+  border-color: #d7e2eb !important;
+}
+
+.account-button {
+  color: #33465a;
+}
+
+.assistant-alert {
+  margin-bottom: 20px;
+}
+
+.assistant-section + .assistant-section,
+.assistant-tabs {
+  margin-top: 20px;
+}
+
+.assistant-tabs > .ant-tabs-nav {
+  padding-inline: 4px;
+}
+
+.assistant-settings .ant-form-item {
+  margin-bottom: 20px;
+}
+
+@keyframes page-enter {
+  from {
+    opacity: 0;
+    transform: translateY(5px);
+  }
+
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
 }
 
 @media (max-width: 1023px) {
-  .metric-grid {
+  .metric-grid,
+  .metrics-grid {
     grid-template-columns: repeat(2, minmax(0, 1fr));
   }
 
@@ -164,25 +446,70 @@ a,
 }
 
 @media (max-width: 575px) {
+  .page-content {
+    padding-bottom: 28px;
+  }
+
+  .page-toolbar {
+    margin-bottom: 18px;
+  }
+
   .page-title {
-    font-size: 20px;
+    font-size: 24px;
   }
 
-  .metric-grid {
-    grid-template-columns: 1fr;
+  .page-description {
+    margin-top: 5px;
+    font-size: 13px;
   }
 
   .metric {
-    min-height: 88px;
+    min-height: 110px;
+    padding: 15px;
+    border-radius: 12px;
+  }
+
+  .metric-value {
+    font-size: 25px;
+  }
+
+  .metric-icon {
+    width: 36px;
+    height: 36px;
+    flex-basis: 36px;
+    border-radius: 10px;
   }
 
   .surface-body,
   .surface-header {
-    padding: 12px;
+    padding-inline: 16px;
+  }
+
+  .surface,
+  .ant-alert {
+    border-radius: 12px;
   }
 
   .ant-pro-layout .ant-pro-layout-content {
     padding-inline: 12px !important;
+    padding-block: 22px 32px;
+  }
+
+  .ant-btn {
+    min-height: 44px;
+  }
+
+  .bot-selector {
+    width: 128px !important;
+  }
+
+  .account-name {
+    display: none;
+  }
+
+  .account-button {
+    width: 42px;
+    padding-inline: 10px;
   }
 
   .ant-modal {
@@ -201,6 +528,13 @@ a,
   }
 }
 
+@media (max-width: 359px) {
+  .metric-grid,
+  .metrics-grid {
+    grid-template-columns: 1fr;
+  }
+}
+
 @media (prefers-reduced-motion: reduce) {
   *,
   *::before,

+ 67 - 60
admin-web/src/layouts/AdminLayout.tsx

@@ -159,66 +159,73 @@ export default function AdminLayout() {
       value={{ bot: selectedProfile, hasPermission, hasAnyPermission }}
     >
       <ProLayout
-      title="Telegram 群管"
-      logo={<Bot size={24} color="#fff" aria-hidden="true" />}
-      location={{ pathname: location.pathname }}
-      menu={{ request: async () => visibleMenuItems }}
-      layout="mix"
-      fixedHeader
-      fixSiderbar
-      siderWidth={224}
-      breakpoint="lg"
-      token={{
-        header: { colorBgHeader: '#ffffff', colorTextMenu: '#17212b' },
-        sider: { colorMenuBackground: '#17212b' },
-        pageContainer: { paddingInlinePageContainerContent: 20 },
-      }}
-      menuItemRender={(item, dom) => (
-        <button
-          type="button"
-          onClick={() => item.path && history.push(item.path)}
-          style={{
-            width: '100%',
-            padding: 0,
-            color: 'inherit',
-            textAlign: 'left',
-            background: 'transparent',
-            border: 0,
-            cursor: 'pointer',
-          }}
-        >
-          {dom}
-        </button>
-      )}
-      avatarProps={{
-        src: undefined,
-        icon: <MessageSquareMore size={18} />,
-        title: (
-          <Dropdown menu={{ items: accountMenu }} trigger={['click']}>
-            <Button type="text" icon={<ChevronDown size={15} />} iconPosition="end">
-              {user.username}
-            </Button>
-          </Dropdown>
-        ),
-        render: (_, dom) => dom,
-      }}
-      actionsRender={() => [
-        <Select
-          key="bot-selector"
-          aria-label="当前机器人"
-          value={selectedBot || undefined}
-          placeholder="选择机器人"
-          style={{ width: 200, maxWidth: '34vw' }}
-          options={bots.map((item) => ({
-            value: item.bot_id,
-            label: item.identity?.username ? `@${item.identity.username}` : item.label,
-          }))}
-          onChange={(value) => {
-            setSelectedBot(value);
-            setSelectedBotId(value);
-          }}
-        />,
-      ]}
+        className="admin-shell"
+        title="Telegram 群管"
+        logo={(
+          <span className="brand-mark">
+            <Bot size={20} aria-hidden="true" />
+          </span>
+        )}
+        location={{ pathname: location.pathname }}
+        menu={{ request: async () => visibleMenuItems }}
+        layout="mix"
+        fixedHeader
+        fixSiderbar
+        siderWidth={224}
+        breakpoint="lg"
+        token={{
+          header: { colorBgHeader: 'rgba(255, 255, 255, 0.9)', colorTextMenu: '#17212b' },
+          sider: { colorMenuBackground: '#111c2b' },
+          pageContainer: { paddingInlinePageContainerContent: 24 },
+        }}
+        menuItemRender={(item, dom) => (
+          <button
+            type="button"
+            className="nav-button"
+            onClick={() => item.path && history.push(item.path)}
+            style={{
+              width: '100%',
+              padding: 0,
+              color: 'inherit',
+              textAlign: 'left',
+              background: 'transparent',
+              border: 0,
+              cursor: 'pointer',
+            }}
+          >
+            {dom}
+          </button>
+        )}
+        avatarProps={{
+          src: undefined,
+          icon: <MessageSquareMore size={18} />,
+          title: (
+            <Dropdown menu={{ items: accountMenu }} trigger={['click']}>
+              <Button className="account-button" type="text" icon={<ChevronDown size={15} />} iconPosition="end">
+                <span className="account-name">{user.username}</span>
+              </Button>
+            </Dropdown>
+          ),
+          render: (_, dom) => dom,
+        }}
+        actionsRender={() => [
+          <Select
+            key="bot-selector"
+            className="bot-selector"
+            aria-label="当前机器人"
+            value={selectedBot || undefined}
+            placeholder="选择机器人"
+            style={{ width: 200 }}
+            options={bots.map((item) => ({
+              value: item.bot_id,
+              label: item.identity?.username ? `@${item.identity.username}` : item.label,
+            }))}
+            onChange={(value) => {
+              setSelectedBot(value);
+              setSelectedBotId(value);
+            }}
+          />,
+        ]}
       >
         <main className="page-content" id="main-content">
           <Outlet />

+ 53 - 10
admin-web/src/pages/BusinessAssistant.tsx

@@ -23,17 +23,21 @@ import {
 import type { ColumnsType } from 'antd/es/table';
 import dayjs from 'dayjs';
 import {
+  Activity,
   BellRing,
   BotMessageSquare,
   CircleCheck,
   CirclePlus,
   FlaskConical,
+  Link2,
   Pause,
   Pencil,
   Play,
   RefreshCw,
   Save,
+  Sparkles,
   Trash2,
+  UserRoundCheck,
 } from 'lucide-react';
 import { useEffect, useMemo, useState } from 'react';
 
@@ -84,6 +88,30 @@ const conversationColors: Record<AssistantConversationStatus, string> = {
   closed: 'default',
 };
 
+const pollingStateLabels: Record<string, string> = {
+  starting: '启动中',
+  running: '运行中',
+  retrying: '重试中',
+  blocked: '已阻塞',
+  stopped: '已停止',
+  error: '异常',
+};
+
+function pollingStateLabel(state?: string): string {
+  if (!state) return '未知';
+  return pollingStateLabels[state] || state;
+}
+
+function pollingStateTone(
+  state?: string,
+): 'default' | 'info' | 'success' | 'warning' | 'danger' {
+  if (state === 'running') return 'success';
+  if (state === 'starting') return 'info';
+  if (state === 'retrying' || state === 'blocked') return 'warning';
+  if (state === 'error') return 'danger';
+  return 'default';
+}
+
 function connectionName(connection: AssistantConnection): string {
   const name = [connection.user.first_name, connection.user.last_name].filter(Boolean).join(' ');
   return name || (connection.user.username ? `@${connection.user.username}` : connection.user.id);
@@ -495,24 +523,37 @@ export default function BusinessAssistantPage() {
     <Spin spinning={loading}>
       <PageHeader
         title="智能接待"
+        description="集中管理 Telegram Business 连接、自动回复与人工接管"
         actions={<Button icon={<RefreshCw size={16} />} onClick={() => void reloadTopLevel()}>刷新状态</Button>}
       />
 
-      <div className="metrics-grid">
-        <Metric label="轮询状态" value={status?.polling_state || '未知'} />
-        <Metric label="已连接账号" value={connections.length} />
-        <Metric label="AI 调用" value={status?.usage.ai_calls || 0} />
-        <Metric label="待人工" value={status?.usage.handoffs || 0} />
+      <div className="metric-grid assistant-metrics">
+        <Metric
+          label="轮询状态"
+          value={pollingStateLabel(status?.polling_state)}
+          hint={status?.last_poll_at ? `最近轮询 ${dayjs(status.last_poll_at).format('HH:mm:ss')}` : '等待首次轮询'}
+          icon={<Activity size={21} />}
+          tone={pollingStateTone(status?.polling_state)}
+        />
+        <Metric label="已连接账号" value={connections.length} hint="Telegram Business" icon={<Link2 size={21} />} tone="info" />
+        <Metric label="AI 调用" value={status?.usage.ai_calls || 0} hint="今日累计" icon={<Sparkles size={21} />} />
+        <Metric
+          label="待人工"
+          value={status?.usage.handoffs || 0}
+          hint="等待运营处理"
+          icon={<UserRoundCheck size={21} />}
+          tone={status?.usage.handoffs ? 'warning' : 'default'}
+        />
       </div>
 
       {status?.last_error ? (
-        <Alert type="warning" showIcon message="智能接待尚未正常运行" description={status.last_error} style={{ marginBottom: 16 }} />
+        <Alert className="assistant-alert" type="warning" showIcon message="智能接待尚未正常运行" description={status.last_error} />
       ) : null}
       {status?.webhook_conflict ? (
-        <Alert type="error" showIcon message="检测到 Webhook 冲突" description={`当前 URL:${status.webhook_url}。系统不会自动删除外部 Webhook。`} style={{ marginBottom: 16 }} />
+        <Alert className="assistant-alert" type="error" showIcon message="检测到 Webhook 冲突" description={`当前 URL:${status.webhook_url}。系统不会自动删除外部 Webhook。`} />
       ) : null}
 
-      <Surface title="OpenAI 兼容模型">
+      <Surface title="OpenAI 兼容模型" className="assistant-section">
         <Form form={providerForm} layout="vertical" onFinish={saveProvider}>
           <Row gutter={16} align="bottom">
             <Col xs={24} lg={8}>
@@ -578,6 +619,7 @@ export default function BusinessAssistantPage() {
       <div style={{ height: 16 }} />
       <Surface
         title="Business 连接账号"
+        className="assistant-section"
         actions={connections.length ? (
           <Select
             aria-label="当前 Business 连接"
@@ -627,12 +669,13 @@ export default function BusinessAssistantPage() {
         <>
           <div style={{ height: 16 }} />
           <Tabs
+            className="assistant-tabs"
             items={[
               {
                 key: 'settings',
                 label: '接待设置',
                 children: (
-                  <Surface>
+                  <Surface className="assistant-section assistant-settings">
                     <Form form={settingsForm} layout="vertical" onFinish={saveSettings}>
                       <Row gutter={16}>
                         <Col xs={24} md={8}>
@@ -836,7 +879,7 @@ export default function BusinessAssistantPage() {
                       />
                     }
                   >
-                    <div className="metrics-grid" style={{ marginBottom: 16 }}>
+                    <div className="metric-grid" style={{ marginBottom: 16 }}>
                       <Metric label="客户" value={usage?.customers || 0} />
                       <Metric label="AI 调用" value={usage?.ai_calls || 0} />
                       <Metric label="待人工" value={usage?.handoffs || 0} />