import React, { useContext, useEffect, useRef, useMemo, useState } from 'react'; import { API, copy, showError, showInfo, showSuccess, getModelCategories, renderModelTag } from '../../helpers'; import { useTranslation } from 'react-i18next'; import { Input, Layout, Modal, Space, Table, Tag, Tooltip, Popover, ImagePreview, Button, Card, Tabs, TabPane, Dropdown, Empty } from '@douyinfe/semi-ui'; import { IllustrationNoResult, IllustrationNoResultDark } from '@douyinfe/semi-illustrations'; import { IconVerify, IconHelpCircle, IconSearch, IconCopy, IconInfoCircle, IconLayers } from '@douyinfe/semi-icons'; import { UserContext } from '../../context/User/index.js'; import { AlertCircle } from 'lucide-react'; const ModelPricing = () => { const { t } = useTranslation(); const [filteredValue, setFilteredValue] = useState([]); const compositionRef = useRef({ isComposition: false }); const [selectedRowKeys, setSelectedRowKeys] = useState([]); 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( () => ({ onChange: (selectedRowKeys, selectedRows) => { setSelectedRowKeys(selectedRowKeys); }, }), [], ); const handleChange = (value) => { if (compositionRef.current.isComposition) { return; } const newFilteredValue = value ? [value] : []; setFilteredValue(newFilteredValue); }; const handleCompositionStart = () => { compositionRef.current.isComposition = true; }; const handleCompositionEnd = (event) => { compositionRef.current.isComposition = false; const value = event.target.value; const newFilteredValue = value ? [value] : []; setFilteredValue(newFilteredValue); }; function renderQuotaType(type) { switch (type) { case 1: return ( {t('按次计费')} ); case 0: return ( {t('按量计费')} ); default: return t('未知'); } } function renderAvailable(available) { return available ? ( {t('您的分组可以使用该模型')} } position='top' key={available} className="bg-green-50" > ) : null; } const columns = [ { title: t('可用性'), dataIndex: 'available', render: (text, record, index) => { return renderAvailable(record.enable_groups.includes(selectedGroup)); }, sorter: (a, b) => { const aAvailable = a.enable_groups.includes(selectedGroup); const bAvailable = b.enable_groups.includes(selectedGroup); return Number(aAvailable) - Number(bAvailable); }, defaultSortOrder: 'descend', }, { title: t('模型名称'), dataIndex: 'model_name', render: (text, record, index) => { return renderModelTag(text, { onClick: () => { copyText(text); } }); }, onFilter: (value, record) => record.model_name.toLowerCase().includes(value.toLowerCase()), filteredValue, }, { title: t('计费类型'), dataIndex: 'quota_type', render: (text, record, index) => { return renderQuotaType(parseInt(text)); }, sorter: (a, b) => a.quota_type - b.quota_type, }, { title: t('可用分组'), dataIndex: 'enable_groups', render: (text, record, index) => { return ( {text.map((group) => { if (usableGroup[group]) { if (group === selectedGroup) { return ( }> {group} ); } else { return ( { setSelectedGroup(group); showInfo( t('当前查看的分组为:{{group}},倍率为:{{ratio}}', { group: group, ratio: groupRatio[group], }), ); }} className="cursor-pointer hover:opacity-80 transition-opacity !rounded-full" > {group} ); } } })} ); }, }, { title: () => (
{t('倍率')} { setModalImageUrl('/ratio.png'); setIsModalOpenurl(true); }} />
), dataIndex: 'model_ratio', render: (text, record, index) => { let content = text; let completionRatio = parseFloat(record.completion_ratio.toFixed(3)); content = (
{t('模型倍率')}:{record.quota_type === 0 ? text : t('无')}
{t('补全倍率')}: {record.quota_type === 0 ? completionRatio : t('无')}
{t('分组倍率')}:{groupRatio[selectedGroup]}
); return content; }, }, { title: t('模型价格'), dataIndex: 'model_price', render: (text, record, index) => { let content = text; if (record.quota_type === 0) { let inputRatioPrice = record.model_ratio * 2 * groupRatio[selectedGroup]; let completionRatioPrice = record.model_ratio * record.completion_ratio * 2 * groupRatio[selectedGroup]; content = (
{t('提示')} ${inputRatioPrice.toFixed(3)} / 1M tokens
{t('补全')} ${completionRatioPrice.toFixed(3)} / 1M tokens
); } else { let price = parseFloat(text) * groupRatio[selectedGroup]; content = (
{t('模型价格')}:${price.toFixed(3)}
); } return content; }, }, ]; const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [userState, userDispatch] = useContext(UserContext); const [groupRatio, setGroupRatio] = useState({}); const [usableGroup, setUsableGroup] = useState({}); const setModelsFormat = (models, groupRatio) => { for (let i = 0; i < models.length; i++) { models[i].key = models[i].model_name; models[i].group_ratio = groupRatio[models[i].model_name]; } models.sort((a, b) => { return a.quota_type - b.quota_type; }); models.sort((a, b) => { if (a.model_name.startsWith('gpt') && !b.model_name.startsWith('gpt')) { return -1; } else if ( !a.model_name.startsWith('gpt') && b.model_name.startsWith('gpt') ) { return 1; } else { return a.model_name.localeCompare(b.model_name); } }); setModels(models); }; const loadPricing = async () => { setLoading(true); let url = '/api/pricing'; const res = await API.get(url); const { success, message, data, group_ratio, usable_group } = res.data; if (success) { setGroupRatio(group_ratio); setUsableGroup(usable_group); setSelectedGroup(userState.user ? userState.user.group : 'default'); setModelsFormat(data, group_ratio); } else { showError(message); } setLoading(false); }; const refresh = async () => { await loadPricing(); }; const copyText = async (text) => { if (await copy(text)) { showSuccess(t('已复制:') + text); } else { Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text }); } }; useEffect(() => { refresh().then(); }, []); const modelCategories = getModelCategories(t); const categoryCounts = useMemo(() => { const counts = {}; if (models.length > 0) { counts['all'] = models.length; Object.entries(modelCategories).forEach(([key, category]) => { if (key !== 'all') { counts[key] = models.filter(model => category.filter(model)).length; } }); } return counts; }, [models, modelCategories]); 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 ( {items.map(item => { const key = item.itemKey; const modelCount = categoryCounts[key] || 0; return ( setActiveKey(item.itemKey)} icon={modelCategories[item.itemKey]?.icon} >
{modelCategories[item.itemKey]?.label || item.itemKey} {modelCount}
); })} } >
{pos === 'start' ? '←' : '→'}
); }; // 检查分类是否有对应的模型 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 ( setActiveKey(key)} className="mt-2" > {Object.entries(modelCategories) .filter(([key]) => availableCategories.includes(key)) .map(([key, category]) => { const modelCount = categoryCounts[key] || 0; return ( {category.icon && {category.icon}} {category.label} {modelCount} } itemKey={key} key={key} /> ); })} ); }; // 优化过滤逻辑 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(() => (
} placeholder={t('模糊搜索模型名称')} className="!rounded-lg" onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onChange={handleChange} showClear size="large" />
), [selectedRowKeys, t]); // 表格组件 const ModelTable = useMemo(() => ( } darkModeImage={} description={t('搜索无结果')} style={{ padding: 30 }} /> } 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), }} /> ), [filteredModels, loading, columns, rowSelection, pageSize, t]); return (
{/* 主卡片容器 */} {/* 顶部状态卡片 */} {/* 装饰性背景元素 */}
{t('模型定价')}
{userState.user ? (
{t('当前分组')}: {userState.user.group},{t('倍率')}: {groupRatio[userState.user.group]}
) : (
{t('未登录,使用默认分组倍率')}: {groupRatio['default']}
)}
{t('分组倍率')}
{groupRatio[selectedGroup] || '1.0'}x
{t('可用模型')}
{models.filter(m => m.enable_groups.includes(selectedGroup)).length}
{t('计费类型')}
2
{/* 计费说明 */}
{t('按量计费费用 = 分组倍率 × 模型倍率 × (提示token数 + 补全token数 × 补全倍率)/ 500000 (单位:美元)')}
{/* 模型分类 Tabs */}
{renderTabs()} {/* 搜索和表格区域 */} {SearchAndActions} {ModelTable}
{/* 倍率说明图预览 */} setIsModalOpenurl(visible)} />
); }; export default ModelPricing;