等了这么久,我终于把后台管理系统常用的表格和表单组件化了!

前言

感觉从进入前端后,做的都是ToB的产品,要不然就是大屏,每次在写那些重复性的表格表单的时候就觉得时间过得好快,感觉一天啥也没做就结束了,但是专注在写重复性代码里好开心啊,这样就不用动脑了!但是今年,就特别想动脑子了,先是用vue3 + antD搞了个给家里用的进销存管理系统前端部分,然后现在又开始琢磨弄一个常用的组件库。所以接下来就开始讲讲我整个搭建的过程吧!

创建组件库项目

基于vue3 + vite + Element Plus 技术框架,适用于PC端。

1. 安装vite脚手架

npm create vite@latest

图片.png
在选择 variant 的时候,可以选择customize with create-vue,相当于自定义安装,可以选择安装ESLint和Prettier,增加代码的规范,我是为了快就直接选择了JavaScript。

2. 改造项目结构

为了区分实际使用的包文件和测试文件
在根目录下新建packages目录,将原有的src目录修改为examples

图片.png
由于项目的入口文件是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,里面包含以下几个文件,

图片.png

8. 上传到npm

首先你需要有一个npm账号,如果没有可以先去npm官网注册。接着你需要在项目下执行命令npm login,输入npm的用户、密码和邮箱,然后会给你发送一次性密码,接着就可以开始上传你的npm包了。

图片.png
出现红框里表示已经上传成功了,你可以去npm里查看你的包情况。注意❗ 每次上传的时候都需要修改package.json里面的版本号,否则上传的时候会报错。

组件库源码

?vue3-dynamic-module

组件详细使用说明

?dynamicModule使用文档
欢迎大家使用交流,组件还不够完美,但是还在完善的路上…

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYKIiv5Z' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片