Bootstrap

记录:vue3+ts+antdvue实际开发中封装的业务hooks

  1. 1.useForm
import { ref, nextTick } from 'vue'
import type { ValidateErrorEntity } from 'ant-design-vue/es/form/interface'
import type { antFormType } from '@/type/interface/antd'
import useErrorMessage from './useErrorMessage'

export default function useForm<F = any>() {
  const { alertError } = useErrorMessage()
  /**表单model */
  const formState = ref<Partial<F>>({})
  /**表单校验提示 */
  const validateMessages = { required: '${label}不能为空' }
  /**表单对象ref */
  const formRef = ref<antFormType>()

  /**
   * @method 设置表单数据
   * @param data 需要设置的数据
   * @returns  void
   */
  function setFormStateData(data: Record<string, any>) {
    Object.assign(formState.value, data)
  }

  /**
   * @method 表单校验
   * @returns  Promise<Record<string, any> | ValidateErrorEntity>
   */
  async function formValidateFields(): Promise<Record<string, any> | ValidateErrorEntity> {
    return new Promise<Record<string, any> | ValidateErrorEntity>(async (resolve, reject) => {
      try {
        await formRef.value?.validateFields()
        resolve(formState.value)
      } catch (error: any) {
        alertError(error)
        reject(error)
      }
    })
  }

  /**
   * @method 移除表单项的校验结果。传入待移除的表单项的 name 属性或者 name 组成的数组,如不传则移除整个表单的校验结果
   * @param nameList 表单对应的name字段组成的数组
   * @returns void
   */
  function clearValidate(nameList?: string | (string | number)[]) {
    if (!nameList) {
      nextTick(() => {
        formRef.value?.clearValidate()
        return
      })
    }
    if (nameList?.length) {
      if (!Array.isArray(nameList)) {
        throw new Error('移除表单校验的name必须为一个数组')
      } else {
        formRef.value?.clearValidate(nameList)
      }
    }
  }

  /**
   * @method 对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
   * @param nameList 表单对应的name字段组成的数组
   * @returns void
   */
  function resetFields(nameList?: string | (string | number)[]) {
    if (!nameList) {
      formRef.value?.resetFields()
      return
    }
    if (nameList?.length) {
      if (!Array.isArray(nameList)) {
        throw new Error('重置的name必须为一个数组')
      } else {
        formRef.value?.resetFields(nameList)
      }
    }
  }

  return {
    formRef,
    formState,
    resetFields,
    clearValidate,
    validateMessages,
    setFormStateData,
    formValidateFields,
  }
}



// 实际使用
<a-form :model="formState" ref="formRef" autocomplete="off" layout="vertical" :validate-messages="validateMessages">
</a-form>

import useForm from '@/hooks/useForm'
import type { receiveType } from '../config'

const { formRef, formState, setFormStateData, formValidateFields } = useForm<receiveType>()

  1. 2.useTable
import { ref, onActivated, onMounted, Ref } from 'vue'
import type { contentTableType, contentSearchType, templateContentType } from '@/type/interface/antd'
import useGlobal, { type interContentHeader, type interContentTable, type axiosResponse } from './useGlobal'

/**
 * @method 生成search和table
 * @param contentHeaderParam 搜索栏配置项
 * @param contentTableParam 表格配置项
 * @param queryParams 分页查询条件
 * @param callback 分页查询请求回调
 * @param handleExtraCb 处理分页数据的回调,请求到分页数据后对分页数据进行一些处理
 */

export default function useTable<D extends object = Record<string, any>, Q extends object = Record<string, any>>(contentHeaderParam: interContentHeader, contentTableParam: interContentTable<D>, queryParams: Q, callback: (...args: Q[]) => Promise<axiosResponse<D[]>>, handleExtraCb?: (args: D[]) => void) {
  const { proxy, getTopMenu } = useGlobal()

  const templateContentDom = ref<templateContentType>()
  const contentTableDom = ref<contentTableType>()
  const contentSearchDom = ref<contentSearchType>()

  /**选中的数据 */
  const selectTableData: Ref<D[]> = ref([])

  onActivated(() => {
    setHttpTableData()
    contentHeaderHeightHandle() // 渲染完(改变contentSearch高度的任何操作都需要加上)
  })

  onMounted(async () => {
    if (!getTopMenu.value) {
      // 仓库级隐藏仓库搜索
      const excludedOptions = ['warehouseId', 'warehouseNameOrCode', 'warehouseCodeOrName', 'departmentCode']
      contentHeaderParam.formOptions = contentHeaderParam.formOptions?.filter((v) => !excludedOptions.includes(v.name))
    }
    await contentSearchDom.value?.initFormState(contentHeaderParam)
    await contentTableDom.value?.initTable(contentTableParam)
    contentHeaderHeightHandle() // 渲染完(改变contentSearch高度的任何操作都需要加上)
    setHttpTableData()
  })

  /**
   * @method 设置查询条件
   * @param queryInfo 查询条件
   */
  function setQueryInfo(queryInfo: Q) {
    queryParams = queryInfo
  }

  /**
   * @method table数据请求
   */
  async function setHttpTableData<T = any>(arg?: T) {
    contentTableParam.loading = true
    const params = {
      pageNum: contentTableParam.pagination?.current,
      pageSize: contentTableParam.pagination?.pageSize,
      ...queryParams,
      ...arg,
    }

    try {
      const { Tag, TotalRecord, ResultCode } = await callback(params)
      if (ResultCode === 200) {
        handleExtraCb?.(Tag)
        await contentTableDom.value?.setHttpTable('dataSource', Tag, TotalRecord)
      }
      contentTableParam.loading = false
    } catch (error) {
      console.log('error', error)
      contentTableParam.loading = false
    } finally {
      contentTableParam.loading = false
    }
  }

  /**
   * @method 搜索/重置
   */
  function contentHeaderHandle(type: string, data: any) {
    Object.assign(queryParams, data)
    contentTableParam.pagination!.current = 1
    contentTableParam.selectedRowKeys = []
    contentTableDom.value?.setHttpTable('selectedRowKeys', [])

    setHttpTableData()
  }

  /**
   * @method 分页改变
   */
  function paginationHandle(page: { current: number; pageSize: number }) {
    contentTableParam.pagination!.current = page.current
    contentTableParam.pagination!.pageSize = page.pageSize
    setHttpTableData()
  }

  /**
   * @method 表格选中
   * @param keys 选中的rowKeys
   */
  function rowSelectionHandle(keys: string[], data: D[]) {
    contentTableParam.selectedRowKeys = keys
    selectTableData.value = data
  }

  /**
   * @method 接受ContentHeader高度改变事件,并改变ContentTable高度
   */
  function contentHeaderHeightHandle() {
    templateContentDom.value?.getContentHeaderHeight()
  }

  /**
   * @method 手动请求Table
   */
  async function handleUpdateTable() {
    await contentTableDom.value?.initTable(contentTableParam)
    setHttpTableData()
  }

  /**
   * @method 启用/禁用
   * @param type on:启用 off:禁用
   * @param callBack 启用/禁用 请求回调 参数为ids和当前state状态
   */
  const handleEnable = proxy!.$_l.debounce(async (type: 'on' | 'off', callBack: (params: { ids: (string | number)[]; state: string | number }) => Promise<axiosResponse<boolean>>) => {
    if (!contentTableParam.selectedRowKeys!.length) {
      proxy!.$message.error('请选择要操作的数据')
      return
    }
    const params = { ids: contentTableParam.selectedRowKeys!, state: type === 'on' ? '1' : '0' }
    try {
      const { Success } = await callBack(params)
      if (Success) {
        contentTableParam.selectedRowKeys = []
        proxy!.$message.success(`${type === 'on' ? '启用' : '禁用'}成功`)
        contentTableDom.value?.setHttpTable('selectedRowKeys', [])
        setHttpTableData()
      }
    } catch (error) {
      console.log('error', error)
    }
  }, 500)

  return {
    contentTableDom,
    contentSearchDom,
    templateContentDom,

    selectTableData,

    handleEnable,
    handleUpdateTable,
    setHttpTableData,
    paginationHandle,
    contentHeaderHandle,
    rowSelectionHandle,
    contentHeaderHeightHandle,
    setQueryInfo,
  }
}

