Files
opsxsql/数据审核平台菜单优化/data-audit-platform-demo/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

1304 lines
56 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>数据审核平台 - 演示</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus@2.5.1/dist/index.css">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
/* 登录页 */
.login-page {
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.login-container {
width: 420px;
padding: 40px;
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
}
.login-header {
text-align: center;
margin-bottom: 32px;
}
.login-header h1 {
font-size: 28px;
font-weight: 600;
color: #303133;
margin: 16px 0 8px;
}
.login-header p {
font-size: 14px;
color: #909399;
}
.login-btn { width: 100%; }
.login-footer {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #ebeef5;
text-align: center;
}
.login-footer p {
font-size: 12px;
color: #909399;
margin: 4px 0;
}
/* 主布局 */
.main-layout { height: 100vh; }
.sidebar {
background-color: #001529;
color: #fff;
overflow-y: auto;
}
.logo {
height: 64px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 16px;
border-bottom: 1px solid rgba(255,255,255,0.1);
}
.logo-text {
font-size: 18px;
font-weight: 600;
margin-left: 12px;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0,0,0,0.1);
}
.header-right {
display: flex;
align-items: center;
gap: 20px;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
}
.user-info:hover {
background-color: #f5f7fa;
}
.main-content {
background-color: #f0f2f5;
padding: 20px;
overflow-y: auto;
}
/* 快捷操作 */
.quick-actions {
margin-bottom: 20px;
}
.actions-row {
display: flex;
justify-content: space-around;
padding: 10px 0;
}
.action-item {
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
padding: 12px 24px;
border-radius: 8px;
transition: all 0.3s;
position: relative;
}
.action-item:hover {
background-color: #f5f7fa;
transform: translateY(-2px);
}
.action-icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.action-label {
font-size: 14px;
color: #606266;
}
/* 统计卡片 */
.stats-row { margin-bottom: 20px; }
.stat-card { padding: 16px; }
.stat-content {
display: flex;
align-items: center;
gap: 16px;
}
.stat-icon {
width: 56px;
height: 56px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.stat-icon.mysql { background-color: #4479A1; }
.stat-icon.redis { background-color: #DC382D; }
.stat-icon.workflow { background-color: #409EFF; }
.stat-icon.today { background-color: #67C23A; }
.stat-value {
font-size: 24px;
font-weight: 600;
color: #303133;
line-height: 1;
margin-bottom: 6px;
}
.stat-label {
font-size: 13px;
color: #909399;
}
.stat-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid #ebeef5;
}
.stat-detail {
font-size: 12px;
color: #67C23A;
}
/* 图表 */
.charts-row { margin-bottom: 20px; }
.chart-container { height: 300px; }
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
/* 实例卡片 */
.instance-card {
margin-bottom: 20px;
transition: all 0.3s;
}
.instance-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(0,0,0,0.1);
}
.instance-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.instance-title {
display: flex;
gap: 12px;
}
.title-text h4 {
margin: 0;
font-size: 16px;
font-weight: 600;
cursor: pointer;
color: #409EFF;
}
.title-text h4:hover {
text-decoration: underline;
}
.title-text p {
margin: 4px 0 0;
font-size: 12px;
color: #909399;
}
.instance-metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
}
.metric-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
background-color: #f5f7fa;
border-radius: 4px;
}
.metric-label {
font-size: 12px;
color: #909399;
margin-bottom: 4px;
}
.metric-value {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.instance-actions {
display: flex;
justify-content: center;
gap: 16px;
}
/* SQL 编辑器 */
.editor-container {
border: 1px solid #dcdfe6;
border-radius: 4px;
overflow: hidden;
margin-bottom: 16px;
}
.sql-textarea {
width: 100%;
min-height: 200px;
padding: 16px;
border: none;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.6;
resize: vertical;
}
.sql-textarea:focus {
outline: none;
}
.result-table {
margin-top: 16px;
}
/* Redis 键列表 */
.key-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.key-item:hover {
background-color: #f5f7fa;
}
.key-item.active {
background-color: #ecf5ff;
}
.key-name {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
/* 菜单样式 */
.el-menu {
border-right: none;
}
.el-sub-menu__title:hover {
background-color: #1890ff !important;
}
.el-menu-item:hover {
background-color: #1890ff !important;
}
.el-menu-item.is-active {
background-color: #1890ff !important;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="https://unpkg.com/vue@3.4.15/dist/vue.global.js"></script>
<script src="https://unpkg.com/element-plus@2.5.1/dist/index.full.js"></script>
<script src="https://unpkg.com/element-plus@2.5.1/dist/locale/zh-cn.js"></script>
<script src="https://unpkg.com/vue-router@4.2.5/dist/vue-router.global.js"></script>
<script src="https://unpkg.com/echarts@5.4.3/dist/echarts.min.js"></script>
<script>
const { createApp, ref, computed, onMounted, reactive } = Vue;
const { createRouter, createWebHashHistory } = VueRouter;
// 模拟数据
const mockStats = {
mysql_instances: 3,
mysql_active: 3,
redis_instances: 2,
redis_keys: 150000,
total_workflows: 128,
pending_workflows: 12,
today_workflows: 8,
today_success: 6,
};
const mockMySQLInstances = [
{ id: 1, name: '生产-MySQL主库', host: 'mysql-prod.example.com', port: 3306, database: 'production', environment: 'prod', status: 'active', version: '8.0.32', connections: 85, qps: 1250, latency: 2, description: '生产环境 MySQL 主库' },
{ id: 2, name: '测试-MySQL', host: 'mysql-test.example.com', port: 3306, database: 'test_db', environment: 'test', status: 'active', version: '8.0.32', connections: 12, qps: 45, latency: 1, description: '测试环境 MySQL' },
{ id: 3, name: '开发-MySQL', host: 'mysql-dev.example.com', port: 3306, database: 'dev_db', environment: 'dev', status: 'active', version: '8.0.32', connections: 8, qps: 20, latency: 1, description: '开发环境 MySQL' },
];
const mockRedisInstances = [
{ id: 1, name: '生产-Redis主库', host: 'redis-prod.example.com', port: 6379, environment: 'prod', status: 'active', version: '7.0.12', redis_mode: 'standalone', total_keys: 100000, used_memory_human: '512M', connected_clients: 50, hit_rate: 95 },
{ id: 2, name: '生产-Redis从库', host: 'redis-slave.example.com', port: 6379, environment: 'prod', status: 'active', version: '7.0.12', redis_mode: 'standalone', total_keys: 100000, used_memory_human: '512M', connected_clients: 30, hit_rate: 95 },
];
const mockWorkflows = [
{ id: 1, title: '添加用户表索引', instance_name: '生产-MySQL主库', workflow_type: 'ddl', risk_level: 'safe', risk_score: 10, status: 'success', submitter_name: 'dev1', created_at: '2024-01-15 10:30:00' },
{ id: 2, title: '修复订单数据', instance_name: '生产-MySQL主库', workflow_type: 'dml', risk_level: 'low', risk_score: 25, status: 'pending', submitter_name: 'dev1', created_at: '2024-01-15 09:15:00' },
{ id: 3, title: '创建新表', instance_name: '测试-MySQL', workflow_type: 'ddl', risk_level: 'safe', risk_score: 5, status: 'approved', submitter_name: 'dev2', created_at: '2024-01-14 16:45:00' },
{ id: 4, title: '批量更新用户状态', instance_name: '生产-MySQL主库', workflow_type: 'dml', risk_level: 'medium', risk_score: 55, status: 'rejected', submitter_name: 'dev1', created_at: '2024-01-14 14:20:00' },
{ id: 5, title: '添加订单表字段', instance_name: '生产-MySQL主库', workflow_type: 'ddl', risk_level: 'low', risk_score: 20, status: 'executing', submitter_name: 'dev2', created_at: '2024-01-14 11:00:00' },
];
// 登录组件
const Login = {
template: `
<div class="login-page">
<div class="login-container">
<div class="login-header">
<el-icon size="48" color="#409EFF"><DataLine /></el-icon>
<h1>数据审核平台</h1>
<p>Database Change Management & SQL Audit Platform</p>
</div>
<el-form :model="form" @keyup.enter="handleLogin">
<el-form-item>
<el-input v-model="form.username" placeholder="用户名" size="large" :prefix-icon="User" />
</el-form-item>
<el-form-item>
<el-input v-model="form.password" type="password" placeholder="密码" size="large" :prefix-icon="Lock" show-password />
</el-form-item>
<el-form-item>
<el-button type="primary" size="large" class="login-btn" :loading="loading" @click="handleLogin">登录</el-button>
</el-form-item>
</el-form>
<div class="login-footer">
<p>默认账号: admin / 123456</p>
<p>DBA账号: dba / 123456</p>
<p>开发账号: dev1 / 123456</p>
</div>
</div>
</div>
`,
setup() {
const form = reactive({ username: '', password: '' });
const loading = ref(false);
const router = VueRouter.useRouter();
const handleLogin = () => {
if (!form.username || !form.password) {
ElementPlus.ElMessage.warning('请输入用户名和密码');
return;
}
loading.value = true;
setTimeout(() => {
loading.value = false;
if (form.password === '123456') {
localStorage.setItem('user', JSON.stringify({ username: form.username, role: form.username === 'admin' ? 'admin' : form.username === 'dba' ? 'dba' : 'developer' }));
ElementPlus.ElMessage.success('登录成功');
router.push('/');
} else {
ElementPlus.ElMessage.error('密码错误,默认密码: 123456');
}
}, 500);
};
return { form, loading, handleLogin, User: 'User', Lock: 'Lock' };
}
};
// 仪表盘组件
const Dashboard = {
template: `
<div>
<!-- 快捷操作 -->
<el-card shadow="hover" class="quick-actions">
<div class="actions-row">
<div class="action-item" @click="$router.push('/audit/workflows/create')">
<div class="action-icon" style="background-color: #409EFF;">
<el-icon size="24" color="#fff"><DocumentChecked /></el-icon>
</div>
<span class="action-label">创建工单</span>
</div>
<div class="action-item" @click="$router.push('/datasource/mysql')">
<div class="action-icon" style="background-color: #67C23A;">
<el-icon size="24" color="#fff"><Coin /></el-icon>
</div>
<span class="action-label">MySQL</span>
</div>
<div class="action-item" @click="$router.push('/datasource/redis')">
<div class="action-icon" style="background-color: #DC382D;">
<el-icon size="24" color="#fff"><Lightning /></el-icon>
</div>
<span class="action-label">Redis</span>
</div>
<div class="action-item" @click="$router.push('/dictionary/tables')">
<div class="action-icon" style="background-color: #E6A23C;">
<el-icon size="24" color="#fff"><Collection /></el-icon>
</div>
<span class="action-label">数据字典</span>
</div>
<div class="action-item" @click="$router.push('/audit/workflows')">
<div class="action-icon" style="background-color: #909399;">
<el-icon size="24" color="#fff"><Timer /></el-icon>
</div>
<span class="action-label">待审核</span>
<el-badge :value="stats.pending_workflows" class="action-badge" v-if="stats.pending_workflows > 0" />
</div>
</div>
</el-card>
<!-- 统计卡片 -->
<el-row :gutter="20" class="stats-row">
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon mysql">
<el-icon size="32" color="#fff"><Coin /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.mysql_instances }}</div>
<div class="stat-label">MySQL 实例</div>
</div>
</div>
<div class="stat-footer">
<span class="stat-detail">{{ stats.mysql_active }} 个正常</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon redis">
<el-icon size="32" color="#fff"><Lightning /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.redis_instances }}</div>
<div class="stat-label">Redis 实例</div>
</div>
</div>
<div class="stat-footer">
<span class="stat-detail">{{ formatNumber(stats.redis_keys) }} 个键</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon workflow">
<el-icon size="32" color="#fff"><Document /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.total_workflows }}</div>
<div class="stat-label">总工单</div>
</div>
</div>
<div class="stat-footer">
<span class="stat-detail" style="color: #E6A23C;">{{ stats.pending_workflows }} 个待审核</span>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card shadow="hover" class="stat-card">
<div class="stat-content">
<div class="stat-icon today">
<el-icon size="32" color="#fff"><Calendar /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ stats.today_workflows }}</div>
<div class="stat-label">今日工单</div>
</div>
</div>
<div class="stat-footer">
<span class="stat-detail">{{ stats.today_success }} 个成功</span>
</div>
</el-card>
</el-col>
</el-row>
<!-- 图表 -->
<el-row :gutter="20" class="charts-row">
<el-col :span="12">
<el-card shadow="hover">
<template #header><div class="card-header"><span>工单趋势(近7天)</span></div></template>
<div ref="trendChart" class="chart-container"></div>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header><div class="card-header"><span>SQL 风险分布</span></div></template>
<div ref="riskChart" class="chart-container"></div>
</el-card>
</el-col>
</el-row>
<!-- 待办和最近活动 -->
<el-row :gutter="20" class="bottom-row">
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>待办工单</span>
<el-button type="primary" link @click="$router.push('/audit/workflows')">查看全部</el-button>
</div>
</template>
<el-timeline>
<el-timeline-item v-for="w in pendingWorkflows" :key="w.id" :type="getRiskType(w.risk_level)" :timestamp="w.created_at">
<div>
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 4px;">
<el-link type="primary">{{ w.title }}</el-link>
<el-tag :type="getRiskType(w.risk_level)" size="small">{{ getRiskLabel(w.risk_level) }}</el-tag>
</div>
<div style="font-size: 12px; color: #909399;">
<span>{{ w.instance_name }}</span> | <span>提交人: {{ w.submitter_name }}</span>
</div>
</div>
</el-timeline-item>
</el-timeline>
</el-card>
</el-col>
<el-col :span="12">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>最近活动</span>
<el-button type="primary" link @click="$router.push('/system/logs')">查看全部</el-button>
</div>
</template>
<el-timeline>
<el-timeline-item v-for="a in activities" :key="a.id" :type="a.type" :timestamp="a.time">
<div style="display: flex; align-items: flex-start; gap: 12px;">
<el-avatar :size="28" :src="a.avatar" />
<div>
<div style="font-size: 14px; color: #303133;">{{ a.content }}</div>
<div style="font-size: 12px; color: #909399; margin-top: 2px;">{{ a.detail }}</div>
</div>
</div>
</el-timeline-item>
</el-timeline>
</el-card>
</el-col>
</el-row>
</div>
`,
setup() {
const stats = ref(mockStats);
const pendingWorkflows = ref(mockWorkflows.filter(w => w.status === 'pending'));
const activities = ref([
{ id: 1, type: 'success', content: '工单 #1234 执行成功', detail: '添加用户表索引', time: '10分钟前', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=dev1' },
{ id: 2, type: 'primary', content: '创建了新工单', detail: '修复订单数据', time: '30分钟前', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=dev2' },
{ id: 3, type: 'warning', content: '审核通过了工单', detail: '创建新表', time: '1小时前', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=dba' },
{ id: 4, type: 'danger', content: '拒绝了工单', detail: '批量更新用户状态 - 影响范围过大', time: '2小时前', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=dba' },
{ id: 5, type: 'info', content: '添加了 MySQL 实例', detail: '测试-MySQL', time: '3小时前', avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=admin' },
]);
const trendChart = ref(null);
const riskChart = ref(null);
const formatNumber = (num) => {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
};
const getRiskType = (level) => ({ high: 'danger', medium: 'warning', low: 'info', safe: 'success' }[level] || 'info');
const getRiskLabel = (level) => ({ high: '高风险', medium: '中风险', low: '低风险', safe: '安全' }[level] || level);
onMounted(() => {
setTimeout(() => {
if (trendChart.value) {
const chart = echarts.init(trendChart.value);
chart.setOption({
tooltip: { trigger: 'axis' },
legend: { data: ['创建', '通过', '拒绝', '执行'] },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] },
yAxis: { type: 'value' },
series: [
{ name: '创建', type: 'line', smooth: true, data: [12, 15, 8, 20, 18, 10, 15], itemStyle: { color: '#409EFF' } },
{ name: '通过', type: 'line', smooth: true, data: [10, 12, 6, 18, 16, 8, 12], itemStyle: { color: '#67C23A' } },
{ name: '拒绝', type: 'line', smooth: true, data: [2, 3, 2, 2, 2, 2, 3], itemStyle: { color: '#F56C6C' } },
{ name: '执行', type: 'line', smooth: true, data: [8, 10, 5, 15, 14, 7, 10], itemStyle: { color: '#E6A23C' } },
]
});
}
if (riskChart.value) {
const chart = echarts.init(riskChart.value);
chart.setOption({
tooltip: { trigger: 'item', formatter: '{a} <br/>{b}: {c} ({d}%)' },
legend: { orient: 'vertical', right: 10, top: 'center' },
series: [{
name: '风险等级',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: { show: false },
emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold' } },
labelLine: { show: false },
data: [
{ value: 5, name: '高风险', itemStyle: { color: '#F56C6C' } },
{ value: 15, name: '中风险', itemStyle: { color: '#E6A23C' } },
{ value: 35, name: '低风险', itemStyle: { color: '#409EFF' } },
{ value: 73, name: '安全', itemStyle: { color: '#67C23A' } },
]
}]
});
}
}, 100);
});
return { stats, pendingWorkflows, activities, trendChart, riskChart, formatNumber, getRiskType, getRiskLabel };
}
};
// MySQL 实例列表
const MySQLInstances = {
template: `
<div>
<el-card shadow="hover" style="margin-bottom: 20px;">
<el-form :inline="true">
<el-form-item label="实例名称">
<el-input placeholder="请输入实例名称" clearable />
</el-form-item>
<el-form-item label="环境">
<el-select placeholder="选择环境" clearable>
<el-option label="开发" value="dev" />
<el-option label="测试" value="test" />
<el-option label="生产" value="prod" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary"><el-icon><Search /></el-icon>搜索</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>MySQL 实例列表</span>
<el-button type="primary"><el-icon><Plus /></el-icon>添加实例</el-button>
</div>
</template>
<el-table :data="instances" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="实例名称" min-width="180">
<template #default="{ row }">
<div style="display: flex; align-items: center; gap: 12px;">
<el-icon size="20" color="#4479A1"><Coin /></el-icon>
<div>
<el-link type="primary" @click="goToDetail(row.id)">{{ row.name }}</el-link>
<div style="font-size: 12px; color: #909399;">{{ row.description }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column prop="host" label="连接信息" min-width="200">
<template #default="{ row }">
<div>{{ row.host }}:{{ row.port }}</div>
<div style="font-size: 12px; color: #909399;">{{ row.database }}</div>
</template>
</el-table-column>
<el-table-column prop="environment" label="环境" width="100">
<template #default="{ row }">
<el-tag :type="getEnvType(row.environment)" effect="dark">{{ getEnvLabel(row.environment) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'danger'" effect="light">
<el-icon v-if="row.status === 'active'"><CircleCheck /></el-icon>
<el-icon v-else><CircleClose /></el-icon>
{{ row.status === 'active' ? '正常' : '异常' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="监控指标" width="200">
<template #default="{ row }">
<div style="display: flex; gap: 16px;">
<el-tooltip content="连接数">
<div style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: #606266;">
<el-icon><User /></el-icon><span>{{ row.connections }}</span>
</div>
</el-tooltip>
<el-tooltip content="QPS">
<div style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: #606266;">
<el-icon><TrendCharts /></el-icon><span>{{ row.qps }}</span>
</div>
</el-tooltip>
<el-tooltip content="延迟">
<div style="display: flex; align-items: center; gap: 4px; font-size: 13px; color: #606266;">
<el-icon><Timer /></el-icon><span>{{ row.latency }}ms</span>
</div>
</el-tooltip>
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="250" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="goToDetail(row.id)"><el-icon><View /></el-icon>管理</el-button>
<el-button type="success" link><el-icon><CircleCheck /></el-icon>测试</el-button>
<el-button type="primary" link><el-icon><Edit /></el-icon>编辑</el-button>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
`,
setup() {
const instances = ref(mockMySQLInstances);
const router = VueRouter.useRouter();
const getEnvType = (env) => ({ dev: 'info', test: 'warning', staging: '', prod: 'danger' }[env] || '');
const getEnvLabel = (env) => ({ dev: '开发', test: '测试', staging: '预发布', prod: '生产' }[env] || env);
const goToDetail = (id) => {
router.push(`/mysql/${id}/query`);
};
return { instances, getEnvType, getEnvLabel, goToDetail };
}
};
// MySQL 详情 - SQL 查询
const MySQLQuery = {
template: `
<el-row :gutter="20">
<el-col :span="5">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>数据库</span>
<el-button type="primary" link><el-icon><Refresh /></el-icon></el-button>
</div>
</template>
<el-select v-model="selectedDB" style="width: 100%; margin-bottom: 16px;">
<el-option v-for="db in databases" :key="db" :label="db" :value="db" />
</el-select>
<div style="margin-bottom: 12px; font-weight: 500;">表 ({{ tables.length }})</div>
<el-input v-model="tableSearch" placeholder="搜索表" size="small" clearable style="margin-bottom: 12px;" />
<el-scrollbar height="calc(100vh - 450px)">
<div v-for="table in filteredTables" :key="table" class="key-item" @click="selectTable(table)">
<el-icon><Document /></el-icon>
<span class="key-name">{{ table }}</span>
</div>
</el-scrollbar>
</el-card>
</el-col>
<el-col :span="19">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<div>
<el-tag type="info" effect="plain">{{ selectedDB || '未选择数据库' }}</el-tag>
<el-divider direction="vertical" />
<span style="font-weight: 500;">SQL 查询</span>
</div>
<div>
<el-button-group>
<el-button type="primary"><el-icon><VideoPlay /></el-icon>执行</el-button>
<el-button><el-icon><MagicStick /></el-icon>格式化</el-button>
<el-button><el-icon><Delete /></el-icon>清空</el-button>
</el-button-group>
<el-divider direction="vertical" />
<el-button><el-icon><DocumentChecked /></el-icon>转为工单</el-button>
</div>
</div>
</template>
<div class="editor-container">
<textarea class="sql-textarea" v-model="sql" placeholder="-- 请输入 SQL 查询语句
-- 支持 SELECT/SHOW/EXPLAIN 等只读操作"></textarea>
</div>
<div v-if="queryResult" class="result-section">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; padding: 12px; background-color: #f5f7fa; border-radius: 4px;">
<div style="display: flex; align-items: center; gap: 16px;">
<el-tag type="success" effect="light"><el-icon><CircleCheck /></el-icon>查询成功</el-tag>
<span style="color: #606266; font-size: 13px;">{{ queryResult.rows.length }} 行</span>
<span style="color: #606266; font-size: 13px;">{{ queryResult.time }}ms</span>
</div>
<el-button type="primary" link><el-icon><Download /></el-icon>导出 CSV</el-button>
</div>
<el-table :data="queryResult.rows" stripe border class="result-table">
<el-table-column v-for="col in queryResult.columns" :key="col" :prop="col" :label="col" min-width="120" />
</el-table>
</div>
</el-card>
</el-col>
</el-row>
`,
setup() {
const databases = ref(['production', 'test_db', 'dev_db']);
const selectedDB = ref('production');
const tables = ref(['users', 'orders', 'products', 'categories', 'logs']);
const tableSearch = ref('');
const sql = ref('SELECT * FROM users LIMIT 100;');
const queryResult = ref({
columns: ['id', 'username', 'email', 'status', 'created_at'],
rows: [
{ id: 1, username: 'user1', email: 'user1@example.com', status: 'active', created_at: '2024-01-15 10:30:00' },
{ id: 2, username: 'user2', email: 'user2@example.com', status: 'active', created_at: '2024-01-16 11:00:00' },
{ id: 3, username: 'user3', email: 'user3@example.com', status: 'inactive', created_at: '2024-01-17 09:15:00' },
],
time: 15.5,
});
const filteredTables = computed(() => {
if (!tableSearch.value) return tables.value;
return tables.value.filter(t => t.toLowerCase().includes(tableSearch.value.toLowerCase()));
});
const selectTable = (table) => {
sql.value = `SELECT * FROM \`${table}\` LIMIT 100;`;
};
return { databases, selectedDB, tables, tableSearch, sql, queryResult, filteredTables, selectTable };
}
};
// Redis 实例列表
const RedisInstances = {
template: `
<div>
<el-card shadow="hover" style="margin-bottom: 20px;">
<el-form :inline="true">
<el-form-item label="实例名称">
<el-input placeholder="请输入实例名称" clearable />
</el-form-item>
<el-form-item label="环境">
<el-select placeholder="选择环境" clearable>
<el-option label="开发" value="dev" />
<el-option label="测试" value="test" />
<el-option label="生产" value="prod" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary"><el-icon><Search /></el-icon>搜索</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-row :gutter="20">
<el-col :span="8" v-for="instance in instances" :key="instance.id">
<el-card shadow="hover" class="instance-card">
<div class="instance-header">
<div class="instance-title">
<el-icon size="24" color="#DC382D"><Lightning /></el-icon>
<div class="title-text">
<h4 @click="goToDetail(instance.id)" style="cursor: pointer; color: #409EFF;">{{ instance.name }}</h4>
<p>{{ instance.host }}:{{ instance.port }}</p>
</div>
</div>
<el-tag :type="instance.status === 'active' ? 'success' : 'danger'" size="small">
{{ instance.status === 'active' ? '正常' : '异常' }}
</el-tag>
</div>
<el-divider />
<div class="instance-metrics">
<div class="metric-item">
<span class="metric-label">版本</span>
<span class="metric-value">{{ instance.version }}</span>
</div>
<div class="metric-item">
<span class="metric-label">模式</span>
<span class="metric-value">{{ instance.redis_mode }}</span>
</div>
<div class="metric-item">
<span class="metric-label">键数</span>
<span class="metric-value">{{ formatNumber(instance.total_keys) }}</span>
</div>
<div class="metric-item">
<span class="metric-label">内存</span>
<span class="metric-value">{{ instance.used_memory_human }}</span>
</div>
<div class="metric-item">
<span class="metric-label">连接</span>
<span class="metric-value">{{ instance.connected_clients }}</span>
</div>
<div class="metric-item">
<span class="metric-label">命中率</span>
<span class="metric-value" :style="{ color: getHitRateColor(instance.hit_rate) }">{{ instance.hit_rate }}%</span>
</div>
</div>
<el-divider />
<div class="instance-actions">
<el-button type="primary" link @click="goToDetail(instance.id)"><el-icon><View /></el-icon>管理</el-button>
<el-button type="success" link><el-icon><CircleCheck /></el-icon>测试</el-button>
<el-button type="primary" link><el-icon><Edit /></el-icon>编辑</el-button>
</div>
</el-card>
</el-col>
</el-row>
</div>
`,
setup() {
const instances = ref(mockRedisInstances);
const router = VueRouter.useRouter();
const formatNumber = (num) => {
if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
return num.toString();
};
const getHitRateColor = (rate) => {
if (rate >= 90) return '#67C23A';
if (rate >= 70) return '#E6A23C';
return '#F56C6C';
};
const goToDetail = (id) => {
router.push(`/redis/${id}/keys`);
};
return { instances, formatNumber, getHitRateColor, goToDetail };
}
};
// Redis 键管理
const RedisKeys = {
template: `
<el-row :gutter="20">
<el-col :span="7">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>键空间</span>
<div>
<el-button type="primary" link><el-icon><Refresh /></el-icon></el-button>
<el-button type="danger" link><el-icon><Delete /></el-icon></el-button>
</div>
</div>
</template>
<el-select v-model="selectedDB" style="width: 100%; margin-bottom: 16px;">
<el-option v-for="db in databases" :key="db.db" :label="'DB ' + db.db + ' (' + db.keys + ' keys)'" :value="db.db" />
</el-select>
<el-input v-model="keyPattern" placeholder="搜索键 (支持 * 通配符)" clearable style="margin-bottom: 12px;">
<template #append><el-button><el-icon><Search /></el-icon></el-button></template>
</el-input>
<el-radio-group v-model="typeFilter" size="small" style="margin-bottom: 12px;">
<el-radio-button label="">全部</el-radio-button>
<el-radio-button label="string">String</el-radio-button>
<el-radio-button label="hash">Hash</el-radio-button>
<el-radio-button label="list">List</el-radio-button>
</el-radio-group>
<el-scrollbar height="calc(100vh - 500px)">
<div v-for="key in keys" :key="key.name" class="key-item" :class="{ active: selectedKey === key.name }" @click="selectKey(key)">
<el-tag :type="getKeyTypeColor(key.type)" size="small">{{ key.type }}</el-tag>
<span class="key-name">{{ key.name }}</span>
<el-tag v-if="key.ttl > 0" type="info" size="small">{{ formatTTL(key.ttl) }}</el-tag>
</div>
</el-scrollbar>
</el-card>
</el-col>
<el-col :span="17">
<el-card shadow="hover">
<template #header>
<div class="card-header">
<div v-if="selectedKey">
<el-tag :type="getKeyTypeColor(currentKey?.type)" size="large">{{ currentKey?.type }}</el-tag>
<span style="font-weight: 500; font-size: 16px; margin-left: 12px;">{{ selectedKey }}</span>
<el-tag v-if="currentKey?.ttl > 0" type="info" style="margin-left: 12px;">TTL: {{ formatTTL(currentKey.ttl) }}</el-tag>
</div>
<span v-else>请选择键查看详情</span>
<div v-if="selectedKey">
<el-button type="primary"><el-icon><Edit /></el-icon>编辑</el-button>
<el-button type="danger"><el-icon><Delete /></el-icon>删除</el-button>
<el-button><el-icon><Refresh /></el-icon>刷新</el-button>
</div>
</div>
</template>
<div v-if="selectedKey" style="background-color: #f5f7fa; padding: 16px; border-radius: 4px; font-family: monospace; font-size: 14px;">
<pre style="margin: 0;">{{ keyValue }}</pre>
</div>
<el-empty v-else description="从左侧选择一个键查看详情" />
</el-card>
</el-col>
</el-row>
`,
setup() {
const databases = ref([{ db: 0, keys: 50000, expires: 10000 }, { db: 1, keys: 30000, expires: 5000 }]);
const selectedDB = ref(0);
const keyPattern = ref('*');
const typeFilter = ref('');
const keys = ref([
{ name: 'user:1001', type: 'hash', ttl: 3600 },
{ name: 'user:1002', type: 'hash', ttl: -1 },
{ name: 'session:abc123', type: 'string', ttl: 1800 },
{ name: 'products:list', type: 'list', ttl: -1 },
{ name: 'tags:set', type: 'set', ttl: -1 },
{ name: 'ranking:zset', type: 'zset', ttl: 86400 },
]);
const selectedKey = ref('');
const currentKey = ref(null);
const keyValue = ref('');
const selectKey = (key) => {
selectedKey.value = key.name;
currentKey.value = key;
keyValue.value = JSON.stringify({ id: '1001', name: '张三', email: 'zhangsan@example.com', age: 28 }, null, 2);
};
const getKeyTypeColor = (type) => ({ string: 'success', hash: 'primary', list: 'warning', set: 'danger', zset: 'info' }[type] || '');
const formatTTL = (ttl) => {
if (ttl < 0) return '永不过期';
if (ttl < 60) return ttl + 's';
if (ttl < 3600) return Math.floor(ttl / 60) + 'm';
if (ttl < 86400) return Math.floor(ttl / 3600) + 'h';
return Math.floor(ttl / 86400) + 'd';
};
return { databases, selectedDB, keyPattern, typeFilter, keys, selectedKey, currentKey, keyValue, selectKey, getKeyTypeColor, formatTTL };
}
};
// SQL 工单列表
const Workflows = {
template: `
<div>
<el-card shadow="hover" style="margin-bottom: 20px;">
<el-form :inline="true">
<el-form-item label="工单标题">
<el-input placeholder="搜索工单" clearable style="width: 250px;" />
</el-form-item>
<el-form-item label="类型">
<el-select placeholder="选择类型" clearable style="width: 120px;">
<el-option label="DDL" value="ddl" />
<el-option label="DML" value="dml" />
</el-select>
</el-form-item>
<el-form-item label="状态">
<el-select placeholder="选择状态" clearable style="width: 120px;">
<el-option label="待审核" value="pending" />
<el-option label="已通过" value="approved" />
<el-option label="已拒绝" value="rejected" />
<el-option label="执行成功" value="success" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary"><el-icon><Search /></el-icon>搜索</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="hover">
<template #header>
<div class="card-header">
<span>工单列表</span>
<el-button type="primary" @click="$router.push('/audit/workflows/create')"><el-icon><Plus /></el-icon>创建工单</el-button>
</div>
</template>
<el-table :data="workflows" stripe>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="title" label="标题" min-width="200">
<template #default="{ row }">
<el-link type="primary" @click="viewDetail(row.id)">{{ row.title }}</el-link>
</template>
</el-table-column>
<el-table-column prop="instance_name" label="数据源" width="150" />
<el-table-column prop="workflow_type" label="类型" width="80">
<template #default="{ row }">
<el-tag :type="row.workflow_type === 'ddl' ? 'primary' : 'success'" size="small">{{ row.workflow_type.toUpperCase() }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="risk_level" label="风险" width="100">
<template #default="{ row }">
<el-tag :type="getRiskType(row.risk_level)" size="small">{{ getRiskLabel(row.risk_level) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="risk_score" label="风险分" width="90">
<template #default="{ row }">
<el-progress :percentage="row.risk_score" :color="getRiskColor(row.risk_score)" :stroke-width="8" />
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)" size="small">{{ getStatusLabel(row.status) }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="submitter_name" label="提交人" width="100" />
<el-table-column prop="created_at" label="创建时间" width="160" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="viewDetail(row.id)">查看</el-button>
<template v-if="row.status === 'pending'">
<el-button type="success" link>通过</el-button>
<el-button type="danger" link>拒绝</el-button>
</template>
</template>
</el-table-column>
</el-table>
</el-card>
</div>
`,
setup() {
const workflows = ref(mockWorkflows);
const router = VueRouter.useRouter();
const getRiskType = (level) => ({ high: 'danger', medium: 'warning', low: 'info', safe: 'success' }[level] || 'info');
const getRiskLabel = (level) => ({ high: '高风险', medium: '中风险', low: '低风险', safe: '安全' }[level] || level);
const getRiskColor = (score) => score >= 50 ? '#F56C6C' : score >= 30 ? '#E6A23C' : score >= 10 ? '#409EFF' : '#67C23A';
const getStatusType = (status) => ({ pending: 'warning', approved: 'success', rejected: 'danger', executing: 'info', success: 'success', failed: 'danger' }[status] || 'info');
const getStatusLabel = (status) => ({ pending: '待审核', approved: '已通过', rejected: '已拒绝', executing: '执行中', success: '成功', failed: '失败' }[status] || status);
const viewDetail = (id) => {
router.push(`/audit/workflows/${id}`);
};
return { workflows, getRiskType, getRiskLabel, getRiskColor, getStatusType, getStatusLabel, viewDetail };
}
};
// 布局组件
const MainLayout = {
template: `
<el-container class="main-layout">
<el-aside width="240px" class="sidebar">
<div class="logo">
<el-icon size="28" color="#fff"><DataLine /></el-icon>
<span class="logo-text">数据审核平台</span>
</div>
<el-menu :default-active="$route.path" :default-openeds="openeds" router background-color="#001529" text-color="#fff" active-text-color="#409EFF">
<el-menu-item index="/">
<el-icon><Odometer /></el-icon>
<span>仪表盘</span>
</el-menu-item>
<el-sub-menu index="/datasource">
<template #title>
<el-icon><DataLine /></el-icon>
<span>数据源管理</span>
</template>
<el-menu-item index="/datasource/mysql">
<el-icon><Coin /></el-icon>
<span>MySQL 实例</span>
</el-menu-item>
<el-menu-item index="/datasource/redis">
<el-icon><Lightning /></el-icon>
<span>Redis 实例</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/audit">
<template #title>
<el-icon><DocumentChecked /></el-icon>
<span>SQL 审核</span>
</template>
<el-menu-item index="/audit/workflows">
<el-icon><Document /></el-icon>
<span>工单列表</span>
</el-menu-item>
<el-menu-item index="/audit/rules">
<el-icon><Warning /></el-icon>
<span>审核规则</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/dictionary">
<template #title>
<el-icon><Collection /></el-icon>
<span>数据字典</span>
</template>
<el-menu-item index="/dictionary/tables">
<el-icon><Grid /></el-icon>
<span>表结构</span>
</el-menu-item>
</el-sub-menu>
<el-sub-menu index="/system">
<template #title>
<el-icon><Setting /></el-icon>
<span>系统管理</span>
</template>
<el-menu-item index="/system/users">
<el-icon><User /></el-icon>
<span>用户管理</span>
</el-menu-item>
<el-menu-item index="/system/logs">
<el-icon><Document /></el-icon>
<span>操作日志</span>
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-aside>
<el-container>
<el-header class="header">
<div></div>
<div class="header-right">
<el-button circle @click="$router.push('/audit/workflows/create')"><el-icon><Plus /></el-icon></el-button>
<el-badge :value="3" class="notification-badge">
<el-icon size="20" style="cursor: pointer;"><Bell /></el-icon>
</el-badge>
<el-dropdown>
<div class="user-info">
<el-avatar :size="32" :src="'https://api.dicebear.com/7.x/avataaars/svg?seed=' + user.username" />
<span>{{ user.username }}</span>
<el-tag size="small" :type="getRoleType(user.role)" style="margin-left: 8px;">{{ getRoleLabel(user.role) }}</el-tag>
<el-icon><ArrowDown /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>个人中心</el-dropdown-item>
<el-dropdown-item>系统设置</el-dropdown-item>
<el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</el-header>
<el-main class="main-content">
<router-view />
</el-main>
</el-container>
</el-container>
`,
setup() {
const router = VueRouter.useRouter();
const user = ref(JSON.parse(localStorage.getItem('user') || '{}'));
const openeds = ref(['/datasource', '/audit', '/dictionary', '/system']);
const getRoleType = (role) => ({ admin: 'danger', dba: 'warning', developer: 'success', auditor: 'info' }[role] || 'info');
const getRoleLabel = (role) => ({ admin: '管理员', dba: 'DBA', developer: '开发者', auditor: '审计员' }[role] || role);
const logout = () => {
localStorage.removeItem('user');
router.push('/login');
};
return { user, openeds, getRoleType, getRoleLabel, logout };
}
};
// 路由配置
const routes = [
{ path: '/login', component: Login },
{
path: '/',
component: MainLayout,
redirect: '/',
children: [
{ path: '/', component: Dashboard },
{ path: '/datasource/mysql', component: MySQLInstances },
{ path: '/datasource/redis', component: RedisInstances },
{ path: '/mysql/:id/query', component: MySQLQuery },
{ path: '/redis/:id/keys', component: RedisKeys },
{ path: '/audit/workflows', component: Workflows },
{ path: '/audit/workflows/create', component: { template: '<el-card><template #header><span>创建工单</span></template><el-alert title="创建工单功能" description="支持 DDL/DML 工单创建和 SQL 审核" type="info" show-icon /></el-card>' } },
{ path: '/audit/rules', component: { template: '<el-card><template #header><span>审核规则</span></template><el-alert title="审核规则功能" description="支持 SQL 审核规则配置和管理" type="info" show-icon /></el-card>' } },
{ path: '/dictionary/tables', component: { template: '<el-card><template #header><span>表结构</span></template><el-alert title="数据字典功能" description="支持表结构查看、字段信息、索引信息" type="info" show-icon /></el-card>' } },
{ path: '/system/users', component: { template: '<el-card><template #header><span>用户管理</span></template><el-alert title="用户管理功能" description="支持用户管理和角色权限控制" type="info" show-icon /></el-card>' } },
{ path: '/system/logs', component: { template: '<el-card><template #header><span>操作日志</span></template><el-alert title="操作日志功能" description="支持操作日志查询和审计" type="info" show-icon /></el-card>' } },
]
}
];
const router = createRouter({
history: createWebHashHistory(),
routes
});
router.beforeEach((to, from, next) => {
if (to.path === '/login') {
next();
return;
}
const user = localStorage.getItem('user');
if (!user) {
next('/login');
return;
}
next();
});
const app = createApp({ template: '<router-view />' });
app.use(router);
app.use(ElementPlus, { locale: ElementPlusLocaleZhCn });
app.mount('#app');
</script>
</body>
</html>