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. 在组件内需要改样式的元素的父元素加上这个类名
安装图标
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"/>
封装【选择图标】组件案例:
例如:
--components
--chooseIocn
--src/index.vue
--index.ts
--menu
--src/index.vue
--index.ts
--index.ts
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)
}
}
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')
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>
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
--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>
-- 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)
})
}
}
--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)
})
}
}
--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)
})
}
}