diff --git a/web/src/components/settings/ChannelSelectorModal.js b/web/src/components/settings/ChannelSelectorModal.js index a09eff1c..72fdb970 100644 --- a/web/src/components/settings/ChannelSelectorModal.js +++ b/web/src/components/settings/ChannelSelectorModal.js @@ -197,7 +197,6 @@ const ChannelSelectorModal = forwardRef(({ value={searchText} onChange={setSearchText} showClear - className="!rounded-full" /> {
-
+
{displayRemark}
diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index de5d9bce..80f7f3cd 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -876,7 +876,7 @@ "加载token失败": "Failed to load token", "配置聊天": "Configure chat", "模型消耗分布": "Model consumption distribution", - "模型调用次数占比": "Proportion of model calls", + "模型调用次数占比": "Model call ratio", "用户消耗分布": "User consumption distribution", "时间粒度": "Time granularity", "天": "day", @@ -1119,6 +1119,10 @@ "平均TPM": "Average TPM", "消耗分布": "Consumption distribution", "调用次数分布": "Models call distribution", + "消耗趋势": "Consumption trend", + "模型消耗趋势": "Model consumption trend", + "调用次数排行": "Models call ranking", + "模型调用次数排行": "Model call ranking", "添加渠道": "Add channel", "测试所有通道": "Test all channels", "删除禁用通道": "Delete disabled channels", @@ -1199,7 +1203,7 @@ "添加用户": "Add user", "角色": "Role", "已绑定的 Telegram 账户": "Bound Telegram account", - "新额度": "New quota", + "新额度:": "New quota: ", "需要添加的额度(支持负数)": "Need to add quota (supports negative numbers)", "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Read-only, user's personal settings, and cannot be modified directly", "请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characterss", diff --git a/web/src/pages/Detail/index.js b/web/src/pages/Detail/index.js index 2463db70..f455abb6 100644 --- a/web/src/pages/Detail/index.js +++ b/web/src/pages/Detail/index.js @@ -366,6 +366,86 @@ const Detail = (props) => { }, }); + // 模型消耗趋势折线图 + const [spec_model_line, setSpecModelLine] = useState({ + type: 'line', + data: [ + { + id: 'lineData', + values: [], + }, + ], + xField: 'Time', + yField: 'Count', + seriesField: 'Model', + legends: { + visible: true, + selectMode: 'single', + }, + title: { + visible: true, + text: t('模型消耗趋势'), + subtext: '', + }, + tooltip: { + mark: { + content: [ + { + key: (datum) => datum['Model'], + value: (datum) => renderNumber(datum['Count']), + }, + ], + }, + }, + color: { + specified: modelColorMap, + }, + }); + + // 模型调用次数排行柱状图 + const [spec_rank_bar, setSpecRankBar] = useState({ + type: 'bar', + data: [ + { + id: 'rankData', + values: [], + }, + ], + xField: 'Model', + yField: 'Count', + seriesField: 'Model', + legends: { + visible: true, + selectMode: 'single', + }, + title: { + visible: true, + text: t('模型调用次数排行'), + subtext: '', + }, + bar: { + state: { + hover: { + stroke: '#000', + lineWidth: 1, + }, + }, + }, + tooltip: { + mark: { + content: [ + { + key: (datum) => datum['Model'], + value: (datum) => renderNumber(datum['Count']), + }, + ], + }, + }, + color: { + specified: modelColorMap, + }, + }); + // ========== Hooks - Memoized Values ========== const performanceMetrics = useMemo(() => { const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000; @@ -853,6 +933,46 @@ const Detail = (props) => { 'barData' ); + // ===== 模型调用次数折线图 ===== + let modelLineData = []; + chartTimePoints.forEach((time) => { + const timeData = Array.from(uniqueModels).map((model) => { + const key = `${time}-${model}`; + const aggregated = aggregatedData.get(key); + return { + Time: time, + Model: model, + Count: aggregated?.count || 0, + }; + }); + modelLineData.push(...timeData); + }); + modelLineData.sort((a, b) => a.Time.localeCompare(b.Time)); + + // ===== 模型调用次数排行柱状图 ===== + const rankData = Array.from(modelTotals) + .map(([model, count]) => ({ + Model: model, + Count: count, + })) + .sort((a, b) => b.Count - a.Count); + + updateChartSpec( + setSpecModelLine, + modelLineData, + `${t('总计')}:${renderNumber(totalTimes)}`, + newModelColors, + 'lineData' + ); + + updateChartSpec( + setSpecRankBar, + rankData, + `${t('总计')}:${renderNumber(totalTimes)}`, + newModelColors, + 'rankData' + ); + setPieData(newPieData); setLineData(newLineData); setConsumeQuota(totalQuota); @@ -1122,28 +1242,53 @@ const Detail = (props) => { {t('消耗分布')} } itemKey="1" /> + + + {t('消耗趋势')} + + } itemKey="2" /> {t('调用次数分布')} - } itemKey="2" /> + } itemKey="3" /> + + + {t('调用次数排行')} + + } itemKey="4" />
} >
- {activeChartTab === '1' ? ( + {activeChartTab === '1' && ( - ) : ( + )} + {activeChartTab === '2' && ( + + )} + {activeChartTab === '3' && ( )} + {activeChartTab === '4' && ( + + )}
diff --git a/web/src/pages/Home/index.js b/web/src/pages/Home/index.js index 7cc4b792..f2db769c 100644 --- a/web/src/pages/Home/index.js +++ b/web/src/pages/Home/index.js @@ -272,10 +272,7 @@ const Home = () => { className="w-full h-screen border-none" /> ) : ( -
+
)}
)} diff --git a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js index 0794d606..ad9c5379 100644 --- a/web/src/pages/Setting/Ratio/UpstreamRatioSync.js +++ b/web/src/pages/Setting/Ratio/UpstreamRatioSync.js @@ -373,7 +373,7 @@ export default function UpstreamRatioSync(props) {
@@ -406,7 +406,7 @@ export default function UpstreamRatioSync(props) { placeholder={t('搜索模型名称')} value={searchKeyword} onChange={setSearchKeyword} - className="!rounded-full w-full sm:w-64" + className="w-full sm:w-64" showClear /> @@ -414,7 +414,7 @@ export default function UpstreamRatioSync(props) { placeholder={t('按倍率类型筛选')} value={ratioTypeFilter} onChange={setRatioTypeFilter} - className="!rounded-full w-full sm:w-48" + className="w-full sm:w-48" showClear onClear={() => setRatioTypeFilter('')} > @@ -704,7 +704,6 @@ export default function UpstreamRatioSync(props) { scroll={{ x: 'max-content' }} size='middle' loading={loading || syncLoading} - className="rounded-xl overflow-hidden" /> ); }; diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index deaefb6a..4a8f46e9 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -22,6 +22,7 @@ import { Row, Col, Input, + InputNumber, } from '@douyinfe/semi-ui'; import { IconUser, @@ -39,7 +40,7 @@ const EditUser = (props) => { const userId = props.editingUser.id; const [loading, setLoading] = useState(true); const [addQuotaModalOpen, setIsModalOpen] = useState(false); - const [addQuotaLocal, setAddQuotaLocal] = useState('0'); + const [addQuotaLocal, setAddQuotaLocal] = useState(''); const [groupOptions, setGroupOptions] = useState([]); const formApiRef = useRef(null); @@ -254,7 +255,6 @@ const EditUser = (props) => { field='quota' label={t('剩余额度')} placeholder={t('请输入新的剩余额度')} - min={0} step={500000} extraText={renderQuotaWithPrompt(values.quota || 0)} rules={[{ required: true, message: t('请输入额度') }]} @@ -328,18 +328,19 @@ const EditUser = (props) => { const current = formApiRef.current?.getValue('quota') || 0; return ( - {`${t('新额度')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`} + {`${t('新额度:')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`} ); })() }
-