vxe-table v4 列表自定义列配置(Promise缓存,拖动排序、置顶)

解决了啥
  1. 列过多 加载缓慢 (50+)
  2. 用户级自定义列配置 只配置想看到的数据
  3. 拖拽排序、默认排序字段、设置列宽、置顶列
  4. 先渲染表格、再递归请求表头下拉 加快渲染
效果展示

在这里插入图片描述

在这里插入图片描述
gif不清楚 视频地址 (视频可见:getOne 即请求自定义配置接口只会触发一次,除非主动清楚缓存)

流程
触发请求配置
有配置
无配置
进入页面
Promise缓存
返回缓存
请求配置
请求配置完成
加载配置列
加载默认列json
渲染列
页面渲染流程

routerStart==> 路由开始
​ routerDone==> 路由结束
​ useCommonHooks==> 请求配置hooks(请求自定义列配置只会在此处触发一次,其余调用皆走缓存,除非主动清除缓存)
​ gridMount==> 表格组件渲染钩子
​ gridQueryRes==> 页面数据请求完毕
​ gridLoadColumns==> 加载列
​ queryHeaderOptions==> 递归请求表头下拉options
在这里插入图片描述

代码

表格组件封装Grid

  1. 进入页面
  <Grid
      ref="gridRef"
      :columns="tableColumn"
      :toolbar-button="toolbarButtonConfig"
      :operte-button="operateButtonConfig"
      :query-api="yourapi"
      @click-fall-back="clickFallBack"
      @query-options="queryHeaderOptions()"
    />
import { useCommonTableConfig } from '@/hooks/useCommon'
//此处会触发2 queryHeaderOptions 为递归请求表头下拉options 在表格加载列完成后会触发此方法
const { tableColumn, queryHeaderOptions } = useCommonTableConfig()
  1. 请求配置useCommon
  /**
   * 组合生成column (JSON转换成列,根据业务需求自行实现)
   */
  const patchTableColumn = (data: CustomColumnProps[]): ColumnProps[] => {
    const temp: ColumnProps[] = [
      {
        type: 'checkbox',
        width: 50,
        fixed: 'left'
      }
    ]
    for (let index = 0; index < data.length; index++) {
      const element = data[index]
      const { slots, field, title, sortable, minWidth, width, selectField, loading, options, rules } = element
      const obj: ColumnProps = {
        slots: {
          header: slots
        },
        children: [{ field, title, sortable, selectField, minWidth, width, formatter: formatterFuncMap[field] || null }]
      }
      if (loading !== undefined) obj.loading = loading
      if (options !== undefined) {
        obj.options = options
        if (!options.length) {
          // 需要请求表头下拉
          queryHeaderField.value.push({
            field,
            api: queryHeaderApiMap[field]
          })
        }
      }
      if (rules !== undefined)
        obj.rules = {
          message: rules.message,
          func: (val) =>
            rules.type === 'decimal'
              ? validDecimal(rules.maxLength as number, rules.maxDecimal as number, val)
              : validInteger(rules.maxLength as number, val)
        }
      temp.push(obj)
    }
    return temp
  }
 /**
   * 请求表头列
   */

  const queryColumns = () => {
    listCustomize($route.name as string)
      .then((res) => {
        const { data } = res
        if (data) {
          const { content } = data
          columns.value = [...patchTableColumn(JSON.parse(content))]
        } else {
          // 抛出异常 获取默认
          throw new Error()
        }
      })
      .catch(() => {
        // 默认
        const { jsonType } = $route.meta
        if (!jsonType)
          console.warn(`your warning`)
        jsonType &&
          listDefault(jsonType).then((res: any) => {
            const { data } = res
            columns.value = [...patchTableColumn(data)]
          })
      })
  }
 onBeforeMount(() => {
    console.log('useCommonHooks==>', new Date().getTime())
    queryColumns()
  })
  1. Promise缓存请求配置 (此处可以不走http请求,将配置缓存在浏览器,自行实现)
npm i @vueuse/core
import { useMemoize } from '@vueuse/core'
const jsonFiles = import.meta.globEager('@/json/*.json')
/**
 *
 * @desc 默认json示例 数组 仅示例 业务无关
 */
{
    "slots": "select",
    "field": "fieldDesc",
    "title": "title",
    "sortable": true,
    "selectField": "field",
    "loading": false,
    "options": []
  }
