「动手试试」vite2+vue3+TypeScript+Element plus搭建后台项目管理模版(一)

3,678 阅读9分钟

前提

最近,Vue的全部生态已经全面转移到vue3版本,Vue3的相关生态也开始逐渐迁移。我们最常用的后台ui库element的vue3版本也在2月7号那天上线了稳定版,也就是说全面使用vue3时机已经成熟了。

本小弟也曾经在公司尝试过vue3.0的开发,但是碍于之前相关的生态很不成熟,开发起来确实磕磕碰碰的,一直提不起兴趣。

后来又陆续地短暂学习了一些TypeScript,vite相关的知识,但是一直没自己上手体验一下,所以这次下定决心记录一下自己搭建一个后台项目管理模版的过程。

本次搭建的后台模版只是一个简易版,后续会继续完善各种功能

仓库地址

初始化项目

这里先进行项目的初始化

使用vite2创建一个项目

//npm创建
npm init vite@latest
//选中vue ts版本
npx: 6 安装成功,用时 3.765 秒
✔ Project name: … sam-vue-admin-ts
✔ Select a framework: › vue
✔ Select a variant: › vue-ts

//安装依赖
cd sam-vue-admin-ts
npm i

// 本地跑起来
npm run dev

//默认的package.json
{
  "name": "sam-vue-admin-ts",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "vue": "^3.2.31"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^1.9.3",
    "typescript": "^4.4.3",
    "vite": "^2.8.1",
    "vue-tsc": "^0.31.3"
  }
}

//初始化的tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"]
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

vite配置目录别名

进入配置文件 vite.config.ts

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
// import形式
import { resolve } from "path";

// https://vitejs.dev/config/
export default defineConfig({
  resolve: {
    alias: {
      "@": resolve(__dirname, "./src"),
    },
  },
  plugins: [
    vue() 
  ],
});

这里ts会提示

