第2章 封装组件初级篇(上)

1.环境搭建,在 vite 脚手架基础上集成 typescript 和 element-plus

https://cn.vitejs.dev/guide/

以下是开发过程中过使用到的包和版本号:package.json

{
  "name": "m-components",
  "version": "0.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vue-tsc --noEmit && vite build",
    "serve": "vite preview"
  },
  "dependencies": {
    "@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"
  },
  "devDependencies": {
    "@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"
  }
}

1.搭建项目:

npm create vite@latest m-components-ui -- --template vue-ts

2.添加端口号:
vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
 + server: {
 +   port: 8080
 + }
})

3.安装路由

npm install vue-router@next element-plus --save

4.代码
router/index.ts

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    component: Home,
  }
]

const router = createRouter({
  routes,
  history: createWebHistory()
})

export default router

views/Home.vue

<template>
  <div class="container">
    <h1>vue3 + typescript + vite2 + element-plus二次封装组件</h1>
  </div>
</template>

<script lang='ts' setup>
</script>

<style lang='scss' scoped>
  .container {
    width: 100%;
    height: 100%;
    display: flex;
    align-items: center;
    justify-content: center;
    h1 {
      font-size: 40px;
      position: relative;
      top: -100px;
    }
  }
</style>

main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'


const app = createApp(App)

app.use(router).use(ElementPlus)
app.mount('#app')

App.vue

<template>
  <router-view></router-view>
</template>


<style lang='scss'>
@import "./styles/base";
@import "./styles/ui";
.notification-popper-class {
  padding: 0 !important;
}
</style>

src/styles/base.scss

* {
  margin: 0;
  padding: 0;
}

html,
body,
#app,
.el-container,
.el-menu {
  height: 100%;
}

src/styles/ui.scss

// 修改组件库内部的样式
// 1. 需要自定义一个类名空间
// 2. 先在浏览器里面调试样式
// 3. 把调试好的类名放在这个类名里面
// 4. 在App.vue里面引入这个文件
// 5. 在组件内需要改样式的元素的父元素加上这个类名

2.全局图标注册,编写转换函数

安装图标

npm install @element-plus/icons

utils/index.js

// 把驼峰转换成横杠连接
export const toLine = (value: string) => {
  return value.replace(/(A-Z)g/, '-$1').toLocaleLowerCase()
}

main.ts

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
+ import * as Icons from '@element-plus/icons'
+ import { toLine } from './utils'
// 全局注册图标 牺牲一点性能
// el-icon-xxx
+ for (let i in Icons) {
  // 注册全部组件
+  app.component(`el-icon-${toLine(i)}`, (Icons as any)[i])
+ }

const app = createApp(App)


app.use(router).use(ElementPlus)
app.mount('#app')

在项目中使用

<el-icon-edit style="width: 1em, height: 1em"/>

3.伸缩菜单

封装【选择图标】组件案例:

1.封装组件的文件夹:

例如:

--components
	--chooseIocn
		--src/index.vue
		--index.ts
	--menu
		--src/index.vue
		--index.ts
--index.ts

2.在components/index.ts中注册

3.页面中引用

4.实战

1.封装chooseIcon组件:

components/chooseIcon/src/index.vue

