/* 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 { Banner, Button, Card, Checkbox, Empty, Input, Modal, Radio, RadioGroup, Space, Switch, Table, Tag, Typography, } from '@douyinfe/semi-ui'; import { IconDelete, IconPlus, IconSave, IconSearch, } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; import { PAGE_SIZE, PRICE_SUFFIX, buildSummaryText, hasValue, useModelPricingEditorState, } from '../hooks/useModelPricingEditorState'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; const { Text } = Typography; const EMPTY_CANDIDATE_MODEL_NAMES = []; const PriceInput = ({ label, value, placeholder, onChange, suffix = PRICE_SUFFIX, disabled = false, extraText = '', headerAction = null, hidden = false, }) => (
{label} {headerAction}
{!hidden ? ( ) : null} {extraText ? (
{extraText}
) : null}
); export default function ModelPricingEditor({ options, refresh, candidateModelNames = EMPTY_CANDIDATE_MODEL_NAMES, filterMode = 'all', allowAddModel = true, allowDeleteModel = true, showConflictFilter = true, listDescription = '', emptyTitle = '', emptyDescription = '', }) { const { t } = useTranslation(); const isMobile = useIsMobile(); const [addVisible, setAddVisible] = useState(false); const [batchVisible, setBatchVisible] = useState(false); const [newModelName, setNewModelName] = useState(''); const { selectedModel, selectedModelName, selectedModelNames, setSelectedModelName, setSelectedModelNames, searchText, setSearchText, currentPage, setCurrentPage, loading, conflictOnly, setConflictOnly, filteredModels, pagedData, selectedWarnings, previewRows, isOptionalFieldEnabled, handleOptionalFieldToggle, handleNumericFieldChange, handleBillingModeChange, handleSubmit, addModel, deleteModel, applySelectedModelPricing, } = useModelPricingEditorState({ options, refresh, t, candidateModelNames, filterMode, }); const columns = useMemo( () => [ { title: t('模型名称'), dataIndex: 'name', key: 'name', render: (text, record) => ( {selectedModelNames.includes(record.name) ? ( {t('已勾选')} ) : null} {record.hasConflict ? ( {t('矛盾')} ) : null} ), }, { title: t('计费方式'), dataIndex: 'billingMode', key: 'billingMode', render: (_, record) => ( {record.billingMode === 'per-request' ? t('按次计费') : t('按量计费')} ), }, { title: t('价格摘要'), dataIndex: 'summary', key: 'summary', render: (_, record) => buildSummaryText(record, t), }, { title: t('操作'), key: 'action', render: (_, record) => ( {allowDeleteModel ? ( ) : null} } placeholder={t('搜索模型名称')} value={searchText} onChange={(value) => setSearchText(value)} style={{ width: isMobile ? '100%' : 220 }} showClear /> {showConflictFilter ? ( setConflictOnly(event.target.checked)} > {t('仅显示矛盾倍率')} ) : null} {listDescription ? (
{listDescription}
) : null} {selectedModelNames.length > 0 ? (
{t('已勾选 {{count}} 个模型', { count: selectedModelNames.length })}
) : null}
setCurrentPage(page), showTotal: true, showSizeChanger: false, }} empty={
{emptyTitle || t('暂无模型')}
} onRow={(record) => ({ style: { background: selectedModelNames.includes(record.name) ? 'var(--semi-color-success-light-default)' : record.name === selectedModelName ? 'var(--semi-color-primary-light-default)' : undefined, boxShadow: selectedModelNames.includes(record.name) ? 'inset 4px 0 0 var(--semi-color-success)' : record.name === selectedModelName ? 'inset 4px 0 0 var(--semi-color-primary)' : undefined, transition: 'background 0.2s ease, box-shadow 0.2s ease', }, onClick: () => setSelectedModelName(record.name), })} scroll={isMobile ? { x: 720 } : undefined} /> {selectedModel.billingMode === 'per-request' ? t('按次计费') : t('按量计费')} ) : null } > {!selectedModel ? ( ) : (
{t('计费方式')}
handleBillingModeChange(event.target.value)} > {t('按量计费')} {t('按次计费')}
{t( '这个界面默认按价格填写,保存时会自动换算回后端需要的倍率 JSON。', )}
{selectedWarnings.length > 0 ? (
{t('当前提示')}
{selectedWarnings.map((warning) => (
{warning}
))}
) : null} {selectedModel.billingMode === 'per-request' ? ( handleNumericFieldChange('fixedPrice', value)} extraText={t('适合 MJ / 任务类等按次收费模型。')} /> ) : ( <>
{t('基础价格')}
handleNumericFieldChange('inputPrice', value)} /> {selectedModel.completionRatioLocked ? ( ) : null} handleNumericFieldChange('completionPrice', value) } headerAction={ handleOptionalFieldToggle('completionPrice', checked) } /> } hidden={ !isOptionalFieldEnabled(selectedModel, 'completionPrice') } disabled={ !hasValue(selectedModel.inputPrice) || selectedModel.completionRatioLocked } extraText={ selectedModel.completionRatioLocked ? t( '后端固定倍率:{{ratio}}。该字段仅展示换算后的价格。', { ratio: selectedModel.lockedCompletionRatio || '-', }, ) : !isOptionalFieldEnabled( selectedModel, 'completionPrice', ) ? t('当前未启用,需要时再打开即可。') : '' } /> handleNumericFieldChange('cachePrice', value)} headerAction={ handleOptionalFieldToggle('cachePrice', checked) } /> } hidden={!isOptionalFieldEnabled(selectedModel, 'cachePrice')} disabled={!hasValue(selectedModel.inputPrice)} extraText={ !isOptionalFieldEnabled(selectedModel, 'cachePrice') ? t('当前未启用,需要时再打开即可。') : '' } /> handleNumericFieldChange('createCachePrice', value) } headerAction={ handleOptionalFieldToggle('createCachePrice', checked) } /> } hidden={ !isOptionalFieldEnabled(selectedModel, 'createCachePrice') } disabled={!hasValue(selectedModel.inputPrice)} extraText={ !isOptionalFieldEnabled( selectedModel, 'createCachePrice', ) ? t('当前未启用,需要时再打开即可。') : '' } />
{t('扩展价格')}
{t('这些价格都是可选项,不填也可以。')}
handleNumericFieldChange('imagePrice', value)} headerAction={ handleOptionalFieldToggle('imagePrice', checked) } /> } hidden={!isOptionalFieldEnabled(selectedModel, 'imagePrice')} disabled={!hasValue(selectedModel.inputPrice)} extraText={ !isOptionalFieldEnabled(selectedModel, 'imagePrice') ? t('当前未启用,需要时再打开即可。') : '' } /> handleNumericFieldChange('audioInputPrice', value) } headerAction={ handleOptionalFieldToggle('audioInputPrice', checked) } /> } hidden={!isOptionalFieldEnabled(selectedModel, 'audioInputPrice')} disabled={!hasValue(selectedModel.inputPrice)} extraText={ !isOptionalFieldEnabled( selectedModel, 'audioInputPrice', ) ? t('当前未启用,需要时再打开即可。') : '' } /> handleNumericFieldChange('audioOutputPrice', value) } headerAction={ handleOptionalFieldToggle('audioOutputPrice', checked) } /> } hidden={ !isOptionalFieldEnabled(selectedModel, 'audioOutputPrice') } disabled={!hasValue(selectedModel.audioInputPrice)} extraText={ !isOptionalFieldEnabled( selectedModel, 'audioInputPrice', ) ? t('请先开启并填写音频输入价格。') : !isOptionalFieldEnabled( selectedModel, 'audioOutputPrice', ) ? t('当前未启用,需要时再打开即可。') : '' } />
)}
{t('保存预览')}
{t( '下面展示这个模型保存后会写入哪些后端字段,便于和原始 JSON 编辑框保持一致。', )}
{previewRows.map((row) => ( {row.label} {row.value} ))}
)}
{allowAddModel ? ( { setAddVisible(false); setNewModelName(''); }} onOk={handleAddModel} > setNewModelName(value)} /> ) : null} setBatchVisible(false)} onOk={() => { if (applySelectedModelPricing()) { setBatchVisible(false); } }} >
{selectedModel ? t( '将把当前编辑中的模型 {{name}} 的价格配置,批量应用到已勾选的 {{count}} 个模型。', { name: selectedModel.name, count: selectedModelNames.length, }, ) : t('请先选择一个作为模板的模型')}
{selectedModel ? (
{t( '适合同系列模型一起定价,例如把 gpt-5.1 的价格批量同步到 gpt-5.1-high、gpt-5.1-low 等模型。', )}
) : null}
); }