2026-01-16 22:06:46 +08:00
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>MIP广告自动化管理系统</title>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<link rel="stylesheet" href="/lib/element-ui.min.css">
|
|
|
|
|
|
<script src="/lib/vue.min.js"></script>
|
|
|
|
|
|
<script src="/lib/element-ui.min.js"></script>
|
|
|
|
|
|
<script src="/lib/axios.min.js"></script>
|
|
|
|
|
|
<script src="/lib/echarts.min.js"></script>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
|
|
|
|
|
<style>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
:root {
|
|
|
|
|
|
--ios-blue: #007AFF;
|
|
|
|
|
|
--ios-green: #34C759;
|
|
|
|
|
|
--ios-orange: #FF9500;
|
|
|
|
|
|
--ios-red: #FF3B30;
|
|
|
|
|
|
--ios-purple: #AF52DE;
|
|
|
|
|
|
--ios-gray: #8E8E93;
|
|
|
|
|
|
--ios-gray-2: #AEAEB2;
|
|
|
|
|
|
--ios-gray-3: #C7C7CC;
|
|
|
|
|
|
--ios-gray-4: #D1D1D6;
|
|
|
|
|
|
--ios-gray-5: #E5E5EA;
|
|
|
|
|
|
--ios-gray-6: #F2F2F7;
|
|
|
|
|
|
--ios-bg: linear-gradient(180deg, #F2F2F7 0%, #E5E5EA 100%);
|
|
|
|
|
|
--card-bg: rgba(255, 255, 255, 0.72);
|
|
|
|
|
|
--glass-bg: rgba(255, 255, 255, 0.6);
|
|
|
|
|
|
--text-primary: #1C1C1E;
|
|
|
|
|
|
--text-secondary: #3C3C43;
|
|
|
|
|
|
--text-tertiary: #48484A;
|
|
|
|
|
|
--shadow-sm: 0 1px 3px rgba(0,0,0,0.06), 0 1px 2px rgba(0,0,0,0.04);
|
|
|
|
|
|
--shadow-md: 0 4px 12px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04);
|
|
|
|
|
|
--shadow-lg: 0 8px 30px rgba(0,0,0,0.12), 0 4px 12px rgba(0,0,0,0.06);
|
|
|
|
|
|
--radius-sm: 10px;
|
|
|
|
|
|
--radius-md: 16px;
|
|
|
|
|
|
--radius-lg: 22px;
|
|
|
|
|
|
--radius-xl: 28px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
|
|
|
|
|
2026-01-16 22:06:46 +08:00
|
|
|
|
body {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', sans-serif;
|
|
|
|
|
|
background: var(--ios-bg);
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
-webkit-font-smoothing: antialiased;
|
|
|
|
|
|
-moz-osx-font-smoothing: grayscale;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
#app { min-height: 100vh; }
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
/* iOS风格顶部导航栏 */
|
|
|
|
|
|
.header {
|
|
|
|
|
|
height: 64px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.72);
|
|
|
|
|
|
backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
padding: 0 28px;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
z-index: 100;
|
|
|
|
|
|
border-bottom: 0.5px solid rgba(0, 0, 0, 0.1);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.header-left {
|
2026-01-16 22:06:46 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
gap: 14px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.logo {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
|
|
|
|
|
border-radius: 10px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.25);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.app-title {
|
|
|
|
|
|
color: var(--text-primary);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
font-weight: 600;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
letter-spacing: -0.3px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header-right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
gap: 16px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.status-pill {
|
2026-01-16 22:06:46 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
background: var(--ios-gray-6);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
font-size: 14px;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--text-secondary);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.status-indicator {
|
2026-01-16 22:06:46 +08:00
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
animation: breathe 2.5s ease-in-out infinite;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.status-indicator.running { background: var(--ios-green); box-shadow: 0 0 8px var(--ios-green); }
|
|
|
|
|
|
.status-indicator.stopped { background: var(--ios-red); }
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes breathe {
|
|
|
|
|
|
0%, 100% { opacity: 1; transform: scale(1); }
|
|
|
|
|
|
50% { opacity: 0.5; transform: scale(0.85); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.refresh-btn {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--ios-gray-6);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
transition: all 0.2s ease;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.refresh-btn:hover { background: var(--ios-gray-5); transform: scale(1.05); }
|
|
|
|
|
|
.refresh-btn:active { transform: scale(0.95); }
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格主体布局 */
|
|
|
|
|
|
.main-layout {
|
2026-01-16 22:06:46 +08:00
|
|
|
|
display: flex;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
padding-top: 64px;
|
|
|
|
|
|
min-height: 100vh;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
/* iOS风格侧边栏 */
|
2026-01-16 22:06:46 +08:00
|
|
|
|
.sidebar {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
width: 240px;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.65);
|
|
|
|
|
|
backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
min-height: calc(100vh - 64px);
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
top: 64px;
|
|
|
|
|
|
bottom: 0;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
overflow-y: auto;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
padding: 20px 12px;
|
|
|
|
|
|
border-right: 0.5px solid rgba(0, 0, 0, 0.08);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.sidebar .el-menu {
|
2026-01-16 22:06:46 +08:00
|
|
|
|
border: none;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu-item {
|
|
|
|
|
|
height: 50px;
|
|
|
|
|
|
line-height: 50px;
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
color: var(--text-secondary) !important;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu-item:hover {
|
|
|
|
|
|
background: rgba(0, 122, 255, 0.08);
|
|
|
|
|
|
color: var(--ios-blue) !important;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.sidebar .el-menu-item.is-active,
|
|
|
|
|
|
.sidebar .el-menu--horizontal > .el-menu-item.is-active {
|
|
|
|
|
|
background: var(--ios-blue) !important;
|
|
|
|
|
|
color: #ffffff !important;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(0, 122, 255, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu-item.is-active i,
|
|
|
|
|
|
.sidebar .el-menu-item.is-active span,
|
|
|
|
|
|
.sidebar .el-menu-item.is-active .el-menu-item__icon {
|
|
|
|
|
|
color: #ffffff !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu-item i {
|
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格内容区 */
|
|
|
|
|
|
.content-wrapper {
|
2026-01-16 22:06:46 +08:00
|
|
|
|
flex: 1;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
margin-left: 240px;
|
|
|
|
|
|
padding: 24px 28px;
|
|
|
|
|
|
background: transparent;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
/* iOS风格统计卡片 */
|
|
|
|
|
|
.stat-cards {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(4, 1fr);
|
|
|
|
|
|
gap: 20px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
background: var(--card-bg);
|
|
|
|
|
|
backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
padding: 24px;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
|
|
|
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
|
|
|
|
border: 0.5px solid rgba(255, 255, 255, 0.5);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card:hover {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
transform: translateY(-6px) scale(1.02);
|
|
|
|
|
|
box-shadow: var(--shadow-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
|
width: 52px;
|
|
|
|
|
|
height: 52px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-icon i {
|
|
|
|
|
|
font-size: 24px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.stat-card.primary .stat-icon { background: rgba(0, 122, 255, 0.12); }
|
|
|
|
|
|
.stat-card.primary .stat-icon i { color: var(--ios-blue); }
|
|
|
|
|
|
.stat-card.success .stat-icon { background: rgba(52, 199, 89, 0.12); }
|
|
|
|
|
|
.stat-card.success .stat-icon i { color: var(--ios-green); }
|
|
|
|
|
|
.stat-card.warning .stat-icon { background: rgba(255, 149, 0, 0.12); }
|
|
|
|
|
|
.stat-card.warning .stat-icon i { color: var(--ios-orange); }
|
|
|
|
|
|
.stat-card.danger .stat-icon { background: rgba(255, 59, 48, 0.12); }
|
|
|
|
|
|
.stat-card.danger .stat-icon i { color: var(--ios-red); }
|
|
|
|
|
|
|
2026-01-16 22:06:46 +08:00
|
|
|
|
.stat-label {
|
|
|
|
|
|
font-size: 14px;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
letter-spacing: -0.2px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-value {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
font-size: 34px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
letter-spacing: -1px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.stat-card.primary .stat-value { color: var(--ios-blue); }
|
|
|
|
|
|
.stat-card.success .stat-value { color: var(--ios-green); }
|
|
|
|
|
|
.stat-card.warning .stat-value { color: var(--ios-orange); }
|
|
|
|
|
|
.stat-card.danger .stat-value { color: var(--ios-red); }
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
/* iOS风格卡片 */
|
|
|
|
|
|
.card {
|
|
|
|
|
|
background: var(--card-bg);
|
|
|
|
|
|
backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
margin-bottom: 24px;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
border: 0.5px solid rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
overflow: hidden;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-header {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
border-bottom: 0.5px solid var(--ios-gray-5);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
background: rgba(255, 255, 255, 0.4);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card-title {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
font-size: 18px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
font-weight: 600;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
letter-spacing: -0.3px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.card-body { padding: 24px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格图表网格 */
|
|
|
|
|
|
.chart-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-card {
|
|
|
|
|
|
background: var(--card-bg);
|
|
|
|
|
|
backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
|
|
|
|
|
border: 0.5px solid rgba(255, 255, 255, 0.5);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.chart-title {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding-left: 12px;
|
|
|
|
|
|
border-left: 3px solid var(--ios-blue);
|
|
|
|
|
|
letter-spacing: -0.2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chart-container { height: 280px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格工具栏 */
|
|
|
|
|
|
.toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toolbar > * {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toolbar .el-input {
|
|
|
|
|
|
width: 220px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toolbar .el-select {
|
|
|
|
|
|
width: 130px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toolbar .el-input-group {
|
|
|
|
|
|
width: 220px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格输入框覆盖 */
|
|
|
|
|
|
.el-input__inner {
|
|
|
|
|
|
border-radius: var(--radius-sm) !important;
|
|
|
|
|
|
border-color: var(--ios-gray-4) !important;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.8) !important;
|
|
|
|
|
|
transition: all 0.2s ease !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-input__inner:focus {
|
|
|
|
|
|
border-color: var(--ios-blue) !important;
|
|
|
|
|
|
box-shadow: 0 0 0 3px rgba(0, 122, 255, 0.15) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格按钮覆盖 */
|
|
|
|
|
|
.el-button {
|
|
|
|
|
|
border-radius: var(--radius-sm) !important;
|
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
|
letter-spacing: -0.2px !important;
|
|
|
|
|
|
transition: all 0.2s ease !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-button--primary {
|
|
|
|
|
|
background: var(--ios-blue) !important;
|
|
|
|
|
|
border-color: var(--ios-blue) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-button--primary:hover {
|
|
|
|
|
|
background: #0066D6 !important;
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-button--success {
|
|
|
|
|
|
background: var(--ios-green) !important;
|
|
|
|
|
|
border-color: var(--ios-green) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-button--danger {
|
|
|
|
|
|
background: var(--ios-red) !important;
|
|
|
|
|
|
border-color: var(--ios-red) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-button--warning {
|
|
|
|
|
|
background: var(--ios-orange) !important;
|
|
|
|
|
|
border-color: var(--ios-orange) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格表格 */
|
2026-01-16 22:06:46 +08:00
|
|
|
|
.el-table {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: transparent !important;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-table th {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
background: rgba(242, 242, 247, 0.8) !important;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
font-weight: 600;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
color: var(--text-secondary) !important;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.5px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.el-table td, .el-table th { padding: 14px 0; }
|
|
|
|
|
|
|
|
|
|
|
|
.el-table--striped .el-table__body tr.el-table__row--striped td {
|
|
|
|
|
|
background: rgba(242, 242, 247, 0.5) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格行悬停效果增强 */
|
|
|
|
|
|
.el-table__body tr {
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-table__body tr:hover > td {
|
|
|
|
|
|
background: rgba(0, 122, 255, 0.06) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-table__body tr:hover {
|
|
|
|
|
|
transform: scale(1.002);
|
|
|
|
|
|
box-shadow: 0 2px 12px rgba(0, 122, 255, 0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格行选中状态 */
|
|
|
|
|
|
.el-table__body tr.current-row > td {
|
|
|
|
|
|
background: rgba(0, 122, 255, 0.1) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-table__body tr.selection-row > td,
|
|
|
|
|
|
.el-table .el-table__body tr.el-table__row.hover-row > td {
|
|
|
|
|
|
background: rgba(0, 122, 255, 0.08) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格复选框样式 */
|
|
|
|
|
|
.el-checkbox__inner {
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
transition: all 0.2s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-checkbox__input.is-checked .el-checkbox__inner {
|
|
|
|
|
|
background-color: var(--ios-blue);
|
|
|
|
|
|
border-color: var(--ios-blue);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-checkbox__inner:hover {
|
|
|
|
|
|
border-color: var(--ios-blue);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-checkbox__inner::after {
|
|
|
|
|
|
left: 6px;
|
|
|
|
|
|
top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 操作列按钮优化 */
|
|
|
|
|
|
.table-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-actions .el-button--text {
|
|
|
|
|
|
padding: 6px 12px !important;
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
transition: all 0.2s ease !important;
|
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-actions .el-button--text:hover {
|
|
|
|
|
|
background: rgba(0, 122, 255, 0.1) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-actions .el-button--text.danger:hover {
|
|
|
|
|
|
background: rgba(255, 59, 48, 0.1) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 操作按钮图标 */
|
|
|
|
|
|
.action-btn {
|
|
|
|
|
|
padding: 6px 10px !important;
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
font-size: 13px !important;
|
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
|
transition: all 0.2s ease !important;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn:hover {
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn.primary {
|
|
|
|
|
|
color: var(--ios-blue) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn.primary:hover {
|
|
|
|
|
|
background: rgba(0, 122, 255, 0.1) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn.danger {
|
|
|
|
|
|
color: var(--ios-red) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn.danger:hover {
|
|
|
|
|
|
background: rgba(255, 59, 48, 0.1) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn.success {
|
|
|
|
|
|
color: var(--ios-green) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-btn.success:hover {
|
|
|
|
|
|
background: rgba(52, 199, 89, 0.1) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格内标签优化 */
|
|
|
|
|
|
.el-table .el-tag {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格内开关优化 */
|
|
|
|
|
|
.el-table .el-switch {
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-table .el-switch__core {
|
|
|
|
|
|
height: 24px !important;
|
|
|
|
|
|
border-radius: 12px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-table .el-switch__core::after {
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-table .el-switch.is-checked .el-switch__core::after {
|
|
|
|
|
|
margin-left: -22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.url-cell {
|
|
|
|
|
|
max-width: 320px;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.url-link {
|
|
|
|
|
|
color: var(--ios-blue);
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
transition: opacity 0.2s;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.url-link:hover { opacity: 0.7; }
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格开关 */
|
|
|
|
|
|
.el-switch.is-checked .el-switch__core {
|
|
|
|
|
|
background-color: var(--ios-green) !important;
|
|
|
|
|
|
border-color: var(--ios-green) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格标签页 */
|
|
|
|
|
|
.el-tabs__item {
|
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
|
font-size: 15px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-tabs__item.is-active {
|
|
|
|
|
|
color: var(--ios-blue) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-tabs__active-bar {
|
|
|
|
|
|
background-color: var(--ios-blue) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格日志查看器 */
|
|
|
|
|
|
.log-viewer {
|
|
|
|
|
|
background: #1C1C1E;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
min-height: 500px;
|
|
|
|
|
|
max-height: calc(100vh - 280px);
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.log-entry {
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 14px;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.log-time { color: #98989D; min-width: 150px; font-size: 12px; }
|
|
|
|
|
|
|
|
|
|
|
|
.log-level {
|
|
|
|
|
|
padding: 3px 10px;
|
|
|
|
|
|
border-radius: 6px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
min-width: 70px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.log-level.INFO { background: var(--ios-blue); color: white; }
|
|
|
|
|
|
.log-level.WARNING { background: var(--ios-orange); color: white; }
|
|
|
|
|
|
.log-level.ERROR { background: var(--ios-red); color: white; }
|
|
|
|
|
|
.log-level.DEBUG { background: var(--ios-gray); color: white; }
|
|
|
|
|
|
|
|
|
|
|
|
.log-message { color: #E5E5EA; flex: 1; word-break: break-all; line-height: 1.5; }
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格配置组 */
|
|
|
|
|
|
.config-group {
|
|
|
|
|
|
margin-bottom: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.config-group-title {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
padding-bottom: 10px;
|
|
|
|
|
|
border-bottom: 1px solid var(--ios-gray-5);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格上传区域 */
|
|
|
|
|
|
.upload-area {
|
|
|
|
|
|
border: 2px dashed var(--ios-gray-4);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: 48px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
transition: all 0.3s ease;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.upload-area:hover {
|
|
|
|
|
|
border-color: var(--ios-blue);
|
|
|
|
|
|
background: rgba(0, 122, 255, 0.04);
|
|
|
|
|
|
transform: scale(1.01);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.upload-icon {
|
|
|
|
|
|
font-size: 52px;
|
|
|
|
|
|
color: var(--ios-blue);
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.upload-text {
|
|
|
|
|
|
color: var(--text-secondary);
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.upload-hint {
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
color: var(--ios-gray);
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格分页 */
|
|
|
|
|
|
.pagination-wrapper {
|
|
|
|
|
|
margin-top: 20px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-pagination {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-pagination .el-pager li {
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-pagination .el-pager li.active {
|
|
|
|
|
|
background: var(--ios-blue) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格标签 */
|
|
|
|
|
|
.el-tag {
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-tag--success {
|
|
|
|
|
|
background: rgba(52, 199, 89, 0.12) !important;
|
|
|
|
|
|
border-color: rgba(52, 199, 89, 0.2) !important;
|
|
|
|
|
|
color: var(--ios-green) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-tag--danger {
|
|
|
|
|
|
background: rgba(255, 59, 48, 0.12) !important;
|
|
|
|
|
|
border-color: rgba(255, 59, 48, 0.2) !important;
|
|
|
|
|
|
color: var(--ios-red) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* iOS风格描述列表 */
|
|
|
|
|
|
.el-descriptions {
|
|
|
|
|
|
background: rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-descriptions__header {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 响应式设计 */
|
|
|
|
|
|
@media (max-width: 1400px) {
|
|
|
|
|
|
.stat-cards { grid-template-columns: repeat(2, 1fr); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 1024px) {
|
|
|
|
|
|
.chart-grid { grid-template-columns: 1fr; }
|
|
|
|
|
|
.toolbar { flex-direction: column; align-items: stretch; }
|
|
|
|
|
|
.toolbar .el-input, .toolbar .el-select { width: 100%; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
|
width: 72px;
|
|
|
|
|
|
padding: 16px 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sidebar .el-menu-item span { display: none; }
|
|
|
|
|
|
.sidebar .el-menu-item { justify-content: center; padding: 0; }
|
|
|
|
|
|
.sidebar .el-menu-item i { margin: 0; font-size: 22px; }
|
|
|
|
|
|
.content-wrapper { margin-left: 72px; padding: 16px; }
|
|
|
|
|
|
.stat-cards { grid-template-columns: repeat(2, 1fr); gap: 12px; }
|
|
|
|
|
|
.stat-card { padding: 16px; }
|
|
|
|
|
|
.stat-value { font-size: 26px; }
|
|
|
|
|
|
.stat-icon { width: 44px; height: 44px; }
|
|
|
|
|
|
.stat-icon i { font-size: 20px; }
|
|
|
|
|
|
.header { padding: 0 16px; }
|
|
|
|
|
|
.app-title { font-size: 16px; }
|
|
|
|
|
|
.chart-container { height: 220px; }
|
|
|
|
|
|
.card-header { padding: 16px; flex-wrap: wrap; gap: 12px; }
|
|
|
|
|
|
.card-body { padding: 16px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 移动端表格优化 */
|
|
|
|
|
|
.el-table { font-size: 13px; }
|
|
|
|
|
|
.url-cell { max-width: 160px; }
|
|
|
|
|
|
.action-btn { padding: 8px !important; font-size: 12px !important; }
|
|
|
|
|
|
.action-btn span { display: none; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
top: auto;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 72px;
|
|
|
|
|
|
min-height: auto;
|
|
|
|
|
|
z-index: 99;
|
|
|
|
|
|
border-right: none;
|
|
|
|
|
|
border-top: 0.5px solid rgba(0, 0, 0, 0.1);
|
|
|
|
|
|
padding: 8px 4px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu {
|
|
|
|
|
|
display: flex !important;
|
|
|
|
|
|
flex-direction: row !important;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
justify-content: space-around;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu-item {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
line-height: 56px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu-item i {
|
|
|
|
|
|
font-size: 20px;
|
|
|
|
|
|
margin: 0 0 2px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu-item span {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar .el-menu-item.is-active {
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-wrapper {
|
|
|
|
|
|
margin-left: 0;
|
|
|
|
|
|
margin-bottom: 72px;
|
|
|
|
|
|
padding: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.main-layout {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
padding: 0 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.status-pill {
|
|
|
|
|
|
padding: 6px 10px;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-cards {
|
|
|
|
|
|
grid-template-columns: repeat(2, 1fr);
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-icon {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-icon i { font-size: 18px; }
|
|
|
|
|
|
.stat-label { font-size: 12px; margin-bottom: 4px; }
|
|
|
|
|
|
.stat-value { font-size: 22px; }
|
|
|
|
|
|
|
|
|
|
|
|
.chart-card { padding: 14px; }
|
|
|
|
|
|
.chart-title { font-size: 14px; margin-bottom: 12px; }
|
|
|
|
|
|
.chart-container { height: 200px; }
|
|
|
|
|
|
|
|
|
|
|
|
.card { margin-bottom: 16px; }
|
|
|
|
|
|
.card-header { padding: 14px; }
|
|
|
|
|
|
.card-title { font-size: 16px; }
|
|
|
|
|
|
.card-body { padding: 14px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 移动端表格 */
|
|
|
|
|
|
.el-table { font-size: 12px; }
|
|
|
|
|
|
.el-table th, .el-table td { padding: 10px 0; }
|
|
|
|
|
|
.url-cell { max-width: 120px; font-size: 12px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 隐藏部分表格列 */
|
|
|
|
|
|
.hide-on-mobile { display: none !important; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 移动端按钮 */
|
|
|
|
|
|
.el-button { padding: 10px 16px !important; }
|
|
|
|
|
|
.action-btn { padding: 10px 8px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 移动端分页 */
|
|
|
|
|
|
.pagination-wrapper {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.el-pagination {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.el-pagination .el-pager li {
|
|
|
|
|
|
min-width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
line-height: 28px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 移动端表单 */
|
|
|
|
|
|
.el-form-item { margin-bottom: 16px; }
|
|
|
|
|
|
.el-form-item__label { font-size: 13px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 移动端上传区域 */
|
|
|
|
|
|
.upload-area { padding: 24px 16px; }
|
|
|
|
|
|
.upload-icon { font-size: 36px; }
|
|
|
|
|
|
.upload-text { font-size: 14px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 移动端日志 */
|
|
|
|
|
|
.log-viewer {
|
|
|
|
|
|
max-height: 350px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
padding: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.log-entry { flex-wrap: wrap; gap: 8px; }
|
|
|
|
|
|
.log-time { min-width: auto; font-size: 11px; }
|
|
|
|
|
|
.log-level { font-size: 10px; padding: 2px 8px; min-width: 55px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 移动端tabs */
|
|
|
|
|
|
.el-tabs__item {
|
|
|
|
|
|
font-size: 14px !important;
|
|
|
|
|
|
padding: 0 12px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 触摸优化 */
|
|
|
|
|
|
@media (hover: none) and (pointer: coarse) {
|
|
|
|
|
|
.el-button, .action-btn, .el-menu-item, .refresh-btn {
|
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
|
min-width: 44px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-switch {
|
|
|
|
|
|
transform: scale(1.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-checkbox__inner {
|
|
|
|
|
|
width: 22px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 滚动条美化 */
|
|
|
|
|
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
|
|
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
|
|
|
|
::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: var(--ios-gray-4);
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
::-webkit-scrollbar-thumb:hover { background: var(--ios-gray-3); }
|
|
|
|
|
|
|
|
|
|
|
|
/* 动画效果 */
|
|
|
|
|
|
.card, .stat-card, .chart-card {
|
|
|
|
|
|
animation: fadeInUp 0.4s ease-out backwards;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 交错动画延迟 */
|
|
|
|
|
|
.stat-card:nth-child(1) { animation-delay: 0.05s; }
|
|
|
|
|
|
.stat-card:nth-child(2) { animation-delay: 0.1s; }
|
|
|
|
|
|
.stat-card:nth-child(3) { animation-delay: 0.15s; }
|
|
|
|
|
|
.stat-card:nth-child(4) { animation-delay: 0.2s; }
|
|
|
|
|
|
|
|
|
|
|
|
.chart-card:nth-child(1) { animation-delay: 0.25s; }
|
|
|
|
|
|
.chart-card:nth-child(2) { animation-delay: 0.3s; }
|
|
|
|
|
|
.chart-card:nth-child(3) { animation-delay: 0.35s; }
|
|
|
|
|
|
.chart-card:nth-child(4) { animation-delay: 0.4s; }
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes fadeInUp {
|
|
|
|
|
|
from {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(20px);
|
|
|
|
|
|
}
|
|
|
|
|
|
to {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 骨架屏动画 */
|
|
|
|
|
|
@keyframes shimmer {
|
|
|
|
|
|
0% { background-position: -200% 0; }
|
|
|
|
|
|
100% { background-position: 200% 0; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton {
|
|
|
|
|
|
background: linear-gradient(90deg,
|
|
|
|
|
|
var(--ios-gray-5) 25%,
|
|
|
|
|
|
var(--ios-gray-6) 37%,
|
|
|
|
|
|
var(--ios-gray-5) 63%);
|
|
|
|
|
|
background-size: 200% 100%;
|
|
|
|
|
|
animation: shimmer 1.5s ease-in-out infinite;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-text {
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
border-radius: 7px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-text.lg { height: 32px; width: 80px; border-radius: 8px; }
|
|
|
|
|
|
.skeleton-text.sm { height: 12px; width: 60%; }
|
|
|
|
|
|
.skeleton-text.xs { height: 10px; width: 40%; }
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-icon {
|
|
|
|
|
|
width: 52px;
|
|
|
|
|
|
height: 52px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-chart {
|
|
|
|
|
|
height: 280px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: 16px 0;
|
|
|
|
|
|
border-bottom: 1px solid var(--ios-gray-5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-row .skeleton-cell {
|
|
|
|
|
|
margin-right: 12px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.skeleton-row .skeleton-cell.flex { flex: 1; }
|
|
|
|
|
|
|
|
|
|
|
|
/* 统计卡片骨架屏 */
|
|
|
|
|
|
.stat-card-skeleton {
|
|
|
|
|
|
background: var(--card-bg);
|
|
|
|
|
|
backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: 24px;
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
|
|
|
|
|
border: 0.5px solid rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格骨架屏 */
|
|
|
|
|
|
.table-skeleton {
|
|
|
|
|
|
padding: 0 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 加载遮罩优化 */
|
|
|
|
|
|
.el-loading-mask {
|
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.85) !important;
|
|
|
|
|
|
backdrop-filter: blur(8px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-loading-spinner .circular {
|
|
|
|
|
|
width: 36px !important;
|
|
|
|
|
|
height: 36px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-loading-spinner .path {
|
|
|
|
|
|
stroke: var(--ios-blue) !important;
|
|
|
|
|
|
stroke-width: 3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-loading-text {
|
|
|
|
|
|
color: var(--ios-blue) !important;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
margin-top: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 按钮加载状态 */
|
|
|
|
|
|
.el-button.is-loading {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.el-button.is-loading:before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: -1px;
|
|
|
|
|
|
top: -1px;
|
|
|
|
|
|
right: -1px;
|
|
|
|
|
|
bottom: -1px;
|
|
|
|
|
|
border-radius: inherit;
|
|
|
|
|
|
background-color: rgba(255,255,255,0.35);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 脉冲动画 - 数据更新提示 */
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
|
0%, 100% { opacity: 1; }
|
|
|
|
|
|
50% { opacity: 0.6; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.data-updating {
|
|
|
|
|
|
animation: pulse 1s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 渐显效果 - 内容区域 */
|
|
|
|
|
|
.content-fade-enter-active {
|
|
|
|
|
|
transition: all 0.3s ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-fade-leave-active {
|
|
|
|
|
|
transition: all 0.2s ease-in;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.content-fade-enter, .content-fade-leave-to {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transform: translateY(10px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 刷新按钮旋转动画 */
|
|
|
|
|
|
.refresh-btn.refreshing i {
|
|
|
|
|
|
animation: spin 0.8s linear infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
|
from { transform: rotate(0deg); }
|
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 表格行淡入动画 */
|
|
|
|
|
|
.el-table__body-wrapper {
|
|
|
|
|
|
transition: opacity 0.25s ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table-loading .el-table__body-wrapper {
|
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 空状态优化 */
|
|
|
|
|
|
.empty-state {
|
|
|
|
|
|
padding: 60px 20px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: var(--ios-gray);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state i {
|
|
|
|
|
|
font-size: 48px;
|
|
|
|
|
|
color: var(--ios-gray-3);
|
|
|
|
|
|
margin-bottom: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state p {
|
|
|
|
|
|
font-size: 15px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 登录页面样式 */
|
|
|
|
|
|
.login-container {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: var(--ios-bg);
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-card {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
|
background: var(--card-bg);
|
|
|
|
|
|
backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
-webkit-backdrop-filter: saturate(180%) blur(20px);
|
|
|
|
|
|
border-radius: var(--radius-xl);
|
|
|
|
|
|
box-shadow: var(--shadow-lg);
|
|
|
|
|
|
padding: 48px 40px;
|
|
|
|
|
|
border: 0.5px solid rgba(255, 255, 255, 0.5);
|
|
|
|
|
|
animation: fadeInUp 0.5s ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-header {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 40px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-logo {
|
|
|
|
|
|
width: 72px;
|
|
|
|
|
|
height: 72px;
|
|
|
|
|
|
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin: 0 auto 20px;
|
|
|
|
|
|
box-shadow: 0 8px 24px rgba(0, 122, 255, 0.3);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-logo i {
|
|
|
|
|
|
font-size: 36px;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-title {
|
|
|
|
|
|
font-size: 24px;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
color: var(--text-primary);
|
|
|
|
|
|
letter-spacing: -0.5px;
|
|
|
|
|
|
margin-bottom: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-subtitle {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: var(--text-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-form .el-form-item {
|
|
|
|
|
|
margin-bottom: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-form .el-input__inner {
|
|
|
|
|
|
height: 48px !important;
|
|
|
|
|
|
font-size: 15px !important;
|
|
|
|
|
|
padding-left: 44px !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-form .el-input__prefix {
|
|
|
|
|
|
left: 12px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-form .el-input__prefix i {
|
|
|
|
|
|
font-size: 18px;
|
|
|
|
|
|
color: var(--ios-gray);
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-form .el-input__suffix {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-btn {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
border-radius: var(--radius-md) !important;
|
|
|
|
|
|
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%) !important;
|
|
|
|
|
|
border: none !important;
|
|
|
|
|
|
box-shadow: 0 4px 16px rgba(0, 122, 255, 0.3);
|
|
|
|
|
|
transition: all 0.3s ease !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-btn:hover {
|
|
|
|
|
|
transform: translateY(-2px);
|
|
|
|
|
|
box-shadow: 0 6px 20px rgba(0, 122, 255, 0.4);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.login-btn:active {
|
|
|
|
|
|
transform: translateY(0);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
/* 用户信息下拉 */
|
|
|
|
|
|
.user-dropdown {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
border-radius: 20px;
|
|
|
|
|
|
background: var(--ios-gray-6);
|
|
|
|
|
|
transition: all 0.2s ease;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.user-dropdown:hover {
|
|
|
|
|
|
background: var(--ios-gray-5);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.user-avatar {
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.user-avatar i {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
color: white;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.user-name {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--text-secondary);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
/* 聊天记录对话框样式 */
|
|
|
|
|
|
.chat-log-dialog .el-dialog__body {
|
|
|
|
|
|
padding: 0;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.chat-log-content {
|
|
|
|
|
|
max-height: 65vh;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
background: #1C1C1E;
|
|
|
|
|
|
border-radius: 0;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
.chat-log-content pre {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 20px 24px;
|
|
|
|
|
|
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.7;
|
|
|
|
|
|
color: #E5E5EA;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-wrap: break-word;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<div id="app" v-loading.fullscreen.lock="fullLoading">
|
|
|
|
|
|
<!-- ==================== 登录页面 ==================== -->
|
|
|
|
|
|
<div class="login-container" v-if="!isLoggedIn">
|
|
|
|
|
|
<div class="login-card">
|
|
|
|
|
|
<div class="login-header">
|
|
|
|
|
|
<div class="login-logo">
|
|
|
|
|
|
<i class="el-icon-data-board"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<h1 class="login-title">MIP 广告自动化</h1>
|
|
|
|
|
|
<p class="login-subtitle">请登录以继续使用系统</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-form :model="loginForm" :rules="loginRules" ref="loginForm" class="login-form" @submit.native.prevent="handleLogin">
|
|
|
|
|
|
<el-form-item prop="username">
|
|
|
|
|
|
<el-input v-model="loginForm.username" placeholder="请输入用户名" prefix-icon="el-icon-user" clearable></el-input>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item prop="password">
|
|
|
|
|
|
<el-input v-model="loginForm.password" type="password" placeholder="请输入密码" prefix-icon="el-icon-lock" show-password @keyup.enter.native="handleLogin"></el-input>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
<el-form-item>
|
|
|
|
|
|
<el-button type="primary" class="login-btn" :loading="loginLoading" @click="handleLogin">登 录</el-button>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== 主应用界面 ==================== -->
|
|
|
|
|
|
<template v-else>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
<!-- 顶部导航 -->
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<header class="header">
|
|
|
|
|
|
<div class="header-left">
|
|
|
|
|
|
<div class="logo">
|
|
|
|
|
|
<i class="el-icon-data-board" style="font-size: 20px; color: white;"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span class="app-title">MIP 广告自动化</span>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="header-right">
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<div class="status-pill">
|
|
|
|
|
|
<span class="status-indicator" :class="schedulerRunning ? 'running' : 'stopped'"></span>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
<span>{{ schedulerStatus }}</span>
|
|
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<button class="refresh-btn" :class="{ refreshing: isRefreshing }" @click="refreshAll" title="刷新">
|
|
|
|
|
|
<i class="el-icon-refresh" style="font-size: 16px; color: var(--text-secondary);"></i>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<el-dropdown @command="handleUserCommand" trigger="click">
|
|
|
|
|
|
<div class="user-dropdown">
|
|
|
|
|
|
<div class="user-avatar"><i class="el-icon-user"></i></div>
|
|
|
|
|
|
<span class="user-name">{{ username }}</span>
|
|
|
|
|
|
<i class="el-icon-arrow-down" style="font-size: 12px; color: var(--ios-gray);"></i>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-dropdown-menu slot="dropdown">
|
|
|
|
|
|
<el-dropdown-item command="logout" icon="el-icon-switch-button">退出登录</el-dropdown-item>
|
|
|
|
|
|
</el-dropdown-menu>
|
|
|
|
|
|
</el-dropdown>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
</header>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<!-- 主体布局 -->
|
|
|
|
|
|
<div class="main-layout">
|
|
|
|
|
|
<!-- 侧边栏 -->
|
|
|
|
|
|
<aside class="sidebar">
|
|
|
|
|
|
<el-menu :default-active="activeMenu" background-color="transparent" text-color="#3C3C43" active-text-color="#ffffff" @select="handleMenuSelect">
|
|
|
|
|
|
<el-menu-item index="dashboard"><i class="el-icon-data-analysis"></i><span>数据概览</span></el-menu-item>
|
|
|
|
|
|
<el-menu-item index="scheduler"><i class="el-icon-time"></i><span>调度管理</span></el-menu-item>
|
|
|
|
|
|
<el-menu-item index="sites"><i class="el-icon-link"></i><span>链接管理</span></el-menu-item>
|
|
|
|
|
|
<el-menu-item index="database"><i class="el-icon-folder"></i><span>数据记录</span></el-menu-item>
|
|
|
|
|
|
<el-menu-item index="logs"><i class="el-icon-document"></i><span>系统日志</span></el-menu-item>
|
|
|
|
|
|
<el-menu-item index="config"><i class="el-icon-setting"></i><span>系统配置</span></el-menu-item>
|
|
|
|
|
|
<el-menu-item index="query-mining"><i class="el-icon-search"></i><span>Query挖掘</span></el-menu-item>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</el-menu>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
</aside>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<!-- 内容区 -->
|
|
|
|
|
|
<main class="content-wrapper">
|
|
|
|
|
|
<!-- ==================== 数据概览 ==================== -->
|
|
|
|
|
|
<section v-show="activeMenu === 'dashboard'">
|
|
|
|
|
|
<!-- 统计卡片 - 骨架屏状态 -->
|
|
|
|
|
|
<div class="stat-cards" v-if="statsLoading">
|
|
|
|
|
|
<div class="stat-card-skeleton" v-for="i in 4" :key="'skeleton-'+i">
|
|
|
|
|
|
<div class="skeleton skeleton-icon"></div>
|
|
|
|
|
|
<div class="skeleton skeleton-text sm"></div>
|
|
|
|
|
|
<div class="skeleton skeleton-text lg"></div>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<!-- 统计卡片 - 正常状态 -->
|
|
|
|
|
|
<div class="stat-cards" v-else>
|
|
|
|
|
|
<div class="stat-card primary">
|
|
|
|
|
|
<div class="stat-icon"><i class="el-icon-files"></i></div>
|
|
|
|
|
|
<div class="stat-label">总站点数</div>
|
|
|
|
|
|
<div class="stat-value">{{ stats.total_sites || 0 }}</div>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<div class="stat-card success">
|
|
|
|
|
|
<div class="stat-icon"><i class="el-icon-mouse"></i></div>
|
|
|
|
|
|
<div class="stat-label">总点击次数</div>
|
|
|
|
|
|
<div class="stat-value">{{ stats.total_clicks || 0 }}</div>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<div class="stat-card warning">
|
|
|
|
|
|
<div class="stat-icon"><i class="el-icon-chat-dot-round"></i></div>
|
|
|
|
|
|
<div class="stat-label">获得回复</div>
|
|
|
|
|
|
<div class="stat-value">{{ stats.total_replies || 0 }}</div>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<div class="stat-card danger">
|
|
|
|
|
|
<div class="stat-icon"><i class="el-icon-data-line"></i></div>
|
|
|
|
|
|
<div class="stat-label">回复率</div>
|
|
|
|
|
|
<div class="stat-value">{{ stats.reply_rate || '0%' }}</div>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<!-- 图表区 - 骨架屏状态 -->
|
|
|
|
|
|
<div class="chart-grid" v-if="chartsLoading">
|
|
|
|
|
|
<div class="chart-card" v-for="i in 4" :key="'chart-skeleton-'+i">
|
|
|
|
|
|
<div class="skeleton skeleton-text" style="width: 120px; margin-bottom: 16px;"></div>
|
|
|
|
|
|
<div class="skeleton skeleton-chart"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<!-- 图表区 - 正常状态 -->
|
|
|
|
|
|
<div class="chart-grid" v-else>
|
|
|
|
|
|
<div class="chart-card">
|
|
|
|
|
|
<div class="chart-title">点击趋势(近7天)</div>
|
|
|
|
|
|
<div id="trendChart" class="chart-container"></div>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<div class="chart-card">
|
|
|
|
|
|
<div class="chart-title">回复率分布</div>
|
|
|
|
|
|
<div id="pieChart" class="chart-container"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-card">
|
|
|
|
|
|
<div class="chart-title">时段分布(近7天)</div>
|
|
|
|
|
|
<div id="hourlyChart" class="chart-container"></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chart-card">
|
|
|
|
|
|
<div class="chart-title">Top10活跃站点</div>
|
|
|
|
|
|
<div id="topSitesChart" class="chart-container"></div>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
</section>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<!-- ==================== 调度管理 ==================== -->
|
|
|
|
|
|
<section v-show="activeMenu === 'scheduler'">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
|
|
|
<span style="font-size: 16px; font-weight: 600;">调度器控制</span>
|
|
|
|
|
|
<el-tag :type="schedulerRunning ? 'success' : 'danger'" effect="dark">
|
|
|
|
|
|
{{ schedulerRunning ? '运行中' : '已停止' }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-row :gutter="20">
|
|
|
|
|
|
<el-col :span="24">
|
|
|
|
|
|
<el-button type="success" icon="el-icon-video-play" @click="startScheduler" :disabled="schedulerRunning">启动调度器</el-button>
|
|
|
|
|
|
<el-button type="danger" icon="el-icon-video-pause" @click="stopScheduler" :disabled="!schedulerRunning">停止调度器</el-button>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
<el-divider></el-divider>
|
|
|
|
|
|
<el-descriptions title="调度规则" :column="2" border>
|
|
|
|
|
|
<el-descriptions-item label="工作时间">{{ config.work_time?.start_hour || 9 }}:00 - {{ config.work_time?.end_hour || 21 }}:00</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="点击间隔">{{ config.click_strategy?.click_interval_minutes || 30 }} 分钟</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="每日点击">{{ config.click_strategy?.min_click_count || 1 }} - {{ config.click_strategy?.max_click_count || 3 }} 次/站点</el-descriptions-item>
|
|
|
|
|
|
<el-descriptions-item label="回复等待">{{ config.task_config?.reply_wait_timeout || 30 }} 秒</el-descriptions-item>
|
|
|
|
|
|
</el-descriptions>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== 链接管理 ==================== -->
|
|
|
|
|
|
<section v-show="activeMenu === 'sites'">
|
|
|
|
|
|
<!-- 站点列表 -->
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
|
|
|
<span style="font-size: 16px; font-weight: 600;">站点列表</span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
ref="csvUpload"
|
|
|
|
|
|
:action="apiBase + '/api/sites/import'"
|
|
|
|
|
|
:data="importParams"
|
|
|
|
|
|
:headers="uploadHeaders"
|
|
|
|
|
|
:on-success="handleUploadSuccess"
|
|
|
|
|
|
:on-error="handleUploadError"
|
|
|
|
|
|
:before-upload="beforeUpload"
|
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
|
:disabled="uploadLoading"
|
|
|
|
|
|
with-credentials
|
|
|
|
|
|
accept=".xlsx,.xls,.csv"
|
|
|
|
|
|
style="display: inline-block; margin-right: 8px;">
|
|
|
|
|
|
<el-button size="small" type="success" icon="el-icon-upload2" :loading="uploadLoading">
|
|
|
|
|
|
{{ uploadLoading ? (importTask ? `导入中 ${importTask.current || 0}/${importTask.total || 0}` : '解析中...') : '导入' }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
<el-button size="small" type="primary" plain icon="el-icon-download" @click="exportSites">导出</el-button>
|
|
|
|
|
|
<el-button size="small" icon="el-icon-refresh" @click="loadSites">刷新</el-button>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<el-row :gutter="16" style="margin-bottom: 16px;">
|
|
|
|
|
|
<el-col :span="6">
|
|
|
|
|
|
<el-input v-model="siteFilter.keyword" placeholder="搜索URL/名称" clearable @clear="loadSites" @keyup.enter.native="loadSites">
|
|
|
|
|
|
<el-button slot="append" icon="el-icon-search" @click="loadSites"></el-button>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
|
<el-select v-model="siteFilter.status" placeholder="状态" clearable @change="loadSites" style="width: 100%;">
|
|
|
|
|
|
<el-option label="启用点击" value="active"></el-option>
|
|
|
|
|
|
<el-option label="暂停点击" value="inactive"></el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
|
<el-select v-model="siteFilter.sort_by" placeholder="排序" @change="loadSites" style="width: 100%;">
|
|
|
|
|
|
<el-option label="创建时间" value="created_at"></el-option>
|
|
|
|
|
|
<el-option label="点击次数" value="click_count"></el-option>
|
|
|
|
|
|
<el-option label="回复次数" value="reply_count"></el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="10" v-if="selectedSites.length">
|
|
|
|
|
|
<el-button-group>
|
|
|
|
|
|
<el-button size="small" type="success" @click="batchEnableSites">启用选中</el-button>
|
|
|
|
|
|
<el-button size="small" type="warning" @click="batchDisableSites">暂停选中</el-button>
|
|
|
|
|
|
<el-button size="small" type="danger" @click="batchDeleteSites">删除选中</el-button>
|
|
|
|
|
|
</el-button-group>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
|
|
|
|
|
|
<el-table :data="siteList" v-loading="sitesLoading" @selection-change="handleSiteSelection" stripe border>
|
|
|
|
|
|
<el-table-column type="selection" width="50"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="site_url" label="链接地址" min-width="300">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-link :href="scope.row.site_url" target="_blank" type="primary" :underline="false">
|
|
|
|
|
|
{{ scope.row.site_url }}
|
|
|
|
|
|
</el-link>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-switch
|
|
|
|
|
|
v-model="scope.row.status"
|
|
|
|
|
|
active-value="active"
|
|
|
|
|
|
inactive-value="inactive"
|
|
|
|
|
|
active-color="#67C23A"
|
|
|
|
|
|
@change="toggleSiteStatus(scope.row)">
|
|
|
|
|
|
</el-switch>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="click_count" label="点击" width="80" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="reply_count" label="回复" width="80" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="created_at" label="创建时间" width="170"></el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="150" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button type="text" size="small" @click="resetSite(scope.row)">
|
|
|
|
|
|
<i class="el-icon-refresh-right"></i> 重置
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
<el-button type="text" size="small" style="color: #F56C6C;" @click="deleteSite(scope.row)">
|
|
|
|
|
|
<i class="el-icon-delete"></i> 删除
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
style="margin-top: 16px; text-align: right;"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
@current-change="handlePageChange"
|
|
|
|
|
|
:current-page="pagination.page"
|
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
|
:page-size="pagination.page_size"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
:total="pagination.total">
|
|
|
|
|
|
</el-pagination>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== 数据记录 ==================== -->
|
|
|
|
|
|
<section v-show="activeMenu === 'database'">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<el-tabs v-model="dbTab" @tab-click="handleDbTabClick">
|
|
|
|
|
|
<el-tab-pane label="点击记录" name="clicks">
|
|
|
|
|
|
<el-row :gutter="16" style="margin-bottom: 16px;">
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-date-picker v-model="clickFilter.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" @change="loadClicks" style="width: 100%;"></el-date-picker>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="16" style="text-align: right;">
|
|
|
|
|
|
<el-button type="primary" size="small" plain icon="el-icon-download" @click="exportClicks">导出</el-button>
|
|
|
|
|
|
<el-button size="small" icon="el-icon-refresh" @click="loadClicks">刷新</el-button>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
<el-table :data="clickList" v-loading="clicksLoading" stripe border>
|
|
|
|
|
|
<el-table-column prop="id" label="ID" width="70"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="site_name" label="站点" width="150"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="site_url" label="URL" min-width="280">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-link :href="scope.row.site_url" target="_blank" type="primary" :underline="false">{{ scope.row.site_url }}</el-link>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="click_time" label="点击时间" width="170"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="user_ip" label="代理IP" width="140"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="device_type" label="设备" width="80" align="center"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
style="margin-top: 16px; text-align: right;"
|
|
|
|
|
|
@current-change="handleClickPageChange"
|
|
|
|
|
|
:current-page="clickPagination.page"
|
|
|
|
|
|
:page-size="clickPagination.page_size"
|
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
|
:total="clickPagination.total">
|
|
|
|
|
|
</el-pagination>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
<el-tab-pane label="互动记录" name="interactions">
|
|
|
|
|
|
<el-row :gutter="16" style="margin-bottom: 16px;">
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-date-picker v-model="interactionFilter.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" value-format="yyyy-MM-dd" @change="loadInteractions" style="width: 100%;"></el-date-picker>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="4">
|
|
|
|
|
|
<el-select v-model="interactionFilter.status" placeholder="状态" clearable @change="loadInteractions" style="width: 100%;">
|
|
|
|
|
|
<el-option label="成功" value="success"></el-option>
|
|
|
|
|
|
<el-option label="失败" value="failed"></el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="12" style="text-align: right;">
|
|
|
|
|
|
<el-button type="primary" size="small" plain icon="el-icon-download" @click="exportInteractions">导出</el-button>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
<el-table :data="interactionList" v-loading="interactionsLoading" stripe border>
|
|
|
|
|
|
<el-table-column prop="id" label="ID" width="70"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="site_name" label="站点" width="150"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="interaction_time" label="时间" width="170"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="reply_content" label="发送内容" min-width="200">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span>{{ scope.row.reply_content || '-' }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="response_received" label="收到回复" width="100" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-tag :type="scope.row.response_received ? 'success' : 'info'" size="mini">{{ scope.row.response_received ? '是' : '否' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="interaction_status" label="状态" width="90" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-tag :type="scope.row.interaction_status === 'success' ? 'success' : 'danger'" size="mini">{{ scope.row.interaction_status || '-' }}</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="操作" width="100" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-button type="text" size="mini" @click="showChatLog(scope.row)" :disabled="!scope.row.response_content">聊天记录</el-button>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
style="margin-top: 16px; text-align: right;"
|
|
|
|
|
|
@current-change="handleInteractionPageChange"
|
|
|
|
|
|
:current-page="interactionPagination.page"
|
|
|
|
|
|
:page-size="interactionPagination.page_size"
|
|
|
|
|
|
layout="total, prev, pager, next"
|
|
|
|
|
|
:total="interactionPagination.total">
|
|
|
|
|
|
</el-pagination>
|
|
|
|
|
|
</el-tab-pane>
|
|
|
|
|
|
</el-tabs>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<!-- ==================== 系统日志 ==================== -->
|
|
|
|
|
|
<section v-show="activeMenu === 'logs'">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
|
|
|
<span style="font-size: 16px; font-weight: 600;">实时日志</span>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<el-select v-model="logFilter.level" size="small" style="width: 100px; margin-right: 8px;" @change="loadLogs">
|
|
|
|
|
|
<el-option label="全部" value="ALL"></el-option>
|
|
|
|
|
|
<el-option label="INFO" value="INFO"></el-option>
|
|
|
|
|
|
<el-option label="WARNING" value="WARNING"></el-option>
|
|
|
|
|
|
<el-option label="ERROR" value="ERROR"></el-option>
|
|
|
|
|
|
</el-select>
|
|
|
|
|
|
<el-switch v-model="logAutoScroll" active-text="自动滚动" style="margin-right: 12px;"></el-switch>
|
|
|
|
|
|
<el-button size="small" icon="el-icon-refresh" @click="loadLogs">刷新</el-button>
|
|
|
|
|
|
</div>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<div class="log-viewer" ref="logViewer">
|
|
|
|
|
|
<div v-for="(log, i) in logs" :key="i" class="log-entry">
|
|
|
|
|
|
<span class="log-time">{{ log.time }}</span>
|
|
|
|
|
|
<span class="log-level" :class="log.level">{{ log.level }}</span>
|
|
|
|
|
|
<span class="log-message">{{ log.message }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-empty v-if="!logs.length" description="暂无日志"></el-empty>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<!-- ==================== 系统配置 ==================== -->
|
|
|
|
|
|
<section v-show="activeMenu === 'config'">
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
|
|
|
<span style="font-size: 16px; font-weight: 600;">系统配置</span>
|
|
|
|
|
|
<el-button type="primary" size="small" @click="saveConfig" :loading="configSaving">保存配置</el-button>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<el-form :model="configForm" label-width="130px">
|
|
|
|
|
|
<el-divider content-position="left">点击策略</el-divider>
|
|
|
|
|
|
<el-row :gutter="24">
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-form-item label="最小点击次数">
|
|
|
|
|
|
<el-input-number v-model="configForm.min_click_count" :min="1" :max="100"></el-input-number>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-form-item label="最大点击次数">
|
|
|
|
|
|
<el-input-number v-model="configForm.max_click_count" :min="1" :max="100"></el-input-number>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-form-item label="点击间隔(分钟)">
|
|
|
|
|
|
<el-input-number v-model="configForm.click_interval_minutes" :min="1" :max="1440"></el-input-number>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<el-divider content-position="left">工作时间</el-divider>
|
|
|
|
|
|
<el-row :gutter="24">
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-form-item label="开始时间(时)">
|
|
|
|
|
|
<el-input-number v-model="configForm.start_hour" :min="0" :max="23"></el-input-number>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-form-item label="结束时间(时)">
|
|
|
|
|
|
<el-input-number v-model="configForm.end_hour" :min="0" :max="23"></el-input-number>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<el-divider content-position="left">任务配置</el-divider>
|
|
|
|
|
|
<el-row :gutter="24">
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-form-item label="任务最小间隔">
|
|
|
|
|
|
<el-input-number v-model="configForm.min_interval_minutes" :min="1" :max="60"></el-input-number>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-form-item label="任务最大间隔">
|
|
|
|
|
|
<el-input-number v-model="configForm.max_interval_minutes" :min="1" :max="60"></el-input-number>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
<el-col :span="8">
|
|
|
|
|
|
<el-form-item label="回复等待(秒)">
|
|
|
|
|
|
<el-input-number v-model="configForm.reply_wait_timeout" :min="5" :max="300"></el-input-number>
|
|
|
|
|
|
</el-form-item>
|
|
|
|
|
|
</el-col>
|
|
|
|
|
|
</el-row>
|
|
|
|
|
|
</el-form>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- ==================== Query挖掘 ==================== -->
|
|
|
|
|
|
<section v-show="activeMenu === 'query-mining'">
|
|
|
|
|
|
<!-- 上传区域 -->
|
|
|
|
|
|
<el-card shadow="never" style="margin-bottom: 20px;">
|
|
|
|
|
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
|
|
|
<span style="font-size: 16px; font-weight: 600;">上传关键词文件</span>
|
|
|
|
|
|
<el-button size="small" type="warning" icon="el-icon-video-play" @click="triggerQueryScan" :loading="queryScanLoading">手动检测</el-button>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 导入模式选择 -->
|
|
|
|
|
|
<el-radio-group v-model="queryImportMode" style="margin-bottom: 16px;">
|
|
|
|
|
|
<el-radio-button label="query_only">仅导入Query</el-radio-button>
|
|
|
|
|
|
<el-radio-button label="full_import">完整导入(科室+关键字+Query)</el-radio-button>
|
|
|
|
|
|
</el-radio-group>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 模式1:仅导入query -->
|
|
|
|
|
|
<div v-if="queryImportMode === 'query_only'">
|
|
|
|
|
|
<el-alert type="info" :closable="false" style="margin-bottom: 12px;">
|
|
|
|
|
|
<div slot="title">
|
|
|
|
|
|
<b>仅导入Query</b> - Excel必须只有 <b>1列</b>(query),多余列会导致报错
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin-top: 6px; font-size: 12px; color: #909399;">
|
|
|
|
|
|
导入后 query_status = draft,所有字段使用默认值
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
<el-table :data="queryOnlyExample" border size="mini" style="width: 220px; margin-bottom: 12px;">
|
|
|
|
|
|
<el-table-column prop="query" label="query" width="220"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
ref="queryUploadMode1"
|
|
|
|
|
|
:action="apiBase + '/api/query/upload'"
|
|
|
|
|
|
:data="{import_mode: 'query_only'}"
|
|
|
|
|
|
:headers="uploadHeaders"
|
|
|
|
|
|
:on-success="handleQueryUploadSuccess"
|
|
|
|
|
|
:on-error="handleQueryUploadError"
|
|
|
|
|
|
:before-upload="beforeQueryUploadMode1"
|
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
|
:disabled="queryUploadLoading"
|
|
|
|
|
|
with-credentials
|
|
|
|
|
|
drag
|
|
|
|
|
|
accept=".xlsx,.xls"
|
|
|
|
|
|
style="width: 100%;">
|
|
|
|
|
|
<i class="el-icon-upload" style="font-size: 48px; color: #C0C4CC; margin-top: 20px;"></i>
|
|
|
|
|
|
<div class="el-upload__text" v-if="!queryUploadLoading">将Excel文件拖到此处,或<em>点击上传</em></div>
|
|
|
|
|
|
<div class="el-upload__text" v-else>上传中,请稍候...</div>
|
|
|
|
|
|
<div class="el-upload__tip" slot="tip" style="color: #F56C6C;">
|
|
|
|
|
|
Excel只能有1列(query),超过1列将报错
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 模式2:完整导入 -->
|
|
|
|
|
|
<div v-if="queryImportMode === 'full_import'">
|
|
|
|
|
|
<el-alert type="info" :closable="false" style="margin-bottom: 12px;">
|
|
|
|
|
|
<div slot="title">
|
|
|
|
|
|
<b>完整导入</b> - Excel必须恰好 <b>3列</b>:第1列 科室,第2列 关键字,第3列 query
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div style="margin-top: 6px; font-size: 12px; color: #909399;">
|
|
|
|
|
|
导入后 query_status = manual_review,科室信息写入 department 字段
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-alert>
|
|
|
|
|
|
<el-table :data="queryFullExample" border size="mini" style="width: 500px; margin-bottom: 12px;">
|
|
|
|
|
|
<el-table-column prop="dept" label="科室" width="120"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="keyword" label="关键字" width="160"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="query" label="query" width="220"></el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-upload
|
|
|
|
|
|
ref="queryUploadMode2"
|
|
|
|
|
|
:action="apiBase + '/api/query/upload'"
|
|
|
|
|
|
:data="{import_mode: 'full_import'}"
|
|
|
|
|
|
:headers="uploadHeaders"
|
|
|
|
|
|
:on-success="handleQueryUploadSuccess"
|
|
|
|
|
|
:on-error="handleQueryUploadError"
|
|
|
|
|
|
:before-upload="beforeQueryUploadMode2"
|
|
|
|
|
|
:show-file-list="false"
|
|
|
|
|
|
:disabled="queryUploadLoading"
|
|
|
|
|
|
with-credentials
|
|
|
|
|
|
drag
|
|
|
|
|
|
accept=".xlsx,.xls"
|
|
|
|
|
|
style="width: 100%;">
|
|
|
|
|
|
<i class="el-icon-upload" style="font-size: 48px; color: #C0C4CC; margin-top: 20px;"></i>
|
|
|
|
|
|
<div class="el-upload__text" v-if="!queryUploadLoading">将Excel文件拖到此处,或<em>点击上传</em></div>
|
|
|
|
|
|
<div class="el-upload__text" v-else>上传中,请稍候...</div>
|
|
|
|
|
|
<div class="el-upload__tip" slot="tip" style="color: #F56C6C;">
|
|
|
|
|
|
Excel必须恰好3列(科室、关键字、query),列数不符将报错
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-upload>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 导入记录 -->
|
|
|
|
|
|
<el-card shadow="never">
|
|
|
|
|
|
<div slot="header" style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
|
|
|
<span style="font-size: 16px; font-weight: 600;">导入记录</span>
|
|
|
|
|
|
<el-button size="small" icon="el-icon-refresh" @click="loadQueryImportRecords">刷新</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<el-table :data="queryImportRecords" v-loading="queryRecordsLoading" stripe border>
|
|
|
|
|
|
<el-table-column prop="filename" label="文件名" min-width="200"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="upload_time" label="上传时间" width="170"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="import_time" label="导入时间" width="170"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="status" label="状态" width="100" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<el-tag size="small" :type="{'pending':'info','running':'','completed':'success','failed':'danger'}[scope.row.status] || 'info'">
|
|
|
|
|
|
{{ {'pending':'待处理','running':'导入中','completed':'已完成','failed':'失败'}[scope.row.status] || scope.row.status }}
|
|
|
|
|
|
</el-tag>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="total_count" label="总数" width="80" align="center"></el-table-column>
|
|
|
|
|
|
<el-table-column prop="success_count" label="成功" width="80" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span style="color: #67C23A;">{{ scope.row.success_count }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="skip_count" label="跳过" width="80" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span style="color: #E6A23C;">{{ scope.row.skip_count }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column prop="fail_count" label="失败" width="80" align="center">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span style="color: #F56C6C;">{{ scope.row.fail_count }}</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
<el-table-column label="错误信息" min-width="150">
|
|
|
|
|
|
<template slot-scope="scope">
|
|
|
|
|
|
<span v-if="scope.row.error_message" style="color: #F56C6C; font-size: 12px;">{{ scope.row.error_message }}</span>
|
|
|
|
|
|
<span v-else style="color: #C0C4CC;">-</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
|
|
|
|
|
<el-pagination
|
|
|
|
|
|
style="margin-top: 16px; text-align: right;"
|
|
|
|
|
|
@current-change="handleQueryPageChange"
|
|
|
|
|
|
:current-page="queryPagination.page"
|
|
|
|
|
|
:page-size="queryPagination.page_size"
|
|
|
|
|
|
:total="queryPagination.total"
|
|
|
|
|
|
layout="total, prev, pager, next">
|
|
|
|
|
|
</el-pagination>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</main>
|
|
|
|
|
|
<el-dialog :visible.sync="chatLogDialog" :title="chatLogTitle" width="65%" top="8vh" custom-class="chat-log-dialog">
|
|
|
|
|
|
<div class="chat-log-content">
|
|
|
|
|
|
<pre>{{ chatLogContent }}</pre>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
2026-02-24 12:46:35 +08:00
|
|
|
|
<span slot="footer" class="dialog-footer">
|
|
|
|
|
|
<el-button @click="chatLogDialog = false">关闭</el-button>
|
|
|
|
|
|
<el-button type="primary" @click="copyChatLog">复制内容</el-button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</el-dialog>
|
|
|
|
|
|
</template>
|
2026-01-16 22:06:46 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
new Vue({
|
|
|
|
|
|
el: '#app',
|
|
|
|
|
|
data() {
|
|
|
|
|
|
return {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 登录相关
|
|
|
|
|
|
isLoggedIn: false,
|
|
|
|
|
|
username: '',
|
|
|
|
|
|
loginLoading: false,
|
|
|
|
|
|
loginForm: {
|
|
|
|
|
|
username: '',
|
|
|
|
|
|
password: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
loginRules: {
|
|
|
|
|
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
|
|
|
|
|
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
activeMenu: 'dashboard',
|
|
|
|
|
|
fullLoading: false,
|
|
|
|
|
|
isRefreshing: false,
|
|
|
|
|
|
statsLoading: true,
|
|
|
|
|
|
chartsLoading: true,
|
|
|
|
|
|
schedulerStatus: '检测中...',
|
|
|
|
|
|
schedulerRunning: false,
|
|
|
|
|
|
stats: {},
|
|
|
|
|
|
config: {},
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 链接管理
|
|
|
|
|
|
siteList: [],
|
|
|
|
|
|
sitesLoading: false,
|
|
|
|
|
|
selectedSites: [],
|
|
|
|
|
|
siteFilter: { keyword: '', status: '', sort_by: 'created_at' },
|
|
|
|
|
|
pagination: { page: 1, page_size: 20, total: 0 },
|
|
|
|
|
|
importParams: {},
|
|
|
|
|
|
uploadLoading: false,
|
|
|
|
|
|
uploadHeaders: {},
|
|
|
|
|
|
importTask: null,
|
|
|
|
|
|
importProgress: 0,
|
|
|
|
|
|
importTimer: null,
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 数据记录
|
|
|
|
|
|
dbTab: 'clicks',
|
|
|
|
|
|
clickList: [],
|
|
|
|
|
|
clicksLoading: false,
|
|
|
|
|
|
clickFilter: { dateRange: [] },
|
|
|
|
|
|
clickPagination: { page: 1, page_size: 20, total: 0 },
|
|
|
|
|
|
interactionList: [],
|
|
|
|
|
|
interactionsLoading: false,
|
|
|
|
|
|
interactionFilter: { dateRange: [], status: '' },
|
|
|
|
|
|
interactionPagination: { page: 1, page_size: 20, total: 0 },
|
|
|
|
|
|
chatLogDialog: false,
|
|
|
|
|
|
chatLogContent: '',
|
|
|
|
|
|
chatLogTitle: '',
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 日志
|
|
|
|
|
|
logs: [],
|
|
|
|
|
|
logFilter: { level: 'ALL' },
|
|
|
|
|
|
logAutoScroll: true,
|
|
|
|
|
|
logTimer: null,
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 配置
|
|
|
|
|
|
configForm: {},
|
|
|
|
|
|
configSaving: false,
|
2026-01-16 22:06:46 +08:00
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// Query挖掘
|
|
|
|
|
|
queryImportMode: 'query_only',
|
|
|
|
|
|
queryImportRecords: [],
|
|
|
|
|
|
queryRecordsLoading: false,
|
|
|
|
|
|
queryUploadLoading: false,
|
|
|
|
|
|
queryScanLoading: false,
|
|
|
|
|
|
queryPagination: { page: 1, page_size: 20, total: 0 },
|
|
|
|
|
|
queryPollTimer: null,
|
|
|
|
|
|
queryOnlyExample: [
|
|
|
|
|
|
{ query: '胃癌早期症状有哪些' },
|
|
|
|
|
|
{ query: '高血压吃什么药好' }
|
|
|
|
|
|
],
|
|
|
|
|
|
queryFullExample: [
|
|
|
|
|
|
{ dept: '消化内科', keyword: '胃癌', query: '胃癌早期症状有哪些' },
|
|
|
|
|
|
{ dept: '心血管内科', keyword: '高血压', query: '高血压吃什么药好' }
|
|
|
|
|
|
],
|
|
|
|
|
|
|
|
|
|
|
|
// 图表
|
|
|
|
|
|
charts: {},
|
|
|
|
|
|
apiBase: ''
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
mounted() {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.apiBase = window.location.origin;
|
|
|
|
|
|
this.checkAuth();
|
|
|
|
|
|
},
|
|
|
|
|
|
beforeDestroy() {
|
|
|
|
|
|
if (this.logTimer) clearInterval(this.logTimer);
|
|
|
|
|
|
if (this.queryPollTimer) clearInterval(this.queryPollTimer);
|
|
|
|
|
|
Object.values(this.charts).forEach(c => c && c.dispose());
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
methods: {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 检查登录状态
|
|
|
|
|
|
async checkAuth() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/auth/check`, { withCredentials: true });
|
|
|
|
|
|
if (data.success && data.data.logged_in) {
|
|
|
|
|
|
this.isLoggedIn = true;
|
|
|
|
|
|
this.username = data.data.username;
|
|
|
|
|
|
this.init();
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('检查登录状态失败', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 登录
|
|
|
|
|
|
handleLogin() {
|
|
|
|
|
|
this.$refs.loginForm.validate(async (valid) => {
|
|
|
|
|
|
if (!valid) return;
|
|
|
|
|
|
this.loginLoading = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/auth/login`, this.loginForm, { withCredentials: true });
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.$message.success('登录成功');
|
|
|
|
|
|
this.isLoggedIn = true;
|
|
|
|
|
|
this.username = data.data.username;
|
|
|
|
|
|
this.loginForm = { username: '', password: '' };
|
|
|
|
|
|
this.init();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error(data.message || '登录失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.$message.error(e.response?.data?.message || '登录失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
this.loginLoading = false;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 登出
|
|
|
|
|
|
async handleLogout() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await axios.post(`${this.apiBase}/api/auth/logout`, {}, { withCredentials: true });
|
|
|
|
|
|
this.isLoggedIn = false;
|
|
|
|
|
|
this.username = '';
|
|
|
|
|
|
this.$message.success('已退出登录');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('登出失败', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 用户下拉菜单
|
|
|
|
|
|
handleUserCommand(command) {
|
|
|
|
|
|
if (command === 'logout') {
|
|
|
|
|
|
this.$confirm('确定要退出登录吗?', '提示', {
|
|
|
|
|
|
confirmButtonText: '确定',
|
|
|
|
|
|
cancelButtonText: '取消',
|
|
|
|
|
|
type: 'warning'
|
|
|
|
|
|
}).then(() => {
|
|
|
|
|
|
this.handleLogout();
|
|
|
|
|
|
}).catch(() => {});
|
|
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-01-16 22:06:46 +08:00
|
|
|
|
async init() {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.statsLoading = true;
|
|
|
|
|
|
this.chartsLoading = true;
|
|
|
|
|
|
await Promise.all([this.getSchedulerStatus(), this.getStatistics(), this.getConfig()]);
|
|
|
|
|
|
this.loadSites();
|
|
|
|
|
|
this.$nextTick(() => { this.initCharts(); this.loadChartData(); });
|
|
|
|
|
|
setInterval(() => { this.getSchedulerStatus(); this.getStatistics(); }, 5000);
|
|
|
|
|
|
// 检查是否有进行中的导入任务
|
|
|
|
|
|
this.checkPendingImportTasks();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async checkPendingImportTasks() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/sites/import/tasks`);
|
|
|
|
|
|
if (data.success && data.data.length > 0) {
|
|
|
|
|
|
const runningTask = data.data.find(t => t.status === 'running' || t.status === 'pending');
|
|
|
|
|
|
if (runningTask) {
|
|
|
|
|
|
this.uploadLoading = true;
|
|
|
|
|
|
this.importTask = runningTask;
|
|
|
|
|
|
this.importProgress = runningTask.progress || 0;
|
|
|
|
|
|
this.startImportPolling(runningTask.id);
|
|
|
|
|
|
this.$message.info(`检测到正在进行的导入任务:${runningTask.filename}`);
|
|
|
|
|
|
}
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('检查导入任务失败', e);
|
|
|
|
|
|
}
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
handleMenuSelect(index) {
|
|
|
|
|
|
this.activeMenu = index;
|
2026-02-24 12:46:35 +08:00
|
|
|
|
if (index === 'dashboard') this.$nextTick(() => this.resizeCharts());
|
|
|
|
|
|
else if (index === 'database') this.loadClicks();
|
|
|
|
|
|
else if (index === 'logs') { this.loadLogs(); this.startLogPolling(); }
|
|
|
|
|
|
else if (index === 'config') this.getConfig();
|
|
|
|
|
|
else if (index === 'query-mining') this.loadQueryImportRecords();
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
async refreshAll() {
|
|
|
|
|
|
this.isRefreshing = true;
|
|
|
|
|
|
await Promise.all([this.getStatistics(), this.loadChartData()]);
|
|
|
|
|
|
setTimeout(() => { this.isRefreshing = false; }, 600);
|
|
|
|
|
|
this.$message.success('已刷新');
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// API调用
|
2026-01-16 22:06:46 +08:00
|
|
|
|
async getSchedulerStatus() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/scheduler/status`);
|
|
|
|
|
|
if (data.success) {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.schedulerRunning = data.data.status === 'running';
|
|
|
|
|
|
const statusMap = {
|
|
|
|
|
|
'running': '运行中',
|
|
|
|
|
|
'stopped': '已停止',
|
|
|
|
|
|
'manual': '手动管理'
|
|
|
|
|
|
};
|
|
|
|
|
|
this.schedulerStatus = statusMap[data.data.status] || '手动管理';
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
} catch (e) { console.error(e); }
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async getStatistics() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/statistics`);
|
2026-02-24 12:46:35 +08:00
|
|
|
|
if (data.success) this.stats = data.data;
|
|
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
|
this.statsLoading = false;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
async getConfig() {
|
2026-01-16 22:06:46 +08:00
|
|
|
|
try {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/config`);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
if (data.success) {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.config = data.data;
|
|
|
|
|
|
this.configForm = {
|
|
|
|
|
|
min_click_count: data.data.click_strategy?.min_click_count || 1,
|
|
|
|
|
|
max_click_count: data.data.click_strategy?.max_click_count || 3,
|
|
|
|
|
|
click_interval_minutes: data.data.click_strategy?.click_interval_minutes || 30,
|
|
|
|
|
|
start_hour: data.data.work_time?.start_hour || 9,
|
|
|
|
|
|
end_hour: data.data.work_time?.end_hour || 21,
|
|
|
|
|
|
min_interval_minutes: data.data.task_config?.min_interval_minutes || 3,
|
|
|
|
|
|
max_interval_minutes: data.data.task_config?.max_interval_minutes || 5,
|
|
|
|
|
|
reply_wait_timeout: data.data.task_config?.reply_wait_timeout || 30
|
|
|
|
|
|
};
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
} catch (e) { console.error(e); }
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async startScheduler() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/scheduler/start`);
|
2026-02-24 12:46:35 +08:00
|
|
|
|
data.success ? this.$message.success('调度器已启动') : this.$message.error(data.message);
|
|
|
|
|
|
this.getSchedulerStatus();
|
|
|
|
|
|
} catch (e) { this.$message.error('启动失败'); }
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async stopScheduler() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/scheduler/stop`);
|
2026-02-24 12:46:35 +08:00
|
|
|
|
data.success ? this.$message.success('调度器已停止') : this.$message.error(data.message);
|
|
|
|
|
|
this.getSchedulerStatus();
|
|
|
|
|
|
} catch (e) { this.$message.error('停止失败'); }
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 链接管理
|
|
|
|
|
|
beforeUpload(file) {
|
|
|
|
|
|
const validExts = ['.xlsx', '.xls', '.csv'];
|
|
|
|
|
|
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
|
|
|
|
if (!validExts.includes(ext)) {
|
|
|
|
|
|
this.$message.error('请上传Excel(.xlsx/.xls)或CSV(.csv)文件');
|
|
|
|
|
|
return false;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.uploadLoading = true;
|
|
|
|
|
|
this.importTask = null;
|
|
|
|
|
|
this.importProgress = 0;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
handleUploadSuccess(res) {
|
|
|
|
|
|
if (res.success && res.task_id) {
|
|
|
|
|
|
// 异步导入:开始轮询进度
|
|
|
|
|
|
this.$message.info(`开始导入 ${res.total} 条数据...`);
|
|
|
|
|
|
this.startImportPolling(res.task_id);
|
|
|
|
|
|
} else if (res.success) {
|
|
|
|
|
|
// 同步导入完成(兼容旧接口)
|
|
|
|
|
|
this.uploadLoading = false;
|
|
|
|
|
|
this.$message.success(res.message);
|
|
|
|
|
|
this.loadSites();
|
|
|
|
|
|
this.getStatistics();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.uploadLoading = false;
|
|
|
|
|
|
this.$message.error(res.message || '导入失败');
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
2026-02-24 12:46:35 +08:00
|
|
|
|
handleUploadError(err) {
|
|
|
|
|
|
this.uploadLoading = false;
|
|
|
|
|
|
this.importTask = null;
|
|
|
|
|
|
this.$message.error('上传失败,请检查网络连接');
|
|
|
|
|
|
console.error('上传错误:', err);
|
|
|
|
|
|
},
|
|
|
|
|
|
startImportPolling(taskId) {
|
|
|
|
|
|
// 轮询导入进度
|
|
|
|
|
|
this.importTimer = setInterval(async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/sites/import/progress/${taskId}`);
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
|
this.importTask = data.data;
|
|
|
|
|
|
this.importProgress = data.data.progress || 0;
|
|
|
|
|
|
|
|
|
|
|
|
if (data.data.status === 'completed') {
|
|
|
|
|
|
this.stopImportPolling();
|
|
|
|
|
|
this.uploadLoading = false;
|
|
|
|
|
|
const s = data.data.stats;
|
|
|
|
|
|
this.$notify({
|
|
|
|
|
|
title: '导入完成',
|
|
|
|
|
|
message: `成功: ${s.success} | 重复: ${s.duplicate} | 跳过: ${s.skipped} | 失败: ${s.failed}`,
|
|
|
|
|
|
type: 'success',
|
|
|
|
|
|
duration: 5000
|
|
|
|
|
|
});
|
|
|
|
|
|
this.loadSites();
|
|
|
|
|
|
this.getStatistics();
|
|
|
|
|
|
this.importTask = null;
|
|
|
|
|
|
} else if (data.data.status === 'error') {
|
|
|
|
|
|
this.stopImportPolling();
|
|
|
|
|
|
this.uploadLoading = false;
|
|
|
|
|
|
this.$message.error(data.data.error || '导入异常');
|
|
|
|
|
|
this.importTask = null;
|
|
|
|
|
|
} else if (data.data.status === 'cancelled') {
|
|
|
|
|
|
this.stopImportPolling();
|
|
|
|
|
|
this.uploadLoading = false;
|
|
|
|
|
|
this.$message.warning('导入已取消');
|
|
|
|
|
|
this.loadSites();
|
|
|
|
|
|
this.importTask = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error('获取导入进度失败', e);
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 500);
|
|
|
|
|
|
},
|
|
|
|
|
|
stopImportPolling() {
|
|
|
|
|
|
if (this.importTimer) {
|
|
|
|
|
|
clearInterval(this.importTimer);
|
|
|
|
|
|
this.importTimer = null;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
},
|
|
|
|
|
|
async cancelImport() {
|
|
|
|
|
|
if (!this.importTask) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await axios.post(`${this.apiBase}/api/sites/import/cancel/${this.importTask.id}`, {}, { withCredentials: true });
|
|
|
|
|
|
this.$message.info('正在取消导入...');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.$message.error('取消失败');
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async loadSites() {
|
|
|
|
|
|
this.sitesLoading = true;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
try {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
const params = { page: this.pagination.page, page_size: this.pagination.page_size, sort_by: this.siteFilter.sort_by, sort_order: 'desc' };
|
|
|
|
|
|
if (this.siteFilter.keyword) params.keyword = this.siteFilter.keyword;
|
|
|
|
|
|
if (this.siteFilter.status) params.status = this.siteFilter.status;
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/sites/paginated`, { params });
|
|
|
|
|
|
if (data.success) { this.siteList = data.data.items; this.pagination.total = data.data.total; }
|
|
|
|
|
|
} catch (e) { this.$message.error('加载失败'); }
|
|
|
|
|
|
this.sitesLoading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
handleSiteSelection(sel) { this.selectedSites = sel; },
|
|
|
|
|
|
handlePageChange(page) { this.pagination.page = page; this.loadSites(); },
|
|
|
|
|
|
handleSizeChange(size) { this.pagination.page_size = size; this.pagination.page = 1; this.loadSites(); },
|
|
|
|
|
|
|
|
|
|
|
|
async toggleSiteStatus(site) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.put(`${this.apiBase}/api/sites/${site.id}/status`, { status: site.status });
|
|
|
|
|
|
data.success ? this.$message.success(site.status === 'active' ? '已启用点击' : '已暂停点击') : (this.$message.error(data.message), this.loadSites());
|
|
|
|
|
|
} catch (e) { this.$message.error('操作失败'); this.loadSites(); }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async batchEnableSites() {
|
|
|
|
|
|
if (!this.selectedSites.length) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/sites/batch/status`, { site_ids: this.selectedSites.map(s => s.id), status: 'active' });
|
|
|
|
|
|
data.success ? (this.$message.success(data.message), this.loadSites()) : this.$message.error(data.message);
|
|
|
|
|
|
} catch (e) { this.$message.error('操作失败'); }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async batchDisableSites() {
|
|
|
|
|
|
if (!this.selectedSites.length) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/sites/batch/status`, { site_ids: this.selectedSites.map(s => s.id), status: 'inactive' });
|
|
|
|
|
|
data.success ? (this.$message.success(data.message), this.loadSites()) : this.$message.error(data.message);
|
|
|
|
|
|
} catch (e) { this.$message.error('操作失败'); }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async batchDeleteSites() {
|
|
|
|
|
|
if (!this.selectedSites.length) return;
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.$confirm(`确定删除选中的 ${this.selectedSites.length} 个站点?`, '确认删除', { type: 'warning' });
|
|
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/sites/batch/delete`, { site_ids: this.selectedSites.map(s => s.id) });
|
|
|
|
|
|
data.success ? (this.$message.success(data.message), this.selectedSites = [], this.loadSites(), this.getStatistics()) : this.$message.error(data.message);
|
|
|
|
|
|
} catch (e) { if (e !== 'cancel') this.$message.error('删除失败'); }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async resetSite(site) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.$confirm('确定重置该链接?', '确认', { type: 'warning' });
|
|
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/urls/${encodeURIComponent(site.site_url)}/reset`);
|
|
|
|
|
|
data.success ? (this.$message.success('重置成功'), this.loadSites()) : this.$message.error(data.message);
|
|
|
|
|
|
} catch (e) { if (e !== 'cancel') this.$message.error('重置失败'); }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async deleteSite(site) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await this.$confirm('确定删除该链接?', '确认', { type: 'warning' });
|
|
|
|
|
|
const { data } = await axios.delete(`${this.apiBase}/api/urls/${encodeURIComponent(site.site_url)}`);
|
|
|
|
|
|
data.success ? (this.$message.success('删除成功'), this.loadSites(), this.getStatistics()) : this.$message.error(data.message);
|
|
|
|
|
|
} catch (e) { if (e !== 'cancel') this.$message.error('删除失败'); }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
exportSites() {
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
if (this.siteFilter.status) params.append('status', this.siteFilter.status);
|
|
|
|
|
|
if (this.siteFilter.keyword) params.append('keyword', this.siteFilter.keyword);
|
|
|
|
|
|
window.open(`${this.apiBase}/api/export/sites?${params}`);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// 数据记录
|
|
|
|
|
|
handleDbTabClick() { this.dbTab === 'clicks' ? this.loadClicks() : this.loadInteractions(); },
|
|
|
|
|
|
|
|
|
|
|
|
async loadClicks() {
|
|
|
|
|
|
this.clicksLoading = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params = { page: this.clickPagination.page, page_size: this.clickPagination.page_size };
|
|
|
|
|
|
if (this.clickFilter.dateRange?.length === 2) { params.start_date = this.clickFilter.dateRange[0]; params.end_date = this.clickFilter.dateRange[1]; }
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/clicks/paginated`, { params });
|
|
|
|
|
|
if (data.success) { this.clickList = data.data.items; this.clickPagination.total = data.data.total; }
|
|
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
|
this.clicksLoading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
handleClickPageChange(page) { this.clickPagination.page = page; this.loadClicks(); },
|
|
|
|
|
|
|
|
|
|
|
|
async loadInteractions() {
|
|
|
|
|
|
this.interactionsLoading = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const params = { page: this.interactionPagination.page, page_size: this.interactionPagination.page_size };
|
|
|
|
|
|
if (this.interactionFilter.dateRange?.length === 2) { params.start_date = this.interactionFilter.dateRange[0]; params.end_date = this.interactionFilter.dateRange[1]; }
|
|
|
|
|
|
if (this.interactionFilter.status) params.status = this.interactionFilter.status;
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/interactions/paginated`, { params });
|
|
|
|
|
|
if (data.success) { this.interactionList = data.data.items; this.interactionPagination.total = data.data.total; }
|
|
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
|
this.interactionsLoading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
handleInteractionPageChange(page) { this.interactionPagination.page = page; this.loadInteractions(); },
|
|
|
|
|
|
|
|
|
|
|
|
showChatLog(row) {
|
|
|
|
|
|
if (!row.response_content) return;
|
|
|
|
|
|
this.chatLogTitle = `聊天记录 #${row.id} - ${row.site_name || '未知站点'}`;
|
|
|
|
|
|
this.chatLogContent = row.response_content;
|
|
|
|
|
|
this.chatLogDialog = true;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
copyChatLog() {
|
|
|
|
|
|
if (!this.chatLogContent) return;
|
|
|
|
|
|
const textarea = document.createElement('textarea');
|
|
|
|
|
|
textarea.value = this.chatLogContent;
|
|
|
|
|
|
textarea.style.position = 'fixed';
|
|
|
|
|
|
textarea.style.opacity = '0';
|
|
|
|
|
|
document.body.appendChild(textarea);
|
|
|
|
|
|
textarea.select();
|
|
|
|
|
|
try {
|
|
|
|
|
|
document.execCommand('copy');
|
|
|
|
|
|
this.$message.success('已复制到剪贴板');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
this.$message.error('复制失败');
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
document.body.removeChild(textarea);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
exportClicks() {
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
if (this.clickFilter.dateRange?.length === 2) { params.append('start_date', this.clickFilter.dateRange[0]); params.append('end_date', this.clickFilter.dateRange[1]); }
|
|
|
|
|
|
window.open(`${this.apiBase}/api/export/clicks?${params}`);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
exportInteractions() {
|
|
|
|
|
|
const params = new URLSearchParams();
|
|
|
|
|
|
if (this.interactionFilter.dateRange?.length === 2) { params.append('start_date', this.interactionFilter.dateRange[0]); params.append('end_date', this.interactionFilter.dateRange[1]); }
|
|
|
|
|
|
window.open(`${this.apiBase}/api/export/interactions?${params}`);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 日志
|
|
|
|
|
|
async loadLogs() {
|
2026-01-16 22:06:46 +08:00
|
|
|
|
try {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/logs/stream`, { params: { limit: 100, level: this.logFilter.level } });
|
2026-01-16 22:06:46 +08:00
|
|
|
|
if (data.success) {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.logs = data.data;
|
|
|
|
|
|
if (this.logAutoScroll) this.$nextTick(() => { const v = this.$refs.logViewer; if (v) v.scrollTop = v.scrollHeight; });
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
} catch (e) { console.error(e); }
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
startLogPolling() {
|
|
|
|
|
|
if (this.logTimer) clearInterval(this.logTimer);
|
|
|
|
|
|
this.logTimer = setInterval(() => { if (this.activeMenu === 'logs') this.loadLogs(); }, 2000);
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 配置
|
|
|
|
|
|
async saveConfig() {
|
|
|
|
|
|
this.configSaving = true;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
try {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/config/update`, this.configForm);
|
|
|
|
|
|
data.success ? (this.$message.success(data.message), data.requires_restart && this.$message.warning('部分配置需重启生效'), this.getConfig()) : this.$message.error(data.message);
|
|
|
|
|
|
} catch (e) { this.$message.error('保存失败'); }
|
|
|
|
|
|
this.configSaving = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
// Query挖掘
|
|
|
|
|
|
async loadQueryImportRecords() {
|
|
|
|
|
|
this.queryRecordsLoading = true;
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await axios.get(`${this.apiBase}/api/query/import/records`, {
|
|
|
|
|
|
params: { page: this.queryPagination.page, page_size: this.queryPagination.page_size },
|
|
|
|
|
|
withCredentials: true
|
2026-01-16 22:06:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
if (data.success) {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.queryImportRecords = data.data.items || [];
|
|
|
|
|
|
this.queryPagination.total = data.data.total || 0;
|
|
|
|
|
|
// 如果有running状态的记录,启动轮询
|
|
|
|
|
|
const hasRunning = this.queryImportRecords.some(r => r.status === 'running' || r.status === 'pending');
|
|
|
|
|
|
if (hasRunning && !this.queryPollTimer) {
|
|
|
|
|
|
this.startQueryPoll();
|
|
|
|
|
|
} else if (!hasRunning && this.queryPollTimer) {
|
|
|
|
|
|
this.stopQueryPoll();
|
|
|
|
|
|
}
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
} catch (e) { console.error('加载导入记录失败', e); }
|
|
|
|
|
|
this.queryRecordsLoading = false;
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
handleQueryPageChange(page) {
|
|
|
|
|
|
this.queryPagination.page = page;
|
|
|
|
|
|
this.loadQueryImportRecords();
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
beforeQueryUploadMode1(file) {
|
|
|
|
|
|
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
|
|
|
|
if (!['.xlsx', '.xls'].includes(ext)) {
|
|
|
|
|
|
this.$message.error('请上传Excel文件(.xlsx/.xls)');
|
|
|
|
|
|
return false;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.queryUploadLoading = true;
|
|
|
|
|
|
return true;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
beforeQueryUploadMode2(file) {
|
|
|
|
|
|
const ext = file.name.substring(file.name.lastIndexOf('.')).toLowerCase();
|
|
|
|
|
|
if (!['.xlsx', '.xls'].includes(ext)) {
|
|
|
|
|
|
this.$message.error('请上传Excel文件(.xlsx/.xls)');
|
|
|
|
|
|
return false;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.queryUploadLoading = true;
|
|
|
|
|
|
return true;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
handleQueryUploadSuccess(res) {
|
|
|
|
|
|
this.queryUploadLoading = false;
|
|
|
|
|
|
if (res.success) {
|
|
|
|
|
|
this.$message.success(`文件 ${res.filename} 上传成功,导入任务已启动`);
|
|
|
|
|
|
this.loadQueryImportRecords();
|
|
|
|
|
|
this.startQueryPoll();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error(res.message || '上传失败');
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
handleQueryUploadError(err) {
|
|
|
|
|
|
this.queryUploadLoading = false;
|
|
|
|
|
|
this.$message.error('上传失败: ' + (err.message || '未知错误'));
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
async triggerQueryScan() {
|
|
|
|
|
|
this.queryScanLoading = true;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
try {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
const { data } = await axios.post(`${this.apiBase}/api/query/import/trigger`, {}, { withCredentials: true });
|
2026-01-16 22:06:46 +08:00
|
|
|
|
if (data.success) {
|
2026-02-24 12:46:35 +08:00
|
|
|
|
this.$message.success('目录扫描任务已启动');
|
|
|
|
|
|
setTimeout(() => this.loadQueryImportRecords(), 2000);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
this.$message.error(data.message || '触发失败');
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
} catch (e) { this.$message.error('触发失败'); }
|
|
|
|
|
|
this.queryScanLoading = false;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
startQueryPoll() {
|
|
|
|
|
|
if (this.queryPollTimer) return;
|
|
|
|
|
|
this.queryPollTimer = setInterval(() => {
|
|
|
|
|
|
if (this.activeMenu === 'query-mining') {
|
|
|
|
|
|
this.loadQueryImportRecords();
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
2026-02-24 12:46:35 +08:00
|
|
|
|
}, 3000);
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
stopQueryPoll() {
|
|
|
|
|
|
if (this.queryPollTimer) {
|
|
|
|
|
|
clearInterval(this.queryPollTimer);
|
|
|
|
|
|
this.queryPollTimer = null;
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
// 图表
|
|
|
|
|
|
initCharts() {
|
|
|
|
|
|
window.addEventListener('resize', () => this.resizeCharts());
|
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
|
|
ensureChartsInit() {
|
|
|
|
|
|
// 确保图表实例存在
|
|
|
|
|
|
const trendEl = document.getElementById('trendChart');
|
|
|
|
|
|
const pieEl = document.getElementById('pieChart');
|
|
|
|
|
|
const hourlyEl = document.getElementById('hourlyChart');
|
|
|
|
|
|
const topSitesEl = document.getElementById('topSitesChart');
|
|
|
|
|
|
|
|
|
|
|
|
if (trendEl && !this.charts.trend) {
|
|
|
|
|
|
this.charts.trend = echarts.init(trendEl);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (pieEl && !this.charts.pie) {
|
|
|
|
|
|
this.charts.pie = echarts.init(pieEl);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (hourlyEl && !this.charts.hourly) {
|
|
|
|
|
|
this.charts.hourly = echarts.init(hourlyEl);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (topSitesEl && !this.charts.topSites) {
|
|
|
|
|
|
this.charts.topSites = echarts.init(topSitesEl);
|
|
|
|
|
|
}
|
2026-01-16 22:06:46 +08:00
|
|
|
|
},
|
|
|
|
|
|
|
2026-02-24 12:46:35 +08:00
|
|
|
|
resizeCharts() { Object.values(this.charts).forEach(c => c && c.resize()); },
|
|
|
|
|
|
|
|
|
|
|
|
async loadChartData() {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const [t, p, h, s] = await Promise.all([
|
|
|
|
|
|
axios.get(`${this.apiBase}/api/statistics/trend?days=7`),
|
|
|
|
|
|
axios.get(`${this.apiBase}/api/statistics/reply-rate`),
|
|
|
|
|
|
axios.get(`${this.apiBase}/api/statistics/hourly`),
|
|
|
|
|
|
axios.get(`${this.apiBase}/api/statistics/top-sites?limit=10`)
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
this.chartsLoading = false;
|
|
|
|
|
|
|
|
|
|
|
|
// 等待DOM更新后再初始化和渲染图表
|
|
|
|
|
|
await this.$nextTick();
|
|
|
|
|
|
await new Promise(r => setTimeout(r, 50));
|
|
|
|
|
|
|
|
|
|
|
|
// 确保图表已初始化
|
|
|
|
|
|
this.ensureChartsInit();
|
|
|
|
|
|
|
|
|
|
|
|
// iOS风格图表配置
|
|
|
|
|
|
const iosBlue = '#007AFF';
|
|
|
|
|
|
const iosGreen = '#34C759';
|
|
|
|
|
|
const iosGray = '#8E8E93';
|
|
|
|
|
|
const iosGray5 = '#E5E5EA';
|
|
|
|
|
|
|
|
|
|
|
|
if (t.data.success && this.charts.trend) {
|
|
|
|
|
|
const d = t.data.data;
|
|
|
|
|
|
this.charts.trend.setOption({
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
|
|
|
|
|
borderColor: '#E5E5EA',
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
textStyle: { color: '#1C1C1E', fontSize: 13 },
|
|
|
|
|
|
boxShadow: '0 4px 12px rgba(0,0,0,0.1)'
|
|
|
|
|
|
},
|
|
|
|
|
|
legend: {
|
|
|
|
|
|
data: ['点击', '成功'],
|
|
|
|
|
|
bottom: 0,
|
|
|
|
|
|
textStyle: { color: '#8E8E93', fontSize: 12 },
|
|
|
|
|
|
itemWidth: 12,
|
|
|
|
|
|
itemHeight: 12,
|
|
|
|
|
|
itemGap: 20
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: { left: '3%', right: '4%', bottom: '15%', top: '8%', containLabel: true },
|
|
|
|
|
|
xAxis: {
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
data: d.dates,
|
|
|
|
|
|
axisLabel: { fontSize: 11, color: '#8E8E93' },
|
|
|
|
|
|
axisLine: { lineStyle: { color: '#E5E5EA' } },
|
|
|
|
|
|
axisTick: { show: false }
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
axisLabel: { fontSize: 11, color: '#8E8E93' },
|
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
|
splitLine: { lineStyle: { color: '#F2F2F7', type: 'dashed' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '点击',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: d.clicks,
|
|
|
|
|
|
smooth: 0.4,
|
|
|
|
|
|
symbol: 'circle',
|
|
|
|
|
|
symbolSize: 6,
|
|
|
|
|
|
lineStyle: { width: 3, color: iosBlue },
|
|
|
|
|
|
areaStyle: {
|
|
|
|
|
|
color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [
|
|
|
|
|
|
{ offset: 0, color: 'rgba(0,122,255,0.25)' },
|
|
|
|
|
|
{ offset: 1, color: 'rgba(0,122,255,0.02)' }
|
|
|
|
|
|
]}
|
|
|
|
|
|
},
|
|
|
|
|
|
itemStyle: { color: iosBlue, borderWidth: 2, borderColor: '#fff' }
|
|
|
|
|
|
},
|
|
|
|
|
|
{
|
|
|
|
|
|
name: '成功',
|
|
|
|
|
|
type: 'line',
|
|
|
|
|
|
data: d.successes,
|
|
|
|
|
|
smooth: 0.4,
|
|
|
|
|
|
symbol: 'circle',
|
|
|
|
|
|
symbolSize: 6,
|
|
|
|
|
|
lineStyle: { width: 3, color: iosGreen },
|
|
|
|
|
|
areaStyle: {
|
|
|
|
|
|
color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [
|
|
|
|
|
|
{ offset: 0, color: 'rgba(52,199,89,0.25)' },
|
|
|
|
|
|
{ offset: 1, color: 'rgba(52,199,89,0.02)' }
|
|
|
|
|
|
]}
|
|
|
|
|
|
},
|
|
|
|
|
|
itemStyle: { color: iosGreen, borderWidth: 2, borderColor: '#fff' }
|
|
|
|
|
|
}
|
|
|
|
|
|
]
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (p.data.success && this.charts.pie) {
|
|
|
|
|
|
const d = p.data.data;
|
|
|
|
|
|
this.charts.pie.setOption({
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'item',
|
|
|
|
|
|
formatter: '{b}: {c} ({d}%)',
|
|
|
|
|
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
|
|
|
|
|
borderColor: '#E5E5EA',
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
textStyle: { color: '#1C1C1E', fontSize: 13 }
|
|
|
|
|
|
},
|
|
|
|
|
|
legend: {
|
|
|
|
|
|
bottom: 0,
|
|
|
|
|
|
textStyle: { color: '#8E8E93', fontSize: 12 },
|
|
|
|
|
|
itemWidth: 12,
|
|
|
|
|
|
itemHeight: 12
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [{
|
|
|
|
|
|
type: 'pie',
|
|
|
|
|
|
radius: ['50%', '75%'],
|
|
|
|
|
|
center: ['50%', '45%'],
|
|
|
|
|
|
avoidLabelOverlap: false,
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
position: 'center',
|
|
|
|
|
|
formatter: d.values[0] ? '{d}%' : '0%',
|
|
|
|
|
|
fontSize: 24,
|
|
|
|
|
|
fontWeight: 600,
|
|
|
|
|
|
color: iosGreen
|
|
|
|
|
|
},
|
|
|
|
|
|
emphasis: {
|
|
|
|
|
|
label: { show: true, fontSize: 26, fontWeight: 700 },
|
|
|
|
|
|
itemStyle: { shadowBlur: 20, shadowColor: 'rgba(0,0,0,0.15)' }
|
|
|
|
|
|
},
|
|
|
|
|
|
data: d.labels.map((l, i) => ({
|
|
|
|
|
|
name: l,
|
|
|
|
|
|
value: d.values[i],
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: i === 0 ? iosGreen : iosGray5,
|
|
|
|
|
|
borderRadius: 4,
|
|
|
|
|
|
borderColor: '#fff',
|
|
|
|
|
|
borderWidth: 2
|
|
|
|
|
|
}
|
|
|
|
|
|
}))
|
|
|
|
|
|
}]
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (h.data.success && this.charts.hourly) {
|
|
|
|
|
|
const d = h.data.data;
|
|
|
|
|
|
this.charts.hourly.setOption({
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
|
|
|
|
|
borderColor: '#E5E5EA',
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
textStyle: { color: '#1C1C1E', fontSize: 13 },
|
|
|
|
|
|
axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(0,122,255,0.08)' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: { left: '3%', right: '4%', bottom: '8%', top: '8%', containLabel: true },
|
|
|
|
|
|
xAxis: {
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
data: d.hours.map(h => `${h}:00`),
|
|
|
|
|
|
axisLabel: { fontSize: 10, color: '#8E8E93', interval: 2 },
|
|
|
|
|
|
axisLine: { lineStyle: { color: '#E5E5EA' } },
|
|
|
|
|
|
axisTick: { show: false }
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
axisLabel: { fontSize: 11, color: '#8E8E93' },
|
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
|
splitLine: { lineStyle: { color: '#F2F2F7', type: 'dashed' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [{
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: d.clicks,
|
|
|
|
|
|
barWidth: '60%',
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: p => (p.dataIndex >= 9 && p.dataIndex < 21) ?
|
|
|
|
|
|
{ type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [
|
|
|
|
|
|
{ offset: 0, color: '#007AFF' },
|
|
|
|
|
|
{ offset: 1, color: '#5856D6' }
|
|
|
|
|
|
]} : '#E5E5EA',
|
|
|
|
|
|
borderRadius: [6, 6, 0, 0]
|
|
|
|
|
|
}
|
|
|
|
|
|
}]
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (s.data.success && this.charts.topSites) {
|
|
|
|
|
|
const d = s.data.data;
|
|
|
|
|
|
this.charts.topSites.setOption({
|
|
|
|
|
|
tooltip: {
|
|
|
|
|
|
trigger: 'axis',
|
|
|
|
|
|
axisPointer: { type: 'shadow', shadowStyle: { color: 'rgba(0,122,255,0.08)' } },
|
|
|
|
|
|
backgroundColor: 'rgba(255,255,255,0.95)',
|
|
|
|
|
|
borderColor: '#E5E5EA',
|
|
|
|
|
|
borderWidth: 1,
|
|
|
|
|
|
textStyle: { color: '#1C1C1E', fontSize: 13 }
|
|
|
|
|
|
},
|
|
|
|
|
|
grid: { left: '3%', right: '8%', bottom: '3%', top: '3%', containLabel: true },
|
|
|
|
|
|
xAxis: {
|
|
|
|
|
|
type: 'value',
|
|
|
|
|
|
axisLabel: { fontSize: 11, color: '#8E8E93' },
|
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
|
splitLine: { lineStyle: { color: '#F2F2F7', type: 'dashed' } }
|
|
|
|
|
|
},
|
|
|
|
|
|
yAxis: {
|
|
|
|
|
|
type: 'category',
|
|
|
|
|
|
data: d.map(s => (s.site_name || s.site_url || '').substring(0, 12)).reverse(),
|
|
|
|
|
|
axisLabel: { fontSize: 11, color: '#3C3C43' },
|
|
|
|
|
|
axisLine: { show: false },
|
|
|
|
|
|
axisTick: { show: false }
|
|
|
|
|
|
},
|
|
|
|
|
|
series: [{
|
|
|
|
|
|
type: 'bar',
|
|
|
|
|
|
data: d.map(s => s.click_count).reverse(),
|
|
|
|
|
|
barWidth: '65%',
|
|
|
|
|
|
itemStyle: {
|
|
|
|
|
|
color: { type: 'linear', x: 0, y: 0, x2: 1, y2: 0, colorStops: [
|
|
|
|
|
|
{ offset: 0, color: '#007AFF' },
|
|
|
|
|
|
{ offset: 1, color: '#5856D6' }
|
|
|
|
|
|
]},
|
|
|
|
|
|
borderRadius: [0, 6, 6, 0]
|
|
|
|
|
|
},
|
|
|
|
|
|
label: {
|
|
|
|
|
|
show: true,
|
|
|
|
|
|
position: 'right',
|
|
|
|
|
|
fontSize: 11,
|
|
|
|
|
|
color: '#8E8E93'
|
|
|
|
|
|
}
|
|
|
|
|
|
}]
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e) { console.error(e); }
|
2026-01-16 22:06:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|