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

989 阅读10分钟

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>()

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)

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
  }
}

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)
  }
}

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()

6.useAutoAllot

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 { 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 { 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.useSpanLoading

/**
 * @method 使用全屏loading
 */
import { ref } from 'vue'
import SpanLoading from '@/views/common/spin-loading/index.vue'
/**
 * @method 使用全屏loading
 */
export default function useSpanLoading() {
  /**是否正在请求 */
  const isPending = ref<boolean>(false)

  /**
   * @method 生成longing组件
   */
  function renderLoadingComponents(): false | JSX.Element {
    return isPending.value && <SpanLoading />
  }

  /**
   * @method 修改状态
   * @param is 是否正在请求
   */
  function changePending(is: boolean) {
    isPending.value = is
  }
  return {
    isPending,
    changePending,

    renderLoadingComponents,
  }
}


// 实际使用
<template>
<a-button type="primary" @click="testLoading" :style="{ marginLeft: '20px' }">全屏loading</a-button>

<renderLoadingComponents />
</template>

<script lang="ts" setup name="utlis">
import useSpanLoading from '@/hooks/useSpanLoading'
const { renderLoadingComponents, changePending } = useSpanLoading()


const testLoading = () => {
  changePending(true)
  setTimeout(() => {
    changePending(false)
  }, 3000)
}

</script>


11.useBatchCalc


/**
 * @method 入库计划单批次计算
 */
export default function useBatchCalc() {
  /**
   * @method 计算计划总件数
   * @desc 计划总件数=箱规*整箱数+零箱件数
   */
  function calcTotalPlanNumber<T extends IGoodsBatchType = IGoodsBatchType>(data: T) {
    if (data.packageUnit === '3') {
      data.planNumberPieces = data.boxGauge * data.fclQuantity + Number(data.zeroQuantity)
      if (!isFinite(data.planNumberPieces)) {
        data.planNumberPieces = 0
      }
    }
  }

  /**
   * @method 计算总重
   * @desc 如果包装单位是箱,总重量 = 箱数*箱重+零箱件数 / 箱规*箱重  四舍五入保留3位小数
   * @desc 如果不是箱,总重量 = 毛重*计划总件数
   */
  function calcTotalWeight<T extends IGoodsBatchType = IGoodsBatchType>(data: T) {
    if (data.packageUnit === '3') {
      // 如果包装单位是箱,总重量 = 箱数*箱重+零箱件数 / 箱规*箱重  四舍五入保留3位小数
      data.totalWeight = Number((data.fclQuantity * data.boxWeight + (data.zeroQuantity / data.boxGauge) * data.boxWeight).toFixed(3))
    } else {
      // 如果不是箱,总重量 = 件重*计划总件数
      data.totalWeight = Number((data.pieceWeight * data.planNumberPieces).toFixed(3))
    }
    if (isNaN(data.totalWeight) || !isFinite(data.totalWeight)) {
      data.totalWeight = 0
    }
  }

  /**
   * @method 计算总体积
   * @desc 总体积 = 单件体积*计划总件数
   */
  function calcTotalVolume<T extends IGoodsBatchType = IGoodsBatchType>(data: T) {
    // 总体积 = 单件体积*计划总件数
    data.totalCapacity = Number((data.capacity * data.planNumberPieces).toFixed(2))

    if (isNaN(data.totalCapacity) || !isFinite(data.totalCapacity)) {
      data.totalCapacity = 0
    }
  }

  /**
   * @method 计算板数
   * @desc 板规数  =  计划件数/板规
   */
  function calcPlateNumber<T extends IGoodsBatchType = IGoodsBatchType>(data: T) {
    data['plateNumber'] = Math.ceil(data.planNumberPieces / data.boardGauge)
    if (isNaN(data['plateNumber']) || !isFinite(data['plateNumber'])) {
      data['plateNumber'] = 0
    }
  }

  /**
   * @method 计算整箱数
   * @desc 整箱数 = 计划总数/箱规
   */
  function calcFclQuantity<T extends IGoodsBatchType = IGoodsBatchType>(data: T) {
    data.fclQuantity = Math.floor(data.planNumberPieces / data.boxGauge)
    if (!isFinite(data.fclQuantity)) {
      data.fclQuantity = 0
    }
  }

  /**
   * @method 计算零箱件数
   * @desc 零箱件数=计划总数/箱规的余数
   */
  function calcZeroQuantity<T extends IGoodsBatchType = IGoodsBatchType>(data: T) {
    data.zeroQuantity = Number((data.planNumberPieces % data.boxGauge).toFixed(2))
    if (!isFinite(data.zeroQuantity)) {
      data.zeroQuantity = 0
    }
  }

  /**
   * @method 计算单件体积
   * @desc 单件体积 = 总体积/总件数
   */
  function calcPieceVolume<T extends IGoodsBatchType = IGoodsBatchType>(data: T) {
    data.capacity = Number((data.totalCapacity / data.planNumberPieces).toFixed(2))
    if (isNaN(data.capacity) || !isFinite(data.capacity)) {
      data.capacity = 0
    }
  }

  /**
   * @method 计算件重/毛重
   * @desc 件重 = 总重/计划件数
   */
  function calcPieceWeight<T extends IGoodsBatchType = IGoodsBatchType>(data: T, cb?: (...args: any[]) => void) {
    data.pieceWeight = Number((data.totalWeight / data.planNumberPieces).toFixed(3))
    if (isNaN(data.pieceWeight) || !isFinite(data.pieceWeight)) {
      data.pieceWeight = 0
    }
    cb?.()
  }

  /**
   * @method 计算箱重
   * @description 箱重= 总重 / (计划件数 / 箱规)
   */
  function calcBoxWeight<T extends IGoodsBatchType = IGoodsBatchType>(data: T) {
    data.boxWeight = Number((data.totalWeight / Number(data.planNumberPieces / data.boxGauge)).toFixed(3))
    if (isNaN(data.boxWeight) || !isFinite(data.boxWeight)) {
      data.boxWeight = 0
    }
  }

  return {
    calcTotalWeight,
    calcTotalVolume,
    calcTotalPlanNumber,

    calcFclQuantity,
    calcZeroQuantity,

    calcBoxWeight,
    calcPlateNumber,
    calcPieceVolume,
    calcPieceWeight,
  }
}

// 实际使用
<a-form-item label="箱重(KG)" name="boxWeight" :wrapper-col="{ offset: 0, span: 24 }">
<a-input-number placeholder="请填写箱重" :precision="3"  :min="0" v-model:value="addBatchFormState.boxWeight" :maxlength="7" allow-clear @change="calcTotalWeight(addBatchFormState)" />
                </a-form-item>

import useBatchCalc from '@/hooks/useBatchCalc'

const { calcTotalWeight, calcTotalVolume, calcTotalPlanNumber, calcFclQuantity, calcZeroQuantity, calcPlateNumber, calcPieceWeight, calcPieceVolume, calcBoxWeight } = useBatchCalc()

12.useDefinedExcel(前端导出excel)

import ExcelJS from 'exceljs'
import { ref } from 'vue'
 
  /**
   * @method 自定义导出
   * @param name 表格名字
   * @param columns 表头 {title、key必传,excelWidth?列的宽度,isEexcelNumber?是否为数字格式}
   * @param dataSource 导出数据
   */
export default function useDefineExcel() {
  const loading = ref(false)
 
  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('库存交易日志', excelParam, contentTableParam.dataSource)"> <template #icon></template>导出</a-button>
 
/** 导出excel */
const { exportExcel, loading } = useDefineExcel()

13.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')

14.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}`;
  });
}

15.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()
      .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",
});

16.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')
}

17.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)

18.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, InputType, InputByType } 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
      const cloneFileList = proxy?.$_l.cloneDeep(fileList) || []
      for (let i = 0; i < cloneFileList.length; i++) {
        const item = cloneFileList[i]
        // 获取文件扩展名
        const extension = item.annexName.split('.').pop()
        // 获取文件名(不含扩展名)
        const baseName = item.annexName.substring(0, item.annexName.length - extension!.length - 1)
        // 文件名重复时会只有一条数据,需要拼接唯一文件name
        item.annexName = `${baseName}_${i + 1}.${extension}`
        const promise = getBlobStream(item.annexUrl).then((data) => {
          // 下载文件, 并存成ArrayBuffer对象(blob)
          // 名称重复时,zip内的数据会只存在一条,用索引拼接唯一名称
          Zip.file<InputType>(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<InputByType[InputType]>((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)

19.useDragTable(antdvue-table)行拖拽

import { ref } from 'vue'

type RecordType = anyObject & { serialNumber: number; isModified: string }

/**
 * @method 表格行拖拽
 * @param dataSource 数据源
 */
export default function useDragTable<T extends RecordType = RecordType>(dataSource: T[]) {
  /**拖拽起始行 */
  const sourceRecord = ref<Partial<T>>({})
  /**拖拽目标行 */
  const targetRecord = ref<Partial<T>>({})
  /**拖拽起始索引 */
  let oldIndex: number | null = null
  /**拖拽目标索引 */
  let newIndex: number | null = null

  /**
   * @method 自定义拖拽行
   * @param record 当前行数据
   * @param index 当前行索引
   */
  function customRow(record: T, index: number) {
    return {
      style: {
        cursor: 'pointer',
      },
      // 鼠标移入
      onMouseenter: (event: MouseEvent) => {
        // 兼容IE
        const ev = event || window.event
        const target = ev.target as HTMLElement
        target.draggable = true
      },
      // 开始拖拽
      onDragstart: (event: Event) => {
        // 兼容IE
        const ev = event || window.event
        ev.stopPropagation()
        // 得到源目标数据
        sourceRecord.value = record
        oldIndex = index
      },
      // 拖动元素经过的元素
      onDragover: (event: DragEvent) => {
        // 兼容 IE
        const ev = event || window.event
        // 阻止默认行为
        ev.preventDefault()
        ev.dataTransfer!.dropEffect = 'move' // 可以去掉拖动时那个+号
        newIndex = index
      },
      // 鼠标松开
      onDrop: (event: Event) => {
        // 兼容IE
        const ev = event || window.event
        // 阻止冒泡
        ev.stopPropagation()
        // 得到目标数据
        targetRecord.value = record
        // 将源数据插入目标数据前面
        newIndex = index

        if (newIndex === oldIndex) return

        // 如果从1拖到10,那么1-10之间的isModified都要改为1,或者从10拖到1
        const startIndex = newIndex > oldIndex! ? oldIndex : newIndex
        const endIndex = newIndex > oldIndex! ? newIndex : oldIndex
        for (let i = startIndex; i! <= endIndex!; i!++) {
          dataSource[i!]['isModified'] = '1'
        }

        dataSource[oldIndex!].serialNumber = newIndex + 1
        dataSource[newIndex!].serialNumber = oldIndex! + 1
        dataSource.splice(oldIndex!, 1)
        dataSource.splice(newIndex, 0, sourceRecord.value)
      },
    }
  }

  return {
    sourceRecord,
    targetRecord,
    oldIndex,
    newIndex,
    customRow,
  }
}



// 使用

<template>
  <a-table :dataSource="dataSource" :columns="columns" :customRow="customRow" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import useDragTable from '@/hooks/useDragTable'

type DataSourceType = anyObject & { serialNumber: number; isModified: string }

const dataSource = ref<DataSourceType[]>([
  {
    key: '1',
    name: '胡彦斌',
    age: 32,
    address: '西湖区湖底公园1号',
    serialNumber: 1,
    isModified: '0',
  },
  {
    key: '2',
    name: '胡彦祖',
    age: 42,
    address: '西湖区湖底公园1号',
    serialNumber: 2,
    isModified: '0',
  },
])

const columns = ref([
  {
    title: '姓名',
    dataIndex: 'name',
    key: 'name',
  },
  {
    title: '年龄',
    dataIndex: 'age',
    key: 'age',
  },
  {
    title: '住址',
    dataIndex: 'address',
    key: 'address',
  },
])

const { customRow } = useDragTable<DataSourceType>(dataSource.value)
</script>

持续更新...