From 3fdcf1d5e6468c735532e67bde5ff1cdf85bb0c6 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 09:14:16 +0800
Subject: [PATCH] refactor: simplify role page and fix pagination keys

---
 rsf-design/src/views/system/common/useCrudPage.js                   |  115 ++++++++
 rsf-design/tests/system-role-scope-contract.test.mjs                |   16 +
 rsf-design/src/views/system/common/usePrintExportPage.js            |   80 ++++++
 rsf-design/tests/system-user-page-contract.test.mjs                 |    8 
 rsf-design/src/views/system/user/index.vue                          |    2 
 rsf-design/src/views/system/role/roleTable.columns.js               |  104 ++++++++
 rsf-design/src/views/system/user/userPage.helpers.js                |    7 
 rsf-design/src/views/system/role/rolePage.helpers.js                |   11 
 rsf-design/src/views/system/role/index.vue                          |  409 ++++++++----------------------
 rsf-design/src/views/system/role/modules/role-permission-dialog.vue |   27 -
 10 files changed, 456 insertions(+), 323 deletions(-)

diff --git a/rsf-design/src/views/system/common/useCrudPage.js b/rsf-design/src/views/system/common/useCrudPage.js
new file mode 100644
index 0000000..2c2b7e8
--- /dev/null
+++ b/rsf-design/src/views/system/common/useCrudPage.js
@@ -0,0 +1,115 @@
+import { ref } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+
+export function useCrudPage({
+  createEmptyModel,
+  buildEditModel,
+  buildSavePayload,
+  saveRequest,
+  updateRequest,
+  deleteRequest,
+  entityName,
+  resolveRecordLabel,
+  refreshCreate,
+  refreshUpdate,
+  refreshRemove
+}) {
+  const dialogVisible = ref(false)
+  const dialogType = ref('add')
+  const currentRecord = ref(createEmptyModel())
+  const selectedRows = ref([])
+
+  const resetCurrentRecord = () => {
+    currentRecord.value = createEmptyModel()
+  }
+
+  const handleSelectionChange = (rows) => {
+    selectedRows.value = Array.isArray(rows) ? rows : []
+  }
+
+  const showDialog = (type, record) => {
+    dialogType.value = type
+    currentRecord.value = type === 'edit' ? buildEditModel(record) : createEmptyModel()
+    dialogVisible.value = true
+  }
+
+  const closeDialog = () => {
+    dialogVisible.value = false
+    resetCurrentRecord()
+  }
+
+  const handleDialogSubmit = async (formData) => {
+    const payload = buildSavePayload(formData)
+    try {
+      if (dialogType.value === 'edit') {
+        await updateRequest(payload)
+        ElMessage.success('淇敼鎴愬姛')
+        closeDialog()
+        await refreshUpdate?.()
+        return
+      }
+
+      await saveRequest(payload)
+      ElMessage.success('鏂板鎴愬姛')
+      closeDialog()
+      await refreshCreate?.()
+    } catch (error) {
+      ElMessage.error(error?.message || '鎻愪氦澶辫触')
+    }
+  }
+
+  const handleDelete = async (record) => {
+    try {
+      const recordLabel = resolveRecordLabel?.(record) || record?.id
+      await ElMessageBox.confirm(`纭畾瑕佸垹闄�${entityName}銆�${recordLabel}銆嶅悧锛焋, '鍒犻櫎纭', {
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        type: 'warning'
+      })
+      await deleteRequest(record.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} 涓�${entityName}鍚楋紵`, '鎵归噺鍒犻櫎纭', {
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        type: 'warning'
+      })
+      await deleteRequest(ids.join(','))
+      ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+      selectedRows.value = []
+      await refreshRemove?.()
+    } catch (error) {
+      if (error !== 'cancel') {
+        ElMessage.error(error?.message || '鎵归噺鍒犻櫎澶辫触')
+      }
+    }
+  }
+
+  return {
+    dialogVisible,
+    dialogType,
+    currentRecord,
+    selectedRows,
+    handleSelectionChange,
+    showDialog,
+    closeDialog,
+    handleDialogSubmit,
+    handleDelete,
+    handleBatchDelete
+  }
+}
diff --git a/rsf-design/src/views/system/common/usePrintExportPage.js b/rsf-design/src/views/system/common/usePrintExportPage.js
new file mode 100644
index 0000000..c202543
--- /dev/null
+++ b/rsf-design/src/views/system/common/usePrintExportPage.js
@@ -0,0 +1,80 @@
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+
+export function usePrintExportPage({
+  downloadFileName,
+  requestExport,
+  resolvePrintRecords,
+  buildPreviewRows,
+  buildPreviewMeta
+}) {
+  const previewVisible = ref(false)
+  const previewRows = ref([])
+  const previewMeta = ref({})
+  const previewToken = ref(0)
+  const activePrintToken = ref(0)
+
+  const handlePreviewVisibleChange = (visible) => {
+    previewVisible.value = Boolean(visible)
+    if (!visible) {
+      activePrintToken.value = 0
+    }
+  }
+
+  const handleExport = async (payload) => {
+    try {
+      const response = await requestExport(payload)
+      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 = downloadFileName
+      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 records = await resolvePrintRecords(payload)
+      if (activePrintToken.value !== token) {
+        return
+      }
+
+      const rows = buildPreviewRows(records)
+      previewRows.value = rows
+      previewMeta.value = buildPreviewMeta(rows)
+      handlePreviewVisibleChange(true)
+    } catch (error) {
+      if (activePrintToken.value !== token) {
+        return
+      }
+      ElMessage.error(error?.message || '鎵撳嵃澶辫触')
+    }
+  }
+
+  return {
+    previewVisible,
+    previewRows,
+    previewMeta,
+    handlePreviewVisibleChange,
+    handleExport,
+    handlePrint
+  }
+}
diff --git a/rsf-design/src/views/system/role/index.vue b/rsf-design/src/views/system/role/index.vue
index b6787aa..49d2e34 100644
--- a/rsf-design/src/views/system/role/index.vue
+++ b/rsf-design/src/views/system/role/index.vue
@@ -20,6 +20,7 @@
             <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板瑙掕壊</ElButton>
             <ElButton
               v-auth="'delete'"
