feat: 添加用户登录功能、添加注册功能、注册审批功能,参考:注册审批功能测试指南.md、驱动代码模块增加插入其他项目代码功能,驱动代码模块增加插入其他项目代码功能,优化测试报告模块前端布局和美化
- 实现超级用户管理系统用户,可进行增删改查,其他用户只可查看本人的数据,不可进行增删改
- 将Django后台管理中的用户配置相关功能迁移到系统中进行适配、可配置禁用、是否管理员、是否超级用户
- 增加用户管理的分组字段、可直接进行分组操作,实现用户管理即可通过分组配置实现各项目的访问权限
- 实现登录页面注册
- 实现管理员首页点击注册用户审批可实现通过不通过
- 实现未审核用户的登录提示与审核未通过的用户登录提示
- 实现一键插入其他本人可访问项目的脚本代码
14 files added
13 files modified
| New file |
| | |
| | | # 首次部署说明 |
| | | |
| | | ## 快速开始(推荐方式) |
| | | |
| | | 使用更新后的脚本一键完成所有初始化: |
| | | |
| | | ```bash |
| | | cd backend |
| | | python add_status_field.py --init |
| | | ``` |
| | | |
| | | 这将自动完成: |
| | | 1. 检查数据库连接 |
| | | 2. 执行数据库迁移(makemigrations + migrate) |
| | | 3. 创建管理员用户(admin/admin123) |
| | | 4. 创建默认用户组 |
| | | 5. 收集静态文件 |
| | | |
| | | ## 使用 Django 标准命令 |
| | | |
| | | 如果您更喜欢使用 Django 的标准命令,也可以按以下步骤执行: |
| | | |
| | | ```bash |
| | | cd backend |
| | | |
| | | # 1. 生成迁移文件 |
| | | python manage.py makemigrations |
| | | |
| | | # 2. 应用迁移 |
| | | python manage.py migrate |
| | | |
| | | # 3. 创建管理员用户(交互式) |
| | | python manage.py createsuperuser |
| | | ``` |
| | | |
| | | ## 脚本功能说明 |
| | | |
| | | ### add_status_field.py - 首次部署初始化脚本 |
| | | |
| | | **完整初始化:** |
| | | ```bash |
| | | python add_status_field.py --init |
| | | ``` |
| | | |
| | | **分步执行:** |
| | | ```bash |
| | | # 仅执行数据库迁移 |
| | | python add_status_field.py --migrate |
| | | |
| | | # 仅创建管理员用户 |
| | | python add_status_field.py --create-admin |
| | | |
| | | # 仅创建用户组 |
| | | python add_status_field.py --create-groups |
| | | |
| | | # 仅收集静态文件 |
| | | python add_status_field.py --collect-static |
| | | ``` |
| | | |
| | | ### check_users.py - 用户管理脚本 |
| | | |
| | | **列出所有用户:** |
| | | ```bash |
| | | python check_users.py --list |
| | | ``` |
| | | |
| | | **创建管理员用户:** |
| | | ```bash |
| | | python check_users.py --create-admin |
| | | ``` |
| | | |
| | | **创建测试用户:** |
| | | ```bash |
| | | python check_users.py --create-test |
| | | ``` |
| | | |
| | | **创建用户组:** |
| | | ```bash |
| | | python check_users.py --create-groups |
| | | ``` |
| | | |
| | | **列出所有用户组:** |
| | | ```bash |
| | | python check_users.py --list-groups |
| | | ``` |
| | | |
| | | **一键初始化用户数据:** |
| | | ```bash |
| | | python check_users.py --init |
| | | ``` |
| | | |
| | | ## 默认账号信息 |
| | | |
| | | **管理员账号:** |
| | | - 用户名: `admin` |
| | | - 密码: `admin123` |
| | | |
| | | ⚠️ **重要提示:** 请在首次登录后立即修改默认密码! |
| | | |
| | | **测试账号:** |
| | | - `testuser` / `test123` - 测试用户 |
| | | - `developer` / `dev123` - 开发人员 |
| | | |
| | | ## 部署前准备 |
| | | |
| | | 1. **配置数据库连接** |
| | | |
| | | 编辑 `backend/conf/env.py` 文件: |
| | | |
| | | ```python |
| | | DATABASE_NAME = 'your_database_name' |
| | | DATABASE_USER = 'your_database_user' |
| | | DATABASE_PASSWORD = 'your_database_password' |
| | | DATABASE_HOST = 'localhost' |
| | | DATABASE_PORT = '3306' |
| | | ``` |
| | | |
| | | 2. **创建数据库** |
| | | |
| | | 在 MySQL 中执行: |
| | | |
| | | ```sql |
| | | CREATE DATABASE your_database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
| | | ``` |
| | | |
| | | 3. **安装依赖** |
| | | |
| | | ```bash |
| | | cd backend |
| | | pip install -r requirements.txt |
| | | ``` |
| | | |
| | | ## 启动服务 |
| | | |
| | | **开发环境:** |
| | | ```bash |
| | | cd backend |
| | | python manage.py runserver |
| | | ``` |
| | | |
| | | **生产环境:** |
| | | ```bash |
| | | cd backend |
| | | gunicorn backend.wsgi:application -c gunicorn_conf.py |
| | | ``` |
| | | |
| | | ## 数据库表结构 |
| | | |
| | | ### lunaruser 应用 |
| | | - `lunaruser_myuser` - 用户表(包含 phone、show_hosts、name、status 字段) |
| | | |
| | | ### lunarlink 应用 |
| | | - `project` - 项目信息表 |
| | | - `config` - 环境信息表 |
| | | - `api` - 接口信息表 |
| | | - `case` - 用例信息表 |
| | | - `case_step` - 用例步骤表 |
| | | - `variables` - 全局变量表 |
| | | - `debugtalk` - 驱动代码表 |
| | | - `report` - 测试报告表 |
| | | - `report_detail` - 测试报告详情表 |
| | | - `relation` - 树形结构关系表 |
| | | - `visit` - 访问日志表 |
| | | - `login_log` - 登录日志表 |
| | | |
| | | ## 常见问题 |
| | | |
| | | ### 1. 数据库连接失败 |
| | | |
| | | 检查 `conf/env.py` 中的数据库配置是否正确,确保数据库已创建且用户有足够的权限。 |
| | | |
| | | ### 2. 迁移失败 |
| | | |
| | | 确保数据库表不存在或为空。如果之前有迁移历史,可能需要删除 `migrations` 目录下的迁移文件(保留 `__init__.py`)。 |
| | | |
| | | ### 3. 静态文件收集失败 |
| | | |
| | | 确保 `STATIC_ROOT` 目录有写入权限。 |
| | | |
| | | ### 4. 用户已存在 |
| | | |
| | | 脚本会自动检测用户是否已存在,如果存在则跳过创建。 |
| | | |
| | | ## 技术支持 |
| | | |
| | | 如有问题,请查看项目文档或联系技术支持团队。 |
| New file |
| | |
| | | #!/usr/bin/env python |
| | | """ |
| | | 首次部署初始化脚本 |
| | | |
| | | 该脚本用于首次部署系统时执行所有必要的初始化操作,包括: |
| | | 1. 检查数据库连接 |
| | | 2. 执行数据库迁移 |
| | | 3. 创建管理员用户 |
| | | 4. 创建默认用户组 |
| | | 5. 收集静态文件 |
| | | |
| | | 使用方法: |
| | | python add_status_field.py --init |
| | | |
| | | 或者使用 Django 标准命令: |
| | | python manage.py makemigrations |
| | | python manage.py migrate |
| | | python manage.py createsuperuser |
| | | """ |
| | | |
| | | import os |
| | | import sys |
| | | import django |
| | | |
| | | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') |
| | | django.setup() |
| | | |
| | | from django.core.management import call_command |
| | | from django.contrib.auth import get_user_model |
| | | from django.contrib.auth.models import Group |
| | | |
| | | |
| | | def check_database_connection(): |
| | | """ |
| | | 检查数据库连接 |
| | | """ |
| | | print("=" * 60) |
| | | print("检查数据库连接...") |
| | | print("=" * 60) |
| | | |
| | | try: |
| | | from django.db import connection |
| | | with connection.cursor() as cursor: |
| | | cursor.execute("SELECT 1") |
| | | print("✓ 数据库连接正常") |
| | | return True |
| | | except Exception as e: |
| | | print(f"✗ 数据库连接失败: {e}") |
| | | print("\n请检查 conf/env.py 中的数据库配置") |
| | | return False |
| | | |
| | | |
| | | def run_migrations(): |
| | | """ |
| | | 执行数据库迁移 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("执行数据库迁移...") |
| | | print("=" * 60) |
| | | |
| | | try: |
| | | print("\n[1/2] 生成迁移文件...") |
| | | call_command('makemigrations', verbosity=1) |
| | | print("✓ 迁移文件生成成功") |
| | | |
| | | print("\n[2/2] 应用迁移...") |
| | | call_command('migrate', verbosity=1) |
| | | print("✓ 数据库迁移成功") |
| | | return True |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 数据库迁移失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return False |
| | | |
| | | |
| | | def create_admin_user(username='admin', password='admin123', email='admin@example.com', name='系统管理员'): |
| | | """ |
| | | 创建管理员用户 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("创建管理员用户...") |
| | | print("=" * 60) |
| | | |
| | | User = get_user_model() |
| | | |
| | | try: |
| | | user = User.objects.get(username=username) |
| | | print(f"管理员用户 '{username}' 已存在,跳过创建") |
| | | print(f" 用户名: {user.username}") |
| | | print(f" 姓名: {user.name}") |
| | | print(f" 邮箱: {user.email}") |
| | | return True |
| | | |
| | | except User.DoesNotExist: |
| | | try: |
| | | user = User.objects.create_superuser( |
| | | username=username, |
| | | email=email, |
| | | password=password |
| | | ) |
| | | user.name = name |
| | | user.phone = None |
| | | user.show_hosts = False |
| | | user.status = 'approved' |
| | | user.save() |
| | | |
| | | print("✓ 管理员用户创建成功") |
| | | print(f" 用户名: {username}") |
| | | print(f" 密码: {password}") |
| | | print(f" 邮箱: {email}") |
| | | print(f" 姓名: {name}") |
| | | print("\n⚠️ 重要提示: 请在首次登录后修改默认密码!") |
| | | return True |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 创建管理员用户失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return False |
| | | |
| | | |
| | | def create_default_groups(): |
| | | """ |
| | | 创建默认用户组 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("创建默认用户组...") |
| | | print("=" * 60) |
| | | |
| | | groups = [ |
| | | {'name': '管理员组', 'description': '拥有所有权限的管理员组'}, |
| | | {'name': '开发人员组', 'description': '开发人员组'}, |
| | | {'name': '测试人员组', 'description': '测试人员组'}, |
| | | {'name': '只读用户组', 'description': '只读用户组'}, |
| | | ] |
| | | |
| | | created_count = 0 |
| | | for group_info in groups: |
| | | try: |
| | | Group.objects.get(name=group_info['name']) |
| | | print(f" 用户组 '{group_info['name']}' 已存在") |
| | | except Group.DoesNotExist: |
| | | try: |
| | | Group.objects.create(name=group_info['name']) |
| | | print(f" ✓ 创建用户组: {group_info['name']}") |
| | | created_count += 1 |
| | | except Exception as e: |
| | | print(f" ✗ 创建用户组 '{group_info['name']}' 失败: {e}") |
| | | |
| | | print(f"\n✓ 成功创建 {created_count} 个用户组") |
| | | return True |
| | | |
| | | |
| | | def collect_static_files(): |
| | | """ |
| | | 收集静态文件 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("收集静态文件...") |
| | | print("=" * 60) |
| | | |
| | | try: |
| | | call_command('collectstatic', '--noinput', verbosity=1) |
| | | print("✓ 静态文件收集成功") |
| | | return True |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 静态文件收集失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return False |
| | | |
| | | |
| | | def display_summary(): |
| | | """ |
| | | 显示部署摘要 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("部署摘要") |
| | | print("=" * 60) |
| | | |
| | | User = get_user_model() |
| | | |
| | | print(f"\n用户数量: {User.objects.count()}") |
| | | print(f"用户组数量: {Group.objects.count()}") |
| | | |
| | | print("\n默认管理员账号:") |
| | | print(" 用户名: admin") |
| | | print(" 密码: admin123") |
| | | print("\n⚠️ 请在首次登录后立即修改默认密码!") |
| | | |
| | | |
| | | def main(): |
| | | """ |
| | | 主函数 |
| | | """ |
| | | import argparse |
| | | |
| | | parser = argparse.ArgumentParser(description='首次部署初始化脚本') |
| | | parser.add_argument('--init', action='store_true', help='执行完整的初始化流程') |
| | | parser.add_argument('--migrate', action='store_true', help='仅执行数据库迁移') |
| | | parser.add_argument('--create-admin', action='store_true', help='仅创建管理员用户') |
| | | parser.add_argument('--create-groups', action='store_true', help='仅创建用户组') |
| | | parser.add_argument('--collect-static', action='store_true', help='仅收集静态文件') |
| | | |
| | | args = parser.parse_args() |
| | | |
| | | if args.init: |
| | | print("\n") |
| | | print("╔" + "=" * 58 + "╗") |
| | | print("║" + " " * 10 + "接口自动化平台 - 首次部署初始化" + " " * 13 + "║") |
| | | print("╚" + "=" * 58 + "╝") |
| | | print() |
| | | |
| | | steps = [ |
| | | ("检查数据库连接", check_database_connection), |
| | | ("执行数据库迁移", run_migrations), |
| | | ("创建管理员用户", create_admin_user), |
| | | ("创建默认用户组", create_default_groups), |
| | | ("收集静态文件", collect_static_files), |
| | | ] |
| | | |
| | | failed_steps = [] |
| | | |
| | | for step_name, step_func in steps: |
| | | if not step_func(): |
| | | failed_steps.append(step_name) |
| | | print(f"\n✗ '{step_name}' 失败,终止部署") |
| | | break |
| | | |
| | | if not failed_steps: |
| | | display_summary() |
| | | print("\n" + "=" * 60) |
| | | print("✓ 部署初始化完成!") |
| | | print("=" * 60) |
| | | print("\n系统已准备就绪,可以启动服务:") |
| | | print(" python manage.py runserver") |
| | | print() |
| | | return 0 |
| | | else: |
| | | print("\n" + "=" * 60) |
| | | print("✗ 部署初始化失败!") |
| | | print("=" * 60) |
| | | print(f"\n失败的步骤: {', '.join(failed_steps)}") |
| | | return 1 |
| | | |
| | | elif args.migrate: |
| | | return 0 if run_migrations() else 1 |
| | | |
| | | elif args.create_admin: |
| | | return 0 if create_admin_user() else 1 |
| | | |
| | | elif args.create_groups: |
| | | return 0 if create_default_groups() else 1 |
| | | |
| | | elif args.collect_static: |
| | | return 0 if collect_static_files() else 1 |
| | | |
| | | else: |
| | | parser.print_help() |
| | | print("\n使用示例:") |
| | | print(" python add_status_field.py --init # 完整初始化") |
| | | print(" python add_status_field.py --migrate # 仅迁移数据库") |
| | | print(" python add_status_field.py --create-admin # 仅创建管理员") |
| | | print("\n或使用 Django 标准命令:") |
| | | print(" python manage.py makemigrations") |
| | | print(" python manage.py migrate") |
| | | print(" python manage.py createsuperuser") |
| | | return 0 |
| | | |
| | | |
| | | if __name__ == '__main__': |
| | | sys.exit(main()) |
| New file |
| | |
| | | from django.db import migrations, models |
| | | |
| | | |
| | | class Migration(migrations.Migration): |
| | | |
| | | dependencies = [ |
| | | ('lunaruser', '0002_myuser_name'), |
| | | ] |
| | | |
| | | operations = [ |
| | | migrations.AddField( |
| | | model_name='myuser', |
| | | name='status', |
| | | field=models.CharField( |
| | | choices=[('pending', '待审核'), ('approved', '已通过'), ('rejected', '未通过')], |
| | | default='approved', |
| | | help_text='用户状态', |
| | | max_length=20, |
| | | verbose_name='状态' |
| | | ), |
| | | ), |
| | | ] |
| | |
| | | from django.contrib.auth.models import AbstractUser |
| | | |
| | | |
| | | # Create your models here. |
| | | class MyUser(AbstractUser): |
| | | """ |
| | | 使用AbstractUser可以对User进行扩展使用,添加用户自定义的属性 |
| | |
| | | null=True, |
| | | help_text="姓名", |
| | | ) |
| | | STATUS_CHOICES = ( |
| | | ('pending', '待审核'), |
| | | ('approved', '已通过'), |
| | | ('rejected', '未通过'), |
| | | ) |
| | | status = models.CharField( |
| | | verbose_name="状态", |
| | | max_length=20, |
| | | choices=STATUS_CHOICES, |
| | | default='approved', |
| | | help_text="用户状态", |
| | | ) |
| | | |
| | | class Meta(AbstractUser.Meta): |
| | | pass |
| | |
| | | |
| | | from rest_framework import serializers |
| | | from django.contrib.auth import get_user_model |
| | | from django.contrib.auth.models import Group |
| | | |
| | | from lunarlink.models import LoginLog |
| | | |
| | |
| | | if attrs['new_password'] != attrs['confirm_password']: |
| | | raise serializers.ValidationError("两次输入的新密码不一致") |
| | | return attrs |
| | | |
| | | |
| | | class UserCreateSerializer(serializers.ModelSerializer): |
| | | """ |
| | | 创建用户序列化 |
| | | """ |
| | | password = serializers.CharField(required=True, min_length=6, max_length=128, write_only=True) |
| | | confirm_password = serializers.CharField(required=True, max_length=128, write_only=True) |
| | | groups = serializers.PrimaryKeyRelatedField( |
| | | many=True, |
| | | queryset=Group.objects.all(), |
| | | required=False |
| | | ) |
| | | |
| | | class Meta: |
| | | model = User |
| | | fields = ['id', 'username', 'password', 'confirm_password', 'name', 'phone', 'is_staff', 'is_superuser', 'is_active', 'groups', 'show_hosts'] |
| | | |
| | | def validate(self, attrs): |
| | | if attrs['password'] != attrs['confirm_password']: |
| | | raise serializers.ValidationError("两次输入的密码不一致") |
| | | return attrs |
| | | |
| | | def create(self, validated_data): |
| | | validated_data.pop('confirm_password') |
| | | password = validated_data.pop('password') |
| | | groups = validated_data.pop('groups', None) |
| | | user = User(**validated_data) |
| | | user.set_password(password) |
| | | user.save() |
| | | if groups: |
| | | user.groups.set(groups) |
| | | return user |
| | | |
| | | |
| | | class UserUpdateSerializer(serializers.ModelSerializer): |
| | | """ |
| | | 更新用户序列化 |
| | | """ |
| | | groups = serializers.PrimaryKeyRelatedField( |
| | | many=True, |
| | | queryset=Group.objects.all(), |
| | | required=False |
| | | ) |
| | | |
| | | class Meta: |
| | | model = User |
| | | fields = ['id', 'username', 'name', 'phone', 'is_staff', 'is_superuser', 'is_active', 'groups', 'show_hosts'] |
| | | |
| | | def update(self, instance, validated_data): |
| | | groups = validated_data.pop('groups', None) |
| | | for attr, value in validated_data.items(): |
| | | setattr(instance, attr, value) |
| | | instance.save() |
| | | if groups is not None: |
| | | instance.groups.set(groups) |
| | | return instance |
| | | |
| | | |
| | | class UserDetailSerializer(serializers.ModelSerializer): |
| | | """ |
| | | 用户详情序列化 |
| | | """ |
| | | groups = serializers.SerializerMethodField() |
| | | accessible_projects = serializers.SerializerMethodField() |
| | | status_display = serializers.CharField(source='get_status_display', read_only=True) |
| | | |
| | | class Meta: |
| | | model = User |
| | | fields = ['id', 'username', 'name', 'phone', 'is_staff', 'is_superuser', 'is_active', 'groups', 'show_hosts', 'date_joined', 'last_login', 'accessible_projects', 'status', 'status_display'] |
| | | |
| | | def get_groups(self, obj): |
| | | return [g.name for g in obj.groups.all()] |
| | | |
| | | def get_accessible_projects(self, obj): |
| | | from lunarlink.models import Project |
| | | user_groups = obj.groups.all() |
| | | return list(Project.objects.filter(groups__in=user_groups).distinct().values_list("name", flat=True)) |
| | | |
| | | |
| | | class UserRegisterSerializer(serializers.ModelSerializer): |
| | | """ |
| | | 用户注册序列化 |
| | | """ |
| | | password = serializers.CharField(required=True, min_length=6, max_length=128, write_only=True) |
| | | confirm_password = serializers.CharField(required=True, max_length=128, write_only=True) |
| | | groups = serializers.PrimaryKeyRelatedField( |
| | | many=True, |
| | | queryset=Group.objects.all(), |
| | | required=False |
| | | ) |
| | | |
| | | class Meta: |
| | | model = User |
| | | fields = ['username', 'password', 'confirm_password', 'name', 'phone', 'groups'] |
| | | extra_kwargs = { |
| | | 'username': {'validators': []}, |
| | | 'phone': {'validators': []} |
| | | } |
| | | |
| | | def validate_username(self, value): |
| | | existing_user = User.objects.filter(username=value).first() |
| | | if existing_user and existing_user.status != 'rejected': |
| | | raise serializers.ValidationError("用户名已存在") |
| | | return value |
| | | |
| | | def validate_phone(self, value): |
| | | if value: |
| | | existing_user = User.objects.filter(phone=value).first() |
| | | if existing_user and existing_user.status != 'rejected': |
| | | raise serializers.ValidationError("手机号已存在") |
| | | return value |
| | | |
| | | def validate(self, attrs): |
| | | if attrs['password'] != attrs['confirm_password']: |
| | | raise serializers.ValidationError("两次输入的密码不一致") |
| | | return attrs |
| | | |
| | | def create(self, validated_data): |
| | | validated_data.pop('confirm_password') |
| | | password = validated_data.pop('password') |
| | | groups = validated_data.pop('groups', None) |
| | | |
| | | username = validated_data['username'] |
| | | phone = validated_data.get('phone') |
| | | |
| | | existing_user = User.objects.filter(username=username).first() |
| | | if existing_user and existing_user.status == 'rejected': |
| | | user = existing_user |
| | | user.name = validated_data.get('name', user.name) |
| | | user.phone = phone |
| | | user.set_password(password) |
| | | user.status = 'pending' |
| | | user.is_active = False |
| | | user.save() |
| | | if groups: |
| | | user.groups.set(groups) |
| | | return user |
| | | |
| | | if phone: |
| | | existing_phone_user = User.objects.filter(phone=phone).first() |
| | | if existing_phone_user and existing_phone_user.status == 'rejected': |
| | | user = existing_phone_user |
| | | user.username = username |
| | | user.name = validated_data.get('name', user.name) |
| | | user.set_password(password) |
| | | user.status = 'pending' |
| | | user.is_active = False |
| | | user.save() |
| | | if groups: |
| | | user.groups.set(groups) |
| | | return user |
| | | |
| | | user = User(**validated_data) |
| | | user.set_password(password) |
| | | user.status = 'pending' |
| | | user.is_active = False |
| | | user.save() |
| | | if groups: |
| | | user.groups.set(groups) |
| | | return user |
| | | |
| | | |
| | | class UserApprovalSerializer(serializers.Serializer): |
| | | """ |
| | | 用户审批序列化 |
| | | """ |
| | | action = serializers.ChoiceField(choices=['approve', 'reject']) |
| | | reason = serializers.CharField(required=False, allow_blank=True, max_length=500) |
| | |
| | | # -*- coding: utf-8 -*- |
| | | """ |
| | | @File : urls.py |
| | | @Time : 2023/1/13 16:09 |
| | | @Author : geekbing |
| | | @LastEditTime : - |
| | | @LastEditors : - |
| | | @Description : 接口路径 |
| | | """ |
| | | |
| | | from django.urls import path |
| | | from django.urls import path, include |
| | | from rest_framework.routers import DefaultRouter |
| | | from lunaruser import views |
| | | |
| | | router = DefaultRouter() |
| | | router.register(r'management', views.UserManagementViewSet, basename='user-management') |
| | | router.register(r'approval', views.UserApprovalViewSet, basename='user-approval') |
| | | |
| | | urlpatterns = [ |
| | | path("login", views.LoginView.as_view()), |
| | | path("register", views.RegisterView.as_view()), |
| | | path("list", views.UserView.as_view()), |
| | | path( |
| | | "login_log", |
| | | views.LoginLogView.as_view({"get": "list"}), |
| | | ), |
| | | path('change_password/', views.ChangePasswordView.as_view(), name='change_password'), |
| | | path('groups', views.GroupListView.as_view(), name='group-list'), |
| | | path('', include(router.urls)), |
| | | ] |
| | |
| | | from rest_framework_jwt.settings import api_settings |
| | | from rest_framework.response import Response |
| | | from rest_framework.views import APIView |
| | | from rest_framework.viewsets import GenericViewSet |
| | | from rest_framework.viewsets import GenericViewSet, ModelViewSet |
| | | from rest_framework.decorators import action |
| | | from drf_yasg.utils import swagger_auto_schema |
| | | from rest_framework import status |
| | | from rest_framework.permissions import IsAuthenticated |
| | | from rest_framework.filters import SearchFilter |
| | | from backend.utils.auth import MyJWTAuthentication |
| | | from backend.utils.permissions import CustomIsAdminUser |
| | | import logging |
| | | |
| | | from backend.utils.request_util import save_login_log |
| | | from lunarlink.models import LoginLog |
| | |
| | | except KeyError: |
| | | return Response(response.KEY_MISS) |
| | | |
| | | user = authenticate(username=username, password=password) |
| | | |
| | | if user is None: |
| | | try: |
| | | user = User.objects.get(username=username) |
| | | except User.DoesNotExist: |
| | | return Response(response.LOGIN_FAILED) |
| | | |
| | | # 0后面还需要优化定义明确 |
| | | if not user.check_password(password): |
| | | return Response(response.LOGIN_FAILED) |
| | | |
| | | if hasattr(user, 'status') and user.status == 'pending': |
| | | return Response({ |
| | | "success": False, |
| | | "msg": "你提交的注册还在审批中,暂时无法登录,可联系管理员确认!" |
| | | }, status=status.HTTP_403_FORBIDDEN) |
| | | |
| | | if hasattr(user, 'status') and user.status == 'rejected': |
| | | return Response({ |
| | | "success": False, |
| | | "msg": "你的注册申请未通过审核,无法登录!" |
| | | }, status=status.HTTP_403_FORBIDDEN) |
| | | |
| | | if user.is_active == 0: |
| | | return Response(response.USER_BLOCKED) |
| | | |
| | | # JWT token creation |
| | | from django.utils import timezone |
| | | user.last_login = timezone.now() |
| | | user.save(update_fields=['last_login']) |
| | | |
| | | jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER |
| | | jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER |
| | | |
| | |
| | | "user": user.username, |
| | | "name": user.name, |
| | | "is_superuser": user.is_superuser, |
| | | "is_staff": user.is_staff, |
| | | "show_hosts": user.show_hosts, |
| | | "token": token, |
| | | } |
| | |
| | | "success": False, |
| | | "msg": str(e) |
| | | }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) |
| | | |
| | | |
| | | class UserManagementViewSet(ModelViewSet): |
| | | """ |
| | | 用户管理视图集 |
| | | 超级用户可以管理所有用户,普通用户只能查看自己的信息 |
| | | """ |
| | | authentication_classes = [MyJWTAuthentication] |
| | | permission_classes = [IsAuthenticated] |
| | | pagination_class = None |
| | | filter_backends = [SearchFilter] |
| | | search_fields = ['username', 'name'] |
| | | |
| | | def get_queryset(self): |
| | | if self.request.user.is_superuser: |
| | | return User.objects.filter(status='approved').prefetch_related('groups') |
| | | else: |
| | | return User.objects.filter(id=self.request.user.id).prefetch_related('groups') |
| | | |
| | | def get_serializer_class(self): |
| | | if self.action == 'create': |
| | | return serializers.UserCreateSerializer |
| | | elif self.action in ['update', 'partial_update']: |
| | | return serializers.UserUpdateSerializer |
| | | else: |
| | | return serializers.UserDetailSerializer |
| | | |
| | | def get_permissions(self): |
| | | if self.action in ['create', 'update', 'partial_update', 'destroy']: |
| | | self.permission_classes = [IsAuthenticated, CustomIsAdminUser] |
| | | return super().get_permissions() |
| | | |
| | | def create(self, request, *args, **kwargs): |
| | | try: |
| | | return super().create(request, *args, **kwargs) |
| | | except Exception as e: |
| | | import logging |
| | | logger = logging.getLogger(__name__) |
| | | logger.error(f"创建用户失败: {str(e)}", exc_info=True) |
| | | return Response({ |
| | | "success": False, |
| | | "msg": f"创建用户失败: {str(e)}" |
| | | }, status=status.HTTP_400_BAD_REQUEST) |
| | | |
| | | def update(self, request, *args, **kwargs): |
| | | try: |
| | | return super().update(request, *args, **kwargs) |
| | | except Exception as e: |
| | | import logging |
| | | logger = logging.getLogger(__name__) |
| | | logger.error(f"更新用户失败: {str(e)}", exc_info=True) |
| | | return Response({ |
| | | "success": False, |
| | | "msg": f"更新用户失败: {str(e)}" |
| | | }, status=status.HTTP_400_BAD_REQUEST) |
| | | |
| | | def perform_create(self, serializer): |
| | | serializer.save() |
| | | |
| | | def perform_update(self, serializer): |
| | | serializer.save() |
| | | |
| | | def perform_destroy(self, instance): |
| | | instance.delete() |
| | | |
| | | |
| | | class RegisterView(APIView): |
| | | """ |
| | | 用户注册视图 |
| | | """ |
| | | |
| | | authentication_classes = () |
| | | permission_classes = () |
| | | |
| | | @swagger_auto_schema(request_body=serializers.UserRegisterSerializer) |
| | | def post(self, request): |
| | | """ |
| | | 用户注册 |
| | | { |
| | | username: str |
| | | password: str |
| | | confirm_password: str |
| | | name: str |
| | | phone: str |
| | | groups: list (optional) |
| | | } |
| | | """ |
| | | serializer = serializers.UserRegisterSerializer(data=request.data) |
| | | if not serializer.is_valid(): |
| | | return Response({ |
| | | "success": False, |
| | | "msg": serializer.errors |
| | | }, status=status.HTTP_400_BAD_REQUEST) |
| | | |
| | | try: |
| | | user = serializer.save() |
| | | return Response({ |
| | | "success": True, |
| | | "msg": "你的注册申请已提交,待管理员审核中!", |
| | | "data": { |
| | | "id": user.id, |
| | | "username": user.username |
| | | } |
| | | }, status=status.HTTP_201_CREATED) |
| | | except Exception as e: |
| | | import logging |
| | | logger = logging.getLogger(__name__) |
| | | logger.error(f"注册失败: {str(e)}", exc_info=True) |
| | | return Response({ |
| | | "success": False, |
| | | "msg": f"注册失败: {str(e)}" |
| | | }, status=status.HTTP_500_INTERNAL_SERVER_ERROR) |
| | | |
| | | |
| | | class UserApprovalViewSet(ModelViewSet): |
| | | """ |
| | | 用户审批视图集 |
| | | """ |
| | | authentication_classes = [MyJWTAuthentication] |
| | | permission_classes = [IsAuthenticated] |
| | | pagination_class = None |
| | | filter_backends = [SearchFilter] |
| | | search_fields = ['username', 'name'] |
| | | |
| | | def get_queryset(self): |
| | | if self.request.user.is_superuser or self.request.user.is_staff: |
| | | return User.objects.filter(status='pending').prefetch_related('groups') |
| | | else: |
| | | return User.objects.none() |
| | | |
| | | def get_serializer_class(self): |
| | | return serializers.UserDetailSerializer |
| | | |
| | | def list(self, request, *args, **kwargs): |
| | | queryset = self.get_queryset() |
| | | serializer = self.get_serializer(queryset, many=True) |
| | | return Response({ |
| | | "success": True, |
| | | "results": serializer.data |
| | | }) |
| | | |
| | | @swagger_auto_schema(request_body=serializers.UserApprovalSerializer) |
| | | @action(detail=True, methods=['post']) |
| | | def approve(self, request, pk=None): |
| | | """ |
| | | 审批用户 |
| | | { |
| | | action: 'approve' | 'reject' |
| | | reason: str (optional) |
| | | } |
| | | """ |
| | | user_id = pk |
| | | try: |
| | | user = User.objects.get(id=user_id) |
| | | except User.DoesNotExist: |
| | | return Response({ |
| | | "success": False, |
| | | "msg": "用户不存在" |
| | | }, status=status.HTTP_404_NOT_FOUND) |
| | | |
| | | serializer = serializers.UserApprovalSerializer(data=request.data) |
| | | if not serializer.is_valid(): |
| | | return Response({ |
| | | "success": False, |
| | | "msg": serializer.errors |
| | | }, status=status.HTTP_400_BAD_REQUEST) |
| | | |
| | | action = serializer.validated_data['action'] |
| | | |
| | | if action == 'approve': |
| | | user.status = 'approved' |
| | | user.is_active = True |
| | | user.save() |
| | | return Response({ |
| | | "success": True, |
| | | "msg": "用户审批已通过" |
| | | }) |
| | | elif action == 'reject': |
| | | user.status = 'rejected' |
| | | user.save() |
| | | return Response({ |
| | | "success": True, |
| | | "msg": "用户审批未通过" |
| | | }) |
| | | |
| | | |
| | | class GroupListView(APIView): |
| | | """ |
| | | 获取分组列表(无需认证) |
| | | 用于注册时选择分组 |
| | | """ |
| | | authentication_classes = () |
| | | permission_classes = () |
| | | |
| | | def get(self, request): |
| | | """ |
| | | 获取所有分组列表 |
| | | """ |
| | | from django.contrib.auth.models import Group |
| | | groups = Group.objects.all() |
| | | group_list = [{"id": group.id, "name": group.name} for group in groups] |
| | | return Response(group_list) |
| | |
| | | "DEFAULT_PERMISSION_CLASSES": ( |
| | | "rest_framework.permissions.IsAuthenticated", # 有经过身份认证确定用户身份才能访问 |
| | | ), |
| | | "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式 |
| | | "DATE_FORMAT": "%Y-%m-%d", # 日期格式 |
| | | } |
| | | |
| | | # ================================================= # |
| | |
| | | |
| | | class CustomIsAdminUser(IsAdminUser): |
| | | message = "您没有执行此操作的权限" |
| | | |
| | | def has_permission(self, request, view): |
| | | return request.user.is_superuser |
| New file |
| | |
| | | import os |
| | | import sys |
| | | import django |
| | | |
| | | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') |
| | | django.setup() |
| | | |
| | | from django.contrib.auth import get_user_model |
| | | from django.contrib.auth.models import Group |
| | | |
| | | User = get_user_model() |
| | | |
| | | |
| | | def list_users(): |
| | | """ |
| | | 列出所有用户 |
| | | """ |
| | | print("=" * 60) |
| | | print("系统用户列表:") |
| | | print("=" * 60) |
| | | users = User.objects.all() |
| | | if not users: |
| | | print("暂无用户") |
| | | else: |
| | | for user in users: |
| | | print(f"\n用户名: {user.username}") |
| | | print(f" 姓名: {user.name}") |
| | | print(f" 邮箱: {user.email}") |
| | | print(f" 手机号: {user.phone}") |
| | | print(f" 状态: {user.status}") |
| | | print(f" 是否管理员: {'是' if user.is_superuser else '否'}") |
| | | print(f" 是否员工: {'是' if user.is_staff else '否'}") |
| | | print(f" 是否激活: {'是' if user.is_active else '否'}") |
| | | print(f" 所属用户组: {', '.join([g.name for g in user.groups.all()]) or '无'}") |
| | | |
| | | |
| | | def create_admin_user(username='admin', password='admin123', email='admin@example.com', name='系统管理员'): |
| | | """ |
| | | 创建管理员用户 |
| | | """ |
| | | try: |
| | | user = User.objects.get(username=username) |
| | | print(f"\n用户 '{username}' 已存在,跳过创建") |
| | | return user |
| | | |
| | | except User.DoesNotExist: |
| | | try: |
| | | user = User.objects.create_superuser( |
| | | username=username, |
| | | email=email, |
| | | password=password |
| | | ) |
| | | user.name = name |
| | | user.phone = None |
| | | user.show_hosts = False |
| | | user.status = 'approved' |
| | | user.save() |
| | | print(f"\n✓ 成功创建管理员用户: {username}") |
| | | print(f" - 姓名: {name}") |
| | | print(f" - 邮箱: {email}") |
| | | print(f" - 密码: {password}") |
| | | print(f" - 状态: {user.status}") |
| | | return user |
| | | |
| | | except Exception as e: |
| | | print(f"\n✗ 创建用户失败: {e}") |
| | | return None |
| | | |
| | | |
| | | def create_test_users(): |
| | | """ |
| | | 创建测试用户 |
| | | """ |
| | | test_users = [ |
| | | { |
| | | 'username': 'testuser', |
| | | 'password': 'test123', |
| | | 'email': 'test@example.com', |
| | | 'name': '测试用户', |
| | | 'is_staff': False, |
| | | 'is_superuser': False |
| | | }, |
| | | { |
| | | 'username': 'developer', |
| | | 'password': 'dev123', |
| | | 'email': 'dev@example.com', |
| | | 'name': '开发人员', |
| | | 'is_staff': True, |
| | | 'is_superuser': False |
| | | } |
| | | ] |
| | | |
| | | created_count = 0 |
| | | print("\n" + "=" * 60) |
| | | print("创建测试用户...") |
| | | print("=" * 60) |
| | | |
| | | for user_data in test_users: |
| | | try: |
| | | user = User.objects.get(username=user_data['username']) |
| | | print(f"用户 '{user_data['username']}' 已存在,跳过创建") |
| | | continue |
| | | |
| | | except User.DoesNotExist: |
| | | try: |
| | | user = User.objects.create_user( |
| | | username=user_data['username'], |
| | | email=user_data['email'], |
| | | password=user_data['password'] |
| | | ) |
| | | user.name = user_data['name'] |
| | | user.is_staff = user_data['is_staff'] |
| | | user.is_superuser = user_data['is_superuser'] |
| | | user.phone = None |
| | | user.show_hosts = False |
| | | user.status = 'approved' |
| | | user.save() |
| | | print(f"✓ 成功创建用户: {user_data['username']} ({user_data['name']})") |
| | | created_count += 1 |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 创建用户 '{user_data['username']}' 失败: {e}") |
| | | |
| | | return created_count |
| | | |
| | | |
| | | def list_groups(): |
| | | """ |
| | | 列出所有用户组 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("系统用户组列表:") |
| | | print("=" * 60) |
| | | groups = Group.objects.all() |
| | | if not groups: |
| | | print("暂无用户组") |
| | | else: |
| | | for group in groups: |
| | | print(f"\n组名: {group.name}") |
| | | print(f" 权限数量: {group.permissions.count()}") |
| | | print(f" 用户数量: {group.user_set.count()}") |
| | | if group.user_set.exists(): |
| | | print(f" 成员: {', '.join([u.username for u in group.user_set.all()])}") |
| | | |
| | | |
| | | def create_default_groups(): |
| | | """ |
| | | 创建默认用户组 |
| | | """ |
| | | groups_config = [ |
| | | {'name': '管理员组', 'description': '拥有所有权限的管理员组'}, |
| | | {'name': '开发人员组', 'description': '开发人员组'}, |
| | | {'name': '测试人员组', 'description': '测试人员组'}, |
| | | {'name': '只读用户组', 'description': '只读用户组'}, |
| | | ] |
| | | |
| | | created_count = 0 |
| | | print("\n" + "=" * 60) |
| | | print("创建默认用户组...") |
| | | print("=" * 60) |
| | | |
| | | for group_config in groups_config: |
| | | try: |
| | | Group.objects.get(name=group_config['name']) |
| | | print(f"用户组 '{group_config['name']}' 已存在") |
| | | except Group.DoesNotExist: |
| | | try: |
| | | Group.objects.create(name=group_config['name']) |
| | | print(f"✓ 创建用户组: {group_config['name']}") |
| | | created_count += 1 |
| | | except Exception as e: |
| | | print(f"✗ 创建用户组 '{group_config['name']}' 失败: {e}") |
| | | |
| | | return created_count |
| | | |
| | | |
| | | if __name__ == '__main__': |
| | | import argparse |
| | | |
| | | parser = argparse.ArgumentParser(description='用户管理脚本') |
| | | parser.add_argument('--list', action='store_true', help='列出所有用户') |
| | | parser.add_argument('--create-admin', action='store_true', help='创建管理员用户') |
| | | parser.add_argument('--create-test', action='store_true', help='创建测试用户') |
| | | parser.add_argument('--create-groups', action='store_true', help='创建默认用户组') |
| | | parser.add_argument('--list-groups', action='store_true', help='列出所有用户组') |
| | | parser.add_argument('--init', action='store_true', help='初始化所有数据(创建管理员、测试用户和用户组)') |
| | | |
| | | args = parser.parse_args() |
| | | |
| | | if args.list: |
| | | list_users() |
| | | elif args.create_admin: |
| | | create_admin_user() |
| | | elif args.create_test: |
| | | create_test_users() |
| | | elif args.create_groups: |
| | | create_default_groups() |
| | | elif args.list_groups: |
| | | list_groups() |
| | | elif args.init: |
| | | print("=" * 60) |
| | | print("初始化用户数据...") |
| | | print("=" * 60) |
| | | |
| | | admin = create_admin_user() |
| | | if admin: |
| | | test_count = create_test_users() |
| | | group_count = create_default_groups() |
| | | |
| | | list_users() |
| | | list_groups() |
| | | |
| | | print("\n" + "=" * 60) |
| | | print("初始化完成!") |
| | | print("=" * 60) |
| | | print(f"✓ 创建了 {test_count} 个测试用户") |
| | | print(f"✓ 创建了 {group_count} 个用户组") |
| | | print("\n默认管理员账号:") |
| | | print(" 用户名: admin") |
| | | print(" 密码: admin123") |
| | | print("\n⚠️ 请在首次登录后修改默认密码!") |
| | | else: |
| | | parser.print_help() |
| New file |
| | |
| | | # 首次部署脚本使用说明 |
| | | |
| | | 本目录包含接口自动化平台首次部署时所需的初始化脚本。 |
| | | |
| | | ## 脚本列表 |
| | | |
| | | ### 1. deploy_init.py - 首次部署初始化脚本(推荐) |
| | | |
| | | 一键执行所有初始化操作,包括: |
| | | - 检查数据库连接 |
| | | - 执行数据库迁移 |
| | | - 创建管理员用户 |
| | | - 创建默认用户组 |
| | | - 收集静态文件 |
| | | |
| | | **使用方法:** |
| | | ```bash |
| | | cd backend |
| | | python scripts/deploy_init.py |
| | | ``` |
| | | |
| | | **默认管理员账号:** |
| | | - 用户名: `admin` |
| | | - 密码: `admin123` |
| | | |
| | | ⚠️ **重要提示:** 请在首次登录后立即修改默认密码! |
| | | |
| | | --- |
| | | |
| | | ### 2. run_migrations.py - 数据库迁移脚本 |
| | | |
| | | 单独执行数据库迁移操作。 |
| | | |
| | | **使用方法:** |
| | | ```bash |
| | | cd backend |
| | | python scripts/run_migrations.py |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 3. create_admin_user.py - 创建管理员用户脚本 |
| | | |
| | | 创建管理员用户和测试用户。 |
| | | |
| | | **使用方法:** |
| | | ```bash |
| | | cd backend |
| | | python scripts/create_admin_user.py |
| | | ``` |
| | | |
| | | **默认创建的用户:** |
| | | - `admin` - 系统管理员(超级用户) |
| | | - `testuser` - 测试用户(普通用户) |
| | | - `developer` - 开发人员(员工用户) |
| | | |
| | | **自定义管理员账号:** |
| | | 修改脚本中的 `create_admin_user()` 函数参数: |
| | | ```python |
| | | create_admin_user( |
| | | username='your_username', |
| | | password='your_password', |
| | | email='your_email@example.com', |
| | | name='your_name' |
| | | ) |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ### 4. create_groups.py - 创建用户组脚本 |
| | | |
| | | 创建默认用户组并分配权限。 |
| | | |
| | | **使用方法:** |
| | | ```bash |
| | | cd backend |
| | | python scripts/create_groups.py |
| | | ``` |
| | | |
| | | **默认创建的用户组:** |
| | | - `管理员组` - 拥有所有权限 |
| | | - `开发人员组` - 可以查看、添加和修改数据 |
| | | - `测试人员组` - 可以查看和添加数据 |
| | | - `只读用户组` - 只能查看数据 |
| | | |
| | | --- |
| | | |
| | | ### 5. collect_static.py - 收集静态文件脚本 |
| | | |
| | | 收集所有静态文件到 STATIC_ROOT 目录。 |
| | | |
| | | **使用方法:** |
| | | ```bash |
| | | cd backend |
| | | python scripts/collect_static.py |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 部署前准备 |
| | | |
| | | ### 1. 配置数据库连接 |
| | | |
| | | 编辑 `backend/conf/env.py` 文件,配置数据库连接信息: |
| | | |
| | | ```python |
| | | DATABASE_NAME = 'your_database_name' |
| | | DATABASE_USER = 'your_database_user' |
| | | DATABASE_PASSWORD = 'your_database_password' |
| | | DATABASE_HOST = 'localhost' |
| | | DATABASE_PORT = '3306' |
| | | ``` |
| | | |
| | | ### 2. 创建数据库 |
| | | |
| | | 在 MySQL 中创建数据库: |
| | | |
| | | ```sql |
| | | CREATE DATABASE your_database_name CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
| | | ``` |
| | | |
| | | ### 3. 安装依赖 |
| | | |
| | | ```bash |
| | | cd backend |
| | | pip install -r requirements.txt |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 完整部署流程 |
| | | |
| | | ### 方式一:使用一键部署脚本(推荐) |
| | | |
| | | ```bash |
| | | cd backend |
| | | python scripts/deploy_init.py |
| | | ``` |
| | | |
| | | ### 方式二:分步执行 |
| | | |
| | | ```bash |
| | | cd backend |
| | | |
| | | # 1. 执行数据库迁移 |
| | | python scripts/run_migrations.py |
| | | |
| | | # 2. 创建管理员用户 |
| | | python scripts/create_admin_user.py |
| | | |
| | | # 3. 创建用户组 |
| | | python scripts/create_groups.py |
| | | |
| | | # 4. 收集静态文件 |
| | | python scripts/collect_static.py |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 启动服务 |
| | | |
| | | ### 开发环境 |
| | | |
| | | ```bash |
| | | cd backend |
| | | python manage.py runserver |
| | | ``` |
| | | |
| | | ### 生产环境 |
| | | |
| | | ```bash |
| | | cd backend |
| | | gunicorn backend.wsgi:application -c gunicorn_conf.py |
| | | ``` |
| | | |
| | | --- |
| | | |
| | | ## 常见问题 |
| | | |
| | | ### 1. 数据库连接失败 |
| | | |
| | | 检查 `conf/env.py` 中的数据库配置是否正确,确保数据库已创建且用户有足够的权限。 |
| | | |
| | | ### 2. 迁移失败 |
| | | |
| | | 确保数据库表不存在或为空。如果之前有迁移历史,可能需要删除 `migrations` 目录下的迁移文件(保留 `__init__.py`)。 |
| | | |
| | | ### 3. 静态文件收集失败 |
| | | |
| | | 确保 `STATIC_ROOT` 目录有写入权限。 |
| | | |
| | | ### 4. 用户已存在 |
| | | |
| | | 脚本会自动检测用户是否已存在,如果存在则跳过创建。 |
| | | |
| | | --- |
| | | |
| | | ## 数据库表结构说明 |
| | | |
| | | ### lunaruser 应用 |
| | | - `lunaruser_myuser` - 用户表(继承自 Django AbstractUser) |
| | | |
| | | ### lunarlink 应用 |
| | | - `project` - 项目信息表 |
| | | - `config` - 环境信息表 |
| | | - `api` - 接口信息表 |
| | | - `case` - 用例信息表 |
| | | - `case_step` - 用例步骤表 |
| | | - `variables` - 全局变量表 |
| | | - `debugtalk` - 驱动代码表 |
| | | - `report` - 测试报告表 |
| | | - `report_detail` - 测试报告详情表 |
| | | - `relation` - 树形结构关系表 |
| | | - `visit` - 访问日志表 |
| | | - `login_log` - 登录日志表 |
| | | |
| | | --- |
| | | |
| | | ## 安全建议 |
| | | |
| | | 1. **修改默认密码:** 首次登录后立即修改管理员密码 |
| | | 2. **使用强密码:** 密码长度至少 8 位,包含大小写字母、数字和特殊字符 |
| | | 3. **限制访问:** 在生产环境中配置防火墙规则 |
| | | 4. **启用 HTTPS:** 生产环境必须使用 HTTPS |
| | | 5. **定期备份:** 定期备份数据库和重要文件 |
| | | |
| | | --- |
| | | |
| | | ## 技术支持 |
| | | |
| | | 如有问题,请查看项目文档或联系技术支持团队。 |
| New file |
| | |
| | | import os |
| | | import sys |
| | | import django |
| | | |
| | | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') |
| | | django.setup() |
| | | |
| | | from django.core.management import call_command |
| | | |
| | | |
| | | def collect_static(): |
| | | """ |
| | | 收集静态文件 |
| | | """ |
| | | print("=" * 60) |
| | | print("开始收集静态文件...") |
| | | print("=" * 60) |
| | | |
| | | try: |
| | | call_command('collectstatic', '--noinput', verbosity=2) |
| | | print("\n✓ 静态文件收集完成!") |
| | | return True |
| | | |
| | | except Exception as e: |
| | | print(f"\n✗ 静态文件收集失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return False |
| | | |
| | | |
| | | def create_superuser(): |
| | | """ |
| | | 交互式创建超级用户 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("创建超级用户 (可选)") |
| | | print("=" * 60) |
| | | print("如需创建超级用户,请按 Ctrl+C 取消,然后单独运行 create_admin_user.py") |
| | | print() |
| | | |
| | | try: |
| | | call_command('createsuperuser') |
| | | return True |
| | | except KeyboardInterrupt: |
| | | print("\n跳过创建超级用户") |
| | | return True |
| | | except Exception as e: |
| | | print(f"创建超级用户失败: {e}") |
| | | return False |
| | | |
| | | |
| | | if __name__ == '__main__': |
| | | success = collect_static() |
| | | if success: |
| | | create_superuser() |
| | | sys.exit(0) |
| | | else: |
| | | sys.exit(1) |
| New file |
| | |
| | | import os |
| | | import sys |
| | | import django |
| | | |
| | | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') |
| | | django.setup() |
| | | |
| | | from django.contrib.auth import get_user_model |
| | | from django.core.exceptions import ValidationError |
| | | |
| | | |
| | | User = get_user_model() |
| | | |
| | | |
| | | def create_admin_user(username='admin', password='admin123', email='admin@example.com', name='系统管理员'): |
| | | """ |
| | | 创建管理员用户 |
| | | |
| | | Args: |
| | | username: 用户名 |
| | | password: 密码 |
| | | email: 邮箱 |
| | | name: 姓名 |
| | | |
| | | Returns: |
| | | User: 创建的用户对象或已存在的用户对象 |
| | | """ |
| | | try: |
| | | user = User.objects.get(username=username) |
| | | print(f"用户 '{username}' 已存在,跳过创建") |
| | | return user |
| | | |
| | | except User.DoesNotExist: |
| | | try: |
| | | user = User.objects.create_superuser( |
| | | username=username, |
| | | email=email, |
| | | password=password |
| | | ) |
| | | user.name = name |
| | | user.phone = None |
| | | user.show_hosts = False |
| | | user.status = 'approved' |
| | | user.save() |
| | | print(f"✓ 成功创建管理员用户: {username}") |
| | | print(f" - 姓名: {name}") |
| | | print(f" - 邮箱: {email}") |
| | | print(f" - 状态: {user.status}") |
| | | return user |
| | | |
| | | except ValidationError as e: |
| | | print(f"✗ 创建用户失败,验证错误: {e}") |
| | | return None |
| | | except Exception as e: |
| | | print(f"✗ 创建用户失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return None |
| | | |
| | | |
| | | def create_test_users(): |
| | | """ |
| | | 创建测试用户 |
| | | """ |
| | | test_users = [ |
| | | { |
| | | 'username': 'testuser', |
| | | 'password': 'test123', |
| | | 'email': 'test@example.com', |
| | | 'name': '测试用户', |
| | | 'is_staff': False, |
| | | 'is_superuser': False |
| | | }, |
| | | { |
| | | 'username': 'developer', |
| | | 'password': 'dev123', |
| | | 'email': 'dev@example.com', |
| | | 'name': '开发人员', |
| | | 'is_staff': True, |
| | | 'is_superuser': False |
| | | } |
| | | ] |
| | | |
| | | created_count = 0 |
| | | for user_data in test_users: |
| | | try: |
| | | user = User.objects.get(username=user_data['username']) |
| | | print(f"用户 '{user_data['username']}' 已存在,跳过创建") |
| | | continue |
| | | |
| | | except User.DoesNotExist: |
| | | try: |
| | | user = User.objects.create_user( |
| | | username=user_data['username'], |
| | | email=user_data['email'], |
| | | password=user_data['password'] |
| | | ) |
| | | user.name = user_data['name'] |
| | | user.is_staff = user_data['is_staff'] |
| | | user.is_superuser = user_data['is_superuser'] |
| | | user.phone = None |
| | | user.show_hosts = False |
| | | user.status = 'approved' |
| | | user.save() |
| | | print(f"✓ 成功创建用户: {user_data['username']} ({user_data['name']})") |
| | | created_count += 1 |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 创建用户 '{user_data['username']}' 失败: {e}") |
| | | |
| | | return created_count |
| | | |
| | | |
| | | def list_all_users(): |
| | | """ |
| | | 列出所有用户 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("当前系统用户列表:") |
| | | print("=" * 60) |
| | | users = User.objects.all() |
| | | if not users: |
| | | print("暂无用户") |
| | | else: |
| | | for user in users: |
| | | print(f" - 用户名: {user.username}") |
| | | print(f" 姓名: {user.name}") |
| | | print(f" 邮箱: {user.email}") |
| | | print(f" 手机号: {user.phone}") |
| | | print(f" 状态: {user.status}") |
| | | print(f" 是否管理员: {'是' if user.is_superuser else '否'}") |
| | | print(f" 是否员工: {'是' if user.is_staff else '否'}") |
| | | print(f" 是否激活: {'是' if user.is_active else '否'}") |
| | | print() |
| | | |
| | | |
| | | if __name__ == '__main__': |
| | | print("=" * 60) |
| | | print("开始初始化用户数据...") |
| | | print("=" * 60) |
| | | |
| | | admin = create_admin_user() |
| | | if admin: |
| | | print("\n创建测试用户...") |
| | | test_count = create_test_users() |
| | | print(f"\n✓ 成功创建 {test_count} 个测试用户") |
| | | |
| | | list_all_users() |
| | | |
| | | print("\n" + "=" * 60) |
| | | print("用户初始化完成!") |
| | | print("=" * 60) |
| | | sys.exit(0) |
| | | else: |
| | | print("\n" + "=" * 60) |
| | | print("用户初始化失败!") |
| | | print("=" * 60) |
| | | sys.exit(1) |
| New file |
| | |
| | | import os |
| | | import sys |
| | | import django |
| | | |
| | | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') |
| | | django.setup() |
| | | |
| | | from django.contrib.auth.models import Group, Permission |
| | | from django.contrib.contenttypes.models import ContentType |
| | | from apps.lunarlink.models import Project, API, Case, Config, Variables, Report |
| | | |
| | | |
| | | def create_default_groups(): |
| | | """ |
| | | 创建默认用户组 |
| | | """ |
| | | groups_config = [ |
| | | { |
| | | 'name': '管理员组', |
| | | 'permissions': ['all'], |
| | | 'description': '拥有所有权限的管理员组' |
| | | }, |
| | | { |
| | | 'name': '开发人员组', |
| | | 'permissions': ['view', 'add', 'change'], |
| | | 'description': '开发人员组,可以查看、添加和修改数据' |
| | | }, |
| | | { |
| | | 'name': '测试人员组', |
| | | 'permissions': ['view', 'add'], |
| | | 'description': '测试人员组,可以查看和添加数据' |
| | | }, |
| | | { |
| | | 'name': '只读用户组', |
| | | 'permissions': ['view'], |
| | | 'description': '只读用户组,只能查看数据' |
| | | } |
| | | ] |
| | | |
| | | created_count = 0 |
| | | for group_config in groups_config: |
| | | try: |
| | | group = Group.objects.get(name=group_config['name']) |
| | | print(f"用户组 '{group_config['name']}' 已存在,跳过创建") |
| | | continue |
| | | |
| | | except Group.DoesNotExist: |
| | | try: |
| | | group = Group.objects.create(name=group_config['name']) |
| | | print(f"✓ 成功创建用户组: {group_config['name']}") |
| | | |
| | | if 'all' in group_config['permissions']: |
| | | all_permissions = Permission.objects.all() |
| | | group.permissions.set(all_permissions) |
| | | print(f" - 已添加所有权限") |
| | | else: |
| | | for perm_type in group_config['permissions']: |
| | | for model in [Project, API, Case, Config, Variables, Report]: |
| | | content_type = ContentType.objects.get_for_model(model) |
| | | codename = f"{perm_type}_{model._meta.model_name}" |
| | | try: |
| | | perm = Permission.objects.get( |
| | | content_type=content_type, |
| | | codename=codename |
| | | ) |
| | | group.permissions.add(perm) |
| | | except Permission.DoesNotExist: |
| | | pass |
| | | |
| | | print(f" - 已添加权限: {', '.join(group_config['permissions'])}") |
| | | |
| | | created_count += 1 |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 创建用户组 '{group_config['name']}' 失败: {e}") |
| | | |
| | | return created_count |
| | | |
| | | |
| | | def list_all_groups(): |
| | | """ |
| | | 列出所有用户组 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("当前系统用户组列表:") |
| | | print("=" * 60) |
| | | groups = Group.objects.all() |
| | | if not groups: |
| | | print("暂无用户组") |
| | | else: |
| | | for group in groups: |
| | | print(f" - 组名: {group.name}") |
| | | print(f" 权限数量: {group.permissions.count()}") |
| | | print(f" 用户数量: {group.user_set.count()}") |
| | | print() |
| | | |
| | | |
| | | if __name__ == '__main__': |
| | | print("=" * 60) |
| | | print("开始初始化用户组数据...") |
| | | print("=" * 60) |
| | | |
| | | count = create_default_groups() |
| | | print(f"\n✓ 成功创建 {count} 个用户组") |
| | | |
| | | list_all_groups() |
| | | |
| | | print("\n" + "=" * 60) |
| | | print("用户组初始化完成!") |
| | | print("=" * 60) |
| New file |
| | |
| | | #!/usr/bin/env python |
| | | """ |
| | | 首次部署初始化脚本 |
| | | |
| | | 该脚本用于首次部署系统时执行所有必要的初始化操作,包括: |
| | | 1. 数据库迁移 |
| | | 2. 创建管理员用户 |
| | | 3. 创建默认用户组 |
| | | 4. 收集静态文件 |
| | | |
| | | 使用方法: |
| | | python scripts/deploy_init.py |
| | | |
| | | 注意事项: |
| | | - 确保已配置好数据库连接信息 (conf/env.py) |
| | | - 确保数据库已创建 |
| | | - 确保已安装所有依赖 (pip install -r requirements.txt) |
| | | """ |
| | | |
| | | import os |
| | | import sys |
| | | import django |
| | | |
| | | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') |
| | | django.setup() |
| | | |
| | | from django.core.management import call_command |
| | | |
| | | |
| | | def check_database_connection(): |
| | | """ |
| | | 检查数据库连接 |
| | | """ |
| | | print("=" * 60) |
| | | print("检查数据库连接...") |
| | | print("=" * 60) |
| | | |
| | | try: |
| | | from django.db import connection |
| | | with connection.cursor() as cursor: |
| | | cursor.execute("SELECT 1") |
| | | print("✓ 数据库连接正常") |
| | | return True |
| | | except Exception as e: |
| | | print(f"✗ 数据库连接失败: {e}") |
| | | print("\n请检查 conf/env.py 中的数据库配置:") |
| | | print(" - DATABASE_NAME") |
| | | print(" - DATABASE_USER") |
| | | print(" - DATABASE_PASSWORD") |
| | | print(" - DATABASE_HOST") |
| | | print(" - DATABASE_PORT") |
| | | return False |
| | | |
| | | |
| | | def run_migrations(): |
| | | """ |
| | | 执行数据库迁移 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("执行数据库迁移...") |
| | | print("=" * 60) |
| | | |
| | | try: |
| | | print("\n[1/2] 生成迁移文件...") |
| | | call_command('makemigrations', verbosity=1) |
| | | print("✓ 迁移文件生成成功") |
| | | |
| | | print("\n[2/2] 应用迁移...") |
| | | call_command('migrate', verbosity=1) |
| | | print("✓ 数据库迁移成功") |
| | | return True |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 数据库迁移失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return False |
| | | |
| | | |
| | | def create_admin_user(): |
| | | """ |
| | | 创建管理员用户 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("创建管理员用户...") |
| | | print("=" * 60) |
| | | |
| | | from django.contrib.auth import get_user_model |
| | | User = get_user_model() |
| | | |
| | | default_username = 'admin' |
| | | default_password = 'admin123' |
| | | default_email = 'admin@example.com' |
| | | default_name = '系统管理员' |
| | | |
| | | try: |
| | | user = User.objects.get(username=default_username) |
| | | print(f"管理员用户 '{default_username}' 已存在,跳过创建") |
| | | print(f" 用户名: {user.username}") |
| | | print(f" 姓名: {user.name}") |
| | | print(f" 邮箱: {user.email}") |
| | | return True |
| | | |
| | | except User.DoesNotExist: |
| | | try: |
| | | user = User.objects.create_superuser( |
| | | username=default_username, |
| | | email=default_email, |
| | | password=default_password |
| | | ) |
| | | user.name = default_name |
| | | user.phone = None |
| | | user.show_hosts = False |
| | | user.status = 'approved' |
| | | user.save() |
| | | |
| | | print("✓ 管理员用户创建成功") |
| | | print(f" 用户名: {default_username}") |
| | | print(f" 密码: {default_password}") |
| | | print(f" 邮箱: {default_email}") |
| | | print(f" 姓名: {default_name}") |
| | | print("\n⚠️ 重要提示: 请在首次登录后修改默认密码!") |
| | | return True |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 创建管理员用户失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return False |
| | | |
| | | |
| | | def create_default_groups(): |
| | | """ |
| | | 创建默认用户组 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("创建默认用户组...") |
| | | print("=" * 60) |
| | | |
| | | from django.contrib.auth.models import Group |
| | | |
| | | groups = [ |
| | | {'name': '管理员组', 'description': '拥有所有权限的管理员组'}, |
| | | {'name': '开发人员组', 'description': '开发人员组'}, |
| | | {'name': '测试人员组', 'description': '测试人员组'}, |
| | | {'name': '只读用户组', 'description': '只读用户组'}, |
| | | ] |
| | | |
| | | created_count = 0 |
| | | for group_info in groups: |
| | | try: |
| | | Group.objects.get(name=group_info['name']) |
| | | print(f" 用户组 '{group_info['name']}' 已存在") |
| | | except Group.DoesNotExist: |
| | | try: |
| | | Group.objects.create(name=group_info['name']) |
| | | print(f" ✓ 创建用户组: {group_info['name']}") |
| | | created_count += 1 |
| | | except Exception as e: |
| | | print(f" ✗ 创建用户组 '{group_info['name']}' 失败: {e}") |
| | | |
| | | print(f"\n✓ 成功创建 {created_count} 个用户组") |
| | | return True |
| | | |
| | | |
| | | def collect_static_files(): |
| | | """ |
| | | 收集静态文件 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("收集静态文件...") |
| | | print("=" * 60) |
| | | |
| | | try: |
| | | call_command('collectstatic', '--noinput', verbosity=1) |
| | | print("✓ 静态文件收集成功") |
| | | return True |
| | | |
| | | except Exception as e: |
| | | print(f"✗ 静态文件收集失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return False |
| | | |
| | | |
| | | def display_summary(): |
| | | """ |
| | | 显示部署摘要 |
| | | """ |
| | | print("\n" + "=" * 60) |
| | | print("部署摘要") |
| | | print("=" * 60) |
| | | |
| | | from django.contrib.auth import get_user_model |
| | | from django.contrib.auth.models import Group |
| | | from apps.lunarlink.models import Project |
| | | |
| | | User = get_user_model() |
| | | |
| | | print(f"\n用户数量: {User.objects.count()}") |
| | | print(f"用户组数量: {Group.objects.count()}") |
| | | print(f"项目数量: {Project.objects.count()}") |
| | | |
| | | print("\n默认管理员账号:") |
| | | print(" 用户名: admin") |
| | | print(" 密码: admin123") |
| | | print("\n⚠️ 请在首次登录后立即修改默认密码!") |
| | | |
| | | |
| | | def main(): |
| | | """ |
| | | 主函数 |
| | | """ |
| | | print("\n") |
| | | print("╔" + "=" * 58 + "╗") |
| | | print("║" + " " * 10 + "接口自动化平台 - 首次部署初始化" + " " * 13 + "║") |
| | | print("╚" + "=" * 58 + "╝") |
| | | print() |
| | | |
| | | steps = [ |
| | | ("检查数据库连接", check_database_connection), |
| | | ("执行数据库迁移", run_migrations), |
| | | ("创建管理员用户", create_admin_user), |
| | | ("创建默认用户组", create_default_groups), |
| | | ("收集静态文件", collect_static_files), |
| | | ] |
| | | |
| | | failed_steps = [] |
| | | |
| | | for step_name, step_func in steps: |
| | | if not step_func(): |
| | | failed_steps.append(step_name) |
| | | print(f"\n✗ '{step_name}' 失败,终止部署") |
| | | break |
| | | |
| | | if not failed_steps: |
| | | display_summary() |
| | | print("\n" + "=" * 60) |
| | | print("✓ 部署初始化完成!") |
| | | print("=" * 60) |
| | | print("\n系统已准备就绪,可以启动服务:") |
| | | print(" python manage.py runserver") |
| | | print("\n或使用 gunicorn 启动:") |
| | | print(" gunicorn backend.wsgi:application -c gunicorn_conf.py") |
| | | print() |
| | | return 0 |
| | | else: |
| | | print("\n" + "=" * 60) |
| | | print("✗ 部署初始化失败!") |
| | | print("=" * 60) |
| | | print(f"\n失败的步骤: {', '.join(failed_steps)}") |
| | | print("\n请检查错误信息并修复后重试") |
| | | print() |
| | | return 1 |
| | | |
| | | |
| | | if __name__ == '__main__': |
| | | sys.exit(main()) |
| New file |
| | |
| | | import os |
| | | import sys |
| | | import django |
| | | |
| | | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings') |
| | | django.setup() |
| | | |
| | | from django.core.management import call_command |
| | | |
| | | |
| | | def run_migrations(): |
| | | """ |
| | | 执行数据库迁移脚本 |
| | | 用于首次部署时创建数据库表结构 |
| | | """ |
| | | print("=" * 60) |
| | | print("开始执行数据库迁移...") |
| | | print("=" * 60) |
| | | |
| | | try: |
| | | print("\n[1/2] 执行 makemigrations...") |
| | | call_command('makemigrations', verbosity=2) |
| | | print("✓ makemigrations 执行成功") |
| | | |
| | | print("\n[2/2] 执行 migrate...") |
| | | call_command('migrate', verbosity=2) |
| | | print("✓ migrate 执行成功") |
| | | |
| | | print("\n" + "=" * 60) |
| | | print("数据库迁移完成!") |
| | | print("=" * 60) |
| | | return True |
| | | |
| | | except Exception as e: |
| | | print(f"\n✗ 数据库迁移失败: {e}") |
| | | import traceback |
| | | traceback.print_exc() |
| | | return False |
| | | |
| | | |
| | | if __name__ == '__main__': |
| | | success = run_migrations() |
| | | sys.exit(0 if success else 1) |
| | |
| | | Authorization: store.token |
| | | } |
| | | }).then(res => { |
| | | // 密码修改成功后清除token和用户信息 |
| | | store.token = null |
| | | store.user = null |
| | | store.name = null |
| | |
| | | return res.data |
| | | }) |
| | | } |
| | | |
| | | export function register(data) { |
| | | return axios({ |
| | | url: '/api/user/register', |
| | | method: 'post', |
| | | data |
| | | }).then(res => { |
| | | return res.data |
| | | }) |
| | | } |
| | | |
| | | export function getUserApprovalList(params) { |
| | | return axios({ |
| | | url: '/api/user/approval/', |
| | | method: 'get', |
| | | params, |
| | | headers: { |
| | | Authorization: store.token |
| | | } |
| | | }).then(res => { |
| | | return res.data |
| | | }) |
| | | } |
| | | |
| | | export function approveUser(userId, data) { |
| | | return axios({ |
| | | url: `/api/user/approval/${userId}/approve/`, |
| | | method: 'post', |
| | | data, |
| | | headers: { |
| | | Authorization: store.token |
| | | } |
| | | }).then(res => { |
| | | return res.data |
| | | }) |
| | | } |
| | |
| | | }}</span> |
| | | </div> |
| | | <div class="right"> |
| | | <div class="approval-button-wrapper" v-if="$store.state.is_superuser || $store.state.is_staff"> |
| | | <el-badge :value="pendingCount" :hidden="pendingCount === 0" class="approval-badge"> |
| | | <el-button type="success" size="small" icon="el-icon-s-check" @click="handleApproval"> |
| | | 注册用户审批 |
| | | </el-button> |
| | | </el-badge> |
| | | </div> |
| | | <div style="float: right; color: #262626; margin-right: 35px;"> |
| | | <el-dropdown trigger="click" @command="handleCommand"> |
| | | <span class="el-dropdown-link"> |
| | |
| | | {{ $store.state.name }} |
| | | <i class="el-icon-arrow-down el-icon--right"></i> |
| | | </span> |
| | | |
| | | <el-dropdown-menu slot="dropdown"> |
| | | <el-dropdown-item command="changePassword"> |
| | | <i class="el-icon-edit"></i>修改密码 |
| | |
| | | <el-button type="primary" @click="submitPasswordChange">确 定</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <el-drawer |
| | | title="注册用户审批" |
| | | :visible.sync="approvalDrawerVisible" |
| | | direction="rtl" |
| | | size="60%" |
| | | :modal="false" |
| | | :append-to-body="true" |
| | | > |
| | | <div class="approval-container"> |
| | | <el-table |
| | | :data="approvalData" |
| | | v-loading="approvalLoading" |
| | | stripe |
| | | style="width: 100%" |
| | | > |
| | | <el-table-column prop="username" label="用户名" width="120"></el-table-column> |
| | | <el-table-column prop="name" label="姓名" width="120"></el-table-column> |
| | | <el-table-column prop="phone" label="手机号" width="120"></el-table-column> |
| | | <el-table-column label="所属分组" width="200"> |
| | | <template slot-scope="scope"> |
| | | <el-tag |
| | | v-for="group in scope.row.groups" |
| | | :key="group" |
| | | size="mini" |
| | | style="margin-right: 3px" |
| | | > |
| | | {{ group }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)"> |
| | | {{ scope.row.status_display }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="注册时间" width="180"> |
| | | <template slot-scope="scope"> |
| | | {{ scope.row.date_joined | datetimeFormat }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" fixed="right" width="200"> |
| | | <template slot-scope="scope"> |
| | | <el-button |
| | | type="success" |
| | | size="mini" |
| | | icon="el-icon-check" |
| | | @click="handleApprove(scope.row, 'approve')" |
| | | > |
| | | 通过 |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | size="mini" |
| | | icon="el-icon-close" |
| | | @click="handleApprove(scope.row, 'reject')" |
| | | > |
| | | 不通过 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div v-if="!approvalLoading && approvalData.length === 0" class="empty-tip"> |
| | | <el-empty description="暂无待审批用户"></el-empty> |
| | | </div> |
| | | </div> |
| | | </el-drawer> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | import { changePassword } from '@/api/user' |
| | | import { changePassword, approveUser } from '@/api/user' |
| | | import { getUserApprovalList } from '@/api/user' |
| | | |
| | | export default { |
| | | data() { |
| | |
| | | |
| | | return { |
| | | passwordDialogVisible: false, |
| | | approvalDrawerVisible: false, |
| | | approvalLoading: false, |
| | | approvalData: [], |
| | | pendingCount: 0, |
| | | passwordForm: { |
| | | old_password: '', |
| | | new_password: '', |
| | |
| | | } |
| | | }, |
| | | methods: { |
| | | addBadgeAnimation() { |
| | | if (this.pendingCount > 0) { |
| | | const badge = document.querySelector('.approval-badge .el-badge__content'); |
| | | if (badge) { |
| | | badge.classList.add('badge-enter'); |
| | | setTimeout(() => { |
| | | badge.classList.remove('badge-enter'); |
| | | }, 1000); |
| | | } |
| | | } |
| | | }, |
| | | getStatusType(status) { |
| | | const statusMap = { |
| | | 'pending': 'warning', |
| | | 'approved': 'success', |
| | | 'rejected': 'danger' |
| | | }; |
| | | return statusMap[status] || 'info'; |
| | | }, |
| | | handleCommand(command) { |
| | | if (command === 'changePassword') { |
| | | // 重置表单数据和验证状态 |
| | |
| | | this.$store.commit("isLogin", null); |
| | | this.setLocalValue("token", null); |
| | | this.setLocalValue("is_superuser", false); |
| | | this.setLocalValue("is_staff", false); |
| | | this.setLocalValue("show_hosts", false); |
| | | this.$store.commit("setProjectName", ""); |
| | | this.$router.push({ name: "Login" }); |
| | | } |
| | | }, |
| | | handleApproval() { |
| | | this.approvalDrawerVisible = true; |
| | | this.getApprovalList(); |
| | | }, |
| | | getApprovalList() { |
| | | this.approvalLoading = true; |
| | | getUserApprovalList().then(resp => { |
| | | this.approvalData = resp.results || resp; |
| | | this.pendingCount = this.approvalData.length; |
| | | this.approvalLoading = false; |
| | | if (this.pendingCount > 0) { |
| | | this.addBadgeAnimation(); |
| | | } |
| | | }).catch(() => { |
| | | this.approvalLoading = false; |
| | | }); |
| | | }, |
| | | handleApprove(row, action) { |
| | | const actionText = action === 'approve' ? '通过' : '不通过'; |
| | | this.$confirm(`确定要${actionText}该用户的注册申请吗?`, "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | }).then(() => { |
| | | this.$api.approveUser(row.id, { action: action }).then(() => { |
| | | this.$message.success(`用户审批已${actionText}`); |
| | | this.pendingCount = Math.max(0, this.pendingCount - 1); |
| | | this.getApprovalList(); |
| | | }); |
| | | }).catch(() => {}); |
| | | }, |
| | | submitPasswordChange() { |
| | | this.$refs.passwordForm.validate(valid => { |
| | |
| | | }) |
| | | } |
| | | }) |
| | | } |
| | | }, |
| | | mounted() { |
| | | if (this.$store.state.is_superuser || this.$store.state.is_staff) { |
| | | this.getApprovalList(); |
| | | setTimeout(() => { |
| | | this.addBadgeAnimation(); |
| | | }, 500); |
| | | } |
| | | } |
| | | } |
| | |
| | | color: #409EFF; |
| | | padding: 0 15px; |
| | | } |
| | | |
| | | .approval-container { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .empty-tip { |
| | | text-align: center; |
| | | padding: 40px 0; |
| | | } |
| | | |
| | | .approval-button-wrapper { |
| | | float: right; |
| | | color: #262626; |
| | | margin-right: 15px; |
| | | display: flex; |
| | | align-items: center; |
| | | height: 49px; |
| | | position: relative; |
| | | } |
| | | |
| | | /* 优化按钮本身 */ |
| | | .approval-button-wrapper .el-button { |
| | | position: relative; |
| | | padding: 7px 12px; |
| | | border-radius: 6px; |
| | | font-weight: 500; |
| | | transition: all 0.3s ease; |
| | | box-shadow: 0 2px 8px rgba(103, 194, 58, 0.2); |
| | | } |
| | | |
| | | .approval-button-wrapper .el-button:hover { |
| | | transform: translateY(-1px); |
| | | box-shadow: 0 4px 12px rgba(103, 194, 58, 0.3); |
| | | } |
| | | |
| | | .approval-button-wrapper .el-button:active { |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | /* 徽章优化 - 更自然的设计 */ |
| | | .approval-badge { |
| | | position: relative; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .approval-badge ::v-deep .el-badge__content { |
| | | position: absolute !important; |
| | | top: -8px !important; |
| | | right: -8px !important; |
| | | height: 20px; |
| | | min-width: 20px; |
| | | line-height: 20px; |
| | | padding: 0 4px; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | border-radius: 10px; |
| | | background: linear-gradient(135deg, #ff6b6b, #ff4757); |
| | | color: white; |
| | | border: 2px solid white; |
| | | box-shadow: 0 3px 6px rgba(255, 107, 107, 0.3); |
| | | z-index: 10; |
| | | animation: badge-pulse 2s infinite; |
| | | } |
| | | |
| | | /* 徽章脉冲动画 */ |
| | | @keyframes badge-pulse { |
| | | 0% { |
| | | box-shadow: 0 0 0 0 rgba(255, 107, 107, 0.4); |
| | | } |
| | | 70% { |
| | | box-shadow: 0 0 0 4px rgba(255, 107, 107, 0); |
| | | } |
| | | 100% { |
| | | box-shadow: 0 0 0 0 rgba(255, 107, 107, 0); |
| | | } |
| | | } |
| | | |
| | | /* 徽章入场动画 */ |
| | | @keyframes badge-bounce { |
| | | 0% { |
| | | transform: scale(0); |
| | | } |
| | | 50% { |
| | | transform: scale(1.2); |
| | | } |
| | | 100% { |
| | | transform: scale(1); |
| | | } |
| | | } |
| | | |
| | | .approval-badge ::v-deep .el-badge__content.badge-enter { |
| | | animation: badge-bounce 0.5s ease; |
| | | } |
| | | |
| | | /* 当徽章值为0时的隐藏效果 */ |
| | | .approval-badge ::v-deep .el-badge__content.is-fixed.is-dot { |
| | | top: -4px !important; |
| | | right: -4px !important; |
| | | height: 8px; |
| | | width: 8px; |
| | | min-width: 8px; |
| | | border: 1px solid white; |
| | | background: linear-gradient(135deg, #ffa502, #ff7f00); |
| | | } |
| | | |
| | | /* 可选:添加图标徽章效果 */ |
| | | .approval-button-wrapper .el-button i { |
| | | position: relative; |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | /* 添加按钮渐变效果 */ |
| | | .approval-button-wrapper .el-button--success { |
| | | background: linear-gradient(135deg, #85d27a, #67c23a); |
| | | border: none; |
| | | } |
| | | |
| | | .approval-button-wrapper .el-button--success:hover { |
| | | background: linear-gradient(135deg, #7ac76f, #5da534); |
| | | } |
| | | |
| | | /* 徽章数字特殊样式 */ |
| | | .approval-badge ::v-deep .el-badge__content .el-badge__inner { |
| | | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| | | letter-spacing: -0.5px; |
| | | } |
| | | |
| | | /* 调整整体布局 */ |
| | | .right { |
| | | position: fixed; |
| | | left: 300px; |
| | | right: 0; |
| | | top: 0; |
| | | display: flex; |
| | | align-items: center; |
| | | justify-content: flex-end; |
| | | padding-right: 10px; |
| | | gap: 10px; /* 增加元素间距 */ |
| | | } |
| | | |
| | | /* 用户下拉菜单样式优化 */ |
| | | .el-dropdown-link { |
| | | cursor: pointer; |
| | | color: #409EFF; |
| | | padding: 8px 16px; |
| | | border-radius: 6px; |
| | | transition: all 0.3s ease; |
| | | display: inline-flex; |
| | | align-items: center; |
| | | } |
| | | |
| | | .el-dropdown-link:hover { |
| | | background-color: rgba(64, 158, 255, 0.1); |
| | | } |
| | | |
| | | .el-dropdown-link i { |
| | | margin-right: 4px; |
| | | } |
| | | |
| | | </style> |
| | |
| | | <template> |
| | | <div class="login-container"> |
| | | <!-- 动态背景 --> |
| | | <div class="background-animation"> |
| | | <div class="floating-shapes"> |
| | | <div class="shape shape-1"></div> |
| | |
| | | <div class="gradient-overlay"></div> |
| | | </div> |
| | | |
| | | <!-- 主登录区域 --> |
| | | <div class="login-content"> |
| | | <!-- 左侧品牌展示区 --> |
| | | <div class="brand-section"> |
| | | <div class="brand-logo-container"> |
| | | <div class="logo-icon"> |
| | | <svg viewBox="0 0 100 100" class="api-icon"> |
| | | <!-- 主API连接图标 --> |
| | | <path d="M20,30 L50,10 L80,30 L80,70 L50,90 L20,70 Z" fill="rgba(0,212,255,0.1)" stroke="rgba(0,212,255,0.6)" stroke-width="2"/> |
| | | <!-- 内部连接层 --> |
| | | <path d="M30,40 L50,25 L70,40 L70,60 L50,75 L30,60 Z" fill="rgba(148,0,211,0.1)" stroke="rgba(148,0,211,0.6)" stroke-width="1.5"/> |
| | | <!-- 核心数据点 --> |
| | | <path d="M40,50 L50,40 L60,50 L60,55 L50,60 L40,55 Z" fill="rgba(255,255,255,0.2)" stroke="rgba(255,255,255,0.8)" stroke-width="1"/> |
| | | <!-- 动态连接线 --> |
| | | <line x1="35" y1="45" x2="45" y2="52" stroke="#00d4ff" stroke-width="1.5" stroke-dasharray="2,2"/> |
| | | <line x1="55" y1="45" x2="65" y2="52" stroke="#9400d3" stroke-width="1.5" stroke-dasharray="2,2"/> |
| | | <!-- 中心数据流点 --> |
| | | <circle cx="50" cy="50" r="4" fill="#00d4ff"> |
| | | <animate attributeName="r" values="4;6;4" dur="2s" repeatCount="indefinite"/> |
| | | <animate attributeName="opacity" values="1;0.7;1" dur="2s" repeatCount="indefinite"/> |
| | | </circle> |
| | | <!-- 外围数据点 --> |
| | | <circle cx="35" cy="35" r="2" fill="#00d4ff"> |
| | | <animate attributeName="opacity" values="0.5;1;0.5" dur="1.5s" repeatCount="indefinite"/> |
| | | </circle> |
| | |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- 右侧登录表单 --> |
| | | <div class="login-form-section"> |
| | | <div class="form-container"> |
| | | <div class="form-header"> |
| | | <h2>欢迎登录</h2> |
| | | <p>请使用您的账号密码登录系统</p> |
| | | <h2>{{ isLoginMode ? '欢迎登录' : '用户注册' }}</h2> |
| | | <p>{{ isLoginMode ? '请使用您的账号密码登录系统' : '请填写以下信息完成注册' }}</p> |
| | | </div> |
| | | |
| | | <el-form |
| | |
| | | @submit.native.prevent="submitForm" |
| | | class="login-form" |
| | | > |
| | | <div class="input-group"> |
| | | <div class="input-group" v-if="!isLoginMode"> |
| | | <label class="input-label">用户名</label> |
| | | <el-input |
| | | v-model="registerForm.username" |
| | | placeholder="请输入用户名" |
| | | prefix-icon="el-icon-user" |
| | | class="modern-input" |
| | | :class="{ 'input-error': registerErrors.username }" |
| | | @blur="validateRegisterField('username')" |
| | | /> |
| | | <transition name="slide-fade"> |
| | | <div v-if="registerErrors.username" class="error-msg">{{ registerErrors.username }}</div> |
| | | </transition> |
| | | </div> |
| | | |
| | | <div class="input-group" v-if="!isLoginMode"> |
| | | <label class="input-label">姓名</label> |
| | | <el-input |
| | | v-model="registerForm.name" |
| | | placeholder="请输入姓名" |
| | | prefix-icon="el-icon-edit" |
| | | class="modern-input" |
| | | :class="{ 'input-error': registerErrors.name }" |
| | | @blur="validateRegisterField('name')" |
| | | /> |
| | | <transition name="slide-fade"> |
| | | <div v-if="registerErrors.name" class="error-msg">{{ registerErrors.name }}</div> |
| | | </transition> |
| | | </div> |
| | | |
| | | <div class="input-group" v-if="!isLoginMode"> |
| | | <label class="input-label">手机号</label> |
| | | <el-input |
| | | v-model="registerForm.phone" |
| | | placeholder="请输入手机号" |
| | | prefix-icon="el-icon-phone" |
| | | class="modern-input" |
| | | :class="{ 'input-error': registerErrors.phone }" |
| | | @blur="validateRegisterField('phone')" |
| | | /> |
| | | <transition name="slide-fade"> |
| | | <div v-if="registerErrors.phone" class="error-msg">{{ registerErrors.phone }}</div> |
| | | </transition> |
| | | </div> |
| | | |
| | | <div class="input-group" v-if="!isLoginMode"> |
| | | <label class="input-label">所属分组(可选)</label> |
| | | <el-select |
| | | v-model="registerForm.groups" |
| | | multiple |
| | | placeholder="请选择分组" |
| | | class="modern-input" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="group in groupList" |
| | | :key="group.id" |
| | | :label="group.name" |
| | | :value="group.id" |
| | | ></el-option> |
| | | </el-select> |
| | | </div> |
| | | |
| | | <div class="input-group" v-if="!isLoginMode"> |
| | | <label class="input-label">密码</label> |
| | | <el-input |
| | | v-model="registerForm.password" |
| | | type="password" |
| | | placeholder="请输入密码" |
| | | prefix-icon="el-icon-lock" |
| | | show-password |
| | | class="modern-input" |
| | | :class="{ 'input-error': registerErrors.password }" |
| | | @blur="validateRegisterField('password')" |
| | | /> |
| | | <transition name="slide-fade"> |
| | | <div v-if="registerErrors.password" class="error-msg">{{ registerErrors.password }}</div> |
| | | </transition> |
| | | </div> |
| | | |
| | | <div class="input-group" v-if="!isLoginMode"> |
| | | <label class="input-label">确认密码</label> |
| | | <el-input |
| | | v-model="registerForm.confirm_password" |
| | | type="password" |
| | | placeholder="请再次输入密码" |
| | | prefix-icon="el-icon-lock" |
| | | show-password |
| | | class="modern-input" |
| | | :class="{ 'input-error': registerErrors.confirm_password }" |
| | | @blur="validateRegisterField('confirm_password')" |
| | | /> |
| | | <transition name="slide-fade"> |
| | | <div v-if="registerErrors.confirm_password" class="error-msg">{{ registerErrors.confirm_password }}</div> |
| | | </transition> |
| | | </div> |
| | | |
| | | <div class="input-group" v-if="isLoginMode"> |
| | | <label class="input-label">用户名</label> |
| | | <el-input |
| | | v-model="loginForm.username" |
| | |
| | | </transition> |
| | | </div> |
| | | |
| | | <div class="input-group"> |
| | | <div class="input-group" v-if="isLoginMode"> |
| | | <label class="input-label">密码</label> |
| | | <el-input |
| | | v-model="loginForm.password" |
| | |
| | | :loading="isLoading" |
| | | @click="submitForm" |
| | | > |
| | | {{ isLoading ? '登录中...' : '登录系统' }} |
| | | {{ isLoading ? (isLoginMode ? '登录中...' : '注册中...') : (isLoginMode ? '登录系统' : '注册账号') }} |
| | | </el-button> |
| | | </el-form> |
| | | |
| | | <div class="form-footer"> |
| | | <p>还没有账号?<a href="#" class="register-link">联系管理员注册</a></p> |
| | | <p v-if="isLoginMode">还没有账号?<a href="#" class="register-link" @click.prevent="switchMode">立即注册</a></p> |
| | | <p v-else>已有账号?<a href="#" class="register-link" @click.prevent="switchMode">返回登录</a></p> |
| | | <p class="copyright">© 2025 APITest Pro 智能接口自动化平台</p> |
| | | </div> |
| | | </div> |
| | |
| | | data() { |
| | | return { |
| | | isLoading: false, |
| | | isLoginMode: true, |
| | | loginForm: { |
| | | username: "", |
| | | password: "" |
| | | }, |
| | | registerForm: { |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | password: "", |
| | | confirm_password: "", |
| | | groups: [] |
| | | }, |
| | | usernameInvalid: "", |
| | | passwordInvalid: "" |
| | | passwordInvalid: "", |
| | | registerErrors: { |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | password: "", |
| | | confirm_password: "" |
| | | }, |
| | | groupList: [] |
| | | }; |
| | | }, |
| | | |
| | | methods: { |
| | | switchMode() { |
| | | this.isLoginMode = !this.isLoginMode; |
| | | this.resetForms(); |
| | | if (!this.isLoginMode) { |
| | | this.getGroupList(); |
| | | } |
| | | }, |
| | | resetForms() { |
| | | this.loginForm = { |
| | | username: "", |
| | | password: "" |
| | | }; |
| | | this.registerForm = { |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | password: "", |
| | | confirm_password: "", |
| | | groups: [] |
| | | }; |
| | | this.usernameInvalid = ""; |
| | | this.passwordInvalid = ""; |
| | | this.registerErrors = { |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | password: "", |
| | | confirm_password: "" |
| | | }; |
| | | }, |
| | | validateUserName() { |
| | | if (this.loginForm.username.replace(/(^\s*)/g, "") === "") { |
| | | this.usernameInvalid = "用户名不能为空"; |
| | |
| | | } |
| | | return true; |
| | | }, |
| | | validateRegisterField(field) { |
| | | const form = this.registerForm; |
| | | let isValid = true; |
| | | |
| | | switch (field) { |
| | | case 'username': |
| | | if (!form.username) { |
| | | this.registerErrors.username = "用户名不能为空"; |
| | | isValid = false; |
| | | } else { |
| | | this.registerErrors.username = ""; |
| | | } |
| | | break; |
| | | case 'name': |
| | | if (!form.name) { |
| | | this.registerErrors.name = "姓名不能为空"; |
| | | isValid = false; |
| | | } else { |
| | | this.registerErrors.name = ""; |
| | | } |
| | | break; |
| | | case 'phone': |
| | | if (!form.phone) { |
| | | this.registerErrors.phone = "手机号不能为空"; |
| | | isValid = false; |
| | | } else if (!/^1[3-9]\d{9}$/.test(form.phone)) { |
| | | this.registerErrors.phone = "手机号格式不正确"; |
| | | isValid = false; |
| | | } else { |
| | | this.registerErrors.phone = ""; |
| | | } |
| | | break; |
| | | case 'password': |
| | | if (!form.password) { |
| | | this.registerErrors.password = "密码不能为空"; |
| | | isValid = false; |
| | | } else if (form.password.length < 6) { |
| | | this.registerErrors.password = "密码长度不能少于6位"; |
| | | isValid = false; |
| | | } else { |
| | | this.registerErrors.password = ""; |
| | | } |
| | | break; |
| | | case 'confirm_password': |
| | | if (!form.confirm_password) { |
| | | this.registerErrors.confirm_password = "请再次输入密码"; |
| | | isValid = false; |
| | | } else if (form.password !== form.confirm_password) { |
| | | this.registerErrors.confirm_password = "两次输入的密码不一致"; |
| | | isValid = false; |
| | | } else { |
| | | this.registerErrors.confirm_password = ""; |
| | | } |
| | | break; |
| | | } |
| | | return isValid; |
| | | }, |
| | | validateRegisterForm() { |
| | | let isValid = true; |
| | | const fields = ['username', 'name', 'phone', 'password', 'confirm_password']; |
| | | fields.forEach(field => { |
| | | if (!this.validateRegisterField(field)) { |
| | | isValid = false; |
| | | } |
| | | }); |
| | | return isValid; |
| | | }, |
| | | handleLoginSuccess(resp) { |
| | | if (resp.success) { |
| | | // 显示登录信息提示 |
| | | this.showLoginNotification(resp); |
| | | |
| | | // 原有路由跳转和存储逻辑 |
| | | this.$router.push({ name: "ProjectList" }); |
| | | this.$store.commit("isLogin", resp.token); |
| | | this.$store.commit("setUser", resp.user); |
| | | this.$store.commit("setName", resp.name); |
| | | this.$store.commit("setId", resp.id); |
| | | this.$store.commit("setIsSuperuser", resp.is_superuser); |
| | | this.$store.commit("setIsStaff", resp.is_staff); |
| | | this.$store.commit("setRouterName", "ProjectList"); |
| | | this.$store.commit("setShowHosts", resp.show_hosts); |
| | | |
| | |
| | | this.setLocalValue("name", resp.name); |
| | | this.setLocalValue("id", resp.id); |
| | | this.setLocalValue("is_superuser", resp.is_superuser); |
| | | this.setLocalValue("is_staff", resp.is_staff); |
| | | this.setLocalValue("routerName", "ProjectList"); |
| | | this.setLocalValue("show_hosts", resp.show_hosts); |
| | | } else { |
| | |
| | | }); |
| | | }, |
| | | submitForm() { |
| | | if (this.isLoginMode) { |
| | | if (this.validateUserName() && this.validatePassword()) { |
| | | this.isLoading = true; |
| | | this.$api.login(this.loginForm).then(resp => { |
| | | this.handleLoginSuccess(resp); |
| | | this.isLoading = false; |
| | | }).catch(error => { |
| | | this.isLoading = false; |
| | | if (error.response && error.response.data && error.response.data.msg) { |
| | | this.$message.error({ |
| | | message: error.response.data.msg, |
| | | duration: 2000, |
| | | center: true |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | } else { |
| | | if (this.validateRegisterForm()) { |
| | | this.isLoading = true; |
| | | this.$api.register(this.registerForm).then(resp => { |
| | | this.$message.success(resp.msg); |
| | | this.isLoading = false; |
| | | this.switchMode(); |
| | | }).catch(error => { |
| | | this.isLoading = false; |
| | | if (error.response && error.response.data && error.response.data.msg) { |
| | | this.$message.error({ |
| | | message: error.response.data.msg, |
| | | duration: 2000, |
| | | center: true |
| | | }); |
| | | } else if (error.response && error.response.data) { |
| | | const errors = error.response.data; |
| | | let errorMsg = ''; |
| | | for (let key in errors) { |
| | | if (Array.isArray(errors[key])) { |
| | | errorMsg += errors[key].join('、') + ';'; |
| | | } else if (typeof errors[key] === 'string') { |
| | | errorMsg += errors[key] + ';'; |
| | | } |
| | | } |
| | | this.$message.error({ |
| | | message: errorMsg || '注册失败,请检查输入信息', |
| | | duration: 3000, |
| | | center: true |
| | | }); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | }, |
| | | getGroupList() { |
| | | this.$api.getGroupList().then(resp => { |
| | | this.groupList = resp; |
| | | }).catch(() => { |
| | | this.groupList = []; |
| | | }); |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.getGroupList(); |
| | | } |
| | | }; |
| | | </script> |
| | |
| | | >项目看板</el-button |
| | | > |
| | | <el-button |
| | | type="warning" |
| | | size="small" |
| | | icon="el-icon-user" |
| | | @click="handleUserManagement" |
| | | >用户管理</el-button |
| | | > |
| | | <el-button |
| | | type="info" |
| | | size="small" |
| | | icon="el-icon-arrow-left" |
| | |
| | | > |
| | | <ProjectDashBoard></ProjectDashBoard> |
| | | </el-drawer> |
| | | |
| | | <el-drawer |
| | | title="用户管理" |
| | | :visible.sync="userManagementVisible" |
| | | direction="rtl" |
| | | size="80%" |
| | | :before-close="handleUserManagementClose" |
| | | :modal="false" |
| | | class="user-management-drawer-wrapper" |
| | | > |
| | | <user-management-drawer v-if="userManagementVisible"></user-management-drawer> |
| | | </el-drawer> |
| | | <el-container> |
| | | <el-main style="padding: 0; margin-left: 10px"> |
| | | <el-table |
| | |
| | | |
| | | <script> |
| | | import ProjectDashBoard from "@/pages/project/ProjectDashBoard.vue"; |
| | | import UserManagementDrawer from "@/pages/user/UserManagementDrawer.vue"; |
| | | export default { |
| | | name: "ProjectList", |
| | | components: { ProjectDashBoard }, |
| | | components: { ProjectDashBoard, UserManagementDrawer }, |
| | | data() { |
| | | return { |
| | | recentEnabledProjects: [], // 存储有启动任务的项目 |
| | |
| | | task_count: 0, |
| | | dialogVisible: false, |
| | | dashBoardVisible: false, |
| | | userManagementVisible: false, |
| | | editVisible: false, |
| | | hoveringSuccess: false, |
| | | hoveringError: false, |
| | |
| | | this.dialogVisible = true; |
| | | this.resetProjectForm(); |
| | | }, |
| | | handleUserManagement() { |
| | | this.userManagementVisible = true; |
| | | }, |
| | | handleUserManagementClose() { |
| | | this.userManagementVisible = false; |
| | | }, |
| | | handleEdit(index, row) { |
| | | this.editVisible = true; |
| | | this.projectForm.name = row["name"]; |
| New file |
| | |
| | | <template> |
| | | <el-container> |
| | | <el-container> |
| | | <el-header style="padding-top: 10px; margin-left: 10px; height: 50px"> |
| | | <div class="env__header"> |
| | | <div class="env__header--item"> |
| | | <el-input |
| | | style="width: 300px" |
| | | size="small" |
| | | placeholder="请输入用户名或姓名" |
| | | v-model="search" |
| | | clearable |
| | | > |
| | | </el-input> |
| | | </div> |
| | | |
| | | <div class="env__header--item"> |
| | | <el-button |
| | | plain |
| | | size="small" |
| | | icon="el-icon-refresh" |
| | | @click="resetSearch" |
| | | >重置</el-button |
| | | > |
| | | </div> |
| | | |
| | | <div class="env__header--item" v-if="isSuperuser"> |
| | | <el-button |
| | | type="primary" |
| | | size="small" |
| | | icon="el-icon-plus" |
| | | @click="handleAdd" |
| | | >新增用户</el-button |
| | | > |
| | | </div> |
| | | |
| | | <div class="env__header--item" v-if="isSuperuser || isStaff"> |
| | | <el-button |
| | | type="success" |
| | | size="small" |
| | | icon="el-icon-s-check" |
| | | @click="handleApproval" |
| | | >注册用户审批</el-button |
| | | > |
| | | </div> |
| | | </div> |
| | | </el-header> |
| | | |
| | | <el-dialog |
| | | :title="dialogTitle" |
| | | width="50%" |
| | | :close-on-click-modal="false" |
| | | :visible.sync="dialogVisible" |
| | | @close="handleDialogClose" |
| | | > |
| | | <el-form |
| | | :inline="true" |
| | | label-position="right" |
| | | :model="userForm" |
| | | :rules="rules" |
| | | ref="userForm" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="用户名" prop="username" v-if="isCreate"> |
| | | <el-input v-model="userForm.username" placeholder="请输入用户名"></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="用户名" prop="username" v-if="!isCreate"> |
| | | <el-input v-model="userForm.username" disabled></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="姓名" prop="name"> |
| | | <el-input v-model="userForm.name" placeholder="请输入姓名"></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="手机号" prop="phone"> |
| | | <el-input v-model="userForm.phone" placeholder="请输入手机号"></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="密码" prop="password" v-if="isCreate"> |
| | | <el-input |
| | | type="password" |
| | | v-model="userForm.password" |
| | | placeholder="请输入密码" |
| | | ></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="确认密码" prop="confirm_password" v-if="isCreate"> |
| | | <el-input |
| | | type="password" |
| | | v-model="userForm.confirm_password" |
| | | placeholder="请再次输入密码" |
| | | ></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否激活" prop="is_active" v-if="isSuperuser"> |
| | | <el-switch v-model="userForm.is_active"></el-switch> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否管理员" prop="is_staff" v-if="isSuperuser"> |
| | | <el-switch v-model="userForm.is_staff"></el-switch> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="超级用户" prop="is_superuser" v-if="isSuperuser"> |
| | | <el-switch v-model="userForm.is_superuser"></el-switch> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="显示Hosts" prop="show_hosts" v-if="isSuperuser"> |
| | | <el-switch v-model="userForm.show_hosts"></el-switch> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="所属分组" prop="groups" v-if="isSuperuser"> |
| | | <el-select |
| | | v-model="userForm.groups" |
| | | multiple |
| | | placeholder="请选择分组" |
| | | style="width: 100%" |
| | | > |
| | | <el-option |
| | | v-for="group in groupList" |
| | | :key="group.id" |
| | | :label="group.name" |
| | | :value="group.id" |
| | | ></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <span slot="footer" class="dialog-footer"> |
| | | <el-button @click="dialogVisible = false">取 消</el-button> |
| | | <el-button type="primary" @click="handleSubmit">确 定</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <el-dialog |
| | | title="用户详情" |
| | | width="50%" |
| | | :close-on-click-modal="false" |
| | | :visible.sync="detailDialogVisible" |
| | | > |
| | | <el-form |
| | | :inline="true" |
| | | label-position="right" |
| | | :model="userDetail" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="用户名"> |
| | | <el-input v-model="userDetail.username" readonly></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="姓名"> |
| | | <el-input v-model="userDetail.name" readonly></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="手机号"> |
| | | <el-input v-model="userDetail.phone" readonly></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否激活"> |
| | | <el-tag :type="userDetail.is_active ? 'success' : 'danger'"> |
| | | {{ userDetail.is_active ? '是' : '否' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否管理员"> |
| | | <el-tag :type="userDetail.is_staff ? 'success' : 'info'"> |
| | | {{ userDetail.is_staff ? '是' : '否' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="超级用户"> |
| | | <el-tag :type="userDetail.is_superuser ? 'success' : 'info'"> |
| | | {{ userDetail.is_superuser ? '是' : '否' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="显示Hosts"> |
| | | <el-tag :type="userDetail.show_hosts ? 'success' : 'info'"> |
| | | {{ userDetail.show_hosts ? '是' : '否' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="所属分组"> |
| | | <el-tag |
| | | v-for="group in userDetail.groups" |
| | | :key="group" |
| | | style="margin-right: 5px" |
| | | > |
| | | {{ group }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="可访问项目"> |
| | | <el-tag |
| | | v-for="project in userDetail.accessible_projects" |
| | | :key="project" |
| | | style="margin-right: 5px" |
| | | > |
| | | {{ project }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="注册时间"> |
| | | <el-input |
| | | :value="userDetail.date_joined | datetimeFormat" |
| | | readonly |
| | | ></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="最后登录"> |
| | | <el-input |
| | | :value="userDetail.last_login ? (userDetail.last_login | datetimeFormat) : '从未登录'" |
| | | readonly |
| | | ></el-input> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <span slot="footer" class="dialog-footer"> |
| | | <el-button @click="detailDialogVisible = false">关 闭</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <el-drawer |
| | | title="注册用户审批" |
| | | :visible.sync="approvalDrawerVisible" |
| | | direction="rtl" |
| | | size="60%" |
| | | > |
| | | <div class="approval-container"> |
| | | <el-table |
| | | :data="approvalData" |
| | | v-loading="approvalLoading" |
| | | stripe |
| | | style="width: 100%" |
| | | > |
| | | <el-table-column prop="username" label="用户名" width="120"></el-table-column> |
| | | <el-table-column prop="name" label="姓名" width="120"></el-table-column> |
| | | <el-table-column prop="phone" label="手机号" width="120"></el-table-column> |
| | | <el-table-column label="所属分组" width="200"> |
| | | <template slot-scope="scope"> |
| | | <el-tag |
| | | v-for="group in scope.row.groups" |
| | | :key="group" |
| | | size="mini" |
| | | style="margin-right: 3px" |
| | | > |
| | | {{ group }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="状态" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)"> |
| | | {{ scope.row.status_display }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="注册时间" width="180"> |
| | | <template slot-scope="scope"> |
| | | {{ scope.row.date_joined | datetimeFormat }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" fixed="right" width="200"> |
| | | <template slot-scope="scope"> |
| | | <el-button |
| | | type="success" |
| | | size="mini" |
| | | icon="el-icon-check" |
| | | @click="handleApprove(scope.row, 'approve')" |
| | | > |
| | | 通过 |
| | | </el-button> |
| | | <el-button |
| | | type="danger" |
| | | size="mini" |
| | | icon="el-icon-close" |
| | | @click="handleApprove(scope.row, 'reject')" |
| | | > |
| | | 不通过 |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <div v-if="!approvalLoading && approvalData.length === 0" class="empty-tip"> |
| | | <el-empty description="暂无待审批用户"></el-empty> |
| | | </div> |
| | | </div> |
| | | </el-drawer> |
| | | |
| | | <el-container> |
| | | <el-main style="margin-left: 10px; margin-top: 10px"> |
| | | <div class="user-body-table"> |
| | | <el-table |
| | | highlight-current-row |
| | | stripe |
| | | :data="userData" |
| | | v-loading="isLoading" |
| | | height="calc(100%)" |
| | | @cell-mouse-enter="cellMouseEnter" |
| | | @cell-mouse-leave="cellMouseLeave" |
| | | > |
| | | <el-table-column label="用户名" width="120"> |
| | | <template slot-scope="scope"> |
| | | <div |
| | | :title="scope.row.username" |
| | | class="table-column" |
| | | > |
| | | {{ scope.row.username }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="姓名" width="120"> |
| | | <template slot-scope="scope"> |
| | | <div |
| | | :title="scope.row.name" |
| | | class="table-column" |
| | | > |
| | | {{ scope.row.name }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="手机号" width="120"> |
| | | <template slot-scope="scope"> |
| | | <div |
| | | :title="scope.row.phone" |
| | | class="table-column" |
| | | > |
| | | {{ scope.row.phone }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="是否激活" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="scope.row.is_active ? 'success' : 'danger'"> |
| | | {{ scope.row.is_active ? '是' : '否' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="是否管理员" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="scope.row.is_staff ? 'success' : 'info'"> |
| | | {{ scope.row.is_staff ? '是' : '否' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="超级用户" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="scope.row.is_superuser ? 'success' : 'info'"> |
| | | {{ scope.row.is_superuser ? '是' : '否' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="所属分组" width="200"> |
| | | <template slot-scope="scope"> |
| | | <div class="table-column"> |
| | | <el-tag |
| | | v-for="group in scope.row.groups" |
| | | :key="group" |
| | | size="mini" |
| | | style="margin-right: 3px" |
| | | > |
| | | {{ group }} |
| | | </el-tag> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="注册时间" width="180"> |
| | | <template slot-scope="scope"> |
| | | <div> |
| | | {{ scope.row.date_joined | datetimeFormat }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="操作" width="200" fixed="right"> |
| | | <template slot-scope="scope"> |
| | | <el-row v-show="currentRow === scope.row"> |
| | | <el-button |
| | | type="success" |
| | | icon="el-icon-view" |
| | | title="查看" |
| | | circle |
| | | size="mini" |
| | | @click="handleView(scope.row)" |
| | | ></el-button> |
| | | <el-button |
| | | type="primary" |
| | | icon="el-icon-edit" |
| | | title="编辑" |
| | | circle |
| | | size="mini" |
| | | v-if="isSuperuser" |
| | | @click="handleEdit(scope.row)" |
| | | ></el-button> |
| | | <el-button |
| | | type="danger" |
| | | icon="el-icon-delete" |
| | | title="删除" |
| | | circle |
| | | size="mini" |
| | | v-if="isSuperuser && scope.row.id !== currentUserId" |
| | | @click="handleDelete(scope.row)" |
| | | ></el-button> |
| | | </el-row> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </el-main> |
| | | </el-container> |
| | | </el-container> |
| | | </el-container> |
| | | </template> |
| | | |
| | | <script> |
| | | export default { |
| | | name: "UserManagement", |
| | | data() { |
| | | var validateConfirmPassword = (rule, value, callback) => { |
| | | if (value !== this.userForm.password) { |
| | | callback(new Error("两次输入的密码不一致")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }; |
| | | |
| | | return { |
| | | search: "", |
| | | currentRow: "", |
| | | isLoading: true, |
| | | userData: [], |
| | | groupList: [], |
| | | dialogVisible: false, |
| | | detailDialogVisible: false, |
| | | approvalDrawerVisible: false, |
| | | approvalLoading: false, |
| | | approvalData: [], |
| | | isCreate: false, |
| | | isSuperuser: false, |
| | | isStaff: false, |
| | | currentUserId: null, |
| | | userForm: { |
| | | id: null, |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | password: "", |
| | | confirm_password: "", |
| | | is_active: true, |
| | | is_staff: false, |
| | | is_superuser: false, |
| | | show_hosts: false, |
| | | groups: [] |
| | | }, |
| | | userDetail: { |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | is_active: false, |
| | | is_staff: false, |
| | | is_superuser: false, |
| | | show_hosts: false, |
| | | groups: [], |
| | | accessible_projects: [], |
| | | date_joined: "", |
| | | last_login: "" |
| | | }, |
| | | rules: { |
| | | username: [ |
| | | { required: true, message: "请输入用户名", trigger: "blur" } |
| | | ], |
| | | name: [ |
| | | { required: true, message: "请输入姓名", trigger: "blur" } |
| | | ], |
| | | password: [ |
| | | { required: true, message: "请输入密码", trigger: "blur" }, |
| | | { min: 6, message: "密码长度不能少于6位", trigger: "blur" } |
| | | ], |
| | | confirm_password: [ |
| | | { required: true, message: "请再次输入密码", trigger: "blur" }, |
| | | { validator: validateConfirmPassword, trigger: "blur" } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | computed: { |
| | | dialogTitle() { |
| | | return this.isCreate ? "新增用户" : "编辑用户"; |
| | | } |
| | | }, |
| | | methods: { |
| | | cellMouseEnter(row) { |
| | | this.currentRow = row; |
| | | }, |
| | | cellMouseLeave() { |
| | | this.currentRow = ""; |
| | | }, |
| | | handleAdd() { |
| | | this.isCreate = true; |
| | | this.dialogVisible = true; |
| | | this.resetForm(); |
| | | }, |
| | | handleEdit(row) { |
| | | this.isCreate = false; |
| | | this.dialogVisible = true; |
| | | this.userForm = { |
| | | id: row.id, |
| | | username: row.username, |
| | | name: row.name, |
| | | phone: row.phone, |
| | | is_active: row.is_active, |
| | | is_staff: row.is_staff, |
| | | is_superuser: row.is_superuser, |
| | | show_hosts: row.show_hosts, |
| | | groups: [] |
| | | }; |
| | | }, |
| | | handleView(row) { |
| | | this.getUserDetail(row.id); |
| | | }, |
| | | handleDelete(row) { |
| | | this.$confirm("确定要删除该用户吗?", "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | }).then(() => { |
| | | this.$api.deleteUser(row.id).then(() => { |
| | | this.$message.success("删除成功"); |
| | | this.getUserList(); |
| | | }); |
| | | }).catch(() => {}); |
| | | }, |
| | | handleSubmit() { |
| | | this.$refs.userForm.validate(valid => { |
| | | if (valid) { |
| | | if (this.isCreate) { |
| | | this.$api.createUser(this.userForm).then(() => { |
| | | this.$message.success("创建成功"); |
| | | this.dialogVisible = false; |
| | | this.getUserList(); |
| | | }); |
| | | } else { |
| | | this.$api.updateUser(this.userForm.id, this.userForm).then(() => { |
| | | this.$message.success("更新成功"); |
| | | this.dialogVisible = false; |
| | | this.getUserList(); |
| | | }); |
| | | } |
| | | } |
| | | }); |
| | | }, |
| | | handleDialogClose() { |
| | | this.$refs.userForm.resetFields(); |
| | | }, |
| | | resetForm() { |
| | | this.userForm = { |
| | | id: null, |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | password: "", |
| | | confirm_password: "", |
| | | is_active: true, |
| | | is_staff: false, |
| | | is_superuser: false, |
| | | show_hosts: false, |
| | | groups: [] |
| | | }; |
| | | }, |
| | | getUserList() { |
| | | this.isLoading = true; |
| | | this.$api.getUserManagementList({ search: this.search }).then(resp => { |
| | | this.userData = resp.results || resp; |
| | | this.isLoading = false; |
| | | }); |
| | | }, |
| | | getUserDetail(userId) { |
| | | this.$api.getUserDetail(userId).then(resp => { |
| | | this.userDetail = resp; |
| | | this.detailDialogVisible = true; |
| | | }); |
| | | }, |
| | | getGroupList() { |
| | | this.$api.getGroupList().then(resp => { |
| | | this.groupList = resp; |
| | | }); |
| | | }, |
| | | resetSearch() { |
| | | this.search = ""; |
| | | this.getUserList(); |
| | | }, |
| | | handleApproval() { |
| | | this.approvalDrawerVisible = true; |
| | | this.getApprovalList(); |
| | | }, |
| | | getApprovalList() { |
| | | this.approvalLoading = true; |
| | | this.$api.getUserApprovalList({ search: this.search }).then(resp => { |
| | | this.approvalData = resp.results || resp; |
| | | this.approvalLoading = false; |
| | | }).catch(() => { |
| | | this.approvalLoading = false; |
| | | }); |
| | | }, |
| | | getStatusType(status) { |
| | | const statusMap = { |
| | | 'pending': 'warning', |
| | | 'approved': 'success', |
| | | 'rejected': 'danger' |
| | | }; |
| | | return statusMap[status] || 'info'; |
| | | }, |
| | | handleApprove(row, action) { |
| | | const actionText = action === 'approve' ? '通过' : '不通过'; |
| | | this.$confirm(`确定要${actionText}该用户的注册申请吗?`, "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | }).then(() => { |
| | | this.$api.approveUser(row.id, { action: action }).then(() => { |
| | | this.$message.success(`用户审批已${actionText}`); |
| | | this.getApprovalList(); |
| | | }); |
| | | }).catch(() => {}); |
| | | } |
| | | }, |
| | | watch: { |
| | | search() { |
| | | this.getUserList(); |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.isSuperuser = this.$store.state.is_superuser; |
| | | this.isStaff = this.$store.state.is_staff; |
| | | this.currentUserId = this.$store.state.id; |
| | | this.getUserList(); |
| | | if (this.isSuperuser) { |
| | | this.getGroupList(); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .env__header { |
| | | display: flex; |
| | | align-items: center; |
| | | margin-left: -30px; |
| | | } |
| | | |
| | | .env__header--item { |
| | | display: flex; |
| | | margin-left: 10px; |
| | | } |
| | | |
| | | .table-column { |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | |
| | | .user-body-table { |
| | | position: fixed; |
| | | bottom: 0; |
| | | right: 0; |
| | | left: 220px; |
| | | top: 100px; |
| | | margin-left: -10px; |
| | | padding-bottom: 60px; |
| | | } |
| | | |
| | | .approval-container { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .empty-tip { |
| | | text-align: center; |
| | | padding: 40px 0; |
| | | } |
| | | </style> |
| New file |
| | |
| | | <template> |
| | | <div class="user-management-drawer"> |
| | | <div class="toolbar"> |
| | | <el-input |
| | | style="width: 300px" |
| | | size="small" |
| | | placeholder="请输入用户名或姓名" |
| | | v-model="search" |
| | | clearable |
| | | > |
| | | </el-input> |
| | | |
| | | <el-button |
| | | plain |
| | | size="small" |
| | | icon="el-icon-refresh" |
| | | @click="resetSearch" |
| | | >重置</el-button |
| | | > |
| | | |
| | | <el-button |
| | | v-if="isSuperuser" |
| | | type="primary" |
| | | size="small" |
| | | icon="el-icon-plus" |
| | | @click="handleAdd" |
| | | >新增用户</el-button |
| | | > |
| | | </div> |
| | | |
| | | <el-dialog |
| | | :title="dialogTitle" |
| | | width="50%" |
| | | :close-on-click-modal="false" |
| | | :modal="false" |
| | | :visible.sync="dialogVisible" |
| | | @close="handleDialogClose" |
| | | append-to-body |
| | | > |
| | | <el-form |
| | | :inline="true" |
| | | label-position="right" |
| | | :model="userForm" |
| | | :rules="rules" |
| | | ref="userForm" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="用户名" prop="username" v-if="isCreate"> |
| | | <el-input v-model="userForm.username" placeholder="请输入用户名"></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="用户名" prop="username" v-if="!isCreate"> |
| | | <el-input v-model="userForm.username" disabled></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="姓名" prop="name"> |
| | | <el-input v-model="userForm.name" placeholder="请输入姓名"></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="手机号" prop="phone"> |
| | | <el-input v-model="userForm.phone" placeholder="请输入手机号"></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="密码" prop="password" v-if="isCreate"> |
| | | <el-input |
| | | type="password" |
| | | v-model="userForm.password" |
| | | placeholder="请输入密码" |
| | | ></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="确认密码" prop="confirm_password" v-if="isCreate"> |
| | | <el-input |
| | | type="password" |
| | | v-model="userForm.confirm_password" |
| | | placeholder="请再次输入密码" |
| | | ></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否激活" prop="is_active"> |
| | | <el-switch v-model="userForm.is_active" :disabled="!isSuperuser"></el-switch> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否管理员" prop="is_staff"> |
| | | <el-switch v-model="userForm.is_staff" :disabled="!isSuperuser"></el-switch> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="超级用户" prop="is_superuser"> |
| | | <el-switch v-model="userForm.is_superuser" :disabled="!isSuperuser"></el-switch> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="显示Hosts" prop="show_hosts"> |
| | | <el-switch v-model="userForm.show_hosts" :disabled="!isSuperuser"></el-switch> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="所属分组" prop="groups"> |
| | | <el-select |
| | | v-model="userForm.groups" |
| | | multiple |
| | | placeholder="请选择分组" |
| | | style="width: 100%" |
| | | :disabled="!isSuperuser" |
| | | > |
| | | <el-option |
| | | v-for="group in groupList" |
| | | :key="group.id" |
| | | :label="group.name" |
| | | :value="group.id" |
| | | ></el-option> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <span slot="footer" class="dialog-footer"> |
| | | <el-button @click="dialogVisible = false">取 消</el-button> |
| | | <el-button type="primary" @click="handleSubmit">确 定</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <el-dialog |
| | | title="用户详情" |
| | | width="50%" |
| | | :close-on-click-modal="false" |
| | | :modal="false" |
| | | :visible.sync="detailDialogVisible" |
| | | append-to-body |
| | | > |
| | | <el-form |
| | | :inline="true" |
| | | label-position="right" |
| | | :model="userDetail" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="用户名"> |
| | | <el-input v-model="userDetail.username" readonly></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="姓名"> |
| | | <el-input v-model="userDetail.name" readonly></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="手机号"> |
| | | <el-input v-model="userDetail.phone" readonly></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否激活"> |
| | | <el-tag :type="userDetail.is_active ? 'success' : 'danger'"> |
| | | {{ userDetail.is_active ? '是' : '否' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="是否管理员"> |
| | | <el-tag :type="userDetail.is_staff ? 'success' : 'info'"> |
| | | {{ userDetail.is_staff ? '是' : '否' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="超级用户"> |
| | | <el-tag :type="userDetail.is_superuser ? 'success' : 'info'"> |
| | | {{ userDetail.is_superuser ? '是' : '否' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="显示Hosts"> |
| | | <el-tag :type="userDetail.show_hosts ? 'success' : 'info'"> |
| | | {{ userDetail.show_hosts ? '是' : '否' }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="所属分组"> |
| | | <el-tag |
| | | v-for="group in userDetail.groups" |
| | | :key="group" |
| | | style="margin-right: 5px" |
| | | > |
| | | {{ group }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="可访问项目"> |
| | | <el-tag |
| | | v-for="project in userDetail.accessible_projects" |
| | | :key="project" |
| | | style="margin-right: 5px" |
| | | > |
| | | {{ project }} |
| | | </el-tag> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="注册时间"> |
| | | <el-input |
| | | :value="userDetail.date_joined | datetimeFormat" |
| | | readonly |
| | | ></el-input> |
| | | </el-form-item> |
| | | |
| | | <el-form-item label="最后登录"> |
| | | <el-input |
| | | :value="formatLastLogin(userDetail.last_login)" |
| | | readonly |
| | | ></el-input> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <span slot="footer" class="dialog-footer"> |
| | | <el-button @click="detailDialogVisible = false">关 闭</el-button> |
| | | </span> |
| | | </el-dialog> |
| | | |
| | | <div class="table-container"> |
| | | <el-table |
| | | highlight-current-row |
| | | stripe |
| | | :data="userData" |
| | | v-loading="isLoading" |
| | | height="calc(100vh - 150px)" |
| | | @cell-mouse-enter="cellMouseEnter" |
| | | @cell-mouse-leave="cellMouseLeave" |
| | | > |
| | | <el-table-column label="用户名" width="120"> |
| | | <template slot-scope="scope"> |
| | | <div |
| | | :title="scope.row.username" |
| | | class="table-column" |
| | | > |
| | | {{ scope.row.username }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="姓名" width="120"> |
| | | <template slot-scope="scope"> |
| | | <div |
| | | :title="scope.row.name" |
| | | class="table-column" |
| | | > |
| | | {{ scope.row.name }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="手机号" width="120"> |
| | | <template slot-scope="scope"> |
| | | <div |
| | | :title="scope.row.phone" |
| | | class="table-column" |
| | | > |
| | | {{ scope.row.phone }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="是否激活" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="scope.row.is_active ? 'success' : 'danger'"> |
| | | {{ scope.row.is_active ? '是' : '否' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="是否管理员" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="scope.row.is_staff ? 'success' : 'info'"> |
| | | {{ scope.row.is_staff ? '是' : '否' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="超级用户" width="100"> |
| | | <template slot-scope="scope"> |
| | | <el-tag :type="scope.row.is_superuser ? 'success' : 'info'"> |
| | | {{ scope.row.is_superuser ? '是' : '否' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="所属分组" width="200"> |
| | | <template slot-scope="scope"> |
| | | <div class="table-column"> |
| | | <el-tag |
| | | v-for="group in scope.row.groups" |
| | | :key="group" |
| | | size="mini" |
| | | style="margin-right: 3px" |
| | | > |
| | | {{ group }} |
| | | </el-tag> |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="注册时间" width="180"> |
| | | <template slot-scope="scope"> |
| | | <div> |
| | | {{ scope.row.date_joined | datetimeFormat }} |
| | | </div> |
| | | </template> |
| | | </el-table-column> |
| | | |
| | | <el-table-column label="操作" width="200" fixed="right"> |
| | | <template slot-scope="scope"> |
| | | <el-row v-show="currentRow === scope.row"> |
| | | <el-button |
| | | type="success" |
| | | icon="el-icon-view" |
| | | title="查看" |
| | | circle |
| | | size="mini" |
| | | @click="handleView(scope.row)" |
| | | ></el-button> |
| | | <el-button |
| | | type="primary" |
| | | icon="el-icon-edit" |
| | | title="编辑" |
| | | circle |
| | | size="mini" |
| | | v-if="isSuperuser" |
| | | @click="handleEdit(scope.row)" |
| | | ></el-button> |
| | | <el-button |
| | | type="danger" |
| | | icon="el-icon-delete" |
| | | title="删除" |
| | | circle |
| | | size="mini" |
| | | v-if="isSuperuser && scope.row.id !== currentUserId" |
| | | @click="handleDelete(scope.row)" |
| | | ></el-button> |
| | | </el-row> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script> |
| | | export default { |
| | | name: "UserManagementDrawer", |
| | | data() { |
| | | var validateConfirmPassword = (rule, value, callback) => { |
| | | if (value !== this.userForm.password) { |
| | | callback(new Error("两次输入的密码不一致")); |
| | | } else { |
| | | callback(); |
| | | } |
| | | }; |
| | | |
| | | return { |
| | | search: "", |
| | | currentRow: "", |
| | | isLoading: true, |
| | | userData: [], |
| | | groupList: [], |
| | | dialogVisible: false, |
| | | detailDialogVisible: false, |
| | | isCreate: false, |
| | | isSuperuser: false, |
| | | currentUserId: null, |
| | | userForm: { |
| | | id: null, |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | password: "", |
| | | confirm_password: "", |
| | | is_active: true, |
| | | is_staff: false, |
| | | is_superuser: false, |
| | | show_hosts: false, |
| | | groups: [] |
| | | }, |
| | | userDetail: { |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | is_active: false, |
| | | is_staff: false, |
| | | is_superuser: false, |
| | | show_hosts: false, |
| | | groups: [], |
| | | accessible_projects: [], |
| | | date_joined: "", |
| | | last_login: "" |
| | | }, |
| | | rules: { |
| | | username: [ |
| | | { required: true, message: "请输入用户名", trigger: "blur" } |
| | | ], |
| | | name: [ |
| | | { required: true, message: "请输入姓名", trigger: "blur" } |
| | | ], |
| | | password: [ |
| | | { required: true, message: "请输入密码", trigger: "blur" }, |
| | | { min: 6, message: "密码长度不能少于6位", trigger: "blur" } |
| | | ], |
| | | confirm_password: [ |
| | | { required: true, message: "请再次输入密码", trigger: "blur" }, |
| | | { validator: validateConfirmPassword, trigger: "blur" } |
| | | ] |
| | | } |
| | | }; |
| | | }, |
| | | computed: { |
| | | dialogTitle() { |
| | | return this.isCreate ? "新增用户" : "编辑用户"; |
| | | } |
| | | }, |
| | | methods: { |
| | | cellMouseEnter(row) { |
| | | this.currentRow = row; |
| | | }, |
| | | cellMouseLeave() { |
| | | this.currentRow = ""; |
| | | }, |
| | | handleAdd() { |
| | | this.isCreate = true; |
| | | this.dialogVisible = true; |
| | | this.resetForm(); |
| | | }, |
| | | handleEdit(row) { |
| | | this.isCreate = false; |
| | | this.dialogVisible = true; |
| | | const groupIds = this.getGroupIdsByName(row.groups || []); |
| | | this.userForm = { |
| | | id: row.id, |
| | | username: row.username, |
| | | name: row.name, |
| | | phone: row.phone, |
| | | is_active: row.is_active, |
| | | is_staff: row.is_staff, |
| | | is_superuser: row.is_superuser, |
| | | show_hosts: row.show_hosts, |
| | | groups: groupIds |
| | | }; |
| | | }, |
| | | handleView(row) { |
| | | this.getUserDetail(row.id); |
| | | }, |
| | | handleDelete(row) { |
| | | this.$confirm("确定要删除该用户吗?", "提示", { |
| | | confirmButtonText: "确定", |
| | | cancelButtonText: "取消", |
| | | type: "warning" |
| | | }).then(() => { |
| | | this.$api.deleteUser(row.id).then(() => { |
| | | this.$message.success("删除成功"); |
| | | this.getUserList(); |
| | | }); |
| | | }).catch(() => {}); |
| | | }, |
| | | handleSubmit() { |
| | | this.$refs.userForm.validate(valid => { |
| | | if (valid) { |
| | | console.log("提交表单数据:", this.userForm); |
| | | if (this.isCreate) { |
| | | this.$api.createUser(this.userForm).then(() => { |
| | | this.$message.success("创建成功"); |
| | | this.dialogVisible = false; |
| | | this.getUserList(); |
| | | }).catch(err => { |
| | | console.error("创建用户失败:", err); |
| | | console.error("错误详情:", err.response ? err.response.data : err.message); |
| | | this.$message.error("创建用户失败,请检查输入信息"); |
| | | }); |
| | | } else { |
| | | this.$api.updateUser(this.userForm.id, this.userForm).then(() => { |
| | | this.$message.success("更新成功"); |
| | | this.dialogVisible = false; |
| | | this.getUserList(); |
| | | }).catch(err => { |
| | | console.error("更新用户失败:", err); |
| | | console.error("错误详情:", err.response ? err.response.data : err.message); |
| | | this.$message.error("更新用户失败,请检查输入信息"); |
| | | }); |
| | | } |
| | | } else { |
| | | this.$message.warning("请检查表单输入"); |
| | | } |
| | | }); |
| | | }, |
| | | handleDialogClose() { |
| | | this.$refs.userForm.resetFields(); |
| | | }, |
| | | resetForm() { |
| | | this.userForm = { |
| | | id: null, |
| | | username: "", |
| | | name: "", |
| | | phone: "", |
| | | password: "", |
| | | confirm_password: "", |
| | | is_active: true, |
| | | is_staff: false, |
| | | is_superuser: false, |
| | | show_hosts: false, |
| | | groups: [] |
| | | }; |
| | | }, |
| | | getUserList() { |
| | | this.isLoading = true; |
| | | console.log("搜索关键词:", this.search); |
| | | this.$api.getUserManagementList({ search: this.search }).then(resp => { |
| | | console.log("搜索结果:", resp); |
| | | this.userData = resp.results || resp; |
| | | this.isLoading = false; |
| | | }).catch(err => { |
| | | console.error("获取用户列表失败:", err); |
| | | this.isLoading = false; |
| | | }); |
| | | }, |
| | | getUserDetail(userId) { |
| | | this.$api.getUserDetail(userId).then(resp => { |
| | | this.userDetail = resp; |
| | | this.detailDialogVisible = true; |
| | | }); |
| | | }, |
| | | getGroupList() { |
| | | this.$api.getGroupList().then(resp => { |
| | | this.groupList = resp; |
| | | }); |
| | | }, |
| | | resetSearch() { |
| | | this.search = ""; |
| | | this.getUserList(); |
| | | }, |
| | | formatLastLogin(lastLogin) { |
| | | if (!lastLogin) { |
| | | return "从未登录"; |
| | | } |
| | | return lastLogin; |
| | | }, |
| | | getGroupIdsByName(groupNames) { |
| | | if (!groupNames || groupNames.length === 0) { |
| | | return []; |
| | | } |
| | | const groupMap = {}; |
| | | this.groupList.forEach(group => { |
| | | groupMap[group.name] = group.id; |
| | | }); |
| | | return groupNames.map(name => groupMap[name]).filter(id => id !== undefined); |
| | | } |
| | | }, |
| | | watch: { |
| | | search() { |
| | | this.getUserList(); |
| | | } |
| | | }, |
| | | mounted() { |
| | | this.isSuperuser = this.$store.state.is_superuser; |
| | | this.currentUserId = this.$store.state.id; |
| | | this.getUserList(); |
| | | if (this.isSuperuser) { |
| | | this.getGroupList(); |
| | | } |
| | | } |
| | | }; |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .user-management-drawer { |
| | | width: 100%; |
| | | height: 100%; |
| | | padding: 0; |
| | | } |
| | | |
| | | .toolbar { |
| | | display: flex; |
| | | align-items: center; |
| | | padding: 10px 20px; |
| | | gap: 10px; |
| | | background: #f5f5f5; |
| | | border-bottom: 1px solid #e0e0e0; |
| | | } |
| | | |
| | | .toolbar .el-input { |
| | | margin-right: 10px; |
| | | } |
| | | |
| | | .table-container { |
| | | padding: 20px; |
| | | height: calc(100vh - 150px); |
| | | overflow: auto; |
| | | } |
| | | |
| | | .table-column { |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | white-space: nowrap; |
| | | } |
| | | </style> |
| | |
| | | |
| | | axios.interceptors.request.use( |
| | | function(config) { |
| | | if ( |
| | | !config.url.startsWith("/api/user") || |
| | | config.url === "/api/user/list" || |
| | | config.url === "/api/user/login_log" |
| | | ) { |
| | | // 在请求拦截中,每次请求,都会加上一个Authorization头 |
| | | config.headers.Authorization = store.token; |
| | | const noAuthUrls = [ |
| | | '/api/user/login', |
| | | '/api/user/register', |
| | | '/api/user/groups' |
| | | ]; |
| | | |
| | | // 取url地址的第四位作为projectId, 如果不存在,默认设置为0 |
| | | if (!noAuthUrls.includes(config.url)) { |
| | | config.headers.Authorization = store.token; |
| | | let projectId = window.location.pathname.split("/")[3]; |
| | | projectId = projectId ? projectId : 0; |
| | | config.headers["Project"] = projectId; |
| | |
| | | duration: 2000 |
| | | }); |
| | | } |
| | | if (status === 403) { |
| | | Message.error({ |
| | | message: error.response.data.detail, |
| | | duration: 2000 |
| | | }); |
| | | } |
| | | } catch (e) { |
| | | Message.error({ |
| | | message: "服务器连接超时, 请重试", |
| | |
| | | |
| | | export const login = params => { |
| | | return axios.post("/api/user/login", params).then(res => res.data); |
| | | }; |
| | | |
| | | export const register = params => { |
| | | return axios.post("/api/user/register", params).then(res => res.data); |
| | | }; |
| | | |
| | | export const addProject = params => { |
| | |
| | | |
| | | export const getGroupList = () => { |
| | | return axios |
| | | .get("/api/lunarlink/project/groups") |
| | | .get("/api/user/groups") |
| | | .then(res => res.data); |
| | | }; |
| | | |
| | |
| | | }).then(res => res.data); |
| | | }; |
| | | |
| | | export const getUserManagementList = (params) => { |
| | | return axios.get("/api/user/management/", { params }).then(res => res.data); |
| | | }; |
| | | |
| | | export const createUser = (data) => { |
| | | return axios.post("/api/user/management/", data).then(res => res.data); |
| | | }; |
| | | |
| | | export const updateUser = (userId, data) => { |
| | | return axios.patch(`/api/user/management/${userId}/`, data).then(res => res.data); |
| | | }; |
| | | |
| | | export const deleteUser = (userId) => { |
| | | return axios.delete(`/api/user/management/${userId}/`).then(res => res.data); |
| | | }; |
| | | |
| | | export const getUserDetail = (userId) => { |
| | | return axios.get(`/api/user/management/${userId}/`).then(res => res.data); |
| | | }; |
| | | |
| | | export const getUserApprovalList = (params) => { |
| | | return axios.get("/api/user/approval/", { params }).then(res => res.data); |
| | | }; |
| | | |
| | | export const approveUser = (userId, data) => { |
| | | return axios.post(`/api/user/approval/${userId}/approve/`, data).then(res => res.data); |
| | | }; |
| | | |
| | |
| | | state.is_superuser = value; |
| | | }, |
| | | |
| | | setIsStaff(state, value) { |
| | | state.is_staff = value; |
| | | }, |
| | | |
| | | setShowHosts(state, value) { |
| | | state.show_hosts = value; |
| | | } |
| | |
| | | token: null, |
| | | user: null, |
| | | is_superuser: false, |
| | | is_staff: false, |
| | | show_hosts: false, |
| | | duration: 2000, |
| | | report_path: "/api/lunarlink/reports/", |
| New file |
| | |
| | | # 用户注册审批功能测试指南 |
| | | |
| | | ## 功能概述 |
| | | |
| | | 本系统实现了完整的用户注册审批流程,包括: |
| | | |
| | | 1. 用户自主注册功能 |
| | | 2. 管理员审批功能 |
| | | 3. 登录状态检查 |
| | | 4. 多管理员支持(一人审批即可) |
| | | |
| | | ## 数据库迁移 |
| | | |
| | | 由于项目依赖问题,请手动执行以下SQL语句添加status字段: |
| | | |
| | | ```sql |
| | | ALTER TABLE lunaruser_myuser |
| | | ADD COLUMN status VARCHAR(20) DEFAULT 'approved' |
| | | COMMENT '用户状态'; |
| | | ``` |
| | | |
| | | 或者运行以下Python脚本: |
| | | |
| | | ```bash |
| | | cd backend |
| | | python add_status_field.py |
| | | ``` |
| | | |
| | | ## 测试步骤 |
| | | |
| | | ### 1. 用户注册测试 |
| | | |
| | | 1. 访问登录页面 |
| | | 2. 点击"立即注册"链接 |
| | | 3. 填写注册表单: |
| | | - 用户名(必填) |
| | | - 姓名(必填) |
| | | - 手机号(必填) |
| | | - 密码(必填,至少6位) |
| | | - 确认密码(必填) |
| | | - 所属分组(可选) |
| | | 4. 点击"注册账号" |
| | | 5. 预期结果:显示"你的注册申请已提交,待管理员审核中!" |
| | | |
| | | ### 2. 管理员审批测试 |
| | | |
| | | 1. 使用超级管理员账号登录 |
| | | 2. 进入用户管理页面 |
| | | 3. 点击"注册用户审批"按钮 |
| | | 4. 查看待审批用户列表 |
| | | 5. 对某个用户点击"通过"或"不通过" |
| | | 6. 预期结果: |
| | | - 通过后,用户状态变为"已通过",is_active变为true |
| | | - 不通过后,用户状态变为"未通过" |
| | | |
| | | ### 3. 登录状态检查测试 |
| | | |
| | | #### 待审核状态登录 |
| | | 1. 使用待审核的用户名和密码登录 |
| | | 2. 预期结果:显示"你提交的注册还在审批中,暂时无法登录,可联系管理员确认!" |
| | | |
| | | #### 未通过状态登录 |
| | | 1. 使用未通过的用户名和密码登录 |
| | | 2. 预期结果:显示"你的注册申请未通过审核,无法登录!" |
| | | |
| | | #### 已通过状态登录 |
| | | 1. 使用已通过的用户名和密码登录 |
| | | 2. 预期结果:正常登录 |
| | | |
| | | ## API接口说明 |
| | | |
| | | ### 注册接口 |
| | | - URL: `POST /api/user/register` |
| | | - 参数: |
| | | ```json |
| | | { |
| | | "username": "testuser", |
| | | "name": "测试用户", |
| | | "phone": "13800138000", |
| | | "password": "123456", |
| | | "confirm_password": "123456", |
| | | "groups": [] |
| | | } |
| | | ``` |
| | | - 响应: |
| | | ```json |
| | | { |
| | | "success": true, |
| | | "msg": "你的注册申请已提交,待管理员审核中!", |
| | | "data": { |
| | | "id": 1, |
| | | "username": "testuser" |
| | | } |
| | | } |
| | | ``` |
| | | |
| | | ### 审批列表接口 |
| | | - URL: `GET /api/user/approval/` |
| | | - 响应: |
| | | ```json |
| | | { |
| | | "success": true, |
| | | "results": [...] |
| | | } |
| | | ``` |
| | | |
| | | ### 审批操作接口 |
| | | - URL: `POST /api/user/approval/{user_id}/approve/` |
| | | - 参数: |
| | | ```json |
| | | { |
| | | "action": "approve" |
| | | } |
| | | ``` |
| | | - 响应: |
| | | ```json |
| | | { |
| | | "success": true, |
| | | "msg": "用户审批已通过" |
| | | } |
| | | ``` |
| | | |
| | | ## 文件修改清单 |
| | | |
| | | ### 后端文件 |
| | | 1. `backend/apps/lunaruser/models.py` - 添加status字段 |
| | | 2. `backend/apps/lunaruser/serializers.py` - 添加注册和审批序列化器 |
| | | 3. `backend/apps/lunaruser/views.py` - 添加注册和审批视图 |
| | | 4. `backend/apps/lunaruser/urls.py` - 添加注册和审批路由 |
| | | 5. `backend/apps/lunaruser/migrations/0003_myuser_status.py` - 数据库迁移文件 |
| | | |
| | | ### 前端文件 |
| | | 1. `frontend/src/pages/login/Login.vue` - 添加注册表单 |
| | | 2. `frontend/src/pages/user/UserManagement.vue` - 添加审批抽屉 |
| | | 3. `frontend/src/restful/api.js` - 添加API接口 |
| | | 4. `frontend/src/api/user.js` - 添加API接口 |
| | | |
| | | ## 注意事项 |
| | | |
| | | 1. 所有现有用户的status默认为'approved',可以正常登录 |
| | | 2. 新注册用户的status默认为'pending',需要管理员审批 |
| | | 3. 审批通过后,status变为'approved',is_active变为true |
| | | 4. 审批不通过后,status变为'rejected',is_active保持false |
| | | 5. 多个超级管理员时,任何一人审批即可生效 |