zhou zhou
9 小时以前 6877c9caa25162e570a3e2a99a5b2ce3ef88368b
#页面优化
4个文件已添加
19个文件已修改
1663 ■■■■■ 已修改文件
rsf-design/src/api/auth.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/system-manage.js 71 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/dict-type/dictDataPage.helpers.js 147 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/dict-type/dictDataTable.columns.js 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/dict-type/dictTypePage.helpers.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/dict-type/dictTypeTable.columns.js 48 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/dict-type/index.vue 110 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue 245 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue 63 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/menu/index.vue 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/menu/menuPage.helpers.js 28 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/menu/menuTable.columns.js 5 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/menu/modules/menu-dialog.vue 50 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/modules/role-edit-dialog.vue 83 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/modules/role-permission-dialog.vue 86 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/rolePage.helpers.js 33 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/role/roleTable.columns.js 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/index.vue 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/modules/user-detail-drawer.vue 16 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/modules/user-dialog.vue 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/modules/user-search.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/views/system/user/userPage.helpers.js 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-design/src/api/auth.js
@@ -114,7 +114,7 @@
}
function fetchGetMenuList() {
  return request.get({
    url: '/auth/menu/v2'
    url: '/auth/menu'
  })
}
export {
rsf-design/src/api/system-manage.js
@@ -9,7 +9,14 @@
    ...(params.phone !== undefined ? { phone: params.phone } : {}),
    ...(params.email !== undefined ? { email: params.email } : {}),
    ...(params.status !== undefined ? { status: params.status } : {}),
    ...(params.deptId !== undefined ? { deptId: params.deptId } : {})
    ...(params.deptId !== undefined ? { deptId: params.deptId } : {}),
    ...(params.code !== undefined ? { code: params.code } : {}),
    ...(params.sex !== undefined ? { sex: params.sex } : {}),
    ...(params.realName !== undefined ? { realName: params.realName } : {}),
    ...(params.idCard !== undefined ? { idCard: params.idCard } : {}),
    ...(params.memo !== undefined ? { memo: params.memo } : {}),
    ...(params.condition !== undefined ? { condition: params.condition } : {}),
    ...(params.roleIds !== undefined ? { roleIds: params.roleIds } : {})
  }
}
@@ -92,7 +99,8 @@
    ...(params.status !== undefined ? { status: params.status } : {}),
    ...(params.timeStart !== undefined ? { timeStart: params.timeStart } : {}),
    ...(params.timeEnd !== undefined ? { timeEnd: params.timeEnd } : {}),
    ...(params.memo !== undefined ? { memo: params.memo } : {})
    ...(params.memo !== undefined ? { memo: params.memo } : {}),
    ...(params.orderBy !== undefined ? { orderBy: params.orderBy } : {})
  }
}
@@ -108,7 +116,8 @@
    ...(params.sort !== undefined ? { sort: params.sort } : {}),
    ...(params.group !== undefined ? { group: params.group } : {}),
    ...(params.status !== undefined ? { status: params.status } : {}),
    ...(params.memo !== undefined ? { memo: params.memo } : {})
    ...(params.memo !== undefined ? { memo: params.memo } : {}),
    ...(params.orderBy !== undefined ? { orderBy: params.orderBy } : {})
  }
}
@@ -208,6 +217,17 @@
  return request.get({ url: `/user/${id}` })
}
async function fetchExportUserReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/user/export`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})
    },
    body: JSON.stringify(payload)
  })
}
function fetchGetRoleList(params) {
  return request.post({ url: '/role/page', params: buildRoleListParams(params) })
}
@@ -239,16 +259,54 @@
  return request.get({ url: `/dictType/${id}` })
}
function fetchGetDictDataDetail(id) {
  return request.get({ url: `/dictData/${id}` })
}
function fetchSaveDictType(params) {
  return request.post({ url: '/dictType/save', params })
}
function fetchSaveDictData(params) {
  return request.post({ url: '/dictData/save', params })
}
function fetchUpdateDictType(params) {
  return request.post({ url: '/dictType/update', params })
}
function fetchUpdateDictData(params) {
  return request.post({ url: '/dictData/update', params })
}
function fetchDeleteDictType(id) {
  return request.post({ url: `/dictType/remove/${id}` })
}
function fetchDeleteDictData(id) {
  return request.post({ url: `/dictData/remove/${id}` })
}
async function fetchExportDictTypeReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/dictType/export`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})
    },
    body: JSON.stringify(payload)
  })
}
async function fetchExportDictDataReport(payload = {}, options = {}) {
  return fetch(`${import.meta.env.VITE_API_URL}/dictData/export`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      ...(options.headers || {})
    },
    body: JSON.stringify(payload)
  })
}
function fetchWaveRulePage(params = {}) {
@@ -880,6 +938,7 @@
  fetchResetUserPassword,
  fetchUpdateUserStatus,
  fetchGetUserDetail,
  fetchExportUserReport,
  fetchGetRoleList,
  fetchOperationRecordPage,
  fetchGetOperationRecordDetail,
@@ -901,9 +960,15 @@
  fetchDeleteSerialRule,
  fetchDictTypePage,
  fetchGetDictTypeDetail,
  fetchGetDictDataDetail,
  fetchSaveDictType,
  fetchSaveDictData,
  fetchUpdateDictType,
  fetchUpdateDictData,
  fetchDeleteDictType,
  fetchDeleteDictData,
  fetchExportDictTypeReport,
  fetchExportDictDataReport,
  fetchDictDataPage,
  fetchWaveRulePage,
  fetchGetWaveRuleDetail,
rsf-design/src/views/system/dict-type/dictDataPage.helpers.js
New file
@@ -0,0 +1,147 @@
import { $t } from '@/locales'
const DEFAULT_DICT_DATA_ORDER_BY = 'sort asc'
function hasValue(value) {
  return value !== '' && value !== void 0 && value !== null
}
function normalizeText(value) {
  return String(value ?? '').trim()
}
function normalizeNumber(value, fallback = 0) {
  if (!hasValue(value)) {
    return fallback
  }
  const normalized = Number(value)
  return Number.isNaN(normalized) ? fallback : normalized
}
export function createDictDataSearchState(dictTypeData = {}) {
  return {
    condition: '',
    dictTypeId: hasValue(dictTypeData.id) ? normalizeNumber(dictTypeData.id, 0) : '',
    dictTypeCode: normalizeText(dictTypeData.code),
    value: '',
    label: '',
    sort: '',
    memo: '',
    status: '',
    orderBy: DEFAULT_DICT_DATA_ORDER_BY
  }
}
export function createDictDataFormState(dictTypeData = {}) {
  return {
    id: null,
    dictTypeId: hasValue(dictTypeData.id) ? normalizeNumber(dictTypeData.id, 0) : 0,
    dictTypeCode: normalizeText(dictTypeData.code),
    value: '',
    label: '',
    group: '',
    sort: 0,
    status: 1,
    memo: ''
  }
}
export function getDictDataPaginationKey() {
  return {
    current: 'current',
    size: 'pageSize'
  }
}
export function getDictDataStatusOptions() {
  return [
    { label: $t('common.status.normal'), value: 1 },
    { label: $t('common.status.frozen'), value: 0 }
  ]
}
export function getDictDataStatusMeta(status, t = $t) {
  return Number(status) === 1
    ? { text: t('common.status.normal'), type: 'success', bool: true }
    : { text: t('common.status.frozen'), type: 'danger', bool: false }
}
export function buildDictDataSearchParams(params = {}) {
  const searchParams = {
    condition: normalizeText(params.condition),
    dictTypeId: hasValue(params.dictTypeId) ? normalizeNumber(params.dictTypeId, 0) : '',
    dictTypeCode: normalizeText(params.dictTypeCode),
    value: normalizeText(params.value),
    label: normalizeText(params.label),
    sort: normalizeText(params.sort),
    memo: normalizeText(params.memo),
    status: params.status,
    orderBy: normalizeText(params.orderBy) || DEFAULT_DICT_DATA_ORDER_BY
  }
  return Object.fromEntries(Object.entries(searchParams).filter(([, value]) => hasValue(value)))
}
export function buildDictDataPageQueryParams(params = {}) {
  return {
    current: params.current || 1,
    pageSize: params.pageSize || params.size || 20,
    ...buildDictDataSearchParams(params)
  }
}
export function buildDictDataDialogModel(record = {}, dictTypeData = {}) {
  return {
    ...createDictDataFormState(dictTypeData),
    ...(record.id ? { id: normalizeNumber(record.id, 0) } : {}),
    dictTypeId: hasValue(record.dictTypeId)
      ? normalizeNumber(record.dictTypeId, 0)
      : createDictDataFormState(dictTypeData).dictTypeId,
    dictTypeCode:
      normalizeText(record.dictTypeCode) || createDictDataFormState(dictTypeData).dictTypeCode,
    value: normalizeText(record.value),
    label: normalizeText(record.label),
    group: normalizeText(record.group),
    sort: normalizeNumber(record.sort, 0),
    status: hasValue(record.status) ? normalizeNumber(record.status, 1) : 1,
    memo: normalizeText(record.memo)
  }
}
export function buildDictDataSavePayload(formData = {}, dictTypeData = {}) {
  const baseForm = createDictDataFormState(dictTypeData)
  return {
    ...(formData.id ? { id: normalizeNumber(formData.id, 0) } : {}),
    dictTypeId: hasValue(formData.dictTypeId)
      ? normalizeNumber(formData.dictTypeId, 0)
      : baseForm.dictTypeId,
    dictTypeCode: normalizeText(formData.dictTypeCode) || baseForm.dictTypeCode,
    value: normalizeText(formData.value),
    label: normalizeText(formData.label),
    group: normalizeText(formData.group),
    sort: normalizeNumber(formData.sort, 0),
    status: hasValue(formData.status) ? normalizeNumber(formData.status, 1) : 1,
    memo: normalizeText(formData.memo)
  }
}
export function normalizeDictDataListRow(record = {}, t = $t) {
  const statusMeta = getDictDataStatusMeta(record.status, t)
  return {
    ...record,
    dictTypeId: hasValue(record.dictTypeId) ? normalizeNumber(record.dictTypeId, 0) : 0,
    dictTypeCode: normalizeText(record.dictTypeCode),
    value: normalizeText(record.value),
    label: normalizeText(record.label),
    group: normalizeText(record.group),
    sort: normalizeNumber(record.sort, 0),
    memo: normalizeText(record.memo),
    statusText: record['status$'] || statusMeta.text,
    statusType: statusMeta.type,
    statusBool: record.statusBool ?? statusMeta.bool,
    updateByLabel: record['updateBy$'] || normalizeText(record.updateByLabel),
    createByLabel: record['createBy$'] || normalizeText(record.createByLabel),
    updateTimeText: record['updateTime$'] || normalizeText(record.updateTime),
    createTimeText: record['createTime$'] || normalizeText(record.createTime)
  }
}
rsf-design/src/views/system/dict-type/dictDataTable.columns.js
New file
@@ -0,0 +1,94 @@
import { h } from 'vue'
import { ElTag } from 'element-plus'
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
import { $t } from '@/locales'
export function createDictDataTableColumns({ handleEdit, handleDelete, t = $t }) {
  return [
    {
      prop: 'dictTypeCode',
      label: '字典编码',
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
      prop: 'value',
      label: '值',
      minWidth: 140,
      showOverflowTooltip: true
    },
    {
      prop: 'label',
      label: '标签',
      minWidth: 160,
      showOverflowTooltip: true
    },
    {
      prop: 'group',
      label: '分组',
      minWidth: 140,
      showOverflowTooltip: true,
      formatter: (row) => row.group || t('common.placeholder.empty')
    },
    {
      prop: 'sort',
      label: t('table.sort'),
      width: 90
    },
    {
      prop: 'status',
      label: t('table.status'),
      width: 100,
      formatter: (row) =>
        h(
          ElTag,
          { type: row.statusType, effect: 'light' },
          () => row.statusText || t('common.placeholder.empty')
        )
    },
    {
      prop: 'updateByLabel',
      label: t('table.updateBy'),
      width: 120,
      formatter: (row) => row.updateByLabel || t('common.placeholder.empty')
    },
    {
      prop: 'updateTimeText',
      label: t('table.updateTime'),
      minWidth: 180,
      formatter: (row) => row.updateTimeText || t('common.placeholder.empty')
    },
    {
      prop: 'memo',
      label: t('table.memo'),
      minWidth: 180,
      showOverflowTooltip: true,
      formatter: (row) => row.memo || t('common.placeholder.empty')
    },
    {
      prop: 'operation',
      label: t('table.operation'),
      width: handleDelete ? 120 : 80,
      align: 'center',
      formatter: (row) => {
        const buttons = [
          h(ArtButtonTable, {
            type: 'edit',
            onClick: () => handleEdit(row)
          })
        ]
        if (handleDelete) {
          buttons.push(
            h(ArtButtonTable, {
              type: 'delete',
              onClick: () => handleDelete(row)
            })
          )
        }
        return h('div', { class: 'flex justify-center' }, buttons)
      }
    }
  ]
}
rsf-design/src/views/system/dict-type/dictTypePage.helpers.js
@@ -1,11 +1,18 @@
import { $t } from '@/locales'
const DEFAULT_DICT_TYPE_ORDER_BY = 'create_time desc'
export function createDictTypeSearchState() {
  return {
    condition: '',
    code: '',
    name: '',
    status: ''
    description: '',
    memo: '',
    timeStart: '',
    timeEnd: '',
    status: '',
    orderBy: DEFAULT_DICT_TYPE_ORDER_BY
  }
}
@@ -38,6 +45,11 @@
    condition: String(params.condition || '').trim(),
    code: String(params.code || '').trim(),
    name: String(params.name || '').trim(),
    description: String(params.description || '').trim(),
    memo: String(params.memo || '').trim(),
    timeStart: String(params.timeStart || '').trim(),
    timeEnd: String(params.timeEnd || '').trim(),
    orderBy: String(params.orderBy || '').trim() || DEFAULT_DICT_TYPE_ORDER_BY,
    ...(params.status !== '' && params.status !== null && params.status !== undefined
      ? { status: Number(params.status) }
      : {})
rsf-design/src/views/system/dict-type/dictTypeTable.columns.js
@@ -3,7 +3,15 @@
import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
import { $t } from '@/locales'
export function createDictTypeTableColumns({ handleView, handleEdit, handleDelete, t = $t }) {
export function createDictTypeTableColumns({
  handleView,
  handleEdit,
  handleDelete,
  handleManageData,
  t = $t
}) {
  const hasStandaloneView = !handleManageData && handleView
  return [
    {
      prop: 'code',
@@ -29,7 +37,11 @@
      label: t('table.status'),
      width: 100,
      formatter: (row) =>
        h(ElTag, { type: row.statusType, effect: 'light' }, () => row.statusText || t('common.placeholder.empty'))
        h(
          ElTag,
          { type: row.statusType, effect: 'light' },
          () => row.statusText || t('common.placeholder.empty')
        )
    },
    {
      prop: 'updateByLabel',
@@ -46,19 +58,35 @@
    {
      prop: 'operation',
      label: t('table.operation'),
      width: handleDelete ? 160 : 120,
      width: handleDelete ? (hasStandaloneView ? 200 : 240) : hasStandaloneView ? 160 : 200,
      align: 'center',
      formatter: (row) => {
        const buttons = [
          h(ArtButtonTable, {
            type: 'view',
            onClick: () => handleView(row)
          }),
        const buttons = []
        if (handleManageData) {
          buttons.push(
            h(ArtButtonTable, {
              type: 'view',
              onClick: () => handleManageData(row)
            })
          )
        }
        if (hasStandaloneView) {
          buttons.push(
            h(ArtButtonTable, {
              type: 'view',
              onClick: () => handleView(row)
            })
          )
        }
        buttons.push(
          h(ArtButtonTable, {
            type: 'edit',
            onClick: () => handleEdit(row)
          })
        ]
        )
        if (handleDelete) {
          buttons.push(
@@ -69,7 +97,7 @@
          )
        }
        return h('div', { class: 'flex justify-center' }, buttons)
        return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
      }
    }
  ]
rsf-design/src/views/system/dict-type/index.vue
@@ -12,7 +12,9 @@
      <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
        <template #left>
          <ElSpace wrap>
            <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>{{ t('pages.system.dictType.buttons.add') }}</ElButton>
            <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>{{
              t('pages.system.dictType.buttons.add')
            }}</ElButton>
            <ElButton
              v-auth="'delete'"
              type="danger"
@@ -21,6 +23,15 @@
              v-ripple
            >
              {{ t('common.actions.batchDelete') }}
            </ElButton>
            <ElButton
              v-auth="'query'"
              :loading="exportLoading"
              :disabled="loading || exportLoading"
              @click="handleExport"
              v-ripple
            >
              {{ t('common.actions.export') }}
            </ElButton>
          </ElSpace>
        </template>
@@ -47,6 +58,11 @@
        :loading="detailLoading"
        :detail-data="detailData"
      />
      <DictDataPanel
        v-model:visible="dictDataPanelVisible"
        :dict-type-data="currentManagedDictTypeData"
      />
    </ElCard>
  </div>
</template>
@@ -54,16 +70,20 @@
<script setup>
  import { useI18n } from 'vue-i18n'
  import { ElMessage } from 'element-plus'
  import { useUserStore } from '@/store/modules/user'
  import { useAuth } from '@/hooks/core/useAuth'
  import { useTable } from '@/hooks/core/useTable'
  import { useCrudPage } from '@/views/system/common/useCrudPage'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import {
    fetchDeleteDictType,
    fetchDictTypePage,
    fetchExportDictTypeReport,
    fetchGetDictTypeDetail,
    fetchSaveDictType,
    fetchUpdateDictType
  } from '@/api/system-manage'
  import DictDataPanel from './modules/dict-data-panel.vue'
  import DictTypeDialog from './modules/dict-type-dialog.vue'
  import DictTypeDetailDrawer from './modules/dict-type-detail-drawer.vue'
  import { createDictTypeTableColumns } from './dictTypeTable.columns'
@@ -81,10 +101,14 @@
  const { t } = useI18n()
  const { hasAuth } = useAuth()
  const userStore = useUserStore()
  const searchForm = ref(createDictTypeSearchState())
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const detailData = ref({})
  const dictDataPanelVisible = ref(false)
  const currentManagedDictTypeData = ref(buildDictTypeDialogModel())
  const exportLoading = ref(false)
  let handleDeleteAction = null
  const searchItems = computed(() => [
@@ -116,6 +140,24 @@
      }
    },
    {
      label: t('pages.system.dictType.table.description'),
      key: 'description',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入字典描述'
      }
    },
    {
      label: t('table.memo'),
      key: 'memo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入备注'
      }
    },
    {
      label: t('table.status'),
      key: 'status',
      type: 'select',
@@ -125,6 +167,26 @@
          { label: t('common.status.normal'), value: 1 },
          { label: t('common.status.frozen'), value: 0 }
        ]
      }
    },
    {
      label: '开始日期',
      key: 'timeStart',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        type: 'date'
      }
    },
    {
      label: '结束日期',
      key: 'timeEnd',
      type: 'date',
      props: {
        clearable: true,
        valueFormat: 'YYYY-MM-DD',
        type: 'date'
      }
    }
  ])
