隐藏
«

vue3+ts封装表格

时间:2024-12-24 19:43     作者:suxiaojun     分类:


这里使用了elementplus+vuetify框架封装的

<template>
  <v-card class="border" elevation="0">
    <v-card-item v-show="showSearch" class="pb-2">
      <el-form :model="form" label-width="auto" :inline="true">
        <template v-for="(item, index) in TableHeaders" :key="index">
          <el-form-item :label="item.label" v-if="item.search?.component === 'Input'">
            <el-input v-model="form[item.field]" :placeholder="item.label" />
          </el-form-item>
          <el-form-item :label="item.label" v-if="item.search?.component === 'Select'">
            <el-select v-model="form[item.field]" :placeholder="item.label" style="min-width: 200px;">
              <el-option v-for="(optionItem) in item.search.optionApi()" :key="optionItem.label"
                :label="optionItem.label" :value="optionItem.value" />
            </el-select>
          </el-form-item>
          <el-form-item :label="item.label" v-if="item.search?.component === 'DatePicker'">
            <el-config-provider :locale="locale">
              <el-date-picker v-model="form[item.field]" type="daterange" range-separator="-"
                :start-placeholder="开始时间" :end-placeholder="结束时间" size="default"
                value-format="YYYY-MM-DD" />
            </el-config-provider>
          </el-form-item>
        </template>
        <el-form-item style="display: block;">
          <el-button type="primary" style="background-color: #5d87ff;color: #ffffff;" @click="submit">
            查询
          </el-button>
          <el-button type="info" @click="handleForm" style="color: #ffffff;">重置</el-button>
          <slot name="searchButton" />
          <el-dropdown v-show="SiftHeader ? SiftHeader : false" style="margin-left: auto" trigger="click"
            :hide-on-click="false">
            <span class="el-dropdown-link d-flex align-center ga-2">
              <img :src="filterSVG" width="20px" alt="">
                  <!-- 可选项表头配置 -->
              <span>配置</span>
            </span>
            <template #dropdown>
              <el-dropdown-menu>
                <el-checkbox-group v-model="check" @change="changeCheckbox">
                  <el-dropdown-item v-for="(item, index) in checkList" :key="index">
                    <el-checkbox :label="item.label" :value="item">
                    </el-checkbox>
                  </el-dropdown-item>
                </el-checkbox-group>
              </el-dropdown-menu>
            </template>
          </el-dropdown>
        </el-form-item>
      </el-form>
    </v-card-item>
    <v-divider v-show="showSearch"></v-divider>
    <v-data-table-server :headers="tableHeaderList" :items="TableData" :items-per-page="pagination.itemsPerPage"
      :page.sync="pagination.page" items-per-page-text="页码文本" show-current-page
      :page-text="页码文本 + props.Total" :items-per-page-options="perPageOptions"
      :items-length="props.Total" @update:options="switchPagination" :hide-default-footer="HideFooter">
      <template v-slot:headers="{ columns }">
        <tr>
          <template v-for="column in columns" :key="column.field">
            <th>
              <div class="mr-2" :style="{ width: column.width ? column.width : '100px' }">
                {{ column.label }}
                <v-tooltip v-if="column.labelTipTitle" location="end" :text="column.labelTipTitle">
                  <template v-slot:activator="{ props }">
                    <v-icon v-bind="props" icon="mdi-help-circle-outline" size="small"></v-icon>
                  </template>
                </v-tooltip>
              </div>
            </th>
          </template>
        </tr>
      </template>
      <template v-slot:item="{ item }">
        <tr>
          <td v-for="row in tableHeaderList" :key="row.id">
            <template v-if="row.field !== 'operate'">
              <div class="d-flex ga-2">
                <span v-show="!row.type">
                  {{ handleFormatter(item, row) }}
                </span>
                <span v-show="row.type === 'time'">
                  {{ formatDate(handleFormatter(item, row)) }}
                </span>
                <!-- vue3动态创建DOM 元素 -->
                <component v-if="row?.render" :is="row.render(item, onRowClick)" />
              </div>
            </template>
            <template v-else>
              <slot name="operate" :item="item" />
            </template>
          </td>
        </tr>
      </template>
    </v-data-table-server>
  </v-card>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref, watch, PropType } from 'vue';
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import en from 'element-plus/es/locale/lang/en'
import { formatDate } from "@/utils/formatDate"
import filterSVG from "@/assets/images/admin-backend/filter.svg"
import { i18n } from "@/utils/i18n";

