Files
abot/admin/dashboard/templates/base.html
liuwei c208dcb2fb 修复后台首屏无样式闪烁并保留空白页兜底
变更项:

- 恢复 app-container 初始 opacity 为 0,避免页面在样式未就绪时先渲染无样式内容

- 新增 DOMContentLoaded 延迟兜底脚本:若 Vue mounted 未执行则自动补加 loaded,避免整页空白

- 保持现有导航与业务逻辑不变,仅修复页面首屏加载体验
2026-04-23 16:09:25 +08:00

1223 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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"
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="至少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: '/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,
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>
<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>