🎨 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.
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './toast.constants';
|
||||
export * from './user.constants';
|
||||
export * from './common.constant';
|
||||
export * from './channel.constants';
|
||||
export * from './user.constants';
|
||||
export * from './toast.constants';
|
||||
export * from './common.constant';
|
||||
export * from './model.constants';
|
||||
|
||||
145
web/src/constants/model.constants.js
Normal file
145
web/src/constants/model.constants.js
Normal file
@@ -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')
|
||||
}
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
@@ -276,3 +276,7 @@ code {
|
||||
.semi-datepicker-range-input {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.semi-tabs-content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
Reference in New Issue
Block a user