目录
后台管理系统实现
1. 项目搭建
1.创建项目
2.启动项目
3.搭建第三方库element-plus
1.安装
2.完整引入
3.按需导入
2.登录页面
1.下载插件
2.配置路由
3.样式配置
4.设置背景图片
5.表单展示
6.使用ts对数据类型限制
7.封装axios和重置
1.安装
2.封装axios
8.登录的逻辑实现
3.首页
1.首页的头部编写
1.首页布局
2.头部布局
2.侧边栏的基本样式
3.侧边栏的动态路由
1.创建Goods.vue页面
2.写入Home.vue的子路由中
3.生成动态路由
4.创建子路由:用户列表User.vue
5.把title渲染到侧边栏
6.index改为动态颜色
7.开启路由模式
8.右边路由内容展示
4.商品列表子路由
1.商品列表的表单搜索页面
2.展示res中的数据
1.复制Form表单
2.规范数据
3.使用
4.外界就可以使用selectData的属性
3.商品列表的数据展示
4.分页处理
1.切割
2.改为显示5页
5.查询功能
1.删除复原
6.封装函数,进行优化
5.用户列表子路由
1.获取用户列表数据
2.定义用户列表的数据类型
3.编写用户列表内容
4.用户列表的查询实现
5.用户列表的编辑页面
1.编辑栏的样式
2.user.ts添加
3.编辑页面数据的获取
4.编辑功能的实现
6.角色列表
1.创建Role.vue,在router/index.ts添加Role子路由
2.角色列表的类型规范
3.写数据
4.页面的编写
5.添加角色功能
6.创建Authority.vue权限列表
7.点击修改权限跳转页面
8.Role.vue修改跳转
9.获取路由参数
11.获取数据
12.定义类型
13.接收数据
14.修改功能
7.解决侧边栏刷新后,无高亮
8.解决进入首页,空白
9.路由守卫
10.退出登录
cd vue3-ts-demo
npm run serve
NPM
npm install element-plus --save
安装 | Element Plus
如果你对打包后的文件大小不是很在乎,那么使用完整导入会更方便。
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//createApp(App).use(router).use(ElementPlus).mount('#app')
// 如果报错的话,设置类型为any
const app:any=createApp(App)
app.use(router).use(ElementPlus).mount('#app')
Unexpected any. Specify a different type @typescript-eslint/no-explicit-any 关闭报错即可
//package.json
"rules": {
"@typescript-eslint/no-explicit-any":["off"]
}
您需要使用额外的插件来导入要使用的组件。
自动导入推荐
1.首先你需要安装unplugin-vue-components
和 unplugin-auto-import
这两款插件
npm install -D unplugin-vue-components unplugin-auto-import
2.然后把下列代码插入到你的 Vite
或 Webpack
的配置文件中
Webpack--->可以在vue.config.js中添加webpack.config.js即可
const { defineConfig } = require('@vue/cli-service')
const AutoImport = require('unplugin-auto-import/webpack')
const Components = require('unplugin-vue-components/webpack')
const { ElementPlusResolver } = require('unplugin-vue-components/resolvers')
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack:{//配置的是webpack的内容
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
}
})
//main.ts
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
createApp(App).use(router).mount('#app')
修改配置了之后,要重启项目
角色列表消息框出现问题,使用完整引入方法
自动生成Vue3代码:Vue VSCode Snippets(下载失败:使用管理员运行vscode)
输入vb即可快速生成模板
router/index.ts
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue'),
},
App.vue
//Login.vue
后台管理系统
登录
重置
Form 表单 | Element Plus
Form 表单 | Element Plus
外边距重叠
样式下拉时,出现父样式随着子样式移动的情况,跟上面外边距一起了(外边距重叠)父元素受子元素影响。可以给使用父元素使用overflow:hidden;也可以给父元素设置padding一定距离。
type文件夹:规范整个项目里面所对应的数据的类型。
//src/type/login.ts
export interface LoginFormInt {
username: string
password: string
}
export class LoginData {
ruleForm: LoginFormInt = {
username:"",
password:""
}
}
//Login.vue
import {LoginData} from '../type/login'
export default defineComponent({
setup() {
const data = reactive(new LoginData());
const rules={//rules是不会变的,可以拿出来
username: [
{
required: true,
message: "请输入账号",
trigger: "blur",
},
{...
return { ...toRefs(data),rules };
}
})
//重置
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
data.ruleForm.username = "";
data.ruleForm.password = "";
};
// data.ruleForm.username=1//不能将类型“number”分配给类型“string”。
// data.ruleForm.username=""//才可以(ts的类型推断)
// 解构出来,就可以直接使用
return { ...toRefs(data),rules,resetForm };
npm install axios
request/index.ts:封装axios的文件夹
import axios from 'axios'
// 创建axios实例
const service = axios.create({
baseURL:
'https://www.fastmock.site/mock/bf1fcb3c2e2945669c2c8d0ecb8009b8/api',
timeout: 5000, //超时值
headers: {
//会返回一个token,需要把token保存到头部里面
'Content-Type': 'application/json;charset=utf-8',
},
})
// 请求拦截
service.interceptors.request.use((config) => {
// headers没有,需要给它一个空的对象
config.headers = config.headers || {}
// 如果没有token,不用在headers加入token(确保它登录了);有就加token
if (localStorage.getItem('token')) {
config.headers.token = localStorage.getItem('token') || ''
}
return config
})
// 响应拦截
service.interceptors.response.use((res)=>{
// 获取code,进行类型定义
const code:number=res.data.code
// 状态码code
if(code!=200){
// 信息错误
return Promise.reject(res.data)
}
return res.data
},(err)=>{//打印错误信息
console.log(err);
})
// 发送请求需要整个实例,把实例暴露出去
export default service
src/request/api.ts:针对发送的请求做处理。
//app.ts
import service from '.'
interface LoginData {
username: string
password: string
}
//登录接口
// 给data做类型规范
export function login(data: LoginData) {
// 返回service,通过axios实例发送请求
return service({
url: '/login',
method: 'post',
data,
})
}
发送请求
import { ref } from "vue";
import type { FormInstance } from 'element-plus'
import {login} from '../request/api'
import { useRouter } from "vue-router";
// 登录
const ruleFormRef = ref()
const router=useRouter()//-->Router
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
// 对表单内容进行验证
//valid布尔类型,为true表示验证成功,反之
formEl.validate((valid) => {
if (valid) {
// console.log("submit!");
login(data.ruleForm).then((res) => {
console.log(res);
// 将token进行保存
localStorage.setItem("token", res.data.token);
// 跳转页面,首页
// $router.push vue2
router.push("/");
});
} else {
console.log("error submit!");
return false;
}
});
// console.log(formEl);
};
return {ruleFormRef, submitForm};
Container 布局容器 | Element Plus
Layout 布局 | Element Plus
//Home.vue
后台管理系统
退出系统
Aside
Main
商品列表
.el-aside {
.el-menu {
// 拿到窗口到底部部分100vh-上面后台管理系统盒子高度80px
height: calc(100vh - 80px);
}
}
Menu 菜单 | Element Plus
根据侧边栏去渲染(展示)对应的内容,在右边区域。点击商品列表,展示商品列表的数据。
需要写Home.vue的子路由,因为只有当它是子路由的时候,才可以在当前页面里面去渲染另一个页面。
//src/router/index.ts
{
path: '/',
name: 'home',
component: HomeView,
children: [
{
path: 'goods',
name: 'goods',
// 懒加载
component: () => import('../views/GoodsView.vue'),
},
],
},
根据首页的子路由,去展示侧边栏的内容。
//src/router/index.ts
name: 'goods',
meta: {
isShow: true,
},//根据meta来设置,有哪些子路由需要展示
//Home.vue
import { useRouter } from "vue-router";
setup() {
const router = useRouter();
console.log(router.getRoutes()); //Array(5)
//拿取到页面所有的路由,发现childern里的值是不确定的,会随着添加而添加,所以需要直接拿到isShow
const list = router.getRoutes().filter((v) => v.meta.isShow); //filter不能在html模块用 vue3
console.log(list);//Array(2)
//渲染页面,需要用到list
return {list}
},
//src/router/index.ts
meta: {
isShow: true,
title:"商品列表"
},
meta: {
isShow: true,
title:"用户列表"
},
{{item.meta.title}}
index为文字点击颜色。
router:开启路由模式,通过el-menu-item 的index来进行跳转。点击哪一个路由,就会跳转到所对应的index路径去。
组件哪个地方需要展示出来,需要设置路由出口。
src/request/api.ts
// 商品列表接口
export function getGoodsList(){
return service({
url:"/getGoodsList",
method:"get"
})
}
执行整个函数getGoodsList,发送请求
import { getGoodsList } from "../request/api";
export default defineComponent({
setup() {
getGoodsList().then((res) => {
console.log(res);
});
//Goods.vue
查询
Form 表单 | Element Plus
//src/type/goods.ts
// 定义接口
export interface ListInt {
userId: number
id: number
title: string
introduce: string
}
interface selectDataInt {
title: string
introduce: string
page: number //页码
count: number //总条数
pagesize: number //默认一页显示几条
}
// 定义class类
export class InitData {
// selectData针对搜索表单去定义的
selectData: selectDataInt = {
title:"",
introduce:"",
page:1,
count:0,
pagesize:10//设置默认展示10条数据
}
list: ListInt[] = [] //展示的内容的数据,接收后台返回的数据
}
//GoodsView.vue
import { defineComponent, reactive, toRefs } from "vue";
//引入
import {InitData} from '../type/goods'
//实例化
const data =reactive(new InitData())
//返回data
return {...toRefs(data)};
//Goods.vue
getGoodsList().then((res) => {
// console.log(res);
// data.list接受后台返回过来的数据
data.list = res.data;
});
Table 表格 | Element Plus
getGoodsList().then((res) => {
data.selectData.count=res.data.length // 赋值总条数
});
Pagination 分页 | Element Plus
import { computed} from "vue";
// list会随着currentChange,sizeChange而更改
const dataList = reactive({
comList: computed(() => {
// 切割
// 1-- [1-10]
// 2-- [11-20]
// 3-- [21-30]
// (slice从0开始的(截取下标),page从1开始的)
// 第一条-1 * 展示的页数
return data.list.slice(
(data.selectData.page - 1) * data.selectData.pagesize, //page=1-->0,page=2-->10
data.selectData.page * data.selectData.pagesize
); //page=1-->10,page=2-->20
// 包头不包尾,0-9,10-19
}),
});
const currentChange = (page: number) => {
data.selectData.page = page;
};
const sizeChange = (pagesize: number) => {
data.selectData.pagesize = pagesize;
};
return { currentChange, sizeChange,dataList };
点击第n页的时候,触发currentChange事件,把当前的page传过来,传过来后把page赋值给 data.selectData.page,这个page就和要展示的数据有关系,(computed:当依赖值发生改变时,属性comList就会发生改变)
Pagination 分页 | Element Plus
//Goods.vue
//type/goods.ts
pagesize: 5, //设置默认展示10条数据
import { InitData,ListInt } from "../type/goods";
//查询
const onSubmit = () => {
// console.log(data.selectData.title);
// console.log(data.selectData.introduce);
let arr: ListInt[] = []; //定义数组,用来接收查询过后的要展示的数据
// 将查询的数据,赋值给它
if (data.selectData.title || data.selectData.introduce) {
//判断两个是否其中一个有值
if (data.selectData.title) {
// value:数组list里的每一个对象
arr = data.list.filter((value) => {//将过滤出来的数组赋值给arr
// 查找输入进来的关键字,不等于-1,说明有这个关键字
return value.title.indexOf(data.selectData.title)!==-1;
});
}
if (data.selectData.introduce) {
arr = data.list.filter((value) => {
return value.introduce.indexOf(data.selectData.introduce)!==-1;
});
}
}else{
// 输入为空,让arr等于原本数组即可
arr=data.list
}
data.selectData.count=arr.length//分页总数改变
// 过滤之后的数组
data.list=arr
};
return { onSubmit };
arr过滤之后的数组,赋值给list,data.list.slice发生改变,comList也就发生改变,:data="dataList.comList"就发生改变了。分页的总数也要发生改变。
删除搜索后,需要恢复原状。监听
import { watch } from "vue";
//监听多个值,以数组形式存在
// 监听输入框的两个属性
watch(
[() => data.selectData.title, () => data.selectData.introduce],
() => {
// 只有属性发生改变,进行判断
if (data.selectData.title == "" && data.selectData.introduce == "") {
// 为空,则重新赋值(1.重新获取数组);2.也可以重新定义数组,去接收它
getGoodsList().then((res) => {
data.list = res.data; //赋值数据
data.selectData.count = res.data.length; // 赋值总条数
});
}
}
);
原先为直接调用,优化为在生命周期去调用,获取数据。
onMounted(() => {
getGoods()
});
// 封装到一个函数里面
const getGoods = () => {
getGoodsList().then((res) => {
// console.log(res);
// data.list接受后台返回过来的数据
data.list = res.data; //赋值数据
data.selectData.count = res.data.length; // 赋值总条数
});
};
watch(
[() => data.selectData.title, () => data.selectData.introduce],
() => {
// 只有属性发生改变,进行判断
if (data.selectData.title == "" && data.selectData.introduce == "") {
// 为空,则重新赋值(1.重新获取数组);2.也可以重新定义数组,去接收它
getGoods()
}
}
);
//request/api.ts
// 用户列表接口
export function getUserList() {
return service({
url: '/getUserList',
method: 'get',
})
}
// 角色列表接口
export function getRoleList() {
return service({
url: '/getRoleList',
method: 'get',
})
}
import { defineComponent, onMounted } from "vue";
import { getUserList, getRoleList } from "../request/api";
export default defineComponent({
setup() {
onMounted(() => {
getUser();
getRole();
});
const getUser = () => {
getUserList().then((res) => {
console.log(res);
});
};
const getRole = () => {
getRoleList().then((res) => {
console.log(res);
});
};
return {};
},
});
用接口来规范对象。
//src/type/user.ts
//id: 1
//nickName: "小明"
//role: (2) [{…}, {…}]
//userName: "小明"
export interface ListInt {
id: number
nickName: string
role: RoleInt[]
userName: string
}
// 为role写的接口
interface RoleInt {
role: number
roleName: string
}
// 查询,通过role选择管理员
interface SelectDataInt {
role: number
nickName: string
}
interface RoleListInt {
authority: number[]
roleId: number
roleName: string
}
// 写类,得到一个对象
export class InitData {
selectData: SelectDataInt = {
nickName: '',
role: 0,
}
list: ListInt[] = [] //接收用户信息的列表
roleList: RoleListInt[] = [] // 接收角色信息的列表
}
//User.vue
import {InitData} from '../type/user'
const data=reactive(new InitData())
return {...toRefs(data)};
角色:选择器选择时管理员还是普通用户。
import { defineComponent, onMounted, reactive, toRefs } from "vue";
import { getUserList, getRoleList } from "../request/api";
setup() {
const data = reactive(new InitData());
onMounted(() => {
getUser();
getRole();
});
const getUser = () => {
getUserList().then((res) => {
console.log(res);
data.list=res.data
});
};
const getRole = () => {
getRoleList().then((res) => {
console.log(res);
data.roleList=res.data
});
};
},
查询
{{ item.roleName }}
了解scope
Remove
const deleteRow = (row: any) => {
console.log(row);
};
return { deleteRow };
有选择器
Select 选择器 | Element Plus
有插槽
Table 表格 | Element Plus
User查询跟Goods的查询相似,复制过来进行更改即可。
import { InitData,ListInt } from "../type/user";
import { defineComponent, onMounted, reactive, toRefs, watch } from "vue";
// 查询
const onSubmit = () => {
// console.log(data.selectData.role);
// console.log(data.selectData.nickName);
let arr: ListInt[] = []; //定义数组,用来接收查询过后的要展示的数据
// 将查询的数据,赋值给它
if (data.selectData.nickName || data.selectData.role) {
//判断两个是否其中一个有值
if (data.selectData.nickName) {
// value:数组list里的每一个对象
arr = data.list.filter((value) => {
//将过滤出来的数组赋值给arr
// 查找输入进来的关键字,不等于-1,说明有这个关键字
return value.nickName.indexOf(data.selectData.nickName) !== -1;
});
}
// 输入小明,又要有管理员,即两个都要成立才行,所以这里过滤的值,是上面已经过滤过的值
if (data.selectData.role) {
arr = (data.selectData.nickName ? arr : data.list).filter((value) => {
// 将过滤出来的数组赋值给arr
// 将role跟选择器中的role进行对比,如果相等就返回出去
return value.role.find(
(item) => item.role === data.selectData.role
);
});
}
} else {
// 输入为空,让arr等于原本数组即可
arr = data.list;
}
// 过滤之后的数组
data.list = arr;
};
// 监听输入框的两个属性
watch([() => data.selectData.nickName, () => data.selectData.role], () => {
// 只有属性发生改变,进行判断
if (data.selectData.nickName == "" || data.selectData.role == 0) {
getUser();
}
});
return { ...toRefs(data), deleteRow, onSubmit };
{{ item.roleName }}
编辑
Dialog 对话框 | Element Plus
// 接收名字和角色
interface ActiveInt {
id: number
nickName: string
// [1,2]
role: number[]
userName: string
}
export class InitData {
isShow = false
active: ActiveInt = {
//选中的对象
id: 0,
nickName: '',
role: [],
userName: '',
}
const changeUser = (row: ListInt) => {
console.log(row);
data.active = {
id: row.id,
nickName: row.nickName,
userName: row.userName,
role: row.role.map((value) => value.role),
};
data.isShow = true;
};
return { changeUser };
const changeUser = (row: ListInt) => {
role: row.role.map((value: any) => value.role || value.roleId),
};
data.isShow = true;
};
const updateUser = () => {
// console.log(data.active);
let obj: any = data.list.find((value) => value.id == data.active.id);
obj.nickName = data.active.nickName;
// data.active.role-->[1,2],选中管理员和用户
// roleList-->roleId-->1,2 既选中管理员又用户.这里有1,上面才会有1.
obj.role = data.roleList.filter(
(value) => data.active.role.indexOf(value.roleId) !== -1
);
console.log(obj.role); //改变之后的值拿到了
// 将选中的内容进行赋值
data.list.forEach((item, i) => {
if (item.id == obj.id) {
data.list[i] = obj;
}
});
data.isShow = false;
};
return { changeUser, updateUser };
实际开发时,点击更改是会调用数据库里面的值,进行更改的。这里是更改的渲染。
{
path: '/role',
name: 'role',
meta: {
isShow: true,
title: '角色列表',
},
component: () => import('../views/RoleView.vue'),
},
// 角色列表接口
export function getRoleList() {
return service({
url: '/getRoleList',
method: 'get',
})
}
import { getRoleList } from "@/request/api";
export default defineComponent({
setup() {
onMounted(() => {
getRoleList().then((res) => {
console.log(res);
});
});
return {};
},
});
role.ts
export interface ListInt{
authority:number[],
roleId:number,
roleName:string
}
export class InitData{
list:ListInt[]=[]
}
import { defineComponent, onMounted, reactive } from "vue";
import { getRoleList } from "@/request/api";
import { InitData } from "@/type/role";
export default defineComponent({
setup() {
const data=reactive(new InitData())
onMounted(() => {
getRoleList().then((res) => {
console.log(res);
data.list=res.data
});
});
return {...toRefs(data) };
},
});
添加角色
修改权限
import { ElMessage, ElMessageBox } from "element-plus";
export default defineComponent({
setup() {
const addRole = () => {
ElMessageBox.prompt("请输入角色名称", "添加", {
confirmButtonText: "确定",
cancelButtonText: "取消",
})
.then(({ value }) => {
//value表示你在输入框中填写的值
if (value) {
//判断输入框中有值,就将对应的值添加到列表中
// 数组的长度会发生改变,
data.list.push({
roleId: data.list.length + 1,
roleName: value,
authority: [],
});
}
ElMessage({
type: "success",
message: `${value}角色添加成功`,
});
})
.catch(() => {
ElMessage({
type: "info",
message: "取消操作",
});
});
};
return { ...toRefs(data), addRole };
},
});
弹框出错,改用全局引入element-plus,无问题
MessageBox 消息弹框 | Element Plus
是首页的子路由。
{
path: '/authority',
name: 'authority',
meta: {
isShow: false,
title: '权限列表',
},
component: () => import('../views/AuthorityView.vue'),
},
import { InitData, ListInt } from "../type/role";
import { useRouter } from "vue-router";
const router=useRouter()
const changeRole = (row: ListInt) => {
router.push({
path: "authority",
query: {
id: row.roleId,
// 把数组变成字符串,给它分隔开
authority: row.authority.join(','),
},
});
};
return { changeRole };
http://localhost:8080/authority?id=1&authority=1,2,4,5,6,7,8,9,11,13,14,15,16
const changeRole = (row: ListInt) => {
router.push({
name: "authority", //name跟path后面的值设的是一样的
params: {
id: row.roleId,
authority: row.authority,
},
});
};
// 当前活跃的路由,路径显示的是哪一个,通过这个函数去获取,就可以得到哪一个对象
import { useRoute } from "vue-router";
export default defineComponent({
setup() {
const route = useRoute();
console.log(route);
return {};
},
});
authority.ts
export class InitData{
id:number
authority:number[]
// 赋值,这两个是从params中来的,需要进行传参
constructor(id:number,authority:number[]){
this.id=id
this.authority=authority
}
}
import { defineComponent, reactive, toRefs } from "vue";
// 当前活跃的路由,路径显示的是哪一个,通过这个函数去获取,就可以得到哪一个对象
import { useRoute } from "vue-router";
import { InitData } from "../type/authority";
export default defineComponent({
setup() {
const route = useRoute();
console.log(route);
// 不知道传过来的属性是什么,所以使用any
const params: any = route.params;
const data = reactive(new InitData(params.id, params.authority));
return { ...toRefs(data) };
},
});
Tree 树形控件 | Element Plus
api.ts
// 权限列表接口
export function getAuthorityList() {
return service({
url: '/getAuthorityList',
method: 'get',
})
}
import { getAuthorityList } from "../request/api";
onMounted(() => {
getAuthorityList().then((res) => {
console.log(res);
});
});
export interface ListInt {
name: string
roleId: number
// 是可选的?
roleList?: ListInt[]
viewRole?: string
}
export class InitData {
list:ListInt[]=[]
}
onMounted(() => {
getAuthorityList().then((res) => {
console.log(res);
data.list=res.data
});
});
父级勾选上了,子级就会自动勾选上,这里不需要,即设置它们不互相关联
Tree 树形控件 | Element Plus
定义Ref,可以通过Ref去获取DOM的注册信息
//authority.ts
treeRef:any
//Authority.vue
确认修改
const changeAuthority = () => {
console.log(data.treeRef);
console.log(data.treeRef.getCheckedKeys());
};
return { changeAuthority };
获取选中的状态,可以拿到拿一下是选中的
// 获取升序
console.log(data.treeRef.getCheckedKeys().sort(function(a:number,b:number){return a-b}));
// 传给后台,后台去进行修改
import { useRouter,useRoute } from "vue-router";
setup() {
const route = useRoute();
return { active:route.path };
},
const routes: Array = [
{
path: '/',
name: 'home',
redirect:"goods",//设置重定向
}
]
如果没有登录,需要登录后才能进入详情页面。设置全局路由守卫
//src/router/index.ts
router.beforeEach((to, from, next) => {
// 给token定义类型,登录了的话为string,未登录则是null
// 从localStorge里进行获取
const token: string | null = localStorage.getItem('token')
// 如果没有token,跳转到登录页面去;去往的路径本来就是登录页面的话,不需要跳转,直接进入token页面。
// 如果去往的页面不是token页面,并且没有token;则让他跳转到token页面
if (!token && to.path !== '/login') {
next('/login')
}
// 如果有token,则让它next就可以了
else {
next()
}
})
将HomeView.vue退出换成按钮形式
退出登录
const delToken =()=>{
// 删除token
localStorage.removeItem('token')
// 跳转到登陆页面去
router.push('/login')
}
return { delToken };
.col-token {
height: 80px;
line-height: 80px;
}
https://gitee.com/gru77/vue-ts-demo.git