@@ -8,15 +8,17 @@
: root {
--bg : #f5f7fa ;
--panel : #ffffff ;
--panel-2 : #eef2f7 ;
--border : #d8dee6 ;
--text : #1f2933 ;
--muted : #6b7785 ;
--accent : #2f6fd1 ;
--accent-2 : #1f5fbf ;
--green : #1a9e54 ;
--red : #d64545 ;
--yellow : #b8860 b ;
--panel-2 : #f8f8f9 ;
--border : #e8eaec ;
--border-in : #dcdee2 ;
--text : #515a6e ;
--muted : #808695 ;
--accent : #2d8cf0 ;
--accent-hover : #57a3f3 ;
--accent-2 : #2487e8 ;
--green : #19be6 b ;
--red : #ed4014 ;
--yellow : #ff9900 ;
--mono : "SFMono-Regular" , Consolas , "Liberation Mono" , Menlo , monospace ;
}
* { box-sizing : border-box ; }
@@ -27,29 +29,30 @@
color : var ( - - text ) ;
font-size : 14 px ;
}
header {
padding : 16 px 24 px ;
border-bottom : 1 px solid var ( - - border ) ;
background : linear-gradient ( 180 deg , #eef2f7 , #f5f7fa ) ;
/* ===== Breadcrumb( View UI Breadcrumb 风格) ===== */
. breadcrumb {
padding : 14 px 24 px ; background : #fff ;
border-bottom : 1 px solid #e8eaec ;
font-size : 14 px ; color : #515a6e ;
}
header . sub . pt { color : var ( - - text ) ; font-weight : 600 ; }
header . sub { margin-top : 4 px ; color : var ( - - muted ) ; font-size : 12 px ; }
. breadcrumb a { color : #515a6e ; text-decoration : none ; transition : color .2 s ; }
. breadcrumb a : hover { color : #2d8cf0 ; }
. breadcrumb . sep { margin : 0 8 px ; color : #dcdee2 ; }
. breadcrumb . current { color : #515a6e ; }
/* ===== Tabs ===== */
/* ===== Tabs( View UI line 型:底部下划线激活) ===== */
. tabs {
display : flex ; gap : 4 px ; padding : 10 px 24 px 0 ;
border-bottom : 1 px solid var ( - - border ) ; background : #e9eef4 ;
display : flex ; gap : 24 px ; padding : 0 24 px ;
border-bottom : 1 px solid var ( - - border - in ) ; background : #fff ;
}
. tabs . tab {
padding : 9 px 20 px ; cursor : pointer ; font-size : 14 px ; color : var ( - - muted ) ;
border : 1 px solid transparent ; border-bottom : none ;
border-radius : 8 px 8 px 0 0 ; user-select : none ;
padding : 11 px 2 px ; cursor : pointer ; font-size : 14 px ; color : var ( - - muted ) ;
border-bottom : 2 px solid transparent ; user-select : none ; transition : color .2 s ;
}
. tabs . tab : hover { color : var ( - - text ) ; }
. tabs . tab : hover { color : var ( - - accent ) ; }
. tabs . tab . active {
color : var ( - - accent ) ; background : var ( - - panel ) ;
border-color : var ( - - border ) ; position : relative ; top : 1 px ;
border-bottom : 1 px solid var ( - - panel ) ;
color : var ( - - accent ) ; font-weight : 500 ;
border-bottom-color : var ( - - accent ) ;
}
. panel { display : none ; padding : 16 px 24 px ; }
. panel . active { display : block ; }
@@ -59,9 +62,9 @@
. left { width : 320 px ; flex : 0 0 320 px ; }
. right { flex : 1 1 auto ; min-width : 0 ; }
. card {
background : var ( - - panel ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 10 px ;
background : #fff ;
border-radius : 4 px ;
box-shadow : 0 1 px 1 px rgba ( 0 , 0 , 0 , .1 ) ; /* View UI Card */
padding : 16 px ;
}
. card + . card { margin-top : 16 px ; }
@@ -71,29 +74,32 @@
input [ type = text ] , input [ type = number ] {
width : 100 % ;
padding : 9 px 11 px ;
background : var ( - - panel -2 ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 8 px ;
background : #fff ;
border : 1 px solid #dcdee2 ;
border-radius : 4 px ;
color : var ( - - text ) ;
font-family : var ( - - mono ) ;
outline : none ;
transition : border-color .2 s , box-shadow .2 s ;
}
input [ type = text ] : focus , input [ type = number ] : focus { border-color : var ( - - accent ) ; }
input [ type = text ] : hover , input [ type = number ] : hover { border-color : #57a3f3 ; }
input [ type = text ] : focus , input [ type = number ] : focus { border-color : var ( - - accent ) ; box-shadow : 0 0 0 2 px rgba ( 45 , 140 , 240 , .2 ) ; outline : none ; }
. btn {
padding : 9 px 14 px ;
border : 1 px solid var ( - - border ) ;
background : var ( - - panel -2 ) ;
padding : 5 px 15 px ;
border : 1 px solid var ( - - border - in ) ;
background : #fff ;
color : var ( - - text ) ;
border-radius : 8 px ;
border-radius : 4 px ;
cursor : pointer ;
font-size : 13 px ;
transition : border-color .2 s , color .2 s , background .2 s ;
}
. btn : hover { border-color : var ( - - accent ) ; }
. btn . primary { background : var ( - - accent -2 ) ; border-color : var ( - - accent ) ; color : #fff ; }
. btn . primary : hover { background : var ( - - accent ) ; }
. btn . danger { background : transparent ; border-color : var ( - - red ) ; color : var ( - - red ) ; }
. btn . danger : hover { background : rgba ( 255 , 107 , 107 , .12 ) ; }
. btn . mini { padding : 3 px 10 px ; font-size : 12 px ; border-radius : 6 px ; }
. btn : hover { border-color : var ( - - accent ) ; color : var ( - - accent ) ; }
. btn . primary { background : var ( - - accent ) ; border-color : var ( - - accent ) ; color : #fff ; }
. btn . primary : hover { background : var ( - - accent - hover ) ; border-color : var ( - - accent - hover ) ; color : #fff ; }
. btn . danger { background : #fff ; border-color : var ( - - red ) ; color : var ( - - red ) ; }
. btn . danger : hover { background : rgba ( 237 , 64 , 20 , .08 ) ; border-color : var ( - - red ) ; color : var ( - - red ) ; }
. btn . mini { padding : 2 px 10 px ; font-size : 12 px ; border-radius : 4 px ; }
. btn-row { display : flex ; gap : 8 px ; flex-wrap : wrap ; }
. hint { color : var ( - - muted ) ; font-size : 11 px ; margin-top : 6 px ; }
@@ -109,8 +115,8 @@
overflow : hidden ;
text-overflow : ellipsis ;
}
. keys-list . k : hover { background : var ( - - panel -2 ) ; }
. keys-list . k . active { background : rgba ( 76 , 154 , 255 , .15 ) ; border-left : 3 px solid var ( - - accent ) ; padding-left : 7 px ; }
. keys-list . k : hover { background : #f3f3f3 ; }
. keys-list . k . active { background : #f0faff ; }
. keys-list . k . kname {
display : block ; color : var ( - - accent ) ; font-size : 11 px ; margin-bottom : 2 px ;
overflow : hidden ; text-overflow : ellipsis ; white-space : nowrap ;
@@ -123,28 +129,30 @@
. meta . mk { color : var ( - - muted ) ; font-size : 12 px ; }
. meta . mv { font-family : var ( - - mono ) ; word-break : break-all ; }
. badge {
display : inline-block ; padding : 2 px 8 px ; border-radius : 999 px ;
font-size : 11 px ; border : 1 px solid var ( - - border ) ; color : var ( - - muted ) ;
display : inline-block ; padding : 0 8 px ; height : 22 px ; line-height : 20 px ;
border-radius : 4 px ; font-size : 12 px ; border : 1 px solid #e8eaec ; color : var ( - - muted ) ; background : #fff ;
}
. badge . string { color : var ( - - green ) ; border-color : rgba ( 54 , 194 , 117 , .4 ) ; }
. badge . hash { color : var ( - - accent ) ; border-color : rgba ( 76 , 154 , 255 , .4 ) ; }
. badge . list , . badge . set , . badge . zset { color : var ( - - yellow ) ; border-color : rgba ( 247 , 201 , 72 , .4 ) ; }
. badge . string { color : var ( - - green ) ; border-color : rgba ( 25 , 190 , 107 , .4 ) ; background : rgba ( 25 , 190 , 107 , .1 ) ; }
. badge . hash { color : var ( - - accent ) ; border-color : rgba ( 45 , 140 , 240 , .4 ) ; background : rgba ( 45 , 140 , 240 , .1 ) ; }
. badge . list , . badge . set , . badge . zset { color : var ( - - yellow ) ; border-color : rgba ( 255 , 153 , 0 , .4 ) ; background : rgba ( 255 , 153 , 0 , .1 ) ; }
. toolbar { display : flex ; gap : 8 px ; align-items : center ; margin-bottom : 12 px ; flex-wrap : wrap ; }
. count { color : var ( - - muted ) ; font-size : 12 px ; }
select {
padding : 7 px 10 px ;
background : var ( - - panel -2 ) ;
border : 1 px solid var ( - - border ) ;
border-radius : 8 px ;
background : #fff ;
border : 1 px solid #dcdee2 ;
border-radius : 4 px ;
color : var ( - - text ) ;
font-size : 13 px ;
outline : none ;
cursor : pointer ;
transition : border-color .2 s , box-shadow .2 s ;
}
select : focus { border-color : var ( - - accent ) ; }
select : hover { border-color : #57a3f3 ; }
select : focus { border-color : var ( - - accent ) ; box-shadow : 0 0 0 2 px rgba ( 45 , 140 , 240 , .2 ) ; }
pre . json {
margin : 0 ; padding : 12 px ; background : #f0f3f7 ; border : 1 px solid var ( - - border ) ;
border-radius : 8 px ; overflow : auto ; max-height : 60 vh ; font-family : var ( - - mono ) ;
margin : 0 ; padding : 12 px ; background : #f8f8f9 ; border : 1 px solid #e8eaec ;
border-radius : 4 px ; overflow : auto ; max-height : 60 vh ; font-family : var ( - - mono ) ;
font-size : 12 px ; line-height : 1.5 ; color : #1f2933 ; white-space : pre ;
}
pre . json . jk { color : #c0392b ; } /* key */
@@ -161,65 +169,135 @@
code { background : var ( - - panel -2 ) ; padding : 1 px 5 px ; border-radius : 4 px ; font-family : var ( - - mono ) ; }
/* ===== 日志表格 ===== */
. filter-bar { display : flex ; gap : 10 px ; flex-wrap : wrap ; align-items : flex-end ; margin-bottom : 14 px ; }
. filter-bar . fi { display : flex ; flex-direction : column ; gap : 4 px ; }
. filter-bar . fi label { margin-bottom : 0 ; }
. filter-bar . fi input [ type = text ] , . filter-bar . fi input [ type = number ] { width : 170 px ; padding : 7 px 10 px ; font-size : 13 px ; }
. filter-bar . fi select { min-width : 110 px ; }
/* 筛选工具栏(参考页面风格:白底圆角卡、一行内联 + 横向滚动) */
. filter-bar { display : flex ; gap : 8 px ; flex-wrap : nowrap ; align-items : center ; background : var ( - - panel ) ; border : 1 px solid #e8eaec ; border-radius : 4 px ; padding : 8 px 12 px ; margin-bottom : 14 px ; overflow-x : auto ; }
. filter-bar . fi { display : flex ; align-items : center ; gap : 5 px ; flex-shrink : 0 ; }
. filter-bar . fi label { margin : 0 ; color : #515a6e ; font-size : 12 px ; white-space : nowrap ; }
. filter-bar . fi input [ type = text ] , . filter-bar . fi input [ type = number ] { width : 105 px ; padding : 5 px 8 px ; font-size : 12 px ; border : 1 px solid #dcdee2 ; border-radius : 4 px ; background : #fff ; color : var ( - - text ) ; outline : none ; transition : border-color .2 s , box-shadow .2 s ; }
. filter-bar . fi input [ type = text ] : hover , . filter-bar . fi input [ type = number ] : hover { border-color : #57a3f3 ; }
. filter-bar . fi input [ type = text ] : focus , . filter-bar . fi input [ type = number ] : focus { border-color : var ( - - accent ) ; box-shadow : 0 0 0 2 px rgba ( 45 , 140 , 240 , .2 ) ; }
. filter-bar . fi select { min-width : 80 px ; padding : 5 px 8 px ; border : 1 px solid #dcdee2 ; border-radius : 4 px ; background : #fff ; font-size : 12 px ; color : var ( - - text ) ; transition : border-color .2 s , box-shadow .2 s ; }
. filter-bar . fi select : hover { border-color : #57a3f3 ; }
. filter-bar . fi select : focus { border-color : var ( - - accent ) ; box-shadow : 0 0 0 2 px rgba ( 45 , 140 , 240 , .2 ) ; }
/* 搜索框左侧放大镜图标 */
. filter-bar . fi . search { position : relative ; }
. filter-bar . fi . search input [ type = text ] { padding-left : 26 px ; }
. filter-bar . fi . search :: before {
content : "\1F50D" ; /* 🔍 */
position : absolute ; left : 9 px ; top : 50 % ; transform : translateY ( -50 % ) ;
color : var ( - - muted ) ; font-size : 13 px ; pointer-events : none ;
}
. filter-bar > . btn . primary { margin-left : auto ; flex-shrink : 0 ; } /* 查询按钮置右 */
table . log { width : 100 % ; border-collapse : collapse ; font-size : 12.5 px ; }
table . log th , table . log td {
padding : 8 px 10 px ; border-bottom : 1 px solid var ( - - border ) ;
padding : 9 px 12 px ; border-bottom : 1 px solid #e8eaec ;
text-align : left ; vertical-align : top ;
}
table . log th { color : var ( - - muted ) ; font-weight : 500 ; white-space : nowrap ; background : var ( - - panel -2 ) ; position : sticky ; top : 0 ; }
table . log tr : hover td { background : rgba ( 76 , 154 , 255 , .05 ) ; }
table . log th { color : #515a6e ; font-weight : 600 ; white-space : nowrap ; background : #f8f8f9 ; position : sticky ; top : 0 ; }
table . log td { color : #515a6e ; }
table . log tr : hover td { background : #ebf7ff ; }
table . log td . mono { font-family : var ( - - mono ) ; font-size : 12 px ; }
table . log td . cell-clip {
display : inline-block ; max-width : 280 px ; overflow : hidden ;
text-overflow : ellipsis ; white-space : nowrap ; vertical-align : bottom ;
}
. table-wrap { overflow : auto ; max-height : 62 vh ; border : 1 px solid var ( - - border ) ; border-radius : 8 px ; }
. tag { display : inline-block ; padding : 1 px 8 px ; border-radius : 999 px ; font-size : 11 px ; border : 1 px solid ; white-space : nowrap ; }
. tag . green { color : var ( - - green ) ; border-color : rgba ( 54 , 194 , 117 , .4 ) ; }
. tag . red { color : var ( - - red ) ; border-color : rgba ( 255 , 107 , 107 , .4 ) ; }
. tag . yellow { color : var ( - - yellow ) ; border-color : rgba ( 247 , 201 , 72 , .4 ) ; }
. tag . blue { color : var ( - - accent ) ; border-color : rgba ( 76 , 154 , 255 , .4 ) ; }
. tag . gray { color : var ( - - muted ) ; border-color : var ( - - border ) ; }
. pager { display : flex ; gap : 10 px ; align-items : center ; margin-top : 12 px ; flex-wrap : wrap ; }
. pager . pinfo { color : var ( - - muted ) ; font-size : 12 px ; }
. row-actions { display : flex ; gap : 6 px ; white-space : nowrap ; }
. table-wrap { overflow : auto ; max-height : 62 vh ; border : 1 px solid #e8eaec ; border-radius : 4 px ; }
. tag { display : inline-block ; padding : 0 8 px ; height : 22 px ; line-height : 20 px ; border-radius : 4 px ; font-size : 12 px ; border : 1 px solid ; white-space : nowrap ; }
. tag . green { color : var ( - - green ) ; border-color : rgba ( 25 , 190 , 107 , .4 ) ; background : rgba ( 25 , 190 , 107 , .1 ) ; }
. tag . red { color : var ( - - red ) ; border-color : rgba ( 237 , 64 , 20 , .4 ) ; background : rgba ( 237 , 64 , 20 , .1 ) ; }
. tag . yellow { color : var ( - - yellow ) ; border-color : rgba ( 255 , 153 , 0 , .4 ) ; background : rgba ( 255 , 153 , 0 , .1 ) ; }
. tag . blue { color : var ( - - accent ) ; border-color : rgba ( 45 , 140 , 240 , .4 ) ; background : rgba ( 45 , 140 , 240 , .1 ) ; }
. tag . gray { color : var ( - - muted ) ; border-color : #e8eaec ; background : #f8f8f9 ; }
/* 分页条:View UI 风格页码按钮,靠右 */
. pager { display : flex ; gap : 8 px ; align-items : center ; justify-content : flex-end ; margin-top : 14 px ; flex-wrap : wrap ; color : #808695 ; font-size : 13 px ; }
. pager . pinfo { color : #808695 ; margin-right : 4 px ; }
. pager . pg { display : inline-flex ; gap : 4 px ; }
. pager . pg-btn { min-width : 32 px ; height : 32 px ; padding : 0 6 px ; border : 1 px solid #dcdee2 ; border-radius : 4 px ; background : #fff ; color : #515a6e ; cursor : pointer ; font-size : 13 px ; text-align : center ; line-height : 30 px ; transition : border-color .2 s , color .2 s ; }
. pager . pg-btn : hover : not ( : disabled ) { border-color : #2d8cf0 ; color : #2d8cf0 ; }
. pager . pg-btn . active { border-color : #2d8cf0 ; color : #2d8cf0 ; font-weight : 600 ; }
. pager . pg-btn : disabled { opacity : .5 ; cursor : not-allowed ; }
. pager . pg-ellipsis { line-height : 32 px ; padding : 0 2 px ; color : #c5c8ce ; }
. pager . size { display : inline-flex ; align-items : center ; gap : 4 px ; }
. pager . size select { padding : 5 px 8 px ; border : 1 px solid #dcdee2 ; border-radius : 4 px ; background : #fff ; font-size : 13 px ; color : #515a6e ; }
. row-actions { display : flex ; gap : 4 px ; white-space : nowrap ; }
. row-actions . btn . mini { padding : 2 px 8 px ; border : 1 px solid transparent ; border-radius : 4 px ; background : none ; color : #2d8cf0 ; font-size : 12 px ; }
. row-actions . btn . mini : hover { border-color : #2d8cf0 ; background : rgba ( 45 , 140 , 240 , .06 ) ; }
/* ===== 详情弹窗 ===== */
/* ===== 详情弹窗( View UI Modal 风格) ===== */
. modal-mask {
position : fixed ; inset : 0 ; background : rgba ( 0 , 0 , 0 , .55 ) ;
position : fixed ; inset : 0 ; background : rgba ( 55 , 55 , 55 , .6 ) ;
display : none ; z-index : 90 ; align-items : center ; justify-content : center ;
}
. modal-mask . show { display : flex ; }
. modal {
width : min ( 860 px , 92 vw ) ; max-height : 84 vh ; overflow : auto ;
background : var ( - - panel ) ; border : 1 px solid var ( - - border ) ;
border-radius : 12 px ; padding : 18 px 20 px ;
width : min ( 860 px , 92 vw ) ; max-height : 84 vh ; overflow : hidden ;
background : #fff ; border-radius : 6 px ;
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , .2 ) ;
display : flex ; flex-direction : column ;
}
. modal h3 { margin : 0 0 12 px ; font-size : 15 px ; }
. modal . modal-header {
padding : 13 px 16 px ; border-bottom : 1 px solid #e8eaec ;
display : flex ; align-items : center ; justify-content : space-between ; flex : 0 0 auto ;
}
. modal . modal-header h3 { margin : 0 ; font-size : 14 px ; font-weight : 600 ; color : #17233d ; }
. modal . mclose {
border : none ; background : none ; cursor : pointer ;
font-size : 20 px ; line-height : 1 ; color : #808695 ; padding : 0 4 px ; border-radius : 4 px ;
transition : color .2 s ;
}
. modal . mclose : hover { color : #2d8cf0 ; }
. modal . modal-body { padding : 16 px ; overflow : auto ; flex : 1 1 auto ; }
. modal . kv { display : grid ; grid-template-columns : 110 px 1 fr ; gap : 8 px 14 px ; margin-bottom : 12 px ; }
. modal . kv . k { color : var ( - - muted ) ; font-size : 12 px ; }
. modal . kv . v { font-family : var ( - - mono ) ; font-size : 12.5 px ; word-break : break-all ; white-space : pre-wrap ; }
. modal . mclose { float : right ; }
. modal . kv . v . scroll { max-height : 160 px ; overflow-y : auto ; padding : 6 px 8 px ; background : #f8f8f9 ; border : 1 px solid #e8eaec ; border-radius : 4 px ; }
. modal . modal-actions { display : none ; gap : 8 px ; justify-content : flex-end ; padding : 12 px 16 px ; border-top : 1 px solid #e8eaec ; flex : 0 0 auto ; }
. toast {
position : fixed ; left : 50 % ; bottom : 28 px ; transform : translateX ( -50 % ) ;
background : #fdecec ; border : 1 px solid var ( - - red ) ; color : #a02020 ;
padding : 10 px 16 px ; border-radius : 8 px ; font-size : 13 px ; max-width : 80 vw ;
display : none ; z-index : 100 ;
/* 确认弹窗(View UI Modal.confirm 风格:居中 + 警示图标) */
. confirm-mask {
position : fixed ; inset : 0 ; z-index : 120 ; display : none ;
background : rgba ( 55 , 55 , 55 , .6 ) ;
align-items : flex-start ; justify-content : center ;
}
. toast . ok { background : #e7f6ed ; border-color : var ( - - green ) ; color : #1a7a40 ; }
. toast . show { display : block ; }
. confirm-mask . show { display : flex ; }
. confirm-box {
margin-top : 20 vh ; width : min ( 416 px , 92 vw ) ;
background : #fff ; border-radius : 6 px ; padding : 20 px 24 px 16 px ;
box-shadow : 0 2 px 8 px rgba ( 0 , 0 , 0 , .2 ) ;
}
. confirm-body { display : flex ; gap : 12 px ; align-items : flex-start ; margin-bottom : 18 px ; }
. confirm-icon {
flex : 0 0 32 px ; width : 32 px ; height : 32 px ; border-radius : 50 % ;
background : #ff9900 ; color : #fff ; font-size : 20 px ; font-weight : 700 ;
display : flex ; align-items : center ; justify-content : center ; margin-top : 2 px ;
}
. confirm-msg { font-size : 14 px ; line-height : 1.6 ; color : #515a6e ; }
. confirm-btns { display : flex ; justify-content : flex-end ; gap : 8 px ; }
/* 消息提示(View UI Message 风格:顶部居中白卡 + 彩色图标) */
. toast , . toast-top {
position : fixed ; top : 16 px ; left : 50 % ; transform : translateX ( -50 % ) ;
background : #fff ; border-radius : 4 px ;
box-shadow : 0 1 px 6 px rgba ( 0 , 0 , 0 , .2 ) ;
padding : 10 px 16 px ; font-size : 14 px ; color : #515a6e ; max-width : 80 vw ;
display : none ; z-index : 100 ; align-items : center ; gap : 8 px ;
}
. toast . show , . toast-top . show { display : flex ; }
. toast :: before , . toast-top :: before {
content : '' ; display : inline-block ; flex : 0 0 16 px ; width : 16 px ; height : 16 px ;
border-radius : 50 % ; font-size : 11 px ; line-height : 16 px ; text-align : center ; color : #fff ;
}
. toast . show :: before , . toast-top . err . show :: before { content : '✕' ; background : #ed4014 ; }
. toast . ok . show :: before , . toast-top . show :: before { content : '✓' ; background : #19be6b ; }
. toast-top { z-index : 130 ; }
< / style >
< / head >
< body >
< header >
< div class = "sub " > < b class = "pt " > 运维工作台: < / b > 接口日志 / 同步日志 / 缓存管理。仅超级管理员可访问,部署后同源访问自动带登录态。 < / div >
< / header >
< div class = "breadcrumb" >
< a href = "# " > 首页 < / a > < span class = "se p" > / < / span >
< a href = "#" > 运维管理 < / a > < span class = "sep" > / < / span >
< span class = "current" id = "bcCurrent" > 接口日志< / span >
< / div >
< div class = "tabs" >
< div class = "tab active" data-tab = "oplog" > 接口日志< / div >
@@ -268,12 +346,9 @@
< / table >
< / div >
< div class = "pager" >
< button class = "btn mini " id = "op_prev " > 上一页 < / button >
< button class = "btn mini " id = "op_next " > 下一页 < / button >
< span class = "pinfo" id = "op_pinfo " > < / span >
< span style = "flex:1" > < / span >
< label style = "margin:0" > 每页< / label >
< select id = "op_size" > < option > 10< / option > < option selected > 20< / option > < option > 50< / option > < option > 100< / option > < / select >
< span class = "pinfo " id = "op_pinfo " > 共 0 条 < / span >
< span class = "pg " id = "op_pg " > < / span >
< span class = "size" > 每页 < select id = "op_size " > < option > 10 < / option > < option selected > 20 < / option > < option > 50 < / option > < option > 100 < / option > < / select > 条 < / span >
< / div >
< / div >
< / div >
@@ -295,7 +370,7 @@
< / select >
< / div >
< div class = "fi" > < label > 大类名称< / label > < input type = "text" id = "sy_syncTypeName" placeholder = "如 同步 / 推送" / > < / div >
< div class = "fi" > < label > 类名.方法名< / label > < input type = "text" id = "sy_classMethod" placeholder = "如 Sync UserBaseService.addGroup " / > < / div >
< div class = "fi" > < label > 类名.方法名< / label > < input type = "text" id = "sy_classMethod" placeholder = "如 Send UserBaseService.deleteTeachers " / > < / div >
< div class = "fi" > < label > 执行状态< / label >
< select id = "sy_status" >
< option value = "" > 全部< / option >
@@ -319,12 +394,9 @@
< / table >
< / div >
< div class = "pager" >
< button class = "btn mini " id = "sy_prev " > 上一页 < / button >
< button class = "btn mini " id = "sy_next " > 下一页 < / button >
< span class = "pinfo" id = "sy_pinfo " > < / span >
< span style = "flex:1" > < / span >
< label style = "margin:0" > 每页< / label >
< select id = "sy_size" > < option > 10< / option > < option selected > 20< / option > < option > 50< / option > < option > 100< / option > < / select >
< span class = "pinfo " id = "sy_pinfo " > 共 0 条 < / span >
< span class = "pg " id = "sy_pg " > < / span >
< span class = "size" > 每页 < select id = "sy_size " > < option > 10 < / option > < option selected > 20 < / option > < option > 50 < / option > < option > 100 < / option > < / select > 条 < / span >
< / div >
< / div >
< / div >
@@ -386,13 +458,33 @@
<!-- ==================== 日志详情弹窗 ==================== -->
< div class = "modal-mask" id = "modalMask" >
< div class = "modal" >
< button class = "btn mini mclose" id = "modalClose" > 关闭 < / button >
< h3 id = "modalTitle" > 日志详情< / h3 >
< div class = "kv " id = "modalKv " > < / div >
< div class = "modal-header" >
< h3 id = "modalTitle" > 日志详情< / h3 >
< button class = "mclose " id = "modalClose" title = "关闭 "> × < / button >
< / div >
< div class = "modal-body" >
< div class = "kv" id = "modalKv" > < / div >
< / div >
< div class = "modal-actions" id = "modalActions" > < / div >
< / div >
< / div >
<!-- ==================== 顶部确认弹窗(重试确认等) ==================== -->
< div class = "confirm-mask" id = "confirmMask" >
< div class = "confirm-box" >
< div class = "confirm-body" >
< span class = "confirm-icon" > !< / span >
< div class = "confirm-msg" id = "confirmMsg" > < / div >
< / div >
< div class = "confirm-btns" >
< button class = "btn" id = "confirmCancel" > 取消< / button >
< button class = "btn primary" id = "confirmOk" > 确定< / button >
< / div >
< / div >
< / div >
< div class = "toast" id = "toast" > < / div >
< div class = "toast-top" id = "toastTop" > < / div >
< script >
// ==================== 公共 ====================
@@ -404,25 +496,18 @@
const API = resolveApi ( ) ;
// ==================== 登录态校验 ====================
// CAS 登录页基础地址;service 指回当前 work-view.html 完整地址,登录成功后会跳回本页
const CAS _LOGIN = 'http://sso.yunhaixingtong.com/cas/front/index.html' ;
function casLoginUrl ( ) {
const service = location . origin + location . pathname ; // 例如 http://192.168.10.166:8086/work-view.html
return CAS _LOGIN + '?service=' + encodeURIComponent ( service ) ;
}
// 主动校验是否登录:调用 /api/GetUserInfo,返回 200 视为已登录;其余(含网络异常)一律跳转 CAS 登录页
// 通过后端 checkLogin 接口完成登录校验:已登录则后端 302 重定向回 work-view.html;
// 未登录则由 CAS 拦截器 302 到 CAS 登录页,登录成功后再回跳本页。
// 用 sessionStorage 标记"已完成校验",避免重定向回本页后再次跳转造成死循环。
let loginChecked = false ;
async function checkLogin ( ) {
if ( loginChecked ) return true ;
try {
// redirect:'manual' 防止浏览器自动跟随 302 跳到 CAS 登录页(否则跟随后 status 会变成 200 造成误判)
const res = await fetch ( API + '/GetUserInfo' , {
method : 'GET' , credentials : 'include' , headers : authHeaders ( ) , redirect : 'manual'
} ) ;
if ( res . status === 200 ) { loginChecked = true ; return true ; }
} catch ( e ) { /* 网络异常视为未登录 */ }
// 登录失败:跳转 CAS 登录页(登录成功会带着 service 跳回本页)
location . replace ( casLoginUrl ( ) ) ;
if ( sessionStorage . getItem ( 'wbLoginChecked' ) === '1' ) {
loginChecked = true ;
return true ;
}
sessionStorage . setItem ( 'wbLoginChecked' , '1' ) ;
location . replace ( API + '/checkLogin' ) ;
return false ;
}
@@ -446,6 +531,15 @@
t . _timer = setTimeout ( ( ) => t . classList . remove ( 'show' ) , 3500 ) ;
}
// 顶部提示(重试结果等,显示在页面顶部)
function toastTop ( msg , ok ) {
const t = $ ( 'toastTop' ) ;
t . textContent = msg ;
t . className = 'toast-top show' + ( ok ? '' : ' err' ) ;
clearTimeout ( t . _timer ) ;
t . _timer = setTimeout ( ( ) => t . classList . remove ( 'show' ) , 3500 ) ;
}
function showLoading ( btn , text ) {
btn . dataset . old = btn . textContent ;
btn . innerHTML = '<span class="spinner"></span>' + text ;
@@ -535,23 +629,38 @@
}
// ==================== Tab 切换 ====================
// 记住当前 Tab 与各表格页码(sessionStorage,点击刷新后恢复)
function saveState ( key , val ) {
try { sessionStorage . setItem ( key , String ( val ) ) ; } catch ( e ) { }
}
function loadState ( key ) {
try { return sessionStorage . getItem ( key ) ; } catch ( e ) { return null ; }
}
function savedPage ( prefix ) {
const v = parseInt ( loadState ( 'wbPage_' + prefix ) , 10 ) ;
return isNaN ( v ) || v < 1 ? 1 : v ;
}
const loadedTabs = { } ;
function switchTab ( name ) {
saveState ( 'wbTab' , name ) ; // 记住点击/切换时的 Tab
document . querySelectorAll ( '.tabs .tab' ) . forEach ( t => t . classList . toggle ( 'active' , t . dataset . tab === name ) ) ;
document . querySelectorAll ( '.panel' ) . forEach ( p => p . classList . toggle ( 'active' , p . id === 'panel-' + name ) ) ;
// 首次切到日志页时自动加载
// 同步面包屑末级文字
const bcMap = { oplog : '接口日志' , synclog : '同步日志' , cache : '缓存管理' } ;
const bcEl = document . getElementById ( 'bcCurrent' ) ;
if ( bcEl ) bcEl . textContent = bcMap [ name ] || '' ;
// 首次切到日志页时自动加载(用记住的页码)
if ( ! loadedTabs [ name ] ) {
loadedTabs [ name ] = true ;
if ( name === 'oplog' ) { loadSchoolOptions ( ) ; loadOpLog ( 1 ) ; }
if ( name === 'synclog' ) { loadSchoolOptions ( ) ; loadSyncLog ( 1 ) ; }
if ( name === 'oplog' ) { loadSchoolOptions ( ) ; loadOpLog ( savedPage ( 'op' ) ) ; }
if ( name === 'synclog' ) { loadSchoolOptions ( ) ; loadSyncLog ( savedPage ( 'sy' ) ) ; }
}
}
document . querySelectorAll ( '.tabs .tab' ) . forEach ( t => { t . onclick = ( ) => switchTab ( t . dataset . tab ) ; } ) ;
// ==================== 通用分页表格状态 ====================
function makePager ( prefix , loader ) {
$ ( prefix + '_prev' ) . onclick = ( ) => loader ( state [ prefix ] . page - 1 ) ;
$ ( prefix + '_next' ) . onclick = ( ) => loader ( state [ prefix ] . page + 1 ) ;
state [ prefix ] . loader = loader ;
$ ( prefix + '_size' ) . onchange = ( ) => loader ( 1 ) ;
}
const state = {
@@ -559,22 +668,54 @@
sy : { page : 1 , totalPages : 1 }
} ;
// View UI 风格分页:共N条 + 页码按钮(含省略号) + 上一页/下一页
function renderPager ( prefix , pageVO ) {
const s = state [ prefix ] ;
s . page = pageVO . current || 1 ;
s . totalPages = pageVO . totalPages || 1 ;
$ ( prefix + '_pinfo' ) . textContent =
'第 ' + s . page + ' / ' + s . totalPages + ' 页 · 共 ' + ( pageVO . total || 0 ) + ' 条' ;
$ ( prefix + '_prev ' ) . disabled = s . page <= 1 ;
$ ( prefix + '_next' ) . disabled = s . page >= s . totalPages ;
saveState ( 'wbPage_' + prefix , s . page ) ; // 记住当前页码,刷新后恢复
$ ( prefix + '_pinfo' ) . textContent = '共 ' + ( pageVO . total || 0 ) + ' 条' ;
const el = $ ( prefix + '_pg ' ) ;
if ( ! el ) return ;
const cur = s . page , total = s . totalPages ;
let html = '<button class="pg-btn" data-p="' + ( cur - 1 ) + '"' + ( cur <= 1 ? ' disabled' : '' ) + '>‹ </button>' ;
const pages = [ ] ;
if ( total <= 7 ) {
for ( let i = 1 ; i <= total ; i ++ ) pages . push ( i ) ;
} else {
pages . push ( 1 ) ;
if ( cur > 3 ) pages . push ( - 1 ) ; // 省略号
for ( let i = Math . max ( 2 , cur - 1 ) ; i <= Math . min ( total - 1 , cur + 1 ) ; i ++ ) pages . push ( i ) ;
if ( cur < total - 2 ) pages . push ( - 2 ) ; // 省略号
pages . push ( total ) ;
}
pages . forEach ( p => {
if ( p < 0 ) html += '<span class="pg-ellipsis">…</span>' ;
else html += '<button class="pg-btn' + ( p === cur ? ' active' : '' ) + '" data-p="' + p + '">' + p + '</button>' ;
} ) ;
html += '<button class="pg-btn" data-p="' + ( cur + 1 ) + '"' + ( cur >= total ? ' disabled' : '' ) + '>› </button>' ;
el . innerHTML = html ;
const loader = state [ prefix ] . loader ;
el . querySelectorAll ( 'button.pg-btn[data-p]' ) . forEach ( b => {
b . onclick = ( ) => { if ( loader ) loader ( Number ( b . dataset . p ) ) ; } ;
} ) ;
}
// ==================== 详情弹窗 ====================
function openModal ( title , rows ) {
function openModal ( title , rows , actionsHtml , retryHandler ) {
$ ( 'modalTitle' ) . textContent = title ;
$ ( 'modalKv' ) . innerHTML = rows . map ( r =>
'<div class="k">' + escHtml ( r [ 0 ] ) + '</div><div class="v">' + escHtml ( r [ 1 ] == null || r [ 1 ] === '' ? '-' : r [ 1 ] ) + '</div> '
'<div class="k">' + escHtml ( r [ 0 ] ) + '</div>' +
'<div class="v' + ( r [ 2 ] ? ' scroll' : '' ) + '">' + escHtml ( r [ 1 ] == null || r [ 1 ] === '' ? '-' : r [ 1 ] ) + '</div>'
) . join ( '' ) ;
const act = $ ( 'modalActions' ) ;
act . innerHTML = actionsHtml || '' ;
act . style . display = actionsHtml ? 'flex' : 'none' ;
act . querySelectorAll ( 'button[data-a]' ) . forEach ( b => {
b . onclick = ( ) => {
if ( b . dataset . a === 'retry' ) ( retryHandler || doSyncRetry ) ( Number ( b . dataset . id ) ) ;
} ;
} ) ;
$ ( 'modalMask' ) . classList . add ( 'show' ) ;
}
$ ( 'modalClose' ) . onclick = ( ) => $ ( 'modalMask' ) . classList . remove ( 'show' ) ;
@@ -661,7 +802,6 @@
'<td><div class="row-actions">' +
'<button class="btn mini" data-a="detail" data-id="' + r . id + '">详情</button>' +
'<button class="btn mini" data-a="download" data-id="' + r . id + '">下载</button>' +
( ok ? '' : '<button class="btn mini" data-a="retry" data-id="' + r . id + '">重试</button>' ) +
'</div></td>' +
'</tr>' ;
} ) . join ( '' ) ;
@@ -674,6 +814,8 @@
if ( action === 'detail' ) {
const r = rows . find ( x => x . id === id ) ;
if ( ! r ) return ;
const lrStatus = r . lastRetryStatus === 1 ? '成功'
: r . lastRetryStatus === 0 ? '失败' : '-' ;
openModal ( '接口日志详情 #' + id , [
[ 'ID' , r . id ] ,
[ '日志级别' , r . logLevel === 1 ? '1-重要(删除)' : r . logLevel === 2 ? '2-次要(新增/编辑)' : r . logLevel === 3 ? '3-一般(查询)' : r . logLevel ] ,
@@ -685,23 +827,18 @@
[ '请求 URI' , r . requestUri ] ,
[ '请求方法' , r . requestMethod ] ,
[ '类名.方法名' , r . classMethod ] ,
[ '请求参数(脱敏)' , r . requestParams ] ,
[ '请求参数(脱敏)' , r . requestParams , true ] ,
[ '状态码' , r . statusCode ] ,
[ '耗时' , ( r . costTime == null ? '-' : r . costTime + ' ms' ) ] ,
[ '异常信息' , r . errorMsg ] ,
[ '异常信息' , r . errorMsg , true ] ,
[ 'User-Agent' , r . userAgent ] ,
[ '时间' , fmtTime ( r . createTime ) ]
[ '时间' , fmtTime ( r . createTime ) ] ,
[ '重试次数' , ( r . retryCount == null ? 0 : r . retryCount ) + ' 次' ] ,
[ '上次重试状态' , lrStatus ] ,
[ '上次重试时间' , r . lastRetryTime ? fmtTime ( r . lastRetryTime ) : '-' ]
] ) ;
} else if ( action === 'download' ) {
downloadFile ( '/syslog/download/' + id , 'audit-' + id + '.txt' ) ;
} else if ( action === 'retry' ) {
if ( ! confirm ( '确认按记录中的 classMethod + requestParams 反射重放该操作?\n执行结果见新增的审计日志。' ) ) return ;
try {
const data = await apiPost ( '/syslog/retry/' + id ) ;
toast ( ( data && ( data . message || data . msg ) ) || '重试已提交' , true ) ;
} catch ( e ) {
toast ( '重试失败:' + e . message ) ;
}
}
}
@@ -768,6 +905,13 @@
}
}
// 类名.方法名 缩写:去掉包名前缀,只保留 类名.方法名(如 ...SendUserBaseService.deleteTeachers → SendUserBaseService.deleteTeachers)
function shortClassMethod ( cm ) {
if ( cm == null ) return '-' ;
const parts = String ( cm ) . split ( '.' ) ;
return parts . length >= 2 ? parts . slice ( - 2 ) . join ( '.' ) : String ( cm ) ;
}
function renderSyRows ( rows ) {
const tb = $ ( 'sy_tbody' ) ;
if ( ! rows . length ) {
@@ -789,7 +933,7 @@
'<td class="mono">' + r . id + '</td>' +
'<td>' + big + '</td>' +
'<td>' + escHtml ( typeText ) + '</td>' +
'<td class="mono"><span class="cell-clip" title="' + escHtml ( r . classMethod ) + '">' + escHtml ( r . classMethod ) + '</span></td>' +
'<td class="mono"><span class="cell-clip" title="' + escHtml ( r . classMethod ) + '">' + escHtml ( shortClassMethod ( r . classMethod ) ) + '</span></td>' +
'<td>' + st + '</td>' +
'<td>' + escHtml ( r . userName || ( r . userId == null ? '-' : r . userId ) ) + '</td>' +
'<td>' + escHtml ( srcName ) + '</td>' +
@@ -798,7 +942,6 @@
'<td><div class="row-actions">' +
'<button class="btn mini" data-a="detail" data-id="' + r . id + '">详情</button>' +
'<button class="btn mini" data-a="download" data-id="' + r . id + '">下载</button>' +
( r . status !== 1 ? '<button class="btn mini" data-a="retry" data-id="' + r . id + '">重试</button>' : '' ) +
'</div></td>' +
'</tr>' ;
} ) . join ( '' ) ;
@@ -811,6 +954,11 @@
if ( action === 'detail' ) {
const r = rows . find ( x => x . id === id ) ;
if ( ! r ) return ;
const lrStatus = r . lastRetryStatus === 1 ? '成功'
: r . lastRetryStatus === 0 ? '失败' : '-' ;
const retryBtn = r . status !== 1
? '<button class="btn primary" data-a="retry" data-id="' + r . id + '">重试</button>'
: '' ;
openModal ( '同步日志详情 #' + id , [
[ 'ID' , r . id ] ,
[ '大类' , ( r . syncTypeName || r . syncType || '-' ) + ( r . syncType ? '( ' + r . syncType + ') ' : '' ) ] ,
@@ -821,20 +969,47 @@
[ '学校 schId' , r . schId ] ,
[ '来源(学校)' , schoolMap [ r . schId ] || r . schId || '-' ] ,
[ '客户端 IP' , r . requestIp ] ,
[ '请求入参(脱敏)' , r . jsonData ] ,
[ '异常信息' , r . errorMsg ] ,
[ '时间' , fmtTime ( r . createTime ) ]
] ) ;
[ '请求入参(脱敏)' , r . jsonData , true ] ,
[ '异常信息' , r . errorMsg , true ] ,
[ '时间' , fmtTime ( r . createTime ) ] ,
[ '重试次数' , ( r . retryCount == null ? 0 : r . retryCount ) + ' 次' ] ,
[ '上次重试状态' , lrStatus ] ,
[ '上次重试时间' , r . lastRetryTime ? fmtTime ( r . lastRetryTime ) : '-' ]
] , retryBtn ) ;
} else if ( action === 'download' ) {
downloadFile ( '/synclog/download/' + id , 'sync-' + id + '.txt' ) ;
} else if ( action === 'retry' ) {
if ( ! confirm ( '确认按记录中的 classMethod + jsonData 反射重放该同步/推送?\n执行结果见新增的同步日志。' ) ) return ;
try {
const data = await apiPost ( '/synclog/retry/' + id ) ;
toast ( ( data && ( data . message || data . msg ) ) || '重试已提交' , true ) ;
} catch ( e ) {
toast ( '重试失败:' + e . message ) ;
}
}
}
// 顶部确认弹窗(替代原生 confirm,提示框显示在页面顶部)
function confirmTop ( msg ) {
return new Promise ( resolve => {
$ ( 'confirmMsg' ) . textContent = msg ;
const mask = $ ( 'confirmMask' ) ;
mask . classList . add ( 'show' ) ;
const done = ( val ) => {
mask . classList . remove ( 'show' ) ;
$ ( 'confirmOk' ) . onclick = null ;
$ ( 'confirmCancel' ) . onclick = null ;
mask . onclick = null ;
resolve ( val ) ;
} ;
$ ( 'confirmOk' ) . onclick = ( ) => done ( true ) ;
$ ( 'confirmCancel' ) . onclick = ( ) => done ( false ) ;
mask . onclick = ( e ) => { if ( e . target === mask ) done ( false ) ; } ;
} ) ;
}
// 重试:从详情弹窗发起(成功提示 + 刷新列表展示最新重试字段)
async function doSyncRetry ( id ) {
if ( ! await confirmTop ( '确认按记录中的 classMethod + jsonData 反射重放该同步/推送?' ) ) return ;
try {
const data = await apiPost ( '/synclog/retry/' + id ) ;
toastTop ( ( data && ( data . message || data . msg ) ) || '重试成功' , true ) ;
$ ( 'modalMask' ) . classList . remove ( 'show' ) ;
loadSyncLog ( state . sy . page ) ; // 刷新当前页,展示最新重试次数/状态/时间
} catch ( e ) {
toastTop ( '重试失败:' + e . message , false ) ;
}
}
@@ -1076,11 +1251,12 @@
$ ( 'keyInput' ) . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' ) queryByKey ( e . target . value . trim ( ) ) ; } ) ;
$ ( 'patternInput' ) . addEventListener ( 'keydown' , e => { if ( e . key === 'Enter' ) listKeys ( e . target . value . trim ( ) || '*' ) ; } ) ;
// 打开页面先校验登录态,未登录直接跳转 CAS;已登录再加载首页
// 打开页面先校验登录态,未登录直接跳转 CAS;已登录再恢复上次的 Tab 与表格页码
( async ( ) => {
const ok = await checkLogin ( ) ;
if ( ! ok ) return ; // 已跳转登录页
switchTab ( 'oplog ') ;
const savedTab = loadState ( 'wbTab ') ;
switchTab ( savedTab === 'oplog' || savedTab === 'synclog' || savedTab === 'cache' ? savedTab : 'oplog' ) ;
} ) ( ) ;
< / script >
< / body >