This commit is contained in:
wangqifan 2025-07-10 10:02:51 +08:00
parent bb2fcf7494
commit 0cc6c77080
20 changed files with 929 additions and 662 deletions

View File

@ -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
View File

@ -0,0 +1,3 @@
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

View File

@ -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():

View File

@ -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)

View File

@ -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
View 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("数据库重置完成!")

View File

@ -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__)

View File

@ -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

View File

@ -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__)

View File

@ -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,14 +37,15 @@ 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():
try:
data = request.get_json()
server = Server(
@ -52,20 +53,25 @@ def create_server():
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.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):
try:
server = Server.query.get_or_404(server_id)
data = request.get_json()
@ -73,14 +79,18 @@ def update_server(server_id):
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.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):
try:
server = Server.query.get_or_404(server_id)
try:
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)}'

View File

@ -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__)

View File

@ -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 }}
</a-button>
<template #overlay>
<a-menu @click="handleUserMenu">
<a-menu-item key="logout">
<LogoutOutlined />
<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-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-button>
</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') {
const handleLogout = () => {
authStore.logout()
router.push('/login')
}
}
</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>

View File

@ -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')

View File

@ -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

View File

@ -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
}
})

View File

@ -1,124 +1,122 @@
<template>
<div class="space-y-6">
<h2 class="text-xl font-semibold">批量执行</h2>
<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-card title="执行配置">
<a-form layout="vertical">
<a-row :gutter="16">
<a-row :gutter="24">
<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-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-form>
</a-col>
<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>
</div>
</a-col>
</a-row>
<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) }})
</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-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>

View File

@ -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"
<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="请输入用户名"
size="large"
prefix="<UserOutlined />"
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
/>
</a-form-item>
</div>
<a-form-item name="password" label="密码">
<a-input-password
v-model:value="loginForm.password"
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">密码</label>
<input
v-model="password"
type="password"
placeholder="请输入密码"
size="large"
prefix="<LockOutlined />"
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
/>
</a-form-item>
</div>
<a-form-item>
<a-checkbox v-model:checked="loginForm.remember">
记住密码
</a-checkbox>
</a-form-item>
<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>
<a-form-item>
<a-button
type="primary"
html-type="submit"
size="large"
:loading="loading"
class="w-full"
<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"
>
登录
</a-button>
</a-form-item>
</a-form>
{{ loading ? '登录中...' : '登录' }}
</button>
</div>
</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 () => {
if (!username.value || !password.value) {
errorMessage.value = '请填写用户名和密码'
return
}
const handleLogin = async (values) => {
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
}

View File

@ -1,149 +1,127 @@
<template>
<div class="space-y-4">
<div class="flex justify-between items-center">
<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">
<PlusOutlined />
添加脚本
新建脚本
</a-button>
</div>
<a-table
:data-source="scripts"
:columns="columns"
:loading="loading"
row-key="id"
:loading="loading"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag :color="getTypeColor(record.type)">
{{ getTypeName(record.type) }}
</a-tag>
</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-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>
</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(() => {

View File

@ -1,18 +1,18 @@
<template>
<div class="space-y-4">
<div class="flex justify-between items-center">
<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">
<PlusOutlined />
添加服务器
添加服务器
</a-button>
</div>
<a-table
:data-source="servers"
:columns="columns"
:loading="loading"
row-key="id"
:loading="loading"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
@ -20,37 +20,58 @@
{{ record.status === 'online' ? '在线' : '离线' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<template v-else-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-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>
</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(() => {

View File

@ -1,144 +1,208 @@
<template>
<div class="space-y-4">
<div class="flex justify-between items-center">
<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">
<PlusOutlined />
添加用户
添加用户
</a-button>
</div>
<a-table
:data-source="users"
:columns="columns"
:loading="loading"
row-key="id"
:loading="loading"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'role'">
<a-tag :color="getRoleColor(record.role)">
{{ getRoleName(record.role) }}
{{ getRoleText(record.role) }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
{{ record.status === 'active' ? '正常' : '禁用' }}
<template v-else-if="column.key === 'is_active'">
<a-tag :color="record.is_active ? 'green' : 'red'">
{{ record.is_active ? '启用' : '禁用' }}
</a-tag>
</template>
<template v-if="column.key === 'action'">
<template v-else-if="column.key === 'action'">
<a-space>
<a-button size="small" @click="editUser(record)">编辑</a-button>
<a-button size="small" @click="editUser(record)">
编辑
</a-button>
<a-button
size="small"
:type="record.status === 'active' ? 'default' : 'primary'"
:type="record.is_active ? 'default' : 'primary'"
@click="toggleUserStatus(record)"
>
{{ record.status === 'active' ? '禁用' : '启用' }}
{{ record.is_active ? '🚫 禁用' : '✅ 启用' }}
</a-button>
<a-button size="small" danger @click="deleteUser(record.id)">删除</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-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-form
:model="formData"
:rules="rules"
ref="formRef"
layout="vertical"
>
<a-form-item label="用户名" name="username">
<a-input
v-model:value="form.username"
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: '查看员'
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 nameMap[role] || role
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()
})