Files
opsxsql/数据审核平台菜单优化/data-audit-platform-pro/index.html
T
loganandClaude Opus 4.7 439155c92e Initial commit: SQL audit platform v0.1.0
FastAPI backend with auth, user, instance, ticket, query, dashboard APIs.
React + TypeScript frontend with mock data UI (10 pages).
SQLite + async SQLAlchemy, JWT auth, role-based access control.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 11:11:57 +08:00

1991 lines
125 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DataGuard Pro - 企业级数据审核平台</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus@2.5.0/dist/index.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/element-plus@2.5.0/dist/index.full.js"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<style>
:root {
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--bg-hover: #30363d;
--border-color: #30363d;
--border-light: #21262d;
--text-primary: #f0f6fc;
--text-secondary: #8b949e;
--text-muted: #6e7681;
--accent-blue: #58a6ff;
--accent-green: #238636;
--accent-red: #da3633;
--accent-orange: #f0883e;
--accent-purple: #8957e5;
--accent-cyan: #39c5cf;
--accent-yellow: #d29922;
--success: #3fb950;
--warning: #d29922;
--danger: #f85149;
--info: #58a6ff;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans SC', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
overflow-x: hidden;
}
.login-container {
min-height: 100vh;
display: flex;
background: linear-gradient(135deg, #0d1117 0%, #161b22 50%, #0d1117 100%);
position: relative;
overflow: hidden;
}
.login-container::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: radial-gradient(ellipse at 20% 20%, rgba(88, 166, 255, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 80%, rgba(137, 87, 229, 0.08) 0%, transparent 50%);
pointer-events: none;
}
.login-left {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px;
position: relative;
z-index: 1;
}
.login-brand { margin-bottom: 40px; }
.login-brand h1 {
font-size: 48px;
font-weight: 700;
background: linear-gradient(135deg, #58a6ff, #8957e5, #39c5cf);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 16px;
}
.login-brand p { font-size: 18px; color: var(--text-secondary); line-height: 1.6; }
.login-features {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
margin-top: 40px;
}
.login-feature-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 16px;
background: rgba(22, 27, 34, 0.6);
border: 1px solid var(--border-color);
border-radius: 12px;
transition: all 0.3s ease;
}
.login-feature-item:hover {
background: rgba(33, 38, 45, 0.8);
border-color: var(--accent-blue);
transform: translateY(-2px);
}
.login-feature-item i { font-size: 24px; color: var(--accent-blue); margin-top: 2px; }
.login-feature-item h4 { font-size: 14px; font-weight: 600; margin-bottom: 4px; color: var(--text-primary); }
.login-feature-item span { font-size: 12px; color: var(--text-secondary); }
.login-right {
width: 480px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px;
background: rgba(22, 27, 34, 0.9);
backdrop-filter: blur(20px);
border-left: 1px solid var(--border-color);
position: relative;
z-index: 1;
}
.login-form-wrapper { width: 100%; }
.login-form-wrapper h2 { font-size: 28px; margin-bottom: 8px; color: var(--text-primary); }
.login-form-wrapper .subtitle { font-size: 14px; color: var(--text-secondary); margin-bottom: 32px; }
.login-form .el-input__wrapper {
background: var(--bg-tertiary);
box-shadow: none !important;
border: 1px solid var(--border-color);
}
.login-form .el-input__wrapper:hover, .login-form .el-input__wrapper.is-focus { border-color: var(--accent-blue); }
.login-form .el-input__inner { color: var(--text-primary); height: 44px; }
.login-form .el-button--primary {
width: 100%;
height: 44px;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
border: none;
font-size: 16px;
font-weight: 600;
}
.login-form .el-button--primary:hover { opacity: 0.9; transform: translateY(-1px); }
.main-layout { display: flex; min-height: 100vh; }
.sidebar {
width: 260px;
background: var(--bg-secondary);
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
position: fixed;
left: 0; top: 0; bottom: 0;
z-index: 100;
transition: width 0.3s ease;
}
.sidebar.collapsed { width: 64px; }
.sidebar-header {
height: 64px;
display: flex;
align-items: center;
padding: 0 20px;
border-bottom: 1px solid var(--border-color);
}
.sidebar-logo {
display: flex;
align-items: center;
gap: 12px;
font-size: 20px;
font-weight: 700;
background: linear-gradient(135deg, #58a6ff, #8957e5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.sidebar-logo i { font-size: 28px; color: var(--accent-blue); -webkit-text-fill-color: initial; }
.sidebar-menu { flex: 1; overflow-y: auto; padding: 12px 0; }
.sidebar-menu::-webkit-scrollbar { width: 4px; }
.sidebar-menu::-webkit-scrollbar-thumb { background: var(--border-color); border-radius: 2px; }
.menu-item {
display: flex;
align-items: center;
padding: 12px 20px;
margin: 2px 12px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-secondary);
position: relative;
}
.menu-item:hover { background: var(--bg-hover); color: var(--text-primary); }
.menu-item.active { background: rgba(88, 166, 255, 0.15); color: var(--accent-blue); }
.menu-item.active::before {
content: '';
position: absolute;
left: 0; top: 50%;
transform: translateY(-50%);
width: 3px; height: 20px;
background: var(--accent-blue);
border-radius: 0 2px 2px 0;
}
.menu-item i { font-size: 18px; width: 24px; text-align: center; margin-right: 12px; }
.menu-item span { font-size: 14px; font-weight: 500; }
.menu-section {
padding: 16px 20px 8px;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-muted);
}
.sidebar-footer { padding: 16px; border-top: 1px solid var(--border-color); }
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 8px;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease;
}
.user-info:hover { background: var(--bg-hover); }
.user-avatar {
width: 36px; height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 14px;
}
.user-details { flex: 1; overflow: hidden; }
.user-name { font-size: 14px; font-weight: 500; color: var(--text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.user-role { font-size: 12px; color: var(--text-muted); }
.main-content { flex: 1; margin-left: 260px; display: flex; flex-direction: column; min-height: 100vh; }
.main-content.expanded { margin-left: 64px; }
.top-header {
height: 64px;
background: var(--bg-secondary);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
position: sticky;
top: 0;
z-index: 50;
}
.header-left { display: flex; align-items: center; gap: 16px; }
.breadcrumb { display: flex; align-items: center; gap: 8px; font-size: 14px; color: var(--text-secondary); }
.breadcrumb .separator { color: var(--text-muted); }
.breadcrumb .current { color: var(--text-primary); font-weight: 500; }
.header-right { display: flex; align-items: center; gap: 8px; }
.header-btn {
width: 36px; height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
color: var(--text-secondary);
position: relative;
}
.header-btn:hover { background: var(--bg-hover); color: var(--text-primary); }
.header-btn .badge { position: absolute; top: 4px; right: 4px; width: 8px; height: 8px; background: var(--danger); border-radius: 50%; }
.content-area { flex: 1; padding: 24px; overflow-y: auto; }
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 24px; }
.stat-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
}
.stat-card:hover { border-color: var(--accent-blue); transform: translateY(-2px); }
.stat-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
.stat-title { font-size: 13px; color: var(--text-secondary); font-weight: 500; }
.stat-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
.stat-icon.blue { background: rgba(88, 166, 255, 0.15); color: var(--accent-blue); }
.stat-icon.green { background: rgba(35, 134, 54, 0.15); color: var(--success); }
.stat-icon.orange { background: rgba(240, 136, 62, 0.15); color: var(--accent-orange); }
.stat-icon.purple { background: rgba(137, 87, 229, 0.15); color: var(--accent-purple); }
.stat-icon.red { background: rgba(218, 54, 51, 0.15); color: var(--danger); }
.stat-value { font-size: 32px; font-weight: 700; color: var(--text-primary); margin-bottom: 8px; }
.stat-footer { display: flex; align-items: center; gap: 8px; font-size: 13px; }
.stat-trend { display: flex; align-items: center; gap: 4px; font-weight: 500; }
.stat-trend.up { color: var(--success); }
.stat-trend.down { color: var(--danger); }
.stat-compare { color: var(--text-muted); }
.dashboard-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 24px; }
.chart-card, .list-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.chart-header, .list-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid var(--border-color);
}
.chart-title, .list-title { font-size: 15px; font-weight: 600; color: var(--text-primary); }
.chart-actions { display: flex; gap: 8px; }
.chart-action-btn {
padding: 6px 12px;
font-size: 12px;
border-radius: 6px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s ease;
}
.chart-action-btn:hover, .chart-action-btn.active {
background: rgba(88, 166, 255, 0.15);
border-color: var(--accent-blue);
color: var(--accent-blue);
}
.chart-body { padding: 20px; height: 300px; }
.list-body { padding: 0; }
.list-item {
display: flex;
align-items: center;
padding: 14px 20px;
border-bottom: 1px solid var(--border-light);
transition: background 0.2s ease;
}
.list-item:last-child { border-bottom: none; }
.list-item:hover { background: var(--bg-hover); }
.list-item-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; margin-right: 14px; font-size: 16px; }
.list-item-icon.mysql { background: rgba(0, 117, 143, 0.2); color: #00758f; }
.list-item-icon.redis { background: rgba(220, 50, 32, 0.2); color: #dc3220; }
.list-item-icon.postgres { background: rgba(51, 103, 145, 0.2); color: #336791; }
.list-item-icon.mongo { background: rgba(71, 162, 72, 0.2); color: #47a248; }
.list-item-content { flex: 1; }
.list-item-title { font-size: 14px; font-weight: 500; color: var(--text-primary); margin-bottom: 4px; }
.list-item-desc { font-size: 12px; color: var(--text-muted); }
.list-item-status { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; }
.status-online { background: rgba(35, 134, 54, 0.15); color: var(--success); }
.status-offline { background: rgba(218, 54, 51, 0.15); color: var(--danger); }
.status-warning { background: rgba(210, 153, 34, 0.15); color: var(--warning); }
.page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; }
.page-title-section h1 { font-size: 24px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; }
.page-title-section p { font-size: 14px; color: var(--text-secondary); }
.page-actions { display: flex; gap: 12px; }
.btn-primary {
padding: 10px 20px;
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
border: none;
border-radius: 8px;
color: white;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary:hover { opacity: 0.9; transform: translateY(-1px); }
.btn-secondary {
padding: 10px 20px;
background: var(--bg-tertiary);
border: 1px solid var(--border-color);
border-radius: 8px;
color: var(--text-primary);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-secondary:hover { background: var(--bg-hover); border-color: var(--text-muted); }
.btn-danger {
padding: 10px 20px;
background: rgba(218, 54, 51, 0.15);
border: 1px solid var(--danger);
border-radius: 8px;
color: var(--danger);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 8px;
}
.btn-danger:hover { background: rgba(218, 54, 51, 0.25); }
.filter-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 20px;
padding: 16px 20px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
flex-wrap: wrap;
}
.data-table {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.el-table { background: transparent; }
.el-table th { background: var(--bg-tertiary) !important; color: var(--text-primary) !important; font-weight: 600; border-bottom: 1px solid var(--border-color) !important; }
.el-table td { background: transparent !important; color: var(--text-primary) !important; border-bottom: 1px solid var(--border-light) !important; }
.el-table tr:hover td { background: var(--bg-hover) !important; }
.tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 500;
}
.tag-mysql { background: rgba(0, 117, 143, 0.15); color: #26a69a; }
.tag-redis { background: rgba(220, 50, 32, 0.15); color: #ef5350; }
.tag-postgres { background: rgba(51, 103, 145, 0.15); color: #42a5f5; }
.tag-mongo { background: rgba(71, 162, 72, 0.15); color: #66bb6a; }
.tag-clickhouse { background: rgba(255, 202, 40, 0.15); color: #ffca28; }
.status-tag {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 500;
}
.status-tag::before { content: ''; width: 6px; height: 6px; border-radius: 50%; }
.status-pending { background: rgba(210, 153, 34, 0.15); color: var(--warning); }
.status-pending::before { background: var(--warning); }
.status-reviewing { background: rgba(88, 166, 255, 0.15); color: var(--accent-blue); }
.status-reviewing::before { background: var(--accent-blue); animation: pulse 2s infinite; }
.status-approved { background: rgba(35, 134, 54, 0.15); color: var(--success); }
.status-approved::before { background: var(--success); }
.status-rejected { background: rgba(218, 54, 51, 0.15); color: var(--danger); }
.status-rejected::before { background: var(--danger); }
.status-executing { background: rgba(137, 87, 229, 0.15); color: var(--accent-purple); }
.status-executing::before { background: var(--accent-purple); animation: pulse 1s infinite; }
.status-success { background: rgba(35, 134, 54, 0.15); color: var(--success); }
.status-success::before { background: var(--success); }
.status-failed { background: rgba(218, 54, 51, 0.15); color: var(--danger); }
.status-failed::before { background: var(--danger); }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.5; } }
.sql-editor-container {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.sql-editor-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.sql-editor-tabs { display: flex; gap: 4px; }
.sql-editor-tab {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 13px;
color: var(--text-secondary);
cursor: pointer;
}
.sql-editor-tab.active { background: rgba(88, 166, 255, 0.15); border-color: var(--accent-blue); color: var(--accent-blue); }
.sql-editor-area {
min-height: 200px;
padding: 16px;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 14px;
line-height: 1.6;
color: var(--text-primary);
background: var(--bg-primary);
resize: vertical;
border: none;
outline: none;
width: 100%;
}
.sql-editor-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: var(--bg-tertiary);
border-top: 1px solid var(--border-color);
font-size: 12px;
color: var(--text-muted);
}
.audit-result-panel {
margin-top: 16px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.audit-result-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.audit-result-title { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 600; }
.audit-score {
display: flex;
align-items: center;
gap: 8px;
padding: 4px 12px;
background: rgba(35, 134, 54, 0.15);
border-radius: 20px;
font-size: 14px;
font-weight: 600;
color: var(--success);
}
.audit-result-list { padding: 8px 0; }
.audit-result-item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--border-light);
}
.audit-result-item:last-child { border-bottom: none; }
.audit-result-icon {
width: 24px; height: 24px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
flex-shrink: 0;
}
.audit-result-icon.error { background: rgba(218, 54, 51, 0.15); color: var(--danger); }
.audit-result-icon.warning { background: rgba(210, 153, 34, 0.15); color: var(--warning); }
.audit-result-icon.info { background: rgba(88, 166, 255, 0.15); color: var(--accent-blue); }
.audit-result-icon.success { background: rgba(35, 134, 54, 0.15); color: var(--success); }
.audit-result-content { flex: 1; }
.audit-result-rule { font-size: 13px; font-weight: 500; color: var(--text-primary); margin-bottom: 4px; }
.audit-result-desc { font-size: 12px; color: var(--text-secondary); margin-bottom: 4px; }
.audit-result-suggestion { font-size: 12px; color: var(--accent-blue); }
.workflow-timeline {
display: flex;
align-items: center;
padding: 20px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
margin-bottom: 20px;
}
.workflow-step { display: flex; flex-direction: column; align-items: center; gap: 8px; }
.workflow-step-icon {
width: 40px; height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
position: relative;
}
.workflow-step-icon.completed { background: var(--success); color: white; }
.workflow-step-icon.active { background: var(--accent-blue); color: white; box-shadow: 0 0 0 4px rgba(88, 166, 255, 0.2); }
.workflow-step-icon.pending { background: var(--bg-tertiary); color: var(--text-muted); border: 2px solid var(--border-color); }
.workflow-step-label { font-size: 12px; color: var(--text-secondary); white-space: nowrap; }
.workflow-step-time { font-size: 11px; color: var(--text-muted); }
.workflow-connector { flex: 1; height: 2px; background: var(--border-color); margin: 0 12px; margin-bottom: 40px; }
.workflow-connector.completed { background: var(--success); }
.workflow-connector.active { background: linear-gradient(90deg, var(--success) 50%, var(--border-color) 50%); }
.channel-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
.channel-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
}
.channel-card:hover { border-color: var(--accent-blue); }
.channel-card.enabled { border-color: var(--success); background: rgba(35, 134, 54, 0.05); }
.channel-icon { width: 48px; height: 48px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 24px; }
.channel-icon.feishu { background: rgba(0, 150, 255, 0.15); color: #00a8ff; }
.channel-icon.dingtalk { background: rgba(0, 160, 255, 0.15); color: #0089ff; }
.channel-icon.wechat { background: rgba(7, 193, 96, 0.15); color: #07c160; }
.channel-icon.telegram { background: rgba(0, 136, 204, 0.15); color: #0088cc; }
.channel-info { flex: 1; }
.channel-name { font-size: 15px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.channel-desc { font-size: 12px; color: var(--text-secondary); }
.channel-status { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; }
.channel-status.enabled { background: rgba(35, 134, 54, 0.15); color: var(--success); }
.channel-status.disabled { background: var(--bg-tertiary); color: var(--text-muted); }
.instance-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
.instance-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
cursor: pointer;
}
.instance-card:hover { border-color: var(--accent-blue); transform: translateY(-2px); }
.instance-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 16px; }
.instance-type-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 22px; }
.instance-type-icon.mysql { background: rgba(0, 117, 143, 0.15); color: #26a69a; }
.instance-type-icon.redis { background: rgba(220, 50, 32, 0.15); color: #ef5350; }
.instance-type-icon.postgres { background: rgba(51, 103, 145, 0.15); color: #42a5f5; }
.instance-type-icon.mongo { background: rgba(71, 162, 72, 0.15); color: #66bb6a; }
.instance-type-icon.clickhouse { background: rgba(255, 202, 40, 0.15); color: #ffca28; }
.instance-status { width: 10px; height: 10px; border-radius: 50%; }
.instance-status.online { background: var(--success); box-shadow: 0 0 0 3px rgba(35, 134, 54, 0.2); }
.instance-status.offline { background: var(--danger); }
.instance-status.warning { background: var(--warning); }
.instance-name { font-size: 16px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.instance-meta { font-size: 12px; color: var(--text-muted); margin-bottom: 16px; }
.instance-metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; padding-top: 16px; border-top: 1px solid var(--border-color); }
.instance-metric { text-align: center; }
.instance-metric-value { font-size: 18px; font-weight: 600; color: var(--text-primary); }
.instance-metric-label { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.pagination-wrapper { display: flex; align-items: center; justify-content: space-between; padding: 16px 20px; border-top: 1px solid var(--border-color); }
.el-pagination { --el-pagination-bg-color: transparent; --el-pagination-button-color: var(--text-secondary); --el-pagination-hover-color: var(--accent-blue); }
.el-pagination .el-pager li { background: transparent; color: var(--text-secondary); }
.el-pagination .el-pager li.active { background: var(--accent-blue); color: white; }
.el-pagination button { background: transparent !important; }
.el-dialog { background: var(--bg-secondary); border: 1px solid var(--border-color); border-radius: 12px; }
.el-dialog__header { border-bottom: 1px solid var(--border-color); padding: 16px 20px; margin: 0; }
.el-dialog__title { color: var(--text-primary); font-weight: 600; }
.el-dialog__body { padding: 20px; color: var(--text-primary); }
.el-dialog__footer { border-top: 1px solid var(--border-color); padding: 16px 20px; }
.el-form-item__label { color: var(--text-secondary); }
.el-input__wrapper, .el-textarea__inner { background: var(--bg-tertiary); box-shadow: none !important; border: 1px solid var(--border-color); }
.el-input__inner, .el-textarea__inner { color: var(--text-primary); }
.el-select .el-input__wrapper { background: var(--bg-tertiary); }
.el-select-dropdown { background: var(--bg-secondary); border: 1px solid var(--border-color); }
.el-select-dropdown__item { color: var(--text-primary); }
.el-select-dropdown__item:hover, .el-select-dropdown__item.selected { background: var(--bg-hover); color: var(--accent-blue); }
.el-checkbox { color: var(--text-secondary); }
.el-checkbox__input.is-checked + .el-checkbox__label { color: var(--accent-blue); }
.el-checkbox__input.is-checked .el-checkbox__inner { background-color: var(--accent-blue); border-color: var(--accent-blue); }
@media (max-width: 1400px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.dashboard-grid { grid-template-columns: 1fr; }
.instance-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 1024px) {
.sidebar { width: 64px; }
.sidebar .menu-item span, .sidebar .menu-section, .sidebar .user-details { display: none; }
.main-content { margin-left: 64px; }
.login-left { display: none; }
.login-right { width: 100%; }
}
@media (max-width: 768px) {
.stats-grid, .instance-grid, .channel-grid { grid-template-columns: 1fr; }
}
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* 查询审计专用样式 */
.query-detail-panel {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
margin-bottom: 20px;
}
.query-detail-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.query-sql-preview {
padding: 16px 20px;
background: var(--bg-primary);
font-family: 'JetBrains Mono', monospace;
font-size: 13px;
color: var(--text-primary);
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.query-metrics {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
padding: 16px 20px;
border-top: 1px solid var(--border-color);
}
.query-metric { text-align: center; }
.query-metric-value { font-size: 20px; font-weight: 600; color: var(--text-primary); }
.query-metric-value.slow { color: var(--danger); }
.query-metric-label { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.result-set-panel {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
overflow: hidden;
}
.result-set-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
background: var(--bg-tertiary);
border-bottom: 1px solid var(--border-color);
}
.sensitive-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 2px 8px;
background: rgba(210, 153, 34, 0.2);
border: 1px solid var(--warning);
border-radius: 4px;
font-size: 11px;
color: var(--warning);
}
.masked-data {
color: var(--warning);
font-style: italic;
}
/* 权限管理样式 */
.permission-tree {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px;
}
.role-card {
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 20px;
cursor: pointer;
transition: all 0.2s ease;
}
.role-card:hover { border-color: var(--accent-blue); }
.role-card.active { border-color: var(--accent-blue); background: rgba(88, 166, 255, 0.05); }
.role-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.role-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
.role-icon.admin { background: rgba(137, 87, 229, 0.15); color: var(--accent-purple); }
.role-icon.dba { background: rgba(88, 166, 255, 0.15); color: var(--accent-blue); }
.role-icon.dev { background: rgba(35, 134, 54, 0.15); color: var(--success); }
.role-icon.guest { background: rgba(139, 148, 158, 0.15); color: var(--text-secondary); }
.role-name { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.role-desc { font-size: 12px; color: var(--text-muted); }
.role-stats { display: flex; gap: 16px; margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border-color); }
.role-stat { font-size: 12px; color: var(--text-secondary); }
.role-stat strong { color: var(--text-primary); margin-right: 4px; }
</style>
</head>
<body>
<div id="app">
<!-- 登录页面 -->
<div v-if="!isLoggedIn" class="login-container">
<div class="login-left">
<div class="login-brand">
<h1><i class="fas fa-shield-alt"></i> DataGuard Pro</h1>
<p>企业级数据审核与变更管理平台<br>支持多数据库类型,提供完善的 SQL 审核、审批流程和审计追踪</p>
</div>
<div class="login-features">
<div class="login-feature-item"><i class="fas fa-database"></i><div><h4>多数据库支持</h4><span>MySQL、PostgreSQL、Redis、MongoDB、ClickHouse</span></div></div>
<div class="login-feature-item"><i class="fas fa-code-check"></i><div><h4>智能 SQL 审核</h4><span>语法检查、规则校验、影响分析</span></div></div>
<div class="login-feature-item"><i class="fas fa-sitemap"></i><div><h4>多级审批流程</h4><span>会签、或签、条件审批灵活配置</span></div></div>
<div class="login-feature-item"><i class="fas fa-bell"></i><div><h4>多渠道通知</h4><span>飞书、钉钉、企微、TG 即时推送</span></div></div>
<div class="login-feature-item"><i class="fas fa-history"></i><div><h4>完整审计追踪</h4><span>所有操作可追溯,支持 SQL 回滚</span></div></div>
<div class="login-feature-item"><i class="fas fa-user-shield"></i><div><h4>细粒度权限</h4><span>RBAC 角色权限,数据访问控制</span></div></div>
</div>
</div>
<div class="login-right">
<div class="login-form-wrapper">
<h2>欢迎回来</h2>
<p class="subtitle">请登录您的账户以继续</p>
<el-form class="login-form" :model="loginForm" @submit.prevent="handleLogin">
<el-form-item>
<el-input v-model="loginForm.username" placeholder="用户名/邮箱" size="large">
<template #prefix><i class="fas fa-user"></i></template>
</el-input>
</el-form-item>
<el-form-item>
<el-input v-model="loginForm.password" type="password" placeholder="密码" size="large" show-password>
<template #prefix><i class="fas fa-lock"></i></template>
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" size="large" @click="handleLogin" :loading="loginLoading">
<i class="fas fa-sign-in-alt"></i> 登录
</el-button>
</el-form-item>
</el-form>
<div style="text-align: center; margin-top: 20px; font-size: 13px; color: var(--text-muted);">
演示账号: admin / admin123 或 developer / dev123
</div>
</div>
</div>
</div>
<!-- 主应用 -->
<div v-else class="main-layout">
<!-- 侧边栏 -->
<aside class="sidebar" :class="{ collapsed: sidebarCollapsed }">
<div class="sidebar-header">
<div class="sidebar-logo">
<i class="fas fa-shield-alt"></i>
<span v-if="!sidebarCollapsed">DataGuard</span>
</div>
</div>
<nav class="sidebar-menu">
<div class="menu-section" v-if="!sidebarCollapsed">概览</div>
<div class="menu-item" :class="{ active: currentView === 'dashboard' }" @click="currentView = 'dashboard'">
<i class="fas fa-chart-pie"></i><span v-if="!sidebarCollapsed">仪表盘</span>
</div>
<div class="menu-section" v-if="!sidebarCollapsed">数据源</div>
<div class="menu-item" :class="{ active: currentView === 'mysql' }" @click="currentView = 'mysql'">
<i class="fas fa-database"></i><span v-if="!sidebarCollapsed">MySQL</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'postgres' }" @click="currentView = 'postgres'">
<i class="fas fa-server"></i><span v-if="!sidebarCollapsed">PostgreSQL</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'redis' }" @click="currentView = 'redis'">
<i class="fas fa-bolt"></i><span v-if="!sidebarCollapsed">Redis</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'mongo' }" @click="currentView = 'mongo'">
<i class="fas fa-leaf"></i><span v-if="!sidebarCollapsed">MongoDB</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'clickhouse' }" @click="currentView = 'clickhouse'">
<i class="fas fa-mouse-pointer"></i><span v-if="!sidebarCollapsed">ClickHouse</span>
</div>
<div class="menu-section" v-if="!sidebarCollapsed">工单管理</div>
<div class="menu-item" :class="{ active: currentView === 'tickets' }" @click="currentView = 'tickets'">
<i class="fas fa-clipboard-list"></i><span v-if="!sidebarCollapsed">数据变更</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'queries' }" @click="currentView = 'queries'">
<i class="fas fa-search"></i><span v-if="!sidebarCollapsed">查询审计</span>
</div>
<div class="menu-section" v-if="!sidebarCollapsed">权限管理</div>
<div class="menu-item" :class="{ active: currentView === 'roles' }" @click="currentView = 'roles'">
<i class="fas fa-user-tag"></i><span v-if="!sidebarCollapsed">角色权限</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'users' }" @click="currentView = 'users'">
<i class="fas fa-users"></i><span v-if="!sidebarCollapsed">用户管理</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'data-permission' }" @click="currentView = 'data-permission'">
<i class="fas fa-lock"></i><span v-if="!sidebarCollapsed">数据权限</span>
</div>
<div class="menu-section" v-if="!sidebarCollapsed">系统配置</div>
<div class="menu-item" :class="{ active: currentView === 'audit-rules' }" @click="currentView = 'audit-rules'">
<i class="fas fa-shield-alt"></i><span v-if="!sidebarCollapsed">审核规则</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'workflow' }" @click="currentView = 'workflow'">
<i class="fas fa-project-diagram"></i><span v-if="!sidebarCollapsed">审批流程</span>
</div>
<div class="menu-item" :class="{ active: currentView === 'notifications' }" @click="currentView = 'notifications'">
<i class="fas fa-bell"></i><span v-if="!sidebarCollapsed">通知渠道</span>
</div>
</nav>
<div class="sidebar-footer">
<div class="user-info" @click="showUserMenu = true">
<div class="user-avatar">{{ currentUser.avatar }}</div>
<div class="user-details" v-if="!sidebarCollapsed">
<div class="user-name">{{ currentUser.name }}</div>
<div class="user-role">{{ currentUser.roleName }}</div>
</div>
</div>
</div>
</aside>
<!-- 主内容 -->
<main class="main-content" :class="{ expanded: sidebarCollapsed }">
<!-- 顶部导航 -->
<header class="top-header">
<div class="header-left">
<div class="breadcrumb">
<span>DataGuard</span><span class="separator">/</span><span class="current">{{ getPageTitle() }}</span>
</div>
</div>
<div class="header-right">
<div class="header-btn" @click="toggleSidebar"><i class="fas fa-bars"></i></div>
<div class="header-btn" @click="showNotifications = true">
<i class="fas fa-bell"></i>
<span v-if="unreadNotifications > 0" class="badge"></span>
</div>
<div class="header-btn" @click="logout"><i class="fas fa-sign-out-alt"></i></div>
</div>
</header>
<!-- 内容区域 -->
<div class="content-area">
<!-- 仪表盘 -->
<div v-if="currentView === 'dashboard'" class="fade-enter-active">
<div class="stats-grid">
<div class="stat-card">
<div class="stat-header"><span class="stat-title">今日工单</span><div class="stat-icon blue"><i class="fas fa-clipboard-list"></i></div></div>
<div class="stat-value">128</div>
<div class="stat-footer"><span class="stat-trend up"><i class="fas fa-arrow-up"></i> 12%</span><span class="stat-compare">较昨日</span></div>
</div>
<div class="stat-card">
<div class="stat-header"><span class="stat-title">待审批</span><div class="stat-icon orange"><i class="fas fa-clock"></i></div></div>
<div class="stat-value">23</div>
<div class="stat-footer"><span class="stat-trend down"><i class="fas fa-arrow-down"></i> 5%</span><span class="stat-compare">较昨日</span></div>
</div>
<div class="stat-card">
<div class="stat-header"><span class="stat-title">查询审计</span><div class="stat-icon purple"><i class="fas fa-search"></i></div></div>
<div class="stat-value">15,234</div>
<div class="stat-footer"><span class="stat-trend up"><i class="fas fa-arrow-up"></i> 8%</span><span class="stat-compare">较昨日</span></div>
</div>
<div class="stat-card">
<div class="stat-header"><span class="stat-title">慢查询</span><div class="stat-icon red"><i class="fas fa-exclamation-triangle"></i></div></div>
<div class="stat-value">47</div>
<div class="stat-footer"><span class="stat-trend down"><i class="fas fa-arrow-down"></i> 12%</span><span class="stat-compare">较昨日</span></div>
</div>
</div>
<div class="dashboard-grid">
<div class="chart-card">
<div class="chart-header">
<span class="chart-title">工单趋势</span>
<div class="chart-actions">
<button :class="['chart-action-btn', chartPeriod === '7d' ? 'active' : '']" @click="chartPeriod = '7d'">近7天</button>
<button :class="['chart-action-btn', chartPeriod === '30d' ? 'active' : '']" @click="chartPeriod = '30d'">近30天</button>
<button :class="['chart-action-btn', chartPeriod === '90d' ? 'active' : '']" @click="chartPeriod = '90d'">近90天</button>
</div>
</div>
<div class="chart-body" ref="trendChart"></div>
</div>
<div class="list-card">
<div class="list-header"><span class="list-title">实例状态</span></div>
<div class="list-body">
<div class="list-item">
<div class="list-item-icon mysql"><i class="fas fa-database"></i></div>
<div class="list-item-content"><div class="list-item-title">生产 MySQL 主库</div><div class="list-item-desc">192.168.1.10:3306</div></div>
<span class="list-item-status status-online">在线</span>
</div>
<div class="list-item">
<div class="list-item-icon redis"><i class="fas fa-bolt"></i></div>
<div class="list-item-content"><div class="list-item-title">Redis 集群</div><div class="list-item-desc">192.168.1.20:6379</div></div>
<span class="list-item-status status-online">在线</span>
</div>
<div class="list-item">
<div class="list-item-icon postgres"><i class="fas fa-server"></i></div>
<div class="list-item-content"><div class="list-item-title">PostgreSQL 分析库</div><div class="list-item-desc">192.168.1.30:5432</div></div>
<span class="list-item-status status-warning">告警</span>
</div>
<div class="list-item">
<div class="list-item-icon mongo"><i class="fas fa-leaf"></i></div>
<div class="list-item-content"><div class="list-item-title">MongoDB 日志库</div><div class="list-item-desc">192.168.1.40:27017</div></div>
<span class="list-item-status status-online">在线</span>
</div>
</div>
</div>
</div>
<div class="list-card">
<div class="list-header"><span class="list-title">待处理工单</span></div>
<div class="data-table">
<el-table :data="pendingTickets" style="width: 100%">
<el-table-column prop="id" label="工单号" width="130" />
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="dbType" label="数据库" width="90">
<template #default="scope"><span :class="'tag tag-' + scope.row.dbType">{{ scope.row.dbType.toUpperCase() }}</span></template>
</el-table-column>
<el-table-column prop="applicant" label="申请人" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope"><span :class="'status-tag status-' + scope.row.status">{{ getStatusText(scope.row.status) }}</span></template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="150" />
<el-table-column label="操作" width="140" fixed="right">
<template #default>
<el-button type="primary" link size="small">查看</el-button>
<el-button type="success" link size="small">审批</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
</div>
<!-- MySQL 实例管理 -->
<div v-if="currentView === 'mysql'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>MySQL 实例管理</h1><p>管理 MySQL 数据库实例,监控连接状态和性能指标</p></div>
<div class="page-actions">
<button class="btn-secondary" @click="refreshInstances"><i class="fas fa-sync-alt"></i> 刷新</button>
<button class="btn-primary" @click="showAddInstance = true"><i class="fas fa-plus"></i> 添加实例</button>
</div>
</div>
<div class="filter-bar">
<el-input v-model="searchQuery" placeholder="搜索实例名称/地址" style="width: 280px" />
<el-select v-model="filterStatus" placeholder="状态" style="width: 120px">
<el-option label="全部" value="" /><el-option label="在线" value="online" />
<el-option label="离线" value="offline" /><el-option label="告警" value="warning" />
</el-select>
<el-select v-model="filterEnv" placeholder="环境" style="width: 120px">
<el-option label="全部" value="" /><el-option label="生产" value="prod" />
<el-option label="测试" value="test" /><el-option label="开发" value="dev" />
</el-select>
</div>
<div class="instance-grid">
<div v-for="instance in filteredInstances" :key="instance.id" class="instance-card" @click="viewInstance(instance)">
<div class="instance-header">
<div class="instance-type-icon mysql"><i class="fas fa-database"></i></div>
<div :class="'instance-status ' + instance.status"></div>
</div>
<div class="instance-name">{{ instance.name }}</div>
<div class="instance-meta">{{ instance.host }}:{{ instance.port }}</div>
<div class="instance-metrics">
<div class="instance-metric"><div class="instance-metric-value">{{ instance.connections }}</div><div class="instance-metric-label">连接数</div></div>
<div class="instance-metric"><div class="instance-metric-value">{{ instance.qps }}</div><div class="instance-metric-label">QPS</div></div>
<div class="instance-metric"><div class="instance-metric-value">{{ instance.latency }}ms</div><div class="instance-metric-label">延迟</div></div>
</div>
</div>
</div>
</div>
<!-- 数据变更工单 -->
<div v-if="currentView === 'tickets'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>数据变更工单</h1><p>管理 SQL 变更工单,提交审核、查看审批进度</p></div>
<div class="page-actions">
<button class="btn-primary" @click="showNewTicket = true"><i class="fas fa-plus"></i> 新建工单</button>
</div>
</div>
<div class="filter-bar">
<el-input v-model="ticketSearch" placeholder="搜索工单标题/编号" style="width: 240px" />
<el-select v-model="ticketStatus" placeholder="工单状态" style="width: 130px">
<el-option label="全部状态" value="" /><el-option label="待审批" value="pending" />
<el-option label="审批中" value="reviewing" /><el-option label="已通过" value="approved" />
<el-option label="已拒绝" value="rejected" /><el-option label="执行中" value="executing" />
<el-option label="执行成功" value="success" /><el-option label="执行失败" value="failed" />
</el-select>
<el-select v-model="ticketDbType" placeholder="数据库类型" style="width: 130px">
<el-option label="全部" value="" /><el-option label="MySQL" value="mysql" />
<el-option label="PostgreSQL" value="postgres" /><el-option label="Redis" value="redis" />
</el-select>
<el-date-picker v-model="ticketDateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 240px" />
</div>
<div class="data-table">
<el-table :data="filteredTickets" style="width: 100%">
<el-table-column prop="id" label="工单号" width="130" />
<el-table-column prop="title" label="标题" min-width="200" show-overflow-tooltip />
<el-table-column prop="dbType" label="数据库" width="90">
<template #default="scope"><span :class="'tag tag-' + scope.row.dbType">{{ scope.row.dbType.toUpperCase() }}</span></template>
</el-table-column>
<el-table-column prop="instance" label="实例" width="140" show-overflow-tooltip />
<el-table-column prop="applicant" label="申请人" width="100" />
<el-table-column prop="status" label="状态" width="100">
<template #default="scope"><span :class="'status-tag status-' + scope.row.status">{{ getStatusText(scope.row.status) }}</span></template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" width="150" />
<el-table-column label="操作" width="140" fixed="right">
<template #default="scope">
<el-button type="primary" link size="small" @click="viewTicket(scope.row)">查看</el-button>
<el-button v-if="scope.row.status === 'pending' && canApprove" type="success" link size="small">审批</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<span style="color: var(--text-muted); font-size: 13px;">共 {{ filteredTickets.length }} 条记录</span>
<el-pagination background layout="prev, pager, next" :total="filteredTickets.length" :page-size="10" />
</div>
</div>
</div>
<!-- 查询审计 -->
<div v-if="currentView === 'queries'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>查询审计</h1><p>记录所有 SQL 查询操作,支持敏感数据脱敏和性能分析</p></div>
<div class="page-actions">
<button class="btn-secondary" @click="exportQueryLog"><i class="fas fa-download"></i> 导出</button>
<button class="btn-primary" @click="showNewQuery = true"><i class="fas fa-plus"></i> 新建查询</button>
</div>
</div>
<div class="filter-bar">
<el-input v-model="querySearch" placeholder="搜索 SQL 关键词/用户" style="width: 260px" />
<el-select v-model="queryDbType" placeholder="数据库" style="width: 120px">
<el-option label="全部" value="" /><el-option label="MySQL" value="mysql" />
<el-option label="PostgreSQL" value="postgres" /><el-option label="Redis" value="redis" />
</el-select>
<el-select v-model="queryStatus" placeholder="执行状态" style="width: 120px">
<el-option label="全部" value="" /><el-option label="成功" value="success" />
<el-option label="失败" value="failed" /><el-option label="慢查询" value="slow" />
</el-select>
<el-select v-model="queryTimeRange" placeholder="时间范围" style="width: 140px">
<el-option label="最近1小时" value="1h" /><el-option label="最近24小时" value="24h" />
<el-option label="最近7天" value="7d" /><el-option label="最近30天" value="30d" />
</el-select>
</div>
<div class="data-table">
<el-table :data="filteredQueries" style="width: 100%" @row-click="viewQueryDetail">
<el-table-column prop="id" label="查询ID" width="120" />
<el-table-column prop="sql" label="SQL 语句" min-width="300" show-overflow-tooltip>
<template #default="scope">
<code style="font-family: monospace; font-size: 12px; color: var(--accent-blue);">{{ scope.row.sqlPreview }}</code>
</template>
</el-table-column>
<el-table-column prop="dbType" label="数据库" width="90">
<template #default="scope"><span :class="'tag tag-' + scope.row.dbType">{{ scope.row.dbType.toUpperCase() }}</span></template>
</el-table-column>
<el-table-column prop="user" label="执行用户" width="100" />
<el-table-column prop="executeTime" label="执行时长" width="100">
<template #default="scope">
<span :class="scope.row.executeTime > 1000 ? 'text-danger' : ''">{{ scope.row.executeTime }}ms</span>
</template>
</el-table-column>
<el-table-column prop="rowCount" label="返回行数" width="90" />
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<el-tag :type="scope.row.status === 'success' ? 'success' : scope.row.status === 'slow' ? 'warning' : 'danger'" size="small">
{{ scope.row.status === 'success' ? '成功' : scope.row.status === 'slow' ? '慢查询' : '失败' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="hasSensitive" label="敏感数据" width="90">
<template #default="scope">
<span v-if="scope.row.hasSensitive" class="sensitive-badge"><i class="fas fa-exclamation"></i> 已脱敏</span>
<span v-else style="color: var(--text-muted); font-size: 12px;">-</span>
</template>
</el-table-column>
<el-table-column prop="timestamp" label="执行时间" width="150" />
</el-table>
<div class="pagination-wrapper">
<span style="color: var(--text-muted); font-size: 13px;">共 {{ filteredQueries.length }} 条记录</span>
<el-pagination background layout="prev, pager, next" :total="filteredQueries.length" :page-size="10" />
</div>
</div>
</div>
<!-- 角色权限 -->
<div v-if="currentView === 'roles'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>角色权限管理</h1><p>配置 RBAC 角色权限,控制用户对系统功能和数据的访问</p></div>
<div class="page-actions">
<button class="btn-primary" @click="showAddRole = true"><i class="fas fa-plus"></i> 新建角色</button>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px;">
<div v-for="role in roles" :key="role.id" :class="['role-card', { active: selectedRole && selectedRole.id === role.id }]" @click="selectRole(role)">
<div class="role-header">
<div :class="['role-icon', role.iconClass]"><i :class="role.icon"></i></div>
<div>
<div class="role-name">{{ role.name }}</div>
<div class="role-desc">{{ role.description }}</div>
</div>
</div>
<div class="role-stats">
<div class="role-stat"><strong>{{ role.userCount }}</strong>用户</div>
<div class="role-stat"><strong>{{ role.permissionCount }}</strong>权限</div>
</div>
</div>
</div>
<div v-if="selectedRole" class="permission-tree">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
<h3 style="font-size: 16px; color: var(--text-primary);">{{ selectedRole.name }} - 权限配置</h3>
<el-button type="primary" size="small" @click="saveRolePermissions"><i class="fas fa-save"></i> 保存权限</el-button>
</div>
<el-tree
ref="permissionTree"
:data="permissionTreeData"
show-checkbox
node-key="id"
:default-checked-keys="selectedRole.permissions"
:props="{ label: 'label', children: 'children' }"
style="background: transparent; color: var(--text-primary);"
/>
</div>
</div>
<!-- 用户管理 -->
<div v-if="currentView === 'users'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>用户管理</h1><p>管理系统用户,分配角色和数据库访问权限</p></div>
<div class="page-actions">
<button class="btn-primary" @click="showAddUser = true"><i class="fas fa-plus"></i> 新建用户</button>
</div>
</div>
<div class="filter-bar">
<el-input v-model="userSearch" placeholder="搜索用户名/邮箱" style="width: 260px" />
<el-select v-model="userRoleFilter" placeholder="角色" style="width: 140px">
<el-option label="全部角色" value="" />
<el-option v-for="role in roles" :key="role.id" :label="role.name" :value="role.id" />
</el-select>
<el-select v-model="userStatusFilter" placeholder="状态" style="width: 120px">
<el-option label="全部" value="" /><el-option label="启用" value="active" />
<el-option label="禁用" value="disabled" />
</el-select>
</div>
<div class="data-table">
<el-table :data="filteredUsers" style="width: 100%">
<el-table-column prop="username" label="用户名" width="120" />
<el-table-column prop="email" label="邮箱" min-width="180" show-overflow-tooltip />
<el-table-column prop="role" label="角色" width="120">
<template #default="scope">
<el-tag :type="getRoleType(scope.row.role)" size="small">{{ scope.row.roleName }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="department" label="部门" width="120" />
<el-table-column prop="lastLogin" label="最后登录" width="150" />
<el-table-column prop="status" label="状态" width="80">
<template #default="scope">
<el-switch v-model="scope.row.status" active-value="active" inactive-value="disabled" />
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="scope">
<el-button type="primary" link size="small" @click="editUser(scope.row)">编辑</el-button>
<el-button type="success" link size="small" @click="configUserPermission(scope.row)">权限</el-button>
<el-button type="danger" link size="small" @click="deleteUser(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination-wrapper">
<span style="color: var(--text-muted); font-size: 13px;">共 {{ filteredUsers.length }} 条记录</span>
<el-pagination background layout="prev, pager, next" :total="filteredUsers.length" :page-size="10" />
</div>
</div>
</div>
<!-- 数据权限 -->
<div v-if="currentView === 'data-permission'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>数据权限</h1><p>配置用户对数据库、表、字段的访问权限,支持行级权限控制</p></div>
<div class="page-actions">
<button class="btn-primary" @click="showAddDataPermission = true"><i class="fas fa-plus"></i> 添加权限</button>
</div>
</div>
<div class="data-table">
<el-table :data="dataPermissions" style="width: 100%">
<el-table-column prop="user" label="用户" width="120" />
<el-table-column prop="instance" label="实例" width="150" show-overflow-tooltip />
<el-table-column prop="database" label="数据库" width="120" />
<el-table-column prop="table" label="表" width="150" />
<el-table-column prop="columns" label="字段权限" min-width="200">
<template #default="scope">
<div v-if="scope.row.columns === '*'">
<el-tag size="small" type="success">全部字段</el-tag>
</div>
<div v-else>
<el-tag v-for="col in scope.row.columns.split(',')" :key="col" size="small" style="margin-right: 4px; margin-bottom: 2px;">{{ col }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="rowFilter" label="行级过滤" width="150">
<template #default="scope">
<el-tag v-if="scope.row.rowFilter" type="warning" size="small">{{ scope.row.rowFilter }}</el-tag>
<span v-else style="color: var(--text-muted);">-</span>
</template>
</el-table-column>
<el-table-column prop="operations" label="操作权限" width="150">
<template #default="scope">
<el-tag v-for="op in scope.row.operations" :key="op" size="small" style="margin-right: 4px;">{{ op }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default>
<el-button type="primary" link size="small">编辑</el-button>
<el-button type="danger" link size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 审核规则 -->
<div v-if="currentView === 'audit-rules'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>SQL 审核规则</h1><p>配置 SQL 审核规则,确保数据变更安全合规</p></div>
<div class="page-actions">
<button class="btn-secondary"><i class="fas fa-file-import"></i> 导入</button>
<button class="btn-primary" @click="showAddRule = true"><i class="fas fa-plus"></i> 添加规则</button>
</div>
</div>
<div class="data-table">
<el-table :data="auditRules" style="width: 100%">
<el-table-column prop="id" label="规则ID" width="90" />
<el-table-column prop="name" label="规则名称" min-width="200" show-overflow-tooltip />
<el-table-column prop="level" label="级别" width="80">
<template #default="scope">
<el-tag :type="scope.row.level === 'error' ? 'danger' : scope.row.level === 'warning' ? 'warning' : 'info'" size="small">
{{ scope.row.level === 'error' ? '错误' : scope.row.level === 'warning' ? '警告' : '提示' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="dbTypes" label="适用数据库" width="160">
<template #default="scope">
<span v-for="db in scope.row.dbTypes" :key="db" :class="'tag tag-' + db" style="margin-right: 4px;">{{ db.toUpperCase() }}</span>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="250" show-overflow-tooltip />
<el-table-column prop="enabled" label="状态" width="70">
<template #default="scope"><el-switch v-model="scope.row.enabled" /></template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default>
<el-button type="primary" link size="small">编辑</el-button>
<el-button type="danger" link size="small">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 审批流程 -->
<div v-if="currentView === 'workflow'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>审批流程配置</h1><p>配置多级审批流程,支持会签、或签、条件审批</p></div>
<div class="page-actions">
<button class="btn-primary" @click="showAddWorkflow = true"><i class="fas fa-plus"></i> 新建流程</button>
</div>
</div>
<div class="data-table">
<el-table :data="workflows" style="width: 100%">
<el-table-column prop="name" label="流程名称" min-width="180" show-overflow-tooltip />
<el-table-column prop="dbTypes" label="适用数据库" width="180">
<template #default="scope">
<span v-for="db in scope.row.dbTypes" :key="db" :class="'tag tag-' + db" style="margin-right: 4px;">{{ db.toUpperCase() }}</span>
</template>
</el-table-column>
<el-table-column prop="steps" label="审批步骤" width="100">
<template #default="scope">{{ scope.row.steps }} 级审批</template>
</el-table-column>
<el-table-column prop="mode" label="审批模式" width="100">
<template #default="scope">
<el-tag :type="scope.row.mode === 'any' ? 'success' : 'primary'" size="small">{{ scope.row.mode === 'any' ? '或签' : '会签' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="description" label="描述" min-width="200" show-overflow-tooltip />
<el-table-column prop="enabled" label="状态" width="70">
<template #default="scope"><el-switch v-model="scope.row.enabled" /></template>
</el-table-column>
<el-table-column label="操作" width="150" fixed="right">
<template #default>
<el-button type="primary" link size="small">编辑</el-button>
<el-button type="success" link size="small">预览</el-button>
</template>
</el-table-column>
</el-table>
</div>
</div>
<!-- 通知渠道 -->
<div v-if="currentView === 'notifications'" class="fade-enter-active">
<div class="page-header">
<div class="page-title-section"><h1>通知渠道配置</h1><p>配置多渠道通知,支持飞书、钉钉、企业微信、Telegram</p></div>
<div class="page-actions">
<button class="btn-secondary" @click="testAllChannels"><i class="fas fa-paper-plane"></i> 测试全部</button>
</div>
</div>
<div class="channel-grid">
<div v-for="channel in notificationChannels" :key="channel.id" :class="['channel-card', { enabled: channel.enabled }]" @click="configureChannel(channel)">
<div :class="['channel-icon', channel.type]"><i :class="channel.icon"></i></div>
<div class="channel-info">
<div class="channel-name">{{ channel.name }}</div>
<div class="channel-desc">{{ channel.description }}</div>
</div>
<span :class="['channel-status', channel.enabled ? 'enabled' : 'disabled']">{{ channel.enabled ? '已启用' : '未启用' }}</span>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- 新建工单对话框 -->
<el-dialog v-model="showNewTicket" title="新建数据变更工单" width="800px" destroy-on-close>
<el-form :model="newTicketForm" label-position="top">
<el-form-item label="工单标题"><el-input v-model="newTicketForm.title" placeholder="请输入工单标题" /></el-form-item>
<el-row :gutter="20">
<el-col :span="12">
<el-form-item label="数据库类型">
<el-select v-model="newTicketForm.dbType" placeholder="选择数据库类型" style="width: 100%">
<el-option label="MySQL" value="mysql" /><el-option label="PostgreSQL" value="postgres" />
<el-option label="Redis" value="redis" /><el-option label="MongoDB" value="mongo" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="目标实例">
<el-select v-model="newTicketForm.instance" placeholder="选择目标实例" style="width: 100%">
<el-option label="生产 MySQL 主库" value="mysql-prod-01" />
<el-option label="测试 MySQL 从库" value="mysql-test-01" />
</el-select>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="SQL 语句">
<div class="sql-editor-container">
<div class="sql-editor-header">
<div class="sql-editor-tabs">
<div :class="['sql-editor-tab', { active: sqlTab === 'change' }]" @click="sqlTab = 'change'"><i class="fas fa-file-code"></i><span>变更 SQL</span></div>
<div :class="['sql-editor-tab', { active: sqlTab === 'rollback' }]" @click="sqlTab = 'rollback'"><i class="fas fa-undo"></i><span>回滚 SQL</span></div>
</div>
</div>
<textarea class="sql-editor-area" v-model="newTicketForm.sql" placeholder="-- 请输入 SQL 语句
-- 支持多条 SQL,使用分号分隔
UPDATE users SET status = 'active' WHERE id = 1;
INSERT INTO logs (action, user_id) VALUES ('update', 1);"></textarea>
<div class="sql-editor-footer">
<span><i class="fas fa-info-circle"></i> 已输入 {{ newTicketForm.sql?.length || 0 }} 个字符</span>
<span><i class="fas fa-magic"></i> 按 Ctrl+Enter 进行 SQL 审核</span>
</div>
</div>
</el-form-item>
<el-form-item label="变更原因">
<el-input v-model="newTicketForm.reason" type="textarea" :rows="3" placeholder="请描述本次数据变更的原因和背景" />
</el-form-item>
</el-form>
<div class="audit-result-panel">
<div class="audit-result-header">
<div class="audit-result-title"><i class="fas fa-shield-alt"></i><span>SQL 审核结果</span></div>
<div class="audit-score"><i class="fas fa-check-circle"></i><span>92 分</span></div>
</div>
<div class="audit-result-list">
<div class="audit-result-item">
<div class="audit-result-icon error"><i class="fas fa-times"></i></div>
<div class="audit-result-content">
<div class="audit-result-rule">禁止无 WHERE 条件的 UPDATE/DELETE</div>
<div class="audit-result-desc">检测到 UPDATE 语句缺少 WHERE 条件</div>
<div class="audit-result-suggestion">建议: 添加 WHERE 条件限制影响范围</div>
</div>
</div>
<div class="audit-result-item">
<div class="audit-result-icon warning"><i class="fas fa-exclamation"></i></div>
<div class="audit-result-content">
<div class="audit-result-rule">大表变更警告</div>
<div class="audit-result-desc">目标表 users 数据量超过 100 万行</div>
<div class="audit-result-suggestion">建议: 考虑分批执行或使用 pt-online-schema-change</div>
</div>
</div>
<div class="audit-result-item">
<div class="audit-result-icon success"><i class="fas fa-check"></i></div>
<div class="audit-result-content">
<div class="audit-result-rule">SQL 语法检查通过</div>
<div class="audit-result-desc">SQL 语法正确,可以正常解析</div>
</div>
</div>
</div>
</div>
<template #footer>
<el-button @click="showNewTicket = false">取消</el-button>
<el-button type="primary" @click="submitTicket">提交审核</el-button>
</template>
</el-dialog>
<!-- 查看工单对话框 -->
<el-dialog v-model="showViewTicket" title="工单详情" width="900px">
<div v-if="currentTicket" class="ticket-detail">
<div class="workflow-timeline">
<div class="workflow-step">
<div class="workflow-step-icon completed"><i class="fas fa-paper-plane"></i></div>
<div class="workflow-step-label">提交</div><div class="workflow-step-time">09:30</div>
</div>
<div class="workflow-connector completed"></div>
<div class="workflow-step">
<div class="workflow-step-icon completed"><i class="fas fa-check"></i></div>
<div class="workflow-step-label">一级审批</div><div class="workflow-step-time">10:15</div>
</div>
<div class="workflow-connector completed"></div>
<div class="workflow-step">
<div class="workflow-step-icon active"><i class="fas fa-clock"></i></div>
<div class="workflow-step-label">二级审批</div><div class="workflow-step-time">进行中</div>
</div>
<div class="workflow-connector"></div>
<div class="workflow-step">
<div class="workflow-step-icon pending"><i class="fas fa-play"></i></div>
<div class="workflow-step-label">执行</div><div class="workflow-step-time">待执行</div>
</div>
</div>
<el-descriptions :column="2" border>
<el-descriptions-item label="工单号">{{ currentTicket.id }}</el-descriptions-item>
<el-descriptions-item label="工单标题">{{ currentTicket.title }}</el-descriptions-item>
<el-descriptions-item label="数据库类型"><span :class="'tag tag-' + currentTicket.dbType">{{ currentTicket.dbType.toUpperCase() }}</span></el-descriptions-item>
<el-descriptions-item label="目标实例">{{ currentTicket.instance }}</el-descriptions-item>
<el-descriptions-item label="申请人">{{ currentTicket.applicant }}</el-descriptions-item>
<el-descriptions-item label="创建时间">{{ currentTicket.createTime }}</el-descriptions-item>
<el-descriptions-item label="当前状态"><span :class="'status-tag status-' + currentTicket.status">{{ getStatusText(currentTicket.status) }}</span></el-descriptions-item>
<el-descriptions-item label="优先级"><el-tag type="danger" size="small">高</el-tag></el-descriptions-item>
</el-descriptions>
<div style="margin-top: 20px;">
<h4 style="margin-bottom: 12px; font-size: 14px; color: var(--text-primary);">SQL 内容</h4>
<div class="sql-editor-container">
<pre style="padding: 16px; margin: 0; font-family: 'JetBrains Mono', monospace; font-size: 13px; color: var(--text-primary); background: var(--bg-primary); overflow-x: auto;">UPDATE users SET status = 'active', updated_at = NOW()
WHERE id IN (1001, 1002, 1003)
AND status != 'active';</pre>
</div>
</div>
<div style="margin-top: 20px;">
<h4 style="margin-bottom: 12px; font-size: 14px; color: var(--text-primary);">审批记录</h4>
<el-timeline>
<el-timeline-item type="primary" :hollow="true">
<div style="font-size: 13px; color: var(--text-primary);"><strong>张三</strong> 提交工单</div>
<div style="font-size: 12px; color: var(--text-muted);">2024-01-15 09:30:25</div>
</el-timeline-item>
<el-timeline-item type="success">
<div style="font-size: 13px; color: var(--text-primary);"><strong>李四</strong> 通过一级审批</div>
<div style="font-size: 12px; color: var(--text-muted);">2024-01-15 10:15:38</div>
<div style="font-size: 12px; color: var(--text-secondary); margin-top: 4px;">审批意见: SQL 符合规范,同意执行</div>
</el-timeline-item>
<el-timeline-item type="primary" :hollow="true">
<div style="font-size: 13px; color: var(--text-primary);"><strong>王五</strong> 二级审批中</div>
<div style="font-size: 12px; color: var(--text-muted);">等待审批...</div>
</el-timeline-item>
</el-timeline>
</div>
</div>
<template #footer>
<el-button @click="showViewTicket = false">关闭</el-button>
<el-button v-if="currentTicket && currentTicket.status === 'pending' && canApprove" type="success">通过</el-button>
<el-button v-if="currentTicket && currentTicket.status === 'pending' && canApprove" type="danger">拒绝</el-button>
</template>
</el-dialog>
<!-- 查询详情对话框 -->
<el-dialog v-model="showQueryDetail" title="查询详情" width="900px">
<div v-if="currentQuery" class="query-detail-panel">
<div class="query-detail-header">
<div style="display: flex; align-items: center; gap: 12px;">
<span :class="'tag tag-' + currentQuery.dbType">{{ currentQuery.dbType.toUpperCase() }}</span>
<span style="font-size: 14px; color: var(--text-secondary);">查询ID: {{ currentQuery.id }}</span>
</div>
<el-tag :type="currentQuery.status === 'success' ? 'success' : currentQuery.status === 'slow' ? 'warning' : 'danger'" size="small">
{{ currentQuery.status === 'success' ? '执行成功' : currentQuery.status === 'slow' ? '慢查询' : '执行失败' }}
</el-tag>
</div>
<div class="query-sql-preview">{{ currentQuery.sql }}</div>
<div class="query-metrics">
<div class="query-metric">
<div :class="['query-metric-value', currentQuery.executeTime > 1000 ? 'slow' : '']">{{ currentQuery.executeTime }}ms</div>
<div class="query-metric-label">执行时长</div>
</div>
<div class="query-metric">
<div class="query-metric-value">{{ currentQuery.rowCount }}</div>
<div class="query-metric-label">返回行数</div>
</div>
<div class="query-metric">
<div class="query-metric-value">{{ currentQuery.scanRows }}</div>
<div class="query-metric-label">扫描行数</div>
</div>
<div class="query-metric">
<div class="query-metric-value">{{ currentQuery.affectedRows }}</div>
<div class="query-metric-label">影响行数</div>
</div>
</div>
</div>
<div v-if="currentQuery && currentQuery.resultSet" class="result-set-panel" style="margin-top: 20px;">
<div class="result-set-header">
<span style="font-size: 14px; font-weight: 600; color: var(--text-primary);">查询结果</span>
<span v-if="currentQuery.hasSensitive" class="sensitive-badge"><i class="fas fa-exclamation"></i> 敏感数据已脱敏</span>
</div>
<el-table :data="currentQuery.resultSet" style="width: 100%" size="small" max-height="300">
<el-table-column v-for="col in currentQuery.resultColumns" :key="col.prop" :prop="col.prop" :label="col.label" min-width="120">
<template #default="scope">
<span v-if="col.sensitive" class="masked-data">{{ maskData(scope.row[col.prop]) }}</span>
<span v-else>{{ scope.row[col.prop] }}</span>
</template>
</el-table-column>
</el-table>
</div>
<template #footer>
<el-button @click="showQueryDetail = false">关闭</el-button>
<el-button type="primary"><i class="fas fa-copy"></i> 复制 SQL</el-button>
</template>
</el-dialog>
<!-- 配置通知渠道对话框 -->
<el-dialog v-model="showChannelConfig" :title="'配置 ' + currentChannel?.name" width="500px">
<el-form :model="channelConfigForm" label-position="top">
<el-form-item label="Webhook URL"><el-input v-model="channelConfigForm.webhook" placeholder="请输入 Webhook 地址" /></el-form-item>
<el-form-item label="密钥 (Secret)"><el-input v-model="channelConfigForm.secret" type="password" placeholder="请输入密钥" show-password /></el-form-item>
<el-form-item label="通知事件">
<el-checkbox-group v-model="channelConfigForm.events">
<el-checkbox label="ticket_created">工单创建</el-checkbox>
<el-checkbox label="ticket_approved">审批通过</el-checkbox>
<el-checkbox label="ticket_rejected">审批拒绝</el-checkbox>
<el-checkbox label="ticket_executed">执行完成</el-checkbox>
<el-checkbox label="ticket_failed">执行失败</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="@提醒用户">
<el-select v-model="channelConfigForm.mentionUsers" multiple placeholder="选择需要提醒的用户" style="width: 100%">
<el-option label="审批人" value="approvers" />
<el-option label="申请人" value="applicant" />
<el-option label="执行人" value="executor" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showChannelConfig = false">取消</el-button>
<el-button type="success" @click="testChannel"><i class="fas fa-paper-plane"></i> 测试</el-button>
<el-button type="primary" @click="saveChannelConfig">保存配置</el-button>
</template>
</el-dialog>
</div>
<script>
const { createApp, ref, reactive, computed, onMounted, nextTick, watch } = Vue;
const { ElMessage, ElMessageBox } = ElementPlus;
createApp({
setup() {
// 登录状态
const isLoggedIn = ref(false);
const loginLoading = ref(false);
const loginForm = reactive({ username: '', password: '' });
const currentUser = reactive({ name: 'Administrator', avatar: 'A', role: 'admin', roleName: '超级管理员' });
// 侧边栏
const sidebarCollapsed = ref(false);
const currentView = ref('dashboard');
const showUserMenu = ref(false);
const showNotifications = ref(false);
const unreadNotifications = ref(3);
// 搜索和筛选
const searchQuery = ref('');
const filterStatus = ref('');
const filterEnv = ref('');
const ticketSearch = ref('');
const ticketStatus = ref('');
const ticketDbType = ref('');
const ticketDateRange = ref(null);
const querySearch = ref('');
const queryDbType = ref('');
const queryStatus = ref('');
const queryTimeRange = ref('');
const userSearch = ref('');
const userRoleFilter = ref('');
const userStatusFilter = ref('');
const chartPeriod = ref('7d');
const sqlTab = ref('change');
// 对话框显示状态
const showNewTicket = ref(false);
const showViewTicket = ref(false);
const showAddInstance = ref(false);
const showAddRule = ref(false);
const showAddWorkflow = ref(false);
const showChannelConfig = ref(false);
const showQueryDetail = ref(false);
const showAddRole = ref(false);
const showAddUser = ref(false);
const showAddDataPermission = ref(false);
const showNewQuery = ref(false);
// 当前选中的数据
const currentTicket = ref(null);
const currentChannel = ref(null);
const currentQuery = ref(null);
const selectedRole = ref(null);
// 表单数据
const newTicketForm = reactive({ title: '', dbType: 'mysql', instance: '', sql: '', reason: '' });
const channelConfigForm = reactive({ webhook: '', secret: '', events: ['ticket_created', 'ticket_executed', 'ticket_failed'], mentionUsers: ['approvers'] });
// 权限控制
const canApprove = computed(() => currentUser.role === 'admin' || currentUser.role === 'dba');
// Mock 数据 - 待处理工单
const pendingTickets = ref([
{ id: 'TK-20240115-001', title: '更新用户状态字段', dbType: 'mysql', applicant: '张三', status: 'pending', createTime: '2024-01-15 09:30' },
{ id: 'TK-20240115-002', title: '清理过期日志数据', dbType: 'postgres', applicant: '李四', status: 'reviewing', createTime: '2024-01-15 10:15' },
{ id: 'TK-20240115-003', title: 'Redis 缓存预热', dbType: 'redis', applicant: '王五', status: 'pending', createTime: '2024-01-15 11:00' },
{ id: 'TK-20240114-008', title: '订单表索引优化', dbType: 'mysql', applicant: '赵六', status: 'reviewing', createTime: '2024-01-14 16:45' },
]);
// Mock 数据 - MySQL 实例
const mysqlInstances = ref([
{ id: 1, name: '生产主库', host: '192.168.1.10', port: 3306, status: 'online', connections: 245, qps: 3250, latency: 2.5 },
{ id: 2, name: '生产从库-01', host: '192.168.1.11', port: 3306, status: 'online', connections: 189, qps: 2800, latency: 3.1 },
{ id: 3, name: '生产从库-02', host: '192.168.1.12', port: 3306, status: 'online', connections: 192, qps: 2750, latency: 2.8 },
{ id: 4, name: '测试库', host: '192.168.2.10', port: 3306, status: 'warning', connections: 45, qps: 120, latency: 15.2 },
{ id: 5, name: '开发库', host: '192.168.3.10', port: 3306, status: 'online', connections: 23, qps: 80, latency: 1.2 },
{ id: 6, name: '归档库', host: '192.168.1.50', port: 3306, status: 'offline', connections: 0, qps: 0, latency: 0 },
]);
// Mock 数据 - 工单列表
const tickets = ref([
{ id: 'TK-20240115-001', title: '更新用户状态字段', dbType: 'mysql', instance: '生产主库', applicant: '张三', status: 'pending', createTime: '2024-01-15 09:30' },
{ id: 'TK-20240115-002', title: '清理过期日志数据', dbType: 'postgres', instance: '日志库', applicant: '李四', status: 'reviewing', createTime: '2024-01-15 10:15' },
{ id: 'TK-20240115-003', title: 'Redis 缓存预热', dbType: 'redis', instance: 'Redis集群', applicant: '王五', status: 'pending', createTime: '2024-01-15 11:00' },
{ id: 'TK-20240114-005', title: '用户表添加索引', dbType: 'mysql', instance: '生产主库', applicant: '赵六', status: 'approved', createTime: '2024-01-14 14:20' },
{ id: 'TK-20240114-006', title: '订单数据归档', dbType: 'mysql', instance: '归档库', applicant: '钱七', status: 'executing', createTime: '2024-01-14 15:30' },
{ id: 'TK-20240114-007', title: '配置表更新', dbType: 'redis', instance: 'Redis集群', applicant: '孙八', status: 'success', createTime: '2024-01-14 16:00' },
{ id: 'TK-20240114-008', title: '订单表索引优化', dbType: 'mysql', instance: '生产主库', applicant: '周九', status: 'failed', createTime: '2024-01-14 16:45' },
{ id: 'TK-20240113-001', title: '用户权限调整', dbType: 'postgres', instance: '权限库', applicant: '吴十', status: 'rejected', createTime: '2024-01-13 09:00' },
]);
// Mock 数据 - 查询审计
const queries = ref([
{ id: 'Q-20240115-001', sql: 'SELECT * FROM users WHERE status = "active"', sqlPreview: 'SELECT * FROM users...', dbType: 'mysql', user: '张三', executeTime: 125, rowCount: 1523, scanRows: 50000, affectedRows: 0, status: 'success', hasSensitive: true, timestamp: '2024-01-15 10:30:25', resultColumns: [{ prop: 'id', label: 'ID' }, { prop: 'name', label: '姓名' }, { prop: 'email', label: '邮箱', sensitive: true }, { prop: 'phone', label: '电话', sensitive: true }], resultSet: [{ id: 1, name: '张三', email: 'zhang***@email.com', phone: '138****8888' }, { id: 2, name: '李四', email: 'li***@email.com', phone: '139****9999' }] },
{ id: 'Q-20240115-002', sql: 'SELECT COUNT(*) FROM orders WHERE created_at > "2024-01-01"', sqlPreview: 'SELECT COUNT(*) FROM orders...', dbType: 'mysql', user: '李四', executeTime: 2500, rowCount: 1, scanRows: 1000000, affectedRows: 0, status: 'slow', hasSensitive: false, timestamp: '2024-01-15 10:28:15' },
{ id: 'Q-20240115-003', sql: 'UPDATE users SET last_login = NOW() WHERE id = 123', sqlPreview: 'UPDATE users SET...', dbType: 'mysql', user: '王五', executeTime: 45, rowCount: 0, scanRows: 1, affectedRows: 1, status: 'success', hasSensitive: false, timestamp: '2024-01-15 10:25:08' },
{ id: 'Q-20240115-004', sql: 'SELECT * FROM sensitive_data WHERE user_id = 456', sqlPreview: 'SELECT * FROM sensitive_data...', dbType: 'postgres', user: '赵六', executeTime: 89, rowCount: 5, scanRows: 100, affectedRows: 0, status: 'success', hasSensitive: true, timestamp: '2024-01-15 10:20:33' },
{ id: 'Q-20240115-005', sql: 'DELETE FROM temp_logs WHERE created_at < "2024-01-01"', sqlPreview: 'DELETE FROM temp_logs...', dbType: 'mysql', user: '钱七', executeTime: 5000, rowCount: 0, scanRows: 500000, affectedRows: 0, status: 'failed', hasSensitive: false, timestamp: '2024-01-15 10:15:42' },
{ id: 'Q-20240115-006', sql: 'GET user:session:123', sqlPreview: 'GET user:session:123', dbType: 'redis', user: '孙八', executeTime: 2, rowCount: 1, scanRows: 1, affectedRows: 0, status: 'success', hasSensitive: false, timestamp: '2024-01-15 10:10:18' },
]);
// Mock 数据 - 审核规则
const auditRules = ref([
{ id: 'R001', name: '禁止无 WHERE 条件的 UPDATE/DELETE', level: 'error', dbTypes: ['mysql', 'postgres'], description: 'UPDATE 和 DELETE 语句必须包含 WHERE 条件', enabled: true },
{ id: 'R002', name: '禁止 SELECT * 查询', level: 'warning', dbTypes: ['mysql', 'postgres'], description: 'SELECT 语句应明确指定字段名', enabled: true },
{ id: 'R003', name: '大表变更警告', level: 'warning', dbTypes: ['mysql', 'postgres'], description: '影响超过 100 万行的变更需要额外审批', enabled: true },
{ id: 'R004', name: '禁止 DROP 操作', level: 'error', dbTypes: ['mysql', 'postgres', 'mongo'], description: '禁止执行 DROP TABLE/DATABASE 操作', enabled: true },
{ id: 'R005', name: 'SQL 注入检测', level: 'error', dbTypes: ['mysql', 'postgres', 'redis'], description: '检测潜在的 SQL 注入风险', enabled: true },
{ id: 'R006', name: '索引使用检查', level: 'info', dbTypes: ['mysql', 'postgres'], description: '检查查询是否使用了索引', enabled: false },
]);
// Mock 数据 - 审批流程
const workflows = ref([
{ id: 1, name: '生产环境变更流程', dbTypes: ['mysql', 'postgres', 'redis'], steps: 3, mode: 'all', description: '需要 DBA 和架构师双重审批', enabled: true },
{ id: 2, name: '测试环境变更流程', dbTypes: ['mysql', 'postgres', 'redis', 'mongo'], steps: 1, mode: 'any', description: '仅需一级审批', enabled: true },
{ id: 3, name: '紧急变更流程', dbTypes: ['mysql', 'postgres'], steps: 2, mode: 'any', description: '紧急情况下快速审批通道', enabled: true },
{ id: 4, name: '数据查询流程', dbTypes: ['mysql', 'postgres', 'clickhouse'], steps: 1, mode: 'any', description: '敏感数据查询审批', enabled: false },
]);
// Mock 数据 - 通知渠道
const notificationChannels = ref([
{ id: 1, name: '飞书', type: 'feishu', icon: 'fas fa-paper-plane', description: '通过飞书机器人发送通知', enabled: true },
{ id: 2, name: '钉钉', type: 'dingtalk', icon: 'fas fa-bell', description: '通过钉钉机器人发送通知', enabled: true },
{ id: 3, name: '企业微信', type: 'wechat', icon: 'fas fa-comments', description: '通过企业微信机器人发送通知', enabled: false },
{ id: 4, name: 'Telegram', type: 'telegram', icon: 'fab fa-telegram', description: '通过 Telegram Bot 发送通知', enabled: false },
]);
// Mock 数据 - 角色
const roles = ref([
{ id: 'admin', name: '超级管理员', description: '拥有所有权限', icon: 'fas fa-crown', iconClass: 'admin', userCount: 2, permissionCount: 56, permissions: ['dashboard', 'mysql', 'postgres', 'redis', 'mongo', 'clickhouse', 'tickets', 'queries', 'roles', 'users', 'data-permission', 'audit-rules', 'workflow', 'notifications'] },
{ id: 'dba', name: '数据库管理员', description: '管理数据库和审核工单', icon: 'fas fa-database', iconClass: 'dba', userCount: 5, permissionCount: 32, permissions: ['dashboard', 'mysql', 'postgres', 'redis', 'tickets', 'queries', 'audit-rules', 'workflow'] },
{ id: 'developer', name: '开发人员', description: '提交工单和查询数据', icon: 'fas fa-code', iconClass: 'dev', userCount: 28, permissionCount: 12, permissions: ['dashboard', 'tickets', 'queries'] },
{ id: 'guest', name: '访客', description: '只读访问', icon: 'fas fa-eye', iconClass: 'guest', userCount: 8, permissionCount: 4, permissions: ['dashboard', 'queries'] },
]);
// Mock 数据 - 权限树
const permissionTreeData = ref([
{ id: 'dashboard', label: '仪表盘' },
{ id: 'datasource', label: '数据源管理', children: [
{ id: 'mysql', label: 'MySQL' },
{ id: 'postgres', label: 'PostgreSQL' },
{ id: 'redis', label: 'Redis' },
{ id: 'mongo', label: 'MongoDB' },
{ id: 'clickhouse', label: 'ClickHouse' },
]},
{ id: 'tickets', label: '数据变更工单' },
{ id: 'queries', label: '查询审计' },
{ id: 'permission', label: '权限管理', children: [
{ id: 'roles', label: '角色权限' },
{ id: 'users', label: '用户管理' },
{ id: 'data-permission', label: '数据权限' },
]},
{ id: 'system', label: '系统配置', children: [
{ id: 'audit-rules', label: '审核规则' },
{ id: 'workflow', label: '审批流程' },
{ id: 'notifications', label: '通知渠道' },
]},
]);
// Mock 数据 - 用户
const users = ref([
{ id: 1, username: 'admin', email: 'admin@company.com', role: 'admin', roleName: '超级管理员', department: '运维部', lastLogin: '2024-01-15 09:30:00', status: 'active' },
{ id: 2, username: 'zhangsan', email: 'zhangsan@company.com', role: 'dba', roleName: '数据库管理员', department: 'DBA组', lastLogin: '2024-01-15 08:45:00', status: 'active' },
{ id: 3, username: 'lisi', email: 'lisi@company.com', role: 'dba', roleName: '数据库管理员', department: 'DBA组', lastLogin: '2024-01-14 18:20:00', status: 'active' },
{ id: 4, username: 'wangwu', email: 'wangwu@company.com', role: 'developer', roleName: '开发人员', department: '研发部', lastLogin: '2024-01-15 10:15:00', status: 'active' },
{ id: 5, username: 'zhaoliu', email: 'zhaoliu@company.com', role: 'developer', roleName: '开发人员', department: '研发部', lastLogin: '2024-01-14 16:30:00', status: 'disabled' },
]);
// Mock 数据 - 数据权限
const dataPermissions = ref([
{ id: 1, user: 'zhangsan', instance: '生产 MySQL 主库', database: 'user_db', table: 'users', columns: '*', rowFilter: '', operations: ['SELECT', 'INSERT', 'UPDATE'] },
{ id: 2, user: 'lisi', instance: '生产 MySQL 主库', database: 'order_db', table: 'orders', columns: 'id,order_no,amount,status', rowFilter: 'created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)', operations: ['SELECT'] },
{ id: 3, user: 'wangwu', instance: '测试 MySQL', database: 'test_db', table: '*', columns: '*', rowFilter: '', operations: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'] },
]);
// 图表引用
const trendChart = ref(null);
const permissionTree = ref(null);
// 计算属性 - 筛选后的数据
const filteredInstances = computed(() => {
return mysqlInstances.value.filter(instance => {
if (filterStatus.value && instance.status !== filterStatus.value) return false;
if (searchQuery.value && !instance.name.includes(searchQuery.value) && !instance.host.includes(searchQuery.value)) return false;
return true;
});
});
const filteredTickets = computed(() => {
return tickets.value.filter(ticket => {
if (ticketStatus.value && ticket.status !== ticketStatus.value) return false;
if (ticketDbType.value && ticket.dbType !== ticketDbType.value) return false;
if (ticketSearch.value && !ticket.title.includes(ticketSearch.value) && !ticket.id.includes(ticketSearch.value)) return false;
return true;
});
});
const filteredQueries = computed(() => {
return queries.value.filter(query => {
if (queryDbType.value && query.dbType !== queryDbType.value) return false;
if (queryStatus.value && query.status !== queryStatus.value) return false;
if (querySearch.value && !query.sql.includes(querySearch.value) && !query.user.includes(querySearch.value)) return false;
return true;
});
});
const filteredUsers = computed(() => {
return users.value.filter(user => {
if (userRoleFilter.value && user.role !== userRoleFilter.value) return false;
if (userStatusFilter.value && user.status !== userStatusFilter.value) return false;
if (userSearch.value && !user.username.includes(userSearch.value) && !user.email.includes(userSearch.value)) return false;
return true;
});
});
// 登录处理
const handleLogin = () => {
if (!loginForm.username || !loginForm.password) {
ElMessage.warning('请输入用户名和密码');
return;
}
loginLoading.value = true;
setTimeout(() => {
loginLoading.value = false;
isLoggedIn.value = true;
if (loginForm.username === 'developer' || loginForm.username === 'dev') {
currentUser.name = 'Developer';
currentUser.avatar = 'D';
currentUser.role = 'developer';
currentUser.roleName = '开发人员';
}
ElMessage.success('登录成功');
nextTick(() => { initCharts(); });
}, 1000);
};
// 登出
const logout = () => {
ElMessageBox.confirm('确定要退出登录吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
isLoggedIn.value = false;
loginForm.username = '';
loginForm.password = '';
ElMessage.success('已退出登录');
});
};
// 切换侧边栏
const toggleSidebar = () => { sidebarCollapsed.value = !sidebarCollapsed.value; };
// 获取页面标题
const getPageTitle = () => {
const titles = { dashboard: '仪表盘', mysql: 'MySQL 实例', postgres: 'PostgreSQL 实例', redis: 'Redis 实例', mongo: 'MongoDB 实例', clickhouse: 'ClickHouse 实例', tickets: '数据变更', queries: '查询审计', roles: '角色权限', users: '用户管理', 'data-permission': '数据权限', 'audit-rules': '审核规则', workflow: '审批流程', notifications: '通知渠道' };
return titles[currentView.value] || '仪表盘';
};
// 获取状态文本
const getStatusText = (status) => {
const statusMap = { pending: '待审批', reviewing: '审批中', approved: '已通过', rejected: '已拒绝', executing: '执行中', success: '执行成功', failed: '执行失败', rollback: '已回滚' };
return statusMap[status] || status;
};
// 获取角色类型
const getRoleType = (role) => {
const typeMap = { admin: 'danger', dba: 'primary', developer: 'success', guest: 'info' };
return typeMap[role] || 'info';
};
// 查看工单
const viewTicket = (ticket) => { currentTicket.value = ticket; showViewTicket.value = true; };
// 查看查询详情
const viewQueryDetail = (row) => { currentQuery.value = row; showQueryDetail.value = true; };
// 数据脱敏
const maskData = (value) => {
if (!value) return '***';
if (value.includes('@')) {
const [name, domain] = value.split('@');
return name.substring(0, 3) + '***@' + domain;
}
if (value.length > 7) {
return value.substring(0, 3) + '****' + value.substring(value.length - 4);
}
return '***';
};
// 提交工单
const submitTicket = () => {
if (!newTicketForm.title || !newTicketForm.sql) {
ElMessage.warning('请填写完整信息');
return;
}
ElMessage.success('工单提交成功,等待审批');
showNewTicket.value = false;
};
// 配置通知渠道
const configureChannel = (channel) => { currentChannel.value = channel; showChannelConfig.value = true; };
// 保存渠道配置
const saveChannelConfig = () => {
if (currentChannel.value) currentChannel.value.enabled = true;
ElMessage.success('配置保存成功');
showChannelConfig.value = false;
};
// 测试渠道
const testChannel = () => { ElMessage.success('测试消息已发送'); };
// 测试全部渠道
const testAllChannels = () => { ElMessage.success('正在测试所有已启用渠道...'); };
// 选择角色
const selectRole = (role) => { selectedRole.value = role; };
// 保存角色权限
const saveRolePermissions = () => { ElMessage.success('角色权限保存成功'); };
// 刷新实例
const refreshInstances = () => { ElMessage.success('实例列表已刷新'); };
// 查看实例
const viewInstance = (instance) => { ElMessage.info(`查看实例: ${instance.name}`); };
// 导出查询日志
const exportQueryLog = () => { ElMessage.success('查询日志导出中...'); };
// 编辑用户
const editUser = (user) => { ElMessage.info(`编辑用户: ${user.username}`); };
// 配置用户权限
const configUserPermission = (user) => { ElMessage.info(`配置权限: ${user.username}`); };
// 删除用户
const deleteUser = (user) => {
ElMessageBox.confirm(`确定要删除用户 ${user.username} 吗?`, '警告', { type: 'warning' }).then(() => {
ElMessage.success('用户已删除');
});
};
// 初始化图表
const initCharts = () => {
if (trendChart.value) {
const chart = echarts.init(trendChart.value);
const option = {
backgroundColor: 'transparent',
tooltip: { trigger: 'axis', backgroundColor: 'rgba(22, 27, 34, 0.95)', borderColor: '#30363d', textStyle: { color: '#f0f6fc' } },
legend: { data: ['提交工单', '执行成功', '执行失败'], textStyle: { color: '#8b949e' }, bottom: 0 },
grid: { left: '3%', right: '4%', bottom: '15%', top: '10%', containLabel: true },
xAxis: { type: 'category', data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'], axisLine: { lineStyle: { color: '#30363d' } }, axisLabel: { color: '#8b949e' } },
yAxis: { type: 'value', axisLine: { show: false }, splitLine: { lineStyle: { color: '#21262d' } }, axisLabel: { color: '#8b949e' } },
series: [
{ name: '提交工单', type: 'line', smooth: true, data: [120, 132, 101, 134, 90, 230, 210], itemStyle: { color: '#58a6ff' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(88, 166, 255, 0.3)' }, { offset: 1, color: 'rgba(88, 166, 255, 0)' }]) } },
{ name: '执行成功', type: 'line', smooth: true, data: [110, 125, 95, 128, 85, 215, 200], itemStyle: { color: '#3fb950' }, areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{ offset: 0, color: 'rgba(63, 185, 80, 0.3)' }, { offset: 1, color: 'rgba(63, 185, 80, 0)' }]) } },
{ name: '执行失败', type: 'line', smooth: true, data: [5, 3, 4, 2, 3, 8, 5], itemStyle: { color: '#f85149' } }
]
};
chart.setOption(option);
}
};
watch(currentView, () => { if (currentView.value === 'dashboard') nextTick(() => initCharts()); });
onMounted(() => { });
return {
isLoggedIn, loginLoading, loginForm, currentUser, sidebarCollapsed, currentView, showUserMenu, showNotifications, unreadNotifications,
searchQuery, filterStatus, filterEnv, ticketSearch, ticketStatus, ticketDbType, ticketDateRange, querySearch, queryDbType, queryStatus, queryTimeRange,
userSearch, userRoleFilter, userStatusFilter, chartPeriod, sqlTab,
showNewTicket, showViewTicket, showAddInstance, showAddRule, showAddWorkflow, showChannelConfig, showQueryDetail, showAddRole, showAddUser, showAddDataPermission, showNewQuery,
currentTicket, currentChannel, currentQuery, selectedRole,
newTicketForm, channelConfigForm, canApprove,
pendingTickets, mysqlInstances, tickets, queries, auditRules, workflows, notificationChannels, roles, permissionTreeData, users, dataPermissions,
trendChart, permissionTree,
filteredInstances, filteredTickets, filteredQueries, filteredUsers,
handleLogin, logout, toggleSidebar, getPageTitle, getStatusText, getRoleType,
viewTicket, viewQueryDetail, maskData, submitTicket, configureChannel, saveChannelConfig, testChannel, testAllChannels,
selectRole, saveRolePermissions, refreshInstances, viewInstance, exportQueryLog, editUser, configUserPermission, deleteUser
};
}
}).use(ElementPlus).mount('#app');
</script>
</body>
</html>