Forráskód Böngészése

fix(setting): 重构授权信息弹框排版并补充演示数据

- 顶部状态卡展示授权状态、授权单位、有效期与剩余天数
- 客户 / 授权 / 设备信息分区展示,机器码支持一键复制
- 加载态改用骨架屏,无数据时提供完整空状态
- 开发环境接口无数据时展示演示数据,可切换正式 / 试用 / 过期
- 修复外层 Result 被误当成授权数据、导致模拟数据不生效的问题
cc 6 napja
szülő
commit
6cd12859ec

+ 689 - 124
ai-electron/frontend/src/core/layouts/default/setting/components/AuthorizationInfoModal.vue

@@ -1,135 +1,405 @@
 <template>
   <BasicModal
     v-bind="$attrs"
-    title="授权信息"
     :width="980"
     :centered="true"
     :showCancelBtn="false"
     @register="registerModal"
     @ok="closeModal"
   >
+    <template #title>
+      <div class="authorization-info__title">
+        <span class="authorization-info__title-icon">
+          <SafetyCertificateOutlined />
+        </span>
+        <span>授权信息</span>
+        <Tooltip v-if="usingMock" title="开发环境未获取到真实授权数据,当前展示演示数据">
+          <span class="authorization-info__mock-chip">演示数据</span>
+        </Tooltip>
+      </div>
+    </template>
+
     <div :class="prefixCls">
-      <div v-if="loading" class="authorization-info__loading">正在加载授权信息...</div>
+      <div v-if="loading" class="authorization-info__loading">
+        <Skeleton active :paragraph="{ rows: 8 }" />
+      </div>
+
+      <template v-else-if="hasDisplayData">
+        <section class="authorization-info__hero" :class="statusClass">
+          <div class="authorization-info__hero-main">
+            <span class="authorization-info__hero-icon">
+              <SafetyCertificateOutlined />
+            </span>
+            <div class="authorization-info__hero-content">
+              <div class="authorization-info__hero-title">
+                <span class="authorization-info__status">{{ statusText }}</span>
+                <span class="authorization-info__status-hint">{{ statusHint }}</span>
+              </div>
+              <div class="authorization-info__hero-subject">{{ unit }}</div>
+              <div class="authorization-info__hero-meta">
+                <CalendarOutlined />
+                <span>授权有效期:{{ expiryText }}</span>
+              </div>
+            </div>
+          </div>
+          <div class="authorization-info__hero-stat">
+            <span class="authorization-info__hero-stat-value" :class="{ 'is-text': isRemainingText }">
+              {{ remainingValue }}
+            </span>
+            <span class="authorization-info__hero-stat-label">{{ remainingLabel }}</span>
+          </div>
+        </section>
 
-      <div v-else-if="infoItems.length">
-        <div v-if="isAuthorizationExpiredState" class="authorization-info__expired-banner">
-          {{ authorizationExpiredMessage }}
+        <div v-if="usingMock" class="authorization-info__mock-switch">
+          <span class="authorization-info__mock-switch-label">演示状态</span>
+          <button
+            v-for="option in MOCK_STATUS_OPTIONS"
+            :key="option.value"
+            type="button"
+            class="authorization-info__mock-switch-item"
+            :class="{ 'is-active': mockStatus === option.value }"
+            @click="setMockStatus(option.value)"
+          >
+            {{ option.label }}
+          </button>
         </div>
