Vue3 & Ant Design Vue3基础
nodejs版本要求:node-v18.16.0-x64
nodejs基础配置
npm -v
node -vnpm 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 = iduserInfo.mobile = mobileuserInfo.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_URLconst service = axios.create({baseURL: serverUrl,timeout: 5000
})// Add a request interceptor 全局请求拦截
service.interceptors.request.use(function (config) {// Do something before request is sentconst token = userInfo.tokenif (token) {config.headers['token'] = token}// 此处还可以设置tokenreturn config},function (error) {// Do something with request errorreturn 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.datasetUserInfo(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-itemlabel=""name="mobile":rules="[{ required: true, message: '请输入手机号!' }]"><a-input v-model:value="loginForm.mobile" placeholder="手机号"/></a-form-item><a-form-itemlabel=""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, 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-popconfirmtitle="删除后不可恢复,确认删除?"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 {@Resourceprivate 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);}
}
源码地址
完整代码参考:
https://gitee.com/galen.zhang/train