<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 = `${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>

components/chooseIcon/index.ts

import { App } from 'vue'
import chooseIcon from './src/index.vue'

// 让这个组件可以通过use的形式使用
export default {
  install(app: App) {
    app.component('m-choose-icon', chooseIcon)
  }
}

2.在components/index.ts中注册

components/index.ts

import { App } from 'vue'
import chooseIcon from './chooseIcon'

const components = [
  chooseIcon,
]

export default {
  install(app: App) {
    components.map(item => {
      app.use(item)
    })
  }
}

在main.ts中全局注册组件,为了在项目中使用

import { createApp } from 'vue'
import App from './App.vue'
import router from './router/index'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as Icons from '@element-plus/icons'
import { toLine } from './utils'

// 引入自己写的组件
+ import mUI from './components/index.ts'

const app = createApp(App)

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

app.use(router).use(ElementPlus)
+ app.use(mUI)
app.mount('#app')

3.views中使用

views/chooseIcon

<template>
  <div>
    <m-choose-icon title="选择图标" v-model:visible="visible">选择图标</m-choose-icon>
  </div>
</template>

<script lang='ts' setup>
import { ref } from 'vue'

let visible = ref<boolean>(false)
</script>

<style lang='scss' scoped>
</style>

4.修改router/index.ts

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '../views/Home.vue'
import Container from '../components/container/src/index.vue'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    component: Container,
    children: [
      {
        path: '/',
        component: Home
      },
      {
        path: '/chooseIcon',
        component: () => import('../views/chooseIcon/index.vue')
      },
      {
        path: '/chooseArea',
        component: () => import('../views/chooseArea/index.vue')
      },
      {
        path: '/trend',
        component: () => import('../views/trend/index.vue')
      },
      {
        path: '/notification',
        component: () => import('../views/notification/index.vue')
      },
      {
        path: '/menu',
        component: () => import('../views/menu/index.vue')
      },
      {
        path: '/chooseTime',
        component: () => import('../views/chooseTime/index.vue')
      },
      {
        path: '/progress',
        component: () => import('../views/progress/index.vue')
      },
      {
        path: '/chooseCity',
        component: () => import('../views/chooseCity/index.vue')
      },
      {
        path: '/form',
        component: () => import('../views/form/index.vue')
      },
      {
        path: '/modalForm',
        component: () => import('../views/modalForm/index.vue')
      },
      {
        path: '/table',
        component: () => import('../views/table/index.vue')
      },
      {
        path: '/calendar',
        component: () => import('../views/calendar/index.vue')
      }
    ]
  }
]

const router = createRouter({
  routes,
  history: createWebHistory()
})

export default router

container组件(container是在router中使用的,因此不需要在components/index.ts文件中注册)

创建以下文件夹:

--components/container
	--navHeader/index.vue
	--navSide/index.vue
	--index.vue

代码

navHeader/index.vue

<template>
  <div class="header">
    <span @click="toggle">
      <el-icon-expand v-if="collapse"></el-icon-expand>
      <el-icon-fold v-else></el-icon-fold>
    </span>
  </div>
</template>

<script lang='ts' setup>
let props = defineProps<{collapse: boolean}>()
let emits = defineEmits(['update:collapse'])
let toggle = () => {
  // 需要修改父组件的数据
  emits('update:collapse', !props.collapse)
}
</script>

<style lang='scss' scoped>
.header {
  height: 60px;
  padding: 0 20px;
  display: flex;
  align-items: center;
}
svg {
  width: 1em;
  height: 1em;
}
</style>

navSide/index.vue

<template>
  <m-menu :collapse="collapse" :data="data" router :defaultActive="$route.path"></m-menu>
</template>

<script lang='ts' setup>
let props = defineProps<{
  collapse: boolean
}>()

let data = [
  {
    icon: 'HomeFilled',
    name: '首页',
    index: '/'
  },
  {
    icon: 'Check',
    name: '图标选择器',
    index: '/chooseIcon'
  },
  {
    icon: 'Location',
    name: '省市区选择',
    index: '/chooseArea'
  },
  {
    icon: 'Sort',
    name: '趋势标记',
    index: '/trend'
  },
  {
    icon: 'Timer',
    name: '时间选择',
    index: '/chooseTime'
  },
  {
    icon: 'Bell',
    name: '通知菜单',
    index: '/notification'
  },
  {
    icon: 'Menu',
    name: '导航菜单',
    index: '/menu'
  },
  {
    icon: 'TurnOff',
    name: '城市选择',
    index: '/chooseCity'
  },
  {
    icon: 'DArrowRight',
    name: '进度条',
    index: '/progress'
  },
  {
    icon: 'ScaleToOriginal',
    name: '日历',
    index: '/calendar'
  },
  {
    icon: 'Setting',
    name: '表单',
    index: '/form'
  },
  {
    icon: 'Setting',
    name: '弹出框表单',
    index: '/modalForm'
  },
  {
    icon: 'ShoppingBag',
    name: '表格',
    index: '/table'
  }
]
</script>

<style lang='scss' scoped>
</style>

index.vue

<template>
  <el-container>
  	// 注意这个地方: width="auto"的设置
    <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>

<script lang='ts' setup>
import { ref } from 'vue'
import NavHeader from './navHeader/index.vue'
import NavSide from './navSide/index.vue'

let isCollapse = ref(false)

</script>

<style lang='scss' scoped>
.el-header {
  padding: 0;
  border-bottom: 1px solid #eee;
}
</style>

menu组件

创建以下文件夹:

-- components/menu
	--src/index.vue
	--src/menu.tsx
	--src/types.ts
	--src/styles/index.scss
	--index.ts

代码

components/menu/src/types.ts

export interface MenuItem {
  // 导航的图标
  icon?: string,
  // 处理之后的图标
  i?: any,
  // 导航的名字
  name: string
  // 导航的标识
  index: string,
  // 导航的子菜单
  children?: MenuItem[]
}

components/menu/src/menu.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>
      )
    }
  }
})

components/menu/src/index.vue

<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>

components/menu/src/styles/index.scss

.menu-icon-svg {
  svg {
    margin-right: 4px;
    width: 1em;
    height: 1em;
  }
}

component/menu/index.ts

import { App } from 'vue'
import menu from './src/index.vue'
import infiniteMenu from './src/menu'

// 让这个组件可以通过use的形式使用
export default {
  install(app: App) {
    app.component('m-menu', menu)
    app.component('m-infinite-menu', infiniteMenu)
  }
}

注意修改:components/index.ts

import { App } from 'vue'
import chooseIcon from './chooseIcon'
+ import menu from './menu'

const components = [
  chooseIcon,
+  menu
]

export default {
  install(app: App) {
    components.map(item => {
      app.use(item)
    })
  }
}

4.图标选择器

--components/chooseIcon
   --src/index.vue
   --index.ts

src/index.vue

<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 = `${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>

index.ts

import { App } from 'vue'
import chooseIcon from './src/index.vue'

// 让这个组件可以通过use的形式使用
export default {
  install(app: App) {
    app.component('m-choose-icon', chooseIcon)
  }
}

注意修改:components/index.ts

import { App } from 'vue'
import chooseIcon from './chooseIcon'
import menu from './menu'
import chooseIcon from './chooseIcon'

const components = [
  	chooseIcon,
	menu,
	chooseIcon
]

export default {
  install(app: App) {
    components.map(item => {
      app.use(item)
    })
  }
}

5. 省市区选择器

--components/chooseArea
	--lib/pca-code.json(文件内容过大,当前只给出数据格式)
	--src/index.vue
	--index.ts 

lib/pac.json数据格式

[
	{
		code: "11",
		name: "北京",
		children: [
			{
				code: "1101",
				name: "市辖区",
				children: [
					{
						code: "110101",
						name: "东城区",
					}	
				]
			}
		]
	}
]

src/index.vue

<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>

components/chooseArea/index.ts

import { App } from 'vue'
import chooseArea from './src/index.vue'

// 让这个组件可以通过use的形式使用
export default {
  install(app: App) {
    app.component('m-choose-area', chooseArea)
  }
}

注意在components/index.ts中注册组件

import chooseArea from './chooseArea'

const components = [
	chooseArea,
]

export default {
	install(app: App) {
		components.map(item => {
			app.use(item)
		})
	}
}

你可能感兴趣的:(javascript,前端,vue.js)