用React给XXL-JOB开发一个新皮肤(三):实现登录页和Layout骨架

目录

  • 一. 简述
  • 二. 接口服务调整
    • 2.1. 登录接口
    • 2.2. 登出接口
    • 2.3. 修改密码接口
    • 2.4. 修改配置文件
  • 三. 前端HTTP 请求
  • 四. 登录页面
    • 4.1. 搭建登录页面
    • 4.2. 对接登录接口
  • 五. Layout 骨架
    • 5.1. 搭建骨架
    • 5.2. Header
    • 5.3. 修改密码
    • 5.4. 退出登录
  • 六. 其他

一. 简述

上一篇文章我们介绍了项目的目录规划、修改vite 配置并且创建了项目的路由信息。此篇文章我们会先实现登录页面和管理页面的 Layout 骨架。

二. 接口服务调整

xxl-job-adminIndexController 中有我们需要的登录和登出接口;但是从接口输出方式可以看出来,并不符合前后端分离的模式,所有我们这里需要做兼容。另外还需考虑的是在不修改 xxl-job-admin 中的代码(不然我们后面对照功能的时候不方便)的前提下,如何可以给我们现在的项目提供接口呢?

所有我这里创建了一个新的项目 xxl-job-admin-new,此时我们项目目录结构如下:
在这里插入图片描述
在这个项目中,maven 依赖相较 xxl-job-admin添加 lombokvalidation这两个,其他的不变;另外我添加全局异常处理和一些必要class,结构如下:
在这里插入图片描述

2.1. 登录接口

对于登录接口也无大的改动,仅将之前的POST表单接口改为 POST JSON 方法提交,并添加了表单验证。代码如下:

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {private final XxlJobService xxlJobService;private final LoginService loginService;public UserController(XxlJobService xxlJobService, LoginService loginService) {this.xxlJobService = xxlJobService;this.loginService = loginService;}@PostMapping("login")@PermissionLimit(limit=false)public ReturnT<UserLoginResponseDto> loginDo(@RequestBody @Validated UserLoginRequestDto param, HttpServletRequest request, HttpServletResponse response){return new ReturnT<>(loginService.login(param, request, response));}
}

这里的@PermissionLimit是做鉴权的,具体可以看PermissionInterceptor拦截器。

这里的登录逻辑也很简单,先校验登录表单,然后通过用户名称获取用户实体XxlJobUser,密码比对;接着将XxlJobUser转为十六进制的字符串;最后将登录信息写入 Cookie 并返回。

这里不做详细的 Java 代码分析,后面有时间在写一个专门把 xxl-job 源码分析的专栏吧。

2.2. 登出接口

登出接口也不变,就是清除 Cookie

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {private final XxlJobService xxlJobService;private final LoginService loginService;public UserController(XxlJobService xxlJobService, LoginService loginService) {this.xxlJobService = xxlJobService;this.loginService = loginService;}@GetMapping("logout")@PermissionLimit(limit=false)public ReturnT<String> logout(HttpServletRequest request, HttpServletResponse response){return new ReturnT<>(loginService.logout(request, response));}
}

2.3. 修改密码接口

修改密码的接口,改为 Get 请求,代码如下 :