/**
 * 获取列表自定义信息详情
 * code 必须为string 设为路由name 保证唯一
 */

export const listCustomize = useMemoize(async (code: string) => {
  const list = await service.get(`yourCustomizeApi`)
  return list
})
export const deleteListCustomizeCache = (code: string) => listCustomize.delete(code)
export const clearListCustomizeCache = () => listCustomize.clear()
/**
 *
 * @desc 添加或修改列表自定义信息
 */
export const createOrUpdate = (params: object) =>
  service.post(`yourApi`, params)
/**
 *
 * @desc 模拟请求 本地默认列
 */
const getColumnJson = (jsonType: string) => {
  return new Promise((resolve) => {
    const key = Object.keys(jsonFiles).filter((jsonKey) => jsonKey.includes(jsonType))[0]
    resolve({
      data: jsonFiles[key]['default'] || [],
      status: 0
    })
  })
}
/**
 *
 * @desc 获取本地json默认列
 */
export const listDefault = useMemoize(async (jsonType: string) => {
  const columns = await getColumnJson(jsonType)
  return columns
})
  1. Grid 组件自定义工具栏
//Grid 组件自定义工具栏  点击弹出自定义列配置弹框
<template>
  <vxe-grid v-bind="gridOptions" ref="xGrid" @page-change="pageChange" @sort-change="sortChange">
    <template #toolbar_tools>
      <div class="vxe-tools--wrapper">
        <button
          v-show="customConfig"
          class="vxe-button type--button size--small is--circle"
          type="button"
          title="列设置"
          @click="customlayout"
        >
          <i class="vxe-button--icon vxe-icon--menu"></i>
        </button>
      </div>
    </template>
  </vxe-grid>

  <n-modal
    v-model:show="showModal"
    preset="card"
    :title="`${t(`route.${$route.meta.title}`)}列表自定义`"
    :mask-closable="false"
    style="width: 980px; margin-top: 150px"
    :z-index="999"
  >
    <custom-layout @hide-modal="hideModal" />
  </n-modal>
</template>
  1. 自定义列配置组件customLayout.tsx
