VUE
vue-admin
1. 项目搭建
创建项目
1.cd C:\Users\lenovo\Desktop\HZ-H5-2213\1213 (进入文件)
npm init vue@latest (创建项目的命令)
Need to install the following packages: create-vue@latest Ok to proceed? (y) y
y 创建项目
√ Project name: ... vue-admin 项目名称 √ Add TypeScript? ... No / Yes 是否添加ts √ Add JSX Support? ... No / Yes 是否添加 jsx √ Add Vue Router for Single Page Application development? ... No / Yes 是否添加路由 √ Add Pinia for state management? ... No / Yes 是否添加 pinia √ Add Vitest for Unit Testing? ... No / Yes 是否测试 √ Add an End-to-End Testing Solution? » No 是否测试 √ Add ESLint for code quality? ... No / Yes 是否设置编码风格(严格模式)
文件目录介绍
a vue-admin 项目目录
b .vscode vscode 中的自带文件
b node_modules 项目的依赖文件
b public 项目的资源公共文件
b .gitignore git忽略文件,不上传提交的文件
b index.html 项目中唯一的 html 文件,项目的入口
b package.json 项目中 node 的配置文件
b README.md 项目的说明文件
b vite.config.js 项目的配置文件
b src 写代码的主场
c assets 项目静态资源文件,会放图片或者样式用
c components 放自己封装的组件用的文件夹
c router 项目路由文件夹
c views (pages) 页面文件
c App.vue 项目的根组件
c main.js 项目入口 js 文件
vscode组件
vscode-styled-components
Vue 3 Snippets
Vue Language Features (Volar)
Vue VSCode Snippets
2. 项目路由基础
因为我们在创建项目的时候有选择 vue-router 所以我们无需自己手动安装
- 导入 vue-router
- 创建 路由规则
- 创建路由实例
- 导出路由实例
- 在main.js导入并挂载路由
createWebHistory 路由模式 (History)
History 前面用的是 / 开头,一般是要后端配合使用,容易产生 404
createWebHashHistory 路由模式 (Hash)
Hash 前面用的是 # 开头,无需配合
// src/router/index.js
/*
1. 先导入 vue-router
createRouter 创建路由的方法
createWebHistory 路由模式 (History)
History 前面用的是 / 开头,一般是要后端配合使用,容易产生 404
createWebHashHistory 路由模式 (Hash)
Hash 前面用的是 # 开头,无需配合
*/
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
// 导入 home 组件
import HomeView from '../views/HomeView.vue'
// 路由规则配置
const routes = [
{
// 访问时的路径
path: '/',
// 命名路由,路由别名
name: 'home',
// 当访问到对应路由后需要展示的组件
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// 路由懒加载,对于一些不确定立即需要显示的页面,可以使用懒加载
component: () => import('../views/AboutView.vue')
}
]
// 创建 router 实例
const router = createRouter({
// 配置路由模式
history: createWebHashHistory(import.meta.env.BASE_URL),
// 配置路由规则
routes
})
// 导出路由
export default router
// src/main.js
// 解构出 createApp
import { createApp } from 'vue'
// 导入根组件
import App from './App.vue'
// 导入路由配置
import router from './router'
// 导入全局样式
import './assets/main.css'
// 创建一个梗 app 实例
const app = createApp(App)
// 将路由挂载到 app 实例中
app.use(router)
// 将 app 实例挂载到 id 为 app 的 元素上
app.mount('#app')
3. 页面UI组件选择
安装
# 选择一个你喜欢的包管理器
# NPM
$ npm install element-plus --save
# Yarn
$ yarn add element-plus
# pnpm
$ pnpm install element-plus
配置自动导入
导入方式分 3 种
- 全部导入,方便开发,不需要考虑用到什么组件导入什么组件,弊端是打包后项目的体积会变大
- 按需导入,自动按需导入不需要考虑导入什么组件,而且打包后项目体积不会特别大
- 手动导入,比较麻烦,用什么就要导什么。打包体积小
cnpm install -D unplugin-vue-components unplugin-auto-import
// vite.config.js
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// ++++++++++
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
// ... +++++++++
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
}
}
})
格式化样式
因为我们开发项目阶段会使用到各种标签,有些标签会有一些自带的样式属性,我们可以通过格式化样式的方式将一些常见的不需要的样式去掉
npm install --save normalize.css
安装完成后在 main.js 中导入即可
// src/main.js
// 解构出 createApp
import { createApp } from 'vue'
// 导入根组件
import App from './App.vue'
// 导入路由配置
import router from './router'
// 导入格式化样式文件
import 'normalize.css/normalize.css' // ++++++++++++++++++
// 导入 element-plus 样式
// import 'element-plus/dist/index.css'
// 导入全局样式
// import './assets/main.css'
// 创建一个梗 app 实例
const app = createApp(App)
// 将路由挂载到 app 实例中
app.use(router)
// 将 app 实例挂载到 id 为 app 的 元素上
app.mount('#app')
添加预编译 CSS
因为在开发中写 css 有些时候不够便利,所以我们在开发中可以使用 less 或者 sass 来进行开发
sass: 安装命令 cnpm i sass
只需要安装不需要做其他多余的配置
在写样式的时候需要在 style 标签中添加一个 lang="scss"
icon 组件的安装
在项目开发过程中会用到很多小的图标,这些需要我们自己来安装使用
npm install @element-plus/icons-vue
在组件中如果想要使用 icon
- 导入对应需要使用的 icon
- 注册该 icon 组件
- 使用 icon
import { Fold, Expand } from "@element-plus/icons-vue";
components: {
Fold,
Expand
}
<el-icon size="30">
<Fold />
</el-icon>
4. 主体搭建
容器布局
https://element-plus.gitee.io/zh-CN/component/container.html#常见页面布局
编写 CSS 样式
<!-- lang="scss" 定义我们的样式是使用 sass 写的 -->
<style lang="scss" scoped>
.el-container {
background-color: #f8f9fa;
}
.el-aside {
background-color: #d0ebff;
}
.el-header {
background-color: #ced4da;
}
</style>
添加标题和点击收起展开
<script>
// 在项目中 万物皆模块
import zfb from "../assets/zfb.png";
import { Fold, Expand } from "@element-plus/icons-vue";
export default {
data () {
return {
url: zfb,
collapse: false
}
},
computed: {
headerLeft () {
return this.collapse ? 'Expand' : 'Fold'
},
asideWidth () {
return this.collapse ? '54px' : '200px'
}
},
components: {
Fold,
Expand,
AsideCom
}
}
</script>
<template>
<div class="common-layout">
<el-container>
<!-- 左侧侧边栏 -->
<el-aside>
<div class="logo-box">
<el-image style="width: 32px; height: 32px" :src="url"></el-image>
<h2 class="manage-title" v-show="!collapse"> 嗨购后台管理系统 </h2>
</div>
</el-aside>
<!-- 右侧内容 -->
<el-container>
<!-- 头部 -->
<el-header>
<el-icon size="30" @click="collapse = !collapse">
<component :is="headerLeft"></component>
</el-icon>
</el-header>
<!-- 主体内容 -->
<el-main>Main</el-main>
</el-container>
</el-container>
</div>
</template>
<!-- lang="scss" 定义我们的样式是使用 sass 写的 -->
<style lang="scss" scoped>
.el-container {
background-color: #f8f9fa;
.el-aside {
background-color: #d0ebff;
/* 直接将计算属性的值拿过来 */
width: v-bind(asideWidth);
/* overflow: hidden; */
transition: all 0.2s;
.logo-box {
height: 60px;
display: flex;
align-items: center;
padding: 0 10px;
box-sizing: border-box;
.el-image {
margin-right: 10px;
}
.manage-title {
font-size: 16px;
}
}
}
}
.el-header {
background-color: #ced4da;
display: flex;
justify-content: space-between;
align-items: center;
}
</style>
侧边栏布局
创建一个组件 components/AsideCom.vue
找到组件
https://element-plus.gitee.io/zh-CN/component/menu.html#侧栏
<!-- src/components/AsideCom.vue -->
<script>
import { Avatar } from "@element-plus/icons-vue";
export default {
data () {
return {
}
},
components: {
Avatar
}
}
</script>
<template>
<el-menu default-active="2" class="el-menu-vertical-demo">
<el-menu-item> 首页 </el-menu-item>
<el-sub-menu index="1">
<template #title>
<el-icon>
<Avatar />
</el-icon>
<span>账号管理</span>
</template>
<el-menu-item> 管理员列表 </el-menu-item>
<el-menu-item> 用户列表 </el-menu-item>
</el-sub-menu>
</el-menu>
</template>
在 HomeView.vue 中导入组件,然后注册组件,使用组件
<script>
// 在项目中 万物皆模块
import zfb from "../assets/zfb.png";
import { Fold, Expand } from "@element-plus/icons-vue";
// @ 代表的是 src 路径
import AsideCom from "@/components/AsideCom.vue"; // +++++++++++++
export default {
data () {
return {
url: zfb,
collapse: false
}
},
computed: {
headerLeft () {
return this.collapse ? 'Expand' : 'Fold'
},
asideWidth () {
return this.collapse ? '54px' : '200px'
}
},
components: {
Fold,
Expand,
AsideCom // +++++++++++++++++
}
}
</script>
<template>
<div class="common-layout">
<el-container>
<!-- 左侧侧边栏 -->
<el-aside>
<div class="logo-box">
<el-image style="width: 32px; height: 32px" :src="url"></el-image>
<h2 class="manage-title" v-show="!collapse"> 嗨购后台管理系统 </h2>
</div>
<!-- 使用自定义组件 ++++++++++++++ -->
<AsideCom />
</el-aside>
<!-- 右侧内容 -->
<el-container>
...
</el-container>
</el-container>
</div>
</template>
<!-- lang="scss" 定义我们的样式是使用 sass 写的 -->
<style lang="scss" scoped>
...
</style>
5 登录页面
静态页面搭建
<script>
export default {
name: 'LoginView',
// 因为要让每个组件都有自身独立的数据
data () {
return {
adminname: '',
password: ''
}
}
}
</script>
<template>
<div class="login-container">
<div class="form-wrap">
<h2 class="header">嗨购后台管理系统</h2>
<el-input v-model="adminname" placeholder="请输入管理员账号" />
<el-input class="psw" v-model="password" placeholder="请输入密码" />
<el-button type="success">登录</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
.login-container {
height: 100%;
background-color: #dbe4ff;
.form-wrap {
position: absolute;
left: 50%;
top: 50%;
width: 360px;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 15px;
padding: 32px 32px 20px;
.header {
text-align: center;
margin-bottom: 32px;
}
.psw {
margin-top: 20px;
}
.el-button {
width: 100%;
margin-top: 20px;
}
}
}
</style>
表单验证
如果需要使用到正则表达式,可以在 vscode 中安装一个 铁皮盒子 的插件,按 F1 进行选择我们要的正则表达式
- 先做表单验证
- 加密
- 提交到服务器
<script>
import { ElMessage } from 'element-plus'
export default {
name: 'LoginView',
// 因为要让每个组件都有自身独立的数据
data () {
return {
adminname: '',
password: ''
}
},
methods: {
submitForm () {
console.log('被调用了', this.adminname, this.password);
if (this.adminname.length < 5) {
// 长度不够
ElMessage.error('管理员账号要大于 5 位')
// 让代码不在执行
return
}
if (!/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]/.test(this.password)) {
ElMessage.error('密码一定要包含数字,大写字母和小写字母')
return
}
// console.log(/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]/.test(this.password));
}
}
}
</script>
<template>
<div class="login-container">
<div class="form-wrap">
<h2 class="header">嗨购后台管理系统</h2>
<el-input v-model="adminname" placeholder="请输入管理员账号" />
<el-input class="psw" v-model="password" placeholder="请输入密码" />
<el-button type="success" @click="submitForm">登录</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
...
</style>
MD5 加密
https://www.npmjs.com/package/md5
安装 md5: npm i md5
在需要使用加密的组件中导入 md5 函数
import md5 from "md5";
// md5 是一个加密函数,我们可以将要加密的字符串传递进去,会返回一个加密好的字符串
// 我们不能相信用户,因为我们无法阻止用户将密码设置为 123456
// 123 456 担心安全问题,所以我们要将用户输入的密码做一次处理
// 注册和登录一定要使用 一样的加密方式
console.log(md5('千峰123教育456前端'));
axios 数据请求
安装命令: npm install axios
直接使用 axios
在需要数据请求的页面中先导入
发送数据请求
import axios from 'axios'
axios.get('http://121.89.205.189:3000/api/pro/list')
.then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
})
封装 axios
- 配置 baseURL
- 请求拦截器,就是在请求之前你还需要做什么配置
- 响应拦截器,就是服务器给出响应后,返回到前端前我们需要做什么操作, res.data
- 就是封装各种请求的方法如 get,post,put...
// src/utils/request.js
import axios from 'axios'
// 封装 baseUrl
// 开发环境(写代码的阶段) 生产环境(代码已经部署服务器了) 测试环境(代码写好阶段)
// development production production
// npm run dev npm run build npm run build
// 获取当前环境,判断当前环境是否为开发环境
// const isDev = process.env.NODE_ENV == 'development'
/*
开发阶段:192.168.1.111
生产阶段:http://121.89.205.189
*/
const request = axios.create({
// isDev 为真
// baseURL: isDev ? '开发环境' : '生产环境'
// baseURL: isDev ? 'http://121.89.205.189:3000/admin' : 'http://121.89.205.189:3000/admin'
// baseURL 是用来配置基础路径用的,以后数据请求的时候不需要每次都写对应的地址了
baseURL: 'http://121.89.205.189:3000/admin',
timeout: 60000
})
// 添加请求拦截器
request.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
// 在发送请求前我们可以将公用的属性设置上
// 比如可以在这里配置对应的 token
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});
// 添加响应拦截器
request.interceptors.response.use(function (response) {
// 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
// 10119 token失效
// 判断用户的登录状态
// 因为服务器响应的时候有两个 data,所以我这里 return response.data;
// 让页面中需要一次 .data 即可
return response.data;
}, function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
});
// 自定义各种数据请求的 axios
export default function ajax (config) {
// 数据请求的时候我们需要什么参数
// 1. 先获取到请求的一些必要参数
const { url = '', method = 'GET', data = {}, headers = {} } = config
// 2. 判断我们请求的类型 get GET GeT
switch (method.toUpperCase()) {
case 'GET':
return request.get(url, { params: data })
case 'POST':
// 1. 表单提交数据
if (headers['content-type'] == 'application/x-www-form-url-encoded') {
// 转换参数类型,格式化数据
const obj = new URLSearchParams()
for (const key in data) {
obj.append(key, data[key])
}
return request.post(url, obj, { headers })
}
// 2. 文件数据
if (headers['content-type'] == 'multipart/form-data') {
// 处理文件的对象
const obj = new FormData()
for (const key in data) {
obj.append(key, data[key])
}
return request.post(url, obj, { headers })
}
// 3. json 数据提交
return request.post(url, data)
case 'PUT':
// 修改数据 --- 数据的更新
return request.put(url, data)
case 'DELETE':
// 删除数据
return request.delete(url, { data })
case 'PATCH':
// 更新局部资源
return request.patch(url, data)
default:
// 如果前面全部都不是
return request.request(config)
}
}
登录功能的实现
创建一个 api/user.js
导入刚封装的 axios
封装请求接口 loginFn
// src/api/user.js
// 导入我们刚封装的 axios
import ajax from '@/utils/request'
// 管理系统登录接口的封装
export function loginFn (params) {
return ajax({
method: 'PoSt',
url: '/admin/login',
data: params
})
}
在 login 页面中我们导入 api/user.js
然后发送数据请求
请求成功后直接跳转到首页
<script>
import { ElMessage } from 'element-plus'
import md5 from "md5";
import axios from 'axios'
// 导入数据请求的方法
import { loginFn } from '@/api/user'
export default {
name: 'LoginView',
// 因为要让每个组件都有自身独立的数据
data () {
return {
adminname: '',
password: ''
}
},
methods: {
submitForm () {
// console.log('被调用了', this.adminname, this.password);
// if (this.adminname.length < 5) {
// // 长度不够
// ElMessage.error('管理员账号要大于 5 位')
// // 让代码不在执行
// return
// }
// if (!/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]/.test(this.password)) {
// ElMessage.error('密码一定要包含数字,大写字母和小写字母')
// return
// }
// console.log(/^(?![a-zA-Z]+$)(?![A-Z0-9]+$)(?![A-Z\W_!@#$%^&*`~()-+=]+$)(?![a-z0-9]+$)(?![a-z\W_!@#$%^&*`~()-+=]+$)(?![0-9\W_!@#$%^&*`~()-+=]+$)[a-zA-Z0-9\W_!@#$%^&*`~()-+=]/.test(this.password));
// md5 是一个加密函数,我们可以将要加密的字符串传递进去,会返回一个加密好的字符串
// 我们不能相信用户,因为我们无法阻止用户将密码设置为 123456
// 123 456 担心安全问题,所以我们要将用户输入的密码做一次处理
// 注册和登录一定要使用 一样的加密方式
// console.log(md5('千峰123教育456前端'));
// 发送数据请求
// 直接发送请求是没有任何问题的,但是如果页面多请求多,服务器地址一旦
// 发生变化,那么我就需要修改很多个地方,所以我们要将其封装起来
// axios.get('http://121.89.205.189:3000/api/pro/list')
// .then(res => {
// console.log(res);
// })
// .catch(err => {
// console.log(err);
// })
// ++++++++++++++++++++++++++++++++
loginFn({
adminname: this.adminname,
password: this.password
}).then(res => {
console.log(res);
if (res.code === '10005') {
// 未注册
ElMessage.error(res.message)
return
} else if (res.code === '10003') {
// 密码错误
ElMessage.error(res.message)
return
} else {
// this.$router 是我们的路由对象
// .push 需要进入到哪个路由中
this.$router.push('/')
// console.log(this.$router);
}
})
// ++++++++++++++++++++++++++++++++
}
}
}
</script>
<template>
<div class="login-container">
<div class="form-wrap">
<h2 class="header">嗨购后台管理系统</h2>
<el-input v-model="adminname" placeholder="请输入管理员账号" />
<el-input class="psw" v-model="password" placeholder="请输入密码" />
<el-button type="success" @click="submitForm">登录</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
...
</style>
6.vuex 全局状态(数据)管理
安装 vue: npm install vuex@next --save
在 src 中创建一个 store 的文件夹,然后在内部创建一个 index.js
导入 vuex 然后创建一个 store
将 store 导出去
在 main.js 中导入 sotre
将 store 挂载在 app 上
定义状态
// src/store/index.js
import { createStore } from "vuex";
// 定义全局状态的实例
const store = createStore({
// 严格模式,在生产环境中不要打开
strict: process.env.NODE_ENV !== 'production',
state () {
// 全局状态
return {
count: 100,
}
},
// 唯一修改数据的方法
mutations: {
// 唯一修改数据的方法
add (state) {
// setTimeout(() => {
state.count++
// }, 5000)
},
addnum (state, value) {
state.count += value
}
},
// 异步修改数据的地方
actions: {
addActions (context) {
console.log(context);
// 所谓的异步也还要调用同步的方法来进行修改数据
setTimeout(() => {
context.commit('add')
}, 5000)
},
addnumActions ({ commit }, value) {
commit('addnum', value)
}
},
// 是 vuex 中的计算属性
getters: {
},
// 状态模块
modules: {
}
})
// 导出创建好的实例
export default store
在组件中使用
- 可以在模板中直接 $store.state.属性 即可
{{ $store.state.count }}
- 通过映射 mapState
import { mapState } from 'vuex' export default{ data () { return { adminname: '', password: '' } }, computed: { //++++++++++++++++++ ...mapState(['count']) } } //组件中使用 {{ count }}
组件中修改全局状态
先在全局状态中定义
mutations:vuex 中唯一修改状态的地方
actions:异步操作的地方
- 通过 this.$store.commit('') 触发 mutations。通过 this.$store.dispatch('')触发 Actions
- 通过 mapMutations 映射
import { mapState, mapMutations } from 'vuex' export default { name: 'LoginView', // 因为要让每个组件都有自身独立的数据 data () { return { adminname: '', password: '' } }, computed: { ...mapState(['count']) }, methods: { ...mapMutations(['updateUserInfo']) } } // 使用 this.updateUserInfo(res.data)
7 登录成功之后的处理
- 在全局状态定义一个用户信息和修改用户信息的方法
- 在登录成功后将数据直接更新到全局状态中即可
// src/store/index.js
import { createStore } from "vuex";
// 定义全局状态的实例
const store = createStore({
// 严格模式,在生产环境中不要打开
strict: process.env.NODE_ENV !== 'production',
state () {
// 全局状态
return {
count: 100,
// 放用户信息的对象
userInfo: {} // ++++++
}
},
// 唯一修改数据的方法
mutations: {
...
// 保存用户数据 +++++++++++
updateUserInfo (state, value) {
state.userInfo = value
}
},
})
// 导出创建好的实例
export default store
<script>
import { ElMessage } from 'element-plus'
import md5 from "md5";
import axios from 'axios'
// 导入数据请求的方法
import { loginFn } from '@/api/user'
import { mapState, mapMutations } from 'vuex' // ++++++++++
export default {
name: 'LoginView',
// 因为要让每个组件都有自身独立的数据
data () {
return {
adminname: '',
password: ''
}
},
computed: {
...mapState(['count']) // +++++++++++++++++++
},
methods: {
...mapMutations(['updateUserInfo']), // ++++++++++++++++++++
submitForm () {
loginFn({
adminname: this.adminname,
password: this.password
}).then(res => {
console.log(res);
if (res.code === '10005') {
// 未注册
ElMessage.error(res.message)
return
} else if (res.code === '10003') {
// 密码错误
ElMessage.error(res.message)
return
} else {
// 第一种修改数据的方法
// this.$store.commit('updateUserInfo', res.data)
// 第二种修改数据的方法
this.updateUserInfo(res.data) // ++++++++++++++++++++++
// this.$router 是我们的路由对象
// .push 需要进入到哪个路由中
this.$router.push('/')
// console.log(this.$router);
}
})
}
}
}
</script>
<template>
<div class="login-container">
<div class="form-wrap">
<h2 class="header"> {{ count }} 嗨购后台管理系统 {{ $store.state.count }}</h2>
<el-input v-model="adminname" placeholder="请输入管理员账号" />
<el-input class="psw" v-model="password" placeholder="请输入密码" />
<el-button type="success" @click="submitForm">登录</el-button>
</div>
</div>
</template>
<style lang="scss" scoped>
...
</style>
vuex 中的数据持久化
解决 vuex 中状态刷新页面后会恢复初始值
- 通过自己手动存储需要持久化的数据
- 通过插件来进行自动持久化
安装插件: npm install --save vuex-persistedstate
使用过程:
1.先导入 import createPersistedState from 'vuex-persistedstate'
2.配置vuex插件 plugins:[]
3.配置需要持久化的数据
plugins: [ // 创建持久化对象 createPersistedState({ // 配置需要持久化的数据 reducer: state => { return { // 需要持久化的数据 userInfo: state.userInfo } } }) ]
// src/store/index.js
import { createStore } from "vuex";
// 导入数据持久化操作的创建函数
import createPersistedState from 'vuex-persistedstate' // ++++++++++++++
// 定义全局状态的实例
const store = createStore({
// 严格模式,在生产环境中不要打开
strict: process.env.NODE_ENV !== 'production',
state () {
// 全局状态
return {
count: 100,
// 放用户信息的对象
userInfo: {
}
}
},
// 唯一修改数据的方法
mutations: {
// 唯一修改数据的方法
add (state) {
// setTimeout(() => {
state.count++
// }, 5000)
},
addnum (state, value) {
state.count += value
},
// 保存用户数据
updateUserInfo (state, value) {
state.userInfo = value
}
},
// 异步修改数据的地方
actions: {
addActions (context) {
console.log(context);
// 所谓的异步也还要调用同步的方法来进行修改数据
setTimeout(() => {
context.commit('add')
}, 5000)
},
addnumActions ({ commit }, value) {
commit('addnum', value)
}
},
// 是 vuex 中的计算属性
getters: {
},
// 状态模块
modules: {
},
// vuex配置插件
plugins: [
// 创建持久化对象
createPersistedState({
// 配置需要持久化的数据
reducer: state => {
return {
// 需要持久化的数据
userInfo: state.userInfo
}
}
})
]
})
// 导出创建好的实例
export default store
在首页验证用户是否登录
mounted () {
console.log(this.$store.state.userInfo.adminname);
if (!this.$store.state.userInfo.adminname) {
// 用户没有登录
this.$router.push('/login')
}
}
退出登录
添加退出登录的按钮
<span> 欢迎 {{ $store.state.userInfo.adminname }}<button @click="logout">退出</button> </span>
logout () { localStorage.clear() this.$store.commit('updateUserInfo', {}) this.$router.push('/login') }
8. 管理员管理
先创建路由和页面
在 views 中创建一个 manager 的文件夹
在文件夹中创建 3 个页面
<!-- src/views/manager/ManagerIndexView.vue -->
<template>
<router-view></router-view>
</template>
<!-- src/views/manager/ManagerListView.vue -->
<template>
<div>
管理员列表
</div>
</template>
<!-- src/views/manager/UserListView.vue -->
<template>
<div>
用户列表
</div>
</template>
路由创建
// src/router/index.js
/*
1. 先导入 vue-router
createRouter 创建路由的方法
createWebHistory 路由模式 (History)
History 前面用的是 / 开头,一般是要后端配合使用,容易产生 404
createWebHashHistory 路由模式 (Hash)
Hash 前面用的是 # 开头,无需配合
*/
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
// 导入 home 组件
import HomeView from '../views/HomeView.vue'
// 导入管路员和用户的组件
import MangerIndexView from "@/views/manager/MangerIndexView.vue";
import MangerListView from "@/views/manager/MangerListView.vue";
import UserListView from "@/views/manager/UserListView.vue";
// 导入轮播图管理相关组件
import BannerIndexView from '../views/banner/BannerIndexView.vue'
import BannerListView from '../views/banner/BannerListView.vue'
import AddBannerView from '../views/banner/AddBannerView.vue'
// 路由规则配置
const routes = [
{
// 访问时的路径
path: '/',
// 命名路由,路由别名
name: 'home',
// 当访问到对应路由后需要展示的组件
component: HomeView,
// 路由嵌套,配置二级路由
children: [
{
path: 'manager',
name: 'manager',
label: '账号管理',
component: MangerIndexView,
// 配置三级路由
children: [
{
path: 'managerlist',
name: 'managerlist',
label: '管理员列表',
component: MangerListView
},
{
path: 'userlist',
name: 'userlist',
label: '用户列表',
component: UserListView
}
]
},
{
path: 'banner',
name: 'banner',
label: '轮播图管理',
component: BannerIndexView,
children: [
{
path: 'bannerlist',
name: 'bannerlist',
label: '轮播图列表',
component: BannerListView,
},
{
path: 'addbanner',
name: 'addbanner',
label: '添加轮播图',
component: AddBannerView,
}
]
}
]
},
{
path: '/login',
name: 'login',
// route level code-splitting
// this generates a separate chunk (About.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// 路由懒加载,对于一些不确定立即需要显示的页面,可以使用懒加载
component: () => import('../views/LoginView.vue')
}
]
// 创建 router 实例
const router = createRouter({
// 配置路由模式
history: createWebHashHistory(import.meta.env.BASE_URL),
// 配置路由规则
routes
})
// 导出路由
export default router
管理员列表
- 获取到所有管理员数据
- 渲染管理员列表
- 添加管理员的功能
- 修改管理员信息
- 删除管理员
渲染管理员列表
// src/api/user.js
// 导入我们刚封装的 axios
import ajax from '@/utils/request'
// 管理系统登录接口的封装
export function loginFn (params) {
return ajax({
method: 'PoSt',
url: '/admin/login',
data: params
})
}
// 获取管理员列表
export function adminList () {
return ajax({
url: '/admin/list',
method: 'get'
})
}
<!-- src/views/manager/ManagerListView.vue -->
<script>
import { adminList } from '@/api/user'
export default {
data () {
return {
tableData: []
}
},
methods: {
},
mounted () {
// 获取管理员列表
adminList().then(res => {
console.log(res.data);
this.tableData = res.data
})
}
}
</script>
<template>
<div>
管理员列表
<el-table :data="tableData" style="width: 100%">
<!-- type="index" 标记当前为索引序号 -->
<!-- label 列标题 -->
<el-table-column type="index" label="序号" width="80" />
<!-- prop 列的数据字段 -->
<el-table-column prop="adminname" label="管理员名称" width="180" />
<el-table-column prop="role" label="管理员权限">
<template #default="scope">
<!-- <div> -->
<!-- {{ scope.row.role == 1 ? '管理员' : '超级管理员' }} -->
<el-tag :type="scope.row.role == 1 ? '' : 'success'">{{ scope.row.role == 1 ? '管理员' : '超级管理员'
}}</el-tag>
<!-- </div> -->
</template>
</el-table-column>
<el-table-column prop="role" label="操作">
<template #default>
<el-button size="small" type="primary">编辑</el-button>
<el-button size="small" type="success">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
分页处理
<!-- src/views/manager/ManagerListView.vue -->
<script>
import { adminList } from '@/api/user'
export default {
data () {
return {
tableData: [],
// 当前页
currentPage: 1
}
},
methods: {
},
mounted () {
// 获取管理员列表
adminList().then(res => {
console.log(res.data);
this.tableData = res.data
})
},
computed: {
// +++++++++++++++ 计算出当前页该显示什么数据
computedTableData () {
return this.tableData.slice((this.currentPage - 1) * 10, (this.currentPage - 1) * 10 + 10)
}
}
}
</script>
<template>
<div>
管理员列表
<el-table :data="computedTableData" style="width: 100%">
<!-- type="index" 标记当前为索引序号 -->
<!-- label 列标题 -->
<el-table-column type="index" :index="(currentPage - 1) * 10 + 1" label="序号" width="80" />
<!-- prop 列的数据字段 -->
<el-table-column prop="adminname" label="管理员名称" width="180" />
<el-table-column prop="role" label="管理员权限">
<template #default="scope">
<!-- <div> -->
<!-- {{ scope.row.role == 1 ? '管理员' : '超级管理员' }} -->
<el-tag :type="scope.row.role == 1 ? '' : 'success'">{{ scope.row.role == 1 ? '管理员' : '超级管理员'
}}</el-tag>
<!-- </div> -->
</template>
</el-table-column>
<el-table-column prop="role" label="操作">
<template #default>
<el-button size="small" type="primary">编辑</el-button>
<el-button size="small" type="success">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 用做分页显示 +++++++++++++++++++++++++++++++++ -->
<el-pagination
background
v-model:current-page="currentPage"
layout="prev, pager, next"
:total="tableData.length" />
</div>
</template>
添加管理员
页面布局,添加一个抽屉效果
<div class="header">
管理员列表
<el-button type="success" @click="drawer = true">添加管理员</el-button>
</div>
<!-- 抽屉效果 -->
<el-drawer v-model="drawer" title="添加管理员">
<el-form label-width="120px">
<el-form-item label="管理员账号">
<el-input placeholder="请输入管理员账号" v-model="formData.adminname" />
</el-form-item>
<el-form-item label="管理员密码">
<el-input placeholder="请输入管理员密码" v-model="formData.password" />
</el-form-item>
<el-form-item label="管理员角色">
<el-select placeholder="请选择角色" v-model="formData.role">
<el-option label="管理员" value="管理员" />
<el-option label="超级管理员" value="超级管理员" />
</el-select>
</el-form-item>
<el-form-item>
<el-tree show-checkbox ref="treeRef" :data="menus" />
</el-form-item>
</el-form>
<el-button @click="add" type="primary">添加</el-button>
</el-drawer>
在添加按钮回调函数中获取我们用户输入的内容
将用户输入的内容发送给后端服务器
添加成功后关闭抽屉然后重新获取最新数据
// 获取树形结构中选中的值
formatCheckedKeys () {
// console.log(this.$refs.treeRef.getCheckedNodes(true));
// 根据选中的值得到父级路由
const list = this.$refs.treeRef.getCheckedNodes(true)
// 用来放父级路由的 label 用的
const tempList = []
// 最终结果用的
const result = []
// list: 是我们选中的子路由
list.forEach(item => {
// parent:选中后子路的父级路由
const parent = this.menus.find(mitem => {
return mitem.children.some(child => child.label == item.label)
})
// console.log('111111', parent);
// 判断当前父级路由是否在 tempList 中已经有了
// 2. 第二次遍历用户列表的时候,发现 tempList 中已经有了父级路由
if (tempList.includes(parent.label)) {
// 就直接找到父级路由,在子路由中再添加一个子路由
result.find(item => item.label == parent.label).children.push(item)
} else {
// 将对应的内容放在 tempList 中
// console.log('11111111', parent.label);
tempList.push(parent.label)
// 1. 第一次执行的时候讲管理员列表添加进去
result.push({
label: parent.label,
path: parent.path,
children: [item]
})
}
console.log(result);
// 将路由权限添加到 formData 中
this.formData.checkedKeys = result
})
},
add () {
// 将管理员数据发送到服务器
// 1. 收集数据
// console.log(this.formData);
// 获取所有的权限路由
this.formatCheckedKeys()
// 2. 将数据发给服务器
// console.log(this.formData);
this.formData.role = this.formData.role == '管理员' ? '1' : '2'
// 将数据添加服务器中
addAdmin(this.formData).then(res => {
// console.log(res);
if (res.code == '200') {
// 成功
ElMessage.success(res.message)
// 关闭抽屉效果
this.drawer = false
// 重新获取最新的数据
// 获取管理员列表
adminList().then(res => {
// console.log(res.data);
this.tableData = res.data
})
} else {
ElMessage.error(res.message)
}
})
}
所有代码
<!-- src/views/manager/ManagerListView.vue -->
<script>
import { adminList, addAdmin } from '@/api/user'
import { routes } from '@/router/index.js'
import { ElMessage } from 'element-plus'
export default {
data () {
return {
tableData: [],
// 当前页
currentPage: 1,
// 是否打开抽屉效果
drawer: false,
// 路由信息
menus: routes[0].children,
// 管理员数据
formData: {
adminname: '',
password: '',
role: '',
checkedKeys: ''
}
}
},
methods: {
// 获取树形结构中选中的值
formatCheckedKeys () {
// console.log(this.$refs.treeRef.getCheckedNodes(true));
// 根据选中的值得到父级路由
const list = this.$refs.treeRef.getCheckedNodes(true)
// 用来放父级路由的 label 用的
const tempList = []
// 最终结果用的
const result = []
// list: 是我们选中的子路由
list.forEach(item => {
// parent:选中后子路的父级路由
const parent = this.menus.find(mitem => {
return mitem.children.some(child => child.label == item.label)
})
// console.log('111111', parent);
// 判断当前父级路由是否在 tempList 中已经有了
// 2. 第二次遍历用户列表的时候,发现 tempList 中已经有了父级路由
if (tempList.includes(parent.label)) {
// 就直接找到父级路由,在子路由中再添加一个子路由
result.find(item => item.label == parent.label).children.push(item)
} else {
// 将对应的内容放在 tempList 中
// console.log('11111111', parent.label);
tempList.push(parent.label)
// 1. 第一次执行的时候讲管理员列表添加进去
result.push({
label: parent.label,
path: parent.path,
children: [item]
})
}
console.log(result);
// 将路由权限添加到 formData 中
this.formData.checkedKeys = result
})
},
add () {
// 将管理员数据发送到服务器
// 1. 收集数据
// console.log(this.formData);
// 获取所有的权限路由
this.formatCheckedKeys()
// 2. 将数据发给服务器
// console.log(this.formData);
this.formData.role = this.formData.role == '管理员' ? '1' : '2'
// 将数据添加服务器中
addAdmin(this.formData).then(res => {
// console.log(res);
if (res.code == '200') {
// 成功
ElMessage.success(res.message)
// 关闭抽屉效果
this.drawer = false
// 重新获取最新的数据
// 获取管理员列表
adminList().then(res => {
// console.log(res.data);
this.tableData = res.data
})
} else {
ElMessage.error(res.message)
}
})
}
},
mounted () {
// 获取管理员列表
adminList().then(res => {
// console.log(res.data);
this.tableData = res.data
})
},
computed: {
computedTableData () {
return this.tableData.slice((this.currentPage - 1) * 10, (this.currentPage - 1) * 10 + 10)
}
}
}
</script>
<template>
<div>
<div class="header">
管理员列表
<el-button type="success" @click="drawer = true">添加管理员</el-button>
</div>
<el-table :data="computedTableData" style="width: 100%">
<!-- type="index" 标记当前为索引序号 -->
<!-- label 列标题 -->
<el-table-column type="index" :index="(currentPage - 1) * 10 + 1" label="序号" width="80" />
<!-- prop 列的数据字段 -->
<el-table-column prop="adminname" label="管理员名称" width="180" />
<el-table-column prop="role" label="管理员权限">
<template #default="scope">
<!-- <div> -->
<!-- {{ scope.row.role == 1 ? '管理员' : '超级管理员' }} -->
<el-tag :type="scope.row.role == 1 ? '' : 'success'">{{ scope.row.role == 1 ? '管理员' : '超级管理员'
}}</el-tag>
<!-- </div> -->
</template>
</el-table-column>
<el-table-column prop="role" label="操作">
<template #default>
<el-button size="small" type="primary">编辑</el-button>
<el-button size="small" type="success">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 用做分页显示 -->
<el-pagination background v-model:current-page="currentPage" layout="prev, pager, next"
:total="tableData.length" />
<!-- 抽屉效果 -->
<el-drawer v-model="drawer" title="添加管理员">
<el-form label-width="120px">
<el-form-item label="管理员账号">
<el-input placeholder="请输入管理员账号" v-model="formData.adminname" />
</el-form-item>
<el-form-item label="管理员密码">
<el-input placeholder="请输入管理员密码" v-model="formData.password" />
</el-form-item>
<el-form-item label="管理员角色">
<el-select placeholder="请选择角色" v-model="formData.role">
<el-option label="管理员" value="管理员" />
<el-option label="超级管理员" value="超级管理员" />
</el-select>
</el-form-item>
<el-form-item>
<el-tree show-checkbox ref="treeRef" :data="menus" />
</el-form-item>
</el-form>
<el-button @click="add" type="primary">添加</el-button>
</el-drawer>
</div>
</template>
<style lang="scss" scoped>
.header {
margin-bottom: 16px;
}
</style>
修改管理员信息
添加一个属性 interfaceType 用来判断我们是添加还是编辑管理员
打开抽屉,将当前用户数据渲染在抽屉中
请求前获取所有的用户数据
将用户数据发送给后端
重新加载最新的数据
<!-- src/views/manager/ManagerListView.vue -->
<script>
import { adminList, addAdmin, updateAdmin, deleteAdmin } from '@/api/user'
import { routes } from '@/router/index.js'
import { ElMessage } from 'element-plus'
export default {
data () {
return {
tableData: [],
// 当前页
currentPage: 1,
// 是否打开抽屉效果
drawer: false,
// 路由信息
menus: routes[0].children,
// 管理员数据
formData: {
adminname: '',
password: '',
role: '',
checkedKeys: ''
},
// 默认选中项
defaultCheckedKeys: [],
interfaceType: '10086', // 10086 添加管理员 10010 编辑管理员
}
},
methods: {
// 获取树形结构中选中的值
formatCheckedKeys () {
// console.log(this.$refs.treeRef.getCheckedNodes(true));
// 根据选中的值得到父级路由
const list = this.$refs.treeRef.getCheckedNodes(true)
// 用来放父级路由的 label 用的
const tempList = []
// 最终结果用的
const result = []
// list: 是我们选中的子路由
list.forEach(item => {
// parent:选中后子路的父级路由
const parent = this.menus.find(mitem => {
return mitem.children.some(child => child.label == item.label)
})
// console.log('111111', parent);
// 判断当前父级路由是否在 tempList 中已经有了
// 2. 第二次遍历用户列表的时候,发现 tempList 中已经有了父级路由
if (tempList.includes(parent.label)) {
// 就直接找到父级路由,在子路由中再添加一个子路由
result.find(item => item.label == parent.label).children.push(item)
} else {
// 将对应的内容放在 tempList 中
// console.log('11111111', parent.label);
tempList.push(parent.label)
// 1. 第一次执行的时候讲管理员列表添加进去
result.push({
label: parent.label,
path: parent.path,
children: [item]
})
}
console.log(result);
// 将路由权限添加到 formData 中
this.formData.checkedKeys = result
})
},
editClick (row) {
this.interfaceType = '10010'
// 打开抽屉
this.drawer = true
// 获取当前的一行数据
// console.log(row);
this.formData.adminname = row.adminname
this.formData.password = ''
this.formData.role = row.role == '1' ? '管理员' : '超级管理员'
const checkedKeys = []
row.checkedKeys.forEach(parent => {
parent.children.forEach(item => {
checkedKeys.push(item.path)
})
})
// console.log(checkedKeys);
this.defaultCheckedKeys = checkedKeys
},
close () {
this.formData = {}
this.defaultCheckedKeys = []
if (this.$refs.treeRef) {
this.$refs.treeRef.setCheckedKeys([])
}
},
update () {
// 获取表单数据
console.log(this.formData);
if (this.formData.adminname == '' || this.formData.password == '') {
ElMessage.error('管理员名称和密码不能为空')
return
}
this.formData.role = this.formData.role == '管理员' ? '1' : '2'
// 获取当前用户选中的权限
this.formatCheckedKeys()
// 提交修改信息了
updateAdmin(this.formData).then(res => {
// console.log(res);
// 关闭抽屉效果
this.drawer = false
// 重新获取最新的数据
// 获取管理员列表
adminList().then(res => {
// console.log(res.data);
this.tableData = res.data
})
})
},
},
mounted () {
// 获取管理员列表
adminList().then(res => {
// console.log(res.data);
this.tableData = res.data
})
},
computed: {
computedTableData () {
return this.tableData.slice((this.currentPage - 1) * 10, (this.currentPage - 1) * 10 + 10)
}
}
}
</script>
<template>
<div>
...
<!-- 用做分页显示 -->
<el-pagination background v-model:current-page="currentPage" layout="prev, pager, next"
:total="tableData.length" />
<!-- 抽屉效果 -->
<el-drawer @close="close" v-model="drawer">
<template #header>
<h4> {{ interfaceType == '10086' ? '添加管理员' : '编辑管理员' }}</h4>
</template>
<el-form label-width="120px">
<el-form-item label="管理员账号">
<el-input placeholder="请输入管理员账号" v-model="formData.adminname" />
</el-form-item>
<el-form-item label="管理员密码">
<el-input placeholder="请输入管理员密码" v-model="formData.password" />
</el-form-item>
<el-form-item label="管理员角色">
<el-select placeholder="请选择角色" v-model="formData.role">
<el-option label="管理员" value="管理员" />
<el-option label="超级管理员" value="超级管理员" />
</el-select>
</el-form-item>
<el-form-item>
<!-- :default-checked-keys="" -->
<el-tree :default-checked-keys="defaultCheckedKeys" :default-expand-all="true" show-checkbox
ref="treeRef" :data="menus" node-key="path" />
</el-form-item>
</el-form>
<el-button v-if="interfaceType == '10086'" @click="add" type="primary">添加</el-button>
<!-- +++++++++++++++++++++++++ -->
<el-button v-else type="primary" @click="update">修改</el-button>
</el-drawer>
</div>
</template>
<style lang="scss" scoped>
.header {
margin-bottom: 16px;
}
</style>
删除管理员
第一步先给按钮添加一个点击事件,同时将当前数据传递过去
第二步发送数据请求将 adminid 传递服务器中然后进行删除
deleteClick (row) {
// console.log(row);
deleteAdmin({ adminid: row.adminid }).then(res => {
if (res.code == 200) {
// 重新获取最新的数据
// 获取管理员列表
adminList().then(res => {
// console.log(res.data);
this.tableData = res.data
})
} else {
ElMessage.error('删除失败...')
}
})
}
9.面包屑
创建一个面包屑组件
将路由导入然后格式化为 map 对象 key - value
将当前路由的 key 和 value 获取到然后存入 list 数组中
遍历 list 数组,渲染内容
<script>
import { routes } from '@/router/index'
export default {
data () {
// 用来记录所有路由 name 对应的 label
this.routesMap = new Map()
// 用来处理将路由处理成 map
this.initRoutesMap(routes)
console.log(this.routesMap);
return {
list: []
}
},
methods: {
/*
0: {"home" => "首页"}
1: {"manager" => "账号管理"}
2: {"managerlist" => "管理员列表"}
3: {"userlist" => "用户列表"}
4: {"banner" => "轮播图管理"}
5: {"bannerlist" => "轮播图列表"}
6: {"addbanner" => "添加轮播图"}
7: {"login" => undefined}
*/
initRoutesMap (routes) {
routes.forEach(route => {
this.routesMap.set(route.name, route.label)
// 如果有子路由
if (route.children) {
this.initRoutesMap(route.children)
}
})
}
},
mounted () {
// 带 r 就是用来做页面路由跳转用的
// this.$router.push()
// 不带 r 的是用来做路由信息查询用的
// this.$route
// console.log(this.$route);
},
watch: {
$route: {
immediate: true,
handler (route) {
// 处理路径
this.list = route.matched.map(item => {
/*
[
{
name: 'home',
label: '首页'
},
{
name: 'manager',
label: '账号管理'
}
]
*/
return {
name: item.name,
label: this.routesMap.get(item.name)
}
})
}
}
}
}
</script>
<template>
<el-breadcrumb separator=">">
<el-breadcrumb-item v-for="item in list" :key="item.name" :to="{ name: item.name }">{{ item.label
}}</el-breadcrumb-item>
<!-- <el-breadcrumb-item :to="{ path: '/' }">homepage</el-breadcrumb-item>
<el-breadcrumb-item><a href="/">promotion management</a></el-breadcrumb-item>
<el-breadcrumb-item>promotion list</el-breadcrumb-item>
<el-breadcrumb-item>promotion detail</el-breadcrumb-item> -->
</el-breadcrumb>
</template>
<style lang="scss" scoped>
.el-breadcrumb {
margin-bottom: 20px;
}
</style>
10 用户列表
先在 api/user.js 文件中先写一个获取数据的方法
在 UserListView.vue 文件中调用该方法
拿到数据并渲染数据
<!-- src/views/manager/UserListView.vue -->
<script>
import { getUserList } from '@/api/user'
export default {
data () {
return {
userlist: [],
currentPage: 1
}
},
mounted () {
getUserList().then(res => {
console.log(res.data);
this.userlist = res.data
})
},
computed: {
computedTableData () {
return this.userlist.slice((this.currentPage - 1) * 10, (this.currentPage - 1) * 10 + 10)
}
}
}
</script>
<template>
<div>
<div>
用户列表
</div>
<el-table :data="computedTableData" style="width: 100%">
<el-table-column prop="telcode" label="用户名" />
<el-table-column prop="tel" label="手机号" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="userid" label="用户 id" />
</el-table>
<!-- 用做分页显示 -->
<el-pagination background v-model:current-page="currentPage" layout="prev, pager, next"
:total="userlist.length" />
</div>
</template>
# 11轮播图管理
添加轮播图
先获取到用户输入和选择的图片
将用户的内容上传到服务器
上传成功后跳转到轮播图列表页面
<script>
import { Plus } from '@element-plus/icons-vue'
import { addBanner } from '@/api/banner'
export default {
data () {
return {
imageUrl: '',
formData: {
alt: '',
link: '',
img: ''
}
}
},
components: {
Plus
},
methods: {
httpRequest (data) {
console.log(data.file);
// 根据文件生成一个图片的 url 地址,该地址是用于展示的临时的
// this.imageUrl = URL.createObjectURL(data.file)
/*
1. 先获取用户选择的图片
2. 将用户选择的图片上传到存放图片的服务器,该服务器会给你返回一个图片地址
3. 将图片地址发送给自己的后端做存储
*/
// 创建一个文件加载器
let reader = new FileReader()
// 指定加载其开始加载文件
reader.readAsDataURL(data.file)
// 加载完成后的回调函数
reader.onload = () => {
// 将文件加载为 base64 的格式
// console.log(reader.result);
this.imageUrl = reader.result
this.formData.img = reader.result
}
},
addBanner () {
// console.log(this.formData);
addBanner(this.formData).then(res => {
// console.log(res);
if (res.code == 200) {
// 提交成功后进入轮播图列表查看
this.$router.push('/banner/bannerlist')
}
})
}
}
}
</script>
<template>
<div>
添加轮播图
<br>
<el-form>
<el-form-item label="请输入 alt">
<el-input v-model="formData.alt" placeholder="请输入 alt" />
</el-form-item>
<el-form-item label="请输入 link">
<el-input v-model="formData.link" placeholder="请输入 link" />
</el-form-item>
<el-form-item>
<el-upload :http-request="httpRequest" class="avatar-uploader" :show-file-list="false">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<el-button @click="addBanner"> 添加 </el-button>
</el-form>
</div>
</template>
<style lang="scss" scoped>
.avatar-uploader .avatar {
width: 178px;
height: 178px;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>
轮播图列表
请求获取到轮播图列表数据
将该数据展示出来
<script>
import { getBannerList } from '@/api/banner'
export default {
data () {
return {
bannerList: []
}
},
mounted () {
getBannerList().then(res => {
// console.log(res);
this.bannerList = res.data
})
}
}
</script>
<template>
<el-table :data="bannerList" style="width: 100%">
<el-table-column type="index" label="序号" />
<el-table-column prop="img" label="图片">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-image :src="scope.row.img" />
</div>
</template>
</el-table-column>
<el-table-column prop="alt" label="提示信息" />
<el-table-column prop="link" label="链接" />
</el-table>
</template>
<style lang="scss" scoped>
.el-image {
width: 80px;
}
</style>
12. echarts
https://echarts.apache.org/zh/index.html
- 安装 npm install echarts --save
- 导入图表
- 将图表绑定在指定元素上
- 设置一个 option
<script>
import * as echarts from 'echarts';
export default {
data () {
return {
myChart: null
}
},
methods: {
changeType (type, x, y) {
this.myChart.setOption({
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
data: x || ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: type || 'pie',
data: y || [5, 20, 36, 10, 10, 20]
}
]
});
}
},
mounted () {
// 基于准备好的dom,初始化echarts实例
this.myChart = echarts.init(document.getElementById('main'));
// 绘制图表
this.changeType('pie')
}
}
</script>
<template>
<div>
<el-button @click="changeType('line')">折线图</el-button>
<el-button type="primary" @click="changeType('bar')">柱状图</el-button>
<el-button type="success" @click="changeType('pie')">饼图</el-button>
<el-button type="info" @click="changeType('scatter')">散点图</el-button>
<el-button type="warning"
@click="changeType('line', ['冰箱', '彩电', '洗衣机', '空调', 'VCD', 'DVD'], ['1000', '150', '5888', '3500', '50', '550'])">电器销量</el-button>
<el-button type="danger"
@click="changeType('', ['草莓', '榴莲', '柠檬', '橙子', '苹果', '圣女果'], ['100', '80', '200', '500', '99', '666'])">水果销量</el-button>
</div>
<div id="main"></div>
</template>
<style lang="scss" scoped>
#main {
height: 600px;
background-color: white;
}
</style>
13 文件导入导出
文件导入
安装插件 : npm install xlsx
选择文件
将文件读成数据流
将数据流读为对象
将对象中工作表1 中的内容读取出来
然后将读取的内容转换成 json
将数据渲染出来
<script>
import * as XLSX from 'xlsx'
export default {
data () {
return {
tableData: []
}
},
methods: {
importClick () {
// 选择文件按钮的点击事件
this.$refs.inp.click()
},
importChange (event) {
// 选中文件后的回调函数
console.log(event.target.files[0]);
// 1. 获取到我们选中的文件
const file = event.target.files[0]
// 创建文件的加载器
const reader = new FileReader()
// 将文件读取成数据流
reader.readAsBinaryString(file)
reader.onload = () => {
// console.log(reader.result);
// 将我们的数据流转换成 js 对象
const boxx = XLSX.read(reader.result, { type: 'binary' })
// 将 工作表1 中的数据提出来
let res = boxx.Sheets['工作表1']
// 将 工作表1 中的内容转换成 json 数据
res = XLSX.utils.sheet_to_json(res)
console.log(res);
//
this.tableData = res
}
}
}
}
</script>
<template>
<div>
<el-button type="success" @click="importClick"> 选择文件 </el-button>
<input ref="inp" hidden type="file" @change="importChange">
<el-table :data="tableData" style="width: 100%">
<el-table-column type="index" label="序号" />
<el-table-column label="图片">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-image :src="scope.row.img1" />
</div>
</template>
</el-table-column>
<el-table-column prop="category" label="分类" />
<el-table-column prop="proname" label="名称" />
</el-table>
</div>
</template>
文件导出
- 直接通过 a 标签进行文件的下载
- 通过 js 进行导出文件
先安装: cnpm install js-export-excel
创建一个文件导出的配置对象
该对象中需要配置文件的名称
配置需要导出的数据
将对象生成为表格然后进行保存
<script>
import { proList } from '@/api/pro'
// 1. 导入
import ExportJsonExcel from 'js-export-excel'
export default {
data () {
return {
tableData: []
}
},
mounted () {
proList().then(res => {
// console.log(res);
this.tableData = res.data
})
},
methods: {
exportClick () {
// 2. 创建导出对象
// 文件配置
let option = {}
// 导出文件的名字
option.fileName = 'proList'
// 需要导出的内容
option.datas = [
{
// 需要导出的数据
sheetData: this.tableData,
// 工作表名称
sheetName: "工作表1",
sheetFilter: ["brand", "category", "desc", 'proname', 'img1', 'originprice'],
sheetHeader: ["品牌", "分类", '描述', '商品名称', '图片地址', '产品价格'],
columnWidths: [5, 5, 10, 20, 30, 40], // 导出后的宽度
}
];
// 3. 实现具体导出
let toExcel = new ExportJsonExcel(option)
toExcel.saveExcel()
}
}
}
</script>
<template>
<div>
<!--
文件导出分两种方法
第一种:直接一个 a 标签即可
<a href="https://code.jquery.com/jquery-3.6.2.min.js" download="">Download the compressed, production jQuery 3.6.2</a>
第二种:自己将数据处理后然后本地保存
-->
<el-button @click="exportClick">文件导出</el-button>
<el-table :data="tableData" style="width: 100%">
<el-table-column type="index" label="序号" />
<el-table-column label="图片">
<template #default="scope">
<div style="display: flex; align-items: center">
<el-image :src="scope.row.img1" />
</div>
</template>
</el-table-column>
<el-table-column prop="category" label="分类" />
<el-table-column prop="proname" label="名称" />
</el-table>
</div>
</template>
14富文本编辑器
将富文本编辑器的文件夹复制到 public 中
在 index.html 中添加对应导入
在组件中使用
<script>
export default {
data () {
return {
}
},
methods: {
submit () {
// 获取内容
console.log(tinymce.activeEditor.getContent());
}
},
mounted () {
// console.log(tinymce);
tinymce.init({
selector: 'textarea#default-editor',
height: 500,
plugins: 'advlist link image lists paste', // 使用插件
paste_data_images: true, // 支持图片粘贴
branding: false, // 取消右下角默认提示
auto_focus: true, // 自动获取焦点
})
}
}
</script>
<template>
<div>
<textarea id="default-editor"></textarea>
<el-button @click="submit"> 点击获取输入内容 </el-button>
</div>
</template>
15 百度地图
先注册为百度的开发者
申请秘钥
创建一个应用,获取到秘钥
在项目的入口 html 中导入百度地图的 api
在组件中直接使用即可
<script>
export default {
data () {
return {
map: null
}
},
methods: {
search () {
// console.log(event.target.value);
var options = {
onSearchComplete: function (results) {
// 判断状态是否正确
if (local.getStatus() == BMAP_STATUS_SUCCESS) {
var s = [];
for (var i = 0; i < results.getCurrentNumPois(); i++) {
s.push(results.getPoi(i).title + ", " + results.getPoi(i).address);
}
document.getElementById("r-result").innerHTML = s.join("<br/>");
}
}
};
var local = new BMapGL.LocalSearch(this.map, options);
local.search(event.target.value);
}
},
mounted () {
this.map = new BMapGL.Map("container");
var point = new BMapGL.Point(120.270922, 30.311587);
// 设置地图显示的中心位置和地图缩放比例
this.map.centerAndZoom(point, 15);
this.map.enableScrollWheelZoom(true); //开启鼠标滚轮缩放
// map.setHeading(64.5); //设置地图旋转角度
// map.setTilt(73); //设置地图的倾斜角度
// map.setMapType(BMAP_EARTH_MAP); // 设置地图类型为地球模式
// console.log(window);
var scaleCtrl = new BMapGL.ScaleControl({
anchor: BMAP_ANCHOR_TOP_RIGHT
}); // 添加比例尺控件
this.map.addControl(scaleCtrl);
var zoomCtrl = new BMapGL.ZoomControl(); // 添加缩放控件
this.map.addControl(zoomCtrl);
var cityCtrl = new BMapGL.CityListControl(); // 添加城市列表控件
this.map.addControl(cityCtrl);
// 更改为自定义的地图模式
// map.setMapStyleV2({
// styleId: '3786c052e48e145feb2d93e2f8f9f14f'
// });
var marker = new BMapGL.Marker(point); // 创建标注
this.map.addOverlay(marker); // 将标注添加到地图中
var polyline = new BMapGL.Polyline([
new BMapGL.Point(116.399, 39.910),
new BMapGL.Point(116.405, 39.920),
new BMapGL.Point(116.425, 39.900),
new BMapGL.Point(116.525, 40.000)
], { strokeColor: "blue", strokeWeight: 2, strokeOpacity: 0.5 });
this.map.addOverlay(polyline);
var geolocation = new BMapGL.Geolocation();
const _this = this
// geolocation.getCurrentPosition(function (r) {
// console.log(r);
// if (this.getStatus() == BMAP_STATUS_SUCCESS) {
// var mk = new BMapGL.Marker(r.point);
// _this.map.addOverlay(mk);
// _this.map.panTo(r.point);
// alert('您的位置:' + r.point.lng + ',' + r.point.lat);
// }
// else {
// alert('failed' + this.getStatus());
// }
// });
}
}
</script>
<template>
<h3>
地图搜索:<input type="text" @keyup.enter="search">
</h3>
<div id="container"></div>
<section id="r-result"></section>
</template>
<style lang="scss" scoped>
#container {
height: 450px;
}
</style>
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了