//实际使用
import useTable from '@/hooks/useTable'
import { wavesRuleQueryPage } from '@/api/module/wavesPlanRuleList_api'
const contentHeaderParam = reactive({
  colSpan: 6, // 4 | 6 | 8 | 12;
  isSearch: true,
  isReset: true,
  formOptions: [
    {
      type: 'input',
      name: 'code',
      defaultVlue: '',
      value: '',
      label: '波次规则',
      labelWidth: '80',
      placeholder: '请输入波次规则代码/名称',
      disabled: false,
    },
    {
      type: 'select',
      name: 'cargoOwnerCode',
      defaultVlue: null,
      value: null,
      label: '货主',
      labelWidth: '80',
      placeholder: '请选择货主',
      disabled: false,
      childrenMap: [],
      fieldNames: { label: 'nameAdCode', value: 'code' },
      filterOption: (input: string, option: any) => option.nameAdCode.toLowerCase().indexOf(input.toLowerCase()) >= 0,
    },
    {
      type: 'select',
      name: 'type',
      defaultVlue: null,
      value: null,
      label: '波次类型',
      labelWidth: '80',
      placeholder: '请选择波次类型',
      size: 'default',
      childrenMap: [],
      fieldNames: { label: 'name', value: 'code' },
      filterOption: (input: string, option: any) => option.name.toLowerCase().indexOf(input.toLowerCase()) >= 0,
    },
    {
      type: 'input',
      name: 'remark',
      defaultVlue: '',
      value: '',
      label: '描述',
      labelWidth: '80',
      placeholder: '请输入描述',
      disabled: false,
    },
    {
      type: 'select',
      name: 'state',
      defaultVlue: 1,
      value: '',
      label: '状态',
      labelWidth: '80',
      placeholder: '请选择状态',
      size: 'default',
      childrenMap: [
        { value: '', name: '全部' },
        { value: 1, name: '启用' },
        { value: 0, name: '禁用' },
      ],
    },
  ],
})

const contentTableParam = reactive({
  isOper: true,
  loading: false, // loading
  isCalcHeight: true, // 是否自动计算table高度
  rowSelection: true, // 选择框
  tableConfig: true, // 选择框
  name: 'WAVE_PLAN_RULE_LIST_MAIN',
  rowKey: 'id',
  selectedRowKeys: [] as string[],
  pagination: {
    // 不需要分页可直接删除整个对象
    pageSize: 20,
    total: 0,
    current: 1,
  },
  columns: [
    { title: '规则代码', key: 'code', dataIndex: 'code', ellipsis: true, resizable: true, width: 120, align: 'center' },
    { title: '规则名称', dataIndex: 'name', ellipsis: true, resizable: true, width: 120, align: 'center' },
    { title: '仓库', key: 'warehouse', dataIndex: 'warehouseCode', ellipsis: true, resizable: true, width: 180, align: 'center' },
    { title: '货主', key: 'cargoOwner', dataIndex: 'cargoOwner', ellipsis: true, resizable: true, width: 300, align: 'center' },
    { title: '是否启用', key: 'stateName', dataIndex: 'stateName', ellipsis: true, resizable: true, width: 150, align: 'center' },
    { title: '波次类型', key: 'typeName', dataIndex: 'typeName', ellipsis: true, resizable: true, width: 150, align: 'center' },
    { title: '波次订单总数限制', key: 'wavesOrderNumber', dataIndex: 'wavesOrderNumberMax', ellipsis: true, resizable: true, width: 220, align: 'center' },
    { title: '波次SKU总数限制', key: 'wavesSkuNumber', dataIndex: 'wavesSkuNumberMax', ellipsis: true, resizable: true, width: 220, align: 'center' },
    { title: '订单商品件数限制', key: 'orderGoodsNumberPieces', dataIndex: 'orderGoodsNumberPiecesMax', ellipsis: true, resizable: true, width: 220, align: 'center' },
    { title: '波次商品总件数限制', key: 'wavesGoodsNumberPieces', dataIndex: 'wavesGoodsNumberPiecesMax', ellipsis: true, resizable: true, width: 220, align: 'center' },
    { title: '操作', key: 'operation', fixed: 'right', width: 120, align: 'center' },
  ],
  dataSource: [],
})
const { contentSearchDom, contentTableDom, templateContentDom, setHttpTableData, rowSelectionHandle, paginationHandle, contentHeaderHandle, contentHeaderHeightHandle } = useTable(contentHeaderParam, contentTableParam, queryInfo, wavesRuleQueryPage)
  1. 3.usePrint
import { ref } from 'vue'
import type { axiosResponse } from '@/type/interface'
import type { IPrintTemplateType } from '@/type/interface/goDownPlan'
import type { contentPrintType } from '@/type/interface/antd'

import useSpanLoading from './useSpanLoading'
import useGlobal from './useGlobal'

/**
 * @method 打印hooks
 */