npm i sortablejs @types/sortablejs
//customLayout.tsx
import {
  NSpace,
  NButton,
  NTooltip,
  NSpin,
  NFormItem,
  NInputNumber,
  type FormItemRule,
  type FormItemInst
} from 'naive-ui'
import type { VxeGridProps, VxeTableInstance } from 'vxe-table'
import { createOrUpdate, listCustomize, deleteListCustomizeCache, listDefault } from '@/api/modules/app'
type Sortable = import('sortablejs')
import { type SortableEvent } from 'sortablejs'
type GridDataProps = {
  title: string
  field: string
}
export default defineComponent({
  name: 'CustomLayout',
  emits: {
    hideModal: (refresh?: boolean) => true
  },
  setup(props, { emit }) {
    const $route = useRoute()
    const loading = ref(false)
    const sortable = shallowRef<Sortable | null>(null)
    const leftGrid = ref({} as VxeTableInstance)
    const rightGrid = ref({} as VxeTableInstance)
    /**
     * 默认column json
     */
    const leftGridOptions = reactive<VxeGridProps>({
      height: 500,
      size: 'small',
      rowConfig: {
        isCurrent: true,
        isHover: true
      },
      columns: [
        {
          title: '全部字段列表',
          headerClassName: 'text-blue',
          children: [
            {
              type: 'checkbox',
              width: 50
            },
            {
              type: 'seq',
              width: 50
            },
            {
              title: '字段名',
              field: 'title',
              minWidth: 150
            }
          ]
        }
      ],
      checkboxConfig: {
        checkMethod: ({ row }) => !selectFields.value!.includes(row.title)
      },
      data: []
    })
    const rightGridOptions = reactive<VxeGridProps>({
      height: 500,
      size: 'small',
      rowConfig: {
        isCurrent: true,
        isHover: true,
        useKey: true
      },
      editConfig: {
        trigger: 'click',
        mode: 'cell'
      },
      emptyText: '当前页面未配置自定义列表!',
      columns: [
        {
          title: '展示字段列表',
          headerClassName: 'text-blue',
          children: [
            {
              type: 'checkbox',
              width: 50
            },
            {
              type: 'seq',
              width: 50
            },
            {
              title: '字段名',
              field: 'title',
              minWidth: 150
            },
            {
              title: '列宽',
              titlePrefix: {
                content: '正整数,50-500'
              },
              field: 'width',
              width: 100,
              editRender: {
                name: '$input',
                immediate: true,
                props: {
                  type: 'number',
                  placeholder: '请输入列宽',
                  clearable: false,
                  min: 50,
                  max: 500
                },
                defaultValue: 50,
                placeholder: '请输入列宽'
              }
            },
            {
              title: '排序方式',
              field: 'sortType',
              width: 120,
              titlePrefix: {
                content: '升、降序只能生效一个字段'
              },
              editRender: {
                name: '$select',
                immediate: true,
                options: [
                  {
                    label: '不排序',
                    value: false
                  },
                  {
                    label: '默认升序',
                    value: 'asc'
                  },
                  {
                    label: '默认降序',
                    value: 'desc'
                  }
                ],
                events: {
                  change: ({ row }, { value }) => sortModeChange(row, value)
                },
                props: {
                  transfer: true,
                  placeholder: '请选择排序方式',
                  clearable: false
                },
                placeholder: '请选择排序方式'
              }
            },

            {
              title: '操作',
              width: 80,
              slots: {
                default: 'operateCell'
              }
            },
            {
              width: 50,
              slots: {
                default: 'dragSlot',
                header: 'dragSlotHeader'
              }
            }
          ]
        }
      ],
      data: []
    })
    /**
     * 已展示的字段
     */

    const selectFields = computed(() => rightGridOptions.data?.map((row) => row.title))
    /**
     *T排序方式设定
     设置为非 false 时,升序、降序仅能留存一个,设置其他为false
     */
    const sortModeChange = (currentRow, value) => {
      if (value) {
        const otherRowHasSort = rightGridOptions.data
          ?.filter((row) => row.sortType)
          .filter((_row) => _row.title !== currentRow.title)
        if (otherRowHasSort?.length) {
          otherRowHasSort.forEach((otherRow) => {
            const otherRowIndex = rightGrid.value.getRowIndex(otherRow)
            rightGridOptions.data!.at(otherRowIndex).sortType = false
          })
        }
      }
    }

    /**
     * 展示 隐藏字段
     */
    const handleClick = (cancel = false) => {
      if (cancel) {
        // 取消
        const selectRow = rightGrid.value?.getCheckboxRecords()
        if (!selectRow.length) return window.$message.warning('请选取至少一条展示字段!')
        rightGrid.value.removeCheckboxRow().then(({ rows }) => {
          rows.forEach((row) => {
            const tempIndex = rightGridOptions.data?.findIndex((ele) => ele.title === row.title)
            rightGridOptions.data?.splice(tempIndex as number, 1)
          })
        })
      } else {
        const selectRow = leftGrid.value?.getCheckboxRecords()
        if (!selectRow.length) return window.$message.warning('请选取至少一条列表字段!')
        rightGridOptions.data?.push(
          ...selectRow.map((row) => {
            return {
              ...row,
              width: row.minWidth || 200,
              sortType: false
            }
          })
        )
        rightGrid.value.reloadData(rightGridOptions.data as [])
        leftGrid.value.clearCheckboxRow()
      }
    }
    /**
     * 保存
     * @param cancel 是否取消
     * @returns
     */
    const formSubmit = (cancel = false) => {
      if (cancel) return emit('hideModal')
      if (!rightGridOptions.data?.length) return window.$message.warning('请配置至少一条展示字段!')
      loading.value = true
      createOrUpdate({
        resourceCode: $route.name,
        content: JSON.stringify(rightGridOptions.data)
      })
        .then((res) => {
          const { msg } = res
          window.$message.success(msg || '操作成功!')
          deleteListCustomizeCache($route.name as string) //清除缓存
          emit('hideModal', true)
        })
        .finally(() => {
          loading.value = false
        })
    }
    /**
     * 获取默认
     */
    const getListDefault = async () => {
      leftGridOptions.loading = true
      listDefault($route.meta.jsonType)
        .then((res: any) => {
          const { data } = res
          leftGridOptions.data = [].concat(data)
        })
        .finally(() => {
          leftGridOptions.loading = false
        })
    }
    /**
     * 获取已保存
     */
    const getListCustomize = async () => {
      rightGridOptions.loading = true
      listCustomize($route.name as string)
        .then((res) => {
          const { data } = res
          if (data) {
            const { content } = data
            rightGridOptions.data = [...JSON.parse(content)]
          }
        })
        .finally(() => {
          rightGridOptions.loading = false
        })
    }

    /**
     * 置顶 |移至指定行
     */

    const handleMoveRow = (toTop: boolean, rowIndex: number) => {
      if (toTop) {
        // 置顶
        const currentRow = rightGridOptions.data?.splice(rowIndex, 1)[0]
        rightGridOptions.data?.splice(0, 0, currentRow)
        rightGrid.value.reloadData(rightGridOptions.data as [])
      } else {
        const seq = ref<number | null>(rowIndex + 1)
        const seqRef = ref<FormItemInst | null>(null)
        const rule: FormItemRule = {
          required: true,
          trigger: ['input', 'blur'],
          validator() {
            if (seq.value === null) return new Error('请输入行序号!')
            if (seq.value === rowIndex + 1) return new Error('请输入不同的行序号!')
            if (!/^\d*$/.test(seq.value + '')) {
              return new Error('行序号应该为大于1的整数!')
            }
          }
        }
        window.$useDialog.info({
          title: '移至指定行',
          showIcon: false,
          style: {
            width: '500px'
          },
          content: () =>
            h(
              'div',
              {
                class: 'text-base'
              },
              h(
                NFormItem,
                {
                  ref: seqRef,
                  label: '行序号',
                  labelPlacement: 'left',
                  labelWidth: 120,
                  rule
                },
                {
                  default: () =>
                    h(NInputNumber, {
                      class: 'w-full text-left',
                      placeholder: '请输入行序号',
                      buttonPlacement: 'both',
                      min: 1,
                      max: rightGridOptions.data?.length,
                      value: seq.value,
                      autofocus: true,
                      'onUpdate:value': (value: number | null) => (seq.value = value)
                    }),
                  label: () =>
                    h('span', [
                      '行序号',
                      h(
                        NTooltip,
                        {
                          trigger: 'hover'
                        },
                        {
                          trigger: () => (
                            <iconpark-icon
                              name="info"
                              color="currentColor"
                              size="16"
                              class="cursor-help vertical-middle op-80"
                            ></iconpark-icon>
                          ),
                          default: () => '整数,1-展示总条数'
                        }
                      )
                    ])
                }
              )
            ),
          positiveText: '确定',
          negativeText: '取消',
          iconPlacement: 'top',
          maskClosable: false,
          onPositiveClick() {
            return new Promise((resolve, reject) => {
              seqRef.value?.validate({
                callback: (errors) => {
                  if (errors) {
                    reject()
                  } else {
                    const currentRow = rightGridOptions.data?.splice(rowIndex, 1)[0]
                    rightGridOptions.data?.splice((seq.value as number) - 1, 0, currentRow)
                    rightGrid.value.reloadData(rightGridOptions.data as [])
                    resolve(null)
                  }
                }
              })
            })
          },
          onNegativeClick: async () => {
            return true
          }
        })
      }
    }
    /**
     * 初始化 sortablejs 行拖动
     */

    const initSortable = () => {
      const { value } = rightGrid
      sortable.value = Sortable.create(value.$el.querySelector('.body--wrapper>.vxe-table--body tbody'), {
        handle: '.drag',
        onEnd: (sortableEvent: SortableEvent) => {
          const { oldIndex, newIndex } = sortableEvent
          const currentRow = rightGridOptions.data?.splice(oldIndex as number, 1)[0]
          rightGridOptions.data?.splice(newIndex as number, 0, currentRow)
          rightGrid.value.reloadData(rightGridOptions.data as [])
        }
      })
    }
    onMounted(() => {
      getListDefault()
      getListCustomize()
      initSortable()
    })
    onBeforeUnmount(() => {
      sortable.value?.destroy()
    })

    return {
      leftGridOptions,
      rightGridOptions,
      leftGrid,
      rightGrid,
      handleClick,
      formSubmit,
      handleMoveRow,
      loading
    }
  },
  render() {
    const { leftGridOptions, rightGridOptions, loading } = this
    return (
      <div>
        <NSpin stroke="#409eff" show={loading}>
          <NSpace vertical size={10}>
            <NSpace justify="space-between" align="center" wrap={false}>
              <vxe-grid ref={(el) => (this.leftGrid = el)} {...leftGridOptions}></vxe-grid>
              <NSpace vertical justify="center" align="center" class="h-full" style="--iconColor: #606266">
                <NTooltip trigger="hover">
                  {{
                    trigger: () => (
                      <iconpark-icon
                        name="to-right"
                        size="28"
                        class="cursor-pointer text-$iconColor"
                        onClick={() => this.handleClick()}
                      ></iconpark-icon>
                    ),
                    default: () => '展示字段'
                  }}
                </NTooltip>
                <NTooltip trigger="hover">
                  {{
                    trigger: () => (
                      <iconpark-icon
                        name="to-left"
                        size="28"
                        class="cursor-pointer text-$iconColor"
                        onClick={() => this.handleClick(true)}
                      ></iconpark-icon>
                    ),
                    default: () => '取消展示'
                  }}
                </NTooltip>
              </NSpace>
              <vxe-grid ref={(el) => (this.rightGrid = el)} {...rightGridOptions}>
                {{
                  dragSlot: () => (
                    <iconpark-icon
                      name="drag"
                      size="16"
                      color="#409eff"
                      class="cursor-move vertical-middle drag"
                    ></iconpark-icon>
                  ),
                  dragSlotHeader: () => (
                    <NTooltip trigger="hover">
                      {{
                        trigger: () => <i class="vxe-cell-help-icon vxe-icon--question"></i>,
                        default: () => '按住后可以上下拖动排序'
                      }}
                    </NTooltip>
                  ),
                  operateCell: ({ rowIndex }) => (
                    <NSpace>
                      <NTooltip trigger="hover" disabled={!rowIndex}>
                        {{
                          trigger: () => (
                            <iconpark-icon
                              name="minus-the-top"
                              size="14"
                              color="currentColor"
                              class={[rowIndex ? 'visible cursor-pointer' : 'invisible']}
                              onClick={() => this.handleMoveRow(true, rowIndex)}
                            ></iconpark-icon>
                          ),
                          default: () => '置顶'
                        }}
                      </NTooltip>
                      <NTooltip trigger="hover">
                        {{
                          trigger: () => (
                            <iconpark-icon
                              name="bring-to-front-one"
                              size="14"
                              color="currentColor"
                              class="cursor-pointer"
                              onClick={() => this.handleMoveRow(false, rowIndex)}
                            ></iconpark-icon>
                          ),
                          default: () => '移至指定行'
                        }}
                      </NTooltip>
                    </NSpace>
                  )
                }}
              </vxe-grid>
            </NSpace>
            <NSpace justify="end" align="center" size={10}>
              <NButton secondary onClick={() => this.formSubmit(true)}>
                取消
              </NButton>
              <NButton type="info" onClick={() => this.formSubmit()}>
                保存
              </NButton>
            </NSpace>
          </NSpace>
        </NSpin>
      </div>
    )
  }
})

