222
schangxiang@126.com
2025-05-06 38b161e4d52362081bfe78fb5b51fbf384db7ce2
HIAWms/web/src/components/Table/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,680 @@
<template>
  <div
    ref="informationTableRef"
    :i="Language.triggerRenderData.i"
    :class="{
      'information-table': true,
      'information-table-drag': props.showDarg,
      'information-table-border': headBorder,
      [dragClass ? dragClass : '']: dragClass,
    }"
  >
    <div
      :style="{
        height: props.isFooter ? 'calc(100% - 42px)' : '100%',
      }"
    >
      <vxe-table
        v-bind="$attrs"
        show-header-overflow
        show-overflow
        width="100%"
        header-row-class-name="information-table-base-header"
        ref="tableRef"
        min-height="45px"
        :row-class-name="rowClassName"
        :row-style="rowStyle"
        :cell-style="cellStyle"
        :empty-text="globalT(emptyText)"
        :size="props.size || 'small'"
        :border="border || true"
        :height="currentHeight"
        :max-height="currentMaxHeight"
        :row-config="{
          isCurrent: true,
          isHover: true,
          keyField: props.id || 'id',
          ...props.rowConfig,
        }"
        :data="dataSource"
        :scroll-y="{ enabled: !!isVScroll, gt: 0 }"
        :scroll-x="{ enabled: !!isVScroll, gt: 0 }"
        :sort-config="{
          remote: isSort,
        }"
        @checkbox-all="selectChangeEvent"
        @checkbox-change="selectChangeEvent"
        @sort-change="onSortChange"
        @current-change="clickRowChange"
      >
        <!-- è‡ªå®šä¹‰åºå·ï¼Œæ‹–拽,选择 -->
        <CustomVxeColumn
          v-bind="props"
          v-model:dataSource="dataSource"
          v-model:contextMenuConfig="contextMenuConfig"
          :id="props.id || 'id'"
          ref="vxeColumnRef"
          @check="onCheck"
          @change="onRadioChange"
          :LanguageScopeKey="props.LanguageScopeKey"
        >
          <template #[name]="data" v-for="name in Object.keys(slots)">
            <slot :name="name" v-bind="data"></slot>
          </template>
        </CustomVxeColumn>
      </vxe-table>
    </div>
    <div @click="onClickFooter" v-if="isFooter" class="information-table-foot">
      <!-- <img src="@/assets/images/+.png" style="width: 12px" /> -->
      <Icon icon="+" :width="12"></Icon>
    </div>
    <paginAtion
      v-bind="props"
      :params="params"
      :totalCount="totalCount"
      :pageSize="props.MaxResultCount || props.pageSize || 50"
      @change="onChange"
      @currentChange="onCurrentChange"
      v-model:pageNum="pageNum"
      :tableRef="tableRef"
    />
    <context-menu
      v-if="contextMenu?.length > 0"
      v-model:show="contextMenuConfig.show"
      :options="contextMenuConfig.options"
    >
      <template v-for="(item, index) in contextMenu" :key="index">
        <context-menu-item
          :label="item.label"
          @click="onHandleMenuItem(item)"
          :disabled="!!contextDisabled(item)"
          :style="{
            filter: contextDisabled(item) ? 'opacity(0.4)' : 'none',
          }"
        >
          <div
            :style="{
              cursor: !contextDisabled(item) ? 'pointer' : 'not-allowed',
            }"
            class="table-context-menu-item-c"
          >
            <div style="width: 16px; margin-right: 7px">
              <Icon :height="16" class="icon-box" :icon="item.icon" />
            </div>
            <div class="label-c">{{ item.label }}</div>
          </div>
        </context-menu-item>
      </template>
    </context-menu>
  </div>
</template>
<script lang="ts" setup>
// @ts-nocheck
import {
  computed,
  nextTick,
  ref,
  useSlots,
  onMounted,
  onUnmounted,
  reactive,
  watch,
  provide,
  defineProps,
  inject,
  useAttrs,
} from 'vue'
import Icon from '../Icon/Icon'
import { ContextMenu, ContextMenuItem } from '@/components/vue3-context-menu'
import Sortable from 'sortablejs'
import isBoolean from 'lodash/isBoolean'
import isNil from 'lodash/isNil'
import { getListData, adjustSort } from './api'
import type {
  ParamsItem,
  TablePropsItemType,
  contextMenuItemType,
} from './index.d'
import CustomVxeColumn from './components/custom-vxe-column.vue'
import paginAtion from './components/pagination.vue'
import { debounce, isFunction } from 'lodash'
import VxeTable from 'vxe-table/es/table/index'
import { getScopeT, Language, _t as globalT } from '@/libs/Language/Language'
const props = defineProps<TablePropsItemType>()
const LanguageScopeKey =
  props.LanguageScopeKey || inject('LanguageScopeKey', '')