-        <div v-else-if="isTrialAuthorization" class="authorization-info__trial-banner">试用授权</div>
-        <div class="authorization-info__list">
-          <div v-for="item in infoItems" :key="item.key" class="authorization-info__item">
-            <div class="authorization-info__label">{{ item.label }}</div>
-            <div
-              class="authorization-info__value"
-              :class="{
-                'is-trial-value': item.key === 'overDateTime' && isTrialAuthorization,
-                'is-expired-value': item.key === 'overDateTime' && isAuthorizationExpiredState,
-              }"
-            >
-              {{ item.value }}
+
+        <div class="authorization-info__columns">
+          <section class="authorization-info__section">
+            <header class="authorization-info__section-head">
+              <TeamOutlined />
+              <span>客户信息</span>
+            </header>
+            <div class="authorization-info__fields">
+              <div class="authorization-info__field">
+                <span class="authorization-info__field-label">客户编号</span>
+                <span class="authorization-info__field-value">{{ customerNo }}</span>
+              </div>
+              <div class="authorization-info__field">
+                <span class="authorization-info__field-label">所属单位</span>
+                <span class="authorization-info__field-value">{{ unit }}</span>
+              </div>
+              <div class="authorization-info__field">
+                <span class="authorization-info__field-label">联系电话</span>
+                <span class="authorization-info__field-value">{{ phone }}</span>
+              </div>
+            </div>
+          </section>
+
+          <section class="authorization-info__section">
+            <header class="authorization-info__section-head">
+              <CalendarOutlined />
+              <span>授权信息</span>
+            </header>
+            <div class="authorization-info__fields">
+              <div class="authorization-info__field">
+                <span class="authorization-info__field-label">授权类型</span>
+                <span class="authorization-info__field-value">{{ statusText }}</span>
+              </div>
+              <div class="authorization-info__field">
+                <span class="authorization-info__field-label">激活时间</span>
+                <span class="authorization-info__field-value">{{ activeDateTimeText }}</span>
+              </div>
+              <div class="authorization-info__field">
+                <span class="authorization-info__field-label">授权时间</span>
+                <span class="authorization-info__field-value">{{ authDateTimeText }}</span>
+              </div>
+              <div class="authorization-info__field">
+                <span class="authorization-info__field-label">授权到期</span>
+                <span class="authorization-info__field-value">{{ expiryText }}</span>
+              </div>
+            </div>
+          </section>
+        </div>
+
+        <section class="authorization-info__section authorization-info__section--device">
+          <header class="authorization-info__section-head">
+            <DesktopOutlined />
+            <span>设备信息</span>
+          </header>
+          <div class="authorization-info__machine">
+            <div class="authorization-info__machine-main">
+              <span class="authorization-info__field-label">机器码</span>
+              <span class="authorization-info__machine-code" :title="machineCode">{{ machineCode }}</span>
             </div>
+            <Button size="small" class="authorization-info__copy-btn" @click="copyMachineCode">
+              <CopyOutlined />
+              复制
+            </Button>
           </div>
+        </section>
+
+        <div v-if="messageText" class="authorization-info__note">
+          <InfoCircleOutlined class="authorization-info__note-icon" />
+          <span>{{ messageText }}</span>
         </div>
-      </div>
+      </template>
 
-      <div v-else class="authorization-info__empty">暂无授权信息</div>
+      <div v-else class="authorization-info__empty">
+        <SafetyCertificateOutlined class="authorization-info__empty-icon" />
+        <div class="authorization-info__empty-title">暂无授权信息</div>
+        <div class="authorization-info__empty-desc">请联系管理员确认授权状态,或重新导入授权文件。</div>
+      </div>
     </div>
   </BasicModal>
 </template>
 
 <script lang="ts" setup>
-  import { computed, ref, watch, unref } from 'vue';
+  import { computed, ref, unref, watch } from 'vue';
+  import { Button, Skeleton, Tooltip } from 'ant-design-vue';
+  import {
+    CalendarOutlined,
+    CopyOutlined,
+    DesktopOutlined,
+    InfoCircleOutlined,
+    SafetyCertificateOutlined,
+    TeamOutlined,
+  } from '@ant-design/icons-vue';
   import { BasicModal, useModalInner } from '@/core/components/Modal';
   import { useDesign } from '@/core/hooks/web/useDesign';
   import { useMessage } from '@/core/hooks/web/useMessage';
   import { authorizationInfoApi } from '@/core/api/sys/login';
-  import {
-    extractAuthorizationCache,
-    getAuthorizationExpiredMessage,
-    isAuthorizationExpired,
-    isFormalAuthorization,
-  } from '@/core/utils/license';
+  import { extractAuthorizationCache, isAuthorizationExpired, isFormalAuthorization } from '@/core/utils/license';
   import { useUserStore } from '@/core/store/modules/case';
 
   const { prefixCls } = useDesign('authorization-info-modal');
   const { createMessage } = useMessage();
   const userStore = useUserStore();
