feat: 分站模型定价页添加分类筛选和价格排序
- 后端 GetDistSiteModels 返回 category 字段 - 前端添加分类图标筛选按钮(对话/补全/向量/图像/音频/视频/重排序) - 模型按输入价格从低到高排序 - 添加中英文 i18n 翻译 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
18856610fd
commit
917314463e
3 files changed
+71
-6
No files matched your search
@@ -148,6 +148,14 @@
|
||||
"pricing.status": "Status",
|
||||
"pricing.online": "Online",
|
||||
"pricing.unknown": "Unknown",
|
||||
"pricing.catAll": "All",
|
||||
"pricing.catChat": "Chat",
|
||||
"pricing.catCompletion": "Completion",
|
||||
"pricing.catEmbedding": "Embedding",
|
||||
"pricing.catImage": "Image",
|
||||
"pricing.catAudio": "Audio",
|
||||
"pricing.catVideo": "Video",
|
||||
"pricing.catRerank": "Rerank",
|
||||
|
||||
"packages.title": "Packages & Plans",
|
||||
"packages.subtitle": "Choose a package that fits your usage. All plans include access to all available models.",
|
||||
|
||||
@@ -148,6 +148,14 @@
|
||||
"pricing.status": "状态",
|
||||
"pricing.online": "在线",
|
||||
"pricing.unknown": "未知",
|
||||
"pricing.catAll": "全部",
|
||||
"pricing.catChat": "对话",
|
||||
"pricing.catCompletion": "补全",
|
||||
"pricing.catEmbedding": "向量",
|
||||
"pricing.catImage": "图像",
|
||||
"pricing.catAudio": "音频",
|
||||
"pricing.catVideo": "视频",
|
||||
"pricing.catRerank": "重排序",
|
||||
|
||||
"packages.title": "套餐方案",
|
||||
"packages.subtitle": "选择适合您用量的套餐。所有方案均可访问全部可用模型。",
|
||||
|
||||
+55
-6
@@ -1,11 +1,23 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { getSiteModels } from '../api';
|
||||
|
||||
const CATEGORIES = [
|
||||
{ value: '', icon: '🌐', labelKey: 'pricing.catAll' },
|
||||
{ value: 'chat', icon: '💬', labelKey: 'pricing.catChat' },
|
||||
{ value: 'completion', icon: '✍️', labelKey: 'pricing.catCompletion' },
|
||||
{ value: 'embedding', icon: '📐', labelKey: 'pricing.catEmbedding' },
|
||||
{ value: 'image', icon: '🖼️', labelKey: 'pricing.catImage' },
|
||||
{ value: 'audio', icon: '🎵', labelKey: 'pricing.catAudio' },
|
||||
{ value: 'video', icon: '🎬', labelKey: 'pricing.catVideo' },
|
||||
{ value: 'rerank', icon: '🔀', labelKey: 'pricing.catRerank' },
|
||||
];
|
||||
|
||||
export default function Pricing() {
|
||||
const { t } = useTranslation();
|
||||
const [models, setModels] = useState([]);
|
||||
const [search, setSearch] = useState('');
|
||||
const [category, setCategory] = useState('');
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -19,10 +31,29 @@ export default function Pricing() {
|
||||
|
||||
const enabledModels = models.filter((m) => m.enabled !== false);
|
||||
|
||||
const filtered = enabledModels.filter((m) =>
|
||||
!search ||
|
||||
(m.display_name || m.model_name || '').toLowerCase().includes(search.toLowerCase())
|
||||
);
|
||||
// Collect categories that actually have models
|
||||
const availableCategories = useMemo(() => {
|
||||
const cats = new Set(enabledModels.map((m) => m.category || 'chat'));
|
||||
return CATEGORIES.filter((c) => c.value === '' || cats.has(c.value));
|
||||
}, [enabledModels]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let list = enabledModels;
|
||||
// Category filter
|
||||
if (category) {
|
||||
list = list.filter((m) => (m.category || 'chat') === category);
|
||||
}
|
||||
// Search filter
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
list = list.filter((m) =>
|
||||
(m.display_name || m.model_name || '').toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
// Sort by input_price ascending
|
||||
list = [...list].sort((a, b) => (Number(a.input_price) || 0) - (Number(b.input_price) || 0));
|
||||
return list;
|
||||
}, [enabledModels, category, search]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
@@ -41,6 +72,24 @@ export default function Pricing() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="flex flex-wrap justify-center gap-2 mb-6">
|
||||
{availableCategories.map((cat) => (
|
||||
<button
|
||||
key={cat.value}
|
||||
onClick={() => setCategory(cat.value)}
|
||||
className={`inline-flex items-center gap-1.5 px-3.5 py-2 rounded-xl text-sm font-medium transition-all ${
|
||||
category === cat.value
|
||||
? 'bg-brand-500 text-white shadow-lg shadow-brand-500/25'
|
||||
: 'glass-sm text-page-secondary hover:text-page hover:bg-white/[0.08]'
|
||||
}`}
|
||||
>
|
||||
<span>{cat.icon}</span>
|
||||
<span>{t(cat.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="max-w-md mx-auto mb-8">
|
||||
<div className="relative">
|
||||
@@ -59,7 +108,7 @@ export default function Pricing() {
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<div className="text-center py-12 text-page-secondary">
|
||||
{search ? t('pricing.noMatch') : t('pricing.noModels')}
|
||||
{search || category ? t('pricing.noMatch') : t('pricing.noModels')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="glass-sm rounded-xl overflow-hidden">
|
||||
|
||||
Reference in new issue
Block a user