1 创建vite项目和配置
1.1 创建vite项目并安装VSCode插件
npm create vue@latest
or
npm init vite@latest
这一指令将会安装并执行 create-vue,它是 Vue 官方的项目脚手架工具。你将会看到一些诸如 TypeScript 和测试支持之类的可选功能提示:
✔ Project name: … <your-project-name>
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
✔ Add Vitest for Unit testing? … No / Yes
✔ Add an End-to-End Testing Solution? … No / Cypress / Nightwatch / Playwright
✔ Add ESLint for code quality? … No / Yes
✔ Add Prettier for code formatting? … No / Yes
✔ Add Vue DevTools 7 extension for debugging? (experimental) … No / Yes
Scaffolding project in ./<your-project-name>...
Done.
npm init vite@latest shop-admin -- --template vue
cd shop-admin
npm install
npm run dev
- 查看registry:npm config get registry
- 设置registry:npm config set registry=https://registry.npmmirror.com
VSCode插件参考:
- Chinese Language Pack
- Vue3 Snippets
- Vue - Official
1.2 Element Plus安装及引用方法
兼容性2.5.0
Element Plus 支持最近两个版本的浏览器。
如果您需要支持旧版本的浏览器,请自行添加 Babel 和相应的 Polyfill 。
由于 Vue 3 不再支持 IE11,Element Plus 也不再支持 IE 浏览器。
版本 | Chrome | Edge | Firefox | Safari |
---|---|---|---|---|
< 2.5.0 | Chrome ≥ 64 | Edge ≥ 79 | Firefox ≥ 78 | Safari ≥ 12 |
2.5.0 + | Chrome ≥ 85 | Edge ≥ 85 | Firefox ≥ 79 | Safari ≥ 14.1 |
版本
Element Plus 目前还处于快速开发迭代中。
使用包管理器
我们建议您使用包管理器(如 NPM、Yarn 或 pnpm)安装 Element Plus,然后您就可以使用打包工具,例如 Vite 或 webpack。
# 选择一个你喜欢的包管理器
# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus
如果您的网络环境不好,建议使用相关镜像服务 cnpm 或 中国 NPM 镜像。
浏览器直接引入
直接通过浏览器的 HTML 标签导入 Element Plus,然后就可以使用全局变量 ElementPlus
了。
根据不同的 CDN 提供商有不同的引入方式, 我们在这里以 unpkg 和 jsDelivr 举例。 你也可以使用其它的 CDN 供应商。
unpkg
html
<head>
<!-- Import style -->
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css" />
<!-- Import Vue 3 -->
<script src="//unpkg.com/vue@3"></script>
<!-- Import component library -->
<script src="//unpkg.com/element-plus"></script>
</head>
jsDelivr
html
<head>
<!-- Import style -->
<link
rel="stylesheet"
href="//cdn.jsdelivr.net/npm/element-plus/dist/index.css"
/>
<!-- Import Vue 3 -->
<script src="//cdn.jsdelivr.net/npm/vue@3"></script>
<!-- Import component library -->
<script src="//cdn.jsdelivr.net/npm/element-plus"></script>
</head>
TIP
我们建议使用 CDN 引入 Element Plus 的用户在链接地址上锁定版本,以免将来 Element Plus 升级时受到非兼容性更新的影响。 锁定版本的方法请查看 unpkg.com。
由于原生的 HTML 解析行为的限制,单个闭合标签可能会导致一些例外情况,所以请使用双封闭标签, 参考
html
<!-- examples -->
<el-table>
<el-table-column></el-table-column>
<el-table-column></el-table-column>
</el-table>
完整引入:
// main.ts
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
1.3 引入windicss工具库和配置、安装代码提示
特性#
- ⚡️ 极速 - 在 Vite 中比 Tailwind 快20~100倍
- 🧩 按需使用 CSS 工具类(与 Tailwind CSS v2 完全兼容)
- 📦 按需使用原生元素样式重置(预检样式)
- 🔥 模块热替换 (HMR)
- 🍃 从
tailwind.config.js
加载配置 - 🤝 与框架无关 - Vue、 React、Svelte and vanilla!
- 📄 CSS
@apply
/@screen
指令转换(也适用于 Vue SFC 的<style>
) - 🎳 支持变量组 - 如
bg-gray-200 hover:(bg-gray-100 text-red-300)
- 😎 "Devtools 设计" - 支持传统的 Tailwind 运行方式
安装#
安装相关包:
npm i -D vite-plugin-windicss windicss
然后,在你的 Vite 配置中添加插件:
vite.config.js
import WindiCSS from 'vite-plugin-windicss'
export default {
plugins: [
WindiCSS(),
],
}
最后,在你的 Vite 入口文件中导入 virtual:windi.css
:
main.js
import 'virtual:windi.css'
现在可以在你的应用中开始使用 classes 工具类 或者 CSS 指令 ,感受一下速度吧!⚡️
VSCode插件参考:
- WindiCSS
1.4 windicss小案件和@apply简化代码
App.vue:
<!-- App.vue -->
<script setup>
</script>
<template>
<button class="btn">我的个性化按钮</button>
<!-- <div class="mb-4">
<el-button round>Round</el-button>
<el-button type="primary" round>Primary</el-button>
<el-button type="success" round>Success</el-button>
<el-button type="info" round>Info</el-button>
<el-button type="warning" round>Warning</el-button>
<el-button type="danger" round>Danger</el-button>
</div> -->
</template>
<style>
.btn{
@apply bg-purple-500 text-indigo-50 px-4 py-4 rounded-full
hover:(bg-purple-900) transition-all duration-500 focus:(ring-8 ring-purple-900);
}
</style>
1.5 引入vue-router4
官方链接:Installation | Vue Router 介绍 | Vue Router
安装vue-rounter4
npm install vue-router@4
1.6 配置路由和404页面捕获
1.6.1 在src下新建pages文件夹,在此pages文件夹下分别新建
index.vue
about.vue
404.vue
1.6.2 在src下新建文件夹router,在router下新建index.js。index.js:
//index.js
import {
createRouter,
createWebHashHistory
} from 'vue-router'
import Index from '@/pages/index.vue'
import About from '@/pages/about.vue'
import NotFound from '@/pages/404.vue'
const routes =[{
path:"/",
component:Index
},{
path:"/about",
component:About
},{
path:"/:pathMatch(.*)*",
name:"NotFound",
component:NotFound
}]
const router = createRouter({
history:createWebHashHistory(),
routes
})
export default router
1.6.3 配置main.js
// import './assets/main.css'
import { createApp } from 'vue'
import App from './App.vue'
import ElementPlus from 'element-plus' //引入element-plus
import 'element-plus/dist/index.css'
import router from './router' //增加路由包导入
const app = createApp(App)
app.use(ElementPlus) //装载ElementPlus
app.use(router) //装载路由
import 'virtual:windi.css'
app.mount('#app')
1.6.4 配置vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import WindiCSS from 'vite-plugin-windicss'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
WindiCSS(),
],
server:{host:'0.0.0.0'}, //增加局域网访问
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
// 有的关于resolve.alias如下:
// resolve:{
// alias:{
// "~":path.resolve(_dirname,"src")
// }
// },
此文件已配置:WindiCSS、局域网访问(server)、reslove.alias(使用@代表根目录)
完善404.vue的内容:
<script>
</script>
<template>
<el-result
icon="error"
title="404错误提示"
sub-title="你找的网页丢失了?还是你输错了网址?"
>
<template #extra>
<el-button type="success" @click="$router.push('/')">返回首页</el-button>
<el-button type="primary" onclick="history.back()">返回上一页</el-button>
<el-button type="primary" @click="$router.push('/about')">关于我们</el-button>
</template>
</el-result>
</template>
<style>
</style>
1.6.5 在App.vue中更新代码<router-view>
<template>
<router-view></router-view>
</template>
2 登录页开发
2.1 登录页源码
<template>
<el-row class="min-h-screen bg-indigo-500">
<el-col :span="16" class="flex items-center justify-center flex-col">
<div>
<div class="font-bold text-5xl text-light-50 mb-4">欢迎光临</div>
<div class="text-light-50">此站点是《Vue3+Vite实战商城后台开发》视频课程的演示地址</div>
</div>
</el-col>
<el-col :span="8" class="bg-light-50 flex items-center justify-center flex-col">
<h2 class="font-bold text-3xl text-gray-800">欢迎回来</h2>
<div class="flex items-center justify-center my-5 text-gray-300 space-x-2">
<span class="h-[1px] w-16 bg-gray-200"></span>
<span>账号密码登录</span>
<span class="h-[1px] w-16 bg-gray-200"></span>
</div>
<el-form :model="form" class="w-[250px]">
<el-form-item label="账号:">
<el-input v-model="form.username" placeholder="请输入用户名"/>
</el-form-item>
<el-form-item label="密码:">
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" round class="w-[250px] duration" color="#626aef">登录</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<script setup>
import { reactive } from 'vue'
// do not use same name with ref
const form = reactive({
username: '',
password: ''
})
const onSubmit = () => {
console.log('submit!')
}
</script>
2.2 登录页响应式处理:
将
<el-col :span="16" ……>
<el-col :span="8" ……>
分别修改为:
<el-col :lg="16" :md="12" ……>
<el-col :lg="8" :md="12" ……>
响应式处理后源码如下:
<template>
<el-row class="min-h-screen bg-indigo-500">
<el-col :lg="16" :md="12" class="flex items-center justify-center flex-col">
<div>
<div class="font-bold text-5xl text-light-50 mb-4">欢迎光临</div>
<div class="text-light-50">此站点是《Vue3+Vite实战商城后台开发》视频课程的演示地址</div>
</div>
</el-col>
<el-col :lg="8" :md="12" class="bg-light-50 flex items-center justify-center flex-col">
<h2 class="font-bold text-3xl text-gray-800">欢迎回来</h2>
<div class="flex items-center justify-center my-5 text-gray-300 space-x-2">
<span class="h-[1px] w-16 bg-gray-200"></span>
<span>账号密码登录</span>
<span class="h-[1px] w-16 bg-gray-200"></span>
</div>
<el-form :model="form" class="w-[250px]">
<el-form-item label="账号:">
<el-input v-model="form.username" placeholder="请输入用户名"/>
</el-form-item>
<el-form-item label="密码:">
<el-input v-model="form.password" type="password" placeholder="请输入密码"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" round class="w-[250px] duration" color="#626aef">登录</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<script setup>
import { reactive } from 'vue'
// do not use same name with ref
const form = reactive({
username: '',
password: ''
})
const onSubmit = () => {
console.log('submit!')
}
</script>
2.3 全局引入图标
2.3.1 安装
使用包管理器
shell
# 选择一个你喜欢的包管理器
# NPM
$ npm install @element-plus/icons-vue
# Yarn
$ yarn add @element-plus/icons-vue
# pnpm
$ pnpm install @element-plus/icons-vue
2.3.2 注册所有图标
您需要从 @element-plus/icons-vue
中导入所有图标并进行全局注册。
main.js/main.ts:
// main.ts
// 如果您正在使用CDN引入,请删除下面一行。
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// const app = createApp(App) 此行在目前的配置中已经存在,请注意以下两行一定放在const app = createApp(App)之后
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
2.3.3 用户名输入框增加User图标
<el-input v-model="form.username" placeholder="请输入用户名">
<template #prefix>
<el-icon><user /></el-icon>
</template>
</el-input>
2.3.4 密码框增加Lock图标
<el-input v-model="form.password" placeholder="请输入密码" show-password>
<template #prefix>
<el-icon><lock /></el-icon>
</template>
</el-input>
2.4 结合@apply实现样式抽离
2.4.1 login.vue抽离前所有代码:
<template>
<el-row class="min-h-screen bg-indigo-500">
<el-col :lg="16" :md="12" class="flex items-center justify-center flex-col">
<div>
<div class="font-bold text-5xl text-light-50 mb-4">欢迎光临</div>
<div class="text-light-50">此站点是《Vue3+Vite实战商城后台开发》视频课程的演示地址</div>
</div>
</el-col>
<el-col :lg="8" :md="12" class="bg-light-50 flex items-center justify-center flex-col">
<h2 class="font-bold text-3xl text-gray-800">欢迎回来</h2>
<div class="flex items-center justify-center my-5 text-gray-300 space-x-2">
<span class="h-[1px] w-16 bg-gray-200"></span>
<span>账号密码登录</span>
<span class="h-[1px] w-16 bg-gray-200"></span>
</div>
<el-form :model="form" class="w-[250px]">
<el-form-item>
<el-input v-model="form.username" placeholder="请输入用户名">
<template #prefix>
<el-icon><user /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input v-model="form.password" placeholder="请输入密码" show-password>
<template #prefix>
<el-icon><lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button round color="#626aef" class="w-[250px]" type="primary" @click="onSubmit">登 录</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<script setup>
import { reactive } from 'vue'
// do not use same name with ref
const form = reactive({
username: '',
password: ''
})
const onSubmit = () => {
console.log('submit!')
}
</script>
2.4.2 login.vue抽离后所有代码:
<template>
<el-row class="login-container">
<el-col :lg="16" :md="12" class="login-left">
<div>
<div>欢迎光临</div>
<div>此站点是《Vue3+Vite实战商城后台开发》视频课程的演示地址</div>
</div>
</el-col>
<el-col :lg="8" :md="12" class="login-right">
<h2>欢迎回来</h2>
<div>
<span class="login-line"></span>
<span>账号密码登录</span>
<span class="login-line"></span>
</div>
<el-form :model="form" class="login-form-width">
<el-form-item>
<el-input v-model="form.username" placeholder="请输入用户名">
<template #prefix>
<el-icon><user /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-input v-model="form.password" placeholder="请输入密码" show-password>
<template #prefix>
<el-icon><lock /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button round color="#626aef" class="login-form-width" type="primary" @click="onSubmit">登 录</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<script setup>
import { reactive } from 'vue'
// do not use same name with ref
const form = reactive({
username: '',
password: ''
})
const onSubmit = () => {
console.log('submit!')
}
</script>
<style scoped>
.login-container{
@apply min-h-screen bg-indigo-500;
}
.login-container .login-left,.login-container .login-right{
@apply flex items-center justify-center flex-col
}
.login-container .login-right{
@apply bg-light-50 flex-col
}
.login-left>div>div:first-child{
@apply font-bold text-5xl text-light-50 mb-4;
}
.login-left>div>div:last-child{
@apply text-gray-200 text-sm;
}
.login-right>h2{
@apply font-bold text-3xl text-gray-800;
}
.login-right>div{
@apply flex items-center justify-center my-5 text-gray-300 space-x-2;
}
.login-line{
@apply h-[1px] w-16 bg-gray-200;
}
.login-form-width{
@apply w-[250px];
}
</style>
2.5 <script setup>语法糖和组合式API
<script setup>
<script setup>
是在单文件组件 (SFC) 中使用组合式 API 的编译时语法糖。当同时使用 SFC 与组合式 API 时该语法是默认推荐。相比于普通的 <script>
语法,它具有更多优势:
- 更少的样板内容,更简洁的代码。
- 能够使用纯 TypeScript 声明 props 和自定义事件。
- 更好的运行时性能 (其模板会被编译成同一作用域内的渲染函数,避免了渲染上下文代理对象)。
- 更好的 IDE 类型推导性能 (减少了语言服务器从代码中抽取类型的工作)。
官方链接:
<script setup> | Vue.js (vuejs.org)
2.6 登录表单验证处理
2.6.1 为<el-form>标签增加属性:ref和:rules
ref="formRef"
:rules="rules"
单句完整内容如下:
<el-form ref="formRef" :rules="rules" :model="form" class="login-form-width">
2.6.2 为el-input的上一级的<el-form-item>添加 prop属性并赋值
<el-form-item prop="username">
<el-form-item prop="password">
完整语句如下:
<el-form-item prop="username">
<el-input v-model="form.username" prefix-icon="user" placeholder="请输入用户名" >
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input :rules="rules" v-model="form.password" placeholder="请输入密码" show-password type="password" >
<template #prefix>
<el-icon>
<lock />
</el-icon>
</template>
</el-input>
</el-form-item>
2.6.3 为使用ref增加import引用并定义
将原来的
import { reactive } from 'vue'
修改为
import { reactive,ref } from 'vue'
const formRef = ref(null) //让formRef类型为响应式
2.6.4 定义rules规则
const rules = {
username:[
{
required: true, //必须输入
message: '用户名不能为空', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},{
min: 2, //最小长度限定为2
max: 15,//最大长度限定为15
message: '用户名长度必须是2至15位字符', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},
],
password:[
{
required: true, //必须输入
message: '密码不能为空', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},{
min: 6, //最小长度限定为6
max: 15, //最大长度限定为15
message: '密码长度必须是6至15位字符', ///验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
}
],
}
2.6.5 为表单提交增加表单验证功能
如果不做此步骤的设定,虽然分别点击“用户名”、“密码”输入框等有失焦验证,但直接点击“登录”按钮将无失焦验证触发。
将以下代码:
// 修改前:无提交表单验证的原来的代码
const onSubmit = () => {
console.log('submit!')
}
// 修改后
const onSubmit = () => {
formRef.value.validate((valid)=>{ //valide用于接收触发验证的结果:true代表验证无错误,false代表验证有错误。
if(!valid){
return false //返回验证失败,页面不做处理
}
console.log("验证通过"); //验证通过,可做进一步功能实现
})
}
2.7 引入axios请求库和登录接口交互
易用、简洁且高效的http库——axios 官方网址:http://www.axios-js.com/zh-cn/
2.7.1 安装
npm install axios
2.7.2 创建实例
在src文件夹下新建文件axios.js:
import axios from 'axios'
const service = axios.create({
baseURL:"/api"
})
export default service
在src文件夹下新建文件夹api,在此api文件夹下新建manager.js:
import axios from "axios"
export function login(username,password){
return axios.post("/admin/login",{
username,
password
})
}
2.7.3 完善引用和代码
login.vue中增加引用及代码:
import { ElNotification } from 'element-plus' //element通知引用
import { login } from '@/api/manager'
import { useRouter } from 'vue-router'
const router = useRouter()
原来的const onSubmit代码修改为:
const onSubmit = () => {
formRef.value.validate((valid)=>{ //valide用于接收触发验证的结果:true代表验证无错误,false代表验证有错误。
if(!valid){
return false //返回验证失败,页面不做处理
}
login(form.username,form.password)
.then(res=>{
console.log(res.data.data);
//提示登录成功
ElNotification({
message: "登录成功",
type: "success",
duration: 3000
})
// 存储token和用户相关信息
// 跳转到后台首页
router.push("/")
})
.catch(err=>{
ElNotification({
message: err.response.data.msg || "请求失败",
type: "error",
duration: 3000
})
})
})
}
vite.config.js文件中原来的:
server:{host:'0.0.0.0'}, //局域网可访问
修改为:
server: {
host: '0.0.0.0', //局域网可访问
open: '#/login', //自动打开登录页面
proxy: {
// 选项写法
'/api': {
target: 'http://ceshi13.dishait.cn',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
}
},
2.8 引入cookie存储用户token
2.8.1 安装vueuse/integrations和useCookies相关引用
npm i @vueuse/integrations //安装vueuse基础
npm i universal-cookie //安装useCookies
2.8.2 为pages/index.vue录入cookie相关代码做测试:
<script setup>
import { useCookies } from '@vueuse/integrations/useCookies'
const cookie = useCookies()
console.log(cookie)
function cookieSet(){
cookie.set("admin-token", "wzs888888")
}
function cookieGet(){
console.log(cookie.get("admin-token"))
}
function cookieRemove(){
cookie.remove("admin-token")
}
</script>
<template>
<div class="px-5 py-5">
后台首页
<el-button @click="cookieSet">设置</el-button>
<el-button @click="cookieGet">读取</el-button>
<el-button @click="cookieRemove">删除</el-button>
</div>
</template>
<style>
</style>
2.8.3 为login.vue增加cookie相关功能处理
增加:
import { useCookies } from '@vueuse/integrations/useCookies' //增加引用
/// …………………… ///
// 存储token和用户相关信息
const cookie = useCookies()
cookie.set("admin-token", res.data.data)
// 跳转到后台首页
router.push("/")
/// …………………… ///
附login.vue全文代码:
<template>
<el-row class="login-container">
<el-col :lg="16" :md="12" class="login-left">
<div>
<div>欢迎使用</div>
<div>Vue3+Vite实战商城后台管理系统</div>
</div>
</el-col>
<el-col :lg="8" :md="12" class="login-right">
<h2>欢迎回来</h2>
<div>
<span class="login-line"></span>
<span>账号密码登录</span>
<span class="login-line"></span>
</div>
<el-form ref="formRef" :rules="rules" :model="form" class="login-form-width">
<el-form-item prop="username">
<el-input v-model="form.username" prefix-icon="user" placeholder="请输入用户名" >
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input :rules="rules" v-model="form.password" placeholder="请输入密码" show-password type="password" >
<template #prefix>
<el-icon>
<lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button round color="#626aef" class="login-form-width" type="primary" @click="onSubmit">登 录</el-button>
</el-form-item>
</el-form>
<div>
<span class="login-line"></span>
<span>Version 1.0.20240822</span>
<span class="login-line"></span>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { reactive,ref } from 'vue'
import { ElNotification } from 'element-plus'
import { login } from '@/api/manager'
import { useRouter } from 'vue-router'
import { useCookies } from '@vueuse/integrations/useCookies'
const router = useRouter()
// do not use same name with ref
const form = reactive({
username: '',
password: ''
})
const formRef = ref(null) //让formRef类型为响应式
const rules = {
username:[
{
required: true, //必须输入
message: '用户名不能为空', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},{
min: 2, //最小长度限定为2
max: 15,//最大长度限定为15
message: '用户名长度必须是2至15位字符', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},
],
password:[
{
required: true, //必须输入
message: '密码不能为空', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},{
min: 6, //最小长度限定为6
max: 15, //最大长度限定为15
message: '密码长度必须是6至15位字符', ///验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
}
],
}
const onSubmit = () => {
formRef.value.validate((valid)=>{ //valide用于接收触发验证的结果:true代表验证无错误,false代表验证有错误。
if(!valid){
return false //返回验证失败,页面不做处理
}
login(form.username,form.password)
.then(res=>{
console.log(res.data.data);
//提示登录成功
ElNotification({
message: "登录成功",
type: "success",
duration: 3000
})
// 存储token和用户相关信息
const cookie = useCookies()
cookie.set("admin-token", res.data.data)
// 跳转到后台首页
router.push("/")
})
.catch(err=>{
ElNotification({
message: err.response.data.msg || "请求失败",
type: "error",
duration: 3000
})
})
})
}
</script>
<style scoped>
.login-container {
@apply min-h-screen bg-indigo-500;
}
.login-container .login-left,
.login-container .login-right {
@apply flex items-center justify-center flex-col
}
.login-container .login-right {
@apply bg-light-50 flex-col
}
.login-left>div>div:first-child {
@apply font-bold text-5xl text-light-50 mb-4;
}
.login-left>div>div:last-child {
@apply text-gray-200 text-sm;
}
.login-right>h2 {
@apply font-bold text-3xl text-gray-800;
}
.login-right>div {
@apply flex items-center justify-center my-5 text-gray-300 space-x-2;
}
.login-line {
@apply h-[1px] w-16 bg-gray-200;
}
.login-form-width {
@apply w-[250px];
}
</style>
2.9 请求拦截器和响应拦截器(axios)
安装axios:
npm install axios
axios.js全代码:
import { useCookies } from '@vueuse/integrations/useCookies';
import axios from 'axios'
import { ElNotification } from 'element-plus';
import {
getToken
} from '@/composables/auth'
const service = axios.create({
baseURL:"/api"
})
// 添加请求拦截器
service.interceptors.request.use(function (config){
// 在发送请求之前做些什么
// 例:往header头自动添加token
const cookie = useCookies()
const token = getToken()
if(token){
config.headers["token"] = token
}
return config;
},
function(error){
// 对请求错误做些什么
return Promise.reject(error)
})
// 添加响应拦截器
service.interceptors.response.use(function(response){
// 对响应数据做点什么
return response.data.data;
},
function(error){
// 对响应错误做些什么
return Promise.reject(error)
})
export default service
src/api/manager.js全代码:
import axios from "@/axios"
export function login(username,password){
return axios.post("/admin/login",{
username,
password
})
}
export function getinfo(){
return axios.post("/admin/getinfo")
}
login.vue全代码:
<template>
<el-row class="login-container">
<el-col :lg="16" :md="12" class="login-left">
<div>
<div>欢迎使用</div>
<div>Vue3+Vite实战商城后台管理系统</div>
</div>
</el-col>
<el-col :lg="8" :md="12" class="login-right">
<h2>欢迎回来</h2>
<div>
<span class="login-line"></span>
<span>账号密码登录</span>
<span class="login-line"></span>
</div>
<el-form ref="formRef" :rules="rules" :model="form" class="login-form-width">
<el-form-item prop="username">
<el-input v-model="form.username" prefix-icon="user" placeholder="请输入用户名" @keyup.enter.native="onFocusPassword">
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input :rules="rules" v-model="form.password" placeholder="请输入密码" @keyup.enter.native="onSubmit" show-password type="password" ref="pwd" >
<template #prefix>
<el-icon>
<lock />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item>
<el-button round color="#626aef" class="login-form-width" type="primary" @click="onSubmit" :loading="loading">登 录</el-button>
</el-form-item>
</el-form>
<div>
<span class="login-line"></span>
<span>Version 1.0.20240822</span>
<span class="login-line"></span>
</div>
</el-col>
</el-row>
</template>
<script setup>
import { reactive,ref } from 'vue'
import { ElNotification } from 'element-plus'
import { login,getinfo } from '@/api/manager'
import { useRouter } from 'vue-router'
import { useCookies } from '@vueuse/integrations/useCookies'
const router = useRouter()
// do not use same name with ref
const form = reactive({
username: '',
password: ''
})
const formRef = ref(null) //让formRef类型为响应式
const loading = ref(false)
const pwd = ref(null)
const onFocusPassword = ()=>{
pwd.value.focus();
}
const rules = {
username:[
{
required: true, //必须输入
message: '用户名不能为空', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},{
min: 2, //最小长度限定为2
max: 15,//最大长度限定为15
message: '用户名长度必须是2至15位字符', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},
],
password:[
{
required: true, //必须输入
message: '密码不能为空', //验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
},{
min: 5, //最小长度限定为5
max: 15, //最大长度限定为15
message: '密码长度必须是5至15位字符', ///验证不符条件时的提示信息
trigger: 'blur' //触发为失去焦点时
}
],
}
const onSubmit = () => {
formRef.value.validate((valid)=>{ //valide用于接收触发验证的结果:true代表验证无错误,false代表验证有错误。
if(!valid){
return false //返回验证失败,页面不做处理
}
loading.value = true
login(form.username,form.password)
.then(res=>{
console.log(res);
//提示登录成功
ElNotification({
message: "登录成功",
type: "success",
duration: 3000
})
// 存储token
const cookie = useCookies()
cookie.set("admin-token",res.token)
// 获取用户相关信息
getinfo().then(res2 => {
console.log(res2)
})
// 跳转到后台首页
router.push("/")
})
})
}
</script>
<style scoped>
.login-container {
@apply min-h-screen bg-indigo-500;
}
.login-container .login-left,
.login-container .login-right {
@apply flex items-center justify-center flex-col
}
.login-container .login-right {
@apply bg-light-50 flex-col
}
.login-left>div>div:first-child {
@apply font-bold text-5xl text-light-50 mb-4;
}
.login-left>div>div:last-child {
@apply text-gray-200 text-sm;
}
.login-right>h2 {
@apply font-bold text-3xl text-gray-800;
}
.login-right>div {
@apply flex items-center justify-center my-5 text-gray-300 space-x-2;
}
.login-line {
@apply h-[1px] w-16 bg-gray-200;
}
.login-form-width {
@apply w-[250px];
}
</style>
细心的亲会发现在此login.vue代码中给“登录”按钮增加了一个loading效果:
-
初始化一个ref类型的逻辑常量loading值为假:loadingconst loading = ref(false)
-
验证表单后提交时即将loading值改为真:loading.value = true
-
在.catch尾部增加:.finally(()=>{ loading.value = false }),将loading值改为假
-
为“登录”按钮增加:loading="loading"属性
2.10 常用工具库封装
在src下新建文件夹composables,在此composables下新建文件:auth.js和util.js。
auth.js文件内容如下:
import { useCookies } from "@vueuse/integrations/useCookies"
const TokenKey = "admin-token"
const cookie = useCookies()
// 获取token
export function getToken(){
return cookie.get(TokenKey)
}
// 设置token
export function setToken(){
return cookie.set(TokenKey,token)
}
// 清除token
export function removeToken(){
return cookie.remove(TokenKey)
}
util.js文件内容如下:
import { ElNotification } from 'element-plus'
// 消息提示
export function toast(message,type = "success",dangerouslyUseHTMLString = false){
ElNotification({
message,
type,
dangerouslyUseHTMLString,
duration:3000
})
}
如有使用到如上封装工具库(函数类)时引用此文件并调用该工具(函数)即可。
2.11 引入vuex状态管理用户信息
2.11.1 Vuex 是什么?
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
什么情况下我应该使用 Vuex?# Vuex 可以帮助我们管理共享状态,并附带了更多的概念和框架。这需要对短期和长期效益进行权衡。 如果您不打算开发大型单页应用,使用 Vuex 可能是繁琐冗余的。确实是如此——如果您的应用够简单,您最好不要使用 Vuex。一个简单的 store 模式就足够您所需了。但是,如果您需要构建一个中大型单页应用,您很可能会考虑如何更好地在组件外部管理状态,Vuex 将会成为自然而然的选择。引用 Redux 的作者 Dan Abramov 的话说就是: Flux 架构就像眼镜:您自会知道什么时候需要它。
2.11.2 安装
npm install vuex@next --save
2.11.3 开始
在 Scrimba 上尝试这节课 每一个 Vuex 应用的核心就是 store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态 (state)。Vuex 和单纯的全局对象有以下两点不同: Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。 你不能直接改变 store 中的状态。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
在src文件夹新建文件夹store,在此store文件夹下新建文件index.js:
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state(){
return {
// 用户信息
user: {}
}
},
mutations: {
// 记录用户信息
SET_USERINFO(state,user){
state.user = user
}
}
})
export default store
在main.js文件中增加引用及use:
// …………
import store from './store'
// …………
app.use(store)
// …………
src\pages\index.vue全代码修改为:
<script setup>
import { useCookies} from '@vueuse/integrations/useCookies'
const cookie = useCookies()
console.log(cookie)
function cookieSet(){
cookie.set("admin-token","wzs888888")
}
function cookieGet(){
console.log(cookie.get("admin-token"))
}
function cookieRemove(){
cookie.remove("admin-token")
}
</script>
<template>
<div>
后台首页 {{ $store.state.user }} // 特别强调下这里调用出user的数据,但因为还未设置全局路由截拦,页里刷新后此处的$store.state.user将无内容显示。
</div>
</template>
2.12 全局路由截拦实现登录判断——router 导航守卫:全局前置守卫
router 导航守卫链接:导航守卫 | Vue Router
导航守卫:正如其名,vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航。这里有很多方式植入路由导航中:全局的,单个路由独享的,或者组件级的。
在src文件夹新建文件permission.js:
import router from "./router"
import { getToken } from "./composables/auth"
import { toast } from "@/composables/util"
// 全局前置守卫
router.beforeEach((to,from,next)=>{
const token = getToken()
// 没有登录,强制跳转至登录页
if(!token && to.path != "/login"){
toast('请先登录','error')
return next({ path: "/login" })
}
// 防止重复登录
if( token && to.path == "/login"){
toast('请勿重复登录','error')
return next({ path: from.path ? from.path : '/' })
}
next()
})
同时,在main.js中引入permission.js:
import './permission'
2.13 登录功能/退出功能完善
完善util.js内容:
import { ElNotification,ElMessageBox } from 'element-plus'
// 消息提示
export function toast(message,type = "success",dangerouslyUseHTMLString = false){
ElNotification({
message,
type,
dangerouslyUseHTMLString,
duration:3000
})
}
// 提示对话框
export function showModal(content = "提示内容",type = "warning",title="警告"){
return ElMessageBox.confirm(
content,title,
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type,
}
)
}
在src\api\manager.js中追加:
export function logout(){
return axios.post('/admin/logout')
}
src\store\index.js:
import { removeToken, setToken } from '@/composables/auth'
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state(){
return {
// 用户信息
user: {}
}
},
mutations: {
// 记录用户信息
SET_USERINFO(state,user){
state.user = user
}
},
actions: {
//登录
login({ commit }, { username, password }){
return new Promise((resolve,reject)=>{
login(username,password).then(res=>{
setToken(res.token)
resolve(res)
}).catch(err=>reject(err))
})
},
// 获取当前登录用户信息
getinfo({ commit }){
return new Promise((resolve,reject)=>{
getinfo().then(res=>{
commit("SET_USERINFO",res)
resolve(res)
}).catch(err=>reject(err))
})
},
// 退出登录
logout({ commit }){
// 移除cookie里的token
removeToken()
// 清除当前用户状态 vuex
commit("SET_USERINFO",{})
}
}
})
export default store
src\pages\index.vue:
<template>
<div class="ml-8 mt-5">
<el-icon class="align-middle mr-2"><user /></el-icon>{{ $store.state.user.username}},欢迎你!
<el-button @click="handleLogout">退出登录</el-button>
</div>
</template>
<script setup>
import { logout } from '@/api/manager'
import { showModal,toast } from '@/composables/util'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
const router = useRouter()
const store = useStore()
function handleLogout(){
showModal("是否要退出登录?").then(res=>{
logout()
.finally(()=>{
// 执行退出动作
store.dispatch("logout")
// 跳转到登录页
router.push('/login')
// 提示退出成功
toast("退出登录成功")
})
})
}
</script>
2.14 全局loading进度条实现 —— nprogress:全局后置守卫
安装命令:
npm i nprogress
在main.js中追加:
import 'nprogress/nprogress.css'
在src\composables\util.js中追加:
// 显示全屏loading
export function showFullLoading(){
nprogress.start()
}
// 隐藏全屏loading
export function hideFullLoading(){
nprogress.done()
}
附src\composables\util.js完整代码:
import { ElNotification,ElMessageBox } from 'element-plus'
import nprogress from 'nprogress' //增加引入
// 消息提示
export function toast(message,type = "success",dangerouslyUseHTMLString = false){
ElNotification({
message,
type,
dangerouslyUseHTMLString,
duration:3000
})
}
// 提示对话框
export function showModal(content = "提示内容",type = "warning",title="警告"){
return ElMessageBox.confirm(
content,title,
{
confirmButtonText: '确认',
cancelButtonText: '取消',
type,
}
)
}
// 启用loading效果:显示全屏Nprogress loading
export function showFullLoading(){
nprogress.start()
}
// 关闭loading效果:隐藏全屏Nprogress loading
export function hideFullLoading(){
nprogress.done()
}
src\App.vue代码:
<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style>
/* 为Npgress的bar设置颜色和高度 */
#nprogress .bar{
background-color: #179963 !important;
height: 3px !important;
}
</style>
src\store\permission.js代码:
import router from "./router"
import { getToken } from "./composables/auth"
import {
toast,
showFullLoading,
hideFullLoading
} from "@/composables/util"
// 全局前置守卫
router.beforeEach((to,from,next)=>{
// 显示loading
showFullLoading()
const token = getToken()
// 没有登录,强制跳转至登录页
if(!token && to.path != "/login"){
toast('请先登录','error')
return next({ path: "/login" })
}
// 防止重复登录
if( token && to.path == "/login"){
toast('请勿重复登录','error')
return next({ path: from.path ? from.path : '/' })
}
next()
})
// 全局后置守卫
router.afterEach((to, from) =>{
// sendToAnalytics(to.fullPath)
hideFullLoading()
})
2.15 动态页面标题实现
2.15.1 在src\router\index.js中为每个模块增加 meta: { title: "页面描述" }:
//index.js
import {
createRouter,
createWebHashHistory
} from 'vue-router'
import Index from '@/pages/index.vue'
import About from '@/pages/about.vue'
import Login from '@/pages/login.vue'
import NotFound from '@/pages/404.vue'
const routes =[{
path:"/",
component:Index,
meta: { title: "后台首页" } // 这部分是为实现动态页面标题而添加的部分
},{
path:"/about",
component:About,
meta: { title: "关于我们" } // 这部分是为实现动态页面标题而添加的部分
},{
path:"/login",
component:Login,
meta: { title: "登录界面" } // 这部分是为实现动态页面标题而添加的部分
},{
path:"/:pathMatch(.*)*",
name:"NotFound",
component:NotFound
}]
const router = createRouter({
history:createWebHashHistory(),
routes
})
export default router
2.15.2 在src\permission.js中的全局前置路由守护 router.beforeEach 中 next() 之前添加:
// 设置页面标题
let title = (to.meta.title ? to.meta.title : "") + " - Vue管理系统"
document.title = title
附permission.js全部代码:
import router from "./router"
import { getToken } from "./composables/auth"
import {
toast,
showFullLoading,
hideFullLoading
} from "@/composables/util"
// 全局前置守卫
router.beforeEach((to,from,next)=>{
// 显示loading
showFullLoading()
const token = getToken()
// 没有登录,强制跳转至登录页
if(!token && to.path != "/login"){
toast('请先登录','error')
return next({ path: "/login" })
}
// 防止重复登录
if( token && to.path == "/login"){
toast('请勿重复登录','error')
return next({ path: from.path ? from.path : '/' })
}
// 设置页面标题 这部分是为实现动态页面标题而添加的部分
let title = (to.meta.title ? to.meta.title : "") + " - Vue管理系统"
document.title = title
next()
})
// 全局后置守卫
router.afterEach((to, from) =>{
// sendToAnalytics(to.fullPath)
hideFullLoading()
})
3 后台全局Layout布局开发
3.1 后台主布局实现
3.1.1 后台主布局文件:src\layouts\admin.vue
在src下新建layouts文件夹,并在此文件夹下新建文件admin.vue:
<template>
<el-container>
<el-header><fheader/></el-header>
<el-contanin>
<el-aside><fmenu/></el-aside>
<el-main>
<ftaglist/>
<router-view></router-view>
</el-main>
</el-contanin>
</el-container>
</template>
<script setup>
import fheader from './components/fheader.vue'
import fmenu from './components/fmenu.vue'
import ftaglist from './components/ftaglist.vue'
</script>
3.1.2 将admin在src\router\index.js引入并修改路由:
修改前:
const routes =[{
path:"/",
component:Index,
meta: { title: "后台首页" }
},{
path:"/about",
component:About,
meta: { title: "关于我们" }
},{
path:"/login",
component:Login,
meta: { title: "登录界面" }
},{
path:"/:pathMatch(.*)*",
name:"NotFound",
component:NotFound
}]
修改后:
const routes =[{
path:"/",
component:Admin,
children:[{
path:"/",
component:Index,
meta: { title: "后台首页" }
}]
},{
path:"/about",
component:About,
meta: { title: "关于我们" }
},{
path:"/login",
component:Login,
meta: { title: "登录界面" }
},{
path:"/:pathMatch(.*)*",
name:"NotFound",
component:NotFound
}]
后续完善头部fhead、侧边菜单fmenu、标签列表ftaglist等三个文件代码内容。
3.2 公共头部开发样式布局
……待续……