变更项: 1. 修复 base.html 顶栏修改密码按钮闭合错误(el-button 被误写为 div)。 2. 移除可选链写法,改为兼容语法,避免低版本浏览器脚本解析失败。 3. 调整 app-container 默认可见,避免脚本异常时整页因 opacity=0 看起来空白。 4. 确认密码校验函数使用稳定上下文引用,避免 this 访问异常。
1091 lines
37 KiB
HTML
1091 lines
37 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{% block title %}机器人管理后台{% endblock %}</title>
|
|
<link rel="icon" href="/static/favicon.ico">
|
|
|
|
<style>
|
|
:root {
|
|
--bg: #f4f7fb;
|
|
--bg-soft: #eef3f8;
|
|
--surface: rgba(255, 255, 255, 0.86);
|
|
--surface-strong: #ffffff;
|
|
--surface-muted: #f8fafc;
|
|
--border: rgba(148, 163, 184, 0.18);
|
|
--border-strong: rgba(148, 163, 184, 0.26);
|
|
--text: #0f172a;
|
|
--text-soft: #475569;
|
|
--text-faint: #94a3b8;
|
|
--primary: #4f46e5;
|
|
--primary-soft: rgba(79, 70, 229, 0.10);
|
|
--primary-soft-2: rgba(99, 102, 241, 0.16);
|
|
--info: #3b82f6;
|
|
--success: #10b981;
|
|
--warning: #f59e0b;
|
|
--danger: #ef4444;
|
|
--shadow-sm: 0 8px 24px rgba(15, 23, 42, 0.06);
|
|
--shadow-md: 0 18px 48px rgba(15, 23, 42, 0.08);
|
|
--radius-xs: 10px;
|
|
--radius-sm: 14px;
|
|
--radius-md: 18px;
|
|
--radius-lg: 24px;
|
|
--topbar-height: 72px;
|
|
--subnav-height: 64px;
|
|
--content-padding: 24px;
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html, body {
|
|
height: 100%;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
overflow: hidden;
|
|
color: var(--text);
|
|
background:
|
|
radial-gradient(circle at top left, rgba(99, 102, 241, 0.10), transparent 28%),
|
|
radial-gradient(circle at top right, rgba(56, 189, 248, 0.08), transparent 24%),
|
|
linear-gradient(180deg, #f8fafc 0%, #f3f6fb 100%);
|
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
|
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
|
}
|
|
|
|
h1, h2, h3, h4, h5, h6, p {
|
|
margin: 0;
|
|
}
|
|
|
|
.app-container {
|
|
min-height: 100vh;
|
|
opacity: 1;
|
|
transition: opacity .24s ease;
|
|
}
|
|
|
|
.app-container.loaded {
|
|
opacity: 1;
|
|
}
|
|
|
|
.topbar {
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 200;
|
|
height: var(--topbar-height);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 18px;
|
|
padding: 0 24px;
|
|
background: rgba(255, 255, 255, 0.72);
|
|
backdrop-filter: blur(18px);
|
|
-webkit-backdrop-filter: blur(18px);
|
|
border-bottom: 1px solid rgba(148, 163, 184, 0.14);
|
|
box-shadow: 0 10px 30px rgba(15, 23, 42, 0.04);
|
|
}
|
|
|
|
.topbar-left,
|
|
.topbar-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.brand {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.brand-logo {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 14px;
|
|
object-fit: cover;
|
|
box-shadow: 0 10px 20px rgba(79, 70, 229, 0.16);
|
|
}
|
|
|
|
.brand-copy {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.brand-title {
|
|
font-size: 17px;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
letter-spacing: 0.01em;
|
|
}
|
|
|
|
.brand-subtitle {
|
|
font-size: 12px;
|
|
color: var(--text-faint);
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.main-nav {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
min-width: 0;
|
|
overflow-x: auto;
|
|
padding: 4px;
|
|
border-radius: 999px;
|
|
background: rgba(255,255,255,0.58);
|
|
border: 1px solid rgba(148, 163, 184, 0.14);
|
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.8);
|
|
}
|
|
|
|
.main-nav::-webkit-scrollbar,
|
|
.sub-nav::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.main-nav-link,
|
|
.sub-nav-link {
|
|
appearance: none;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-soft);
|
|
cursor: pointer;
|
|
font: inherit;
|
|
white-space: nowrap;
|
|
transition: all .18s ease;
|
|
}
|
|
|
|
.main-nav-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
min-height: 38px;
|
|
padding: 0 16px;
|
|
border-radius: 999px;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.main-nav-link:hover {
|
|
background: rgba(255,255,255,0.92);
|
|
color: var(--text);
|
|
}
|
|
|
|
.main-nav-link.is-active {
|
|
color: var(--primary);
|
|
background: linear-gradient(135deg, rgba(79,70,229,0.12), rgba(99,102,241,0.08));
|
|
box-shadow: inset 0 0 0 1px rgba(99,102,241,0.10);
|
|
}
|
|
|
|
.status-pill,
|
|
.user-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 10px 14px;
|
|
border-radius: 999px;
|
|
background: rgba(255,255,255,0.72);
|
|
border: 1px solid var(--border);
|
|
box-shadow: inset 0 1px 0 rgba(255,255,255,0.9);
|
|
color: var(--text-soft);
|
|
font-size: 13px;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.status-dot,
|
|
.user-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.status-dot {
|
|
background: var(--success);
|
|
box-shadow: 0 0 0 4px rgba(16, 185, 129, 0.12);
|
|
}
|
|
|
|
.user-dot {
|
|
background: var(--info);
|
|
box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.10);
|
|
}
|
|
|
|
.logout-btn {
|
|
color: var(--text-soft) !important;
|
|
padding: 10px 14px !important;
|
|
border-radius: 999px !important;
|
|
transition: all .18s ease !important;
|
|
}
|
|
|
|
.account-btn {
|
|
color: var(--text-soft) !important;
|
|
padding: 10px 14px !important;
|
|
border-radius: 999px !important;
|
|
transition: all .18s ease !important;
|
|
}
|
|
|
|
.account-btn:hover {
|
|
color: var(--primary) !important;
|
|
background: var(--primary-soft) !important;
|
|
}
|
|
|
|
.logout-btn:hover {
|
|
color: var(--primary) !important;
|
|
background: var(--primary-soft) !important;
|
|
}
|
|
|
|
.subnav-bar {
|
|
position: sticky;
|
|
top: var(--topbar-height);
|
|
z-index: 190;
|
|
height: var(--subnav-height);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 16px;
|
|
padding: 0 24px;
|
|
background: rgba(248, 250, 252, 0.84);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
|
|
}
|
|
|
|
.subnav-title {
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 3px;
|
|
}
|
|
|
|
.subnav-title h2 {
|
|
font-size: 19px;
|
|
font-weight: 700;
|
|
color: var(--text);
|
|
}
|
|
|
|
.subnav-title p {
|
|
font-size: 13px;
|
|
color: var(--text-soft);
|
|
}
|
|
|
|
.sub-nav {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
min-width: 0;
|
|
overflow-x: auto;
|
|
padding-bottom: 2px;
|
|
}
|
|
|
|
.subnav-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
}
|
|
|
|
.sub-nav-link {
|
|
min-height: 36px;
|
|
padding: 0 14px;
|
|
border-radius: 999px;
|
|
border: 1px solid rgba(148, 163, 184, 0.12);
|
|
background: rgba(255,255,255,0.64);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.sub-nav-link:hover {
|
|
background: rgba(255,255,255,0.95);
|
|
color: var(--text);
|
|
border-color: rgba(148, 163, 184, 0.18);
|
|
}
|
|
|
|
.sub-nav-link.is-active {
|
|
background: linear-gradient(135deg, rgba(79,70,229,0.12), rgba(99,102,241,0.08));
|
|
color: var(--primary);
|
|
border-color: rgba(99, 102, 241, 0.12);
|
|
}
|
|
|
|
.layout-shell {
|
|
height: calc(100vh - var(--topbar-height) - var(--subnav-height));
|
|
overflow: hidden;
|
|
}
|
|
|
|
.content {
|
|
height: 100%;
|
|
overflow-y: auto;
|
|
padding: var(--content-padding);
|
|
}
|
|
|
|
.content-inner {
|
|
min-height: calc(100vh - var(--topbar-height) - var(--subnav-height) - (var(--content-padding) * 2));
|
|
}
|
|
|
|
.toolbar-card,
|
|
.el-card {
|
|
border-radius: var(--radius-md) !important;
|
|
border: 1px solid var(--border) !important;
|
|
background: rgba(255, 255, 255, 0.82) !important;
|
|
box-shadow: var(--shadow-sm) !important;
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.el-card__header {
|
|
padding: 16px 18px !important;
|
|
background: linear-gradient(180deg, rgba(255,255,255,0.82), rgba(248,250,252,0.92)) !important;
|
|
border-bottom: 1px solid rgba(148, 163, 184, 0.12) !important;
|
|
color: var(--text) !important;
|
|
}
|
|
|
|
.el-card__body {
|
|
padding: 18px !important;
|
|
}
|
|
|
|
.el-button {
|
|
border-radius: 12px !important;
|
|
font-weight: 500 !important;
|
|
transition: all .18s ease !important;
|
|
}
|
|
|
|
.el-button--primary {
|
|
background: linear-gradient(135deg, #4f46e5, #6366f1) !important;
|
|
border-color: transparent !important;
|
|
box-shadow: 0 10px 20px rgba(79, 70, 229, 0.18) !important;
|
|
}
|
|
|
|
.el-button--primary:hover,
|
|
.el-button--primary:focus {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 14px 24px rgba(79, 70, 229, 0.22) !important;
|
|
}
|
|
|
|
.el-button--primary.is-plain,
|
|
.el-button--success.is-plain,
|
|
.el-button--warning.is-plain,
|
|
.el-button--danger.is-plain,
|
|
.el-button--info.is-plain {
|
|
background: rgba(255, 255, 255, 0.96) !important;
|
|
box-shadow: 0 6px 16px rgba(15, 23, 42, 0.06) !important;
|
|
}
|
|
|
|
.el-button--primary.is-plain {
|
|
color: #4338ca !important;
|
|
border-color: rgba(99, 102, 241, 0.34) !important;
|
|
}
|
|
|
|
.el-button--primary.is-plain:hover,
|
|
.el-button--primary.is-plain:focus {
|
|
color: #ffffff !important;
|
|
border-color: transparent !important;
|
|
background: linear-gradient(135deg, #4f46e5, #6366f1) !important;
|
|
box-shadow: 0 12px 22px rgba(79, 70, 229, 0.20) !important;
|
|
}
|
|
|
|
.el-button--success.is-plain {
|
|
color: #047857 !important;
|
|
border-color: rgba(16, 185, 129, 0.34) !important;
|
|
}
|
|
|
|
.el-button--success.is-plain:hover,
|
|
.el-button--success.is-plain:focus {
|
|
color: #ffffff !important;
|
|
border-color: transparent !important;
|
|
background: linear-gradient(135deg, #10b981, #34d399) !important;
|
|
box-shadow: 0 12px 22px rgba(16, 185, 129, 0.20) !important;
|
|
}
|
|
|
|
.el-button--warning.is-plain {
|
|
color: #b45309 !important;
|
|
border-color: rgba(245, 158, 11, 0.34) !important;
|
|
}
|
|
|
|
.el-button--warning.is-plain:hover,
|
|
.el-button--warning.is-plain:focus {
|
|
color: #ffffff !important;
|
|
border-color: transparent !important;
|
|
background: linear-gradient(135deg, #f59e0b, #fbbf24) !important;
|
|
box-shadow: 0 12px 22px rgba(245, 158, 11, 0.20) !important;
|
|
}
|
|
|
|
.el-button--danger.is-plain {
|
|
color: #b91c1c !important;
|
|
border-color: rgba(239, 68, 68, 0.34) !important;
|
|
}
|
|
|
|
.el-button--danger.is-plain:hover,
|
|
.el-button--danger.is-plain:focus {
|
|
color: #ffffff !important;
|
|
border-color: transparent !important;
|
|
background: linear-gradient(135deg, #ef4444, #f87171) !important;
|
|
box-shadow: 0 12px 22px rgba(239, 68, 68, 0.20) !important;
|
|
}
|
|
|
|
.el-button--info.is-plain {
|
|
color: #475569 !important;
|
|
border-color: rgba(100, 116, 139, 0.34) !important;
|
|
}
|
|
|
|
.el-button--info.is-plain:hover,
|
|
.el-button--info.is-plain:focus {
|
|
color: #ffffff !important;
|
|
border-color: transparent !important;
|
|
background: linear-gradient(135deg, #64748b, #94a3b8) !important;
|
|
box-shadow: 0 12px 22px rgba(100, 116, 139, 0.20) !important;
|
|
}
|
|
|
|
.el-button--default {
|
|
background: rgba(255,255,255,0.85) !important;
|
|
border-color: var(--border-strong) !important;
|
|
color: var(--text) !important;
|
|
}
|
|
|
|
.el-button--default:hover,
|
|
.el-button--default:focus {
|
|
border-color: rgba(99,102,241,0.3) !important;
|
|
color: var(--primary) !important;
|
|
background: rgba(255,255,255,0.96) !important;
|
|
}
|
|
|
|
.el-button--text,
|
|
.el-button--text:not(.is-disabled) {
|
|
color: #334155 !important;
|
|
font-weight: 600 !important;
|
|
padding: 6px 10px !important;
|
|
border-radius: 10px !important;
|
|
background: rgba(248, 250, 252, 0.9) !important;
|
|
border: 1px solid rgba(148, 163, 184, 0.14) !important;
|
|
}
|
|
|
|
.el-button--text:hover,
|
|
.el-button--text:focus,
|
|
.el-button--text:not(.is-disabled):hover,
|
|
.el-button--text:not(.is-disabled):focus {
|
|
color: var(--primary) !important;
|
|
background: rgba(99, 102, 241, 0.08) !important;
|
|
border-color: rgba(99, 102, 241, 0.18) !important;
|
|
}
|
|
|
|
.el-button--text [class*="el-icon-"],
|
|
.el-button--text span {
|
|
color: inherit !important;
|
|
}
|
|
|
|
.el-table .el-button--text,
|
|
.el-table .el-button--text:not(.is-disabled),
|
|
.el-card .el-button--text,
|
|
.el-card .el-button--text:not(.is-disabled) {
|
|
color: #1e293b !important;
|
|
background: rgba(255, 255, 255, 0.92) !important;
|
|
border-color: rgba(100, 116, 139, 0.18) !important;
|
|
box-shadow: 0 4px 12px rgba(15, 23, 42, 0.04) !important;
|
|
}
|
|
|
|
.el-table .el-button--text:hover,
|
|
.el-table .el-button--text:focus,
|
|
.el-card .el-button--text:hover,
|
|
.el-card .el-button--text:focus {
|
|
color: var(--primary) !important;
|
|
background: rgba(99, 102, 241, 0.10) !important;
|
|
border-color: rgba(99, 102, 241, 0.20) !important;
|
|
box-shadow: 0 8px 16px rgba(79, 70, 229, 0.10) !important;
|
|
}
|
|
|
|
.el-button--success,
|
|
.el-button--warning,
|
|
.el-button--danger,
|
|
.el-button--info {
|
|
color: #fff !important;
|
|
border-color: transparent !important;
|
|
}
|
|
|
|
.el-button--success { background: linear-gradient(135deg, #10b981, #34d399) !important; }
|
|
.el-button--warning { background: linear-gradient(135deg, #f59e0b, #fbbf24) !important; }
|
|
.el-button--danger { background: linear-gradient(135deg, #ef4444, #f87171) !important; }
|
|
.el-button--info { background: linear-gradient(135deg, #64748b, #94a3b8) !important; }
|
|
|
|
.el-input__inner,
|
|
.el-textarea__inner {
|
|
height: 40px;
|
|
border-radius: 12px !important;
|
|
background: rgba(248,250,252,0.9) !important;
|
|
border-color: rgba(148,163,184,0.22) !important;
|
|
color: var(--text) !important;
|
|
transition: all .18s ease !important;
|
|
}
|
|
|
|
.el-textarea__inner {
|
|
min-height: 110px;
|
|
}
|
|
|
|
.el-input__inner::placeholder,
|
|
.el-textarea__inner::placeholder {
|
|
color: var(--text-faint) !important;
|
|
}
|
|
|
|
.el-input__inner:focus,
|
|
.el-textarea__inner:focus {
|
|
border-color: rgba(99,102,241,0.42) !important;
|
|
box-shadow: 0 0 0 4px rgba(99,102,241,0.10) !important;
|
|
background: #fff !important;
|
|
}
|
|
|
|
.el-tag {
|
|
border-radius: 999px !important;
|
|
border: none !important;
|
|
padding: 0 10px !important;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.el-table {
|
|
color: var(--text) !important;
|
|
background: transparent !important;
|
|
border-radius: 14px !important;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.el-table::before,
|
|
.el-table--group::after,
|
|
.el-table--border::after,
|
|
.el-table__fixed-right::before,
|
|
.el-table__fixed::before {
|
|
display: none !important;
|
|
}
|
|
|
|
.el-table th {
|
|
background: rgba(248,250,252,0.95) !important;
|
|
color: var(--text-soft) !important;
|
|
font-weight: 600 !important;
|
|
border-bottom: 1px solid rgba(148,163,184,0.12) !important;
|
|
}
|
|
|
|
.el-table tr,
|
|
.el-table td,
|
|
.el-table__expanded-cell {
|
|
background: rgba(255,255,255,0.45) !important;
|
|
}
|
|
|
|
.el-table td,
|
|
.el-table th.is-leaf {
|
|
border-bottom: 1px solid rgba(148,163,184,0.10) !important;
|
|
}
|
|
|
|
.el-table--border,
|
|
.el-table--group {
|
|
border: 1px solid rgba(148,163,184,0.14) !important;
|
|
}
|
|
|
|
.el-table--border th,
|
|
.el-table--border td {
|
|
border-right: 1px solid rgba(148,163,184,0.08) !important;
|
|
}
|
|
|
|
.el-table__row:hover > td {
|
|
background: rgba(99,102,241,0.05) !important;
|
|
}
|
|
|
|
.el-dialog,
|
|
.el-message-box {
|
|
border-radius: 22px !important;
|
|
overflow: hidden !important;
|
|
border: 1px solid rgba(148,163,184,0.14) !important;
|
|
box-shadow: var(--shadow-md) !important;
|
|
background: rgba(255,255,255,0.94) !important;
|
|
backdrop-filter: blur(16px);
|
|
-webkit-backdrop-filter: blur(16px);
|
|
}
|
|
|
|
.el-dialog__header,
|
|
.el-message-box__header {
|
|
padding: 18px 20px !important;
|
|
border-bottom: 1px solid rgba(148,163,184,0.10) !important;
|
|
background: rgba(248,250,252,0.86) !important;
|
|
}
|
|
|
|
.el-dialog__body,
|
|
.el-message-box__content {
|
|
color: var(--text) !important;
|
|
}
|
|
|
|
.el-pagination .el-pager li,
|
|
.el-pagination button {
|
|
border-radius: 10px !important;
|
|
border: 1px solid rgba(148,163,184,0.14) !important;
|
|
background: rgba(255,255,255,0.92) !important;
|
|
}
|
|
|
|
.el-pagination .el-pager li.active {
|
|
background: linear-gradient(135deg, #4f46e5, #6366f1) !important;
|
|
color: #fff !important;
|
|
}
|
|
|
|
.el-tabs__item {
|
|
color: var(--text-soft) !important;
|
|
}
|
|
|
|
.el-tabs__item.is-active,
|
|
.el-tabs__item:hover {
|
|
color: var(--primary) !important;
|
|
}
|
|
|
|
.el-tabs__active-bar {
|
|
background: var(--primary) !important;
|
|
height: 3px !important;
|
|
border-radius: 999px !important;
|
|
}
|
|
|
|
.toolbar-card {
|
|
margin-bottom: 18px;
|
|
}
|
|
|
|
.time-filter-form {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.time-range-inline {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 8px;
|
|
border-radius: 999px;
|
|
background: rgba(255,255,255,0.82);
|
|
border: 1px solid rgba(148, 163, 184, 0.16);
|
|
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.05);
|
|
backdrop-filter: blur(8px);
|
|
-webkit-backdrop-filter: blur(8px);
|
|
}
|
|
|
|
.time-range-inline .el-select .el-input__inner {
|
|
height: 32px !important;
|
|
border-radius: 999px !important;
|
|
padding-left: 12px !important;
|
|
padding-right: 30px !important;
|
|
min-width: 110px;
|
|
font-size: 12px !important;
|
|
}
|
|
|
|
.time-range-inline .el-button {
|
|
min-height: 32px !important;
|
|
padding: 0 10px !important;
|
|
border-radius: 999px !important;
|
|
}
|
|
|
|
.v-modal {
|
|
background: rgba(15, 23, 42, 0.16) !important;
|
|
backdrop-filter: blur(4px);
|
|
-webkit-backdrop-filter: blur(4px);
|
|
}
|
|
|
|
a {
|
|
color: var(--primary);
|
|
}
|
|
|
|
a:hover {
|
|
color: #4338ca;
|
|
}
|
|
|
|
@media (max-width: 1320px) {
|
|
.topbar,
|
|
.subnav-bar,
|
|
.content {
|
|
padding-left: 18px;
|
|
padding-right: 18px;
|
|
}
|
|
|
|
.topbar {
|
|
gap: 12px;
|
|
}
|
|
|
|
.status-pill {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
.password-dialog-tip {
|
|
margin-top: 8px;
|
|
color: var(--text-faint);
|
|
font-size: 12px;
|
|
line-height: 1.7;
|
|
}
|
|
</style>
|
|
|
|
<link rel="stylesheet" href="/static/css/element-ui/theme-chalk/index.min.css">
|
|
{% block styles %}{% endblock %}
|
|
<script src="/static/js/chart.js"></script>
|
|
<script src="/static/js/vue.js"></script>
|
|
<script src="/static/js/element-ui/index.min.js"></script>
|
|
<script src="/static/js/axios.min.js"></script>
|
|
{% block head %}{% endblock %}
|
|
</head>
|
|
<body>
|
|
<div id="app" class="app-container">
|
|
<header class="topbar">
|
|
<div class="topbar-left">
|
|
<div class="brand">
|
|
<img src="/static/logo.png" class="brand-logo" alt="logo">
|
|
<div class="brand-copy">
|
|
<div class="brand-title">机器人控制台</div>
|
|
<div class="brand-subtitle">更产品化的运营、消息与系统工作台</div>
|
|
</div>
|
|
</div>
|
|
|
|
<nav class="main-nav" aria-label="主导航">
|
|
<button
|
|
v-for="item in navGroups"
|
|
:key="item.key"
|
|
class="main-nav-link"
|
|
:class="{ 'is-active': activeGroup === item.key }"
|
|
@click="goToPrimary(item)">
|
|
<i :class="item.icon"></i>
|
|
<span>{% raw %}{{ item.label }}{% endraw %}</span>
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
|
|
<div class="topbar-right">
|
|
<div class="status-pill">
|
|
<span class="status-dot"></span>
|
|
<span>控制台在线</span>
|
|
</div>
|
|
<div class="user-pill">
|
|
<span class="user-dot"></span>
|
|
<span>{{ session.get('username', '管理员') }} 已登录</span>
|
|
</div>
|
|
<el-button type="text" class="account-btn" @click="openPasswordDialog">
|
|
<i class="el-icon-lock"></i> 修改密码
|
|
</el-button>
|
|
<el-button type="text" class="logout-btn" @click="logout">
|
|
<i class="el-icon-switch-button"></i> 退出
|
|
</el-button>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="subnav-bar">
|
|
<div class="subnav-title">
|
|
<h2>{% raw %}{{ activeGroupMeta.label }}{% endraw %}</h2>
|
|
<p>{% raw %}{{ activeGroupMeta.description }}{% endraw %}</p>
|
|
</div>
|
|
<div class="subnav-right">
|
|
<div v-if="showTimeRangeSelector" class="time-range-inline">
|
|
<el-select v-model="timeRange" size="mini" @change="loadData">
|
|
<el-option label="最近7天" value="7"></el-option>
|
|
<el-option label="最近30天" value="30"></el-option>
|
|
<el-option label="最近90天" value="90"></el-option>
|
|
</el-select>
|
|
<el-button size="mini" type="primary" plain @click="loadData">
|
|
<i class="el-icon-refresh"></i>
|
|
</el-button>
|
|
</div>
|
|
<nav class="sub-nav" aria-label="二级导航">
|
|
<button
|
|
v-for="item in currentSubNav"
|
|
:key="item.path"
|
|
class="sub-nav-link"
|
|
:class="{ 'is-active': isPathActive(item.path) }"
|
|
@click="go(item.path)">
|
|
{% raw %}{{ item.label }}{% endraw %}
|
|
</button>
|
|
</nav>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="layout-shell">
|
|
<main class="content">
|
|
<div class="content-inner">
|
|
{% block content %}{% endblock %}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<el-dialog
|
|
title="修改后台登录密码"
|
|
:visible.sync="passwordDialogVisible"
|
|
width="460px"
|
|
:close-on-click-modal="false">
|
|
<el-form
|
|
ref="passwordFormRef"
|
|
:model="passwordForm"
|
|
:rules="passwordRules"
|
|
label-width="96px">
|
|
<el-form-item label="旧密码" prop="old_password">
|
|
<el-input v-model="passwordForm.old_password" type="password" show-password autocomplete="off" placeholder="请输入当前密码"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="新密码" prop="new_password">
|
|
<el-input v-model="passwordForm.new_password" type="password" show-password autocomplete="off" placeholder="至少6位"></el-input>
|
|
</el-form-item>
|
|
<el-form-item label="确认新密码" prop="confirm_password">
|
|
<el-input v-model="passwordForm.confirm_password" type="password" show-password autocomplete="off" placeholder="请再次输入新密码"></el-input>
|
|
</el-form-item>
|
|
</el-form>
|
|
<div class="password-dialog-tip">
|
|
提示:修改成功后将立即生效,建议使用强密码(字母、数字、符号组合)。
|
|
</div>
|
|
<span slot="footer" class="dialog-footer">
|
|
<el-button @click="passwordDialogVisible = false">取消</el-button>
|
|
<el-button type="primary" :loading="passwordSubmitting" @click="submitPasswordChange">确认修改</el-button>
|
|
</span>
|
|
</el-dialog>
|
|
</div>
|
|
|
|
<script>
|
|
const NAV_GROUPS = [
|
|
{
|
|
key: 'overview',
|
|
label: '概览',
|
|
icon: 'el-icon-s-home',
|
|
description: '查看整体运行、趋势与核心经营指标',
|
|
defaultPath: '/',
|
|
items: [
|
|
{ label: '首页概览', path: '/' }
|
|
]
|
|
},
|
|
{
|
|
key: 'messages',
|
|
label: '消息',
|
|
icon: 'el-icon-chat-line-square',
|
|
description: '处理消息、日志与定时推送相关工作',
|
|
defaultPath: '/messages',
|
|
items: [
|
|
{ label: '消息列表', path: '/messages' },
|
|
{ label: '定时推送', path: '/message_push' }
|
|
]
|
|
},
|
|
{
|
|
key: 'friend-circle',
|
|
label: '朋友圈',
|
|
icon: 'el-icon-picture-outline-round',
|
|
description: '集中管理朋友圈查看、发布、点赞与评论流程',
|
|
defaultPath: '/friend_circle',
|
|
items: [
|
|
{ label: '朋友圈管理', path: '/friend_circle' }
|
|
]
|
|
},
|
|
{
|
|
key: 'groups',
|
|
label: '群组',
|
|
icon: 'el-icon-s-cooperation',
|
|
description: '管理群组、权限、通讯录与虚拟群组能力',
|
|
defaultPath: '/groups',
|
|
items: [
|
|
{ label: '群组统计', path: '/groups' },
|
|
{ label: '通讯录', path: '/contacts' },
|
|
{ label: '虚拟群组', path: '/virtual_group' }
|
|
]
|
|
},
|
|
{
|
|
key: 'plugins',
|
|
label: '插件',
|
|
icon: 'el-icon-s-grid',
|
|
description: '查看插件使用、管理插件与接口说明',
|
|
defaultPath: '/plugins',
|
|
items: [
|
|
{ label: '插件统计', path: '/plugins' },
|
|
{ label: '插件管理', path: '/plugins_manage' },
|
|
{ label: '插件定时任务', path: '/plugin_schedules' },
|
|
{ label: '群级插件配置', path: '/group_plugin_config' },
|
|
{ label: '接口文档', path: '/api_docs' }
|
|
]
|
|
},
|
|
{
|
|
key: 'system',
|
|
label: '系统',
|
|
icon: 'el-icon-cpu',
|
|
description: '关注用户、资源与文件等平台基础能力',
|
|
defaultPath: '/users',
|
|
items: [
|
|
{ label: '用户统计', path: '/users' },
|
|
{ label: '资源监控', path: '/system_status' },
|
|
{ label: '系统定时任务', path: '/system_jobs' },
|
|
{ label: '全局配置', path: '/system_llm' },
|
|
{ label: '文件浏览', path: '/file_browser' },
|
|
{ label: '运行日志', path: '/wx_logs' },
|
|
{ label: '错误日志', path: '/errors' }
|
|
]
|
|
}
|
|
];
|
|
|
|
const baseApp = {
|
|
data() {
|
|
const vm = this;
|
|
return {
|
|
currentView: '1',
|
|
timeRange: '7',
|
|
showTimeRangeSelector: false,
|
|
navGroups: NAV_GROUPS,
|
|
// 账号密码修改弹窗状态。
|
|
passwordDialogVisible: false,
|
|
passwordSubmitting: false,
|
|
passwordForm: {
|
|
old_password: '',
|
|
new_password: '',
|
|
confirm_password: ''
|
|
},
|
|
passwordRules: {
|
|
old_password: [
|
|
{ required: true, message: '请输入旧密码', trigger: 'blur' }
|
|
],
|
|
new_password: [
|
|
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
|
{ min: 6, message: '新密码长度至少6位', trigger: 'blur' }
|
|
],
|
|
confirm_password: [
|
|
{ required: true, message: '请再次输入新密码', trigger: 'blur' },
|
|
{
|
|
validator: function(rule, value, callback) {
|
|
if (value !== vm.passwordForm.new_password) {
|
|
callback(new Error('两次输入的新密码不一致'));
|
|
return;
|
|
}
|
|
callback();
|
|
},
|
|
trigger: 'blur'
|
|
}
|
|
]
|
|
}
|
|
}
|
|
},
|
|
computed: {
|
|
activeGroup() {
|
|
const path = this.normalizePath(window.location.pathname);
|
|
const matched = this.navGroups.find(group =>
|
|
group.items.some(item => this.normalizePath(item.path) === path)
|
|
);
|
|
return matched ? matched.key : 'overview';
|
|
},
|
|
activeGroupMeta() {
|
|
return this.navGroups.find(group => group.key === this.activeGroup) || this.navGroups[0];
|
|
},
|
|
currentSubNav() {
|
|
return this.activeGroupMeta.items || [];
|
|
}
|
|
},
|
|
created() {
|
|
const path = window.location.pathname;
|
|
this.showTimeRangeSelector = ['/', '/plugins', '/users', '/groups', '/errors'].includes(path);
|
|
},
|
|
mounted() {
|
|
document.querySelector('.app-container').classList.add('loaded');
|
|
},
|
|
methods: {
|
|
normalizePath(path) {
|
|
if (!path) return '/';
|
|
if (path.length > 1 && path.endsWith('/')) {
|
|
return path.slice(0, -1);
|
|
}
|
|
return path;
|
|
},
|
|
isPathActive(path) {
|
|
return this.normalizePath(window.location.pathname) === this.normalizePath(path);
|
|
},
|
|
go(path) {
|
|
if (path && this.normalizePath(window.location.pathname) !== this.normalizePath(path)) {
|
|
window.location.href = path;
|
|
}
|
|
},
|
|
goToPrimary(group) {
|
|
if (!group) return;
|
|
const current = this.normalizePath(window.location.pathname);
|
|
const ownPaths = (group.items || []).map(item => this.normalizePath(item.path));
|
|
if (ownPaths.includes(current)) return;
|
|
this.go(group.defaultPath || (group.items && group.items[0] && group.items[0].path));
|
|
},
|
|
handleSelect(key) {
|
|
const routes = {
|
|
'1': '/',
|
|
'2': '/plugins',
|
|
'3': '/users',
|
|
'4': '/groups',
|
|
'5': '/errors',
|
|
'7': '/messages',
|
|
'9': '/wx_logs',
|
|
'10': '/contacts',
|
|
'11': '/plugins_manage',
|
|
'12': '/virtual_group',
|
|
'13': '/api_docs',
|
|
'14': '/system_status',
|
|
'17': '/system_llm',
|
|
'15': '/file_browser',
|
|
'16': '/message_push'
|
|
};
|
|
if (routes[key] && window.location.pathname !== routes[key]) {
|
|
window.location.href = routes[key];
|
|
}
|
|
},
|
|
logout() {
|
|
this.$confirm('确认退出登录吗?', '提示', {
|
|
confirmButtonText: '确定',
|
|
cancelButtonText: '取消',
|
|
type: 'warning'
|
|
}).then(() => {
|
|
window.location.href = '/logout';
|
|
});
|
|
},
|
|
openPasswordDialog() {
|
|
// 打开弹窗前重置表单,避免上次输入残留。
|
|
this.passwordDialogVisible = true;
|
|
this.passwordSubmitting = false;
|
|
this.passwordForm = {
|
|
old_password: '',
|
|
new_password: '',
|
|
confirm_password: ''
|
|
};
|
|
this.$nextTick(() => {
|
|
if (this.$refs.passwordFormRef) {
|
|
this.$refs.passwordFormRef.clearValidate();
|
|
}
|
|
});
|
|
},
|
|
submitPasswordChange() {
|
|
if (!this.$refs.passwordFormRef) {
|
|
return;
|
|
}
|
|
this.$refs.passwordFormRef.validate((valid) => {
|
|
if (!valid) {
|
|
return;
|
|
}
|
|
this.passwordSubmitting = true;
|
|
axios.post('/api/auth/change_password', this.passwordForm)
|
|
.then((response) => {
|
|
const data = response.data || {};
|
|
if (!data.success) {
|
|
this.$message.error(data.error || '修改密码失败');
|
|
return;
|
|
}
|
|
this.$message.success(data.message || '密码修改成功');
|
|
this.passwordDialogVisible = false;
|
|
})
|
|
.catch((error) => {
|
|
let errorMsg = '修改密码失败,请稍后重试';
|
|
try {
|
|
if (error && error.response && error.response.data && error.response.data.error) {
|
|
errorMsg = error.response.data.error;
|
|
}
|
|
} catch (e) {
|
|
// 读取错误信息失败时保持默认文案即可。
|
|
}
|
|
this.$message.error(errorMsg);
|
|
})
|
|
.finally(() => {
|
|
this.passwordSubmitting = false;
|
|
});
|
|
});
|
|
}
|
|
}
|
|
};
|
|
</script>
|
|
|
|
{% block scripts %}{% endblock %}
|
|
</body>
|
|
</html>
|