/**
 * @description 可配置相关插槽
 * searchButton 搜索框中按钮插槽
 * operate 表格中操作列中插槽
 */

/**
 * @param {Array} TableHeaders // 表格表头
 * @param {Array} TableData // 表格数据
 * @param {Boolean} SiftHeader // 是否使用筛选表头
 * @param {Number} Total // 分页总数
 * @structure
 * field: 对应数据的key
 * label: 表头文本
 * labelTip: 表头是否显示提示图标,默认flase
 * labelTipTitle: 表头提示文本
 * type: time  后端返回时间戳时配置类型,为time时转为YYYY-MM-DD hh:mm:ss格式
 * search: {
 *  hidden: 此数据是否使用搜索,默认true
 *  component: Select下拉框  DatePicker时间筛选  Input输入框
 *  componentProps: {
 *   type
 *   unlinkPanels
 *   valueFormat
 *  }
 * }
 * formatter: function // 用来处理需要转化的数据,返回是一个简单类型
 * render: function // 用来实现动态创建DOM元素
 */
const props = defineProps({
  TableHeaders: Array as () => any,
  TableData: Array as () => any,
  SiftHeader: Boolean,
  Total: Number,
  HideFooter: Boolean,
  onRowClick: Function as PropType<(row: any, value: any) => void>
})
// 默认分页数和数据量
const pagination = reactive({
  page: 1,
  itemsPerPage: 10, // 每页显示的行数
})
const perPageOptions = reactive([
  { value: 10, title: '10' },
  { value: 25, title: '25' },
  { value: 50, title: '50' },
  { value: 100, title: '100' }
])
/**
 * @description 表格表头
 * 为了自定义表头数据,不使用props中的数据而是重新定义变量接收
 */
const tableHeaderList = ref([])
/* 搜索表单 */
const form: any = reactive({})
/**
 * 显示搜索表单控件
 * 如果表头中存在需要搜索条件则为true
 */
const showSearch = ref(false)
// 筛选默认勾选
const check: any = ref(props.TableHeaders)
// 筛选列表
const checkList: any = ref(props.TableHeaders)

/**
 * @description 处理搜索表单和筛选
 */
const handleForm = () => {
  // 筛选是否设置了hidden
  props.TableHeaders.forEach((item: any) => {
    if (!item.search.hidden) {
      form[item.field] = ''
      showSearch.value = true
    }
  })
}
/* 多选框发生变化时触发 */
const changeCheckbox = (value: string[]) => {
  // 原数组对比组件中返回的数组,然后进行重组
  const list = props.TableHeaders.map((item: any) => {
    let data = null
    value.forEach((row: any) => {
      if (row.label === item.label) {
        data = item
        return
      }
    })
    return data
  })
  // 剔除掉要隐藏的对象
  tableHeaderList.value = list.filter((row: any) => {
    return row != null
  })
}
// element的国际化
const locale = ref(zhCn)
watch(
  [() => props.TableHeaders, () => i18n.global.locale],
  ([newHeaders, newLocale], [oldHeaders, oldLocale]) => {
    check.value = newHeaders
    checkList.value = newHeaders
    // element的时间选择器切换中英文
    locale.value = newLocale.value == 'cn' ? zhCn : en
    // 重新调用 changeCheckbox 以应用新的表头
    changeCheckbox(check.value)
  },
  { immediate: true, deep: true }
)

const emit = defineEmits()
/**
 * @description 提交搜索表单
 * 父组件使用@onSubmit 绑定事件
 */
const submit = () => {
  emit('onSubmit', form);
}
/**
 * @description 切换分页
 * 父组件使用@pagination 绑定事件
 */
const switchPagination = (options: any) => {
  pagination.page = options.page;
  pagination.itemsPerPage = options.itemsPerPage;
  emit('pagination', options);
}

/**
 * @description 处理formatter
 * @param item 列表数据
 * @param row 表头定义对象
 */
const handleFormatter = (item: any, row: any) => {
  if ('formatter' in row) {
    return row.formatter(item[row.field])
  } else {
    return item[row.field]
  }
}

onMounted(() => {
  handleForm()
})
</script>

