<template>
|
<div class="transfer-page art-full-height">
|
<ArtSearchBar
|
v-model="searchForm"
|
:items="searchItems"
|
:showExpand="true"
|
@search="handleSearch"
|
@reset="handleReset"
|
/>
|
|
<ElCard class="art-table-card">
|
<ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
|
<template #left>
|
<ElSpace wrap>
|
<ElButton v-if="canCreate" type="primary" @click="showDialog('add')" v-ripple>新增调拨单</ElButton>
|
<ElButton
|
v-if="canDelete"
|
type="danger"
|
:disabled="selectedRows.length === 0"
|
@click="handleBatchDelete"
|
v-ripple
|
>
|
批量删除
|
</ElButton>
|
<ListExportPrint
|
class="inline-flex"
|
:preview-visible="previewVisible"
|
@update:previewVisible="handlePreviewVisibleChange"
|
:report-title="reportTitle"
|
:selected-rows="selectedRows"
|
:query-params="reportQueryParams"
|
:columns="columns"
|
:preview-rows="previewRows"
|
:preview-meta="resolvedPreviewMeta"
|
:total="pagination.total"
|
:disabled="loading"
|
@export="handleExport"
|
@print="handlePrint"
|
/>
|
</ElSpace>
|
</template>
|
</ArtTableHeader>
|
|
<ArtTable
|
:loading="loading"
|
:data="data"
|
:columns="columns"
|
:pagination="pagination"
|
@selection-change="handleSelectionChange"
|
@pagination:size-change="handleSizeChange"
|
@pagination:current-change="handleCurrentChange"
|
/>
|
|
<TransferDialog
|
v-model:visible="dialogVisible"
|
:dialog-type="dialogType"
|
:transfer-data="currentTransferData"
|
:type-options="typeOptions"
|
:area-options="areaOptions"
|
:submit-loading="dialogSubmitting"
|
@submit="handleDialogSubmit"
|
/>
|
|
<TransferDetailDrawer
|
v-model:visible="detailDrawerVisible"
|
:loading="detailLoading"
|
:orders-loading="detailOrdersLoading"
|
:detail="detailData"
|
:order-rows="detailOrderRows"
|
:order-pagination="detailOrderPagination"
|
@size-change="handleDetailSizeChange"
|
@current-change="handleDetailCurrentChange"
|
/>
|
</ElCard>
|
</div>
|
</template>
|
|
<script setup>
|
import { computed, onMounted, reactive, ref } from 'vue'
|
import { useRouter } from 'vue-router'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useAuth } from '@/hooks/core/useAuth'
|
import { useUserStore } from '@/store/modules/user'
|
import { useTable } from '@/hooks/core/useTable'
|
import { useCrudPage } from '@/views/system/common/useCrudPage'
|
import { usePrintExportPage } from '@/views/system/common/usePrintExportPage'
|
import ListExportPrint from '@/components/biz/list-export-print/index.vue'
|
import { defaultResponseAdapter } from '@/utils/table/tableUtils'
|
import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
|
import { fetchDictDataPage } from '@/api/system-manage'
|
import { fetchWarehouseAreasList } from '@/api/warehouse-areas'
|
import {
|
fetchDeleteTransfer,
|
fetchExportTransferReport,
|
fetchTransferDetail,
|
fetchTransferMany,
|
fetchTransferOrdersPage,
|
fetchTransferPage,
|
fetchTransferPubOutStock,
|
fetchSaveTransfer,
|
fetchUpdateTransfer
|
} from '@/api/transfer'
|
import TransferDialog from './modules/transfer-dialog.vue'
|
import TransferDetailDrawer from './modules/transfer-detail-drawer.vue'
|
import { createTransferTableColumns } from './transferTable.columns.js'
|
import {
|
TRANSFER_REPORT_STYLE,
|
TRANSFER_REPORT_TITLE,
|
buildTransferDetailOrderQueryParams,
|
buildTransferDialogModel,
|
buildTransferPageQueryParams,
|
buildTransferPrintRows,
|
buildTransferReportMeta,
|
buildTransferSavePayload,
|
buildTransferSearchParams,
|
createTransferFormState,
|
createTransferSearchState,
|
getTransferPaginationKey,
|
getTransferSourceOptions,
|
getTransferStatusOptions,
|
getTransferExceStatusOptions,
|
normalizeTransferDetailRecord,
|
normalizeTransferOrderRow,
|
normalizeTransferRow,
|
resolveTransferAreaOptions,
|
resolveTransferTypeOptions
|
} from './transferPage.helpers.js'
|
|
defineOptions({ name: 'Transfer' })
|
|
const { hasAuth } = useAuth()
|
const userStore = useUserStore()
|
const router = useRouter()
|
|
const reportTitle = TRANSFER_REPORT_TITLE
|
const searchForm = ref(createTransferSearchState())
|
const typeOptions = ref([])
|
const areaOptions = ref([])
|
const selectedRows = ref([])
|
const detailDrawerVisible = ref(false)
|
const detailLoading = ref(false)
|
const detailOrdersLoading = ref(false)
|
const detailData = ref({})
|
const detailOrderRows = ref([])
|
const activeTransferId = ref(null)
|
const activeTransferCode = ref('')
|
const dialogSubmitting = ref(false)
|
const detailOrderPagination = reactive({
|
current: 1,
|
size: 20,
|
total: 0
|
})
|
|
const canCreate = computed(() => hasAuth('add'))
|
const canDelete = computed(() => hasAuth('delete'))
|
const canUpdate = computed(() => hasAuth('update'))
|
|
const reportQueryParams = computed(() => buildTransferSearchParams(searchForm.value))
|
const searchItems = computed(() => [
|
{ label: '关键字', key: 'condition', type: 'input', props: { clearable: true, placeholder: '请输入单号/备注/仓库/库区' } },
|
{ label: '调拨单号', key: 'code', type: 'input', props: { clearable: true, placeholder: '请输入调拨单号' } },
|
{
|
label: '调拨类型',
|
key: 'type',
|
type: 'select',
|
props: { clearable: true, filterable: true, options: typeOptions.value }
|
},
|
{
|
label: '来源',
|
key: 'source',
|
type: 'select',
|
props: { clearable: true, options: getTransferSourceOptions() }
|
},
|
{
|
label: '执行状态',
|
key: 'exceStatus',
|
type: 'select',
|
props: { clearable: true, options: getTransferExceStatusOptions() }
|
},
|
{ label: '源仓库', key: 'orgWareName', type: 'input', props: { clearable: true, placeholder: '请输入源仓库' } },
|
{ label: '目标仓库', key: 'tarWareName', type: 'input', props: { clearable: true, placeholder: '请输入目标仓库' } },
|
{ label: '源库区', key: 'orgAreaName', type: 'input', props: { clearable: true, placeholder: '请输入源库区' } },
|
{ label: '目标库区', key: 'tarAreaName', type: 'input', props: { clearable: true, placeholder: '请输入目标库区' } },
|
{
|
label: '状态',
|
key: 'status',
|
type: 'select',
|
props: { clearable: true, options: getTransferStatusOptions() }
|
},
|
{ label: '备注', key: 'memo', type: 'input', props: { clearable: true, placeholder: '请输入备注' } }
|
])
|
|
function handleSelectionChange(rows) {
|
selectedRows.value = Array.isArray(rows) ? rows : []
|
handleCrudSelectionChange(rows)
|
}
|
|
async function loadTransferDetail(transferId) {
|
detailLoading.value = true
|
try {
|
const response = await guardRequestWithMessage(
|
fetchTransferDetail(transferId),
|
{},
|
{ timeoutMessage: '调拨单详情加载超时,已停止等待' }
|
)
|
detailData.value = normalizeTransferDetailRecord(response)
|
} finally {
|
detailLoading.value = false
|
}
|
}
|
|
async function loadTransferOrders(code) {
|
detailOrdersLoading.value = true
|
try {
|
const response = await guardRequestWithMessage(
|
fetchTransferOrdersPage(
|
buildTransferDetailOrderQueryParams({
|
code,
|
current: detailOrderPagination.current,
|
pageSize: detailOrderPagination.size
|
})
|
),
|
{ records: [], total: 0, current: detailOrderPagination.current, size: detailOrderPagination.size },
|
{ timeoutMessage: '关联单据加载超时,已停止等待' }
|
)
|
const normalized = defaultResponseAdapter(response)
|
detailOrderRows.value = normalized.records.map((item) => normalizeTransferOrderRow(item))
|
detailOrderPagination.total = Number(normalized.total || 0)
|
detailOrderPagination.current = Number(normalized.current || detailOrderPagination.current || 1)
|
detailOrderPagination.size = Number(normalized.size || detailOrderPagination.size || 20)
|
} catch (error) {
|
detailOrderRows.value = []
|
detailOrderPagination.total = 0
|
ElMessage.error(error?.message || '获取关联单据失败')
|
} finally {
|
detailOrdersLoading.value = false
|
}
|
}
|
|
async function openDetail(row) {
|
activeTransferId.value = row.id
|
activeTransferCode.value = row.code || ''
|
detailOrderPagination.current = 1
|
detailOrderRows.value = []
|
detailData.value = {}
|
detailDrawerVisible.value = true
|
try {
|
await loadTransferDetail(row.id)
|
activeTransferCode.value = detailData.value.code || row.code || activeTransferCode.value
|
await loadTransferOrders(activeTransferCode.value)
|
} catch (error) {
|
detailDrawerVisible.value = false
|
detailData.value = {}
|
detailOrderRows.value = []
|
ElMessage.error(error?.message || '获取调拨单详情失败')
|
}
|
}
|
|
async function openEditDialog(row) {
|
try {
|
const detail = await guardRequestWithMessage(
|
fetchTransferDetail(row.id),
|
{},
|
{ timeoutMessage: '调拨单详情加载超时,已停止等待' }
|
)
|
showDialog('edit', detail)
|
} catch (error) {
|
ElMessage.error(error?.message || '获取调拨单详情失败')
|
}
|
}
|
|
async function handlePublish(row) {
|
try {
|
await ElMessageBox.confirm(`确定要下发调拨单「${row.code || row.id}」吗?`, '下发确认', {
|
confirmButtonText: '确定',
|
cancelButtonText: '取消',
|
type: 'warning'
|
})
|
const response = await fetchTransferPubOutStock({ id: row.id })
|
if (response?.code !== 200 && response?.success !== true) {
|
throw new Error(response?.message || '下发执行失败')
|
}
|
ElMessage.success(response?.message || '下发执行成功')
|
await refreshData()
|
if (detailDrawerVisible.value && activeTransferId.value === row.id) {
|
await loadTransferDetail(row.id)
|
await loadTransferOrders(row.code || activeTransferCode.value)
|
}
|
} catch (error) {
|
if (error === 'cancel' || error?.message === 'cancel') return
|
ElMessage.error(error?.message || '下发执行失败')
|
}
|
}
|
|
async function handleActionClick(action, row) {
|
if (action?.disabled) return
|
if (action?.key === 'view') {
|
await openDetail(row)
|
return
|
}
|
if (action?.key === 'items') {
|
if (!row?.id) return
|
router.push({
|
path: '/orders/transfer-item',
|
query: {
|
transferId: String(row.id)
|
}
|
})
|
return
|
}
|
if (action?.key === 'edit') {
|
if (!canUpdate.value) return
|
await openEditDialog(row)
|
return
|
}
|
if (action?.key === 'delete') {
|
if (!canDelete.value) return
|
await handleDeleteAction?.(row)
|
return
|
}
|
if (action?.key === 'publish') {
|
if (!canUpdate.value) return
|
await handlePublish(row)
|
}
|
}
|
|
const {
|
columns,
|
columnChecks,
|
data,
|
loading,
|
pagination,
|
replaceSearchParams,
|
resetSearchParams,
|
handleSizeChange,
|
handleCurrentChange,
|
refreshData,
|
refreshCreate,
|
refreshUpdate,
|
refreshRemove,
|
getData
|
} = useTable({
|
core: {
|
apiFn: fetchTransferPage,
|
apiParams: buildTransferPageQueryParams(searchForm.value),
|
paginationKey: getTransferPaginationKey(),
|
columnsFactory: () => createTransferTableColumns({ handleActionClick })
|
},
|
transform: {
|
dataTransformer: (records) => (Array.isArray(records) ? records.map((item) => normalizeTransferRow(item)) : [])
|
}
|
})
|
|
const {
|
dialogVisible,
|
dialogType,
|
currentRecord: currentTransferData,
|
selectedRows: crudSelectedRows,
|
handleSelectionChange: handleCrudSelectionChange,
|
showDialog,
|
handleDialogSubmit: handleCrudDialogSubmit,
|
handleDelete,
|
handleBatchDelete
|
} = useCrudPage({
|
createEmptyModel: () => createTransferFormState(),
|
buildEditModel: (record) => buildTransferDialogModel(record),
|
buildSavePayload: (formData) => buildTransferSavePayload(formData, areaOptions.value),
|
saveRequest: fetchSaveTransfer,
|
updateRequest: fetchUpdateTransfer,
|
deleteRequest: fetchDeleteTransfer,
|
entityName: '调拨单',
|
resolveRecordLabel: (record) => record?.code || record?.id,
|
refreshCreate,
|
refreshUpdate,
|
refreshRemove
|
})
|
const handleDeleteAction = handleDelete
|
|
function handleDialogSubmit(formData) {
|
dialogSubmitting.value = true
|
return handleCrudDialogSubmit(formData).finally(() => {
|
dialogSubmitting.value = false
|
})
|
}
|
|
async function loadTypeOptions() {
|
const response = await guardRequestWithMessage(
|
fetchDictDataPage({ current: 1, pageSize: 200, dictTypeCode: 'sys_transfer_type', status: 1 }),
|
{ records: [] },
|
{ timeoutMessage: '调拨类型选项加载超时,已停止等待' }
|
)
|
typeOptions.value = resolveTransferTypeOptions(defaultResponseAdapter(response).records)
|
}
|
|
async function loadAreaOptions() {
|
const response = await guardRequestWithMessage(
|
fetchWarehouseAreasList(),
|
{ records: [] },
|
{ timeoutMessage: '库区选项加载超时,已停止等待' }
|
)
|
areaOptions.value = resolveTransferAreaOptions(defaultResponseAdapter(response).records)
|
}
|
|
function handleSearch(params) {
|
searchForm.value = { ...searchForm.value, ...params }
|
replaceSearchParams(buildTransferSearchParams(searchForm.value))
|
getData()
|
}
|
|
function handleReset() {
|
searchForm.value = createTransferSearchState()
|
resetSearchParams()
|
}
|
|
async function handleDetailCurrentChange(current) {
|
detailOrderPagination.current = current
|
await loadTransferOrders(activeTransferCode.value)
|
}
|
|
async function handleDetailSizeChange(size) {
|
detailOrderPagination.size = size
|
detailOrderPagination.current = 1
|
await loadTransferOrders(activeTransferCode.value)
|
}
|
|
const resolvePrintRecords = async (payload) => {
|
if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
|
return defaultResponseAdapter(await fetchTransferMany(payload.ids)).records
|
}
|
return defaultResponseAdapter(
|
await fetchTransferPage({
|
...reportQueryParams.value,
|
current: 1,
|
pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
|
})
|
).records
|
}
|
|
const {
|
previewVisible,
|
previewRows,
|
previewMeta: rawPreviewMeta,
|
handlePreviewVisibleChange,
|
handleExport,
|
handlePrint
|
} = usePrintExportPage({
|
downloadFileName: 'transfer.xlsx',
|
requestExport: (payload) =>
|
fetchExportTransferReport(Array.isArray(payload?.ids) && payload.ids.length > 0 ? reportQueryParams.value : payload, {
|
headers: {
|
Authorization: userStore.accessToken || ''
|
}
|
}),
|
resolvePrintRecords,
|
buildPreviewRows: (records) => buildTransferPrintRows(records),
|
buildPreviewMeta: (rows) => {
|
const now = new Date()
|
return {
|
reportTitle,
|
reportDate: now.toLocaleDateString('zh-CN'),
|
printedAt: now.toLocaleString('zh-CN', { hour12: false }),
|
operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
|
count: rows.length,
|
reportStyle: {
|
titleAlign: 'center',
|
titleLevel: 'strong',
|
orientation: 'landscape',
|
density: 'compact',
|
showSequence: true
|
}
|
}
|
}
|
})
|
|
const resolvedPreviewMeta = computed(() =>
|
buildTransferReportMeta({
|
previewMeta: rawPreviewMeta.value,
|
count: previewRows.value.length,
|
orientation: rawPreviewMeta.value?.reportStyle?.orientation || TRANSFER_REPORT_STYLE.orientation
|
})
|
)
|
|
onMounted(async () => {
|
await Promise.allSettled([loadTypeOptions(), loadAreaOptions(), getData()])
|
})
|
</script>
|