4-1 导航菜单-导航菜单的需求分析和数据设计
二级菜单源码
<template>
<el-menu
class="el-menu-vertical-demo"
:default-active="defaultActive"
:router="router"
v-bind="$attrs"
>
<template v-for="(item, i) in data" :key="i">
<el-menu-item v-if="!item[children] || !item[children].length" :index="item[index]">
<component v-if="item[icon]" :is="`el-icon-${toLine(item[icon])}`"></component>
<span>{{ item[name] }}</span>
</el-menu-item>
<el-sub-menu v-if="item[children] && item[children].length" :index="item[index]">
<template #title>
<component v-if="item[icon]" :is="`el-icon-${toLine(item[icon])}`"></component>
<span>{{ item[name] }}</span>
</template>
<el-menu-item v-for="(item1, index1) in item[children]" :key="index1" :index="item1.index">
<component v-if="item1[icon]" :is="`el-icon-${toLine(item1[icon])}`"></component>
<span>{{ item1[name] }}</span>
</el-menu-item>
</el-sub-menu>
</template>
</el-menu>
</template>
<script lang='ts' setup>
import { PropType } from 'vue'
import { toLine } from '../../../utils'
let props = defineProps({
// 导航菜单的数据
data: {
type: Array as PropType<any[]>,
required: true
},
// 默认选中的菜单
defaultActive: {
type: String,
default: ''
},
// 是否是路由模式
router: {
type: Boolean,
default: false
},
// 键名
// 菜单标题的键名
name: {
type: String,
default: 'name'
},
// 菜单标识的键名
index: {
type: String,
default: 'index'
},
// 菜单图标的键名
icon: {
type: String,
default: 'icon'
},
// 菜单子菜单的键名
children: {
type: String,
default: 'children'
},
})
</script>
<style lang='scss' scoped>
svg {
margin-right: 4px;
width: 1em;
height: 1em;
}
.el-menu-vertical-demo:not(.el-menu--collapse) {
width: 200px;
}
</style>
4-2 导航菜单-巧用template实现两级结构的菜单
子组件使用 $attrs
来收集并注入未在子组件props里定义的props属性,在组件封装时免于定义一长串的props
使用方法
子组件使用v-bind来接收$attrs
<el-menu
class="el-menu-vertical-demo"
:default-active="defaultActive"
:router="router"
v-bind="$attrs"
>
那么父组件上传递非props属性
<m-menu text-color="red"/>
子组件会接受text-color属性到自己组件上, 此时的子组件相当于
<el-menu
class="el-menu-vertical-demo"
:default-active="defaultActive"
:router="router"
text-color="red"
>
4-3导航菜单-使用tsx实现无限层级菜单
无级限菜单
import { defineComponent, PropType, useAttrs } from 'vue'
import { MenuItem } from './types'
import * as Icons from '@element-plus/icons'
import './styles/index.scss'
export default defineComponent({
props: {
// 导航菜单的数据
data: {
type: Array as PropType<MenuItem[]>,
required: true
},
// 默认选中的菜单
defaultActive: {
type: String,
default: ''
},
// 是否是路由模式
router: {
type: Boolean,
default: false
},
// 菜单标题的键名
name: {
type: String,
default: 'name'
},
// 菜单标识的键名
index: {
type: String,
default: 'index'
},
// 菜单图标的键名
icon: {
type: String,
default: 'icon'
},
// 菜单子菜单的键名
children: {
type: String,
default: 'children'
},
},
setup(props, ctx) {
// 封装一个渲染无限层级菜单的方法
// 函数会返回一段jsx的代码
let renderMenu = (data: any[]) => {
return data.map((item: any) => {
// 每个菜单的图标
item.i = (Icons as any)[item[props.icon!]]
// 处理sub-menu的插槽
let slots = {
title: () => {
return <>
<item.i />
<span>{item[props.name]}</span>
</>
}
}
// 递归渲染children
if (item[props.children!] && item[props.children!].length) {
return (
<el-sub-menu index={item[props.index]} v-slots={slots}>
{renderMenu(item[props.children!])}
</el-sub-menu>
)
}
// 正常渲染普通的菜单
return (
<el-menu-item index={item[props.index]}>
<item.i />
<span>{item[props.name]}</span>
</el-menu-item>
)
})
}
let attrs = useAttrs()
return () => {
return (
<el-menu
class="menu-icon-svg"
default-active={props.defaultActive}
router={props.router}
{...attrs}
>
{renderMenu(props.data)}
</el-menu>
)
}
}
})
- 在Vue3中使用jsx
npm i -D @vitejs/plugin-vue-jsx
- 在vite.config.js配置jsx插件
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue(), vueJsx()],
server: {
port: 8080
}
})
-
使用jsx 只能使用defineComponent语法,不能使用setup的语法糖
-
在jsx中使用attrs
let attrs = useAttrs()
<el-menu
class="menu-icon-svg"
default-active={props.defaultActive}
router={props.router}
{...attrs} // 展开属性
>
{renderMenu(props.data)}
</el-menu>
- 在jsx中使用slot
在jsx中无法直接使用template ,需要搭配v-slots和slots对象
先定义一个slots对象
// 处理el-sub-menu的插槽
let slots = {
title: () => {
return <>
<item.i />
<span>{item[props.name]}</span>
</>
}
}
使用v-slots的指令接受slot对象
if (item[props.children!] && item[props.children!].length) {
return (
<el-sub-menu index={item[props.index]} v-slots={slots}>
{renderMenu(item[props.children!])}
</el-sub-menu>
)
}
- TS的类型断言使用
item.i = (Icons as any)[item[props.icon!]]
4-4导航菜单-利用封装好的导航菜单组件改造项目整体结构
4-5导航菜单-完善细节并增加自定义键名功能
有时间接口的键名和实际定义的组件变量名不一定,可以使用ES6的动态key,在子组件上接受props来接受传入的动态键名
封装使用props的键名,这样就灵活处理掉了写死的键名
item[props.name]
4-6进度条-完成进度条动态加载效果
<template>
<el-progress :percentage="p" v-bind="$attrs"></el-progress>
</template>
<script lang='ts' setup>
import { onMounted, ref } from 'vue'
// 标识动画加载过程中改变的值
let num = ref<number>(0)
let props = defineProps({
// 进度条进度
percentage: {
type: Number,
required: true
},
// 是否有动画效果
isAnimate: {
type: Boolean,
default: false
},
// 动画时长(毫秒)
time: {
type: Number,
default: 3000
},
})
let p = ref(0)
onMounted(() => {
if (props.isAnimate) {
// 规定时间内加载完成
let t = Math.ceil(props.time / props.percentage)
let timer = setInterval(() => {
p.value += 1
if (p.value >= props.percentage) {
p.value = props.percentage
clearInterval(timer)
}
}, t)
}
})
</script>
<style lang='scss' scoped>
</style>
4-7时间选择组件-完成时间选择组件的全部功能
<template>
<div style="display: flex;">
<div style="margin-right: 20px;">
<el-time-select
v-model="startTime"
:placeholder="startPlaceholder"
:start="startTimeStart"
:step="startStep"
:end="startTimeEnd"
v-bind="$attrs.startOptions"
></el-time-select>
</div>
<div>
<el-time-select
v-model="endTime"
:min-time="startTime"
:placeholder="endPlaceholder"
:start="endTimeStart"
:step="endStep"
:end="endTimeEnd"
:disabled="endTimeDisabled"
v-bind="$attrs.endOptions"
></el-time-select>
</div>
</div>
</template>
<script lang='ts' setup>
import {ref, watch} from 'vue'
let props = defineProps({
// 开始时间的占位符
startPlaceholder: {
type: String,
default: '请选择开始时间'
},
// 结束时间的占位符
endPlaceholder: {
type: String,
default: '请选择结束时间'
},
// 开始时间的开始选择
startTimeStart: {
type: String,
default: "08:00"
},
// 开始时间的步进
startStep: {
type: String,
default: "00:30"
},
// 开始时间的结束选择
startTimeEnd: {
type: String,
default: "24:00"
},
// 结束时间的开始选择
endTimeStart: {
type: String,
default: "08:00"
},
// 结束时间的步进
endStep: {
type: String,
default: "00:30"
},
// 结束时间的结束选择
endTimeEnd: {
type: String,
default: "24:00"
},
})
let emits = defineEmits(['startChange', 'endChange'])
// 开始时间
let startTime = ref<string>('')
// 结束时间
let endTime = ref<string>('')
// 是否禁用结束时间
let endTimeDisabled = ref<boolean>(true)
// 监听开始时间的变化
watch(() => startTime.value, val => {
if (val === '') {
endTime.value = ''
endTimeDisabled.value = true
}
else {
endTimeDisabled.value = false
// 给父组件分发事件
emits('startChange', val)
}
})
// 监听结束时间的变化
watch(() => endTime.value, val => {
if (val !== '') {
emits('endChange', {
startTime: startTime.value,
endTime: val
})
}
})
</script>
<style lang='scss' scoped>
</style>
4-8时间选择组件-完成日期选择组件所有功能
<template>
<div style="display: flex;">
<div style="margin-right: 20px;">
<el-date-picker
v-model="startDate"
type="date"
:placeholder="startPlaceholder"
:disabledDate="startDisabledDate"
v-bind="$attrs.startOptions"
></el-date-picker>
</div>
<div>
<el-date-picker
v-model="endDate"
type="date"
:placeholder="endPlaceholder"
:disabled="endDateDisabled"
:disabledDate="endDisabledDate"
v-bind="$attrs.endOptions"
></el-date-picker>
</div>
</div>
</template>
<script lang='ts' setup>
import { ref, watch } from 'vue'
let props = defineProps({
// 开始日期的占位符
startPlaceholder: {
type: String,
default: '请选择开始日期'
},
// 结束日期的占位符
endPlaceholder: {
type: String,
default: '请选择结束日期'
},
// 是否禁用选择今天之前的日期
disableToday: {
type: Boolean,
default: true
}
})
let emits = defineEmits(['startChange', 'endChange'])
// 开始日期
let startDate = ref<Date | null>(null)
// 结束日期
let endDate = ref<Date | null>(null)
// 控制结束日期的禁用状态
let endDateDisabled = ref<boolean>(true)
// 禁用开始日期的函数
let startDisabledDate = (time: Date) => {
if (props.disableToday) return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}
// 禁用结束日期的函数
let endDisabledDate = (time: Date) => {
if (startDate.value) {
return time.getTime() < startDate.value.getTime() + 1000 * 60 * 60 * 24
}
}
// 监听开始的日期
watch(() => startDate.value, val => {
if (!val) {
endDateDisabled.value = true
endDate.value = null
} else {
emits('startChange', val)
endDateDisabled.value = false
}
})
// 监听结束的日期
watch(() => endDate.value, val => {
if (val) {
emits('endChange', {
startDate: startDate.value,
endDate: val
})
}
})
</script>
<style lang='scss' scoped>
</style>
Element-Plus是否禁用选择今天之前的日期
:disabledDate="startDisabledDate"
接受一个返回的时间对象,判断哪些时间是否禁用
let startDisabledDate = (time: Date) => {
if (props.disableToday) return time.getTime() < Date.now() - 1000 * 60 * 60 * 24
}
4-9时间选择组件-修复日期选择组件结束日期未清空问题
联动watch时 需要联动清空相关的数据
4-10城市选择组件-组合式使用组件完成基本布局
<template>
<el-popover v-model:visible="visible" placement="bottom-start" :width="430" trigger="click">
<template #reference>
<div class="result">
<div>{{ result }}</div>
<div>
<el-icon-arrowdown :class="{ 'rotate': visible }"></el-icon-arrowdown>
</div>
</div>
</template>
<div class="container">
<el-row>
<el-col :span="8">
<el-radio-group v-model="radioValue" size="small">
<el-radio-button label="按城市"></el-radio-button>
<el-radio-button label="按省份"></el-radio-button>
</el-radio-group>
</el-col>
<el-col :offset="1" :span="15">
<el-select
@change="changeSelect"
placeholder="请搜索城市"
size="small"
v-model="selectValue"
filterable
:filter-method="filterMethod"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select>
</el-col>
</el-row>
<template v-if="radioValue === '按城市'">
<div class="city">
<!-- <div v-for="(value, key) in cities">{{key}}</div> -->
<!-- 字母区域 -->
<div
class="city-item"
@click="clickChat(item)"
v-for="(item, index) in Object.keys(cities)"
>{{ item }}</div>
</div>
<el-scrollbar max-height="300px">
<template v-for="(value, key) in cities" :key="key">
<el-row style="margin-bottom: 10px;" :id="key">
<el-col :span="2">{{ key }}:</el-col>
<el-col :span="22" class="city-name">
<div
@click="clickItem(item)"
class="city-name-item"
v-for="(item, index) in value"
:key="item.id"
>
<div>{{ item.name }}</div>
</div>
</el-col>
</el-row>
</template>
</el-scrollbar>
</template>
<template v-else>
<div class="province">
<div
class="province-item"
v-for="(item, index) in Object.keys(provinces)"
:key="index"
@click="clickChat(item)"
>{{ item }}</div>
</div>
<el-scrollbar max-height="300px">
<template v-for="(item, index) in Object.values(provinces)" :key="index">
<template v-for="(item1, index1) in item" :key="index1">
<el-row style="margin-bottom: 10px;" :id="item1.id">
<el-col :span="3">{{ item1.name }}:</el-col>
<el-col :span="21" class="province-name">
<div
class="province-name-item"
v-for="(item2, index2) in item1.data"
:key="index2"
>
<div @click="clickProvince(item2)">{{ item2 }}</div>
</div>
</el-col>
</el-row>
</template>
</template>
</el-scrollbar>
</template>
</div>
</el-popover>
</template>
<script lang='ts' setup>
import { ref, onMounted } from 'vue'
import city from '../lib/city'
import { City } from './types'
import province from '../lib/province.json'
// 分发事件
let emits = defineEmits(['changeCity', "changeProvince"])
// 最终选择的结果
let result = ref<string>('请选择')
// 控制弹出层的显示
let visible = ref<boolean>(false)
// 单选框的值 按城市还是按省份选择
let radioValue = ref<string>('按城市')
// 下拉框的值 搜索下拉框
let selectValue = ref<string>('')
// 下拉框显示城市的数据
let options = ref<City[]>([])
// 所有的城市数据
let cities = ref(city.cities)
// 所有省份的数据
let provinces = ref(province)
// 所有的城市数据
let allCity = ref<City[]>([])
// 点击每个城市
let clickItem = (item: City) => {
// 给结果赋值
result.value = item.name
// 关闭弹出层
visible.value = false
emits('changeCity', item)
}
let clickProvince = (item: string) => {
// 给结果赋值
result.value = item
// 关闭弹出层
visible.value = false
emits('changeProvince', item)
}
// 点击字母区域
let clickChat = (item: string) => {
let el = document.getElementById(item)
if (el) el.scrollIntoView()
}
// 自定义搜索过滤
let filterMethod = (val: string) => {
let values = Object.values(cities.value).flat(2)
if (val === '') {
options.value = values
} else {
if (radioValue.value === '按城市') {
// 中文和拼音一起过滤
options.value = values.filter(item => {
return item.name.includes(val) || item.spell.includes(val)
})
} else {
// 中文过滤
options.value = values.filter(item => {
return item.name.includes(val)
})
}
}
}
// 下拉框选择
let changeSelect = (val: number) => {
let city = allCity.value.find(item => item.id === val)!
result.value = city.name
if (radioValue.value === '按城市') {
emits('changeCity', city)
} else {
emits('changeProvince', city.name)
}
}
onMounted(() => {
// 获取下拉框的城市数据
let values = Object.values(cities.value).flat(2)
allCity.value = values
options.value = values
})
</script>
<style lang='scss' scoped>
.result {
display: flex;
align-items: center;
width: fit-content;
cursor: pointer;
}
.rotate {
transform: rotate(180deg);
}
svg {
width: 1em;
height: 1em;
position: relative;
top: 2px;
margin-left: 4px;
transition: all 0.25s linear;
}
.container {
padding: 6px;
}
.city,
.province {
display: flex;
align-items: center;
flex-wrap: wrap;
margin-top: 10px;
margin-bottom: 10px;
&-item {
padding: 3px 6px;
margin-right: 8px;
border: 1px solid #eee;
margin-bottom: 8px;
cursor: pointer;
}
}
.city-name,
.province-name {
display: flex;
align-items: center;
flex-wrap: wrap;
.city-name-item,
.province-name-item {
margin-right: 6px;
margin-bottom: 6px;
cursor: pointer;
}
}
</style>
el-radio-group 使用按钮组件的切换组件
<el-radio-group v-model="radioValue" size="small">
<el-radio-button label="按城市"></el-radio-button>
<el-radio-button label="按省份"></el-radio-button>
</el-radio-group>
4-11城市选择组件-获取城市数据并显示所有城市
剪头动画的翻转的实现, 显示和隐藏时动态添加一个过渡类名rotate
.rotate {
transform: rotate(180deg);
}
svg {
width: 1em;
height: 1em;
position: relative;
top: 2px;
margin-left: 4px;
transition: all 0.25s linear;
}
El-scroll 滚动内容区组件,定义滚动区域的最大显示高度
<el-scrollbar max-height="300px"></el-scroll>
4-12城市选择组件-绑定事件并实现点击字母跳转到对应区域
Element.scrollIntoView() 方法让当前的元素滚动到浏览器窗口的可视区域
语法
element.scrollIntoView(); // 等同于element.scrollIntoView(true)
element.scrollIntoView(alignToTop); // Boolean型参数
element.scrollIntoView(scrollIntoViewOptions); // Object型参数
参数有两种写法
alignToTop
: 布尔值类型
true
元素的顶端将和其所在滚动区的可视区域的最顶端对齐, 等同于scrollIntoViewOptions: {block: "start", inline: "nearest"}
默认值
false
元素的底部将和其所在的滚动区域的可视区域的底部的对齐,等同于scrollIntoViewOptions: {block: "end", inline: "nearest"}
默认值
scrollIntoViewOptions
:对象类型(可选参数)
一个包含下列属性的对象:
behavior
可选
定义动画过渡效果, "auto"或 "smooth"
之一。默认为 "auto"
。
block
可选
定义垂直方向的对齐, "start", "center", "end"
, 或 "nearest"
之一。默认为 "start"
。
inline
可选
定义水平方向的对齐, "start", "center", "end"
, 或 "nearest"
之一。默认为 "nearest"
。
示例
var element = document.getElementById("box");
element.scrollIntoView();
element.scrollIntoView(false);
element.scrollIntoView({block: "end"});
element.scrollIntoView({behavior: "instant", block: "end", inline: "nearest"});
4-13城市选择组件-完善按省份选择城市
ES6 数组方法flat()
作用:将数组扁平化,对每一项的值进行循环处理,如果该项的值也是数组,则取出来(相当于去掉了这项数组的[]括号)
flat(n)将每一项的数组偏平化,n默认是1,表示扁平化深度.
let a = ['a',["a","b"],{"a":"aaaa"},
[
{"bb":"bbbb"},
{"c":[
{"c1":"ccc111111"}
]
},
[12,22,32]
]
];
let b = a.flat(); // [ 'a', 'a', 'b', {'a': 'aaaa'}, {'bb':'bbbb'},{'c':[...]},12,22,32]
b[4]["bb"] = "b2b2b2b2b2b2"; //影响了原数组a,以及新数组bb
b[5].c[0].c1 = "ccc12121211111"; //影响了原数组a,以及新数组bb
let bb = a.flat(2);
console.log(a, b, bb);
复制代码
下面用自己的代码实现flat
let a = ['a',["a","b"],{"a":"aaaa"},
[
{"bb":"bbbb"},
{"c":[
{"c1":"ccc111111"}
]
},
[12,22,32]
]
];
function myFlat(dept) {
let arr = this;
dept = typeof dept === "undefined" ? 1 : dept;
if (typeof dept !== "number") {
return;
}
let res = JSON.parse(JSON.stringify(arr));
let testNum = 0;
let reduceArr = function (_arr) {
console.log(++testNum);
return _arr.reduce(function(prevItem, curItem){
return prevItem.concat(curItem);
},[])
};
for (let i = 0; i < dept; i++) {
let hasArrayItem = res.some(function(item){
return Array.isArray(item);
});
if (hasArrayItem) {
res = reduceArr(res);
}
}
return res;
}
Array.prototype.myFlat = myFlat;
let c = a.myFlat();
c[4]["bb"] = "b2b2b2b2b2b2"; //并没有影响原数组a,以及新数组bb
let cc = a.myFlat(2);
console.log(a, c, cc);
4-14城市选择组件-使用filter-method实现搜索过滤
El-select 自动过滤
<el-select
@change="changeSelect"
placeholder="请搜索城市"
size="small"
v-model="selectValue"
filterable
:filter-method="filterMethod"
>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>