zhou zhou
22 小时以前 1553782fd262f97a336fecc8b38f8f309fc08ae6
rsf-design/src/views/basic-info/matnr-group/index.vue
@@ -1,141 +1,99 @@
<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
@@ -147,7 +105,6 @@
    MATNR_GROUP_REPORT_STYLE,
    MATNR_GROUP_REPORT_TITLE,
    buildMatnrGroupDialogModel,
    buildMatnrGroupPageQueryParams,
    buildMatnrGroupPrintRows,
    buildMatnrGroupReportMeta,
    buildMatnrGroupSavePayload,
@@ -155,11 +112,8 @@
    buildMatnrGroupTreeQueryParams,
    createMatnrGroupSearchState,
    createMatnrGroupTreeSelectOptions,
    getMatnrGroupPaginationKey,
    normalizeMatnrGroupDetailRecord,
    normalizeMatnrGroupListRow,
    normalizeMatnrGroupTreeRows,
    resolveMatnrGroupTreeNodeLabel
    normalizeMatnrGroupTreeRows
  } from './matnrGroupPage.helpers'
  defineOptions({ name: 'MatnrGroup' })
@@ -167,31 +121,23 @@
  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(() => [
    {
@@ -202,56 +148,16 @@
        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 || ''
@@ -262,51 +168,53 @@
    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) {
@@ -314,32 +222,6 @@
      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,
@@ -354,8 +236,8 @@
  } = useCrudPage({
    createEmptyModel: () =>
      buildMatnrGroupDialogModel({}, {
        parentId: selectedGroupId.value ?? 0,
        parCode: resolveGroupCode(selectedGroupId.value ?? 0),
        parentId: 0,
        parCode: '',
        resolveParentCode: resolveGroupCode
      }),
    buildEditModel: (record) => buildMatnrGroupDialogModel(record, { resolveParentCode: resolveGroupCode }),
@@ -375,18 +257,11 @@
    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()
@@ -401,15 +276,9 @@
  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 {
@@ -440,34 +309,17 @@
    })
  )
  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) {
@@ -494,8 +346,12 @@
    }
  }
  onMounted(async () => {
    await loadGroupTree()
    await getData()
  function toggleExpand() {
    isExpanded.value = !isExpanded.value
    syncTreeExpandState()
  }
  onMounted(() => {
    loadGroupTree()
  })
</script>