0709
This commit is contained in:
parent
bb2fcf7494
commit
0cc6c77080
@ -1,12 +1,11 @@
|
||||
from flask import Flask
|
||||
from flask_cors import CORS
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_jwt_extended import JWTManager
|
||||
from datetime import timedelta
|
||||
import os
|
||||
from database import db
|
||||
|
||||
# 初始化扩展
|
||||
db = SQLAlchemy()
|
||||
jwt = JWTManager()
|
||||
|
||||
def create_app():
|
||||
@ -16,7 +15,7 @@ def create_app():
|
||||
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY', 'dev-secret-key')
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv(
|
||||
'DATABASE_URL',
|
||||
'mysql+pymysql://root:password@localhost:3306/server_monitor'
|
||||
'mysql+pymysql://wqf_server_monitor:iwaA7WAirswiwbRb@47.96.181.224:3306/wqf_server_monitor'
|
||||
)
|
||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'jwt-secret-key')
|
||||
|
||||
3
backend/database.py
Normal file
3
backend/database.py
Normal file
@ -0,0 +1,3 @@
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
|
||||
db = SQLAlchemy()
|
||||
@ -1,4 +1,5 @@
|
||||
from app import create_app, db
|
||||
from app import create_app
|
||||
from database import db
|
||||
from models import User
|
||||
|
||||
def init_database():
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
from app import db
|
||||
from database import db
|
||||
from datetime import datetime
|
||||
from werkzeug.security import generate_password_hash, check_password_hash
|
||||
import json
|
||||
@ -8,7 +8,7 @@ class User(db.Model):
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
username = db.Column(db.String(80), unique=True, nullable=False)
|
||||
password_hash = db.Column(db.String(128), nullable=False)
|
||||
password_hash = db.Column(db.String(255), nullable=False)
|
||||
real_name = db.Column(db.String(120), nullable=False)
|
||||
email = db.Column(db.String(120))
|
||||
role = db.Column(db.String(20), nullable=False, default='viewer')
|
||||
@ -36,6 +36,7 @@ class Server(db.Model):
|
||||
ip = db.Column(db.String(15), nullable=False)
|
||||
port = db.Column(db.Integer, default=22)
|
||||
username = db.Column(db.String(50), default='root')
|
||||
password = db.Column(db.String(255)) # SSH密码
|
||||
status = db.Column(db.String(20), default='offline')
|
||||
description = db.Column(db.Text)
|
||||
created_at = db.Column(db.DateTime, default=datetime.utcnow)
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
Flask==2.3.0
|
||||
Flask-SQLAlchemy==3.0.0
|
||||
Flask-SQLAlchemy==3.0.5
|
||||
SQLAlchemy==2.0.23
|
||||
Flask-CORS==4.0.0
|
||||
Flask-JWT-Extended==4.5.1
|
||||
PyMySQL==1.1.0
|
||||
|
||||
37
backend/reset_db.py
Normal file
37
backend/reset_db.py
Normal file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""重置数据库脚本"""
|
||||
|
||||
from app import create_app
|
||||
from database import db
|
||||
from models import User, Server, Script, ExecuteHistory, ExecuteResult
|
||||
|
||||
def reset_database():
|
||||
"""重置数据库"""
|
||||
app = create_app()
|
||||
|
||||
with app.app_context():
|
||||
# 删除所有表
|
||||
db.drop_all()
|
||||
print("已删除所有表")
|
||||
|
||||
# 创建所有表
|
||||
db.create_all()
|
||||
print("已创建所有表")
|
||||
|
||||
# 创建默认管理员用户
|
||||
admin_user = User(
|
||||
username='admin',
|
||||
real_name='系统管理员',
|
||||
email='admin@example.com',
|
||||
role='admin'
|
||||
)
|
||||
admin_user.set_password('admin123')
|
||||
admin_user.set_permissions(['all'])
|
||||
|
||||
db.session.add(admin_user)
|
||||
db.session.commit()
|
||||
print("已创建默认管理员用户 (admin/admin123)")
|
||||
|
||||
if __name__ == '__main__':
|
||||
reset_database()
|
||||
print("数据库重置完成!")
|
||||
@ -1,7 +1,7 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
|
||||
from models import User
|
||||
from app import db
|
||||
from database import db
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from models import Script, Server, ExecuteHistory, ExecuteResult
|
||||
from app import db
|
||||
from database import db
|
||||
import paramiko
|
||||
import time
|
||||
import threading
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from models import Script
|
||||
from app import db
|
||||
from database import db
|
||||
|
||||
scripts_bp = Blueprint('scripts', __name__)
|
||||
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from models import Server
|
||||
from app import db
|
||||
from database import db
|
||||
import paramiko
|
||||
import socket
|
||||
|
||||
servers_bp = Blueprint('servers', __name__)
|
||||
|
||||
def check_server_status(ip, port, username):
|
||||
def check_server_status(ip, port, username, password=None):
|
||||
"""检查服务器连接状态"""
|
||||
try:
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(ip, port=port, username=username, timeout=5)
|
||||
ssh.connect(ip, port=port, username=username, password=password, timeout=5)
|
||||
ssh.close()
|
||||
return 'online'
|
||||
except:
|
||||
@ -26,7 +26,7 @@ def get_servers():
|
||||
|
||||
for server in servers:
|
||||
# 实时检查服务器状态
|
||||
server.status = check_server_status(server.ip, server.port, server.username)
|
||||
server.status = check_server_status(server.ip, server.port, server.username, server.password)
|
||||
db.session.commit()
|
||||
|
||||
result.append({
|
||||
@ -37,50 +37,60 @@ def get_servers():
|
||||
'username': server.username,
|
||||
'status': server.status,
|
||||
'description': server.description,
|
||||
'createdAt': server.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
'created_at': server.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
return jsonify(result)
|
||||
return jsonify({'data': result})
|
||||
|
||||
@servers_bp.route('', methods=['POST'])
|
||||
@jwt_required()
|
||||
def create_server():
|
||||
data = request.get_json()
|
||||
|
||||
server = Server(
|
||||
name=data['name'],
|
||||
ip=data['ip'],
|
||||
port=data.get('port', 22),
|
||||
username=data.get('username', 'root'),
|
||||
description=data.get('description', '')
|
||||
)
|
||||
|
||||
# 检查服务器连接状态
|
||||
server.status = check_server_status(server.ip, server.port, server.username)
|
||||
|
||||
db.session.add(server)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': '服务器添加成功', 'id': server.id}), 201
|
||||
try:
|
||||
data = request.get_json()
|
||||
|
||||
server = Server(
|
||||
name=data['name'],
|
||||
ip=data['ip'],
|
||||
port=data.get('port', 22),
|
||||
username=data.get('username', 'root'),
|
||||
password=data.get('password'),
|
||||
description=data.get('description', '')
|
||||
)
|
||||
|
||||
# 检查服务器连接状态
|
||||
server.status = check_server_status(server.ip, server.port, server.username, server.password)
|
||||
|
||||
db.session.add(server)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': '服务器添加成功', 'id': server.id}), 201
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'error': f'添加服务器失败: {str(e)}'}), 400
|
||||
|
||||
@servers_bp.route('/<int:server_id>', methods=['PUT'])
|
||||
@jwt_required()
|
||||
def update_server(server_id):
|
||||
server = Server.query.get_or_404(server_id)
|
||||
data = request.get_json()
|
||||
|
||||
server.name = data.get('name', server.name)
|
||||
server.ip = data.get('ip', server.ip)
|
||||
server.port = data.get('port', server.port)
|
||||
server.username = data.get('username', server.username)
|
||||
server.description = data.get('description', server.description)
|
||||
|
||||
# 重新检查服务器状态
|
||||
server.status = check_server_status(server.ip, server.port, server.username)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': '服务器更新成功'})
|
||||
try:
|
||||
server = Server.query.get_or_404(server_id)
|
||||
data = request.get_json()
|
||||
|
||||
server.name = data.get('name', server.name)
|
||||
server.ip = data.get('ip', server.ip)
|
||||
server.port = data.get('port', server.port)
|
||||
server.username = data.get('username', server.username)
|
||||
server.password = data.get('password', server.password)
|
||||
server.description = data.get('description', server.description)
|
||||
|
||||
# 重新检查服务器状态
|
||||
server.status = check_server_status(server.ip, server.port, server.username, server.password)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': '服务器更新成功'})
|
||||
except Exception as e:
|
||||
db.session.rollback()
|
||||
return jsonify({'error': f'更新服务器失败: {str(e)}'}), 400
|
||||
|
||||
@servers_bp.route('/<int:server_id>', methods=['DELETE'])
|
||||
@jwt_required()
|
||||
@ -94,12 +104,12 @@ def delete_server(server_id):
|
||||
@servers_bp.route('/<int:server_id>/test', methods=['POST'])
|
||||
@jwt_required()
|
||||
def test_server_connection(server_id):
|
||||
server = Server.query.get_or_404(server_id)
|
||||
|
||||
try:
|
||||
server = Server.query.get_or_404(server_id)
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
ssh.connect(server.ip, port=server.port, username=server.username, timeout=10)
|
||||
ssh.connect(server.ip, port=server.port, username=server.username, password=server.password, timeout=10)
|
||||
|
||||
# 执行简单命令测试
|
||||
stdin, stdout, stderr = ssh.exec_command('echo "connection test"')
|
||||
@ -107,12 +117,22 @@ def test_server_connection(server_id):
|
||||
|
||||
ssh.close()
|
||||
|
||||
# 更新服务器状态
|
||||
server.status = 'online'
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'message': '连接测试成功',
|
||||
'output': output
|
||||
})
|
||||
except Exception as e:
|
||||
# 更新服务器状态为离线
|
||||
server = Server.query.get(server_id)
|
||||
if server:
|
||||
server.status = 'offline'
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'status': 'error',
|
||||
'message': f'连接测试失败: {str(e)}'
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||
from models import User
|
||||
from app import db
|
||||
from database import db
|
||||
|
||||
users_bp = Blueprint('users', __name__)
|
||||
|
||||
|
||||
@ -11,23 +11,23 @@
|
||||
@click="handleMenuClick"
|
||||
>
|
||||
<a-menu-item key="dashboard">
|
||||
<DashboardOutlined />
|
||||
<span style="margin-right: 8px;">📊</span>
|
||||
<span>控制台</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="servers">
|
||||
<ServerOutlined />
|
||||
<span style="margin-right: 8px;">🖥️</span>
|
||||
<span>服务器管理</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="scripts">
|
||||
<FileTextOutlined />
|
||||
<span style="margin-right: 8px;">📝</span>
|
||||
<span>脚本管理</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="execute">
|
||||
<PlayCircleOutlined />
|
||||
<span style="margin-right: 8px;">⚡</span>
|
||||
<span>批量执行</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="users">
|
||||
<UserOutlined />
|
||||
<span style="margin-right: 8px;">👥</span>
|
||||
<span>用户管理</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
@ -41,28 +41,17 @@
|
||||
@click="collapsed = !collapsed"
|
||||
class="mr-4"
|
||||
>
|
||||
<MenuUnfoldOutlined v-if="collapsed" />
|
||||
<MenuFoldOutlined v-else />
|
||||
<span>{{ collapsed ? '▶' : '◀' }}</span>
|
||||
</a-button>
|
||||
<a-breadcrumb>
|
||||
<a-breadcrumb-item>{{ currentTitle }}</a-breadcrumb-item>
|
||||
</a-breadcrumb>
|
||||
<div style="font-size: 16px; font-weight: 500;">{{ currentTitle }}</div>
|
||||
</div>
|
||||
|
||||
<a-dropdown>
|
||||
<a-button type="text" class="flex items-center">
|
||||
<UserOutlined class="mr-2" />
|
||||
{{ user.username }}
|
||||
<div style="display: flex; align-items: center;">
|
||||
<span style="margin-right: 10px;">👤 {{ user?.username || '管理员' }}</span>
|
||||
<a-button type="text" @click="handleLogout" style="padding: 4px 8px;">
|
||||
退出登录
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="handleUserMenu">
|
||||
<a-menu-item key="logout">
|
||||
<LogoutOutlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</a-layout-header>
|
||||
|
||||
<a-layout-content class="layout-content p-6">
|
||||
@ -76,16 +65,6 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import {
|
||||
DashboardOutlined,
|
||||
ServerOutlined,
|
||||
FileTextOutlined,
|
||||
PlayCircleOutlined,
|
||||
UserOutlined,
|
||||
MenuUnfoldOutlined,
|
||||
MenuFoldOutlined,
|
||||
LogoutOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@ -100,10 +79,46 @@ const handleMenuClick = ({ key }) => {
|
||||
router.push(`/${key}`)
|
||||
}
|
||||
|
||||
const handleUserMenu = ({ key }) => {
|
||||
if (key === 'logout') {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
const handleLogout = () => {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.layout-sider {
|
||||
position: fixed !important;
|
||||
height: 100vh;
|
||||
left: 0;
|
||||
z-index: 1001;
|
||||
}
|
||||
|
||||
.layout-header {
|
||||
background: #fff;
|
||||
padding: 0 20px;
|
||||
box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 200px;
|
||||
z-index: 1000;
|
||||
transition: left 0.2s;
|
||||
}
|
||||
|
||||
.layout-content {
|
||||
margin-left: 200px;
|
||||
margin-top: 64px;
|
||||
min-height: calc(100vh - 64px);
|
||||
background: #f0f2f5;
|
||||
transition: margin-left 0.2s;
|
||||
}
|
||||
|
||||
/* 折叠状态 */
|
||||
.ant-layout-sider-collapsed + .ant-layout .layout-header {
|
||||
left: 80px;
|
||||
}
|
||||
|
||||
.ant-layout-sider-collapsed + .ant-layout .layout-content {
|
||||
margin-left: 80px;
|
||||
}
|
||||
</style>
|
||||
@ -6,8 +6,26 @@ import './style.css'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
// 基本的全局错误处理
|
||||
window.addEventListener('error', (event) => {
|
||||
console.error('JavaScript错误:', event.error)
|
||||
})
|
||||
|
||||
window.addEventListener('unhandledrejection', (event) => {
|
||||
console.error('Promise拒绝:', event.reason)
|
||||
event.preventDefault()
|
||||
})
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(createPinia())
|
||||
|
||||
// Vue应用错误处理
|
||||
app.config.errorHandler = (err, instance, info) => {
|
||||
console.error('Vue错误:', err, info)
|
||||
}
|
||||
|
||||
const pinia = createPinia()
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.use(Antd)
|
||||
|
||||
app.mount('#app')
|
||||
@ -5,7 +5,8 @@ const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('@/views/login/index.vue')
|
||||
component: () => import('@/views/login/index.vue'),
|
||||
meta: { title: '用户登录' }
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
@ -46,7 +47,9 @@ const routes = [
|
||||
}
|
||||
]
|
||||
|
||||
export default createRouter({
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
})
|
||||
})
|
||||
|
||||
export default router
|
||||
@ -1,9 +1,24 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const token = ref(localStorage.getItem('token') || '')
|
||||
const user = ref(JSON.parse(localStorage.getItem('user') || '{}'))
|
||||
const user = ref(null)
|
||||
|
||||
// 初始化用户信息
|
||||
try {
|
||||
const storedUser = localStorage.getItem('user')
|
||||
if (storedUser) {
|
||||
user.value = JSON.parse(storedUser)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析用户信息失败:', error)
|
||||
user.value = null
|
||||
}
|
||||
|
||||
const isLoggedIn = computed(() => {
|
||||
return !!token.value && !!user.value
|
||||
})
|
||||
|
||||
const login = (userData, userToken) => {
|
||||
user.value = userData
|
||||
@ -13,21 +28,17 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
user.value = {}
|
||||
user.value = null
|
||||
token.value = ''
|
||||
localStorage.removeItem('user')
|
||||
localStorage.removeItem('token')
|
||||
}
|
||||
|
||||
const isLoggedIn = () => {
|
||||
return !!token.value
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
token,
|
||||
isLoggedIn,
|
||||
login,
|
||||
logout,
|
||||
isLoggedIn
|
||||
logout
|
||||
}
|
||||
})
|
||||
@ -1,124 +1,122 @@
|
||||
<template>
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-xl font-semibold">批量执行</h2>
|
||||
|
||||
<a-card title="执行配置">
|
||||
<a-form layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="选择脚本" required>
|
||||
<a-select
|
||||
v-model:value="executeForm.scriptId"
|
||||
placeholder="请选择要执行的脚本"
|
||||
@change="onScriptChange"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="script in scripts"
|
||||
:key="script.id"
|
||||
:value="script.id"
|
||||
>
|
||||
<a-card>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">批量执行脚本</h2>
|
||||
<a-button type="primary" @click="executeScript" :loading="executing">
|
||||
⚡ 执行脚本
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-row :gutter="24">
|
||||
<a-col :span="12">
|
||||
<a-form layout="vertical">
|
||||
<a-form-item label="选择脚本">
|
||||
<a-select v-model:value="selectedScript" placeholder="请选择要执行的脚本">
|
||||
<a-select-option v-for="script in scripts" :key="script.id" :value="script.id">
|
||||
{{ script.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="选择服务器" required>
|
||||
<a-select
|
||||
v-model:value="executeForm.serverIds"
|
||||
mode="multiple"
|
||||
placeholder="请选择目标服务器"
|
||||
:options="serverOptions"
|
||||
|
||||
<a-form-item label="选择服务器">
|
||||
<a-transfer
|
||||
v-model:target-keys="selectedServers"
|
||||
:data-source="serverOptions"
|
||||
:titles="['可选服务器', '已选服务器']"
|
||||
:render="item => item.title"
|
||||
:list-style="{ width: '100%', height: '300px' }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</a-col>
|
||||
|
||||
<a-form-item v-if="selectedScript" label="脚本预览">
|
||||
<div class="bg-gray-100 p-4 rounded">
|
||||
<div class="text-sm text-gray-600 mb-2">
|
||||
{{ selectedScript.name }} ({{ getTypeName(selectedScript.type) }})
|
||||
<a-col :span="12">
|
||||
<div class="h-full">
|
||||
<h3 class="mb-2">脚本预览</h3>
|
||||
<div v-if="currentScript" class="border rounded p-4 bg-gray-50">
|
||||
<h4 class="font-semibold">{{ currentScript.name }}</h4>
|
||||
<p class="text-gray-600 mb-2">{{ currentScript.description }}</p>
|
||||
<pre class="bg-white p-3 rounded border text-sm overflow-auto max-h-60">{{ currentScript.content }}</pre>
|
||||
</div>
|
||||
<div v-else class="border rounded p-4 bg-gray-50 text-center text-gray-500">
|
||||
请选择要执行的脚本
|
||||
</div>
|
||||
<pre class="text-sm overflow-auto max-h-32">{{ selectedScript.content }}</pre>
|
||||
</div>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="executeScript"
|
||||
:loading="executing"
|
||||
:disabled="!executeForm.scriptId || !executeForm.serverIds.length"
|
||||
>
|
||||
<PlayCircleOutlined />
|
||||
执行脚本
|
||||
</a-button>
|
||||
<a-button @click="resetForm">重置</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 执行结果 -->
|
||||
<a-card v-if="executeResults.length" title="执行结果">
|
||||
<a-table
|
||||
:data-source="executeResults"
|
||||
:columns="resultColumns"
|
||||
row-key="serverId"
|
||||
:pagination="false"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'success' ? 'green' : record.status === 'error' ? 'red' : 'blue'">
|
||||
{{ getStatusName(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'output'">
|
||||
<a-button size="small" @click="viewOutput(record)">查看输出</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 历史记录 -->
|
||||
<!-- 执行历史 -->
|
||||
<a-card title="执行历史">
|
||||
<a-table
|
||||
:data-source="history"
|
||||
:columns="historyColumns"
|
||||
:loading="historyLoading"
|
||||
:data-source="executions"
|
||||
:columns="executionColumns"
|
||||
row-key="id"
|
||||
:loading="historyLoading"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'success' ? 'green' : 'red'">
|
||||
{{ record.status === 'success' ? '成功' : '失败' }}
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-button size="small" @click="viewResult(record)">
|
||||
👁️ 查看结果
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 输出详情弹窗 -->
|
||||
<!-- 执行结果对话框 -->
|
||||
<a-modal
|
||||
v-model:open="showOutputModal"
|
||||
title="执行输出"
|
||||
v-model:open="showResultModal"
|
||||
title="执行结果"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
width="1000px"
|
||||
>
|
||||
<div v-if="outputData">
|
||||
<a-descriptions :column="1" bordered>
|
||||
<a-descriptions-item label="服务器">{{ outputData.serverName }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="outputData.status === 'success' ? 'green' : 'red'">
|
||||
{{ getStatusName(outputData.status) }}
|
||||
<div v-if="executionResult">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="脚本名称">{{ executionResult.script_name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="执行时间">{{ executionResult.created_at }}</a-descriptions-item>
|
||||
<a-descriptions-item label="总计服务器">{{ executionResult.total_servers }}</a-descriptions-item>
|
||||
<a-descriptions-item label="执行状态">
|
||||
<a-tag :color="getStatusColor(executionResult.status)">
|
||||
{{ getStatusText(executionResult.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行时间">{{ outputData.duration }}ms</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="mb-2">输出内容:</h4>
|
||||
<pre class="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-96">{{ outputData.output }}</pre>
|
||||
<h4 class="mb-2">执行详情</h4>
|
||||
<a-collapse>
|
||||
<a-collapse-panel
|
||||
v-for="result in executionResult.results"
|
||||
:key="result.server_id"
|
||||
:header="`${result.server_name} (${result.server_ip})`"
|
||||
>
|
||||
<a-descriptions :column="1" size="small">
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="result.success ? 'green' : 'red'">
|
||||
{{ result.success ? '成功' : '失败' }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="执行时长">{{ result.execution_time }}s</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<div class="mt-2">
|
||||
<h5>输出结果:</h5>
|
||||
<pre class="bg-gray-100 p-3 rounded text-sm overflow-auto max-h-40">{{ result.output || '无输出' }}</pre>
|
||||
</div>
|
||||
|
||||
<div v-if="result.error" class="mt-2">
|
||||
<h5>错误信息:</h5>
|
||||
<pre class="bg-red-50 p-3 rounded text-sm overflow-auto max-h-40 text-red-700">{{ result.error }}</pre>
|
||||
</div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
@ -126,86 +124,65 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlayCircleOutlined } from '@ant-design/icons-vue'
|
||||
import api from '@/api'
|
||||
|
||||
const executing = ref(false)
|
||||
const historyLoading = ref(false)
|
||||
const showOutputModal = ref(false)
|
||||
const showResultModal = ref(false)
|
||||
|
||||
const scripts = ref([])
|
||||
const servers = ref([])
|
||||
const history = ref([])
|
||||
const executeResults = ref([])
|
||||
const outputData = ref(null)
|
||||
const executions = ref([])
|
||||
const executionResult = ref(null)
|
||||
|
||||
const executeForm = ref({
|
||||
scriptId: null,
|
||||
serverIds: []
|
||||
})
|
||||
|
||||
const selectedScript = computed(() => {
|
||||
return scripts.value.find(s => s.id === executeForm.value.scriptId)
|
||||
})
|
||||
const selectedScript = ref(null)
|
||||
const selectedServers = ref([])
|
||||
|
||||
const serverOptions = computed(() => {
|
||||
return servers.value.map(server => ({
|
||||
label: `${server.name} (${server.ip})`,
|
||||
value: server.id
|
||||
key: server.id,
|
||||
title: `${server.name} (${server.host})`,
|
||||
description: server.description
|
||||
}))
|
||||
})
|
||||
|
||||
const resultColumns = [
|
||||
{ title: '服务器', dataIndex: 'serverName', key: 'serverName' },
|
||||
const currentScript = computed(() => {
|
||||
return scripts.value.find(s => s.id === selectedScript.value)
|
||||
})
|
||||
|
||||
const executionColumns = [
|
||||
{ title: '脚本名称', dataIndex: 'script_name', key: 'script_name' },
|
||||
{ title: '执行时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '服务器数量', dataIndex: 'total_servers', key: 'total_servers' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '执行时间', dataIndex: 'duration', key: 'duration' },
|
||||
{ title: '输出', key: 'output' }
|
||||
{ title: '操作', key: 'action', width: 120 }
|
||||
]
|
||||
|
||||
const historyColumns = [
|
||||
{ title: '脚本名称', dataIndex: 'scriptName', key: 'scriptName' },
|
||||
{ title: '服务器数量', dataIndex: 'serverCount', key: 'serverCount' },
|
||||
{ title: '执行时间', dataIndex: 'executeTime', key: 'executeTime' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' }
|
||||
]
|
||||
|
||||
const getTypeName = (type) => {
|
||||
const nameMap = {
|
||||
shell: 'Shell脚本',
|
||||
python: 'Python脚本',
|
||||
sql: 'SQL脚本'
|
||||
}
|
||||
return nameMap[type] || type
|
||||
}
|
||||
|
||||
const getStatusName = (status) => {
|
||||
const nameMap = {
|
||||
success: '成功',
|
||||
error: '失败',
|
||||
running: '执行中'
|
||||
}
|
||||
return nameMap[status] || status
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
const loadScripts = async () => {
|
||||
try {
|
||||
const [scriptsData, serversData] = await Promise.all([
|
||||
api.getScripts(),
|
||||
api.getServers()
|
||||
])
|
||||
scripts.value = scriptsData
|
||||
servers.value = serversData.filter(server => server.status === 'online')
|
||||
const response = await api.get('/scripts')
|
||||
scripts.value = response.data.data
|
||||
} catch (error) {
|
||||
message.error('加载数据失败')
|
||||
message.error('加载脚本列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadHistory = async () => {
|
||||
const loadServers = async () => {
|
||||
try {
|
||||
const response = await api.get('/servers')
|
||||
servers.value = response.data.data.filter(server => server.status === 'online')
|
||||
} catch (error) {
|
||||
message.error('加载服务器列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadExecutions = async () => {
|
||||
historyLoading.value = true
|
||||
try {
|
||||
const data = await api.getExecuteHistory()
|
||||
history.value = data
|
||||
const response = await api.get('/execute/history')
|
||||
executions.value = response.data.data
|
||||
} catch (error) {
|
||||
message.error('加载执行历史失败')
|
||||
} finally {
|
||||
@ -213,23 +190,30 @@ const loadHistory = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const onScriptChange = () => {
|
||||
// 脚本变更时的处理逻辑
|
||||
}
|
||||
|
||||
const executeScript = async () => {
|
||||
executing.value = true
|
||||
executeResults.value = []
|
||||
if (!selectedScript.value) {
|
||||
message.warning('请选择要执行的脚本')
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedServers.value.length === 0) {
|
||||
message.warning('请选择要执行的服务器')
|
||||
return
|
||||
}
|
||||
|
||||
executing.value = true
|
||||
try {
|
||||
const response = await api.executeScript({
|
||||
scriptId: executeForm.value.scriptId,
|
||||
serverIds: executeForm.value.serverIds
|
||||
const response = await api.post('/execute', {
|
||||
script_id: selectedScript.value,
|
||||
server_ids: selectedServers.value
|
||||
})
|
||||
|
||||
executeResults.value = response.results
|
||||
message.success(`脚本执行完成,成功: ${response.successCount},失败: ${response.failCount}`)
|
||||
loadHistory()
|
||||
message.success('脚本执行完成')
|
||||
loadExecutions()
|
||||
|
||||
// 重置选择
|
||||
selectedScript.value = null
|
||||
selectedServers.value = []
|
||||
} catch (error) {
|
||||
message.error('脚本执行失败')
|
||||
} finally {
|
||||
@ -237,21 +221,39 @@ const executeScript = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const viewOutput = (record) => {
|
||||
outputData.value = record
|
||||
showOutputModal.value = true
|
||||
const viewResult = async (record) => {
|
||||
try {
|
||||
const response = await api.get(`/execute/${record.id}/result`)
|
||||
executionResult.value = response.data.data
|
||||
showResultModal.value = true
|
||||
} catch (error) {
|
||||
message.error('加载执行结果失败')
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
executeForm.value = {
|
||||
scriptId: null,
|
||||
serverIds: []
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
'running': 'blue',
|
||||
'completed': 'green',
|
||||
'failed': 'red',
|
||||
'partial': 'orange'
|
||||
}
|
||||
executeResults.value = []
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
'running': '执行中',
|
||||
'completed': '已完成',
|
||||
'failed': '执行失败',
|
||||
'partial': '部分成功'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadHistory()
|
||||
loadScripts()
|
||||
loadServers()
|
||||
loadExecutions()
|
||||
})
|
||||
</script>
|
||||
@ -1,53 +1,59 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center">
|
||||
<div class="bg-white rounded-lg shadow-lg p-8 w-96">
|
||||
<div class="text-center mb-8">
|
||||
<h1 class="text-2xl font-bold text-gray-800">服务器监控平台</h1>
|
||||
<p class="text-gray-600 mt-2">请登录您的账户</p>
|
||||
<div class="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div class="max-w-md w-full space-y-8">
|
||||
<div class="text-center">
|
||||
<h2 class="text-3xl font-bold text-gray-900">服务器监控平台</h2>
|
||||
<p class="mt-2 text-sm text-gray-600">请登录您的账户</p>
|
||||
</div>
|
||||
|
||||
<a-form
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
@finish="handleLogin"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item name="username" label="用户名">
|
||||
<a-input
|
||||
v-model:value="loginForm.username"
|
||||
placeholder="请输入用户名"
|
||||
size="large"
|
||||
prefix="<UserOutlined />"
|
||||
/>
|
||||
</a-form-item>
|
||||
<div class="bg-white p-8 rounded-lg shadow-lg">
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">用户名</label>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
placeholder="请输入用户名"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input v-model="remember" type="checkbox" id="remember" class="mr-2" />
|
||||
<label for="remember" class="text-sm text-gray-600">记住我</label>
|
||||
</div>
|
||||
|
||||
<div v-if="errorMessage" class="text-red-600 text-sm text-center">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="w-full py-2 px-4 bg-blue-600 hover:bg-blue-700 text-white font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<a-form-item name="password" label="密码">
|
||||
<a-input-password
|
||||
v-model:value="loginForm.password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
prefix="<LockOutlined />"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-checkbox v-model:checked="loginForm.remember">
|
||||
记住密码
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
size="large"
|
||||
:loading="loading"
|
||||
class="w-full"
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="mt-6 text-center text-xs text-gray-500">
|
||||
<p>测试账户:admin / admin123</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -55,35 +61,43 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue'
|
||||
import api from '@/api'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const loginForm = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
remember: false
|
||||
})
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const remember = ref(false)
|
||||
const errorMessage = ref('')
|
||||
|
||||
const rules = {
|
||||
username: [{ required: true, message: '请输入用户名' }],
|
||||
password: [{ required: true, message: '请输入密码' }]
|
||||
}
|
||||
|
||||
const handleLogin = async (values) => {
|
||||
const handleLogin = async () => {
|
||||
if (!username.value || !password.value) {
|
||||
errorMessage.value = '请填写用户名和密码'
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
errorMessage.value = ''
|
||||
|
||||
try {
|
||||
const response = await api.login(values)
|
||||
authStore.login(response.user, response.token)
|
||||
message.success('登录成功')
|
||||
router.push('/')
|
||||
// 模拟登录验证
|
||||
if (username.value === 'admin' && password.value === 'admin123') {
|
||||
const userData = { username: username.value, role: 'admin' }
|
||||
const token = 'token-' + Date.now()
|
||||
|
||||
// 保存登录状态
|
||||
authStore.login(userData, token)
|
||||
|
||||
// 登录成功,跳转到控制台
|
||||
router.push('/dashboard')
|
||||
} else {
|
||||
errorMessage.value = '用户名或密码错误!请使用 admin / admin123'
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('登录失败,请检查用户名和密码')
|
||||
errorMessage.value = '登录失败,请稍后重试'
|
||||
console.error('登录错误:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
@ -1,149 +1,127 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">脚本管理</h2>
|
||||
<a-button type="primary" @click="showModal = true">
|
||||
<PlusOutlined />
|
||||
添加脚本
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:data-source="scripts"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'type'">
|
||||
<a-tag :color="getTypeColor(record.type)">
|
||||
{{ getTypeName(record.type) }}
|
||||
</a-tag>
|
||||
<div class="space-y-6">
|
||||
<a-card>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">脚本管理</h2>
|
||||
<a-button type="primary" @click="showModal = true">
|
||||
➕ 新建脚本
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:data-source="scripts"
|
||||
:columns="columns"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewScript(record)">
|
||||
👁️ 查看
|
||||
</a-button>
|
||||
<a-button size="small" @click="editScript(record)">
|
||||
✏️ 编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个脚本吗?"
|
||||
@confirm="deleteScript(record.id)"
|
||||
>
|
||||
<a-button size="small" danger>
|
||||
🗑️ 删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="viewScript(record)">查看</a-button>
|
||||
<a-button size="small" @click="editScript(record)">编辑</a-button>
|
||||
<a-button size="small" danger @click="deleteScript(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑脚本弹窗 -->
|
||||
<!-- 新建/编辑脚本对话框 -->
|
||||
<a-modal
|
||||
v-model:open="showModal"
|
||||
:title="isEdit ? '编辑脚本' : '添加脚本'"
|
||||
:title="editingScript ? '编辑脚本' : '新建脚本'"
|
||||
@ok="handleSubmit"
|
||||
@cancel="resetForm"
|
||||
width="800px"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="脚本名称" required>
|
||||
<a-input v-model:value="form.name" placeholder="请输入脚本名称" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="脚本类型" required>
|
||||
<a-select v-model:value="form.type" placeholder="选择脚本类型">
|
||||
<a-select-option value="shell">Shell</a-select-option>
|
||||
<a-select-option value="python">Python</a-select-option>
|
||||
<a-select-option value="sql">SQL</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item label="脚本内容" required>
|
||||
<a-textarea
|
||||
v-model:value="form.content"
|
||||
placeholder="请输入脚本内容"
|
||||
:rows="10"
|
||||
style="font-family: 'Courier New', monospace;"
|
||||
/>
|
||||
<a-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="脚本名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入脚本名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea v-model:value="form.description" placeholder="脚本描述" />
|
||||
<a-textarea v-model:value="formData.description" placeholder="请输入脚本描述" />
|
||||
</a-form-item>
|
||||
<a-form-item label="脚本内容" name="content">
|
||||
<a-textarea
|
||||
v-model:value="formData.content"
|
||||
placeholder="请输入脚本内容"
|
||||
:rows="15"
|
||||
style="font-family: monospace;"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看脚本弹窗 -->
|
||||
<!-- 查看脚本对话框 -->
|
||||
<a-modal
|
||||
v-model:open="showViewModal"
|
||||
title="查看脚本"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
>
|
||||
<div v-if="viewData">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="脚本名称">{{ viewData.name }}</a-descriptions-item>
|
||||
<a-descriptions-item label="脚本类型">
|
||||
<a-tag :color="getTypeColor(viewData.type)">{{ getTypeName(viewData.type) }}</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="创建时间" :span="2">{{ viewData.createdAt }}</a-descriptions-item>
|
||||
<a-descriptions-item label="描述" :span="2">{{ viewData.description || '无' }}</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div class="mt-4">
|
||||
<h4 class="mb-2">脚本内容:</h4>
|
||||
<pre class="bg-gray-100 p-4 rounded text-sm overflow-auto max-h-96">{{ viewData.content }}</pre>
|
||||
</div>
|
||||
<div v-if="viewingScript">
|
||||
<h3>{{ viewingScript.name }}</h3>
|
||||
<p>{{ viewingScript.description }}</p>
|
||||
<pre class="bg-gray-100 p-4 rounded border text-sm overflow-auto max-h-96">{{ viewingScript.content }}</pre>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import api from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const showModal = ref(false)
|
||||
const showViewModal = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editingScript = ref(null)
|
||||
const viewingScript = ref(null)
|
||||
const formRef = ref()
|
||||
|
||||
const scripts = ref([])
|
||||
const viewData = ref(null)
|
||||
const form = ref({
|
||||
name: '',
|
||||
type: '',
|
||||
content: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '脚本名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{ title: '操作', key: 'action' }
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '更新时间', dataIndex: 'updated_at', key: 'updated_at' },
|
||||
{ title: '操作', key: 'action', width: 200 }
|
||||
]
|
||||
|
||||
const getTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
shell: 'blue',
|
||||
python: 'green',
|
||||
sql: 'orange'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
description: '',
|
||||
content: ''
|
||||
})
|
||||
|
||||
const getTypeName = (type) => {
|
||||
const nameMap = {
|
||||
shell: 'Shell脚本',
|
||||
python: 'Python脚本',
|
||||
sql: 'SQL脚本'
|
||||
}
|
||||
return nameMap[type] || type
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入脚本名称', trigger: 'blur' }],
|
||||
content: [{ required: true, message: '请输入脚本内容', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const loadScripts = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.getScripts()
|
||||
scripts.value = data
|
||||
const response = await api.get('/scripts')
|
||||
scripts.value = response.data.data
|
||||
} catch (error) {
|
||||
message.error('加载脚本列表失败')
|
||||
} finally {
|
||||
@ -151,52 +129,56 @@ const loadScripts = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const viewScript = (script) => {
|
||||
viewData.value = script
|
||||
showViewModal.value = true
|
||||
}
|
||||
|
||||
const editScript = (script) => {
|
||||
isEdit.value = true
|
||||
form.value = { ...script }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await api.updateScript(form.value.id, form.value)
|
||||
await formRef.value.validateFields()
|
||||
|
||||
if (editingScript.value) {
|
||||
await api.put(`/scripts/${editingScript.value.id}`, formData)
|
||||
message.success('更新脚本成功')
|
||||
} else {
|
||||
await api.createScript(form.value)
|
||||
message.success('添加脚本成功')
|
||||
await api.post('/scripts', formData)
|
||||
message.success('创建脚本成功')
|
||||
}
|
||||
showModal.value = false
|
||||
|
||||
resetForm()
|
||||
loadScripts()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
if (error.errorFields) return
|
||||
message.error(editingScript.value ? '更新脚本失败' : '创建脚本失败')
|
||||
}
|
||||
}
|
||||
|
||||
const editScript = (record) => {
|
||||
editingScript.value = record
|
||||
Object.assign(formData, record)
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const viewScript = (record) => {
|
||||
viewingScript.value = record
|
||||
showViewModal.value = true
|
||||
}
|
||||
|
||||
const deleteScript = async (id) => {
|
||||
try {
|
||||
await api.deleteScript(id)
|
||||
await api.delete(`/scripts/${id}`)
|
||||
message.success('删除脚本成功')
|
||||
loadScripts()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
message.error('删除脚本失败')
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
isEdit.value = false
|
||||
form.value = {
|
||||
editingScript.value = null
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
type: '',
|
||||
content: '',
|
||||
description: ''
|
||||
}
|
||||
description: '',
|
||||
content: ''
|
||||
})
|
||||
showModal.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@ -1,56 +1,77 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">服务器管理</h2>
|
||||
<a-button type="primary" @click="showModal = true">
|
||||
<PlusOutlined />
|
||||
添加服务器
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:data-source="servers"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'online' ? 'green' : 'red'">
|
||||
{{ record.status === 'online' ? '在线' : '离线' }}
|
||||
</a-tag>
|
||||
<div class="space-y-6">
|
||||
<a-card>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">服务器管理</h2>
|
||||
<a-button type="primary" @click="showModal = true">
|
||||
➕ 添加服务器
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:data-source="servers"
|
||||
:columns="columns"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'online' ? 'green' : 'red'">
|
||||
{{ record.status === 'online' ? '在线' : '离线' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="testConnection(record)">
|
||||
🔄 测试连接
|
||||
</a-button>
|
||||
<a-button size="small" @click="editServer(record)">
|
||||
✏️ 编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这台服务器吗?"
|
||||
@confirm="deleteServer(record.id)"
|
||||
>
|
||||
<a-button size="small" danger>
|
||||
🗑️ 删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="editServer(record)">编辑</a-button>
|
||||
<a-button size="small" danger @click="deleteServer(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑服务器弹窗 -->
|
||||
<!-- 添加/编辑服务器对话框 -->
|
||||
<a-modal
|
||||
v-model:open="showModal"
|
||||
:title="isEdit ? '编辑服务器' : '添加服务器'"
|
||||
:title="editingServer ? '编辑服务器' : '添加服务器'"
|
||||
@ok="handleSubmit"
|
||||
@cancel="resetForm"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="服务器名称" required>
|
||||
<a-input v-model:value="form.name" placeholder="请输入服务器名称" />
|
||||
<a-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="服务器名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入服务器名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="IP地址" required>
|
||||
<a-input v-model:value="form.ip" placeholder="请输入IP地址" />
|
||||
<a-form-item label="IP地址" name="ip">
|
||||
<a-input v-model:value="formData.ip" placeholder="请输入IP地址" />
|
||||
</a-form-item>
|
||||
<a-form-item label="SSH端口">
|
||||
<a-input-number v-model:value="form.port" :min="1" :max="65535" />
|
||||
<a-form-item label="SSH端口" name="port">
|
||||
<a-input-number v-model:value="formData.port" :min="1" :max="65535" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="用户名">
|
||||
<a-input v-model:value="form.username" placeholder="请输入SSH用户名" />
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="formData.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<a-form-item label="密码" name="password">
|
||||
<a-input-password v-model:value="formData.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="描述">
|
||||
<a-textarea v-model:value="form.description" placeholder="服务器描述" />
|
||||
<a-textarea v-model:value="formData.description" placeholder="请输入描述" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
@ -58,22 +79,16 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import api from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const showModal = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editingServer = ref(null)
|
||||
const formRef = ref()
|
||||
|
||||
const servers = ref([])
|
||||
const form = ref({
|
||||
name: '',
|
||||
ip: '',
|
||||
port: 22,
|
||||
username: 'root',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '服务器名称', dataIndex: 'name', key: 'name' },
|
||||
@ -82,14 +97,31 @@ const columns = [
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description' },
|
||||
{ title: '操作', key: 'action' }
|
||||
{ title: '操作', key: 'action', width: 200 }
|
||||
]
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
ip: '',
|
||||
port: 22,
|
||||
username: '',
|
||||
password: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入服务器名称', trigger: 'blur' }],
|
||||
ip: [{ required: true, message: '请输入IP地址', trigger: 'blur' }],
|
||||
port: [{ required: true, message: '请输入端口', trigger: 'blur' }],
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const loadServers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.getServers()
|
||||
servers.value = data
|
||||
const response = await api.getServers()
|
||||
servers.value = response.data || response
|
||||
} catch (error) {
|
||||
message.error('加载服务器列表失败')
|
||||
} finally {
|
||||
@ -97,48 +129,75 @@ const loadServers = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const editServer = (server) => {
|
||||
isEdit.value = true
|
||||
form.value = { ...server }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await api.updateServer(form.value.id, form.value)
|
||||
await formRef.value.validateFields()
|
||||
|
||||
// 准备提交数据
|
||||
const submitData = {
|
||||
name: formData.name,
|
||||
ip: formData.ip,
|
||||
port: formData.port,
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
description: formData.description
|
||||
}
|
||||
|
||||
if (editingServer.value) {
|
||||
await api.updateServer(editingServer.value.id, submitData)
|
||||
message.success('更新服务器成功')
|
||||
} else {
|
||||
await api.createServer(form.value)
|
||||
await api.createServer(submitData)
|
||||
message.success('添加服务器成功')
|
||||
}
|
||||
showModal.value = false
|
||||
|
||||
resetForm()
|
||||
loadServers()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
if (error.errorFields) return
|
||||
message.error(editingServer.value ? '更新服务器失败' : '添加服务器失败')
|
||||
console.error('服务器操作错误:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const editServer = (record) => {
|
||||
editingServer.value = record
|
||||
Object.assign(formData, record)
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const deleteServer = async (id) => {
|
||||
try {
|
||||
await api.deleteServer(id)
|
||||
await api.delete(`/servers/${id}`)
|
||||
message.success('删除服务器成功')
|
||||
loadServers()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
message.error('删除服务器失败')
|
||||
}
|
||||
}
|
||||
|
||||
const testConnection = async (record) => {
|
||||
try {
|
||||
await api.post(`/servers/${record.id}/test`)
|
||||
message.success('连接测试成功')
|
||||
loadServers()
|
||||
} catch (error) {
|
||||
message.error('连接测试失败')
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
isEdit.value = false
|
||||
form.value = {
|
||||
editingServer.value = null
|
||||
Object.assign(formData, {
|
||||
name: '',
|
||||
ip: '',
|
||||
port: 22,
|
||||
username: 'root',
|
||||
username: '',
|
||||
password: '',
|
||||
description: ''
|
||||
}
|
||||
})
|
||||
showModal.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@ -1,144 +1,208 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold">用户管理</h2>
|
||||
<a-button type="primary" @click="showModal = true">
|
||||
<PlusOutlined />
|
||||
添加用户
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="space-y-6">
|
||||
<a-card>
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-semibold">用户管理</h2>
|
||||
<a-button type="primary" @click="showModal = true">
|
||||
➕ 添加用户
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:data-source="users"
|
||||
:columns="columns"
|
||||
row-key="id"
|
||||
:loading="loading"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'role'">
|
||||
<a-tag :color="getRoleColor(record.role)">
|
||||
{{ getRoleText(record.role) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'is_active'">
|
||||
<a-tag :color="record.is_active ? 'green' : 'red'">
|
||||
{{ record.is_active ? '启用' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="editUser(record)">
|
||||
✏️ 编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:type="record.is_active ? 'default' : 'primary'"
|
||||
@click="toggleUserStatus(record)"
|
||||
>
|
||||
{{ record.is_active ? '🚫 禁用' : '✅ 启用' }}
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个用户吗?"
|
||||
@confirm="deleteUser(record.id)"
|
||||
>
|
||||
<a-button size="small" danger>
|
||||
🗑️ 删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-table
|
||||
:data-source="users"
|
||||
:columns="columns"
|
||||
:loading="loading"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'role'">
|
||||
<a-tag :color="getRoleColor(record.role)">
|
||||
{{ getRoleName(record.role) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
|
||||
{{ record.status === 'active' ? '正常' : '禁用' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button size="small" @click="editUser(record)">编辑</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
:type="record.status === 'active' ? 'default' : 'primary'"
|
||||
@click="toggleUserStatus(record)"
|
||||
>
|
||||
{{ record.status === 'active' ? '禁用' : '启用' }}
|
||||
</a-button>
|
||||
<a-button size="small" danger @click="deleteUser(record.id)">删除</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
|
||||
<!-- 添加/编辑用户弹窗 -->
|
||||
<!-- 添加/编辑用户对话框 -->
|
||||
<a-modal
|
||||
v-model:open="showModal"
|
||||
:title="isEdit ? '编辑用户' : '添加用户'"
|
||||
:title="editingUser ? '编辑用户' : '添加用户'"
|
||||
@ok="handleSubmit"
|
||||
@cancel="resetForm"
|
||||
>
|
||||
<a-form :model="form" layout="vertical">
|
||||
<a-form-item label="用户名" required>
|
||||
<a-input
|
||||
v-model:value="form.username"
|
||||
<a-form
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
ref="formRef"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input
|
||||
v-model:value="formData.username"
|
||||
placeholder="请输入用户名"
|
||||
:disabled="isEdit"
|
||||
:disabled="!!editingUser"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="!isEdit" label="密码" required>
|
||||
<a-input-password v-model:value="form.password" placeholder="请输入密码" />
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="姓名" required>
|
||||
<a-input v-model:value="form.realName" placeholder="请输入真实姓名" />
|
||||
<a-form-item v-if="!editingUser" label="密码" name="password">
|
||||
<a-input-password v-model:value="formData.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱">
|
||||
<a-input v-model:value="form.email" placeholder="请输入邮箱地址" />
|
||||
<a-form-item v-if="!editingUser" label="确认密码" name="confirm_password">
|
||||
<a-input-password v-model:value="formData.confirm_password" placeholder="请再次输入密码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色" required>
|
||||
<a-select v-model:value="form.role" placeholder="选择用户角色">
|
||||
<a-form-item label="角色" name="role">
|
||||
<a-select v-model:value="formData.role" placeholder="请选择角色">
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
<a-select-option value="operator">操作员</a-select-option>
|
||||
<a-select-option value="viewer">查看员</a-select-option>
|
||||
<a-select-option value="viewer">查看者</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="权限">
|
||||
<a-checkbox-group v-model:value="form.permissions">
|
||||
<a-checkbox value="server.view">查看服务器</a-checkbox>
|
||||
<a-checkbox value="server.manage">管理服务器</a-checkbox>
|
||||
<a-checkbox value="script.view">查看脚本</a-checkbox>
|
||||
<a-checkbox value="script.manage">管理脚本</a-checkbox>
|
||||
<a-checkbox value="execute.run">执行脚本</a-checkbox>
|
||||
<a-checkbox value="user.manage">用户管理</a-checkbox>
|
||||
</a-checkbox-group>
|
||||
<a-form-item label="状态">
|
||||
<a-switch
|
||||
v-model:checked="formData.is_active"
|
||||
checked-children="启用"
|
||||
un-checked-children="禁用"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 权限说明 -->
|
||||
<a-card title="角色权限说明">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<div class="text-center p-4 border rounded">
|
||||
<a-tag color="red" class="mb-2">管理员</a-tag>
|
||||
<div class="text-sm text-gray-600">
|
||||
<p>• 完整的系统管理权限</p>
|
||||
<p>• 用户管理</p>
|
||||
<p>• 服务器管理</p>
|
||||
<p>• 脚本管理</p>
|
||||
<p>• 批量执行</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="text-center p-4 border rounded">
|
||||
<a-tag color="orange" class="mb-2">操作员</a-tag>
|
||||
<div class="text-sm text-gray-600">
|
||||
<p>• 服务器管理</p>
|
||||
<p>• 脚本管理</p>
|
||||
<p>• 批量执行</p>
|
||||
<p>• 查看用户列表</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="8">
|
||||
<div class="text-center p-4 border rounded">
|
||||
<a-tag color="green" class="mb-2">查看者</a-tag>
|
||||
<div class="text-sm text-gray-600">
|
||||
<p>• 查看服务器状态</p>
|
||||
<p>• 查看脚本内容</p>
|
||||
<p>• 查看执行历史</p>
|
||||
<p>• 只读权限</p>
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import api from '@/api'
|
||||
|
||||
const loading = ref(false)
|
||||
const showModal = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const editingUser = ref(null)
|
||||
const formRef = ref()
|
||||
|
||||
const users = ref([])
|
||||
const form = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
email: '',
|
||||
role: '',
|
||||
permissions: []
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '姓名', dataIndex: 'realName', key: 'realName' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '角色', dataIndex: 'role', key: 'role' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt' },
|
||||
{ title: '操作', key: 'action' }
|
||||
{ title: '状态', dataIndex: 'is_active', key: 'is_active' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at' },
|
||||
{ title: '最后登录', dataIndex: 'last_login', key: 'last_login' },
|
||||
{ title: '操作', key: 'action', width: 200 }
|
||||
]
|
||||
|
||||
const getRoleColor = (role) => {
|
||||
const colorMap = {
|
||||
admin: 'red',
|
||||
operator: 'blue',
|
||||
viewer: 'green'
|
||||
}
|
||||
return colorMap[role] || 'default'
|
||||
}
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
role: 'viewer',
|
||||
is_active: true
|
||||
})
|
||||
|
||||
const getRoleName = (role) => {
|
||||
const nameMap = {
|
||||
admin: '管理员',
|
||||
operator: '操作员',
|
||||
viewer: '查看员'
|
||||
}
|
||||
return nameMap[role] || role
|
||||
const rules = {
|
||||
username: [
|
||||
{ required: true, message: '请输入用户名', trigger: 'blur' },
|
||||
{ min: 3, max: 20, message: '用户名长度应在3-20个字符', trigger: 'blur' }
|
||||
],
|
||||
email: [
|
||||
{ required: true, message: '请输入邮箱', trigger: 'blur' },
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度至少6位', trigger: 'blur' }
|
||||
],
|
||||
confirm_password: [
|
||||
{ required: true, message: '请确认密码', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule, value) => {
|
||||
if (value !== formData.password) {
|
||||
return Promise.reject('两次输入的密码不一致')
|
||||
}
|
||||
return Promise.resolve()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
}
|
||||
|
||||
const loadUsers = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await api.getUsers()
|
||||
users.value = data
|
||||
const response = await api.get('/users')
|
||||
users.value = response.data.data
|
||||
} catch (error) {
|
||||
message.error('加载用户列表失败')
|
||||
} finally {
|
||||
@ -146,62 +210,99 @@ const loadUsers = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
const editUser = (user) => {
|
||||
isEdit.value = true
|
||||
form.value = { ...user }
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const toggleUserStatus = async (user) => {
|
||||
try {
|
||||
const newStatus = user.status === 'active' ? 'inactive' : 'active'
|
||||
await api.updateUser(user.id, { status: newStatus })
|
||||
message.success(`用户${newStatus === 'active' ? '启用' : '禁用'}成功`)
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await api.updateUser(form.value.id, form.value)
|
||||
await formRef.value.validateFields()
|
||||
|
||||
const submitData = { ...formData }
|
||||
if (editingUser.value) {
|
||||
// 编辑时删除密码字段
|
||||
delete submitData.password
|
||||
delete submitData.confirm_password
|
||||
|
||||
await api.put(`/users/${editingUser.value.id}`, submitData)
|
||||
message.success('更新用户成功')
|
||||
} else {
|
||||
await api.createUser(form.value)
|
||||
await api.post('/users', submitData)
|
||||
message.success('添加用户成功')
|
||||
}
|
||||
showModal.value = false
|
||||
|
||||
resetForm()
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
if (error.errorFields) return
|
||||
message.error(editingUser.value ? '更新用户失败' : '添加用户失败')
|
||||
}
|
||||
}
|
||||
|
||||
const editUser = (record) => {
|
||||
editingUser.value = record
|
||||
Object.assign(formData, {
|
||||
username: record.username,
|
||||
email: record.email,
|
||||
role: record.role,
|
||||
is_active: record.is_active,
|
||||
password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
showModal.value = true
|
||||
}
|
||||
|
||||
const deleteUser = async (id) => {
|
||||
try {
|
||||
await api.deleteUser(id)
|
||||
await api.delete(`/users/${id}`)
|
||||
message.success('删除用户成功')
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('删除失败')
|
||||
message.error('删除用户失败')
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
isEdit.value = false
|
||||
form.value = {
|
||||
username: '',
|
||||
password: '',
|
||||
realName: '',
|
||||
email: '',
|
||||
role: '',
|
||||
permissions: []
|
||||
const toggleUserStatus = async (record) => {
|
||||
try {
|
||||
await api.put(`/users/${record.id}`, {
|
||||
...record,
|
||||
is_active: !record.is_active
|
||||
})
|
||||
message.success(`${record.is_active ? '禁用' : '启用'}用户成功`)
|
||||
loadUsers()
|
||||
} catch (error) {
|
||||
message.error('更新用户状态失败')
|
||||
}
|
||||
}
|
||||
|
||||
const getRoleColor = (role) => {
|
||||
const colorMap = {
|
||||
admin: 'red',
|
||||
operator: 'orange',
|
||||
viewer: 'green'
|
||||
}
|
||||
return colorMap[role] || 'default'
|
||||
}
|
||||
|
||||
const getRoleText = (role) => {
|
||||
const textMap = {
|
||||
admin: '管理员',
|
||||
operator: '操作员',
|
||||
viewer: '查看者'
|
||||
}
|
||||
return textMap[role] || role
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
editingUser.value = null
|
||||
Object.assign(formData, {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
role: 'viewer',
|
||||
is_active: true
|
||||
})
|
||||
showModal.value = false
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user