1.环境准备
(1) 安装Node 下载
(2) 安装VSCode 下载
(3)安装插件(TS环境必须)
插件市场搜索 Vue Language Features (Volar)
和 TypeScript Vue Plugin (Volar)
安装,且禁用 Vetur
2.创建项目
npm create vite@latest
目录结构
3.路径别名配置
4.安装组件库(以elementUI plus为例)
根据官方文档进行安装 安装 | Element Plus (element-plus.org)
(1) 安装组件库:
(2) 按需引入
arduino
npm install -D unplugin-vue-components unplugin-auto-import
(3)vite配置( vite.config.ts)
javascript
import { defineConfig } from 'vite'
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: [
// ...
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
})
(4)icon图标引入
typescript
1. 安装element-plus Icon库--> npm install @element-plus/icons-vue
2. 注册所有图标(main.ts)
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
const app = createApp(App)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
5.安装CSS预处理语言(sass)
(1) 安装sass
js
npm i -D sass
(2)创建 variables.scss
变量文件,添加变量 $bg-color
定义,注意规范变量以 $
开头
js
// src/styles/variables.scss
$bg-color-red:#de3131;
$bg-color-blue:#206dc5;
(3) Vite
配置导入 SCSS
全局变量文件
js
// vite.config.ts
css: {
// CSS 预处理器
preprocessorOptions: {
//define global scss variable
scss: {
javascriptEnabled: true,
additionalData: `@use "@/styles/variables.scss" as *;`
}
}
}
(4)style
标签使用SCSS
全局变量
js
<style lang="scss" scoped>
.testBox1{
width: 100px;
height: 100px;
background-color:$bg-color-red;
}
.testBox2{
width: 100px;
height: 100px;
background-color:$bg-color-blue;
}
</style>
6.安装原子化库(UnoCSS)
(1)安装
js
npm install -D unocss
(2)vite.config.ts 配置
js
import UnoCSS from 'unocss/vite'
export default {
plugins: [
UnoCSS({ /* options */ }),
],
}
(3)main.ts 引入 uno.css
js
// src/main.ts
import 'uno.css'
(4)vscode 安装unocss插件
(5)vscode安装 UnoCSS 插件之后,输入类名没有智能提示。 参考地址UnoCSS 插件智能提示不生效解决_unocss插件不提示-CSDN博客
解决:
1)新建单独配置文件 uno.config.ts,配置好后重启vscode,如还未生效则进行第二步
js
// uno.config.ts
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetUno,
presetWebFonts,
transformerDirectives,
transformerVariantGroup
} from 'unocss'
export default defineConfig({
shortcuts: [
// ...
],
theme: {
colors: {
// ...
}
},
presets: [
presetUno(),
presetAttributify(),
presetIcons(),
presetTypography(),
presetWebFonts({
fonts: {
// ...
},
}),
],
transformers: [
transformerDirectives(),
transformerVariantGroup(),
],
})
2)VScode 开启智能提示
检查配置文件是否有如下代码,没有请自行添加 配置好后的效果如下
7.安装pinia状态机(实现数据共享:跨组件,跨页面)
(1)安装
js
npm install pinia
(2)新建store文件夹
index.ts
js
import { createPinia } from "pinia";
const store = createPinia();
//子模块在主模块抛出
export { usePermissionStore } from "./modules/permission";
export { useUserStore } from "./modules/user";
export { useStationStore } from "./modules/station";
export * from "./modules/setting";
export default store;
子模块: user.ts
(4)在main.ts引入
(5)使用
父组件
子组件
效果:
初始状态
点击父组件
点击子组件
(6)数据持久化存储 配置 | pinia-plugin-persistedstate (prazdevs.github.io)
1)安装 pinia-plugin-persistedstate插件
js
npm i pinia-plugin-persistedstate
2)在store/index.ts 注册该插件 3)使用
js
**介绍三个常用属性:**
1.key:存储名称。
2.storage:存储方式。
3.paths:用于指定 state 中哪些数据需要被持久化。`[]` 表示不持久化任何状态,`undefined` 或 `null` 表示持久化整个 state。
8.环境变量配置
1.参照vite文档环境变量和模式 | Vite 官方中文文档 (vitejs.dev)
(1).env.production 生产环境
(2).env.development 开发环境
(3) env.d.ts 环境变量类型定义,用于智能提示
默认情况下,开发服务器 (dev
命令) 运行在 development
(开发) 模式,而 build
命令则运行在 production
(生产) 模式。 (4)使用:
2.在Public文件夹下创建环境变量js文件 (1)新建环境变量文件
(2) 通过vite.config.ts配置文件引入
(3)在html入口文件使用
9.axios封装
(1)安装
js
npm install axios
(2)封装axios,在utils/request.ts
js
import useUserStore from "@/store/modules/user";
import axios, { AxiosError, AxiosInstance, InternalAxiosRequestConfig, AxiosResponse } from "axios";
import { ElMessage } from "element-plus";
//定义类型
type ReqInterceptor = (config: InternalAxiosRequestConfig) => InternalAxiosRequestConfig;
interface VAxiosConfig {
config: InternalAxiosRequestConfig;
requestInterceptor?: ReqInterceptor;
}
//封装Axios类
export default class VAxios {
private axiosInstance: AxiosInstance;
private static VAxiosInstance: VAxios;
private constructor(vConfig: VAxiosConfig) {
const { config, requestInterceptor } = vConfig;
this.axiosInstance = axios.create(config);
this.setRequestInterceptors(requestInterceptor);
this.setResponseInterceptors();
}
/** 创建实例 */
public static createInstance(config: VAxiosConfig) {
this.VAxiosInstance = new this(config);
return this.VAxiosInstance;
}
/** 设置请求拦截器 */
private setRequestInterceptors(reqInterceptor?: ReqInterceptor) {
const onError = (error: AxiosError) => Promise.reject(error);
if (reqInterceptor) {
this.axiosInstance.interceptors.request.use(reqInterceptor, onError);
} else {
this.axiosInstance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const userStore = useUserStore();
if (config.headers) {
// 已登录才带上 Authorization
if (userStore.userInfo?.access_token) {
config.headers["Authorization"] = "Bearer " + userStore.userInfo.access_token;
}
}
return config;
}, onError);
}
}
//? 跨域时 NetWork Error 没有拦截到
/** 设置响应拦截器 */
private setResponseInterceptors() {
this.axiosInstance.interceptors.response.use(
(response: AxiosResponse) => {
if (typeof response.data === "object" && !response.data.code) {
return response.data; // 文件导出
}
// // 设备找不到返回 500 // 不能直接返回,否则导致错误处理失效
// if (response.data.code === 500) return response.data;
if (response.data.code !== 200) {
// console.log(response);
// throw 终止后续逻辑
throw ElMessage.error(response.data.message || "访问出错");
}
return response.data;
},
(error: AxiosError | Error) => {
if (!axios.isAxiosError(error) || !error.response) return Promise.reject(error.message);
switch (error.response.status) {
case 0:
throw ElMessage.error("网络错误");
case 400:
throw ElMessage.error("请求错误");
case 401:
throw ElMessage.error("请登录后操作");
case 403:
// router.replace("/403")
throw ElMessage.error("403 权限不足");
case 404:
throw ElMessage.error("404 API 地址错误");
case 405:
throw ElMessage.error("405 方法不支持");
case 500:
throw ElMessage.error("500 服务器内部错误");
case 502:
throw ElMessage.error("502 网络服务器错误");
case 504:
throw ElMessage.error("504 网关超时");
default:
throw ElMessage.error(
typeof error.response.data === "string" || error.response.data instanceof String
? String(error.response.data)
: ""
// : (error.response.data as unknown)?.message ?? (error.response.data as any)?.error
);
}
}
);
}
//删除请求头
removeHeader(key: string) {
if (!this.axiosInstance) return;
delete this.axiosInstance.defaults.headers[key];
}
//设置请求头
setHeaders(headers: Record<string, string>) {
if (!this.axiosInstance) return;
Object.assign(this.axiosInstance.defaults.headers, headers);
}
//请求方法
/** 请求 */
async request<T = unknown>(config: InternalAxiosRequestConfig): Promise<T> {
return this.axiosInstance.request(config);
}
/** get 请求 */
async get<T = unknown>(url: string, config?: InternalAxiosRequestConfig): Promise<T> {
return this.axiosInstance.get(url, config);
}
/**
* post 请求
* @param url 请求地址
* @param data body参数
* @param config axios 配置
*/
async post<T = unknown>(url: string, data?: unknown, config?: InternalAxiosRequestConfig): Promise<T> {
return this.axiosInstance.post(url, data, config);
}
/**
* put 请求
* @param url 请求地址
* @param data body参数
* @param config axios 配置
*/
async put<T = unknown>(url: string, data?: unknown, config?: InternalAxiosRequestConfig): Promise<T> {
return this.axiosInstance.put(url, data, config);
}
/**
* patch 请求
* @param url 请求地址
* @param data body参数
* @param config axios 配置
*/
async patch<T = unknown>(url: string, data?: unknown, config?: InternalAxiosRequestConfig): Promise<T> {
return this.axiosInstance.patch(url, data, config);
}
/**
* delete 请求
* @param url 请求地址
* @param config axios 配置
*/
async delete<T = unknown>(url: string, config?: InternalAxiosRequestConfig): Promise<T> {
return this.axiosInstance.delete(url, config);
}
}
(2)http.ts
js
import { API_URL } from "@/config";
import useUserStore from "@/store/models/user";
import VAxios from "./request";
export default VAxios.createInstance({
config: {
baseURL: API_URL,
headers: {
"Content-Type": "application/json; charset=utf-8",
Accept: "*/*",
},
// 考虑到文件上传
timeout: 1 * 90 * 1000,
},
requestInterceptor: (config) => {
const userStore = useUserStore();
if (config.headers) {
// 已登录才带上 Authorization
if (userStore.userInfo?.access_token) {
config.headers["Authorization"] = "Bearer " + userStore.userInfo.access_token;
}
}
return config;
},
});
(3)写API接口
js
import request from "@/utils/http";
import { UserLoginRes } from "@/types/user";
import { GetUserAuthRes } from "./models/user";
/** 用户登录 API */
export async function loginApi(username: string, password: string) {
return request.post<UserLoginRes>("/oauth/login", undefined, {
params: {
username,
password,
grant_type: "password",
client_secret: "iUD4dCYxn6AetqRJ2v",
client_id: "tenant_pc",
},
headers: {
platform: "pc",
},
});
}
/**
* @link /api/v1/user/auth/authorities
* 获取权限列表
*/
export function getUserAuthApi() {
return request.get<GetUserAuthRes>("/api/v1/user/auth/authorities");
}
10.vue-router
目录结构
(1)安装
js
npm i vue-router
(2)配置路由 router/index.ts
js
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import errorRoutes from "./models/error";
import others from "./models/other";
const baseRoutes: Array<RouteRecordRaw> = [
{
path: "/",
redirect: "/home",
},
{
path: "/login",
component: () => import("@/views/login.vue"),
},
...others,
];
const routes = baseRoutes.concat(errorRoutes);
const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL),
routes,
});
export default router;
(3)路由守卫
js
/** 路由守卫 */
import useUserStore from "@/store/models/user";
import { ElLoading, ElMessage } from "element-plus";
import router from ".";
// import NProgress from "nprogress";
import "nprogress/nprogress.css";
const loginPath = "/login";
let elLoading: null | { close: () => void } = null;
router.beforeEach((to, from) => {
// NProgress.start();
elLoading = ElLoading.service({
lock: true,
text: "加载中",
background: "rgba(0, 0, 0, 0.7)",
});
/** token 不存在或过期则返回 true */
function isNoTokenOrExpired() {
const { userInfo } = useUserStore();
if (!userInfo || !userInfo.access_token) return true;
if (!userInfo || !userInfo.expires_at) return true;
if (userInfo.expires_at < Date.now()) return true;
return false;
}
// 未登录或 token 过期则跳转登录页面
if (to.path !== loginPath && isNoTokenOrExpired()) {
ElMessage.error("请重新登录");
return "/login";
}
// 登录且 token 未过期则不允许访问登录页面
if (to.path === "/login" && !isNoTokenOrExpired()) {
return from.path || "/home";
}
});
router.afterEach(() => {
// NProgress.done();
elLoading?.close();
});
(4)在main.ts 中引入