export default function usePrint() {
  const { isPending: printLoading, changePending } = useSpanLoading()
  /**下拉框选中值,用来指定templateId */
  const print = ref(null)

  /**初始不加载子组件的print组件,否则会影响父组件的打印组件实例,导致打印空白 */
  const isShowPrint = ref(false)

  /**打印Ref绑定dom */
  const printDom = ref<contentPrintType>()

  /**打印下拉选项 */
  const printOptions = ref<IPrintTemplateType[]>([])

  /**后端返回的html数组 */
  const htmlArrays = ref<string[]>([])

  const { proxy } = useGlobal()

  /**
   * @method 打印模版
   * @param type 打印模版选项
   * @returns Promise<void>
   */
  async function initPrintOptions(type: string) {
    const { Success, Tag } = await proxy?.$api.goDownPlanList_api.printTemplateGetTemplateList({ type })
    if (Success) {
      printOptions.value = Tag
    }
  }

  /**
   * @method 下拉选择打印
   * @param cb 请求回调函数
   * @param params 请求参数
   * @returns Promise<void>
   */
  const handlePrint = async (cb: (arg: Record<string, any>) => Promise<axiosResponse<string[]>>, params: Record<string, any>) => {
    if (!print.value) {
      proxy?.$message.error('请先选择打印模板')
      return
    }
    changePending(true)
    printLoading.value = true
    try {
      const { Success, Tag } = await cb(params)
      if (Success) {
        changePending(false)
        htmlArrays.value = Tag
        printDom.value?.toPrint()
      }
    } catch (error) {
      console.log('error', error)
      changePending(false)
    } finally {
      changePending(false)
    }
  }

  /**
   * @method 托盘单条/多条打印
   * @param cb 打印请求回调
   * @param params 请求参数
   * @returns Promise<void>
   */
  const labelPrint = proxy!.$_l.debounce(async (cb: (arg: Record<string, any>) => Promise<axiosResponse<string[]>>, params: Record<string, any>) => {
    changePending(true)
    try {
      const { Success, Tag } = await cb(params)
      if (Success) {
        changePending(false)
        htmlArrays.value = Tag
        printDom.value?.toPrint()
      }
    } catch (error) {
      console.log('error', error)
      changePending(false)
    } finally {
      changePending(false)
    }
  }, 500)

  /**
   * @method 打印dialog弹出或关闭
   * @param cb 打印弹窗关闭后的回调
   */
  function printDialogChange(cb?: (...args: any[]) => any) {
    print.value = null
    cb?.()
  }

  return {
    print,
    printDom,
    labelPrint,
    htmlArrays,
    isShowPrint,
    handlePrint,
    printOptions,
    printLoading,
    initPrintOptions,
    printDialogChange,
  }
}


// 实际使用
import usePrint from '@/hooks/usePrint'
const { htmlArrays, handlePrint, initPrintOptions, printDialogChange, printOptions, print, printDom } = usePrint()
const idList = ref<string[]>([]) // 打印所需id集合

const selectChange = (value: any, option: any, type: 'print') => {
  /**
   * @method 下拉框change
   */
  if (!idList.value.length) {
    globalProperties.$message.error('请选择需要操作的数据')
    print.value = null
    return
  }
  const params = {
    idList: idList.value,
    templateId: print.value,
  }
  switch (type) {
    case 'print':
      if (print.value) {
        handlePrint(stockTransferPrint, params)
      }
      break
    default:
      break
  }
}
  1. 4.useErrorMessage
import type { ValidateErrorEntity } from 'ant-design-vue/es/form/interface'
import useGlobal from './useGlobal'

/**
 * @method 表单必填项校验全局弹窗提示hooks
 */

export default function useErrorMessage() {
  const { proxy } = useGlobal()

  /**
   * @method 表单必填项校验失败时使用error提示必填
   * @param errorArray 必填字段与name等
   */
  function alertError(errorArray: ValidateErrorEntity) {
    const { errorFields } = errorArray
    for (const item of errorFields) {
      if (item?.errors?.length) {
        for (const v of item.errors) {
          proxy?.$message.error(v)
          // 此处加return是为了按顺序提示
          return
        }
      }
    }
  }

  return {
    alertError,
  }
}



// 实际使用
import useErrorMessage from '@/hooks/useErrorMessage'
const { alertError } = useErrorMessage()
/**
 * @method 保存新增
 */
async function handleSave() {
  try {
    let formState = await wavesRuleFromRef.value.formValidateFields()
    const params = {
      code: formState.code,
      name: formState.name,
      remark: formState.remark,
      type: formState.type,
      warehouseId: activeWareHouse.value.warehouseId,
      warehouseCode: activeWareHouse.value.warehouseCode,
      warehouseName: activeWareHouse.value.warehouseName,
      detail: formState,
    }
    const { Success } = await globalProperties.$api.wavesPlanRuleList_api.wavesRuleAdd(params)
    if (Success) {
      globalProperties.$message.success('新增成功')
      router.push({ name: 'wavesPlanRuleList' })
    }
  } catch (error: any) {
    alertError(error)
  }
}
  1. 5.useDrawer

/**
 * @method 使用抽屉的hooks
 * @returns { * }
 */
export default function useDrawer(): any {
  /**当前活跃key */
  const activeKey = ref<string>('1') //
  /**抽屉配置 */
  const drawerConfig = reactive({
    data: {
      visible: false,
      title: '',
      placement: 'right',
      width: 1500,
      footer: true,
    },
  })

  /**
   * @method 设置抽屉配置
   * @param config 抽屉配置项
   */
  function setDrawerConfig(config: Record<string, any>) {
    Object.assign(drawerConfig.data, config)
  }

  /**
   * @method 关闭抽屉
   * @param type
   * @param e
   */
  function drawerCloseHandle(type: 'after' | 'close', e: any) {
    if (!e) {
      activeKey.value = '1'
    }
  }

  /**
   * @method 打开抽屉
   */
  function open() {
    drawerConfig.data.visible = true
  }

  return {
    activeKey,
    drawerConfig,
    setDrawerConfig,
    open,
    drawerCloseHandle,
  }
}

// 实际使用
  <TemplateDrawer :drawerConfig="drawerConfig" @drawerCloseHandle="drawerCloseHandle">
    <template #drawerContent>
      <a-tabs v-model:activeKey="activeKey" size="large">
        <a-tab-pane key="1" tab="主信息" force-render>
          <wavesPlanRuleForm ref="baseFormRef" />
        </a-tab-pane>
      </a-tabs>
    </template>
    <template #footer>
      <a-button type="primary" v-show="recordParams.type === 'edit'" @click="handleOpera('save')"> 保存</a-button>
      <a-button type="primary" v-show="recordParams.type === 'see'" @click="handleOpera('edit')"> 编辑</a-button>
    </template>
  </TemplateDrawer>

import useDrawer from '@/hooks/useDrawer'
const { activeKey, drawerConfig, setDrawerConfig, open, drawerCloseHandle } = useDrawer()
  1. 6.useAutoAllot
