赞
踩

最下面有相关文档链接, 此文章提供大致步骤与部分封装
//初始化项目 1、npm init vue@latest 2、选装 (空格输入yes或者no, 一路yes即可) // 初始化包 3、npm install //运行 4、npm run dev //安装axios 5、npm install axios -D //安装 element plus 6、npm install element-plus -D //按需动态引入element 组件的([推荐,也可以全局安装](https://element-plus.gitee.io/zh-CN/guide/quickstart.html)) 7、npm install unplugin-vue-components unplugin-auto-import unplugin-icons -D //安装less (也可以安装scss) 8、npm install --save less //mitt 使用 (兄弟组件通信) 9、npm install mitt -S 如遇安装失败 请改为cnpm安装
//设置/用户代码片段 { "Print to console": { "prefix": "sc1", "body": [ "<template>", " <div>", " </div>", "</template>", "", "<script lang='ts' setup>", "import { onMounted } from 'vue';", "onMounted(() => {", "});", "</script>", "<style lang='less' scoped>", "", "</style>" ], "description": "Log output to console" } }
在AboutView.vue/HomeView.vue里 输入sc1 ,点击tab
<template>
<div>{{ name }}</div>
</template>
<script lang="ts" setup>
import { onMounted, ref } from "vue";
const name = ref<string>("home");
onMounted(() => {});
</script>
<style lang="less" scoped></style>
<template>
<router-view></router-view>
</template>
import { createRouter,createWebHashHistory } from 'vue-router' const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: () => import('../views/HomeView.vue') }, { path: '/about', name: 'about', component: () => import('../views/AboutView.vue') } ] }) export default router
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueJsx from '@vitejs/plugin-vue-jsx' import AutoImport from 'unplugin-auto-import/vite'; import Components from 'unplugin-vue-components/vite'; import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'; // https://vitejs.dev/config/ export default defineConfig({ //配置根目录, 跨域 base: './', server: { proxy: { '/api': { target: 'http://httpbin.org', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }, plugins: [ vue(), //动态按需引入element plus组件 AutoImport({ resolvers: [ ElementPlusResolver(), ], }), Components({ resolvers: [ ElementPlusResolver(), ], }), vueJsx(), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } }, //打包配置 build: { emptyOutDir: true, } })
main.ts改为
import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' import router from './router' import './assets/base.css' import 'element-plus/dist/index.css' const app = createApp(App) // 全局引入 element icons import * as Icons from "@element-plus/icons-vue"; // 引入mitt import mitt from 'mitt' // 注册element Icons组件 for (const [key, component] of Object.entries(Icons)) { app.component(key, component) } // 注册Mit const Mit = mitt() declare module "vue" { export interface ComponentCustomProperties { $Bus: typeof Mit } } //挂载全局API app.config.globalProperties.$Bus = Mit app.use(createPinia()) app.use(router) app.mount('#app')
公共样式assets/base.css添加
/* fade-transform */ .fade-transform-leave-active, .fade-transform-enter-active { transition: all 0.2s; } .fade-transform-enter-from { opacity: 0; transition: all 0.2s; transform: translateX(-30px); } .fade-transform-leave-to { opacity: 0; transition: all 0.2s; transform: translateX(30px); } *{ padding:0px; margin:0px; } .fl{ float: left; } .fr{ float: right; } .overflow{ overflow: hidden; } .mb20{ margin-bottom: 20px; } .mt20{ margin-top: 20px; } .mr20{ margin-right: 20px; }
至此初始化完成, 现在打开运行的项目应该是这样的


