From 6877c9caa25162e570a3e2a99a5b2ce3ef88368b Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 13 四月 2026 13:48:37 +0800
Subject: [PATCH] #页面优化

---
 rsf-design/src/api/system-manage.js                                 |   71 ++
 rsf-design/src/views/system/dict-type/dictDataPage.helpers.js       |  147 +++++
 rsf-design/src/views/system/user/modules/user-dialog.vue            |   78 ++
 rsf-design/src/views/system/dict-type/dictTypePage.helpers.js       |   14 
 rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue   |  333 +++++++++++
 rsf-design/src/views/system/dict-type/dictDataTable.columns.js      |   94 +++
 rsf-design/src/views/system/dict-type/dictTypeTable.columns.js      |   48 +
 rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue  |  245 ++++++++
 rsf-design/src/views/system/role/roleTable.columns.js               |   10 
 rsf-design/src/views/system/role/rolePage.helpers.js                |   33 
 rsf-design/src/views/system/user/modules/user-detail-drawer.vue     |   16 
 rsf-design/src/views/system/role/modules/role-permission-dialog.vue |   86 ++
 rsf-design/src/views/system/dict-type/index.vue                     |  110 +++
 rsf-design/src/views/system/menu/modules/menu-dialog.vue            |   50 +
 rsf-design/src/views/system/menu/index.vue                          |   16 
 rsf-design/src/views/system/menu/menuPage.helpers.js                |   28 
 rsf-design/src/views/system/user/index.vue                          |  104 +++
 rsf-design/src/views/system/user/userPage.helpers.js                |   18 
 rsf-design/src/views/system/user/modules/user-search.vue            |    9 
 rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue  |   63 ++
 rsf-design/src/views/system/menu/menuTable.columns.js               |    5 
 rsf-design/src/views/system/role/modules/role-edit-dialog.vue       |   83 ++
 rsf-design/src/api/auth.js                                          |    2 
 23 files changed, 1,565 insertions(+), 98 deletions(-)

diff --git a/rsf-design/src/api/auth.js b/rsf-design/src/api/auth.js
index 0d070a1..36b0ab9 100644
--- a/rsf-design/src/api/auth.js
+++ b/rsf-design/src/api/auth.js
@@ -114,7 +114,7 @@
 }
 function fetchGetMenuList() {
   return request.get({
-    url: '/auth/menu/v2'
+    url: '/auth/menu'
   })
 }
 export {
diff --git a/rsf-design/src/api/system-manage.js b/rsf-design/src/api/system-manage.js
index 2e62426..44abc64 100644
--- a/rsf-design/src/api/system-manage.js
+++ b/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,
diff --git a/rsf-design/src/views/system/dict-type/dictDataPage.helpers.js b/rsf-design/src/views/system/dict-type/dictDataPage.helpers.js
new file mode 100644
index 0000000..9bbd4cd
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/dictDataPage.helpers.js
@@ -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)
+  }
+}
diff --git a/rsf-design/src/views/system/dict-type/dictDataTable.columns.js b/rsf-design/src/views/system/dict-type/dictDataTable.columns.js
new file mode 100644
index 0000000..63add5b
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/dictDataTable.columns.js
@@ -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)
+      }
+    }
+  ]
+}
diff --git a/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js b/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js
index 752260e..f357a74 100644
--- a/rsf-design/src/views/system/dict-type/dictTypePage.helpers.js
+++ b/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) }
       : {})
diff --git a/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js b/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js
index a0c5190..4211382 100644
--- a/rsf-design/src/views/system/dict-type/dictTypeTable.columns.js
+++ b/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)
       }
     }
   ]
