Merge remote-tracking branch 'origin/alpha' into alpha

This commit is contained in:
CaIon
2025-07-05 14:14:48 +08:00
7 changed files with 166 additions and 21 deletions

View File

@@ -197,7 +197,6 @@ const ChannelSelectorModal = forwardRef(({
value={searchText} value={searchText}
onChange={setSearchText} onChange={setSearchText}
showClear showClear
className="!rounded-full"
/> />
<Table <Table

View File

@@ -119,7 +119,7 @@ const UsersTable = () => {
<Tooltip content={remark} position="top" showArrow> <Tooltip content={remark} position="top" showArrow>
<Tag color='white' size='large' shape='circle' className="!text-xs"> <Tag color='white' size='large' shape='circle' className="!text-xs">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<div className="w-2 h-2 flex-shrink-0" style={{ backgroundColor: '#10b981' }} /> <div className="w-2 h-2 flex-shrink-0 rounded-full" style={{ backgroundColor: '#10b981' }} />
{displayRemark} {displayRemark}
</div> </div>
</Tag> </Tag>

View File

@@ -876,7 +876,7 @@
"加载token失败": "Failed to load token", "加载token失败": "Failed to load token",
"配置聊天": "Configure chat", "配置聊天": "Configure chat",
"模型消耗分布": "Model consumption distribution", "模型消耗分布": "Model consumption distribution",
"模型调用次数占比": "Proportion of model calls", "模型调用次数占比": "Model call ratio",
"用户消耗分布": "User consumption distribution", "用户消耗分布": "User consumption distribution",
"时间粒度": "Time granularity", "时间粒度": "Time granularity",
"天": "day", "天": "day",
@@ -1119,6 +1119,10 @@
"平均TPM": "Average TPM", "平均TPM": "Average TPM",
"消耗分布": "Consumption distribution", "消耗分布": "Consumption distribution",
"调用次数分布": "Models call distribution", "调用次数分布": "Models call distribution",
"消耗趋势": "Consumption trend",
"模型消耗趋势": "Model consumption trend",
"调用次数排行": "Models call ranking",
"模型调用次数排行": "Model call ranking",
"添加渠道": "Add channel", "添加渠道": "Add channel",
"测试所有通道": "Test all channels", "测试所有通道": "Test all channels",
"删除禁用通道": "Delete disabled channels", "删除禁用通道": "Delete disabled channels",
@@ -1199,7 +1203,7 @@
"添加用户": "Add user", "添加用户": "Add user",
"角色": "Role", "角色": "Role",
"已绑定的 Telegram 账户": "Bound Telegram account", "已绑定的 Telegram 账户": "Bound Telegram account",
"新额度": "New quota", "新额度": "New quota: ",
"需要添加的额度(支持负数)": "Need to add quota (supports negative numbers)", "需要添加的额度(支持负数)": "Need to add quota (supports negative numbers)",
"此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Read-only, user's personal settings, and cannot be modified directly", "此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改": "Read-only, user's personal settings, and cannot be modified directly",
"请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characterss", "请输入新的密码,最短 8 位": "Please enter a new password, at least 8 characterss",

View File

@@ -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 ========== // ========== Hooks - Memoized Values ==========
const performanceMetrics = useMemo(() => { const performanceMetrics = useMemo(() => {
const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000; const timeDiff = (Date.parse(end_timestamp) - Date.parse(start_timestamp)) / 60000;
@@ -853,6 +933,46 @@ const Detail = (props) => {
'barData' '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); setPieData(newPieData);
setLineData(newLineData); setLineData(newLineData);
setConsumeQuota(totalQuota); setConsumeQuota(totalQuota);
@@ -1122,28 +1242,53 @@ const Detail = (props) => {
{t('消耗分布')} {t('消耗分布')}
</span> </span>
} itemKey="1" /> } itemKey="1" />
<TabPane tab={
<span>
<IconPulse />
{t('消耗趋势')}
</span>
} itemKey="2" />
<TabPane tab={ <TabPane tab={
<span> <span>
<IconPieChart2Stroked /> <IconPieChart2Stroked />
{t('调用次数分布')} {t('调用次数分布')}
</span> </span>
} itemKey="2" /> } itemKey="3" />
<TabPane tab={
<span>
<IconHistogram />
{t('调用次数排行')}
</span>
} itemKey="4" />
</Tabs> </Tabs>
</div> </div>
} }
> >
<div style={{ height: 400 }}> <div style={{ height: 400 }}>
{activeChartTab === '1' ? ( {activeChartTab === '1' && (
<VChart <VChart
spec={spec_line} spec={spec_line}
option={CHART_CONFIG} option={CHART_CONFIG}
/> />
) : ( )}
{activeChartTab === '2' && (
<VChart
spec={spec_model_line}
option={CHART_CONFIG}
/>
)}
{activeChartTab === '3' && (
<VChart <VChart
spec={spec_pie} spec={spec_pie}
option={CHART_CONFIG} option={CHART_CONFIG}
/> />
)} )}
{activeChartTab === '4' && (
<VChart
spec={spec_rank_bar}
option={CHART_CONFIG}
/>
)}
</div> </div>
</Card> </Card>

View File

@@ -272,10 +272,7 @@ const Home = () => {
className="w-full h-screen border-none" className="w-full h-screen border-none"
/> />
) : ( ) : (
<div <div className="mt-[64px]" dangerouslySetInnerHTML={{ __html: homePageContent }} />
className="text-base md:text-lg p-4 md:p-6 lg:p-8 overflow-x-hidden max-w-6xl mx-auto"
dangerouslySetInnerHTML={{ __html: homePageContent }}
></div>
)} )}
</div> </div>
)} )}