<template> <div class="common-layout"> <el-container> <el-aside><Aside></Aside></el-aside> <el-container> <el-header><Header></Header></el-header> <el-main> <router-view v-slot="{ Component, route }"> <transition appear name="fade-transform" mode="out-in"> <component :is="Component" :key="route.path" /> </transition> </router-view> </el-main> </el-container> </el-container> </div> </template> <script lang="ts" setup> import { RouterView } from "vue-router"; import Header from "./header/index.vue"; import Aside from "./aside/index.vue"; </script> <style lang="less" scoped> .el-aside { width: auto; } .common-layout { height: 100vh !important; } .el-header { padding: 0 !important; width: 100%; } </style>
stores文件删除没用的自带ts,新建index.ts
import { defineStore } from 'pinia' // 建议抽出去写,然后在这里面import interface GlobalState { token: any; } export const GlobalStore = defineStore('GlobalStore', { state: (): GlobalState => ({ // 所有这些属性都将自动推断其类型 token: localStorage.getItem("_vue3_token") != null ? localStorage.getItem("_vue3_token") : "", }), getters: { }, actions: { setToken(token: string) { localStorage.setItem('_vue3_token', token) this.token = token }, logOut() { localStorage.removeItem("_vue3_token") this.token = "" } }, })
左侧菜单aside/index.vue
<template> <div> <el-menu :default-active="activeMenu" class="el-menu-vertical-demo" background-color="#293c55" text-color="rgba(255,255,255,0.45)" active-text-color="#f9f9f9" :collapse="isCollapse" router > <el-menu-item style="text-align: center; display: block" index="0" >LOGO</el-menu-item > <el-menu-item index="/home"> <el-icon><Coffee /></el-icon> <template #title>home</template> </el-menu-item> <el-menu-item index="/about"> <el-icon><Coffee /></el-icon> <template #title>about</template> </el-menu-item> </el-menu> </div> </template> <script lang="ts" setup> import { useRoute } from "vue-router"; import { ref, getCurrentInstance, computed } from "vue"; const route = useRoute(); const instance = getCurrentInstance(); const activeMenu = computed(() => route.path); const isCollapse = ref<boolean>(false); instance?.proxy?.$Bus.on("isCollapse", (data: any) => { isCollapse.value = data.value; }); </script> <style lang="less" scoped> .logo { height: 60px; background-color: #545c64; line-height: 60px; text-align: center; width: 200px; } .el-menu-vertical-demo:not(.el-menu--collapse) { width: 200px; height: 100vh; } .el-menu-vertical-demo { height: 100vh; } </style>
顶部导航header/index.vue
<template> <div class="header overflow"> <div class="expand fl" @click="collapseChange"> <el-icon v-if="!isCollapse"><Fold /></el-icon> <el-icon v-else><Expand /></el-icon> </div> <div class="item_style mr20"> <el-dropdown trigger="click" @command="handleCommand"> <div style="width: 60px"> {{ store.token }} </div> <template #dropdown> <el-dropdown-menu> <el-dropdown-item v-for="item in dropdownList" :key="item.value" :command="item.value" >{{ item.label }}</el-dropdown-item > </el-dropdown-menu> </template> </el-dropdown> </div> </div> </template> <script lang="ts" setup> import { ref, getCurrentInstance, onMounted, reactive } from "vue"; import { ElMessage, ElMessageBox } from "element-plus"; import { useRouter } from "vue-router"; import { GlobalStore } from "@/stores"; const isCollapse = ref<boolean>(false); const instance = getCurrentInstance(); const collapseChange = () => { isCollapse.value = !isCollapse.value; instance?.proxy?.$Bus.emit("isCollapse", isCollapse); }; const router = useRouter(); const store = GlobalStore(); const dropdownList = reactive<any>([ { label: "退出登录", value: "logout", }, ]); const logout = () => { store.logOut(); router.replace({ path: "/login", }); ElMessage({ type: "success", message: "退出成功", }); }; const handleCommand = (command: string | number | object) => { switch (command) { case "logout": ElMessageBox.confirm("确认退出吗?", "Warning", { type: "warning", }) .then(() => { logout(); }) .catch(() => {}); break; default: break; } }; onMounted(() => {}); </script> <style lang="less" scoped> .header { height: 60px; box-shadow: 0 2px 8px rgb(0 0 0 / 15%); line-height: 60px; .expand { width: 60px; text-align: center; } .item_style { float: right; height: 60px; text-align: center; line-height: 60px; text-align: center; cursor: pointer; } } .el-dropdown { line-height: 60px; } </style>
修改router/index.ts
import { createRouter, createWebHashHistory } from 'vue-router' import Layouts from '@/layouts/index.vue' import { GlobalStore } from '../stores' const router = createRouter({ history: createWebHashHistory(import.meta.env.BASE_URL), routes: [ { path: '/login', name: 'login', component: () => import('@/views/login.vue') }, { path: '/', name: '首页', component: Layouts, redirect: '/home', children: [ { path: '/home', name: 'home', component: () => import('../views/HomeView.vue') }, { path: '/about', name: 'about', component: () => import('../views/AboutView.vue') } ] }, { path: '/:pathMatch(.*)*', name: 'notFound', component: () => import('@/views/notFound.vue') } ] }) router.beforeEach(async (to, from, next) => { // 1.如果访问登录页,直接过 if (to.path == '/login') return next(); // 2.如果没有token,重定向到login const globalStore = GlobalStore() if (!globalStore.token) return next({ path: '/login', replace: true }) // 3.如果没有菜单列表,就重新请求菜单列表并添加动态路由 // 4.正常访问页面 next(); }) export default router
至此你的项目应该是这样

src下新建utils/snow.ts, 新建登录login.vue 404 notFound,vue

utils/snow.ts
export default function snow() { let canvas:any = document.getElementById('snow'), // 初始化画笔 context = canvas.getContext('2d'), // 定义画布宽高 w = window.innerWidth, h = window.innerHeight, // 定义雪花数量和位置及大小集合 num = 200, snows: any[] = []; // 设置画布大小 canvas.width = w; canvas.height = h-5; // 随机雪花位置及大小 for (let i = 0; i < num; i++) { snows.push({ x: Math.random() * w, y: Math.random() * h, r: Math.random() * 5+1 }) } // 半径[1,6), 大于6 从左往右飘,小于6从又往左飘, 上下推荐大于10 let move = () => { for (let i = 0; i < num; i++) { let snow = snows[i]; snow.y += (10-snow.r)/5 snow.x += (8-snow.r)/5 if (snow.x > w) snow.x = 0 if (snow.y > h) snow.y = 0 } } let draw = () => { context.clearRect(0, 0, w, h); context.beginPath(); context.fillStyle = "rgba(255,255,255,.5)"; context.shadowColor = "rgba(255,255,255,.5)"; context.shadowBlur = 10; for (let i = 0; i < num; i++) { let snow = snows[i]; context.moveTo(snow.x, snow.y) context.arc(snow.x, snow.y, snow.r, 0, Math.PI * 2) } context.fill(); context.closePath(); move() } draw() let timer = setInterval(draw, 50) return { timer:timer } }
login.vue
<template> <div class="view"> <canvas id="snow"></canvas> <div class="content"> <div class="login"> <img class="logo" src="@/assets/logo.svg" alt="" /> <h2>H-Admin</h2> </div> <el-form :model="ruleForm" :rules="rules" ref="ruleFormRef" class="demo-ruleForm" :size="formSize" status-icon > <el-form-item prop="name"> <el-input v-model="ruleForm.name" placeholder="用户名:密码:随便"> <template #prefix> <el-icon class="el-input__icon"><user /></el-icon> </template> </el-input> </el-form-item> <el-form-item prop="password"> <el-input v-model="ruleForm.password" placeholder="密码:随便" show-password @keyup.enter.native="login(ruleFormRef)" > <template #prefix> <el-icon class="el-input__icon"><lock /></el-icon> </template> </el-input> </el-form-item> <el-form-item> <el-button style="width: 100%" type="primary" @click="login(ruleFormRef)" >登录</el-button > </el-form-item> </el-form> </div> </div> </template> <script lang="ts" setup> import { onMounted, onUnmounted, reactive, ref } from "vue"; import type { FormInstance, FormRules } from "element-plus"; import { useRouter } from "vue-router"; import { GlobalStore } from "@/stores"; import { ElMessage } from "element-plus"; import snow from "@/utils/snow"; const timer = ref<any>(null); const router = useRouter(); const store = GlobalStore(); const formSize = ref("large"); const ruleFormRef = ref<FormInstance>(); interface userForm { name: string; password: string; } const ruleForm = reactive<userForm>({ name: "", password: "", }); const rules = reactive<FormRules>({ name: [{ required: true, message: "请输入账号", trigger: "blur" }], password: [{ required: true, message: "请输入密码", trigger: "blur" }], }); const login = async (formEl: FormInstance | undefined) => { if (!formEl) return; await formEl.validate(async (valid, fields) => { if (valid) { // store.token = ruleForm.name; store.setToken(ruleForm.name); // 2.后续可在此添加动态路由 ElMessage.success("登录成功"); router.push({ path: "/", }); } }); }; onMounted(() => { //使用雪花飘落背景 timer.value = snow().timer; }); //组件销毁时 关闭,清楚定时器 onUnmounted(() => { clearInterval(timer.value); timer.value = null; }); </script> <style lang="less" scoped> .view { // background-color: #293c55; background: url("../assets/snow.png") no-repeat; background-size: cover; height: 100vh; } .content { position: absolute; left: 50%; top: 50%; margin-left: -200px; margin-top: -200px; width: 320px; padding: 40px 40px 30px; background-color: #fff; border-radius: 10px; box-shadow: 2px 3px 7px rgb(0 0 0 / 20%); .login { margin-bottom: 20px; display: flex; justify-content: center; align-items: center; .logo { width: 50px; margin-right: 10px; } } } </style>
notFound.vue
<template> <div class="notFound"> <h1>404</h1> <h4 class="mt20">抱歉,您访问的页面不存在</h4> <el-button class="mt20" type="primary" @click="router.push('/')" >返回首页</el-button > </div> </template> <script lang="ts" setup> import { useRouter } from "vue-router"; const router = useRouter(); </script> <style lang="less" scoped> .notFound { margin-top: 200px; text-align: center; } </style>
src下新建request文件,api文件用来放接口,index.ts封装axiso

requers/index.ts
import axios from 'axios' import router from '@/router' import { GlobalStore } from "../stores"; import { ref } from 'vue' import { ElMessage, ElLoading } from 'element-plus' const store = GlobalStore(); // 创建一个 axios 实例 const service = axios.create({ baseURL: '/api', // 所有的请求地址前缀部分 timeout: 60*1000, // 请求超时时间毫秒 withCredentials: true, // 异步请求携带cookie headers: { // 设置后端需要的传参类型 'Content-Type': 'application/json', 'token': store.token||'', 'X-Requested-With': 'XMLHttpRequest', }, }) // 全局加载 const ElLoadingNum = ref<any>(0) const Loading = ref<any>(""); function startElLoading() { if (ElLoadingNum.value == 0) { Loading.value = ElLoading.service({ lock: true, text: "Loading", background: "rgba(0, 0, 0, 0.7)" }); } ElLoadingNum.value++; } function endElLoading() { ElLoadingNum.value--; if (ElLoadingNum.value <= 0) { Loading.value.close(); } } const toLogin = () => { router.replace({ path: '/login' }); } const errorHandle = (status:any, other:any) => { // 状态码判断 switch (status) { // 401: 未登录状态,跳转登录页 case 401: toLogin(); break; // 清除token并跳转登录页 case 403: ElMessage.error('登录过期,请重新登录'); store.logOut(); setTimeout(() => { toLogin(); }, 1000); break; case 404: ElMessage.error('请求的资源不存在'); break; case 405: ElMessage.error('请求405'); break; case 504: ElMessage.error('请求504'); break; default: ElMessage(other); } } // 添加请求拦截器 service.interceptors.request.use( (config) => { startElLoading() // 在发送请求之前做些什么 return config }, (error) => { startElLoading() // 对请求错误做些什么 return Promise.reject(error) } ) // 添加响应拦截器 service.interceptors.response.use( (response) => { // 对响应成功做点什么 endElLoading() return Promise.resolve(response.data) }, (error) => { endElLoading() if (error) { // 对响应错误做点什么 errorHandle(error.response.status, error.message); return Promise.reject(error) } else { // 处理断网的情况 if (!window.navigator.onLine) { ElMessage.error('网络异常'); } else { ElMessage.error('数据加载失败,请稍后重试'); return Promise.reject(error); } } } ) export default service
api/index.ts
// 导入axios实例
import httpRequest from '../index'
// 定义接口的传参
interface UserInfoParam {
userID: string,
}
// 获取用户信息
export function getUserInfo(param: UserInfoParam) {
return httpRequest({
url: '/post',
method: 'post',
data: param,
})
}
修改HomeView.vue 来做测试
<template> <div> <div>{{ name }}</div> <el-button @click="getData" type="success">接口</el-button> {{ info.data }} </div> </template> <script lang="ts" setup> import { onMounted, ref, reactive } from "vue"; import { getUserInfo } from "@/request/api/index"; import { ElMessage } from "element-plus"; const name = ref<string>("home"); const info = reactive({ data: {}, }); const getData = () => { const param = { userID: "10001", }; getUserInfo(param).then((res: any) => { info.data = res.data; ElMessage.success("请求成功"); }); }; onMounted(() => {}); </script> <style lang="less" scoped></style>

根目录新建 .env.development .env.production , 等多个配置文件

.env.development
# 本地接口请求地址
VITE_BASE_API='/api'
.env.production
#生产
#接口请求地址
VITE_BASE_API=http://httpbin.org
你还可以继续建test测试啥的
package.json对应添加
"build": "run-p type-check build-dev",
"build:prod": "run-p type-check build-prod",
.....

requerst/index.ts对应修改

build不通命令打包不同环境
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。