@@ -153,6 +215,11 @@
    }
  }
  function openDictDataPanel(row) {
    currentManagedDictTypeData.value = buildDictTypeDialogModel(row)
    dictDataPanelVisible.value = true
  }
  const {
    columns,
    columnChecks,
@@ -178,6 +245,9 @@
          handleView: openDetail,
          handleEdit: openEditDialog,
          handleDelete: hasAuth('delete') ? (row) => handleDeleteAction?.(row) : null,
          handleManageData: hasAuth('system:dictData:list')
            ? (row) => openDictDataPanel(row)
            : null,
          t
        })
    },
@@ -216,6 +286,44 @@
  })
  handleDeleteAction = handleDelete
  async function handleExport() {
    exportLoading.value = true
    try {
      const response = await guardRequestWithMessage(
        fetchExportDictTypeReport(buildDictTypeSearchParams(searchForm.value), {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }),
        null,
        {
          timeoutMessage: '数据字典导出超时,已停止等待'
        }
      )
      if (!response) {
        return
      }
      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 = 'dict-type.xlsx'
      document.body.appendChild(link)
      link.click()
      link.remove()
      window.URL.revokeObjectURL(downloadUrl)
      ElMessage.success('导出成功')
    } catch (error) {
      ElMessage.error(error?.message || '导出失败')
    } finally {
      exportLoading.value = false
    }
  }
  function handleSearch(params) {
    replaceSearchParams(buildDictTypeSearchParams(params))
    getData()
rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue
New file
@@ -0,0 +1,245 @@
<template>
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    width="820px"
    align-center
    @update:model-value="handleVisibleUpdate"
    @closed="handleClosed"
  >
    <ArtForm
      ref="formRef"
      v-model="form"
      :items="formItems"
      :rules="rules"
      :span="12"
      :gutter="20"
      label-width="100px"
      :show-reset="false"
      :show-submit="false"
    />
    <template #footer>
      <span class="dialog-footer">
        <ElButton @click="handleCancel">{{ t('common.cancel') }}</ElButton>
        <ElButton type="primary" @click="handleSubmit">{{ t('common.confirm') }}</ElButton>
      </span>
    </template>
  </ElDialog>
</template>
<script setup>
  import { ElMessageBox } from 'element-plus'
  import { useI18n } from 'vue-i18n'
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import {
    buildDictDataDialogModel,
    createDictDataFormState,
    getDictDataStatusOptions
  } from '../dictDataPage.helpers'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    dictTypeData: { type: Object, default: () => ({}) },
    dictData: { type: Object, default: () => ({}) }
  })
  const emit = defineEmits(['update:visible', 'submit'])
  const formRef = ref()
  const form = reactive(createDictDataFormState())
  const initialSnapshot = ref(createComparableSnapshot())
  const { t } = useI18n()
  const isEdit = computed(() => Boolean(form.id))
  const dialogTitle = computed(() => (isEdit.value ? '编辑字典项' : '新增字典项'))
  const rules = computed(() => ({
    dictTypeId: [{ required: true, message: '缺少字典类型ID', trigger: 'blur' }],
    dictTypeCode: [{ required: true, message: '缺少字典类型编码', trigger: 'blur' }],
    value: [{ required: true, message: '请输入字典值', trigger: 'blur' }],
    label: [{ required: true, message: '请输入字典标签', trigger: 'blur' }]
  }))
  const formItems = computed(() => [
    {
      label: '字典类型ID',
      key: 'dictTypeId',
      type: 'input',
      props: {
        disabled: true
      }
    },
    {
      label: '字典编码',
      key: 'dictTypeCode',
      type: 'input',
      props: {
        disabled: true
      }
    },
    {
      label: '字典值',
      key: 'value',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入字典值'
      }
    },
    {
      label: '字典标签',
      key: 'label',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入字典标签'
      }
    },
    {
      label: '分组',
      key: 'group',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入分组'
      }
    },
    {
      label: t('table.sort'),
      key: 'sort',
      type: 'number',
      props: {
        min: 0,
        controlsPosition: 'right',
        style: { width: '100%' }
      }
    },
    {
      label: t('table.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        placeholder: '请选择状态',
        options: getDictDataStatusOptions()
      }
    },
    {
      label: t('table.memo'),
      key: 'memo',
      type: 'input',
      span: 24,
      props: {
        type: 'textarea',
        rows: 3,
        clearable: true,
        placeholder: '请输入备注'
      }
    }
  ])
  function resetForm() {
    Object.assign(form, createDictDataFormState(props.dictTypeData))
    initialSnapshot.value = createComparableSnapshot()
    formRef.value?.clearValidate?.()
  }
  function loadFormData() {
    const nextForm = buildDictDataDialogModel(props.dictData, props.dictTypeData)
    Object.assign(form, nextForm)
    initialSnapshot.value = createComparableSnapshot(nextForm)
  }
  async function handleSubmit() {
    if (!formRef.value) return
    try {
      await formRef.value.validate()
      emit('submit', { ...form })
    } catch {
      return
    }
  }
  function closeDialog() {
    emit('update:visible', false)
  }
  async function handleCancel() {
    if (!(await confirmDiscardIfDirty())) {
      return
    }
    closeDialog()
  }
  function handleVisibleUpdate(nextVisible) {
    if (!nextVisible) {
      handleCancel()
      return
    }
    emit('update:visible', true)
  }
  function handleClosed() {
    resetForm()
  }
  watch(
    () => props.visible,
    (visible) => {
      if (visible) {
        loadFormData()
        nextTick(() => formRef.value?.clearValidate?.())
      }
    },
    { immediate: true }
  )
  watch(
    () => props.dictData,
    () => {
      if (props.visible) {
        loadFormData()
      }
    },
    { deep: true }
  )
  watch(
    () => props.dictTypeData,
    () => {
      if (props.visible) {
        loadFormData()
      }
    },
    { deep: true }
  )
  function createComparableSnapshot(source = form) {
    return JSON.stringify({
      ...createDictDataFormState(props.dictTypeData),
      ...source
    })
  }
  function isDirty() {
    return createComparableSnapshot() !== initialSnapshot.value
  }
  async function confirmDiscardIfDirty() {
    if (!isDirty()) {
      return true
    }
    try {
      await ElMessageBox.confirm('当前内容尚未保存,确定要关闭吗?', '未保存提示', {
        confirmButtonText: '放弃修改',
        cancelButtonText: '继续编辑',
        type: 'warning'
      })
      return true
    } catch {
      return false
    }
  }
