This commit is contained in:
wangqifan 2025-07-09 22:44:18 +08:00
parent 00cde4676a
commit bb2fcf7494
30 changed files with 5498 additions and 0 deletions

View File

@ -0,0 +1,320 @@
---
description:
globs:
alwaysApply: false
---
---
description: A comprehensive guide to best practices for developing RESTful APIs using Flask and Flask-RESTful, covering code organization, performance, security, and testing.
globs: *.py
---
# flask-restful Best Practices: A Comprehensive Guide
This document provides a comprehensive guide to developing RESTful APIs using Flask and Flask-RESTful, emphasizing maintainability, scalability, and performance. It covers various aspects, including code organization, common patterns, performance considerations, security, testing, and common pitfalls.
## Library Information:
- Name: flask-restful
- Tags: web, api, python, flask
## 1. Code Organization and Structure
### 1.1 Directory Structure Best Practices
A well-organized directory structure is crucial for maintainability. Here's a recommended structure:
project/
├── api/
│ ├── __init__.py
│ ├── resources/
│ │ ├── __init__.py
│ │ ├── user.py # User resource
│ │ ├── item.py # Item resource
│ ├── models/
│ │ ├── __init__.py
│ │ ├── user_model.py # User model
│ │ ├── item_model.py # Item model
│ ├── schemas/
│ │ ├── __init__.py
│ │ ├── user_schema.py # User schema (using Marshmallow)
│ │ ├── item_schema.py # Item schema
│ ├── utils/
│ │ ├── __init__.py
│ │ ├── auth.py # Authentication utilities
│ ├── app.py # Flask application initialization
│ ├── config.py # Configuration settings
├── tests/
│ ├── __init__.py
│ ├── test_user.py # Unit tests for user resource
│ ├── test_item.py # Unit tests for item resource
├── migrations/ # Alembic migrations (if using SQLAlchemy)
├── venv/ # Virtual environment
├── requirements.txt # Project dependencies
├── Pipfile # Pipenv file
├── Pipfile.lock # Pipenv lock file
├── README.md
* **api/**: Contains all API-related code.
* **api/resources/**: Defines the API resources (e.g., User, Item).
* **api/models/**: Defines the data models (e.g., User, Item).
* **api/schemas/**: Defines the serialization/deserialization schemas (using Marshmallow or similar).
* **api/utils/**: Utility functions (e.g., authentication, authorization).
* **api/app.py**: Initializes the Flask application.
* **api/config.py**: Stores configuration settings (e.g., database URI).
* **tests/**: Contains unit and integration tests.
* **migrations/**: Contains database migration scripts (if using SQLAlchemy).
* **venv/**: The virtual environment (should not be committed to version control).
* **requirements.txt**: Lists project dependencies. Alternatively `Pipfile` and `Pipfile.lock` for pipenv
* **README.md**: Project documentation.
### 1.2 File Naming Conventions
* Python files: Use snake_case (e.g., `user_model.py`, `item_resource.py`).
* Class names: Use PascalCase (e.g., `UserModel`, `ItemResource`).
* Variable names: Use snake_case (e.g., `user_id`, `item_name`).
* Constants: Use SCREAMING_SNAKE_CASE (e.g., `MAX_RETRIES`, `API_VERSION`).
### 1.3 Module Organization
* Group related resources into modules (e.g., a `user` module containing `user_model.py`, `user_resource.py`, `user_schema.py`).
* Use Blueprints to organize related views and other code. Blueprints can be registered with the app, tying all operations to it.
* Keep modules small and focused on a single responsibility.
* Use clear and descriptive module names.
### 1.4 Component Architecture
A common component architecture involves the following layers:
* **Resource Layer:** Exposes the API endpoints using `flask-restful`'s `Resource` class.
* **Service Layer:** Contains the business logic and interacts with the data models.
* **Model Layer:** Defines the data models and interacts with the database (if applicable).
* **Schema Layer:** Defines the serialization/deserialization logic using libraries like Marshmallow.
### 1.5 Code Splitting Strategies
* **Functional Decomposition:** Split code into functions based on their functionality.
* **Modular Decomposition:** Split code into modules based on related functionality (e.g., user management, item management).
* **Layered Architecture:** As described above, separate the resource, service, model, and schema layers.
## 2. Common Patterns and Anti-patterns
### 2.1 Design Patterns
* **Resource Controller:** Use `flask-restful`'s `Resource` class to define API endpoints. It provides a structured way to handle different HTTP methods for a given resource.
* **Data Access Object (DAO):** Encapsulate database access logic within DAOs to abstract the database implementation from the rest of the application.
* **Repository Pattern:** Similar to DAO, but provides a higher-level abstraction for accessing data, often used with ORMs like SQLAlchemy.
* **Serialization/Deserialization:** Use libraries like Marshmallow to handle the serialization and deserialization of data between Python objects and JSON.
* **Middleware:** Implement custom middleware to handle tasks like authentication, logging, and request validation.
* **Factory Pattern:** Used to create objects, especially resources and their dependencies, decoupling code from concrete implementations.
### 2.2 Recommended Approaches for Common Tasks
* **Input Validation:** Use Marshmallow schemas for input validation. Define the expected data types, required fields, and validation rules in the schema.
* **Authentication:** Implement authentication using JWT (JSON Web Tokens). Use libraries like `Flask-JWT-Extended` or `Authlib` to handle JWT generation, verification, and storage.
* **Authorization:** Implement authorization using roles and permissions. Use decorators to restrict access to specific endpoints based on user roles.
* **Error Handling:** Implement centralized error handling using Flask's `errorhandler` decorator. Return meaningful error messages and appropriate HTTP status codes to the client.
* **Pagination:** Implement pagination for large datasets to improve performance. Use query parameters to specify the page number and page size.
* **API Versioning:** Use URL prefixes or custom headers to version your API. This allows you to introduce breaking changes without affecting existing clients.
* **Rate Limiting:** Implement rate limiting to prevent abuse of your API. Use libraries like `Flask-Limiter` to limit the number of requests that can be made from a specific IP address within a given time period.
### 2.3 Anti-patterns and Code Smells
* **Fat Resources:** Avoid putting too much logic directly into the resource classes. Delegate business logic to service classes.
* **Tight Coupling:** Avoid tight coupling between resources and models. Use interfaces or abstract classes to decouple components.
* **Ignoring Errors:** Always handle errors gracefully and return meaningful error messages to the client.
* **Lack of Input Validation:** Failing to validate input can lead to security vulnerabilities and data corruption.
* **Hardcoding Configuration:** Avoid hardcoding configuration values in the code. Use environment variables or configuration files instead.
* **Inconsistent Naming:** Use consistent naming conventions throughout the codebase.
* **Over-engineering:** Avoid over-engineering solutions. Keep the code simple and focused on solving the specific problem.
### 2.4 State Management
Flask-RESTful is designed to be stateless. However, if you need to manage state, consider the following:
* **Session Management:** Use Flask's session management capabilities for storing user-specific data across requests.
* **Caching:** Use caching mechanisms (e.g., Redis, Memcached) for storing frequently accessed data to improve performance.
* **Database:** Store persistent state in a database.
### 2.5 Error Handling
* **Global Exception Handling:** Use `app.errorhandler` to handle exceptions globally. This provides a centralized way to catch unhandled exceptions and return appropriate error responses.
* **Custom Exceptions:** Define custom exception classes for specific error conditions. This makes it easier to handle errors in a consistent way.
* **Logging:** Log errors and exceptions to a file or a logging service. This helps with debugging and troubleshooting.
* **HTTP Status Codes:** Return appropriate HTTP status codes to indicate the success or failure of a request.
## 3. Performance Considerations
### 3.1 Optimization Techniques
* **Database Optimization:** Optimize database queries, use indexes, and consider caching database results.
* **Caching:** Use caching mechanisms (e.g., Redis, Memcached) to store frequently accessed data.
* **Gunicorn with multiple workers:** Gunicorn is a WSGI server that can run multiple worker processes to handle concurrent requests. This can significantly improve performance.
* **Asynchronous Tasks:** Use Celery or other task queues for long-running tasks to avoid blocking the main thread.
* **Code Profiling:** Use profiling tools to identify performance bottlenecks in the code.
* **Connection Pooling:** Use connection pooling to reduce the overhead of establishing database connections.
* **Avoid N+1 Query Problem:** When fetching related data, use eager loading or join queries to avoid the N+1 query problem.
### 3.2 Memory Management
* **Use Generators:** Use generators for processing large datasets to avoid loading the entire dataset into memory.
* **Close Database Connections:** Always close database connections after use to release resources.
* **Limit Data Serialization:** Avoid serializing large amounts of data unnecessarily.
* **Garbage Collection:** Be aware of Python's garbage collection mechanism and avoid creating circular references.
### 3.3 Rendering Optimization
* **Use Templates:** Use Jinja2 templates for rendering HTML content. Templates can be cached to improve performance.
* **Minimize DOM Manipulation:** Minimize DOM manipulation in the client-side JavaScript code. Use techniques like virtual DOM to improve performance.
* **Compress Responses:** Use gzip compression to reduce the size of the responses.
### 3.4 Bundle Size Optimization
* **Use a CDN:** Use a Content Delivery Network (CDN) to serve static assets like CSS, JavaScript, and images.
* **Minify CSS and JavaScript:** Minify CSS and JavaScript files to reduce their size.
* **Tree Shaking:** Use tree shaking to remove unused code from the JavaScript bundles.
### 3.5 Lazy Loading
* **Lazy Load Images:** Use lazy loading for images to improve page load time.
* **Lazy Load Modules:** Use lazy loading for modules that are not immediately needed.
## 4. Security Best Practices
### 4.1 Common Vulnerabilities
* **SQL Injection:** Occurs when user input is directly inserted into SQL queries.
* **Cross-Site Scripting (XSS):** Occurs when malicious JavaScript code is injected into the website.
* **Cross-Site Request Forgery (CSRF):** Occurs when a malicious website tricks the user into performing an action on the legitimate website.
* **Authentication and Authorization Flaws:** Occurs when authentication or authorization mechanisms are not properly implemented.
* **Denial of Service (DoS):** Occurs when an attacker floods the server with requests, making it unavailable to legitimate users.
### 4.2 Input Validation
* **Validate All Input:** Validate all user input, including query parameters, request bodies, and headers.
* **Use Whitelisting:** Use whitelisting to allow only specific characters or values. Avoid blacklisting, which can be easily bypassed.
* **Escape Output:** Escape output to prevent XSS vulnerabilities.
### 4.3 Authentication and Authorization
* **Use JWT (JSON Web Tokens):** Use JWT for authentication. JWTs are a standard way to represent claims securely between two parties.
* **Implement Role-Based Access Control (RBAC):** Implement RBAC to control access to resources based on user roles.
* **Use Strong Passwords:** Enforce strong password policies, such as minimum length, complexity, and expiration.
* **Implement Two-Factor Authentication (2FA):** Implement 2FA for added security.
* **Rate Limiting:** Apply rate limits to prevent brute-force attacks.
### 4.4 Data Protection
* **Encrypt Sensitive Data:** Encrypt sensitive data at rest and in transit.
* **Use HTTPS:** Use HTTPS to encrypt communication between the client and the server.
* **Store Passwords Securely:** Store passwords securely using a one-way hash function like bcrypt or Argon2.
* **Regularly Back Up Data:** Regularly back up data to prevent data loss.
### 4.5 Secure API Communication
* **Use HTTPS:** Always use HTTPS for API communication.
* **Validate SSL Certificates:** Validate SSL certificates to prevent man-in-the-middle attacks.
* **Use API Keys:** Use API keys to identify and authenticate clients.
* **Implement CORS:** Implement Cross-Origin Resource Sharing (CORS) to control which domains can access the API.
## 5. Testing Approaches
### 5.1 Unit Testing
* **Test Individual Components:** Unit tests should focus on testing individual components in isolation.
* **Mock Dependencies:** Use mocking to isolate the component being tested from its dependencies.
* **Test Edge Cases:** Test edge cases and boundary conditions.
* **Use Assertions:** Use assertions to verify that the code behaves as expected.
### 5.2 Integration Testing
* **Test Interactions Between Components:** Integration tests should focus on testing the interactions between components.
* **Use a Test Database:** Use a test database for integration tests.
* **Test the API Endpoints:** Test the API endpoints to ensure that they return the correct data.
### 5.3 End-to-End Testing
* **Test the Entire Application:** End-to-end tests should focus on testing the entire application, including the front-end and back-end.
* **Use a Testing Framework:** Use a testing framework like Selenium or Cypress to automate end-to-end tests.
### 5.4 Test Organization
* **Separate Test Files:** Create separate test files for each module or component.
* **Use Descriptive Test Names:** Use descriptive test names to indicate what is being tested.
* **Organize Tests by Feature:** Organize tests by feature to make it easier to find and run tests.
### 5.5 Mocking and Stubbing
* **Use Mocking Libraries:** Use mocking libraries like `unittest.mock` or `pytest-mock` to create mock objects.
* **Stub External Dependencies:** Stub external dependencies to isolate the component being tested.
* **Verify Interactions:** Verify that the component being tested interacts with its dependencies as expected.
## 6. Common Pitfalls and Gotchas
### 6.1 Frequent Mistakes
* **Incorrect HTTP Method:** Using the wrong HTTP method for an operation (e.g., using GET to create a resource).
* **Missing Content-Type Header:** Failing to set the `Content-Type` header in the request.
* **Incorrect JSON Format:** Sending or receiving invalid JSON data.
* **Ignoring the `request` Object:** Not properly handling the request object.
* **Failing to Handle Errors:** Not gracefully handling exceptions and returning meaningful error messages.
* **Not Protecting Against CSRF:** Not implementing CSRF protection.
### 6.2 Edge Cases
* **Empty Data:** Handling cases where the database returns empty data.
* **Invalid Input:** Handling cases where the user provides invalid input.
* **Network Errors:** Handling network errors and timeouts.
* **Concurrent Requests:** Handling concurrent requests and race conditions.
### 6.3 Version-Specific Issues
* **Compatibility with Flask and Other Libraries:** Ensuring compatibility between flask-restful and other libraries.
* **Deprecated Features:** Being aware of deprecated features and migrating to the new ones.
### 6.4 Compatibility Concerns
* **Python Versions:** Ensuring compatibility with different Python versions.
* **Database Drivers:** Ensuring compatibility with different database drivers.
* **Operating Systems:** Ensuring compatibility with different operating systems.
### 6.5 Debugging Strategies
* **Use Logging:** Use logging to track the flow of execution and identify errors.
* **Use a Debugger:** Use a debugger to step through the code and inspect variables.
* **Read Error Messages Carefully:** Pay attention to error messages and stack traces.
* **Use Postman or curl:** Use Postman or curl to test API endpoints.
## 7. Tooling and Environment
### 7.1 Recommended Development Tools
* **Virtual Environment:** Use a virtual environment (e.g., `venv`, `pipenv`, `conda`) to isolate project dependencies.
* **IDE:** Use an Integrated Development Environment (IDE) like VS Code, PyCharm, or Sublime Text.
* **REST Client:** Use a REST client like Postman or Insomnia to test API endpoints.
* **Database Client:** Use a database client like DBeaver or pgAdmin to manage databases.
### 7.2 Build Configuration
* **Use a Build System:** Use a build system like Make or Fabric to automate build tasks.
* **Specify Dependencies:** Specify all project dependencies in a `requirements.txt` file or Pipfile.
* **Use a Configuration File:** Use a configuration file to store configuration values.
### 7.3 Linting and Formatting
* **Use a Linter:** Use a linter like pylint or flake8 to enforce code style guidelines.
* **Use a Formatter:** Use a formatter like black or autopep8 to automatically format the code.
### 7.4 Deployment
* **Use a WSGI Server:** Use a WSGI server like Gunicorn or uWSGI to deploy the application.
* **Use a Reverse Proxy:** Use a reverse proxy like Nginx or Apache to handle incoming requests and route them to the WSGI server.
* **Use a Load Balancer:** Use a load balancer to distribute traffic across multiple servers.
* **Use a Process Manager:** Use a process manager like Systemd or Supervisor to manage the WSGI server process.
### 7.5 CI/CD Integration
* **Use a CI/CD Tool:** Use a CI/CD tool like Jenkins, GitLab CI, or CircleCI to automate the build, test, and deployment process.
* **Run Tests Automatically:** Run unit tests and integration tests automatically on every commit.
* **Deploy Automatically:** Deploy the application automatically after the tests pass.

120
.cursor/rules/vue3.mdc Normal file
View File

@ -0,0 +1,120 @@
---
description:
globs:
alwaysApply: false
---
---
description: This rule provides best practices and coding standards for Vue 3 projects, covering code organization, performance, security, testing, tooling, and common pitfalls to ensure maintainable and efficient applications. It aims to guide developers in writing high-quality Vue 3 code.
globs: *.vue
---
- **Code Organization and Structure**:
- **Directory Structure**: Adopt a feature-based directory structure. Group related files (components, stores, utilities) within feature-specific directories rather than separating by file type. This enhances maintainability and discoverability.
- Example:
src/
components/
MyComponent.vue
...
views/
MyView.vue
...
features/
user-profile/
components/
UserProfileCard.vue
composables/
useUserProfileData.js
store/
userProfile.js
...
- **File Naming Conventions**: Use PascalCase for component file names (e.g., `MyComponent.vue`). Use camelCase for variable and function names (e.g., `myVariable`, `myFunction`). Use kebab-case for component selectors in templates (e.g., `<my-component>`).
- **Module Organization**: Utilize ES modules (`import`/`export`) for modularity and code reusability. Group related functions and components into modules.
- **Component Architecture**: Favor a component-based architecture. Design components to be small, reusable, and composable. Use props for data input and events for data output. Consider using a component library (e.g., Vuetify, Element Plus) for pre-built components.
- **Code Splitting Strategies**: Implement lazy loading for components and routes to reduce initial bundle size. Use dynamic imports for on-demand loading of modules.
- Example:
javascript
// Route-based code splitting
const routes = [
{
path: '/about',
component: () => import('./views/About.vue')
}
]
- **Common Patterns and Anti-patterns**:
- **Design Patterns**: Apply common design patterns such as composition API, provider/inject, and observer pattern where applicable.
- **Composition API**: Organize component logic into composable functions for reusability and maintainability.
- **Provider/Inject**: Use `provide` and `inject` to share data between components without prop drilling.
- **Recommended Approaches**: Utilize `v-model` for two-way data binding, computed properties for derived state, and watchers for side effects. Use the Composition API for enhanced code organization and reusability.
- **Anti-patterns and Code Smells**: Avoid directly mutating props. Avoid excessive use of global variables. Avoid complex logic within templates. Avoid tight coupling between components. Avoid over-engineering solutions.
- **State Management**: Choose a state management solution (e.g., Vuex, Pinia) for complex applications. Favor Pinia for Vue 3 due to its simpler API and improved TypeScript support. Decouple components from state management logic using actions and mutations.
- **Error Handling**: Implement global error handling using `app.config.errorHandler`. Use `try...catch` blocks for handling synchronous errors. Utilize `Promise.catch` for handling asynchronous errors. Provide user-friendly error messages.
- Example:
javascript
// Global error handler
app.config.errorHandler = (err, vm, info) => {
console.error('Global error:', err, info);
// Report error to server or display user-friendly message
}
- **Performance Considerations**:
- **Optimization Techniques**: Use `v-once` for static content. Use `v-memo` to memoize parts of the template. Use `key` attribute for `v-for` loops to improve rendering performance.
- **Memory Management**: Avoid creating memory leaks by properly cleaning up event listeners and timers. Use `onBeforeUnmount` lifecycle hook to release resources.
- **Rendering Optimization**: Use virtual DOM efficiently. Minimize unnecessary re-renders by using `ref` and `reactive` appropriately. Use `shouldUpdate` hook in functional components to control updates.
- **Bundle Size Optimization**: Use code splitting, tree shaking, and minification to reduce bundle size. Remove unused dependencies. Use smaller alternative libraries where possible.
- **Lazy Loading**: Implement lazy loading for images, components, and routes. Use `IntersectionObserver` API for lazy loading images.
- **Security Best Practices**:
- **Common Vulnerabilities**: Prevent Cross-Site Scripting (XSS) attacks by sanitizing user input. Prevent Cross-Site Request Forgery (CSRF) attacks by using CSRF tokens. Prevent SQL injection attacks by using parameterized queries.
- **Input Validation**: Validate user input on both client-side and server-side. Use appropriate data types and formats. Escape special characters.
- **Authentication and Authorization**: Implement secure authentication and authorization mechanisms. Use HTTPS to encrypt communication. Store passwords securely using hashing and salting.
- **Data Protection**: Protect sensitive data using encryption. Avoid storing sensitive data in client-side storage. Follow privacy best practices.
- **Secure API Communication**: Use HTTPS for API communication. Validate API responses. Implement rate limiting to prevent abuse.
- **Testing Approaches**:
- **Unit Testing**: Write unit tests for individual components, functions, and modules. Use Jest or Vitest as a test runner. Mock dependencies to isolate units of code.
- **Integration Testing**: Write integration tests to verify the interaction between components and modules. Use Vue Test Utils for component testing.
- **End-to-End Testing**: Write end-to-end tests to simulate user interactions and verify the application's overall functionality. Use Cypress or Playwright for end-to-end testing.
- **Test Organization**: Organize tests into separate directories based on the component or module being tested. Use descriptive test names.
- **Mocking and Stubbing**: Use mocks and stubs to isolate units of code and simulate dependencies. Use `jest.mock` or `vi.mock` for mocking modules.
- **Common Pitfalls and Gotchas**:
- **Frequent Mistakes**: Forgetting to register components. Incorrectly using `v-if` and `v-show`. Mutating props directly. Not handling asynchronous operations correctly. Ignoring error messages.
- **Edge Cases**: Handling empty arrays or objects. Dealing with browser compatibility issues. Managing state in complex components.
- **Version-Specific Issues**: Being aware of breaking changes between Vue 2 and Vue 3. Using deprecated APIs.
- **Compatibility Concerns**: Ensuring compatibility with different browsers and devices. Testing on different screen sizes and resolutions.
- **Debugging Strategies**: Using Vue Devtools for debugging. Using `console.log` statements for inspecting variables. Using a debugger for stepping through code.
- **Tooling and Environment**:
- **Recommended Development Tools**: Use VS Code with the Volar extension for Vue 3 development. Use Vue CLI or Vite for project scaffolding. Use Vue Devtools for debugging.
- **Build Configuration**: Configure Webpack or Rollup for building the application. Optimize build settings for production. Use environment variables for configuration.
- **Linting and Formatting**: Use ESLint with the `eslint-plugin-vue` plugin for linting Vue code. Use Prettier for code formatting. Configure linting and formatting rules to enforce code style.
- **Deployment Best Practices**: Use a CDN for serving static assets. Use server-side rendering (SSR) or pre-rendering for improved SEO and performance. Deploy to a reliable hosting platform.
- **CI/CD Integration**: Integrate linting, testing, and building into the CI/CD pipeline. Use automated deployment tools. Monitor application performance and errors.
- **Additional Best Practices**:
- **Accessibility (A11y)**: Ensure components are accessible by using semantic HTML, providing ARIA attributes where necessary, and testing with screen readers.
- **Internationalization (i18n)**: Implement i18n from the start if multilingual support is required. Use a library like `vue-i18n` to manage translations.
- **Documentation**: Document components and composables using JSDoc or similar tools. Generate documentation automatically using tools like Storybook.
- **Vue 3 Specific Recommendations**:
- **TypeScript**: Use TypeScript for improved type safety and code maintainability. Define component props and emits with type annotations.
- **Teleport**: Use the `Teleport` component to render content outside the component's DOM hierarchy, useful for modals and tooltips.
- **Suspense**: Use the `Suspense` component to handle asynchronous dependencies gracefully, providing fallback content while waiting for data to load.
- **Naming Conventions**:
- Components: PascalCase (e.g., `MyComponent.vue`)
- Variables/Functions: camelCase (e.g., `myVariable`, `myFunction`)
- Props/Events: camelCase (e.g., `myProp`, `myEvent`)
- Directives: kebab-case (e.g., `v-my-directive`)
- **Composition API Best Practices**:
- **Reactive Refs**: Use `ref` for primitive values and `reactive` for objects.
- **Readonly Refs**: Use `readonly` to prevent accidental mutations of reactive data.
- **Computed Properties**: Use `computed` for derived state and avoid complex logic within templates.
- **Lifecycle Hooks**: Use `onMounted`, `onUpdated`, `onUnmounted`, etc., to manage component lifecycle events.
- **Watchers**: Use `watch` for reacting to reactive data changes and performing side effects.

51
backend/app.py Normal file
View File

@ -0,0 +1,51 @@
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
# 初始化扩展
db = SQLAlchemy()
jwt = JWTManager()
def create_app():
app = Flask(__name__)
# 配置
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'
)
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['JWT_SECRET_KEY'] = os.getenv('JWT_SECRET_KEY', 'jwt-secret-key')
app.config['JWT_ACCESS_TOKEN_EXPIRES'] = timedelta(hours=24)
# 初始化扩展
db.init_app(app)
jwt.init_app(app)
CORS(app)
# 注册蓝图
from routes.auth import auth_bp
from routes.servers import servers_bp
from routes.scripts import scripts_bp
from routes.execute import execute_bp
from routes.users import users_bp
app.register_blueprint(auth_bp, url_prefix='/api/auth')
app.register_blueprint(servers_bp, url_prefix='/api/servers')
app.register_blueprint(scripts_bp, url_prefix='/api/scripts')
app.register_blueprint(execute_bp, url_prefix='/api/execute')
app.register_blueprint(users_bp, url_prefix='/api/users')
# 创建数据表
with app.app_context():
db.create_all()
return app
if __name__ == '__main__':
app = create_app()
app.run(debug=True, host='0.0.0.0', port=5000)

39
backend/init_db.py Normal file
View File

@ -0,0 +1,39 @@
from app import create_app, db
from models import User
def init_database():
"""初始化数据库和创建默认管理员用户"""
app = create_app()
with app.app_context():
# 创建所有表
db.create_all()
# 检查是否已有管理员用户
admin = User.query.filter_by(username='admin').first()
if not admin:
# 创建默认管理员
admin = User(
username='admin',
real_name='系统管理员',
email='admin@example.com',
role='admin'
)
admin.set_password('admin123')
admin.set_permissions([
'server.view', 'server.manage',
'script.view', 'script.manage',
'execute.run', 'user.manage'
])
db.session.add(admin)
db.session.commit()
print('默认管理员用户创建成功')
print('用户名: admin')
print('密码: admin123')
else:
print('管理员用户已存在')
if __name__ == '__main__':
init_database()

80
backend/models.py Normal file
View File

@ -0,0 +1,80 @@
from app import db
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
import json
class User(db.Model):
__tablename__ = 'users'
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)
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')
permissions = db.Column(db.Text)
status = db.Column(db.String(20), default='active')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
def set_permissions(self, permissions_list):
self.permissions = json.dumps(permissions_list)
def get_permissions(self):
return json.loads(self.permissions) if self.permissions else []
class Server(db.Model):
__tablename__ = 'servers'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
ip = db.Column(db.String(15), nullable=False)
port = db.Column(db.Integer, default=22)
username = db.Column(db.String(50), default='root')
status = db.Column(db.String(20), default='offline')
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class Script(db.Model):
__tablename__ = 'scripts'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
type = db.Column(db.String(20), nullable=False)
content = db.Column(db.Text, nullable=False)
description = db.Column(db.Text)
created_by = db.Column(db.Integer, db.ForeignKey('users.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
class ExecuteHistory(db.Model):
__tablename__ = 'execute_history'
id = db.Column(db.Integer, primary_key=True)
script_id = db.Column(db.Integer, db.ForeignKey('scripts.id'))
script_name = db.Column(db.String(100))
server_count = db.Column(db.Integer)
success_count = db.Column(db.Integer, default=0)
fail_count = db.Column(db.Integer, default=0)
status = db.Column(db.String(20), default='running')
executed_by = db.Column(db.Integer, db.ForeignKey('users.id'))
execute_time = db.Column(db.DateTime, default=datetime.utcnow)
class ExecuteResult(db.Model):
__tablename__ = 'execute_results'
id = db.Column(db.Integer, primary_key=True)
history_id = db.Column(db.Integer, db.ForeignKey('execute_history.id'))
server_id = db.Column(db.Integer, db.ForeignKey('servers.id'))
server_name = db.Column(db.String(100))
status = db.Column(db.String(20))
output = db.Column(db.Text)
error = db.Column(db.Text)
duration = db.Column(db.Integer)
execute_time = db.Column(db.DateTime, default=datetime.utcnow)

9
backend/requirements.txt Normal file
View File

@ -0,0 +1,9 @@
Flask==2.3.0
Flask-SQLAlchemy==3.0.0
Flask-CORS==4.0.0
Flask-JWT-Extended==4.5.1
PyMySQL==1.1.0
paramiko==3.3.0
python-dotenv==1.0.0
marshmallow==3.20.0
bcrypt==4.0.0

60
backend/routes/auth.py Normal file
View File

@ -0,0 +1,60 @@
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
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['POST'])
def login():
data = request.get_json()
username = data.get('username')
password = data.get('password')
if not username or not password:
return jsonify({'message': '用户名和密码不能为空'}), 400
user = User.query.filter_by(username=username).first()
if user and user.check_password(password):
if user.status != 'active':
return jsonify({'message': '账户已被禁用'}), 403
access_token = create_access_token(identity=user.id)
return jsonify({
'token': access_token,
'user': {
'id': user.id,
'username': user.username,
'realName': user.real_name,
'role': user.role,
'permissions': user.get_permissions()
}
})
return jsonify({'message': '用户名或密码错误'}), 401
@auth_bp.route('/logout', methods=['POST'])
@jwt_required()
def logout():
return jsonify({'message': '退出登录成功'})
@auth_bp.route('/profile', methods=['GET'])
@jwt_required()
def profile():
user_id = get_jwt_identity()
user = User.query.get(user_id)
if not user:
return jsonify({'message': '用户不存在'}), 404
return jsonify({
'user': {
'id': user.id,
'username': user.username,
'realName': user.real_name,
'email': user.email,
'role': user.role,
'permissions': user.get_permissions()
}
})

179
backend/routes/execute.py Normal file
View File

@ -0,0 +1,179 @@
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
import paramiko
import time
import threading
execute_bp = Blueprint('execute', __name__)
def execute_script_on_server(server, script_content, history_id):
"""在单个服务器上执行脚本"""
start_time = time.time()
try:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(server.ip, port=server.port, username=server.username, timeout=30)
stdin, stdout, stderr = ssh.exec_command(script_content)
output = stdout.read().decode('utf-8', errors='ignore')
error = stderr.read().decode('utf-8', errors='ignore')
ssh.close()
duration = int((time.time() - start_time) * 1000)
status = 'success' if not error else 'error'
# 保存执行结果
result = ExecuteResult(
history_id=history_id,
server_id=server.id,
server_name=server.name,
status=status,
output=output,
error=error,
duration=duration
)
db.session.add(result)
db.session.commit()
return {
'serverId': server.id,
'serverName': server.name,
'status': status,
'output': output,
'error': error,
'duration': duration
}
except Exception as e:
duration = int((time.time() - start_time) * 1000)
error_msg = str(e)
result = ExecuteResult(
history_id=history_id,
server_id=server.id,
server_name=server.name,
status='error',
output='',
error=error_msg,
duration=duration
)
db.session.add(result)
db.session.commit()
return {
'serverId': server.id,
'serverName': server.name,
'status': 'error',
'output': '',
'error': error_msg,
'duration': duration
}
@execute_bp.route('', methods=['POST'])
@jwt_required()
def execute_script():
user_id = get_jwt_identity()
data = request.get_json()
script_id = data['scriptId']
server_ids = data['serverIds']
# 获取脚本和服务器信息
script = Script.query.get_or_404(script_id)
servers = Server.query.filter(Server.id.in_(server_ids)).all()
if not servers:
return jsonify({'message': '未找到有效的服务器'}), 400
# 创建执行历史记录
history = ExecuteHistory(
script_id=script.id,
script_name=script.name,
server_count=len(servers),
executed_by=user_id
)
db.session.add(history)
db.session.commit()
# 并发执行脚本
results = []
threads = []
def execute_on_server(server):
result = execute_script_on_server(server, script.content, history.id)
results.append(result)
# 创建线程池
for server in servers:
thread = threading.Thread(target=execute_on_server, args=(server,))
threads.append(thread)
thread.start()
# 等待所有线程完成
for thread in threads:
thread.join()
# 统计结果
success_count = sum(1 for r in results if r['status'] == 'success')
fail_count = len(results) - success_count
# 更新历史记录
history.success_count = success_count
history.fail_count = fail_count
history.status = 'completed'
db.session.commit()
return jsonify({
'message': '脚本执行完成',
'historyId': history.id,
'results': results,
'successCount': success_count,
'failCount': fail_count
})
@execute_bp.route('/history', methods=['GET'])
@jwt_required()
def get_execute_history():
history = ExecuteHistory.query.order_by(ExecuteHistory.execute_time.desc()).limit(50).all()
result = []
for h in history:
result.append({
'id': h.id,
'scriptName': h.script_name,
'serverCount': h.server_count,
'successCount': h.success_count,
'failCount': h.fail_count,
'status': h.status,
'executeTime': h.execute_time.strftime('%Y-%m-%d %H:%M:%S')
})
return jsonify(result)
@execute_bp.route('/history/<int:history_id>/results', methods=['GET'])
@jwt_required()
def get_execute_results(history_id):
results = ExecuteResult.query.filter_by(history_id=history_id).all()
result = []
for r in results:
result.append({
'id': r.id,
'serverId': r.server_id,
'serverName': r.server_name,
'status': r.status,
'output': r.output,
'error': r.error,
'duration': r.duration,
'executeTime': r.execute_time.strftime('%Y-%m-%d %H:%M:%S')
})
return jsonify(result)

85
backend/routes/scripts.py Normal file
View File

@ -0,0 +1,85 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models import Script
from app import db
scripts_bp = Blueprint('scripts', __name__)
@scripts_bp.route('', methods=['GET'])
@jwt_required()
def get_scripts():
scripts = Script.query.all()
result = []
for script in scripts:
result.append({
'id': script.id,
'name': script.name,
'type': script.type,
'content': script.content,
'description': script.description,
'createdBy': script.created_by,
'createdAt': script.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'updatedAt': script.updated_at.strftime('%Y-%m-%d %H:%M:%S')
})
return jsonify(result)
@scripts_bp.route('', methods=['POST'])
@jwt_required()
def create_script():
user_id = get_jwt_identity()
data = request.get_json()
script = Script(
name=data['name'],
type=data['type'],
content=data['content'],
description=data.get('description', ''),
created_by=user_id
)
db.session.add(script)
db.session.commit()
return jsonify({'message': '脚本创建成功', 'id': script.id}), 201
@scripts_bp.route('/<int:script_id>', methods=['GET'])
@jwt_required()
def get_script(script_id):
script = Script.query.get_or_404(script_id)
return jsonify({
'id': script.id,
'name': script.name,
'type': script.type,
'content': script.content,
'description': script.description,
'createdBy': script.created_by,
'createdAt': script.created_at.strftime('%Y-%m-%d %H:%M:%S'),
'updatedAt': script.updated_at.strftime('%Y-%m-%d %H:%M:%S')
})
@scripts_bp.route('/<int:script_id>', methods=['PUT'])
@jwt_required()
def update_script(script_id):
script = Script.query.get_or_404(script_id)
data = request.get_json()
script.name = data.get('name', script.name)
script.type = data.get('type', script.type)
script.content = data.get('content', script.content)
script.description = data.get('description', script.description)
db.session.commit()
return jsonify({'message': '脚本更新成功'})
@scripts_bp.route('/<int:script_id>', methods=['DELETE'])
@jwt_required()
def delete_script(script_id):
script = Script.query.get_or_404(script_id)
db.session.delete(script)
db.session.commit()
return jsonify({'message': '脚本删除成功'})

119
backend/routes/servers.py Normal file
View File

@ -0,0 +1,119 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models import Server
from app import db
import paramiko
import socket
servers_bp = Blueprint('servers', __name__)
def check_server_status(ip, port, username):
"""检查服务器连接状态"""
try:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(ip, port=port, username=username, timeout=5)
ssh.close()
return 'online'
except:
return 'offline'
@servers_bp.route('', methods=['GET'])
@jwt_required()
def get_servers():
servers = Server.query.all()
result = []
for server in servers:
# 实时检查服务器状态
server.status = check_server_status(server.ip, server.port, server.username)
db.session.commit()
result.append({
'id': server.id,
'name': server.name,
'ip': server.ip,
'port': server.port,
'username': server.username,
'status': server.status,
'description': server.description,
'createdAt': server.created_at.strftime('%Y-%m-%d %H:%M:%S')
})
return jsonify(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
@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': '服务器更新成功'})
@servers_bp.route('/<int:server_id>', methods=['DELETE'])
@jwt_required()
def delete_server(server_id):
server = Server.query.get_or_404(server_id)
db.session.delete(server)
db.session.commit()
return jsonify({'message': '服务器删除成功'})
@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:
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect(server.ip, port=server.port, username=server.username, timeout=10)
# 执行简单命令测试
stdin, stdout, stderr = ssh.exec_command('echo "connection test"')
output = stdout.read().decode().strip()
ssh.close()
return jsonify({
'status': 'success',
'message': '连接测试成功',
'output': output
})
except Exception as e:
return jsonify({
'status': 'error',
'message': f'连接测试失败: {str(e)}'
}), 400

118
backend/routes/users.py Normal file
View File

@ -0,0 +1,118 @@
from flask import Blueprint, request, jsonify
from flask_jwt_extended import jwt_required, get_jwt_identity
from models import User
from app import db
users_bp = Blueprint('users', __name__)
@users_bp.route('', methods=['GET'])
@jwt_required()
def get_users():
users = User.query.all()
result = []
for user in users:
result.append({
'id': user.id,
'username': user.username,
'realName': user.real_name,
'email': user.email,
'role': user.role,
'status': user.status,
'permissions': user.get_permissions(),
'createdAt': user.created_at.strftime('%Y-%m-%d %H:%M:%S')
})
return jsonify(result)
@users_bp.route('', methods=['POST'])
@jwt_required()
def create_user():
data = request.get_json()
# 检查用户名是否已存在
if User.query.filter_by(username=data['username']).first():
return jsonify({'message': '用户名已存在'}), 400
user = User(
username=data['username'],
real_name=data['realName'],
email=data.get('email', ''),
role=data['role']
)
user.set_password(data['password'])
user.set_permissions(data.get('permissions', []))
db.session.add(user)
db.session.commit()
return jsonify({'message': '用户创建成功', 'id': user.id}), 201
@users_bp.route('/<int:user_id>', methods=['GET'])
@jwt_required()
def get_user(user_id):
user = User.query.get_or_404(user_id)
return jsonify({
'id': user.id,
'username': user.username,
'realName': user.real_name,
'email': user.email,
'role': user.role,
'status': user.status,
'permissions': user.get_permissions(),
'createdAt': user.created_at.strftime('%Y-%m-%d %H:%M:%S')
})
@users_bp.route('/<int:user_id>', methods=['PUT'])
@jwt_required()
def update_user(user_id):
user = User.query.get_or_404(user_id)
data = request.get_json()
user.real_name = data.get('realName', user.real_name)
user.email = data.get('email', user.email)
user.role = data.get('role', user.role)
user.status = data.get('status', user.status)
if 'permissions' in data:
user.set_permissions(data['permissions'])
# 如果提供了新密码,则更新密码
if 'password' in data and data['password']:
user.set_password(data['password'])
db.session.commit()
return jsonify({'message': '用户更新成功'})
@users_bp.route('/<int:user_id>', methods=['DELETE'])
@jwt_required()
def delete_user(user_id):
current_user_id = get_jwt_identity()
# 不能删除自己
if user_id == current_user_id:
return jsonify({'message': '不能删除自己的账户'}), 400
user = User.query.get_or_404(user_id)
db.session.delete(user)
db.session.commit()
return jsonify({'message': '用户删除成功'})
@users_bp.route('/<int:user_id>/reset-password', methods=['POST'])
@jwt_required()
def reset_password(user_id):
user = User.query.get_or_404(user_id)
data = request.get_json()
new_password = data.get('newPassword')
if not new_password:
return jsonify({'message': '新密码不能为空'}), 400
user.set_password(new_password)
db.session.commit()
return jsonify({'message': '密码重置成功'})

13
frontend/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>服务器监控管理平台</title>
<link rel="icon" href="/favicon.ico">
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

2944
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
frontend/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "server-monitor-platform",
"version": "1.0.0",
"description": "服务器监控管理平台",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"vue": "^3.3.0",
"vue-router": "^4.2.0",
"pinia": "^2.1.0",
"ant-design-vue": "^4.0.0",
"axios": "^1.4.0",
"@ant-design/icons-vue": "^7.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.0",
"vite": "^4.3.0",
"tailwindcss": "^3.3.0",
"autoprefixer": "^10.4.0",
"postcss": "^8.4.0"
}
}

View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}

9
frontend/src/App.vue Normal file
View File

@ -0,0 +1,9 @@
<template>
<div id="app">
<RouterView />
</div>
</template>
<script setup>
import { RouterView } from 'vue-router'
</script>

56
frontend/src/api/index.js Normal file
View File

@ -0,0 +1,56 @@
import axios from 'axios'
import { message } from 'ant-design-vue'
const api = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器
api.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
api.interceptors.response.use(
response => response.data,
error => {
message.error(error.response?.data?.message || '请求失败')
return Promise.reject(error)
}
)
export default {
// 用户认证
login: (data) => api.post('/auth/login', data),
logout: () => api.post('/auth/logout'),
// 服务器管理
getServers: () => api.get('/servers'),
createServer: (data) => api.post('/servers', data),
updateServer: (id, data) => api.put(`/servers/${id}`, data),
deleteServer: (id) => api.delete(`/servers/${id}`),
// 脚本管理
getScripts: () => api.get('/scripts'),
createScript: (data) => api.post('/scripts', data),
updateScript: (id, data) => api.put(`/scripts/${id}`, data),
deleteScript: (id) => api.delete(`/scripts/${id}`),
// 批量执行
executeScript: (data) => api.post('/execute', data),
getExecuteHistory: () => api.get('/execute/history'),
// 用户管理
getUsers: () => api.get('/users'),
createUser: (data) => api.post('/users', data),
updateUser: (id, data) => api.put(`/users/${id}`, data),
deleteUser: (id) => api.delete(`/users/${id}`)
}

View File

@ -0,0 +1,109 @@
<template>
<a-layout class="min-h-screen">
<a-layout-sider v-model:collapsed="collapsed" class="layout-sider">
<div class="text-white text-center py-4 text-lg font-bold">
服务器监控平台
</div>
<a-menu
theme="dark"
mode="inline"
:selected-keys="[activeMenu]"
@click="handleMenuClick"
>
<a-menu-item key="dashboard">
<DashboardOutlined />
<span>控制台</span>
</a-menu-item>
<a-menu-item key="servers">
<ServerOutlined />
<span>服务器管理</span>
</a-menu-item>
<a-menu-item key="scripts">
<FileTextOutlined />
<span>脚本管理</span>
</a-menu-item>
<a-menu-item key="execute">
<PlayCircleOutlined />
<span>批量执行</span>
</a-menu-item>
<a-menu-item key="users">
<UserOutlined />
<span>用户管理</span>
</a-menu-item>
</a-menu>
</a-layout-sider>
<a-layout>
<a-layout-header class="layout-header px-4 flex items-center justify-between">
<div class="flex items-center">
<a-button
type="text"
@click="collapsed = !collapsed"
class="mr-4"
>
<MenuUnfoldOutlined v-if="collapsed" />
<MenuFoldOutlined v-else />
</a-button>
<a-breadcrumb>
<a-breadcrumb-item>{{ currentTitle }}</a-breadcrumb-item>
</a-breadcrumb>
</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 />
退出登录
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</a-layout-header>
<a-layout-content class="layout-content p-6">
<RouterView />
</a-layout-content>
</a-layout>
</a-layout>
</template>
<script setup>
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()
const authStore = useAuthStore()
const collapsed = ref(false)
const user = computed(() => authStore.user)
const activeMenu = computed(() => route.name?.toLowerCase())
const currentTitle = computed(() => route.meta?.title || '控制台')
const handleMenuClick = ({ key }) => {
router.push(`/${key}`)
}
const handleUserMenu = ({ key }) => {
if (key === 'logout') {
authStore.logout()
router.push('/login')
}
}
</script>

13
frontend/src/main.js Normal file
View File

@ -0,0 +1,13 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import './style.css'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(Antd)
app.mount('#app')

View File

@ -0,0 +1,52 @@
import { createRouter, createWebHistory } from 'vue-router'
import Layout from '@/layout/index.vue'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/login/index.vue')
},
{
path: '/',
component: Layout,
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'),
meta: { title: '控制台' }
},
{
path: 'servers',
name: 'Servers',
component: () => import('@/views/servers/index.vue'),
meta: { title: '服务器管理' }
},
{
path: 'scripts',
name: 'Scripts',
component: () => import('@/views/scripts/index.vue'),
meta: { title: '脚本管理' }
},
{
path: 'execute',
name: 'Execute',
component: () => import('@/views/execute/index.vue'),
meta: { title: '批量执行' }
},
{
path: 'users',
name: 'Users',
component: () => import('@/views/users/index.vue'),
meta: { title: '用户管理' }
}
]
}
]
export default createRouter({
history: createWebHistory(),
routes
})

View File

@ -0,0 +1,33 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('token') || '')
const user = ref(JSON.parse(localStorage.getItem('user') || '{}'))
const login = (userData, userToken) => {
user.value = userData
token.value = userToken
localStorage.setItem('user', JSON.stringify(userData))
localStorage.setItem('token', userToken)
}
const logout = () => {
user.value = {}
token.value = ''
localStorage.removeItem('user')
localStorage.removeItem('token')
}
const isLoggedIn = () => {
return !!token.value
}
return {
user,
token,
login,
logout,
isLoggedIn
}
})

27
frontend/src/style.css Normal file
View File

@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.layout-header {
background: #fff;
box-shadow: 0 1px 4px rgba(0,21,41,.08);
}
.layout-sider {
background: #001529;
}
.layout-content {
background: #f0f2f5;
min-height: calc(100vh - 64px);
}

View File

@ -0,0 +1,90 @@
<template>
<div class="space-y-6">
<div class="grid grid-cols-4 gap-6">
<a-card class="text-center">
<div class="text-3xl font-bold text-blue-600">{{ stats.serverCount }}</div>
<div class="text-gray-500 mt-2">服务器总数</div>
</a-card>
<a-card class="text-center">
<div class="text-3xl font-bold text-green-600">{{ stats.onlineCount }}</div>
<div class="text-gray-500 mt-2">在线服务器</div>
</a-card>
<a-card class="text-center">
<div class="text-3xl font-bold text-purple-600">{{ stats.scriptCount }}</div>
<div class="text-gray-500 mt-2">脚本数量</div>
</a-card>
<a-card class="text-center">
<div class="text-3xl font-bold text-orange-600">{{ stats.executeCount }}</div>
<div class="text-gray-500 mt-2">今日执行</div>
</a-card>
</div>
<div class="grid grid-cols-2 gap-6">
<a-card title="最近执行记录">
<a-table
:data-source="recentExecutions"
:columns="executionColumns"
:pagination="false"
size="small"
/>
</a-card>
<a-card title="服务器状态">
<a-table
:data-source="serverStatus"
:columns="serverColumns"
:pagination="false"
size="small"
/>
</a-card>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const stats = ref({
serverCount: 0,
onlineCount: 0,
scriptCount: 0,
executeCount: 0
})
const recentExecutions = ref([])
const serverStatus = ref([])
const executionColumns = [
{ title: '脚本名称', dataIndex: 'scriptName', key: 'scriptName' },
{ title: '执行时间', dataIndex: 'executeTime', key: 'executeTime' },
{ title: '状态', dataIndex: 'status', key: 'status' }
]
const serverColumns = [
{ title: '服务器名称', dataIndex: 'name', key: 'name' },
{ title: 'IP地址', dataIndex: 'ip', key: 'ip' },
{ title: '状态', dataIndex: 'status', key: 'status' }
]
onMounted(async () => {
// API
stats.value = {
serverCount: 12,
onlineCount: 10,
scriptCount: 8,
executeCount: 25
}
recentExecutions.value = [
{ key: 1, scriptName: '系统更新', executeTime: '2024-01-15 10:30', status: '成功' },
{ key: 2, scriptName: '日志清理', executeTime: '2024-01-15 09:15', status: '成功' },
{ key: 3, scriptName: '备份数据', executeTime: '2024-01-15 08:00', status: '失败' }
]
serverStatus.value = [
{ key: 1, name: 'Web-01', ip: '192.168.1.10', status: '在线' },
{ key: 2, name: 'DB-01', ip: '192.168.1.20', status: '在线' },
{ key: 3, name: 'Cache-01', ip: '192.168.1.30', status: '离线' }
]
})
</script>

View File

@ -0,0 +1,257 @@
<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"
>
{{ 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>
</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"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 'success' ? 'green' : 'red'">
{{ record.status === 'success' ? '成功' : '失败' }}
</a-tag>
</template>
</template>
</a-table>
</a-card>
<!-- 输出详情弹窗 -->
<a-modal
v-model:open="showOutputModal"
title="执行输出"
:footer="null"
width="800px"
>
<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) }}
</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>
</div>
</div>
</a-modal>
</div>
</template>
<script setup>
import { ref, computed, onMounted } 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 scripts = ref([])
const servers = ref([])
const history = ref([])
const executeResults = ref([])
const outputData = ref(null)
const executeForm = ref({
scriptId: null,
serverIds: []
})
const selectedScript = computed(() => {
return scripts.value.find(s => s.id === executeForm.value.scriptId)
})
const serverOptions = computed(() => {
return servers.value.map(server => ({
label: `${server.name} (${server.ip})`,
value: server.id
}))
})
const resultColumns = [
{ title: '服务器', dataIndex: 'serverName', key: 'serverName' },
{ title: '状态', dataIndex: 'status', key: 'status' },
{ title: '执行时间', dataIndex: 'duration', key: 'duration' },
{ title: '输出', key: 'output' }
]
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 () => {
try {
const [scriptsData, serversData] = await Promise.all([
api.getScripts(),
api.getServers()
])
scripts.value = scriptsData
servers.value = serversData.filter(server => server.status === 'online')
} catch (error) {
message.error('加载数据失败')
}
}
const loadHistory = async () => {
historyLoading.value = true
try {
const data = await api.getExecuteHistory()
history.value = data
} catch (error) {
message.error('加载执行历史失败')
} finally {
historyLoading.value = false
}
}
const onScriptChange = () => {
//
}
const executeScript = async () => {
executing.value = true
executeResults.value = []
try {
const response = await api.executeScript({
scriptId: executeForm.value.scriptId,
serverIds: executeForm.value.serverIds
})
executeResults.value = response.results
message.success(`脚本执行完成,成功: ${response.successCount},失败: ${response.failCount}`)
loadHistory()
} catch (error) {
message.error('脚本执行失败')
} finally {
executing.value = false
}
}
const viewOutput = (record) => {
outputData.value = record
showOutputModal.value = true
}
const resetForm = () => {
executeForm.value = {
scriptId: null,
serverIds: []
}
executeResults.value = []
}
onMounted(() => {
loadData()
loadHistory()
})
</script>

View File

@ -0,0 +1,91 @@
<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>
<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>
<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>
</div>
</template>
<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 rules = {
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }]
}
const handleLogin = async (values) => {
loading.value = true
try {
const response = await api.login(values)
authStore.login(response.user, response.token)
message.success('登录成功')
router.push('/')
} catch (error) {
message.error('登录失败,请检查用户名和密码')
} finally {
loading.value = false
}
}
</script>

View File

@ -0,0 +1,205 @@
<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>
</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-modal
v-model:open="showModal"
:title="isEdit ? '编辑脚本' : '添加脚本'"
@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-item>
<a-form-item label="描述">
<a-textarea v-model:value="form.description" placeholder="脚本描述" />
</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>
</a-modal>
</div>
</template>
<script setup>
import { ref, 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 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' }
]
const getTypeColor = (type) => {
const colorMap = {
shell: 'blue',
python: 'green',
sql: 'orange'
}
return colorMap[type] || 'default'
}
const getTypeName = (type) => {
const nameMap = {
shell: 'Shell脚本',
python: 'Python脚本',
sql: 'SQL脚本'
}
return nameMap[type] || type
}
const loadScripts = async () => {
loading.value = true
try {
const data = await api.getScripts()
scripts.value = data
} catch (error) {
message.error('加载脚本列表失败')
} finally {
loading.value = false
}
}
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)
message.success('更新脚本成功')
} else {
await api.createScript(form.value)
message.success('添加脚本成功')
}
showModal.value = false
resetForm()
loadScripts()
} catch (error) {
message.error('操作失败')
}
}
const deleteScript = async (id) => {
try {
await api.deleteScript(id)
message.success('删除脚本成功')
loadScripts()
} catch (error) {
message.error('删除失败')
}
}
const resetForm = () => {
isEdit.value = false
form.value = {
name: '',
type: '',
content: '',
description: ''
}
}
onMounted(() => {
loadScripts()
})
</script>

View File

@ -0,0 +1,147 @@
<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>
</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-modal
v-model:open="showModal"
:title="isEdit ? '编辑服务器' : '添加服务器'"
@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-item>
<a-form-item label="IP地址" required>
<a-input v-model:value="form.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>
<a-form-item label="用户名">
<a-input v-model:value="form.username" placeholder="请输入SSH用户名" />
</a-form-item>
<a-form-item label="描述">
<a-textarea v-model:value="form.description" placeholder="服务器描述" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, 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 servers = ref([])
const form = ref({
name: '',
ip: '',
port: 22,
username: 'root',
description: ''
})
const columns = [
{ title: '服务器名称', dataIndex: 'name', key: 'name' },
{ title: 'IP地址', dataIndex: 'ip', key: 'ip' },
{ title: '端口', dataIndex: 'port', key: 'port' },
{ title: '用户名', dataIndex: 'username', key: 'username' },
{ title: '状态', dataIndex: 'status', key: 'status' },
{ title: '描述', dataIndex: 'description', key: 'description' },
{ title: '操作', key: 'action' }
]
const loadServers = async () => {
loading.value = true
try {
const data = await api.getServers()
servers.value = data
} catch (error) {
message.error('加载服务器列表失败')
} finally {
loading.value = false
}
}
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)
message.success('更新服务器成功')
} else {
await api.createServer(form.value)
message.success('添加服务器成功')
}
showModal.value = false
resetForm()
loadServers()
} catch (error) {
message.error('操作失败')
}
}
const deleteServer = async (id) => {
try {
await api.deleteServer(id)
message.success('删除服务器成功')
loadServers()
} catch (error) {
message.error('删除失败')
}
}
const resetForm = () => {
isEdit.value = false
form.value = {
name: '',
ip: '',
port: 22,
username: 'root',
description: ''
}
}
onMounted(() => {
loadServers()
})
</script>

View File

@ -0,0 +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>
<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 ? '编辑用户' : '添加用户'"
@ok="handleSubmit"
@cancel="resetForm"
>
<a-form :model="form" layout="vertical">
<a-form-item label="用户名" required>
<a-input
v-model:value="form.username"
placeholder="请输入用户名"
:disabled="isEdit"
/>
</a-form-item>
<a-form-item v-if="!isEdit" label="密码" required>
<a-input-password v-model:value="form.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item label="姓名" required>
<a-input v-model:value="form.realName" placeholder="请输入真实姓名" />
</a-form-item>
<a-form-item label="邮箱">
<a-input v-model:value="form.email" placeholder="请输入邮箱地址" />
</a-form-item>
<a-form-item label="角色" required>
<a-select v-model:value="form.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>
</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>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, 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 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' }
]
const getRoleColor = (role) => {
const colorMap = {
admin: 'red',
operator: 'blue',
viewer: 'green'
}
return colorMap[role] || 'default'
}
const getRoleName = (role) => {
const nameMap = {
admin: '管理员',
operator: '操作员',
viewer: '查看员'
}
return nameMap[role] || role
}
const loadUsers = async () => {
loading.value = true
try {
const data = await api.getUsers()
users.value = data
} catch (error) {
message.error('加载用户列表失败')
} finally {
loading.value = false
}
}
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)
message.success('更新用户成功')
} else {
await api.createUser(form.value)
message.success('添加用户成功')
}
showModal.value = false
resetForm()
loadUsers()
} catch (error) {
message.error('操作失败')
}
}
const deleteUser = async (id) => {
try {
await api.deleteUser(id)
message.success('删除用户成功')
loadUsers()
} catch (error) {
message.error('删除失败')
}
}
const resetForm = () => {
isEdit.value = false
form.value = {
username: '',
password: '',
realName: '',
email: '',
role: '',
permissions: []
}
}
onMounted(() => {
loadUsers()
})
</script>

View File

@ -0,0 +1,12 @@
module.exports = {
content: ['./src/**/*.{vue,js,ts}', './index.html'],
theme: {
extend: {
colors: {
primary: '#1890ff',
secondary: '#f0f2f5'
}
}
},
plugins: []
}

21
frontend/vite.config.js Normal file
View File

@ -0,0 +1,21 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:5000',
changeOrigin: true
}
}
}
})