二、使用Vue3 + Vue CLI 实现系统前端模块的搭建

发布时间 2023-04-17 09:55:14作者: 夏雪冬蝉

主要内容

前端模块的搭建: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]);
}
main.js

短信验证码登录流程

 

 

注册登录二合一界面开发

 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>
login.vue

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 />&nbsp;模拟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>
login.vue

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 }
MemberSendCodeReq.java

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     }
MemberService.java

MemberController.java新增

1 @PostMapping("/send-code")
2     public CommonResp<Long> sendCode(@Valid MemberSendCodeReq req) {
3         memberService.sendCode(req);
4 
5         return new CommonResp<>();
6     }
MemberController.java

测试

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 }
MemberLoginResp.java

修改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 }
MemberService.java

封装一个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 }
MemberLoginResp.java

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 />&nbsp;模拟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>
login.vue

增加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 });
main.js

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
.env.dev
1 NODE_ENV=development
2 VUE_APP_SERVER=http://train.zihans.com
.env.prod

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>
main.vue

修改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
index.js

增加页面跳转

  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 />&nbsp;模拟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>
login.vue

制作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-header.vue

增加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>
the-side.vue

修改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>
main.vue