| | |
| | | <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="matnr-group-page art-full-height"> |
| | | <ArtSearchBar |
| | | v-model="searchForm" |
| | | :items="searchItems" |
| | | :show-expand="false" |
| | | @search="handleSearch" |
| | | @reset="handleReset" |
| | | /> |
| | | |
| | | <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> |
| | | <ElCard class="art-table-card"> |
| | | <ArtTableHeader |
| | | :loading="loading" |
| | | :show-zebra="false" |
| | | v-model:columns="columnChecks" |
| | | @refresh="handleRefresh" |
| | | > |
| | | <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> |
| | | <ElButton @click="toggleExpand" v-ripple> |
| | | {{ isExpanded ? '收起' : '展开' }} |
| | | </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="flattenedTableRows.length" |
| | | :disabled="loading" |
| | | @export="handleExport" |
| | | @print="handlePrint" |
| | | /> |
| | | </ElSpace> |
| | | </template> |
| | | </ArtTableHeader> |
| | | |
| | | <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" |
| | | <ArtTable |
| | | ref="tableRef" |
| | | rowKey="id" |
| | | :loading="loading" |
| | | :columns="columns" |
| | | :data="tableData" |
| | | :stripe="false" |
| | | :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" |
| | | :default-expand-all="true" |
| | | @selection-change="handleSelectionChange" |
| | | /> |
| | | |
| | | <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> |
| | | <MatnrGroupDialog |
| | | v-model:visible="dialogVisible" |
| | | :dialog-type="dialogType" |
| | | :group-data="currentGroupData" |
| | | :parent-group-options="parentGroupOptions" |
| | | :resolve-parent-code="resolveGroupCode" |
| | | @submit="handleDialogSubmit" |
| | | /> |
| | | |
| | | <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> |
| | | <MatnrGroupDetailDrawer |
| | | v-model:visible="detailDrawerVisible" |
| | | :loading="detailLoading" |
| | | :detail="detailData" |
| | | /> |
| | | </ElCard> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { computed, nextTick, onMounted, reactive, ref } from 'vue' |
| | | 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 |
| | |
| | | MATNR_GROUP_REPORT_STYLE, |
| | | MATNR_GROUP_REPORT_TITLE, |
| | | buildMatnrGroupDialogModel, |
| | | buildMatnrGroupPageQueryParams, |
| | | buildMatnrGroupPrintRows, |
| | | buildMatnrGroupReportMeta, |
| | | buildMatnrGroupSavePayload, |
| | |
| | | buildMatnrGroupTreeQueryParams, |
| | | createMatnrGroupSearchState, |
| | | createMatnrGroupTreeSelectOptions, |
| | | getMatnrGroupPaginationKey, |
| | | normalizeMatnrGroupDetailRecord, |
| | | normalizeMatnrGroupListRow, |
| | | normalizeMatnrGroupTreeRows, |
| | | resolveMatnrGroupTreeNodeLabel |
| | | normalizeMatnrGroupTreeRows |
| | | } 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 loading = ref(false) |
| | | const isExpanded = ref(true) |
| | | const tableRef = ref() |
| | | const detailDrawerVisible = ref(false) |
| | | const detailLoading = ref(false) |
| | | const detailData = ref({}) |
| | | const tableData = ref([]) |
| | | let handleDeleteAction = null |
| | | |
| | | const reportTitle = MATNR_GROUP_REPORT_TITLE |
| | | const reportQueryParams = computed(() => |
| | | buildMatnrGroupPageQueryParams({ |
| | | ...searchForm.value, |
| | | parentId: selectedGroupId.value ?? '' |
| | | }) |
| | | ) |
| | | const initialSearchState = createMatnrGroupSearchState() |
| | | const searchForm = reactive({ ...initialSearchState }) |
| | | |
| | | const groupLookupMap = computed(() => buildMatnrGroupTreeLookupMap(groupTreeData.value)) |
| | | const parentGroupOptions = computed(() => createMatnrGroupTreeSelectOptions(groupTreeData.value)) |
| | | |
| | | const treeProps = { |
| | | label: 'displayLabel', |
| | | children: 'children' |
| | | } |
| | | const reportQueryParams = computed(() => buildMatnrGroupTreeQueryParams(searchForm)) |
| | | const groupLookupMap = computed(() => buildMatnrGroupTreeLookupMap(tableData.value)) |
| | | const parentGroupOptions = computed(() => createMatnrGroupTreeSelectOptions(tableData.value)) |
| | | const flattenedTableRows = computed(() => flattenMatnrGroupRows(tableData.value)) |
| | | |
| | | const searchItems = computed(() => [ |
| | | { |
| | |
| | | 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 flattenMatnrGroupRows(records = []) { |
| | | if (!Array.isArray(records)) { |
| | | return [] |
| | | } |
| | | |
| | | return records.flatMap((item) => [item, ...flattenMatnrGroupRows(item?.children || [])]) |
| | | } |
| | | |
| | | function resolveGroupCode(id) { |
| | | return groupLookupMap.value.get(Number(id))?.code || '' |
| | |
| | | if (!node) { |
| | | return '' |
| | | } |
| | | return node.displayLabel || resolveMatnrGroupTreeNodeLabel(node) |
| | | return node.displayLabel || node.label || [node.name, node.code].filter(Boolean).join(' · ') |
| | | } |
| | | |
| | | const selectedGroupLabel = computed(() => { |
| | | if (selectedGroupId.value === null || selectedGroupId.value === undefined) { |
| | | return '全部分组' |
| | | } |
| | | return resolveGroupLabel(selectedGroupId.value) || '全部分组' |
| | | }) |
| | | const { columnChecks, columns } = useTableColumns(() => |
| | | createMatnrGroupTableColumns({ |
| | | handleView: openDetail, |
| | | handleEdit: hasAuth('update') ? openEditDialog : null, |
| | | handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null, |
| | | canEdit: hasAuth('update'), |
| | | canDelete: hasAuth('delete') |
| | | }) |
| | | ) |
| | | |
| | | async function applyTableFilters() { |
| | | replaceSearchParams( |
| | | buildMatnrGroupPageQueryParams({ |
| | | ...searchForm.value, |
| | | parentId: selectedGroupId.value ?? '' |
| | | async function syncTreeExpandState() { |
| | | await nextTick() |
| | | if (!tableRef.value?.elTableRef) { |
| | | return |
| | | } |
| | | |
| | | const toggleRows = (rows = []) => { |
| | | rows.forEach((row) => { |
| | | if (Array.isArray(row.children) && row.children.length) { |
| | | tableRef.value.elTableRef.toggleRowExpansion(row, isExpanded.value) |
| | | toggleRows(row.children) |
| | | } |
| | | }) |
| | | ) |
| | | await getData() |
| | | } |
| | | |
| | | toggleRows(tableData.value) |
| | | } |
| | | |
| | | async function loadGroupTree() { |
| | | groupTreeLoading.value = true |
| | | let selectionCleared = false |
| | | loading.value = true |
| | | try { |
| | | const records = await guardRequestWithMessage( |
| | | fetchMatnrGroupTree(buildMatnrGroupTreeQueryParams({ condition: groupSearch.value })), |
| | | fetchMatnrGroupTree(buildMatnrGroupTreeQueryParams(searchForm)), |
| | | [], |
| | | { 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() |
| | | } |
| | | tableData.value = normalizeMatnrGroupTreeRows(Array.isArray(records) ? records : []) |
| | | await syncTreeExpandState() |
| | | } catch (error) { |
| | | groupTreeData.value = [] |
| | | tableData.value = [] |
| | | ElMessage.error(error?.message || '物料分组加载失败') |
| | | } finally { |
| | | groupTreeLoading.value = false |
| | | loading.value = false |
| | | } |
| | | return selectionCleared |
| | | } |
| | | |
| | | async function loadGroupDetail(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, |
| | |
| | | } = useCrudPage({ |
| | | createEmptyModel: () => |
| | | buildMatnrGroupDialogModel({}, { |
| | | parentId: selectedGroupId.value ?? 0, |
| | | parCode: resolveGroupCode(selectedGroupId.value ?? 0), |
| | | parentId: 0, |
| | | parCode: '', |
| | | resolveParentCode: resolveGroupCode |
| | | }), |
| | | buildEditModel: (record) => buildMatnrGroupDialogModel(record, { resolveParentCode: resolveGroupCode }), |
| | |
| | | deleteRequest: fetchDeleteMatnrGroup, |
| | | entityName: '物料分组', |
| | | resolveRecordLabel: (record) => record?.name || record?.code || record?.id, |
| | | refreshCreate: refreshTreeAndTable, |
| | | refreshUpdate: refreshTreeAndTable, |
| | | refreshRemove: refreshTreeAndTable |
| | | refreshCreate: loadGroupTree, |
| | | refreshUpdate: loadGroupTree, |
| | | refreshRemove: loadGroupTree |
| | | }) |
| | | handleDeleteAction = handleDelete |
| | | |
| | | async function refreshTreeAndTable() { |
| | | const selectionCleared = await loadGroupTree() |
| | | if (!selectionCleared) { |
| | | await refreshData() |
| | | } |
| | | } |
| | | |
| | | const buildPreviewMeta = (rows) => { |
| | | const now = new Date() |
| | |
| | | |
| | | const resolvePrintRecords = async (payload) => { |
| | | if (Array.isArray(payload?.ids) && payload.ids.length > 0) { |
| | | return defaultResponseAdapter(await fetchGetMatnrGroupMany(payload.ids)).records |
| | | return await fetchGetMatnrGroupMany(payload.ids) |
| | | } |
| | | return defaultResponseAdapter( |
| | | await fetchMatnrGroupPage({ |
| | | ...reportQueryParams.value, |
| | | current: 1, |
| | | pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20 |
| | | }) |
| | | ).records |
| | | return flattenedTableRows.value |
| | | } |
| | | |
| | | const { |
| | |
| | | }) |
| | | ) |
| | | |
| | | function handleSearch(params) { |
| | | replaceSearchParams( |
| | | buildMatnrGroupPageQueryParams({ |
| | | ...params, |
| | | parentId: selectedGroupId.value ?? '' |
| | | }) |
| | | ) |
| | | getData() |
| | | function handleSearch() { |
| | | loadGroupTree() |
| | | } |
| | | |
| | | function handleReset() { |
| | | Object.assign(searchForm.value, createMatnrGroupSearchState()) |
| | | selectedGroupId.value = null |
| | | resetSearchParams() |
| | | Object.assign(searchForm, { ...initialSearchState }) |
| | | loadGroupTree() |
| | | } |
| | | |
| | | 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() |
| | | function handleRefresh() { |
| | | loadGroupTree() |
| | | } |
| | | |
| | | async function openDetail(row) { |
| | |
| | | } |
| | | } |
| | | |
| | | onMounted(async () => { |
| | | await loadGroupTree() |
| | | await getData() |
| | | function toggleExpand() { |
| | | isExpanded.value = !isExpanded.value |
| | | syncTreeExpandState() |
| | | } |
| | | |
| | | onMounted(() => { |
| | | loadGroupTree() |
| | | }) |
| | | </script> |