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()
const formState = ref<Partial<F>>({})
const validateMessages = { required: '${label}不能为空' }
const formRef = ref<antFormType>()
function setFormStateData(data: Record<string, any>) {
Object.assign(formState.value, data)
}
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)
}
})
}
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)
}
}
}
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'
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()
})
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()
setHttpTableData()
})
function setQueryInfo(queryInfo: Q) {
queryParams = queryInfo
}
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
}
}
function contentHeaderHandle(type: string, data: any) {
Object.assign(queryParams, data)
contentTableParam.pagination!.current = 1
contentTableParam.selectedRowKeys = []
contentTableDom.value?.setHttpTable('selectedRowKeys', [])
setHttpTableData()
}
function paginationHandle(page: { current: number; pageSize: number }) {
contentTableParam.pagination!.current = page.current
contentTableParam.pagination!.pageSize = page.pageSize
setHttpTableData()
}
function rowSelectionHandle(keys: string[], data: D[]) {
contentTableParam.selectedRowKeys = keys
selectTableData.value = data
}
function contentHeaderHeightHandle() {
templateContentDom.value?.getContentHeaderHeight()
}
async function handleUpdateTable() {
await contentTableDom.value?.initTable(contentTableParam)
setHttpTableData()
}
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'
export default function usePrint() {
const { isPending: printLoading, changePending } = useSpanLoading()
const print = ref(null)
const isShowPrint = ref(false)
const printDom = ref<contentPrintType>()
const printOptions = ref<IPrintTemplateType[]>([])
const htmlArrays = ref<string[]>([])
const { proxy } = useGlobal()
async function initPrintOptions(type: string) {
const { Success, Tag } = await proxy?.$api.goDownPlanList_api.printTemplateGetTemplateList({ type })
if (Success) {
printOptions.value = Tag
}
}
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)
}
}
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)
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[]>([])
const selectChange = (value: any, option: any, type: 'print') => {
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'
export default function useErrorMessage() {
const { proxy } = useGlobal()
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 {
alertError,
}
}
import useErrorMessage from '@/hooks/useErrorMessage'
const { alertError } = useErrorMessage()
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
export default function useDrawer(): any {
const activeKey = ref<string>('1')
const drawerConfig = reactive({
data: {
visible: false,
title: '',
placement: 'right',
width: 1500,
footer: true,
},
})
function setDrawerConfig(config: Record<string, any>) {
Object.assign(drawerConfig.data, config)
}
function drawerCloseHandle(type: 'after' | 'close', e: any) {
if (!e) {
activeKey.value = '1'
}
}
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()
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()
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'
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>
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
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()
}
}
const disconnect = () => {
if (socket) {
socket.close()
socket = null
isConnected.value = false
stopHeartbeat()
}
}
const send = (message: string) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(message)
} else {
console.log('WebSocket 连接尚未建立')
}
}
const startHeartbeat = () => {
stopHeartbeat()
heartbeatTimer = setInterval(() => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify({ type: 'heartbeat' }))
} else {
stopHeartbeat()
reconnect()
}
}, heartbeatInterval)
}
const stopHeartbeat = () => {
if (heartbeatTimer) {
clearInterval(heartbeatTimer)
heartbeatTimer = null
}
}
const stopReconnection = () => {
clearInterval(reconnectionTimer)
reconnectionTimer = null
}
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";
export default function usePage<Q extends Record<string, any> = Record<string, any>, D extends Record<string, any> = Record<string, any>>(config: IUsePageConfig<Q, D>) {
const dataSource: Ref<D[]> = ref([]);
onMounted(() => {
getPageList();
});
function setQueryInfo(queryInfo: Q) {
config.queryParams = queryInfo;
dataSource.value = [];
getPageList();
}
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);
}
}
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";
export default function useGetHeaderHeight(config: IUseGetHeaderHeightConfig) {
const headerHeight = ref<number>(0);
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() {
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)
}
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)
}
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()
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
<div ref="modalTitleRef" style="width: 100%; cursor: move">{{ title }}</div>
</template>
<template
<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'
export default function useDownLoadZip<D = IDownLoadFileListDto & anyObject>() {
const selectTableData = ref<D[]>([])
const { proxy } = useGlobal()
const { isPending: downloadLoading, changePending } = useSpanLoading()
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)
item.annexName = `${baseName}_${i + 1}.${extension}`
const promise = getBlobStream(item.annexUrl).then((data) => {
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')}`)
changePending(false)
} catch (error) {
changePending(false)
console.log('文件压缩-error', error)
proxy?.$message.error('文件压缩失败')
}
}
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
annexUrl: string
id: string
warehousingPlanId: string
uid?: string
}
const { selectTableData, downloadLoading, downLoadFile } = useDownLoadZip<IAnnexList>()
const rowSelectionHandle = (keys: Array<string>, data: IAnnexList[]) => {
contentDownloadTableParam.selectedRowKeys = keys
selectTableData.value = data
}
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 }
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
function customRow(record: T, index: number) {
return {
style: {
cursor: 'pointer',
},
onMouseenter: (event: MouseEvent) => {
const ev = event || window.event
const target = ev.target as HTMLElement
target.draggable = true
},
onDragstart: (event: Event) => {
const ev = event || window.event
ev.stopPropagation()
sourceRecord.value = record
oldIndex = index
},
onDragover: (event: DragEvent) => {
const ev = event || window.event
ev.preventDefault()
ev.dataTransfer!.dropEffect = 'move'
newIndex = index
},
onDrop: (event: Event) => {
const ev = event || window.event
ev.stopPropagation()
targetRecord.value = record
newIndex = index
if (newIndex === oldIndex) return
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>
持续更新...