• 使用 Vue3 + ts 开发一个ProTable


    前台实现

    实现效果

     

    技术栈

    vue3 + typescript + element-plus

    使用方法

    1. <template>
    2.   <el-tabs type="border-card" v-model="activeName">
    3.     <el-tab-pane
    4.     :label="item.label"
    5.     v-for="(item, index) in templateConfig"
    6.     :key="index" :name="item.name"
    7.     lazy
    8.     >
    9.     
    10.     <pro-table
    11.       :columns="item.columns"
    12.       :type="item.name"
    13.       :request-url="requestUrl"
    14.     >
    15.     pro-table>
    16.     el-tab-pane>
    17.   el-tabs>
    18. template>
    19. <script lang="ts" setup>
    20. import { ref } from 'vue'
    21. import ProTable from './components/ProTable/index.vue'
    22. import { ColumnPropsRequestUrl } from './components/ProTable/types'
    23. import { projectConfig, projectConfigBatchDelete } from './service/api'
    24. const activeName = ref('user')
    25. interface TemplateConfig {
    26.   name: string
    27.   label: string
    28.   columnsColumnProps[],
    29. }
    30. const requestUrlRequestUrl = {
    31.   create: projectConfig,
    32.   list: projectConfig,
    33.   update: projectConfig,
    34.   destroy: projectConfig,
    35.   batchDelete: projectConfigBatchDelete
    36. }
    37. const templateConfig = ref<TemplateConfig[]>([
    38.   {
    39.     label'ProTable',
    40.     name'user',
    41.     columns: [
    42.       {
    43.         key'userName',
    44.         title'用户名',
    45.         searchType'el-input'
    46.       },
    47.       {
    48.         key'password',
    49.         title'密码',
    50.         searchType'el-input'
    51.       },
    52.       {
    53.         key'email',
    54.         title'邮箱',
    55.         searchType'el-input'
    56.       },
    57.       {
    58.         key'phone',
    59.         title'手机号',
    60.         searchType'el-input'
    61.       },
    62.       {
    63.         key'role',
    64.         title'角色',
    65.         searchType'z-select',
    66.         attrs: {
    67.           options: [
    68.             {
    69.               label'管理员',
    70.               value'admin'
    71.             },
    72.             {
    73.               label'普通用户',
    74.               value'user'
    75.             }
    76.           ]
    77.         }
    78.       },
    79.       {
    80.         key'status',
    81.         title'状态',
    82.         searchType'z-select',
    83.         attrs: {
    84.           options: [
    85.             {
    86.               label'启用',
    87.               value1
    88.             },
    89.             {
    90.               label'禁用',
    91.               value0
    92.             }
    93.           ]
    94.         },
    95.         columnType'status'
    96.       },
    97.       {
    98.         key'hasUseArray',
    99.         title'是否使用数组参数?',
    100.         searchfalse,
    101.         searchType'useExpandField',
    102.         showfalse,
    103.         addfalse
    104.       },
    105.       {
    106.         key'arrayParams',
    107.         title'数组参数',
    108.         searchType'z-array',
    109.         searchfalse,
    110.         width120,
    111.         addfalse,
    112.         showfalse
    113.       },
    114.       {
    115.         key'hasUseArray',
    116.         title'是否使用JSON参数?',
    117.         searchfalse,
    118.         searchType'useExpandField',
    119.         showfalse,
    120.         addfalse
    121.       },
    122.       {
    123.         key'jsonParams',
    124.         title'JSON参数',
    125.         searchType'z-json',
    126.         searchfalse,
    127.         width120,
    128.         addfalse,
    129.         showfalse
    130.       },
    131.       {
    132.         key'createdAt',
    133.         title'创建时间',
    134.         width180,
    135.         searchType'el-date-picker',
    136.         addfalse
    137.       },
    138.       {
    139.         key'updatedAt',
    140.         title'更新时间',
    141.         width180,
    142.         searchType'el-date-picker',
    143.         addfalse
    144.       },
    145.       {
    146.         key'action',
    147.         title'操作',
    148.         searchfalse,
    149.         addfalse,
    150.         width150
    151.       }
    152.     ]
    153.   }
    154. ])
    155. script>
    156. <style lang="less">
    157. style>
    158. 复制代码

    ProTable 设计思路

    页面整体分为5个区域,

    1. 表单搜索区域

    2. 表格功能按钮区域

    3. 表格右上角操作区域

    4. 表格主题区域

    5. 表格分页区域

    要考虑的问题?

    1. 那些区域是要支持传入slot的?

    2. 表格原有的slot是否要交给用户来传递,还是在内部进行封装?如colum是状态的时候需要映射成tag,是数组类型的时候映射成表格,是json的时候需要点击查看详情?假设每个表格都要处理的的话就太麻烦,我们希望通过一个字段来控制它。

    3. column的某一列是否需要复制的功能?

    4. 列字段需要编辑的功能?

    实现的过程中有哪些细节?

    1. 表格的高度,把可表格可视区域的大小交给用户自己来控制,把批量操作按钮放在最下面(fixed定位)。这样用户可以在最大区域内看到表格的内容。

    编码风格

    1. 组件上面属性如果超过三个,就换行

    2. eslint使用的是standard风格。

    css 小知识

    1. <div class='box'>
    2.   <div class='z'>div>
    3. div>
    4. 复制代码
    1. *{
    2.   box-sizing: border-box;
    3. }
    4. .box{
    5.   display: inline-block;
    6.     vertical-align: top;
    7. }
    8. .z{
    9.   height32px;
    10.   border1px solid;
    11.   width100px;
    12.   display: inline-block;
    13. }
    14. 复制代码

    如果把盒子变成了行内元素之后,若其内部还是行内元素,那么就会产生间隙,就会导致其高度与父元素高度不同。如下。

     

    image.png

    解决方法也很简单,则需要设置其子元素的vertical-align属性,或者设置font-size: 0,其根本原因是因为中间的文本元素也占位置。再或者不使用inline-block,换做inline-flex属性完全没有问题,因为在element-plus组件库中也大量的使用了这个属性,兼容性也很nice。

    这个几个解决方法很早就知道了,就是关于vertical-algin这个,以及与line-height的关系还是有点模糊,一直也没去深究。

     

    深入浅出vertical-align和line-height[1] 简单说 CSS的vertical-align[2]

    还有联想到baseline这个东西在flex,align-items属性:交叉轴上的一个属性很像。

    链接[3]

    表格操作

    1. 添加数据之后,重新获取数据的时候pageIndex要重置为1,删除数据的时候也是一样。

    2. 编辑数据的时候,pageIndex不变,还是当前页码。

    3. 总结下来,就是当数据条数会发生改变的时候,都会重置pageIndex1。当用户操作不会影响数据总条数的时候,pageSize还维持当前不变。

    总结

    1. 使用了一个库,可以监听dom元素大小的变化,resize-observer-polyfill[4]。

    2. 在 3.x 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute。开发者可以自己选择要保留哪一个。

    1. <div id="red" v-bind="{ id: 'blue' }">div>
    2. <div id="blue">div>
    3. <div v-bind="{ id: 'blue' }" id="red">div>
    4. <div id="red">div>
    5. 复制代码

    文档地址\# v-bind 合并行为

    参考文章

    一个较新的JavaScript API——ResizeObserver 的使用

    后期功能扩展

    1. 字段之间有关联关系情况的处理,暂时还没想好。

    2. 扩展一下slot

    3. 等等。。

    迭代中....

     

    后台实现

    数据库 mysql

    我这里使用的是 xampp安装的,我们来查看一下版本。这是什么版本?假的吧,真的到10了吗?先不管了,能用就行。

     

    image.png

    建表

    1. CREATE TABLE `project_config`  (
    2.   `id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
    3.   `type` varchar(255CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '配置类型',
    4.   `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '配置的json字符串',
    5.   `created_at` datetime NOT NULL,
    6.   `updated_at` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
    7.   PRIMARY KEY (`id`) USING BTREE
    8. ) ENGINE = InnoDB AUTO_INCREMENT = 65 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = COMPACT;
    9. 复制代码

    新建项目

    1. npm init egg --type=simple
    2. 复制代码

    项目目录大致如下所示,

     

    image.png

    RESTful 风格的 URL 定义

     

    image.png

    Sequelize

    1. npm install --save egg-sequelize mysql2
    2. 复制代码
    • 在 config/plugin.js 中引入 egg-sequelize 插件, 这里我们引入了一个库egg-cors来帮我们实现cors

    1. 'use strict';
    2. /** @type Egg.EggPlugin */
    3. exports.sequelize = {
    4.   enable: true,
    5.   package'egg-sequelize',
    6. };
    7. exports.cors = {
    8.   enable: true,
    9.   package'egg-cors',
    10. };
    11. 复制代码
    • 在 config/config.default.js 中编写 sequelize 配置

    1. /* eslint valid-jsdoc: "off" */
    2. 'use strict';
    3. /**
    4.  * @param {Egg.EggAppInfo} appInfo app info
    5.  */
    6. module.exports = appInfo => {
    7.   /**
    8.    * built-in config
    9.    * @type {Egg.EggAppConfig}
    10.    **/
    11.   const config = exports = {};
    12.   // use for cookie sign key, should change to your own and keep security
    13.   config.keys = appInfo.name + '_1655529530112_7627';
    14.   // add your middleware config here
    15.   config.middleware = [];
    16.   config.security = {
    17.     csrf: {
    18.       enablefalse,
    19.       ignoreJSONtrue,
    20.     },
    21.   };
    22.   config.cors = {
    23.     origin'*',
    24.     allowMethods'GET,HEAD,PUT,POST,DELETE,PATCH',
    25.   };
    26.   // add your user config here
    27.   const userConfig = {
    28.     // myAppName: 'egg',
    29.   };
    30.   // sequelize
    31.   const sequelize = {
    32.     dialect'mysql',
    33.     host'127.0.0.1',
    34.     port3306,
    35.     username'root',
    36.     password'123456',
    37.     database'test_database',
    38.     timezone'+08:00',
    39.     dialectOptions: {
    40.       dateStringstrue,
    41.       typeCasttrue,
    42.     },
    43.     define: {
    44.       freezeTableNametrue// 模型名强制和表明一致
    45.       underscoredtrue// 字段以下划线(_)来分割(默认是驼峰命名风格)
    46.     },
    47.   };
    48.   return {
    49.     ...config,
    50.     ...userConfig,
    51.     sequelize,
    52.   };
    53. };
    54. 复制代码

    1、时间格式化

    类型需要采用:Sequelize.DATE

    初始化Sequelize的时候传入dialectOptions参数,及timezone

    1. timezone'+08:00',  // 改为标准时区
    2. dialectOptions: {
    3.   dateStringstrue,
    4.   typeCasttrue,
    5. },
    6. 复制代码

    下面就开始编写

    controller

    对这块需要安装lodash,懂的都懂。

    controller/ProjectConfig.js

    1. 'use strict';
    2. const { success } = require('../utils/res');
    3. const { omit, pick } = require('lodash');
    4. const Controller = require('egg').Controller;
    5. class ProjectConfigController extends Controller {
    6.   async index() {
    7.     const { ctx } = this;
    8.     const { pageSize, pageIndex } = ctx.query;
    9.     const { Op, fn, col, where, literal } = this.app.Sequelize;
    10.     // 固定的查询参数
    11.     const stableQuery = pick(ctx.query, [ 'type''createdAt''updatedAt' ]);
    12.     const stableQueryArgs = Object.keys(stableQuery)
    13.       .filter(key => Boolean(stableQuery[key]))
    14.       .map(key => {
    15.         return {
    16.           [key]: stableQuery[key],
    17.         };
    18.       });
    19.     const whereCondition = omit(ctx.query, [ 'pageIndex''pageSize''type''createdAt''updatedAt' ]);
    20.     // 需要模糊查询的参数
    21.     const whereArgs = Object.keys(whereCondition)
    22.       .filter(key => Boolean(whereCondition[key]))
    23.       .map(key => {
    24.         return where(fn('json_extract'col('value'), literal(`\'$.${key}\'`)), {
    25.           [Op.like]: `%${whereCondition[key]}%`,
    26.         });
    27.       });
    28.     const query = {
    29.       where: {
    30.         [Op.and]: [
    31.           ...stableQueryArgs,
    32.           ...whereArgs,
    33.         ],
    34.       },
    35.       order: [
    36.         [ 'createdAt''DESC' ],
    37.       ],
    38.       limitNumber(pageSize), // 每页显示数量
    39.       offset: (pageIndex - 1) * pageSize, // 当前页数
    40.     };
    41.     const data = await ctx.model.ProjectConfig.findAndCountAll(query);
    42.     ctx.body = success(data);
    43.   }
    44.   async create() {
    45.     const { ctx } = this;
    46.     const { type, value } = ctx.request.body;
    47.     const data = await ctx.model.ProjectConfig.create({ type, value });
    48.     ctx.body = success(data);
    49.   }
    50.   async update() {
    51.     const { ctx } = this;
    52.     const { type, value } = ctx.request.body;
    53.     const { id } = ctx.params;
    54.     const data = await ctx.model.ProjectConfig.update({ type, value }, { where: { id } });
    55.     ctx.body = success(data);
    56.   }
    57.   async destroy() {
    58.     const { ctx } = this;
    59.     const { id } = ctx.params;
    60.     console.log(id);
    61.     const data = await ctx.model.ProjectConfig.destroy({ where: { id } });
    62.     ctx.body = success(data);
    63.   }
    64.   async batchDestroy() {
    65.     const { ctx } = this;
    66.     const { ids } = ctx.request.body;
    67.     console.log(ids);
    68.     const { Op } = this.app.Sequelize;
    69.     const data = await ctx.model.ProjectConfig.destroy({
    70.       where: {
    71.         id: {
    72.           [Op.in]: ids,
    73.         },
    74.       },
    75.     });
    76.     ctx.body = success(data);
    77.   }
    78. }
    79. module.exports = ProjectConfigController;
    80. 复制代码

    模糊查询

    1. SELECT json_extract(字段名,'$.json结构'FROM 表名;
    2. 复制代码

    sequelize高级查询

    1. Post.findAll({
    2.   where: sequelize.where(sequelize.fn('char_length', sequelize.col('content')), 7)
    3. });
    4. // SELECT ... FROM "posts" AS "post" WHERE char_length("content") = 7
    5. 复制代码

    中文文档[7],英文看的吃力,看中文的也无妨,不寒碜。^_^

    model

    model/project_config.js

    1. 'use strict';
    2. module.exports = app => {
    3.   const { STRINGINTEGERTEXTDATE } = app.Sequelize;
    4.   const ProjectConfig = app.model.define('project_config', {
    5.     id: { typeINTEGERprimaryKeytrueautoIncrementtrue },
    6.     type: { typeSTRING },
    7.     value: {
    8.       typeTEXT,
    9.       get() {
    10.         return this.getDataValue('value') ? JSON.parse(this.getDataValue('value')) : null;
    11.       },
    12.       set(value) {
    13.         this.setDataValue('value'JSON.stringify(value));
    14.       },
    15.     },
    16.     createdAt: { typeDATE },
    17.     updatedAt: { typeDATE },
    18.   });
    19.   return ProjectConfig;
    20. };
    21. 复制代码

    router.js

    1. 'use strict';
    2. /**
    3.  * @param {Egg.Applicationapp - egg application
    4.  */
    5. module.exports = app => {
    6.   const { router, controller } = app;
    7.   router.get('/api/projectConfig', controller.projectConfig.index);
    8.   router.post('/api/projectConfig', controller.projectConfig.create);
    9.   router.put('/api/projectConfig/:id', controller.projectConfig.update);
    10.   router.delete('/api/projectConfig/:id', controller.projectConfig.destroy);
    11.   router.post('/api/projectConfig/batchDelete', controller.projectConfig.batchDestroy);
    12. };
    13. 复制代码

    API 文档 Apifox

    先快速测试一把,然后去对前端代码。

     

    image.png

    ts用到的一些只是

    1. 在类型别名(type alias)的声明中可以使用 `keyof``typeof``in` 等关键字来进行一些强大的类型操作
    1. interface A {
    2.   x: number;
    3.   y: string;
    4. }
    5. // 拿到 A 类型的 key 字面量枚举类型,相当于 type B = 'x' | 'y'
    6. type B = keyof A;
    7. const json = { foo: 1, bar: 'hi' };
    8. // 根据 ts 的类型推论生成一个类型。此时 C 的类型为 { foo: number; bar: string; }
    9. type C = typeof json;
    10. // 根据已有类型生成相关新类型,此处将 A 类型的所有成员变成了可选项,相当于 type D = { x?: numbery?: string; };
    11. type D = {
    12.   [T in keyof A]?: A[T];
    13. };
    14. 复制代码

    在比如用一个联合类型来约束对象的key,用interface我就没实现,貌似.

    1. export type FormItemType = 'el-input' | 'z-select' | 'el-date-picker'
    2. // 目前发现 interface 的key 只能是三种 string number symbol   keyof any
    3. type IPlaceholderMapping = {
    4.   [key in FormItemType]: string
    5. }
    6. export const placeholderMapping: IPlaceholderMapping = {
    7.   'el-input''请输入',
    8.   'z-select''请选择',
    9.   'el-date-picker''请选择日期'
    10. }
    11. 复制代码

     ProTable前端代码地址:https://github.com/xiuxiuyifan/ProTable

  • 相关阅读:
    第1篇:熊猫烧香之手动查杀
    【力扣】动态规划题目之“最”系列
    ASEMI整流桥KBU1510与GBJ1510能通用吗?
    Redis中的原子操作(2)-redis中使用Lua脚本保证命令原子性
    认识Java笔记(2)
    无涯教程-JavaScript - BETA.INV函数
    Spring注解@Transactional是什么?具体的使用方法
    sql注入总结
    多变量线性回归练习
    SIT测试和UAT测试区别
  • 原文地址:https://blog.csdn.net/suzhiwei_boke/article/details/126078084