const _t = LanguageScopeKey ? getScopeT(LanguageScopeKey) : globalT
const attrs = useAttrs()
const emit = defineEmits([
  'drag',
  'check',
  'sort',
  'page',
  'rowClick',
  'update:dataSource',
  'clickFooter',
  'update',
  'load',
  'beforeLoad',
  'reload',
  'update:total',
  'change',
])
const waitEventFns = ref<any>([])
const baseParams = reactive<any>({
  SkipCount: 0,
  MaxResultCount: props.pageSize || 50,
})
const vxeColumnRef = ref()
const rowClassName = (...arg: any) => {
  const name = 'information-table-base-row'
  const className = props.rowClassName
  if (className) {
    if (isFunction(className)) {
      return `${name} ${className(...arg) || ''}`
    }
    return `${name} ${className || ''}`
  } else {
    return `${name} able`
  }
}
const params = computed<any>(() => {
  const p = props.params || {}
  Object.assign(baseParams, p)
  for (let key in baseParams) {
    if (baseParams[key] === '') {
      delete baseParams[key]
    }
  }
  return baseParams
})
const emptyText = computed(() => {
  return props.emptyText || '暂无数据'
})
const contextMenuConfig = ref<contextMenuItemType>({
  show: false,
  current: null,
  options: {
    zIndex: 2000,
    minWidth: 132,
    x: 0,
    y: 0,
  },
})
const informationTableRef = ref(null)
const pageNum = ref(1)
const tableRef = ref()
const slots = useSlots()
const total = ref(props.total || 0)
let dropR: any = null
provide('tableRef', tableRef)
const totalCount = computed({
  get() {
    if (!isNil(props.total)) {
      return props.total
    } else {
      return total.value
    }
  },
  set(v) {
    if (!isNil(props.total)) {
      emit('update:total', v)
    } else {
      total.value = v
    }
  },
})
const contextDisabled: any = computed(() => {
  return (item: any) => {
    if (typeof item.disabled === 'function') {
      return item.disabled(contextMenuConfig.value.current)
    }
    if (item.disabled !== undefined) {
      if (!isNil(item.disabled?.value)) {
        return item.disabled?.value
      } else {
        return item.disabled
      }
    }
    return false
  }
})
const dragClass = computed(() => {
  if (isBoolean(props.showDarg)) return false
  return props.showDarg
})
const dataSource = computed({
  get() {
    return props.dataSource
  },
  set(v) {
    if (v) {
      emit('update:dataSource', v)
    }
  },
})
const currentHeight = computed(() => {
  if (props.height === 'auto') {
    return ''
  }
  return props.maxHeight ? '' : '100%'
})
const currentMaxHeight = computed(() => {
  if (props.height === 'auto') {
    return ''
  }
  if (props.isFooter) {
    if (props.maxHeight) {
      const h = parseInt(props.maxHeight) - 42
      return h > 0 ? h : '100%'
    }
    return '100%'
  } else {
    return props.maxHeight ? props.maxHeight : '100%'
  }
})
/**
 * èœå•
 * @param item
 */
const onHandleMenuItem = (item: any) => {
  item.fn && item.fn(contextMenuConfig.value.current, pageNum.value)
}
/**
 * ç‚¹å‡»åº•部添加
 */
const onClickFooter = () => {
  emit('clickFooter')
}
const onChange = () => {
  onCurrentChange(Number(pageNum.value))
}
/**
 * å•选选中
 * @param row
 */
const onRadioChange = (row: any) => {
  emit('change', row)
}
const onCheck = (
  records: Record<string, any>[],
  selectionMap: Record<string, boolean>
) => {
  emit('check', records, selectionMap)
}
const selectChangeEvent = (records: any[]) => {
  // console.log(records, 'records')
}
/**
 * è®¾ç½®é€‰ä¸­
 * @param keys
 * @param checked
 */
