<template>
|
<div class="matnr-group-page art-full-height flex flex-col gap-4 xl:flex-row">
|
<ElCard class="w-full shrink-0 xl:w-[320px]">
|
<div class="mb-3 flex items-center justify-between gap-3">
|
<div>
|
<div class="text-base font-medium text-[var(--art-text-primary)]">物料分组</div>
|
<div class="text-xs text-[var(--art-text-secondary)]">
|
{{ selectedGroupLabel }}
|
</div>
|
</div>
|
<ElButton text @click="handleResetGroup">全部</ElButton>
|
</div>
|
|
<div class="mb-3 flex items-center gap-2">
|
<ElInput
|
v-model.trim="groupSearch"
|
clearable
|
placeholder="搜索物料分组"
|
@clear="handleGroupSearch"
|
@keyup.enter="handleGroupSearch"
|
/>
|
<ElButton @click="handleGroupSearch">搜索</ElButton>
|
</div>
|
|
<ElScrollbar class="h-[calc(100vh-260px)] pr-1">
|
<div v-if="groupTreeLoading" class="py-6">
|
<ElSkeleton :rows="10" animated />
|
</div>
|
<ElEmpty v-else-if="!groupTreeData.length" description="暂无物料分组" />
|
<ElTree
|
v-else
|
:data="groupTreeData"
|
:props="treeProps"
|
node-key="id"
|
highlight-current
|
default-expand-all
|
:current-node-key="selectedGroupId"
|
@node-click="handleGroupNodeClick"
|
>
|
<template #default="{ data }">
|
<div class="flex items-center gap-2">
|
<span class="font-medium">{{ data.name || '--' }}</span>
|
<span class="text-xs text-[var(--art-text-secondary)]">{{ data.code || '--' }}</span>
|
</div>
|
</template>
|
</ElTree>
|
</ElScrollbar>
|
</ElCard>
|
|
<div class="min-w-0 flex-1 space-y-4">
|
<ArtSearchBar
|
v-model="searchForm"
|
:items="searchItems"
|
:showExpand="true"
|
@search="handleSearch"
|
@reset="handleReset"
|
/>
|
|
<ElCard class="art-table-card">
|
<ArtTableHeader :loading="loading" v-model:columns="columnChecks" @refresh="refreshData">
|
<template #left>
|
<ElSpace wrap>
|
<ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>新增分组</ElButton>
|
<ElButton
|
v-auth="'delete'"
|
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"
|
/>
|
|
<MatnrGroupDialog
|
v-model:visible="dialogVisible"
|
:dialog-type="dialogType"
|
:group-data="currentGroupData"
|
:parent-group-options="parentGroupOptions"
|
:resolve-parent-code="resolveGroupCode"
|
@submit="handleDialogSubmit"
|
/>
|
|
<MatnrGroupDetailDrawer
|
v-model:visible="detailDrawerVisible"
|
:loading="detailLoading"
|
:detail="detailData"
|
/>
|
</ElCard>
|
</div>
|
</div>
|
</template>
|
|
<script setup>
|
import { ElMessage } from 'element-plus'
|
import { computed, onMounted, ref } from 'vue'
|
import { useUserStore } from '@/store/modules/user'
|
import { useAuth } from '@/hooks/core/useAuth'
|
import { useTable } from '@/hooks/core/useTable'
|
import { useTableColumns } from '@/hooks/core/useTableColumns'
|
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 {
|
fetchDeleteMatnrGroup,
|
fetchExportMatnrGroupReport,
|
fetchGetMatnrGroupDetail,
|
fetchGetMatnrGroupMany,
|
fetchMatnrGroupPage,
|
fetchMatnrGroupTree,
|
fetchSaveMatnrGroup,
|
fetchUpdateMatnrGroup
|
} from '@/api/matnr-group'
|
import MatnrGroupDialog from './modules/matnr-group-dialog.vue'
|
import MatnrGroupDetailDrawer from './modules/matnr-group-detail-drawer.vue'
|
import { createMatnrGroupTableColumns } from './matnrGroupTable.columns'
|
import {
|
MATNR_GROUP_REPORT_STYLE,
|
MATNR_GROUP_REPORT_TITLE,
|
buildMatnrGroupDialogModel,
|
buildMatnrGroupPageQueryParams,
|
buildMatnrGroupPrintRows,
|
buildMatnrGroupReportMeta,
|
buildMatnrGroupSavePayload,
|
buildMatnrGroupTreeLookupMap,
|
buildMatnrGroupTreeQueryParams,
|
createMatnrGroupSearchState,
|
createMatnrGroupTreeSelectOptions,
|
getMatnrGroupPaginationKey,
|
normalizeMatnrGroupDetailRecord,
|
normalizeMatnrGroupListRow,
|
normalizeMatnrGroupTreeRows,
|
resolveMatnrGroupTreeNodeLabel
|
} from './matnrGroupPage.helpers'
|
|
defineOptions({ name: 'MatnrGroup' })
|
|
const { hasAuth } = useAuth()
|
const userStore = useUserStore()
|
|
const searchForm = ref(createMatnrGroupSearchState())
|
const groupSearch = ref('')
|
const groupTreeLoading = ref(false)
|
const groupTreeData = ref([])
|
const selectedGroupId = ref(null)
|
const detailDrawerVisible = ref(false)
|
const detailLoading = ref(false)
|
const detailData = ref({})
|
let handleDeleteAction = null
|
|
const reportTitle = MATNR_GROUP_REPORT_TITLE
|
const reportQueryParams = computed(() =>
|
buildMatnrGroupPageQueryParams({
|
...searchForm.value,
|
parentId: selectedGroupId.value ?? ''
|
})
|
)
|
|
const groupLookupMap = computed(() => buildMatnrGroupTreeLookupMap(groupTreeData.value))
|
const parentGroupOptions = computed(() => createMatnrGroupTreeSelectOptions(groupTreeData.value))
|
|
const treeProps = {
|
label: 'displayLabel',
|
children: 'children'
|
}
|
|
const searchItems = computed(() => [
|
{
|
label: '关键字',
|
key: 'condition',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: '请输入分组编码/名称/备注'
|
}
|
},
|
{
|
label: '分组编码',
|
key: 'code',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: '请输入分组编码'
|
}
|
},
|
{
|
label: '分组名称',
|
key: 'name',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: '请输入分组名称'
|
}
|
},
|
{
|
label: '上级编码',
|
key: 'parCode',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: '请输入上级编码'
|
}
|
},
|
{
|
label: '状态',
|
key: 'status',
|
type: 'select',
|
props: {
|
clearable: true,
|
options: [
|
{ label: '正常', value: 1 },
|
{ label: '冻结', value: 0 }
|
]
|
}
|
},
|
{
|
label: '备注',
|
key: 'memo',
|
type: 'input',
|
props: {
|
clearable: true,
|
placeholder: '请输入备注'
|
}
|
}
|
])
|
|
function resolveGroupCode(id) {
|
return groupLookupMap.value.get(Number(id))?.code || ''
|
}
|
|
function resolveGroupLabel(id) {
|
const node = groupLookupMap.value.get(Number(id))
|
if (!node) {
|
return ''
|
}
|
return node.displayLabel || resolveMatnrGroupTreeNodeLabel(node)
|
}
|
|
const selectedGroupLabel = computed(() => {
|
if (selectedGroupId.value === null || selectedGroupId.value === undefined) {
|
return '全部分组'
|
}
|
return resolveGroupLabel(selectedGroupId.value) || '全部分组'
|
})
|
|
async function applyTableFilters() {
|
replaceSearchParams(
|
buildMatnrGroupPageQueryParams({
|
...searchForm.value,
|
parentId: selectedGroupId.value ?? ''
|
})
|
)
|
await getData()
|
}
|
|
async function loadGroupTree() {
|
groupTreeLoading.value = true
|
let selectionCleared = false
|
try {
|
const records = await guardRequestWithMessage(
|
fetchMatnrGroupTree(buildMatnrGroupTreeQueryParams({ condition: groupSearch.value })),
|
[],
|
{ timeoutMessage: '物料分组加载超时,已停止等待' }
|
)
|
const normalizedTree = normalizeMatnrGroupTreeRows(Array.isArray(records) ? records : [])
|
groupTreeData.value = normalizedTree
|
if (selectedGroupId.value && !groupLookupMap.value.get(Number(selectedGroupId.value))) {
|
selectedGroupId.value = null
|
selectionCleared = true
|
}
|
if (selectionCleared) {
|
await applyTableFilters()
|
}
|
} catch (error) {
|
groupTreeData.value = []
|
ElMessage.error(error?.message || '物料分组加载失败')
|
} finally {
|
groupTreeLoading.value = false
|
}
|
return selectionCleared
|
}
|
|
async function loadGroupDetail(id) {
|
return guardRequestWithMessage(fetchGetMatnrGroupDetail(id), {}, {
|
timeoutMessage: '物料分组详情加载超时,已停止等待'
|
})
|
}
|
|
const { columns, columnChecks, data, loading, pagination, getData, replaceSearchParams, resetSearchParams, handleSizeChange, handleCurrentChange, refreshData, refreshCreate, refreshUpdate, refreshRemove } =
|
useTable({
|
core: {
|
apiFn: fetchMatnrGroupPage,
|
apiParams: buildMatnrGroupPageQueryParams(reportQueryParams.value),
|
paginationKey: getMatnrGroupPaginationKey(),
|
columnsFactory: () =>
|
createMatnrGroupTableColumns({
|
handleView: openDetail,
|
handleEdit: hasAuth('update') ? openEditDialog : null,
|
handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
|
resolveParentLabel: resolveGroupLabel,
|
canEdit: hasAuth('update'),
|
canDelete: hasAuth('delete')
|
})
|
},
|
transform: {
|
dataTransformer: (records) => {
|
if (!Array.isArray(records)) {
|
return []
|
}
|
return records.map((item) => normalizeMatnrGroupListRow(item, resolveGroupLabel))
|
}
|
}
|
})
|
|
const {
|
dialogVisible,
|
dialogType,
|
currentRecord: currentGroupData,
|
selectedRows,
|
handleSelectionChange,
|
showDialog,
|
handleDialogSubmit,
|
handleDelete,
|
handleBatchDelete
|
} = useCrudPage({
|
createEmptyModel: () =>
|
buildMatnrGroupDialogModel({}, {
|
parentId: selectedGroupId.value ?? 0,
|
parCode: resolveGroupCode(selectedGroupId.value ?? 0),
|
resolveParentCode: resolveGroupCode
|
}),
|
buildEditModel: (record) => buildMatnrGroupDialogModel(record, { resolveParentCode: resolveGroupCode }),
|
buildSavePayload: (formData) => buildMatnrGroupSavePayload(formData),
|
saveRequest: (payload) => {
|
if (payload.id !== void 0 && payload.id !== null && Number(payload.parentId) === Number(payload.id)) {
|
throw new Error('上级分组不能选择当前分组')
|
}
|
return fetchSaveMatnrGroup(payload)
|
},
|
updateRequest: (payload) => {
|
if (payload.id !== void 0 && payload.id !== null && Number(payload.parentId) === Number(payload.id)) {
|
throw new Error('上级分组不能选择当前分组')
|
}
|
return fetchUpdateMatnrGroup(payload)
|
},
|
deleteRequest: fetchDeleteMatnrGroup,
|
entityName: '物料分组',
|
resolveRecordLabel: (record) => record?.name || record?.code || record?.id,
|
refreshCreate: refreshTreeAndTable,
|
refreshUpdate: refreshTreeAndTable,
|
refreshRemove: refreshTreeAndTable
|
})
|
handleDeleteAction = handleDelete
|
|
async function refreshTreeAndTable() {
|
const selectionCleared = await loadGroupTree()
|
if (!selectionCleared) {
|
await refreshData()
|
}
|
}
|
|
const buildPreviewMeta = (rows) => {
|
const now = new Date()
|
return {
|
reportDate: now.toLocaleDateString('zh-CN'),
|
printedAt: now.toLocaleString('zh-CN', { hour12: false }),
|
operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
|
count: rows.length,
|
reportStyle: { ...MATNR_GROUP_REPORT_STYLE }
|
}
|
}
|
|
const resolvePrintRecords = async (payload) => {
|
if (Array.isArray(payload?.ids) && payload.ids.length > 0) {
|
return defaultResponseAdapter(await fetchGetMatnrGroupMany(payload.ids)).records
|
}
|
return defaultResponseAdapter(
|
await fetchMatnrGroupPage({
|
...reportQueryParams.value,
|
current: 1,
|
pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
|
})
|
).records
|
}
|
|
const {
|
previewVisible,
|
previewRows,
|
previewMeta,
|
handlePreviewVisibleChange,
|
handleExport,
|
handlePrint
|
} = usePrintExportPage({
|
downloadFileName: 'matnr-group.xlsx',
|
requestExport: (payload) =>
|
fetchExportMatnrGroupReport(payload, {
|
headers: {
|
Authorization: userStore.accessToken || ''
|
}
|
}),
|
resolvePrintRecords,
|
buildPreviewRows: (records) => buildMatnrGroupPrintRows(records, resolveGroupLabel),
|
buildPreviewMeta
|
})
|
|
const resolvedPreviewMeta = computed(() =>
|
buildMatnrGroupReportMeta({
|
previewMeta: previewMeta.value,
|
count: previewRows.value.length,
|
orientation: previewMeta.value?.reportStyle?.orientation || MATNR_GROUP_REPORT_STYLE.orientation
|
})
|
)
|
|
function handleSearch(params) {
|
replaceSearchParams(
|
buildMatnrGroupPageQueryParams({
|
...params,
|
parentId: selectedGroupId.value ?? ''
|
})
|
)
|
getData()
|
}
|
|
function handleReset() {
|
Object.assign(searchForm.value, createMatnrGroupSearchState())
|
selectedGroupId.value = null
|
resetSearchParams()
|
}
|
|
async function handleGroupSearch() {
|
await loadGroupTree()
|
}
|
|
async function handleGroupNodeClick(node) {
|
selectedGroupId.value = Number(node?.id || 0) || null
|
await applyTableFilters()
|
}
|
|
async function handleResetGroup() {
|
selectedGroupId.value = null
|
await applyTableFilters()
|
}
|
|
async function openDetail(row) {
|
detailDrawerVisible.value = true
|
detailLoading.value = true
|
try {
|
const detail = await loadGroupDetail(row.id)
|
detailData.value = normalizeMatnrGroupDetailRecord(detail, resolveGroupLabel)
|
} catch (error) {
|
detailDrawerVisible.value = false
|
detailData.value = {}
|
ElMessage.error(error?.message || '获取物料分组详情失败')
|
} finally {
|
detailLoading.value = false
|
}
|
}
|
|
async function openEditDialog(row) {
|
try {
|
const detail = await loadGroupDetail(row.id)
|
showDialog('edit', detail)
|
} catch (error) {
|
ElMessage.error(error?.message || '获取物料分组详情失败')
|
}
|
}
|
|
onMounted(async () => {
|
await loadGroupTree()
|
await getData()
|
})
|
</script>
|