Просмотр исходного кода

Merge remote-tracking branch 'origin/alpha' into alpha

CaIon 8 месяцев назад
Родитель
Сommit
1e25bf700d

+ 0 - 1
web/src/components/settings/ChannelSelectorModal.js

@@ -197,7 +197,6 @@ const ChannelSelectorModal = forwardRef(({
           value={searchText}
           onChange={setSearchText}
           showClear
-          className="!rounded-full"
         />
 
         <Table

+ 1 - 1
web/src/components/table/UsersTable.js

@@ -119,7 +119,7 @@ const UsersTable = () => {
             <Tooltip content={remark} position="top" showArrow>
               <Tag color='white' size='large' shape='circle' className="!text-xs">
                 <div className="flex items-center gap-1">
-                  <div className="w-2 h-2 flex-shrink-0" style={{ backgroundColor: '#10b981' }} />
+                  <div className="w-2 h-2 flex-shrink-0 rounded-full" style={{ backgroundColor: '#10b981' }} />
                   {displayRemark}
                 </div>
               </Tag>

+ 6 - 2
web/src/i18n/locales/en.json

@@ -876,7 +876,7 @@
   "加载token失败": "Failed to load token",
   "配置聊天": "Configure chat",
   "模型消耗分布": "Model consumption distribution",
-  "模型调用次数占比": "Proportion of model calls",
+  "模型调用次数占比": "Model call ratio",
   "用户消耗分布": "User consumption distribution",
   "时间粒度": "Time granularity",
   "天": "day",
@@ -1119,6 +1119,10 @@
   "平均TPM": "Average TPM",
   "消耗分布": "Consumption distribution",
   "调用次数分布": "Models call distribution",
+  "消耗趋势": "Consumption trend",
+  "模型消耗趋势": "Model consumption trend",
+  "调用次数排行": "Models call ranking",
+  "模型调用次数排行": "Model call ranking",
   "添加渠道": "Add channel",
   "测试所有通道": "Test all channels",
   "删除禁用通道": "Delete disabled channels",
@@ -1199,7 +1203,7 @@
   "添加用户": "Add user",
   "角色": "Role",
   "已绑定的 Telegram 账户": "Bound Telegram account",
-  "新额度": "New quota",
+  "新额度": "New quota: ",
   "需要添加的额度(支持负数)": "Need to add quota (supports negative numbers)",
   "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Read-only, user's personal settings, and cannot be modified directly",
   "请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characterss",

+ 148 - 3
web/src/pages/Detail/index.js

@@ -366,6 +366,86 @@ const Detail = (props) => {
     },
   });
 
+  // 模型消耗趋势折线图
+  const [spec_model_line, setSpecModelLine] = useState({
+    type: 'line',
+    data: [
+      {
+        id: 'lineData',
+        values: [],
+      },
+    ],
+    xField: 'Time',
+    yField: 'Count',
+    seriesField: 'Model',
+    legends: {
+      visible: true,
+      selectMode: 'single',
+    },
+    title: {
+      visible: true,
+      text: t('模型消耗趋势'),
+      subtext: '',
+    },
+    tooltip: {
+      mark: {
+        content: [
+          {
+            key: (datum) => datum['Model'],
+            value: (datum) => renderNumber(datum['Count']),
+          },
+        ],
+      },
+    },
+    color: {
+      specified: modelColorMap,
+    },
+  });
+
+  // 模型调用次数排行柱状图
+  const [spec_rank_bar, setSpecRankBar] = useState({
+    type: 'bar',
+    data: [
+      {
+        id: 'rankData',
+        values: [],
+      },
+    ],
+    xField: 'Model',
+    yField: 'Count',
+    seriesField: 'Model',
+    legends: {
+      visible: true,
+      selectMode: 'single',
+    },
+    title: {
+      visible: true,
+      text: t('模型调用次数排行'),
+      subtext: '',
+    },
+    bar: {
+      state: {
+        hover: {
+          stroke: '#000',
+          lineWidth: 1,
+        },
+      },
+    },
+    tooltip: {
+      mark: {
+        content: [
+          {
+            key: (datum) => datum['Model'],
+            value: (datum) => renderNumber(datum['Count']),
+          },
+        ],
+      },
+    },
+    color: {
+      specified: modelColorMap,
+    },
+  });
+
   // ========== Hooks - Memoized Values ==========
   const performanceMetrics = useMemo(() => {
     const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
@@ -853,6 +933,46 @@ const Detail = (props) => {
       'barData'
     );
 
+    // ===== 模型调用次数折线图 =====
+    let modelLineData = [];
+    chartTimePoints.forEach((time) => {
+      const timeData = Array.from(uniqueModels).map((model) => {
+        const key = `${time}-${model}`;
+        const aggregated = aggregatedData.get(key);
+        return {
+          Time: time,
+          Model: model,
+          Count: aggregated?.count || 0,
+        };
+      });
+      modelLineData.push(...timeData);
+    });
+    modelLineData.sort((a, b) => a.Time.localeCompare(b.Time));
+
+    // ===== 模型调用次数排行柱状图 =====
+    const rankData = Array.from(modelTotals)
+      .map(([model, count]) => ({
+        Model: model,
+        Count: count,
+      }))
+      .sort((a, b) => b.Count - a.Count);
+
+    updateChartSpec(
+      setSpecModelLine,
+      modelLineData,
+      `${t('总计')}:${renderNumber(totalTimes)}`,
+      newModelColors,
+      'lineData'
+    );
+
+    updateChartSpec(
+      setSpecRankBar,
+      rankData,
+      `${t('总计')}:${renderNumber(totalTimes)}`,
+      newModelColors,
+      'rankData'
+    );
+
     setPieData(newPieData);
     setLineData(newLineData);
     setConsumeQuota(totalQuota);
@@ -1122,28 +1242,53 @@ const Detail = (props) => {
                         {t('消耗分布')}
                       </span>
                     } itemKey="1" />
+                    <TabPane tab={
+                      <span>
+                        <IconPulse />
+                        {t('消耗趋势')}
+                      </span>
+                    } itemKey="2" />
                     <TabPane tab={
                       <span>
                         <IconPieChart2Stroked />
                         {t('调用次数分布')}
                       </span>
-                    } itemKey="2" />
+                    } itemKey="3" />
+                    <TabPane tab={
+                      <span>
+                        <IconHistogram />
+                        {t('调用次数排行')}
+                      </span>
+                    } itemKey="4" />
                   </Tabs>
                 </div>
               }
             >
               <div style={{ height: 400 }}>
-                {activeChartTab === '1' ? (
+                {activeChartTab === '1' && (
                   <VChart
                     spec={spec_line}
                     option={CHART_CONFIG}
                   />
-                ) : (
+                )}
+                {activeChartTab === '2' && (
+                  <VChart
+                    spec={spec_model_line}
+                    option={CHART_CONFIG}
+                  />
+                )}
+                {activeChartTab === '3' && (
                   <VChart
                     spec={spec_pie}
                     option={CHART_CONFIG}
                   />
                 )}
+                {activeChartTab === '4' && (
+                  <VChart
+                    spec={spec_rank_bar}
+                    option={CHART_CONFIG}
+                  />
+                )}
               </div>
             </Card>
 

+ 1 - 4
web/src/pages/Home/index.js

@@ -272,10 +272,7 @@ const Home = () => {
               className="w-full h-screen border-none"
             />
           ) : (
-            <div
-              className="text-base md:text-lg p-4 md:p-6 lg:p-8 overflow-x-hidden max-w-6xl mx-auto"
-              dangerouslySetInnerHTML={{ __html: homePageContent }}
-            ></div>
+            <div className="mt-[64px]" dangerouslySetInnerHTML={{ __html: homePageContent }} />
           )}
         </div>
       )}

+ 4 - 5
web/src/pages/Setting/Ratio/UpstreamRatioSync.js

@@ -373,7 +373,7 @@ export default function UpstreamRatioSync(props) {
         <div className="flex flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
           <Button
             icon={<RefreshCcw size={14} />}
-            className="!rounded-full w-full md:w-auto mt-2"
+            className="w-full md:w-auto mt-2"
             onClick={() => {
               setModalVisible(true);
               if (allChannels.length === 0) {
@@ -393,7 +393,7 @@ export default function UpstreamRatioSync(props) {
                 type='secondary'
                 onClick={applySync}
                 disabled={!hasSelections}
-                className="!rounded-full w-full md:w-auto mt-2"
+                className="w-full md:w-auto mt-2"
               >
                 {t('应用同步')}
               </Button>
@@ -406,7 +406,7 @@ export default function UpstreamRatioSync(props) {
               placeholder={t('搜索模型名称')}
               value={searchKeyword}
               onChange={setSearchKeyword}
-              className="!rounded-full w-full sm:w-64"
+              className="w-full sm:w-64"
               showClear
             />
 
@@ -414,7 +414,7 @@ export default function UpstreamRatioSync(props) {
               placeholder={t('按倍率类型筛选')}
               value={ratioTypeFilter}
               onChange={setRatioTypeFilter}
-              className="!rounded-full w-full sm:w-48"
+              className="w-full sm:w-48"
               showClear
               onClear={() => setRatioTypeFilter('')}
             >
@@ -704,7 +704,6 @@ export default function UpstreamRatioSync(props) {
         scroll={{ x: 'max-content' }}
         size='middle'
         loading={loading || syncLoading}
-        className="rounded-xl overflow-hidden"
       />
     );
   };

+ 6 - 5
web/src/pages/User/EditUser.js

@@ -22,6 +22,7 @@ import {
   Row,
   Col,
   Input,
+  InputNumber,
 } from '@douyinfe/semi-ui';
 import {
   IconUser,
@@ -39,7 +40,7 @@ const EditUser = (props) => {
   const userId = props.editingUser.id;
   const [loading, setLoading] = useState(true);
   const [addQuotaModalOpen, setIsModalOpen] = useState(false);
-  const [addQuotaLocal, setAddQuotaLocal] = useState('0');
+  const [addQuotaLocal, setAddQuotaLocal] = useState('');
   const [groupOptions, setGroupOptions] = useState([]);
   const formApiRef = useRef(null);
 
@@ -254,7 +255,6 @@ const EditUser = (props) => {
                           field='quota'
                           label={t('剩余额度')}
                           placeholder={t('请输入新的剩余额度')}
-                          min={0}
                           step={500000}
                           extraText={renderQuotaWithPrompt(values.quota || 0)}
                           rules={[{ required: true, message: t('请输入额度') }]}
@@ -328,18 +328,19 @@ const EditUser = (props) => {
               const current = formApiRef.current?.getValue('quota') || 0;
               return (
                 <Text type='secondary' className='block mb-2'>
-                  {`${t('新额度')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`}
+                  {`${t('新额度')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`}
                 </Text>
               );
             })()
           }
         </div>
-        <Input
+        <InputNumber
           placeholder={t('需要添加的额度(支持负数)')}
-          type='number'
           value={addQuotaLocal}
           onChange={setAddQuotaLocal}
+          style={{ width: '100%' }}
           showClear
+          step={500000}
         />
       </Modal>
     </>