2-1 搭建vite项目并配置路由和element-plus
npm init vite@lastest x-components -- --template vue-ts
node: 16.6.2
@element-plus/icons: 0.0.11,
@fullcalendar/core: 5.10.1,
@fullcalendar/daygrid: 5.10.1,
@fullcalendar/interaction: 5.10.1,
@types/lodash: 4.14.176,
axios: 0.24.0,
element-plus: 1.1.0-beta.20,
lodash: 4.17.21,
vue: 3.2.16,
vue-router: 4.0.12,
wangeditor: 4.7.9
@types/mockjs: 1.0.4,
@vitejs/plugin-vue: 1.9.3,
@vitejs/plugin-vue-jsx: 1.2.0,
mockjs: 1.1.0,
sass: 1.43.2,
sass-loader: 12.2.0,
typescript: 4.4.3,
vite: 2.6.4,
vue-tsc: 0.3.0
2-2 全局注册图标
原始图标组件名为驼峰命名,通过正则替换将其转换为中划线的形式
// 把驼峰转换成横杠连接
export const toLine = (value: string) => {
return value.replace(/(A-Z)g/, '-$1').toLocaleLowerCase()
}
main.ts 全局注册图标组件
// el-icon-xxx
for (let i in Icons) {
// 注册全部组件
app.component(`el-icon-${toLine(i)}`, (Icons as any)[i])
}
2-3 伸缩菜单-完成伸缩菜单基本功能
布局组件 el-container
<template>
<el-container>
<el-aside width="auto">
<nav-side :collapse="isCollapse"></nav-side> // 菜单
</el-aside>
<el-container>
<el-header>
<nav-header v-model:collapse="isCollapse"></nav-header> // 头部
</el-header>
<el-main>
<router-view></router-view> // 路由
</el-main>
</el-container>
</el-container>
</template>
el-container的样式属性
element 导航菜单撑满全屏
html,
body,
#app,
.el-container,
.el-menu {
height: 100%;
}
css 反向选择器, :not()
用来匹配不符合一组选择器的元素。由于它的作用是防止特定的元素被选中,它也被称为反选伪类(negation pseudo-class)。
/* 选择所有不是段落(p)的元素 */
:not(p) {
color: blue;
}
2-4 伸缩菜单-抽离头部和侧边栏组件并完善伸缩菜单
子组件内修改父组件的传递的props值
// 父组件
<nav-side :collapse="isCollapse"></nav-side>
// 子组件
<script lang='ts' setup>
let props = defineProps<{
collapse: boolean
}>()
let emits = defineEmits(['update:collapse'])
let toggle = () => {
// 需要修改父组件的数据
emits('update:collapse', !props.collapse)
}
</script>
2-5图标选择器-巧用两次watch控制弹框的显示与隐藏
<template>
<el-button @click="handleClick" type="primary">
<slot></slot>
</el-button>
<div class="m-choose-icon-dialog-body-height">
<el-dialog :title="title" v-model="dialogVisible">
<div class="container">
<div
class="item"
v-for="(item, index) in Object.keys(ElIcons)"
:key="index"
@click="clickItem(item)"
>
<div class="text">
<component :is="`el-icon-${toLine(item)}`"></component>
</div>
<div class="icon">{{ item }}</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script lang='ts' setup>
import * as ElIcons from '@element-plus/icons'
import { watch, ref } from 'vue'
import { toLine } from '../../../utils'
import { useCopy } from '../../../hooks/useCopy'
let props = defineProps<{
// 弹出框的标题
title: string,
// 控制弹出框的显示与隐藏
visible: boolean
}>()
let emits = defineEmits(['update:visible'])
// 拷贝一份父组件传递过来的visible
let dialogVisible = ref<boolean>(props.visible)
// 点击按钮显示弹出框
let handleClick = () => {
// 需要修改父组件的数据
emits('update:visible', !props.visible)
}
// 点击图标
let clickItem = (item: string) => {
let text = `<el-icon-${toLine(item)} />`
// 复制文本
useCopy(text)
// 关闭弹框
dialogVisible.value = false
}
// 监听visible的变化 只能监听第一次的变化
watch(() => props.visible, val => {
dialogVisible.value = val
})
// 监听组件内部的dialogVisible变化
watch(() => dialogVisible.value, val => {
emits('update:visible', val)
})
</script>
<style lang='scss' scoped>
.container {
display: flex;
align-items: center;
flex-wrap: wrap;
}
.item {
width: 25%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-bottom: 20px;
cursor: pointer;
height: 70px;
}
.text {
font-size: 14px;
}
.icon {
flex: 1;
}
svg {
width: 2em;
height: 2em;
}
</style>
Button 插槽使用
<el-button @click="handleClick" type="primary">
<slot></slot>
</el-button>
watch Element-Plus dialog 双向绑定无效, 主要是因为el-dialog的dialogVisible有内部和外部两种状态,点击组件的关闭时只修改了内部状态的dialogVisible,需要再次传递到外部组件的dialogVisible
// 监听visible的变化 只能监听第一次的变化
watch(() => props.visible, val => {
dialogVisible.value = val
})
// 监听组件内部的dialogVisible变化
watch(() => dialogVisible.value, val => {
emits('update:visible', val)
})
vue调试方法 ,直接在页面上输出变量的值
深度拷贝的手动实现
function deepClone(obj){
let objClone = Array.isArray(obj)?[]:{};
if(obj && typeof obj==="object"){
for(key in obj){
if(obj.hasOwnProperty(key)){
//判断ojb子元素是否为对象,如果是,递归复制
if(obj[key]&&typeof obj[key] ==="object"){
objClone[key] = deepClone(obj[key]);
}else{
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
对于父组件传递过来的值, 直接 watch
组件的 props
对应的属性即可,props
本身是一个 reactive
,如果某个属性是个对象,它会被 Proxy
深度代理,想要防止相同的引用则需要深拷贝(如果确定只有一层浅拷贝即可)
2-6 图标选择器-巧用component动态组件显示所有的图标
通过Object.keys循环一个对象
<div v-for="(item, index) in Object.keys(ElIcons)"
:key="index"
@click="clickItem(item)"
</div>
动态加载组件
<div class="text">
<component :is="`el-icon-${toLine(item)}`"></component>
</div>
<div class="icon">{{ item }}</div>
</div>
2-7图标选择器-利用命名空间修改dialog样式
>>>
和/deep/
已经被弃用了
改成:deep()就没问题了,注意:这里没有空格,这里没有空格,这里没有空格。不然样式不生效
//警告例子
<style scoped>
/deep/ .main{
background: #df2929;
}
>>> .main{
background: #df2929;
}
</style>
//正确例子
<style scoped>
:deep(.main){
background: #df2929;
}
</style>
2-8 图标选择器-通过自定义hooks函数实现复制功能
useCopy,创建临时页面控件, 调用浏览器复制执行命令 document.execCommand('Copy')
import { ElMessage } from 'element-plus'
// 复制文本
export const useCopy = (text: string) => {
// 创建输入框
let input = document.createElement('input')
// 给输入框value赋值
input.value = text
// 追加到body里面去
document.body.appendChild(input)
// 选择输入框的操作
input.select()
// 执行复制操作
document.execCommand('Copy')
// 删除加入的输入框
document.body.removeChild(input)
// 提示用户
ElMessage.success('复制成功')
}
2-9 省市区选择组件-利用github获取到省市区数据
省市区编码树形文件文件
https://github.com/pfinal/city/blob/master/pca-code.json
2-10 省市区选择组件-巧用watch来达到三级联动效果
<template>
<div>
<el-select clearable placeholder="请选择省份" v-model="province">
<el-option v-for="item in areas" :key="item.code" :value="item.code" :label="item.name"></el-option>
</el-select>
<el-select
clearable
:disabled="!province"
style="margin: 0 10px;"
placeholder="请选择城市"
v-model="city"
>
<el-option v-for="item in selectCity" :key="item.code" :value="item.code" :label="item.name"></el-option>
</el-select>
<el-select clearable :disabled="!province || !city" placeholder="请选择区域" v-model="area">
<el-option v-for="item in selectArea" :key="item.code" :value="item.code" :label="item.name"></el-option>
</el-select>
</div>
</template>
<script lang='ts' setup>
import { ref, watch } from 'vue'
import allAreas from '../lib/pca-code.json'
export interface AreaItem {
name: string,
code: string,
children?: AreaItem[]
}
export interface Data {
name: string,
code: string
}
// 下拉框选择省份的值
let province = ref<string>('')
// 下拉框选择城市的值
let city = ref<string>('')
// 下拉框选择区域的值
let area = ref<string>('')
// 所有的省市区数据
let areas = ref(allAreas)
// 城市下拉框的所有的值
let selectCity = ref<AreaItem[]>([])
// 区域下拉框的所有的值
let selectArea = ref<AreaItem[]>([])
// 分发事件给父组件
let emits = defineEmits(['change'])
// 监听选择省份
watch(() => province.value, val => {
if (val) {
let cities = areas.value.find(item => item.code === province.value)!.children!
selectCity.value = cities
}
city.value = ''
area.value = ''
})
// 监听选择城市
watch(() => city.value, val => {
if (val) {
let area = selectCity.value.find(item => item.code === city.value)!.children!
selectArea.value = area
}
area.value = ''
})
// 监听选择区域
watch(() => area.value, val => {
if (val) {
let provinceData: Data = {
code: province.value,
name: province.value && allAreas.find(item => item.code === province.value)!.name
}
let cityData: Data = {
code: city.value,
name: city.value && selectCity.value.find(item => item.code === city.value)!.name
}
let areaData: Data = {
code: val,
name: val && selectArea.value.find(item => item.code === val)!.name
}
emits('change', {
province: provinceData,
city: cityData,
area: areaData
})
}
})
</script>
<style lang='scss' scoped>
</style>
watch的同时需要清空上次的选择,实现clearable的功能
2-11省市区选择组件-完善省市区联动组件并给父组件分发事件
使用emit 分发事件
// 定义分发事件给父组件的事件
let emits = defineEmits(['change'])
// 分发事件
emits('change', {
province: provinceData,
city: cityData,
area: areaData
})
使用 TypeScript 定义 ref 响应式数据类型
let selectCity = ref<AreaItem[]>([])
在TypeScript里面,有3 4个地方会出现问号操作符,他们分别是
三元运算符
// 当 isNumber(input) 为 True 是返回 ? : 之间的部分; isNumber(input) 为 False 时
// 返回 : ; 之间的部分
const a = isNumber(input) ? input : String(input);
参数
// 这里的 ?表示这个参数 field 是一个可选参数
function getUser(user: string, field?: string) {
}
成员
// 这里的?表示这个name属性有可能不存在
class A {
name?: string
}
interface B {
name?: string
}
安全链式调用
// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
return new Error().stack.split('\n');
// 我们可以添加?操作符,当stack属性存在时,调用 stack.split。若stack不存在,则返回空
return new Error().stack?.split('\n');
// 以上代码等同以下代码, 感谢 @dingyanhe 的监督
const err = new Error();
return err.stack && err.stack.split('\n');
什么是!(感叹号)操作符?
在TypeScript里面有3个地方会出现感叹号操作符,他们分别是
一元运算符
// ! 就是将之后的结果取反,比如:
// 当 isNumber(input) 为 True 时返回 False; isNumber(input) 为 False 时返回True
const a = !isNumber(input);
成员
// 因为接口B里面name被定义为可空的值,但是实际情况是不为空的,那么我们就可以
// 通过在class里面使用!,重新强调了name这个不为空值
class A implemented B {
name!: string
}
interface B {
name?: string
}
强制链式调用
// 这里 Error对象定义的stack是可选参数,如果这样写的话编译器会提示
// 出错 TS2532: Object is possibly 'undefined'.
new Error().stack.split('\n');
// 我们确信这个字段100%出现,那么就可以添加!,强调这个字段一定存在
new Error().stack!.split('\n');
2-12利用app.use特性全局注册组件
单个组件文件夹结构
menu tree -L 2
.
├── index.ts // 入口文件
└── src
├── index.vue // 主文件
├── menu.tsx //jsx文件
├── styles // 样式文件
└── types.ts // 类型文件
在组件文件夹的根目录下创建index.ts文件,暴露install方法,接受一个App类型的参数
import { App } from 'vue'
import chooseArea from './src/index.vue'
// 让这个组件可以通过use的形式使用
export default {
install(app: App) {
app.component('m-choose-area', chooseArea)
}
}
创建组件库的入口文件
import { App } from 'vue'
import chooseArea from './chooseArea'
import chooseIcon from './chooseIcon'
import trend from './trend'
import notification from './notification'
import list from './list'
import menu from './menu'
import chooseTime from './chooseTime'
import chooseDate from './chooseDate'
import progress from './progress'
import chooseCity from './chooseCity'
import form from './form'
import modalForm from './modalForm'
import table from './table'
import calendar from './calendar'
const components = [
chooseArea,
chooseIcon,
trend,
notification,
list,
menu,
chooseTime,
chooseDate,
chooseCity,
progress,
form,
modalForm,
table,
calendar
]
export default {
install(app: App) {
components.map(item => {
app.use(item)
})
}
}
在main.ts中use组件库
import mUI from './components'
app.use(router).use(ElementPlus).use(mUI)