zhou zhou
2026-03-30 e9283ffe6822b12ec5dd2ccf4dc13a369b227a61
rsf-design/src/views/system/role/index.vue
@@ -5,8 +5,8 @@
      v-show="showSearchBar"
      v-model="searchForm"
      @search="handleSearch"
      @reset="resetSearchParams"
    ></RoleSearch>
      @reset="handleReset"
    />
    <ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
      <ArtTableHeader
@@ -17,62 +17,115 @@
      >
        <template #left>
          <ElSpace wrap>
            <ElButton @click="showDialog('add')" v-ripple>新增角色</ElButton>
            <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>新增角色</ElButton>
            <ElButton
              v-auth="'delete'"
              :disabled="selectedRows.length === 0"
              @click="handleBatchDelete"
              v-ripple
              >
                批量删除
              </ElButton>
            <span v-auth="'query'" class="inline-flex">
              <ListExportPrint
                :preview-visible="previewVisible"
                @update:previewVisible="handlePreviewVisibleChange"
                :report-title="reportTitle"
                :selected-rows="selectedRows"
                :query-params="reportQueryParams"
                :columns="roleReportColumns"
                :preview-rows="previewRows"
                :preview-meta="resolvedPreviewMeta"
                :total="pagination.total"
                :disabled="loading"
                @export="handleExport"
                @print="handlePrint"
              />
            </span>
          </ElSpace>
        </template>
      </ArtTableHeader>
      <!-- 表格 -->
      <ArtTable
        :loading="loading"
        :data="data"
        :columns="columns"
        :pagination="pagination"
        @selection-change="handleSelectionChange"
        @pagination:size-change="handleSizeChange"
        @pagination:current-change="handleCurrentChange"
      >
      </ArtTable>
      />
    </ElCard>
    <!-- 角色编辑弹窗 -->
    <RoleEditDialog
      v-model="dialogVisible"
      v-model:visible="dialogVisible"
      :dialog-type="dialogType"
      :role-data="currentRoleData"
      @success="refreshData"
      @submit="handleDialogSubmit"
    />
    <!-- 菜单权限弹窗 -->
    <RolePermissionDialog
      v-model="permissionDialog"
      v-model:visible="permissionDialogVisible"
      :role-data="currentRoleData"
      :scope-type="permissionScopeType"
      @success="refreshData"
    />
  </div>
