七、Vue3+Ts+Vite+AntdUI构建后台基础模板——结构性调整

257 阅读3分钟

一、使用Visual Studio Code。

  1. vscode相较于webstorm
    • 对ts有更加完善的提示和适配。
    • 尤其是对于Vue3+Ts有比较好的开发体验。
    • 所以在本章节开始调整使用vscode工具开发。
  2. 将项目用vscode打开。
  3. 开发中所必须的插件:
    • Vue Language Features (Volar)
    • Prettier - Code formatter
    • ESLint
  4. 开发中要禁用的插件:
    • Vetur
    • vue
    • 及其他代码格式化插件
  5. 修改.prettierrc解决换行ESLint报错的问题
{
    "tabWidth": 4,
    "singleQuote": true,
    "semi": true,
    "printWidth": 200,
    "endOfLine": "auto"
}

  1. 执行格式化命令
npx prettier --write .
  1. 重启VScode

二、AntdUI组件库升级

  1. AntdUI目前推出了最新版本3.2.3。

    • 3.x 版本在性能、易用性、功能上都有了很大的提升
    • 官网建议尽快升级 3.x 版本
  2. 修改package.json文件

{
    "name": "vue3-admin",
    "version": "0.0.0",
    "scripts": {
        "dev": "cross-env NODE_ENV=development vite",
        "build": "vue-tsc --noEmit && vite build",
        "serve": "vite preview"
    },
    "dependencies": {
        "ant-design-vue": "^3.2.3",
        ......
    },
    "devDependencies": {
        ......
    }
}
  1. 执行安装依赖命令
npm i

三、升级Vue版本到3.2.X以上

  1. 升级vue版本到3.2.X以上
  2. 修改package.json文件
{
    "name": "vue3-admin",
    "version": "0.0.0",
    "scripts": {
        "dev": "cross-env NODE_ENV=development vite",
        "build": "vue-tsc --noEmit && vite build",
        "serve": "vite preview"
    },
    "dependencies": {
        "vue": "^3.2.36",
        ......
    },
    "devDependencies": {
        ......
    }
}
  1. 执行安装依赖命令
npm i

四、使用setup语法糖

  1. 修改.eslintrc.js
module.exports = {
......
    rules: {
        // 解决v-model指令报红
        'vue/no-v-model-argument': 'off',
        ......
    },
};

  1. App.vue
<template>
    <layout v-if="route.meta.layout" />
    <a-spin :spinning="store.state.loading.loadingState" :delay="300" size="large" v-else>
        <router-view />
    </a-spin>
</template>

<script lang="ts" setup>
import layout from '@/layout/index.vue';
import { useRoute } from 'vue-router';
import { useStore } from 'vuex';

const route = useRoute();
const store = useStore();
</script>
<style lang="less">......</style>
  1. @/layout/index.vue
<template>
    <a-layout class="layout">
        <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
            <div class="logo" />
            <a-menu theme="dark" mode="inline" v-model:selectedKeys="selectedKeys">
                <a-menu-item key="/">
                    <dashboard-outlined />
                    <span>首页</span>
                </a-menu-item>
            </a-menu>
        </a-layout-sider>
        <a-layout>
            <a-layout-header style="background: #fff; padding: 0 20px">
                <menu-unfold-outlined v-if="collapsed" class="trigger" @click="() => (collapsed = !collapsed)" />
                <menu-fold-outlined v-else class="trigger" @click="() => (collapsed = !collapsed)" />
            </a-layout-header>

            <a-layout-content style="margin: 24px 16px; padding: 24px; background: #fff; min-height: 280px">
                <a-spin :spinning="store.state.loading.loadingState" :delay="300" size="large">
                    <router-view />
                </a-spin>
            </a-layout-content>
        </a-layout>
    </a-layout>
</template>

<script lang="ts" setup>
import { DashboardOutlined, MenuUnfoldOutlined, MenuFoldOutlined } from '@ant-design/icons-vue';
import { ref } from 'vue';
import { useStore } from 'vuex';

const store = useStore();
const selectedKeys = ref<string[]>(['/']);
const collapsed = ref<boolean>(false);
</script>

<style lang="less">......</style>
  1. @/views/Home/index.vue
<template>
    <div>
        <h1>home</h1>
    </div>
