diff --git a/model/ability.go b/model/ability.go
index 1d7c53f..3f26aa5 100644
--- a/model/ability.go
+++ b/model/ability.go
@@ -33,7 +33,8 @@ func GetAllEnableAbilityWithChannels() ([]AbilityWithChannel, error) {
err := DB.Table("abilities").
Select("abilities.*, channels.type as channel_type").
Joins("left join channels on abilities.channel_id = channels.id").
- Where("abilities.enabled = ?", true).
+ Joins("left join models on abilities.model = models.model_name").
+ Where("abilities.enabled = ? and models.model_name is NOT null", true).
Scan(&abilities).Error
return abilities, err
}
diff --git a/model/model_meta.go b/model/model_meta.go
index 860b960..4f76898 100644
--- a/model/model_meta.go
+++ b/model/model_meta.go
@@ -15,6 +15,17 @@ const (
NameRuleSuffix
)
+const (
+ ModelTypeChat = 1 + iota
+ ModelTypeImage
+ ModelTypeAudio
+ ModelTypeVideo
+ ModelTypeEmbedding
+ ModelTypeRerank
+ ModelTypeVision
+ ModelTypeOther
+)
+
type BoundChannel struct {
Name string `json:"name"`
Type int `json:"type"`
@@ -26,6 +37,7 @@ type Model struct {
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)"`
+ Type int `json:"type" gorm:"default:0"`
VendorID int `json:"vendor_id,omitempty" gorm:"index"`
Endpoints string `json:"endpoints,omitempty" gorm:"type:text"`
Status int `json:"status" gorm:"default:1"`
diff --git a/model/pricing.go b/model/pricing.go
index cb687d0..6dfe69b 100644
--- a/model/pricing.go
+++ b/model/pricing.go
@@ -28,6 +28,7 @@ type Pricing struct {
EnableGroup []string `json:"enable_groups"`
SupportedEndpointTypes []constant.EndpointType `json:"supported_endpoint_types"`
PricingVersion string `json:"pricing_version,omitempty"`
+ Type int `json:"type"`
}
type PricingVendor struct {
@@ -279,13 +280,14 @@ func updatePricing() {
// 补充模型元数据(描述、标签、供应商、状态)
if meta, ok := metaMap[model]; ok {
// 若模型被禁用(status!=1),则直接跳过,不返回给前端
- if meta.Status != 1 {
+ if meta.Status != 1 || meta.Type == 0 {
continue
}
pricing.Description = meta.Description
pricing.Icon = meta.Icon
pricing.Tags = meta.Tags
pricing.VendorID = meta.VendorID
+ pricing.Type = meta.Type
}
modelPrice, findPrice := ratio_setting.GetModelPrice(model, false)
if findPrice {
diff --git a/web/package.json b/web/package.json
index 97c7c82..145448d 100644
--- a/web/package.json
+++ b/web/package.json
@@ -6,13 +6,15 @@
"dependencies": {
"@douyinfe/semi-icons": "^2.63.1",
"@douyinfe/semi-ui": "^2.69.1",
- "@lobehub/icons": "^2.0.0",
+ "@lobehub/icons": "^5.0.1",
+ "@lobehub/ui": "^4.38.0",
"@visactor/react-vchart": "~1.8.8",
"@visactor/vchart": "~1.8.8",
"@visactor/vchart-semi-theme": "~1.8.8",
"axios": "1.13.5",
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
+ "es-toolkit": "^1.21.0",
"history": "^5.3.0",
"i18next": "^23.16.8",
"i18next-browser-languagedetector": "^7.2.0",
diff --git a/web/src/components/common/ui/HorizontalFilterRow.jsx b/web/src/components/common/ui/HorizontalFilterRow.jsx
new file mode 100644
index 0000000..b98cce6
--- /dev/null
+++ b/web/src/components/common/ui/HorizontalFilterRow.jsx
@@ -0,0 +1,140 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React, { useRef } from 'react';
+import { Button, Tag, Skeleton } from '@douyinfe/semi-ui';
+import { IconChevronLeft, IconChevronRight } from '@douyinfe/semi-icons';
+import { useMinimumLoadingTime } from '../../../hooks/common/useMinimumLoadingTime';
+
+/**
+ * 横向筛选项行,通过左右箭头翻页
+ * @param {string} title 标题
+ * @param {Array<{value:any,label:string,icon?:React.ReactNode,tagCount?:number}>} items 选项列表
+ * @param {*} activeValue 当前选中值
+ * @param {(value:any)=>void} onChange 选择回调
+ * @param {boolean} loading 加载中
+ * @param {Function} t i18n
+ */
+const HorizontalFilterRow = ({
+ title,
+ items = [],
+ activeValue,
+ onChange,
+ loading = false,
+ t = (v) => v,
+}) => {
+ const scrollRef = useRef(null);
+ const showSkeleton = useMinimumLoadingTime(loading);
+
+ const handleScroll = (dir) => {
+ const el = scrollRef.current;
+ if (!el) return;
+ const step = el.clientWidth * 0.8;
+ el.scrollBy({
+ left: dir === 'left' ? -step : step,
+ behavior: 'smooth',
+ });
+ };
+
+ if (showSkeleton) {
+ return (
+
+
+
+ {[1, 2, 3, 4, 5].map((i) => (
+
+ ))}
+
+
+ );
+ }
+
+ return (
+
+
+ {title}
+
+
+
}
+ onClick={() => handleScroll('left')}
+ className='flex-shrink-0'
+ aria-label={t('向左')}
+ />
+
+ {items.map((item) => {
+ const isDisabled =
+ item.disabled ||
+ (typeof item.tagCount === 'number' && item.tagCount === 0);
+ const isActive = activeValue === item.value;
+
+ return (
+
+ );
+ })}
+
+
}
+ onClick={() => handleScroll('right')}
+ className='flex-shrink-0'
+ aria-label={t('向右')}
+ />
+
+
+ );
+};
+
+export default HorizontalFilterRow;
diff --git a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx
index 6542137..c23e9f8 100644
--- a/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx
+++ b/web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx
@@ -26,6 +26,7 @@ import ModelHeader from './components/ModelHeader';
import ModelBasicInfo from './components/ModelBasicInfo';
import ModelEndpoints from './components/ModelEndpoints';
import ModelPricingTable from './components/ModelPricingTable';
+import ModelCodeSnippet from './components/ModelCodeSnippet';
const { Text } = Typography;
@@ -99,6 +100,7 @@ const ModelDetailSideSheet = ({
autoGroups={autoGroups}
t={t}
/>
+
>
)}
diff --git a/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx b/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx
new file mode 100644
index 0000000..cad1549
--- /dev/null
+++ b/web/src/components/table/model-pricing/modal/components/ModelCodeSnippet.jsx
@@ -0,0 +1,232 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React, { useState, useMemo, useContext } from 'react';
+import {
+ Card,
+ Avatar,
+ Typography,
+ RadioGroup,
+ Radio,
+ TextArea,
+ Button,
+} from '@douyinfe/semi-ui';
+import { IconCode } from '@douyinfe/semi-icons';
+import { copy, showSuccess, showInfo } from '../../../../../helpers';
+import { StatusContext } from '../../../../../context/Status';
+
+const { Text } = Typography;
+
+const LANG_OPTIONS = [
+ { value: 'python', label: 'Python' },
+ { value: 'java', label: 'Java' },
+ { value: 'go', label: 'Go' },
+ { value: 'shell', label: 'Shell' },
+];
+
+// 四种语言的调用示例代码模板,使用 __MODEL_NAME__ 与 __BASE_URL__ 作为占位符
+const getDefaultCodeByLang = () => ({
+ python: `
+from openai import OpenAI
+
+client = OpenAI(
+ api_key="",
+ base_url="__BASE_URL__"
+)
+
+response = client.chat.completions.create(
+ model="__MODEL_NAME__",
+ messages=[
+ {"role": "system", "content": "You are a helpful assistant."},
+ {"role": "user", "content": "Hello, how are you?"}
+ ],
+ max_tokens=128000,
+ temperature=0.7
+)
+
+print(response.choices[0].message.content)
+ `,
+ java: `
+import com.openai.OpenAI;
+import com.openai.models.*;
+
+OpenAI client = new OpenAI("", "__BASE_URL__");
+
+ChatCompletionRequest request = ChatCompletionRequest.builder()
+ .model("__MODEL_NAME__")
+ .messages(Arrays.asList(
+ new ChatMessage("system", "You are a helpful assistant."),
+ new ChatMessage("user", "Hello, how are you?")
+ ))
+ .maxTokens(1000)
+ .temperature(0.7)
+ .build();
+
+ChatCompletion response = client.chatCompletions().create(request);
+System.out.println(response.getChoices().get(0).getMessage().getContent());
+ `,
+ go: `
+package main
+
+import (
+ "context"
+ "fmt"
+ "github.com/openai/openai-go"
+)
+
+func main() {
+ client := openai.NewClient("", "__BASE_URL__")
+
+ messages := []openai.ChatMessage{
+ {Role: "system", Content: "You are a helpful assistant."},
+ {Role: "user", Content: "Hello, how are you?"},
+ }
+
+ response, err := client.ChatCompletions.Create(context.Background(), openai.ChatCompletionRequest{
+ Model: "__MODEL_NAME__",
+ Messages: messages,
+ MaxTokens: 128000,
+ Temperature: 0.7,
+ })
+
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(response.Choices[0].Message.Content)
+}
+ `,
+ shell: `
+#!/bin/bash
+
+API_KEY=""
+MODEL_ID="__MODEL_NAME__"
+BASE_URL="__BASE_URL__"
+
+curl -X POST "$BASE_URL/v1/chat/completions" \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer $API_KEY" \
+ -d '{
+ "model": "'$MODEL_ID'",
+ "messages": [
+ {
+ "role": "system",
+ "content": "You are a helpful assistant."
+ },
+ {
+ "role": "user",
+ "content": "Hello, how are you?"
+ }
+ ],
+ "max_tokens": 128000,
+ "temperature": 0.7
+ }'
+ `,
+});
+
+const PLACEHOLDER_MODEL = '__MODEL_NAME__';
+const PLACEHOLDER_BASE_URL = '__BASE_URL__';
+
+const ModelCodeSnippet = ({ modelData, t }) => {
+ const [statusState] = useContext(StatusContext);
+ const serverAddress =
+ statusState?.status?.server_address || (typeof window !== 'undefined' ? window.location.origin : '');
+ const [lang, setLang] = useState('python');
+ const codeByLang = useMemo(() => {
+ const templates = getDefaultCodeByLang();
+ const modelName = modelData?.model_name || '';
+ const baseUrl = serverAddress || '';
+ return Object.fromEntries(
+ Object.entries(templates).map(([key, code]) => {
+ let result = code.replaceAll(PLACEHOLDER_BASE_URL, baseUrl);
+ result = result.replaceAll(PLACEHOLDER_MODEL, modelName);
+ return [key, result];
+ }),
+ );
+ }, [modelData?.model_name, serverAddress]);
+
+ const currentCode = codeByLang[lang] || '';
+
+ const handleCopy = async () => {
+ if (!currentCode || currentCode.trim() === '') {
+ showInfo(t('当前语言暂无示例代码'));
+ return;
+ }
+ const ok = await copy(currentCode);
+ if (ok) {
+ showSuccess(t('已复制到剪切板'));
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
{t('调用示例')}
+
+ {t('使用 OpenAI 兼容接口调用该模型的示例代码')}
+
+
+
+
+
+
+ {t('选择语言')}
+
+ setLang(e.target.value)}
+ direction='horizontal'
+ >
+ {LANG_OPTIONS.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default ModelCodeSnippet;
diff --git a/web/src/components/table/model-pricing/view/card/PricingCardView.jsx b/web/src/components/table/model-pricing/view/card/PricingCardView.jsx
index 6d53286..849ea97 100644
--- a/web/src/components/table/model-pricing/view/card/PricingCardView.jsx
+++ b/web/src/components/table/model-pricing/view/card/PricingCardView.jsx
@@ -74,13 +74,14 @@ const PricingCardView = ({
selectedRowKeys = [],
setSelectedRowKeys,
openModelDetail,
+ gridColsClass = 'grid-cols-1 xl:grid-cols-2 2xl:grid-cols-3',
+ showPagination = true,
}) => {
const showSkeleton = useMinimumLoadingTime(loading);
const startIndex = (currentPage - 1) * pageSize;
- const paginatedModels = filteredModels.slice(
- startIndex,
- startIndex + pageSize,
- );
+ const paginatedModels = showPagination
+ ? filteredModels.slice(startIndex, startIndex + pageSize)
+ : filteredModels;
const getModelKey = (model) => model.key ?? model.model_name ?? model.id;
const isMobile = useIsMobile();
@@ -234,7 +235,7 @@ const PricingCardView = ({
return (
-
+
{paginatedModels.map((model, index) => {
const modelKey = getModelKey(model);
const isSelected = selectedRowKeys.includes(modelKey);
@@ -357,7 +358,7 @@ const PricingCardView = ({
{/* 分页 */}
- {filteredModels.length > 0 && (
+ {showPagination && filteredModels.length > 0 && (
{
const [filterEndpointType, setFilterEndpointType] = useState('all'); // 端点类型筛选: 'all' | string
const [filterVendor, setFilterVendor] = useState('all'); // 供应商筛选: 'all' | 'unknown' | string
const [filterTag, setFilterTag] = useState('all'); // 模型标签筛选: 'all' | string
+ const [filterType, setFilterType] = useState('all'); // 模型类型筛选: 'all' | number (model_meta.ModelType)
const [pageSize, setPageSize] = useState(20);
const [currentPage, setCurrentPage] = useState(1);
const [currency, setCurrency] = useState('USD');
@@ -135,6 +136,12 @@ export const useModelPricingData = () => {
});
}
+ // 模型类型筛选(pricing.Type / model.Type)
+ if (filterType !== 'all') {
+ const typeNum = Number(filterType);
+ result = result.filter((model) => model.type === typeNum);
+ }
+
// 搜索筛选
if (searchValue.length > 0) {
const searchTerm = searchValue.toLowerCase();
@@ -159,6 +166,7 @@ export const useModelPricingData = () => {
filterEndpointType,
filterVendor,
filterTag,
+ filterType,
]);
const rowSelection = useMemo(
@@ -321,6 +329,7 @@ export const useModelPricingData = () => {
filterEndpointType,
filterVendor,
filterTag,
+ filterType,
searchValue,
]);
@@ -350,6 +359,8 @@ export const useModelPricingData = () => {
setFilterVendor,
filterTag,
setFilterTag,
+ filterType,
+ setFilterType,
pageSize,
setPageSize,
currentPage,
diff --git a/web/src/hooks/model-pricing/usePricingFilterCounts.js b/web/src/hooks/model-pricing/usePricingFilterCounts.js
index 3bb3d85..dd422d2 100644
--- a/web/src/hooks/model-pricing/usePricingFilterCounts.js
+++ b/web/src/hooks/model-pricing/usePricingFilterCounts.js
@@ -37,6 +37,7 @@ export const usePricingFilterCounts = ({
filterEndpointType = 'all',
filterVendor = 'all',
filterTag = 'all',
+ filterType = 'all',
searchValue = '',
}) => {
// 均使用同一份模型列表,避免创建新引用
@@ -84,6 +85,12 @@ export const usePricingFilterCounts = ({
if (!tagsArr.includes(filterTag.toLowerCase())) return false;
}
+ // 模型类型(pricing.Type / model.Type)
+ if (!ignore.includes('type') && filterType !== 'all') {
+ const typeNum = Number(filterType);
+ if (model.type !== typeNum) return false;
+ }
+
// 搜索
if (!ignore.includes('search') && searchValue) {
const term = searchValue.toLowerCase();
@@ -112,6 +119,7 @@ export const usePricingFilterCounts = ({
filterEndpointType,
filterVendor,
filterTag,
+ filterType,
searchValue,
],
);
@@ -124,6 +132,7 @@ export const usePricingFilterCounts = ({
filterQuotaType,
filterVendor,
filterTag,
+ filterType,
searchValue,
],
);
@@ -136,6 +145,7 @@ export const usePricingFilterCounts = ({
filterQuotaType,
filterEndpointType,
filterTag,
+ filterType,
searchValue,
],
);
@@ -148,6 +158,20 @@ export const usePricingFilterCounts = ({
filterQuotaType,
filterEndpointType,
filterVendor,
+ filterType,
+ searchValue,
+ ],
+ );
+
+ const typeModels = useMemo(
+ () => allModels.filter((m) => matchesFilters(m, ['type'])),
+ [
+ allModels,
+ filterGroup,
+ filterQuotaType,
+ filterEndpointType,
+ filterVendor,
+ filterTag,
searchValue,
],
);
@@ -160,6 +184,7 @@ export const usePricingFilterCounts = ({
filterEndpointType,
filterVendor,
filterTag,
+ filterType,
searchValue,
],
);
@@ -170,5 +195,6 @@ export const usePricingFilterCounts = ({
vendorModels,
groupCountModels,
tagModels,
+ typeModels,
};
};
diff --git a/web/src/pages/Home/HomePricingFilters.jsx b/web/src/pages/Home/HomePricingFilters.jsx
new file mode 100644
index 0000000..7726a0d
--- /dev/null
+++ b/web/src/pages/Home/HomePricingFilters.jsx
@@ -0,0 +1,197 @@
+/*
+Copyright (C) 2025 QuantumNous
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, either version 3 of the
+License, or (at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see .
+
+For commercial licensing, please contact support@quantumnous.com
+*/
+
+import React, { useMemo, useState } from 'react';
+import { ImagePreview } from '@douyinfe/semi-ui';
+import HorizontalFilterRow from '../../components/common/ui/HorizontalFilterRow';
+import PricingCardView from '../../components/table/model-pricing/view/card/PricingCardView';
+import ModelDetailSideSheet from '../../components/table/model-pricing/modal/ModelDetailSideSheet';
+import { useModelPricingData } from '../../hooks/model-pricing/useModelPricingData';
+import { usePricingFilterCounts } from '../../hooks/model-pricing/usePricingFilterCounts';
+import { getLobeHubIcon } from '../../helpers';
+
+// 与 model_meta.go ModelType 常量一致:1=Chat, 2=Image, 3=Audio, 4=Video, 5=Embedding, 6=Rerank, 7=Vision, 8=Other
+const MODEL_TYPE_LIST = [
+ { value: 1, labelKey: '对话' },
+ { value: 2, labelKey: '图像' },
+ { value: 3, labelKey: '音频' },
+ { value: 4, labelKey: '视频' },
+ { value: 5, labelKey: '嵌入' },
+ { value: 6, labelKey: '重排序' },
+ { value: 7, labelKey: '视觉' },
+ { value: 8, labelKey: '其他' },
+];
+
+const HomePricingFilters = ({ t }) => {
+ const [showRatio, setShowRatio] = useState(false);
+ const pricingData = useModelPricingData();
+ const {
+ vendorModels,
+ typeModels,
+ } = usePricingFilterCounts({
+ models: pricingData.models,
+ filterGroup: pricingData.filterGroup,
+ filterQuotaType: pricingData.filterQuotaType,
+ filterEndpointType: pricingData.filterEndpointType,
+ filterVendor: pricingData.filterVendor,
+ filterTag: pricingData.filterTag,
+ filterType: pricingData.filterType,
+ searchValue: pricingData.searchValue,
+ });
+
+ const vendorItems = useMemo(() => {
+ const models = pricingData.models;
+ const vendorSet = new Set();
+ const vendorIcons = new Map();
+ let hasUnknown = false;
+ (models || []).forEach((m) => {
+ if (m.vendor_name) {
+ vendorSet.add(m.vendor_name);
+ if (m.vendor_icon && !vendorIcons.has(m.vendor_name)) {
+ vendorIcons.set(m.vendor_name, m.vendor_icon);
+ }
+ } else {
+ hasUnknown = true;
+ }
+ });
+ const vendors = Array.from(vendorSet).sort();
+ const getCount = (v) => {
+ if (v === 'all') return vendorModels.length;
+ if (v === 'unknown') return vendorModels.filter((m) => !m.vendor_name).length;
+ return vendorModels.filter((m) => m.vendor_name === v).length;
+ };
+ const items = [
+ { value: 'all', label: t('全部供应商'), tagCount: getCount('all'), disabled: (models || []).length === 0 },
+ ];
+ vendors.forEach((v) => {
+ const count = getCount(v);
+ items.push({
+ value: v,
+ label: v,
+ icon: vendorIcons.get(v) ? getLobeHubIcon(vendorIcons.get(v), 16) : null,
+ tagCount: count,
+ disabled: count === 0,
+ });
+ });
+ if (hasUnknown) {
+ items.push({
+ value: 'unknown',
+ label: t('未知供应商'),
+ tagCount: getCount('unknown'),
+ disabled: getCount('unknown') === 0,
+ });
+ }
+ return items;
+ }, [pricingData.models, vendorModels, t]);
+
+ const typeItems = useMemo(() => {
+ const models = pricingData.models;
+ const getCount = (typeVal) => {
+ if (typeVal === 'all') return typeModels.length;
+ const n = Number(typeVal);
+ return typeModels.filter((m) => m.type === n).length;
+ };
+ const items = [
+ { value: 'all', label: t('全部类型'), tagCount: getCount('all'), disabled: (models || []).length === 0 },
+ ];
+ MODEL_TYPE_LIST.forEach(({ value, labelKey }) => {
+ const count = getCount(value);
+ items.push({
+ value: String(value),
+ label: t(labelKey),
+ tagCount: count,
+ disabled: count === 0,
+ });
+ });
+ return items;
+ }, [pricingData.models, typeModels, t]);
+
+ return (
+
+
+
+
+
+
+
+
+
pricingData.setIsModalOpenurl(visible)}
+ />
+
+
+
+ );
+};
+
+export default HomePricingFilters;
diff --git a/web/src/pages/Home/index.jsx b/web/src/pages/Home/index.jsx
index c153c1b..e1a238c 100644
--- a/web/src/pages/Home/index.jsx
+++ b/web/src/pages/Home/index.jsx
@@ -40,6 +40,7 @@ import {
} from '@douyinfe/semi-icons';
import { Link } from 'react-router-dom';
import NoticeModal from '../../components/layout/NoticeModal';
+import HomePricingFilters from './HomePricingFilters';
import {
Moonshot,
OpenAI,
@@ -162,9 +163,9 @@ const Home = () => {
{/* 背景模糊晕染球 */}
-
+
{/* 居中内容区 */}
-
+
{
)}
- {/* 框架兼容性图标 */}
-
-
-
- {t('支持众多的大模型供应商')}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 30+
-
-
-
-
+
+ {/* 模型广场筛选项与列表 */}
+
+
diff --git a/web/vite.config.js b/web/vite.config.js
index 73e4621..65828e0 100644
--- a/web/vite.config.js
+++ b/web/vite.config.js
@@ -29,6 +29,11 @@ export default defineConfig({
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
+ // 绕过 semi-ui exports 限制,dist/css/semi.css 不在 package.json exports 中
+ '@douyinfe/semi-ui/dist/css/semi.css': path.resolve(
+ __dirname,
+ 'node_modules/@douyinfe/semi-ui/dist/css/semi.css',
+ ),
},
},
plugins: [