const setSelectRow = (keys: any[], checked = true) => {
  const $table = tableRef.value
  if ($table) {
    const fn = () => {
      const rows: any[] = []
      dataSource.value.forEach((item: any) => {
        if (keys.includes(item[props?.id || 'id'])) {
          rows.push(item)
        }
      })
      vxeColumnRef.value.setCheckboxRow(rows, checked)
      emit('check', rows)
    }
    waitEventFns.value.push(fn)
  }
}
/**
 * rows选中
 * @param rows
 * @param checked
 */
const setSelectRowByObj = (rows: any[], checked = true) => {
  if (vxeColumnRef) {
    const fn = () => {
      vxeColumnRef.value.setCheckboxRow(rows, checked)
    }
    waitEventFns.value.push(fn)
  }
}
/**
 * è®¾ç½®é€‰ä¸­
 * @param id
 */
const setRadioRowKey = (id) => {
  vxeColumnRef.value.setRadioRowKey(id)
}
/**
 * è®¾ç½®é€‰ä¸­
 * @param row
 */
const setRadioRow = (row) => {
  vxeColumnRef.value.setRadioRow(row)
}
/**
 * è®¾ç½®å…¨é€‰é€‰ä¸­çŠ¶æ€
 * @param checked é€‰ä¸­çŠ¶æ€
 */
const setAllCheckboxRow = (checked = false) => {
  const $table = tableRef.value
  if ($table) {
    waitEventFns.value.push(() =>
      vxeColumnRef.value.selectChangeAllEvent(checked)
    )
  }
}
/**
 * è®¾ç½®å•行高亮
 * @param key
 */
const setCurrentRow = (key: string) => {
  const $table = tableRef.value
  if ($table) {
    const fn = () => {
      const row = dataSource.value.find((item: any) => {
        return key === item[props?.id || 'id']
      })
      if (row) {
        tableRef.value.setCurrentRow(row)
      }
    }
    waitEventFns.value.push(fn)
  }
}
/**
 * æ¸…除选中
 */
const clearSelectEvent = () => {
  const $vxeColumnRef = vxeColumnRef.value
  if ($vxeColumnRef) {
    $vxeColumnRef.clearSelection()
  }
}
const onSortChange = (row: any) => {
  const column = props.columns.find((item) => item.field === row.field)
  if (column) {
    if (row.order === 'asc') {
      params.value[column.sortKey] = true
    } else if (row.order === 'desc') {
      params.value[column.sortKey] = false
    } else {
      delete params.value[column.sortKey]
    }
    getTableList()
  }
  emit('sort', row)
}
/**
 * è®¾ç½®å•选
 * @param row
 */
const setRow = (row: any) => {
  const $table = tableRef.value
  if ($table) {
    const fn = () => {
      tableRef.value.setCurrentRow(row)
    }
    waitEventFns.value.push(fn)
  }
}
const clickRowChange = (tableData: Record<string, any>[]) => {
  const $table = tableRef.value
  if ($table) {
    emit('rowClick', tableData)
  }
}
const onCurrentChange = async (current: number) => {
  pageNum.value = current
  // @ts-ignore
  params.value.SkipCount = (current - 1) * params.value.MaxResultCount
  if (props.url) {
    await getTableList()
  }
  emit('page', current)
}
const sortableInit = () => {
  const moveDom = tableRef.value?.$el?.querySelector(
    `.body--wrapper>.vxe-table--body tbody`
  ) as HTMLElement
  if (moveDom && Sortable) {
    dropR = new Sortable(moveDom, {
      handle: '.drag-move',
      chosenClass: 'sortable-chosen',
      swapThreshold: 1,
      draggable: '.able',
      animation: 150,
      onEnd: (sortableEvent: any) => {
        const newIndex = sortableEvent.newIndex as number
        const oldIndex = sortableEvent.oldIndex as number
        const data = [...dataSource.value]
        const currRow = data.splice(oldIndex, 1)[0]
        data.splice(newIndex, 0, currRow)
        dataSource.value = []
        nextTick(async () => {
          dataSource.value = data
          if (props.sortUrlTpl) {
            const sortData = {
              id: currRow.id,
              sort:
                newIndex + 1 + (pageNum.value - 1) * baseParams.MaxResultCount,
            }
            await adjustSort(sortData, props.sortUrlTpl)
          }
          emit('drag', newIndex, oldIndex, currRow)
        })
      },
    })
  }
}
const getTableList = () => {
  return new Promise(async (r: any) => {
    const res = await getListData(params.value, props.url || '')
    if (props.dataTransformer && isFunction(props.dataTransformer)) {
      dataSource.value =
        props.dataTransformer(res?.items || res) || res?.items || res
    } else {
      dataSource.value = res?.items || res
    }
    totalCount.value = res?.totalCount || 0
    r()
    emit('reload')
  })
}
const getList = (formData?: Record<string, any>) => {
  return new Promise((resolve) => {
    if (formData) {
      Object.assign(params.value, formData)
    }
    nextTick(async () => {
      await getTableList()
      clearAll()
      resolve(dataSource.value)
    })
  })
}
/**
 * èŽ·å–åˆ—è¡¨æäº¤å‚æ•°
 */