找不到名称“__dirname”。ts(2304)
any
// 我们安装一下node相关的types
npm install @types/node --save-dev
//接着配置一下tsconfig.json
{
  "compilerOptions": {
    "target": "esnext",
    "useDefineForClassFields": true,
    "module": "esnext",
    "moduleResolution": "node",
    "strict": true,
    "jsx": "preserve",
    "sourceMap": true,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "lib": ["esnext", "dom"],
      //这里配置路径
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ],
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

安装css预处理器(sass)

npm install -D sass

修改.vue文件的style标签,添加 lang="scss" HelloWorld.vue

<style lang="scss" scoped>
a {
  color: #42b983;
}

label {
  margin: 0 0.5em;
  font-weight: bold;
}

code {
  background-color: #eee;
  padding: 2px 4px;
  border-radius: 4px;
  color: #304455;
}
</style>

项目安装element-plus,使用unplugin-vue-components自动按需引入

npm install element-plus --save

npm install unplugin-vue-components -D

这个unplugin-vue-components插件还可以导入自己的组件,但是我在这里没有进行这样配置,

有兴趣的可以看看这位大佬的文章

尤大推荐的神器unplugin-vue-components

vite.config.ts配置,unplugin-vue-components提供了很多知名ui库的resolvers可以直接使用

在这里我只使用ElementPlusResolver

import Components from "unplugin-vue-components/vite";
import ViteComponents, {
  //AntDesignVueResolver,
  ElementPlusResolver,
  //VantResolver,
} from 'unplugin-vue-components/resolvers'


Components({
  resolvers: [
    //AntDesignVueResolver(),
    ElementPlusResolver(),
    //VantResolver(),
  ]
})

接着我们修改一下HelloWorld.vue看看效果

<el-button type="button" @click="count++">count is: {{ count }}</el-button>

可以看到连样式都自动引入了

使用unplugin-auto-import/vite自动引入vue相关的hooks

npm i -D unplugin-auto-import

支持vue, vue-router, vue-i18n, @vueuse/head, @vueuse/core等自动引入

有兴趣的可以看看这位大佬的文章

juejin.cn/post/701244…

修改vite配置

plugins: [
    vue(),
    Components({
      resolvers: [ElementPlusResolver()],
    }),
    AutoImport({
      imports: ['vue', 'vue-router'],
      dts: 'src/auto-imports.d.ts'
    })
  ],

修改HelloWorld.vue

把之前的引入注释掉看看效果

<script setup lang="ts">
// import { ref } from 'vue'

defineProps<{ msg: string }>()

const count = ref(0)
</script>

安装会用到的一些依赖

全局状态管理

pinia(也可以使用vuex4,pinia在使用上更简便)

npm install vuex@next --save

npm install pinia@next

参考大佬:

尤大推荐的神器unplugin-vue-components

路由

vue-router4

npm install vue-router@next

http请求相关

axios

npm install axios --save

@types/qs

npm install @types/qs -D

qs

npm install qs --save

完善项目目录

我们继续完善src文件夹的内容

App.vue
components.d.ts
http
plugins
views
assets
types
constants
layout
router
auto-imports.d.ts
env.d.ts
libs
store
components
hooks
main.ts

布局容器组件以及路由配置

布局容器

src/layout文件夹下分别创建baseLayout,baseView,components三个文件夹

components/BaseHeader.vue创建头部模块

<script setup lang="ts">
import useUserStore from '@/store/modules/user.module';
import { ElMessage } from 'element-plus';
const UserStore = useUserStore();
const router = useRouter();
const state = reactive({
	circleUrl: 'https://cube.elemecdn.com/3/7c/3ea6beec64369c2642b92c6726f1epng.png'
});
const userName = computed(() => UserStore.userInfo.userName);
const clickHandler_logout = () => {
	UserStore.logout();
	ElMessage.success('退出登录成功');
	router.push('/Login');
};

const { circleUrl } = toRefs(state);
</script>

<template>
	<div class="baseHeader">
		<div class="baseHeader_right">
			<el-dropdown>
				<div class="baseHeader-memu">
					<el-avatar :size="40" :src="circleUrl"></el-avatar>
					<div class="baseHeader-name">{{ userName || '运营人员' }}</div>
				</div>
				<template #dropdown>
					<el-dropdown-menu>
						<el-dropdown-item @click="clickHandler_logout">退出登录</el-dropdown-item>
					</el-dropdown-menu>
				</template>
			</el-dropdown>
		</div>
	</div>
</template>

<style lang="scss">
.baseHeader {
	height: 100%;
	width: 100%;
	background-color: #fff;
	border-bottom: 1px solid #f1f1f1;
	box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
	&_right {
		height: 100%;
		float: right;

		.baseHeader-memu {
			height: 60px;
			display: flex;
			justify-content: center;
			align-items: center;
			.baseHeader-name {
				padding: 0 10px;
			}
		}
	}
}
</style>

components/BaseAside.vue创建菜单侧边栏模块

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

<template>
	<div class="baseAside">
		<div class="baseAside-title">vite-admin</div>
		<BaseSiderMenu />
	</div>
</template>

<style lang="scss">
.baseAside {
	height: 100vh;
	width: 100%;
	background-color: #545c64;
	.baseAside-title {
		height: 60px;
		width: 100%;
		color: #fff;
		font-size: large;
		line-height: 60px;
		text-align: center;
	}
}
</style>

layout/baseLayout创建一个index.vue文件,作为一种基础布局容器组件

<script setup lang="ts">
import BaseHeader from "../components/BaseHeader.vue";
import BaseAside from "../components/BaseAside.vue";
</script>

<template>
  <el-container>
    <el-aside width="200px">
      <BaseAside />
    </el-aside>
    <el-container>
      <el-header>
        <BaseHeader />
      </el-header>
      <el-main>
        <router-view />
      </el-main>
    </el-container>
  </el-container>
</template>

<style></style>

layout/baseView创建一个index.vue文件,作为一种内容容器组件

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

<template>
	<div class="baseView">
		<router-view v-slot="{ Component, route }">
			<transition name="fade-transform" mode="out-in">
				<component :is="Component" :key="route.fullPath" />
			</transition>
		</router-view>
	</div>
</template>

<style>
</style>

基础的布局模块就完成了

路由配置

src/router文件夹创建modules文件夹,以及index.ts,route.ts文件

src/types文件夹创建index.ts文件,

modules:存放子模块的相关路由

index.ts:负责路由初始化,路由钩子,路由挂载

route.ts:负责定义相关的路由表

types/index.ts:负责定义项目的types

先modules创建几个子模块路由

modules/home.module.ts

import BaseView from '@/layout/baseView/index.vue';

export default {
	path: 'home',
	component: BaseView,
	meta: {
		title: '首页'
	},
	children: [
		{
			path: '',
			redirect: '/home/index'
		},
		{
			path: 'index',
			component: () => import('@/views/home/index.vue'),
			meta: {
				title: '首页'
			}
		},
		{
			path: 'edit',
			component: () => import('@/views/home/edit.vue'),
			meta: {
				title: '编辑'
			}
		},
		{
			path: 'list',
			component: () => import('@/views/home/list.vue'),
			meta: {
				title: '列表'
			}
		}
	]
};

modules/other.module.ts

import BaseView from '@/layout/baseView/index.vue';

export default {
	path: 'other',
	component: BaseView,
	meta: {
		title: '其他'
	},
	children: [
		{
			path: '',
			redirect: '/other/index'
		},
		{
			path: 'index',
			component: () => import('@/views/other/index.vue'),
			meta: {
				title: '其他'
			}
		},
		{
			path: 'edit',
			component: () => import('@/views/other/edit.vue'),
			meta: {
				title: '编辑'
			}
		},
		{
			path: 'list',
			component: () => import('@/views/other/list.vue'),
			meta: {
				title: '列表'
			}
		}
	]
};

src/types/index.ts 添加路由相关type

import { RouteRecordRaw } from 'vue-router';

// 路由meta
export type TRouteMeta = {
	title?: string; // 页面标题
	icon?: string; // 侧边菜单栏图标
	activeMenu?: string;
	slideHidden?: boolean; // 侧边菜单栏中是否需要隐藏
	breadcrumb?: boolean; // 页面面包屑中是否需要显示当前路由
	auth?: string | number; // 权限
};

// 路由对象
export type TRouteRecordRaw = {
	meta?: TRouteMeta;
} & RouteRecordRaw;

export type TMenuRaw = {
    label:string | undefined,
    path:string,
    children?:TMenuRaw[] | undefined
}


router/route.ts 路由

const BaseLayout = () => import('@/layout/baseLayout/index.vue');
const Login = () => import('@/views/login/index.vue');
import home from './modules/home.module';
import other from './modules/other.module';
import { TRouteRecordRaw, TMenuRaw } from '@/types/index';

const mainRoute = [home, other];

export const baseRoute = [
	{
		path: '/login',
		name: 'login',
		component: Login
	},
	{
		path: '/',
		component: BaseLayout,
		redirect: '/home',
		children: [...mainRoute]
	},
	{
		path: '/error/:status',
		name: 'error',
		component: () => import('@/views/error/index.vue')
	},
	/* 错误路由 */
	{
		path: '/:catchAll(.*)',
		component: () => import('@/views/error/index.vue')
	}
];
//转换成菜单
const transform_menu = (route: TRouteRecordRaw[]) => {
	return route
		.map(item => {
			let menuItem: TMenuRaw = {
				path: '/' + item.path,
				label: item.meta?.title
			};
			item.children && (menuItem.children = transform_menu(item.children));
			return menuItem;
		})
		.filter(item => item.path !== '/');
};

export const menuRoute: TMenuRaw[] = transform_menu(mainRoute);

router/index.ts

import type { App } from 'vue';
import { createRouter, createWebHashHistory } from 'vue-router';
import { baseRoute } from './route';
import useUserStore from '@/store/modules/user.module';

const routerHistory = createWebHashHistory();
// createWebHashHistory hash 路由
// createWebHistory history 路由
// createMemoryHistory 带缓存 history 路由

const router = createRouter({
	history: routerHistory,
	routes: baseRoute
});

let UserStore:any = null

router.beforeEach((to, form, next) => {
	!UserStore&&(UserStore = useUserStore())
	if (to.name == 'login') {
		// 如果已登录则不会跳转到登录页
		if (UserStore.loginInfo.isLogin) {
			if (to.name == 'login') next('/');
			else next();
		} else {
			next();
		}
	} else {
		// 未登录无法打开除登录页以外的页面
		if (UserStore.loginInfo.isLogin) {
			next();
		} else {
			next('/login');
		}
	}
});

export function setupRouter(app: App<Element>): void {
	app.use(router);
}
export default router;

路由表自动生成菜单

src/components/BaseSiderMenu/index.vue

创建测边栏菜单

<script setup lang="ts">
import { menuRoute } from '@/router/route';
const router = useRouter();
const route = useRoute();
const menuList = reactive(menuRoute);
const activeMenu = computed(() => {
	const { path } = route;
	return path;
});

const clickHandler_menu = (key: string, keyPath: string[]) => {
	console.log(key, keyPath);
	router.push({ path: key });
};
</script>

<template>
	<el-menu active-text-color="#ffd04b" :default-active="activeMenu" background-color="#545c64" text-color="#fff" @select="clickHandler_menu" class="el-menu-vertical-demo">
		<el-sub-menu v-for="menu in menuList" :key="menu.path" :index="menu.path">
			<template #title>
				<span>{{ menu.label }}</span>
			</template>
			<el-menu-item v-for="item in menu.children" :key="item.path" :index="menu.path + item.path">{{ item.label }}</el-menu-item>
		</el-sub-menu>
	</el-menu>
</template>

<style>
.el-menu-vertical-demo {
	border-right: 0;
	/* width: 100%; */
}
</style>

创建登录页、错误页面

view文件夹下创建login,error文件夹

登录与注册页

login/index.vue

<script setup lang="ts">
import { ElForm, ElMessage } from 'element-plus';
import loginApi from '@/http/api/loginApi';
import useUserStore from '@/store/modules/user.module';
const UserStore = useUserStore();
const router = useRouter();
const isLoginType = ref(true);
const clickHandler_Type = (type: boolean, formEl: InstanceType<typeof ElForm> | undefined) => {
	isLoginType.value = type;
	resetForm(formEl);
};
const resetForm = (formEl: InstanceType<typeof ElForm> | undefined) => {
	if (!formEl) return;
	formEl.resetFields();
};
// 登录
const loginObject = reactive({
	name: '',
	password: ''
});
const loginFormRef = ref<InstanceType<typeof ElForm>>();
const loginRules = reactive({
	name: [
		{
			required: true,
			message: 'Please input name',
			trigger: 'blur'
		}
	],
	password: [
		{
			required: true,
			message: 'Please input password',
			trigger: 'blur'
		}
	]
});
const post_login = async () => {
	let params = {
		phone: loginObject.name,
		password: loginObject.password
	};
	// let [results, message, code]: any = await loginApi.login(params);
	const results = {
		createdTime: '111',
		id: '111',
		phone: '111',
		userName: '111',
		token:'mock-token'
	};
	const message = '登录成功';

	if (results) {
		ElMessage.success(message);
		const { token } = results;
		UserStore.login(results, { isLogin: true, token });
		router.push('/');
	}
};
const clickHandler_login = (formEl: InstanceType<typeof ElForm> | undefined) => {
	if (!formEl) return;
	formEl.validate(valid => {
		if (valid) {
			post_login();
		} else {
			return false;
		}
	});
};
// 注册
const registerObject = reactive({
	name: '',
	password: ''
});
const registerFormRef = ref<InstanceType<typeof ElForm>>();
const registerRules = reactive({
	name: [
		{
			required: true,
			message: 'Please input name',
			trigger: 'blur'
		}
	],
	password: [
		{
			required: true,
			message: 'Please input password',
			trigger: 'blur'
		}
	]
});
const clickHandler_register = (formEl: InstanceType<typeof ElForm> | undefined) => {
	if (!formEl) return;
	formEl.validate(valid => {
		if (valid) {
		} else {
			return false;
		}
	});
};
</script>

<template>
	<div class="login">
		<div class="login-box">
			<h1>vite-admin</h1>
			<!-- 登录 -->
			<el-form status-icon ref="loginFormRef" label-position="top" :model="loginObject" hide-required-asterisk :rules="loginRules" v-if="isLoginType">
				<el-form-item>
					<h2 class="form-title">用户登录</h2>
				</el-form-item>
				<el-form-item label="用户名" prop="name">
					<el-input v-model="loginObject.name"></el-input>
				</el-form-item>
				<el-form-item label="密码" prop="password">
					<el-input type="password" v-model="loginObject.password"></el-input>
				</el-form-item>
				<el-form-item>
					<el-button class="button-sub" type="primary" @click="clickHandler_login(loginFormRef)">登录</el-button>
				</el-form-item>
				<el-form-item>
					<el-button class="button-sub" @click="clickHandler_Type(false, loginFormRef)" type="text">用户注册</el-button>
				</el-form-item>
			</el-form>
			<!-- 注册 -->
			<el-form status-icon ref="registerFormRef" label-position="top" :model="registerObject" hide-required-asterisk :rules="registerRules" v-if="!isLoginType">
				<el-form-item>
					<h2 class="form-title">用户注册</h2>
				</el-form-item>
				<el-form-item label="用户名" prop="name">
					<el-input v-model="registerObject.name"></el-input>
				</el-form-item>
				<el-form-item label="密码" prop="password">
					<el-input type="password" v-model="registerObject.password"></el-input>
				</el-form-item>
				<el-form-item>
					<el-button class="button-sub" type="primary" @click="clickHandler_register(loginFormRef)">注册账号</el-button>
				</el-form-item>
				<el-form-item>
					<el-button @click="clickHandler_Type(true, registerFormRef)" type="text">返回登录</el-button>
				</el-form-item>
			</el-form>
		</div>
	</div>
</template>

<style lang="scss" scoped>
.login {
	background: rgb(217, 236, 255);
	height: 100vh;
	width: 100vw;
	.login-box {
		width: 400px;
		height: 500px;
		padding: 20px;
		background-color: #fff;
		border-radius: 16px;
		position: absolute;
		top: 0;
		left: 0;
		right: 0;
		bottom: 0;
		margin: auto;
		text-align: center;
		box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);

		.form-title {
			width: 100%;
			text-align: center;
		}
		.button-sub {
			width: 100%;
			margin-top: 20px;
		}
	}
}
</style>