+  const isDev = import.meta.env.DEV;
+
+  type MockStatus = 'formal' | 'trial' | 'expired';
+
+  const MOCK_STATUS_OPTIONS: Array<{ label: string; value: MockStatus }> = [
+    { label: '正式授权', value: 'formal' },
+    { label: '试用授权', value: 'trial' },
+    { label: '已过期', value: 'expired' },
+  ];
+
+  const MOCK_BASE = {
+    clientNo: 'QJ-2026-0086',
+    unit: '中数矩阵(成都)科技有限公司',
+    phone: '18908093240',
+    machinecode: 'A9DEBA7C48078E4B78A79A8609EEA5ABA96FAA137E3A720F4E69B01FEF090164',
+    activeDateTime: '2026-03-01 09:00:00',
+    authdatetime: '2026-03-01',
+  };
+
+  /** 开发环境演示数据:接口无真实授权信息时用来看排版与三种授权状态 */
+  const MOCK_AUTHORIZATION_INFO: Record<MockStatus, Record<string, any>> = {
+    formal: {
+      ...MOCK_BASE,
+      overdatetime: '2099-09-09',
+      message: '正式授权,长期有效。',
+    },
+    trial: {
+      ...MOCK_BASE,
+      overdatetime: '2027-03-01',
+      remainingDate: 126,
+      message: '试用授权,到期前请及时导入正式授权文件。',
+    },
+    expired: {
+      ...MOCK_BASE,
+      overdatetime: '2026-05-01',
+      remainingDate: 0,
+      message: '授权已于 2026-05-01 过期,请重新导入有效授权文件。',
+    },
+  };
 
   const loading = ref(false);
   const info = ref<Record<string, any> | null>(null);
+  const usingMock = ref(false);
+  const mockStatus = ref<MockStatus>('formal');
 
   function normalizeFieldKey(value: string) {
     return value.replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
   }
 
