Vue3 后台管理
Vue3 后台管理
Echo1.项目初始化及使用的技术
vite 为我们提供了一个快速的开发工具,让我们能够更加迅速地构建web项目。
模块式更新,闪电般的冷启动
npm 切换为国内的镜像(目前不知道是否还能使用)
先查看当前的镜像
1
npm config get registry
1
npm config set registry https://registry.npmmirror.com
使用vite 创建项目
1
npm create vite@latest shop-back -- --template vue
进入项目 并安装依赖
1
2
3cd shop-back
npm install
npm run dev项目目录介绍
项目目录和vue2的项目目录无差别
插件
中文插件
vloar 语法提示插件
vue3snippets vue3语法提示插件
1.1Element Plus 基本使用
安装element plus 配置相关的样式库
1
npm install element-plus --save
配置element plus (练习项目中直接使用全部引入)
/src/main1
2
3
4
5
6
7
8
9
10
11
12
13import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// 引入element plus 及样式库
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')检验一下Element Plus 是否安装好了
1.2Windicss Css框架 及小案例
安装windicss 及官方库
1
npm i -D vite-plugin-windicss windicss
配置
/src/vite.config.js
1
2
3
4
5
6
7
8
9
10
11import { 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()],
})/src/mian.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
// 引入element plus 及样式库
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// windicss------------------------------
import 'virtual:windi.css'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')windicss 小案例
1
2
3
4
5
6
7
8
9
10
11<template>
<HelloWorld msg="Vite + Vue" />
<button class="btn">按钮</button>
</template>
<style scoped>
.btn{
@apply w-12 h-12 bg-blue-600 text-dark-700 hover:(bg-red-500 text-gray-50 shadow-xl shadow-dark-600 border-dark-900 border-4);
}
</style>
1.3 VueRouter
安装
1
npm install vue-router@4
引入并配置
/src/router/index.js
1 | // 1. 引入 |
/mian.js
1 | // 引入router |
1.4 配置文件系统路径别名
基本90%的文件都是src目录下所以需要为src配置一个别名
vite.config.js
1 | // 配置别名 |
配置路由
新建一个view 或者pages的目录用于存储页面然后在vuerouter中进行配置
/scr/Router/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 1. 引入
import {createRouter,createWebHashHistory} from "vue-router"
import Home from "@/View/index.vue"
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
const routes = [
{path:'/',component:Home}
// { path: '/', component: Home },
// { path: '/about', component: About },
]
// 3. 创建路由实例并传递 `routes` 配置
const router = createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history:createWebHashHistory(),
routes, // `routes: routes` 的缩写
})
export default router/App.vue
1
2
3
4
5
6
7
8
9
10
11<script setup>
</script>
<template>
<router-view></router-view>
</template>
<style scoped>
</style>
404 页面
vue-router 配置404页面
src/page/404.vue
1 404 页面 根据自己需求来router/index.js
1
2
3
4
5
6
7
8 const routes = [
{ path: '/', component: index },
// 配置404页面
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: error },
]
2. 登录页
2.1登录静态页面及响应式的处理
1 | <template> |
2.3登录页图标的引入
下载icon图标
1
npm install @element-plus/icons-vue
main.js 中引入icon图标
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// 引入icon
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// 创建并挂载实例
const app = createApp(App)
// icon图标
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(ElementPlus)
app.use(router)
app.mount('#app')input 中使用icon图标 引入使用
1
2
3
4
5//使用
<el-input v-model="form.username" class="input" placeholder="请输入账号" :prefix-icon="User"></el-input>
//引入
import { User, Lock } from '@element-plus/icons-vue'
2.3登录时表单校验处理
from 中定义一个属性 rules,属性值就是具体的表单校验规则
定义对应的表单校验规则
form-item 中通过prop来接受对应的表单校验规则进行表单的校验
1
2
3
4
5
6
7
8
9
10
11
12//1 ---:rules="rules" roues 就是对应的表单校验规则
<el-form :model="form" ref="formRef" :rules="rules" :inline="false" size="normal">
<el-form-item prop="username">
<el-input v-model="form.username" class="input" placeholder="请输入账号" :prefix-icon="User"></el-input>
</el-form-item>
//3. prop 是指定对应的规则
<el-form-item prop="password">
<el-input v-model="form.password" class="input2" placeholder="请输入密码" :prefix-icon="Lock"></el-input>
</el-form-item>
</el-form>
<el-button type="primary" size="default" @click="submit" class="button">登录</el-button>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
```js
// 2. 表单校验规则
const rules = reactive({
username: [
// 必填项 提示信息 触发方式
{ required: true, message: '请输入账号', trigger: 'blur' },
// 长度
// 最小 提示信息 触发方式
{ min: 3, max: 10, message: '账号为3-10个字符', trigger: 'blur' },
],
password: [
// 必填项 提示信息 触发方式
{ required: true, message: '请输入密码', trigger: 'blur' },
// 长度
// 最小 提示信息 触发方式
{ min: 3, max: 10, message: '账号为3-10个字符', trigger: 'blur' },
]
})
这一块坑非常多,简单跟大家总结几个
el-form 进行数据绑定的时候是用的 :model=”form” 写的时候可能会写成v-model
1
2
3
4
5
6
7
8 v-model:
双向数据绑定: v-model 主要用于实现表单输入元素(如 <input>、<textarea>、<select>)的双向数据绑定。这意味着它不仅将数据从 Vue 实例传递到视图(HTML),还自动同步用户在界面上的输入回 Vue 实例中。当表单控件的值发生变化时,与之绑定的数据项也会自动更新,反之亦然。
组件中的使用: 在自定义组件中,通过在组件上使用 v-model,可以实现父子组件间的数据双向绑定。这要求组件内部正确使用 model 选项和相应的事件(通常是 input 事件)来处理数据更新。
:model:
简写形式: :model 是 v-bind:model 的简写形式,用于单向数据绑定,即将父组件的数据传递给子组件作为属性。这意味着数据流动是从父组件到子组件,但不会自动从子组件传回父组件。
只读传递: 当你使用 :model 或 v-bind:model 时,你是在设置一个属性的初始值,之后子组件内部对这个值的更改不会反映回父组件,除非子组件显式地通过事件触发来通知父组件。本质上来讲一个是单向的数据绑定,一个是双向的数据绑定,但是在element plus中 el-form进行的是单向的数据绑定,意思是只读数据,只读数据的原因是,它拿到数据了以后,会将数据的名称和rules 进行比价,并不需要改变原来的值
el-item 中是进行的双向的数据绑定,也就是说用的是v-model
prop=”校验规则名称” 校验规则的名称必须和v-model 绑定的数据的名称一致
校验规则只是对用户的输入进行校验,就算没通过也是能直接进行登录的,所以这个问题还需要我们自己解决
2.4判断表单校验规则是否通过
通过ref 拿到form节点
form节点身上有一个方法validate,该方法接收一个回调函数,函数接受三个参数,分别为:
1
2
3
4
5prop: FormItemProp:表单项的属性名称,与表单模型中的字段相对应。在Element UI或Element Plus中,这是用来标识表单字段的键值,用于确定是哪个字段的校验规则触发了此回调。
isValid: boolean:表示当前校验是否通过。如果校验成功,则为true;反之,如果校验失败,则为false。
message: string:当校验不通过时,需要向用户展示的错误消息。这个字符串可以自定义,以便提供具体的错误反馈。判断校验是否通过
1
2
3
4
5
6
7
8
9
10
11
12const userform=ref(null)
const submit=()=>{
userform.value.validate((isValid)=>{
if(!isValid){
console.log(isValid);
return false
}else{
alert("欢迎登录")
}
})
}
2.5 登录页前后端交互
安装axios
1
npm install axios
配置封装axios
/src/utlis/axios.js
1
2
3
4
5
6
7
8import axios from "axios";
const instance = axios.create({
baseURL: 'http://ceshi13.dishait.cn',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
export default instance登录接口
/src/api/manager.js
1
2
3
4
5
6
7
8
9import axios from "@/utils/axios.js"
// 登录接口
export const login=(username, password)=>{
axios.post('/admin/login',{
username,
password
})
}点击登录发送请求
1
2
3
4
5
6
7
8
9
10
11
12
13
14const submit = () => {
userform.value.validate((isValid) => {
if (!isValid) {
console.log(isValid);
return false
} else {
login(form.userName, form.passWord).then((result) => {
}).catch((err) => {
})
}
})
}跨域
跨域问题:
同源策略:同源策略是浏览器的,是浏览器的安全策略。对同源请求放行(自己人),对异源做限制。同源指:网络协议相同,域名相同,端口相同,不考虑路径。
同源请求:页面向外面发送各种各样的请求,当页面源和目标源相同的情况下就是同源请求。如css js 请求这种,但是css img 这种基本不会造成安全问题,所以一般不做限制。对网络请求发出的跨域会做出严格的限制。
页面通过Ajax等发送网络请求,请求服务器内容, 这个时候服务器会收到请求,并响应。但是响应的结果会被浏览器校验(类似于闸门),校验有两种情况,要么是通过,要么是不通过(跨域)
解决跨域:
jsonp:可以自己了解一下,本质上是把跨域问题绕过去了,并没有解决。
配置后端:浏览器是通过Access-control-xxxx这个字段来判断是否通过,有两种情况如果这个字段是“*” 意思就是允许所有请求,如果是一个域名,那么就是指允许这个域名的请求。 当然后端可以通过一些中间件,来配置某些某些域名是可以通过的。
前端配置代理服务器:设置一个中转服务器,前端向中转服务器发送请求,中转服务器,向后端转发请求。 前端请求的地址只需要和中转服务器保持一致就可以。
专业的服务器代理工具:nginx等
vite配置跨域
vite.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 配置别名
import path from "path"
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import WindiCSS from 'vite-plugin-windicss'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
alias: {
"@": path.resolve(__dirname, "src")
}
},
server: {
proxy: {
// 使用 '/api' 来代理域名
'/api': {
target: 'http://ceshi13.dishait.cn',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
},
}
},
plugins: [vue(), WindiCSS()],
})```
baseUrl的换成代理的
/src/utils/axios.js
1
2
3
4
5
6
7import axios from "axios";
const instance = axios.create({
baseURL: '/api',
timeout: 1000,
headers: {'X-Custom-Header': 'foobar'}
});
export default instance
这个时候你就不是再向后端的这个地址发送请求了,而是向/api发送请求,也就不存跨域问题了。
- 发送请求并获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const submit = () => {
userform.value.validate((isValid) => {
if (!isValid) {
console.log(isValid);
return false
} else {
login(form.userName, form.passWord)
.then((result) => {
console.log(result)
}).catch((err) => {
ElNotification({
title: err.response.data.msg,
type: 'info',
})
})
}
})
}
2.6登录成功
- 提示成功
- 存储用户信息和token
- 跳转
- 提示成功(同失败)
2.7 UseCookie
使用Vueuse中的us额Cookie来存储token
安装usecookie
1
2npm i @vueuse/integrations
npm i universal-cookie使用
第一步引入:import { useCookies } from ‘@vueuse/integrations/useCookies’
第二步实例化:const cookie = useCookies( )
第三步调用方法:
比如存: cookie .set("admin-token", res.data.data.token); 比如取: console.log(cookie .get("admin-token")); 比如删: cookie .remove("admin-token");
方法有:get, getAll, set, remove, addChangeListener,removeChangeL\
- 将token存入到Cookie中,当然你也可以存储到本地仓库中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import { useCookies } from '@vueuse/integrations/useCookies'
import { useRouter } from "vue-router";
const router=useRouter()
const cookie=useCookies()
const submit = () => {
userform.value.validate((isValid) => {
if (!isValid) {
console.log(isValid);
return false
} else {
login(form.userName, form.passWord)
.then((result) => {
// 成功提示
ElNotification({
title:"欢迎回来",
type: 'success',
})
// 存储token
cookie.set('admin-token',result.data.data.token)
// 跳转到首页
router.push('/')
}).catch((err) => {
ElNotification({
title: err.response.data.msg,
type: 'info',
})
})
}
})
}
2.8请求拦截器响应拦截器
请求前和请求后都做一些处理
请求成功了直接返回数据
请求失败了进行统一的错误处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36import axios from "axios";
import { ElNotification } from 'element-plus'
const instance = axios.create({
baseURL: '/api'
});
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data.data;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
// 错误的统一处理---直接将错误信息弹出
ElNotification({
title: error.response.data.msg||"请求错误",
type: 'info',
//提示时间为3秒
duration:3000
})
return Promise.reject(error);
});
export default instance在请求前在请求头前添加token
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45import axios from "axios";
import { ElNotification } from 'element-plus'
import { useCookies } from '@vueuse/integrations/useCookies'
const cookie=useCookies()
const instance = axios.create({
baseURL: '/api'
});
// 添加请求拦截器
instance.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// token
const token=cookie.get('admin-token')
// 有token就将token传到header
if(token){
config.headers['token']=token
}
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data.data;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
// 错误的统一处理---直接将错误信息弹出
ElNotification({
title: error.response.data.msg||"请求错误",
type: 'info',
//提示时间为3秒
duration:3000
})
return Promise.reject(error);
});
export default instance
2.9获取用户信息
封装api
/scr/api/manager.js
1
2
3
4
5// 获取用户信息
// http://ceshi13.dishait.cn/admin/getinfo
export const getinfo=()=>{
return axios.post('/admin/getinfo')
}引入并使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39import { useCookies } from '@vueuse/integrations/useCookies'
import { useRouter } from "vue-router";
import {getinfo} from '@/api/manager.js';
const router=useRouter()
const cookie=useCookies()
const submit = () => {
userform.value.validate((isValid) => {
if (!isValid) {
console.log(isValid);
return false
} else {
login(form.userName, form.passWord)
.then((result) => {
// console.log(result);
// 成功提示
ElNotification({
title:"欢迎回来",
type: 'success',
})
// 存储token
cookie.set('admin-token',result.token)
// 获取用户信息
getinfo().then((result) => {
console.log(result);
}).catch((err) => {
});
// 跳转到首页
router.push('/')
}).catch((err) => {
// ElNotification({
// title: err.response.data.msg,
// type: 'info',
// })
})
}
})
}
2.10 防抖节流
防抖(回城嘲讽)
当某个事件在执行期间,如果有新的事件进来,就讲以前的事件作废,重新开始新的一轮。
核心:重新开始应用场景:
搜索框输入,在输入后的某一段时间在发送请求,而不是刚输入就发送请求
文本编译器实时保存: 输入后某段时间发送保存的请求实现思路:
1
2
3
4
5
6
7
8
9
10
11
12let timerId=null;
const onKeyup=()=>{
//防抖
if(timerId!=null){
clearTimeout(timerId)
}
timerId=setTimeout(()=>{
log("防抖,这里写需要执行的代码")
},1000)
}节流
就是连续触发事件,但是在设定的时间内就只执行一次。
核心:不打断应用场景
高频事件:例如快速点击,下拉事件等
实现思路
1
2
3
4
5
6
7
8
9
10
11
12
13let timerId=null
const onKeyup=()=>{
if(timerId!=null){
return
}
timerId=setTimeout(()=>{
log("节流,这里写需要执行的代码")
//执行完毕后 将timerId设置为空
timerId=null
},1000)
}
中节流的使用
使用 element loading 属性。在请求前使用为按钮添加loading效果 在请求结束后取消loading效果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26 <el-button type="primary" size="default" @click="submit" class="button" :loading="buttonLoading">登录</el-button>
const buttonLoading = ref(false)
//登录方法
const submit = () => {
formRef.value.validate((isValid) => {
if (isValid) {
// loading 效果
buttonLoading.value = true;
login(form.username, form.password).then((result) => {
// 存储token
setToken('admin-token', result)
// 路由跳转
router.push("/")
showMsg("登录成功", "欢迎回来", "success")
}).catch((err) => {
// showMsg("登录失败", err, "info")
}).finally(() => {
buttonLoading.value = false;
})
}
})
}
elementplus结合网络请求实现防抖节流
1
<el-button type="primary" round class="btn" @click="submit" :loading="isLoading">登录</el-button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40// 是否loading
let isLoading=ref(false)const router=useRouter()
const cookie=useCookies()
const submit = () => {
userform.value.validate((isValid) => {
if (!isValid) {
console.log(isValid);
return false
} else {
// 请求开始之前就是loading状态
isLoading.value=true
login(form.userName, form.passWord)
.then((result) => {
// console.log(result);
// 成功提示
ElNotification({
title:"欢迎回来",
type: 'success',
})
// 存储token
cookie.set('admin-token',result.token)
// 获取用户信息
getinfo().then((result) => {
console.log(result);
}).catch((err) => {
});
// 跳转到首页
router.push('/')
}).catch((err) => {
// ElNotification({
// title: err.response.data.msg,
// type: 'info',
// })
}).finally(()=>{
// 无论成功还是失败都取消loading状态
isLoading.value=false
})
}
}) }
2.11 方法封装
将一些通用的方法进行封装
设置token
/src/utils/token.js
1 | import { useCookies } from '@vueuse/integrations/useCookies' |
用到Cookie的地方可以进行替换,这里就不做演示了。
- 封装一个语法提示的方法
/src/utils/prompt.js
1 | // 用户提示 |
2.12Vuex状态管理
- 安装Vuex
1 | npm install vuex@next --save |
配置
/src/store/index.js
1 | import { createStore } from 'vuex' |
main.js
1 | // vuex |
2.13 登录后存储用户信息
src/store/index.js
1 | import { createStore } from 'vuex' |
login
1 | getinfo().then((result) => { |
2.14路由守卫实现登录判断
/src/Router/index.js
1 |
|
2.15 使用vuex action 自动获取用户登录信息
声明一个action,在这个action中发送请求获取数据存储到Vuex中。然后在路由守卫那里调用action。
src/store/index.js
1 | import { createStore } from 'vuex' |
/src/router/index.js
1 | router.beforeEach((to, from,next) => { |
2.16js 原生addEventListener方法添加键盘监听事件
addEventListener() 方法用于向指定元素添加监听事件。且同一元素目标可重复添加,不会覆盖之前相同事件,配合 removeEventListener() 方法来移除事件。
参数说明:有三个参数
参数一、事件名称,字符串,必填。
事件名称不用带 “on” 前缀,点击事件直接写:”click”,键盘放开事件写:”keyup”参数二、执行函数,必填。
填写需要执行的函数,如:function(){代码…} 当目标对象事件触发时,会传入一个事件参数,参数名称可自定义,如填写event,不需要也可不填写。 事件对象的类型取决于特定的事件。例如, “click” 事件属于 MouseEvent(鼠标事件) 对象。
参数三、触发类型,布尔型,可空
true - 事件在捕获阶段执行
false - 事件在冒泡阶段执行,默认是false
使用 addEventListener实现回车登录
src/page/login.vue1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17//键盘监听事件
function onkeyUp (e) {
if (e.key == 'Enter') {
submit()
}
}
// 在页面加载完成后添加键盘监听事件
onMounted(() => {
// 添加键盘监听事件,等某个按键谈起的时候触发,接收一个回调函数,判断是否是enter按键,如果是就触发登录
document.addEventListener('keyup', onkeyUp)
})
// 页面卸载前卸载掉键盘监听事件
onBeforeUnmount(() => {
document.removeEventListener('keyup', onkeyUp)
})
2.17Vue3+elementplus 键盘监听
1 | <el-input v-model="form.passWord" placeholder="请输入密码" :prefix-icon="Lock" @keyup.enter="submit"></el-input> |
2.8 退出登录
在首页中有一个按钮, 点击以后,弹出消息确认框,是否退出登录,如果用户点击退出登录则,发送退出登录请求,清除用户token,清除vuex中存储的用户信息,回到登录页
- 封装消息确认框
src\utils\prompt.js
1 | // 确认框 |
src/view/home.vue
1 | <script setup> |
退出登录
发送请求
删除vuex中的数据
删除Cookie中的数据
回到登录页
发送请求
src/api/manager.js
1
2
3export const logout = () => {
return axios.post('/admin/logout')
}删除数据 清除cookie 回到登录页
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33<script setup>
import {useStore} from 'vuex';
import {showModel} from '@/utils/prompt.js';
import {logout} from '@/api/manager.js';
import {removeCookie} from '@/utils/token.js';
import { useRouter } from 'vue-router';
const store=useStore()
const router=useRouter()
const userlogout=()=>{
showModel("退出登录","确认退出登录",'error').then((result) => {
logout().finally(()=>{
removeCookie('admin-token')
store.commit('setUserInfo',{})
router.push('/login')
})
}).catch((err) => {
});
}
</script>
<template>
<div> im index</div>
{{ store.state.user }}
<el-button type="primary" size="default" @click="userlogout">退出登录</el-button>
</template>
2.19动态页面标题实现
src/router.js
为不同的页面配置不同的路由标题
为每一个路由对象添加一个mate
1 | const routes = [ |
路由拦截器的地方设置title
1 | // 如果登录了自动获取用户信息 |
3. 首页主控台
3.1 后台首页布局及路由配置
后台主布局的实现,后台管理首页部分的主要布局的抽离等等
新建一个layouts 作为首页的主布局容器
src/layout/index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<template>
<el-container >
<el-header >
头部部分
</el-header>
<!-- 下面主要部分 -->
<el-container >
<el-aside >
侧边部分
</el-aside>
<!-- 主要容器部分 -->
<el-container>
标签导航
<el-main >
主要部分
</el-main>
</el-container>
</el-container>
</el-container>
</template>首页部分的组件非常多,所以需要我们把共用的组件抽离出来,将共用的组件抽离出来,放到components 中,引入到主文件的位置
封装对应的header aside 部分 实现组件化
src\components
Fheader
FMenu
FTaglist
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29<script>
import FHeader from './components/FHeader.vue';
import FMenu from './components/FMenu.vue';
import FTag from './components/FTag.vue';
</script>
<template>
<el-container>
<el-header>
<!-- 头部部分 -->
<f-header></f-header>
</el-header>
<!-- 下面主要部分 -->
<el-container>
<el-aside>
<FMenu></FMenu>
</el-aside>
<!-- 主要容器部分 -->
<el-container>
<FTag></FTag>
<el-main>
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</el-container>
</template>配置路由
router/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// 1. 引入
import { createRouter, createWebHashHistory } from "vue-router"
import Home from "@/View/index.vue"
import NotFound from "@/View/404.vue"
import login from '@/View/login.vue'
import admin from '@/layout/admin.vue';
// 2. 定义一些路由
// 每个路由都需要映射到一个组件。
const routes = [
{path:'/',component:admin,children:[
{ path: '/', component: Home ,meta:{title:"首页"}},
]},
{ path: '/login', component: login ,meta:{title:"登录"}},
// 将匹配所有内容并将其放在 `route.params.pathMatch` 下
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound,meta:{title:"未发现该页面"} },
// { path: '/', component: Home },
// { path: '/about', component: About },
]
// 3. 创建路由实例并传递 `routes` 配置
const router = createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: createWebHashHistory(),
routes, // `routes: routes` 的缩写
})import {getcookie} from “@/utils/token.js”
import {toast} from “@/utils/prompt.js”
import store from “../store”router.beforeEach((to, from,next) => {
const token =getcookie(“admin-token”)
if(!token&&to.path!=’/login’){
toast(“您还未登录”,’error’,”请先登录”)
return next({path:”/login”})
}if(token&&to.path==’/login’){
alert(“已经登录了”)
return next({path:from.path})
}if(token){
store.dispatch(‘getuserInfo’)
}let title=’湖北文理学院理工学院 ‘+(to.meta.title?to.meta.title:’’)
document.title=title
next();})
export default router
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
## 3.2头部组件的布局和开发
- 静态布局
```html
<template>
<div class="header">
<!-- logo 部分 -->
<span class="icon"> <el-icon class="mr-2"><ElementPlus /> </el-icon> 湖北文理学院理工学院</span>
<!-- 左侧图标 -->
<el-icon class="icons"><Fold /></el-icon>
<!-- 刷新 -->
<el-icon class="icons"> <Refresh /> </el-icon>
<!-- 用户信息部分 -->
<div class="avatar">
<el-icon class="icons"><Operation /></el-icon>
<!-- 全屏 -->
<el-icon class="icons"> <FullScreen /></el-icon>
<el-dropdown class="dropdown">
<span class="avatar-icon">
<el-avatar :size="32" :src="$store.state.user.avatar" class="mr-2"/>
<span class="name">{{ $store.state.user.username }}</span>
<el-icon class="name">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.header {
height: 64px;
width: 100%;
position: fixed;
top: 0;
left: 0;
display: flex;
align-items: center;
@apply bg-blue-400;
}
.avatar{
display: flex;
justify-content: center;
align-items: center;
margin-left: auto;
}
.icon{
width: 320px;
display: flex;
justify-content: center;
align-items: center;
font-size: 24px;
font-weight: 400;
color: #ffffff;
}
.dropdown{
width: 200px;
display: flex;
justify-content: center;
align-items: center;
}
.icons{
font-size: 18px;
width: 60px;
font-weight: 300;
color: #ffffff;
}
.avatar-icon{
display: flex;
justify-content: center;
align-items: center;
}
.name{
font-size: 18px;
color: #ffffff;
font-weight: 300;
}
</style>
3.3退出登录功能
1 |
|
3.4文字提示功能
tooltip 标签即可
1 | <el-tooltip content="官网" placement="bottom" effect="light"> |
3.5刷新页面
1 | <!-- 刷新 --> |
3.6全屏
1.安装核心功能包
1 | npm i @vueuse/core |
2.引入并使用全屏的方法
1 | // 引入并使用全屏的方法 |
3.组件中调用全屏方法
1 | <!-- 全屏 --> |
4.全屏方法
1 | // 切换全屏 |
3.7 修改密码
点击修改密码出现弹出层,用于收集用户相关的信息。
修改密码Api
1
2
3
4// 修改密码
export const updatepassword = () => {
return axios.post('/admin/updatepassword')
}添加一个抽屉(弹出层)用于用户信息
1
2
3<el-drawer v-model="showdrawer" title="I am the title"></el-drawer>
const showdrawer=ref(true)点击修改密码弹出
1
2
3
4
5
6
7
8
9
10
11
12
13
14const showdrawer=ref(false)
const handleclick=(e)=>{
switch (e) {
case 'rePassword':
showModel.value=true
break;
case "Logout":
userlogout()
break;
}
}弹出层中给一个表单,绑定数据,校验都写好
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21const showdrawer=ref(false)
const form=ref({
username:"",
oldPassword:'',
newPassword:''
})
const rules = {
username: [
{ required: true, message: '密码为admin.请正确输入', trigger: 'blur' },
{ min: 3, max: 5, message: '密码为5个字符', trigger: 'blur' },
],
oldPassword: [
{ required: true, message: '密码为admin.请正确输入', trigger: 'blur' },
{ min: 3, max: 5, message: '密码为5个字符', trigger: 'blur' },
],
newPassword: [
{ required: true, message: '密码为admin.请正确输入', trigger: 'blur' },
{ min: 3, max: 5, message: '密码为5个字符', trigger: 'blur' },
],
}<el-drawer v-model="showdrawer" title="修改密码"> <el-form :model="form" ref="formRef" :rules="rules" label-width="100px" :inline="false" size="normal"> <el-form-item label="账号" prop="username"> <el-input v-model="form.username"></el-input> </el-form-item> <el-form-item label="旧密码" prop="oldPassword"> <el-input v-model="form.oldPassword"></el-input> </el-form-item> <el-form-item label="新密码" prop="newPassword"> <el-input v-model="form.newPassword"></el-input> </el-form-item> <el-form-item> <el-button type="primary" size="default" @click=""></el-button> </el-form-item> </el-form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
- 修改密码
> 表单校验,成功后发送网络请求
## 3.8 非法token
> 有的用户清除缓存的时候会把token也清除掉,这个时候如果有需要token的网络请求就会报错,所以需要我们自己做一些处理。
/src/axios.js
```js
// 添加响应拦截器
instance.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return response.data.data;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
if(error.response.data.msg==='非法token,请先登录!'){
// 退出登录的一系列操作
}
// 错误的统一处理---直接将错误信息弹出
ElNotification({
title: error.response.data.msg||"请求错误",
type: 'info',
//提示时间为3秒
duration:3000
})
return Promise.reject(error);
});
export default instance
3.9通用的弹窗组件的封装
src/components/Drawer.vue
静态及基本功能
1 | <template> |
完整formDrower
1 | <template> |
Fheader需改变
1 | <script setup> |
如果喜欢别的弹窗组件,封装也可以
src\components\Fdialog.vue
1 | <template> |
Fheader中引入并使用
1 | <Fdialog ref="fdialogRef" @submit="onsubmit" title="修改密码" size="40%"> |
3.10 侧边栏
静态布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<template>
<div class="menu">
<el-menu default-active="2" class="el-menu-vertical-demo">
<el-sub-menu index="1">
<template #title>
<el-icon>
<location />
</el-icon>
<span>Navigator One</span>
</template>
<el-menu-item-group title="Group One">
<el-menu-item index="1-1">item one</el-menu-item>
<el-menu-item index="1-2">item two</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="Group Two">
<el-menu-item index="1-3">item three</el-menu-item>
</el-menu-item-group>
<el-sub-menu index="1-4">
<template #title>item four</template>
<el-menu-item index="1-4-1">item one</el-menu-item>
</el-sub-menu>
</el-sub-menu>
</el-menu>
</div>
</template>
<style scoped>
.menu{
width: 250px;
@apply shadow-md;
position: fixed;
top: 64px;
bottom: 0;
overflow-y: auto;
}
</style>admin.vue中需要引入并使用
动态的数据渲染先写一些固定的数据,然后渲染出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73<script setup>
const menus = [{
name: "后台面板",
icon: 'help',
child: [{
name: "主控台",
icon: 'help',
},
{
name: "主控台",
icon: 'help',
},
{
name: "主控台",
icon: 'help',
}
]
},
{
name: "主控台",
iconL: 'help'
}
]
</script>
<template>
<div class="menu">
<el-menu default-active="2" class="el-menu-vertical-demo">
<template v-for="item in menus" :key="index">
<!-- 含二级菜单 -->
<el-sub-menu v-if="item.child && item.child.length > 0" :index="item.name">
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.name }}</span>
</template>
<el-menu-item v-for="(item2, index2) in item.child" :index="item.frontpath">
<template #title>
<el-icon>
<component :is="item2.icon"></component>
</el-icon>
<span>{{ item2.name }}</span>
</template>
</el-menu-item>
</el-sub-menu>
<!-- 一级菜单 -->
<el-menu-item v-else :index="item.frontpath">
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.name }}</span>
</template>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<style scoped>
.menu {
width: 250px;
@apply shadow-md;
position: fixed;
top: 64px;
bottom: 0;
overflow-y: auto;
}
</style>绑定事件,路由跳转。
```vue <script setup> import {useRouter} from 'vue-router'; const router=useRouter() const menus = [{ name: "后台面板", icon: 'help', child: [{ name: "主控台", icon: 'help', }, { name: "主控台", icon: 'help', frontpath: "/" }, { name: "主控台", icon: 'help', frontpath: "/" } ] }, { name: "主控台", icon: 'help', frontpath: "/123123" } ] const handleSelect = (e) => { router.push(e) } </script> <template> <div class="menu"> <el-menu default-active="2" class="el-menu-vertical-demo" @select="handleSelect"> <template v-for="item in menus" :key="index"> <!-- 含二级菜单 --> <el-sub-menu v-if="item.child && item.child.length > 0" :index="item.name"> <template #title> <el-icon> <component :is="item.icon"></component> </el-icon> <span>{{ item.name }}</span> </template> <el-menu-item v-for="(item2, index2) in item.child" :index="item.frontpath"> <template #title> <el-icon> <component :is="item2.icon"></component> </el-icon> <span>{{ item2.name }}</span> </template> </el-menu-item> </el-sub-menu> <!-- 一级菜单 --> <el-menu-item v-else :index="item.frontpath"> <template #title> <el-icon> <component :is="item.icon"></component> </el-icon> <span>{{ item.name }}</span> </template> </el-menu-item> </template> </el-menu> </div> </template> <style scoped> .menu { width: 250px; @apply shadow-md; position: fixed; top: 64px; bottom: 0; overflow-y: auto; } </style>
3.11侧边栏的展开折叠
在仓库中新建一个新的变量, 通过这个变量来控制侧边栏的展开或者折叠
store/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
state() {
return {
userinfo: {},
// 侧边栏宽度
asideWidth:'250px'
};
},
// 修改数据的唯一方式
mutations: {
setuserinfo(state, value) {
state.userinfo = value;
},
setAsideWidth(state){
state.asideWidth=state.asideWidth==='250px'?'60px':'250px'
}
},
在header部分绑定对应的点击事件来改变 aside的值
fheader.vue
1
2
3
4
5
6
7
8
9
10<el-tooltip content="折叠侧边栏" placement="bottom" effect="light">
<el-icon :size="20" color="#fff" @click="asideWidth">
<Fold v-if="store.state.asideWidth=='250px'" />
<Expand v-else/>
</el-icon>
</el-tooltip>
const asideWidth=()=>{
store.commit('setAsideWidth')
}找到菜单组件折叠
1
2
3
4
5
6
7
8
9<el-menu default-active="2" class="el-menu-vertical-demo" @select="handleSelect" :style="{width:store.state.asideWidth}" :collapse="collapse">
const collapse=computed(()=>{
if(store.state.asideWidth=='250px'){
return false
}else{
return true
}
})侧边栏加一个动画过渡
1
2
3.el-aside{
transition: all 0.2s;
}
3.13菜单选中和路由关联
- 默认激活菜单的 index default-active
1 | <el-menu default-active="defaultActive" class="el-menu-vertical-demo" @select="handleSelect" :style="{width:store.state.asideWidth}" :collapse="collapse"> |
- 默认激活的菜单index 等于当前的路由
1 | // 点击跳转到当前路径 |
3.14 使用后端数据渲染菜单
将菜单数据和权限数据存储到store
/src/store/index.js
定义数据数据了以后给一个修改数据的方法,然后获取到数据了以后直接将相关数据存储到数据中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81// 公共的仓库 vuex
import { createStore } from 'vuex';
import { getinfo } from '@/api/manager.js';
import {logout} from '@/api/manager.js';
import {removecookie} from '@/utils/cookie.js';
import router from '@/router/index.js';
// 创建一个新的 store 实例
const store = createStore({
// 存储数据的
state() {
return {
userinfo: {},
// 侧边栏宽度
asideWidth:'250px',
// 菜单数据
menu:[],
// 权限数据
ruleNames:[]
};
},
// 修改数据的唯一方式
mutations: {
setuserinfo(state, value) {
state.userinfo = value;
},
setAsideWidth(state){
state.asideWidth=state.asideWidth==='250px'?'60px':'250px'
},
// 修改菜单
setMenuList(state,value){
state.menu=value
},
// 修改权限数据
setruleNames(state,value){
state.ruleNames=value
}
},
// 用于处理异步的
actions: {
// 请求用户数据
getuserinfo(context) {
getinfo()
.then((result) => {
// 直接放到state
context.commit('setuserinfo', result);
context.commit('setMenuList',result.menus)
context.commit('setruleNames',result.releNames)
})
.catch((err) => {});
},
// 退出登录
Ulogout(context) {
logout().finally(() => {
//删除用户数据 vuex
// store.commit('setuserinfo', {});
context.commit('setuserinfo', {})
// 清除token cookie
removecookie('admin-token');
// 回到登录页
router.push('/login');
});
},
},
});
export default store;页面中拿到数据进行渲染
Fmenu.vue
1
2
3const menus=computed(()=>{
return store.state.menu
})
3.15根据数据动态添加路由
这一块内容太复杂,把代码复制一下即可。
根据后端传递的菜单动态的添加路由
抽离路由守卫(主要是为了方便后续的操作)
scr/router/index.js
1
2// 引入历史管理 创建router实例对象一个构造函数
import { createWebHashHistory, createRouter } from "vue-router";import Home from “@/view/Home.vue”;
import Login from “@/view/Login.vue”;
import NotFound from “@/view/404.vue”;
import Admin from “@/layout/admin.vue”import Test from “@/view/text.vue”
// 路由的相关配置
const routes = [
{
path: “/“,
component: Admin,
meta: {
title: “首页”,
},
children:[
{path:’/‘,component:Home},
{path:”/test”,component:Test}
]
},
{
path: “/login”,
component: Login,
meta: {
title: “登录”,
},
},
// 404 页面
{
path: “/:pathMatch(.)“,
name: “NotFound”,
component: NotFound,
meta: {
title: “未发现该页面”,
},
},
];// 3. 创建路由实例并传递
routes
配置
const router = createRouter({
// 4. 内部提供了 history 模式的实现。为了简单起见,我们在这里使用 hash 模式。
history: createWebHashHistory(),
routes, //routes: routes
的缩写
});
export default router;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/src/router/permission.js
```js
import router from "./index.js"
import { getCookie } from "@/utils/cookie.js";
import { toast } from "@/utils/prompt.js";
import store from "../store";
// 全局前置路由守卫
router.beforeEach((to, from, next) => {
const token = getCookie("admin-token");
// 没登录 并且不是跳转到登录页
if (!token && to.path !== "/login") {
toast("您未登录,请先登录。", "", "info");
next({ path: "/login" });
}
// 已经登录了就不能跳转到登录页
if (token && to.path === "/login") {
toast("您已登录,请勿重复登录", "", "success");
next({ path: from.path });
}
// 如果登录了,获取数据存储到vuex中
if (token) {
// store.dispatch('getuserinfo')
store.dispatch("getuserinfo");
}
// 跳转的路由的title拿过来
let title= to.meta.title
if(title){
document.title="湖北文理学院理工学院-"+title+"页"
}else{
document.title="湖北文理学院理工学院"
}
next();
});
main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33// main.ts
import { createApp } from 'vue'
// import "./style.css"
// 引入element plus 和样式
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
// 引入windicss
import 'virtual:windi.css'
// 引入store
import store from '@/store/index.js';
// 引入router
import router from "./router/index.js"
// 引入所有的icon图标
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
app.use(store)
app.use(router)
import "@/router/permission.js"
app.mount('#app')有一些路由是公用的,如404 login 首页,但是首页中的子路由是需要动态生成的。首页的子路由都是需要自己去进行配置。
算了太复杂了 不整了,手动配置吧
4.标签导航
点击对应的路由上方出现对应的标签。
4.1静态样式
layout/components/Ftag.vue
1 | <script setup> |
admin.vue
1 | <script setup> |
4.2数据绑定
1 | <script setup> |
4.3路由绑定一下tab
让激活的tab为当前的路径
1 | import {useRoute} from 'vue-router'; |
除了是首页全部显示关闭按钮
1 | <el-tab-pane v-for="item in tablist" :key="item.path" :label="item.title" :name="clearable" class="tab" :closable="item.path!=='/'"> |
4.4标签导航实现
点击侧边栏,出现一个新的tag,并且是处于激活状态的。
通过router提供的方法来拿到当前的路径
然后拿到title 和path
判断当前tab有没有当前的路径,没有就添加
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70<script setup>
import { ref } from 'vue'
import {useRoute,onBeforeRouteUpdate} from 'vue-router';
const route=useRoute()
// 激活的tab
const activeTab = ref(route.path)
onBeforeRouteUpdate((to,form)=>{
// 当前路由
let nowPath=to.path
// 当前路由标题
let nowtitle=to.meta.title
// 首先判断一下当前有没有路由
let tab=tablist.value.findIndex(item=>item.path===nowPath)===-1
// 如果没有tab
if(tab){
tablist.value.push({
title:nowtitle,
path:nowPath
})
}
})
const tablist = ref([
{
title: '首页',
path:"/"
},
// {
// title: '商城管理',
// path:"/goods/list"
// },
])
</script>
<template>
<div class="tag">
<el-tabs v-model="activeTab" type="card" editable class="demo-tabs" @edit="handleTabsEdit">
<el-tab-pane v-for="item in tablist" :key="item.path" :label="item.title" :name="clearable" class="tab" :closable="item.path!=='/'">
</el-tab-pane>
</el-tabs>
</div>
</template>
<style scoped>
.tag{
position: fixed;
top: 68px;
}
:deep(.el-tabs--card>.el-tabs__header .el-tabs__item){
height: 40px !important;
border: 1px solid #ccc;
border-radius: 10px;
margin: 0 5px;
}
:deep(.el-tabs--card>.el-tabs__header .el-tabs__nav){
border: 0;
}
:deep(.el-tabs--card>.el-tabs__header){
border: 0 !important;
}
.tagItem{
background-color: yellowgreen;
}
</style>
设置当前的tab等于当前的path,让tab处于激活的状态
1 | onBeforeRouteUpdate((to,form)=>{ |
4.5 标签导航跳转路由
tab绑定对应事件
1
2
3
4
5
6<div class="tag">
<el-tabs v-model="activeTab" type="card" editable class="demo-tabs" @edit="handleTabsEdit" @tab-change="tabChange">
<el-tab-pane v-for="item in tablist" :key="item.path" :label="item.title" :name="clearable" class="tab" :closable="item.path!=='/'">
</el-tab-pane>
</el-tabs>
</div>事件
1
2
3
4// 点击tab跳转
const tabChange=(t)=>{
router.push(t)
}
4.6 关闭标签导航
关闭标签导航,如果有下一个就切换为下一个,如果没有就切换为上一个。
1 | <div class="tag"> |
1 | const removeTab=(e)=>{ |
5.后台面板实现
5.1 页面布局及数据渲染
获取对应的数据
封装api 发送请求获取数据
/src/api/index.js
1
2
3
4
5
6
7//首页相关的网络请求
import axios from "@/utils/axios.js"
export const statistics1=()=>{
return axios.get('/admin/statistics1')
}/src/view/home.vue
1
2
3
4
5
6
7
8
9
10
11
12
13<script setup>
import {statistics1} from "@/api/index.js"
import {ref} from 'vue';
const panels=ref([])
statistics1().then((result) => {
panels.value=result.panels
console.log(panels.value);
}).catch((err) => {
console.log(err);
});
</script>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
- 页面布局及渲染数据
```vue
<script setup>
import { statistics1 } from "@/api/index.js"
import { ref } from 'vue';
const panels = ref([])
statistics1().then((result) => {
panels.value = result.panels
console.log(panels.value);
}).catch((err) => {
console.log(err);
});
</script>
<template>
<el-row :gutter="20">
<el-col :span="6" :offset="0" v-for="(item, index) in panels" :key="index">
<el-card shadow="always">
<template #header>
<div class="card1header">
<span>{{ item.title }}</span>
<el-tag :type="item.unitColor" size="normal" effect="dark">{{ item.unit }}</el-tag>
</div>
</template>
<span class="headerTitle">{{ item.value }}</span>
<el-divider direction="horizontal" content-position="center"></el-divider>
<div class="headerBottom">
<span>{{ item.subTitle }}</span>
<span>{{ item.subValue }}</span>
</div>
</el-card>
</el-col>
</el-row>
</template>
<style scoped>
.card1header {
display: flex;
justify-content: space-between;
}
.headerTitle {
font-size: 30px;
font-weight: 600;
}
.headerBottom{
display: flex;
justify-content: space-between;
}
</style>
5.2 统计面板骨架屏开发
骨架屏就是在页面还请求到数据的时候显示一个轮廓
静态页面布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41<script setup>
import { statistics1 } from "@/api/index.js"
import { ref } from 'vue';
import CountTo from '@/components/CountTo.vue';
const panels = ref([])
statistics1().then((result) => {
panels.value = result.panels
console.log(panels.value);
}).catch((err) => {
console.log(err);
});
</script>
<template>
<el-row :gutter="20">
<template v-if="panels.length === 0">
<el-col :span="6" :offset="0" v-for="i in 4" :key="i">
<el-skeleton style="width:100%">
<template #template>
<el-card shadow="always">
<template #header>
<div class="card1header">
<el-skeleton-item variant="p" style="width: 50%" />
<el-skeleton-item variant="p" style="width: 10%" />
</div>
</template>
<el-skeleton-item variant="p" style="width: 80%" />
<el-divider direction="horizontal" content-position="center"></el-divider>
<div class="headerBottom">
<el-skeleton-item variant="p" style="width: 30%" />
<el-skeleton-item variant="p" style="width: 10%" />
</div>
</el-card>
</template>
</el-skeleton>
</el-col>
</template><span>{{ item.title }}</span> <el-tag :type="item.unitColor" size="normal" effect="dark">{{ item.unit }}</el-tag> </div> </template> <span class="headerTitle"> <CountTo :value="item.value.toFixed(2)"></CountTo></span> <el-divider direction="horizontal" content-position="center"></el-divider> <div class="headerBottom"> <span>{{ item.subTitle }}</span> <span>{{ item.subValue }}</span> </div> </el-card> </el-col>
1
2
3
4
5
6
7
8
9
10
## 5.3数字滚动框实现
> 数字有滚动动画
- 下载实现该功能的库
```npm
npm i gsap定义一个专门实现数字滚动的组件
/src/coponents/countTo.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<script setup>
import { reactive, watch } from "vue"
import gsap from 'gsap'
// 初始值
const d = reactive({
num: 0
})
// 接收一个最终值
const props = defineProps({
value: {
type: Number,
ddefault: 0
}
})
// 定义一个方法 从初始值一直滚动到最终值
const numberTo = () => {
// 从谁开始 经过多长时间 变成谁
gsap.to(d, { duration: 0.5 ,num:props.value}
)}
// 调用这个方法
numberTo()
// 监听数据的变化 一旦产生变化了就直接调用动画
watch(()=>props.value,()=>{
numberTo()
})
</script>
<template>
{{ d.num }}
</template>页面中使用这个组件
1
<span class="headerTitle"> <CountTo :value="item.value.toFixed(2)"></CountTo></span>
5.4分类组件开发
拿数据,渲染页面
数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50[
{
icon:"user",
color:"text-light-blue-500",
title:"用户",
path:"/user/list"
},
{
icon:"shopping-cart",
color:"text-violet-500",
title:"商品",
path:"/goods/list"
},
{
icon:"tickets",
color:"text-fuchsia-500",
title:"订单",
path:"/order/list"
},
{
icon:"chat-dot-square",
color:"text-teal-500",
title:"评价",
path:"/comment/list"
},
{
icon:"picture",
color:"text-rose-500",
title:"图库",
path:"/image/list"
},
{
icon:"bell",
color:"text-green-500",
title:"公告",
path:"/notice/list"
},
{
icon:"set-up",
color:"text-grey-500",
title:"配置",
path:"/setting/base"
},
{
icon:"files",
color:"text-yellow-500",
title:"优惠券",
path:"/coupon/list"
}
]渲染页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67<template>
<el-row :gutter="20" class="mt-5">
<el-col :span="3" :offset="0" v-for="(item,index) in iconNavs" :key="index">
<el-card shadow="hover" @click="$router.push(item.path)">
<div class="flex flex-col items-center justify-center cursor-pointer">
<el-icon :size="16" :class="item.color">
<component :is="item.icon"/>
</el-icon>
<span class="text-sm mt-2">{{ item.title }}</span>
</div>
</el-card>
</el-col>
</el-row>
</template>
<script setup>
const iconNavs = [
{
icon:"user",
color:"text-light-blue-500",
title:"用户",
path:"/user/list"
},
{
icon:"shopping-cart",
color:"text-violet-500",
title:"商品",
path:"/goods/list"
},
{
icon:"tickets",
color:"text-fuchsia-500",
title:"订单",
path:"/order/list"
},
{
icon:"chat-dot-square",
color:"text-teal-500",
title:"评价",
path:"/comment/list"
},
{
icon:"picture",
color:"text-rose-500",
title:"图库",
path:"/image/list"
},
{
icon:"bell",
color:"text-green-500",
title:"公告",
path:"/notice/list"
},
{
icon:"set-up",
color:"text-grey-500",
title:"配置",
path:"/setting/base"
},
{
icon:"files",
color:"text-yellow-500",
title:"优惠券",
path:"/coupon/list"
}
]
</script>
5.5Echarts订单统计
封装接口
/src/api/index.js
1
2
3
4export const statistics3=(type)=>{
return axios.get('/admin/statistics3?type'+type)
}下载echarts
1
npm i echarts
组件静态布局及部分小功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<script setup>
import {ref} from 'vue';
const option=[
{
text:"近一个月",
value:"month"
},
{
text:"近一周",
value:"week"
},
{
text:"近24小时",
value:"hour"
}
]
const activeTag=ref('week')
const changeTag=(value)=>{
activeTag.value=value
}
</script>
<template>
<el-card shadow="hover" >
<template #header>
<div class="flex justify-between">
<span>订单统计</span>
<el-check-tag v-for="item in option" :key="item.value" :checked="item.value===activeTag" @click="changeTag(item.value)">{{item.text}}</el-check-tag>
</div>
</template>
</el-card>
</template>Echarts 的基本使用
下载echarts
1
npm i echarts
基本使用看官网案例
<script setup> import * as echarts from 'echarts'; import { ref, onMounted } from 'vue'; const option = [ { text: "近一个月", value: "month" }, { text: "近一周", value: "week" }, { text: "近24小时", value: "hour" } ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
const activeTag = ref('week')
const changeTag = (value) => {
activeTag.value = value
}
// 因为获取元素需要再页面加载以后执行 所以使用生命周期函数
onMounted(() => {
var chartDom =document.querySelector('.container')
var myChart = echarts.init(chartDom);
let option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisTick: {
alignWithLabel: true
}
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'Direct',
type: 'bar',
barWidth: '60%',
data: [10, 52, 200, 334, 390, 330, 220]
}
]
};
option && myChart.setOption(option);
})
</script>
<template>
<el-card shadow="hover">
<template #header>
<div class="flex justify-between">
<span>订单统计</span>
<el-check-tag v-for="item in option" :key="item.value" :checked="item.value === activeTag"
@click="changeTag(item.value)">{{ item.text }}</el-check-tag>
</div>
</template>
<div class="container">
</div>
</el-card>
</template>
<style scoped>
.container {
height: 300px;
width: 100%;
}
</style><script setup>
import * as echarts from 'echarts';
import { ref, onMounted } from 'vue';
const option = [
{
text: "近一个月",
value: "month"
},
{
text: "近一周",
value: "week"
},
{
text: "近24小时",
value: "hour"
}
]
const activeTag = ref('week')
const changeTag = (value) => {
activeTag.value = value
}
// 因为获取元素需要再页面加载以后执行 所以使用生命周期函数
onMounted(() => {
var chartDom =document.querySelector('.container')
var myChart = echarts.init(chartDom);
let option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
axisTick: {
alignWithLabel: true
}
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: 'Direct',
type: 'bar',
barWidth: '60%',
data: [10, 52, 200, 334, 390, 330, 220]
}
]
};
option && myChart.setOption(option);
})
</script>
<template>
<el-card shadow="hover">
<template #header>
<div class="flex justify-between">
<span>订单统计</span>
<el-check-tag v-for="item in option" :key="item.value" :checked="item.value === activeTag"
@click="changeTag(item.value)">{{ item.text }}</el-check-tag>
</div>
</template>
<div class="container">
</div>
</el-card>
</template>
<style scoped>
.container {
height: 300px;
width: 100%;
}
</style>
5.6Echarts 数据交互
拿到数据了以后给到Echarts中的配置数据的地方
```js
订单统计<div class="container"> </div> </el-card>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
## 5.7店铺交易提示
拿数据进行渲染
两个组件是一样的所以说可以封装成一个组件
```html
<template>
<el-card shadow="hover" :body-style="{ padding: '20px' }">
<template #header>
<div class="flex justify-between">
<span>{{ title }}</span>
<el-tag type="danger" size="normal" effect="dark">
{{ tip }}
</el-tag>
</div>
</template>
<el-row :gutter="20">
<el-col :span="6" :offset="0">
<el-card shadow="hover">
<div class="flex flex-col items-center justify-center cursor-pointer">
<span>1</span>
<p>审核中</p>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
</template>
<script setup>
const props=defineProps({
title:{
type:String,
default:"标题"
},
tip:{
type:String,
default:"内容"
}
})
</script>
<style scoped>
.card{
width: 100%;
height: 100%;
}
</style>请求数据, 然后交给对应的组件进行渲染
1
2
3export const statistics2=(type)=>{
return axios.get('/admin/statistics2')
}发请求,获取数据
1
2
3
4
5
6
7
8const panels = ref([])
const goods=ref([])
statistics2().then((result) => {
goods.value=result.goods
order.value=result.order
})数据给组件
1
2
3
4
5
6
7
8
9
<el-row :gutter="20">
<el-col :span="12" :offset="0"><indexChart></indexChart></el-col>
<el-col :span="12" :offset="0">
<indexCard title="商品汇总" tip="哈哈" :value="goods"></indexCard>
</el-col>
</el-row>组件接收数据渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56<template>
<el-card shadow="hover" :body-style="{ padding: '20px' }">
<template #header>
<div class="flex justify-between">
<span>{{ title }}</span>
<el-tag type="danger" size="normal" effect="dark">
{{ tip }}
</el-tag>
</div>
</template>
<el-row :gutter="20">
<el-col :span="6" :offset="0" v-for="item in value" :key="item.label">
<el-card shadow="hover">
<div class="flex flex-col items-center justify-center cursor-pointer">
<span>{{ item.value }}</span>
<p>{{ item.label }}</p>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
</template>
<script setup>
const props=defineProps({
title:{
type:String,
default:"标题"
},
tip:{
type:String,
default:"内容"
},
value:{
type:[]
}
})
</script>
<style scoped>
.card{
width: 100%;
height: 100%;
}
</style>6.图库模块
6.1图库静态布局
view 下面新建一个image文件夹放图库相关的页面代码
路由配置一下
router/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19{
path: "/",
component: Admin,
meta: {
title: "首页",
},
children:[
{path:'/',component:Home,meta:{
title:'首页'
}},
{path:'/goods/list',component:shop,meta:{
title:'商品管理'
}},
{path:"/image/list",component:image,meta:{
title:"图库管理"
}}
]
},页面静态
使用container 布局容器 进行布局
主要是要限制容器的高度, 需要撑满整个剩余空间
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32<template>
<el-container class="out" :style="{height:h+'px'}">
<el-header height="">
<!-- Header content -->
</el-header>
<el-container >
<el-aside width="200px">
<!-- Aside content -->
</el-aside>
<el-main height="">
<!-- Main content -->
</el-main>
</el-container>
</el-container>
</template>
<script setup>
// 拿到浏览器高度或者视口高度
const windowHeight=window.innerHeight||document.body.clientHeight
// 主要区域的高度等于 浏览器-64 -tab- 40pading
const h=windowHeight-64-60-40
</script>
<style setup>
.out{
@apply bg-blue-50;
}
</style>主体结构静态布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103<template>
<el-container class="out" :style="{ height: h + 'px' }">
<el-header class="imgHeader">
Header content
</el-header>
<el-container>
<el-aside class="imgAside">
<!-- 上方的滚动列表区域 -->
<div class="top">
<div v-for="i in 100" :key="i">11</div>
</div>
<!-- 下方的分页 -->
<div class="bottom">
分页
</div>
</el-aside>
<el-main class="imgMain">
<!-- 上方的滚动列表区域 -->
<div class="top">
<div v-for="i in 100" :key="i">11</div>
</div>
<!-- 下方的分页 -->
<div class="bottom">
分页
</div>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
// 拿到浏览器高度或者视口高度
const windowHeight = window.innerHeight || document.body.clientHeight
// 主要区域的高度等于 浏览器-64 -tab- 40pading
const h = windowHeight - 64 - 60 - 40
</script>
<style setup>
.out {
@apply bg-blue-50;
}
.imgHeader{
@apply flex justify-start items-center bg-blue-100;
border-bottom: 1px solid #797979;
}
.imgAside{
width: 220px;
@apply bg-blue-400;
position: relative;
}
.imgAside .top{
width: 220px;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 50px;
overflow-y: auto;
}
.imgAside .bottom{
position: absolute;
bottom: 0;
height: 50px;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
}
/* 主体区域 */
.imgMain{
position: relative;
@apply bg-violet-200;
}
.imgMain .top{
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 50px;
overflow-y: auto;
}
.imgMain .bottom{
position: absolute;
bottom: 0;
height: 50px;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
}
</style>修改滚动条
App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27<style scoped>
/* 滚动条主体 */
::-webkit-scrollbar{
width: 4px ;
height: 6px ;
}
/* 边角部分 */
::-webkit-scrollbar-corner{
display: block ;
}
/* 小方块 */
::-webkit-scrollbar-thumb{
border-radius: 8px ;
background-color: rgba(0, 0, 0, 0.2) ;
}
::-webkit-scrollbar-thumb,
::-webkit-scrollbar-track{
border-right-color: transparent ;
border-left-color: transparent ;
background-color: rgba(206, 206, 206, 0.1) ;
}
</style>
6.2图库 组件拆分
后续需要单独使用到图库模块中的内容,尤其是侧边栏和主要区域,所以我们需要对图库模块进行拆分。
6.3图库分类
静态布局
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125<template>
<el-container class="out" :style="{ height: h + 'px' }">
<el-header class="imgHeader">
Header content
</el-header>
<el-container>
<el-aside class="imgAside">
<!-- 上方的滚动列表区域 -->
<div class="top">
<div class="asideList">
<span>标题</span>
<div>
</div>
</div>
</div>
<!-- 下方的分页 -->
<div class="bottom">
分页
</div>
</el-aside>
<el-main class="imgMain">
<!-- 上方的滚动列表区域 -->
<div class="top">
<div v-for="i in 100" :key="i">11</div>
</div>
<!-- 下方的分页 -->
<div class="bottom">
分页
</div>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
// 拿到浏览器高度或者视口高度
const windowHeight = window.innerHeight || document.body.clientHeight
// 主要区域的高度等于 浏览器-64 -tab- 40pading
const h = windowHeight - 64 - 60 - 40
</script>
<style setup>
.out {
@apply bg-blue-50;
}
.imgHeader {
@apply flex justify-start items-center bg-blue-100;
border-bottom: 1px solid #797979;
}
.imgAside {
width: 220px;
position: relative;
}
.imgAside .top {
width: 220px;
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 50px;
overflow-y: auto;
}
.imgAside .bottom {
position: absolute;
bottom: 0;
height: 50px;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
}
/* 主体区域 */
.imgMain {
position: relative;
@apply bg-violet-200;
}
.imgMain .top {
position: absolute;
top: 0;
right: 0;
left: 0;
bottom: 50px;
overflow-y: auto;
}
.imgMain .bottom {
position: absolute;
bottom: 0;
height: 50px;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
}
.asideList {
border-bottom: 1px solid #ccc;
@apply flex items-center justify-between p-3;
background-color: #d81313;
}
.asideList :active{
background-color:yellow ;
}
.btn{
padding: 0 5px;
}
</style>封装api(图库模块)
api/image.js
1
2
3
4
5
6
7// 首页数据相关的请求
import axios from "@/utils/axios.js"
export const getimageList=(page)=>{
return axios.get('/admin/image_class/'+page)
}发送请求获取数据
拿数据渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96<script setup>
import { ref } from "vue";
import {getimageList} from "@/api/image.js"
const h = window.innerHeight || document.body.clientHeight
const containerHeight = h - 64 - 40
const imageList=ref([])
getimageList(1).then((result) => {
imageList.value=result.list
}).catch((err) => {
});
</script>
<template>
<el-container :style="{ height: containerHeight + 'px' }" class="out">
<el-header class="header">
Header content
</el-header>
<el-container>
<el-aside class="aside">
<ul class="asideList">
<li v-for="(item,index) in imageList" :key="index">
<span>{{ item.name}}</span>
<div> <el-icon><Cpu /></el-icon>
<el-icon><Cpu /></el-icon></div>
</li>
</ul>
</el-aside>
<el-main class="mian">
Main content
</el-main>
</el-container>
</el-container>
</template>
<style scoped>
.out {
background-color: #ccc;
position: relative;
}
.header {
height: 50px;
position: absolute;
top: 0;
left: 0;
right: 0;
border-bottom: 1px solid #7a7a7a;
@apply bg-blue-200;
}
.aside {
width: 200px;
position: absolute;
top: 51px;
left: 0;
bottom: 0;
background-color: yellowgreen;
}
.mian {
position: absolute;
top: 51px;
left: 200px;
right: 0;
bottom: 0;
background-color: rgb(248, 160, 160);
}
:deep(.el-main) {
padding-bottom: 10px ;
margin-bottom: 10px ;
}
.asideList li{
height: 40px;
/* background-color: red; */
display: flex;
justify-content: space-between;
align-items: center;
}
.asideList li:hover{
background-color: #7a7a7a;
}
.active{
background-color: red;
}
</style>
6.4分页
分页组件
1
<el-pagination layout="prev, pager, next" :total="50" />
分页组件的基本数据
1
2
3
4
5
6const imageList=ref([])
getimageList(activepage.value).then((result) => {
imageList.value=result.list
}).catch((err) => {
});// 分页数据
// 当前页
const activepage=ref(1)
// 总共有多少条
const total=ref(0)
// 一页多少条
const limit=ref(10)1
2
3
4
5
分页组件绑定数据
```js
<el-pagination layout="prev, next" :total="total" :current-page="activepage" :page-size="limit"/>分页组件结合接口
当前页码发生变化的时候直接请求数据
1
2
3
4
5
6
7
8<script setup>
import { ref } from "vue";
import { getimageList } from "@/api/image.js"
const h = window.innerHeight || document.body.clientHeight
const containerHeight = h - 64 - 40
const imageList = ref([])// 分页数据
// 当前页
const activepage = ref(1)
// 总共有多少条
const total = ref(0)
// 一页多少条
const limit = ref(10)const getData = (p=null) => {
if (p) {
getimageList(p).then((result) => {
imageList.value = result.list
total.value = result.totalCount
})
} else {
getimageList(activepage.value).then((result) => {
imageList.value = result.list
total.value = result.totalCount
})
}
}getData()
const pageChange = (p) => {
getData(p)
}Header content <el-container> <el-aside class="aside"> <ul class="asideList"> <li v-for="(item, index) in imageList" :key="index"> <span>{{ item.name }}</span> <div> <el-icon> <Cpu /> </el-icon> <el-icon> <Cpu /> </el-icon> </div> </li> </ul> <el-pagination layout="prev, next" :total="total" :current-page="activepage" :page-size="limit" @current-change="pageChange" /> </el-aside> <el-main class="mian"> Main content </el-main> </el-container> </el-container>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
## 6.5 新增图片分类
点击新增按钮,弹出新增图片分类的组件,点击确认后新增图片分类
```js
<script setup>
import { ref } from "vue";
import { getimageList } from "@/api/image.js"
const h = window.innerHeight || document.body.clientHeight
const containerHeight = h - 64 - 40
const imageList = ref([])
// 分页数据
// 当前页
const activepage = ref(1)
// 总共有多少条
const total = ref(0)
// 一页多少条
const limit = ref(10)
// 获取图片列表数据
const getData = (p=null) => {
if (p) {
getimageList(p).then((result) => {
imageList.value = result.list
total.value = result.totalCount
})
} else {
getimageList(activepage.value).then((result) => {
imageList.value = result.list
total.value = result.totalCount
})
}
}
getData()
const pageChange = (p) => {
getData(p)
}
import Drawer from '@/components/Drawer.vue';
// 新增图片分类
// 打开抽屉组件
const deawerRef=ref(null)
const form=ref({
name:"",
order:50
})
const addimgList=()=>{
// 打开弹出层
deawerRef.value.open()
}
// 点击确认
const onsubmit=()=>{
}
</script>
<template>
<Drawer ref="deawerRef" @onsubmit="onsubmit">
<el-form :model="form" ref="formref" :rules="rules" label-width="80px" :inline="false" size="normal">
<el-form-item label="名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.order" />
</el-form-item>
</el-form>
</Drawer>
<el-container :style="{ height: containerHeight + 'px' }" class="out">
<el-header class="header">
<el-button type="primary" size="default" @click="addimgList">新增图片分类</el-button>
</el-header>
<el-container>
<el-aside class="aside">
<ul class="asideList">
<li v-for="(item, index) in imageList" :key="index">
<span>{{ item.name }}</span>
<div> <el-icon>
<Cpu />
</el-icon>
<el-icon>
<Cpu />
</el-icon>
</div>
</li>
</ul>
<el-pagination layout="prev, next" :total="total" :current-page="activepage" :page-size="limit"
@current-change="pageChange" />
</el-aside>
<el-main class="mian">
Main content
</el-main>
</el-container>
</el-container>
</template>
<style scoped>
.out {
background-color: #ccc;
position: relative;
}
.header {
height: 50px;
position: absolute;
top: 0;
left: 0;
right: 0;
border-bottom: 1px solid #7a7a7a;
@apply bg-blue-200;
}
.aside {
width: 200px;
position: absolute;
top: 51px;
left: 0;
bottom: 40px;
background-color: yellowgreen;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.mian {
position: absolute;
top: 51px;
left: 200px;
right: 0;
bottom: 0;
background-color: rgb(248, 160, 160);
}
:deep(.el-main) {
padding-bottom: 10px !important;
margin-bottom: 10px !important;
}
.asideList li {
height: 40px;
/* background-color: red; */
display: flex;
justify-content: space-between;
align-items: center;
}
.asideList li:hover {
background-color: #7a7a7a;
}
.active {
background-color: red;
}
</style>
数据交互
api 请求
1
2
3
4
5
export const addimagelist=(obj)=>{
return axios.get('/admin/image_class',obj)
}携带相关数据发送请求
1
2
3
4
5
6
7
8
9// 点击确认
const onsubmit=()=>{
addimagelist(form.value).then((result) => {
getData(1)
}).catch((err) => {
});
}
}6.6新增图片分类
api
1
2
3
4
export const addimagelist=(obj)=>{
return axios.post('/admin/image_class',obj)
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- 点击编辑 拿到当前的这一项,然后将当前这一项的内容给到抽屉的form 进行编辑
```js
<ul class="asideList">
<li v-for="(item, index) in imageList" :key="index" @click="changeActive(index)" :class="{active:index===activeIndex}">
<span>{{ item.name }}</span>
<div> <el-icon @click="edit(index)"> <Cpu /></el-icon>
<el-icon> <Cpu /></el-icon>
</div>
</li>
</ul>
// 激活相关
const activeIndex=ref(0)
const changeActive=(index)=>{
activeIndex.value=index
}
// 编辑
const edit=(index)=>{
console.log(index);
// 打开弹出层
deawerRef.value.open()
form.value.name=imageList.value[index].name
form.value.order=imageList.value[index].order
}
6.7修改图片分类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210<script setup>
import { ref } from "vue";
import { getimageList, addimagelist } from "@/api/image.js"
const h = window.innerHeight || document.body.clientHeight
const containerHeight = h - 64 - 40
const imageList = ref([])
// 分页数据
// 当前页
const activepage = ref(1)
// 总共有多少条
const total = ref(0)
// 一页多少条
const limit = ref(10)
// 获取图片列表数据
const getData = (p = null) => {
if (p) {
getimageList(p).then((result) => {
imageList.value = result.list
total.value = result.totalCount
})
} else {
getimageList(activepage.value).then((result) => {
imageList.value = result.list
total.value = result.totalCount
})
}
}
getData()
const pageChange = (p) => {
getData(p)
}
import Drawer from '@/components/Drawer.vue';
// 新增图片分类
// 打开抽屉组件
const deawerRef = ref(null)
// 当前编辑的id
const editId = ref(null)
const form = ref({
name: "",
order: 50
})
// 点击确认
const onsubmit = () => {
if (edit.value) {
// 执行新增操作
addimagelist(form.value).then((result) => {
getData(1)
}).catch((err) => {
});
} else {
// 执行修改操作
}
}
// 激活相关
const activeIndex = ref(0)
const changeActive = (index) => {
activeIndex.value = index
}
// 新增
const addimgList = () => {
edit.value = null
// 打开弹出层
deawerRef.value.open()
// 数据置空
form.value.name = ""
form.value.order = 50
// 打开弹出层
deawerRef.value.open()
}
// 编辑
const edit = (index) => {
edit.value == imageList.value[index].id
// 打开弹出层
deawerRef.value.open()
form.value.name = imageList.value[index].name
form.value.order = imageList.value[index].order
}
</script>
<template>
<Drawer ref="deawerRef" @submit="onsubmit">
<el-form :model="form" ref="formref" :rules="rules" label-width="80px" :inline="false" size="normal">
<el-form-item label="名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="排序">
<el-input-number v-model="form.order" />
</el-form-item>
</el-form>
</Drawer>
<el-container :style="{ height: containerHeight + 'px' }" class="out">
<el-header class="header">
<el-button type="primary" size="default" @click="addimgList">新增图片分类</el-button>
</el-header>
<el-container>
<el-aside class="aside">
<ul class="asideList">
<li v-for="(item, index) in imageList" :key="index" @click="changeActive(index)"
:class="{ active: index === activeIndex }">
<span>{{ item.name }}</span>
<div> <el-icon @click="edit(index)">
<Cpu />
</el-icon>
<el-icon>
<Cpu />
</el-icon>
</div>
</li>
</ul>
<el-pagination layout="prev, next" :total="total" :current-page="activepage" :page-size="limit"
@current-change="pageChange" />
</el-aside>
<el-main class="mian">
Main content
</el-main>
</el-container>
</el-container>
</template>
<style scoped>
.out {
background-color: #ccc;
position: relative;
}
.header {
height: 50px;
position: absolute;
top: 0;
left: 0;
right: 0;
border-bottom: 1px solid #7a7a7a;
@apply bg-blue-200;
}
.aside {
width: 200px;
position: absolute;
top: 51px;
left: 0;
bottom: 40px;
background-color: yellowgreen;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
}
.mian {
position: absolute;
top: 51px;
left: 200px;
right: 0;
bottom: 0;
background-color: rgb(248, 160, 160);
}
:deep(.el-main) {
padding-bottom: 10px ;
margin-bottom: 10px ;
}
.asideList li {
height: 40px;
/* background-color: red; */
display: flex;
justify-content: space-between;
align-items: center;
}
.asideList li:hover {
background-color: #7a7a7a;
}
.active {
background-color: red;
}
</style>6.8删除图片分类
给按钮绑定点击事件, 拿到当前要删除的哪一项,然后发送请求进行删除即可
6.9 图片列表
点击事件,发送请求获取数据渲染页面,分页功能
1
2
3
4
5
6
7
8
9
10
// 激活相关
const activeIndex = ref(0)
const changeActive = (index) => {
debugger
activeIndex.value = index
getimgs(imageList.value[index].id,1)
debugger
}6.10 图片列表渲染
拿数据渲染,切换后重新渲染
评论匿名评论隐私政策✅ 你无需删除空行,直接评论以获取最佳展示效果