</template>

<script lang="ts" setup>

</script>

<style lang="less">......</style>
  1. @/views/Login/index.vue
<template>
    <a-row class="layout" type="flex" justify="center" align="middle">
        <a-card class="login_card" :bodyStyle="{ height: '100%', padding: 'unset' }" hoverable>
            <div class="card_body">
                <div class="login_img">
                    <img src="https://via.placeholder.com/320x180/fff.png?text=Tian-Admin" />
                    <p>Tian-Admin</p>
                </div>
                <div class="login_form">
                    <a-form class="form" ref="loginFormRef" layout="vertical" :rules="loginRules" :model="loginForm">
                        <a-form-item label="账号:" name="username">
                            <a-input v-model:value="loginForm.username" size="large" />
                        </a-form-item>
                        <a-form-item label="密码:" name="password">
                            <a-input-password v-model:value="loginForm.password" size="large" />
                        </a-form-item>
                        <a-form-item>
                            <a-button type="primary" size="large" block @click="onSubmit">提交</a-button>
                        </a-form-item>
                    </a-form>
                </div>
            </div>
        </a-card>
    </a-row>
</template>

<script lang="ts" setup>
import { reactive, ref, toRaw } from 'vue';
import { useRouter } from 'vue-router';
import { useStore } from 'vuex';

import LoginAPI from '@/request/api/loginAPI';

interface loginFormConfig {
    username: string;
    password: string;
}

interface loginResConfig {
    token: string;
    userInfo: any;
}

const router = useRouter();
const store = useStore();
const loginFormRef = ref();
const loginForm: loginFormConfig = reactive({
    username: '',
    password: '',
});
const loginRules = {
    username: [
        {
            required: true,
            message: '请输入用户名',
            trigger: 'blur',
        },
    ],
    password: [
        {
            required: true,
            message: '请输入密码',
            trigger: 'blur',
        },
    ],
};

const onSubmit = () => {
    loginFormRef.value.validate().then(async () => {
        const res = (await LoginAPI.setLogin(toRaw(loginForm))) as loginResConfig;
        if (res) {
            console.log(res);
            store.commit('user/setToken', res.token);
            store.commit('user/setUserInfo', res.userInfo);
            router.push({ path: '/' });
        }
    });
};
</script>

<style lang="less">......</style>

五、使用Pinia替换Vuex

  1. Pinia介绍
  • Pinia 是 Vue 的存储库,它允许您跨组件/页面共享状态。
  • Pinia 提供了一个更简单的 API,具有更少的规范。
  • 提供了 Composition-API 风格的 API。
  • 与 TypeScript 一起使用时具有可靠的类型推断支持。
  1. 安装,执行命令
npm install pinia
  1. 安装Pinia持久化插件
  2. 安装,执行命令
npm install pinia-plugin-persist
  1. 在项目中引入
  2. 修改 @/main.ts
import { createApp } from 'vue';
import App from '@/App.vue';
import router from '@/router';
import store from '@/store';
import { createPinia } from 'pinia';
import piniaPluginPersist from 'pinia-plugin-persist';
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';

const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersist);

app.use(router);
app.use(store);
app.use(pinia);
app.use(Antd);
app.mount('#app');
  1. 增加pinia-plugin-persist的types
  2. 修改tsconfig.json
{
    "compilerOptions": {
        ......
        "types": [
            "pinia-plugin-persist"
        ]
    },
   ......
}
  1. 先保留store目录,在src下新建pinia目录
  2. 在src/pinia目录下新建modules目录
  3. 在src/pinia目录下新建index.ts
  4. 在src/pinia/modules目录新建loading.ts和user.ts文件
  5. src/pinia/modules/loading.ts
import { defineStore } from 'pinia';

const useLoadingStore = defineStore('loading', {
    state: () => {
        return {
            loadingState: false,
        };
    },
    actions: {
        showLoading() {
            this.loadingState = true;
        },
        hideLoading() {
            this.loadingState = false;
        },
    },
});

export default useLoadingStore;
  1. src/pinia/modules/user.ts
import { defineStore } from 'pinia';