错误页

error/index.vue


<script setup lang="ts">
const router = useRouter()
const clickHandler_back = () => {
    router.go(-1)
}
</script>

<template>
	<div class="error-page">
        <el-result
        icon="error"
        title="找不到该路由"
        sub-title=""
      >
        <template #extra>
          <el-button @click="clickHandler_back" type="primary">返回上一页</el-button>
        </template>
      </el-result>
    </div>
</template>

<style lang="scss" scoped>
.error-page{
    width: 100%;
    display: flex;
    height: 500px;
    justify-content: center;
    align-items: center;
}
</style>

其他路由的模拟页面

都放在了github里自己copy,也可以自行模拟

全局状态管理(pinia)

项目中我们用到的是pinia进行全局的状态管理,这是尤大推荐的一个轻量级库

创建全局挂载入口

src/store/index.ts 创建一个入口,导出一个setupStore方法

import { createPinia } from 'pinia'; //全局管理
import type { App } from 'vue';

export function setupStore(app: App<Element>): void {
	const pinia = createPinia();
	app.use(pinia);
}

创建用户模块

src/store/modules/user.module.ts

import { ref, reactive, computed } from 'vue';
import { defineStore } from 'pinia';
import storage from '@/libs/storage';


// 对外部暴露一个 use 方法,该方法会导出我们定义的 state
interface UserInfoRaw {
	createdTime: string;
	id: number | string;
	phone: string;
	userName: string;
}
interface loginInfoRaw {
	isLogin: boolean;
	token: string;
}
const useUserStore = defineStore('user', function () {
	const userInfo: UserInfoRaw | null = reactive<UserInfoRaw>(
		storage.get('userInfo') || {
			createdTime: '',
			id: '',
			phone: '',
			userName: ''
		}
	);
	const loginInfo: loginInfoRaw = reactive<loginInfoRaw>({
		isLogin: false || storage.get('isLogin'),
		token: '' || storage.get('token')
	});

	// 设置登录状态
	const setLogin = (data: loginInfoRaw) => {
		loginInfo.isLogin = data.isLogin;
		loginInfo.token = data.token;
		storage.set('isLogin', true);
		storage.set('token', data.token);
	};
	const resetLogin = () => {
		loginInfo.isLogin = false;
		loginInfo.token = '';
	};
	// 设置用户状态
	const setUserInfo = (data: UserInfoRaw) => {
		userInfo.createdTime = data.createdTime;
		userInfo.id = data.id;
		userInfo.phone = data.phone;
		userInfo.userName = data.userName;
		storage.set('userInfo', data);
	};
	// 清空用户状态
	const resetUserInfo = () => {
		userInfo.createdTime = '';
		userInfo.id = '';
		userInfo.phone = '';
		userInfo.userName = '';
	};
	const login = (user: UserInfoRaw, data: loginInfoRaw) => {
		setLogin(data);
		setUserInfo(user);
	};
	const logout = () => {
		resetLogin();
		resetUserInfo();
		storage.clear();
	};
	return {
		userInfo,
		setUserInfo,
		resetUserInfo,
		loginInfo,
		setLogin,
		resetLogin,
		login,
		logout
	};
});

