Browse Source

✨ feat: Add model icon support across backend and UI; prefer model icon over vendor; add icon column in Models table

Backend:
- Model: Add `icon` field to `model.Model` (gorm: varchar(128)); auto-migrated via GORM.
- Pricing API: Extend `model.Pricing` with `icon` and populate from model meta in `GetPricing()`.

Frontend:
- EditModelModal: Add `icon` input (with @lobehub/icons helper link); wire into init/load/submit flows.
- ModelHeader / PricingCardView: Prefer rendering `model.icon`; fallback to `vendor_icon`; final fallback to initials avatar.
- Models table: Add leading “Icon” column, rendering `model.icon` or `vendor` icon via `getLobeHubIcon`.

Notes:
- Backward-compatible. Existing data without `icon` remain unaffected.
- No manual SQL needed; column is added by AutoMigrate.

Affected files:
- model/model_meta.go
- model/pricing.go
- web/src/components/table/models/modals/EditModelModal.jsx
- web/src/components/table/model-pricing/modal/components/ModelHeader.jsx
- web/src/components/table/model-pricing/view/card/PricingCardView.jsx
- web/src/components/table/models/ModelsColumnDefs.js
t0ng7u 6 months ago
parent
commit
cb75e25a1a

+ 1 - 0
model/model_meta.go

