重写复现方法
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
package com.mini.capi.biz.controller;
|
||||||
|
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 前端控制器
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @since 2025-08-28
|
||||||
|
*/
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/biz/apiUser")
|
||||||
|
public class ApiUserController {
|
||||||
|
|
||||||
|
}
|
||||||
86
src/main/java/com/mini/capi/biz/domain/ApiUser.java
Normal file
86
src/main/java/com/mini/capi/biz/domain/ApiUser.java
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
package com.mini.capi.biz.domain;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
*
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @since 2025-08-28
|
||||||
|
*/
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@TableName("biz_api_user")
|
||||||
|
public class ApiUser implements Serializable {
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
|
||||||
|
@TableField("create_time")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户编号
|
||||||
|
*/
|
||||||
|
@TableId(value = "user_id", type = IdType.AUTO)
|
||||||
|
private String userId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录名称
|
||||||
|
*/
|
||||||
|
@TableField("api_user")
|
||||||
|
private String apiUser;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 登录密码
|
||||||
|
*/
|
||||||
|
@TableField("api_pswd")
|
||||||
|
private String apiPswd;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户名称
|
||||||
|
*/
|
||||||
|
@TableField("uname")
|
||||||
|
private String uname;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*/
|
||||||
|
@TableField("ustatus")
|
||||||
|
private String ustatus;
|
||||||
|
|
||||||
|
@TableField("update_time")
|
||||||
|
private LocalDateTime updateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 租户id
|
||||||
|
*/
|
||||||
|
@TableField("f_tenant_id")
|
||||||
|
private String fTenantId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程id
|
||||||
|
*/
|
||||||
|
@TableField("f_flow_id")
|
||||||
|
private String fFlowId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程任务主键
|
||||||
|
*/
|
||||||
|
@TableField("f_flow_task_id")
|
||||||
|
private String fFlowTaskId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 流程任务状态
|
||||||
|
*/
|
||||||
|
@TableField("f_flow_state")
|
||||||
|
private Integer fFlowState;
|
||||||
|
}
|
||||||
16
src/main/java/com/mini/capi/biz/mapper/ApiUserMapper.java
Normal file
16
src/main/java/com/mini/capi/biz/mapper/ApiUserMapper.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.mini.capi.biz.mapper;
|
||||||
|
|
||||||
|
import com.mini.capi.biz.domain.ApiUser;
|
||||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* Mapper 接口
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @since 2025-08-28
|
||||||
|
*/
|
||||||
|
public interface ApiUserMapper extends BaseMapper<ApiUser> {
|
||||||
|
|
||||||
|
}
|
||||||
16
src/main/java/com/mini/capi/biz/service/ApiUserService.java
Normal file
16
src/main/java/com/mini/capi/biz/service/ApiUserService.java
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package com.mini.capi.biz.service;
|
||||||
|
|
||||||
|
import com.mini.capi.biz.domain.ApiUser;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 服务类
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @since 2025-08-28
|
||||||
|
*/
|
||||||
|
public interface ApiUserService extends IService<ApiUser> {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.mini.capi.biz.service.impl;
|
||||||
|
|
||||||
|
import com.mini.capi.biz.domain.ApiUser;
|
||||||
|
import com.mini.capi.biz.mapper.ApiUserMapper;
|
||||||
|
import com.mini.capi.biz.service.ApiUserService;
|
||||||
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* 服务实现类
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @author gaoxq
|
||||||
|
* @since 2025-08-28
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
public class ApiUserServiceImpl extends ServiceImpl<ApiUserMapper, ApiUser> implements ApiUserService {
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
package com.mini.capi.config;
|
package com.mini.capi.config;
|
||||||
|
|
||||||
import com.mini.capi.utils.vToken;
|
import com.mini.capi.biz.domain.ApiUser;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
public class AuthInterceptor implements HandlerInterceptor {
|
public class AuthInterceptor implements HandlerInterceptor {
|
||||||
@@ -13,9 +14,9 @@ public class AuthInterceptor implements HandlerInterceptor {
|
|||||||
public boolean preHandle(HttpServletRequest request,
|
public boolean preHandle(HttpServletRequest request,
|
||||||
HttpServletResponse response,
|
HttpServletResponse response,
|
||||||
Object handler) throws Exception {
|
Object handler) throws Exception {
|
||||||
|
HttpSession session = request.getSession();
|
||||||
String token = request.getHeader("Authorization");
|
ApiUser apiUser = (ApiUser) session.getAttribute("Authorization");
|
||||||
if (token == null || !vToken.isValidToken(token)) {
|
if (apiUser == null) {
|
||||||
response.sendRedirect(request.getContextPath() + "/login");
|
response.sendRedirect(request.getContextPath() + "/login");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/main/java/com/mini/capi/model/auth/Result.java
Normal file
52
src/main/java/com/mini/capi/model/auth/Result.java
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
package com.mini.capi.model.auth;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Result implements Serializable {
|
||||||
|
|
||||||
|
// 状态码:200表示成功,其他表示错误
|
||||||
|
private int code;
|
||||||
|
// 响应信息
|
||||||
|
private String msg;
|
||||||
|
// 响应数据(可选)
|
||||||
|
private Object data;
|
||||||
|
|
||||||
|
// 私有构造方法,防止直接创建实例
|
||||||
|
private Result() {}
|
||||||
|
|
||||||
|
// 成功响应
|
||||||
|
public static Result success(String msg) {
|
||||||
|
Result result = new Result();
|
||||||
|
result.code = 200;
|
||||||
|
result.msg = msg;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带数据的成功响应
|
||||||
|
public static Result success(String msg, Object data) {
|
||||||
|
Result result = new Result();
|
||||||
|
result.code = 200;
|
||||||
|
result.msg = msg;
|
||||||
|
result.data = data;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误响应
|
||||||
|
public static Result error(String msg) {
|
||||||
|
Result result = new Result();
|
||||||
|
result.code = 500; // 500表示服务器错误,也可以根据实际情况使用其他错误码
|
||||||
|
result.msg = msg;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 带自定义错误码的错误响应
|
||||||
|
public static Result error(int code, String msg) {
|
||||||
|
Result result = new Result();
|
||||||
|
result.code = code;
|
||||||
|
result.msg = msg;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,7 +29,7 @@ public class demo {
|
|||||||
.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper"));
|
.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/src/main/resources/mapper"));
|
||||||
})
|
})
|
||||||
.strategyConfig(builder -> {
|
.strategyConfig(builder -> {
|
||||||
builder.addInclude("biz_sync_task_log")
|
builder.addInclude("biz_api_user")
|
||||||
.addTablePrefix("biz_")
|
.addTablePrefix("biz_")
|
||||||
.entityBuilder()
|
.entityBuilder()
|
||||||
.enableLombok()
|
.enableLombok()
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.mini.capi.sys.controller;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
|
import com.mini.capi.biz.domain.ApiUser;
|
||||||
|
import com.mini.capi.biz.service.ApiUserService;
|
||||||
|
import com.mini.capi.model.auth.Result;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/Sys/login")
|
||||||
|
public class LoginController {
|
||||||
|
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ApiUserService userService;
|
||||||
|
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class LoginRequest implements Serializable {
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 密码校验(生产环境需替换为加密比对)
|
||||||
|
*/
|
||||||
|
private boolean verifyPassword(String rawPassword, String encodedPassword) {
|
||||||
|
return Objects.equals(rawPassword, encodedPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用户登录
|
||||||
|
*/
|
||||||
|
@PostMapping("/userLogin")
|
||||||
|
public Result login(@RequestBody LoginRequest user, HttpSession session) {
|
||||||
|
try {
|
||||||
|
QueryWrapper<ApiUser> queryWrapper = new QueryWrapper<>();
|
||||||
|
queryWrapper.eq("api_user", user.getUsername())
|
||||||
|
.eq("ustatus", 1);
|
||||||
|
ApiUser apiUser = userService.getOne(queryWrapper);
|
||||||
|
if (apiUser == null) {
|
||||||
|
return Result.error("账户不存在");
|
||||||
|
}
|
||||||
|
if (!verifyPassword(user.getPassword(), apiUser.getApiPswd())) {
|
||||||
|
// 可记录登录失败日志,用于后续风控
|
||||||
|
return Result.error("账户或密码错误");
|
||||||
|
}
|
||||||
|
session.setAttribute("Authorization", apiUser);
|
||||||
|
return Result.success("登录成功");
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Result.error("登录失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,4 +10,13 @@ public class LoginPageController {
|
|||||||
public String loginPage() {
|
public String loginPage() {
|
||||||
return "forward:/index.html";
|
return "forward:/index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 首页
|
||||||
|
*/
|
||||||
|
@GetMapping("/welcome")
|
||||||
|
public String welcomePage() {
|
||||||
|
return "forward:/views/demo.html";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
25
src/main/resources/mapper/ApiUserMapper.xml
Normal file
25
src/main/resources/mapper/ApiUserMapper.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.mini.capi.biz.mapper.ApiUserMapper">
|
||||||
|
|
||||||
|
<!-- 通用查询映射结果 -->
|
||||||
|
<resultMap id="BaseResultMap" type="com.mini.capi.biz.domain.ApiUser">
|
||||||
|
<id column="user_id" property="userId" />
|
||||||
|
<result column="create_time" property="createTime" />
|
||||||
|
<result column="api_user" property="apiUser" />
|
||||||
|
<result column="api_pswd" property="apiPswd" />
|
||||||
|
<result column="uname" property="uname" />
|
||||||
|
<result column="ustatus" property="ustatus" />
|
||||||
|
<result column="update_time" property="updateTime" />
|
||||||
|
<result column="f_tenant_id" property="fTenantId" />
|
||||||
|
<result column="f_flow_id" property="fFlowId" />
|
||||||
|
<result column="f_flow_task_id" property="fFlowTaskId" />
|
||||||
|
<result column="f_flow_state" property="fFlowState" />
|
||||||
|
</resultMap>
|
||||||
|
|
||||||
|
<!-- 通用查询结果列 -->
|
||||||
|
<sql id="Base_Column_List">
|
||||||
|
create_time, user_id, api_user, api_pswd, uname, ustatus, update_time, f_tenant_id, f_flow_id, f_flow_task_id, f_flow_state
|
||||||
|
</sql>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
324
src/main/resources/static/assets/css/login-style.css
Normal file
324
src/main/resources/static/assets/css/login-style.css
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
/* 全局样式重置:增加box-sizing强制继承,避免尺寸计算偏差 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 样式变量:统一管理,减少硬编码 */
|
||||||
|
:root {
|
||||||
|
--primary-color: #3498db;
|
||||||
|
--primary-dark: #2980b9;
|
||||||
|
--primary-light: #e3f2fd;
|
||||||
|
--bg-gradient-1: #2c3e50;
|
||||||
|
--bg-gradient-2: #3498db;
|
||||||
|
--white: #fff;
|
||||||
|
--gray-light: #f8f9fa;
|
||||||
|
--gray-border: #ddd;
|
||||||
|
--gray-hover: #f1f1f1;
|
||||||
|
--text-gray: #555;
|
||||||
|
--text-light-gray: #888;
|
||||||
|
--danger-color: #e74c3c;
|
||||||
|
--shadow-normal: 0 10px 30px rgba(0, 0, 0, 0.15);
|
||||||
|
--shadow-modal: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
|
--shadow-input: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||||
|
--transition-base: all 0.25s ease;
|
||||||
|
--login-box-width: 420px; /* 固定登录框宽度,避免变形 */
|
||||||
|
--input-height: 48px; /* 固定输入框高度,统一视觉 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 页面主体:固定布局,确保登录框不被挤压 */
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, var(--bg-gradient-1), var(--bg-gradient-2));
|
||||||
|
padding: 0 80px;
|
||||||
|
overflow: hidden; /* 禁止页面滚动 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------- 左侧品牌区域:固定尺寸和位置 -------------------------- */
|
||||||
|
.brand-side {
|
||||||
|
width: 55%;
|
||||||
|
max-width: 700px;
|
||||||
|
color: var(--white);
|
||||||
|
padding: 40px;
|
||||||
|
text-align: center;
|
||||||
|
margin-right: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-title-group {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-side h1 {
|
||||||
|
font-size: 3.2rem;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-shadow: 0 3px 12px rgba(0, 0, 0, 0.25);
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-divider {
|
||||||
|
width: 80px;
|
||||||
|
height: 3px;
|
||||||
|
background: linear-gradient(90deg, transparent, var(--white), transparent);
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-side p {
|
||||||
|
font-size: 1.15rem;
|
||||||
|
opacity: 0.95;
|
||||||
|
line-height: 1.7;
|
||||||
|
text-shadow: 0 1px 5px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 18px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon i {
|
||||||
|
font-size: 2.8rem;
|
||||||
|
opacity: 0.95;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon i:hover {
|
||||||
|
transform: translateY(-6px) scale(1.05);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------- 右侧登录容器:固定尺寸,彻底解决变形 -------------------------- */
|
||||||
|
.login-container {
|
||||||
|
width: var(--login-box-width); /* 固定宽度,不随内容拉伸 */
|
||||||
|
background: var(--white);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: var(--shadow-normal);
|
||||||
|
overflow: hidden;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
position: relative;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 12px 35px rgba(0, 0, 0, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header {
|
||||||
|
text-align: center;
|
||||||
|
padding: 25px;
|
||||||
|
background: var(--gray-light);
|
||||||
|
border-bottom: 1px solid var(--gray-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-header h2 {
|
||||||
|
font-size: 1.7rem;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单区域:固定内边距,避免内容溢出 */
|
||||||
|
.login-form {
|
||||||
|
padding: 35px 30px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表单组:固定间距,不随内容变化 */
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 28px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.required-mark {
|
||||||
|
color: var(--danger-color);
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 9px;
|
||||||
|
color: var(--text-gray);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------- 输入框组:固定高度+定位,彻底解决变形 -------------------------- */
|
||||||
|
.input-group {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
height: var(--input-height); /* 固定容器高度,与输入框一致 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框:固定高度+内边距,不拉伸 */
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%; /* 继承容器高度,避免自行拉伸 */
|
||||||
|
padding: 0 18px 0 45px; /* 左侧留足图标空间,右侧留足按钮空间 */
|
||||||
|
border: 1px solid var(--gray-border);
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1rem;
|
||||||
|
outline: none;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
background-color: var(--white);
|
||||||
|
resize: none;
|
||||||
|
appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框聚焦:仅改变边框和阴影,不改变尺寸 */
|
||||||
|
.form-group input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: var(--shadow-input);
|
||||||
|
background-color: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:not(:placeholder-shown) {
|
||||||
|
border-color: #b3d9f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 输入框图标:固定左侧定位,不随输入框变化 */
|
||||||
|
.input-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px; /* 固定左侧距离,不偏移 */
|
||||||
|
color: var(--text-light-gray);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
z-index: 1;
|
||||||
|
width: 20px; /* 固定图标宽度,避免抖动 */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:not(:placeholder-shown) + .input-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 密码切换按钮:固定最右侧,不偏移 */
|
||||||
|
.toggle-pwd {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px; /* 固定右侧距离 */
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%); /* 垂直居中 */
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-light-gray);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-pwd:hover {
|
||||||
|
background-color: var(--gray-hover);
|
||||||
|
color: var(--text-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 登录按钮:固定尺寸,不拉伸 */
|
||||||
|
.btn-login {
|
||||||
|
width: 100%;
|
||||||
|
height: 52px; /* 固定按钮高度 */
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--white);
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 1.05rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
box-shadow: 0 3px 8px rgba(52, 152, 219, 0.2);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 5px 12px rgba(41, 128, 185, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-login:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
box-shadow: 0 2px 5px rgba(41, 128, 185, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -------------------------- 弹窗:固定层级,避免被遮挡 -------------------------- */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999; /* 最高层级,不被任何元素遮挡 */
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--white);
|
||||||
|
padding: 25px;
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 320px;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: var(--shadow-modal);
|
||||||
|
transform: translateY(-20px);
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.show .modal-content {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content .icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: var(--danger-color);
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--text-gray);
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content button {
|
||||||
|
padding: 10px 25px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: var(--white);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-base);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content button:hover {
|
||||||
|
background: var(--primary-dark);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content button:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
175
src/main/resources/static/assets/js/login-script.js
Normal file
175
src/main/resources/static/assets/js/login-script.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* 登录逻辑处理:阻止表单默认提交,校验用户名/密码,调用登录接口
|
||||||
|
* @param {Event} e - 表单提交事件
|
||||||
|
* @returns {boolean} - 返回false阻止默认提交
|
||||||
|
*/
|
||||||
|
function handleLogin(e) {
|
||||||
|
// 1. 阻止表单默认提交行为(避免页面刷新)
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 2. 获取并清理用户名、密码(去除前后空格)
|
||||||
|
const username = document.getElementById('username').value.trim();
|
||||||
|
const password = document.getElementById('password').value.trim();
|
||||||
|
|
||||||
|
// 3. 前端简单校验(空值判断)
|
||||||
|
if (!username) {
|
||||||
|
showModal('请输入用户名');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
showModal('请输入密码');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 调用后端登录接口(POST请求)
|
||||||
|
fetch('Sys/login/userLogin', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json' // 声明请求体为JSON格式
|
||||||
|
},
|
||||||
|
body: JSON.stringify({username, password}) // 转换为JSON字符串
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
// 4.1 校验HTTP响应状态(非200-299范围视为错误)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`请求失败,状态码:${response.status}`);
|
||||||
|
}
|
||||||
|
// 4.2 解析响应体(假设后端返回JSON格式)
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
// 5. 处理后端返回结果(假设接口返回 { code: 200, msg: '成功' } 结构)
|
||||||
|
if (data.code === 200) {
|
||||||
|
// 登录成功:跳转到demo页面(可根据实际需求修改跳转路径)
|
||||||
|
window.location.href = 'welcome';
|
||||||
|
} else {
|
||||||
|
// 业务错误:显示后端返回的错误信息,无信息时显示默认提示
|
||||||
|
showModal(data.msg || '登录失败,请检查账号密码是否正确');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// 6. 捕获请求异常(网络错误、接口500等)
|
||||||
|
showModal(`登录异常:${error.message}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示错误弹窗
|
||||||
|
* @param {string} msg - 弹窗提示内容
|
||||||
|
*/
|
||||||
|
function showModal(msg) {
|
||||||
|
const errorMsgElem = document.getElementById('errorMsg');
|
||||||
|
const modalElem = document.getElementById('errorModal');
|
||||||
|
|
||||||
|
// 设置弹窗文本内容
|
||||||
|
errorMsgElem.innerText = msg;
|
||||||
|
// 显示弹窗(通过flex布局实现居中)
|
||||||
|
modalElem.style.display = 'flex';
|
||||||
|
// 延迟添加show类(触发过渡动画,避免动画不生效)
|
||||||
|
setTimeout(() => {
|
||||||
|
modalElem.classList.add('show');
|
||||||
|
}, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭错误弹窗
|
||||||
|
*/
|
||||||
|
function closeModal() {
|
||||||
|
const modalElem = document.getElementById('errorModal');
|
||||||
|
|
||||||
|
// 移除show类(触发淡出动画)
|
||||||
|
modalElem.classList.remove('show');
|
||||||
|
// 动画结束后隐藏弹窗(300ms与CSS过渡时间保持一致)
|
||||||
|
setTimeout(() => {
|
||||||
|
modalElem.style.display = 'none';
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空指定输入框内容
|
||||||
|
* @param {string} inputId - 输入框的ID属性值
|
||||||
|
*/
|
||||||
|
function clearInput(inputId) {
|
||||||
|
const inputElem = document.getElementById(inputId);
|
||||||
|
if (inputElem) {
|
||||||
|
// 清空输入框内容
|
||||||
|
inputElem.value = '';
|
||||||
|
// 触发input事件(更新清空按钮的显示状态)
|
||||||
|
inputElem.dispatchEvent(new Event('input'));
|
||||||
|
// 聚焦到当前输入框(提升用户体验)
|
||||||
|
inputElem.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切换密码输入框的可见性(明文/密文切换)
|
||||||
|
*/
|
||||||
|
function togglePasswordVisibility() {
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
const toggleBtn = passwordInput.parentElement.querySelector('.toggle-pwd i');
|
||||||
|
|
||||||
|
if (passwordInput.type === 'password') {
|
||||||
|
// 切换为明文显示
|
||||||
|
passwordInput.type = 'text';
|
||||||
|
// 更换图标为"眼睛"(表示当前是明文)
|
||||||
|
toggleBtn.classList.remove('bi-eye-slash');
|
||||||
|
toggleBtn.classList.add('bi-eye');
|
||||||
|
} else {
|
||||||
|
// 切换为密文显示
|
||||||
|
passwordInput.type = 'password';
|
||||||
|
// 更换图标为"眼睛斜杠"(表示当前是密文)
|
||||||
|
toggleBtn.classList.remove('bi-eye');
|
||||||
|
toggleBtn.classList.add('bi-eye-slash');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入框内容变化时的动态反馈:控制清空按钮的显示/隐藏
|
||||||
|
* @param {HTMLInputElement} inputElem - 输入框元素
|
||||||
|
*/
|
||||||
|
function handleInputChange(inputElem) {
|
||||||
|
const clearBtn = inputElem.parentElement.querySelector('.clear-btn');
|
||||||
|
if (clearBtn) {
|
||||||
|
// 输入框有内容时显示清空按钮,无内容时隐藏
|
||||||
|
clearBtn.hidden = !inputElem.value.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载完成后:绑定全局交互事件
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// 1. 获取用户名和密码输入框元素
|
||||||
|
const usernameInput = document.getElementById('username');
|
||||||
|
const passwordInput = document.getElementById('password');
|
||||||
|
|
||||||
|
// 2. 绑定输入框内容变化事件(控制清空按钮显示)
|
||||||
|
if (usernameInput) {
|
||||||
|
usernameInput.addEventListener('input', () => handleInputChange(usernameInput));
|
||||||
|
}
|
||||||
|
if (passwordInput) {
|
||||||
|
passwordInput.addEventListener('input', () => handleInputChange(passwordInput));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 点击弹窗外部关闭弹窗
|
||||||
|
const modalElem = document.getElementById('errorModal');
|
||||||
|
modalElem.addEventListener('click', (e) => {
|
||||||
|
// 仅当点击弹窗背景(而非内容区)时关闭
|
||||||
|
if (e.target === modalElem) {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 按ESC键关闭弹窗
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
// 仅当弹窗显示时生效
|
||||||
|
if (e.key === 'Escape' && modalElem.style.display === 'flex') {
|
||||||
|
closeModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 5. 页面初始化时,默认聚焦到用户名输入框(提升用户体验)
|
||||||
|
if (usernameInput) {
|
||||||
|
usernameInput.focus();
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,10 +1,71 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>Title</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.2/font/bootstrap-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="./assets/css/login-style.css">
|
||||||
|
<title>cApi登录</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<!-- 左侧品牌区域 -->
|
||||||
|
<div class="brand-side">
|
||||||
|
<div class="brand-icon">
|
||||||
|
<i class="bi bi-shield-lock" style="color: #ecf0f1;"></i>
|
||||||
|
<i class="bi bi-key" style="color: #2ecc71;"></i>
|
||||||
|
<i class="bi bi-server" style="color: #1abc9c;"></i>
|
||||||
|
</div>
|
||||||
|
<div class="brand-title-group">
|
||||||
|
<h1>cApi管理系统</h1>
|
||||||
|
<div class="brand-divider"></div>
|
||||||
|
<p>安全、高效的一站式管理解决方案,为您的业务保驾护航</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧登录容器:固定宽度,避免变形 -->
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-header">
|
||||||
|
<h2>系统登录</h2>
|
||||||
|
</div>
|
||||||
|
<form class="login-form" onsubmit="return handleLogin(event)">
|
||||||
|
<!-- 用户名输入组:结构扁平化,避免嵌套冲突 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="username">
|
||||||
|
账户
|
||||||
|
<span class="required-mark">*</span>
|
||||||
|
</label>
|
||||||
|
<!-- 输入框容器:仅包裹输入框+功能按钮,固定定位规则 -->
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" id="username" placeholder="请输入用户名" required>
|
||||||
|
<i class="bi bi-person input-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 密码输入组:同用户名结构,保持一致性 -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="password">
|
||||||
|
密码
|
||||||
|
<span class="required-mark">*</span>
|
||||||
|
</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" id="password" placeholder="请输入密码" required>
|
||||||
|
<i class="bi bi-lock input-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="submit" class="btn-login">登录</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误提示弹窗:固定层级,避免被遮挡 -->
|
||||||
|
<div class="modal" id="errorModal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<i class="bi bi-exclamation-circle icon"></i>
|
||||||
|
<p id="errorMsg"></p>
|
||||||
|
<button onclick="closeModal()">确定</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="./assets/js/login-script.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
10
src/main/resources/static/views/demo.html
Normal file
10
src/main/resources/static/views/demo.html
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Title</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user