@@ -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: 14px;
}
header {
padding: 16px 24px;
border-bottom : 1px solid var(--border) ;
background: linear-gradient(180deg, #eef2f7, #f5f7fa) ;
/* ===== Breadcrumb( View UI Breadcrumb 风格) ===== */
.breadcrumb {
padding : 14 px 24px; background: #fff ;
border-bottom: 1px solid #e8eaec ;
font-size: 14px; color: #515a6e;
}
header .sub .pt { color: var(--text); font-weight: 600 ; }
header .sub { margin-top: 4px; color: var(--muted); font-size: 12px ; }
.breadcrumb a { color: #515a6e; text-decoration: none; transition: color .2s ; }
.breadcrumb a:hover { color: #2d8cf0 ; }
.breadcrumb .sep { margin: 0 8px; color: #dcdee2; }
.breadcrumb .current { color: #515a6e; }
/* ===== Tabs ===== */
/* ===== Tabs( View UI line 型:底部下划线激活) ===== */
.tabs {
display: flex; gap: 4px; padding: 10px 24px 0 ;
border-bottom: 1px solid var(--border); background: #e9eef4 ;
display: flex; gap: 2 4px; padding: 0 24px;
border-bottom: 1px solid var(--border-in ); background: #fff ;
}
.tabs .tab {
padding: 9 px 20 px; cursor: pointer; font-size: 14px; color: var(--muted);
border: 1 px solid transparent; border-bottom: none ;
border-radius: 8px 8px 0 0; user-select: none;
padding: 11 px 2px; cursor: pointer; font-size: 14px; color: var(--muted);
border-bottom : 2 px solid transparent; user-select: none; transition: color .2s ;
}
.tabs .tab:hover { color: var(--tex t); }
.tabs .tab:hover { color: var(--accen t); }
.tabs .tab.active {
color: var(--accent); background: var(--panel) ;
border-color: var(--border); position: relative; top: 1px ;
border-bottom: 1px solid var(--panel);
color: var(--accent); font-weight: 500 ;
border-bottom- color: var(--accent) ;
}
.panel { display: none; padding: 16px 24px; }
.panel.active { display: block; }
@@ -59,9 +62,9 @@
.left { width: 320px; flex: 0 0 320px; }
.right { flex: 1 1 auto; min-width: 0; }
.card {
background: var(--panel) ;
border: 1 px solid var(--border) ;
border-radius: 10px;
background: #fff ;
border-radius : 4 px;
box-shadow: 0 1px 1px rgba(0,0,0,.1); /* View UI Card */
padding: 16px;
}
.card + .card { margin-top: 16px; }
@@ -71,29 +74,32 @@
input[type=text], input[type=number] {
width: 100%;
padding: 9px 11px;
background: var(--panel-2) ;
border: 1px solid var(--border) ;
border-radius: 8 px;
background: #fff ;
border: 1px solid #dcdee2 ;
border-radius: 4 px;
color: var(--text);
font-family: var(--mono);
outline: none;
transition: border-color .2s, box-shadow .2s;
}
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 2px rgba(45,140,240,.2); outline: none; }
.btn {
padding: 9 px 14 px;
border: 1px solid var(--border);
background: var(--panel-2) ;
padding: 5 px 15 px;
border: 1px solid var(--border-in );
background: #fff ;
color: var(--text);
border-radius: 8 px;
border-radius: 4 px;
cursor: pointer;
font-size: 13px;
transition: border-color .2s, color .2s, background .2s;
}
.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 10px; font-size: 12px; 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 10px; font-size: 12px; border-radius: 4 px; }
.btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
.hint { color: var(--muted); font-size: 11px; margin-top: 6px; }
@@ -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: 3px solid var(--accent); padding-left: 7px ; }
.keys-list .k:hover { background: #f3f3f3 ; }
.keys-list .k.active { background: #f0faff ; }
.keys-list .k .kname {
display: block; color: var(--accent); font-size: 11px; margin-bottom: 2px;
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
@@ -123,28 +129,30 @@
.meta .mk { color: var(--muted); font-size: 12px; }
.meta .mv { font-family: var(--mono); word-break: break-all; }
.badge {
display: inline-block; padding: 2px 8px; border-radius: 999 px;
font-size: 11 px; border: 1px solid var(--border) ; color: var(--muted);
display: inline-block; padding: 0 8px; height: 22px; line-height: 20 px;
border-radius: 4px; font-size: 12 px; border: 1px solid #e8eaec ; color: var(--muted); background: #fff;
}
.badge.string { color: var(--green); border-color: rgba(54 ,194,11 7,.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(2 5,190,107,.4); background: rgba(25,190,10 7,.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: 8px; align-items: center; margin-bottom: 12px; flex-wrap: wrap; }
.count { color: var(--muted); font-size: 12px; }
select {
padding: 7px 10px;
background: var(--panel-2) ;
border: 1px solid var(--border) ;
border-radius: 8 px;
background: #fff ;
border: 1px solid #dcdee2 ;
border-radius: 4 px;
color: var(--text);
font-size: 13px;
outline: none;
cursor: pointer;
transition: border-color .2s, box-shadow .2s;
}
select:focus { border-color: var(--accent) ; }
select:hover { border-color: #57a3f3 ; }
select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(45,140,240,.2); }
pre.json {
margin: 0; padding: 12px; background: #f0f3f7 ; border: 1px solid var(--border) ;
border-radius: 8 px; overflow: auto; max-height: 60vh; font-family: var(--mono);
margin: 0; padding: 12px; background: #f8f8f9 ; border: 1px solid #e8eaec ;
border-radius: 4 px; overflow: auto; max-height: 60vh; font-family: var(--mono);
font-size: 12px; 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: 1px 5px; border-radius: 4px; font-family: var(--mono); }
/* ===== 日志表格 ===== */
.filter-bar { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; margin-bottom: 14px; }
.filter-bar .fi { display: flex; flex-direction: column; gap : 4px; }
.filter-bar .fi label { margin-bottom : 0; }
.filter-bar .fi input[type=text], .filter-bar .fi input[type=number] { width: 170px; padding: 7px 10px ; font-size: 13 px; }
.filter-bar .fi select { min-width: 110px ; }
/* 筛选工具栏(参考页面风格:白底圆角卡、一行内联 + 横向滚动) */
.filter-bar { display: flex; gap: 8px; flex-wrap: nowrap; align-items: center; background: var(--panel); border: 1px solid #e8eaec; border-radius: 4px; padding: 8px 12px; margin-bottom : 1 4px; overflow-x: auto; }
.filter-bar .fi { display: flex; align-items: center; gap: 5px; 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: 105px; padding: 5px 8px; font-size: 12px; border: 1px solid #dcdee2; border-radius: 4px; background: #fff; color: var(--text); outline: none; transition: border-color .2s, box-shadow .2s ; }
.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 2px rgba(45,140,240,.2); }
.filter-bar .fi select { min-width: 80px; padding: 5px 8px; border: 1px solid #dcdee2; border-radius: 4px; background: #fff; font-size: 12px; color: var(--text); transition: border-color .2s, box-shadow .2s; }
.filter-bar .fi select:hover { border-color: #57a3f3; }
.filter-bar .fi select:focus { border-color: var(--accent); box-shadow: 0 0 0 2px rgba(45,140,240,.2); }
/* 搜索框左侧放大镜图标 */
.filter-bar .fi.search { position: relative; }
.filter-bar .fi.search input[type=text] { padding-left: 26px; }
.filter-bar .fi.search::before {
content: "\1F50D"; /* 🔍 */
position: absolute; left: 9px; top: 50%; transform: translateY(-50%);
color: var(--muted); font-size: 13px; pointer-events: none;
}
.filter-bar > .btn.primary { margin-left: auto; flex-shrink: 0; } /* 查询按钮置右 */
table.log { width: 100%; border-collapse: collapse; font-size: 12.5px; }
table.log th, table.log td {
padding: 8 px 10 px; border-bottom: 1px solid var(--border) ;
padding: 9 px 12 px; border-bottom: 1px solid #e8eaec ;
text-align: left; vertical-align: top;
}
table.log th { color: var(--muted) ; font-weight: 5 00; 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: 6 00; 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: 12px; }
table.log td .cell-clip {
display: inline-block; max-width: 280px; overflow: hidden;
text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom;
}
.table-wrap { overflow: auto; max-height: 62vh; border: 1px solid var(--border) ; border-radius: 8 px; }
.tag { display: inline-block; padding: 1px 8 px; border-radius: 999 px; font-size: 11 px; border: 1px solid; white-space: nowrap; }
.tag.green { color: var(--green); border-color: rgba(54 ,194,11 7,.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: 10px; align-items: center; margin-top: 12px; flex-wrap: wrap; }
.pager .pinfo { color: var(--muted) ; font-size: 12 px; }
.row-actions { display: flex; gap: 6px; white-space: nowrap ; }
.table-wrap { overflow: auto; max-height: 62vh; border: 1px solid #e8eaec ; border-radius: 4 px; }
.tag { display: inline-block; padding: 0 8px; height: 22px; line-height: 20 px; border-radius: 4 px; font-size: 12 px; border: 1px solid; white-space: nowrap; }
.tag.green { color: var(--green); border-color: rgba(2 5,190,107,.4); background: rgba(25,190,10 7,.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: 8px; align-items: center; justify-content: flex-end; margin-top: 14px; flex-wrap: wrap; color: #808695 ; font-size: 13 px; }
.pager .pinfo { color: #808695; margin-right: 4px ; }
.pager .pg { display: inline-flex; gap: 4px; }
.pager .pg-btn { min-width: 32px; height: 32px; padding: 0 6px; border: 1px solid #dcdee2; border-radius: 4px; background: #fff; color: #515a6e; cursor: pointer; font-size: 13px; text-align: center; line-height: 30px; transition: border-color .2s, color .2s; }
.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: 32px; padding: 0 2px; color: #c5c8ce; }
.pager .size { display: inline-flex; align-items: center; gap: 4px; }
.pager .size select { padding: 5px 8px; border: 1px solid #dcdee2; border-radius: 4px; background: #fff; font-size: 13px; color: #515a6e; }
.row-actions { display: flex; gap: 4px; white-space: nowrap; }
.row-actions .btn.mini { padding: 2px 8px; border: 1px solid transparent; border-radius: 4px; background: none; color: #2d8cf0; font-size: 12px; }
.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(860px, 92vw); max-height: 84vh; overflow: auto ;
background: var(--panel) ; border: 1 px solid var(--border) ;
border-radius: 12px; padding: 18px 20px ;
width: min(860px, 92vw); max-height: 84vh; overflow: hidden ;
background: #fff ; border-radius : 6 px;
box-shadow: 0 2px 8px rgba(0,0,0,.2) ;
display: flex; flex-direction: column;
}
.modal h3 { margin: 0 0 12px; font-size: 15px; }
.modal .modal-header {
padding: 13px 16px; border-bottom: 1px solid #e8eaec;
display: flex; align-items: center; justify-content: space-between; flex: 0 0 auto;
}
.modal .modal-header h3 { margin: 0; font-size: 14px; font-weight: 600; color: #17233d; }
.modal .mclose {
border: none; background: none; cursor: pointer;
font-size: 20px; line-height: 1; color: #808695; padding: 0 4px; border-radius: 4px;
transition: color .2s;
}
.modal .mclose:hover { color: #2d8cf0; }
.modal .modal-body { padding: 16px; overflow: auto; flex: 1 1 auto; }
.modal .kv { display: grid; grid-template-columns: 110px 1fr; gap: 8px 14px; margin-bottom: 12px; }
.modal .kv .k { color: var(--muted); font-size: 12px; }
.modal .kv .v { font-family: var(--mono); font-size: 12.5px; word-break: break-all; white-space: pre-wrap; }
.modal .mclose { float: right ; }
.modal .kv .v.scroll { max-height: 160px; overflow-y: auto; padding: 6px 8px; background: #f8f8f9; border: 1px solid #e8eaec; border-radius: 4px ; }
.modal .modal-actions { display: none; gap: 8px; justify-content: flex-end; padding: 12px 16px; border-top: 1px solid #e8eaec; flex: 0 0 auto; }
.toast {
position: fixed; left: 50%; bottom: 28px; transform: translateX(-50%);
background: #fdecec; bor der : 1px solid var(--red); color: #a02020 ;
padding: 10px 16px; border-radius: 8px; font-size: 13px; max-width: 80vw ;
display: none; z-index: 100 ;
/* 确认弹窗(View UI Modal.confirm 风格:居中 + 警示图标) */
.confirm-mask {
position: fixed; inset: 0; z-in dex : 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: 20vh; width: min(416px, 92vw);
background: #fff; border-radius: 6px; padding: 20px 24px 16px;
box-shadow: 0 2px 8px rgba(0,0,0,.2);
}
.confirm-body { display: flex; gap: 12px; align-items: flex-start; margin-bottom: 18px; }
.confirm-icon {
flex: 0 0 32px; width: 32px; height: 32px; border-radius: 50%;
background: #ff9900; color: #fff; font-size: 20px; font-weight: 700;
display: flex; align-items: center; justify-content: center; margin-top: 2px;
}
.confirm-msg { font-size: 14px; line-height: 1.6; color: #515a6e; }
.confirm-btns { display: flex; justify-content: flex-end; gap: 8px; }
/* 消息提示(View UI Message 风格:顶部居中白卡 + 彩色图标) */
.toast, .toast-top {
position: fixed; top: 16px; left: 50%; transform: translateX(-50%);
background: #fff; border-radius: 4px;
box-shadow: 0 1px 6px rgba(0,0,0,.2);
padding: 10px 16px; font-size: 14px; color: #515a6e; max-width: 80vw;
display: none; z-index: 100; align-items: center; gap: 8px;
}
.toast.show, .toast-top.show { display: flex; }
.toast::before, .toast-top::before {
content: ''; display: inline-block; flex: 0 0 16px; width: 16px; height: 16px;
border-radius: 50%; font-size: 11px; line-height: 16px; 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 >