</script>
rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue
New file
@@ -0,0 +1,333 @@
<template>
  <ElDrawer
    :model-value="visible"
    title="字典项管理"
    size="1100px"
    destroy-on-close
    @update:model-value="handleVisibleChange"
  >
    <div class="dict-data-panel space-y-4">
      <div class="text-sm text-[var(--art-text-secondary)]">
        当前字典:{{ currentDictTypeLabel }}
      </div>
      <ArtSearchBar
        v-model="searchForm"
        :items="searchItems"
        :showExpand="false"
        @search="handleSearch"
        @reset="handleReset"
      />
      <ElCard class="art-table-card">
        <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
          <template #left>
            <ElSpace wrap>
              <ElButton v-if="hasAuth('system:dictData:save')" @click="showDialog('add')" v-ripple>
                新增字典项
              </ElButton>
              <ElButton
                v-if="hasAuth('system:dictData:remove')"
                type="danger"
                :disabled="selectedRows.length === 0"
                @click="handleBatchDelete"
                v-ripple
              >
                {{ t('common.actions.batchDelete') }}
              </ElButton>
              <ElButton
                v-if="hasAuth('system:dictData:list')"
                :loading="exportLoading"
                :disabled="loading || exportLoading"
                @click="handleExport"
                v-ripple
              >
                {{ t('common.actions.export') }}
              </ElButton>
            </ElSpace>
          </template>
        </ArtTableHeader>
        <ArtTable
          :loading="loading"
          :data="data"
          :columns="columns"
          :pagination="pagination"
          @selection-change="handleSelectionChange"
          @pagination:size-change="handleSizeChange"
          @pagination:current-change="handleCurrentChange"
        />
      </ElCard>
      <DictDataDialog
        v-model:visible="dialogVisible"
        :dict-type-data="dictTypeData"
        :dict-data="currentDictData"
        @submit="handleDialogSubmit"
      />
    </div>
  </ElDrawer>