View File

@@ -373,7 +373,7 @@ export default function UpstreamRatioSync(props) {
<div className="flex flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1"> <div className="flex flex-col md:flex-row gap-2 w-full md:w-auto order-2 md:order-1">
<Button <Button
icon={<RefreshCcw size={14} />} icon={<RefreshCcw size={14} />}
className="!rounded-full w-full md:w-auto mt-2" className="w-full md:w-auto mt-2"
onClick={() => { onClick={() => {
setModalVisible(true); setModalVisible(true);
if (allChannels.length === 0) { if (allChannels.length === 0) {
@@ -393,7 +393,7 @@ export default function UpstreamRatioSync(props) {
type='secondary' type='secondary'
onClick={applySync} onClick={applySync}
disabled={!hasSelections} disabled={!hasSelections}
className="!rounded-full w-full md:w-auto mt-2" className="w-full md:w-auto mt-2"
> >
{t('应用同步')} {t('应用同步')}
</Button> </Button>
@@ -406,7 +406,7 @@ export default function UpstreamRatioSync(props) {
placeholder={t('搜索模型名称')} placeholder={t('搜索模型名称')}
value={searchKeyword} value={searchKeyword}
onChange={setSearchKeyword} onChange={setSearchKeyword}
className="!rounded-full w-full sm:w-64" className="w-full sm:w-64"
showClear showClear
/> />
@@ -414,7 +414,7 @@ export default function UpstreamRatioSync(props) {
placeholder={t('按倍率类型筛选')} placeholder={t('按倍率类型筛选')}
value={ratioTypeFilter} value={ratioTypeFilter}
onChange={setRatioTypeFilter} onChange={setRatioTypeFilter}
className="!rounded-full w-full sm:w-48" className="w-full sm:w-48"
showClear showClear
onClear={() => setRatioTypeFilter('')} onClear={() => setRatioTypeFilter('')}
> >
@@ -704,7 +704,6 @@ export default function UpstreamRatioSync(props) {
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
size='middle' size='middle'
loading={loading || syncLoading} loading={loading || syncLoading}
className="rounded-xl overflow-hidden"
/> />
); );
}; };

View File

@@ -22,6 +22,7 @@ import {
Row, Row,
Col, Col,
Input, Input,
InputNumber,
} from '@douyinfe/semi-ui'; } from '@douyinfe/semi-ui';
import { import {
IconUser, IconUser,
@@ -39,7 +40,7 @@ const EditUser = (props) => {
const userId = props.editingUser.id; const userId = props.editingUser.id;
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [addQuotaModalOpen, setIsModalOpen] = useState(false); const [addQuotaModalOpen, setIsModalOpen] = useState(false);
const [addQuotaLocal, setAddQuotaLocal] = useState('0'); const [addQuotaLocal, setAddQuotaLocal] = useState('');
const [groupOptions, setGroupOptions] = useState([]); const [groupOptions, setGroupOptions] = useState([]);
const formApiRef = useRef(null); const formApiRef = useRef(null);
@@ -254,7 +255,6 @@ const EditUser = (props) => {
field='quota' field='quota'
label={t('剩余额度')} label={t('剩余额度')}
placeholder={t('请输入新的剩余额度')} placeholder={t('请输入新的剩余额度')}
min={0}
step={500000} step={500000}
extraText={renderQuotaWithPrompt(values.quota || 0)} extraText={renderQuotaWithPrompt(values.quota || 0)}
rules={[{ required: true, message: t('请输入额度') }]} rules={[{ required: true, message: t('请输入额度') }]}
@@ -328,18 +328,19 @@ const EditUser = (props) => {
const current = formApiRef.current?.getValue('quota') || 0; const current = formApiRef.current?.getValue('quota') || 0;
return ( return (
<Text type='secondary' className='block mb-2'> <Text type='secondary' className='block mb-2'>
{`${t('新额度')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`} {`${t('新额度')}${renderQuota(current)} + ${renderQuota(addQuotaLocal)} = ${renderQuota(current + parseInt(addQuotaLocal || 0))}`}
</Text> </Text>
); );
})() })()
} }
</div> </div>
<Input <InputNumber
placeholder={t('需要添加的额度(支持负数)')} placeholder={t('需要添加的额度(支持负数)')}
type='number'
value={addQuotaLocal} value={addQuotaLocal}
onChange={setAddQuotaLocal} onChange={setAddQuotaLocal}
style={{ width: '100%' }}
showClear showClear
step={500000}
/> />
</Modal> </Modal>
</> </>