export default useUserStore;

登录页中使用store

仅提取部分代码展示

import useUserStore from '@/store/modules/user.module'; //导入
const UserStore = useUserStore(); //初始化
UserStore.login(results, { isLogin: true, token }); //使用

网络请求封装(axios)

网络请求方面依然是使用axios库

在http文件夹下创建request.ts文件以及api文件夹

api文件夹文件夹下创建一个loginApi.ts写入登录模块相关的请求

封装请求

这里我做了以下处理,初始化axios的实例,简单封装post,get,postJSON三种请求方式,添加全局loading设置,根据状态码处理请求响应的数据

import axios, { AxiosError, AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import qs from 'qs';
import { Path, TIME_OUT, HTTP_REQUEST_CODE, HTTP_REQUEST_STA } from '@/constants/http';
import { ElMessage } from 'element-plus';
import router from '@/router';
import { showLoading, hideLoading } from '@/components/baseFullScreenLoading';
import useUserStore from '@/store/modules/user.module';
let UserStore: any = null;

// 配置新建一个 axios 实例
const instance: AxiosInstance = axios.create({
	baseURL: Path.HTTP_URL,
	timeout: TIME_OUT
});
// 添加请求拦截器
instance.interceptors.request.use(
	(config: AxiosRequestConfig) => {
		!UserStore && (UserStore = useUserStore());
		const token = UserStore.loginInfo.token;
		if (token && config.headers) {
			config.headers['token'] = `${UserStore.loginInfo.token}`;
		}
		return config;
	},
	(error: AxiosError) => {
		return Promise.reject(error);
	}
);

// 添加响应拦截器
instance.interceptors.response.use(
	(response: AxiosResponse) => {
		hideLoading();
		switch (response.status) {
			case HTTP_REQUEST_STA.LOGIN_ERROR:
				return [null];
			case HTTP_REQUEST_STA.SUCCESS:
				return handle_response(response);
			default:
				return [null];
		}
	},
	(error: AxiosError) => {
		hideLoading();
		ElMessage.error('请求异常,稍后重试!');
		return Promise.reject(error);
	}
);

export default {
	/**
	 * @param {String} url 接口地址
	 * @param {Object} data 参数
	 * @param {Boolean} isLoading 是否显示loading
	 */
	post(url: string, data = {}, isLoading: boolean = true) {
		if (isLoading) {
			showLoading();
		}
		return instance({
			url,
			method: 'post',
			data: qs.stringify(data),
			headers: {
				'Content-Type': 'application/x-www-form-urlencoded'
			}
		});
	},
	/**
	 * @param {String} url 接口地址
	 * @param {Object} data 参数
	 * @param {Boolean} isLoading 是否显示loading
	 */
	postJSON(url: string, data: any, isLoading = true) {
		if (isLoading) {
			showLoading();
		}
		return instance({
			url,
			method: 'post',
			data: data,
			headers: {
				'Content-Type': 'application/json'
			}
		});
	},
	/**
	 * @param {String} url 接口地址
	 * @param {Object} data 参数
	 * @param {Boolean} isLoading 是否显示loading
	 */
	get(url: string, data: object, isLoading = true) {
		if (isLoading) {
			showLoading();
		}
		return instance({
			url,
			method: 'get',
			params: data
		});
	}
};

// 处理响应回来的数据
function handle_response(response: any) {
	switch (response.data.code) {
		case HTTP_REQUEST_CODE.SUCCESS:
			return [response.data.results ? response.data.results : true, response.data.message, response.data.code];
		case HTTP_REQUEST_CODE.LOGIN_OUT:
			!UserStore && (UserStore = useUserStore());
			UserStore.logout();
			ElMessage.error(response.data.message);
			router.push('Login');
			return [null, response.data.message, response.data.code];
		default:
			ElMessage.error(response.data.message);
	}
}

封装全局loading

创建loading文件,这里我们使用el-loading

src/components/baseFullScreenLoading/index.ts

import { ElLoading } from "element-plus";

let loadingInstance:any;
const showLoading = () => {
  loadingInstance = ElLoading.service({
    fullscreen: true,
    text: "loading...",
    background: "rgba(0,0,0,0.6)",
  });
};

const hideLoading = () => {
  loadingInstance && loadingInstance.close();
};

export { showLoading, hideLoading };

创建请求模块

这里我们创建一个登录模块


import request from '../request'
const prefix = '/question-Server'
const login = {
	// 登录接口
	login: (params: any) => {
		return request.postJSON(`${prefix}/user/login`, params)
	},
}


export default {
	...login
}

在页面中使用

import loginApi from '@/http/api/loginApi';
let [results, message, code]: any = await loginApi.login(params);

样式管理

我们在src/assets下styles文件夹,管理全局样式

全局样式定义

这部分scss代码已放置到github中,不管是vue2还是vue3项目都是通用的

elementPlus全局样式重置及全局API样式引入

这里主要是参考了elementPlus官网的写法

覆盖scss变量的写法(说实话是有点麻烦)

创建src/assets/styles/element.scss


$--header: 
  (
    'padding': 0 0,
    'height': 60px,
  );

@forward "element-plus/theme-chalk/src/common/var.scss" with
  (
    $header:$--header
  );


修改vite.config.ts

css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/assets/styles/element.scss" as *;`,
      },
    },
  },

