记录:vue3+ts+antdvue实际开发中封装的业务hooks
vue3+ts在实际开发中封装常用的自定义hooks
·
-
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 { 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)
持续更新中...
更多推荐
已为社区贡献5条内容
所有评论(0)