二、使用Vue3 + Vue CLI 实现系统前端模块的搭建
主要内容
前端模块的搭建:Vue CLI5 + Vue3 + Ant Design Vue3
完成手机号登录/注册功能
收获
学会纯前端项目的搭建
理解前后端分离架构
本地环境准备
vue cli安装: https://cli.vuejs.org/zh/guide/installation.html
流程: 安装node得到npm,使用npm安装vue cli(脚手架),使用vue cli创建项目
Vue CLl版本和Node版本有关,用Node V12只能下载到Vue CLl V4.X,必须用Node V18才能下载到Vue CLIV5.X
IDEA支持配置多个版本的Node,类似配置多个JDK
使用淘宝镜像: npm config set registry https://registry.npm.taobao.org
查看当前镜像使用的地址 npm config get registry
在IDEA中配置Node.js
创建基于Vue CLI的web模块
可以使用下列任一命令安装这个新的包:
npm install -g @vue/cli # OR yarn global add @vue/cli
还可以用这个命令来检查其版本是否正确
vue --version
得到最新版本5.0.8
创建项目:vue create web,对应的vue版本为3.2.13
创建后执行
cd web npm run serve
成功
指定端口
web模块集成Ant Design Vue
https://www.antdv.com/docs/vue/getting-started-cn
Ant Design Vue是阿里团队开源的基于Vue的UI组件
UI组件有很多可选,一种是选择基于CSS的,如:Bootstrap,适合各种前端框架。一种是选择基于Vue的UI组件,只能用于Vue框架。
Element(由饿了么团队开源)在Vue2时是最热门框架,Vue3出来后,没有第一时间跟着升级,后来才出了基于Vue3的Element Plus。
cd web npm i --save ant-design-vue 得到:"ant-design-vue": "^3.2.17"
package.json,类似maven的pom.xml,用于引入依赖package-lock.json,用于锁定小版本号
- 锁定当前依赖的版本
- 锁定当前依赖的第三方依赖的版本
main.js全局注册
import Antd from 'ant-design-vue'; createApp(App).use(Antd).use(store).use(router).mount('#app')
安装图标
npm install --save @ant-design/icons-vue
全局使用图标
import * as Icons from '@ant-design/icons-vue'; const app = createApp(App); app.use(Antd).use(store).use(router).mount('#app') //全局使用图标 const icons = Icons; for (const i in icons) { app.component(i, icons[i]); }
短信验证码登录流程
注册登录二合一界面开发
router\index.js采取懒加载方式加载login.vue
{ path: '/login', component: () => import('../views/login.vue') }
1 <template> 2 <a-form 3 :model="formState" 4 name="basic" 5 :label-col="{ span: 8 }" 6 :wrapper-col="{ span: 16 }" 7 autocomplete="off" 8 @finish="onFinish" 9 @finishFailed="onFinishFailed" 10 > 11 <a-form-item 12 label="Username" 13 name="username" 14 :rules="[{ required: true, message: 'Please input your username!' }]" 15 > 16 <a-input v-model:value="formState.username" /> 17 </a-form-item> 18 19 <a-form-item 20 label="Password" 21 name="password" 22 :rules="[{ required: true, message: 'Please input your password!' }]" 23 > 24 <a-input-password v-model:value="formState.password" /> 25 </a-form-item> 26 27 <a-form-item name="remember" :wrapper-col="{ offset: 8, span: 16 }"> 28 <a-checkbox v-model:checked="formState.remember">Remember me</a-checkbox> 29 </a-form-item> 30 31 <a-form-item :wrapper-col="{ offset: 8, span: 16 }"> 32 <a-button type="primary" html-type="submit">Submit</a-button> 33 </a-form-item> 34 </a-form> 35 </template> 36 37 <script> 38 import { defineComponent, reactive } from 'vue'; 39 //定义组件 40 export default defineComponent({ 41 setup() { 42 //声明响应式变量:reactive,ref,会跟form中的formState绑定起来 43 const formState = reactive({ 44 username: '', 45 password: '', 46 remember: true, 47 }); 48 const onFinish = values => { 49 console.log('Success:', values); 50 }; 51 const onFinishFailed = errorInfo => { 52 console.log('Failed:', errorInfo); 53 }; 54 return { 55 formState, 56 onFinish, 57 onFinishFailed, 58 }; 59 }, 60 }); 61 </script> 62 <style> 63 64 </style>
app.vue仅保留
<template>
<router-view/>
</template>
更改login.vue,需要用到Grid栅格组件
1 <template> 2 <a-row class="login"> 3 <a-col :span="8" :offset="8" class="login-main"> 4 <h1 style="text-align: center"><car-two-tone /> 模拟12306售票系统</h1> 5 <a-form 6 :model="loginForm" 7 name="basic" 8 autocomplete="off" 9 @finish="onFinish" 10 @finishFailed="onFinishFailed" 11 > 12 <a-form-item 13 label="" 14 name="mobile" 15 :rules="[{ required: true, message: '请输入手机号!' }]" 16 > 17 <a-input v-model:value="loginForm.mobile" placeholder="手机号"/> 18 </a-form-item> 19 20 <a-form-item 21 label="" 22 name="code" 23 :rules="[{ required: true, message: '请输入验证码!' }]" 24 > 25 <a-input v-model:value="loginForm.code"> 26 <template #addonAfter> 27 <a @click="sendCode">获取验证码</a> 28 </template> 29 </a-input> 30 <!--<a-input v-model:value="loginForm.code" placeholder="验证码"/>--> 31 </a-form-item> 32 33 <a-form-item> 34 <a-button type="primary" block html-type="submit">登录</a-button> 35 </a-form-item> 36 37 </a-form> 38 </a-col> 39 </a-row> 40 </template> 41 42 <script> 43 import { defineComponent, reactive } from 'vue'; 44 export default defineComponent({ 45 name: "login-view", 46 setup() { 47 const loginForm = reactive({ 48 mobile: '18888888888', 49 code: '', 50 }); 51 const onFinish = values => { 52 console.log('Success:', values); 53 }; 54 const onFinishFailed = errorInfo => { 55 console.log('Failed:', errorInfo); 56 }; 57 return { 58 loginForm, 59 onFinish, 60 onFinishFailed, 61 }; 62 }, 63 }); 64 </script> 65 66 <style> 67 .login-main h1 { 68 font-size: 25px; 69 font-weight: bold; 70 } 71 .login-main { 72 margin-top: 100px; 73 padding: 30px 30px 20px; 74 border: 2px solid grey; 75 border-radius: 10px; 76 background-color: #fcfcfc; 77 } 78 </style>
web页面
后端增加发送短信验证码接口
创建MemberSendCodeReq.java
1 package com.zihans.train.member.req; 2 3 import jakarta.validation.constraints.NotBlank; 4 import jakarta.validation.constraints.Pattern; 5 6 public class MemberSendCodeReq { 7 8 @NotBlank(message = "【手机号】不能为空") 9 @Pattern(regexp = "^1\\d{10}$", message = "手机号码格式错误") 10 private String mobile; 11 12 public String getMobile() { 13 return mobile; 14 } 15 16 public void setMobile(String mobile) { 17 this.mobile = mobile; 18 } 19 20 @Override 21 public String toString() { 22 return "MemberSendCodeReq{" + 23 "mobile='" + mobile + '\'' + 24 '}'; 25 } 26 }
MemberService.java新增
1 private static final Logger LOG = LoggerFactory.getLogger(MemberService.class); 2 3 /** 4 * 发送验证码 5 */ 6 public void sendCode(MemberSendCodeReq req) { 7 String mobile = req.getMobile(); 8 MemberExample memberExample = new MemberExample(); 9 memberExample.createCriteria().andMobileEqualTo(mobile); 10 List<Member> list = memberMapper.selectByExample(memberExample); 11 12 //如果手机号不存在,插入一条记录 13 if (CollUtil.isEmpty(list)) { 14 LOG.info("手机号码不存在,插入一条记录"); 15 //return list.get(0).getId(); 16 Member member = new Member(); 17 member.setId(SnowUtil.getSnowflakeNextId()); 18 member.setMobile(mobile); 19 20 memberMapper.insert(member); 21 } else { 22 LOG.info("手机号码存在,不插入记录"); 23 } 24 25 //生成验证码 26 String code = "8888"; 27 // String code = RandomUtil.randomString(6); 28 LOG.info("生成短信验证码:{}", code); 29 30 //保存短信记录表:手机号,短信验证码,有效期,是否已使用,业务类型,发送时间,使用时间 31 LOG.info("保存短信记录表"); 32 33 //对接短信通道,发送短信 34 LOG.info("对接短信通道"); 35 }
MemberController.java新增
1 @PostMapping("/send-code") 2 public CommonResp<Long> sendCode(@Valid MemberSendCodeReq req) { 3 memberService.sendCode(req); 4 5 return new CommonResp<>(); 6 }
测试
POST http://localhost:8000/member/member/send-code Content-Type: application/x-www-form-urlencoded mobile=13812345678
增加短信验证码登录接口
异常枚举类中添加
+ MEMBER_MOBILE_EXIST("手机号已注册"), + MEMBER_MOBILE_NOT_EXIST("请先获取短信验证码"), + MEMBER_MOBILE_CODE_ERROR("短信验证码错误");
创建MemberLoginResp.java
1 package com.zihans.train.member.req; 2 3 import jakarta.validation.constraints.NotBlank; 4 import jakarta.validation.constraints.Pattern; 5 6 public class MemberLoginReq { 7 8 @NotBlank(message = "【手机号】不能为空") 9 @Pattern(regexp = "^1\\d{10}$", message = "手机号码格式错误") 10 private String mobile; 11 12 @NotBlank(message = "【手机号】不能为空") 13 private String code; 14 15 public String getMobile() { 16 return mobile; 17 } 18 19 public void setMobile(String mobile) { 20 this.mobile = mobile; 21 } 22 23 public String getCode() { 24 return code; 25 } 26 27 public void setCode(String code) { 28 this.code = code; 29 } 30 31 @Override 32 public String toString() { 33 final StringBuffer sb = new StringBuffer("MemberLoginReq{"); 34 sb.append("mobile='").append(mobile).append('\''); 35 sb.append(", code='").append(code).append('\''); 36 sb.append('}'); 37 return sb.toString(); 38 } 39 }
修改MemberService.java
1 package com.zihans.train.member.service; 2 3 4 import cn.hutool.core.bean.BeanUtil; 5 import cn.hutool.core.collection.CollUtil; 6 import cn.hutool.core.util.ObjectUtil; 7 import com.zihans.train.common.exception.BusinessException; 8 import com.zihans.train.common.exception.BusinessExceptionEnum; 9 import com.zihans.train.common.util.SnowUtil; 10 import com.zihans.train.member.domain.Member; 11 import com.zihans.train.member.domain.MemberExample; 12 import com.zihans.train.member.mapper.MemberMapper; 13 import com.zihans.train.member.req.MemberLoginReq; 14 import com.zihans.train.member.req.MemberRegisterReq; 15 import com.zihans.train.member.req.MemberSendCodeReq; 16 import com.zihans.train.member.resp.MemberLoginResp; 17 import jakarta.annotation.Resource; 18 import org.slf4j.Logger; 19 import org.slf4j.LoggerFactory; 20 import org.springframework.stereotype.Service; 21 22 import java.util.List; 23 24 @Service 25 public class MemberService { 26 27 private static final Logger LOG = LoggerFactory.getLogger(MemberService.class); 28 @Resource 29 private MemberMapper memberMapper; 30 31 public int count() { 32 return Math.toIntExact(memberMapper.countByExample(null)); 33 } 34 35 /** 36 * 注册 37 */ 38 public long register(MemberRegisterReq req) { 39 String mobile = req.getMobile(); 40 Member memberDB = selectByMobile(mobile); 41 42 if (ObjectUtil.isNull(memberDB)) { 43 //return list.get(0).getId(); 44 throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_EXIST); 45 } 46 47 Member member = new Member(); 48 member.setId(SnowUtil.getSnowflakeNextId()); 49 member.setMobile(mobile); 50 51 memberMapper.insert(member); 52 return member.getId(); 53 54 } 55 56 /** 57 * 发送验证码 58 */ 59 public void sendCode(MemberSendCodeReq req) { 60 String mobile = req.getMobile(); 61 Member memberDB = selectByMobile(mobile); 62 63 //如果手机号不存在,插入一条记录 64 if (ObjectUtil.isNull(memberDB)) { 65 LOG.info("手机号码不存在,插入一条记录"); 66 //return list.get(0).getId(); 67 Member member = new Member(); 68 member.setId(SnowUtil.getSnowflakeNextId()); 69 member.setMobile(mobile); 70 71 memberMapper.insert(member); 72 } else { 73 LOG.info("手机号码存在,不插入记录"); 74 } 75 76 //生成验证码 77 String code = "8888"; 78 // String code = RandomUtil.randomString(6); 79 LOG.info("生成短信验证码:{}", code); 80 81 //保存短信记录表:手机号,短信验证码,有效期,是否已使用,业务类型,发送时间,使用时间 82 LOG.info("保存短信记录表"); 83 84 //对接短信通道,发送短信 85 LOG.info("对接短信通道"); 86 } 87 88 /** 89 * 验证码登录 90 */ 91 public MemberLoginResp login(MemberLoginReq req) { 92 String mobile = req.getMobile(); 93 String code = req.getCode(); 94 Member memberDB = selectByMobile(mobile); 95 96 //如果手机号不存在,插入一条记录 97 if (ObjectUtil.isNull(memberDB)) { 98 throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_NOT_EXIST); 99 } 100 101 //校验短信验证码 102 if ("8888".equals(code)) { 103 throw new BusinessException(BusinessExceptionEnum.MEMBER_MOBILE_CODE_ERROR); 104 } 105 106 // MemberLoginResp memberLoginResp = new MemberLoginResp(); 107 // memberLoginResp.setId(); 108 // memberLoginResp.setMobile(); 109 return BeanUtil.copyProperties(memberDB, MemberLoginResp.class); 110 } 111 112 private Member selectByMobile(String mobile) { 113 MemberExample memberExample = new MemberExample(); 114 memberExample.createCriteria().andMobileEqualTo(mobile); 115 List<Member> list = memberMapper.selectByExample(memberExample); 116 117 //如果手机号不存在,插入一条记录 118 if (CollUtil.isEmpty(list)) { 119 return null; 120 } else { 121 return list.get(0); 122 123 } 124 125 } 126 }
封装一个response类,防止把隐私信息返回
1 package com.zihans.train.member.resp; 2 3 public class MemberLoginResp { 4 private Long id; 5 6 private String mobile; 7 8 public Long getId() { 9 return id; 10 } 11 12 public void setId(Long id) { 13 this.id = id; 14 } 15 16 public String getMobile() { 17 return mobile; 18 } 19 20 public void setMobile(String mobile) { 21 this.mobile = mobile; 22 } 23 24 @Override 25 public String toString() { 26 StringBuilder sb = new StringBuilder(); 27 sb.append(getClass().getSimpleName()); 28 sb.append(" ["); 29 sb.append("Hash = ").append(hashCode()); 30 sb.append(", id=").append(id); 31 sb.append(", mobile=").append(mobile); 32 sb.append("]"); 33 return sb.toString(); 34 } 35 }
MemberController.java中新增login方法
@PostMapping("/login") public CommonResp<MemberLoginResp> login(@Valid MemberLoginReq req) { MemberLoginResp resp = memberService.login(req); return new CommonResp<>(resp); }
测试
POST http://localhost:8000/member/member/login Content-Type: application/x-www-form-urlencoded mobile=18888888888&code=8888
集成Axios完成登录功能
后端完成了相应的接口,前端来调用接口。
安装axios、
npm install axios
解决
gateway模块修改配置
spring:
cloud:
gateway:
# 配置路由转发,将/member/交给gateway管理
routes:
- id: member
uri: http://127.0.0.1:8001
predicates:
- Path=/member/**
globalcors:
cors-configurations:
'[/**]':
# 是否允许携带cookie
allowCredentials: true
# 允许携带的头信息
allowedHeaders: '*'
# 允许的请求方式
allowedMethods: '*'
# 允许请求来源(老版本叫allowedOrigin)
allowedOriginPatterns: '*'
# 跨域检测的有效期,会发起一个OPTION请求
maxAge: 3600
MemberController修改
@PostMapping("/send-code") public CommonResp<Long> sendCode(@Valid @RequestBody MemberSendCodeReq req) { memberService.sendCode(req); return new CommonResp<>(); }
@PostMapping("/login")
public CommonResp<MemberLoginResp> login(@Valid @RequestBody MemberLoginReq req) {
MemberLoginResp resp = memberService.login(req);
return new CommonResp<>(resp);
}
前端页面
1 <template> 2 <a-row class="login"> 3 <a-col :span="8" :offset="8" class="login-main"> 4 <h1 style="text-align: center"><car-two-tone /> 模拟12306售票系统</h1> 5 <a-form 6 :model="loginForm" 7 name="basic" 8 autocomplete="off" 9 > 10 <a-form-item 11 label="" 12 name="mobile" 13 :rules="[{ required: true, message: '请输入手机号!' }]" 14 > 15 <a-input v-model:value="loginForm.mobile" placeholder="手机号"/> 16 </a-form-item> 17 18 <a-form-item 19 label="" 20 name="code" 21 :rules="[{ required: true, message: '请输入验证码!' }]" 22 > 23 <a-input v-model:value="loginForm.code"> 24 <template #addonAfter> 25 <a @click="sendCode">获取验证码</a> 26 </template> 27 </a-input> 28 <!--<a-input v-model:value="loginForm.code" placeholder="验证码"/>--> 29 </a-form-item> 30 31 <a-form-item> 32 <a-button type="primary" block @click="login">登录</a-button> 33 </a-form-item> 34 35 </a-form> 36 </a-col> 37 </a-row> 38 </template> 39 40 <script> 41 import { defineComponent, reactive } from 'vue'; 42 import axios from 'axios'; 43 import { notification } from 'ant-design-vue'; 44 45 export default defineComponent({ 46 name: "login-view", 47 setup() { 48 const loginForm = reactive({ 49 mobile: '13000000000', 50 code: '', 51 }); 52 53 const sendCode = () => { 54 axios.post("http://localhost:8000/member/member/send-code", { 55 mobile: loginForm.mobile 56 }).then(response => { 57 console.log(response); 58 let data = response.data; 59 if (data.success) { 60 notification.success({ description: '发送验证码成功!' }); 61 loginForm.code = "8888"; 62 } else { 63 notification.error({ description: data.message }); 64 } 65 }); 66 }; 67 68 const login = () => { 69 axios.post("http://localhost:8000/member/member/login", loginForm).then((response) => { 70 let data = response.data; 71 if (data.success) { 72 notification.success({ description: '登录成功!' }); 73 console.log("登录成功:", data.content); 74 } else { 75 notification.error({ description: data.message }); 76 } 77 }) 78 }; 79 80 return { 81 loginForm, 82 sendCode, 83 login 84 }; 85 }, 86 }); 87 </script> 88 89 <style> 90 .login-main h1 { 91 font-size: 25px; 92 font-weight: bold; 93 } 94 .login-main { 95 margin-top: 100px; 96 padding: 30px 30px 20px; 97 border: 2px solid grey; 98 border-radius: 10px; 99 background-color: #fcfcfc; 100 } 101 </style>
增加Axios拦截器配置
可通过axios拦截器打印请求日志和返回结果,也可以加入统—参数,比如单点登录token,也可以统—处理某个错误返回码
修改main.js
1 import { createApp } from 'vue' 2 import App from './App.vue' 3 import router from './router' 4 import store from './store' 5 import Antd from 'ant-design-vue'; 6 import 'ant-design-vue/dist/antd.css'; 7 import * as Icons from '@ant-design/icons-vue'; 8 import axios from "axios"; 9 10 const app = createApp(App); 11 app.use(Antd).use(store).use(router).mount('#app') 12 13 //全局使用图标 14 const icons = Icons; 15 for (const i in icons) { 16 app.component(i, icons[i]); 17 } 18 19 /** 20 * axios拦截器 21 */ 22 axios.interceptors.request.use(function (config) { 23 console.log('请求参数:', config); 24 return config; 25 }, error => { 26 return Promise.reject(error); 27 }); 28 axios.interceptors.response.use(function (response) { 29 console.log('返回结果:', response); 30 return response; 31 }, error => { 32 console.log('返回错误:', error); 33 return Promise.reject(error); 34 });
Vue CLI多环境配置
在web根目录下增加文件.env.xxx,Xxx表是不同的环境
启动命令里增加--mode xxx,就启动xxx环境的配置增加多环境变量:
NODE_ENV是内置变量
自定义变量用"VUE_APP_"开头
使用变量:
process.env.XXX
1 NODE_ENV=development 2 VUE_APP_SERVER=http://localhost:8000
1 NODE_ENV=development 2 VUE_APP_SERVER=http://train.zihans.com
main.js中增加日志
axios.defaults.baseURL = process.env.VUE_APP_SERVER;
console.log('环境:', process.env.NODE_ENV); console.log('服务端', process.env.VUE_APP_SERVER);
并没有读出服务端信息,此时要更改配置来读取dev
"serve-dev": "vue-cli-service serve --mode dev --port 9000", "serve-prod": "vue-cli-service serve --mode prod --port 9000",
增加web控台主页
新建main.view
1 <template> 2 <a-layout id="components-layout-demo-top-side-2"> 3 <a-layout-header class="header"> 4 <div class="logo" /> 5 <a-menu 6 v-model:selectedKeys="selectedKeys1" 7 theme="dark" 8 mode="horizontal" 9 :style="{ lineHeight: '64px' }" 10 > 11 <a-menu-item key="1">nav 1</a-menu-item> 12 <a-menu-item key="2">nav 2</a-menu-item> 13 <a-menu-item key="3">nav 3</a-menu-item> 14 </a-menu> 15 </a-layout-header> 16 <a-layout> 17 <a-layout-sider width="200" style="background: #fff"> 18 <a-menu 19 v-model:selectedKeys="selectedKeys2" 20 v-model:openKeys="openKeys" 21 mode="inline" 22 :style="{ height: '100%', borderRight: 0 }" 23 > 24 <a-sub-menu key="sub1"> 25 <template #title> 26 <span> 27 <user-outlined /> 28 subnav 1 29 </span> 30 </template> 31 <a-menu-item key="1">option1</a-menu-item> 32 <a-menu-item key="2">option2</a-menu-item> 33 <a-menu-item key="3">option3</a-menu-item> 34 <a-menu-item key="4">option4</a-menu-item> 35 </a-sub-menu> 36 <a-sub-menu key="sub2"> 37 <template #title> 38 <span> 39 <laptop-outlined /> 40 subnav 2 41 </span> 42 </template> 43 <a-menu-item key="5">option5</a-menu-item> 44 <a-menu-item key="6">option6</a-menu-item> 45 <a-menu-item key="7">option7</a-menu-item> 46 <a-menu-item key="8">option8</a-menu-item> 47 </a-sub-menu> 48 <a-sub-menu key="sub3"> 49 <template #title> 50 <span> 51 <notification-outlined /> 52 subnav 3 53 </span> 54 </template> 55 <a-menu-item key="9">option9</a-menu-item> 56 <a-menu-item key="10">option10</a-menu-item> 57 <a-menu-item key="11">option11</a-menu-item> 58 <a-menu-item key="12">option12</a-menu-item> 59 </a-sub-menu> 60 </a-menu> 61 </a-layout-sider> 62 <a-layout style="padding: 0 24px 24px"> 63 <a-breadcrumb style="margin: 16px 0"> 64 <a-breadcrumb-item>Home</a-breadcrumb-item> 65 <a-breadcrumb-item>List</a-breadcrumb-item> 66 <a-breadcrumb-item>App</a-breadcrumb-item> 67 </a-breadcrumb> 68 <a-layout-content 69 :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }" 70 > 71 Content 72 </a-layout-content> 73 </a-layout> 74 </a-layout> 75 </a-layout> 76 </template> 77 <script> 78 import { UserOutlined, LaptopOutlined, NotificationOutlined } from '@ant-design/icons-vue'; 79 import { defineComponent, ref } from 'vue'; 80 export default defineComponent({ 81 components: { 82 UserOutlined, 83 LaptopOutlined, 84 NotificationOutlined, 85 }, 86 setup() { 87 return { 88 selectedKeys1: ref(['2']), 89 selectedKeys2: ref(['1']), 90 collapsed: ref(false), 91 openKeys: ref(['sub1']), 92 }; 93 }, 94 }); 95 </script> 96 <style> 97 #components-layout-demo-top-side-2 .logo { 98 float: left; 99 width: 120px; 100 height: 31px; 101 margin: 16px 24px 16px 0; 102 background: rgba(255, 255, 255, 0.3); 103 } 104 105 .ant-row-rtl #components-layout-demo-top-side-2 .logo { 106 float: right; 107 margin: 16px 0 16px 24px; 108 } 109 110 .site-layout-background { 111 background: #fff; 112 } 113 </style>
修改index.js
import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', component: () => import('../views/login.vue') }, { path: '/', component: () => import('../views/main.vue') } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router
增加页面跳转
1 <template> 2 <a-row class="login"> 3 <a-col :span="8" :offset="8" class="login-main"> 4 <h1 style="text-align: center"><car-two-tone /> 模拟12306售票系统</h1> 5 <a-form 6 :model="loginForm" 7 name="basic" 8 autocomplete="off" 9 > 10 <a-form-item 11 label="" 12 name="mobile" 13 :rules="[{ required: true, message: '请输入手机号!' }]" 14 > 15 <a-input v-model:value="loginForm.mobile" placeholder="手机号"/> 16 </a-form-item> 17 18 <a-form-item 19 label="" 20 name="code" 21 :rules="[{ required: true, message: '请输入验证码!' }]" 22 > 23 <a-input v-model:value="loginForm.code"> 24 <template #addonAfter> 25 <a @click="sendCode">获取验证码</a> 26 </template> 27 </a-input> 28 <!--<a-input v-model:value="loginForm.code" placeholder="验证码"/>--> 29 </a-form-item> 30 31 <a-form-item> 32 <a-button type="primary" block @click="login">登录</a-button> 33 </a-form-item> 34 35 </a-form> 36 </a-col> 37 </a-row> 38 </template> 39 40 <script> 41 import { defineComponent, reactive } from 'vue'; 42 import axios from 'axios'; 43 import { notification } from 'ant-design-vue'; 44 import {useRouter} from 'vue-router' 45 46 export default defineComponent({ 47 name: "login-view", 48 setup() { 49 const router = useRouter(); 50 const loginForm = reactive({ 51 mobile: '13000000000', 52 code: '', 53 }); 54 55 const sendCode = () => { 56 axios.post("/member/member/send-code", { 57 mobile: loginForm.mobile 58 }).then(response => { 59 let data = response.data; 60 if (data.success) { 61 notification.success({ description: '发送验证码成功!' }); 62 loginForm.code = "8888"; 63 } else { 64 notification.error({ description: data.message }); 65 } 66 }); 67 }; 68 69 const login = () => { 70 axios.post("/member/member/login", loginForm).then((response) => { 71 let data = response.data; 72 if (data.success) { 73 notification.success({ description: '登录成功!' }); 74 //登陆成功,跳到控台主页 75 router.push("/"); 76 } else { 77 notification.error({ description: data.message }); 78 } 79 }) 80 }; 81 82 return { 83 loginForm, 84 sendCode, 85 login 86 }; 87 }, 88 }); 89 </script> 90 91 <style> 92 .login-main h1 { 93 font-size: 25px; 94 font-weight: bold; 95 } 96 .login-main { 97 margin-top: 100px; 98 padding: 30px 30px 20px; 99 border: 2px solid grey; 100 border-radius: 10px; 101 background-color: #fcfcfc; 102 } 103 </style>
制作Vue3公共组件
增加the-header组件
1 <template> 2 <a-layout-header class="header"> 3 <div class="logo" /> 4 <a-menu 5 v-model:selectedKeys="selectedKeys1" 6 theme="dark" 7 mode="horizontal" 8 :style="{ lineHeight: '64px' }" 9 > 10 <a-menu-item key="1">nav 11</a-menu-item> 11 <a-menu-item key="2">nav 2</a-menu-item> 12 <a-menu-item key="3">nav 3</a-menu-item> 13 </a-menu> 14 </a-layout-header> 15 </template> 16 17 <script> 18 import {defineComponent, ref} from 'vue'; 19 20 export default defineComponent({ 21 name: "the-header-view", 22 setup() { 23 24 return { 25 selectedKeys1: ref(['2']), 26 }; 27 }, 28 }); 29 </script> 30 31 <!-- Add "scoped" attribute to limit CSS to this component only --> 32 <style scoped> 33 34 </style>
增加the-side组件
1 <template> 2 <a-layout-sider width="200" style="background: #fff"> 3 <a-menu 4 v-model:selectedKeys="selectedKeys2" 5 v-model:openKeys="openKeys" 6 mode="inline" 7 :style="{ height: '100%', borderRight: 0 }" 8 > 9 <a-sub-menu key="sub1"> 10 <template #title> 11 <span> 12 <user-outlined /> 13 subnav 11 14 </span> 15 </template> 16 <a-menu-item key="1">option1</a-menu-item> 17 <a-menu-item key="2">option2</a-menu-item> 18 <a-menu-item key="3">option3</a-menu-item> 19 <a-menu-item key="4">option4</a-menu-item> 20 </a-sub-menu> 21 <a-sub-menu key="sub2"> 22 <template #title> 23 <span> 24 <laptop-outlined /> 25 subnav 2 26 </span> 27 </template> 28 <a-menu-item key="5">option5</a-menu-item> 29 <a-menu-item key="6">option6</a-menu-item> 30 <a-menu-item key="7">option7</a-menu-item> 31 <a-menu-item key="8">option8</a-menu-item> 32 </a-sub-menu> 33 <a-sub-menu key="sub3"> 34 <template #title> 35 <span> 36 <notification-outlined /> 37 subnav 3 38 </span> 39 </template> 40 <a-menu-item key="9">option9</a-menu-item> 41 <a-menu-item key="10">option10</a-menu-item> 42 <a-menu-item key="11">option11</a-menu-item> 43 <a-menu-item key="12">option12</a-menu-item> 44 </a-sub-menu> 45 </a-menu> 46 </a-layout-sider> 47 </template> 48 49 <script> 50 import {defineComponent, ref} from 'vue'; 51 52 export default defineComponent({ 53 name: "the-sider-view", 54 setup() { 55 56 return { 57 selectedKeys2: ref(['1']), 58 openKeys: ref(['sub1']), 59 }; 60 }, 61 }); 62 </script> 63 64 <!-- Add "scoped" attribute to limit CSS to this component only --> 65 <style scoped> 66 67 </style>
修改main.vue
1 <template> 2 <a-layout id="components-layout-demo-top-side-2"> 3 <the-header-view></the-header-view> 4 <a-layout> 5 <the-sider-view></the-sider-view> 6 <a-layout style="padding: 0 24px 24px"> 7 <a-breadcrumb style="margin: 16px 0"> 8 <a-breadcrumb-item>Home</a-breadcrumb-item> 9 <a-breadcrumb-item>List</a-breadcrumb-item> 10 <a-breadcrumb-item>App</a-breadcrumb-item> 11 </a-breadcrumb> 12 <a-layout-content 13 :style="{ background: '#fff', padding: '24px', margin: 0, minHeight: '280px' }" 14 > 15 Content 16 </a-layout-content> 17 </a-layout> 18 </a-layout> 19 </a-layout> 20 </template> 21 <script> 22 import { defineComponent } from 'vue'; 23 import TheHeaderView from "@/components/the-header"; 24 import TheSiderView from "@/components/the-sider"; 25 export default defineComponent({ 26 components: { 27 TheSiderView, 28 TheHeaderView, 29 }, 30 setup() { 31 return { 32 }; 33 }, 34 }); 35 </script> 36 <style> 37 #components-layout-demo-top-side-2 .logo { 38 float: left; 39 width: 120px; 40 height: 31px; 41 margin: 16px 24px 16px 0; 42 background: rgba(255, 255, 255, 0.3); 43 } 44 45 .ant-row-rtl #components-layout-demo-top-side-2 .logo { 46 float: right; 47 margin: 16px 0 16px 24px; 48 } 49 50 .site-layout-background { 51 background: #fff; 52 } 53 </style>