over~

在这里插入图片描述

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
vxe-table是一个基于vue的表格组件,支持增删改查、虚拟滚动、懒加载、快捷菜单、数据校验、树形结构、打印导出、表单渲染、数据分页、模态窗口、自定义模板、灵活的配置项、丰富的扩展插件等... 设计理念: 面向现代浏览器,高效的简洁 API 设计 模块化表格、按需加载、插件化扩展 为单行编辑表格而设计,支持增删改查及更多扩展,强大的功能的同时兼具性能 功能: Basic table (基础表格) Grid (高级表格) Size (尺寸) Striped (斑马线条纹) Table with border (带边框) Cell style (单元格样式) Column resizable (拖动) Maximum table height (最大高度) Resize height and width (响应式宽高) Fixed column (固定) Grouping table head (表头分组) Highlight row and column (高亮行、) Table sequence (序号) Radio (单选) Checkbox (多选) Sorting (排序) Filter (筛选) Rowspan and colspan (合并行或) Footer summary (表尾合计) Import (导入) Export (导出) Print (打印) Show/Hide column (显示/隐藏) Loading (加载中) Formatted content (格式化内容) Custom template (自定义模板) Context menu(快捷菜单) Virtual Scroller(虚拟滚动) Expandable row (展开行) Pager(分页) Form(表单) Toolbar(工具栏) Tree table (树形表格) Editable CRUD(增删改查) Validate(数据校验) Data Proxy(数据代理) Keyboard navigation(键盘导航) Modal window(模态窗口) Charts(图表工具) 更新日志: v4.0.20 table 修改单选框、复选框获取值错误问题 grid 修复 KeepAlive 中报错问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值