import { ref } from 'vue'
import type { baseOutBoundType, batchType, locationType, trayType } from '@/type/interface/outBound'

import useGlobal from './useGlobal'
/**
 * @method 自动分配出库hooks
 */
export default function useAutoAllot() {
  /**分配库存---在指定分配-点击分配后改为true */
  const showAssignInventory = ref<boolean>(false)
  /**分配库存右侧表格---在指定分配-点击左侧表格行后改为true */
  const showAssignInventoryRight = ref<boolean>(false)
  /**分配库存  =>分配的索引 =>用于分配完成更新行状态 */
  const allotIdx = ref<number>(0)
  /**分配库存 => 点击库位对应的索引 */
  const allotLocationIdx = ref<number>(0)
  const { proxy } = useGlobal()

  /**
   * @method 填写分配件数后自动分配库位件数和托盘件数
   * @param record 当前行数据
   */
  function autoAllotLocation(record: batchType, field = 'planNumberPieces') {
    if (!record?.stockList?.length) return
    /**分配件数(剩余件数) */
    let allotNums = record[field]
    //库位数据
    for (const item of record?.stockList) {
      // 如果剩余件数小于每一项最大件数,
      if (allotNums < item.availableNumberPieces) {
        item.planNumberPieces = allotNums
        allotNums = 0
      } else {
        // 每一条的分配数量 = 最大件数
        item.planNumberPieces = item.availableNumberPieces
        // 左侧分配件数  = 左侧分配件数 - 每一条的分配数量
        allotNums = allotNums - item.planNumberPieces
      }
      autoAllotTray(item)
      calcPlanBoxNums(item)
    }
  }

  /**
   * @method 给当前行自动分配(库位->托盘)
   * @params record 当前行数据
   */
  function autoAllotTray(record: locationType) {
    if (!record.containerList?.length) return
    // 左侧分配件数(剩余件数)
    let allotNums = record.planNumberPieces
    // 右侧托盘数据
    for (const item of record?.containerList) {
      // 如果剩余件数小于每一项最大件数,
      if (allotNums < item.availableNumberPieces) {
        item.planNumberPieces = allotNums
        allotNums = 0
      } else {
        // 每一条的分配数量 = 最大件数
        item.planNumberPieces = item.availableNumberPieces
        // 左侧分配件数  = 左侧分配件数 - 每一条的分配数量
        allotNums = allotNums - item.planNumberPieces
      }
      // 计算整箱数和零箱件数,如果包装单位是箱 需要回显整箱数和零箱件数
      calcPlanBoxNums(item)
    }
  }

  /**
   * @method 根据托盘计算库位的总计划件数和批次的总数
   * @param batchArr 批次数据
   */
  function calcTotalLocation(batchArr: batchType[]) {
    batchArr[allotIdx.value].stockList[allotLocationIdx.value].planNumberPieces = batchArr[allotIdx.value].stockList[allotLocationIdx.value]?.containerList
      ?.map((v: { planNumberPieces: number }) => v.planNumberPieces)
      ?.reduce((prev: number, curr: number): number => {
        return prev + curr
      }, 0)
    calcPlanBoxNums(batchArr[allotIdx.value].stockList[allotLocationIdx.value])
  }

  /**
   * @method 取消分配后 将库位分配件数和托盘分配件数全部重置为0
   * @params record 当前要取消分配的行
   */
  function clearAllotPieces(record: batchType) {
    if (!record?.stockList?.length) return
    if (record.stockList?.length) {
      for (const item of record.stockList) {
        item.planNumberPieces = 0
        item.planZeroQuantity = 0
        item.planFclQuantity = 0
        if (item?.containerList?.length) {
          for (const el of item?.containerList) {
            el.planNumberPieces = 0
            el.planZeroQuantity = 0
            el.planFclQuantity = 0
          }
        }
      }
    }
  }

  /**
   * @method 输入整箱数/零箱件数时计算总件数
   * @param data 当前行数据
   * @param type location:库位 tray:托盘 batchType:批次数据
   */
  function calcPieces(data: locationType | trayType, type: 'location' | 'tray', batchArr: batchType[]) {
    calcPlanNumPieces(data)
    if (type === 'location') {
      // 如果是输入库位,则需要自动分配右侧的托盘数量(若有托盘)
      if (!(data as locationType).containerList?.length) return
      autoAllotTray(data as locationType)
    }
    if (type === 'tray') {
      // 如果是输入了托盘,则需要换算出库位的总计划件数
      calcTotalLocation(batchArr)
    }
    // 校验零箱件数是否大于箱规
    validateAllotRules?.(data, type)
  }

  /**
   * @method 计算计划总件数
   * @param data 批次行数据
   */
  function calcPlanNumPieces(data: baseOutBoundType | batchType | locationType | trayType) {
    const { boxGauge, planFclQuantity, planZeroQuantity } = data || {}
    data.planNumberPieces = Number((planFclQuantity * boxGauge + planZeroQuantity).toFixed(2))
    if (isNaN(data.planNumberPieces) || !isFinite(data.planNumberPieces)) {
      data.planNumberPieces = 0
    }
  }

  /**
   * @method 计算整箱数和零箱件数
   * @param record 当前行数据
   */
  function calcPlanBoxNums(record: baseOutBoundType | batchType | locationType | trayType) {
    record.planFclQuantity = Math.floor(record.planNumberPieces / record.boxGauge)
    record.planZeroQuantity = record.planNumberPieces % record.boxGauge
    if (isNaN(record.planFclQuantity) || !isFinite(record.planFclQuantity)) {
      record.planFclQuantity = 0
    }
    if (isNaN(record.planZeroQuantity) || !isFinite(record.planZeroQuantity)) {
      record.planZeroQuantity = 0
    }
  }

  /**
   * @method 计算总重
   * @param data 批次行数据
   */
  function calcTotalWeight(record: baseOutBoundType | batchType | locationType | trayType) {
    if (record.packagingUnit === '3') {
      // 计划箱数 * 箱重 + (零箱 / 箱规)* 箱重 = 总重量
      record.totalWeight = Number((record?.planFclQuantity * record?.boxWeight + (record?.planZeroQuantity / record?.boxGauge) * record?.boxWeight).toFixed(3))
    } else {
      //总重量 = 计划件数 * 件重
      record.totalWeight = Number((record?.planNumberPieces * record?.pieceWeight).toFixed(3))
    }
    if (isNaN(record?.totalWeight) || !isFinite(record?.totalWeight)) {
      record.totalWeight = 0
    }
  }

  /**
   * @method 验证是否符合分配规则
   * @desc  填写零箱件数时,判断零箱件数是否大于箱规
   */
  const validateAllotRules = proxy?.$_l.debounce((data: batchType | locationType | trayType, type: 'batch' | 'location' | 'tray') => {
    const { boxGauge, planZeroQuantity } = data
    if (boxGauge > 0 && planZeroQuantity >= boxGauge) {
      switch (type) {
        case 'batch':
          proxy?.$message.error(`批次${(data as batchType).batchCode}下的零箱件数不允许大于或等于箱规`)
          break
        case 'location':
          proxy?.$message.warning(`库位${(data as locationType).locationCode}下的零箱件数不允许大于或等于箱规`)
          break
        case 'tray':
          proxy?.$message.warning(`托盘${(data as trayType).containerCode}的零箱件数不允许大于或等于箱规`)
          break
        default:
          break
      }
    }
  }, 500)

  /**
   * @method 校验计划数量是否大于可用数量,如果大于可用数量,则不满足分配规则,不允许分配完成
   * @param batchArr 批次数据
   */
  function validatePickingTotalNums(batchArr: batchType[]) {
    return new Promise<void>((resolve, reject) => {
      if (!batchArr[allotIdx.value]?.stockList?.length) {
        // 如果没有库位数量,停止校验
        resolve()
      } else {
        for (const item of batchArr[allotIdx.value].stockList) {
          // 如果没有托盘数量,就只校验库位的计划数量即可
          if (batchArr[allotIdx.value]?.packagingUnit === '3' && item.boxGauge > 0 && item.planZeroQuantity >= item.boxGauge) {
            reject(`库位${item.locationCode}的零箱件数不允许大于或等于箱规`)
            break
          }
          if (item.planNumberPieces > item.availableNumberPieces) {
            reject(`库位${item.locationCode}的分配数量不允许大于可用件数`)
            break
          }
          // 如果精确到托盘,则需校验托盘的计划数量
          for (const k of item.containerList) {
            if (batchArr[allotIdx.value]?.packagingUnit === '3' && k.boxGauge > 0 && k.planZeroQuantity >= k.boxGauge) {
              reject(`托盘${k.containerCode}的零箱件数不允许大于或等于箱规`)
              break
            }
            if (k.planNumberPieces > k.availableNumberPieces) {
              reject(`托盘${k.containerCode}的分配数量不允许大于可用件数`)
              break
            }
          }
        }
        resolve()
      }
    })
  }

  /**
   * @method 校验分配库存-库位计划总数与托盘总数是否相等
   * @param batchArr 批次数据
   */
  function validatePickingLocationNums(batchArr: batchType[]) {
    return new Promise<void>((resolve, reject) => {
      for (const item of batchArr) {
        if (item?.stockList) {
          for (const v of item?.stockList) {
            if (v?.containerList?.length) {
              const totalTrayPieces = v?.containerList.reduce((prev: number, next: any) => {
                return prev + next.planNumberPieces
              }, 0)
              if (totalTrayPieces !== v.planNumberPieces) {
                reject(`库位代码:${v.locationCode} 计划件数与该库位下托盘总计划件数不一致,请重新输入`)
              } else {
                resolve()
              }
            }
          }
          resolve()
        }
      }
    })
  }

  return {
    allotIdx,
    calcPieces,
    autoAllotTray,
    calcPlanBoxNums,
    calcTotalWeight,
    clearAllotPieces,
    allotLocationIdx,
    calcPlanNumPieces,
    autoAllotLocation,
    calcTotalLocation,
    validateAllotRules,
    showAssignInventory,
    showAssignInventoryRight,
    validatePickingTotalNums,
    validatePickingLocationNums,
  }
}

