From f6f5a8af7e6c5359e13d7c75172adb0bc6cac555 Mon Sep 17 00:00:00 2001
From: hyb <kk_huangyangbo@163.com>
Date: Fri, 23 Jan 2026 01:53:11 +0000
Subject: [PATCH] feat: 添加用户登录功能、添加注册功能、注册审批功能,参考:注册审批功能测试指南.md、驱动代码模块增加插入其他项目代码功能,驱动代码模块增加插入其他项目代码功能,优化测试报告模块前端布局和美化 - 实现超级用户管理系统用户,可进行增删改查,其他用户只可查看本人的数据,不可进行增删改 - 将Django后台管理中的用户配置相关功能迁移到系统中进行适配、可配置禁用、是否管理员、是否超级用户 - 增加用户管理的分组字段、可直接进行分组操作,实现用户管理即可通过分组配置实现各项目的访问权限 - 实现登录页面注册 - 实现管理员首页点击注册用户审批可实现通过不通过 - 实现未审核用户的登录提示与审核未通过的用户登录提示 - 实现一键插入其他本人可访问项目的脚本代码
---
测试组/Test_platform/Interface_automation/backend/add_status_field.py | 275 +++
测试组/Test_platform/Interface_automation/backend/scripts/collect_static.py | 58
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py | 170 ++
测试组/Test_platform/Interface_automation/frontend/src/store/state.js | 1
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0003_myuser_status.py | 22
测试组/Test_platform/Interface_automation/backend/scripts/README.md | 231 +++
测试组/Test_platform/Interface_automation/backend/scripts/__init__.py | 0
测试组/Test_platform/Interface_automation/backend/scripts/deploy_init.py | 258 +++
测试组/Test_platform/Interface_automation/frontend/src/pages/user/UserManagementDrawer.vue | 591 ++++++++
测试组/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue | 303 ++++
测试组/Test_platform/Interface_automation/frontend/src/store/mutations.js | 4
测试组/Test_platform/Interface_automation/backend/scripts/create_groups.py | 110 +
测试组/Test_platform/Interface_automation/backend/check_users.py | 223 +++
测试组/Test_platform/Interface_automation/backend/scripts/run_migrations.py | 43
测试组/Test_platform/Interface_automation/backend/backend/utils/permissions.py | 3
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py | 20
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/models.py | 13
测试组/Test_platform/Interface_automation/backend/backend/settings.py | 2
测试组/Test_platform/Interface_automation/注册审批功能测试指南.md | 144 ++
测试组/Test_platform/Interface_automation/backend/scripts/create_admin_user.py | 158 ++
测试组/Test_platform/Interface_automation/backend/DEPLOY.md | 186 ++
测试组/Test_platform/Interface_automation/frontend/src/pages/user/UserManagement.vue | 687 +++++++++
测试组/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue | 307 ++++
测试组/Test_platform/Interface_automation/backend/apps/lunaruser/views.py | 236 +++
测试组/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue | 29
测试组/Test_platform/Interface_automation/frontend/src/api/user.js | 37
测试组/Test_platform/Interface_automation/frontend/src/restful/api.js | 55
27 files changed, 4,103 insertions(+), 63 deletions(-)
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/DEPLOY.md" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/DEPLOY.md"
new file mode 100644
index 0000000..8d3baab
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/DEPLOY.md"
@@ -0,0 +1,186 @@
+# 首次部署说明
+
+## 快速开始(推荐方式)
+
+使用更新后的脚本一键完成所有初始化:
+
+```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. 用户已存在
+
+脚本会自动检测用户是否已存在,如果存在则跳过创建。
+
+## 技术支持
+
+如有问题,请查看项目文档或联系技术支持团队。
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/add_status_field.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/add_status_field.py"
new file mode 100644
index 0000000..96fb74d
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/add_status_field.py"
@@ -0,0 +1,275 @@
+#!/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())
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0003_myuser_status.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0003_myuser_status.py"
new file mode 100644
index 0000000..386876a
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/migrations/0003_myuser_status.py"
@@ -0,0 +1,22 @@
+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='状态'
+ ),
+ ),
+ ]
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/models.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/models.py"
index 0138d52..4a7e376 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/models.py"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/models.py"
@@ -2,7 +2,6 @@
from django.contrib.auth.models import AbstractUser
-# Create your models here.
class MyUser(AbstractUser):
"""
使用AbstractUser可以对User进行扩展使用,添加用户自定义的属性
@@ -27,6 +26,18 @@
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
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py"
index 0f05856..5bcac19 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/serializers.py"
@@ -10,6 +10,7 @@
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
@@ -67,3 +68,172 @@
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)
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py"
index 6016ba7..8ab6853 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/urls.py"
@@ -1,22 +1,20 @@
-# -*- 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)),
]
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/views.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/views.py"
index 4d6fa9b..c75e23e 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/views.py"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/apps/lunaruser/views.py"
@@ -4,11 +4,15 @@
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
@@ -43,16 +47,33 @@
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
@@ -65,6 +86,7 @@
"user": user.username,
"name": user.name,
"is_superuser": user.is_superuser,
+ "is_staff": user.is_staff,
"show_hosts": user.show_hosts,
"token": token,
}
@@ -144,3 +166,205 @@
"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)
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/backend/settings.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/backend/settings.py"
index b24a522..9cf6887 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/backend/settings.py"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/backend/settings.py"
@@ -158,6 +158,8 @@
"DEFAULT_PERMISSION_CLASSES": (
"rest_framework.permissions.IsAuthenticated", # 有经过身份认证确定用户身份才能访问
),
+ "DATETIME_FORMAT": "%Y-%m-%d %H:%M:%S", # 日期时间格式
+ "DATE_FORMAT": "%Y-%m-%d", # 日期格式
}
# ================================================= #
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/backend/utils/permissions.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/backend/utils/permissions.py"
index dcae617..cd624ec 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/backend/utils/permissions.py"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/backend/utils/permissions.py"
@@ -62,3 +62,6 @@
class CustomIsAdminUser(IsAdminUser):
message = "您没有执行此操作的权限"
+
+ def has_permission(self, request, view):
+ return request.user.is_superuser
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/check_users.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/check_users.py"
new file mode 100644
index 0000000..fa1c1b5
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/check_users.py"
@@ -0,0 +1,223 @@
+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()
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/README.md" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/README.md"
new file mode 100644
index 0000000..94c8b3e
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/README.md"
@@ -0,0 +1,231 @@
+# 首次部署脚本使用说明
+
+本目录包含接口自动化平台首次部署时所需的初始化脚本。
+
+## 脚本列表
+
+### 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. **定期备份:** 定期备份数据库和重要文件
+
+---
+
+## 技术支持
+
+如有问题,请查看项目文档或联系技术支持团队。
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/__init__.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/__init__.py"
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/__init__.py"
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/collect_static.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/collect_static.py"
new file mode 100644
index 0000000..8dec938
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/collect_static.py"
@@ -0,0 +1,58 @@
+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)
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/create_admin_user.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/create_admin_user.py"
new file mode 100644
index 0000000..f7072f1
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/create_admin_user.py"
@@ -0,0 +1,158 @@
+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)
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/create_groups.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/create_groups.py"
new file mode 100644
index 0000000..3735186
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/create_groups.py"
@@ -0,0 +1,110 @@
+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)
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/deploy_init.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/deploy_init.py"
new file mode 100644
index 0000000..90916fa
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/deploy_init.py"
@@ -0,0 +1,258 @@
+#!/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())
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/run_migrations.py" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/run_migrations.py"
new file mode 100644
index 0000000..8c24452
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/backend/scripts/run_migrations.py"
@@ -0,0 +1,43 @@
+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)
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/api/user.js" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/api/user.js"
index 0042831..41b4d96 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/api/user.js"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/api/user.js"
@@ -10,11 +10,46 @@
Authorization: store.token
}
}).then(res => {
- // 密码修改成功后清除token和用户信息
store.token = null
store.user = null
store.name = null
store.id = 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
+ })
}
\ No newline at end of file
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue"
index 70ff7c9..b0e2953 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/home/components/Header.vue"
@@ -11,6 +11,13 @@
}}</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">
@@ -18,7 +25,6 @@
{{ $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>修改密码
@@ -71,11 +77,81 @@
<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() {
@@ -89,6 +165,10 @@
return {
passwordDialogVisible: false,
+ approvalDrawerVisible: false,
+ approvalLoading: false,
+ approvalData: [],
+ pendingCount: 0,
passwordForm: {
old_password: '',
new_password: '',
@@ -110,6 +190,25 @@
}
},
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') {
// 重置表单数据和验证状态
@@ -128,10 +227,42 @@
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 => {
@@ -166,6 +297,14 @@
})
}
})
+ }
+ },
+ mounted() {
+ if (this.$store.state.is_superuser || this.$store.state.is_staff) {
+ this.getApprovalList();
+ setTimeout(() => {
+ this.addBadgeAnimation();
+ }, 500);
}
}
}
@@ -219,4 +358,164 @@
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>
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue"
index 45565c9..49e1f77 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/login/Login.vue"
@@ -1,6 +1,5 @@
<template>
<div class="login-container">
- <!-- 动态背景 -->
<div class="background-animation">
<div class="floating-shapes">
<div class="shape shape-1"></div>
@@ -12,28 +11,20 @@
<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>
@@ -68,12 +59,11 @@
</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
@@ -81,7 +71,104 @@
@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"
@@ -96,7 +183,7 @@
</transition>
</div>
- <div class="input-group">
+ <div class="input-group" v-if="isLoginMode">
<label class="input-label">密码</label>
<el-input
v-model="loginForm.password"
@@ -119,12 +206,13 @@
: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>
@@ -140,16 +228,63 @@
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 = "用户名不能为空";
@@ -164,18 +299,83 @@
}
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);
@@ -184,6 +384,7 @@
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 {
@@ -207,14 +408,68 @@
});
},
submitForm() {
- if (this.validateUserName() && this.validatePassword()) {
- this.isLoading = true;
- this.$api.login(this.loginForm).then(resp => {
- this.handleLoginSuccess(resp);
- this.isLoading = false;
- });
+ 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>
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue"
index 62d34ea..7c5a597 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/project/ProjectList.vue"
@@ -24,6 +24,13 @@
>项目看板</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"
@@ -375,6 +382,18 @@
>
<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
@@ -550,9 +569,10 @@
<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: [], // 存储有启动任务的项目
@@ -563,6 +583,7 @@
task_count: 0,
dialogVisible: false,
dashBoardVisible: false,
+ userManagementVisible: false,
editVisible: false,
hoveringSuccess: false,
hoveringError: false,
@@ -675,6 +696,12 @@
this.dialogVisible = true;
this.resetProjectForm();
},
+ handleUserManagement() {
+ this.userManagementVisible = true;
+ },
+ handleUserManagementClose() {
+ this.userManagementVisible = false;
+ },
handleEdit(index, row) {
this.editVisible = true;
this.projectForm.name = row["name"];
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/user/UserManagement.vue" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/user/UserManagement.vue"
new file mode 100644
index 0000000..509ffb4
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/user/UserManagement.vue"
@@ -0,0 +1,687 @@
+<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>
\ No newline at end of file
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/user/UserManagementDrawer.vue" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/user/UserManagementDrawer.vue"
new file mode 100644
index 0000000..7cd54b1
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/pages/user/UserManagementDrawer.vue"
@@ -0,0 +1,591 @@
+<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>
\ No newline at end of file
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/restful/api.js" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/restful/api.js"
index 8a91dad..9e4ced7 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/restful/api.js"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/restful/api.js"
@@ -10,15 +10,14 @@
axios.interceptors.request.use(
function(config) {
- if (
- !config.url.startsWith("/api/user") ||
- config.url === "/api/user/list" ||
- config.url === "/api/user/login_log"
- ) {
- // 在请求拦截中,每次请求,都会加上一个Authorization头
+ const noAuthUrls = [
+ '/api/user/login',
+ '/api/user/register',
+ '/api/user/groups'
+ ];
+
+ if (!noAuthUrls.includes(config.url)) {
config.headers.Authorization = store.token;
-
- // 取url地址的第四位作为projectId, 如果不存在,默认设置为0
let projectId = window.location.pathname.split("/")[3];
projectId = projectId ? projectId : 0;
config.headers["Project"] = projectId;
@@ -58,12 +57,6 @@
duration: 2000
});
}
- if (status === 403) {
- Message.error({
- message: error.response.data.detail,
- duration: 2000
- });
- }
} catch (e) {
Message.error({
message: "服务器连接超时, 请重试",
@@ -77,6 +70,10 @@
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 => {
@@ -143,7 +140,7 @@
export const getGroupList = () => {
return axios
- .get("/api/lunarlink/project/groups")
+ .get("/api/user/groups")
.then(res => res.data);
};
@@ -527,3 +524,31 @@
}).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);
+};
+
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/store/mutations.js" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/store/mutations.js"
index 3690295..840c041 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/store/mutations.js"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/store/mutations.js"
@@ -31,6 +31,10 @@
state.is_superuser = value;
},
+ setIsStaff(state, value) {
+ state.is_staff = value;
+ },
+
setShowHosts(state, value) {
state.show_hosts = value;
}
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/store/state.js" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/store/state.js"
index 2dcd625..818a8ce 100644
--- "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/store/state.js"
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/frontend/src/store/state.js"
@@ -5,6 +5,7 @@
token: null,
user: null,
is_superuser: false,
+ is_staff: false,
show_hosts: false,
duration: 2000,
report_path: "/api/lunarlink/reports/",
diff --git "a/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/\346\263\250\345\206\214\345\256\241\346\211\271\345\212\237\350\203\275\346\265\213\350\257\225\346\214\207\345\215\227.md" "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/\346\263\250\345\206\214\345\256\241\346\211\271\345\212\237\350\203\275\346\265\213\350\257\225\346\214\207\345\215\227.md"
new file mode 100644
index 0000000..ef626b2
--- /dev/null
+++ "b/\346\265\213\350\257\225\347\273\204/Test_platform/Interface_automation/\346\263\250\345\206\214\345\256\241\346\211\271\345\212\237\350\203\275\346\265\213\350\257\225\346\214\207\345\215\227.md"
@@ -0,0 +1,144 @@
+# 用户注册审批功能测试指南
+
+## 功能概述
+
+本系统实现了完整的用户注册审批流程,包括:
+
+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. 多个超级管理员时,任何一人审批即可生效
--
Gitblit v1.9.1