目录
(一)header组件面包屑的实现
header组件显示面包屑路径
思路:将当前路径信息存储在currentItem中,根据点击aside菜单跳转到对应路由并更改currentitem信息
1.通过store存储面包屑数据和操作
将aside组件的菜单列表存储到store中
tabList用于存储所有currentItem的数据
在menu.ts中:
// 存储菜单列表
const menuListData = reactive([
{
path: "/",
name: "home",
label: "首页",
icon: "house",
url: "Home/Home",
},
{
path: "/mall",
name: "mall",
label: "商品管理",
icon: "video-play",
url: "MallManage/MallManage",
},
{
path: "/user",
name: "user",
label: "用户管理",
icon: "user",
url: "UserManage/UserManage",
},
{
label: "其他",
icon: "location",
children: [
{
path: "/page1",
name: "page1",
label: "Page1",
icon: "setting",
url: "Other/PageOne",
},
{
path: "/page2",
name: "page2",
label: "Page2",
icon: "setting",
url: "Other/PageTwo",
},
],
},
])
// tabslist 用于确定当前路径
const tabsList = [
{
path: "/",
name: "home",
label: "首页",
icon: "house",
url: "Home/Home",
},
{
path: "/mall",
name: "mall",
label: "商品管理",
icon: "video-play",
url: "MallManage/MallManage",
},
{
path: "/user",
name: "user",
label: "用户管理",
icon: "user",
url: "UserManage/UserManage",
},
{
path: "/page1",
name: "page1",
label: "Page1",
icon: "setting",
url: "Other/PageOne",
},
{
path: "/page2",
name: "page2",
label: "Page2",
icon: "setting",
url: "Other/PageTwo",
},
]
// 存储当前路径信息
let currentItem = ref({
path: "/",
name: "home",
label: "首页",
icon: "house",
url: "Home/Home",
})
// 更改当前路径信息
function changeCurrent(item: any) {
currentItem.value.label = item.label
currentItem.value.path = item.path
}
// 每次页面刷新时将面包屑当前路径保存上
function findCurrentItem() {
// as any是因为find()一定能找到对应的item
currentItem.value = breadList.find((item) => {
return item.path == useRoute().path
}) as any
}
2.点击aside组件菜单二级面包屑展示
在component/commonAside中:
// 跳转到对应的路由
function goRouter(item: any) {
menustore.changeCurrent(item)
router.push({
name: item.name,
})
}
3.展示面包屑
<!-- 面包屑 -->
<el-breadcrumb separator="/" class="bread-crumb">
<el-breadcrumb-item :to="{ path: '/' }"
@click="menuStore.changeCurrent({ path: '/', name: 'home', label: '首页', icon: 'house', url: 'Home/Home' })">首页</el-breadcrumb-item>
<el-breadcrumb-item v-if="menuStore.currentItem.path != '/'"
:to="menuStore.currentItem.path">
{{ menuStore.currentItem.label }}</el-breadcrumb-item>
</el-breadcrumb>
因为一级面包屑一定是首页,所以点击一级面包屑将‘/’路径的数据传给currentItem
4.刷新组件时的问题
当刷新网页后,路由还是保持不变,但currentItem中存储的当前路径会清空变为默认数据,因此需要在每次项目刷新后,判断当前路径,将tabList中符合当前路径的item对象传给currentItem
在stores/menu.ts中:
// 每次页面刷新时将面包屑当前路径保存上
function findCurrentItem() {
// as any是因为find()一定能找到对应的item
currentItem.value = breadList.find((item) => {
return item.path == useRoute().path
}) as any
}
在component/commonAside中:
// 每次页面刷新时将面包屑当前路径保存上,防止刷新后currentItem数据丢失
// 这个函数放哪里运行都行
menustore.findCurrentItem()
感觉这个解决方法不太简洁,专门新增了一个面包屑标签数组breadList,后面不同用户登录会导致菜单列表不同,到那时再看看面包屑这样处理会不会出问题
(二)tags标签页
实现效果如下:
二级面包屑和tag要实时对应,在写代码实现的时候要特别注意
1.静态组件编写
tags标签页单独用一个非路由组件编写
在stores/menu中:
// tagsList 标签列表 (默认首页标签一直存在)
let tagsList = reactive([
{
path: "/",
name: "home",
label: "首页",
icon: "house",
url: "Home/Home",
},
])
在component/commonTag中:
<script setup lang="ts">
import { useMenuStore } from '@/stores/Menu';
import { reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const menuStore = useMenuStore()
let tags = menuStore.tagsList
</script>
<template>
<div class="tags">
<el-tag v-for="(tag, index) in tags" :key="tag.name"
:closable="tag.name != 'home'" :disable-transitions="false"
:effect="route.name == tag.name ? 'dark' : 'plain'" >
{{ tag.label }}</el-tag>
</div>
</template>
2.点击菜单选项新增tag和点击tag跳转路由
点击aside组件的菜单选项实现新增tag
在stores/menu中:
// 点击菜单选项增加taglist 并更新当前currentItem
function addTag(item: any) {
// 当点击路径不是首页时更新currentItem
if (item.name == 'home') {
currentItem.value = {
path: "/",
name: "home",
label: "首页",
icon: "house",
url: "Home/Home",
}
} else {
currentItem.value = item
}
// 查找item是否在tagslist中存在
let index = tagsList.findIndex((tag) => {
return tag.name == item.name
})
// 如果item不在tagslist中就加进去,在的话就不做操作
index == -1 ? tagsList.push(item) : ''
}
点击tag跳转到对应路由
要注意跳转路由时也需要更改面包屑的当前路径数据currentItem
在component/commonTag中:
// 点击标签跳转到对应路径 并改变当前tag的数据
function changeCurrentTag(tag: any) {
// 跳转到对应路径
router.push(tag.path)
// 更改currentItem
menuStore.addTag(tag)
}
<el-tag v-for="(tag, index) in tags" :key="tag.name"
:closable="tag.name != 'home'" :disable-transitions="false"
:effect="route.name == tag.name ? 'dark' : 'plain'"
@click="changeCurrentTag(tag)">
{{ tag.label }}
</el-tag>
3.删除tag的路由跳转解决
删除tab操作 element-plus自带属性
在stores/menu中:
// 删除tagslist中的tag
function deleteTag(item: any) {
// 寻找要删除的item的索引
let index = tagsList.findIndex((tag) => {
return tag.name == item.name
})
// 删除
tagsList.splice(index, 1)
}
要注意:在删除tag是要判断删除的是否是当前路由的tag,如果是,就将路由跳转到前一个tag,同时要更改面包屑的当前路径数据
在component/commonTag中:
// 删除tag
function handleClose(tag: any, index: number) {
// 删除对应tag
menuStore.deleteTag(tag)
// 如果删除的是所在的路由 就将路由跳转到前一个tag索引
if (tag.name == route.name) {
router.push(tags[index - 1].path)
// 更改currentItem
menuStore.addTag(tags[index - 1])
}
}
<el-tag v-for="(tag, index) in tags" :key="tag.name"
:closable="tag.name != 'home'" :disable-transitions="false"
:effect="route.name == tag.name ? 'dark' : 'plain'"
@click="changeCurrentTag(tag)"
@close="handleClose(tag, index)">
{{ tag.label }}
</el-tag>
4.刷新后tagslist数据丢失问题解决
点击刷新后tagslist里只会有最开始首页的标签,如果处于其他的路由,就需要重新获取当前路径并将数据加入到tagsList中。
在stores/menu中:
// 每次页面刷新时将面包屑当前路径保存上
// 将当前路由对应的tag加到taglist中
function findCurrentItem() {
// as any是因为find()一定能找到对应的item
currentItem.value = tabsList.find((item) => {
return item.path == route.path
}) as any
// 将当前路由的tag重新保存在taglist中
if (currentItem.value.name != 'home') {
tagsList.push(currentItem.value)
}
}
(三)user用户管理页的完成
1.通过mock模拟数据数组
随机生成100个数据,发送查询请求携带参数关键字(搜索功能)、页数、每页限制数量(分页器功能)
get请求采用params传递参数,在config中通过url获取,需要对其进行处理才能使用数据
url为:/mock/user/getuserlist?keyword=&page=1&limit=13,无法与/mock/user/getuserlist匹配,因此要用到正则
在mock/userMockServer中:
// 引入mock.js
import Mock from "mockjs";
// 随机生成100条user数据
let userList = []
for (let i = 0; i < 100; i++) {
// 用于计算年龄
let d1 = new Date().getFullYear()
let d2 = Mock.Random.date()
let d3 = parseInt(d2.split('-')[0])
userList.push(Mock.mock({
id: Mock.Random.guid(),
name: Mock.Random.cname(),
addr: Mock.mock('@county(true)'),
// 'age|18-60': 1,
age: d1 - d3 + 1,
birth: d2,
sex: Mock.Random.integer(0, 1)
}))
}
// 将传入的params参数转换为对象形式
function paramToObj(url: string) {
// params通过路径../..?xx=xx&xx=xx的形式传入
let params = url.split("?")[1]
// 判断有没有params参数
if (!params) return {}
// 对传入的params的格式进行转换
// 对params解码 将url中的中文等解码出来
let res = '{"' +
decodeURIComponent(params)
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"') +
'"}'
return JSON.parse(res)
}
// 获取列表数据
// get请求使用params参数传递参数 在config.url中获取
Mock.mock(/mock\/user\/getuserlist/, 'get', (config) => {
// 解构赋值 关键字可为空、页数、每页数量限制
let { keyword, page = 1, limit = 13 } = paramToObj(config.url)
return {
code: 200,
data: {
userList
}
}
})
在src/api中:
// 获取用户列表数据 传入关键字(可选)、页数page、每页限制limit
// 通常使用params传递参数 参数会展示在url上
export const getUserList = (params: any) => mockRequests({
url: 'user/getuserlist',
method: 'get',
params
})
在view/user中:
import { onMounted } from 'vue'
import { getUserList } from '@/api';
onMounted(async () => {
let result = await getUserList({ keyword: 'csq', page: 1, limit: 20 })
// 性别是以0和1存储的,展示时是男女
userList.value.forEach((item: any) => {
item.sex = item.sex === 1 ? '女' : '男'
})
2.使用element-plus搭建结构
<div class="user">
<el-container>
<el-header>
<el-button class="add" type="primary" size="" @click="">增加+</el-button>
<div class="search">
<input type="text" placeholder="请输入关键字" v-model="keyword">
<el-button type="primary" icon="Search" @click="searchUser">搜索</el-button>
</div>
</el-header>
<el-main>
<el-table :data="userList" style="width: 100%;height: 622px;" stripe>
<el-table-column :prop="item.prop" :label="item.label" :width="item.width" v-for="item in tableLabel"
:key="item.prop" />
<el-table-column fixed="right" label="操作" width="200">
<template #default="scope">
<el-button size="small" @click="">修改</el-button>
<el-button size="small" type="danger" @click="">删除</el-button>
</template>
</el-table-column>
</el-table>
</el-main>
</el-container>
<el-footer>
<el-pagination background layout="prev, pager, next" :page-size="config.limit" :total="total"
v-model:current-page="currentPage" />
</el-footer>
</div>
3.实现分页展示功能
使用watch检测currentPage的改变(更清晰有效)
官方建议不使用change-current事件(如下)
// 查询用户数组的参数项
let config = reactive({
keyword: '',
page: 1,
limit: 14,
})
// 总条数
let total = ref(0)
// 当前页数
let currentPage = ref(1)
// 双向绑定实时监听分页器的当前页数
// 因为官方不建议使用函数current-change 可能随时会取消该函数
watch(currentPage, (newValue, oldValue) => {
config.page = newValue
// 重新展示数据
getUserData()
})
<el-pagination background layout="prev, pager, next"
:page-size="config.limit" :total="total"
v-model:current-page="currentPage" />
将新的页数数据传给config后,/getuserlist要接收并根据page重新发送数据
// 解构赋值 关键字可为空、页数、每页数量限制
let { keyword, page = 1, limit = 14 } = paramToObj(config.url)
// 分页展示
let list = userList.filter((item, index) => {
return (index >= (page - 1) * limit && index < page * limit)
})
return {
code: 200,
data: {
userList: list,
total: count
}
}
4.实现搜索框搜索功能
在输入框中输入关键字(允许搜索姓名、地址),点击搜索将keyword传给/getuserlist并重新发送数据
// 存储输入框的关键字
let keyword = ref('')
// 根据关键字搜索
function searchUser() {
config.keyword = keyword.value
getUserData()
config.keyword = ''
}
<div class="search">
<input type="text" placeholder="请输入关键字" v-model="keyword">
<el-button type="primary" icon="Search" @click="searchUser">搜索</el-button>
</div>
先进行搜索关键字,再对获得的数组进行页码操作
// 获取列表数据
// get请求使用params参数传递参数 在config.url中获取
Mock.mock(/mock\/user\/getuserlist/, 'get', (config) => {
// 解构赋值 关键字可为空、页数、每页数量限制
let { keyword, page = 1, limit = 14 } = paramToObj(config.url)
// 判断有无关键字并返回对应有关键字的数组
let pageList = userList.filter((item) => {
if (keyword && item.name.indexOf(keyword) === -1 && item.addr.indexOf(keyword) === -1) return false
return true
})
// 分页展示
let list = pageList.filter((item, index) => {
return index >= (page - 1) * limit && index < page * limit
})
return {
code: 200,
data: {
userList: list,
total: pageList.length
}
}
})
5.新增用户+模态框表单
点击增加按钮弹出对话框,输入新增用户信息,点击确定提交增加用户请求,再展示到列表中
(1)通过el-dialog和el-form搭建框架
// 新增用户的对话框 默认不显示
let centerDialogVisible = ref(false)
// 提交格式 用于提交新增用户或修改用户的信息
let ruleForm: any = reactive({
name: '',
age: '',
sex: '',
birth: '',
addr: ''
})
<el-dialog v-model="centerDialogVisible" title="新增用户" width="30%" align-center :close-on-click-modal="false"
:close-on-press-escape="false">
<span>
<el-form :model="ruleForm" label-width="120px" ref="ruleFormRef">
<el-form-item label="姓名" prop="name" :rules="[{ required: true, message: '性别是必填项' }]">
<el-input v-model="ruleForm.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="年龄" prop="age" :rules="[{ required: true, message: '年龄是必填项' },
{ type: 'number', message: '请输入数字' }]">
<el-input v-model.number="ruleForm.age" placeholder="请输入年龄" />
</el-form-item>
<el-form-item label="性别" prop="sex" :rules="[{ required: true, message: '性别是必填项' }]">
<el-select v-model="ruleForm.sex" placeholder="请输入性别">
<el-option label="男" value="男" />
<el-option label="女" value="女" />
</el-select>
</el-form-item>
<el-form-item label="出生日期" prop="birth" :rules="[{ required: true, message: '出生日期是必填项' }]">
<el-date-picker v-model="ruleForm.birth" type="date" placeholder="请输入出生日期" value-format="YYYY-MM-DD" />
</el-form-item>
<el-form-item label="地址" prop="addr" :rules="[{ required: true, message: '地址是必填项' }]">
<el-cascader v-model="ruleForm.addr" :options="pcaTextArr" />
</el-form-item>
</el-form>
</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelRuleForm">取消</el-button>
<el-button type="primary" @click="submitRuleForm">确定</el-button>
</span>
</template>
</el-dialog>
地址(省市区) 的选择采用一个中国地区选择的插件element-china-area-data
安装:
npm install element-china-area-data -S
使用省市区三级联动(纯文字)
// 引入省市区地区选择插件
import { pcaTextArr } from "element-china-area-data";
let ruleForm: any = reactive({
name: '',
age: '',
sex: '',
birth: '',
addr: ''
})
<el-form-item label="地址" prop="addr" :rules="[{ required: true, message: '地址是必填项' }]">
<el-cascader v-model="ruleForm.addr" :options="pcaTextArr" />
</el-form-item>
会以数组的形式存储在addr中
后续提交表单的时候再更改格式
(2)表单功能完善+表单格式验证
在对话框中点击取消或确定后,存储的表单数据和验证提示信息不会消失,需要通过el-form提供的方法resetField对该表单项进行重置,将其值重置为初始值并移除校验结果
// 将el-form绑定到ruleFormRef
const ruleFormRef = ref<FormInstance>()
// 清空表单输入框内数据
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
}
// 提交新增用户数据 发送请求
function submitRuleForm() {
...
// 隐藏模态框
centerDialogVisible.value = false
// 清空表单
resetForm(ruleFormRef.value)
}
// 取消提交用户表单操作
function cancelRuleForm() {
// 隐藏模态框
centerDialogVisible.value = false
// 清空表单
resetForm(ruleFormRef.value)
}
<el-form :model="ruleForm" label-width="120px" ref="ruleFormRef">
<el-form-item label="姓名" prop="name"
:rules="[{ required: true, message: '性别是必填项' }]">
<el-input v-model="ruleForm.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="年龄" prop="age"
:rules="[{ required: true, message: '年龄是必填项' },
{ type: 'number', message: '请输入数字' }]">
<el-input v-model.number="ruleForm.age" placeholder="请输入年龄" />
</el-form-item>
...
</el-form>
...
<template #footer>
<span class="dialog-footer">
<el-button @click="cancelRuleForm">取消</el-button>
<el-button type="primary" @click="submitRuleForm">确定</el-button>
</span>
</template>
表单格式的基本验证
通过el-form-item的rules属性设置,必须搭配prop对应名称才能生效
上面代码出现过了
(3)发送新增用户请求
配置对应的mock请求
在mock/userMockServer中:
// 请求添加数据
Mock.mock('/mock/user/adduserlist', 'post', config => {
let { name, age, sex, birth, addr } = JSON.parse(config.body)
userList.unshift({
id: Mock.Random.guid(),
name, age, sex, birth, addr
})
return {
code: 200,
data: {
message: 'ok了'
}
}
})
在api/index中:
// 请求添加用户数据
// 通常会使用data传递参数 参数存储在请求的body里
export const adduserlist = (data: any) => mockRequests({
url: '/user/adduserlist',
method: 'post',
data
})
点击确定按钮发送请求
validate函数:对整个表单的内容进行验证。 接收一个回调函数,或返回 Promise
如果添加新用户成功,就重新请求渲染数据
// 提交新增用户数据 发送请求
function submitRuleForm() {
if (!ruleFormRef.value) return
// 判断表单验证是否通过
ruleFormRef.value.validate(async (valid) => {
if (valid) {
// 转换格式
ruleForm.sex = ruleForm.sex == '女' ? 1 : 0
ruleForm.addr = ruleForm.addr.join(' ')
// 请求添加数据
let res = await adduserlist(ruleForm) as any
if (res.code == 200) { // 使用断言 否则报错没有code属性
// 刷新数据
getUserData()
}
// 隐藏模态框
centerDialogVisible.value = false
// 清空表单
resetForm(ruleFormRef.value)
} else {
ElMessage({
showClose: true,
message: '请输入正确的数据',
type: 'error',
})
}
})
}
6.修改用户数据
因为修改用户数据也需要用到ruleForm数据和表单对话框,因此可以和新增用户的功能公用,只需要加个标志区分即可
添加标志handleFlag实现新增和修改功能的区分
// 标志 是edit还是add
let handleFlag = ref('add')
// 点击增加按钮
function handleAdd() {
centerDialogVisible.value = true
handleFlag.value = 'add'
}
// 点击编辑按钮
function handleEdit(row: any) {
centerDialogVisible.value = true
handleFlag.value = 'edit'
console.log(row);
}
<el-dialog :title="handleFlag == 'add' ? '新增用户' : '编辑用户'">
点击编辑后需要获取对应行的数据,并将其展示在表单输入框中
获取行数据:#default="scope"
使用template中的作用域插槽,它的作用是在外部获取组件内的数据 ,这里是为了获取这一行的数据,我们让slot-scope值为scope,那么由scope.row就可以得到数据
// 点击编辑按钮
function handleEdit(row: any) {
centerDialogVisible.value = true
handleFlag.value = 'edit'
nextTick(() => {
// 将row的属性和值赋给ruleForm 浅拷贝
Object.assign(ruleForm, row)
// 将空号分割的字符串重新变为数组
ruleForm.addr = ruleForm.addr.split(' ')
console.log(ruleForm);
})
}
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)">修改</el-button>
<el-button size="small" type="danger" @click="">删除</el-button>
</template>
发送修改用户数据的请求
配置对应的mock请求
在mock/userMockServer中:
// 请求修改数据
Mock.mock('/mock/user/updateuserlist', 'post', config => {
let { id, name, age, sex, birth, addr } = JSON.parse(config.body)
// 寻找到对应的用户数据
userList.forEach(item => {
if (item.id === id) {
item.name = name
item.age = parseInt(age)
item.sex = sex
item.birth = birth
item.addr = addr
}
})
return {
code: 200,
data: {
message: 'ok了'
}
}
})
在api/index中:
// 请求修改用户数据
export const updateUserList = (data: any) => mockRequests({
url: '/user/updateuserlist',
method: 'post',
data
})
在view/user中:
// 提交用户数据 发送请求
function submitRuleForm() {
if (!ruleFormRef.value) return
// 判断表单验证是否通过
ruleFormRef.value.validate(async (valid) => {
if (valid) {
if (handleFlag.value == 'add') { // 新增
// 转换格式
ruleForm.sex = ruleForm.sex == '女' ? 1 : 0
ruleForm.addr = ruleForm.addr.join(' ')
// 请求添加数据
let res = await adduserlist(ruleForm) as any
if (res.code == 200) { // 使用断言 否则报错没有code属性
// 隐藏模态框
centerDialogVisible.value = false
// 清空表单
resetForm(ruleFormRef.value)
// 刷新数据
getUserData()
}
} else { // 编辑
// 转换格式
ruleForm.sex = ruleForm.sex == '女' ? 1 : 0
ruleForm.addr = ruleForm.addr.join(' ')
// 请求更新数据
let res = await updateUserList(ruleForm) as any
if (res.code == 200) {
// 隐藏模态框
centerDialogVisible.value = false
// 清空表单
resetForm(ruleFormRef.value)
// 刷新数据
getUserData()
}
}
} else {
ElMessage({
showClose: true,
message: '请输入正确的数据',
type: 'error',
})
}
})
}
7.删除用户数据
在mock/userMockServer中:
// 请求删除数据
Mock.mock('/mock/user/deleteuserlist', 'delete', config => {
// 要删除的用户的id
let deleteId = config.body
userList = userList.filter((item: any) => {
return item.id != deleteId
})
return {
code: 200,
data: {
message: '删了'
},
}
})
在api/index中:
// 请求删除用户数据 根据id删除
export const deleteUserList = (data: any) => mockRequests({
url: '/user/deleteuserlist',
method: 'delete',
data
})
import { getUserList, adduserlist, updateUserList, deleteUserList } from '@/api';
// 删除用户
function deleteUser(row: any) {
ElMessageBox.confirm('确定删除该用户吗?',)
.then(async () => {
await deleteUserList(row.id).then(res => {
// console.log(res);
getUserData()
})
ElMessage({
showClose: true,
type: 'success',
message: '删除成功',
})
})
.catch(() => {
ElMessage({
type: 'info',
message: 'Delete canceled',
})
})
}