init
This commit is contained in:
parent
00cde4676a
commit
bb2fcf7494
320
.cursor/rules/flask-resuful.mdc
Normal file
320
.cursor/rules/flask-resuful.mdc
Normal 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
120
.cursor/rules/vue3.mdc
Normal 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
51
backend/app.py
Normal 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
39
backend/init_db.py
Normal 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
80
backend/models.py
Normal 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
9
backend/requirements.txt
Normal 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
60
backend/routes/auth.py
Normal 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
179
backend/routes/execute.py
Normal 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
85
backend/routes/scripts.py
Normal 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
119
backend/routes/servers.py
Normal 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
118
backend/routes/users.py
Normal 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
13
frontend/index.html
Normal 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
2944
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
frontend/package.json
Normal file
25
frontend/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
frontend/postcss.config.js
Normal file
6
frontend/postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
9
frontend/src/App.vue
Normal file
9
frontend/src/App.vue
Normal 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
56
frontend/src/api/index.js
Normal 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}`)
|
||||||
|
}
|
||||||
109
frontend/src/layout/index.vue
Normal file
109
frontend/src/layout/index.vue
Normal 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
13
frontend/src/main.js
Normal 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')
|
||||||
52
frontend/src/router/index.js
Normal file
52
frontend/src/router/index.js
Normal 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
|
||||||
|
})
|
||||||
33
frontend/src/store/auth.js
Normal file
33
frontend/src/store/auth.js
Normal 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
27
frontend/src/style.css
Normal 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);
|
||||||
|
}
|
||||||
90
frontend/src/views/dashboard/index.vue
Normal file
90
frontend/src/views/dashboard/index.vue
Normal 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>
|
||||||
257
frontend/src/views/execute/index.vue
Normal file
257
frontend/src/views/execute/index.vue
Normal 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>
|
||||||
91
frontend/src/views/login/index.vue
Normal file
91
frontend/src/views/login/index.vue
Normal 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>
|
||||||
205
frontend/src/views/scripts/index.vue
Normal file
205
frontend/src/views/scripts/index.vue
Normal 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>
|
||||||
147
frontend/src/views/servers/index.vue
Normal file
147
frontend/src/views/servers/index.vue
Normal 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>
|
||||||
208
frontend/src/views/users/index.vue
Normal file
208
frontend/src/views/users/index.vue
Normal 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>
|
||||||
12
frontend/tailwind.config.js
Normal file
12
frontend/tailwind.config.js
Normal 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
21
frontend/vite.config.js
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
x
Reference in New Issue
Block a user