diff --git a/rsf-design/src/views/system/dict-type/index.vue b/rsf-design/src/views/system/dict-type/index.vue
index cdba2f6..8f0dc32 100644
--- a/rsf-design/src/views/system/dict-type/index.vue
+++ b/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()
diff --git a/rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue b/rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue
new file mode 100644
index 0000000..fbf0aea
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/modules/dict-data-dialog.vue
@@ -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>
diff --git a/rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue b/rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue
new file mode 100644
index 0000000..bdcd3ae
--- /dev/null
+++ b/rsf-design/src/views/system/dict-type/modules/dict-data-panel.vue
@@ -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>
diff --git a/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue b/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue
index 178ddc0..04e46cb 100644
--- a/rsf-design/src/views/system/dict-type/modules/dict-type-dialog.vue
+++ b/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>
diff --git a/rsf-design/src/views/system/menu/index.vue b/rsf-design/src/views/system/menu/index.vue
index ea98983..2a201d2 100644
--- a/rsf-design/src/views/system/menu/index.vue
+++ b/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
   }
 
diff --git a/rsf-design/src/views/system/menu/menuPage.helpers.js b/rsf-design/src/views/system/menu/menuPage.helpers.js
index 03d60e3..e69fb5a 100644
--- a/rsf-design/src/views/system/menu/menuPage.helpers.js
+++ b/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()
diff --git a/rsf-design/src/views/system/menu/menuTable.columns.js b/rsf-design/src/views/system/menu/menuTable.columns.js
index e038694..3d2e11b 100644
--- a/rsf-design/src/views/system/menu/menuTable.columns.js
+++ b/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 || ''
       }
     },
     {
diff --git a/rsf-design/src/views/system/menu/modules/menu-dialog.vue b/rsf-design/src/views/system/menu/modules/menu-dialog.vue
index eb0e103..0981837 100644
--- a/rsf-design/src/views/system/menu/modules/menu-dialog.vue
+++ b/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
         }
       }
diff --git a/rsf-design/src/views/system/role/modules/role-edit-dialog.vue b/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
index fbec2b9..e4fa75a 100644
--- a/rsf-design/src/views/system/role/modules/role-edit-dialog.vue
+++ b/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>
diff --git a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
index 8d8909a..25f22dc 100644
--- a/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
+++ b/rsf-design/src/views/system/role/modules/role-permission-dialog.vue
@@ -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>
diff --git a/rsf-design/src/views/system/role/rolePage.helpers.js b/rsf-design/src/views/system/role/rolePage.helpers.js
index 791f914..7e41492 100644
--- a/rsf-design/src/views/system/role/rolePage.helpers.js
+++ b/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
diff --git a/rsf-design/src/views/system/role/roleTable.columns.js b/rsf-design/src/views/system/role/roleTable.columns.js
index 4f436f4..692abd1 100644
--- a/rsf-design/src/views/system/role/roleTable.columns.js
+++ b/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 || '-'
diff --git a/rsf-design/src/views/system/user/index.vue b/rsf-design/src/views/system/user/index.vue
index 8800cf1..53fcf15 100644
--- a/rsf-design/src/views/system/user/index.vue
+++ b/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>
diff --git a/rsf-design/src/views/system/user/modules/user-detail-drawer.vue b/rsf-design/src/views/system/user/modules/user-detail-drawer.vue
index 9bf4210..8fccc71 100644
--- a/rsf-design/src/views/system/user/modules/user-detail-drawer.vue
+++ b/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:
diff --git a/rsf-design/src/views/system/user/modules/user-dialog.vue b/rsf-design/src/views/system/user/modules/user-dialog.vue
index 72725a3..85a50be 100644
--- a/rsf-design/src/views/system/user/modules/user-dialog.vue
+++ b/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>
diff --git a/rsf-design/src/views/system/user/modules/user-search.vue b/rsf-design/src/views/system/user/modules/user-search.vue
index 849a4bc..0a56626 100644
--- a/rsf-design/src/views/system/user/modules/user-search.vue
+++ b/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',
diff --git a/rsf-design/src/views/system/user/userPage.helpers.js b/rsf-design/src/views/system/user/userPage.helpers.js
index 8189f25..91cdbda 100644
--- a/rsf-design/src/views/system/user/userPage.helpers.js
+++ b/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))
   )
 }
 

--
Gitblit v1.9.1