+              type="danger"
               :disabled="selectedRows.length === 0"
               @click="handleBatchDelete"
               v-ripple
@@ -85,13 +86,14 @@
     fetchUpdateRole
   } from '@/api/system-manage'
   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 RoleSearch from './modules/role-search.vue'
   import RoleEditDialog from './modules/role-edit-dialog.vue'
   import RolePermissionDialog from './modules/role-permission-dialog.vue'
-  import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+  import { createRoleTableColumns } from './roleTable.columns'
   import { defaultResponseAdapter } from '@/utils/table/tableUtils'
-  import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
   import {
     buildRoleDialogModel,
     buildRolePageQueryParams,
@@ -100,7 +102,7 @@
     buildRoleSavePayload,
     buildRoleSearchParams,
     createRoleSearchState,
-    getRoleStatusMeta,
+    getRolePaginationKey,
     normalizeRoleListRow,
     ROLE_REPORT_STYLE,
     ROLE_REPORT_TITLE,
@@ -111,184 +113,19 @@
 
   const searchForm = ref(createRoleSearchState())
   const showSearchBar = ref(false)
-  const dialogVisible = ref(false)
-  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,
-    data,
-    loading,
-    pagination,
-    getData,
-    replaceSearchParams,
-    resetSearchParams,
-    handleSizeChange,
-    handleCurrentChange,
-    refreshData,
-    refreshCreate,
-    refreshUpdate,
-    refreshRemove
-  } = useTable({
-    core: {
-      apiFn: fetchRolePage,
-      apiParams: buildRolePageQueryParams(searchForm.value),
-      columnsFactory: () => [
-        { type: 'selection', width: 52, fixed: 'left' },
-        {
-          prop: 'name',
-          label: '瑙掕壊鍚嶇О',
-          minWidth: 140,
-          showOverflowTooltip: true
-        },
-        {
-          prop: 'code',
-          label: '瑙掕壊缂栫爜',
-          minWidth: 140,
-          showOverflowTooltip: true
-        },
-        {
-          prop: 'memo',
-          label: '澶囨敞',
-          minWidth: 180,
-          showOverflowTooltip: true
-        },
-        {
-          prop: 'status',
-          label: '鐘舵��',
-          width: 120,
-          formatter: (row) => {
-            const statusMeta = getRoleStatusMeta(row.statusBool ?? row.status)
-            return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
-          }
-        },
-        {
-          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: 120,
-          fixed: 'right',
-          formatter: (row) =>
-            h('div', [
-              h(ArtButtonMore, {
-                list: [
-                  {
-                    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',
-                    auth: 'edit'
-                  },
-                  {
-                    key: 'delete',
-                    label: '鍒犻櫎瑙掕壊',
-                    icon: 'ri:delete-bin-4-line',
-                    color: '#f56c6c',
-                    auth: 'delete'
-                  }
-                ],
-                onClick: (item) => handleActionClick(item, row)
-              })
-            ])
-        }
-      ]
-    },
-    transform: {
-      dataTransformer: (records) => {
-        if (!Array.isArray(records)) {
-          return []
-        }
-        return records.map((item) => normalizeRoleListRow(item))
-      }
-    }
-  })
-
-  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) => {
-    replaceSearchParams(buildRoleSearchParams(params))
-    getData()
+  function openScopeDialog(scopeType, row) {
+    permissionScopeType.value = scopeType
+    currentRoleData.value = buildRoleDialogModel(row)
+    permissionDialogVisible.value = true
   }
 
-  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) => {
+  function handleActionClick(item, row) {
     switch (item.key) {
       case 'scope-menu':
         openScopeDialog('menu', row)
@@ -313,137 +150,121 @@
     }
   }
 
-  const openScopeDialog = (scopeType, row) => {
-    permissionScopeType.value = scopeType
-    currentRoleData.value = buildRoleDialogModel(row)
-    permissionDialogVisible.value = true
-  }
-
-  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
+  const {
+    columns,
+    columnChecks,
+    data,
+    loading,
+    pagination,
+    getData,
+    replaceSearchParams,
+    resetSearchParams,
+    handleSizeChange,
+    handleCurrentChange,
+    refreshData,
+    refreshCreate,
+    refreshUpdate,
+    refreshRemove
+  } = useTable({
+    core: {
+      apiFn: fetchRolePage,
+      apiParams: buildRolePageQueryParams(searchForm.value),
+      paginationKey: getRolePaginationKey(),
+      columnsFactory: () => createRoleTableColumns(handleActionClick)
+    },
+    transform: {
+      dataTransformer: (records) => {
+        if (!Array.isArray(records)) {
+          return []
+        }
+        return records.map((item) => normalizeRoleListRow(item))
       }
-      await fetchSaveRole(payload)
-      ElMessage.success('鏂板鎴愬姛')
-      dialogVisible.value = false
-      currentRoleData.value = buildRoleDialogModel()
-      await refreshCreate()
-    } catch (error) {
-      ElMessage.error(error?.message || '鎻愪氦澶辫触')
+    }
+  })
+
+  const {
+    dialogVisible,
+    dialogType,
+    currentRecord: currentRoleData,
+    selectedRows,
+    handleSelectionChange,
+    showDialog,
+    handleDialogSubmit,
+    handleDelete,
+    handleBatchDelete
+  } = useCrudPage({
+    createEmptyModel: () => buildRoleDialogModel(),
+    buildEditModel: (record) => buildRoleDialogModel(record),
+    buildSavePayload: (formData) => buildRoleSavePayload(formData),
+    saveRequest: fetchSaveRole,
+    updateRequest: fetchUpdateRole,
+    deleteRequest: fetchDeleteRole,
+    entityName: '瑙掕壊',
+    resolveRecordLabel: (record) => record?.name || record?.code || record?.id,
+    refreshCreate,
+    refreshUpdate,
+    refreshRemove
+  })
+
+  const buildPreviewDialogMeta = (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
     }
   }
 
