Explorar el Código

chore: refine codex usage modal layout (#4386)

* chore: refine codex usage modal layout

* fix: polish codex usage modal responsiveness
Seefs hace 2 semanas
padre
commit
d586a567e4
Se han modificado 1 ficheros con 178 adiciones y 19 borrados
  1. 178 19
      web/src/components/table/channels/modals/CodexUsageModal.jsx

+ 178 - 19
web/src/components/table/channels/modals/CodexUsageModal.jsx

@@ -29,6 +29,7 @@ import {
   Collapse,
 } from '@douyinfe/semi-ui';
 import { API, showError } from '../../../../helpers';
+import { MOBILE_BREAKPOINT } from '../../../../hooks/common/useIsMobile';
 
 const { Text } = Typography;
 
@@ -98,10 +99,12 @@ const resolveRateLimitWindows = (data) => {
   }
 
   if (!fiveHourWindow) {
-    fiveHourWindow = windows.find((windowData) => windowData !== weeklyWindow) ?? null;
+    fiveHourWindow =
+      windows.find((windowData) => windowData !== weeklyWindow) ?? null;
   }
   if (!weeklyWindow) {
-    weeklyWindow = windows.find((windowData) => windowData !== fiveHourWindow) ?? null;
+    weeklyWindow =
+      windows.find((windowData) => windowData !== fiveHourWindow) ?? null;
   }
 
   return { fiveHourWindow, weeklyWindow };
@@ -135,6 +138,40 @@ const getDisplayText = (value) => {
   return String(value).trim();
 };
 
+const isMobileViewport = () =>
+  typeof window !== 'undefined' && window.innerWidth < MOBILE_BREAKPOINT;
+
+const getCodexUsageModalLayout = () => {
+  if (isMobileViewport()) {
+    return {
+      width: 'calc(100vw - 16px)',
+      style: {
+        top: 8,
+        maxWidth: 'calc(100vw - 16px)',
+        margin: '0 auto',
+      },
+      bodyStyle: {
+        maxHeight: 'calc(100vh - 148px)',
+        overflowY: 'auto',
+        padding: '16px 16px 12px',
+      },
+    };
+  }
+
+  return {
+    width: 900,
+    style: {
+      top: 24,
+      maxWidth: 'min(900px, 92vw)',
+    },
+    bodyStyle: {
+      maxHeight: 'calc(100vh - 172px)',
+      overflowY: 'auto',
+      padding: '20px 24px 16px',
+    },
+  };
+};
+
 const formatAccountTypeLabel = (value, t) => {
   const tt = typeof t === 'function' ? t : (v) => v;
   const normalized = normalizePlanType(value);
@@ -224,7 +261,7 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
 
   return (
     <div className='rounded-lg border border-semi-color-border bg-semi-color-bg-0 p-3'>
-      <div className='flex items-center justify-between gap-2'>
+      <div className='flex flex-wrap items-start justify-between gap-x-3 gap-y-1'>
         <div className='font-medium'>{title}</div>
         <Text type='tertiary' size='small'>
           {tt('重置时间:')}
@@ -262,12 +299,86 @@ const RateLimitWindowCard = ({ t, title, windowData }) => {
   );
 };
 
+const RateLimitWindowGrid = ({ t, fiveHourWindow, weeklyWindow }) => {
+  const tt = typeof t === 'function' ? t : (v) => v;
+
+  return (
+    <div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
+      <RateLimitWindowCard
+        t={tt}
+        title={tt('5小时窗口')}
+        windowData={fiveHourWindow}
+      />
+      <RateLimitWindowCard
+        t={tt}
+        title={tt('每周窗口')}
+        windowData={weeklyWindow}
+      />
+    </div>
+  );
+};
+
+const RateLimitGroupSection = ({
+  t,
+  title,
+  description,
+  rateLimitSource,
+  statusTag,
+  meteredFeature,
+}) => {
+  const tt = typeof t === 'function' ? t : (v) => v;
+  const { fiveHourWindow, weeklyWindow } =
+    resolveRateLimitWindows(rateLimitSource);
+  const featureText = getDisplayText(meteredFeature);
+
+  return (
+    <section className='space-y-3'>
+      <div className='flex flex-wrap items-start justify-between gap-3'>
+        <div className='min-w-0 space-y-2'>
+          <div className='flex flex-wrap items-center gap-2'>
+            <div className='text-sm font-semibold text-semi-color-text-0'>
+              {title}
+            </div>
+            {statusTag}
+          </div>
+          {(description || featureText) && (
+            <div className='flex flex-wrap items-center gap-2 text-xs text-semi-color-text-2'>
+              {description ? <span>{description}</span> : null}
+              {featureText ? (
+                <div className='inline-flex max-w-full items-center gap-2 rounded-full bg-semi-color-fill-0 px-2 py-1'>
+                  <span className='text-[11px] text-semi-color-text-2'>
+                    metered_feature
+                  </span>
+                  <span className='min-w-0 break-all font-mono text-xs text-semi-color-text-0'>
+                    {featureText}
+                  </span>
+                </div>
+              ) : null}
+            </div>
+          )}
+        </div>
+      </div>
+
+      <RateLimitWindowGrid
+        t={tt}
+        fiveHourWindow={fiveHourWindow}
+        weeklyWindow={weeklyWindow}
+      />
+    </section>
+  );
+};
+
 const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
   const tt = typeof t === 'function' ? t : (v) => v;
   const [showRawJson, setShowRawJson] = useState(false);
   const data = payload?.data ?? null;
   const rateLimit = data?.rate_limit ?? {};
-  const { fiveHourWindow, weeklyWindow } = resolveRateLimitWindows(data);
+  const additionalRateLimits = Array.isArray(data?.additional_rate_limits)
+    ? data.additional_rate_limits.filter(
+        (item) =>
+          item && typeof item === 'object' && Object.keys(item).length > 0,
+      )
+    : [];
   const upstreamStatus = payload?.upstream_status;
   const accountType = data?.plan_type ?? rateLimit?.plan_type;
   const accountTypeLabel = formatAccountTypeLabel(accountType, tt);
@@ -277,7 +388,9 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
   const email = data?.email;
   const accountId = data?.account_id;
   const errorMessage =
-    payload?.success === false ? getDisplayText(payload?.message) || tt('获取用量失败') : '';
+    payload?.success === false
+      ? getDisplayText(payload?.message) || tt('获取用量失败')
+      : '';
 
   const rawText =
     typeof data === 'string' ? data : JSON.stringify(data ?? payload, null, 2);
@@ -313,7 +426,12 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
               </Tag>
             </div>
           </div>
-          <Button size='small' type='tertiary' theme='outline' onClick={onRefresh}>
+          <Button
+            size='small'
+            type='tertiary'
+            theme='outline'
+            onClick={onRefresh}
+          >
             {tt('刷新')}
           </Button>
         </div>
@@ -355,22 +473,61 @@ const CodexUsageView = ({ t, record, payload, onCopy, onRefresh }) => {
             {tt('额度窗口')}
           </div>
           <Text type='tertiary' size='small'>
-            {tt('用于观察当前帐号在 Codex 上游的限额使用情况')}
+            {tt(
+              '用于观察当前帐号在 Codex 上游的基础限额与附加计费能力使用情况',
+            )}
           </Text>
         </div>
       </div>
 
-      <div className='grid grid-cols-1 gap-3 md:grid-cols-2'>
-        <RateLimitWindowCard
+      <div className='space-y-5'>
+        <RateLimitGroupSection
           t={tt}
-          title={tt('5小时窗口')}
-          windowData={fiveHourWindow}
-        />
-        <RateLimitWindowCard
-          t={tt}
-          title={tt('每周窗口')}
-          windowData={weeklyWindow}
+          title={tt('基础额度')}
+          description={tt('当前帐号的基础额度窗口')}
+          rateLimitSource={data}
+          statusTag={statusTag}
         />
+
+        {additionalRateLimits.length > 0 ? (
+          <div className='space-y-4 border-t border-semi-color-border pt-4'>
+            <div>
+              <div className='text-sm font-semibold text-semi-color-text-0'>
+                {tt('附加额度')}
+              </div>
+              <Text type='tertiary' size='small'>
+                {tt('按模型或能力拆分的附加计费能力窗口')}
+              </Text>
+            </div>
+
+            <div className='space-y-4'>
+              {additionalRateLimits.map((item, index) => {
+                const limitName =
+                  getDisplayText(item?.limit_name) ||
+                  getDisplayText(item?.metered_feature) ||
+                  `${tt('附加额度')} ${index + 1}`;
+
+                return (
+                  <div
+                    key={`${limitName}-${getDisplayText(item?.metered_feature)}-${index}`}
+                    className={
+                      index > 0 ? 'border-t border-semi-color-border pt-4' : ''
+                    }
+                  >
+                    <RateLimitGroupSection
+                      t={tt}
+                      title={limitName}
+                      description={tt('附加计费能力')}
+                      rateLimitSource={item}
+                      statusTag={resolveUsageStatusTag(tt, item?.rate_limit)}
+                      meteredFeature={item?.metered_feature}
+                    />
+                  </div>
+                );
+              })}
+            </div>
+          </div>
+        ) : null}
       </div>
 
       <Collapse
@@ -489,12 +646,14 @@ const CodexUsageLoader = ({ t, record, initialPayload, onCopy }) => {
 
 export const openCodexUsageModal = ({ t, record, payload, onCopy }) => {
   const tt = typeof t === 'function' ? t : (v) => v;
+  const layout = getCodexUsageModalLayout();
 
   Modal.info({
     title: tt('Codex 帐号与用量'),
-    centered: true,
-    width: 900,
-    style: { maxWidth: '95vw' },
+    centered: false,
+    width: layout.width,
+    style: layout.style,
+    bodyStyle: layout.bodyStyle,
     content: (
       <CodexUsageLoader
         t={tt}