zhou zhou
昨天 3fdcf1d5e6468c735532e67bde5ff1cdf85bb0c6
refactor: simplify role page and fix pagination keys
3个文件已添加
7个文件已修改
765 ■■■■■ 已修改文件
rsf-design/src/views/system/common/useCrudPage.js 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/common/usePrintExportPage.js 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/index.vue 395 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/modules/role-permission-dialog.vue 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/rolePage.helpers.js 11 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/roleTable.columns.js 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/index.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/userPage.helpers.js 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/system-role-scope-contract.test.mjs 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/tests/system-user-page-contract.test.mjs 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/common/useCrudPage.js
New file
@@ -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
  }
}
rsf-design/src/views/system/common/usePrintExportPage.js
New file
@@ -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
  }
}
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,106 +150,74 @@
    }
  }
  const openScopeDialog = (scopeType, row) => {
    permissionScopeType.value = scopeType
    currentRoleData.value = buildRoleDialogModel(row)
    permissionDialogVisible.value = true
  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 []
  }
  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
        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 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 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 {
    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 handlePrint = async (payload) => {
    const token = previewToken.value + 1
    previewToken.value = token
    activePrintToken.value = token
    previewVisible.value = false
    previewRows.value = []
    previewMeta.value = {}
    try {
  const resolvePrintRecords = async (payload) => {
      const response = Array.isArray(payload?.ids) && payload.ids.length > 0
        ? await fetchGetRoleMany(payload.ids)
        : await fetchRolePrintPage({
@@ -420,30 +225,46 @@
            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
    return defaultResponseAdapter(response).records
      }
      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
  const {
    previewVisible,
    previewRows,
    previewMeta,
    handlePreviewVisibleChange,
    handleExport,
    handlePrint
  } = usePrintExportPage({
    downloadFileName: 'role.xlsx',
    requestExport: (payload) =>
      fetchExportRoleReport(payload, {
        headers: {
          Authorization: userStore.accessToken || ''
      }
      handlePreviewVisibleChange(true)
    } catch (error) {
      if (activePrintToken.value !== token) {
        return
      }),
    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()
      }
      ElMessage.error(error?.message || '打印失败')
    }
  const handleReset = () => {
    Object.assign(searchForm.value, createRoleSearchState())
    resetSearchParams()
  }
</script>
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) => {
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)
}
rsf-design/src/views/system/role/roleTable.columns.js
New file
@@ -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)
          })
        ])
    }
  ]
}
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',
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 {
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', () => {
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({