</template>
<script setup>
  import { useUserStore } from '@/store/modules/user'
  import {
    fetchExportRoleReport,
    fetchDeleteRole,
    fetchGetRoleMany,
    fetchRolePrintPage,
    fetchRolePage,
    fetchSaveRole,
    fetchUpdateRole
  } from '@/api/system-manage'
  import { useTable } from '@/hooks/core/useTable'
  import ListExportPrint from '@/components/biz/list-export-print/index.vue'
  import RoleSearch from './modules/role-search.vue'
  import RoleEditDialog from './modules/role-edit-dialog.vue'
  import RolePermissionDialog from './modules/role-permission-dialog.vue'
  import { useTable } from '@/hooks/core/useTable'
  import { fetchGetRoleList } from '@/api/system-manage'
  import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
  import { ElTag, ElMessageBox } from 'element-plus'
  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
  import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
  import {
    buildRoleDialogModel,
    buildRolePageQueryParams,
    buildRolePrintRows,
    buildRoleReportMeta,
    buildRoleSavePayload,
    buildRoleSearchParams,
    createRoleSearchState,
    getRoleStatusMeta,
    normalizeRoleListRow,
    ROLE_REPORT_STYLE,
    ROLE_REPORT_TITLE,
    resolveRoleReportColumns
  } from './rolePage.helpers'
  defineOptions({ name: 'Role' })
  const searchForm = ref({
    roleName: void 0,
    roleCode: void 0,
    description: void 0,
    enabled: void 0,
    daterange: void 0
  })
  const searchForm = ref(createRoleSearchState())
  const showSearchBar = ref(false)
  const dialogVisible = ref(false)
  const permissionDialog = ref(false)
  const currentRoleData = ref(void 0)
  const dialogType = ref('add')
  const currentRoleData = ref(buildRoleDialogModel())
  const permissionDialogVisible = ref(false)
  const permissionScopeType = ref('menu')
  const selectedRows = ref([])
  const previewVisible = ref(false)
  const previewRows = ref([])
  const previewMeta = ref({})
  const previewToken = ref(0)
  const activePrintToken = ref(0)
  const userStore = useUserStore()
  const reportTitle = ROLE_REPORT_TITLE
  const reportQueryParams = computed(() => buildRoleSearchParams(searchForm.value))
  const {
    columns,
    columnChecks,
@@ -84,130 +137,313 @@
    resetSearchParams,
    handleSizeChange,
    handleCurrentChange,
    refreshData
    refreshData,
    refreshCreate,
    refreshUpdate,
    refreshRemove
  } = useTable({
    // 核心配置
    core: {
      apiFn: fetchGetRoleList,
      apiParams: {
        current: 1,
        size: 20
      },
      // 排除 apiParams 中的属性
      excludeParams: ['daterange'],
      apiFn: fetchRolePage,
      apiParams: buildRolePageQueryParams(searchForm.value),
      columnsFactory: () => [
        { type: 'selection', width: 52, fixed: 'left' },
        {
          prop: 'roleId',
          label: '角色ID',
          width: 100
        },
        {
          prop: 'roleName',
          prop: 'name',
          label: '角色名称',
          minWidth: 120
        },
        {
          prop: 'roleCode',
          label: '角色编码',
          minWidth: 120
        },
        {
          prop: 'description',
          label: '角色描述',
          minWidth: 150,
          minWidth: 140,
          showOverflowTooltip: true
        },
        {
          prop: 'enabled',
          label: '角色状态',
          width: 100,
          prop: 'code',
          label: '角色编码',
          minWidth: 140,
          showOverflowTooltip: true
        },
        {
          prop: 'memo',
          label: '备注',
          minWidth: 180,
          showOverflowTooltip: true
        },
        {
          prop: 'status',
          label: '状态',
          width: 120,
          formatter: (row) => {
            const statusConfig = row.enabled
              ? { type: 'success', text: '启用' }
              : { type: 'warning', text: '禁用' }
            return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
            const statusMeta = getRoleStatusMeta(row.statusBool ?? row.status)
            return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
          }
        },
        {
          prop: 'createTime',
          label: '创建日期',
          width: 180,
          sortable: true
          prop: 'updateTimeText',
          label: '更新时间',
          minWidth: 180,
          sortable: true,
          formatter: (row) => row.updateTimeText || '-'
        },
        {
          prop: 'createTimeText',
          label: '创建时间',
          minWidth: 180,
          sortable: true,
          formatter: (row) => row.createTimeText || '-'
        },
        {
          prop: 'operation',
          label: '操作',
          width: 80,
          width: 120,
          fixed: 'right',
          formatter: (row) =>
            h('div', [
              h(ArtButtonMore, {
                list: [
                  {
                    key: 'permission',
                    label: '菜单权限',
                    icon: 'ri:user-3-line'
                    key: 'scope-menu',
                    label: '网页权限',
                    icon: 'ri:layout-2-line',
                    auth: 'edit'
                  },
                  {
                    key: 'scope-pda',
                    label: 'PDA权限',
                    icon: 'ri:smartphone-line',
                    auth: 'edit'
                  },
                  {
                    key: 'scope-matnr',
                    label: '物料权限',
                    icon: 'ri:archive-line',
                    auth: 'edit'
                  },
                  {
                    key: 'scope-warehouse',
                    label: '仓库权限',
                    icon: 'ri:store-2-line',
                    auth: 'edit'
                  },
                  {
                    key: 'edit',
                    label: '编辑角色',
                    icon: 'ri:edit-2-line'
                    icon: 'ri:edit-2-line',
                    auth: 'edit'
                  },
                  {
                    key: 'delete',
                    label: '删除角色',
                    icon: 'ri:delete-bin-4-line',
                    color: '#f56c6c'
                    color: '#f56c6c',
                    auth: 'delete'
                  }
                ],
                onClick: (item) => buttonMoreClick(item, row)
                onClick: (item) => handleActionClick(item, row)
              })
            ])
        }
      ]
    },
    transform: {
      dataTransformer: (records) => {
        if (!Array.isArray(records)) {
          return []
        }
        return records.map((item) => normalizeRoleListRow(item))
      }
    }
  })
  const dialogType = ref('add')
  const showDialog = (type, row) => {
    dialogVisible.value = true
    dialogType.value = type
    currentRoleData.value = row
  }
  const roleReportColumns = computed(() => resolveRoleReportColumns(columns.value))
  const resolvedPreviewMeta = computed(() =>
    buildRoleReportMeta({
      previewMeta: previewMeta.value,
      count: previewRows.value.length,
      titleAlign: ROLE_REPORT_STYLE.titleAlign,
      titleLevel: ROLE_REPORT_STYLE.titleLevel
    })
  )
  const handleSearch = (params) => {
    const { daterange, ...filtersParams } = params
    const [startTime, endTime] = Array.isArray(daterange) ? daterange : [null, null]
    replaceSearchParams({ ...filtersParams, startTime, endTime })
    replaceSearchParams(buildRoleSearchParams(params))
    getData()
  }
  const buttonMoreClick = (item, row) => {
  const handleReset = () => {
    Object.assign(searchForm.value, createRoleSearchState())
    resetSearchParams()
  }
  const handleSelectionChange = (rows) => {
    selectedRows.value = Array.isArray(rows) ? rows : []
  }
  const handlePreviewVisibleChange = (visible) => {
    previewVisible.value = Boolean(visible)
    if (!visible) {
      activePrintToken.value = 0
    }
  }
  const showDialog = (type, row) => {
    dialogType.value = type
    currentRoleData.value = type === 'edit' ? buildRoleDialogModel(row) : buildRoleDialogModel()
    dialogVisible.value = true
  }
  const handleActionClick = (item, row) => {
    switch (item.key) {
      case 'permission':
        showPermissionDialog(row)
      case 'scope-menu':
        openScopeDialog('menu', row)
        break
      case 'scope-pda':
        openScopeDialog('pda', row)
        break
      case 'scope-matnr':
        openScopeDialog('matnr', row)
        break
      case 'scope-warehouse':
        openScopeDialog('warehouse', row)
        break
      case 'edit':
        showDialog('edit', row)
        break
      case 'delete':
        deleteRole(row)
        handleDelete(row)
        break
      default:
        break
    }
  }
  const showPermissionDialog = (row) => {
    permissionDialog.value = true
    currentRoleData.value = row
  const openScopeDialog = (scopeType, row) => {
    permissionScopeType.value = scopeType
    currentRoleData.value = buildRoleDialogModel(row)
    permissionDialogVisible.value = true
  }
  const deleteRole = (row) => {
    ElMessageBox.confirm(`确定删除角色"${row.roleName}"吗?此操作不可恢复!`, '删除确认', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    })
      .then(() => {
        ElMessage.success('删除成功')
        refreshData()
  const handleDialogSubmit = async (formData) => {
    const payload = buildRoleSavePayload(formData)
    try {
      if (dialogType.value === 'edit') {
        await fetchUpdateRole(payload)
        ElMessage.success('修改成功')
        dialogVisible.value = false
        currentRoleData.value = buildRoleDialogModel()
        await refreshUpdate()
        return
      }
      await fetchSaveRole(payload)
      ElMessage.success('新增成功')
      dialogVisible.value = false
      currentRoleData.value = buildRoleDialogModel()
      await refreshCreate()
    } catch (error) {
      ElMessage.error(error?.message || '提交失败')
    }
  }
  const handleDelete = async (row) => {
    try {
      await ElMessageBox.confirm(`确定要删除角色「${row.name || row.code || row.id}」吗?`, '删除确认', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      })
      .catch(() => {
        ElMessage.info('已取消删除')
      await fetchDeleteRole(row.id)
      ElMessage.success('删除成功')
      await refreshRemove()
    } catch (error) {
      if (error !== 'cancel') {
        ElMessage.error(error?.message || '删除失败')
      }
    }
  }
  const handleBatchDelete = async () => {
    if (!selectedRows.value.length) return
    const ids = selectedRows.value.map((item) => item.id).filter((id) => id !== void 0 && id !== null)
    if (!ids.length) return
    try {
      await ElMessageBox.confirm(`确定要批量删除选中的 ${ids.length} 个角色吗?`, '批量删除确认', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      })
      await fetchDeleteRole(ids.join(','))
      ElMessage.success('批量删除成功')
      selectedRows.value = []
      await refreshRemove()
    } catch (error) {
      if (error !== 'cancel') {
        ElMessage.error(error?.message || '批量删除失败')
      }
    }
  }
  const handleExport = async (payload) => {
    try {
      const response = await fetchExportRoleReport(payload, {
        headers: {
          Authorization: userStore.accessToken || ''
        }
      })
      if (!response.ok) {
        throw new Error(`导出失败 (${response.status})`)
      }
      const blob = await response.blob()
      const downloadUrl = window.URL.createObjectURL(blob)
      const link = document.createElement('a')
      link.href = downloadUrl
      link.download = 'role.xlsx'
      document.body.appendChild(link)
      link.click()
      link.remove()
      window.URL.revokeObjectURL(downloadUrl)
      ElMessage.success('导出成功')
    } catch (error) {
      ElMessage.error(error?.message || '导出失败')
    }
  }
  const handlePrint = async (payload) => {
    const token = previewToken.value + 1
    previewToken.value = token
    activePrintToken.value = token
    previewVisible.value = false
    previewRows.value = []
    previewMeta.value = {}
    try {
      const response = Array.isArray(payload?.ids) && payload.ids.length > 0
        ? await fetchGetRoleMany(payload.ids)
        : await fetchRolePrintPage({
            ...reportQueryParams.value,
            current: 1,
            pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
          })
      if (activePrintToken.value !== token) {
        return
      }
      const records = defaultResponseAdapter(response).records
      if (activePrintToken.value !== token) {
        return
      }
      const rows = buildRolePrintRows(records)
      const now = new Date()
      previewRows.value = rows
      previewMeta.value = {
        reportTitle,
        reportDate: now.toLocaleDateString('zh-CN'),
        printedAt: now.toLocaleString('zh-CN', { hour12: false }),
        operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
        count: rows.length
      }
      handlePreviewVisibleChange(true)
    } catch (error) {
      if (activePrintToken.value !== token) {
        return
      }
      ElMessage.error(error?.message || '打印失败')
    }
  }
</script>