</template>
<script setup>
  import { computed, ref, watch } from 'vue'
  import { ElMessage } from 'element-plus'
  import { useI18n } from 'vue-i18n'
  import { useUserStore } from '@/store/modules/user'
  import { useAuth } from '@/hooks/core/useAuth'
  import { useTable } from '@/hooks/core/useTable'
  import { useCrudPage } from '@/views/system/common/useCrudPage'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import {
    fetchDeleteDictData,
    fetchDictDataPage,
    fetchExportDictDataReport,
    fetchGetDictDataDetail,
    fetchSaveDictData,
    fetchUpdateDictData
  } from '@/api/system-manage'
  import DictDataDialog from './dict-data-dialog.vue'
  import { createDictDataTableColumns } from '../dictDataTable.columns'
  import {
    buildDictDataDialogModel,
    buildDictDataPageQueryParams,
    buildDictDataSavePayload,
    buildDictDataSearchParams,
    createDictDataSearchState,
    getDictDataPaginationKey,
    getDictDataStatusOptions,
    normalizeDictDataListRow
  } from '../dictDataPage.helpers'
  const props = defineProps({
    visible: { type: Boolean, default: false },
    dictTypeData: { type: Object, default: () => ({}) }
  })
  const emit = defineEmits(['update:visible'])
  const { t } = useI18n()
  const { hasAuth } = useAuth()
  const userStore = useUserStore()
  const exportLoading = ref(false)
  const searchForm = ref(createDictDataSearchState(props.dictTypeData))
  let handleDeleteAction = null
  const currentDictTypeLabel = computed(() => {
    const code = props.dictTypeData?.code || '-'
    const name = props.dictTypeData?.name || '未选择'
    return `${name}(${code})`
  })
  const searchItems = computed(() => [
    {
      label: t('table.keyword'),
      key: 'condition',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入字典值/标签/备注'
      }
    },
    {
      label: '字典值',
      key: 'value',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入字典值'
      }
    },
    {
      label: '字典标签',
      key: 'label',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入字典标签'
      }
    },
    {
      label: t('table.sort'),
      key: 'sort',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入排序'
      }
    },
    {
      label: t('table.memo'),
      key: 'memo',
      type: 'input',
      props: {
        clearable: true,
        placeholder: '请输入备注'
      }
    },
    {
      label: t('table.status'),
      key: 'status',
      type: 'select',
      props: {
        clearable: true,
        options: getDictDataStatusOptions()
      }
    }
  ])
  function buildPanelParams(extra = {}) {
    return buildDictDataPageQueryParams({
      ...searchForm.value,
      ...extra,
      dictTypeId: props.dictTypeData?.id,
      dictTypeCode: props.dictTypeData?.code
    })
  }
  const {
    columns,
    columnChecks,
    data,
    loading,
    pagination,
    getData,
    replaceSearchParams,
    handleSizeChange,
    handleCurrentChange,
    refreshData,
    refreshCreate,
    refreshUpdate,
    refreshRemove
  } = useTable({
    core: {
      apiFn: fetchDictDataPage,
      apiParams: buildPanelParams(),
      immediate: false,
      paginationKey: getDictDataPaginationKey(),
      columnsFactory: () =>
        createDictDataTableColumns({
          handleEdit: openEditDialog,
          handleDelete: hasAuth('system:dictData:remove')
            ? (row) => handleDeleteAction?.(row)
            : null,
          t
        })
    },
    transform: {
      dataTransformer: (records) =>
        Array.isArray(records) ? records.map((item) => normalizeDictDataListRow(item, t)) : []
    }
  })
  const {
    dialogVisible,
    dialogType,
    currentRecord: currentDictData,
    selectedRows,
    handleSelectionChange,
    showDialog,
    handleDialogSubmit,
    handleDelete,
    handleBatchDelete
  } = useCrudPage({
    createEmptyModel: () => buildDictDataDialogModel({}, props.dictTypeData),
    buildEditModel: (record) => buildDictDataDialogModel(record, props.dictTypeData),
    buildSavePayload: (formData) => buildDictDataSavePayload(formData, props.dictTypeData),
    saveRequest: fetchSaveDictData,
    updateRequest: fetchUpdateDictData,
    deleteRequest: fetchDeleteDictData,
    entityName: '字典项',
    resolveRecordLabel: (record) => record?.label || record?.value || record?.id,
    refreshCreate,
    refreshUpdate,
    refreshRemove
  })
  handleDeleteAction = handleDelete
  async function openEditDialog(row) {
    try {
      currentDictData.value = buildDictDataDialogModel(
        await fetchGetDictDataDetail(row.id),
        props.dictTypeData
      )
      dialogVisible.value = true
      dialogType.value = 'edit'
    } catch (error) {
      ElMessage.error(error?.message || '获取字典项详情失败')
    }
  }
  async function handleExport() {
    exportLoading.value = true
    try {
      const response = await guardRequestWithMessage(
        fetchExportDictDataReport(buildDictDataSearchParams(buildPanelParams()), {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }),
        null,
        {
          timeoutMessage: '字典项导出超时,已停止等待'
        }
      )
      if (!response) {
        return
      }
      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 = 'dict-data.xlsx'
      document.body.appendChild(link)
      link.click()
      link.remove()
      window.URL.revokeObjectURL(downloadUrl)
      ElMessage.success('导出成功')
    } catch (error) {
      ElMessage.error(error?.message || '导出失败')
    } finally {
      exportLoading.value = false
    }
  }
  function handleSearch(params) {
    searchForm.value = {
      ...searchForm.value,
      ...params,
      dictTypeId: props.dictTypeData?.id,
      dictTypeCode: props.dictTypeData?.code
    }
    replaceSearchParams(buildPanelParams(params))
    getData()
  }
  function handleReset() {
    searchForm.value = createDictDataSearchState(props.dictTypeData)
    selectedRows.value = []
    replaceSearchParams(buildPanelParams(searchForm.value))
    getData()
  }
  function handleVisibleChange(visible) {
    emit('update:visible', visible)
  }
  watch(
    () => [props.visible, props.dictTypeData?.id],
    ([visible, dictTypeId]) => {
      if (!visible || !dictTypeId) {
        return
      }
      searchForm.value = createDictDataSearchState(props.dictTypeData)
      selectedRows.value = []
      replaceSearchParams(buildPanelParams(searchForm.value))
      getData()
    },
    { immediate: true }
  )
</script>
rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue
@@ -2,9 +2,11 @@
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    width="820px"
    align-center
    @update:model-value="handleCancel"
    @update:model-value="handleVisibleUpdate"
    @closed="handleClosed"
  >
    <ArtForm
