Deng
Deng
基于Vue3+Vite+TS二次封装element-plus业务组件 | odjBlog
    欢迎来到odjBlog的博客!

基于Vue3+Vite+TS二次封装element-plus业务组件

技术总结及问题解决 odjbin 11个月前 (01-12) 56次浏览 0个评论

代码仓库地址: https://gitee.com/lin-lin-DJ/second-elementplus

搭建 vite 项目并配置路由和 elementplus

  • npm init vite@latest second-elementplus -- --template vue-ts
  • npm i -S vue-router@next element-plus

全局注册图标

  • npm install @element-plus/icons
  • 在此想要把 vue3 图标的驼峰命名法 改造成 el-icon-xxx 使用图标组件, 所以我们先编写一个工具类
//把驼峰转换成横杠链接
export const toLine=(value:string )=>{
return value.replace(/(A-Z)g/,'-$1').toLocaleLowerCase()
}
  • main.ts
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as Icons from '@element-plus/icons'
import {toLine} from "./utils";

const app = createApp(App)

//全局注册图标 牺牲一点性能
for (let i in Icons){
    //注册全部组件
    app.component(`el-icon-${toLine(i)}`, (Icons as any)[i])
}

app.use(ElementPlus)
app.mount('#app')
  • 即可在 vue 组件里面使用 el-icon-xxx 使用图标
 <el-icon-edit/>

伸缩菜单功能

  • 利用 elementplus 中 el-menu 的 collapse 属性和 v-if 判断收缩/展开图标, 实现伸缩菜单功能

巧用两次 watch 控制弹框的显示与隐藏

  • 存在问题: 点两次按钮才出现弹窗?
<template>
  <el-button @click="handleClick" type="primary">
    <slot></slot>
  </el-button>
  <el-dialog :modelValue="dialogVisible" :title="title">111</el-dialog>
</template>

<script lang="ts" setup>
import {ref, watch} from "vue";

const props = defineProps<{
  //弹出框的标题
  title: string,
  //控制弹出框的显示与隐藏
  visible: boolean,
}>()
let emits = defineEmits(['update:visible'])
//拷贝一份父组件传递过来的 visible 值
let dialogVisible = ref<boolean>(props.visible)
const handleClick = () => {
  emits('update:visible', !props.visible)
}
// 监听父组件传递过来的值, 只能监听第一次的变化
watch(() => props.visible, (newValue) => {
  dialogVisible.value=newValue
})
//监听组件内部的 dialogVisible 变化
watch(() => dialogVisible.value, (newValue) => {
  emits('update:visible', newValue)
})
</script>
  • 父组件调用
<template>
<choose-icon title="选择图标" v-model:visible="visible">选择图标
</choose-icon>
</template>

<script lang="ts" setup>
import ChooseIcon from '../../components/chooseIcon/src/index.vue'
import {ref} from "vue";
const visible = ref<boolean>(false)
</script>

巧用 component 动态组件显示所有的图标

<el-dialog :modelValue="dialogVisible" :title="title">
    <div class="container">
      <div class="item" v-for="(item,index) in Object.keys(ElIcons)"  :key="index">
        <div>
          <component :is="`el-icon-${toLine(item)}`"></component>
        </div>
        <div>{{item}}</div>
      </div>
    </div>
  </el-dialog>
<style scoped lang="less">
.container{
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  .item{
    width: 20%;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    margin-bottom: 15px;
    height: 70px;
    svg{
      width: 2em;
      height: 2em;
    }
  }
}
</style>

通过自定义 hooks 函数实现复制功能

  • hooks/useCopy/index.ts
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('复制成功')
}
//点击图标
const clickItem = (item:string) => {
  let text=`<el-icon-${toLine(item)}/>`
  useCopy(text)
  dialogVisible.value=false
}

省市区选择组件

  • 演示效果

  • 组件代码
<template>
  <div>
    <el-select placeholder="请选择省份" v-model="province" clearable>
      <el-option v-for="item in areas" :key="item.code" :value="item.code" :label="item.name"></el-option>
    </el-select>
    <el-select :disabled="!province" style="margin: 0 10px" placeholder="请选择城市" v-model="city" clearable>
      <el-option v-for="item in selectCity" :key="item.code" :value="item.code" :label="item.name"></el-option>
    </el-select>
    <el-select :disabled="!province||!city" placeholder="请选择区县" v-model="area" clearable>
      <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 {watch, ref} 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,
}

const province = ref<string>('')//省份
const city = ref<string>('')//城市
const area = ref<string>('')//区域
const areas = ref(allAreas)//所有省市区数据
//todo 用 computed 会报错, 选择 省市区之后, 再选择省份会报错 Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'children')
//城市下拉框的所有的值
const selectCity = ref<AreaItem[]>([])
//区域下拉框的所有的值
const selectArea = ref<AreaItem[]>([])
// const selectCity = computed(() => {
//   if (!province.value) {
//     return []
//   } else {
//     return areas.value.find(item => item.code === province.value)!.children
//   }
// })

//分发事件给父组件
const emits = defineEmits(['change'])

//监听选择省份
watch(() => province.value, (newValue) => {
  if (newValue) {
    //加 ! 是指一定是有值的
    selectCity.value = areas.value.find(item => item.code === newValue)!.children!
  }
  city.value=''
  area.value=''
})
//监听选择城市
watch(() => city.value, (newValue) => {
  if (newValue) {
    selectArea.value = selectCity.value.find(item => item.code === newValue)!.children!
  }
  area.value=''
})
//监听选择区域
watch(() => area.value, (newValue) => {
 if(newValue){
   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:newValue,
     name:newValue&&selectArea.value.find(item=>item.code=== newValue)!.name
   }
   emits('change',{
     province:provinceData,
     city:cityData,
     area:areaData
   })
 }
})
</script>

