使用 Vue3 + Pinia + Ant Design Vue3 搭建后台管理系统

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/[email protected] -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')

添加一个测试页面






Antd栅格把页面平均分成24份



使用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


登录页面







页面增删改查操作





前端跨域问题

前后端分离项目,前端在请求后台接口时会出现跨域问题
这个后端项目使用到了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 {
    /** 默认每页的条数 */
    public static final int PAGE_SIZE_DEFAULT = 10;
    /**
     * 业务上的成功或失败
     */
    private boolean success = true;

    /**
     * 返回信息
     */
    private String message;

    /**
     * 返回泛型数据,自定义类型
     */
    private List 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  CommonPageResp SUCCESS(String message, List data, PageInfo pageInfo) {
        return new CommonPageResp<>(true, message, data, pageInfo.getTotal(), pageInfo.getPageNum(), pageInfo.getPageSize());
    }

    public CommonPageResp(boolean success, String message, List 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 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 passengerList = passengerMapper.selectByExample(passengerExample);
        PageInfo pageInfo = new PageInfo<>(passengerList);

        List list = BeanUtil.copyToList(passengerList, PassengerQueryResp.class);
        return CommonPageResp.SUCCESS("", list, pageInfo);
    }
}

源码地址

完整代码参考:
https://gitee.com/galen.zhang/train

你可能感兴趣的:(Vue,前端)