@@ -29,6 +31,7 @@
</template>
<script setup>
  import { ElMessageBox } from 'element-plus'
  import { useI18n } from 'vue-i18n'
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import { buildDictTypeDialogModel, createDictTypeFormState } from '../dictTypePage.helpers'
@@ -41,15 +44,20 @@
  const emit = defineEmits(['update:visible', 'submit'])
  const formRef = ref()
  const form = reactive(createDictTypeFormState())
  const initialSnapshot = ref(createComparableSnapshot())
  const { t } = useI18n()
  const isEdit = computed(() => Boolean(form.id))
  const dialogTitle = computed(() =>
    isEdit.value ? t('pages.system.dictType.dialog.titleEdit') : t('pages.system.dictType.dialog.titleCreate')
    isEdit.value
      ? t('pages.system.dictType.dialog.titleEdit')
      : t('pages.system.dictType.dialog.titleCreate')
  )
  const rules = computed(() => ({
    code: [{ required: true, message: t('pages.system.dictType.validation.code'), trigger: 'blur' }],
    code: [
      { required: true, message: t('pages.system.dictType.validation.code'), trigger: 'blur' }
    ],
    name: [{ required: true, message: t('pages.system.dictType.validation.name'), trigger: 'blur' }]
  }))
@@ -110,11 +118,14 @@
  function resetForm() {
    Object.assign(form, createDictTypeFormState())
    initialSnapshot.value = createComparableSnapshot()
    formRef.value?.clearValidate?.()
  }
  function loadFormData() {
    Object.assign(form, buildDictTypeDialogModel(props.dictTypeData))
    const nextForm = buildDictTypeDialogModel(props.dictTypeData)
    Object.assign(form, nextForm)
    initialSnapshot.value = createComparableSnapshot(nextForm)
  }
  async function handleSubmit() {
@@ -127,8 +138,23 @@
    }
  }
  function handleCancel() {
  function closeDialog() {
    emit('update:visible', false)
  }
  async function handleCancel() {
    if (!(await confirmDiscardIfDirty())) {
      return
    }
    closeDialog()
  }
  function handleVisibleUpdate(nextVisible) {
    if (!nextVisible) {
      handleCancel()
      return
    }
    emit('update:visible', true)
  }
  function handleClosed() {
@@ -155,4 +181,31 @@
    },
    { deep: true }
  )
  function createComparableSnapshot(source = form) {
    return JSON.stringify({
      ...createDictTypeFormState(),
      ...source
    })
  }
  function isDirty() {
    return createComparableSnapshot() !== initialSnapshot.value
  }
  async function confirmDiscardIfDirty() {
    if (!isDirty()) {
      return true
    }
    try {
      await ElMessageBox.confirm('当前内容尚未保存,确定要关闭吗?', '未保存提示', {
        confirmButtonText: '放弃修改',
        cancelButtonText: '继续编辑',
        type: 'warning'
      })
      return true
    } catch {
      return false
    }
  }
</script>
rsf-design/src/views/system/menu/index.vue
@@ -16,9 +16,15 @@
        @refresh="handleRefresh"
      >
        <template #left>
          <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple>{{ t('pages.system.menu.buttons.add') }}</ElButton>
          <ElButton v-auth="'add'" @click="handleAddMenu" v-ripple>{{
            t('pages.system.menu.buttons.add')
          }}</ElButton>
          <ElButton @click="toggleExpand" v-ripple>
            {{ isExpanded ? t('pages.system.menu.actions.collapse') : t('pages.system.menu.actions.expand') }}
            {{
              isExpanded
                ? t('pages.system.menu.actions.collapse')
                : t('pages.system.menu.actions.expand')
            }}
          </ElButton>
        </template>
      </ArtTableHeader>
@@ -153,7 +159,7 @@
  function handleAddMenu() {
    dialogType.value = 'menu'
    editData.value = null
    lockMenuType.value = true
    lockMenuType.value = false
    dialogVisible.value = true
  }
@@ -172,14 +178,14 @@
  function handleEditMenu(row) {
    dialogType.value = 'menu'
    editData.value = row
    lockMenuType.value = true
    lockMenuType.value = false
    dialogVisible.value = true
  }
  function handleEditAuth(row) {
    dialogType.value = 'button'
    editData.value = row
    lockMenuType.value = true
    lockMenuType.value = false
    dialogVisible.value = true
  }
