Flask测试平台开发,登陆重构

概述

我们在开篇的时候实现了简单的登陆功能,也实现了一个前后端联调的登陆功能,但是你有没有发现,那个登陆只是一个简单的登陆,且密码在接口返回的过程中是铭文密码,在生产环境中使用肯定是不行的,一般密码都是需要加密的,要么是MD5加密,或者哈希加密,接下来我们重构登陆接口,一同连登陆页面的风格也进行重构,使得风格更加现代化

首先设计数据库关联

  1. 用户表(sys_user

    CREATE TABLE sys_user (
    id bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
    username varchar(50) NOT NULL COMMENT '用户名',
    password varchar(100) NOT NULL COMMENT '密码哈希',
    name varchar(50) NOT NULL COMMENT '姓名',
    email varchar(100) DEFAULT NULL COMMENT '邮箱',
    phone varchar(20) DEFAULT NULL COMMENT '手机号',
    status tinyint NOT NULL DEFAULT '1' COMMENT '状态(0-禁用,1-正常)',
    create_by bigint NOT NULL COMMENT '创建人ID',
    created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_username (username)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户';

    -- 默认超级管理员 (密码: admin123)
    INSERT INTO sys_user (id, username, password, name, email, status, create_by)
    VALUES (1, 'admin', '2a10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '超级管理员', 'admin@example.com', 1, 1);

  2. 角色表(sys_role

    CREATE TABLE sys_role (
    id bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
    role_name varchar(50) NOT NULL COMMENT '角色名称',
    role_code varchar(50) NOT NULL COMMENT '角色编码',
    description varchar(255) DEFAULT NULL COMMENT '角色描述',
    created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_role_code (role_code)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';

    -- 默认角色
    INSERT INTO sys_role (id, role_name, role_code, description)
    VALUES (1, '超级管理员', 'SUPER_ADMIN', '拥有系统所有权限'),
    (2, '普通用户', 'NORMAL_USER', '拥有基础操作权限');

  3. 用户角色关联表(sys_user_role

    CREATE TABLE sys_user_role (
    id bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
    user_id bigint NOT NULL COMMENT '用户ID',
    role_id bigint NOT NULL COMMENT '角色ID',
    created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_user_role (user_id,role_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';

    -- 超级管理员关联超级管理员角色
    INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1);

  4. 菜单表(sys_menu

    CREATE TABLE sys_menu (
    id bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
    parent_id bigint NOT NULL DEFAULT '0' COMMENT '父菜单ID',
    menu_name varchar(50) NOT NULL COMMENT '菜单名称',
    path varchar(255) DEFAULT NULL COMMENT '路由路径',
    component varchar(255) DEFAULT NULL COMMENT '前端组件',
    icon varchar(50) DEFAULT NULL COMMENT '图标',
    sort int NOT NULL DEFAULT '0' COMMENT '排序',
    menu_type tinyint NOT NULL COMMENT '菜单类型(1-目录,2-菜单,3-按钮)',
    permission varchar(100) DEFAULT NULL COMMENT '权限标识',
    is_show tinyint NOT NULL DEFAULT '1' COMMENT '是否显示(0-隐藏,1-显示)',
    created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    updated_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';

    -- 默认菜单示例
    INSERT INTO sys_menu (menu_name, parent_id, path, component, icon, sort, menu_type, permission)
    VALUES
    ('系统管理', 0, '/system', 'Layout', 'el-icon-setting', 1, 1, NULL),
    ('用户管理', 1, 'user', 'system/user/index', NULL, 1, 2, 'system:user:list'),
    ('角色管理', 1, 'role', 'system/role/index', NULL, 2, 2, 'system:role:list'),
    ('菜单管理', 1, 'menu', 'system/menu/index', NULL, 3, 2, 'system:menu:list'),
    ('用户查询', 2, NULL, NULL, NULL, 1, 3, 'system:user:query'),
    ('用户新增', 2, NULL, NULL, NULL, 2, 3, 'system:user:add'),
    ('用户编辑', 2, NULL, NULL, NULL, 3, 3, 'system:user:edit'),
    ('用户删除', 2, NULL, NULL, NULL, 4, 3, 'system:user:delete');

  5. 角色权限关联表(sys_role_menu

    CREATE TABLE sys_role_menu (
    id bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
    role_id bigint NOT NULL COMMENT '角色ID',
    menu_id bigint NOT NULL COMMENT '菜单ID',
    created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    PRIMARY KEY (id),
    UNIQUE KEY uk_role_menu (role_id,menu_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色菜单关联表';

    -- 超级管理员拥有所有权限
    INSERT INTO sys_role_menu (role_id, menu_id) SELECT 1, id FROM sys_menu;

我们的密码现在是使用的bcrypt加密,所以需要在utils工具中编写JTW,用户解密与数据库中的密码进行比对,符合账号正确密码正确才能登陆系统

复制代码
# apis/utils/jwt.py
from functools import wraps
import jwt
from datetime import datetime, timedelta
from flask import current_app, g, request, jsonify
from sqlalchemy import select
from extensions import db


def generate_token(user_id):
    """生成JWT Token"""
    expire = datetime.utcnow() + timedelta(seconds=current_app.config['JWT_ACCESS_TOKEN_EXPIRES'])
    payload = {'exp': expire, 'sub': user_id}
    return jwt.encode(
        payload,
        current_app.config['JWT_SECRET_KEY'],
        algorithm='HS256'
    )


def verify_token(token):
    """验证Token"""
    try:
        payload = jwt.decode(
            token,
            current_app.config['JWT_SECRET_KEY'],
            algorithms=['HS256']
        )
        return payload['sub']  # 返回用户ID
    except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
        return None


def login_required(f):
    """登录认证装饰器"""

    @wraps(f)
    def decorated_function(*args, **kwargs):
        # 1. 检查白名单(如果有配置)
        whitelist = current_app.config.get('PERMISSION_WHITELIST', [])
        if request.path in whitelist:
            return f(*args, **kwargs)

        # 2. 获取Token
        token = request.headers.get('Authorization')
        if not token or not token.startswith('Bearer '):
            return jsonify(code=401, message='未授权访问'), 401

        # 3. 验证Token
        user_id = verify_token(token[7:])
        if not user_id:
            return jsonify(code=401, message='Token已过期或无效'), 401

        # 4. 检查用户状态
        sys_user = db.metadata.tables['sys_user']
        user = db.session.execute(
            select(sys_user).where(sys_user.c.id == user_id)
        ).first()

        if not user or user.status != 1:
            return jsonify(code=401, message='用户不存在或已被禁用'), 401

        # 5. 保存用户信息到请求上下文
        g.user_id = user_id
        g.user = dict(user)  # 将行转换为字典

        # 6. 设置到request对象中,供admin_required使用
        request.current_user_id = user_id

        return f(*args, **kwargs)

    return decorated_function


def admin_required(f):
    """管理员权限装饰器"""

    @wraps(f)
    def decorated_function(*args, **kwargs):
        # 确保先经过login_required认证
        if not hasattr(request, 'current_user_id'):
            return jsonify(code=401, message='请先登录'), 401

        user_id = request.current_user_id

        # 检查用户是否为超级管理员
        sys_user_role = db.metadata.tables['sys_user_role']
        sys_role = db.metadata.tables['sys_role']

        # 查询用户是否有超级管理员角色
        is_admin = db.session.execute(
            select(1).select_from(sys_user_role)
            .join(sys_role, sys_user_role.c.role_id == sys_role.c.id)
            .where(
                (sys_user_role.c.user_id == user_id) &
                (sys_role.c.role_code == 'SUPER_ADMIN')  # 使用role_code而不是roleName
            )
        ).scalar()

        if not is_admin:
            return jsonify(code=403, message='需要管理员权限'), 403

        return f(*args, **kwargs)

    return decorated_function


def permission_required(permission):
    """权限校验装饰器"""

    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not hasattr(g, 'user_id'):
                return jsonify(code=401, message='未授权访问'), 401

            # 这里需要根据您的权限系统实现具体的权限检查逻辑
            # 暂时先返回成功,等权限系统完善后再实现
            return f(*args, **kwargs)

        return decorated_function

    return decorator

建立好数据模型

复制代码
# models/menu.py
from extensions import db  # 从 extensions.py 导入 db,而非 from . import db


class Menu(db.Model):
    __tablename__ = 'sys_menu'  # 对应数据库中的 sys_menu 表
    # __table__ = db.metadata.tables['sys_menu']  # 直接关联反射表

    id = db.Column(db.BigInteger, primary_key=True)
    parent_id = db.Column(db.BigInteger, default=0, nullable=False)  # 父菜单ID
    menu_name = db.Column(db.String(50), nullable=False)  # 菜单名称
    path = db.Column(db.String(255))  # 路由路径
    component = db.Column(db.String(255))  # 前端组件
    menu_type = db.Column(db.SmallInteger, nullable=False)  # 1-目录 2-菜单 3-按钮
    permission = db.Column(db.String(100))  # 权限标识(如 system:user:list)
    sort = db.Column(db.Integer, default=0)  # 排序
    is_show = db.Column(db.SmallInteger, default=1)  # 是否显示

relationship.py

复制代码
# models/relationship.py
from sqlalchemy import Table, Column, BigInteger, ForeignKey
from . import db  # 导入你的 db 实例(SQLAlchemy)

# 显式定义 sys_user_role 关联表(数据库中已存在的表)
sys_user_role = Table(
    'sys_user_role',  # 数据库表名(必须与手动创建的表名一致)
    db.metadata,
    Column('id', BigInteger, primary_key=True),
    Column('user_id', BigInteger, ForeignKey('sys_user.id'), nullable=False),  # 关联 sys_user.id
    Column('role_id', BigInteger, ForeignKey('sys_role.id'), nullable=False)   # 关联 sys_role.id
)

# 显式定义 sys_role_menu 关联表
sys_role_menu = Table(
    'sys_role_menu',  # 数据库表名(必须与手动创建的表名一致)
    db.metadata,
    Column('id', BigInteger, primary_key=True),
    Column('role_id', BigInteger, ForeignKey('sys_role.id'), nullable=False),
    Column('menu_id', BigInteger, ForeignKey('sys_menu.id'), nullable=False)
)

role.py

复制代码
# models/role.py
from extensions import db  # 从 extensions.py 导入 db
from .menu import Menu     # 导入 Menu 模型(此时 Menu 模型已能正确导入 db)
from .relationship import sys_role_menu  # 导入关联表 Table 对象



class Role(db.Model):
    __tablename__ = 'sys_role'
    # _table__ = db.metadata.tables['sys_role']  # 直接关联反射表

    id = db.Column(db.BigInteger, primary_key=True)
    role_name = db.Column(db.String(50), nullable=False)
    role_code = db.Column(db.String(50), unique=True, nullable=False)
    description = db.Column(db.String(255))
    menus = db.relationship(Menu, secondary=sys_role_menu, backref='roles')  # 直接使用 Menu 类

user.py

复制代码
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from extensions import db  # 从 extensions.py 导入 db
from .role import Role     # 导入 Role 模型
from .relationship import sys_user_role


class User(db.Model):
    __tablename__ = 'sys_user'  # 使用已设计的用户表

    id = db.Column(db.BigInteger, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    password = db.Column(db.String(100), nullable=False)
    name = db.Column(db.String(50), nullable=False)
    status = db.Column(db.SmallInteger, default=1, nullable=False)
    create_by = db.Column(db.BigInteger, nullable=False)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)

    # 修复多对多关系:使用导入的 sys_user_role Table 对象
    roles = db.relationship(Role, secondary=sys_user_role, backref='users')  # 直接使用 Role 类

    def set_password(self, password):
        self.password = generate_password_hash(password)

    def check_password(self, password):
        return check_password_hash(self.password, password)

重构登陆接口

复制代码
# -*- coding:utf-8 -*-
import json

from flask import Blueprint
from flask import Blueprint, request, jsonify, g
from models import db
from models.role import Role
from models.user import User
# from utils.jwt import generate_token,login_required
from extensions import db  # 导入 db 实例
from utils.jwt import login_required

app_user = Blueprint("app_user", __name__)

# 原有登录接口改造
from sqlalchemy import select
import bcrypt  # 导入 bcrypt 库

@app_user.route("/api/user/login", methods=['POST'])
def login():
    data = request.get_json()
    username = data.get('username')
    password = data.get('password', '').encode('utf-8')  # 前端密码转为 bytes

    # 1. 反射 sys_user 表并查询用户
    sys_user = db.metadata.tables['sys_user']
    query = select(sys_user).where(sys_user.c.username == username)
    user = db.session.execute(query).fetchone()

    # 2. 基础验证(用户存在 + 状态正常)
    if not user:
        return jsonify(code=40000, message="用户不存在"), 401  # 用户不存在
    if user.status != 1:  # 假设 1=正常,0=禁用
        return jsonify(code=40000, message="用户禁用"), 403  # 用户禁用

    # 3. bcrypt 密码验证(核心修复)
    db_password = user.password.encode('utf-8')  # 数据库密文转为 bytes
    if not bcrypt.checkpw(password, db_password):  # 验证明文密码与密文是否匹配
        return jsonify(code=40000, message="密码错误"), 401  # 密码错误

    # 4. 登录成功(生成 Token 等后续逻辑)
    return jsonify(code=20000, message="success", data={"token": "your_token"})


# 新增用户信息接口
# apis/user.py(假设 info 接口代码)
@app_user.route("/api/user/info", methods=['GET'])
def get_user_info():
    token = request.args.get('token')

    # 1. 验证 token(省略,假设已验证通过)
    # 2. 查询用户信息(使用之前反射表的方式)
    sys_user = db.metadata.tables['sys_user']
    user = db.session.execute(select(sys_user).where(sys_user.c.username == 'admin')).fetchone()

    # 3. 构造符合前端预期的响应格式
    return jsonify({
        "code": 20000,  # 成功状态码(必须为前端约定的值)
        "message": "success",  # 成功消息
        "data": {
            "name": user.name,  # 用户名(从数据库获取)
            "roles": ["admin"],  # 用户角色(根据实际权限获取)
            "avatar": "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif"  # 头像(可选)
        }
    })


# 新增用户管理接口(管理员创建子账号)
@app_user.route("/api/system/user", methods=['POST'])
@login_required
def create_user():
    current_user = g.user
    # 1. 校验管理员权限
    if not any(role.role_code == 'SUPER_ADMIN' for role in current_user.roles):
        return jsonify(code=403, message='无权限创建用户'), 403

    # 2. 处理请求数据
    data = request.get_json()
    new_user = User(
        username=data['username'],
        name=data['name'],
        create_by=current_user.id
    )
    new_user.set_password(data['password'])  # 密码加密

    # 3. 关联角色
    role_ids = data.get('role_ids', [])
    roles = Role.query.filter(Role.id.in_(role_ids)).all()
    new_user.roles = roles

    db.session.add(new_user)
    db.session.commit()
    return jsonify(code=20000, message='用户创建成功')

配置文件中需要做如下配置,在config中增加配置

复制代码
# 权限相关配置
JWT_SECRET_KEY = 'your-jwt-secret-key'  # 已有的 JWT 密钥
JWT_ACCESS_TOKEN_EXPIRES = 3600  # Token 有效期 1 小时(已有的配置)

# 添加:权限白名单路径(无需认证即可访问的接口)
PERMISSION_WHITELIST = [
    '/api/user/login',  # 登录接口(必须添加,否则无法登录)
    '/api/user/register',  # 注册接口(如果有)
    '/api/common/captcha',  # 验证码接口(如果有)
    # 前端静态资源路径(如果需要,如 /login.html, /favicon.ico)
    '/', '/login.html', '/favicon.ico'
]

重启服务后重新登陆系统此时可以成功登录

重构登陆页面,之前的登陆页面效果如下,看着不是很直观,我们现在重构下风格--重构前

重构后源码如下:

复制代码
<template>
  <div class="login-container">
    <!-- 左侧背景区域 -->
    <div class="login-left">
      <div class="left-content">
        <div class="logo">
          <svg-icon icon-class="logo" />
          <img src="@/assets/logo.gif" alt="R-B平台LOGO" class="platform-logo">
          <span class="logo-text"></span>
        </div>
        <h1 class="welcome-text">欢迎回来</h1>
        <p class="welcome-subtext">高效、安全、稳定的RB业务测试平台 简称REBORT</p>
        <div class="features">
          <div class="feature-item">
            <svg-icon icon-class="shield" class="feature-icon" />
            <div>
              <h3>安全可靠</h3>
              <p>需求统计管理</p>
            </div>
          </div>
          <div class="feature-item">
            <svg-icon icon-class="rocket" class="feature-icon" />
            <div>
              <h3>高效稳定</h3>
              <p>99.9%的可用性,工具集成,自动化</p>
            </div>
          </div>
          <div class="feature-item">
            <svg-icon icon-class="customize" class="feature-icon" />
            <div>
              <h3>灵活跳转</h3>
              <p>环境地址快速跳转</p>
            </div>
          </div>
        </div>
      </div>
      <div class="copyright">
        © 2025 REBORT平台 版权所有 一切皆有可能
      </div>
    </div>

    <!-- 右侧登录表单区域 -->
    <div class="login-right">
      <div class="login-form-container">
        <div class="form-header">
          <h2>RB-TEST 平台</h2>
          <p>使用您的账号密码登录系统</p>
        </div>

        <el-form
          ref="loginForm"
          :model="loginForm"
          :rules="loginRules"
          class="login-form"
          auto-complete="on"
          label-position="left"
        >
          <el-form-item prop="username" class="form-item">
            <span class="svg-container">
              <svg-icon icon-class="user" />
            </span>
            <el-input
              ref="username"
              v-model="loginForm.username"
              placeholder="请输入用户名"
              name="username"
              type="text"
              tabindex="1"
              auto-complete="on"
            />
          </el-form-item>

          <el-form-item prop="password" class="form-item">
            <span class="svg-container">
              <svg-icon icon-class="password" />
            </span>
            <el-input
              :key="passwordType"
              ref="password"
              v-model="loginForm.password"
              :type="passwordType"
              placeholder="请输入密码"
              name="password"
              tabindex="2"
              auto-complete="on"
              @keyup.enter.native="handleLogin"
            />
            <span class="show-pwd" @click="showPwd">
              <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
            </span>
          </el-form-item>

          <div class="form-options">
            <el-checkbox v-model="rememberMe">记住我</el-checkbox>
            <router-link to="/forgot-password" class="forgot-password">忘记密码?</router-link>
          </div>

          <el-button
            :loading="loading"
            type="primary"
            class="login-btn"
            @click.native.prevent="handleLogin"
          >
            <span v-if="!loading">登 录</span>
            <span v-else>登录中...</span>
          </el-button>

          <div class="other-login">
            <p class="divider"><span>其他登录方式</span></p>
            <div class="social-login">
              <el-tooltip content="微信登录" placement="top">
                <div class="social-icon wechat">
                  <svg-icon icon-class="wechat" />
                </div>
              </el-tooltip>
              <el-tooltip content="企业微信" placement="top">
                <div class="social-icon QYwx">
                  <svg-icon icon-class="QYwx" />
                </div>
              </el-tooltip>
              <el-tooltip content="QQ登录" placement="top">
                <div class="social-icon QQ">
                  <svg-icon icon-class="QQ" />
                </div>
              </el-tooltip>
            </div>
          </div>

          <div class="register-tip">
            还没有账号? <router-link to="/register">立即注册</router-link>
          </div>
        </el-form>
      </div>
    </div>
  </div>
</template>

<script>
import { validUsername } from '@/utils/validate'

export default {
  name: 'Login',
  data() {
    const validateUsername = (rule, value, callback) => {
      if (!validUsername(value)) {
        callback(new Error('请输入正确的用户名'))
      } else {
        callback()
      }
    }
    const validatePassword = (rule, value, callback) => {
      if (value.length < 6) {
        callback(new Error('密码长度不能少于6位'))
      } else {
        callback()
      }
    }
    return {
      loginForm: {
        username: '',
        password: ''
      },
      loginRules: {
        username: [{ required: true, trigger: 'blur', validator: validateUsername }],
        password: [{ required: true, trigger: 'blur', validator: validatePassword }]
      },
      loading: false,
      passwordType: 'password',
      redirect: undefined,
      rememberMe: false
    }
  },
  watch: {
    $route: {
      handler: function(route) {
        this.redirect = route.query && route.query.redirect
      },
      immediate: true
    }
  },
  methods: {
    showPwd() {
      if (this.passwordType === 'password') {
        this.passwordType = ''
      } else {
        this.passwordType = 'password'
      }
      this.$nextTick(() => {
        this.$refs.password.focus()
      })
    },
    handleLogin() {
      this.$refs.loginForm.validate(valid => {
        if (valid) {
          this.loading = true
          this.$store.dispatch('user/login', this.loginForm).then(() => {
            this.$router.push({ path: this.redirect || '/' })
            this.loading = false
          }).catch(() => {
            this.loading = false
          })
        } else {
          console.log('error submit!!')
          return false
        }
      })
    }
  }
}
</script>

<style lang="scss" scoped>
// 颜色变量 - 统一紫色背景色
$primary-color: #4361ee;
$primary-hover: #3a56d4;
$white: #ffffff;
$light-bg: #f8fafc;
$text-color: #2d3748;
$light-text: #718096;
$border-color: #e2e8f0;
$error-color: #ef4444;
$purple-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); // 统一的紫色渐变
$purple-dark: #5a4ca8; // 深紫色,用于装饰元素

.login-container {
  display: flex;
  min-height: 100vh;
  width: 100%;
  background: $purple-gradient; // 容器应用统一渐变背景

  // 左侧内容区域
  .login-left {
    flex: 1;
    color: $white;
    padding: 2rem;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    position: relative;
    overflow: hidden;

    // 装饰元素 - 统一风格
    &::before {
      content: '';
      position: absolute;
      top: 10%;
      left: 15%;
      width: 60px;
      height: 60px;
      background: rgba(255, 255, 255, 0.1);
      border-radius: 50%;
    }
    &::after {
      content: '';
      position: absolute;
      bottom: 15%;
      right: 20%;
      width: 120px;
      height: 120px;
      background: rgba(255, 255, 255, 0.05);
      border-radius: 50%;
    }

    /* 大LOGO样式 */
    .platform-logo {
      width: 300px;
      height: 300px;
      object-fit: contain;
    }

    .left-content {
      position: relative;
      z-index: 2;
      max-width: 500px;
      margin: 0 auto;
      padding: 2rem;

      .logo {
        display: flex;
        align-items: center;
        margin-bottom: 3rem;

        .svg-icon {
          width: 40px;
          height: 40px;
        }

        .logo-text {
          font-size: 1.5rem;
          font-weight: bold;
          margin-left: 0.75rem;
        }
      }

      .welcome-text {
        font-size: 2.5rem;
        font-weight: 600;
        margin-bottom: 1rem;
      }

      .welcome-subtext {
        font-size: 1.125rem;
        opacity: 0.9;
        margin-bottom: 3rem;
      }

      .features {
        .feature-item {
          display: flex;
          align-items: center;
          margin-bottom: 1.5rem;

          .feature-icon {
            width: 40px;
            height: 40px;
            margin-right: 1rem;
            background: rgba(255, 255, 255, 0.1);
            border-radius: 8px;
            padding: 8px;
          }

          h3 {
            font-size: 1.125rem;
            margin: 0 0 0.25rem;
          }

          p {
            margin: 0;
            opacity: 0.8;
            font-size: 0.875rem;
          }
        }
      }
    }

    .copyright {
      position: relative;
      z-index: 2;
      text-align: center;
      font-size: 0.875rem;
      opacity: 0.7;
      padding: 1rem;
    }
  }

  // 右侧登录表单区域 - 关键修改:移除独立背景色,使用容器统一背景
  .login-right {
    width: 590px;
    display: flex;
    align-items: center;
    padding: 2rem 1rem;
    position: relative; // 用于定位装饰元素

    // 添加与左侧风格一致的装饰元素
    &::before {
      content: '';
      position: absolute;
      top: 20%;
      right: 30%;
      width: 60px;
      height: 60px;
      background: rgba(255, 255, 255, 0.1);
      border-radius: 50%;
    }
    &::after {
      content: '';
      position: absolute;
      bottom: 25%;
      right: 25%;
      width: 30px;
      height: 30px;
      background: rgba(255, 255, 255, 0.2);
      border-radius: 50%;
    }

    .login-form-container {
      width: 300%;
      max-width: 350px; // 表单调整
      margin: 0 auto;
      background: $white;
      padding: 2.5rem 1.5rem;
      border-radius: 12px;
      box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
      animation: fadeIn 0.5s ease;
      position: relative;
      z-index: 2; // 确保表单在装饰元素之上

      .form-header {
        margin-bottom: 1.5rem;
        text-align: center;

        h2 {
          font-size: 1.5rem;
          color: $text-color;
          margin: 0 0 0.5rem;
        }

        p {
          color: $light-text;
          margin: 0;
          font-size: 0.875rem;
        }
      }

      .login-form {
        .form-item {
          margin-bottom: 1rem;

          .el-input {
            width: 100%;

            ::v-deep .el-input__inner {
              height: 42px;
              padding-left: 40px;
              border-radius: 6px;
              border: 1px solid $border-color;
              transition: all 0.3s;

              &:focus {
                border-color: $primary-color;
                box-shadow: 0 0 0 2px rgba(67, 97, 238, 0.2);
              }
            }
          }

          .svg-container {
            position: absolute;
            left: 12px;
            top: 50%;
            transform: translateY(-50%);
            color: $light-text;
            z-index: 2;
          }

          .show-pwd {
            position: absolute;
            right: 12px;
            top: 50%;
            transform: translateY(-50%);
            color: $light-text;
            cursor: pointer;
            z-index: 2;

            &:hover {
              color: $primary-color;
            }
          }
        }

        .form-options {
          display: flex;
          justify-content: space-between;
          align-items: center;
          margin-bottom: 1rem;
          font-size: 0.8rem;

          ::v-deep .el-checkbox {
            color: $light-text;
          }

          .forgot-password {
            color: $light-text;
            font-size: 0.8rem;
            text-decoration: none;

            &:hover {
              color: $primary-color;
              text-decoration: underline;
            }
          }
        }

        .login-btn {
          width: 100%;
          height: 42px;
          background: $primary-color;
          border: none;
          border-radius: 6px;
          font-size: 0.9rem;
          font-weight: 500;
          color: $white;
          margin-bottom: 1rem;
          transition: all 0.3s;

          &:hover {
            background: $primary-hover;
          }

          &:active {
            transform: scale(0.98);
          }
        }

        .other-login {
          margin: 1rem 0;

          .divider {
            display: flex;
            align-items: center;
            color: $light-text;
            font-size: 0.7rem;
            margin: 0.5rem 0;

            &::before, &::after {
              content: "";
              flex: 1;
              height: 1px;
              background: $border-color;
              margin: 0 0.5rem;
            }
          }

          .social-login {
            display: flex;
            justify-content: center;
            gap: 1rem;

            .social-icon {
              width: 36px;
              height: 36px;
              border-radius: 50%;
              display: flex;
              align-items: center;
              justify-content: center;
              cursor: pointer;
              transition: all 0.3s;
              background: $light-bg;

              .svg-icon {
                width: 18px;
                height: 18px;
              }

              &.wechat {
                color: #07c160;

                &:hover {
                  background: rgba(7, 193, 96, 0.1);
                }
              }

              &.QYwx {
                color: #2AAB7F;

                &:hover {
                  background: rgba(42, 171, 127, 0.1);
                }
              }

              &.QQ {
                color: $primary-color;

                &:hover {
                  background: rgba(67, 97, 238, 0.1);
                }
              }
            }
          }
        }

        .register-tip {
          text-align: center;
          color: $light-text;
          font-size: 0.8rem;
          margin-top: 1rem;

          a {
            color: $primary-color;
            text-decoration: none;
            font-weight: 500;

            &:hover {
              text-decoration: underline;
            }
          }
        }
      }
    }
  }
}

// 动画效果
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(10px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

// 响应式调整
@media (max-width: 992px) {
  .login-container {
    flex-direction: column;

    .login-left {
      display: none;
    }

    .login-right {
      width: 100%;
      padding: 2rem 1rem;
    }
  }
}

// 错误状态样式
::v-deep .el-form-item.is-error {
  .el-input__inner {
    border-color: $error-color !important;
  }

  .svg-container {
    color: $error-color !important;
  }
}
</style>

看看重构后的效果,整体效果是不是看上去更加直观

好了,登陆重构到这里结束