@Slf4j
@RestController
@RequestMapping("user")
public class UserController {private final XxlJobService xxlJobService;private final LoginService loginService;public UserController(XxlJobService xxlJobService, LoginService loginService) {this.xxlJobService = xxlJobService;this.loginService = loginService;}@GetMapping("updatePwd")public ReturnT<String> updatePwd(HttpServletRequest request, @RequestParam("password") String password){password = password.trim();if (!(password.length()>=4 && password.length()<=20)) {return new ReturnT<String>(PASSWORD_LEN_ERROR, PASSWORD_LEN_ERROR.getMsg());}// md5 passwordString md5Password = DigestUtils.md5DigestAsHex(password.getBytes());// update pwdXxlJobUser loginUser = (XxlJobUser) request.getAttribute(LoginServiceImpl.LOGIN_IDENTITY_KEY);// 修改loginService.updateUser(loginUser.getId(), md5Password);return new ReturnT<>("密码修改成功");}
}

这里的代码,仅修改了loginService.updateUser(loginUser.getId(), md5Password);将修改的逻辑下放到了 service 层。

2.4. 修改配置文件

最后我们需要将application.properties 中关于 freemarkerresources 的配置移除,并将启动端口改为 9090;另外还需要将 logback.xml 中的日志文件修改一个其他的名字。

此时我们启动项目是无法启动,因为报一个i18n 的错误,此时需要我们在 XxlJobScheduler.java 中将 iniI8n()的初始化方法注释就可以了。
在这里插入图片描述
好了,现在我们可以启动 xxl-job-admin-new 的服务了。
在这里插入图片描述

上面的部分是关于后端接口修改,不太理解的前端同学可以直接访问项目启动后端服务就可以了!

三. 前端HTTP 请求

这里我们是用的是 axios 请求接口,一般来说大家都会封装下,我在 naive 的开源项目naive-ui-admin 中看到一个封装很不错的 axios 请求工具,我在此基础上修改了下封装到了 utils 中的 request.ts工具类。

这里封装比较麻烦,但是很好用,有时间可以开一篇文章详细介绍下,如何封装的。

在之前文章中,我们在 src 目录中有一个 api 的目录,就是专门存放,我们调用后端接口的 api 函数,这里我们将用户相关的操作都放到 user.ts中。

import {User} from "@/types";
import {https} from "@/utils/request.ts";namespace UserApi {/*** 登录* @param param* @constructor*/export const UserLogin = (param: User.UserLoginFormProp): Promise<User.UserLoginResultProp> => {return https.request({url: '/user/login',method: 'post',data: param})}/*** 退出登录* @constructor*/export const UserLogout = (): Promise<string> => {return https.request({url: '/user/logout',method: 'get'})}/*** 修改密码* @param pwd* @constructor*/export const UpdatePassword = (pwd: string): Promise<string> => {return https.request({url: '/user/updatePwd',method: 'get',params: {password: pwd}})}
}export default UserApi

这里面的User.UserLoginFormPropUser.UserLoginResultProp是登录的输入和输出模型定义(文章后面会有定义的具体属性)。

这里我们还需要在vite.config.ts中开启代理:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from "path";// https://vitejs.dev/config/
export default defineConfig({server: {port: 9091,proxy: {"/api": {target: "http://localhost:9090/xxl-job-admin",changeOrigin: true, // 跨域设置rewrite: (path) => path.replace(/^\/api/, ""), // 将 api 前缀去掉},},},
})

四. 登录页面

这里的登录页面我们这里借鉴 Ant Design Pro的登录页面,略作修改就可以了。
在这里插入图片描述

4.1. 搭建登录页面

这里登录页面就是一个 form 表单,直接使用https://ant-design.antgroup.com/components/form-cn的表单组件。代码如下:

import styled from "@emotion/styled";
import {Button, Card, Checkbox, Form, Input, message, Typography} from "antd";
import {LockOutlined, UserOutlined} from "@ant-design/icons";
import {User} from "@/types";
import {useNavigate} from "react-router-dom";
import {DEFAULT_HOME_PATH} from "@/config/constant.ts"; // 定义的一些静态常量const LoginPage = () => {const navigate = useNavigate(); // 路由切换 hooksconst [form] = Form.useForm<User.UserLoginFormProp>();const onSubmit = () => {form.validateFields().then(value => {console.log("value => ", value);// TODO 调用登录接口})}return <Container><Card className="login-card" style={{width: 400}}><Typography.Title level={3} style={{textAlign: 'center', marginBottom: 40}}>任务调度中心</Typography.Title><Formname="normal_login"className="login-form"initialValues={{ remember: true }}form={form}><Form.Itemname="username"rules={[{ required: true, message: '请输入账号名称!' }]}><Input prefix={<UserOutlined className="site-form-item-icon" />} placeholder="请输入登录账号" /></Form.Item><Form.Itemname="password"rules={[{ required: true, message: '请输入账号密码!' }]}><Input prefix={<LockOutlined className="site-form-item-icon" />} type="password" placeholder="请输入登录密码"/></Form.Item><Form.Item className="form-login-btn"><Form.Item name="ifRemember" valuePropName="unchecked" noStyle><Checkbox>记住密码</Checkbox></Form.Item><Button type="primary" className="login-form-button" onClick={onSubmit}>登录</Button></Form.Item></Form></Card></Container>
}

这里的<Container>标签是通过@emotion/styled创建样式化组件。

const Container = styled.div`display: flex;flex-direction: column;align-items: center;height: 100vh;overflow: auto;background-image: url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr');background-size: 100% 100%;.login-card {position: absolute;top: 20%;.ant-card-body {padding-top: 8px;.form-login-btn {.ant-form-item-control-input-content {display: flex;flex-direction: row;justify-content: space-between;align-items: center;}}}}
`

运行项目,效果如下:
在这里插入图片描述

4.2. 对接登录接口

我们在 types目录添加一个 user.type.ts 文件,存放用户相关的接口模型(这个模型就是上面定义的)。

namespace User {// UserLoginFormProp 用户登录表单属性export interface UserLoginFormProp {username: string; // 用户名password: string; // 密码ifRemember: boolean; // 记住密码}// UserLoginResultProp 用户登录返回属性export interface UserLoginResultProp {userId: number; // 用户名username: string; // 密码role: number;token: string;permission: string[];}
}export default User;

最后使用 index.ts导出

import User from "@/types/user.type.ts";export type {User
}

这里对接登录接口,我们使用了 ahooks 中的 useRequest ,具体使用就是官方文档中有详细的介绍。https://ahooks.gitee.io/zh-CN/hooks/use-request/index

下面看一下如何借助 useRequest 轻松的调用我们的接口:

const loginRequest = useRequest(UserApi.UserLogin, {manual: true, // 表示手动触发onSuccess: (result) => {console.log(result);message.success('登录成功');navigate(DEFAULT_HOME_PATH); // 跳转到首页}
})const onSubmit = () => {form.validateFields().then(value => {loginRequest.run(value) // 手动调用登录接口})
}

最后我们看一下浏览器的Cookie
在这里插入图片描述

五. Layout 骨架

整个 Layout 骨架主要分为左侧菜单、Header 和内容三部分,这里我们直接使用 antd 实现骨架。

5.1. 搭建骨架

直接上代码:

<Container>{/* 菜单 */}<Layout.Sider trigger={null} collapsible collapsed={collapsed}><div className="xxl-logo-vertical" /><Menutheme="dark"mode="inline"selectedKeys={selectedKeys}onClick={({key}) => clickMenu(key)}items={[{key: '/xxl-job/report',icon: <BarChartOutlined />,label: '运行报表',},{key: '/xxl-job/task',icon: <SnippetsOutlined />,label: '任务管理',},{key: '/xxl-job/dispatch',icon: <CalendarOutlined />,label: '调度日志',},{key: '/xxl-job/executor',icon: <MenuOutlined />,label: '执行器管理',},{key: '/xxl-job/user',icon: <UserOutlined />,label: '用户管理',},{key: '/xxl-job/course',icon: <BulbOutlined />,label: '使用教程',},]}/></Layout.Sider>{/* 内容 */}<Layout>{/* 头 */}<Layout.Header style={{ padding: 0, background: colorBgContainer, display: "flex" }}><Buttontype="text"icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}onClick={() => setCollapsed(!collapsed)}style={{fontSize: '16px',width: 64,height: 64,}}/><div style={{flex: '1 1 0%'}} /><div className="header-right-content"><div style={{height: '100%'}}><div className="header-right-content-inner"><Dropdown key="translate" menu={{ items: translates}} placement="bottom" arrow={{ pointAtCenter: true }}><div className="ant-dropdown-trigger-str"><TranslationOutlined /></div></Dropdown><Dropdown key="items" menu={{ items, onClick: logoutClick}} placement="bottom" arrow={{ pointAtCenter: true }}><div className="ant-dropdown-trigger-logout"><Avatar src="https://zos.alipayobjects.com/rmsportal/ODTLcjxAfvqbxHnVXCYX.png" /><span>管理员</span></div></Dropdown></div></div></div></Layout.Header>{/* 内容 */}<Layout.Content style={{margin: '16px 16px',padding: 24,minHeight: 280,background: colorBgContainer,borderRadius: borderRadiusLG,}}><Outlet /></Layout.Content></Layout><Modal title="修改密码" open={showModel} onOk={okUpdatePassword} onCancel={closeUpdatePassword}><Input placeholder="请输入新密码" value={password} onChange={e => setPassword(e.target.value)}/></Modal>
</Container>

这里的 Menu 菜单涉及到一个点击菜单切换路由的功能,代码如下:

const navigate = useNavigate(); // 路由切换 hooks
const [selectedKeys, setSelectedKeys] = useState<string[]>([DEFAULT_MENU_KEY]); // 默认路由const clickMenu = (data: string) => {setSelectedKeys([data]) // 选中菜单navigate(data) // 切换路由
}

路由切换提现为组件切换,主要是通过<Outlet />,前面的文章已经介绍过了。

5.2. Header

接着我们这个需要在介绍下,Header 部分的代码,这里包含一个菜单的收缩按钮,另一个是就是一个用户头像(包含修改密码和退出登录);还有一个切换语言的下拉组件,后续还会添加一个主题切换的下拉按组件。这两个大同小异,代码如下:

const translates: MenuProps['items'] = [{key: 'chine',label: (<span><span role="img" aria-label="English" style={{marginRight: 8}}>🇨🇳</span>中文</span>)},{key: 'english',label: (<span><span role="img" aria-label="English" style={{marginRight: 8}}>🇺🇸</span>English</span>)}];const items: MenuProps['items'] = [{key: '1',label: '修改密码',icon: <EditOutlined />},{key: '2',label: '退出登录',icon: <LogoutOutlined />,danger: true}];// 用户头像下拉组件的点击事件const logoutClick: MenuProps['onClick'] = (e) => {if (e.key === '2') {logoutRequest.run(); // 退出登录} else if (e.key === '1') {setShowModel(true); // 修改密码的 model 弹窗控制事件}};

5.3. 修改密码

修改密码这里是通过模态框弹出一个Input 组件,用户输出新的密码,调用修改密码接口执行操作的。

<Modal title="修改密码" open={showModel} onOk={okUpdatePassword} onCancel={closeUpdatePassword}><Input placeholder="请输入新密码" value={password} onChange={e => setPassword(e.target.value)}/>
</Modal>

对应的点击事件代码如下:

const [showModel, setShowModel] = useState(false);
const [password, setPassword] = useState<string>("");// 更新密码
const updatePasswordRequest = useRequest(UpdatePassword, {manual: true,onSuccess: (res) => {message.success(res);setShowModel(false);}
})// 模态框 确定按钮事件
const okUpdatePassword = () => {updatePasswordRequest.run(password);
}// 模态框 取消按钮事件
const closeUpdatePassword = () => {setShowModel(false);setPassword(''); // 这里需要清空 input 框内容
}

5.4. 退出登录

退出登录,后端接口会将当前的 cookie清除,前端只需要调用退出登录的接口,并将返回登录页面,代码如下:

const logoutRequest = useRequest(UserLogout, {manual: true,onSuccess: (res) => {message.success(res);navigate(LOGIN_PAGE);}
})

好了,这个部分的代码就搞定了,最后我们启动运行,预览如下:
在这里插入图片描述

六. 其他

上面代码并没有全不展示出来,仅展示重要的代码。需要查看完整代码可以查看仓库:https://gitee.com/molonglove/xxl-job/tree/2.4.0.1/。

下一篇文章,我们将实现用户管理部分的代码页面和接口。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/618310.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

视频监控平台的管理员账号在所有客户端都无法登录的问题解决

目 录 一、问题描述 二、问题排查 1、看问题提示 2、看日志信息 3、问题定位 三、问题解决 1. 添加权限角色 2、添加操作用户 3、验证 一、问题描述 AS-V1000视频监控平台安装部署完成后&#xff0c;发现管理员admin不能到web客户端&#xff0c;觉…

D25XB100-ASEMI家用电器整流桥D25XB100

编辑&#xff1a;ll D25XB100-ASEMI家用电器整流桥D25XB100 型号&#xff1a;D25XB100 品牌&#xff1a;ASEMI 封装&#xff1a;GBJ-5&#xff08;带康铜丝&#xff09; 平均正向整流电流&#xff08;Id&#xff09;&#xff1a;25A 最大反向击穿电压&#xff08;VRM&…

AcWing 103. 电影(map、pair连用or离散化)

题目 方法一&#xff08;mappair&#xff09; 其实上面这么长巴拉巴拉就是在说 首先&#xff0c;每个科学家会的语言都不同。但是呢每部电影的字幕和语言是不一样的&#xff08;字幕和语言一定不相同&#xff09; 要求找到一部电影使得在场能听懂的科学家最多&#xff08;如果存…

Linux Kernel Stack Overflow/Linux 内核栈溢出

不同于Linux应用程序的栈能够动态增长&#xff0c;Linux内核栈是固定的&#xff0c;并且比较小&#xff0c;比如Linux 2.6.x内核&#xff0c;在X86 32位架构上一般是4K或8K&#xff08;在进行内核编译时&#xff0c;Kernel hacking下进行配置&#xff0c;默认8K&#xff09;&am…

上门回收小程序开发,让回收更加简单

资源回收一直是当下深受大众关注的话题&#xff0c;如何做到资源不浪费&#xff0c;成为了大众要考虑的问题。在人们环保意识的加深下&#xff0c;回收行业也是获得了大众的关注&#xff0c;逐渐形成了一个新的商业模式。 随着互联网技术的发展&#xff0c;回收行业也更加方便…

C/S架构,集成三维影像后处理功能,自主版权的一套医院PACS系统源码

一、PACS简介 PACS&#xff08;PictureArchivingandCommunicationsSystem&#xff09;即图像存储与传输系统&#xff0c;是应用于医院的数字医疗设备如CT、MR&#xff08;磁共振&#xff09;、US&#xff08;超声成像&#xff09;、X光机、DSA&#xff08;数字减影&#xff09…

KIBANA可视化管理界面说明

更说明转自https://blog.csdn.net/IT_ZRS/article/details/125496588 1 主要结构功能 使用浏览器访问 ip:5601 默认端口&#xff0c;进入首页 Discover&#xff1a;日志管理视图 主要进行搜索和查询Visualize&#xff1a;统计视图 构建可视化的图表Dashboard&#xf…

【目标检测】YOLOv5算法实现(八):模型验证

本系列文章记录本人硕士阶段YOLO系列目标检测算法自学及其代码实现的过程。其中算法具体实现借鉴于ultralytics YOLO源码Github&#xff0c;删减了源码中部分内容&#xff0c;满足个人科研需求。   本系列文章主要以YOLOv5为例完成算法的实现&#xff0c;后续修改、增加相关模…

JavaWeb,CSS的学习

CSS&#xff0c;层叠样式表&#xff08;Cascading Style Sheets&#xff09;&#xff0c;能够对网页中元素位置的排版进行像素级精确控制&#xff0c;支持几乎所有的字体字号样式&#xff0c;拥有网页对象和模型样式编辑的能力&#xff0c;简单来说&#xff0c;美化页面。 CSS…

c++临时对象的探讨及相关性能提升

产生临时对象的情况 我们定义一个类进行测试 class tempVal { public:int v1, v2;tempVal(int v1 0, int v2 0);tempVal(const tempVal& t) :v1(t.v1), v2(t.v2) {cout << "调用拷贝构造函数" << endl;}virtual ~tempVal() {cout << "…

【python】——turtle动态画

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

AR HUD全面「上新」

AR HUD赛道正在迎来新的时代。 上周&#xff0c;蔚来ET9正式发布亮相&#xff0c;新车定位为D级行政旗舰轿车&#xff0c;其中&#xff0c;在智能座舱交互层面&#xff0c;继理想L系列、长安深蓝S7之后&#xff0c;也首次取消仪表盘&#xff0c;取而代之的是业内首个全焦段AR H…

分块矩阵的定义、计算

目录 一、定义 二、分块矩阵的加减乘法 三、考点 一、定义 分块&#xff0c;顾名思义&#xff0c;将整个矩阵分成几部分&#xff0c;如下图所示 二、分块矩阵的加减乘法 三、考点 分块矩阵的考点不多&#xff0c;一般来说&#xff0c;有一种&#xff1a; 求分块矩阵的转置…

PHP如何拆分中文名字(包括少数民族名字)

/*** param string|null $name* return array|null*/ function splitName($name) {if (empty($name) || empty(trim($name))) {return null;}//该正则是用来提取$name参数里面的中文字符的。preg_match_all(/[\x{4e00}-\x{9fff}]/u, $name, $matchers);$matchersCount isset($…

2024年,谷歌云首席技术官眼中的生成AI三大支柱,来看看有啥新花样

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

App在线封装的革命性创新

随着移动互联网的蓬勃发展&#xff0c;App已经成为我们日常生活中不可或缺的一部分。从购物、交通、社交到娱乐&#xff0c;几乎每个人的智能手机都装载着数十个应用程序&#xff0c;以满足各式各样的需求。然而&#xff0c;对于许多非技术背景的企业家和小型企业而言&#xff…

【机器学习】模型参数优化工具:Optuna使用分步指南(附XGB/LGBM调优代码)

常用的调参方式和工具包 常用的调参方式包括网格搜索(Grid Search)、**随机搜索(Random Search)和贝叶斯优化(Bayesian Optimization)**等。 工具包方面&#xff0c;Scikit-learn提供了GridSearchCV和RandomizedSearchCV等用于网格搜索和随机搜索的工具。另外&#xff0c;有一…

VS报错:error:LNK2005 _main 已经在 *.obj 中定义

应该是重定义了&#xff0c;但是又解决不了&#xff0c;看似又没有重定义啊&#xff0c;就在一个文件定义了啊&#xff1f;怎么会出现这种情况呢&#xff1f;关键是&#xff0c;编译报错&#xff0c;程序运行不了了。 这里提一下我的前期操作&#xff0c;是因为将一个头文件和…

NULL是什么?

NULL是一个编程术语&#xff0c;通常用于表示一个空值或无效值。在很多编程语言中&#xff0c;NULL用于表示一个变量或指针不引用任何有效的对象或内存位置。 NULL可以看作是一个特殊的值&#xff0c;表示缺少有效的数据或引用。当一个变量被赋予NULL值时&#xff0c;它表示该变…

【面试宝典】图解ARP协议、TCP协议、UDP协议

一、ARP协议 二、TCP协议 三、UDP协议 四、TCP和UDP的区别