Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
A
aidso-data
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Yaowentong
aidso-data
Commits
a385cade
Commit
a385cade
authored
Jun 17, 2026
by
Yaowentong
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
修复
parent
bb703b73
Changes
2
Show whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
1245 additions
and
0 deletions
+1245
-0
queue_monitor.css
aidso_geo/core/static/dashboard/queue_monitor.css
+649
-0
queue_monitor.js
aidso_geo/core/static/dashboard/queue_monitor.js
+596
-0
No files found.
aidso_geo/core/static/dashboard/queue_monitor.css
0 → 100644
View file @
a385cade
*
{
box-sizing
:
border-box
;
}
body
{
margin
:
0
;
font-family
:
-apple-system
,
BlinkMacSystemFont
,
"Segoe UI"
,
"PingFang SC"
,
"Microsoft YaHei"
,
Arial
,
sans-serif
;
background
:
#eef2f7
;
color
:
#1f2937
;
}
.layout
{
display
:
grid
;
grid-template-columns
:
220px
minmax
(
0
,
1
fr
);
min-height
:
100vh
;
}
.sidebar
{
padding
:
28px
18px
;
background
:
#17202b
;
border-right
:
1px
solid
rgba
(
255
,
255
,
255
,
0.08
);
box-shadow
:
10px
0
24px
rgba
(
15
,
23
,
42
,
0.12
);
}
.nav-btn
{
display
:
flex
;
align-items
:
center
;
width
:
100%
;
min-height
:
46px
;
margin-bottom
:
10px
;
border
:
1px
solid
transparent
;
border-radius
:
10px
;
background
:
rgba
(
255
,
255
,
255
,
0.04
);
color
:
#cbd5e1
;
font-size
:
15px
;
font-weight
:
700
;
line-height
:
20px
;
text-align
:
left
;
padding
:
0
14px
;
cursor
:
pointer
;
transition
:
background
0.18s
ease
,
color
0.18s
ease
,
border-color
0.18s
ease
;
}
.nav-btn.active
,
.nav-btn
:hover
{
background
:
#2563eb
;
border-color
:
rgba
(
255
,
255
,
255
,
0.14
);
color
:
#fff
;
}
.main
{
min-width
:
0
;
padding
:
30px
;
background
:
#eef2f7
;
}
.header
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin-bottom
:
24px
;
}
.title
{
color
:
#111827
;
font-size
:
28px
;
font-weight
:
700
;
}
.desc
{
margin-top
:
6px
;
font-size
:
14px
;
color
:
#6b7280
;
}
.right
{
display
:
flex
;
align-items
:
center
;
gap
:
12px
;
}
.last-time
,
.section-note
{
font-size
:
13px
;
color
:
#6b7280
;
}
.btn
,
.quick-btn
{
border
:
none
;
border-radius
:
8px
;
background
:
#2563eb
;
color
:
#fff
;
padding
:
10px
18px
;
cursor
:
pointer
;
font-size
:
14px
;
font-weight
:
700
;
box-shadow
:
0
10px
18px
rgba
(
37
,
99
,
235
,
0.18
);
transition
:
background
0.18s
ease
,
box-shadow
0.18s
ease
,
transform
0.18s
ease
;
}
.btn
:hover
,
.quick-btn
:hover
{
background
:
#1d4ed8
;
box-shadow
:
0
12px
22px
rgba
(
37
,
99
,
235
,
0.24
);
}
.quick-btn
{
background
:
#f1f5f9
;
color
:
#374151
;
box-shadow
:
none
;
}
.quick-btn.active
{
background
:
#2563eb
;
color
:
#fff
;
box-shadow
:
0
10px
18px
rgba
(
37
,
99
,
235
,
0.18
);
}
.panel
{
display
:
none
;
}
.panel.active
{
display
:
block
;
}
.summary
{
display
:
grid
;
grid-template-columns
:
repeat
(
4
,
1
fr
);
gap
:
16px
;
margin-bottom
:
24px
;
}
.card
,
.table-wrap
,
.trend-panel
{
background
:
#fff
;
border
:
1px
solid
#e5e7eb
;
border-radius
:
14px
;
box-shadow
:
0
14px
30px
rgba
(
15
,
23
,
42
,
0.06
);
}
.card
{
padding
:
20px
;
}
.card-name
{
color
:
#6b7280
;
font-size
:
14px
;
margin-bottom
:
10px
;
}
.card-value
{
font-size
:
30px
;
font-weight
:
700
;
color
:
#111827
;
}
.section-head
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
margin
:
8px
0
12px
;
}
.section-title
{
color
:
#111827
;
font-size
:
20px
;
font-weight
:
700
;
}
.table-wrap
{
overflow
:
hidden
;
margin-bottom
:
24px
;
}
.trend-panel
{
position
:
relative
;
padding
:
18px
;
}
.trend-loading-mask
{
position
:
absolute
;
inset
:
0
;
z-index
:
20
;
display
:
none
;
align-items
:
center
;
justify-content
:
center
;
border-radius
:
14px
;
background
:
rgba
(
248
,
250
,
252
,
0.78
);
backdrop-filter
:
blur
(
2px
);
}
.trend-panel.loading
.trend-loading-mask
{
display
:
flex
;
}
.loading-box
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
padding
:
12px
16px
;
border
:
1px
solid
#dbeafe
;
border-radius
:
12px
;
background
:
#fff
;
color
:
#2563eb
;
font-size
:
14px
;
font-weight
:
800
;
box-shadow
:
0
14px
30px
rgba
(
15
,
23
,
42
,
0.12
);
}
.loading-spinner
{
width
:
16px
;
height
:
16px
;
border
:
2px
solid
#bfdbfe
;
border-top-color
:
#2563eb
;
border-radius
:
999px
;
animation
:
spin
0.8s
linear
infinite
;
}
@keyframes
spin
{
to
{
transform
:
rotate
(
360deg
);
}
}
.filter-row
{
display
:
flex
;
flex-wrap
:
wrap
;
align-items
:
end
;
gap
:
12px
;
margin-bottom
:
20px
;
padding
:
14px
;
border-radius
:
12px
;
background
:
#f8fafc
;
border
:
1px
solid
#edf2f7
;
}
.quick-range
{
display
:
flex
;
gap
:
8px
;
}
.field
{
display
:
flex
;
flex-direction
:
column
;
gap
:
6px
;
}
.field
label
{
font-size
:
14px
;
color
:
#6b7280
;
}
.field
input
{
min-width
:
220px
;
border
:
1px
solid
#d7dce3
;
border-radius
:
8px
;
padding
:
9px
10px
;
background
:
#fff
;
color
:
#1f2937
;
font-size
:
14px
;
}
.field
input
:focus
{
outline
:
none
;
border-color
:
#2563eb
;
box-shadow
:
0
0
0
3px
rgba
(
37
,
99
,
235
,
0.12
);
}
.trend-chart
{
width
:
100%
;
min-height
:
300px
;
overflow-x
:
auto
;
border-radius
:
12px
;
background
:
#f8fafc
;
border
:
1px
solid
#edf2f7
;
}
.daily-grid
{
display
:
grid
;
grid-auto-flow
:
column
;
grid-auto-columns
:
minmax
(
320px
,
1
fr
);
gap
:
14px
;
min-width
:
100%
;
padding
:
14px
;
}
.day-card
{
min-width
:
0
;
border
:
1px
solid
#e5e7eb
;
border-radius
:
12px
;
background
:
#fff
;
box-shadow
:
0
10px
22px
rgba
(
15
,
23
,
42
,
0.04
);
}
.day-card-head
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
padding
:
14px
14px
10px
;
border-bottom
:
1px
solid
#edf2f7
;
}
.day-date
{
color
:
#111827
;
font-size
:
16px
;
font-weight
:
800
;
}
.day-total
{
color
:
#64748b
;
font-size
:
13px
;
font-weight
:
700
;
}
.day-list
{
display
:
grid
;
gap
:
10px
;
padding
:
12px
14px
14px
;
}
.day-row
{
display
:
grid
;
grid-template-columns
:
minmax
(
100px
,
1
fr
)
minmax
(
90px
,
42%
)
58px
;
align-items
:
center
;
gap
:
8px
;
min-height
:
24px
;
}
.day-platform
{
color
:
#334155
;
font-size
:
13px
;
font-weight
:
700
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.day-bar
{
height
:
9px
;
border-radius
:
999px
;
background
:
#eef2f7
;
overflow
:
hidden
;
}
.day-bar-inner
{
height
:
100%
;
min-width
:
3px
;
border-radius
:
999px
;
}
.day-count
{
color
:
#111827
;
font-size
:
13px
;
font-weight
:
700
;
text-align
:
right
;
}
.chart-empty
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
min-height
:
220px
;
color
:
#6b7280
;
font-size
:
14px
;
}
.trend-summary
{
margin-top
:
16px
;
padding
:
16px
;
border
:
1px
solid
#edf2f7
;
border-radius
:
12px
;
background
:
linear-gradient
(
180deg
,
#ffffff
0%
,
#f8fafc
100%
);
}
.trend-summary-title
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
margin-bottom
:
12px
;
}
.trend-summary-title-main
{
color
:
#111827
;
font-size
:
16px
;
font-weight
:
800
;
}
.trend-summary-actions
{
display
:
flex
;
align-items
:
center
;
gap
:
10px
;
}
.trend-summary-range
{
color
:
#64748b
;
font-size
:
13px
;
font-weight
:
700
;
}
.trend-switch
{
display
:
inline-grid
;
grid-template-columns
:
1
fr
1
fr
;
gap
:
2px
;
padding
:
3px
;
border
:
1px
solid
#d7dce3
;
border-radius
:
10px
;
background
:
#f1f5f9
;
}
.trend-switch-btn
{
border
:
none
;
border-radius
:
7px
;
background
:
transparent
;
color
:
#64748b
;
padding
:
6px
11px
;
cursor
:
pointer
;
font-size
:
13px
;
font-weight
:
800
;
}
.trend-switch-btn.active
{
background
:
#2563eb
;
color
:
#fff
;
box-shadow
:
0
8px
16px
rgba
(
37
,
99
,
235
,
0.18
);
}
.trend-summary
svg
{
display
:
block
;
width
:
100%
;
height
:
220px
;
}
.trend-label
{
fill
:
#64748b
;
font-size
:
12px
;
font-weight
:
700
;
}
.trend-total-label
{
fill
:
#2563eb
;
font-size
:
12px
;
font-weight
:
800
;
}
.trend-hover-hit
{
cursor
:
crosshair
;
}
.trend-tooltip
{
position
:
fixed
;
z-index
:
30
;
display
:
none
;
min-width
:
260px
;
max-width
:
360px
;
max-height
:
420px
;
overflow-y
:
auto
;
padding
:
10px
12px
;
border
:
1px
solid
rgba
(
148
,
163
,
184
,
0.28
);
border-radius
:
10px
;
background
:
rgba
(
15
,
23
,
42
,
0.94
);
color
:
#fff
;
box-shadow
:
0
16px
34px
rgba
(
15
,
23
,
42
,
0.24
);
pointer-events
:
none
;
}
.trend-tooltip-title
{
margin-bottom
:
8px
;
font-size
:
14px
;
font-weight
:
800
;
}
.trend-tooltip-total
{
display
:
flex
;
justify-content
:
space-between
;
margin-bottom
:
8px
;
color
:
#cbd5e1
;
font-size
:
12px
;
}
.trend-tooltip-total
strong
{
color
:
#fff
;
font-size
:
13px
;
}
.trend-tooltip-list
{
display
:
grid
;
gap
:
4px
;
padding-top
:
6px
;
border-top
:
1px
solid
rgba
(
148
,
163
,
184
,
0.22
);
}
.trend-tooltip-row
{
display
:
grid
;
grid-template-columns
:
10px
minmax
(
0
,
1
fr
)
auto
;
align-items
:
center
;
gap
:
8px
;
color
:
#e5e7eb
;
font-size
:
12px
;
line-height
:
22px
;
}
.trend-tooltip-dot
{
width
:
9px
;
height
:
9px
;
border-radius
:
999px
;
}
.trend-tooltip-name
{
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.trend-tooltip-count
{
color
:
#fff
;
font-weight
:
800
;
}
table
{
width
:
100%
;
border-collapse
:
collapse
;
}
thead
{
background
:
#f8fafc
;
}
th
,
td
{
padding
:
15px
18px
;
border-bottom
:
1px
solid
#edf0f5
;
text-align
:
left
;
font-size
:
14px
;
}
th
{
font-weight
:
600
;
color
:
#64748b
;
}
tbody
tr
:hover
{
background
:
#f8fafc
;
}
.platform
,
.num
{
font-weight
:
700
;
}
.num-ok
{
color
:
#16a34a
;
}
.num-warn
{
color
:
#f59e0b
;
}
.num-danger
{
color
:
#dc2626
;
}
.status
{
display
:
inline-block
;
padding
:
4px
10px
;
border-radius
:
999px
;
font-size
:
12px
;
font-weight
:
600
;
}
.status-ok
{
background
:
#dcfce7
;
color
:
#166534
;
}
.status-warn
{
background
:
#fef3c7
;
color
:
#92400e
;
}
.status-danger
{
background
:
#fee2e2
;
color
:
#991b1b
;
}
.error
{
display
:
none
;
margin-top
:
16px
;
padding
:
12px
14px
;
border-radius
:
8px
;
background
:
#fee2e2
;
color
:
#991b1b
;
font-size
:
14px
;
}
@media
(
max-width
:
900px
)
{
.layout
{
grid-template-columns
:
1
fr
;
}
.sidebar
{
display
:
flex
;
gap
:
10px
;
padding
:
14px
;
box-shadow
:
none
;
}
.nav-btn
{
justify-content
:
center
;
margin-bottom
:
0
;
}
.summary
{
grid-template-columns
:
repeat
(
2
,
1
fr
);
}
.header
{
flex-direction
:
column
;
align-items
:
flex-start
;
gap
:
12px
;
}
}
@media
(
max-width
:
600px
)
{
.main
{
padding
:
14px
;
}
.summary
{
grid-template-columns
:
1
fr
;
}
th
,
td
{
padding
:
10px
;
font-size
:
13px
;
}
.field
,
.field
input
,
.filter-row
.btn
,
.quick-range
{
width
:
100%
;
}
.quick-btn
{
flex
:
1
;
}
}
aidso_geo/core/static/dashboard/queue_monitor.js
0 → 100644
View file @
a385cade
const
API_URL
=
"/api/queue/status"
;
const
STATS_API_URL
=
"/api/queue/platformStats"
;
const
platforms
=
[
"BDAI"
,
"DB"
,
"DOUBA"
,
"DP"
,
"DPA"
,
"DYAI"
,
"KIMI"
,
"TXYB"
,
"TXYBA"
,
"TYQW"
,
"TYQWA"
,
"WXYY"
];
let
currentPanel
=
"queue"
;
let
loadedPanels
=
{
queue
:
false
,
trend
:
false
};
let
trendChartMode
=
"platform"
;
let
lastTrendData
=
null
;
function
showPanel
(
panel
)
{
currentPanel
=
panel
;
document
.
getElementById
(
"queuePanel"
).
classList
.
toggle
(
"active"
,
panel
===
"queue"
);
document
.
getElementById
(
"trendPanel"
).
classList
.
toggle
(
"active"
,
panel
===
"trend"
);
document
.
getElementById
(
"queueNavBtn"
).
classList
.
toggle
(
"active"
,
panel
===
"queue"
);
document
.
getElementById
(
"trendNavBtn"
).
classList
.
toggle
(
"active"
,
panel
===
"trend"
);
document
.
getElementById
(
"pageTitle"
).
innerText
=
panel
===
"queue"
?
"GEO 队列监控面板"
:
"GEO历史任务数量趋势"
;
document
.
getElementById
(
"pageDesc"
).
innerText
=
panel
===
"queue"
?
"查看各平台 stream_batch / batch 队列积压情况"
:
"按时间范围查看各平台历史任务趋势"
;
hideError
();
if
(
!
loadedPanels
[
panel
])
{
refreshCurrentPanel
();
}
}
function
refreshCurrentPanel
()
{
if
(
currentPanel
===
"queue"
)
{
loadQueueStatus
();
}
else
{
loadPlatformStats
();
}
}
function
hideError
()
{
const
errorBox
=
document
.
getElementById
(
"errorBox"
);
errorBox
.
style
.
display
=
"none"
;
errorBox
.
innerText
=
""
;
}
function
showError
(
message
)
{
const
errorBox
=
document
.
getElementById
(
"errorBox"
);
errorBox
.
style
.
display
=
"block"
;
errorBox
.
innerText
=
message
;
}
function
setTrendLoading
(
isLoading
)
{
document
.
querySelector
(
".trend-panel"
).
classList
.
toggle
(
"loading"
,
isLoading
);
}
function
getNumClass
(
num
)
{
if
(
num
===
0
)
{
return
"num-ok"
;
}
if
(
num
<
500
)
{
return
"num-warn"
;
}
return
"num-danger"
;
}
function
getStatus
(
total
)
{
if
(
total
===
0
)
{
return
{
text
:
"正常"
,
className
:
"status status-ok"
};
}
if
(
total
<
500
)
{
return
{
text
:
"轻微积压"
,
className
:
"status status-warn"
};
}
return
{
text
:
"严重积压"
,
className
:
"status status-danger"
};
}
function
renderData
(
data
)
{
const
tbody
=
document
.
getElementById
(
"queueTableBody"
);
let
totalCount
=
0
;
let
streamBatchCount
=
0
;
let
batchCount
=
0
;
tbody
.
innerHTML
=
""
;
platforms
.
forEach
(
platform
=>
{
const
streamKey
=
`
${
platform
}
:geo:stream_batch:list`
;
const
batchKey
=
`
${
platform
}
:geo:batch:list`
;
const
streamValue
=
Number
(
data
[
streamKey
]
||
0
);
const
batchValue
=
Number
(
data
[
batchKey
]
||
0
);
const
platformTotal
=
streamValue
+
batchValue
;
const
status
=
getStatus
(
platformTotal
);
const
tr
=
document
.
createElement
(
"tr"
);
streamBatchCount
+=
streamValue
;
batchCount
+=
batchValue
;
totalCount
+=
platformTotal
;
tr
.
innerHTML
=
`
<td class="platform">
${
platform
}
</td>
<td class="num
${
getNumClass
(
streamValue
)}
">
${
streamValue
}
</td>
<td class="num
${
getNumClass
(
batchValue
)}
">
${
batchValue
}
</td>
<td class="num
${
getNumClass
(
platformTotal
)}
">
${
platformTotal
}
</td>
<td><span class="
${
status
.
className
}
">
${
status
.
text
}
</span></td>
`
;
tbody
.
appendChild
(
tr
);
});
document
.
getElementById
(
"totalCount"
).
innerText
=
totalCount
;
document
.
getElementById
(
"streamBatchCount"
).
innerText
=
streamBatchCount
;
document
.
getElementById
(
"batchCount"
).
innerText
=
batchCount
;
document
.
getElementById
(
"commitCount"
).
innerText
=
Number
(
data
[
"geo:task_commit:list"
]
||
0
);
document
.
getElementById
(
"lastTime"
).
innerText
=
"最后刷新:"
+
new
Date
().
toLocaleString
();
}
function
getStatsItems
(
responseData
)
{
if
(
Array
.
isArray
(
responseData
))
{
return
responseData
;
}
if
(
responseData
&&
Array
.
isArray
(
responseData
.
data
))
{
return
responseData
.
data
;
}
return
[];
}
function
normalizeStats
(
responseData
)
{
return
getStatsItems
(
responseData
)
.
map
(
item
=>
({
time
:
normalizeStatDate
(
item
[
"时间"
]
||
item
.
time
||
item
.
date
||
item
.
begin
||
""
),
platform
:
item
[
"平台"
]
||
item
.
platform
||
item
.
name
||
"未知平台"
,
count
:
Number
(
item
[
"数量"
]
||
item
.
count
||
item
.
value
||
0
)
}))
.
filter
(
item
=>
item
.
time
&&
Number
.
isFinite
(
item
.
count
));
}
function
normalizeStatDate
(
value
)
{
return
String
(
value
||
""
).
slice
(
0
,
10
);
}
function
createSvgEl
(
tag
,
attrs
=
{})
{
const
el
=
document
.
createElementNS
(
"http://www.w3.org/2000/svg"
,
tag
);
Object
.
keys
(
attrs
).
forEach
(
key
=>
el
.
setAttribute
(
key
,
attrs
[
key
]));
return
el
;
}
function
renderPlatformTrend
(
responseData
)
{
const
chart
=
document
.
getElementById
(
"platformTrendChart"
);
const
summary
=
document
.
getElementById
(
"trendSummary"
);
const
statsNote
=
document
.
getElementById
(
"statsNote"
);
const
colors
=
[
"#2563eb"
,
"#16a34a"
,
"#f59e0b"
,
"#dc2626"
,
"#7c3aed"
,
"#0891b2"
,
"#db2777"
,
"#475569"
];
const
items
=
normalizeStats
(
responseData
);
chart
.
innerHTML
=
""
;
summary
.
innerHTML
=
""
;
if
(
!
items
.
length
)
{
statsNote
.
innerText
=
"暂无趋势数据"
;
chart
.
innerHTML
=
'<div class="chart-empty">当前时间范围暂无历史任务趋势数据</div>'
;
return
;
}
const
chartLabels
=
[...
new
Set
(
items
.
map
(
item
=>
item
.
time
))].
sort
();
const
cardLabels
=
[...
chartLabels
].
reverse
();
const
oldestDate
=
chartLabels
[
0
];
const
newestDate
=
chartLabels
[
chartLabels
.
length
-
1
];
const
platformTotals
=
new
Map
();
const
pointMap
=
new
Map
();
items
.
forEach
(
item
=>
{
platformTotals
.
set
(
item
.
platform
,
(
platformTotals
.
get
(
item
.
platform
)
||
0
)
+
item
.
count
);
const
pointKey
=
`
${
item
.
platform
}
__
${
item
.
time
}
`
;
pointMap
.
set
(
pointKey
,
(
pointMap
.
get
(
pointKey
)
||
0
)
+
item
.
count
);
});
const
platformList
=
[...
platformTotals
.
entries
()]
.
sort
((
a
,
b
)
=>
b
[
1
]
-
a
[
1
])
.
map
(([
platform
])
=>
platform
);
const
maxCount
=
Math
.
max
(...
pointMap
.
values
(),
1
);
const
platformColorMap
=
new
Map
(
platformList
.
map
((
platform
,
index
)
=>
[
platform
,
colors
[
index
%
colors
.
length
]]));
const
dayTotals
=
chartLabels
.
map
(
label
=>
({
date
:
label
,
total
:
platformList
.
reduce
((
sum
,
platform
)
=>
sum
+
(
pointMap
.
get
(
`
${
platform
}
__
${
label
}
`
)
||
0
),
0
)
}));
lastTrendData
=
{
labels
:
chartLabels
,
platformList
,
pointMap
,
platformColorMap
,
dayTotals
};
const
grid
=
document
.
createElement
(
"div"
);
grid
.
className
=
"daily-grid"
;
cardLabels
.
forEach
(
label
=>
{
const
dayItems
=
platformList
.
map
(
platform
=>
({
platform
,
value
:
pointMap
.
get
(
`
${
platform
}
__
${
label
}
`
)
||
0
,
color
:
platformColorMap
.
get
(
platform
)
}));
const
dayTotal
=
dayItems
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
value
,
0
);
const
card
=
document
.
createElement
(
"div"
);
const
head
=
document
.
createElement
(
"div"
);
const
date
=
document
.
createElement
(
"div"
);
const
total
=
document
.
createElement
(
"div"
);
const
list
=
document
.
createElement
(
"div"
);
card
.
className
=
"day-card"
;
head
.
className
=
"day-card-head"
;
date
.
className
=
"day-date"
;
total
.
className
=
"day-total"
;
list
.
className
=
"day-list"
;
date
.
innerText
=
label
;
total
.
innerText
=
`总量
${
dayTotal
}
`
;
head
.
appendChild
(
date
);
head
.
appendChild
(
total
);
dayItems
.
forEach
(
item
=>
{
const
row
=
document
.
createElement
(
"div"
);
const
name
=
document
.
createElement
(
"div"
);
const
bar
=
document
.
createElement
(
"div"
);
const
barInner
=
document
.
createElement
(
"div"
);
const
count
=
document
.
createElement
(
"div"
);
row
.
className
=
"day-row"
;
name
.
className
=
"day-platform"
;
bar
.
className
=
"day-bar"
;
barInner
.
className
=
"day-bar-inner"
;
count
.
className
=
"day-count"
;
name
.
innerText
=
item
.
platform
;
barInner
.
style
.
width
=
`
${
Math
.
max
(
2
,
Math
.
round
((
item
.
value
/
maxCount
)
*
100
))}
%`
;
barInner
.
style
.
background
=
item
.
color
;
count
.
innerText
=
item
.
value
;
bar
.
appendChild
(
barInner
);
row
.
appendChild
(
name
);
row
.
appendChild
(
bar
);
row
.
appendChild
(
count
);
list
.
appendChild
(
row
);
});
card
.
appendChild
(
head
);
card
.
appendChild
(
list
);
grid
.
appendChild
(
card
);
});
chart
.
appendChild
(
grid
);
renderTrendSummary
();
statsNote
.
innerText
=
`共
${
platformList
.
length
}
个平台,
${
oldestDate
}
至
${
newestDate
}
`
;
document
.
getElementById
(
"lastTime"
).
innerText
=
"最后刷新:"
+
new
Date
().
toLocaleString
();
}
function
renderTrendSummary
()
{
if
(
!
lastTrendData
)
{
return
;
}
const
summary
=
document
.
getElementById
(
"trendSummary"
);
const
titleText
=
trendChartMode
===
"total"
?
"每日总量趋势"
:
"各平台每日趋势"
;
const
series
=
trendChartMode
===
"total"
?
[{
name
:
"总量"
,
color
:
"#2563eb"
,
values
:
lastTrendData
.
dayTotals
.
map
(
item
=>
item
.
total
)
}]
:
lastTrendData
.
platformList
.
map
(
platform
=>
({
name
:
platform
,
color
:
lastTrendData
.
platformColorMap
.
get
(
platform
),
values
:
lastTrendData
.
labels
.
map
(
label
=>
lastTrendData
.
pointMap
.
get
(
`
${
platform
}
__
${
label
}
`
)
||
0
)
}));
const
maxValue
=
Math
.
max
(...
series
.
flatMap
(
item
=>
item
.
values
),
1
);
const
width
=
Math
.
max
(
summary
.
clientWidth
-
32
,
720
);
const
height
=
220
;
const
padding
=
{
top
:
26
,
right
:
18
,
bottom
:
40
,
left
:
52
};
const
plotWidth
=
width
-
padding
.
left
-
padding
.
right
;
const
plotHeight
=
height
-
padding
.
top
-
padding
.
bottom
;
const
xScale
=
index
=>
lastTrendData
.
labels
.
length
>
1
?
padding
.
left
+
(
plotWidth
/
(
lastTrendData
.
labels
.
length
-
1
))
*
index
:
padding
.
left
+
plotWidth
/
2
;
const
yScale
=
value
=>
padding
.
top
+
plotHeight
-
(
value
/
maxValue
)
*
plotHeight
;
const
head
=
document
.
createElement
(
"div"
);
const
title
=
document
.
createElement
(
"span"
);
const
actions
=
document
.
createElement
(
"div"
);
const
range
=
document
.
createElement
(
"span"
);
const
switchWrap
=
document
.
createElement
(
"div"
);
const
platformBtn
=
document
.
createElement
(
"button"
);
const
totalBtn
=
document
.
createElement
(
"button"
);
const
svg
=
createSvgEl
(
"svg"
,
{
viewBox
:
`0 0
${
width
}
${
height
}
`
,
role
:
"img"
});
const
tooltip
=
document
.
createElement
(
"div"
);
summary
.
innerHTML
=
""
;
head
.
className
=
"trend-summary-title"
;
title
.
className
=
"trend-summary-title-main"
;
actions
.
className
=
"trend-summary-actions"
;
range
.
className
=
"trend-summary-range"
;
switchWrap
.
className
=
"trend-switch"
;
platformBtn
.
className
=
`trend-switch-btn
${
trendChartMode
===
"platform"
?
"active"
:
""
}
`
;
totalBtn
.
className
=
`trend-switch-btn
${
trendChartMode
===
"total"
?
"active"
:
""
}
`
;
title
.
innerText
=
titleText
;
range
.
innerText
=
`
${
lastTrendData
.
labels
[
lastTrendData
.
labels
.
length
-
1
]}
至
${
lastTrendData
.
labels
[
0
]}
`
;
platformBtn
.
innerText
=
"平台"
;
totalBtn
.
innerText
=
"总量"
;
platformBtn
.
onclick
=
()
=>
setTrendChartMode
(
"platform"
);
totalBtn
.
onclick
=
()
=>
setTrendChartMode
(
"total"
);
switchWrap
.
appendChild
(
platformBtn
);
switchWrap
.
appendChild
(
totalBtn
);
head
.
appendChild
(
title
);
actions
.
appendChild
(
range
);
actions
.
appendChild
(
switchWrap
);
head
.
appendChild
(
actions
);
tooltip
.
className
=
"trend-tooltip"
;
for
(
let
i
=
0
;
i
<=
3
;
i
++
)
{
const
value
=
Math
.
round
((
maxValue
/
3
)
*
i
);
const
y
=
yScale
(
value
);
svg
.
appendChild
(
createSvgEl
(
"line"
,
{
x1
:
padding
.
left
,
y1
:
y
,
x2
:
width
-
padding
.
right
,
y2
:
y
,
stroke
:
i
===
0
?
"#cbd5e1"
:
"#e5e7eb"
,
"stroke-width"
:
"1"
}));
const
axisText
=
createSvgEl
(
"text"
,
{
x
:
padding
.
left
-
10
,
y
:
y
+
4
,
"text-anchor"
:
"end"
,
class
:
"trend-label"
});
axisText
.
textContent
=
value
;
svg
.
appendChild
(
axisText
);
}
series
.
forEach
(
line
=>
{
const
points
=
line
.
values
.
map
((
value
,
index
)
=>
({
date
:
lastTrendData
.
labels
[
index
],
value
,
x
:
xScale
(
index
),
y
:
yScale
(
value
)
}));
svg
.
appendChild
(
createSvgEl
(
"polyline"
,
{
points
:
points
.
map
(
point
=>
`
${
point
.
x
}
,
${
point
.
y
}
`
).
join
(
" "
),
fill
:
"none"
,
stroke
:
line
.
color
,
"stroke-width"
:
trendChartMode
===
"total"
?
"3"
:
"2"
,
"stroke-opacity"
:
trendChartMode
===
"total"
?
"1"
:
"0.78"
,
"stroke-linecap"
:
"round"
,
"stroke-linejoin"
:
"round"
}));
points
.
forEach
(
point
=>
{
svg
.
appendChild
(
createSvgEl
(
"circle"
,
{
cx
:
point
.
x
,
cy
:
point
.
y
,
r
:
trendChartMode
===
"total"
?
"5"
:
"3"
,
fill
:
line
.
color
,
stroke
:
"#fff"
,
"stroke-width"
:
"2"
}));
if
(
trendChartMode
===
"total"
)
{
const
totalText
=
createSvgEl
(
"text"
,
{
x
:
point
.
x
,
y
:
Math
.
max
(
14
,
point
.
y
-
10
),
"text-anchor"
:
"middle"
,
class
:
"trend-total-label"
});
totalText
.
textContent
=
point
.
value
;
svg
.
appendChild
(
totalText
);
}
});
});
lastTrendData
.
labels
.
forEach
((
label
,
index
)
=>
{
const
dateText
=
createSvgEl
(
"text"
,
{
x
:
xScale
(
index
),
y
:
height
-
12
,
"text-anchor"
:
"middle"
,
class
:
"trend-label"
});
dateText
.
textContent
=
label
.
slice
(
5
);
svg
.
appendChild
(
dateText
);
});
lastTrendData
.
labels
.
forEach
((
label
,
index
)
=>
{
const
left
=
index
===
0
?
padding
.
left
:
(
xScale
(
index
-
1
)
+
xScale
(
index
))
/
2
;
const
right
=
index
===
lastTrendData
.
labels
.
length
-
1
?
width
-
padding
.
right
:
(
xScale
(
index
)
+
xScale
(
index
+
1
))
/
2
;
const
hit
=
createSvgEl
(
"rect"
,
{
x
:
left
,
y
:
padding
.
top
,
width
:
Math
.
max
(
12
,
right
-
left
),
height
:
plotHeight
,
fill
:
"transparent"
,
"pointer-events"
:
"all"
,
class
:
"trend-hover-hit"
});
hit
.
addEventListener
(
"mouseenter"
,
event
=>
showTrendDayTooltip
(
event
,
label
));
hit
.
addEventListener
(
"mousemove"
,
moveTrendDayTooltip
);
hit
.
addEventListener
(
"mouseleave"
,
hideTrendDayTooltip
);
svg
.
appendChild
(
hit
);
});
summary
.
appendChild
(
head
);
summary
.
appendChild
(
svg
);
summary
.
appendChild
(
tooltip
);
}
function
setTrendChartMode
(
mode
)
{
trendChartMode
=
mode
;
renderTrendSummary
();
}
function
moveTrendDayTooltip
(
event
)
{
const
tooltip
=
document
.
querySelector
(
".trend-tooltip"
);
if
(
!
tooltip
)
{
return
;
}
const
offset
=
14
;
const
tooltipWidth
=
tooltip
.
offsetWidth
||
280
;
const
tooltipHeight
=
tooltip
.
offsetHeight
||
120
;
let
left
=
event
.
clientX
+
offset
;
let
top
=
event
.
clientY
+
offset
;
if
(
left
+
tooltipWidth
>
window
.
innerWidth
-
12
)
{
left
=
event
.
clientX
-
tooltipWidth
-
offset
;
}
if
(
top
+
tooltipHeight
>
window
.
innerHeight
-
12
)
{
top
=
event
.
clientY
-
tooltipHeight
-
offset
;
}
tooltip
.
style
.
left
=
`
${
Math
.
max
(
12
,
left
)}
px`
;
tooltip
.
style
.
top
=
`
${
Math
.
max
(
12
,
top
)}
px`
;
}
function
showTrendDayTooltip
(
event
,
date
)
{
const
tooltip
=
document
.
querySelector
(
".trend-tooltip"
);
if
(
!
tooltip
||
!
lastTrendData
)
{
return
;
}
const
dayItems
=
lastTrendData
.
platformList
.
map
(
platform
=>
({
platform
,
value
:
lastTrendData
.
pointMap
.
get
(
`
${
platform
}
__
${
date
}
`
)
||
0
,
color
:
lastTrendData
.
platformColorMap
.
get
(
platform
)
})).
sort
((
a
,
b
)
=>
b
.
value
-
a
.
value
);
const
dayTotal
=
dayItems
.
reduce
((
sum
,
item
)
=>
sum
+
item
.
value
,
0
);
tooltip
.
innerHTML
=
""
;
const
title
=
document
.
createElement
(
"div"
);
const
total
=
document
.
createElement
(
"div"
);
const
totalValue
=
document
.
createElement
(
"strong"
);
const
list
=
document
.
createElement
(
"div"
);
title
.
className
=
"trend-tooltip-title"
;
title
.
innerText
=
date
;
total
.
className
=
"trend-tooltip-total"
;
total
.
innerHTML
=
"<span>当天总量</span>"
;
totalValue
.
innerText
=
dayTotal
;
total
.
appendChild
(
totalValue
);
list
.
className
=
"trend-tooltip-list"
;
dayItems
.
forEach
(
item
=>
{
const
row
=
document
.
createElement
(
"div"
);
const
dot
=
document
.
createElement
(
"span"
);
const
name
=
document
.
createElement
(
"span"
);
const
count
=
document
.
createElement
(
"span"
);
row
.
className
=
"trend-tooltip-row"
;
dot
.
className
=
"trend-tooltip-dot"
;
dot
.
style
.
background
=
item
.
color
;
name
.
className
=
"trend-tooltip-name"
;
name
.
innerText
=
item
.
platform
;
count
.
className
=
"trend-tooltip-count"
;
count
.
innerText
=
item
.
value
;
row
.
appendChild
(
dot
);
row
.
appendChild
(
name
);
row
.
appendChild
(
count
);
list
.
appendChild
(
row
);
});
tooltip
.
appendChild
(
title
);
tooltip
.
appendChild
(
total
);
tooltip
.
appendChild
(
list
);
tooltip
.
style
.
display
=
"block"
;
moveTrendDayTooltip
(
event
);
}
function
hideTrendDayTooltip
()
{
const
tooltip
=
document
.
querySelector
(
".trend-tooltip"
);
if
(
tooltip
)
{
tooltip
.
style
.
display
=
"none"
;
}
}
function
toDateInputValue
(
date
)
{
const
offsetDate
=
new
Date
(
date
.
getTime
()
-
date
.
getTimezoneOffset
()
*
60000
);
return
offsetDate
.
toISOString
().
slice
(
0
,
10
);
}
function
setQuickRange
(
days
,
shouldLoad
=
true
)
{
const
beginInput
=
document
.
getElementById
(
"beginTime"
);
const
endInput
=
document
.
getElementById
(
"endTime"
);
const
now
=
new
Date
();
const
start
=
new
Date
(
now
);
start
.
setDate
(
start
.
getDate
()
-
days
+
1
);
start
.
setHours
(
0
,
0
,
0
,
0
);
beginInput
.
value
=
toDateInputValue
(
start
);
endInput
.
value
=
toDateInputValue
(
now
);
document
.
getElementById
(
"range3Btn"
).
classList
.
toggle
(
"active"
,
days
===
3
);
document
.
getElementById
(
"range7Btn"
).
classList
.
toggle
(
"active"
,
days
===
7
);
document
.
getElementById
(
"range30Btn"
).
classList
.
toggle
(
"active"
,
days
===
30
);
if
(
shouldLoad
&&
currentPanel
===
"trend"
)
{
loadPlatformStats
();
}
else
if
(
shouldLoad
)
{
loadedPanels
.
trend
=
false
;
}
}
function
clearQuickRangeActive
()
{
document
.
getElementById
(
"range3Btn"
).
classList
.
remove
(
"active"
);
document
.
getElementById
(
"range7Btn"
).
classList
.
remove
(
"active"
);
document
.
getElementById
(
"range30Btn"
).
classList
.
remove
(
"active"
);
}
function
handleDateRangeChange
()
{
clearQuickRangeActive
();
loadedPanels
.
trend
=
false
;
if
(
currentPanel
===
"trend"
)
{
loadPlatformStats
();
}
}
function
buildStatsUrl
()
{
const
begin
=
document
.
getElementById
(
"beginTime"
).
value
;
const
end
=
document
.
getElementById
(
"endTime"
).
value
;
if
(
!
begin
||
!
end
)
{
throw
new
Error
(
"请选择开始时间和结束时间"
);
}
if
(
begin
>
end
)
{
throw
new
Error
(
"开始时间不能晚于结束时间"
);
}
return
`
${
STATS_API_URL
}
?
${
new
URLSearchParams
({
begin
,
end
}).
toString
()}
`
;
}
async
function
fetchJson
(
url
)
{
const
res
=
await
fetch
(
url
,
{
method
:
"GET"
,
cache
:
"no-store"
});
if
(
!
res
.
ok
)
{
throw
new
Error
(
"接口请求失败,状态码:"
+
res
.
status
);
}
return
res
.
json
();
}
async
function
loadQueueStatus
()
{
hideError
();
try
{
renderData
(
await
fetchJson
(
API_URL
));
loadedPanels
.
queue
=
true
;
}
catch
(
e
)
{
showError
(
"队列数据加载失败:"
+
e
.
message
);
}
}
async
function
loadPlatformStats
()
{
const
statsNote
=
document
.
getElementById
(
"statsNote"
);
hideError
();
statsNote
.
innerText
=
"加载中"
;
trendChartMode
=
"platform"
;
setTrendLoading
(
true
);
try
{
renderPlatformTrend
(
await
fetchJson
(
buildStatsUrl
()));
loadedPanels
.
trend
=
true
;
}
catch
(
e
)
{
statsNote
.
innerText
=
"加载失败"
;
showError
(
"历史任务趋势加载失败:"
+
e
.
message
);
}
finally
{
setTrendLoading
(
false
);
}
}
document
.
getElementById
(
"beginTime"
).
addEventListener
(
"change"
,
handleDateRangeChange
);
document
.
getElementById
(
"endTime"
).
addEventListener
(
"change"
,
handleDateRangeChange
);
setQuickRange
(
3
,
false
);
loadQueueStatus
();
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