Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
6ba7e552
authored
May 20, 2025
by
Apple\Apple
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
♻
️Refactor: Detail Page
parent
74d4a541
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
227 additions
and
165 deletions
+227
-165
web/src/i18n/locales/en.json
+6
-1
web/src/pages/Detail/index.js
+221
-164
No files found.
web/src/i18n/locales/en.json
View file @
6ba7e552
...
...
@@ -1406,5 +1406,9 @@
"New API项目仓库地址:"
:
"New API project repository address: "
,
"NewAPI © {{currentYear}} QuantumNous | 基于 One API v0.5.4 © 2023 JustSong。"
:
"NewAPI © {{currentYear}} QuantumNous | Based on One API v0.5.4 © 2023 JustSong."
,
"本项目根据MIT许可证授权,需在遵守Apache-2.0协议的前提下使用。"
:
"This project is licensed under the MIT License and must be used in compliance with the Apache-2.0 License."
,
"管理员暂时未设置任何关于内容"
:
"The administrator has not set any custom About content yet"
"管理员暂时未设置任何关于内容"
:
"The administrator has not set any custom About content yet"
,
"早上好"
:
"Good morning"
,
"中午好"
:
"Good afternoon"
,
"下午好"
:
"Good afternoon"
,
"晚上好"
:
"Good evening"
}
\ No newline at end of file
web/src/pages/Detail/index.js
View file @
6ba7e552
...
...
@@ -2,16 +2,14 @@ import React, { useContext, useEffect, useRef, useState } from 'react';
import
{
initVChartSemiTheme
}
from
'@visactor/vchart-semi-theme'
;
import
{
Button
,
Card
,
Col
,
Descriptions
,
Form
,
Layout
,
Row
,
Spin
,
Tabs
,
Typography
,
IconButton
,
Modal
,
}
from
'@douyinfe/semi-ui'
;
import
{
IconRefresh
,
IconSearch
}
from
'@douyinfe/semi-icons'
;
import
{
VChart
}
from
'@visactor/react-vchart'
;
import
{
API
,
...
...
@@ -25,8 +23,6 @@ import {
modelColorMap
,
renderNumber
,
renderQuota
,
renderQuotaNumberWithDigit
,
stringToColor
,
modelToColor
,
}
from
'../../helpers/render'
;
import
{
UserContext
}
from
'../../context/User/index.js'
;
...
...
@@ -35,6 +31,7 @@ import { useTranslation } from 'react-i18next';
const
Detail
=
(
props
)
=>
{
const
{
t
}
=
useTranslation
();
const
{
Text
}
=
Typography
;
const
formRef
=
useRef
();
let
now
=
new
Date
();
const
[
userState
,
userDispatch
]
=
useContext
(
UserContext
);
...
...
@@ -67,6 +64,8 @@ const Detail = (props) => {
);
const
[
pieData
,
setPieData
]
=
useState
([{
type
:
'null'
,
value
:
'0'
}]);
const
[
lineData
,
setLineData
]
=
useState
([]);
const
[
searchModalVisible
,
setSearchModalVisible
]
=
useState
(
false
);
const
[
spec_pie
,
setSpecPie
]
=
useState
({
type
:
'pie'
,
data
:
[
...
...
@@ -200,6 +199,22 @@ const Detail = (props) => {
// 添加一个新的状态来存储模型-颜色映射
const
[
modelColors
,
setModelColors
]
=
useState
({});
// 显示搜索Modal
const
showSearchModal
=
()
=>
{
setSearchModalVisible
(
true
);
};
// 关闭搜索Modal
const
handleCloseModal
=
()
=>
{
setSearchModalVisible
(
false
);
};
// 搜索Modal确认按钮
const
handleSearchConfirm
=
()
=>
{
refresh
();
setSearchModalVisible
(
false
);
};
const
handleInputChange
=
(
value
,
name
)
=>
{
if
(
name
===
'data_export_default_time'
)
{
setDataExportDefaultTime
(
value
);
...
...
@@ -416,165 +431,207 @@ const Detail = (props) => {
}
},
[]);
// 数据卡片信息
const
statsData
=
[
{
title
:
t
(
'当前余额'
),
value
:
renderQuota
(
userState
?.
user
?.
quota
),
icon
:
'💰'
,
color
:
'bg-blue-50'
,
},
{
title
:
t
(
'历史消耗'
),
value
:
renderQuota
(
userState
?.
user
?.
used_quota
),
icon
:
'📊'
,
color
:
'bg-purple-50'
,
},
{
title
:
t
(
'请求次数'
),
value
:
userState
.
user
?.
request_count
,
icon
:
'🔄'
,
color
:
'bg-green-50'
,
},
{
title
:
t
(
'统计额度'
),
value
:
renderQuota
(
consumeQuota
),
icon
:
'💲'
,
color
:
'bg-yellow-50'
,
},
{
title
:
t
(
'统计Tokens'
),
value
:
isNaN
(
consumeTokens
)
?
0
:
consumeTokens
,
icon
:
'🔤'
,
color
:
'bg-pink-50'
,
},
{
title
:
t
(
'统计次数'
),
value
:
times
,
icon
:
'📈'
,
color
:
'bg-teal-50'
,
},
{
title
:
t
(
'平均RPM'
),
value
:
(
times
/
((
Date
.
parse
(
end_timestamp
)
-
Date
.
parse
(
start_timestamp
))
/
60000
)
).
toFixed
(
3
),
icon
:
'⏱️'
,
color
:
'bg-indigo-50'
,
},
{
title
:
t
(
'平均TPM'
),
value
:
(()
=>
{
const
tpm
=
consumeTokens
/
((
Date
.
parse
(
end_timestamp
)
-
Date
.
parse
(
start_timestamp
))
/
60000
);
return
isNaN
(
tpm
)
?
'0'
:
tpm
.
toFixed
(
3
);
})(),
icon
:
'📝'
,
color
:
'bg-orange-50'
,
},
];
// 获取问候语
const
getGreeting
=
()
=>
{
const
hours
=
new
Date
().
getHours
();
let
greeting
=
''
;
if
(
hours
>=
5
&&
hours
<
12
)
{
greeting
=
t
(
'早上好'
);
}
else
if
(
hours
>=
12
&&
hours
<
14
)
{
greeting
=
t
(
'中午好'
);
}
else
if
(
hours
>=
14
&&
hours
<
18
)
{
greeting
=
t
(
'下午好'
);
}
else
{
greeting
=
t
(
'晚上好'
);
}
const
username
=
userState
?.
user
?.
username
||
''
;
return
`👋
${
greeting
}
,
${
username
}
`
;
};
return
(
<>
<
Layout
>
<
Layout
.
Header
>
<
h3
>
{
t
(
'数据看板'
)}
<
/h3
>
<
/Layout.Header
>
<
Layout
.
Content
>
<
Form
ref
=
{
formRef
}
layout
=
'horizontal'
style
=
{{
marginTop
:
10
}}
>
<>
<
Form
.
DatePicker
field
=
'start_timestamp'
label
=
{
t
(
'起始时间'
)}
style
=
{{
width
:
272
}}
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
'dateTime'
name
=
'start_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'start_timestamp'
)
}
/
>
<
Form
.
DatePicker
field
=
'end_timestamp'
fluid
label
=
{
t
(
'结束时间'
)}
style
=
{{
width
:
272
}}
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
'dateTime'
name
=
'end_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
<
Form
.
Select
field
=
'data_export_default_time'
label
=
{
t
(
'时间粒度'
)}
style
=
{{
width
:
176
}}
initValue
=
{
dataExportDefaultTime
}
placeholder
=
{
t
(
'时间粒度'
)}
name
=
'data_export_default_time'
optionList
=
{[
{
label
:
t
(
'小时'
),
value
:
'hour'
},
{
label
:
t
(
'天'
),
value
:
'day'
},
{
label
:
t
(
'周'
),
value
:
'week'
},
]}
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'data_export_default_time'
)
}
><
/Form.Select
>
{
isAdminUser
&&
(
<>
<
Form
.
Input
field
=
'username'
label
=
{
t
(
'用户名称'
)}
style
=
{{
width
:
176
}}
value
=
{
username
}
placeholder
=
{
t
(
'可选值'
)}
name
=
'username'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'username'
)}
/
>
<
/
>
)}
<
Button
label
=
{
t
(
'查询'
)}
type
=
'primary'
htmlType
=
'submit'
className
=
'btn-margin-right'
onClick
=
{
refresh
}
loading
=
{
loading
}
style
=
{{
marginTop
:
24
}}
<
div
className
=
"bg-gray-50 min-h-screen"
>
<
div
className
=
"flex items-center justify-between mb-6"
>
<
h2
className
=
"text-2xl font-semibold text-gray-800"
>
{
getGreeting
()}
<
/h2
>
<
div
className
=
"flex gap-3"
>
<
IconButton
icon
=
{
<
IconSearch
/>
}
onClick
=
{
showSearchModal
}
className
=
"bg-green-500 text-white hover:bg-green-600 !rounded-full"
size
=
"large"
/>
<
IconButton
icon
=
{
<
IconRefresh
/>
}
onClick
=
{
refresh
}
loading
=
{
loading
}
className
=
"bg-blue-500 text-white hover:bg-blue-600 !rounded-full"
size
=
"large"
/>
<
/div
>
<
/div
>
{
/* 搜索条件Modal */
}
<
Modal
title
=
{
t
(
'搜索条件'
)}
visible
=
{
searchModalVisible
}
onOk
=
{
handleSearchConfirm
}
onCancel
=
{
handleCloseModal
}
closeOnEsc
=
{
true
}
width
=
{
700
}
centered
>
<
Form
ref
=
{
formRef
}
layout
=
'vertical'
className
=
"w-full"
>
<
Form
.
DatePicker
field
=
'start_timestamp'
label
=
{
t
(
'起始时间'
)}
className
=
"w-full mb-4"
initValue
=
{
start_timestamp
}
value
=
{
start_timestamp
}
type
=
'dateTime'
name
=
'start_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'start_timestamp'
)}
/
>
<
Form
.
DatePicker
field
=
'end_timestamp'
label
=
{
t
(
'结束时间'
)}
className
=
"w-full mb-4"
initValue
=
{
end_timestamp
}
value
=
{
end_timestamp
}
type
=
'dateTime'
name
=
'end_timestamp'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'end_timestamp'
)}
/
>
<
Form
.
Select
field
=
'data_export_default_time'
label
=
{
t
(
'时间粒度'
)}
className
=
"w-full mb-4"
initValue
=
{
dataExportDefaultTime
}
placeholder
=
{
t
(
'时间粒度'
)}
name
=
'data_export_default_time'
optionList
=
{[
{
label
:
t
(
'小时'
),
value
:
'hour'
},
{
label
:
t
(
'天'
),
value
:
'day'
},
{
label
:
t
(
'周'
),
value
:
'week'
},
]}
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'data_export_default_time'
)}
/
>
{
isAdminUser
&&
(
<
Form
.
Input
field
=
'username'
label
=
{
t
(
'用户名称'
)}
className
=
"w-full mb-4"
value
=
{
username
}
placeholder
=
{
t
(
'可选值'
)}
name
=
'username'
onChange
=
{(
value
)
=>
handleInputChange
(
value
,
'username'
)}
/
>
)}
<
/Form
>
<
/Modal
>
<
Spin
spinning
=
{
loading
}
>
<
div
className
=
"mb-6"
>
<
div
className
=
"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4"
>
{
statsData
.
map
((
stat
,
idx
)
=>
(
<
Card
key
=
{
idx
}
shadows
=
'hover'
className
=
{
`
${
stat
.
color
}
border-0 !rounded-2xl w-full`
}
headerLine
=
{
false
}
>
{
t
(
'查询'
)}
<
/Button
>
<
Form
.
Section
><
/Form.Section
>
<
/
>
<
/Form
>
<
Spin
spinning
=
{
loading
}
>
<
Row
gutter
=
{{
xs
:
16
,
sm
:
16
,
md
:
16
,
lg
:
24
,
xl
:
24
,
xxl
:
24
}}
style
=
{{
marginTop
:
20
}}
type
=
'flex'
justify
=
'space-between'
>
<
Col
span
=
{
styleState
.
isMobile
?
24
:
8
}
>
<
Card
className
=
'panel-desc-card'
>
<
Descriptions
row
size
=
'small'
>
<
Descriptions
.
Item
itemKey
=
{
t
(
'当前余额'
)}
>
{
renderQuota
(
userState
?.
user
?.
quota
)}
<
/Descriptions.Item
>
<
Descriptions
.
Item
itemKey
=
{
t
(
'历史消耗'
)}
>
{
renderQuota
(
userState
?.
user
?.
used_quota
)}
<
/Descriptions.Item
>
<
Descriptions
.
Item
itemKey
=
{
t
(
'请求次数'
)}
>
{
userState
.
user
?.
request_count
}
<
/Descriptions.Item
>
<
/Descriptions
>
<
/Card
>
<
/Col
>
<
Col
span
=
{
styleState
.
isMobile
?
24
:
8
}
>
<
Card
>
<
Descriptions
row
size
=
'small'
>
<
Descriptions
.
Item
itemKey
=
{
t
(
'统计额度'
)}
>
{
renderQuota
(
consumeQuota
)}
<
/Descriptions.Item
>
<
Descriptions
.
Item
itemKey
=
{
t
(
'统计Tokens'
)}
>
{
consumeTokens
}
<
/Descriptions.Item
>
<
Descriptions
.
Item
itemKey
=
{
t
(
'统计次数'
)}
>
{
times
}
<
/Descriptions.Item
>
<
/Descriptions
>
<
/Card
>
<
/Col
>
<
Col
span
=
{
styleState
.
isMobile
?
24
:
8
}
>
<
Card
>
<
Descriptions
row
size
=
'small'
>
<
Descriptions
.
Item
itemKey
=
{
t
(
'平均RPM'
)}
>
{(
times
/
((
Date
.
parse
(
end_timestamp
)
-
Date
.
parse
(
start_timestamp
))
/
60000
)
).
toFixed
(
3
)}
<
/Descriptions.Item
>
<
Descriptions
.
Item
itemKey
=
{
t
(
'平均TPM'
)}
>
{(
consumeTokens
/
((
Date
.
parse
(
end_timestamp
)
-
Date
.
parse
(
start_timestamp
))
/
60000
)
).
toFixed
(
3
)}
<
/Descriptions.Item
>
<
/Descriptions
>
<
/Card
>
<
/Col
>
<
/Row
>
<
Card
style
=
{{
marginTop
:
20
}}
>
<
Tabs
type
=
'line'
defaultActiveKey
=
'1'
>
<
Tabs
.
TabPane
tab
=
{
t
(
'消耗分布'
)}
itemKey
=
'1'
>
<
div
style
=
{{
height
:
500
}}
>
<
VChart
spec
=
{
spec_line
}
option
=
{{
mode
:
'desktop-browser'
}}
/
>
<
div
className
=
"flex items-center"
>
<
div
className
=
"text-2xl mr-3"
>
{
stat
.
icon
}
<
/div
>
<
div
>
<
div
className
=
"text-sm text-gray-500"
>
{
stat
.
title
}
<
/div
>
<
div
className
=
"text-xl font-semibold"
>
{
stat
.
value
}
<
/div
>
<
/div
>
<
/Tabs.TabPane
>
<
Tabs
.
TabPane
tab
=
{
t
(
'调用次数分布'
)}
itemKey
=
'2'
>
<
div
style
=
{{
height
:
500
}}
>
<
VChart
spec
=
{
spec_pie
}
option
=
{{
mode
:
'desktop-browser'
}}
/
>
<
/div
>
<
/Tabs.TabPane
>
<
/Tabs
>
<
/Card
>
<
/Spin
>
<
/Layout.Content
>
<
/Layout
>
<
/
>
<
/div
>
<
/Card
>
))}
<
/div
>
<
/div
>
<
div
className
=
"grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6"
>
<
Card
shadows
=
'hover'
className
=
"shadow-sm !rounded-2xl"
headerLine
=
{
true
}
title
=
{
t
(
'模型消耗分布'
)}
>
<
div
style
=
{{
height
:
400
}}
>
<
VChart
spec
=
{
spec_line
}
option
=
{{
mode
:
'desktop-browser'
}}
/
>
<
/div
>
<
/Card
>
<
Card
shadows
=
'hover'
className
=
"shadow-sm !rounded-2xl"
headerLine
=
{
true
}
title
=
{
t
(
'模型调用次数占比'
)}
>
<
div
style
=
{{
height
:
400
}}
>
<
VChart
spec
=
{
spec_pie
}
option
=
{{
mode
:
'desktop-browser'
}}
/
>
<
/div
>
<
/Card
>
<
/div
>
<
/Spin
>
<
/div
>
);
};
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment