Vue3 & Ant Design Vue3基础
nodejs版本要求:node-v18.16.0-x64
nodejs基础配置
npm -v
node -v
npm config set prefix "D:\software\nodejs\node_global"
npm config set cache "D:\software\nodejs\node_cache"
npm config get registry
npm config set registry https://registry.npm.taobao.org
安装Vue3
npm install @vue/cli -g
vue --version
#npm install @vue/cli@5.0.8 -g 安装指定版本
#npm uninstall @vue/cli -g
使用Vue创建前端项目
npm create vue@latest
√ Project name: ...web
√ Add TypeScript? ... No
√ Add JSX Support? ... No
√ Add Vue Router for Single Page Application development? ... Yes
√ Add Pinia for state management? ... Yes
√ Add Vitest for Unit Testing? ... No
√ Add an End-to-End Testing Solution? >> No
√ Add ESLint for code quality? ... Yes
√ Add Prettier for code formatting? ... Yes
启动前端项目
cd web
npm install
npm run dev
浏览器访问:http://localhost:5173
修改端口号,修改配置 vite.config.js
export default defineConfig({
server: {
port: 9000
},
plugins: [
vue(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
再次访问:http://localhost:9000/
https://antdv.com/components/overview-cn
Ant Design Vue官方文档
安装Ant Design Vue
npm install ant-design-vue --save
npm install --save @ant-design/icons-vue
#自动按需引入组件
npm install unplugin-vue-components -D
修改配置文件vite.config.js
import {fileURLToPath, URL} from 'node:url'
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite';
import {AntDesignVueResolver} from 'unplugin-vue-components/resolvers';
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 9000
},
plugins: [
vue(),
Components({
resolvers: [
AntDesignVueResolver({
importStyle: false, // css in js
}),
],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
修改main.js
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue';
import App from './App.vue'
import router from './router'
import 'ant-design-vue/dist/reset.css';
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.use(Antd)
app.mount('#app')
添加一个测试页面
<script setup>
import {ZoomOutOutlined} from "@ant-design/icons-vue";
</script>
<template>
<div class="about">
<h1>This is an about page</h1>
<a-button>测试</a-button><br>
<ZoomOutOutlined />
</div>
</template>
<style>
</style>
Antd栅格把页面平均分成24份
<template>
<a-row>
<a-col :span="24">col</a-col>
</a-row>
<a-row>
<a-col :span="12">col-12</a-col>
<a-col :span="12">col-12</a-col>
</a-row>
<a-row>
<a-col :span="8">col-8</a-col>
<a-col :span="8">col-8</a-col>
<a-col :span="8">col-8</a-col>
</a-row>
<a-row>
<a-col :span="6">col-6</a-col>
<a-col :span="6">col-6</a-col>
<a-col :span="6">col-6</a-col>
<a-col :span="6">col-6</a-col>
</a-row>
</template>
使用Pinia管理用户状态
刷新页面,Pinia中的数据会丢失,使用Pinia插件做数据持久化
npm install --save zipson
npm install --save pinia-plugin-persistedstate
修改main.js
import './assets/main.css'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import Antd from 'ant-design-vue';
import App from './App.vue'
import router from './router'
import 'ant-design-vue/dist/reset.css';
const app = createApp(App)
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate);//pinia数据持久化
app.use(pinia)
app.use(router)
app.use(Antd)
app.mount('#app')
使用Pinia保存用户状态,添加文件src/stores/user.js
import {reactive} from 'vue'
import {defineStore} from 'pinia'
import {stringify, parse} from 'zipson'
const MEMBER = "MEMBER"
export const useUserStore = defineStore('user', () => {
const userInfo = reactive({
id: '',
mobile: '',
token: ''
})
function setUserInfo({id, mobile, token}) {
userInfo.id = id
userInfo.mobile = mobile
userInfo.token = token
}
function clearUserInfo() {
userInfo.id = ''
userInfo.mobile = ''
userInfo.token = ''
}
return {userInfo, setUserInfo, clearUserInfo}
}, {
persist: {
key: MEMBER,
storage: sessionStorage,
// paths: ['count'],
serializer: {
deserialize: parse,
serialize: stringify
},
beforeRestore: (ctx) => {
console.log(`about to restore '${ctx.store.$id}'`)
},
afterRestore: (ctx) => {
console.log(`just restored '${ctx.store.$id}'`)
},
debug: true,
}
})
ite多环境配置
https://vitejs.cn/vite3-cn/guide/env-and-mode.html#env-variables
官方配置文档
在根目录创建文件 .env.development
NODE_ENV=development
#自定义变量需要以VITE_开头
VITE_APP_BASE_URL=http://localhost:8000
生产环境 .env.production
NODE_ENV=production
VITE_APP_BASE_URL=http://train.intmall.com
使用环境变量
axios.defaults.baseURL = import.meta.env.VITE_APP_BASE_URL;
console.log(process.env.NODE_ENV)
console.log(import.meta.env.VITE_APP_BASE_URL)
封装网络请求工具类Axios
npm install axios --save
封装网络请求工具类 src/utils/request.js
import axios from 'axios'
import {notification} from 'ant-design-vue';
import {useUserStore} from '@/stores/user';
import router from '@/router'
const {userInfo, clearUserInfo} = useUserStore()
export const serverUrl = import.meta.env.VITE_APP_BASE_URL
const service = axios.create({
baseURL: serverUrl,
timeout: 5000
})
// Add a request interceptor 全局请求拦截
service.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = userInfo.token
if (token) {
config.headers['token'] = token
}
// 此处还可以设置token
return config
},
function (error) {
// Do something with request error
return Promise.reject(error)
}
)
// Add a response interceptor 全局相应拦截
service.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
// 如果是固定的数据返回模式,此处可以做继续完整的封装
const resData = response.data || {}
if (resData.success) {
return resData
}
notification.error({description: resData.message});
return Promise.reject(resData.message)
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
// Do something with response error
// 此处需要对返回的状态码或者异常信息作统一处理
console.log('error', error)
const response = error.response;
const status = response.status;
if (status === 401) {
// 判断状态码是401 跳转到登录页
console.log("未登录或登录超时,跳到登录页");
clearUserInfo()
notification.error({description: "未登录或登录超时"});
router.push('/login')
}
return Promise.reject(error)
}
)
export const get = (url, params) => {
return service.get(url, {
params
})
}
export const post = (url, data) => service.post(url, data)
export const put = (url, data) => service.put(url, data)
export const del = (url, data) => service.delete(url)
遇到的问题:
useRouter失效,router无法跳转页面
https://blog.csdn.net/qq_57700056/article/details/133530562
后台接口调用示例: src/api/userApi.js
import { get, post, put, del } from "../utils/request";
// 用户登录
export async function login(data) {
return post('/member/member/login', data)
}
export async function sendCode(data) {
return post('/member/member/sendCode', data)
}
export async function getUserCount() {
return get('/member/member/count')
}
export async function savePassenger(data) {
return post('/member/passenger/save', data)
}
export async function queryPassengerList(data) {
return post('/member/passenger/queryList', data)
}
export async function deletePassenger(id) {
return del(`/member/passenger/delete/${id}`)
}
// 导出 userApi 方法
export default {
login,
sendCode,
getUserCount,
savePassenger,
deletePassenger,
queryPassengerList
}
前端页面路由配置
增加路由防卫,判断要跳转的页面是否需要登录
src/router/index.js
由于router挂载比pinia要早,守卫在在使用pinia时,pinia还没有挂载,把pinia写在守卫里面即可解决问题
import {createRouter, createWebHistory} from 'vue-router'
import {notification} from 'ant-design-vue';
import {useUserStore} from '@/stores/user';
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'main',
component: () => import('../views/MainView.vue'),
children: [
{
path: '/welcome',
name: 'welcome',
component: () => import('../views/main/WelcomeView.vue')
}, {
path: '/passenger',
name: 'passenger',
component: () => import('../views/main/PassengerView.vue')
}
]
},
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue'),
meta: {
noToken: true
}
}, {
path: '',
redirect: '/welcome'
}
]
})
// 路由登录拦截
router.beforeEach((to, from, next) => {
// 要不要对meta.noToken属性做监控拦截
if (to.matched.some(function (item) {
console.log(item, "是否不需要登录校验:", item.meta.noToken || false);
return !item.meta.noToken
})) {
const {userInfo} = useUserStore()
console.log("页面登录校验开始:", userInfo);
if (!userInfo.token) {
console.log("用户未登录或登录超时!");
notification.error({description: "未登录或登录超时"});
next('/login');
} else {
next();
}
} else {
next();
}
});
export default router
登录页面
<script setup>
import {reactive} from 'vue';
import {useRouter} from 'vue-router'
import {CodepenCircleOutlined} from "@ant-design/icons-vue";
import {notification} from 'ant-design-vue';
import userApi from '../api/userApi';
import { useUserStore } from '@/stores/user';
const { setUserInfo } = useUserStore()
const router = useRouter();
const loginForm = reactive({
mobile: '',
code: '',
});
const onFinish = async (value) => {
// 执行登录逻辑
const respData = await userApi.login(value);
const data = respData.data
setUserInfo(data)
console.log('Success:', value, data);
notification.success({ description: '登录成功!' });
router.push("/welcome");
};
const onFinishFailed = errorInfo => {
console.log('Failed:', errorInfo);
};
const sendCode = async () => {
await userApi.sendCode({
mobile: loginForm.mobile
})
notification.success({ description: '发送验证码成功!' });
loginForm.code = "8888";
}
</script>
<template>
<a-row class="login">
<a-col :span="8" :offset="8" class="login-main">
<h1 style="text-align: center">
<CodepenCircleOutlined/> 模拟12306售票系统
</h1>
<a-form
:model="loginForm"
name="basic"
autocomplete="off"
@finish="onFinish"
@finishFailed="onFinishFailed"
>
<a-form-item
label=""
name="mobile"
:rules="[{ required: true, message: '请输入手机号!' }]"
>
<a-input v-model:value="loginForm.mobile" placeholder="手机号"/>
</a-form-item>
<a-form-item
label=""
name="code"
:rules="[{ required: true, message: '请输入验证码!' }]"
>
<a-input v-model:value="loginForm.code">
<template #addonAfter>
<a @click="sendCode">获取验证码</a>
</template>
</a-input>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 8, span: 16 }">
<a-button type="primary" html-type="submit">登录</a-button>
</a-form-item>
</a-form>
</a-col>
</a-row>
</template>
<style scoped>
.login-main h1 {
font-size: 25px;
font-weight: bold;
}
.login-main {
margin-top: 100px;
padding: 30px 30px 20px;
border: 2px solid grey;
border-radius: 10px;
background-color: #fcfcfc;
}
</style>
退出登录
<script setup>
import {ref} from "vue";
import {useUserStore} from '@/stores/user';
const {userInfo, clearUserInfo} = useUserStore()
const selectedKeys1 = ref(['2']);
</script>
<template>
<a-layout-header class="header">
<div class="logo"/>
<div style="float: right; color: white;">
您好:{{userInfo.mobile}}
<router-link to="/login" @click.native="clearUserInfo()" style="color: white;">
退出登录
</router-link>
</div>
<a-menu
v-model:selectedKeys="selectedKeys1"
theme="dark"
mode="horizontal"
:style="{ lineHeight: '64px' }"
>
<a-menu-item key="1">nav 1</a-menu-item>
<a-menu-item key="2">nav 2</a-menu-item>
<a-menu-item key="3">nav 3abc</a-menu-item>
</a-menu>
</a-layout-header>
</template>
页面增删改查操作
<script setup>
import {ref, reactive} from 'vue';
import {notification} from "ant-design-vue";
import {cloneDeep} from 'lodash-es';
import userApi from '@/api/userApi';
import {PASSENGER_TYPE_ARRAY} from '@/assets/js/enums'
const visible = ref(false);
const loading = ref(false);
let passenger = ref({
id: undefined,
memberId: undefined,
name: undefined,
idCard: undefined,
type: undefined,
createTime: undefined,
updateTime: undefined,
});
const passengers = ref([]);
const pagination = reactive({
total: 0,
current: 1,
pageSize: 2
})
const columns = [
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '身份证',
dataIndex: 'idCard',
key: 'idCard',
},
{
title: '旅客类型',
dataIndex: 'type',
key: 'type',
},
{
title: '操作',
dataIndex: 'operation'
},
]
const handleQuery = (param) => {
if (!param) {
param = {
"page": 1,
"size": pagination.pageSize
}
}
loading.value = true;
userApi.queryPassengerList({
"page": param.page,
"limit": param.size
}).then(res => {
console.log('res', res)
loading.value = false;
passengers.value = res.data;
pagination.total = res.count;
pagination.current = res.page
})
}
const handleTableChange = (pagination) => {
handleQuery({
page: pagination.current,
size: pagination.pageSize
})
}
handleQuery()
const onAdd = () => {
passenger.value = {};
visible.value = true;
}
const onEdit = (record) => {
console.log("record", record)
passenger.value = cloneDeep(record);
visible.value = true;
}
const onDelete = (record) => {
console.log("delete record", record)
userApi.deletePassenger(record.id).then(() => {
notification.success({description: "删除成功!"});
handleQuery({
page: pagination.current,
size: pagination.pageSize,
});
})
}
const handleOk = () => {
userApi.savePassenger(passenger.value).then(resp => {
notification.success({description: "保存成功!"});
visible.value = false;
handleQuery({
page: pagination.current,
size: pagination.pageSize
})
})
}
</script>
<template>
<p>
<a-space>
<a-button type="primary" @click="handleQuery()">刷新</a-button>
<a-button type="primary" @click="onAdd">新增</a-button>
</a-space>
</p>
<a-table :dataSource="passengers" :columns="columns" :pagination="pagination" @change="handleTableChange"
:loading="loading">
<template #bodyCell="{ column, text, record }">
<template v-if="column.dataIndex === 'operation'">
<a-space>
<a @click="onEdit(record)">编辑</a>
<a-popconfirm
title="删除后不可恢复,确认删除?"
ok-text="确认" cancel-text="取消"
@confirm="onDelete(record)"
>
<a style="color: red">删除</a>
</a-popconfirm>
</a-space>
</template>
<template v-else-if="column.dataIndex === 'type'">
<span v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code">
<span v-if="item.code === record.type">
{{item.desc}}
</span>
</span>
</template>
</template>
</a-table>
<a-modal v-model:open="visible" title="乘车人" @ok="handleOk"
ok-text="确认" cancel-text="取消">
<a-form
:model="passenger" :label-col="{span: 4}" :wrapper-col="{ span: 20 }"
>
<a-form-item label="姓名">
<a-input v-model:value="passenger.name"/>
</a-form-item>
<a-form-item label="身份证">
<a-input v-model:value="passenger.idCard"/>
</a-form-item>
<a-form-item label="旅客类型">
<a-select v-model:value="passenger.type">
<a-select-option v-for="item in PASSENGER_TYPE_ARRAY" :key="item.code" :value="item.code">
{{ item.desc }}
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
</template>
前端跨域问题
前后端分离项目,前端在请求后台接口时会出现跨域问题
这个后端项目使用到了gateway,在配置文件中加入:
# 允许请求来源(老版本叫allowedOrigin)
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedOriginPatterns=*
# 允许携带的头信息
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedHeaders=*
# 允许的请求方式
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedMethods=*
# 是否允许携带cookie
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowCredentials=true
# 跨域检测的有效期,会发起一个OPTION请求
spring.cloud.gateway.globalcors.cors-configurations.[/**].maxAge=3600
后端分页查询方法
#CommonPageParam.java
@Data
public class CommonPageParam {
@NotNull(message = "页码不能为空")
private Integer page;
@NotNull(message = "每页数量不能为空")
@Max(value = 100, message = "分页条数不能超过100")
private Integer limit;
}
#PassengerQueryReq.java
@Data
public class PassengerQueryReq extends CommonPageParam {
private Long memberId;
}
#CommonPageResp.java
@Data
@NoArgsConstructor
public class CommonPageResp<T> {
/** 默认每页的条数 */
public static final int PAGE_SIZE_DEFAULT = 10;
/**
* 业务上的成功或失败
*/
private boolean success = true;
/**
* 返回信息
*/
private String message;
/**
* 返回泛型数据,自定义类型
*/
private List<T> data;
/**
* 总数
*/
private Long count;
/**
* 页码
*/
private Integer page;
/**
* 每页数量
*/
private Integer limit;
public Integer getPage() {
if (page == null || page < 1) {
return 1;
}
return page;
}
public Integer getLimit() {
if (limit == null) {
return PAGE_SIZE_DEFAULT;
}
return limit;
}
public static <T> CommonPageResp<T> SUCCESS(String message, List<T> data, PageInfo pageInfo) {
return new CommonPageResp<>(true, message, data, pageInfo.getTotal(), pageInfo.getPageNum(), pageInfo.getPageSize());
}
public CommonPageResp(boolean success, String message, List<T> data, Long count, Integer page, Integer limit) {
this.success = success;
this.message = message;
this.data = data;
this.count = count;
this.page = page;
this.limit = limit;
}
}
#PassengerService.java
@Service
@Slf4j
public class PassengerService {
@Resource
private PassengerMapper passengerMapper;
public CommonPageResp<PassengerQueryResp> queryList(PassengerQueryReq req) {
PassengerExample passengerExample = new PassengerExample();
passengerExample.setOrderByClause("id desc");
PassengerExample.Criteria criteria = passengerExample.createCriteria();
if (ObjectUtil.isNotNull(req.getMemberId())) {
criteria.andMemberIdEqualTo(req.getMemberId());
}
PageHelper.startPage(req.getPage(), req.getLimit());
List<Passenger> passengerList = passengerMapper.selectByExample(passengerExample);
PageInfo<Passenger> pageInfo = new PageInfo<>(passengerList);
List<PassengerQueryResp> list = BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
return CommonPageResp.SUCCESS("", list, pageInfo);
}
}
使用线程本地变量存储用户信息
拦截器从token中获取用户信息
@Slf4j
@Component
public class MemberInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("MemberInterceptor开始");
//获取header的token参数
String token = request.getHeader("token");
if (StrUtil.isNotBlank(token)) {
log.info("获取会员登录token:{}", token);
JSONObject loginMember = JwtUtil.getJSONObject(token);
log.info("当前登录会员:{}", loginMember);
MemberLoginResp member = JSONUtil.toBean(loginMember, MemberLoginResp.class);
LoginMemberContext.setMember(member);
}
log.info("MemberInterceptor结束");
return true;
}
}
配置开启拦截器
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
@Resource
private MemberInterceptor memberInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 路径不要包含context-path
registry.addInterceptor(memberInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
"/hello",
"/member/sendCode",
"/member/login"
);
}
}
设置用户到本地线程
import com.intmall.train.common.domain.resp.MemberLoginResp;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class LoginMemberContext {
private static ThreadLocal<MemberLoginResp> member = new ThreadLocal<>();
public static MemberLoginResp getMember() {
return member.get();
}
public static void setMember(MemberLoginResp member) {
LoginMemberContext.member.set(member);
}
public static Long getId() {
try {
return member.get().getId();
} catch (Exception e) {
log.error("获取登录会员信息异常", e);
throw e;
}
}
}
源码地址
完整代码参考: