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

解决了啥

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

效果展示

vxe-table v4 列表自定义列配置(Promise缓存,拖动排序、置顶)_第1张图片


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~

vxe-table v4 列表自定义列配置(Promise缓存,拖动排序、置顶)_第2张图片

你可能感兴趣的:(import,this,缓存,javascript,前端,vue.js,es6)