rsf-design/src/views/system/menu/menuPage.helpers.js
@@ -28,7 +28,13 @@
}
export function getMenuDisplayTitle(row = {}, titleFormatter = defaultMenuTitleFormatter) {
  return titleFormatter(normalizeMenuTitleKey(row))
  const normalizedTitle = normalizeMenuTitleKey(row)
  const formattedTitle = titleFormatter(normalizedTitle)
  if (formattedTitle) {
    return formattedTitle
  }
  return defaultMenuTitleFormatter(row.name || row.meta?.title || '')
}
export function getMenuDisplayIcon(row = {}) {
@@ -69,7 +75,11 @@
  }))
}
export function buildMenuTreeOptions(tree = [], titleFormatter = defaultMenuTitleFormatter, t = $t) {
export function buildMenuTreeOptions(
  tree = [],
  titleFormatter = defaultMenuTitleFormatter,
  t = $t
) {
  return [
    {
      label: t('table.topLevelMenu'),
@@ -137,10 +147,18 @@
  })
}
export function filterMenuTree(items = [], filters = {}, titleFormatter = defaultMenuTitleFormatter) {
export function filterMenuTree(
  items = [],
  filters = {},
  titleFormatter = defaultMenuTitleFormatter
) {
  const results = []
  const searchName = String(filters.name || '').toLowerCase().trim()
  const searchRoute = String(filters.route || '').toLowerCase().trim()
  const searchName = String(filters.name || '')
    .toLowerCase()
    .trim()
  const searchRoute = String(filters.route || '')
    .toLowerCase()
    .trim()
  for (const item of items) {
    const menuTitle = getMenuDisplayTitle(item, titleFormatter).toLowerCase()
rsf-design/src/views/system/menu/menuTable.columns.js
@@ -81,10 +81,7 @@
        if (row.meta?.isAuthButton) {
          return row.authority || row.meta?.authMark || ''
        }
        if (!row.meta?.authList?.length) return row.authority || ''
        return t('pages.system.menu.messages.authCount', {
          count: row.meta.authList.length
        })
        return row.authority || ''
      }
    },
    {
rsf-design/src/views/system/menu/modules/menu-dialog.vue
@@ -2,6 +2,7 @@
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    :close-on-click-modal="false"
    @update:model-value="handleCancel"
    width="760px"
    align-center
@@ -70,20 +71,49 @@
  const isEdit = computed(() => Boolean(form.id))
  const dialogTitle = computed(() =>
    form.menuType === 'button'
      ? t(isEdit.value ? 'pages.system.menu.form.titleEditButton' : 'pages.system.menu.form.titleAddButton')
      : t(isEdit.value ? 'pages.system.menu.form.titleEditMenu' : 'pages.system.menu.form.titleAddMenu')
      ? t(
          isEdit.value
            ? 'pages.system.menu.form.titleEditButton'
            : 'pages.system.menu.form.titleAddButton'
        )
      : t(
          isEdit.value
            ? 'pages.system.menu.form.titleEditMenu'
            : 'pages.system.menu.form.titleAddMenu'
        )
  )
  const disableMenuType = computed(() => props.lockType || isEdit.value)
  const rules = computed(() => ({
    name: [{ required: true, message: form.menuType === 'button' ? t('pages.system.menu.form.validationButtonName') : t('pages.system.menu.form.validationMenuName'), trigger: 'blur' }],
    name: [
      {
        required: true,
        message:
          form.menuType === 'button'
            ? t('pages.system.menu.form.validationButtonName')
            : t('pages.system.menu.form.validationMenuName'),
        trigger: 'blur'
      }
    ],
    route:
      form.menuType === 'menu'
        ? [{ required: true, message: t('pages.system.menu.form.validationRoute'), trigger: 'blur' }]
        ? [
            {
              required: true,
              message: t('pages.system.menu.form.validationRoute'),
              trigger: 'blur'
            }
          ]
        : [],
    authority:
      form.menuType === 'button'
        ? [{ required: true, message: t('pages.system.menu.form.validationAuthority'), trigger: 'blur' }]
        ? [
            {
              required: true,
              message: t('pages.system.menu.form.validationAuthority'),
              trigger: 'blur'
            }
          ]
        : []
  }))
@@ -109,12 +139,18 @@
        }
      },
      {
        label: form.menuType === 'button' ? t('pages.system.menu.form.nameButton') : t('pages.system.menu.form.nameMenu'),
        label:
          form.menuType === 'button'
            ? t('pages.system.menu.form.nameButton')
            : t('pages.system.menu.form.nameMenu'),
        key: 'name',
        type: 'input',
        span: 24,
        props: {
          placeholder: form.menuType === 'button' ? t('pages.system.menu.form.placeholderButtonName') : t('pages.system.menu.form.placeholderMenuName'),
          placeholder:
            form.menuType === 'button'
              ? t('pages.system.menu.form.placeholderButtonName')
              : t('pages.system.menu.form.placeholderMenuName'),
          clearable: true
        }
      }
rsf-design/src/views/system/role/modules/role-edit-dialog.vue
@@ -2,9 +2,11 @@
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    width="720px"
    align-center
    @update:model-value="handleCancel"
    @update:model-value="handleVisibleUpdate"
    @closed="handleClosed"
  >
    <ArtForm
@@ -30,7 +32,12 @@
<script setup>
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import { buildRoleDialogModel, createRoleFormState, getRoleStatusOptions } from '../rolePage.helpers'
  import { ElMessageBox } from 'element-plus'
  import {
    buildRoleDialogModel,
    createRoleFormState,
    getRoleStatusOptions
  } from '../rolePage.helpers'
  import { useI18n } from 'vue-i18n'
  const props = defineProps({
@@ -42,6 +49,7 @@
  const emit = defineEmits(['update:visible', 'submit'])
  const formRef = ref()
  const form = reactive(createRoleFormState())
  const initialSnapshot = ref(createComparableSnapshot())
  const { t } = useI18n()
  const isEdit = computed(() => props.dialogType === 'edit')
@@ -50,7 +58,9 @@
  )
  const rules = computed(() => ({
    name: [{ required: true, message: t('pages.system.role.dialog.validationName'), trigger: 'blur' }]
    name: [
      { required: true, message: t('pages.system.role.dialog.validationName'), trigger: 'blur' }
    ]
  }))
  function createInputFormItem(label, key, placeholder, extraProps = {}, extraConfig = {}) {
@@ -67,7 +77,14 @@
    }
  }
  function createSelectFormItem(label, key, placeholder, options, extraProps = {}, extraConfig = {}) {
  function createSelectFormItem(
    label,
    key,
    placeholder,
    options,
    extraProps = {},
    extraConfig = {}
  ) {
    return {
      label,
      key,
@@ -110,11 +127,14 @@
  const resetForm = () => {
    Object.assign(form, createRoleFormState())
    initialSnapshot.value = createComparableSnapshot()
    formRef.value?.clearValidate?.()
  }
  const loadFormData = () => {
    Object.assign(form, buildRoleDialogModel(props.roleData))
    const nextForm = buildRoleDialogModel(props.roleData)
    Object.assign(form, nextForm)
    initialSnapshot.value = createComparableSnapshot(nextForm)
  }
  const handleSubmit = async () => {
@@ -127,8 +147,23 @@
    }
  }
  const handleCancel = () => {
  const closeDialog = () => {
    emit('update:visible', false)
  }
  const handleCancel = async () => {
    if (!(await confirmDiscardIfDirty())) {
      return
    }
    closeDialog()
  }
  const handleVisibleUpdate = (nextVisible) => {
    if (!nextVisible) {
      handleCancel()
      return
    }
    emit('update:visible', true)
  }
  const handleClosed = () => {
@@ -157,4 +192,40 @@
    },
    { deep: true }
  )
  watch(
    () => props.dialogType,
    () => {
      if (props.visible) {
        loadFormData()
      }
    }
  )
  function createComparableSnapshot(source = form) {
    return JSON.stringify({
      ...createRoleFormState(),
      ...source
    })
  }
  function isDirty() {
    return createComparableSnapshot() !== initialSnapshot.value
  }
  async function confirmDiscardIfDirty() {
    if (!isDirty()) {
      return true
    }
    try {
      await ElMessageBox.confirm('当前内容尚未保存,确定要关闭吗?', '未保存提示', {
        confirmButtonText: '放弃修改',
        cancelButtonText: '继续编辑',
        type: 'warning'
      })
      return true
    } catch {
      return false
    }
  }
</script>
rsf-design/src/views/system/role/modules/role-permission-dialog.vue
@@ -24,10 +24,23 @@
        <div v-else class="space-y-3">
          <div class="flex items-center justify-between gap-3">
            <ElSpace wrap>
              <ElButton @click="handleSelectAll(config.scopeType)">{{ t('pages.system.role.permission.selectAll') }}</ElButton>
              <ElButton @click="handleClear(config.scopeType)">{{ t('pages.system.role.permission.clear') }}</ElButton>
              <ElButton @click="handleSelectAll(config.scopeType)">{{
                t('pages.system.role.permission.selectAll')
              }}</ElButton>
              <ElButton @click="handleClear(config.scopeType)">{{
                t('pages.system.role.permission.clear')
              }}</ElButton>
              <ElButton @click="handleToggleExpand(config.scopeType)">
                {{
                  scopeState[config.scopeType].expandAll
                    ? t('common.actions.collapse')
                    : t('common.actions.expand')
                }}
              </ElButton>
            </ElSpace>
            <ElButton type="primary" @click="handleSave(config.scopeType)">{{ t('pages.system.role.permission.saveCurrent') }}</ElButton>
            <ElButton type="primary" @click="handleSave(config.scopeType)">{{
              t('pages.system.role.permission.saveCurrent')
            }}</ElButton>
          </div>
          <div class="flex items-center gap-3">
@@ -38,16 +51,19 @@
              @clear="handleSearch(config.scopeType)"
              @keyup.enter="handleSearch(config.scopeType)"
            />
            <ElButton @click="handleSearch(config.scopeType)">{{ t('common.actions.search') }}</ElButton>
            <ElButton @click="handleSearch(config.scopeType)">{{
              t('common.actions.search')
            }}</ElButton>
          </div>
          <ElScrollbar height="56vh">
            <ElTree
              :key="`${config.scopeType}-${scopeState[config.scopeType].treeVersion}`"
              :ref="(el) => setTreeRef(config.scopeType, el)"
              :data="scopeState[config.scopeType].treeData"
              node-key="id"
              show-checkbox
              :default-expand-all="true"
              :default-expand-all="scopeState[config.scopeType].expandAll"
              :default-checked-keys="scopeState[config.scopeType].checkedKeys"
              :props="treeProps"
              @check="handleTreeCheck(config.scopeType)"
@@ -73,10 +89,13 @@
    buildRoleScopeSubmitPayload,
    getRoleScopeConfig,
    normalizeScopeKeys,
    normalizeScopeKey,
    normalizeRoleScopeTreeData
  } from '../rolePage.helpers'
  import { fetchGetRoleScopeList, fetchGetRoleScopeTree, fetchUpdateRoleScope } from '@/api/system-manage'
  import {
    fetchGetRoleScopeList,
    fetchGetRoleScopeTree,
    fetchUpdateRoleScope
  } from '@/api/system-manage'
  import { resolveBackendMenuTitle } from '@/utils/backend-menu-title'
  import { formatMenuTitle } from '@/utils/router'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
@@ -92,21 +111,28 @@
  const emit = defineEmits(['update:visible', 'success'])
  const { t } = useI18n()
  const scopeConfigs = ['menu', 'pda', 'matnr', 'warehouse'].map((scopeType) => getRoleScopeConfig(scopeType))
  const scopeConfigs = ['menu', 'pda', 'matnr', 'warehouse'].map((scopeType) =>
    getRoleScopeConfig(scopeType)
  )
  const activeScopeType = ref(props.scopeType || 'menu')
  const treeRefs = reactive({})
  const treeProps = {
    label: 'label',
    children: 'children'
  }
  const scopeState = reactive(Object.fromEntries(scopeConfigs.map((config) => [config.scopeType, createScopeTabState()])))
  const scopeState = reactive(
    Object.fromEntries(scopeConfigs.map((config) => [config.scopeType, createScopeTabState()]))
  )
  const visible = computed({
    get: () => props.visible,
    set: (value) => emit('update:visible', value)
  })
  const roleLabel = computed(() => props.roleData?.name || props.roleData?.code || t('pages.system.role.permission.unselected'))
  const roleLabel = computed(
    () =>
      props.roleData?.name || props.roleData?.code || t('pages.system.role.permission.unselected')
  )
  function createScopeTabState() {
    return {
@@ -115,7 +141,9 @@
      treeData: [],
      checkedKeys: [],
      halfCheckedKeys: [],
      condition: ''
      condition: '',
      expandAll: true,
      treeVersion: 0
    }
  }
@@ -135,13 +163,17 @@
      const selectionRequest = reloadSelection
        ? fetchGetRoleScopeList(config.scopeType, props.roleData.id)
        : Promise.resolve(state.checkedKeys)
      const treeRequest = fetchGetRoleScopeTree(config.scopeType, { condition: state.condition || '' })
      const treeRequest = fetchGetRoleScopeTree(config.scopeType, {
        condition: state.condition || ''
      })
      const guardedResult = await guardRequestWithMessage(
        Promise.all([selectionRequest, treeRequest]),
        null,
        {
          timeoutMessage: t('pages.system.role.permission.scopeLoadTimeout', { title: config.title })
          timeoutMessage: t('pages.system.role.permission.scopeLoadTimeout', {
            title: config.title
          })
        }
      )
      if (!guardedResult) {
@@ -155,9 +187,12 @@
      state.treeData = normalizeRoleScopeTreeData(config.scopeType, treeData)
      state.checkedKeys = normalizeScopeKeys(checkedIds)
      state.halfCheckedKeys = []
      state.treeVersion += 1
      state.loaded = true
    } catch (error) {
      ElMessage.error(error?.message || t('pages.system.role.permission.scopeLoadFailed', { title: config.title }))
      ElMessage.error(
        error?.message || t('pages.system.role.permission.scopeLoadFailed', { title: config.title })
      )
    } finally {
      state.loading = false
      nextTick(() => {
@@ -207,6 +242,16 @@
    handleTreeCheck(scopeType)
  }
  const handleToggleExpand = (scopeType) => {
    const state = scopeState[scopeType]
    state.expandAll = !state.expandAll
    state.treeVersion += 1
    nextTick(() => {
      treeRefs[scopeType]?.setCheckedKeys(state.checkedKeys)
      handleTreeCheck(scopeType)
    })
  }
  const handleSave = async (scopeType) => {
    if (!props.roleData?.id) return
    try {
@@ -241,7 +286,7 @@
    }
    if (activeScopeType.value === 'menu') {
      const resolvedTitle = resolveBackendMenuTitle(rawLabel, data?.component || '')
      return resolvedTitle ? formatMenuTitle(resolvedTitle) : ''
      return resolvedTitle ? formatMenuTitle(resolvedTitle) : rawLabel
    }
    return rawLabel
  }
@@ -292,12 +337,9 @@
    }
  )
  watch(
    activeScopeType,
    async (scopeType) => {
      if (props.visible && scopeType) {
        await ensureScopeLoaded(scopeType)
      }
  watch(activeScopeType, async (scopeType) => {
    if (props.visible && scopeType) {
      await ensureScopeLoaded(scopeType)
    }
  )
  })
</script>
rsf-design/src/views/system/role/rolePage.helpers.js
@@ -5,13 +5,16 @@
  0: { type: 'danger', key: 'common.status.disabled', bool: false }
}
const DEFAULT_ROLE_ORDER_BY = 'create_time asc'
export function createRoleSearchState() {
  return {
    name: '',
    code: '',
    memo: '',
    status: void 0,
    condition: ''
    condition: '',
    orderBy: DEFAULT_ROLE_ORDER_BY
  }
}
@@ -46,7 +49,8 @@
    code: normalizeText(params.code),
    memo: normalizeText(params.memo),
    status: params.status,
    condition: normalizeText(params.condition)
    condition: normalizeText(params.condition),
    orderBy: normalizeText(params.orderBy) || DEFAULT_ROLE_ORDER_BY
  }
  return Object.fromEntries(Object.entries(searchParams).filter(([, value]) => hasValue(value)))
@@ -260,7 +264,7 @@
    scopeType === 'menu' && Array.isArray(metaSource.authList) && metaSource.authList.length
      ? metaSource.authList.map((auth, index) => ({
          id: normalizeScopeKey(auth.id ?? auth.authMark ?? `${node.id || 'auth'}-${index}`),
          label: normalizeScopeTitle(auth.title || auth.name || auth.authMark || ''),
          label: resolveScopeNodeTitle(auth),
          type: 1,
          isAuthButton: true,
          authMark: auth.authMark || auth.authority || auth.code || '',
@@ -275,9 +279,7 @@
  return {
    id: normalizeScopeKey(node.id ?? node.value),
    label: normalizeScopeTitle(
      node.label || node.title || node.name || metaSource.title || node.code || ''
    ),
    label: resolveScopeNodeTitle(node, metaSource),
    type: node.type,
    path: node.path || '',
    component: node.component || '',
@@ -292,7 +294,7 @@
  const metaSource = node.meta && typeof node.meta === 'object' ? node.meta : node
  return {
    id: normalizeScopeKey(node.id ?? node.value),
    label: normalizeScopeTitle(node.label || node.title || node.name || metaSource.title || ''),
    label: resolveScopeNodeTitle(node, metaSource),
    type: 1,
    isAuthButton: true,
    authMark: node.authMark || metaSource.authMark || metaSource.authority || metaSource.code || '',
@@ -326,6 +328,23 @@
  return trimmedTitle
}
function resolveScopeNodeTitle(source = {}, metaSource = source) {
  return normalizeScopeTitle(
    source.name ||
      metaSource.name ||
      source.label ||
      source.title ||
      metaSource.title ||
      source.code ||
      metaSource.code ||
      source.authMark ||
      metaSource.authMark ||
      source.authority ||
      metaSource.authority ||
      ''
  )
}
function normalizeRoleId(value) {
  if (!hasValue(value)) {
    return void 0
rsf-design/src/views/system/role/roleTable.columns.js
@@ -63,7 +63,11 @@
    width,
    formatter: (row) => {
      const statusMeta = resolveMeta(row)
      return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
      return h(
        ElTag,
        { type: row.statusType || statusMeta.type, effect: 'light' },
        () => row.statusText || statusMeta.text || '-'
      )
    }
  }
}
@@ -74,7 +78,9 @@
    createTextColumn('name', $t('pages.system.role.table.name'), 140),
    createTextColumn('code', $t('pages.system.role.table.code'), 140),
    createTextColumn('memo', $t('pages.system.role.table.memo'), 180),
    createTagColumn('status', $t('pages.system.role.table.status'), 120, (row) => getRoleStatusMeta(row.statusBool ?? row.status)),
    createTagColumn('status', $t('pages.system.role.table.status'), 120, (row) =>
      getRoleStatusMeta(row.statusBool ?? row.status)
    ),
    createTextColumn('updateTimeText', $t('pages.system.role.table.updateTime'), 180, {
      sortable: true,
      formatter: (row) => row.updateTimeText || '-'
rsf-design/src/views/system/user/index.vue
@@ -14,6 +14,18 @@
        <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 v-auth="'query'" :loading="exportLoading" @click="handleExport" v-ripple
              >导出</ElButton
            >
          </ElSpace>
        </template>
      </ArtTableHeader>
@@ -23,6 +35,7 @@
        :data="data"
        :columns="columns"
        :pagination="pagination"
        @selection-change="handleSelectionChange"
        @pagination:size-change="handleSizeChange"
        @pagination:current-change="handleCurrentChange"
      />
@@ -48,8 +61,10 @@
<script setup>
  import request from '@/utils/http'
  import { guardRequestWithMessage } from '@/utils/sys/requestGuard'
  import { useUserStore } from '@/store/modules/user'
  import {
    fetchDeleteUser,
    fetchExportUserReport,
    fetchGetDeptTree,
    fetchGetRoleOptions,
    fetchGetUserDetail,
@@ -89,10 +104,13 @@
  const detailDrawerVisible = ref(false)
  const detailLoading = ref(false)
  const detailUserData = ref({})
  const selectedRows = ref([])
  const roleOptions = ref([])
  const deptTreeOptions = ref([])
  const exportLoading = ref(false)
  const RESET_PASSWORD = '123456'
  const { hasAuth } = useAuth()
  const userStore = useUserStore()
  const fetchUserPage = (params = {}) => {
    return request.post({
@@ -122,6 +140,12 @@
      apiParams: buildUserPageQueryParams(searchForm.value),
      paginationKey: getUserPaginationKey(),
      columnsFactory: () => [
        {
          type: 'selection',
          width: 52,
          fixed: 'left',
          align: 'center'
        },
        {
          prop: 'username',
          label: '用户名',
@@ -277,6 +301,10 @@
    getData()
  }
  const handleSelectionChange = (rows) => {
    selectedRows.value = Array.isArray(rows) ? rows : []
  }
  const handleReset = () => {
    Object.assign(searchForm.value, createUserSearchState())
    resetSearchParams()
@@ -342,17 +370,46 @@
  const handleDelete = async (row) => {
    try {
      await ElMessageBox.confirm(`确定要删除用户「${row.username || row.nickname || row.id}」吗?`, '删除确认', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        type: 'warning'
      })
      await ElMessageBox.confirm(
        `确定要删除用户「${row.username || row.nickname || row.id}」吗?`,
        '删除确认',
        {
          confirmButtonText: '确定',
          cancelButtonText: '取消',
          type: 'warning'
        }
      )
      await fetchDeleteUser(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 fetchDeleteUser(ids.join(','))
      ElMessage.success('批量删除成功')
      selectedRows.value = []
      await refreshRemove()
    } catch (error) {
      if (error !== 'cancel') {
        ElMessage.error(error?.message || '批量删除失败')
      }
    }
  }
@@ -404,4 +461,41 @@
      row._statusLoading = false
    }
  }
  const handleExport = async () => {
    exportLoading.value = true
    try {
      const response = await guardRequestWithMessage(
        fetchExportUserReport(buildUserSearchParams(searchForm.value), {
          headers: {
            Authorization: userStore.accessToken || ''
          }
        }),
        null,
        {
          timeoutMessage: '用户导出超时,已停止等待'
        }
      )
      if (!response) {
        return
      }
      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 = 'user.xlsx'
      document.body.appendChild(link)
      link.click()
      link.remove()
      window.URL.revokeObjectURL(downloadUrl)
      ElMessage.success('导出成功')
    } catch (error) {
      ElMessage.error(error?.message || '导出失败')
    } finally {
      exportLoading.value = false
    }
  }
</script>
rsf-design/src/views/system/user/modules/user-detail-drawer.vue
@@ -16,10 +16,18 @@
        <ElDescriptionsItem label="邮箱">{{ displayData.email || '--' }}</ElDescriptionsItem>
        <ElDescriptionsItem label="真实姓名">{{ displayData.realName || '--' }}</ElDescriptionsItem>
        <ElDescriptionsItem label="身份证号">{{ displayData.idCard || '--' }}</ElDescriptionsItem>
        <ElDescriptionsItem label="出生日期">{{ displayData.birthday || '--' }}</ElDescriptionsItem>
        <ElDescriptionsItem label="工号">{{ displayData.code || '--' }}</ElDescriptionsItem>
        <ElDescriptionsItem label="性别">{{ sexLabel }}</ElDescriptionsItem>
        <ElDescriptionsItem label="创建时间">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
        <ElDescriptionsItem label="更新时间">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
        <ElDescriptionsItem label="个人简介">{{
          displayData.introduction || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem label="创建时间">{{
          displayData.createTimeText || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem label="更新时间">{{
          displayData.updateTimeText || '--'
        }}</ElDescriptionsItem>
        <ElDescriptionsItem label="备注">{{ displayData.memo || '--' }}</ElDescriptionsItem>
      </ElDescriptions>
    </ElSkeleton>
@@ -38,7 +46,9 @@
  const emit = defineEmits(['update:visible'])
  const displayData = computed(() => normalizeUserListRow(props.userData))
  const statusLabel = computed(() => getUserStatusMeta(displayData.value.statusBool ?? displayData.value.status).text)
  const statusLabel = computed(
    () => getUserStatusMeta(displayData.value.statusBool ?? displayData.value.status).text
  )
  const sexLabel = computed(() => {
    switch (displayData.value.sex) {
      case 1:
rsf-design/src/views/system/user/modules/user-dialog.vue
@@ -2,7 +2,9 @@
  <ElDialog
    :title="dialogTitle"
    :model-value="visible"
    @update:model-value="handleCancel"
    :close-on-click-modal="false"
    :close-on-press-escape="false"
    @update:model-value="handleVisibleUpdate"
    width="960px"
    align-center
    class="user-dialog"
@@ -31,6 +33,7 @@
<script setup>
  import ArtForm from '@/components/core/forms/art-form/index.vue'
  import { ElMessageBox } from 'element-plus'
  import { buildUserDialogModel, createUserFormState } from '../userPage.helpers'
  const props = defineProps({
@@ -44,6 +47,7 @@
  const emit = defineEmits(['update:visible', 'submit'])
  const formRef = ref()
  const form = reactive(createUserFormState())
  const initialSnapshot = ref(createComparableSnapshot())
  const isEdit = computed(() => props.type === 'edit')
  const dialogTitle = computed(() => (isEdit.value ? '编辑用户' : '新增用户'))
@@ -214,6 +218,27 @@
      }
    },
    {
      label: '出生日期',
      key: 'birthday',
      type: 'input',
      props: {
        placeholder: '请输入出生日期',
        clearable: true
      }
    },
    {
      label: '个人简介',
      key: 'introduction',
      type: 'input',
      props: {
        type: 'textarea',
        rows: 3,
        placeholder: '请输入个人简介',
        clearable: true
      },
      span: 24
    },
    {
      label: '状态',
      key: 'status',
      type: 'select',
@@ -242,11 +267,14 @@
  const resetForm = () => {
    Object.assign(form, createUserFormState())
    initialSnapshot.value = createComparableSnapshot()
    formRef.value?.clearValidate?.()
  }
  const loadFormData = () => {
    Object.assign(form, buildUserDialogModel(props.userData))
    const nextForm = buildUserDialogModel(props.userData)
    Object.assign(form, nextForm)
    initialSnapshot.value = createComparableSnapshot(nextForm)
  }
  const handleSubmit = async () => {
@@ -259,8 +287,23 @@
    }
  }
  const handleCancel = () => {
  const closeDialog = () => {
    emit('update:visible', false)
  }
  const handleCancel = async () => {
    if (!(await confirmDiscardIfDirty())) {
      return
    }
    closeDialog()
  }
  const handleVisibleUpdate = (nextVisible) => {
    if (!nextVisible) {
      handleCancel()
      return
    }
    emit('update:visible', true)
  }
  const handleClosed = () => {
@@ -298,4 +341,33 @@
      }
    }
  )
  function createComparableSnapshot(source = form) {
    return JSON.stringify({
      ...createUserFormState(),
      ...source,
      roleIds: Array.isArray(source?.roleIds) ? [...source.roleIds] : [],
      userRoleIds: Array.isArray(source?.userRoleIds) ? [...source.userRoleIds] : []
    })
  }
  function isDirty() {
    return createComparableSnapshot() !== initialSnapshot.value
  }
  async function confirmDiscardIfDirty() {
    if (!isDirty()) {
      return true
    }
    try {
      await ElMessageBox.confirm('当前内容尚未保存,确定要关闭吗?', '未保存提示', {
        confirmButtonText: '放弃修改',
        cancelButtonText: '继续编辑',
        type: 'warning'
      })
      return true
    } catch {
      return false
    }
  }
</script>
rsf-design/src/views/system/user/modules/user-search.vue
@@ -133,6 +133,15 @@
      }
    },
    {
      label: '备注',
      key: 'memo',
      type: 'input',
      props: {
        placeholder: '请输入备注',
        clearable: true
      }
    },
    {
      label: '关键字',
      key: 'condition',
      type: 'input',
rsf-design/src/views/system/user/userPage.helpers.js
@@ -11,6 +11,7 @@
    sex: void 0,
    realName: '',
    idCard: '',
    memo: '',
    condition: ''
  }
}
@@ -31,6 +32,8 @@
    email: '',
    realName: '',
    idCard: '',
    birthday: '',
    introduction: '',
    memo: '',
    status: 1
  }
@@ -49,6 +52,7 @@
    sex: params.sex,
    realName: params.realName,
    idCard: params.idCard,
    memo: params.memo,
    condition: params.condition
  }
@@ -94,6 +98,8 @@
    email: record.email || '',
    realName: record.realName || '',
    idCard: record.idCard || '',
    birthday: record.birthday || '',
    introduction: record.introduction || '',
    memo: record.memo || '',
    status: record.status !== undefined && record.status !== null ? record.status : 1
  }
@@ -135,6 +141,8 @@
    email: form.email || '',
    realName: form.realName || '',
    idCard: form.idCard || '',
    birthday: form.birthday || '',
    introduction: form.introduction || '',
    memo: form.memo || '',
    status: form.status !== undefined && form.status !== null ? form.status : 1
  }
@@ -171,9 +179,7 @@
    return []
  }
  return tree
    .map((node) => normalizeDeptTreeNode(node))
    .filter(Boolean)
  return tree.map((node) => normalizeDeptTreeNode(node)).filter(Boolean)
}
export function normalizeRoleOptions(roles = []) {
@@ -254,11 +260,7 @@
        : []
  return Array.from(
    new Set(
      directRoleIds
        .map((item) => normalizeRoleId(item))
        .filter((item) => item !== void 0)
    )
    new Set(directRoleIds.map((item) => normalizeRoleId(item)).filter((item) => item !== void 0))
  )
}