<style lang="scss" scoped>
::v-deep(.el-form-item__label-wrap) {
  margin-left: 0 !important;
}
</style>

下面是使用这个封装组件的父组件
HTML文件

<OperateTable :TableHeaders="ActivationTableHeaderList" :TableData="ActivationTableDataList"
                :Total="total" @onSubmit="(childParams: any) => subSearch('Activation')(childParams)"
                @pagination="(childParams: any) => switchPagination('Activation')(childParams)"
                :on-row-click="handleRowClick">
                <template v-slot:operate="{ item }">
                  <v-menu open-on-hover :v-model="selectOperate">
                    <template v-slot:activator="{ props }">
                      <v-icon icon="mdi-dots-horizontal" start v-bind="props" />
                    </template>
                    <v-list class="theme-list">
                      <v-list-item v-for="(operation, index) in operationList" :key="index" color="primary"
                        :ripple="true" @click="handleAction(operation, item)">
                        <v-list-item-title v-if="operation.label !== '冻结'" class="text-subtitle-1 font-weight-regular">
                          {{ operation.label }}
                        </v-list-item-title>
                        <v-list-item-title v-else class="text-subtitle-1 font-weight-regular">
                          {{ operation.label === '冻结' && item.status === 1 ? '冻结' : '解冻' }}
                        </v-list-item-title>
                      </v-list-item>
                    </v-list>
                  </v-menu>
                </template>
              </OperateTable>

JS部分

import { ref, reactive, onMounted, computed, provide, h } from 'vue';
/* 为了使用动态使用icon引入组件 */
import { VIcon } from 'vuetify/components';

/* 列表数据 */
const TableDataList: any = ref([])

/* 搜索查询 */
const getUserList = async (value: string) => {
  const data: any = {
    page: tablePage.value,
    size: itemsPerPage.value,
    ...searchFrom.value
  }
  const res: any = await getUserListApi(data)
  if (res.code !== 0) return
  TableDataList.value = res.data.list
  total.value = res.data.total
}

/* 动态DOM元素的事件 */
const handleRowClick = async (row: any, value: string) => {
  const params = {
    key: row[value]
  }
  const res: any = await decryptApi(params)
  if (res.code !== 0) return

};
/* 定义表头 */
const TableHeaderList = computed(() => {
  return ([
    {
      field: 'id',
      label: '卡ID',
      width: '60px',
      search: {
        component: 'Input',
      }
    },
    {
      field: 'card_no',
      label: '卡号',
      search: {
        component: 'Input',
      }
    },
    {
      field: 'cardholder',
      label: '持卡人',
      width: '130px',
      search: {
        hidden: true
      }
    },
    {
      field: 'expire',
      label: '有效期(MM/YY)',
      width: '130px',
      search: {
        hidden: true
      }
    },
    {
      field: 'safe_code',
      label: '安全码',
      width: '160px',
      search: {
        hidden: true
      },
      // Vue3特性--动态插入DOM元素
      render: (row: any) => h(VIcon, { icon: 'mdi-eye-outline', style: 'color: #939a9d; cursor: pointer;', onClick: () => handleRowClick(row, 'safe_code') })
    },
    {
      field: 'tag',
      label: '标签',
      search: {
        component: 'Input',
      }
    },
    {
      field: 'created_at',
      label: '建卡时间',
      width: '130px',
      search: {
        hidden: true
      }
    },
    {
      field: 'status',
      label: '卡状态',
      width: '130px',
      search: {
        component: 'Select',
        optionApi: () => {
          const res = [
            {
              value: 0,
              label: '冻结'
            },
            {
              value: 1,
              label: '激活'
            }
          ]
          return res
        }
      },
      formatter: (cellValue: number) => {
        let typeText: string | number = ''
        switch (cellValue) {
          case 0:
            typeText = '冻结'
            break
          case 1:
            typeText = '激活'
            break
          default:
            typeText = cellValue
            break
        }
        return typeText
      }
    },
    {
      field: 'balance',
      label: '可用余额',
      width: '130px',
      search: {
        hidden: true
      },
      formatter: (cellValue: any) => {
        const number = Math.floor(cellValue * 100 / 100)
        return number.toFixed(2)
      }
    },
    {
      field: 'operate',
      label: t("operate"),
      width: '50px',
      search: {
        hidden: true
      }
    }
  ])
});