前言
感觉从进入前端后,做的都是ToB的产品,要不然就是大屏,每次在写那些重复性的表格表单的时候就觉得时间过得好快,感觉一天啥也没做就结束了,但是专注在写重复性代码里好开心啊,这样就不用动脑了!但是今年,就特别想动脑子了,先是用vue3 + antD
搞了个给家里用的进销存管理系统前端部分,然后现在又开始琢磨弄一个常用的组件库。所以接下来就开始讲讲我整个搭建的过程吧!
创建组件库项目
基于vue3 + vite + Element Plus 技术框架,适用于PC端。
1. 安装vite脚手架
npm create vite@latest
在选择 variant 的时候,可以选择customize with create-vue
,相当于自定义安装,可以选择安装ESLint和Prettier
,增加代码的规范,我是为了快就直接选择了JavaScript。
2. 改造项目结构
为了区分实际使用的包文件和测试文件
在根目录下新建packages目录,将原有的src目录修改为examples
由于项目的入口文件是src下的main.ts文件,所以需要将index.html
文件里的地址修改为:
<script type="module" src="/examples/main.ts"></script>
3. 安装UI框架和npm依赖包
npm install element-plus --save
npm i
在main.ts里引入Element Plus
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import request from "@/api";
app.use(ElementPlus, {});
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
4. 编写组件
- 在packages目录下新建table目录、index.js文件
- 在table目录下新建table.vue和index.js文件
<template>
<div class="container">
<div class="table-control">
<div v-for="item in controlList" :key="item.code" class="control-item">
<el-button
v-if="btIncludes.includes(item.btType)"
:type="item.type"
:link="item.btType === 'text'"
:plain="item.btType === 'secondary'"
:text="item.btType === 'threeLevel'"
:bg="item.btType === 'threeLevel'"
@click.stop="handleTableControl(item.label)"
>{{ item.label }}</el-button>
<el-button v-if="item.btType === 'iconTextBt'" :type="item.type"
@click.stop="handleTableControl(item.label)">
<img :src="getAssetsFile(`${item.icon}.png`)" class="icon-size" />{{ item.label }}
</el-button>
<el-tooltip
v-if="item.btType === 'iconBt'"
class="box-item"
:effect="item.effect"
:content="item.label"
:placement="item.placement">
<el-button link @click.stop="handleTableControl(item.label)">
<img :src="getAssetsFile(`${item.icon}.png`)" class="icon-size" />
</el-button>
</el-tooltip>
<el-tooltip
v-if="item.btType === 'listSet'"
class="box-item"
:effect="item.effect"
content="列设置"
:placement="item.placement"
>
<div class="checkbox-box" @click.stop="openPopover">
<el-popover v-model:visible="listSetting.visible" placement="right" :width="160" trigger="click">
<template #reference>
<el-button link>
<img :src="getAssetsFile(`${item.icon}.png`)" class="icon-size" />
</el-button>
</template>
<el-checkbox
v-model="listSetting.checkAll"
:indeterminate="listSetting.isIndeterminate"
@change="handleCheckAllChange"
>列展示</el-checkbox
>
<el-divider />
<el-checkbox-group v-model="listSetting.checkedColum" @change="handleCheckedChange">
<el-checkbox
v-for="colum in listSetting.columList"
:key="colum.code"
:label="colum.label"
:disabled="colum.isChecked"
>
{{ colum.label }}
</el-checkbox>
</el-checkbox-group>
</el-popover>
</div>
</el-tooltip>
</div>
</div>
<div class="table-wrap">
<el-table
ref="multipleTableRef"
:data="tableParams.datas"
@selection-change="handleSelectionChange"
stripe style="width: 100%"
>
<el-table-column v-if="isMultiple" type="selection" fixed width="55" />
<el-table-column type="index" width="70" label="序号" fixed :index="indexMethod" />
<template v-for="(item, index) in tableParams.headers" :key="index">
<el-table-column
v-if="item.show && !item.filters"
:prop="item.value"
:label="item.label"
:width="item.width"
:show-overflow-tooltip="item.showOverflow"
:fixed="item.showFixed"
:sortable="item.sortable"
>
<template #default="scope">
<span v-if="item.value === 'control'" v-for="(val, num) in tableParams.operations" :key="num">
<el-button link type="primary" size="small" @click="handleClick(val.type, scope.row)">{{val.label}}</el-button>
</span>
<span v-else-if="item.showImage">
<el-image :src="scope.row[`${item.value}`]" fit="scale-down" />
</span>
<span v-else>{{ scope.row[`${item.value}`] }}</span>
</template>
</el-table-column>
<el-table-column
v-if="item.filters"
:prop="item.value"
:label="item.label"
:width="item.width"
:show-overflow-tooltip="item.showOverflow"
:fixed="item.showFixed"
:filters="item.filters"
:filter-method="filterHandler">
<template #default="scope">
<span>{{ scope.row[`${item.value}`] }}</span>
</template>
</el-table-column>
</template>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="tablePagination.currentPage"
v-model:page-size="tablePagination.pageSize"
:small="true"
:background="true"
layout="total, prev, pager, next, jumper"
:total="tablePagination.total"
@current-change="handleCurrentChange"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts" name="DynamicTable">
import {onMounted, reactive, ref, watch} from "vue";
import { TableColumnCtx } from 'element-plus'
const props = defineProps({
tableItems: {
type: Object,
require: true,
default: () => {
return {};
}
},
pagination: {
type: Object,
require: true,
default: () => {
return {};
}
}
});
const emit = defineEmits(['handleTableControl', 'dataChange']);
const tableParams = reactive({
datas: [],
headers: [
{
label: '名称',
value: 'name',
width: '120',
show: true,
showOverflow: true,
showFixed: true
}
],
operations: []
});
const tablePagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
});
const isMultiple = ref(false);
const multipleSelection = ref([]);
const controlList = ref([]);
const listSetting = reactive({
visible: false,
checkAll: true,
isIndeterminate: false,
columList: [],
checkedColum: [] as any
});
const btIncludes = ['basic', 'secondary', 'threeLevel', 'text']
watch(
() => props.tableItems,
(val) => {
if (val) {
dataRender();
}
},
{ deep: true }
)
onMounted(() => {
dataRender();
})
/**
* 获取assets静态资源
* @param url
*/
const getAssetsFile = (url: string) => {
return new URL(`../../examples/assets/icons/${url}`, import.meta.url).href;
}
/**
* 分页后序号连续
* @param index
*/
const indexMethod = (index: any) => {
const { currentPage, pageSize } = tablePagination;
return index + 1 + (currentPage - 1) * pageSize;
}
/**
* 分页跳转
* @param val
*/
const handleCurrentChange = (val: number) => {
const params = {
type: 'pagination',
value: val
}
emit('dataChange', params);
}
const dataRender = () => {
const { header, tableData, tableOperations, multiple, tableControl } = props.tableItems;
tableParams.headers = header;
tableParams.datas = tableData;
tableParams.operations = tableOperations;
isMultiple.value = multiple;
controlList.value = tableControl;
listSetting.columList = header;
const {currentPage, pageSize, total} = props.pagination;
tablePagination.currentPage = currentPage;
tablePagination.pageSize = pageSize;
tablePagination.total = total;
multipleSelection.value = [];
showAllColum();
}
/**
* 表格操作事件
* @param type
*/
const handleTableControl = (type: string) => {
const params = {
type: type
}
if (type === '删除') {
params['value'] = multipleSelection.value;
}
emit('handleTableControl', params);
}
/**
* 表格数据操作
* @param type: 操作的类型
*/
const handleClick = (type: string, row: any) => {
const params = {
type: type,
value: row
}
emit('dataChange', params);
}
const handleSelectionChange = (val: any) => {
multipleSelection.value = val;
}
const filterHandler = (
value: string,
row: any,
column: TableColumnCtx<any>
) => {
const property = column['property']
return row[property] === value
}
// 展开列设置
const openPopover = () => {
listSetting.visible = true;
};
const showAllColum = () => {
listSetting.checkedColum = listSetting.columList.map((item: any) => {
if (item.show) {
return item.label;
}
});
};
/**
* 列全选
* @param val
*/
const handleCheckAllChange = (val: boolean) => {
if (val) {
tableParams.headers.map((item: any) => {
item.show = true;
});
} else {
tableParams.headers.map((item: any) => {
item.show = false;
});
}
showAllColum();
listSetting.isIndeterminate =
tableParams.headers.length > 0 && tableParams.headers.length < listSetting.columList.length;
};
/**
* 列选中
* @param value
*/
const handleCheckedChange = (value: string[]) => {
tableParams.headers.map((item: any) => {
item.show = value.includes(item.label);
});
showAllColum();
const checkedCount = value.length;
listSetting.isIndeterminate = checkedCount > 0 && checkedCount < listSetting.columList.length;
};
</script>
<style scoped>
.container {
width: 100%;
background-color: #ffffff;
}
/*
公共样式
*/
.container .control-item {
margin-right: 8px;
padding: 0 6px;
}
.container .control-item:last-child {
margin-right: 0;
}
.container .control-item .icon-size {
height: 1rem;
width: 1rem;
margin-right: 2px;
}
/*
表格操作栏样式
*/
.table-control {
width: auto;
display: flex;
place-items: center;
justify-content: flex-end;
margin-bottom: 12px;
}
/**
表格样式
*/
.table-wrap {
width: auto;
}
.table-wrap .header-style {
background-color: #f2f2f2;
}
/*
分页样式
*/
.pagination {
margin-top: 16px;
}
.pagination:after {
display: block;
clear: both;
content: '';
}
.el-pagination > .is-first {
flex: 1;
}
</style>
用于导出该组件,name在script
标签上,所以获取名字的形参是__name
import DynamicTable from "./dynamic-table.vue";
DynamicTable.install = (App) => {
App.component(DynamicTable.__name, DynamicTable)
}
export default DynamicTable;
examples根目录下的index.js,用于导出所有的组件
import DynamicTable from "./table/dynamic-table.vue";
export { DynamicTable }
const components = [DynamicTable]
const install = (App) => {
components.forEach((item) => {
App.component(item.__name, item)
})
}
export default {
install
}
5. 修改配置项
- 在vite.config.ts修改打包配置
build: {
outDir: 'lib',
lib: {
// 指定组件编译入口文件
entry: resolve(__dirname, 'packages/index.js'),
name: 'Vue3DynamicModule',
fileName: 'vue3-dynamic-module'
},
// 打包配置
rollupOptions: {
external: ['vue'],
output: {
// 在 UMD 构建模式下位这些外部化的依赖提供全局变量
globals: {
vue: 'vue'
}
}
}
}
- 在package.json增加exports对象,最后打包使用的就是这里面的配置
"exports": {
"./lib/style.css": "./lib/style.css",
".": {
"import": "./lib/vue3-dynamic-module.mjs",
"require": "./lib/vue3-dynamic-module.umd.js"
}
}
- 在根目录下添加
.npmignore
文件,忽略不需要上传到npm库的文件
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw*
# 以下是新增的
# 要忽略目录和指定文件
.vscode
examples/
packages/
public/
vite.config.js
*.map
*.html
6. 在examples进行引入组件测试
新建一个table组件,直接在App.vue
文件里引入:
<template>
<DynamicTable
:table-items="tableItems"
:pagination="pagination"
@handle-table-control="handleTableControl"
@data-change="tableChange"
></DynamicTable>
</template>
<script setup lang="ts">
import DynamicTable from "../../packages/table/dynamic-table.vue";
import { reactive } from "vue";
const tableItems = reactive({
header: [
{
label: '名称',
value: 'title',
width: '300',
show: true,
isChecked: true,
showOverflow: true,
showFixed: true,
filters: [
{ text: '巴洛克风盛宴', value: '巴洛克风盛宴' },
{ text: '大弯的生日', value: '大弯的生日' }
]
},
{
label: '图片源',
value: 'copyright',
width: '700',
show: true,
isChecked: false,
showOverflow: true,
showFixed: true
},
{
label: '壁纸',
value: 'url',
width: '200',
show: true,
isChecked: true,
showOverflow: false,
showFixed: false,
showImage: true
},
{
label: '更新时间',
value: 'startdate',
width: '200',
show: true,
isChecked: false,
showOverflow: false,
showFixed: false,
sortable: true
},
{
label: '操作',
value: 'control',
width: 'auto',
show: true,
isChecked: true,
showOverflow: false,
showFixed: false
}
],
tableControl: [
{
code: '3',
btType: 'iconTextBt',
type: 'primary',
label: '新建',
icon: 'add'
},
{
code: '4',
btType: 'iconTextBt',
type: 'danger',
label: '删除',
icon: 'del'
},
{
code: '6',
btType: 'iconBt',
type: 'primary',
label: '刷新',
effect: 'dark',
placement: 'top',
icon: 'refresh'
},
{
code: '7',
btType: 'listSet',
type: 'primary',
label: '列设置',
effect: 'dark',
placement: 'top',
icon: 'set'
}
],
tableData: [],
tableOperations: [
{
type: 'download',
label: '下载原图'
},
{
type: 'edit',
label: '编辑'
}
],
multiple: true
});
const pagination = reactive({
currentPage: 1,
pageSize: 10,
total: 0
});
const handleTableControl = (params) => {
const { type, value } = params;
if (type === '刷新') {
tableList();
} else if (type === '删除') {
if (value.length === 0) {
ElMessage.warning('请选择至少一条数据!')
} else {
dialogData.title = '删除';
dialogData.info = '确定将选择的数据删除?';
dialogData.tipsVisible = true;
}
} else if (type === '新建') {
dialogData.title = '新建';
formData.name = '';
formData.password = '';
formData.category = '';
formData.region = '';
formData.number = 0;
formData.date = '';
formData.id = '';
dialogData.formVisible = true;
}
}
const tableChange = (params: {val: number}) => {
const { type, value } = params;
switch (type) {
case 'pagination':
pagination.currentPage = value;
break;
case 'edit':
dialogData.title = '编辑';
formData.name = value.title;
formData.password = value.password;
formData.category = value.category;
formData.region = value.region;
formData.number = value.top;
formData.date = value.date;
formData.id = value.id;
dialogData.formVisible = true;
break;
default:
break;
}
}
</script>
7. 打包输出lib库
npm run build
输出目录lib,里面包含以下几个文件,
8. 上传到npm
首先你需要有一个npm账号,如果没有可以先去npm官网注册。接着你需要在项目下执行命令npm login
,输入npm的用户、密码和邮箱,然后会给你发送一次性密码,接着就可以开始上传你的npm包了。
出现红框里表示已经上传成功了,你可以去npm里查看你的包情况。注意❗ 每次上传的时候都需要修改package.json
里面的版本号
,否则上传的时候会报错。
组件库源码
组件详细使用说明
?dynamicModule使用文档
欢迎大家使用交流,组件还不够完美,但是还在完善的路上…