Browse Source

🎨 refactor(ModelPricing): enhance UI/UX with modern design ModelPricing component

This commit implements a comprehensive UI/UX overhaul of the ModelPricing component,
focusing on improved aesthetics and responsiveness while maintaining existing API logic.

Key improvements:
- Redesigned status card with gradient background and floating elements
- Implemented responsive grid layout for pricing metrics
- Enhanced visual hierarchy with Semi UI components
- Added smooth transitions and hover effects
- Optimized spacing and typography for better readability
- Unified design language with PersonalSettings component
- Integrated Tailwind CSS 3.0 utility classes
- Added decorative elements for visual interest
- Improved mobile responsiveness across all breakpoints
- Enhanced accessibility with proper contrast ratios

The redesign follows modern UI/UX best practices while maintaining consistency
with the application's design system.
Apple\Apple 9 months ago
parent
commit
00c1ff05de

+ 334 - 158
web/src/components/ModelPricing.js

@@ -3,7 +3,6 @@ import { API, copy, showError, showInfo, showSuccess } from '../helpers';
 import { useTranslation } from 'react-i18next';
 
 import {
-  Banner,
   Input,
   Layout,
   Modal,
@@ -14,15 +13,21 @@ import {
   Popover,
   ImagePreview,
   Button,
+  Card,
+  Tabs,
+  TabPane,
+  Dropdown,
 } from '@douyinfe/semi-ui';
 import {
-  IconMore,
   IconVerify,
-  IconUploadError,
   IconHelpCircle,
+  IconSearch,
+  IconCopy,
+  IconInfoCircle,
 } from '@douyinfe/semi-icons';
 import { UserContext } from '../context/User/index.js';
-import Text from '@douyinfe/semi-ui/lib/es/typography/text';
+import { Settings, AlertCircle } from 'lucide-react';
+import { MODEL_CATEGORIES } from '../constants';
 
 const ModelPricing = () => {
   const { t } = useTranslation();
@@ -32,6 +37,8 @@ const ModelPricing = () => {
   const [modalImageUrl, setModalImageUrl] = useState('');
   const [isModalOpenurl, setIsModalOpenurl] = useState(false);
   const [selectedGroup, setSelectedGroup] = useState('default');
+  const [activeKey, setActiveKey] = useState('all');
+  const [pageSize, setPageSize] = useState(10);
 
   const rowSelection = useMemo(
     () => ({
@@ -49,6 +56,7 @@ const ModelPricing = () => {
     const newFilteredValue = value ? [value] : [];
     setFilteredValue(newFilteredValue);
   };
+
   const handleCompositionStart = () => {
     compositionRef.current.isComposition = true;
   };
@@ -61,17 +69,16 @@ const ModelPricing = () => {
   };
 
   function renderQuotaType(type) {
-    // Ensure all cases are string literals by adding quotes.
     switch (type) {
       case 1:
         return (
-          <Tag color='teal' size='large'>
+          <Tag color='teal' size='large' shape='circle'>
             {t('按次计费')}
           </Tag>
         );
       case 0:
         return (
-          <Tag color='violet' size='large'>
+          <Tag color='violet' size='large' shape='circle'>
             {t('按量计费')}
           </Tag>
         );
@@ -88,15 +95,9 @@ const ModelPricing = () => {
         }
         position='top'
         key={available}
-        style={{
-          backgroundColor: 'rgba(var(--semi-blue-4),1)',
-          borderColor: 'rgba(var(--semi-blue-4),1)',
-          color: 'var(--semi-color-white)',
-          borderWidth: 1,
-          borderStyle: 'solid',
-        }}
+        className="bg-green-50"
       >
-        <IconVerify style={{ color: 'green' }} size='large' />
+        <IconVerify style={{ color: 'rgb(22 163 74)' }} size='large' />
       </Popover>
     ) : null;
   }
@@ -106,7 +107,6 @@ const ModelPricing = () => {
       title: t('可用性'),
       dataIndex: 'available',
       render: (text, record, index) => {
-        // if record.enable_groups contains selectedGroup, then available is true
         return renderAvailable(record.enable_groups.includes(selectedGroup));
       },
       sorter: (a, b) => {
@@ -115,28 +115,29 @@ const ModelPricing = () => {
         return Number(aAvailable) - Number(bAvailable);
       },
       defaultSortOrder: 'descend',
+      width: 100,
     },
     {
       title: t('模型名称'),
       dataIndex: 'model_name',
       render: (text, record, index) => {
         return (
-          <>
-            <Tag
-              color='green'
-              size='large'
-              onClick={() => {
-                copyText(text);
-              }}
-            >
-              {text}
-            </Tag>
-          </>
+          <Tag
+            color='green'
+            size='large'
+            shape='circle'
+            onClick={() => {
+              copyText(text);
+            }}
+          >
+            {text}
+          </Tag>
         );
       },
       onFilter: (value, record) =>
         record.model_name.toLowerCase().includes(value.toLowerCase()),
       filteredValue,
+      width: 200,
     },
     {
       title: t('计费类型'),
@@ -145,19 +146,19 @@ const ModelPricing = () => {
         return renderQuotaType(parseInt(text));
       },
       sorter: (a, b) => a.quota_type - b.quota_type,
+      width: 120,
     },
     {
       title: t('可用分组'),
       dataIndex: 'enable_groups',
       render: (text, record, index) => {
-        // enable_groups is a string array
         return (
-          <Space>
+          <Space wrap>
             {text.map((group) => {
               if (usableGroup[group]) {
                 if (group === selectedGroup) {
                   return (
-                    <Tag color='blue' size='large' prefixIcon={<IconVerify />}>
+                    <Tag color='blue' size='large' shape='circle' prefixIcon={<IconVerify />}>
                       {group}
                     </Tag>
                   );
@@ -175,6 +176,7 @@ const ModelPricing = () => {
                           }),
                         );
                       }}
+                      className="cursor-pointer hover:opacity-80 transition-opacity !rounded-full"
                     >
                       {group}
                     </Tag>
@@ -188,56 +190,40 @@ const ModelPricing = () => {
     },
     {
       title: () => (
-        <span style={{ display: 'flex', alignItems: 'center' }}>
-          {t('倍率')}
-          <Popover
-            content={
-              <div style={{ padding: 8 }}>
-                {t('倍率是为了方便换算不同价格的模型')}
-                <br />
-                {t('点击查看倍率说明')}
-              </div>
-            }
-            position='top'
-            style={{
-              backgroundColor: 'rgba(var(--semi-blue-4),1)',
-              borderColor: 'rgba(var(--semi-blue-4),1)',
-              color: 'var(--semi-color-white)',
-              borderWidth: 1,
-              borderStyle: 'solid',
-            }}
-          >
+        <div className="flex items-center space-x-1">
+          <span>{t('倍率')}</span>
+          <Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
             <IconHelpCircle
+              className="text-blue-500 cursor-pointer"
               onClick={() => {
                 setModalImageUrl('/ratio.png');
                 setIsModalOpenurl(true);
               }}
             />
-          </Popover>
-        </span>
+          </Tooltip>
+        </div>
       ),
       dataIndex: 'model_ratio',
       render: (text, record, index) => {
         let content = text;
         let completionRatio = parseFloat(record.completion_ratio.toFixed(3));
         content = (
-          <>
-            <Text>
+          <div className="space-y-1">
+            <div className="text-gray-700">
               {t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
-            </Text>
-            <br />
-            <Text>
+            </div>
+            <div className="text-gray-700">
               {t('补全倍率')}:
               {record.quota_type === 0 ? completionRatio : t('无')}
-            </Text>
-            <br />
-            <Text>
+            </div>
+            <div className="text-gray-700">
               {t('分组倍率')}:{groupRatio[selectedGroup]}
-            </Text>
-          </>
+            </div>
+          </div>
         );
-        return <div>{content}</div>;
+        return content;
       },
+      width: 200,
     },
     {
       title: t('模型价格'),
@@ -245,7 +231,6 @@ const ModelPricing = () => {
       render: (text, record, index) => {
         let content = text;
         if (record.quota_type === 0) {
-          // 这里的 *2 是因为 1倍率=0.002刀,请勿删除
           let inputRatioPrice =
             record.model_ratio * 2 * groupRatio[selectedGroup];
           let completionRatioPrice =
@@ -254,26 +239,26 @@ const ModelPricing = () => {
             2 *
             groupRatio[selectedGroup];
           content = (
-            <>
-              <Text>
-                {t('提示')} ${inputRatioPrice} / 1M tokens
-              </Text>
-              <br />
-              <Text>
-                {t('补全')} ${completionRatioPrice} / 1M tokens
-              </Text>
-            </>
+            <div className="space-y-1">
+              <div className="text-gray-700">
+                {t('提示')} ${inputRatioPrice.toFixed(3)} / 1M tokens
+              </div>
+              <div className="text-gray-700">
+                {t('补全')} ${completionRatioPrice.toFixed(3)} / 1M tokens
+              </div>
+            </div>
           );
         } else {
           let price = parseFloat(text) * groupRatio[selectedGroup];
           content = (
-            <>
-              ${t('模型价格')}:${price}
-            </>
+            <div className="text-gray-700">
+              ${t('模型价格')}:${price.toFixed(3)}
+            </div>
           );
         }
-        return <div>{content}</div>;
+        return content;
       },
+      width: 250,
     },
   ];
 
@@ -288,12 +273,10 @@ const ModelPricing = () => {
       models[i].key = models[i].model_name;
       models[i].group_ratio = groupRatio[models[i].model_name];
     }
-    // sort by quota_type
     models.sort((a, b) => {
       return a.quota_type - b.quota_type;
     });
 
-    // sort by model_name, start with gpt is max, other use localeCompare
     models.sort((a, b) => {
       if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) {
         return -1;
@@ -312,9 +295,7 @@ const ModelPricing = () => {
 
   const loadPricing = async () => {
     setLoading(true);
-
-    let url = '';
-    url = `/api/pricing`;
+    let url = '/api/pricing';
     const res = await API.get(url);
     const { success, message, data, group_ratio, usable_group } = res.data;
     if (success) {
@@ -334,10 +315,9 @@ const ModelPricing = () => {
 
   const copyText = async (text) => {
     if (await copy(text)) {
-      showSuccess('已复制:' + text);
+      showSuccess(t('已复制:') + text);
     } else {
-      // setSearchKeyword(text);
-      Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text });
+      Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text });
     }
   };
 
@@ -345,88 +325,284 @@ const ModelPricing = () => {
     refresh().then();
   }, []);
 
-  return (
-    <>
-      <Layout>
-        {userState.user ? (
-          <Banner
-            type='success'
-            fullMode={false}
-            closeIcon='null'
-            description={t('您的默认分组为:{{group}},分组倍率为:{{ratio}}', {
-              group: userState.user.group,
-              ratio: groupRatio[userState.user.group],
-            })}
-          />
-        ) : (
-          <Banner
-            type='warning'
-            fullMode={false}
-            closeIcon='null'
-            description={t('您还未登陆,显示的价格为默认分组倍率: {{ratio}}', {
-              ratio: groupRatio['default'],
-            })}
-          />
-        )}
-        <br />
-        <Banner
-          type='info'
-          fullMode={false}
-          description={
-            <div>
-              {t(
-                '按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)',
-              )}
-            </div>
-          }
-          closeIcon='null'
-        />
-        <br />
-        <Space style={{ marginBottom: 16 }}>
+  const modelCategories = MODEL_CATEGORIES(t);
+
+  const renderArrow = (items, pos, handleArrowClick) => {
+    const style = {
+      width: 32,
+      height: 32,
+      margin: '0 12px',
+      display: 'flex',
+      justifyContent: 'center',
+      alignItems: 'center',
+      borderRadius: '100%',
+      background: 'rgba(var(--semi-grey-1), 1)',
+      color: 'var(--semi-color-text)',
+      cursor: 'pointer',
+    };
+    return (
+      <Dropdown
+        render={
+          <Dropdown.Menu>
+            {items.map(item => (
+              <Dropdown.Item
+                key={item.itemKey}
+                onClick={() => setActiveKey(item.itemKey)}
+                icon={modelCategories[item.itemKey]?.icon}
+              >
+                {modelCategories[item.itemKey]?.label || item.itemKey}
+              </Dropdown.Item>
+            ))}
+          </Dropdown.Menu>
+        }
+      >
+        <div style={style} onClick={handleArrowClick}>
+          {pos === 'start' ? '←' : '→'}
+        </div>
+      </Dropdown>
+    );
+  };
+
+  // 检查分类是否有对应的模型
+  const availableCategories = useMemo(() => {
+    if (!models.length) return ['all'];
+
+    return Object.entries(modelCategories).filter(([key, category]) => {
+      if (key === 'all') return true;
+      return models.some(model => category.filter(model));
+    }).map(([key]) => key);
+  }, [models]);
+
+  // 渲染标签页
+  const renderTabs = () => {
+    return (
+      <Tabs
+        renderArrow={renderArrow}
+        activeKey={activeKey}
+        type="card"
+        collapsible
+        onChange={key => setActiveKey(key)}
+      >
+        {Object.entries(modelCategories)
+          .filter(([key]) => availableCategories.includes(key))
+          .map(([key, category]) => (
+            <TabPane
+              tab={
+                <span className="flex items-center gap-2">
+                  {category.icon && <span className="w-4 h-4">{category.icon}</span>}
+                  {category.label}
+                </span>
+              }
+              itemKey={key}
+              key={key}
+            />
+          ))}
+      </Tabs>
+    );
+  };
+
+  // 优化过滤逻辑
+  const filteredModels = useMemo(() => {
+    let result = models;
+
+    // 先按分类过滤
+    if (activeKey !== 'all') {
+      result = result.filter(model => modelCategories[activeKey].filter(model));
+    }
+
+    // 再按搜索词过滤
+    if (filteredValue.length > 0) {
+      const searchTerm = filteredValue[0].toLowerCase();
+      result = result.filter(model =>
+        model.model_name.toLowerCase().includes(searchTerm)
+      );
+    }
+
+    return result;
+  }, [activeKey, models, filteredValue]);
+
+  // 搜索和操作区组件
+  const SearchAndActions = useMemo(() => (
+    <Card className="!rounded-xl mb-6" shadows='hover'>
+      <div className="flex flex-wrap items-center gap-4">
+        <div className="flex-1 min-w-[200px]">
           <Input
+            prefix={<IconSearch />}
             placeholder={t('模糊搜索模型名称')}
-            style={{ width: 200 }}
+            className="!rounded-lg"
             onCompositionStart={handleCompositionStart}
             onCompositionEnd={handleCompositionEnd}
             onChange={handleChange}
             showClear
+            size="large"
           />
-          <Button
-            theme='light'
-            type='tertiary'
-            style={{ width: 150 }}
-            onClick={() => {
-              copyText(selectedRowKeys);
-            }}
-            disabled={selectedRowKeys == ''}
-          >
-            {t('复制选中模型')}
-          </Button>
-        </Space>
-        <Table
-          style={{ marginTop: 5 }}
-          columns={columns}
-          dataSource={models}
-          loading={loading}
-          pagination={{
-            formatPageText: (page) =>
-              t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
-                start: page.currentStart,
-                end: page.currentEnd,
-                total: models.length,
-              }),
-            pageSize: models.length,
-            showSizeChanger: false,
-          }}
-          rowSelection={rowSelection}
-        />
-        <ImagePreview
-          src={modalImageUrl}
-          visible={isModalOpenurl}
-          onVisibleChange={(visible) => setIsModalOpenurl(visible)}
-        />
+        </div>
+        <Button
+          theme='light'
+          type='primary'
+          icon={<IconCopy />}
+          onClick={() => copyText(selectedRowKeys)}
+          disabled={selectedRowKeys.length === 0}
+          className="!rounded-lg !bg-blue-500 hover:!bg-blue-600 text-white"
+          size="large"
+        >
+          {t('复制选中模型')}
+        </Button>
+      </div>
+    </Card>
+  ), [selectedRowKeys, t]);
+
+  // 表格组件
+  const ModelTable = useMemo(() => (
+    <Card className="!rounded-xl overflow-hidden" shadows='hover'>
+      <Table
+        columns={columns}
+        dataSource={filteredModels}
+        loading={loading}
+        rowSelection={rowSelection}
+        className="custom-table"
+        pagination={{
+          defaultPageSize: 10,
+          pageSize: pageSize,
+          showSizeChanger: true,
+          pageSizeOptions: [10, 20, 50, 100],
+          formatPageText: (page) =>
+            t('第 {{start}} - {{end}} 条,共 {{total}} 条', {
+              start: page.currentStart,
+              end: page.currentEnd,
+              total: filteredModels.length,
+            }),
+          onPageSizeChange: (size) => setPageSize(size),
+        }}
+      />
+    </Card>
+  ), [filteredModels, loading, columns, rowSelection, pageSize, t]);
+
+  return (
+    <div className="min-h-screen bg-gray-50">
+      <Layout>
+        <Layout.Content>
+          <div className="flex justify-center p-4 sm:p-6 md:p-8">
+            <div className="w-full">
+              {/* 主卡片容器 */}
+              <Card className="!rounded-2xl shadow-lg border-0">
+                {/* 顶部状态卡片 */}
+                <Card
+                  className="!rounded-2xl !border-0 !shadow-2xl overflow-hidden mb-6"
+                  style={{
+                    background: 'linear-gradient(135deg, #6366f1 0%, #8b5cf6 25%, #a855f7 50%, #c084fc 75%, #d8b4fe 100%)',
+                    position: 'relative'
+                  }}
+                  bodyStyle={{ padding: 0 }}
+                >
+                  {/* 装饰性背景元素 */}
+                  <div className="absolute inset-0 overflow-hidden">
+                    <div className="absolute -top-10 -right-10 w-40 h-40 bg-white opacity-5 rounded-full"></div>
+                    <div className="absolute -bottom-16 -left-16 w-48 h-48 bg-white opacity-3 rounded-full"></div>
+                    <div className="absolute top-1/2 right-1/4 w-24 h-24 bg-yellow-400 opacity-10 rounded-full"></div>
+                  </div>
+
+                  <div className="relative p-6 sm:p-8" style={{ color: 'white' }}>
+                    <div className="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4 lg:gap-6">
+                      <div className="flex items-start">
+                        <div className="w-10 h-10 sm:w-12 sm:h-12 rounded-xl bg-white/10 flex items-center justify-center mr-3 sm:mr-4">
+                          <Settings size={20} className="text-white" />
+                        </div>
+                        <div className="flex-1 min-w-0">
+                          <div className="text-base sm:text-lg font-semibold mb-1 sm:mb-2">
+                            {t('模型定价')}
+                          </div>
+                          <div className="text-sm text-white/80">
+                            {userState.user ? (
+                              <div className="flex items-center">
+                                <IconVerify className="mr-1.5 flex-shrink-0" size="small" />
+                                <span className="truncate">
+                                  {t('当前分组')}: {userState.user.group},{t('倍率')}: {groupRatio[userState.user.group]}
+                                </span>
+                              </div>
+                            ) : (
+                              <div className="flex items-center">
+                                <AlertCircle size={14} className="mr-1.5 flex-shrink-0" />
+                                <span className="truncate">
+                                  {t('未登录,使用默认分组倍率')}: {groupRatio['default']}
+                                </span>
+                              </div>
+                            )}
+                          </div>
+                        </div>
+                      </div>
+
+                      <div className="grid grid-cols-3 gap-2 sm:gap-3 mt-2 lg:mt-0">
+                        <div
+                          className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
+                          style={{ backdropFilter: 'blur(10px)' }}
+                        >
+                          <div className="text-xs text-white/70 mb-0.5">{t('分组倍率')}</div>
+                          <div className="text-sm sm:text-base font-semibold">{groupRatio[selectedGroup] || '1.0'}x</div>
+                        </div>
+                        <div
+                          className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
+                          style={{ backdropFilter: 'blur(10px)' }}
+                        >
+                          <div className="text-xs text-white/70 mb-0.5">{t('可用模型')}</div>
+                          <div className="text-sm sm:text-base font-semibold">
+                            {models.filter(m => m.enable_groups.includes(selectedGroup)).length}
+                          </div>
+                        </div>
+                        <div
+                          className="text-center px-2 py-2 sm:px-3 sm:py-2.5 bg-white/10 rounded-lg backdrop-blur-sm hover:bg-white/20 transition-colors duration-200"
+                          style={{ backdropFilter: 'blur(10px)' }}
+                        >
+                          <div className="text-xs text-white/70 mb-0.5">{t('计费类型')}</div>
+                          <div className="text-sm sm:text-base font-semibold">2</div>
+                        </div>
+                      </div>
+                    </div>
+
+                    {/* 计费说明 */}
+                    <div className="mt-4 sm:mt-5">
+                      <div className="flex items-start">
+                        <div
+                          className="w-full flex items-start space-x-2 px-3 py-2 sm:px-4 sm:py-2.5 rounded-lg text-xs sm:text-sm"
+                          style={{
+                            backgroundColor: 'rgba(255, 255, 255, 0.2)',
+                            color: 'white',
+                            backdropFilter: 'blur(10px)'
+                          }}
+                        >
+                          <IconInfoCircle className="flex-shrink-0 mt-0.5" size="small" />
+                          <span>
+                            {t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}
+                          </span>
+                        </div>
+                      </div>
+                    </div>
+
+                    <div className="absolute top-0 left-0 w-full h-2 bg-gradient-to-r from-yellow-400 via-orange-400 to-red-400" style={{ opacity: 0.6 }}></div>
+                  </div>
+                </Card>
+
+                {/* 模型分类 Tabs */}
+                <div className="mb-6">
+                  {renderTabs()}
+
+                  {/* 搜索和表格区域 */}
+                  {SearchAndActions}
+                  {ModelTable}
+                </div>
+
+                {/* 倍率说明图预览 */}
+                <ImagePreview
+                  src={modalImageUrl}
+                  visible={isModalOpenurl}
+                  onVisibleChange={(visible) => setIsModalOpenurl(visible)}
+                />
+              </Card>
+            </div>
+          </div>
+        </Layout.Content>
       </Layout>
-    </>
+    </div>
   );
 };
 

+ 3 - 2
web/src/constants/index.js

@@ -1,4 +1,5 @@
-export * from './toast.constants';
+export * from './channel.constants';
 export * from './user.constants';
+export * from './toast.constants';
 export * from './common.constant';
-export * from './channel.constants';
+export * from './model.constants';

+ 145 - 0
web/src/constants/model.constants.js

@@ -0,0 +1,145 @@
+import {
+    OpenAI,
+    Claude,
+    Gemini,
+    Moonshot,
+    Zhipu,
+    Qwen,
+    DeepSeek,
+    Minimax,
+    Wenxin,
+    Spark,
+    Midjourney,
+    Hunyuan,
+    Cohere,
+    Cloudflare,
+    Ai360,
+    Yi,
+    Jina,
+    Mistral,
+    XAI,
+    Ollama,
+    Doubao,
+} from '@lobehub/icons';
+
+export const MODEL_CATEGORIES = (t) => ({
+    all: {
+        label: t('全部模型'),
+        icon: null,
+        filter: () => true
+    },
+    openai: {
+        label: 'OpenAI',
+        icon: <OpenAI />,
+        filter: (model) => model.model_name.toLowerCase().includes('gpt') ||
+            model.model_name.toLowerCase().includes('dall-e') ||
+            model.model_name.toLowerCase().includes('whisper') ||
+            model.model_name.toLowerCase().includes('tts') ||
+            model.model_name.toLowerCase().includes('text-') ||
+            model.model_name.toLowerCase().includes('babbage') ||
+            model.model_name.toLowerCase().includes('davinci') ||
+            model.model_name.toLowerCase().includes('curie') ||
+            model.model_name.toLowerCase().includes('ada')
+    },
+    anthropic: {
+        label: 'Anthropic',
+        icon: <Claude.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('claude')
+    },
+    gemini: {
+        label: 'Gemini',
+        icon: <Gemini.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('gemini')
+    },
+    moonshot: {
+        label: 'Moonshot',
+        icon: <Moonshot />,
+        filter: (model) => model.model_name.toLowerCase().includes('moonshot')
+    },
+    zhipu: {
+        label: t('智谱'),
+        icon: <Zhipu.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('chatglm') ||
+            model.model_name.toLowerCase().includes('glm-')
+    },
+    qwen: {
+        label: t('通义千问'),
+        icon: <Qwen.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('qwen')
+    },
+    deepseek: {
+        label: 'DeepSeek',
+        icon: <DeepSeek.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('deepseek')
+    },
+    minimax: {
+        label: 'MiniMax',
+        icon: <Minimax.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('abab')
+    },
+    baidu: {
+        label: t('文心一言'),
+        icon: <Wenxin.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('ernie')
+    },
+    xunfei: {
+        label: t('讯飞星火'),
+        icon: <Spark.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('spark')
+    },
+    midjourney: {
+        label: 'Midjourney',
+        icon: <Midjourney />,
+        filter: (model) => model.model_name.toLowerCase().includes('mj_')
+    },
+    tencent: {
+        label: t('腾讯混元'),
+        icon: <Hunyuan.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('hunyuan')
+    },
+    cohere: {
+        label: 'Cohere',
+        icon: <Cohere.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('command')
+    },
+    cloudflare: {
+        label: 'Cloudflare',
+        icon: <Cloudflare.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('@cf/')
+    },
+    ai360: {
+        label: t('360智脑'),
+        icon: <Ai360.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('360')
+    },
+    yi: {
+        label: t('零一万物'),
+        icon: <Yi.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('yi')
+    },
+    jina: {
+        label: 'Jina',
+        icon: <Jina />,
+        filter: (model) => model.model_name.toLowerCase().includes('jina')
+    },
+    mistral: {
+        label: 'Mistral AI',
+        icon: <Mistral.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('mistral')
+    },
+    xai: {
+        label: 'xAI',
+        icon: <XAI />,
+        filter: (model) => model.model_name.toLowerCase().includes('grok')
+    },
+    llama: {
+        label: 'Llama',
+        icon: <Ollama />,
+        filter: (model) => model.model_name.toLowerCase().includes('llama')
+    },
+    doubao: {
+        label: t('豆包'),
+        icon: <Doubao.Color />,
+        filter: (model) => model.model_name.toLowerCase().includes('doubao')
+    }
+}); 

+ 12 - 1
web/src/i18n/locales/en.json

@@ -1515,5 +1515,16 @@
   "用户分组配置": "User group configuration",
   "请选择可以使用该渠道的分组,留空则不更改": "Please select the groups that can use this channel, leaving blank will not change",
   "启用全部": "Enable all",
-  "禁用全部": "Disable all"
+  "禁用全部": "Disable all",
+  "模型定价": "Model Pricing",
+  "当前分组": "Current group",
+  "全部模型": "All Models",
+  "智谱": "Zhipu AI",
+  "通义千问": "Qwen",
+  "文心一言": "ERNIE Bot",
+  "讯飞星火": "Spark Desk",
+  "腾讯混元": "Hunyuan",
+  "360智脑": "360 AI Brain",
+  "零一万物": "Yi",
+  "豆包": "Doubao"
 }

+ 4 - 0
web/src/index.css

@@ -276,3 +276,7 @@ code {
 .semi-datepicker-range-input {
   border-radius: 9999px;
 }
+
+.semi-tabs-content {
+  padding: 0 !important;
+}