const useUserStore = defineStore('user', {
    state: () => {
        return {
            token:'',
            userInfo: {},
        };
    },
    actions: {
        setToken(token: string) {
            this.token = token;
        },
        setUserInfo(userInfo) {
            this.userInfo = userInfo;
        },
        clearToken() {
            this.token = '';
        },
        clearUser() {
            this.userInfo = {};
        },
    },
    persist: {
        enabled: true,
        strategies: [
            { key: 'token', storage: localStorage, paths: ['token'] },
            { key: 'userInfo', storage: localStorage, paths: ['userInfo'] },
        ],
    },
});

export default useUserStore;
  1. src/pinia/index.ts
import useUserStore from './modules/user';
import useLoadingStore from './modules/loading';

export { useUserStore, useLoadingStore };
  1. 完成上述操作后
  2. 删除原本的/src/store目录
  3. 将pinia目录改为store目录
  4. 修改main.ts
import { createApp } from 'vue';
import App from '@/App.vue';
import router from '@/router';
import { createPinia } from 'pinia';
import piniaPluginPersist from 'pinia-plugin-persist';
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css';

const app = createApp(App);
const pinia = createPinia();
pinia.use(piniaPluginPersist);

app.use(router);
app.use(pinia);
app.use(Antd);
app.mount('#app');

六、修改项目中原本的store

  1. @/App.vue
<template>
    <layout v-if="route.meta.layout" />
    <a-spin :spinning="loadingStore.loadingState" :delay="300" size="large" v-else>
        <router-view />
    </a-spin>
</template>

<script lang="ts" setup>
import { useRoute } from 'vue-router';
import { useLoadingStore } from '@/store';
import layout from '@/layout/index.vue';

const route = useRoute();
const loadingStore = useLoadingStore();
</script>

<style lang="less">
......
</style>
  1. @/request/http.ts
......

import { useLoadingStore, useUserStore } from 

......

const loadingStore = useLoadingStore();
const userStore = useUserStore();

......

const Fetch = ({
   .....
}: AxiosConfig) => {
    if (loading) {
        loadingStore.showLoading();
    }

    if (userStore.token) {
        Object.assign(headers, {
            token: userStore.token,
        });
    }

    return new Promise((resolve, reject) => {
        instance({
            url,
            method,
            data,
            params,
            headers,
        })
            .then((res) => {
                loadingStore.hideLoading();
                resolve(res.data.data);
            })
            .catch((err) => {
                loadingStore.hideLoading();
                message.error('请求失败');
                reject(err);
            });
    });
};

export default Fetch;
  1. @/router/index.ts
// @/router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router';
import { useUserStore } from '@/store';

......

router.beforeEach((to, from, next) => {
    const userStore = useUserStore();
    if (to.name !== 'Login' && !userStore.token) {
        next({ name: 'Login' });
    } else {
        next();
    }
});

export default router;

  1. @/layout/index.vue
<template>
    <a-layout class="layout">
        <a-layout-sider v-model:collapsed="collapsed" :trigger="null" collapsible>
           ......
        </a-layout-sider>
        <a-layout>
            ......

            <a-layout-content style="margin: 24px 16px; padding: 24px; background: #fff; min-height: 280px">
                <a-spin :spinning="loadingStore.loadingState" :delay="300" size="large">
                    <router-view />
                </a-spin>
            </a-layout-content>
        </a-layout>
    </a-layout>
</template>

<script lang="ts" setup>
......
import { useLoadingStore } from '@/store';

const loadingStore = useLoadingStore();
......
</script>

<style lang="less" scoped>
......
</style>
  1. @/views/Login/index
<template>
    ......
</template>

<script lang="ts" setup>
......

import { useUserStore } from '@/store';

......

const router = useRouter();
const userStore = useUserStore();

......

const onSubmit = () => {
    loginFormRef.value.validate().then(async () => {
        const res = (await LoginAPI.setLogin(toRaw(loginForm))) as loginResConfig;
        if (res) {
            console.log(res);
            userStore.setToken(res.token);
            userStore.setUserInfo(res.userInfo);
            router.push({ path: '/' });
        }
    });
};
</script>

<style lang="less" scoped>
......
</style>

七、源代码地址

https://github.com/jiangzetian/vue3-admin-template

八、视频演示及源码

本文演示视频:点击浏览

更多前端内容欢迎关注公众号:天小天个人网