<style scoped>
.el-select {
  width: 200px;
}
</style>
  • 父组件调用
<template>
<choose-area @change="changeArea"/>
</template>

<script setup lang="ts">
import ChooseArea from '../../components/chooseArea/src/index.vue'
const changeArea=(val)=>{
  console.log(val)
}
</script>

利用 app.use 特性全局注册组件

    1. 在 components/chooseArea 组件里面创建 index.ts
import {App} from 'vue'
import chooseArea from './src/index.vue'

//让这个组件可以通过 use 的形式使用
export default {
  install(app: App) {
    app.component('choose-area', chooseArea)
  }
}
  • 2.在 components 里面创建 index.ts
import {App} from 'vue'
import chooseArea from './chooseArea'
import chooseIcon from './chooseIcon'

const components=[
    chooseIcon,
    chooseArea
]
export default {
    install(app: App) {
        components.map(item=>{
            app.use(item)
        })
    }
}
  • 3.在 main.ts 中全局引入组件
import App from './App.vue'
import mUI from './components'//全局
//import chooseArea from './components/chooseArea'//按需引入

const app = createApp(App)
app.use(mUI)
//.use(chooseArea)
  • 4.在 views 页面中就不用引入组件了

通知菜单

  • 想实现的效果

  • 通知图标封装
<template>
  <el-badge :value="value" :max="max" :is-dot="isDot">
    <component :is="`el-icon-${toLine(icon)}`"></component>
  </el-badge>
</template>

<script lang="ts" setup>
import {toLine} from "../../../utils";

const props = defineProps({
  //显示的图标
  icon: {
    type: String,
    default: 'Bell'
  },
  //通知数量
  value: {
    type: [String, Number],
    default: ''
  },
  //最大值
  max: {
    type: Number
  },
  //是否显示小圆点
  isDot: {
    type: Boolean,
    default: false
  }
})
</script>

使用 tsx 实现无限层级菜单

  • npm i -D @vitejs/plugin-vue-jsx

  • vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
  plugins: [vue(),vueJsx()],
  server:{
    port:8080
  }
})
  • menu.tsx
import {defineComponent, PropType, useAttrs} from "vue";
import {MenuItem} from "./types";
import * as Icons from '@element-plus/icons'
import './styles/index.css'
export default defineComponent({
    props: {
        //导航菜单的数据
        data: {
            type: Array as PropType<MenuItem[]>,
            required: true
        },
        //默认选中的菜单
        defaultActive: {
            type: String,
            default: ''
        },
        //是否是路由模式
        router: {
            type: Boolean,
            default: false
        }
    },
    setup(props, ctx) {
        //封装一个渲染无限层级菜单的方法
        //函数会返回一段 jsx 的代码
        let renderMenu = (data: MenuItem[]) => {
            return data.map((item: MenuItem) => {
                //每个菜单的图标
                item.i = (Icons as any)[item.icon!]
                //处理 sub-menu 的插槽
                let slots = {
                    title: () => {
                        return <>
                            <item.i/>
                            <span>{item.name}</span>
                        </>
                    }
                }
                //递归渲染 children
                if (item.children && item.children.length) {
                    return (
                        <el-sub-menu index={item.index} v-slots={slots}>
                            {renderMenu(item.children)}
                        </el-sub-menu>
                    )
                }
                //正常渲染普通的菜单
                return (
                    <el-menu-item index={item.index}>
                        <item.i/>
                        <span>{item.name}</span>
                    </el-menu-item>
                )
            })
        }
        let attrs= useAttrs()
        return ()=> {
           return (
                <el-menu default-active={props.defaultActive} router={props.router} {...attrs}>
                    {renderMenu(props.data)}
                </el-menu>
            )
        }
    }
})

进度条组件

  • 完成进度条动态加载效果
  • progress/src/index.vue
<template>
  <div>
    <el-progress v-bind="$attrs" :percentage="p"></el-progress>
  </div>
</template>

<script lang="ts" setup>

import {onMounted, ref} from "vue";

const props = defineProps({
  //进度条进度
  percentage: {
    type: Number,
    default: 60
  },
  //进度条是否有动画效果
  isAnimation: {
    type: Boolean,
    default: false
  },
  //动画时长(毫秒)
  time: {
    type: Number,
    default: 3000
  },
})
let p = ref(0)
onMounted(() => {
  if (props.isAnimation) {

    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)
  } else {
    p.value = props.percentage
  }
})
</script>

城市选择

  • 点击字母跳转到对应位置
  • 绑定 id, 使用 dom 的原生方法
  • 扩展: 小程序里面富文本如何跳转到指定文字的区域
let el= document.getElementById(item)
if(el) el.scrollIntoView()

表单组件

功能

1.可配置型表单,通过 json 对象的方式自动生成表单
2.具备更完善的功能,表单验证,动态删减表单,集成第三方的插件,
3.用法简单,扩展性强,可维护性强
4.能够用在更多的场景,比如弹框嵌套表单

准备工作

1.分析element-plus表单能够在哪些方面做优化
2.完善我们封装表单的类型,支持 ts
3.封装的表单要具备element-plus原表单的所有功能
4.集成第三方的插件: markdown 编辑器, 富文本编辑器...

注意

  • npm i -S lodash @types/lodash
  • 在 Vue 组件中处理引用类型(如对象和数组)时,使用深拷贝可以避免意外的副作用。深拷贝会创建一个新的对象或数组,并且不会与原始数据共享引用,这样可以确保数据的独立性。

表格组件

日历组件 fullcalendar

喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
已稳定运行:3年255天3小时50分