1274 lines
43 KiB
HTML
1274 lines
43 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: 0;
|
||
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;
|
||
}
|
||
|
||
/* 修改密码弹窗:默认宽度受限,避免在中小屏超出视口。 */
|
||
.password-dialog {
|
||
width: min(460px, 92vw) !important;
|
||
}
|
||
|
||
@media (max-width: 1024px) {
|
||
.topbar {
|
||
gap: 10px;
|
||
padding-left: 12px;
|
||
padding-right: 12px;
|
||
}
|
||
|
||
.brand-subtitle {
|
||
display: none;
|
||
}
|
||
|
||
.main-nav-link {
|
||
padding: 0 10px;
|
||
min-height: 34px;
|
||
font-size: 13px;
|
||
}
|
||
|
||
.topbar-right {
|
||
gap: 8px;
|
||
}
|
||
|
||
.user-pill {
|
||
display: none;
|
||
}
|
||
|
||
.account-btn,
|
||
.logout-btn {
|
||
padding: 8px 10px !important;
|
||
font-size: 12px !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.topbar {
|
||
height: auto;
|
||
flex-wrap: wrap;
|
||
align-items: stretch;
|
||
padding-top: 10px;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.topbar-left {
|
||
width: 100%;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
}
|
||
|
||
.brand {
|
||
min-width: 0;
|
||
}
|
||
|
||
.brand-title {
|
||
font-size: 15px;
|
||
}
|
||
|
||
.main-nav {
|
||
width: 100%;
|
||
order: 3;
|
||
}
|
||
|
||
.topbar-right {
|
||
margin-left: auto;
|
||
width: auto;
|
||
}
|
||
|
||
.account-btn,
|
||
.logout-btn {
|
||
padding: 6px 8px !important;
|
||
min-width: 0;
|
||
font-size: 12px !important;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.subnav-bar {
|
||
top: auto;
|
||
position: static;
|
||
height: auto;
|
||
flex-wrap: wrap;
|
||
padding: 10px 12px;
|
||
gap: 10px;
|
||
}
|
||
|
||
.subnav-right {
|
||
width: 100%;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.sub-nav {
|
||
width: 100%;
|
||
}
|
||
|
||
.layout-shell {
|
||
height: calc(100vh - 150px);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
/* 小屏下仅对“修改密码”弹窗做堆叠标签,不影响其它弹窗。 */
|
||
.password-dialog .el-form-item__label {
|
||
float: none;
|
||
display: block;
|
||
text-align: left;
|
||
line-height: 20px;
|
||
padding: 0 0 6px;
|
||
}
|
||
|
||
.password-dialog .el-form-item__content {
|
||
margin-left: 0 !important;
|
||
}
|
||
}
|
||
</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"
|
||
:close-on-press-escape="!passwordDialogLocked"
|
||
:show-close="!passwordDialogLocked"
|
||
:before-close="handlePasswordDialogBeforeClose"
|
||
custom-class="password-dialog">
|
||
<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="至少8位,建议字母+数字/符号组合"></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">
|
||
提示:修改成功后将立即生效,建议使用强密码(至少 8 位,且包含两类以上字符)。
|
||
</div>
|
||
<span slot="footer" class="dialog-footer">
|
||
<el-button v-if="!passwordDialogLocked" @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: '/fun_command_rules' },
|
||
{ 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,
|
||
passwordDialogLocked: 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: 8, message: '新密码长度至少8位', trigger: 'blur' },
|
||
{
|
||
validator: function(rule, value, callback) {
|
||
const passwordText = String(value || '');
|
||
let score = 0;
|
||
if (/[A-Za-z]/.test(passwordText)) score += 1;
|
||
if (/\d/.test(passwordText)) score += 1;
|
||
if (/[^A-Za-z0-9]/.test(passwordText)) score += 1;
|
||
if (passwordText && score < 2) {
|
||
callback(new Error('新密码需至少包含字母、数字、符号中的两类'));
|
||
return;
|
||
}
|
||
callback();
|
||
},
|
||
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');
|
||
this.loadSecurityStatus();
|
||
},
|
||
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';
|
||
});
|
||
},
|
||
loadSecurityStatus() {
|
||
// 登录后主动拉一次账号安全状态:
|
||
// 1. 若仍在使用默认/弱口令,这里会强制打开改密弹窗;
|
||
// 2. 这样用户不需要额外摸索入口,首次进入后台就能完成风险收敛。
|
||
axios.get('/api/auth/security_status')
|
||
.then((response) => {
|
||
const data = (response.data || {}).data || {};
|
||
if (data.force_password_change) {
|
||
this.passwordDialogLocked = true;
|
||
this.openPasswordDialog();
|
||
this.$alert('当前后台账号仍在使用默认或弱密码,请先修改密码后再继续操作。', '安全提示', {
|
||
confirmButtonText: '去修改',
|
||
showClose: false,
|
||
closeOnClickModal: false,
|
||
closeOnPressEscape: false,
|
||
type: 'warning'
|
||
}).catch(() => {});
|
||
}
|
||
})
|
||
.catch(() => {
|
||
// 安全状态获取失败时不阻塞页面使用,避免偶发接口异常影响整体后台。
|
||
});
|
||
},
|
||
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();
|
||
}
|
||
});
|
||
},
|
||
handlePasswordDialogBeforeClose(done) {
|
||
if (this.passwordDialogLocked) {
|
||
this.$message.warning('当前账号需要先完成密码修改,暂时不能关闭该弹窗。');
|
||
return;
|
||
}
|
||
done();
|
||
},
|
||
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;
|
||
this.passwordDialogLocked = 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>
|
||
|
||
<script>
|
||
// 页面显示兜底逻辑:
|
||
// 1) 正常情况下由 Vue mounted 时给 .app-container 增加 loaded,页面平滑显示;
|
||
// 2) 若脚本中途报错导致 mounted 未执行,这里在 DOM 就绪后做一次延迟兜底,避免“整页空白”。
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
setTimeout(function() {
|
||
var app = document.querySelector('.app-container');
|
||
if (app && !app.classList.contains('loaded')) {
|
||
app.classList.add('loaded');
|
||
}
|
||
}, 1200);
|
||
});
|
||
</script>
|
||
|
||
{% block scripts %}{% endblock %}
|
||
</body>
|
||
</html>
|