const getParams = () => {
  return params.value
}
/**
 * èŽ·å–åˆ†é¡µå‚æ•°
 */
const getPaginationParams = () => {
  return {
    pageNum: pageNum.value,
    pageSize: params.value.MaxResultCount,
    totalCount: totalCount.value,
  }
}
/**
 * é˜Ÿåˆ—处理事件函数
 */
const handleEventFns = () => {
  if (!waitEventFns.value.length || !dataSource.value.length) return
  nextTick(() => {
    let fn: () => void = waitEventFns.value.pop()
    while (fn) {
      fn()
      fn = waitEventFns.value.pop()
    }
  })
}
/**
 * è·³è½¬
 */
const scrollToRowLine = () => {
  const t = setTimeout(() => {
    tableRef.value?.scrollToRow(dataSource.value[dataSource.value.length - 1])
    clearTimeout(t)
  })
}
/**
 * è·³è½¬
 */
const skipRow = (row: any) => {
  const t = setTimeout(() => {
    tableRef.value?.scrollToRow(row)
    tableRef.value.setCurrentRow(row)
    clearTimeout(t)
  })
}
/**
 * æ»šåŠ¨å¹¶å®šä½åˆ°é‚£ä¸€è¡Œï¼Œå¦‚æœ‰åˆ†é¡µï¼Œéœ€è¦è·³è½¬åˆ°åˆ†é¡µ
 */
const scrollToRow = async ({ row, skip }: any) => {
  if (skip) {
    if (totalCount.value > dataSource.value.length) {
      pageNum.value = Math.ceil(totalCount.value / params.value.MaxResultCount)
      params.value.SkipCount = (pageNum.value - 1) * params.value.MaxResultCount
    }
    await getTableList()
    skipRow(dataSource.value[dataSource.value.length - 1])
  } else {
    skipRow(row)
  }
}
const clearAll = () => {
  tableRef.value?.clearAll()
  vxeColumnRef.value?.clearSelection()
}
/**
 * æ¸…除缓存
 * æ‰‹åŠ¨æ¸…é™¤è¡¨æ ¼æ‰€æœ‰æ¡ä»¶ï¼Œè¿˜åŽŸåˆ°åˆå§‹çŠ¶æ€
 */
const resetTable = () => {
  tableRef.value?.clearAll()
}
Language.useChange((lang: typeof Language) => {
  if (props.url) {
    getList()
  }
})
onUnmounted(() => {
  dropR?.destroy()
})
onMounted(async () => {
  emit('beforeLoad')
  if (props.url) {
    await getTableList()
  }
  if (props.selections?.length && dataSource.value.length) {
    setSelectRow(props.selections)
  }
  sortableInit()
  // è¡¨æ ¼æ•°æ®åŠ è½½å®Œæˆçš„å›žè°ƒ
  emit('load')
})
watch(dataSource, () => {
  if (Array.isArray(dataSource.value)) {
    tableRef.value?.reloadData(dataSource.value)
    if (props.autoFirstClickRow) {
      if (dataSource.value.length) {
        const row = dataSource.value[0]
        setCurrentRow(row[props?.id || 'id'])
        emit('rowClick', { row })
      }
    }
    nextTick(handleEventFns)
  }
})
watch(() => waitEventFns.value.length, debounce(handleEventFns, 100))
const exposeMap = {
  resetTable,
  clearAll,
  setCurrentRow,
  setSelectRow,
  setAllCheckboxRow,
  setRow,
  clearSelectEvent,
  getList,
  getTableList,
  getParams,
  getPaginationParams,
  setSelectRowByObj,
  scrollToRow,
  scrollToRowLine,
  handleEventFns,
  setRadioRowKey,
  setRadioRow,
}
defineExpose(exposeMap)
emit('update', exposeMap)
</script>
<style lang="scss">
@import url('./index.scss');
</style>
<style lang="scss" scoped>
@import url('./index.module.scss');
</style>