// 实际使用
import useAutoAllot from '@/hooks/useAutoAllot'
const { allotIdx, allotLocationIdx, calcTotalLocation, autoAllotLocation, autoAllotTray, clearAllotPieces, validatePickingTotalNums, validatePickingLocationNums } = useAutoAllot()
7.useImport
import { ref, reactive, watch } from 'vue'
import { axiosResponse } from '@/type/interface'

import useGlobal from './useGlobal'

type CallBackType = ((...args: any[]) => string) | string
type importType = 'add' | 'update'
export default function useImport() {
  /**导入类型,新增导入/更新导入 */
  const importType = ref<importType>('add')

  const { proxy } = useGlobal()
  /**导入参数 */
  const importData = reactive({
    data: {
      importShow: false,
      upLoadUrl: '',
      title: '新增导入',
    },
  })

  watch(
    () => importType.value,
    (now) => {
      importData.data.title = now === 'add' ? '新增导入' : '更新导入'
    }
  )

  /**
   * @method 打开导入弹窗
   * @param callBack 获取导入地址函数 / 导入地址
   */
  function handleImport<T = CallBackType>(callBack: T extends CallBackType ? T : never) {
    importData.data.upLoadUrl = typeof callBack === 'function' ? callBack() : callBack
    importData.data.importShow = true
    importType.value = importData.data.upLoadUrl.includes('update') ? 'update' : 'add'
  }

  /**
   * @method 下载模板
   */
  async function onDownload(callBack: () => Promise<axiosResponse<string>>) {
    try {
      const { Success, Tag, ResultCode } = await callBack()
      if (ResultCode === 200 && Success) {
        proxy?.$_u.uploadFile(Tag)
      }
    } catch (error) {
      console.log('下载模板error', error)
    }
  }

  /**
   * @method 导入弹窗关闭事件
   * @param is 是否关闭
   * @param callBack 关闭后回调(一般为重新请求)
   */
  function importClosed(is: boolean, callBack: (...args: any[]) => void) {
    importData.data.importShow = is
    callBack()
  }

  return {
    importType,
    importData,
    onDownload,
    importClosed,
    handleImport,
  }
}


// 实际使用
import templateImport from '@/components/template-import/index.vue'
import useImport from '@/hooks/useImport'

<templateImport 
:importData="importData" 
@closeImport="(is:boolean)=>importClosed(is,setHttpTableData)" 
@download="onDownload(containerImportTemplate)" 
@downloadFile="(url:string)=>proxy!?.$_u.uploadFile(url)"
>
</templateImport>

const { importData, importClosed, onDownload, handleImport } = useImport()
8.useExport
import type { axiosResponse } from '@/type/interface'

import useSpanLoading from './useSpanLoading'
import useGlobal from './useGlobal'

export default function useExport() {
  const { proxy } = useGlobal()
  const { isPending: exportLoading, changePending } = useSpanLoading()

  /**
   * @method 导出
   * @param from 单据来源
   * @param callBack 请求回调
   * @param exportInfo 导出参数
   */
  const handleExport = proxy!.$_l.debounce(async (from: string, callBack: (exportInfo: Record<string, any>) => Promise<axiosResponse<string>>, exportInfo: Record<string, any>) => {
    changePending(true)
    try {
      const { Success, Tag, ResultCode } = await callBack(exportInfo)
      if (ResultCode === 200 && Success) {
        changePending(false)
        Tag ? proxy!.$_u.uploadFile(Tag) : proxy!.$message.error(`暂无${from}信息导出数据`)
      }
    } catch (error) {
      changePending(false)
      console.log(`${from}导出error`, error)
    } finally {
      changePending(false)
    }
  }, 500)

  return {
    exportLoading,
    handleExport,
  }
}