@@ -38,6 +38,7 @@ type Model struct {
     Id          int            `json:"id"`
     ModelName   string         `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,priority:1"`
     Description string         `json:"description,omitempty" gorm:"type:text"`
+    Icon        string         `json:"icon,omitempty" gorm:"type:varchar(128)"`
     Tags        string         `json:"tags,omitempty" gorm:"type:varchar(255)"`
     VendorID    int            `json:"vendor_id,omitempty" gorm:"index"`
     Endpoints   string         `json:"endpoints,omitempty" gorm:"type:text"`

+ 2 - 0
model/pricing.go

@@ -16,6 +16,7 @@ import (
 type Pricing struct {
     ModelName              string                  `json:"model_name"`
     Description            string                  `json:"description,omitempty"`
+    Icon                   string                  `json:"icon,omitempty"`
     Tags                   string                  `json:"tags,omitempty"`
     VendorID               int                     `json:"vendor_id,omitempty"`
     QuotaType              int                     `json:"quota_type"`
@@ -272,6 +273,7 @@ func updatePricing() {
                 continue
             }
             pricing.Description = meta.Description
+            pricing.Icon = meta.Icon
             pricing.Tags = meta.Tags
             pricing.VendorID = meta.VendorID
         }

+ 12 - 2
web/src/components/table/model-pricing/modal/components/ModelHeader.jsx

@@ -29,9 +29,19 @@ const CARD_STYLES = {
 };
 
 const ModelHeader = ({ modelData, vendorsMap = {}, t }) => {
-  // 获取模型图标(使用供应商图标)
+  // 获取模型图标(优先模型图标,其次供应商图标)
   const getModelIcon = () => {
-    // 优先使用供应商图标
+    // 1) 优先使用模型自定义图标
+    if (modelData?.icon) {
+      return (
+        <div className={CARD_STYLES.container}>
+          <div className={CARD_STYLES.icon}>
+            {getLobeHubIcon(modelData.icon, 32)}
+          </div>
+        </div>
+      );
+    }
+    // 2) 退化为供应商图标
     if (modelData?.vendor_icon) {
       return (
         <div className={CARD_STYLES.container}>

+ 11 - 1
web/src/components/table/model-pricing/view/card/PricingCardView.jsx

@@ -81,7 +81,17 @@ const PricingCardView = ({
         </div>
       );
     }
-    // 优先使用供应商图标
+    // 1) 优先使用模型自定义图标
+    if (model.icon) {
+      return (
+        <div className={CARD_STYLES.container}>
+          <div className={CARD_STYLES.icon}>
+            {getLobeHubIcon(model.icon, 32)}
+          </div>
+        </div>
+      );
+    }
+    // 2) 退化为供应商图标
     if (model.vendor_icon) {
       return (
         <div className={CARD_STYLES.container}>

+ 18 - 0
web/src/components/table/models/ModelsColumnDefs.js

@@ -33,6 +33,17 @@ function renderTimestamp(timestamp) {
   return <>{timestamp2string(timestamp)}</>;
 }
 
+// Render model icon column: prefer model.icon, then fallback to vendor icon
+const renderModelIconCol = (record, vendorMap) => {
+  const iconKey = record?.icon || vendorMap[record?.vendor_id]?.icon;
+  if (!iconKey) return '-';
+  return (
+    <div className="flex items-center justify-center">
+      {getLobeHubIcon(iconKey, 20)}
+    </div>
+  );
+};
+
 // Render vendor column with icon
 const renderVendorTag = (vendorId, vendorMap, t) => {
   if (!vendorId || !vendorMap[vendorId]) return '-';
@@ -222,6 +233,13 @@ export const getModelsColumns = ({
   vendorMap,
 }) => {
   return [
+    {
+      title: t('图标'),
+      dataIndex: 'icon',
+      width: 70,
+      align: 'center',
+      render: (text, record) => renderModelIconCol(record, vendorMap),
+    },
     {
       title: t('模型名称'),
       dataIndex: 'model_name',

+ 23 - 0
web/src/components/table/models/modals/EditModelModal.jsx

@@ -33,6 +33,7 @@ import {
   Row,
 } from '@douyinfe/semi-ui';
 import { Save, X, FileText } from 'lucide-react';
+import { IconLink } from '@douyinfe/semi-icons';
 import { API, showError, showSuccess } from '../../../../helpers';
 import { useTranslation } from 'react-i18next';
 import { useIsMobile } from '../../../../hooks/common/useIsMobile';
@@ -112,6 +113,7 @@ const EditModelModal = (props) => {
   const getInitValues = () => ({
     model_name: props.editingModel?.model_name || '',
     description: '',
+    icon: '',
     tags: [],
     vendor_id: undefined,
     vendor: '',
@@ -314,6 +316,27 @@ const EditModelModal = (props) => {
                     />
                   </Col>
 
+                  <Col span={24}>
+                    <Form.Input
+                      field='icon'
+                      label={t('模型图标')}
+                      placeholder={t('请输入图标名称')}
+                      extraText={
+                        <span>
+                          {t('图标使用@lobehub/icons库,如:OpenAI、Claude.Color,支持链式参数:OpenAI.Avatar.type={\'platform\'}、OpenRouter.Avatar.shape={\'square\'},查询所有可用图标请 ')}
+                          <Typography.Text
+                            link={{ href: 'https://icons.lobehub.com/components/lobe-hub', target: '_blank' }}
+                            icon={<IconLink />}
+                            underline
+                          >
+                            {t('请点击我')}
+                          </Typography.Text>
+                        </span>
+                      }
+                      showClear
+                    />
+                  </Col>
+
                   <Col span={24}>
                     <Form.TextArea
                       field='description'

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

@@ -1907,5 +1907,12 @@
   "确定要禁用所有的密钥吗?": "Are you sure you want to disable all keys?",
   "确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled keys?",
   "此操作不可撤销,将永久删除已自动禁用的密钥": "This operation cannot be undone, and all automatically disabled keys will be permanently deleted.",
-  "删除自动禁用密钥": "Delete auto disabled keys"
+  "删除自动禁用密钥": "Delete auto disabled keys",
+  "图标": "Icon",
+  "模型图标": "Model icon",
+  "请输入图标名称": "Please enter the icon name",
+  "精确名称匹配": "Exact name matching",
+  "前缀名称匹配": "Prefix name matching",
+  "后缀名称匹配": "Suffix name matching",
+  "包含名称匹配": "Contains name matching"
 }