-  const handleDelete = async (row) => {
-    try {
-      await ElMessageBox.confirm(`纭畾瑕佸垹闄よ鑹层��${row.name || row.code || row.id}銆嶅悧锛焋, '鍒犻櫎纭', {
-        confirmButtonText: '纭畾',
-        cancelButtonText: '鍙栨秷',
-        type: 'warning'
-      })
-      await fetchDeleteRole(row.id)
-      ElMessage.success('鍒犻櫎鎴愬姛')
-      await refreshRemove()
-    } catch (error) {
-      if (error !== 'cancel') {
-        ElMessage.error(error?.message || '鍒犻櫎澶辫触')
-      }
-    }
+  const resolvePrintRecords = async (payload) => {
+    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
+        })
+    return defaultResponseAdapter(response).records
   }
 
-  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, {
+  const {
+    previewVisible,
+    previewRows,
+    previewMeta,
+    handlePreviewVisibleChange,
+    handleExport,
+    handlePrint
+  } = usePrintExportPage({
+    downloadFileName: 'role.xlsx',
+    requestExport: (payload) =>
+      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 || '瀵煎嚭澶辫触')
-    }
+      }),
+    resolvePrintRecords,
+    buildPreviewRows: (records) => buildRolePrintRows(records),
+    buildPreviewMeta: (rows) => buildPreviewDialogMeta(rows)
+  })
+
+  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) => {
+    replaceSearchParams(buildRoleSearchParams(params))
+    getData()
   }
 
-  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 || '鎵撳嵃澶辫触')
-    }
+  const handleReset = () => {
+    Object.assign(searchForm.value, createRoleSearchState())
+    resetSearchParams()
   }
 </script>
diff --git a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
index 552ba28..779b85f 100644
--- a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
+++ b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
@@ -72,6 +72,8 @@
   import {
     buildRoleScopeSubmitPayload,
     getRoleScopeConfig,
+    normalizeScopeKeys,
+    normalizeScopeKey,
     normalizeRoleScopeTreeData
   } from '../rolePage.helpers'
   import { fetchGetRoleScopeList, fetchGetRoleScopeTree, fetchUpdateRoleScope } from '@/api/system-manage'
@@ -152,31 +154,6 @@
     }
 
     await loadScopeData(scopeType, { reloadSelection })
-  }
-
-  const normalizeScopeKeys = (keys = []) => {
-    if (!Array.isArray(keys)) {
-      return []
-    }
-
-    return Array.from(
-      new Set(
-        keys
-          .map((key) => normalizeScopeKey(key))
-          .filter((key) => key !== '')
-      )
-    )
-  }
-
-  const normalizeScopeKey = (value) => {
-    if (value === '' || value === null || value === void 0) {
-      return ''
-    }
-    const numeric = Number(value)
-    if (Number.isNaN(numeric)) {
-      return String(value)
-    }
-    return String(numeric)
   }
 
   const setTreeRef = (scopeType, el) => {
diff --git a/rsf-design/src/views/system/role/rolePage.helpers.js b/rsf-design/src/views/system/role/rolePage.helpers.js
index 77b7b81..d01d937 100644
--- a/rsf-design/src/views/system/role/rolePage.helpers.js
+++ b/rsf-design/src/views/system/role/rolePage.helpers.js
@@ -41,6 +41,13 @@
   }
 }
 
+export function getRolePaginationKey() {
+  return {
+    current: 'current',
+    size: 'pageSize'
+  }
+}
+
 const ROLE_REPORT_COLUMNS = [
   { source: 'name', label: '瑙掕壊鍚嶇О' },
   { source: 'code', label: '瑙掕壊缂栫爜' },
@@ -278,7 +285,7 @@
   }
 }
 
-function normalizeScopeKeys(keys = []) {
+export function normalizeScopeKeys(keys = []) {
   if (!Array.isArray(keys)) {
     return []
   }
@@ -292,7 +299,7 @@
   )
 }
 
-function normalizeScopeKey(value) {
+export function normalizeScopeKey(value) {
   const normalized = normalizeRoleId(value)
   return normalized === void 0 ? '' : String(normalized)
 }
diff --git a/rsf-design/src/views/system/role/roleTable.columns.js b/rsf-design/src/views/system/role/roleTable.columns.js
new file mode 100644
index 0000000..b8a95f0
--- /dev/null
+++ b/rsf-design/src/views/system/role/roleTable.columns.js
@@ -0,0 +1,104 @@
+import { h } from 'vue'
+import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
+import { ElTag } from 'element-plus'
+import { getRoleStatusMeta } from './rolePage.helpers'
+
+const ROLE_MORE_ACTIONS = [
+  {
+    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',
+    auth: 'edit'
+  },
+  {
+    key: 'delete',
+    label: '鍒犻櫎瑙掕壊',
+    icon: 'ri:delete-bin-4-line',
+    color: '#f56c6c',
+    auth: 'delete'
+  }
+]
+
+export function createRoleTableColumns(handleActionClick) {
+  return [
+    { type: 'selection', width: 52, fixed: 'left' },
+    {
+      prop: 'name',
+      label: '瑙掕壊鍚嶇О',
+      minWidth: 140,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'code',
+      label: '瑙掕壊缂栫爜',
+      minWidth: 140,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'memo',
+      label: '澶囨敞',
+      minWidth: 180,
+      showOverflowTooltip: true
+    },
+    {
+      prop: 'status',
+      label: '鐘舵��',
+      width: 120,
+      formatter: (row) => {
+        const statusMeta = getRoleStatusMeta(row.statusBool ?? row.status)
+        return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+      }
+    },
+    {
+      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: 120,
+      fixed: 'right',
+      formatter: (row) =>
+        h('div', [
+          h(ArtButtonMore, {
+            list: ROLE_MORE_ACTIONS,
+            onClick: (item) => handleActionClick(item, row)
+          })
+        ])
+    }
+  ]
+}
diff --git a/rsf-design/src/views/system/user/index.vue b/rsf-design/src/views/system/user/index.vue
index 634f8c9..5adc86b 100644
--- a/rsf-design/src/views/system/user/index.vue
+++ b/rsf-design/src/views/system/user/index.vue
@@ -70,6 +70,7 @@
     buildUserSavePayload,
     buildUserSearchParams,
     createUserSearchState,
+    getUserPaginationKey,
     getUserStatusMeta,
     mergeUserDetailRecord,
     normalizeDeptTreeOptions,
@@ -118,6 +119,7 @@
     core: {
       apiFn: fetchUserPage,
       apiParams: buildUserPageQueryParams(searchForm.value),
+      paginationKey: getUserPaginationKey(),
       columnsFactory: () => [
         {
           prop: 'username',
diff --git a/rsf-design/src/views/system/user/userPage.helpers.js b/rsf-design/src/views/system/user/userPage.helpers.js
index a2173a2..8189f25 100644
--- a/rsf-design/src/views/system/user/userPage.helpers.js
+++ b/rsf-design/src/views/system/user/userPage.helpers.js
@@ -71,6 +71,13 @@
   }
 }
 
+export function getUserPaginationKey() {
+  return {
+    current: 'current',
+    size: 'pageSize'
+  }
+}
+
 export function buildUserDialogModel(record = {}) {
   const roleIds = normalizeRoleIds(record)
   return {
diff --git a/rsf-design/tests/system-role-scope-contract.test.mjs b/rsf-design/tests/system-role-scope-contract.test.mjs
index dd8b2af..c44030d 100644
--- a/rsf-design/tests/system-role-scope-contract.test.mjs
+++ b/rsf-design/tests/system-role-scope-contract.test.mjs
@@ -8,6 +8,7 @@
   buildRoleSavePayload,
   buildRoleScopeSubmitPayload,
   buildRoleSearchParams,
+  getRolePaginationKey,
   getRoleScopeConfig,
   normalizeRoleScopeTreeData,
   normalizeRoleListRow,
@@ -25,6 +26,10 @@
 )
 const roleIndexSource = fs.readFileSync(
   new URL('../src/views/system/role/index.vue', import.meta.url),
+  'utf8'
+)
+const roleTableColumnsSource = fs.readFileSync(
+  new URL('../src/views/system/role/roleTable.columns.js', import.meta.url),
   'utf8'
 )
 
@@ -60,6 +65,13 @@
       name: '绠$悊鍛�'
     }
   )
+})
+
+test('role page uses backend pageSize pagination key', () => {
+  assert.deepEqual(getRolePaginationKey(), {
+    current: 'current',
+    size: 'pageSize'
+  })
 })
 
 test('buildRoleDialogModel normalizes backend role data into the form model', () => {
@@ -196,8 +208,8 @@
   assert.match(roleIndexSource, /v-auth=\"'add'\"/)
   assert.match(roleIndexSource, /v-auth=\"'delete'\"/)
   assert.match(roleIndexSource, /v-auth=\"'query'\"/)
-  assert.match(roleIndexSource, /auth: 'edit'/)
-  assert.match(roleIndexSource, /auth: 'delete'/)
+  assert.match(roleTableColumnsSource, /auth: 'edit'/)
+  assert.match(roleTableColumnsSource, /auth: 'delete'/)
 })
 
 test('createRoleSearchState exposes the role search form model', () => {
diff --git a/rsf-design/tests/system-user-page-contract.test.mjs b/rsf-design/tests/system-user-page-contract.test.mjs
index 37da9ce..484333b 100644
--- a/rsf-design/tests/system-user-page-contract.test.mjs
+++ b/rsf-design/tests/system-user-page-contract.test.mjs
@@ -6,6 +6,7 @@
   buildUserPageQueryParams,
   buildUserSavePayload,
   buildUserSearchParams,
+  getUserPaginationKey,
   getUserStatusMeta,
   mergeUserDetailRecord,
   normalizeDeptTreeOptions,
@@ -63,6 +64,13 @@
   )
 })
 
+test('user page uses backend pageSize pagination key', () => {
+  assert.deepEqual(getUserPaginationKey(), {
+    current: 'current',
+    size: 'pageSize'
+  })
+})
+
 test('buildUserDialogModel normalizes backend edit data into the form model', () => {
   assert.deepEqual(
     buildUserDialogModel({

--
Gitblit v1.9.1