vue3+ts+pinia+vue-router框架构建
约 1530 字大约 5 分钟
2024-11-13
创建Vue3脚手架
npm create vite@latest创建目录结构
src
├── api
├── assets
├── components
├── config
├── directives
├── enums
├── hooks
├── languages
├── layouts
├── router
├── stores
├── styles
├── types
├── utils
├── views安装 Element Plus UI 组件库
1. 安装 Element Plus
npm install element-plus2. 安装图标库
npm install @element-plus/icons-vue3. 完整引入(main.ts)
import { createApp } from "vue";
import App from "./App.vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
const app = createApp(App);
// 注册所有图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
app.use(ElementPlus);
app.mount("#app");4. 按需引入(推荐)
npm install -D unplugin-vue-components unplugin-auto-import配置 vite.config.ts:
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
});配置 TypeScript
1. 安装 TypeScript 相关依赖
npm install -D typescript vue-tsc @types/node2. tsconfig.json 配置
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}3. 创建类型声明文件 src/types/index.ts
// 环境变量类型声明
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
readonly VITE_APP_TITLE: string;
}
interface ImportMeta {
readonly env: ImportMetaEnv;
}
// 路由类型
declare module "vue-router" {
interface RouteMeta {
title?: string;
requiresAuth?: boolean;
icon?: string;
}
}安装和配置 Vue Router
1. 安装 Vue Router
npm install vue-router@42. 创建路由配置文件 src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
// 静态路由
const constantRoutes: RouteRecordRaw[] = [
{
path: "/login",
name: "Login",
component: () => import("@/views/login/index.vue"),
meta: { title: "登录" },
},
{
path: "/",
name: "Layout",
component: () => import("@/layouts/index.vue"),
redirect: "/dashboard",
children: [
{
path: "dashboard",
name: "Dashboard",
component: () => import("@/views/dashboard/index.vue"),
meta: { title: "首页", icon: "HomeFilled" },
},
],
},
{
path: "/404",
name: "NotFound",
component: () => import("@/views/error/404.vue"),
meta: { title: "404" },
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: constantRoutes,
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
},
});
// 路由守卫
router.beforeEach((to, from, next) => {
// 设置页面标题
document.title = to.meta.title ? `${to.meta.title} - 管理系统` : "管理系统";
next();
});
export default router;3. 动态路由实现
创建 src/router/permission.ts:
import router from "./index";
import { ElMessage } from "element-plus";
import { useUserStore } from "@/stores/user";
// 白名单
const whiteList = ["/login", "/404"];
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const token = userStore.token;
document.title = to.meta.title ? `${to.meta.title} - 管理系统` : "管理系统";
if (token) {
if (to.path === "/login") {
next({ path: "/" });
} else {
if (userStore.roles.length === 0) {
try {
// 获取用户信息和权限
await userStore.getUserInfo();
// 动态添加路由
const accessRoutes = await userStore.generateRoutes();
accessRoutes.forEach((route: any) => {
router.addRoute(route);
});
next({ ...to, replace: true });
} catch (error) {
await userStore.resetToken();
ElMessage.error("获取用户信息失败");
next(`/login?redirect=${to.path}`);
}
} else {
next();
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) {
next();
} else {
next(`/login?redirect=${to.path}`);
}
}
});
export default router;4. 在 main.ts 中引入路由
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
const app = createApp(App);
app.use(router);
app.mount("#app");安装和配置 Pinia
1. 安装 Pinia
npm install pinia
# 持久化插件
npm install pinia-plugin-persistedstate2. 创建 Pinia 实例 src/stores/index.ts
import { createPinia } from "pinia";
import piniaPluginPersistedstate from "pinia-plugin-persistedstate";
const pinia = createPinia();
pinia.use(piniaPluginPersistedstate);
export default pinia;3. 创建用户 Store src/stores/user.ts
import { defineStore } from "pinia";
import { login, getUserInfo, logout } from "@/api/user";
import { setToken, removeToken, getToken } from "@/utils/auth";
export const useUserStore = defineStore("user", {
state: () => ({
token: getToken() || "",
username: "",
avatar: "",
roles: [] as string[],
permissions: [] as string[],
}),
getters: {
isAdmin: (state) => state.roles.includes("admin"),
hasPermission: (state) => (permission: string) => {
return state.permissions.includes(permission);
},
},
actions: {
// 登录
async login(userInfo: { username: string; password: string }) {
try {
const { data } = await login(userInfo);
this.token = data.token;
setToken(data.token);
return data;
} catch (error) {
return Promise.reject(error);
}
},
// 获取用户信息
async getUserInfo() {
try {
const { data } = await getUserInfo();
this.username = data.username;
this.avatar = data.avatar;
this.roles = data.roles;
this.permissions = data.permissions;
return data;
} catch (error) {
return Promise.reject(error);
}
},
// 动态生成路由
async generateRoutes() {
// 根据用户权限动态生成路由
// 这里可以从后端获取动态路由数据
const asyncRoutes = [
{
path: "/system",
name: "System",
component: () => import("@/layouts/index.vue"),
redirect: "/system/user",
meta: { title: "系统管理", icon: "Setting" },
children: [
{
path: "user",
name: "User",
component: () => import("@/views/system/user/index.vue"),
meta: { title: "用户管理", icon: "User" },
},
{
path: "role",
name: "Role",
component: () => import("@/views/system/role/index.vue"),
meta: { title: "角色管理", icon: "UserFilled" },
},
],
},
];
return asyncRoutes;
},
// 退出登录
async logout() {
try {
await logout();
this.resetToken();
} catch (error) {
return Promise.reject(error);
}
},
// 重置token
resetToken() {
this.token = "";
this.username = "";
this.avatar = "";
this.roles = [];
this.permissions = [];
removeToken();
},
},
persist: {
key: "user-store",
storage: localStorage,
paths: ["token"],
},
});4. 创建应用 Store src/stores/app.ts
import { defineStore } from "pinia";
export const useAppStore = defineStore("app", {
state: () => ({
sidebar: {
opened: true,
withoutAnimation: false,
},
device: "desktop",
size: "default",
}),
actions: {
toggleSidebar() {
this.sidebar.opened = !this.sidebar.opened;
this.sidebar.withoutAnimation = false;
},
closeSidebar(withoutAnimation: boolean) {
this.sidebar.opened = false;
this.sidebar.withoutAnimation = withoutAnimation;
},
toggleDevice(device: string) {
this.device = device;
},
setSize(size: string) {
this.size = size;
},
},
persist: {
key: "app-store",
storage: localStorage,
},
});5. 在 main.ts 中引入 Pinia
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import pinia from "./stores";
const app = createApp(App);
app.use(router);
app.use(pinia);
app.mount("#app");配置路径别名和 Vite
vite.config.ts 完整配置
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import { resolve } from "path";
import AutoImport from "unplugin-auto-import/vite";
import Components from "unplugin-vue-components/vite";
import { ElementPlusResolver } from "unplugin-vue-components/resolvers";
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()],
imports: ["vue", "vue-router", "pinia"],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
"@": resolve(__dirname, "src"),
},
},
server: {
port: 3000,
open: true,
proxy: {
"/api": {
target: "http://localhost:8080",
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ""),
},
},
},
});创建工具函数
1. 认证工具 src/utils/auth.ts
const TOKEN_KEY = "admin-token";
export function getToken() {
return localStorage.getItem(TOKEN_KEY);
}
export function setToken(token: string) {
return localStorage.setItem(TOKEN_KEY, token);
}
export function removeToken() {
return localStorage.removeItem(TOKEN_KEY);
}2. 请求封装 src/utils/request.ts
import axios from "axios";
import { ElMessage, ElMessageBox } from "element-plus";
import { useUserStore } from "@/stores/user";
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || "/api",
timeout: 15000,
});
// 请求拦截器
service.interceptors.request.use(
(config) => {
const userStore = useUserStore();
if (userStore.token) {
config.headers["Authorization"] = `Bearer ${userStore.token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
// 响应拦截器
service.interceptors.response.use(
(response) => {
const res = response.data;
if (res.code !== 200) {
ElMessage({
message: res.message || "Error",
type: "error",
duration: 5 * 1000,
});
if (res.code === 401) {
ElMessageBox.confirm("登录已过期,请重新登录", "提示", {
confirmButtonText: "重新登录",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
const userStore = useUserStore();
userStore.resetToken();
location.reload();
});
}
return Promise.reject(new Error(res.message || "Error"));
} else {
return res;
}
},
(error) => {
ElMessage({
message: error.message,
type: "error",
duration: 5 * 1000,
});
return Promise.reject(error);
},
);
export default service;3. API 接口 src/api/user.ts
import request from "@/utils/request";
export interface LoginData {
username: string;
password: string;
}
export function login(data: LoginData) {
return request({
url: "/auth/login",
method: "post",
data,
});
}
export function getUserInfo() {
return request({
url: "/auth/userInfo",
method: "get",
});
}
export function logout() {
return request({
url: "/auth/logout",
method: "post",
});
}创建布局和页面组件
1. 布局组件 src/layouts/index.vue
<template>
<div class="layout-container">
<el-container>
<el-aside :width="sidebarWidth">
<!-- 侧边栏 -->
</el-aside>
<el-container>
<el-header>
<!-- 顶部导航栏 -->
</el-header>
<el-main>
<router-view />
</el-main>
</el-container>
</el-container>
</div>
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useAppStore } from "@/stores/app";
const appStore = useAppStore();
const sidebarWidth = computed(() => {
return appStore.sidebar.opened ? "200px" : "64px";
});
</script>
<style scoped lang="scss">
.layout-container {
height: 100vh;
}
</style>环境变量配置
1. .env.development
VITE_APP_TITLE=管理系统
VITE_API_BASE_URL=/api2. .env.production
VITE_APP_TITLE=管理系统
VITE_API_BASE_URL=https://api.example.com总结
完成以上步骤后,您将拥有一个完整的 Vue3 + TypeScript + Pinia + Vue Router + Element Plus 的企业级项目框架,具备以下特性:
✅ TypeScript 类型安全 ✅ Pinia 状态管理(支持持久化) ✅ Vue Router 动态路由和权限管理 ✅ Element Plus UI 组件库 ✅ Axios 请求封装 ✅ 完整的目录结构 ✅ 路径别名配置 ✅ 环境变量支持 ✅ 路由守卫和权限控制
现在可以开始开发您的业务功能了!