我们在用api的方式调用el-message时,会发现没有加载对应的样式,这是因为unplugin-vue-components是加载时引入的,所以像一些elloading,elmessage的组件样式在第一次使用的时候并不会加载进来,所以官网还是建议全局单独引入对应的scss

src/main.ts

import { createApp } from "vue";
import App from "./App.vue";
import router, { setupRouter } from "./router"; // 路由
import { setupStore } from "./store"

import "element-plus/theme-chalk/src/message.scss"
import "element-plus/theme-chalk/src/loading.scss"
import './assets/styles/index.scss'


const app = createApp(App);
setupStore(app)
setupRouter(app); // 引入路由

router.isReady().then(() => {
  app.mount("#app");
});

环境变量配置

在以往使用webpack开发过程中,我们都会设置.env文件进行开发,测试,生产环境的配置

在vite同样也是使用.env文件

配置env环境变量

我们在项目根目录创建

.env.development

.env.production

.env.staging

这代表着三种模式

接着以.env.development为例子

VITE_MODE_NAME=development
VITE_BASE_URL= 'http://xxxx'
VITE_APP_TITLE=vite-admin-ts-dev

修改src/env.d.ts提供ts支持

/// <reference types="vite/client" />

declare module '*.vue' {
  import { DefineComponent } from 'vue'
  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
  const component: DefineComponent<{}, {}, any>
  export default component
}

interface ImportMetaEnv {
  VITE_MODE_NAME: string,
  VITE_BASE_URL: string,
  VITE_APP_TITLE: string
}

接着修改package.json

"scripts": {
    "dev": "vite --host --mode development",
    "build": "vue-tsc --noEmit && vite build",
    "serve": "vite preview --host"
  },

代码中使用环境变量

下面举一个配置不同环境下访问服务端地址的例子

src/constants/http.ts

/**
 * @description 路径常量
 */
export const Path = {
	HTTP_URL: import.meta.env.VITE_BASE_URL as string
};

这样在开发和打包的时候,就会根据你执行的scripts命令去切换不同的环境变量

小结

这次开发的一个后台模版个人认为是比较粗糙的hhh,像很多功能都还没有加上去,一些ts用法非常混乱,一些架构上还是没好好设计,这个我打算是放在后期再慢慢根据需求继续完善吧,先把一个最小可运行版本放出来,后续每次完善一个模块就记录一篇。在使用elementPlus上一直都有踩坑,因为elementPlus一直在更新版本,其对应的一些自动按需引入的包也在同步更新,所以导致了一直在修改。希望大佬们如果有修改意见的话,可以多多给小弟提提hhh。