// 实际使用

<a-button @click="handleOpera('export')" :loading='exportLoading'> 导出</a-button>

import useExport from '@/hooks/useExport'

const { exportLoading,handleExport } = useExport()

/**
 * 操作按钮
 * @param type 操作类型 add:新增 on:启用 off:禁用 import:导入 export:导出 print:打印
 */
const handleOpera = (type: operaType) => {
  switch (type) {
    case 'add':
      router.push({ name: 'containerAdd' })
      break
    case 'on':
    case 'off':
      handleEnable(type)
      break
    case 'import':
      handleImport(proxy!.$api.containerList_api.containerImportData)
      break
    case 'export':
      handleExport('容器管理', containerExportData, queryInfo)
      break

    default:
      break
  }
}
9.useGlobal
import { computed, getCurrentInstance } from 'vue'

import { useStore } from '@/store'
import { useRoute, useRouter } from 'vue-router'

import * as TYPES from '@/type/mutation-types'

export type { interContentHeader, interContentTable, ColumnType } from '@/type/interface/content'
export type { axiosResponse } from '@/type/interface'
export type { dictionaryType } from '@/type/interface/dictionary'
export type { FormInstance } from 'ant-design-vue'

/**
 * @method  导出全局公用对象
 */
export default function useGlobal() {
  /**当前组件实例 */
  const { proxy } = getCurrentInstance()!

  /**store */
  const store = useStore()

  /**全局路由对象 */
  const router = useRouter()

  /**当前路由对象 */
  const route = useRoute()

  /**是否企业级 */
  const getTopMenu = computed(() => store.state.app.activeTopMenu === TYPES['JSLX_01'])

  /**当前活跃仓库 */
  const activeWareHouse = computed(() => store.state.app.activeWareHouse)

  /**
   * @method 是否有按钮权限
   */
  const hasPermission = computed<(code: string) => boolean>(() => (code: string) => {
    return store.state.app.permission.includes(code)
  })

  /**
   * @method 是否有组织权限
   */
  const hasOrganization = computed<(code: string) => boolean>(() => (code: string) => {
    return store.state.app.userinfo.organizationCode === code
  })

  return {
    proxy,

    store,
    getTopMenu,
    activeWareHouse,

    route,
    router,

    hasPermission,
    hasOrganization,
  }
}


// 实际使用
import useGlobal, { type interContentHeader, type interContentTable } from '@/hooks/useGlobal'
const { proxy, router, getTopMenu, activeWareHouse } = useGlobal()
10.useDefinedExcel(前端导出excel)
import ExcelJS from 'exceljs'
import { ref } from 'vue'


export default function useDefineExcel() {
  const loading = ref(false)

  /**
   * @method 自定义导出
   * @param name 表格名字
   * @param columns 表头 {title、key必传,excelWidth?列的宽度,isEexcelNumber?是否为数字格式}
   * @param dataSource 导出数据
   */

  const exportExcel = (name: string, columns: any, dataSource: any) => {
    loading.value = true
    const workbook = new ExcelJS.Workbook()
    const worksheet = workbook.addWorksheet(name)

    // 表头
    const data: any = []
    columns.forEach((item: any) => {
      data.push({ header: item.title, key: item.key, width: item.excelWidth || 20 })
    })
    worksheet.columns = data

    // 添加数据
    dataSource.forEach((item: any) => {
      const dataRow = worksheet.addRow(item)
      dataRow.font = { size: 12 }
      dataRow.eachCell((cell) => {
        // 换行,水平垂直居中
        cell.alignment = { wrapText: true, horizontal: 'center', vertical: 'middle' }
      })
    })

    columns.forEach((item: any, index: number) => {
      // 转换为数字格式
      if (item.isEexcelNumber) {
        worksheet.getColumn(index + 1).numFmt = '0'
      }
    })

    // 导出文件
    workbook.xlsx
      .writeBuffer()
      .then((buffer) => {
        downloadFile(buffer, `${name}.xlsx`)
        loading.value = false
      })
      .catch((err) => {
        loading.value = false
        throw new Error(err)
      })
  }

  const downloadFile = (buffer: any, fileName: any) => {
    const blob = new Blob([buffer], { type: 'application/octet-stream' })
    const link = document.createElement('a')
    link.href = window.URL.createObjectURL(blob)
    link.download = fileName
    link.click()
  }

  return { exportExcel, loading }
}

使用
import useDefineExcel from '@/hooks/useDefinedExcel'
<a-button :loading="loading" v-permission="'CD00261'" :style="{ marginLeft: '10px' }" @click="exportExcel('库存交易日志', column, dataSource)"> <template #icon></template>导出</a-button>

/** 导出excel */
const { exportExcel, loading } = useDefineExcel()

11.useWebSocket
import { ref } from 'vue'

const DEFAULT_HEARTBEAT_INTERVAL = 2000 // 心跳和重连间隔时间
const MAX_COUNT = 5 //重连次数
interface OptionsType {
  heartbeatInterval?: number
  maxCount?: number
}
export default function useWebSocket(url: string, options: OptionsType = {}) {
  const { heartbeatInterval = DEFAULT_HEARTBEAT_INTERVAL, maxCount = MAX_COUNT } = options
  /** 存放webstocket */
  let socket: any = null
  /** 心跳定时器 */
  let heartbeatTimer: any = null
  /** 重连定时器 */
  let reconnectionTimer: any = null
  /** 计数 */
  let count = 0
  /** 服务端返回的数据 */
  const serverMessage = ref()
  const isConnected = ref(false)
  let test = 1
  const connect = () => {
    socket = new WebSocket(url)

    socket.onopen = () => {
      count = 0
      isConnected.value = true
      console.log('WebSocket 连接成功')
      stopReconnection()
      startHeartbeat()
    }

    socket.onmessage = (event: any) => {
      console.log('收到消息:', JSON.parse(event.data))
      serverMessage.value = event.data + test++
    }

    socket.onclose = () => {
      isConnected.value = false
      console.log('WebSocket 连接关闭')
      stopHeartbeat()
      reconnect()
    }
  }

  /**
   * @method 关闭webstocket
   */
  const disconnect = () => {
    if (socket) {
      socket.close()
      socket = null
      isConnected.value = false
      stopHeartbeat()
    }
  }

  /**
   * @method 发送
   */
  const send = (message: string) => {
    if (socket && socket.readyState === WebSocket.OPEN) {
      socket.send(message)
    } else {
      console.log('WebSocket 连接尚未建立')
    }
  }

  /**
   * @method 开启心跳
   */
  const startHeartbeat = () => {
    stopHeartbeat() // 先停止之前的心跳计时器,以防重复启动

    heartbeatTimer = setInterval(() => {
      if (socket && socket.readyState === WebSocket.OPEN) {
        // 发送心跳消息
        socket.send(JSON.stringify({ type: 'heartbeat' }))
      } else {
        stopHeartbeat()
        reconnect()
      }
    }, heartbeatInterval)
  }

  /**
   * @method  关闭心跳
   */
  const stopHeartbeat = () => {
    if (heartbeatTimer) {
      clearInterval(heartbeatTimer)
      heartbeatTimer = null
    }
  }

  /**
   * @method  关闭重连
   */
  const stopReconnection = () => {
    clearInterval(reconnectionTimer)
    reconnectionTimer = null
  }

  /**
   * @method  重连
   */
  const reconnect = () => {
    // 如果重连超过次数则停止
    stopReconnection()

    if (count >= maxCount) return

    reconnectionTimer = setInterval(() => {
      console.log('尝试重新连接 WebSocket')
      connect()
      count++
    }, heartbeatInterval)
  }

  connect() // 初始化时建立连接

  return { serverMessage, send, disconnect }
}