-  const overDateTimeValue = computed(() => {
-    const source = info.value || {};
-    const matchedKey = ['overDateTime', 'overdatetime'].find((key) =>
-      Object.prototype.hasOwnProperty.call(source, key),
+  function pickField(source: Record<string, any>, keys: string[]) {
+    if (!source) return undefined;
+    const matchedKey = keys.find((key) => Object.prototype.hasOwnProperty.call(source, key));
+    if (matchedKey) return source[matchedKey];
+    const normalizedKey = Object.keys(source).find((key) =>
+      keys.some((item) => normalizeFieldKey(item) === normalizeFieldKey(key)),
     );
-    const matchedNormalizedKey = Object.keys(source).find((key) =>
-      ['overDateTime', 'overdatetime'].some((item) => normalizeFieldKey(item) === normalizeFieldKey(key)),
-    );
-    const value = matchedKey ? source[matchedKey] : matchedNormalizedKey ? source[matchedNormalizedKey] : undefined;
-    return value === null || value === undefined ? '' : `${value}`.trim();
+    return normalizedKey ? source[normalizedKey] : undefined;
+  }
+
+  function formatDate(value?: unknown) {
+    const text = String(value ?? '').trim();
+    if (!text) return '—';
+    return text.replace('T', ' ').split(' ')[0];
+  }
+
+  function formatDateTime(value?: unknown) {
+    const text = String(value ?? '').trim();
+    if (!text) return '—';
+    return text.replace('T', ' ').replace(/\.\d+$/, '');
+  }
+
+  function calcRemainingDays(overDateTime: string) {
+    if (!overDateTime) return null;
+    const end = new Date(overDateTime.replace(/-/g, '/'));
+    if (Number.isNaN(end.getTime())) return null;
+    const now = new Date();
+    const endDay = new Date(end.getFullYear(), end.getMonth(), end.getDate());
+    const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
+    return Math.ceil((endDay.getTime() - today.getTime()) / 86400000);
+  }
+
+  const activeInfo = computed<Record<string, any>>(() => {
+    if (usingMock.value) return MOCK_AUTHORIZATION_INFO[mockStatus.value];
+    return info.value || {};
+  });
+
+  const hasDisplayData = computed(() => usingMock.value || Object.keys(activeInfo.value).length > 0);
+
+  const overDateTimeValue = computed(() => {
+    const value = pickField(activeInfo.value, ['overDateTime', 'overdatetime']);
+    return value === null || value === undefined ? '' : String(value).trim();
   });
 
-  const isTrialAuthorization = computed(
-    () => !!overDateTimeValue.value && !isFormalAuthorization(overDateTimeValue.value),
+  const isExpired = computed(() => isAuthorizationExpired(overDateTimeValue.value));
+  const isFormal = computed(() => isFormalAuthorization(overDateTimeValue.value));
+  const isTrial = computed(() => !!overDateTimeValue.value && !isFormal.value);
+
+  const statusClass = computed(() => {
+    if (isExpired.value) return 'is-expired';
+    if (isTrial.value) return 'is-trial';
+    if (isFormal.value) return 'is-formal';
+    return 'is-unknown';
+  });
+
+  const statusText = computed(() => {
+    if (isExpired.value) return '已过期';
+    if (isTrial.value) return '试用授权';
+    if (isFormal.value) return '正式授权';
+    return '授权信息';
+  });
+
+  const statusHint = computed(() => {
+    if (isExpired.value) {
+      return overDateTimeValue.value ? `已于 ${formatDate(overDateTimeValue.value)} 过期` : '授权已过期';
+    }
+    if (isTrial.value) {
+      return `试用期至 ${formatDate(overDateTimeValue.value)}`;
+    }
+    if (isFormal.value) {
+      return '长期有效,无需续期';
+    }
+    return '未获取到授权有效期';
+  });
+
+  const remainingDays = computed(() => {
+    if (isFormal.value) return null;
+    const raw = pickField(activeInfo.value, ['remainingDate', 'remainingdate']);
+    const numeric = Number(raw);
+    if (raw !== null && raw !== undefined && `${raw}`.trim() !== '' && Number.isFinite(numeric)) {
+      return Math.max(0, Math.trunc(numeric));
+    }
+    const calculated = calcRemainingDays(overDateTimeValue.value);
+    return calculated === null ? null : Math.max(0, calculated);
+  });
+
+  const remainingValue = computed(() => {
+    if (isFormal.value) return '长期';
+    if (isExpired.value) return '已过期';
+    return remainingDays.value === null ? '—' : String(remainingDays.value);
+  });
+
+  const remainingLabel = computed(() => {
+    if (isFormal.value) return '授权期限';
+    if (isExpired.value) return '当前状态';
+    return '剩余天数';
+  });
+
+  const isRemainingText = computed(() => isFormal.value || isExpired.value || remainingDays.value === null);
+
+  const expiryText = computed(() => (isFormal.value ? '长期有效' : formatDate(overDateTimeValue.value)));
+
+  const unit = computed(() => {
+    const value = pickField(activeInfo.value, ['unit']);
+    return String(value ?? '').trim() || '未设置授权单位';
+  });
+
+  const customerNo = computed(() => {
+    const value = pickField(activeInfo.value, ['clientNo', 'clientno']);
+    return String(value ?? '').trim() || '—';
+  });
+
+  const phone = computed(() => {
+    const value = pickField(activeInfo.value, ['phone']);
+    return String(value ?? '').trim() || '—';
+  });
+
+  const machineCode = computed(() => {
+    const value = pickField(activeInfo.value, ['machinecode', 'machineCode']);
+    return String(value ?? '').trim() || '—';
+  });
+
+  const activeDateTimeText = computed(() =>
+    formatDateTime(pickField(activeInfo.value, ['activeDateTime', 'activateDateTime', 'activatetime'])),
   );
-  const isAuthorizationExpiredState = computed(() => isAuthorizationExpired(overDateTimeValue.value));
-  const authorizationExpiredMessage = computed(() => getAuthorizationExpiredMessage(overDateTimeValue.value));
-
-  const infoItems = computed(() => {
-    const source = info.value || {};
-    const fieldList = [
-      { keys: ['clientNo'], label: '客户名称' },
-      { keys: ['unit'], label: '所属单位' },
-      { keys: ['phone'], label: '联系电话' },
-      { keys: ['activeDateTime', 'activateDateTime', 'activatetime'], label: '激活时间' },
-      { keys: ['authDateTime', 'authdatetime'], label: '授权时间' },
-      {
-        keys: ['overDateTime', 'overdatetime'],
-        label: isTrialAuthorization.value ? '试用结束时间' : '授权结束时间',
-      },
-      { keys: ['remainingDate', 'remainingdate'], label: '剩余天数' },
-      { keys: ['message'], label: '说明' },
-    ];
-
-    return fieldList
-      .map(({ keys, label }) => {
-        const matchedKey = keys.find((key) => Object.prototype.hasOwnProperty.call(source, key));
-        const matchedNormalizedKey = Object.keys(source).find((key) =>
-          keys.some((item) => normalizeFieldKey(item) === normalizeFieldKey(key)),
-        );
-        const value = matchedKey ? source[matchedKey] : matchedNormalizedKey ? source[matchedNormalizedKey] : undefined;
-        if (value === null || value === undefined || `${value}` === '') return null;
-        return {
-          key: keys[0],
-          label,
-          value: `${value}`,
-        };
-      })
-      .filter(Boolean) as Array<{ key: string; label: string; value: string }>;
+
+  const authDateTimeText = computed(() => formatDate(pickField(activeInfo.value, ['authDateTime', 'authdatetime'])));
+
+  const messageText = computed(() => {
+    const value = pickField(activeInfo.value, ['message']);
+    return String(value ?? '').trim();
   });
 
   const [registerModal, { closeModal, setModalProps, getOpen }] = useModalInner();
 
+  function setMockStatus(status: MockStatus) {
+    mockStatus.value = status;
+  }
+
+  async function copyMachineCode() {
+    if (!machineCode.value || machineCode.value === '—') return;
+    try {
+      await navigator.clipboard.writeText(machineCode.value);
+      createMessage.success('机器码已复制');
+    } catch {
+      createMessage.error('复制失败,请手动选择复制');
+    }
+  }
+
   async function loadAuthorizationInfo() {
     loading.value = true;
     info.value = null;
+    usingMock.value = false;
     setModalProps({ confirmLoading: false });
     try {
       const res = await authorizationInfoApi();
       const raw = (res as Record<string, any> | null) || null;
       const payload =
-        raw && typeof raw === 'object' && 'data' in raw && raw.data && typeof raw.data === 'object' ? raw.data : raw;
-      info.value = (payload as Record<string, any> | null) || null;
+        raw && typeof raw === 'object' && 'data' in raw
+          ? raw.data && typeof raw.data === 'object'
+            ? raw.data
+            : null
+          : raw;
+      const hasData =
+        !!payload &&
+        Object.entries(payload).some(([, value]) => value !== null && value !== undefined && `${value}`.trim() !== '');
+
+      if (!hasData) {
+        if (isDev) {
+          usingMock.value = true;
+          mockStatus.value = 'formal';
+        }
+        return;
+      }
+
+      info.value = payload;
       const { authorizationLabel, overDateTime, authorizationUnit } = extractAuthorizationCache(payload);
       userStore.setPageCache('authorizationLabel', authorizationLabel);
       userStore.setPageCache('licenseOverDateTime', overDateTime);
       userStore.setPageCache('authorizationUnit', authorizationUnit);
     } catch (error: any) {
+      if (isDev) {
+        usingMock.value = true;
+        mockStatus.value = 'formal';
+        return;
+      }
       createMessage.error(error?.message || '获取授权信息失败,请稍后重试');
     } finally {
       loading.value = false;
@@ -150,88 +420,383 @@
   @prefix-cls: ~'jeesite-authorization-info-modal';
 
   .@{prefix-cls} {
-    padding-right: 8px;
-    min-height: 520px;
     display: flex;
     flex-direction: column;
+    gap: 16px;
+    padding-right: 4px;
 
-    .authorization-info__loading,
-    .authorization-info__empty {
-      min-height: 420px;
-      padding: 46px 0;
+    .authorization-info__title {
+      display: inline-flex;
+      align-items: center;
+      gap: 10px;
+      color: #111827;
       font-size: 16px;
-      color: #6b7280;
-      text-align: center;
+      font-weight: 600;
     }
 
-    .authorization-info__list {
-      display: grid;
-      grid-template-columns: repeat(2, minmax(0, 1fr));
+    .authorization-info__title-icon {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 28px;
+      height: 28px;
+      color: #fff;
+      font-size: 16px;
+      background: #2a50ec;
+      border-radius: 9px;
+    }
+
+    .authorization-info__mock-chip {
+      padding: 0 8px;
+      color: #b45309;
+      font-size: 11px;
+      font-weight: 500;
+      line-height: 20px;
+      background: #fff7ed;
+      border: 1px solid #fed7aa;
+      border-radius: 10px;
+      cursor: default;
+    }
+
+    .authorization-info__loading {
+      padding: 8px 4px;
+    }
+
+    .authorization-info__hero {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      gap: 24px;
+      padding: 20px 22px;
+      background: #f4f8ff;
+      border: 1px solid #dbe7ff;
+      border-radius: 16px;
+
+      &.is-trial {
+        background: #fff8ef;
+        border-color: #fde3c8;
+
+        .authorization-info__hero-icon {
+          color: #d97706;
+        }
+
+        .authorization-info__status {
+          color: #b45309;
+        }
+
+        .authorization-info__hero-stat-value {
+          color: #d97706;
+        }
+      }
+
+      &.is-expired {
+        background: #fef2f2;
+        border-color: #fecaca;
+
+        .authorization-info__hero-icon {
+          color: #dc2626;
+        }
+
+        .authorization-info__status {
+          color: #b91c1c;
+        }
+
+        .authorization-info__hero-stat-value {
+          color: #dc2626;
+        }
+      }
+
+      &.is-unknown {
+        background: #f8fafc;
+        border-color: #e5e7eb;
+
+        .authorization-info__hero-icon {
+          color: #6b7280;
+        }
+
+        .authorization-info__status {
+          color: #4b5563;
+        }
+
+        .authorization-info__hero-stat-value {
+          color: #6b7280;
+        }
+      }
+    }
+
+    .authorization-info__hero-main {
+      display: flex;
+      align-items: center;
       gap: 16px;
-      align-content: start;
+      min-width: 0;
     }
 
-    .authorization-info__trial-banner {
-      margin-bottom: 14px;
-      font-size: 28px;
+    .authorization-info__hero-icon {
+      display: inline-flex;
+      flex: 0 0 auto;
+      align-items: center;
+      justify-content: center;
+      width: 52px;
+      height: 52px;
+      color: #2a50ec;
+      font-size: 26px;
+      background: #fff;
+      border-radius: 14px;
+      box-shadow: 0 4px 12px rgb(15 23 42 / 6%);
+    }
+
+    .authorization-info__hero-content {
+      min-width: 0;
+    }
+
+    .authorization-info__hero-title {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      gap: 10px;
+    }
+
+    .authorization-info__status {
+      color: #1d4ed8;
+      font-size: 16px;
       font-weight: 700;
-      line-height: 1.2;
-      color: #ef4444;
-      text-align: center;
-      letter-spacing: 2px;
     }
 
-    .authorization-info__expired-banner {
-      padding: 14px 16px;
-      margin-bottom: 14px;
-      font-size: 18px;
+    .authorization-info__status-hint {
+      color: #6b7280;
+      font-size: 12px;
+    }
+
+    .authorization-info__hero-subject {
+      margin-top: 4px;
+      overflow: hidden;
+      color: #111827;
+      font-size: 20px;
+      font-weight: 600;
+      line-height: 1.4;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .authorization-info__hero-meta {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+      margin-top: 6px;
+      color: #6b7280;
+      font-size: 12px;
+
+      .anticon {
+        color: #9ca3af;
+      }
+    }
+
+    .authorization-info__hero-stat {
+      display: flex;
+      flex: 0 0 auto;
+      flex-direction: column;
+      align-items: flex-end;
+      gap: 2px;
+    }
+
+    .authorization-info__hero-stat-value {
+      color: #2a50ec;
+      font-size: 40px;
       font-weight: 700;
-      line-height: 1.6;
-      color: #b91c1c;
-      text-align: center;
-      background: #fef2f2;
-      border: 1px solid #fecaca;
-      border-radius: 12px;
+      line-height: 1.1;
+      font-variant-numeric: tabular-nums;
+
+      &.is-text {
+        font-size: 26px;
+      }
     }
 
-    .authorization-info__item {
-      padding: 15px;
-      background: #f8fafc;
-      border: 1px solid #e5e7eb;
+    .authorization-info__hero-stat-label {
+      color: #6b7280;
+      font-size: 12px;
+    }
+
+    .authorization-info__mock-switch {
+      display: flex;
+      align-items: center;
+      flex-wrap: wrap;
+      gap: 8px;
+      padding: 8px 12px;
+      color: #92400e;
+      font-size: 12px;
+      background: #fffbeb;
+      border: 1px solid #fde68a;
       border-radius: 10px;
     }
 
-    .authorization-info__label {
+    .authorization-info__mock-switch-label {
+      font-weight: 600;
+    }
+
+    .authorization-info__mock-switch-item {
+      padding: 2px 10px;
+      color: #92400e;
+      font-size: 12px;
+      line-height: 20px;
+      background: #fff;
+      border: 1px solid #fcd34d;
+      border-radius: 999px;
+      cursor: pointer;
+
+      &.is-active {
+        color: #fff;
+        background: #d97706;
+        border-color: #d97706;
+      }
+    }
+
+    .authorization-info__columns {
+      display: grid;
+      grid-template-columns: repeat(2, minmax(0, 1fr));
+      gap: 16px;
+    }
+
+    .authorization-info__section {
+      padding: 16px;
+      background: #fff;
+      border: 1px solid #e7edf6;
+      border-radius: 14px;
+    }
+
+    .authorization-info__section-head {
+      display: flex;
+      align-items: center;
+      gap: 8px;
       margin-bottom: 10px;
-      font-size: 18px;
+      color: #111827;
+      font-size: 14px;
       font-weight: 600;
+
+      .anticon {
+        color: #2a50ec;
+        font-size: 15px;
+      }
+    }
+
+    .authorization-info__field {
+      display: flex;
+      gap: 12px;
+      padding: 10px 0;
+      border-bottom: 1px dashed #edf1f7;
+
+      &:last-child {
+        border-bottom: 0;
+      }
+    }
+
+    .authorization-info__field-label {
+      flex: 0 0 72px;
+      color: #8a94a6;
+      font-size: 12px;
+      line-height: 20px;
+    }
+
+    .authorization-info__field-value {
+      flex: 1 1 auto;
+      min-width: 0;
       color: #111827;
+      font-size: 13px;
+      line-height: 20px;
+      word-break: break-all;
+    }
+
+    .authorization-info__machine {
+      display: flex;
+      align-items: flex-end;
+      gap: 16px;
+    }
+
+    .authorization-info__machine-main {
+      flex: 1 1 auto;
+      min-width: 0;
     }
 
-    .authorization-info__value {
-      font-size: 17px;
-      line-height: 1.8;
-      color: #4b5563;
+    .authorization-info__machine .authorization-info__field-label {
+      display: block;
+      margin-bottom: 6px;
+    }
+
+    .authorization-info__machine-code {
+      display: block;
+      padding: 8px 10px;
+      color: #334155;
+      font-family: Consolas, Monaco, 'Courier New', monospace;
+      font-size: 12px;
+      line-height: 18px;
       word-break: break-all;
-      white-space: pre-wrap;
+      background: #f8fafc;
+      border: 1px solid #e5e7eb;
+      border-radius: 8px;
+    }
 
-      &.is-trial-value {
-        font-size: 24px;
-        font-weight: 700;
-        color: #ef4444;
-      }
+    .authorization-info__copy-btn {
+      flex: 0 0 auto;
+      margin-bottom: 2px;
+    }
 
-      &.is-expired-value {
-        font-size: 24px;
-        font-weight: 700;
-        color: #b91c1c;
-      }
+    .authorization-info__note {
+      display: flex;
+      align-items: flex-start;
+      gap: 8px;
+      padding: 12px 14px;
+      color: #475569;
+      font-size: 12.5px;
+      line-height: 1.7;
+      background: #f8fbff;
+      border: 1px solid #dbe7ff;
+      border-radius: 12px;
+    }
+
+    .authorization-info__note-icon {
+      margin-top: 3px;
+      color: #2a50ec;
+    }
+
+    .authorization-info__empty {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      min-height: 320px;
+      text-align: center;
+    }
+
+    .authorization-info__empty-icon {
+      margin-bottom: 14px;
+      color: #cbd5e1;
+      font-size: 42px;
+    }
+
+    .authorization-info__empty-title {
+      color: #374151;
+      font-size: 16px;
+      font-weight: 600;
+    }
+
+    .authorization-info__empty-desc {
+      margin-top: 6px;
+      color: #9ca3af;
+      font-size: 12px;
     }
   }
 
   @media screen and (max-width: 767px) {
     .@{prefix-cls} {
-      .authorization-info__list {
+      .authorization-info__hero {
+        align-items: flex-start;
+        flex-direction: column;
+      }
+
+      .authorization-info__hero-stat {
+        align-items: flex-start;
+      }
+
+      .authorization-info__columns {
         grid-template-columns: 1fr;
       }
     }