✨ 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
This commit is contained in:
@@ -38,6 +38,7 @@ type Model struct {
|
|||||||
Id int `json:"id"`
|
Id int `json:"id"`
|
||||||
ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,priority:1"`
|
ModelName string `json:"model_name" gorm:"size:128;not null;uniqueIndex:uk_model_name,priority:1"`
|
||||||
Description string `json:"description,omitempty" gorm:"type:text"`
|
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)"`
|
Tags string `json:"tags,omitempty" gorm:"type:varchar(255)"`
|
||||||
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
|
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
|
||||||
Endpoints string `json:"endpoints,omitempty" gorm:"type:text"`
|
Endpoints string `json:"endpoints,omitempty" gorm:"type:text"`
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
type Pricing struct {
|
type Pricing struct {
|
||||||
ModelName string `json:"model_name"`
|
ModelName string `json:"model_name"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
|
Icon string `json:"icon,omitempty"`
|
||||||
Tags string `json:"tags,omitempty"`
|
Tags string `json:"tags,omitempty"`
|
||||||
VendorID int `json:"vendor_id,omitempty"`
|
VendorID int `json:"vendor_id,omitempty"`
|
||||||
QuotaType int `json:"quota_type"`
|
QuotaType int `json:"quota_type"`
|
||||||
@@ -272,6 +273,7 @@ func updatePricing() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pricing.Description = meta.Description
|
pricing.Description = meta.Description
|
||||||
|
pricing.Icon = meta.Icon
|
||||||
pricing.Tags = meta.Tags
|
pricing.Tags = meta.Tags
|
||||||
pricing.VendorID = meta.VendorID
|
pricing.VendorID = meta.VendorID
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,9 +29,19 @@ const CARD_STYLES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ModelHeader = ({ modelData, vendorsMap = {}, t }) => {
|
const ModelHeader = ({ modelData, vendorsMap = {}, t }) => {
|
||||||
// 获取模型图标(使用供应商图标)
|
// 获取模型图标(优先模型图标,其次供应商图标)
|
||||||
const getModelIcon = () => {
|
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) {
|
if (modelData?.vendor_icon) {
|
||||||
return (
|
return (
|
||||||
<div className={CARD_STYLES.container}>
|
<div className={CARD_STYLES.container}>
|
||||||
|
|||||||
@@ -81,7 +81,17 @@ const PricingCardView = ({
|
|||||||
</div>
|
</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) {
|
if (model.vendor_icon) {
|
||||||
return (
|
return (
|
||||||
<div className={CARD_STYLES.container}>
|
<div className={CARD_STYLES.container}>
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ function renderTimestamp(timestamp) {
|
|||||||
return <>{timestamp2string(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
|
// Render vendor column with icon
|
||||||
const renderVendorTag = (vendorId, vendorMap, t) => {
|
const renderVendorTag = (vendorId, vendorMap, t) => {
|
||||||
if (!vendorId || !vendorMap[vendorId]) return '-';
|
if (!vendorId || !vendorMap[vendorId]) return '-';
|
||||||
@@ -222,6 +233,13 @@ export const getModelsColumns = ({
|
|||||||
vendorMap,
|
vendorMap,
|
||||||
}) => {
|
}) => {
|
||||||
return [
|
return [
|
||||||
|
{
|
||||||
|
title: t('图标'),
|
||||||
|
dataIndex: 'icon',
|
||||||
|
width: 70,
|
||||||
|
align: 'center',
|
||||||
|
render: (text, record) => renderModelIconCol(record, vendorMap),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: t('模型名称'),
|
title: t('模型名称'),
|
||||||
dataIndex: 'model_name',
|
dataIndex: 'model_name',
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import { Save, X, FileText } from 'lucide-react';
|
import { Save, X, FileText } from 'lucide-react';
|
||||||
|
import { IconLink } from '@douyinfe/semi-icons';
|
||||||
import { API, showError, showSuccess } from '../../../../helpers';
|
import { API, showError, showSuccess } from '../../../../helpers';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../../hooks/common/useIsMobile';
|
||||||
@@ -112,6 +113,7 @@ const EditModelModal = (props) => {
|
|||||||
const getInitValues = () => ({
|
const getInitValues = () => ({
|
||||||
model_name: props.editingModel?.model_name || '',
|
model_name: props.editingModel?.model_name || '',
|
||||||
description: '',
|
description: '',
|
||||||
|
icon: '',
|
||||||
tags: [],
|
tags: [],
|
||||||
vendor_id: undefined,
|
vendor_id: undefined,
|
||||||
vendor: '',
|
vendor: '',
|
||||||
@@ -314,6 +316,27 @@ const EditModelModal = (props) => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</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}>
|
<Col span={24}>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
field='description'
|
field='description'
|
||||||
|
|||||||
@@ -1907,5 +1907,12 @@
|
|||||||
"确定要禁用所有的密钥吗?": "Are you sure you want to disable all keys?",
|
"确定要禁用所有的密钥吗?": "Are you sure you want to disable all keys?",
|
||||||
"确定要删除所有已自动禁用的密钥吗?": "Are you sure you want to delete all automatically disabled 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.",
|
"此操作不可撤销,将永久删除已自动禁用的密钥": "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"
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user