使用
import useWebSocket from '@/stocket'

let ws = useWebSocket('ws://localhost:1427')
12.uniapp-usePage
import { ref, type Ref, onMounted } from "vue";
import type { IUsePageConfig } from "./types/usePageConfig";

/**
 * @method 使用page列表的hook
 * @param config IUsePageConfig hook配置项
 * @returns
 */
export default function usePage<Q extends Record<string, any> = Record<string, any>, D extends Record<string, any> = Record<string, any>>(config: IUsePageConfig<Q, D>) {
  /**表格数据源 */
  // 这里不能使用ref泛型给D泛型,会推导为UnwrapRefSimple类型
  const dataSource: Ref<D[]> = ref([]);

  onMounted(() => {
    getPageList();
  });

  /**
   * @method 设置查询条件
   * @param queryInfo 查询条件
   */
  function setQueryInfo(queryInfo: Q) {
    config.queryParams = queryInfo;
    dataSource.value = [];
    getPageList();
  }

  /**
   * @method 列表请求
   * @param arg 额外的参数
   */
  async function getPageList<T = any>(arg?: T) {
    try {
      config.loadMoreConfig.status = "loading";
      const { Tag, Success } = await config.api({
        ...(config.queryParams as Q),
        ...arg,
      });
      if (Success) {
        config.loadMoreConfig.status = !!Tag.length ? "more" : "noMore";
        config.handleExtraCb?.(Tag);
        dataSource.value = dataSource.value?.concat(Tag);
      }
    } catch (error) {
      config.loadMoreConfig.status = "more";
      console.log("请求列表-error", error);
    }
  }

  /**
   * @method 触底事件
   */
  function handleScrollToLower() {
    config.queryParams.pageNum = (config.queryParams.pageNum ?? 0) + 1;
    getPageList();
  }

  return {
    dataSource,

    setQueryInfo,
    getPageList,
    handleScrollToLower,
  };
}

使用:
import usePage from "@/hooks/usePage";
import { warehousingPlanQueryPage, dictionariesBillStatusFindDropDown } from "./testApi";
import type { ITestSearchParams, ITestDataSource } from "./type";

const loadConfig = reactive({
  status: "more",
});

const { dataSource, handleScrollToLower, setQueryInfo } = usePage<ITestSearchParams, ITestDataSource>({
  api: warehousingPlanQueryPage,
  queryParams: searchParams,
  loadMoreConfig: loadConfig,
  handleExtraCb: handleTag,
});

function handleTag(Tag: ITestDataSource[]) {
  Tag.forEach((item) => {
    item["cargoName"] = `【${item.cargoOwnerCode}】${item.cargoOwnerName}`;
  });
}
13.uniapp-useGetHeaderHeight
import { ref, nextTick } from "vue";
import type { IUseGetHeaderHeightConfig } from "./types/useGetHeaderHeightConfig";

/**
 * @method 获取指定dom元素的高度
 * @param config
 * @description https://uniapp.dcloud.net.cn/api/ui/nodes-info.html#createselectorquery
 */
export default function useGetHeaderHeight(config: IUseGetHeaderHeightConfig) {
  const headerHeight = ref<number>(0);
  // 这里nextTick写箭头函数会导致this的类型丢失
  nextTick(() => {
    uni
      .createSelectorQuery()
      .in(this)
      .select(config.className)
      .boundingClientRect((data) => {
        headerHeight.value = (data as UniApp.NodeInfo).height!;
      })
      .exec();
  });
  return {
    headerHeight,
  };
}

使用:

    <view class="test-header">
      <content-nav :navConfig="navConfig" @back="handleBack">
        <template #right> <uni-icons type="reload" size="22"></uni-icons> </template>
      </content-nav>
      <content-search :searchConfig="searchConfig" @search="search" />
      <content-time ref="contentTimeRef" :statusOptions="options"         
              :timeConfig="timeConfig" @handleTimeChange="changeTime" />
    </view>

    <scroll-view refresher-background="f7f7f7" scroll-y :style="{ height: `calc(100% - ${headerHeight}px)` }" @scrolltolower="handleScrollToLower">

    </scroll-view>

import useHeaderHeight from "@/hooks/useGetHeaderHeight";

const { headerHeight } = useHeaderHeight({
  className: ".test-header",
});

14.useStorage
type storageType = 'session' | 'local'
export default function useStorage() {
  /**
   * @method 读取缓存
   * @param type sessionStorage | localStorage
   * @param key 要读取的key
   * @returns 根据key返回本地数据
   */
  function getStorage<D = any>(type: storageType, key: string): D {
    let _data: any = {}
    _data = type === 'session' ? sessionStorage.getItem(key) : localStorage.getItem(key)
    return JSON.parse(_data)
  }

  /**
   * @method 设置缓存
   * @param type sessionStorage | localStorage
   * @param key 要设置的key
   * @param data 要设置的值
   */
  function setStorage<D = any>(type: storageType, key: string, data: D) {
    const _data = JSON.stringify(data)
    type === 'session' ? sessionStorage.setItem(key, _data) : localStorage.setItem(key, _data)
  }

  /**
   * @method 移除缓存
   * @param type sessionStorage | localStorage
   * @param key 要移除的key
   */
  function removeStorage(type: storageType, key: string) {
    type === 'session' ? sessionStorage.removeItem(key) : localStorage.removeItem(key)
  }

  return {
    getStorage,
    setStorage,
    removeStorage,
  }
}


