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>
958 lines
44 KiB
HTML
958 lines
44 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;
|
|
}
|
|
.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: 24px;
|
|
}
|
|
.user-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
}
|
|
.main-content {
|
|
background-color: #f0f2f5;
|
|
padding: 20px;
|
|
overflow-y: auto;
|
|
}
|
|
.stats-row { margin-bottom: 20px; }
|
|
.stat-card {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 20px;
|
|
}
|
|
.stat-icon {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin-right: 16px;
|
|
}
|
|
.stat-value {
|
|
font-size: 28px;
|
|
font-weight: 600;
|
|
color: #303133;
|
|
line-height: 1;
|
|
margin-bottom: 8px;
|
|
}
|
|
.stat-label {
|
|
font-size: 14px;
|
|
color: #909399;
|
|
}
|
|
.card-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
.charts-row { margin-bottom: 20px; }
|
|
.chart-container { height: 300px; }
|
|
.section-title {
|
|
font-size: 16px;
|
|
font-weight: 500;
|
|
margin-bottom: 16px;
|
|
}
|
|
.instance-name {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.user-info-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
.user-detail .username {
|
|
font-weight: 500;
|
|
color: #303133;
|
|
}
|
|
.user-detail .email {
|
|
font-size: 12px;
|
|
color: #909399;
|
|
}
|
|
.sql-content {
|
|
background-color: #f5f7fa;
|
|
padding: 16px;
|
|
border-radius: 4px;
|
|
font-family: 'Courier New', monospace;
|
|
font-size: 14px;
|
|
line-height: 1.6;
|
|
overflow-x: auto;
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
}
|
|
.audit-item {
|
|
margin-bottom: 16px;
|
|
}
|
|
.audit-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 4px;
|
|
}
|
|
.audit-message {
|
|
color: #606266;
|
|
}
|
|
.audit-suggestion {
|
|
color: #409eff;
|
|
font-size: 13px;
|
|
margin-top: 4px;
|
|
}
|
|
</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 = {
|
|
total_instances: 5,
|
|
total_workflows: 128,
|
|
pending_workflows: 12,
|
|
today_workflows: 8,
|
|
risk_distribution: { high: 5, medium: 15, low: 35, safe: 73 },
|
|
db_type_distribution: { mysql: 3, redis: 1, postgresql: 1 },
|
|
recent_workflows: [
|
|
{ id: 1, title: '添加用户表索引', instance_name: '生产-MySQL主库', workflow_type: 'ddl', risk_level: 'safe', 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', 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', status: 'approved', submitter_name: 'dev2', created_at: '2024-01-14 16:45:00' },
|
|
]
|
|
};
|
|
|
|
const mockInstances = [
|
|
{ id: 1, name: '生产-MySQL主库', db_type: 'mysql', host: 'mysql-prod.example.com', port: 3306, database: 'production', environment: 'prod', status: 'active', description: '生产环境 MySQL 主库' },
|
|
{ id: 2, name: '测试-MySQL', db_type: 'mysql', host: 'mysql-test.example.com', port: 3306, database: 'test_db', environment: 'test', status: 'active', description: '测试环境 MySQL' },
|
|
{ id: 3, name: '生产-Redis', db_type: 'redis', host: 'redis-prod.example.com', port: 6379, environment: 'prod', status: 'active', description: '生产环境 Redis 缓存' },
|
|
{ id: 4, name: '开发-MySQL', db_type: 'mysql', host: 'mysql-dev.example.com', port: 3306, database: 'dev_db', environment: 'dev', status: 'active', description: '开发环境 MySQL' },
|
|
{ id: 5, name: '生产-PostgreSQL', db_type: 'postgresql', host: 'pg-prod.example.com', port: 5432, database: 'production', environment: 'prod', status: 'active', description: '生产环境 PostgreSQL' },
|
|
];
|
|
|
|
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 mockUsers = [
|
|
{ id: 1, username: 'admin', email: 'admin@example.com', role: 'admin', department: '技术部', phone: '13800138001', is_active: true, created_at: '2024-01-01 10:00:00' },
|
|
{ id: 2, username: 'dba', email: 'dba@example.com', role: 'dba', department: '运维部', phone: '13800138002', is_active: true, created_at: '2024-01-02 10:00:00' },
|
|
{ id: 3, username: 'dev1', email: 'dev1@example.com', role: 'developer', department: '研发部', phone: '13800138003', is_active: true, created_at: '2024-01-03 10:00:00' },
|
|
{ id: 4, username: 'dev2', email: 'dev2@example.com', role: 'developer', department: '研发部', phone: '13800138004', is_active: true, created_at: '2024-01-04 10:00:00' },
|
|
{ id: 5, username: 'auditor', email: 'auditor@example.com', role: 'auditor', department: '安全部', phone: '13800138005', is_active: true, created_at: '2024-01-05 10: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-row :gutter="20" class="stats-row">
|
|
<el-col :span="6">
|
|
<el-card shadow="hover">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background-color: #409EFF;">
|
|
<el-icon size="32" color="#fff"><DataLine /></el-icon>
|
|
</div>
|
|
<div>
|
|
<div class="stat-value">{{ stats.total_instances }}</div>
|
|
<div class="stat-label">数据源</div>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card shadow="hover">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background-color: #67C23A;">
|
|
<el-icon size="32" color="#fff"><Document /></el-icon>
|
|
</div>
|
|
<div>
|
|
<div class="stat-value">{{ stats.total_workflows }}</div>
|
|
<div class="stat-label">总工单</div>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card shadow="hover">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background-color: #E6A23C;">
|
|
<el-icon size="32" color="#fff"><Timer /></el-icon>
|
|
</div>
|
|
<div>
|
|
<div class="stat-value">{{ stats.pending_workflows }}</div>
|
|
<div class="stat-label">待审核</div>
|
|
</div>
|
|
</div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="6">
|
|
<el-card shadow="hover">
|
|
<div class="stat-card">
|
|
<div class="stat-icon" style="background-color: #F56C6C;">
|
|
<el-icon size="32" color="#fff"><Calendar /></el-icon>
|
|
</div>
|
|
<div>
|
|
<div class="stat-value">{{ stats.today_workflows }}</div>
|
|
<div class="stat-label">今日工单</div>
|
|
</div>
|
|
</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>风险分布</span></div></template>
|
|
<div ref="riskChart" class="chart-container"></div>
|
|
</el-card>
|
|
</el-col>
|
|
<el-col :span="12">
|
|
<el-card shadow="hover">
|
|
<template #header><div class="card-header"><span>数据库类型分布</span></div></template>
|
|
<div ref="dbTypeChart" class="chart-container"></div>
|
|
</el-card>
|
|
</el-col>
|
|
</el-row>
|
|
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>最近工单</span>
|
|
<el-button type="primary" link @click="$router.push('/workflows')">查看全部</el-button>
|
|
</div>
|
|
</template>
|
|
<el-table :data="stats.recent_workflows" stripe>
|
|
<el-table-column prop="id" label="ID" width="80" />
|
|
<el-table-column prop="title" label="标题" min-width="200" />
|
|
<el-table-column prop="instance_name" label="数据源" width="150" />
|
|
<el-table-column prop="workflow_type" label="类型" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.workflow_type === 'ddl' ? 'primary' : 'success'">{{ 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)">{{ getRiskLabel(row.risk_level) }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="status" label="状态" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="getStatusType(row.status)">{{ 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="180" />
|
|
</el-table>
|
|
</el-card>
|
|
</div>
|
|
`,
|
|
setup() {
|
|
const stats = ref(mockStats);
|
|
const riskChart = ref(null);
|
|
const dbTypeChart = ref(null);
|
|
|
|
const getRiskType = (level) => ({ high: 'danger', medium: 'warning', low: 'info', safe: 'success' }[level] || 'info');
|
|
const getRiskLabel = (level) => ({ high: '高风险', medium: '中风险', low: '低风险', safe: '安全' }[level] || level);
|
|
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);
|
|
|
|
onMounted(() => {
|
|
setTimeout(() => {
|
|
if (riskChart.value) {
|
|
const chart = echarts.init(riskChart.value);
|
|
chart.setOption({
|
|
tooltip: { trigger: 'item' },
|
|
legend: { bottom: '5%' },
|
|
series: [{
|
|
type: 'pie',
|
|
radius: ['40%', '70%'],
|
|
data: [
|
|
{ value: stats.value.risk_distribution.high, name: '高风险', itemStyle: { color: '#F56C6C' } },
|
|
{ value: stats.value.risk_distribution.medium, name: '中风险', itemStyle: { color: '#E6A23C' } },
|
|
{ value: stats.value.risk_distribution.low, name: '低风险', itemStyle: { color: '#409EFF' } },
|
|
{ value: stats.value.risk_distribution.safe, name: '安全', itemStyle: { color: '#67C23A' } },
|
|
]
|
|
}]
|
|
});
|
|
}
|
|
if (dbTypeChart.value) {
|
|
const chart = echarts.init(dbTypeChart.value);
|
|
chart.setOption({
|
|
tooltip: { trigger: 'item' },
|
|
legend: { bottom: '5%' },
|
|
series: [{
|
|
type: 'pie',
|
|
radius: '60%',
|
|
data: Object.entries(stats.value.db_type_distribution).map(([name, value]) => ({ name: name.toUpperCase(), value }))
|
|
}]
|
|
});
|
|
}
|
|
}, 100);
|
|
});
|
|
|
|
return { stats, riskChart, dbTypeChart, getRiskType, getRiskLabel, getStatusType, getStatusLabel };
|
|
}
|
|
};
|
|
|
|
// 数据源管理组件
|
|
const Instances = {
|
|
template: `
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<div>
|
|
<el-input v-model="searchQuery" placeholder="搜索数据源" clearable style="width: 300px;">
|
|
<template #prefix><el-icon><Search /></el-icon></template>
|
|
</el-input>
|
|
</div>
|
|
<el-button type="primary"><el-icon><Plus /></el-icon>添加数据源</el-button>
|
|
</div>
|
|
</template>
|
|
<el-table :data="filteredInstances" stripe>
|
|
<el-table-column prop="id" label="ID" width="80" />
|
|
<el-table-column prop="name" label="名称" min-width="180">
|
|
<template #default="{ row }">
|
|
<div class="instance-name">
|
|
<el-icon size="18" :color="getDbTypeColor(row.db_type)"><Coin /></el-icon>
|
|
<span>{{ row.name }}</span>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="db_type" label="类型" width="120">
|
|
<template #default="{ row }">
|
|
<el-tag :type="getDbTypeTag(row.db_type)">{{ row.db_type.toUpperCase() }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="host" label="主机" min-width="180">
|
|
<template #default="{ row }">{{ row.host }}:{{ row.port }}</template>
|
|
</el-table-column>
|
|
<el-table-column prop="environment" label="环境" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="getEnvType(row.environment)">{{ 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'">{{ row.status === 'active' ? '正常' : '异常' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="200" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button type="primary" link @click="testConnection(row)">测试</el-button>
|
|
<el-button type="primary" link>编辑</el-button>
|
|
<el-button type="danger" link>删除</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
`,
|
|
setup() {
|
|
const searchQuery = ref('');
|
|
const instances = ref(mockInstances);
|
|
const filteredInstances = computed(() => {
|
|
if (!searchQuery.value) return instances.value;
|
|
return instances.value.filter(i => i.name.toLowerCase().includes(searchQuery.value.toLowerCase()));
|
|
});
|
|
|
|
const getDbTypeColor = (type) => ({ mysql: '#4479A1', redis: '#DC382D', postgresql: '#336791', mongodb: '#47A248' }[type] || '#909399');
|
|
const getDbTypeTag = (type) => ({ mysql: 'primary', redis: 'danger', postgresql: 'info', mongodb: 'success' }[type] || '');
|
|
const getEnvType = (env) => ({ dev: 'info', test: 'warning', staging: '', prod: 'danger' }[env] || '');
|
|
const getEnvLabel = (env) => ({ dev: '开发', test: '测试', staging: '预发布', prod: '生产' }[env] || env);
|
|
|
|
const testConnection = (row) => {
|
|
ElementPlus.ElMessage.success(`连接成功: ${row.name}`);
|
|
};
|
|
|
|
return { searchQuery, filteredInstances, getDbTypeColor, getDbTypeTag, getEnvType, getEnvLabel, testConnection };
|
|
}
|
|
};
|
|
|
|
// SQL 工单组件
|
|
const Workflows = {
|
|
template: `
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<div>
|
|
<el-input v-model="searchQuery" placeholder="搜索工单" clearable style="width: 250px;">
|
|
<template #prefix><el-icon><Search /></el-icon></template>
|
|
</el-input>
|
|
</div>
|
|
<el-button type="primary" @click="$router.push('/workflows/create')"><el-icon><Plus /></el-icon>创建工单</el-button>
|
|
</div>
|
|
</template>
|
|
<el-table :data="filteredWorkflows" 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 @click="handleReview(row, 'approve')">通过</el-button>
|
|
<el-button type="danger" link @click="handleReview(row, 'reject')">拒绝</el-button>
|
|
</template>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
`,
|
|
setup() {
|
|
const searchQuery = ref('');
|
|
const workflows = ref(mockWorkflows);
|
|
const filteredWorkflows = computed(() => {
|
|
if (!searchQuery.value) return workflows.value;
|
|
return workflows.value.filter(w => w.title.toLowerCase().includes(searchQuery.value.toLowerCase()));
|
|
});
|
|
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(`/workflows/${id}`);
|
|
};
|
|
|
|
const handleReview = (row, action) => {
|
|
ElementPlus.ElMessage.success(`${action === 'approve' ? '通过' : '拒绝'}工单: ${row.title}`);
|
|
};
|
|
|
|
return { searchQuery, filteredWorkflows, getRiskType, getRiskLabel, getRiskColor, getStatusType, getStatusLabel, viewDetail, handleReview };
|
|
}
|
|
};
|
|
|
|
// 创建工单组件
|
|
const CreateWorkflow = {
|
|
template: `
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<span>创建 SQL 工单</span>
|
|
<el-button @click="$router.back()">返回</el-button>
|
|
</div>
|
|
</template>
|
|
<el-steps :active="currentStep" finish-status="success" style="margin-bottom: 32px;">
|
|
<el-step title="填写信息" />
|
|
<el-step title="SQL 审核" />
|
|
<el-step title="提交工单" />
|
|
</el-steps>
|
|
<div v-if="currentStep === 0" style="max-width: 800px; margin: 0 auto;">
|
|
<el-form :model="form" label-width="100px">
|
|
<el-form-item label="标题"><el-input v-model="form.title" placeholder="请输入工单标题" /></el-form-item>
|
|
<el-form-item label="数据源">
|
|
<el-select v-model="form.instance_id" placeholder="选择数据源" style="width: 100%;">
|
|
<el-option v-for="instance in instances" :key="instance.id" :label="instance.name" :value="instance.id" />
|
|
</el-select>
|
|
</el-form-item>
|
|
<el-form-item label="工单类型">
|
|
<el-radio-group v-model="form.workflow_type">
|
|
<el-radio-button label="ddl">DDL (结构变更)</el-radio-button>
|
|
<el-radio-button label="dml">DML (数据变更)</el-radio-button>
|
|
</el-radio-group>
|
|
</el-form-item>
|
|
<el-form-item label="SQL 语句">
|
|
<el-input v-model="form.sql_content" type="textarea" :rows="10" placeholder="请输入 SQL 语句..." />
|
|
</el-form-item>
|
|
<el-form-item label="描述">
|
|
<el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入工单描述(可选)" />
|
|
</el-form-item>
|
|
</el-form>
|
|
<div style="display: flex; justify-content: center; gap: 16px; margin-top: 32px;">
|
|
<el-button type="primary" @click="handleAudit">审核 SQL</el-button>
|
|
</div>
|
|
</div>
|
|
<div v-if="currentStep === 1" style="max-width: 800px; margin: 0 auto;">
|
|
<el-alert :title="'审核完成,风险评分: ' + auditResult.risk_score + ',风险等级: ' + getRiskLabel(auditResult.risk_level)" :type="getRiskType(auditResult.risk_level)" show-icon style="margin-bottom: 24px;" />
|
|
<div v-if="auditResult.sql_results.length > 0">
|
|
<h4 class="section-title">审核结果</h4>
|
|
<div v-for="(result, index) in auditResult.sql_results" :key="index" class="audit-item">
|
|
<div class="audit-header">
|
|
<el-tag :type="getRiskType(result.risk_level)" size="small">{{ getRiskLabel(result.risk_level) }}</el-tag>
|
|
<span style="font-weight: 500;">{{ result.rule_name }}</span>
|
|
</div>
|
|
<div class="audit-message">{{ result.message }}</div>
|
|
<div v-if="result.suggestion" class="audit-suggestion">建议: {{ result.suggestion }}</div>
|
|
</div>
|
|
</div>
|
|
<el-result v-else icon="success" title="SQL 审核通过" sub-title="未发现风险问题" />
|
|
<div style="display: flex; justify-content: center; gap: 16px; margin-top: 32px;">
|
|
<el-button @click="currentStep = 0">上一步</el-button>
|
|
<el-button type="primary" @click="handleSubmit">提交工单</el-button>
|
|
</div>
|
|
</div>
|
|
<div v-if="currentStep === 2" style="max-width: 800px; margin: 0 auto;">
|
|
<el-result icon="success" title="工单创建成功" sub-title="工单编号: 10086">
|
|
<template #extra>
|
|
<el-button type="primary" @click="$router.push('/workflows')">查看工单</el-button>
|
|
<el-button @click="createAnother">创建新工单</el-button>
|
|
</template>
|
|
</el-result>
|
|
</div>
|
|
</el-card>
|
|
`,
|
|
setup() {
|
|
const currentStep = ref(0);
|
|
const instances = ref(mockInstances);
|
|
const form = reactive({ title: '', instance_id: null, workflow_type: 'ddl', sql_content: '', description: '' });
|
|
const auditResult = ref({ risk_score: 0, risk_level: 'safe', sql_results: [] });
|
|
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 handleAudit = () => {
|
|
if (!form.title || !form.instance_id || !form.sql_content) {
|
|
ElementPlus.ElMessage.warning('请填写完整信息');
|
|
return;
|
|
}
|
|
const sqlUpper = form.sql_content.toUpperCase();
|
|
const results = [];
|
|
let riskScore = 0;
|
|
if (sqlUpper.includes('DELETE') && !sqlUpper.includes('WHERE')) {
|
|
results.push({ rule_id: 1, rule_name: '禁止DELETE无WHERE条件', risk_level: 'high', message: 'DELETE语句缺少WHERE条件,将删除全表数据', suggestion: '请添加WHERE条件限制删除范围' });
|
|
riskScore += 50;
|
|
}
|
|
if (sqlUpper.includes('UPDATE') && !sqlUpper.includes('WHERE')) {
|
|
results.push({ rule_id: 2, rule_name: '禁止UPDATE无WHERE条件', risk_level: 'high', message: 'UPDATE语句缺少WHERE条件,将更新全表数据', suggestion: '请添加WHERE条件限制更新范围' });
|
|
riskScore += 50;
|
|
}
|
|
if (sqlUpper.includes('SELECT *')) {
|
|
results.push({ rule_id: 6, rule_name: 'SELECT * 建议指定列', risk_level: 'low', message: '建议使用具体的列名替代 *', suggestion: '列出需要的字段名' });
|
|
riskScore += 10;
|
|
}
|
|
let riskLevel = 'safe';
|
|
if (riskScore >= 50) riskLevel = 'high';
|
|
else if (riskScore >= 30) riskLevel = 'medium';
|
|
else if (riskScore >= 10) riskLevel = 'low';
|
|
auditResult.value = { risk_score: riskScore, risk_level: riskLevel, sql_results: results };
|
|
currentStep.value = 1;
|
|
};
|
|
|
|
const handleSubmit = () => {
|
|
ElementPlus.ElMessage.success('工单创建成功');
|
|
currentStep.value = 2;
|
|
};
|
|
|
|
const createAnother = () => {
|
|
currentStep.value = 0;
|
|
form.title = '';
|
|
form.instance_id = null;
|
|
form.workflow_type = 'ddl';
|
|
form.sql_content = '';
|
|
form.description = '';
|
|
};
|
|
|
|
return { currentStep, instances, form, auditResult, getRiskType, getRiskLabel, handleAudit, handleSubmit, createAnother };
|
|
}
|
|
};
|
|
|
|
// 工单详情组件
|
|
const WorkflowDetail = {
|
|
template: `
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<div style="display: flex; align-items: center; gap: 16px;">
|
|
<el-button @click="$router.back()"><el-icon><ArrowLeft /></el-icon>返回</el-button>
|
|
<span style="font-size: 18px; font-weight: 500;">工单详情 #{{ workflow.id }}</span>
|
|
<el-tag :type="getStatusType(workflow.status)">{{ getStatusLabel(workflow.status) }}</el-tag>
|
|
</div>
|
|
<div>
|
|
<el-button v-if="workflow.status === 'pending'" type="success">通过</el-button>
|
|
<el-button v-if="workflow.status === 'pending'" type="danger">拒绝</el-button>
|
|
<el-button v-if="workflow.status === 'approved'" type="primary">执行</el-button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<el-descriptions title="基本信息" :column="3" border>
|
|
<el-descriptions-item label="标题">{{ workflow.title }}</el-descriptions-item>
|
|
<el-descriptions-item label="类型">
|
|
<el-tag :type="workflow.workflow_type === 'ddl' ? 'primary' : 'success'">{{ workflow.workflow_type.toUpperCase() }}</el-tag>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="风险等级">
|
|
<el-tag :type="getRiskType(workflow.risk_level)">{{ getRiskLabel(workflow.risk_level) }} ({{ workflow.risk_score }}分)</el-tag>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="数据源">{{ workflow.instance_name }}</el-descriptions-item>
|
|
<el-descriptions-item label="提交人">{{ workflow.submitter_name }}</el-descriptions-item>
|
|
<el-descriptions-item label="创建时间">{{ workflow.created_at }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
<div style="margin-top: 24px;">
|
|
<h4 class="section-title">SQL 语句</h4>
|
|
<pre class="sql-content">{{ workflow.sql_content }}</pre>
|
|
</div>
|
|
<div style="margin-top: 24px;">
|
|
<h4 class="section-title">审批流程</h4>
|
|
<el-steps :active="2" finish-status="success">
|
|
<el-step title="提交工单" :description="workflow.submitter_name" />
|
|
<el-step title="审核" description="待审核" />
|
|
<el-step title="执行" description="待执行" />
|
|
</el-steps>
|
|
</div>
|
|
</el-card>
|
|
`,
|
|
setup() {
|
|
const route = VueRouter.useRoute();
|
|
const workflow = ref(mockWorkflows.find(w => w.id === parseInt(route.params.id)) || mockWorkflows[0]);
|
|
const getRiskType = (level) => ({ high: 'danger', medium: 'warning', low: 'info', safe: 'success' }[level] || 'info');
|
|
const getRiskLabel = (level) => ({ high: '高风险', medium: '中风险', low: '低风险', safe: '安全' }[level] || level);
|
|
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);
|
|
return { workflow, getRiskType, getRiskLabel, getStatusType, getStatusLabel };
|
|
}
|
|
};
|
|
|
|
// 用户管理组件
|
|
const Users = {
|
|
template: `
|
|
<el-card shadow="hover">
|
|
<template #header>
|
|
<div class="card-header">
|
|
<el-input v-model="searchQuery" placeholder="搜索用户" clearable style="width: 300px;">
|
|
<template #prefix><el-icon><Search /></el-icon></template>
|
|
</el-input>
|
|
<el-button type="primary"><el-icon><Plus /></el-icon>添加用户</el-button>
|
|
</div>
|
|
</template>
|
|
<el-table :data="filteredUsers" stripe>
|
|
<el-table-column label="用户" min-width="200">
|
|
<template #default="{ row }">
|
|
<div class="user-info-cell">
|
|
<el-avatar :size="40" :src="'https://api.dicebear.com/7.x/avataaars/svg?seed=' + row.username" />
|
|
<div class="user-detail">
|
|
<div class="username">{{ row.username }}</div>
|
|
<div class="email">{{ row.email }}</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="role" label="角色" width="120">
|
|
<template #default="{ row }">
|
|
<el-tag :type="getRoleType(row.role)">{{ getRoleLabel(row.role) }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="department" label="部门" width="150" />
|
|
<el-table-column prop="phone" label="电话" width="150" />
|
|
<el-table-column prop="created_at" label="创建时间" width="180" />
|
|
<el-table-column prop="is_active" label="状态" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="row.is_active ? 'success' : 'info'" size="small">{{ row.is_active ? '启用' : '禁用' }}</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="150" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button type="primary" link>编辑</el-button>
|
|
<el-button type="danger" link>禁用</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
</el-card>
|
|
`,
|
|
setup() {
|
|
const searchQuery = ref('');
|
|
const users = ref(mockUsers);
|
|
const filteredUsers = computed(() => {
|
|
if (!searchQuery.value) return users.value;
|
|
return users.value.filter(u => u.username.toLowerCase().includes(searchQuery.value.toLowerCase()) || u.email.toLowerCase().includes(searchQuery.value.toLowerCase()));
|
|
});
|
|
const getRoleType = (role) => ({ admin: 'danger', dba: 'warning', developer: 'success', auditor: 'info' }[role] || 'info');
|
|
const getRoleLabel = (role) => ({ admin: '管理员', dba: 'DBA', developer: '开发者', auditor: '审计员' }[role] || role);
|
|
return { searchQuery, filteredUsers, getRoleType, getRoleLabel };
|
|
}
|
|
};
|
|
|
|
// 布局组件
|
|
const MainLayout = {
|
|
template: `
|
|
<el-container class="main-layout">
|
|
<el-aside width="220px" 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" router background-color="#001529" text-color="#fff" active-text-color="#409EFF">
|
|
<el-menu-item v-for="item in menuItems" :key="item.path" :index="item.path">
|
|
<el-icon><component :is="item.icon" /></el-icon>
|
|
<span>{{ item.title }}</span>
|
|
</el-menu-item>
|
|
</el-menu>
|
|
</el-aside>
|
|
<el-container>
|
|
<el-header class="header">
|
|
<div></div>
|
|
<div class="header-right">
|
|
<el-badge :value="3" class="notification-badge">
|
|
<el-icon size="20" style="cursor: pointer;"><Bell /></el-icon>
|
|
</el-badge>
|
|
<el-dropdown @command="handleCommand">
|
|
<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-icon><ArrowDown /></el-icon>
|
|
</div>
|
|
<template #dropdown>
|
|
<el-dropdown-menu>
|
|
<el-dropdown-item command="profile">个人中心</el-dropdown-item>
|
|
<el-dropdown-item command="settings">系统设置</el-dropdown-item>
|
|
<el-dropdown-item divided command="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 menuItems = [
|
|
{ path: '/', title: '仪表盘', icon: 'Odometer' },
|
|
{ path: '/instances', title: '数据源管理', icon: 'DataLine' },
|
|
{ path: '/workflows', title: 'SQL 工单', icon: 'Document' },
|
|
{ path: '/query', title: 'SQL 查询', icon: 'Search' },
|
|
{ path: '/dictionary', title: '数据字典', icon: 'Collection' },
|
|
{ path: '/audit-rules', title: '审核规则', icon: 'Warning' },
|
|
{ path: '/redis', title: 'Redis 管理', icon: 'Coin' },
|
|
{ path: '/users', title: '用户管理', icon: 'User' },
|
|
{ path: '/settings', title: '系统设置', icon: 'Setting' },
|
|
];
|
|
|
|
const handleCommand = (command) => {
|
|
if (command === 'logout') {
|
|
localStorage.removeItem('user');
|
|
router.push('/login');
|
|
}
|
|
};
|
|
|
|
return { user, menuItems, handleCommand };
|
|
}
|
|
};
|
|
|
|
// 路由配置
|
|
const routes = [
|
|
{ path: '/login', component: Login },
|
|
{
|
|
path: '/',
|
|
component: MainLayout,
|
|
redirect: '/',
|
|
children: [
|
|
{ path: '/', component: Dashboard },
|
|
{ path: '/instances', component: Instances },
|
|
{ path: '/workflows', component: Workflows },
|
|
{ path: '/workflows/create', component: CreateWorkflow },
|
|
{ path: '/workflows/:id', component: WorkflowDetail },
|
|
{ path: '/query', component: { template: '<el-card><template #header><span>SQL 查询</span></template><el-alert title="SQL 查询功能" description="支持在线 SQL 编辑器和查询结果展示" type="info" show-icon /></el-card>' } },
|
|
{ path: '/dictionary', component: { template: '<el-card><template #header><span>数据字典</span></template><el-alert title="数据字典功能" description="支持表结构查看、字段信息、索引信息" 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: '/redis', component: { template: '<el-card><template #header><span>Redis 管理</span></template><el-alert title="Redis 管理功能" description="支持 Redis 信息监控和命令执行" type="info" show-icon /></el-card>' } },
|
|
{ path: '/users', component: Users },
|
|
{ path: '/settings', 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>
|