使用:
 <a-checkbox v-model:checked="isRememberPassWord" @change="loginStorageUserName" :disabled="loading">记住登录账号</a-checkbox>

import useStorage from '@/hooks/useStorage'

const { setStorage, getStorage, removeStorage } = useStorage()

/**
 * @method 记住账号
 */
const loginStorageUserName = (e: Event) => {
  const check = (e.target as HTMLInputElement).checked
  if (!formState.value.account) return proxy?.$message.error('请输入账号')
  check ? setStorage<string>('session', 'account', formState.value.account) : removeStorage('session', 'account')
}
 15.useModalDrag
import { watch, watchEffect, ref, computed, CSSProperties, type Ref } from 'vue'
import { useDraggable } from '@vueuse/core'

export default function useModalDrag(targetEle: Ref<HTMLElement | undefined>) {
  const { x, y, isDragging } = useDraggable(targetEle)

  const startX = ref<number>(0)
  const startY = ref<number>(0)
  const startedDrag = ref(false)
  const transformX = ref(0)
  const transformY = ref(0)
  const preTransformX = ref(0)
  const preTransformY = ref(0)
  const dragRect = ref({ left: 0, right: 0, top: 0, bottom: 0 })

  watch([x, y], () => {
    if (!startedDrag.value) {
      startX.value = x.value
      startY.value = y.value
      const bodyRect = document.body.getBoundingClientRect()
      const titleRect = targetEle.value?.getBoundingClientRect()
      dragRect.value.right = bodyRect.width - (titleRect?.width || 0)
      dragRect.value.bottom = bodyRect.height - (titleRect?.height || 0)
      preTransformX.value = transformX.value
      preTransformY.value = transformY.value
    }
    startedDrag.value = true
  })
  watch(isDragging, () => {
    if (!isDragging) {
      startedDrag.value = false
    }
  })

  watchEffect(() => {
    if (startedDrag.value) {
      transformX.value = preTransformX.value + Math.min(Math.max(dragRect.value.left, x.value), dragRect.value.right) - startX.value
      transformY.value = preTransformY.value + Math.min(Math.max(dragRect.value.top, y.value), dragRect.value.bottom) - startY.value
    }
  })

  const transformStyle = computed<CSSProperties>(() => {
    return {
      transform: `translate(${transformX.value}px, ${transformY.value}px)`,
    }
  })

  return {
    transformStyle,
  }
}



使用:
<a-modal v-model:visible="true">
    <template #title>
      <div ref="modalTitleRef" style="width: 100%; cursor: move">{{ title }}</div>
    </template>
    <template #modalRender="{ originVNode }">
      <div :style="transformStyle">
        <component :is="originVNode" />
      </div>
    </template>
</a-modal>


import useModalDrag from '@/hooks/useModalDrag'

const modalTitleRef = ref<HTMLElement>()
const { transformStyle } = useModalDrag(modalTitleRef)

16.useDownLoadZip
​​​​​​​
import { ref } from 'vue'

import JSZip from 'jszip'
import FileSaver from 'file-saver'
import dayjs from 'dayjs'

import useGlobal from './useGlobal'
import useSpanLoading from './useSpanLoading'
import type { IDownLoadConfig, IDownLoadFileListDto } from './types/useDownLoadZipConfig'

/**
 * @method 批量下载文件为zip格式
 */
export default function useDownLoadZip<D = IDownLoadFileListDto & anyObject>() {
  const selectTableData = ref<D[]>([])
  const { proxy } = useGlobal()
  const { isPending: downloadLoading, changePending } = useSpanLoading()

  /**
   * @method 下载文件 传入文件数组
   * @param fileList 实例:[{annexUrl:www.123.jpg,annexName:'123.jpg'}]   url:文件网络路径,name:文件名字
   * @param zipName 导出zip文件名称
   */
  async function downLoadFile(downLoadConfig: IDownLoadConfig) {
    try {
      changePending(true)
      const Zip = new JSZip()
      const cache = {}
      const promises = []
      const { fileList, zipName } = downLoadConfig
      for (const item of fileList) {
        const promise = getBlobStream(item.annexUrl).then((data: any) => {
          // 下载文件, 并存成ArrayBuffer对象(blob)
          Zip.file(item.annexName, data, { binary: true }) // 逐个添加文件
          cache[item.annexName] = data
        })
        promises.push(promise)
      }

      await Promise.all(promises)
      const content = await Zip.generateAsync({ type: 'blob' })
      /**生成二进制流 */
      FileSaver.saveAs(content, zipName || `${dayjs(new Date()).format('YYYY-MM-DD')}`) // 利用file-saver保存文件  自定义文件名
      changePending(false)
    } catch (error) {
      changePending(false)
      console.log('文件压缩-error', error)
      proxy?.$message.error('文件压缩失败')
    }
  }

  /**
   * @method 通过请求获取文件的blob流
   * @param url 文件url
   */
  function getBlobStream(url: string) {
    return new Promise((resolve, reject) => {
      const xmlHttp = new XMLHttpRequest()
      xmlHttp.open('GET', url, true)
      xmlHttp.responseType = 'blob'
      xmlHttp.onload = function () {
        if (this.status === 200) {
          resolve(this.response)
        } else {
          reject(this.status)
        }
      }
      xmlHttp.send()
    })
  }

  return {
    downloadLoading,

    downLoadFile,
    selectTableData,
  }
}


使用


<template>
    <a-table :row-selection="{onSelect: rowSelectionHandle}">
    </a-table>
    <a-button type="primary" @click="handleMultiDownload">批量下载</a-button>
</template>


import useDownLoadZip from '@/hooks/useDownLoadZip'
interface IAnnexList {
  /**附件名称 */
  annexName: string
  /**附件url */
  annexUrl: string
  /**附件id */
  id: string
  /**计划单id */
  warehousingPlanId: string
  /**上传uid */
  uid?: string
}

const { selectTableData, downloadLoading, downLoadFile } = useDownLoadZip<IAnnexList>()

/**
 *@method table选中
 */
const rowSelectionHandle = (keys: Array<string>, data: IAnnexList[]) => {
  contentDownloadTableParam.selectedRowKeys = keys
  selectTableData.value = data
}

/**
 * @method 批量下载
 */
const handleMultiDownload = proxy?.$_l.debounce(() => {
  if (!selectTableData.value.length) return proxy?.$message.error('请选择需要操作的数据')
  const downLoadFileConfig = {
    fileList: selectTableData.value,
    zipName: '测试压缩包',
  }
  downLoadFile(downLoadFileConfig)
}, 300)

持续更新中...

;