From e9283ffe6822b12ec5dd2ccf4dc13a369b227a61 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期一, 30 三月 2026 08:32:06 +0800
Subject: [PATCH] chore: sync rsf-design from isolated worktree

---
 rsf-design/src/directives/core/roles.js                         |   21 
 rsf-design/src/config/index.js                                  |    2 
 rsf-design/src/views/system/user/modules/user-dialog.vue        |  355 +++++-
 rsf-design/src/views/auth/login/index.vue                       |  118 --
 rsf-design/src/directives/core/auth.js                          |   15 
 rsf-design/src/views/system/role/modules/role-search.vue        |   97 +-
 rsf-design/src/views/dashboard/console/index.vue                |  271 +++++
 rsf-design/build/manualChunks.js                                |   49 +
 rsf-design/src/locales/langs/en.json                            |    2 
 rsf-design/src/views/system/role/rolePage.helpers.js            |  320 ++++++
 rsf-design/src/locales/langs/zh.json                            |    2 
 rsf-design/src/views/system/user/modules/user-detail-drawer.vue |   56 +
 rsf-design/src/router/core/MenuProcessor.js                     |    5 
 rsf-design/tests/system-role-print-export-page.test.mjs         |   97 ++
 rsf-design/src/router/guards/beforeEach.js                      |    4 
 rsf-design/src/views/system/user/index.vue                      |  444 ++++++--
 rsf-design/tests/auth-contract.test.mjs                         |  123 ++
 rsf-design/src/hooks/core/useAuth.js                            |  103 ++
 rsf-design/src/views/system/role/index.vue                      |  428 +++++++--
 rsf-design/tests/list-export-print-contract.test.mjs            |  103 ++
 rsf-design/src/api/auth.js                                      |  116 ++
 rsf-design/src/store/modules/user.js                            |    8 
 22 files changed, 2,204 insertions(+), 535 deletions(-)

diff --git a/rsf-design/build/manualChunks.js b/rsf-design/build/manualChunks.js
new file mode 100644
index 0000000..f803252
--- /dev/null
+++ b/rsf-design/build/manualChunks.js
@@ -0,0 +1,49 @@
+const CHUNK_GROUPS = [
+  {
+    name: 'vendor-echarts',
+    packages: ['echarts/']
+  },
+  {
+    name: 'vendor-editor',
+    packages: ['@wangeditor/', 'highlight.js/']
+  },
+  {
+    name: 'vendor-xlsx',
+    packages: ['xlsx/']
+  },
+  {
+    name: 'vendor-media',
+    packages: ['xgplayer/']
+  },
+  {
+    name: 'vendor-element-plus',
+    packages: ['element-plus/', '@element-plus/']
+  },
+  {
+    name: 'vendor-vue',
+    packages: ['vue-router/', 'pinia/', '@vueuse/']
+  },
+  {
+    name: 'vendor-utils',
+    packages: ['@iconify/', 'file-saver/', 'axios/']
+  }
+]
+
+function createManualChunks(id) {
+  if (!id || !id.includes('/node_modules/')) {
+    return void 0
+  }
+
+  const normalizedId = id.replace(/\\/g, '/')
+  const packagePath = normalizedId.split('/node_modules/').pop() || ''
+
+  for (const group of CHUNK_GROUPS) {
+    if (group.packages.some((pkg) => packagePath.startsWith(pkg))) {
+      return group.name
+    }
+  }
+
+  return void 0
+}
+
+export { createManualChunks }
diff --git a/rsf-design/src/api/auth.js b/rsf-design/src/api/auth.js
index b7c9fbb..c420267 100644
--- a/rsf-design/src/api/auth.js
+++ b/rsf-design/src/api/auth.js
@@ -1,19 +1,111 @@
 import request from '@/utils/http'
+
+function buildLoginPayload({ username, password }) {
+  return { username, password }
+}
+
+function normalizeLoginParams(params) {
+  return {
+    username: params?.username || params?.userName || '',
+    password: params?.password
+  }
+}
+
+function normalizeLoginResponse(payload) {
+  const data = payload?.data || payload || {}
+  return {
+    accessToken: data.accessToken || '',
+    refreshToken: data.refreshToken || '',
+    user: data.user || {}
+  }
+}
+
+function normalizeUserInfo(data) {
+  const normalizedRoles = normalizeRoleCodes(data?.roles)
+  const normalizedButtons = normalizeButtonMarks(data)
+  return {
+    ...data,
+    roles: normalizedRoles,
+    buttons: normalizedButtons
+  }
+}
+
+function normalizeRoleCodes(roles) {
+  if (!Array.isArray(roles)) {
+    return []
+  }
+
+  return Array.from(
+    new Set(
+      roles
+        .map((item) => {
+          if (typeof item === 'string') {
+            return item.trim()
+          }
+          if (item && typeof item === 'object') {
+            return item.code || item.name || ''
+          }
+          return ''
+        })
+        .filter(Boolean)
+    )
+  )
+}
+
+function normalizeButtonMarks(data) {
+  const directButtons = Array.isArray(data?.buttons) ? data.buttons : []
+  const authorityButtons = Array.isArray(data?.authorities)
+    ? data.authorities.map((item) => item?.authority || item?.authMark || '')
+    : []
+
+  return Array.from(
+    new Set(
+      [...directButtons, ...authorityButtons]
+        .map((item) => (typeof item === 'string' ? item.trim() : ''))
+        .filter(Boolean)
+    )
+  )
+}
+
 function fetchLogin(params) {
-  return request.post({
-    url: '/api/auth/login',
-    params
-    // showSuccessMessage: true // 鏄剧ず鎴愬姛娑堟伅
-    // showErrorMessage: false // 涓嶆樉绀洪敊璇秷鎭�
-  })
+  return request
+    .post({
+      url: '/login',
+      params: buildLoginPayload(normalizeLoginParams(params))
+      // showSuccessMessage: true // 鏄剧ず鎴愬姛娑堟伅
+      // showErrorMessage: false // 涓嶆樉绀洪敊璇秷鎭�
+    })
+    .then((response) => {
+      const normalized = normalizeLoginResponse(response)
+      return {
+        token: normalized.accessToken,
+        accessToken: normalized.accessToken,
+        refreshToken: normalized.refreshToken,
+        user: normalized.user
+      }
+    })
 }
 function fetchGetUserInfo() {
+  return request
+    .get({
+      url: '/auth/user'
+      // 鑷畾涔夎姹傚ご
+      // headers: {
+      //   'X-Custom-Header': 'your-custom-value'
+      // }
+    })
+    .then((response) => normalizeUserInfo(response))
+}
+function fetchGetMenuList() {
   return request.get({
-    url: '/api/user/info'
-    // 鑷畾涔夎姹傚ご
-    // headers: {
-    //   'X-Custom-Header': 'your-custom-value'
-    // }
+    url: '/auth/menu'
   })
 }
-export { fetchGetUserInfo, fetchLogin }
+export {
+  buildLoginPayload,
+  fetchGetMenuList,
+  fetchGetUserInfo,
+  fetchLogin,
+  normalizeLoginResponse,
+  normalizeUserInfo
+}
diff --git a/rsf-design/src/config/index.js b/rsf-design/src/config/index.js
index 5880ec6..71de6ff 100644
--- a/rsf-design/src/config/index.js
+++ b/rsf-design/src/config/index.js
@@ -5,7 +5,7 @@
 const appConfig = {
   // 绯荤粺淇℃伅
   systemInfo: {
-    name: 'Art Design Pro'
+    name: 'RSF Design'
     // 绯荤粺鍚嶇О
   },
   // 绯荤粺涓婚
diff --git a/rsf-design/src/directives/core/auth.js b/rsf-design/src/directives/core/auth.js
index 266328b..21f96eb 100644
--- a/rsf-design/src/directives/core/auth.js
+++ b/rsf-design/src/directives/core/auth.js
@@ -1,7 +1,18 @@
 import { router } from '@/router'
+import { useUserStore } from '@/store/modules/user'
+import { useAppMode } from '@/hooks/core/useAppMode'
+import { extractRouteAuthMarks, extractUserButtons, hasAuthPermission } from '@/hooks/core/useAuth'
+
 function checkAuthPermission(el, binding) {
-  const authList = router.currentRoute.value.meta.authList || []
-  const hasPermission = authList.some((item) => item.authMark === binding.value)
+  const authList = extractRouteAuthMarks(router.currentRoute.value.meta.authList)
+  const buttons = extractUserButtons(useUserStore().getUserInfo)
+  const { isBackendMode } = useAppMode()
+  const hasPermission = hasAuthPermission(binding.value, {
+    authList,
+    buttons,
+    isBackendMode: isBackendMode.value,
+    routePath: router.currentRoute.value.path
+  })
   if (!hasPermission) {
     removeElement(el)
   }
diff --git a/rsf-design/src/directives/core/roles.js b/rsf-design/src/directives/core/roles.js
index 2fb4155..bf0c186 100644
--- a/rsf-design/src/directives/core/roles.js
+++ b/rsf-design/src/directives/core/roles.js
@@ -1,7 +1,26 @@
 import { useUserStore } from '@/store/modules/user'
+
+function extractRoleCodes(roles) {
+  if (!Array.isArray(roles)) {
+    return []
+  }
+
+  return roles
+    .map((item) => {
+      if (typeof item === 'string') {
+        return item
+      }
+      if (item && typeof item === 'object') {
+        return item.code || item.name || ''
+      }
+      return ''
+    })
+    .filter(Boolean)
+}
+
 function checkRolePermission(el, binding) {
   const userStore = useUserStore()
-  const userRoles = userStore.getUserInfo.roles
+  const userRoles = extractRoleCodes(userStore.getUserInfo.roles)
   if (!userRoles?.length) {
     removeElement(el)
     return
diff --git a/rsf-design/src/hooks/core/useAuth.js b/rsf-design/src/hooks/core/useAuth.js
index 5a4949c..b222810 100644
--- a/rsf-design/src/hooks/core/useAuth.js
+++ b/rsf-design/src/hooks/core/useAuth.js
@@ -2,21 +2,106 @@
 import { storeToRefs } from 'pinia'
 import { useUserStore } from '@/store/modules/user'
 import { useAppMode } from '@/hooks/core/useAppMode'
-const userStore = useUserStore()
+
+function extractRouteAuthMarks(authList) {
+  if (!Array.isArray(authList)) {
+    return []
+  }
+
+  return authList
+    .map((item) => {
+      if (typeof item === 'string') {
+        return item
+      }
+      if (item && typeof item === 'object') {
+        return item.authMark || ''
+      }
+      return ''
+    })
+    .filter(Boolean)
+}
+
+function extractUserButtons(info) {
+  return Array.isArray(info?.buttons) ? info.buttons : []
+}
+
+const BUTTON_ACTION_MAP = {
+  query: 'list',
+  add: 'save',
+  edit: 'update',
+  delete: 'remove'
+}
+
+function resolveRouteResourceKey(routePath) {
+  const pathSegments = String(routePath || '')
+    .split('/')
+    .filter(Boolean)
+
+  const rawSegment = pathSegments[pathSegments.length - 1]
+  if (!rawSegment) {
+    return ''
+  }
+
+  return rawSegment.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
+}
+
+function matchesBackendButton(requiredAuth, buttons, routePath) {
+  if (buttons.includes(requiredAuth)) {
+    return true
+  }
+
+  const action = BUTTON_ACTION_MAP[requiredAuth]
+  const resourceKey = resolveRouteResourceKey(routePath)
+  if (!action || !resourceKey) {
+    return false
+  }
+
+  return buttons.some(
+    (item) =>
+      typeof item === 'string' && item.includes(`:${resourceKey}:`) && item.endsWith(`:${action}`)
+  )
+}
+
+function hasAuthPermission(
+  requiredAuth,
+  { authList = [], buttons = [], isBackendMode = false, routePath = '' } = {}
+) {
+  const requiredList = Array.isArray(requiredAuth) ? requiredAuth : [requiredAuth]
+
+  if (!requiredList.length) {
+    return true
+  }
+
+  if (isBackendMode) {
+    return requiredList.some((item) => matchesBackendButton(item, buttons, routePath))
+  }
+
+  return requiredList.some((item) => authList.includes(item))
+}
+
 const useAuth = () => {
+  const userStore = useUserStore()
   const route = useRoute()
-  const { isFrontendMode } = useAppMode()
+  const { isBackendMode } = useAppMode()
   const { info } = storeToRefs(userStore)
-  const frontendAuthList = info.value?.buttons ?? []
-  const backendAuthList = Array.isArray(route.meta.authList) ? route.meta.authList : []
+  const authList = computed(() => extractRouteAuthMarks(route.meta.authList))
+  const buttons = computed(() => extractUserButtons(info.value))
   const hasAuth = (auth) => {
-    if (isFrontendMode.value) {
-      return frontendAuthList.includes(auth)
-    }
-    return backendAuthList.some((item) => item?.authMark === auth)
+    return hasAuthPermission(auth, {
+      authList: authList.value,
+      buttons: buttons.value,
+      isBackendMode: isBackendMode.value,
+      routePath: route.path
+    })
   }
   return {
     hasAuth
   }
 }
-export { useAuth }
+export {
+  extractRouteAuthMarks,
+  extractUserButtons,
+  hasAuthPermission,
+  resolveRouteResourceKey,
+  useAuth
+}
diff --git a/rsf-design/src/locales/langs/en.json b/rsf-design/src/locales/langs/en.json
index e9ee97d..3b7f747 100644
--- a/rsf-design/src/locales/langs/en.json
+++ b/rsf-design/src/locales/langs/en.json
@@ -257,9 +257,11 @@
       "notFound": "404",
       "serverError": "500"
     },
+    "userLogin": "Login Logs",
     "system": {
       "title": "System Settings",
       "user": "User Manage",
+      "userLogin": "Login Logs",
       "role": "Role Manage",
       "userCenter": "User Center",
       "menu": "Menu Manage"
diff --git a/rsf-design/src/locales/langs/zh.json b/rsf-design/src/locales/langs/zh.json
index c72e9ea..fe64198 100644
--- a/rsf-design/src/locales/langs/zh.json
+++ b/rsf-design/src/locales/langs/zh.json
@@ -257,9 +257,11 @@
       "notFound": "404",
       "serverError": "500"
     },
+    "userLogin": "鐧诲綍鏃ュ織",
     "system": {
       "title": "绯荤粺绠$悊",
       "user": "鐢ㄦ埛绠$悊",
+      "userLogin": "鐧诲綍鏃ュ織",
       "role": "瑙掕壊绠$悊",
       "userCenter": "涓汉涓績",
       "menu": "鑿滃崟绠$悊"
diff --git a/rsf-design/src/router/core/MenuProcessor.js b/rsf-design/src/router/core/MenuProcessor.js
index 1a32f2f..f031609 100644
--- a/rsf-design/src/router/core/MenuProcessor.js
+++ b/rsf-design/src/router/core/MenuProcessor.js
@@ -1,8 +1,9 @@
 import { useUserStore } from '@/store/modules/user'
 import { useAppMode } from '@/hooks/core/useAppMode'
-import { fetchGetMenuList } from '@/api/system-manage'
+import { fetchGetMenuList } from '@/api/auth'
 import { asyncRoutes } from '../routes/asyncRoutes'
 import { RoutesAlias } from '../routesAlias'
+import { adaptBackendMenuTree } from '../adapters/backendMenuAdapter'
 import { formatMenuTitle } from '@/utils'
 class MenuProcessor {
   /**
@@ -36,7 +37,7 @@
    */
   async processBackendMenu() {
     const list = await fetchGetMenuList()
-    return this.filterEmptyMenus(list)
+    return adaptBackendMenuTree(list)
   }
   /**
    * 鏍规嵁瑙掕壊杩囨护鑿滃崟
diff --git a/rsf-design/src/router/guards/beforeEach.js b/rsf-design/src/router/guards/beforeEach.js
index f3e37d4..d49d94a 100644
--- a/rsf-design/src/router/guards/beforeEach.js
+++ b/rsf-design/src/router/guards/beforeEach.js
@@ -10,7 +10,7 @@
 import { loadingService } from '@/utils/ui'
 import { useCommon } from '@/hooks/core/useCommon'
 import { useWorktabStore } from '@/store/modules/worktab'
-import { fetchGetUserInfo } from '@/api/auth'
+import { fetchGetUserInfo, normalizeUserInfo } from '@/api/auth'
 import { ApiStatus } from '@/utils/http/status'
 import { isHttpError } from '@/utils/http/error'
 import { RouteRegistry, MenuProcessor, IframeRouteManager, RoutePermissionValidator } from '../core'
@@ -185,7 +185,7 @@
 }
 async function fetchUserInfo() {
   const userStore = useUserStore()
-  const data = await fetchGetUserInfo()
+  const data = normalizeUserInfo(await fetchGetUserInfo())
   userStore.setUserInfo(data)
   userStore.checkAndClearWorktabs()
 }
diff --git a/rsf-design/src/store/modules/user.js b/rsf-design/src/store/modules/user.js
index 19839ae..6c471b3 100644
--- a/rsf-design/src/store/modules/user.js
+++ b/rsf-design/src/store/modules/user.js
@@ -41,11 +41,9 @@
     const setLockPassword = (password) => {
       lockPassword.value = password
     }
-    const setToken = (newAccessToken, newRefreshToken) => {
-      accessToken.value = newAccessToken
-      if (newRefreshToken) {
-        refreshToken.value = newRefreshToken
-      }
+    const setToken = (newAccessToken, newRefreshToken = '') => {
+      accessToken.value = newAccessToken || ''
+      refreshToken.value = newRefreshToken || ''
     }
     const logOut = () => {
       const currentUserId = info.value.userId
diff --git a/rsf-design/src/views/auth/login/index.vue b/rsf-design/src/views/auth/login/index.vue
index f3e4a2f..1f6abd5 100644
--- a/rsf-design/src/views/auth/login/index.vue
+++ b/rsf-design/src/views/auth/login/index.vue
@@ -18,18 +18,6 @@
             @keyup.enter="handleSubmit"
             style="margin-top: 25px"
           >
-            <ElFormItem prop="account">
-              <ElSelect v-model="formData.account" @change="setupAccount">
-                <ElOption
-                  v-for="account in accounts"
-                  :key="account.key"
-                  :label="account.label"
-                  :value="account.key"
-                >
-                  <span>{{ account.label }}</span>
-                </ElOption>
-              </ElSelect>
-            </ElFormItem>
             <ElFormItem prop="username">
               <ElInput
                 class="custom-height"
@@ -47,31 +35,6 @@
                 show-password
               />
             </ElFormItem>
-
-            <!-- 鎺ㄦ嫿楠岃瘉 -->
-            <div class="relative pb-5 mt-6">
-              <div
-                class="relative z-[2] overflow-hidden select-none rounded-lg border border-transparent tad-300"
-                :class="{ '!border-[#FF4E4F]': !isPassing && isClickPass }"
-              >
-                <ArtDragVerify
-                  ref="dragVerify"
-                  v-model:value="isPassing"
-                  :text="$t('login.sliderText')"
-                  textColor="var(--art-gray-700)"
-                  :successText="$t('login.sliderSuccessText')"
-                  progressBarBg="var(--main-color)"
-                  :background="isDark ? '#26272F' : '#F1F1F4'"
-                  handlerBg="var(--default-box-color)"
-                />
-              </div>
-              <p
-                class="absolute top-0 z-[1] px-px mt-2 text-xs text-[#f56c6c] tad-300"
-                :class="{ 'translate-y-10': !isPassing && isClickPass }"
-              >
-                {{ $t('login.placeholder.slider') }}
-              </p>
-            </div>
 
             <div class="flex-cb mt-2 text-sm">
               <ElCheckbox v-model="formData.rememberPassword">{{
@@ -111,51 +74,20 @@
   import AppConfig from '@/config'
   import { useUserStore } from '@/store/modules/user'
   import { useI18n } from 'vue-i18n'
-  import { HttpError } from '@/utils/http/error'
-  import { fetchLogin } from '@/api/auth'
+  import { fetchGetUserInfo, fetchLogin, normalizeLoginResponse } from '@/api/auth'
   import { ElNotification } from 'element-plus'
-  import { useSettingStore } from '@/store/modules/setting'
   defineOptions({ name: 'Login' })
-  const settingStore = useSettingStore()
-  const { isDark } = storeToRefs(settingStore)
   const { t, locale } = useI18n()
   const formKey = ref(0)
   watch(locale, () => {
     formKey.value++
   })
-  const accounts = computed(() => [
-    {
-      key: 'super',
-      label: t('login.roles.super'),
-      userName: 'Super',
-      password: '123456',
-      roles: ['R_SUPER']
-    },
-    {
-      key: 'admin',
-      label: t('login.roles.admin'),
-      userName: 'Admin',
-      password: '123456',
-      roles: ['R_ADMIN']
-    },
-    {
-      key: 'user',
-      label: t('login.roles.user'),
-      userName: 'User',
-      password: '123456',
-      roles: ['R_USER']
-    }
-  ])
-  const dragVerify = ref()
   const userStore = useUserStore()
   const router = useRouter()
   const route = useRoute()
-  const isPassing = ref(false)
-  const isClickPass = ref(false)
   const systemName = AppConfig.systemInfo.name
   const formRef = ref()
   const formData = reactive({
-    account: '',
     username: '',
     password: '',
     rememberPassword: true
@@ -165,49 +97,27 @@
     password: [{ required: true, message: t('login.placeholder.password'), trigger: 'blur' }]
   }))
   const loading = ref(false)
-  onMounted(() => {
-    setupAccount('super')
-  })
-  const setupAccount = (key) => {
-    const selectedAccount = accounts.value.find((account) => account.key === key)
-    formData.account = key
-    formData.username = selectedAccount?.userName ?? ''
-    formData.password = selectedAccount?.password ?? ''
-  }
   const handleSubmit = async () => {
     if (!formRef.value) return
     try {
       const valid = await formRef.value.validate()
       if (!valid) return
-      if (!isPassing.value) {
-        isClickPass.value = true
-        return
-      }
       loading.value = true
-      const { username, password } = formData
-      const { token, refreshToken } = await fetchLogin({
-        userName: username,
-        password
-      })
-      if (!token) {
-        throw new Error('Login failed - no token received')
-      }
-      userStore.setToken(token, refreshToken)
+      const payload = normalizeLoginResponse(await fetchLogin(formData))
+      if (!payload.accessToken) return
+      userStore.setToken(payload.accessToken, payload.refreshToken)
       userStore.setLoginStatus(true)
-      showLoginSuccessNotice()
-      const redirect = route.query.redirect
-      router.push(redirect || '/')
-    } catch (error) {
-      if (!(error instanceof HttpError)) {
-        console.error('[Login] Unexpected error:', error)
+      const userInfo = await fetchGetUserInfo()
+      if (userInfo && Object.keys(userInfo).length > 0) {
+        userStore.setUserInfo(userInfo)
+      } else if (payload.user && Object.keys(payload.user).length > 0) {
+        userStore.setUserInfo(payload.user)
       }
+      showLoginSuccessNotice()
+      router.push(route.query.redirect || '/')
     } finally {
       loading.value = false
-      resetDragVerify()
     }
-  }
-  const resetDragVerify = () => {
-    dragVerify.value.reset()
   }
   const showLoginSuccessNotice = () => {
     setTimeout(() => {
@@ -224,10 +134,4 @@
 
 <style scoped>
   @import './style.css';
-</style>
-
-<style lang="scss" scoped>
-  :deep(.el-select__wrapper) {
-    height: 40px !important;
-  }
 </style>
diff --git a/rsf-design/src/views/dashboard/console/index.vue b/rsf-design/src/views/dashboard/console/index.vue
index 39fa3be..90691e1 100644
--- a/rsf-design/src/views/dashboard/console/index.vue
+++ b/rsf-design/src/views/dashboard/console/index.vue
@@ -1,44 +1,249 @@
-<!-- 宸ヤ綔鍙伴〉闈� -->
 <template>
-  <div>
-    <CardList></CardList>
+  <div class="art-full-height flex flex-col gap-5">
+    <section
+      class="overflow-hidden rounded-3xl border border-white/10 bg-[linear-gradient(135deg,var(--art-main-bg-color),var(--art-card-bg-color))] p-6 shadow-[0_24px_80px_rgba(15,23,42,0.08)]"
+    >
+      <div class="flex flex-col gap-6 lg:flex-row lg:items-end lg:justify-between">
+        <div class="max-w-3xl">
+          <div
+            class="mb-3 inline-flex items-center gap-2 rounded-full border border-emerald-500/20 bg-emerald-500/10 px-3 py-1 text-xs font-medium text-emerald-600"
+          >
+            <span class="size-2 rounded-full bg-emerald-500"></span>
+            RSF Phase 1 Landing
+          </div>
+          <h1
+            class="m-0 text-3xl font-semibold tracking-tight text-[var(--art-gray-900)] md:text-4xl"
+          >
+            杩愯楠ㄦ灦宸茬粡鍒囧埌 `rsf-design`
+          </h1>
+          <p class="mt-4 max-w-2xl text-sm leading-7 text-[var(--art-gray-600)] md:text-base">
+            褰撳墠鍏ュ彛宸茬粡鎺ュ叆鐪熷疄鍚庣鐧诲綍銆佸姩鎬佽彍鍗曞拰鏉冮檺閾捐矾銆傝繖涓椤靛彧灞曠ず宸茬粡鍙敤鐨� phase-1
+            鑳藉姏锛屼笉鍐嶄繚鐣欐ā鏉块噷鐨勭ず渚嬪浘琛ㄥ拰婕旂ず鏁版嵁銆�
+          </p>
+        </div>
 
-    <ElRow :gutter="20">
-      <ElCol :sm="24" :md="12" :lg="10">
-        <ActiveUser />
-      </ElCol>
-      <ElCol :sm="24" :md="12" :lg="14">
-        <SalesOverview />
-      </ElCol>
-    </ElRow>
+        <div class="rounded-2xl border border-white/10 bg-white/70 px-4 py-3 backdrop-blur-sm">
+          <p class="text-xs uppercase tracking-[0.24em] text-[var(--art-gray-500)]"
+            >Current Entry</p
+          >
+          <p class="mt-2 text-lg font-semibold text-[var(--art-gray-900)]">
+            {{ currentUserName }}
+          </p>
+          <p class="mt-1 text-sm text-[var(--art-gray-600)]">
+            {{ currentUserRoleText }} 路 {{ currentMenuLabel }}
+          </p>
+        </div>
+      </div>
+    </section>
 
-    <ElRow :gutter="20">
-      <ElCol :sm="24" :md="24" :lg="12">
-        <NewUser />
-      </ElCol>
-      <ElCol :sm="24" :md="12" :lg="6">
-        <Dynamic />
-      </ElCol>
-      <ElCol :sm="24" :md="12" :lg="6">
-        <TodoList />
-      </ElCol>
-    </ElRow>
+    <section class="grid gap-4 md:grid-cols-3">
+      <ArtStatsCard
+        title="宸叉帴鍏ュ悗绔�"
+        :count="backendSwitchCount"
+        description="鐧诲綍銆佺敤鎴蜂俊鎭�佽彍鍗曞叏閮ㄦ潵鑷� rsf-server"
+        icon="ri:server-line"
+      />
+      <ArtStatsCard
+        title="鍔ㄦ�佽彍鍗�"
+        :count="visibleMenuCount"
+        description="浠呭彂甯� phase-1 鍏佽杩涘叆鐨勬柊鍏ュ彛"
+        icon="ri:route-line"
+      />
+      <ArtStatsCard
+        title="鏉冮檺閾捐矾"
+        :count="permissionSignalCount"
+        description="瑙掕壊涓庢潈闄愯妭鐐瑰凡浠庣湡瀹炵敤鎴锋暟鎹仮澶�"
+        icon="ri:shield-check-line"
+      />
+    </section>
 
-    <AboutProject />
+    <section class="grid gap-4 xl:grid-cols-[1.15fr_0.85fr]">
+      <ElCard
+        class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]"
+        shadow="never"
+      >
+        <template #header>
+          <div class="flex items-center justify-between">
+            <div>
+              <h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">鐪熷疄杩愯鐘舵��</h2>
+              <p class="mt-1 text-xs text-[var(--art-gray-500)]">
+                杩欎簺淇℃伅鏉ヨ嚜褰撳墠鐧诲綍鐢ㄦ埛鍜岃彍鍗� store
+              </p>
+            </div>
+            <ElTag type="success" effect="light">Backend mode</ElTag>
+          </div>
+        </template>
+
+        <div class="grid gap-4 md:grid-cols-2">
+          <div class="rounded-2xl bg-[var(--art-gray-50)] p-4">
+            <p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">User</p>
+            <p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]">
+              {{ currentUserName }}
+            </p>
+            <p class="mt-2 text-sm text-[var(--art-gray-600)]">
+              {{ currentUserRoleText }}
+            </p>
+            <div class="mt-4 flex flex-wrap gap-2">
+              <ElTag v-for="role in currentRoles" :key="role" type="info" effect="plain">
+                {{ role }}
+              </ElTag>
+              <ElTag v-if="!currentRoles.length" type="info" effect="plain">No roles</ElTag>
+            </div>
+          </div>
+
+          <div class="rounded-2xl bg-[var(--art-gray-50)] p-4">
+            <p class="text-xs uppercase tracking-[0.2em] text-[var(--art-gray-500)]">Permissions</p>
+            <p class="mt-3 text-xl font-semibold text-[var(--art-gray-900)]">
+              {{ currentAuthorities.length }} auth nodes
+            </p>
+            <p class="mt-2 text-sm text-[var(--art-gray-600)]">
+              鏉冮檺鑺傜偣鐩存帴鏉ヨ嚜褰撳墠鐢ㄦ埛鐨勭湡瀹� `authorities` 杞借嵎锛屼笉鍐嶄緷璧栨ā鏉挎紨绀烘�併��
+            </p>
+            <div class="mt-4 flex flex-wrap gap-2">
+              <ElTag v-for="item in previewAuthorities" :key="item" type="warning" effect="plain">
+                {{ item }}
+              </ElTag>
+              <ElTag v-if="!previewAuthorities.length" type="warning" effect="plain">
+                No authorities
+              </ElTag>
+            </div>
+          </div>
+        </div>
+      </ElCard>
+
+      <ElCard
+        class="rounded-3xl border border-white/10 shadow-[0_18px_50px_rgba(15,23,42,0.06)]"
+        shadow="never"
+      >
+        <template #header>
+          <div>
+            <h2 class="m-0 text-base font-semibold text-[var(--art-gray-900)]">鑿滃崟鎺ュ叆娓呭崟</h2>
+            <p class="mt-1 text-xs text-[var(--art-gray-500)]">
+              褰撳墠鑿滃崟鏍戠敤浜庨獙璇� `rsf-design` 鏄惁鐪熸鎺ヤ綇鍚庣鍙戝竷
+            </p>
+          </div>
+        </template>
+
+        <div class="space-y-3">
+          <div
+            v-for="item in menuPreview"
+            :key="item.key"
+            class="flex items-center justify-between rounded-2xl border border-[var(--art-gray-200)] px-4 py-3"
+          >
+            <div>
+              <p class="text-sm font-medium text-[var(--art-gray-900)]">{{ item.title }}</p>
+              <p class="mt-1 text-xs text-[var(--art-gray-500)]">{{ item.description }}</p>
+            </div>
+            <ElTag :type="item.type" effect="light">
+              {{ item.value }}
+            </ElTag>
+          </div>
+        </div>
+      </ElCard>
+    </section>
   </div>
 </template>
 
 <script setup>
-  import CardList from './modules/card-list.vue'
-  import ActiveUser from './modules/active-user.vue'
-
-  import SalesOverview from './modules/sales-overview.vue'
-  import NewUser from './modules/new-user.vue'
-
-  import Dynamic from './modules/dynamic-stats.vue'
-  import TodoList from './modules/todo-list.vue'
-
-  import AboutProject from './modules/about-project.vue'
+  import { storeToRefs } from 'pinia'
+  import { useUserStore } from '@/store/modules/user'
+  import { useMenuStore } from '@/store/modules/menu'
+  import { formatMenuTitle } from '@/utils/router'
 
   defineOptions({ name: 'Console' })
+
+  const userStore = useUserStore()
+  const menuStore = useMenuStore()
+  const { getUserInfo } = storeToRefs(userStore)
+  const { menuList } = storeToRefs(menuStore)
+
+  const currentUser = computed(() => getUserInfo.value || {})
+  const currentUserName = computed(() => {
+    return (
+      currentUser.value.userName ||
+      currentUser.value.username ||
+      currentUser.value.nickname ||
+      'RSF User'
+    )
+  })
+  const currentRoles = computed(() => {
+    const roles = currentUser.value.roles
+    if (!Array.isArray(roles)) return []
+    return roles
+      .map((role) => {
+        if (typeof role === 'string') {
+          return role
+        }
+        return role?.code || role?.name || role?.title || ''
+      })
+      .filter(Boolean)
+  })
+  const currentAuthorities = computed(() => {
+    const authorities = currentUser.value.authorities
+    if (!Array.isArray(authorities)) return []
+    return authorities.map((item) => item?.authority || '').filter(Boolean)
+  })
+  const currentMenuLabel = computed(() => {
+    const firstMenu = menuList.value?.[0]
+    return formatMenuTitle(firstMenu?.meta?.title || 'menus.dashboard.console')
+  })
+  const currentUserRoleText = computed(() => {
+    if (!currentRoles.value.length) {
+      return 'No role information yet'
+    }
+    return `Roles: ${currentRoles.value.join(' / ')}`
+  })
+  const backendSwitchCount = computed(() => {
+    return currentUserName.value !== 'RSF User' || menuList.value.length > 0 ? 1 : 0
+  })
+  const visibleMenuCount = computed(() => countVisibleMenus(menuList.value))
+  const permissionSignalCount = computed(() => {
+    return currentRoles.value.length + currentAuthorities.value.length
+  })
+  const previewAuthorities = computed(() => currentAuthorities.value.slice(0, 4))
+  const menuPreview = computed(() => {
+    const total = visibleMenuCount.value
+    const rootCount = Array.isArray(menuList.value) ? menuList.value.length : 0
+    const firstMenu = menuList.value?.[0]
+    const firstChildren = Array.isArray(firstMenu?.children) ? firstMenu.children.length : 0
+
+    return [
+      {
+        key: 'entry',
+        title: '鍏ュ彛妯″紡',
+        description: '褰撳墠椤甸潰浠� backend mode 杩愯锛岃彍鍗曠敱鏈嶅姟绔┍鍔�',
+        value: 'backend',
+        type: 'success'
+      },
+      {
+        key: 'menus',
+        title: '鍙鑿滃崟',
+        description: '宸查�氳繃鍔ㄦ�佽矾鐢遍�傞厤鍚庤繘鍏ュ墠绔彍鍗曟爲',
+        value: `${total}`,
+        type: 'primary'
+      },
+      {
+        key: 'root',
+        title: '涓�绾х洰褰�',
+        description: '鏍圭骇鑿滃崟鑺傜偣鏁伴噺',
+        value: `${rootCount}`,
+        type: 'info'
+      },
+      {
+        key: 'children',
+        title: '棣栦釜鐩綍瀛愰」',
+        description: '鐢ㄤ簬蹇�熺‘璁よ彍鍗曟爲宸茶姝g‘灞曞紑',
+        value: `${firstChildren}`,
+        type: 'warning'
+      }
+    ]
+  })
+
+  function countVisibleMenus(items) {
+    if (!Array.isArray(items)) return 0
+    return items.reduce((total, item) => {
+      const current = item?.meta?.isHide ? 0 : 1
+      return total + current + countVisibleMenus(item?.children)
+    }, 0)
+  }
 </script>
diff --git a/rsf-design/src/views/system/role/index.vue b/rsf-design/src/views/system/role/index.vue
index d6a11ca..b6787aa 100644
--- a/rsf-design/src/views/system/role/index.vue
+++ b/rsf-design/src/views/system/role/index.vue
@@ -5,8 +5,8 @@
       v-show="showSearchBar"
       v-model="searchForm"
       @search="handleSearch"
-      @reset="resetSearchParams"
-    ></RoleSearch>
+      @reset="handleReset"
+    />
 
     <ElCard class="art-table-card" :style="{ 'margin-top': showSearchBar ? '12px' : '0' }">
       <ArtTableHeader
@@ -17,62 +17,115 @@
       >
         <template #left>
           <ElSpace wrap>
-            <ElButton @click="showDialog('add')" v-ripple>鏂板瑙掕壊</ElButton>
+            <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板瑙掕壊</ElButton>
+            <ElButton
+              v-auth="'delete'"
+              :disabled="selectedRows.length === 0"
+              @click="handleBatchDelete"
+              v-ripple
+              >
+                鎵归噺鍒犻櫎
+              </ElButton>
+            <span v-auth="'query'" class="inline-flex">
+              <ListExportPrint
+                :preview-visible="previewVisible"
+                @update:previewVisible="handlePreviewVisibleChange"
+                :report-title="reportTitle"
+                :selected-rows="selectedRows"
+                :query-params="reportQueryParams"
+                :columns="roleReportColumns"
+                :preview-rows="previewRows"
+                :preview-meta="resolvedPreviewMeta"
+                :total="pagination.total"
+                :disabled="loading"
+                @export="handleExport"
+                @print="handlePrint"
+              />
+            </span>
           </ElSpace>
         </template>
       </ArtTableHeader>
 
-      <!-- 琛ㄦ牸 -->
       <ArtTable
         :loading="loading"
         :data="data"
         :columns="columns"
         :pagination="pagination"
+        @selection-change="handleSelectionChange"
         @pagination:size-change="handleSizeChange"
         @pagination:current-change="handleCurrentChange"
-      >
-      </ArtTable>
+      />
     </ElCard>
 
-    <!-- 瑙掕壊缂栬緫寮圭獥 -->
     <RoleEditDialog
-      v-model="dialogVisible"
+      v-model:visible="dialogVisible"
       :dialog-type="dialogType"
       :role-data="currentRoleData"
-      @success="refreshData"
+      @submit="handleDialogSubmit"
     />
 
-    <!-- 鑿滃崟鏉冮檺寮圭獥 -->
     <RolePermissionDialog
-      v-model="permissionDialog"
+      v-model:visible="permissionDialogVisible"
       :role-data="currentRoleData"
+      :scope-type="permissionScopeType"
       @success="refreshData"
     />
   </div>
 </template>
 
 <script setup>
+  import { useUserStore } from '@/store/modules/user'
+  import {
+    fetchExportRoleReport,
+    fetchDeleteRole,
+    fetchGetRoleMany,
+    fetchRolePrintPage,
+    fetchRolePage,
+    fetchSaveRole,
+    fetchUpdateRole
+  } from '@/api/system-manage'
+  import { useTable } from '@/hooks/core/useTable'
+  import ListExportPrint from '@/components/biz/list-export-print/index.vue'
   import RoleSearch from './modules/role-search.vue'
   import RoleEditDialog from './modules/role-edit-dialog.vue'
-
   import RolePermissionDialog from './modules/role-permission-dialog.vue'
-
-  import { useTable } from '@/hooks/core/useTable'
-  import { fetchGetRoleList } from '@/api/system-manage'
   import ArtButtonMore from '@/components/core/forms/art-button-more/index.vue'
-  import { ElTag, ElMessageBox } from 'element-plus'
+  import { defaultResponseAdapter } from '@/utils/table/tableUtils'
+  import { ElMessage, ElMessageBox, ElTag } from 'element-plus'
+  import {
+    buildRoleDialogModel,
+    buildRolePageQueryParams,
+    buildRolePrintRows,
+    buildRoleReportMeta,
+    buildRoleSavePayload,
+    buildRoleSearchParams,
+    createRoleSearchState,
+    getRoleStatusMeta,
+    normalizeRoleListRow,
+    ROLE_REPORT_STYLE,
+    ROLE_REPORT_TITLE,
+    resolveRoleReportColumns
+  } from './rolePage.helpers'
+
   defineOptions({ name: 'Role' })
-  const searchForm = ref({
-    roleName: void 0,
-    roleCode: void 0,
-    description: void 0,
-    enabled: void 0,
-    daterange: void 0
-  })
+
+  const searchForm = ref(createRoleSearchState())
   const showSearchBar = ref(false)
   const dialogVisible = ref(false)
-  const permissionDialog = ref(false)
-  const currentRoleData = ref(void 0)
+  const dialogType = ref('add')
+  const currentRoleData = ref(buildRoleDialogModel())
+  const permissionDialogVisible = ref(false)
+  const permissionScopeType = ref('menu')
+  const selectedRows = ref([])
+  const previewVisible = ref(false)
+  const previewRows = ref([])
+  const previewMeta = ref({})
+  const previewToken = ref(0)
+  const activePrintToken = ref(0)
+  const userStore = useUserStore()
+  const reportTitle = ROLE_REPORT_TITLE
+  const reportQueryParams = computed(() => buildRoleSearchParams(searchForm.value))
+
   const {
     columns,
     columnChecks,
@@ -84,130 +137,313 @@
     resetSearchParams,
     handleSizeChange,
     handleCurrentChange,
-    refreshData
+    refreshData,
+    refreshCreate,
+    refreshUpdate,
+    refreshRemove
   } = useTable({
-    // 鏍稿績閰嶇疆
     core: {
-      apiFn: fetchGetRoleList,
-      apiParams: {
-        current: 1,
-        size: 20
-      },
-      // 鎺掗櫎 apiParams 涓殑灞炴��
-      excludeParams: ['daterange'],
+      apiFn: fetchRolePage,
+      apiParams: buildRolePageQueryParams(searchForm.value),
       columnsFactory: () => [
+        { type: 'selection', width: 52, fixed: 'left' },
         {
-          prop: 'roleId',
-          label: '瑙掕壊ID',
-          width: 100
-        },
-        {
-          prop: 'roleName',
+          prop: 'name',
           label: '瑙掕壊鍚嶇О',
-          minWidth: 120
-        },
-        {
-          prop: 'roleCode',
-          label: '瑙掕壊缂栫爜',
-          minWidth: 120
-        },
-        {
-          prop: 'description',
-          label: '瑙掕壊鎻忚堪',
-          minWidth: 150,
+          minWidth: 140,
           showOverflowTooltip: true
         },
         {
-          prop: 'enabled',
-          label: '瑙掕壊鐘舵��',
-          width: 100,
+          prop: 'code',
+          label: '瑙掕壊缂栫爜',
+          minWidth: 140,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'memo',
+          label: '澶囨敞',
+          minWidth: 180,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'status',
+          label: '鐘舵��',
+          width: 120,
           formatter: (row) => {
-            const statusConfig = row.enabled
-              ? { type: 'success', text: '鍚敤' }
-              : { type: 'warning', text: '绂佺敤' }
-            return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
+            const statusMeta = getRoleStatusMeta(row.statusBool ?? row.status)
+            return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
           }
         },
         {
-          prop: 'createTime',
-          label: '鍒涘缓鏃ユ湡',
-          width: 180,
-          sortable: true
+          prop: 'updateTimeText',
+          label: '鏇存柊鏃堕棿',
+          minWidth: 180,
+          sortable: true,
+          formatter: (row) => row.updateTimeText || '-'
+        },
+        {
+          prop: 'createTimeText',
+          label: '鍒涘缓鏃堕棿',
+          minWidth: 180,
+          sortable: true,
+          formatter: (row) => row.createTimeText || '-'
         },
         {
           prop: 'operation',
           label: '鎿嶄綔',
-          width: 80,
+          width: 120,
           fixed: 'right',
           formatter: (row) =>
             h('div', [
               h(ArtButtonMore, {
                 list: [
                   {
-                    key: 'permission',
-                    label: '鑿滃崟鏉冮檺',
-                    icon: 'ri:user-3-line'
+                    key: 'scope-menu',
+                    label: '缃戦〉鏉冮檺',
+                    icon: 'ri:layout-2-line',
+                    auth: 'edit'
+                  },
+                  {
+                    key: 'scope-pda',
+                    label: 'PDA鏉冮檺',
+                    icon: 'ri:smartphone-line',
+                    auth: 'edit'
+                  },
+                  {
+                    key: 'scope-matnr',
+                    label: '鐗╂枡鏉冮檺',
+                    icon: 'ri:archive-line',
+                    auth: 'edit'
+                  },
+                  {
+                    key: 'scope-warehouse',
+                    label: '浠撳簱鏉冮檺',
+                    icon: 'ri:store-2-line',
+                    auth: 'edit'
                   },
                   {
                     key: 'edit',
                     label: '缂栬緫瑙掕壊',
-                    icon: 'ri:edit-2-line'
+                    icon: 'ri:edit-2-line',
+                    auth: 'edit'
                   },
                   {
                     key: 'delete',
                     label: '鍒犻櫎瑙掕壊',
                     icon: 'ri:delete-bin-4-line',
-                    color: '#f56c6c'
+                    color: '#f56c6c',
+                    auth: 'delete'
                   }
                 ],
-                onClick: (item) => buttonMoreClick(item, row)
+                onClick: (item) => handleActionClick(item, row)
               })
             ])
         }
       ]
+    },
+    transform: {
+      dataTransformer: (records) => {
+        if (!Array.isArray(records)) {
+          return []
+        }
+        return records.map((item) => normalizeRoleListRow(item))
+      }
     }
   })
-  const dialogType = ref('add')
-  const showDialog = (type, row) => {
-    dialogVisible.value = true
-    dialogType.value = type
-    currentRoleData.value = row
-  }
+
+  const roleReportColumns = computed(() => resolveRoleReportColumns(columns.value))
+  const resolvedPreviewMeta = computed(() =>
+    buildRoleReportMeta({
+      previewMeta: previewMeta.value,
+      count: previewRows.value.length,
+      titleAlign: ROLE_REPORT_STYLE.titleAlign,
+      titleLevel: ROLE_REPORT_STYLE.titleLevel
+    })
+  )
+
   const handleSearch = (params) => {
-    const { daterange, ...filtersParams } = params
-    const [startTime, endTime] = Array.isArray(daterange) ? daterange : [null, null]
-    replaceSearchParams({ ...filtersParams, startTime, endTime })
+    replaceSearchParams(buildRoleSearchParams(params))
     getData()
   }
-  const buttonMoreClick = (item, row) => {
+
+  const handleReset = () => {
+    Object.assign(searchForm.value, createRoleSearchState())
+    resetSearchParams()
+  }
+
+  const handleSelectionChange = (rows) => {
+    selectedRows.value = Array.isArray(rows) ? rows : []
+  }
+
+  const handlePreviewVisibleChange = (visible) => {
+    previewVisible.value = Boolean(visible)
+    if (!visible) {
+      activePrintToken.value = 0
+    }
+  }
+
+  const showDialog = (type, row) => {
+    dialogType.value = type
+    currentRoleData.value = type === 'edit' ? buildRoleDialogModel(row) : buildRoleDialogModel()
+    dialogVisible.value = true
+  }
+
+  const handleActionClick = (item, row) => {
     switch (item.key) {
-      case 'permission':
-        showPermissionDialog(row)
+      case 'scope-menu':
+        openScopeDialog('menu', row)
+        break
+      case 'scope-pda':
+        openScopeDialog('pda', row)
+        break
+      case 'scope-matnr':
+        openScopeDialog('matnr', row)
+        break
+      case 'scope-warehouse':
+        openScopeDialog('warehouse', row)
         break
       case 'edit':
         showDialog('edit', row)
         break
       case 'delete':
-        deleteRole(row)
+        handleDelete(row)
+        break
+      default:
         break
     }
   }
-  const showPermissionDialog = (row) => {
-    permissionDialog.value = true
-    currentRoleData.value = row
+
+  const openScopeDialog = (scopeType, row) => {
+    permissionScopeType.value = scopeType
+    currentRoleData.value = buildRoleDialogModel(row)
+    permissionDialogVisible.value = true
   }
-  const deleteRole = (row) => {
-    ElMessageBox.confirm(`纭畾鍒犻櫎瑙掕壊"${row.roleName}"鍚楋紵姝ゆ搷浣滀笉鍙仮澶嶏紒`, '鍒犻櫎纭', {
-      confirmButtonText: '纭畾',
-      cancelButtonText: '鍙栨秷',
-      type: 'warning'
-    })
-      .then(() => {
-        ElMessage.success('鍒犻櫎鎴愬姛')
-        refreshData()
+
+  const handleDialogSubmit = async (formData) => {
+    const payload = buildRoleSavePayload(formData)
+    try {
+      if (dialogType.value === 'edit') {
+        await fetchUpdateRole(payload)
+        ElMessage.success('淇敼鎴愬姛')
+        dialogVisible.value = false
+        currentRoleData.value = buildRoleDialogModel()
+        await refreshUpdate()
+        return
+      }
+      await fetchSaveRole(payload)
+      ElMessage.success('鏂板鎴愬姛')
+      dialogVisible.value = false
+      currentRoleData.value = buildRoleDialogModel()
+      await refreshCreate()
+    } catch (error) {
+      ElMessage.error(error?.message || '鎻愪氦澶辫触')
+    }
+  }
+
+  const handleDelete = async (row) => {
+    try {
+      await ElMessageBox.confirm(`纭畾瑕佸垹闄よ鑹层��${row.name || row.code || row.id}銆嶅悧锛焋, '鍒犻櫎纭', {
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        type: 'warning'
       })
-      .catch(() => {
-        ElMessage.info('宸插彇娑堝垹闄�')
+      await fetchDeleteRole(row.id)
+      ElMessage.success('鍒犻櫎鎴愬姛')
+      await refreshRemove()
+    } catch (error) {
+      if (error !== 'cancel') {
+        ElMessage.error(error?.message || '鍒犻櫎澶辫触')
+      }
+    }
+  }
+
+  const handleBatchDelete = async () => {
+    if (!selectedRows.value.length) return
+    const ids = selectedRows.value.map((item) => item.id).filter((id) => id !== void 0 && id !== null)
+    if (!ids.length) return
+
+    try {
+      await ElMessageBox.confirm(`纭畾瑕佹壒閲忓垹闄ら�変腑鐨� ${ids.length} 涓鑹插悧锛焋, '鎵归噺鍒犻櫎纭', {
+        confirmButtonText: '纭畾',
+        cancelButtonText: '鍙栨秷',
+        type: 'warning'
       })
+      await fetchDeleteRole(ids.join(','))
+      ElMessage.success('鎵归噺鍒犻櫎鎴愬姛')
+      selectedRows.value = []
+      await refreshRemove()
+    } catch (error) {
+      if (error !== 'cancel') {
+        ElMessage.error(error?.message || '鎵归噺鍒犻櫎澶辫触')
+      }
+    }
+  }
+
+  const handleExport = async (payload) => {
+    try {
+      const response = await fetchExportRoleReport(payload, {
+        headers: {
+          Authorization: userStore.accessToken || ''
+        }
+      })
+      if (!response.ok) {
+        throw new Error(`瀵煎嚭澶辫触 (${response.status})`)
+      }
+      const blob = await response.blob()
+      const downloadUrl = window.URL.createObjectURL(blob)
+      const link = document.createElement('a')
+      link.href = downloadUrl
+      link.download = 'role.xlsx'
+      document.body.appendChild(link)
+      link.click()
+      link.remove()
+      window.URL.revokeObjectURL(downloadUrl)
+      ElMessage.success('瀵煎嚭鎴愬姛')
+    } catch (error) {
+      ElMessage.error(error?.message || '瀵煎嚭澶辫触')
+    }
+  }
+
+  const handlePrint = async (payload) => {
+    const token = previewToken.value + 1
+    previewToken.value = token
+    activePrintToken.value = token
+    previewVisible.value = false
+    previewRows.value = []
+    previewMeta.value = {}
+
+    try {
+      const response = Array.isArray(payload?.ids) && payload.ids.length > 0
+        ? await fetchGetRoleMany(payload.ids)
+        : await fetchRolePrintPage({
+            ...reportQueryParams.value,
+            current: 1,
+            pageSize: Number(pagination.total) > 0 ? Number(pagination.total) : Number(payload?.pageSize) || 20
+          })
+      if (activePrintToken.value !== token) {
+        return
+      }
+      const records = defaultResponseAdapter(response).records
+      if (activePrintToken.value !== token) {
+        return
+      }
+
+      const rows = buildRolePrintRows(records)
+      const now = new Date()
+      previewRows.value = rows
+      previewMeta.value = {
+        reportTitle,
+        reportDate: now.toLocaleDateString('zh-CN'),
+        printedAt: now.toLocaleString('zh-CN', { hour12: false }),
+        operator: userStore.getUserInfo?.name || userStore.getUserInfo?.username || '',
+        count: rows.length
+      }
+      handlePreviewVisibleChange(true)
+    } catch (error) {
+      if (activePrintToken.value !== token) {
+        return
+      }
+      ElMessage.error(error?.message || '鎵撳嵃澶辫触')
+    }
   }
 </script>
diff --git a/rsf-design/src/views/system/role/modules/role-search.vue b/rsf-design/src/views/system/role/modules/role-search.vue
index afcb688..a8838ab 100644
--- a/rsf-design/src/views/system/role/modules/role-search.vue
+++ b/rsf-design/src/views/system/role/modules/role-search.vue
@@ -3,84 +3,85 @@
     ref="searchBarRef"
     v-model="formData"
     :items="formItems"
-    :rules="rules"
+    :showExpand="false"
     @reset="handleReset"
     @search="handleSearch"
-  >
-  </ArtSearchBar>
+  />
 </template>
 
 <script setup>
+  import { createRoleSearchState } from '../rolePage.helpers'
+
   const props = defineProps({
     modelValue: { required: true }
   })
+
   const emit = defineEmits(['update:modelValue', 'search', 'reset'])
   const searchBarRef = ref()
+
   const formData = computed({
     get: () => props.modelValue,
     set: (val) => emit('update:modelValue', val)
   })
-  const rules = {}
-  const statusOptions = ref([
-    { label: '鍚敤', value: true },
-    { label: '绂佺敤', value: false }
-  ])
+
   const formItems = computed(() => [
     {
       label: '瑙掕壊鍚嶇О',
-      key: 'roleName',
+      key: 'name',
       type: 'input',
-      placeholder: '璇疯緭鍏ヨ鑹插悕绉�',
-      clearable: true
-    },
-    {
-      label: '瑙掕壊缂栫爜',
-      key: 'roleCode',
-      type: 'input',
-      placeholder: '璇疯緭鍏ヨ鑹茬紪鐮�',
-      clearable: true
-    },
-    {
-      label: '瑙掕壊鎻忚堪',
-      key: 'description',
-      type: 'input',
-      placeholder: '璇疯緭鍏ヨ鑹叉弿杩�',
-      clearable: true
-    },
-    {
-      label: '瑙掕壊鐘舵��',
-      key: 'enabled',
-      type: 'select',
       props: {
-        placeholder: '璇烽�夋嫨鐘舵��',
-        options: statusOptions.value,
+        placeholder: '璇疯緭鍏ヨ鑹插悕绉�',
         clearable: true
       }
     },
     {
-      label: '鍒涘缓鏃ユ湡',
-      key: 'daterange',
-      type: 'datetime',
+      label: '瑙掕壊缂栫爜',
+      key: 'code',
+      type: 'input',
       props: {
-        style: { width: '100%' },
-        placeholder: '璇烽�夋嫨鏃ユ湡鑼冨洿',
-        type: 'daterange',
-        rangeSeparator: '鑷�',
-        startPlaceholder: '寮�濮嬫棩鏈�',
-        endPlaceholder: '缁撴潫鏃ユ湡',
-        valueFormat: 'YYYY-MM-DD',
-        shortcuts: [
-          { text: '浠婃棩', value: [/* @__PURE__ */ new Date(), /* @__PURE__ */ new Date()] },
-          { text: '鏈�杩戜竴鍛�', value: [new Date(Date.now() - 6048e5), /* @__PURE__ */ new Date()] },
-          { text: '鏈�杩戜竴涓湀', value: [new Date(Date.now() - 2592e6), /* @__PURE__ */ new Date()] }
+        placeholder: '璇疯緭鍏ヨ鑹茬紪鐮�',
+        clearable: true
+      }
+    },
+    {
+      label: '澶囨敞',
+      key: 'memo',
+      type: 'input',
+      props: {
+        placeholder: '璇疯緭鍏ュ娉�',
+        clearable: true
+      }
+    },
+    {
+      label: '鍏抽敭瀛�',
+      key: 'condition',
+      type: 'input',
+      props: {
+        placeholder: '杈撳叆鍏抽敭瀛楁悳绱�',
+        clearable: true
+      }
+    },
+    {
+      label: '鐘舵��',
+      key: 'status',
+      type: 'select',
+      props: {
+        placeholder: '璇烽�夋嫨鐘舵��',
+        clearable: true,
+        options: [
+          { label: '姝e父', value: 1 },
+          { label: '绂佺敤', value: 0 }
         ]
       }
     }
   ])
-  const handleReset = () => {
+
+  function handleReset() {
+    emit('update:modelValue', createRoleSearchState())
     emit('reset')
   }
-  const handleSearch = async (params) => {
+
+  async function handleSearch(params) {
     await searchBarRef.value.validate()
     emit('search', params)
   }
diff --git a/rsf-design/src/views/system/role/rolePage.helpers.js b/rsf-design/src/views/system/role/rolePage.helpers.js
new file mode 100644
index 0000000..77b7b81
--- /dev/null
+++ b/rsf-design/src/views/system/role/rolePage.helpers.js
@@ -0,0 +1,320 @@
+export function createRoleSearchState() {
+  return {
+    name: '',
+    code: '',
+    memo: '',
+    status: void 0,
+    condition: ''
+  }
+}
+
+export function createRoleFormState() {
+  return {
+    id: void 0,
+    name: '',
+    code: '',
+    memo: '',
+    status: 1
+  }
+}
+
+export function buildRoleSearchParams(params = {}) {
+  const searchParams = {
+    name: params.name,
+    code: params.code,
+    memo: params.memo,
+    status: params.status,
+    condition: params.condition
+  }
+
+  return Object.fromEntries(
+    Object.entries(searchParams).filter(([, value]) => value !== '' && value !== void 0 && value !== null)
+  )
+}
+
+export function buildRolePageQueryParams(params = {}) {
+  const { current, size, pageSize, ...filters } = params
+  return {
+    current: current || 1,
+    pageSize: pageSize || size || 20,
+    ...buildRoleSearchParams(filters)
+  }
+}
+
+const ROLE_REPORT_COLUMNS = [
+  { source: 'name', label: '瑙掕壊鍚嶇О' },
+  { source: 'code', label: '瑙掕壊缂栫爜' },
+  { source: 'statusText', label: '鐘舵��' },
+  { source: 'memo', label: '澶囨敞' },
+  { source: 'createTimeText', label: '鍒涘缓鏃堕棿' },
+  { source: 'updateTimeText', label: '鏇存柊鏃堕棿' }
+]
+
+const ROLE_REPORT_SOURCE_ALIAS = {
+  status: 'statusText'
+}
+
+export const ROLE_REPORT_TITLE = '瑙掕壊绠$悊鎶ヨ〃'
+
+export const ROLE_REPORT_STYLE = {
+  titleAlign: 'center',
+  titleLevel: 'strong'
+}
+
+export function getRoleReportColumns() {
+  return ROLE_REPORT_COLUMNS.map((column) => ({ ...column }))
+}
+
+export function resolveRoleReportColumns(columns = []) {
+  if (!Array.isArray(columns)) {
+    return []
+  }
+
+  const allowedColumns = new Map(ROLE_REPORT_COLUMNS.map((column) => [column.source, column]))
+  const seenSources = new Set()
+
+  return columns
+    .map((column) => {
+      if (!column || typeof column !== 'object') {
+        return null
+      }
+
+      const source = ROLE_REPORT_SOURCE_ALIAS[column.source ?? column.prop] ?? column.source ?? column.prop
+      if (!source || !allowedColumns.has(source) || seenSources.has(source)) {
+        return null
+      }
+
+      seenSources.add(source)
+      const allowedColumn = allowedColumns.get(source)
+      return {
+        source,
+        label: column.label || allowedColumn.label
+      }
+    })
+    .filter(Boolean)
+}
+
+export function buildRolePrintRows(records = []) {
+  if (!Array.isArray(records)) {
+    return []
+  }
+
+  return records.map((record) => normalizeRoleListRow(record))
+}
+
+export function buildRoleReportMeta({
+  previewMeta = {},
+  count = 0,
+  titleAlign = ROLE_REPORT_STYLE.titleAlign,
+  titleLevel = ROLE_REPORT_STYLE.titleLevel
+} = {}) {
+  return {
+    reportTitle: ROLE_REPORT_TITLE,
+    reportDate: previewMeta.reportDate,
+    printedAt: previewMeta.printedAt,
+    operator: previewMeta.operator,
+    count,
+    reportStyle: {
+      titleAlign,
+      titleLevel,
+      orientation: 'portrait',
+      density: 'compact',
+      showSequence: true
+    }
+  }
+}
+
+export function buildRoleDialogModel(record = {}) {
+  return {
+    ...createRoleFormState(),
+    id: normalizeRoleId(record.id),
+    name: record.name || '',
+    code: record.code || '',
+    memo: record.memo || '',
+    status: record.status !== void 0 && record.status !== null ? record.status : 1
+  }
+}
+
+export function buildRoleSavePayload(form = {}) {
+  return {
+    id: normalizeRoleId(form.id),
+    name: form.name || '',
+    code: form.code || '',
+    memo: form.memo || '',
+    status: form.status !== void 0 && form.status !== null ? form.status : 1
+  }
+}
+
+export function normalizeRoleListRow(record = {}) {
+  const statusMeta = getRoleStatusMeta(record.statusBool ?? record.status)
+  return {
+    ...record,
+    statusBool: record.statusBool !== void 0 ? Boolean(record.statusBool) : statusMeta.bool,
+    statusText: statusMeta.text,
+    statusType: statusMeta.type,
+    createTimeText: record.createTime$ || record.createTime || '',
+    updateTimeText: record.updateTime$ || record.updateTime || ''
+  }
+}
+
+export function getRoleStatusMeta(status) {
+  if (status === true || status === 1) {
+    return { type: 'success', text: '姝e父', bool: true }
+  }
+  if (status === false || status === 0) {
+    return { type: 'danger', text: '绂佺敤', bool: false }
+  }
+  return { type: 'info', text: '鏈煡', bool: false }
+}
+
+export function getRoleScopeConfig(scopeType) {
+  const configMap = {
+    menu: {
+      scopeType: 'menu',
+      title: '缃戦〉鏉冮檺',
+      listUrl: '/role/scope/list',
+      treeUrl: '/menu/tree'
+    },
+    pda: {
+      scopeType: 'pda',
+      title: 'PDA鏉冮檺',
+      listUrl: '/rolePda/scope/list',
+      treeUrl: '/menuPda/tree'
+    },
+    matnr: {
+      scopeType: 'matnr',
+      title: '鐗╂枡鏉冮檺',
+      listUrl: '/roleMatnr/scope/list',
+      treeUrl: '/menuMatnrGroup/tree'
+    },
+    warehouse: {
+      scopeType: 'warehouse',
+      title: '浠撳簱鏉冮檺',
+      listUrl: '/roleWarehouse/scope/list',
+      treeUrl: '/menuWarehouse/tree'
+    }
+  }
+
+  const config = configMap[scopeType]
+  if (!config) {
+    throw new Error(`Unsupported scope type: ${scopeType}`)
+  }
+  return config
+}
+
+export function buildRoleScopeSubmitPayload(roleId, checkedKeys = [], halfCheckedKeys = []) {
+  return {
+    id: normalizeRoleId(roleId),
+    menuIds: {
+      checked: normalizeScopeKeys(checkedKeys),
+      halfChecked: normalizeScopeKeys(halfCheckedKeys)
+    }
+  }
+}
+
+export function normalizeRoleScopeTreeData(scopeType, treeData = []) {
+  if (!Array.isArray(treeData)) {
+    return []
+  }
+
+  return treeData
+    .map((node) => normalizeRoleScopeNode(scopeType, node))
+    .filter(Boolean)
+}
+
+function normalizeRoleScopeNode(scopeType, node) {
+  if (!node || typeof node !== 'object') {
+    return null
+  }
+
+  if (scopeType === 'menu' && node.type === 1) {
+    return buildScopeAuthNode(node)
+  }
+
+  const children = Array.isArray(node.children)
+    ? normalizeRoleScopeTreeData(scopeType, node.children)
+    : []
+  const metaSource = node.meta && typeof node.meta === 'object' ? node.meta : node
+  const authNodes = 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 || ''),
+        type: 1,
+        isAuthButton: true,
+        authMark: auth.authMark || auth.authority || auth.code || '',
+        children: []
+      }))
+    : []
+
+  const mergedChildren =
+    authNodes.length > 0 && !children.some((child) => child.isAuthButton)
+      ? [...children, ...authNodes]
+      : children
+
+  return {
+    id: normalizeScopeKey(node.id ?? node.value),
+    label: normalizeScopeTitle(
+      node.label || node.title || node.name || metaSource.title || node.code || ''
+    ),
+    type: node.type,
+    path: node.path || '',
+    component: node.component || '',
+    isAuthButton: Boolean(node.isAuthButton),
+    authMark: node.authMark || metaSource.authMark || metaSource.authority || metaSource.code || '',
+    meta: metaSource,
+    children: mergedChildren
+  }
+}
+
+function buildScopeAuthNode(node) {
+  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 || ''),
+    type: 1,
+    isAuthButton: true,
+    authMark: node.authMark || metaSource.authMark || metaSource.authority || metaSource.code || '',
+    children: []
+  }
+}
+
+function normalizeScopeKeys(keys = []) {
+  if (!Array.isArray(keys)) {
+    return []
+  }
+
+  return Array.from(
+    new Set(
+      keys
+        .map((key) => normalizeRoleId(key))
+        .filter((key) => key !== void 0)
+    )
+  )
+}
+
+function normalizeScopeKey(value) {
+  const normalized = normalizeRoleId(value)
+  return normalized === void 0 ? '' : String(normalized)
+}
+
+function normalizeScopeTitle(title) {
+  if (typeof title !== 'string') {
+    return ''
+  }
+  const trimmedTitle = title.trim()
+  if (trimmedTitle.startsWith('menu.')) {
+    return `menus.${trimmedTitle.slice('menu.'.length)}`
+  }
+  return trimmedTitle
+}
+
+function normalizeRoleId(value) {
+  if (value === '' || value === null || value === void 0) {
+    return void 0
+  }
+  const numeric = Number(value)
+  if (Number.isNaN(numeric)) {
+    return value
+  }
+  return numeric
+}
diff --git a/rsf-design/src/views/system/user/index.vue b/rsf-design/src/views/system/user/index.vue
index bf169fd..634f8c9 100644
--- a/rsf-design/src/views/system/user/index.vue
+++ b/rsf-design/src/views/system/user/index.vue
@@ -1,81 +1,104 @@
 <!-- 鐢ㄦ埛绠$悊椤甸潰 -->
-<!-- art-full-height 鑷姩璁$畻鍑洪〉闈㈠墿浣欓珮搴� -->
-<!-- art-table-card 涓�涓鍚堢郴缁熸牱寮忕殑 class锛屽悓鏃惰嚜鍔ㄦ拺婊″墿浣欓珮搴� -->
-<!-- 鏇村 useTable 浣跨敤绀轰緥璇风Щ姝ヨ嚦 鍔熻兘绀轰緥 涓嬮潰鐨勯珮绾ц〃鏍肩ず渚嬫垨鑰呮煡鐪嬪畼鏂规枃妗� -->
-<!-- useTable 鏂囨。锛歨ttps://www.artd.pro/docs/zh/guide/hooks/use-table.html -->
 <template>
   <div class="user-page art-full-height">
-    <!-- 鎼滅储鏍� -->
-    <UserSearch v-model="searchForm" @search="handleSearch" @reset="resetSearchParams"></UserSearch>
+    <UserSearch
+      v-model="searchForm"
+      :dept-tree-options="deptTreeOptions"
+      :role-options="roleOptions"
+      @search="handleSearch"
+      @reset="handleReset"
+    />
 
     <ElCard class="art-table-card">
-      <!-- 琛ㄦ牸澶撮儴 -->
       <ArtTableHeader v-model:columns="columnChecks" :loading="loading" @refresh="refreshData">
         <template #left>
           <ElSpace wrap>
-            <ElButton @click="showDialog('add')" v-ripple>鏂板鐢ㄦ埛</ElButton>
+            <ElButton v-auth="'add'" @click="showDialog('add')" v-ripple>鏂板鐢ㄦ埛</ElButton>
           </ElSpace>
         </template>
       </ArtTableHeader>
 
-      <!-- 琛ㄦ牸 -->
       <ArtTable
         :loading="loading"
         :data="data"
         :columns="columns"
         :pagination="pagination"
-        @selection-change="handleSelectionChange"
         @pagination:size-change="handleSizeChange"
         @pagination:current-change="handleCurrentChange"
-      >
-      </ArtTable>
+      />
 
-      <!-- 鐢ㄦ埛寮圭獥 -->
       <UserDialog
         v-model:visible="dialogVisible"
         :type="dialogType"
         :user-data="currentUserData"
+        :role-options="roleOptions"
+        :dept-tree-options="deptTreeOptions"
         @submit="handleDialogSubmit"
+      />
+
+      <UserDetailDrawer
+        v-model:visible="detailDrawerVisible"
+        :loading="detailLoading"
+        :user-data="detailUserData"
       />
     </ElCard>
   </div>
 </template>
 
 <script setup>
+  import request from '@/utils/http'
+  import {
+    fetchDeleteUser,
+    fetchGetDeptTree,
+    fetchGetRoleOptions,
+    fetchGetUserDetail,
+    fetchResetUserPassword,
+    fetchSaveUser,
+    fetchUpdateUser,
+    fetchUpdateUserStatus
+  } from '@/api/system-manage'
+  import { useTable } from '@/hooks/core/useTable'
+  import { useAuth } from '@/hooks/core/useAuth'
+  import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
+  import { ElMessage, ElMessageBox, ElSwitch, ElTag } from 'element-plus'
   import UserSearch from './modules/user-search.vue'
   import UserDialog from './modules/user-dialog.vue'
+  import UserDetailDrawer from './modules/user-detail-drawer.vue'
+  import {
+    buildUserDialogModel,
+    buildUserPageQueryParams,
+    buildUserSavePayload,
+    buildUserSearchParams,
+    createUserSearchState,
+    getUserStatusMeta,
+    mergeUserDetailRecord,
+    normalizeDeptTreeOptions,
+    normalizeRoleOptions,
+    normalizeUserListRow,
+    formatUserRoleNames
+  } from './userPage.helpers'
 
-  import ArtButtonTable from '@/components/core/forms/art-button-table/index.vue'
-  import { ACCOUNT_TABLE_DATA } from '@/mock/temp/formData'
-  import { useTable } from '@/hooks/core/useTable'
-  import { fetchGetUserList } from '@/api/system-manage'
-  import { ElTag, ElMessageBox, ElImage } from 'element-plus'
   defineOptions({ name: 'User' })
+
+  const searchForm = ref(createUserSearchState())
   const dialogType = ref('add')
   const dialogVisible = ref(false)
-  const currentUserData = ref({})
-  const selectedRows = ref([])
-  const searchForm = ref({
-    userName: void 0,
-    userGender: void 0,
-    userPhone: void 0,
-    userEmail: void 0,
-    status: '1'
-  })
-  const USER_STATUS_CONFIG = {
-    1: { type: 'success', text: '鍦ㄧ嚎' },
-    2: { type: 'info', text: '绂荤嚎' },
-    3: { type: 'warning', text: '寮傚父' },
-    4: { type: 'danger', text: '娉ㄩ攢' }
+  const currentUserData = ref(buildUserDialogModel())
+  const detailDrawerVisible = ref(false)
+  const detailLoading = ref(false)
+  const detailUserData = ref({})
+  const roleOptions = ref([])
+  const deptTreeOptions = ref([])
+  const RESET_PASSWORD = '123456'
+  const { hasAuth } = useAuth()
+
+  const fetchUserPage = (params = {}) => {
+    return request.post({
+      url: '/user/page',
+      params: buildUserPageQueryParams(params)
+    })
   }
-  const getUserStatusConfig = (status) => {
-    return (
-      USER_STATUS_CONFIG[status] || {
-        type: 'info',
-        text: '鏈煡'
-      }
-    )
-  }
+
   const {
     columns,
     columnChecks,
@@ -87,136 +110,283 @@
     resetSearchParams,
     handleSizeChange,
     handleCurrentChange,
-    refreshData
+    refreshData,
+    refreshCreate,
+    refreshUpdate,
+    refreshRemove
   } = useTable({
-    // 鏍稿績閰嶇疆
     core: {
-      apiFn: fetchGetUserList,
-      apiParams: {
-        current: 1,
-        size: 20,
-        ...searchForm.value
-      },
-      // 鑷畾涔夊垎椤靛瓧娈垫槧灏勶紝鏈缃椂灏嗕娇鐢ㄥ叏灞�閰嶇疆 tableConfig.ts 涓殑 paginationKey
-      // paginationKey: {
-      //   current: 'pageNum',
-      //   size: 'pageSize'
-      // },
+      apiFn: fetchUserPage,
+      apiParams: buildUserPageQueryParams(searchForm.value),
       columnsFactory: () => [
-        { type: 'selection' },
-        // 鍕鹃�夊垪
-        { type: 'index', width: 60, label: '搴忓彿' },
-        // 搴忓彿
         {
-          prop: 'userInfo',
+          prop: 'username',
           label: '鐢ㄦ埛鍚�',
-          width: 280,
-          // visible: false, // 榛樿鏄惁鏄剧ず鍒�
+          minWidth: 140,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'nickname',
+          label: '鏄电О',
+          minWidth: 120,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'deptLabel',
+          label: '閮ㄩ棬',
+          minWidth: 140,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'phone',
+          label: '鎵嬫満鍙�',
+          minWidth: 130
+        },
+        {
+          prop: 'email',
+          label: '閭',
+          minWidth: 180,
+          showOverflowTooltip: true
+        },
+        {
+          prop: 'roleNames',
+          label: '瑙掕壊',
+          minWidth: 180,
+          showOverflowTooltip: true,
+          formatter: (row) => formatUserRoleNames(row.roles) || row.roleNames || '-'
+        },
+        {
+          prop: 'status',
+          label: '鐘舵��',
+          width: 180,
           formatter: (row) => {
-            return h('div', { class: 'user flex-c' }, [
-              h(ElImage, {
-                class: 'size-9.5 rounded-md',
-                src: row.avatar,
-                previewSrcList: [row.avatar],
-                // 鍥剧墖棰勮鏄惁鎻掑叆鑷� body 鍏冪礌涓婏紝鐢ㄤ簬瑙e喅琛ㄦ牸鍐呴儴鍥剧墖棰勮鏍峰紡寮傚父
-                previewTeleported: true
+            const statusMeta = getUserStatusMeta(row.statusBool ?? row.status)
+            if (!hasAuth('edit')) {
+              return h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
+            }
+            return h('div', { class: 'flex items-center gap-2' }, [
+              h(ElSwitch, {
+                modelValue: row.statusBool ?? statusMeta.bool,
+                loading: row._statusLoading,
+                'onUpdate:modelValue': (value) => handleStatusChange(row, value)
               }),
-              h('div', { class: 'ml-2' }, [
-                h('p', { class: 'user-name' }, row.userName),
-                h('p', { class: 'email' }, row.userEmail)
-              ])
+              h(ElTag, { type: statusMeta.type, effect: 'light' }, () => statusMeta.text)
             ])
           }
         },
         {
-          prop: 'userGender',
-          label: '鎬у埆',
+          prop: 'updateTimeText',
+          label: '鏇存柊鏃堕棿',
+          minWidth: 180,
           sortable: true,
-          formatter: (row) => row.userGender
-        },
-        { prop: 'userPhone', label: '鎵嬫満鍙�' },
-        {
-          prop: 'status',
-          label: '鐘舵��',
-          formatter: (row) => {
-            const statusConfig = getUserStatusConfig(row.status)
-            return h(ElTag, { type: statusConfig.type }, () => statusConfig.text)
-          }
+          formatter: (row) => row.updateTimeText || '-'
         },
         {
-          prop: 'createTime',
-          label: '鍒涘缓鏃ユ湡',
-          sortable: true
+          prop: 'createTimeText',
+          label: '鍒涘缓鏃堕棿',
+          minWidth: 180,
+          sortable: true,
+          formatter: (row) => row.createTimeText || '-'
         },
         {
           prop: 'operation',
           label: '鎿嶄綔',
-          width: 120,
+          width: 220,
           fixed: 'right',
-          // 鍥哄畾鍒�
-          formatter: (row) =>
-            h('div', [
-              h(ArtButtonTable, {
-                type: 'edit',
-                onClick: () => showDialog('edit', row)
-              }),
-              h(ArtButtonTable, {
-                type: 'delete',
-                onClick: () => deleteUser(row)
-              })
-            ])
+          formatter: (row) => {
+            const buttons = []
+
+            if (hasAuth('query')) {
+              buttons.push(
+                h(ArtButtonTable, {
+                  type: 'view',
+                  onClick: () => openDetail(row)
+                })
+              )
+            }
+
+            if (hasAuth('edit')) {
+              buttons.push(
+                h(ArtButtonTable, {
+                  type: 'edit',
+                  onClick: () => openEditDialog(row)
+                }),
+                h(ArtButtonTable, {
+                  icon: 'ri:key-2-line',
+                  iconClass: 'bg-warning/12 text-warning',
+                  onClick: () => handleResetPassword(row)
+                })
+              )
+            }
+
+            if (hasAuth('delete')) {
+              buttons.push(
+                h(ArtButtonTable, {
+                  type: 'delete',
+                  onClick: () => handleDelete(row)
+                })
+              )
+            }
+
+            return h('div', buttons)
+          }
         }
       ]
     },
-    // 鏁版嵁澶勭悊
     transform: {
-      // 鏁版嵁杞崲鍣� - 鏇挎崲澶村儚
       dataTransformer: (records) => {
         if (!Array.isArray(records)) {
-          console.warn('鏁版嵁杞崲鍣�: 鏈熸湜鏁扮粍绫诲瀷锛屽疄闄呮敹鍒�:', typeof records)
           return []
         }
-        return records.map((item, index) => {
-          return {
-            ...item,
-            avatar: ACCOUNT_TABLE_DATA[index % ACCOUNT_TABLE_DATA.length].avatar
-          }
-        })
+        return records.map((item) => normalizeUserListRow(item))
       }
     }
   })
-  const handleSearch = (params) => {
-    replaceSearchParams(params)
-    getData()
-  }
-  const showDialog = (type, row) => {
-    console.log('鎵撳紑寮圭獥:', { type, row })
-    dialogType.value = type
-    currentUserData.value = row || {}
-    nextTick(() => {
-      dialogVisible.value = true
-    })
-  }
-  const deleteUser = (row) => {
-    console.log('鍒犻櫎鐢ㄦ埛:', row)
-    ElMessageBox.confirm(`纭畾瑕佹敞閿�璇ョ敤鎴峰悧锛焋, '娉ㄩ攢鐢ㄦ埛', {
-      confirmButtonText: '纭畾',
-      cancelButtonText: '鍙栨秷',
-      type: 'error'
-    }).then(() => {
-      ElMessage.success('娉ㄩ攢鎴愬姛')
-    })
-  }
-  const handleDialogSubmit = async () => {
+
+  const loadLookups = async () => {
     try {
-      dialogVisible.value = false
-      currentUserData.value = {}
+      const [roles, depts] = await Promise.all([fetchGetRoleOptions({}), fetchGetDeptTree({})])
+      roleOptions.value = normalizeRoleOptions(roles)
+      deptTreeOptions.value = normalizeDeptTreeOptions(depts)
     } catch (error) {
-      console.error('鎻愪氦澶辫触:', error)
+      console.error('鍔犺浇鐢ㄦ埛椤靛瓧鍏稿け璐�', error)
     }
   }
-  const handleSelectionChange = (selection) => {
-    selectedRows.value = selection
-    console.log('閫変腑琛屾暟鎹�:', selectedRows.value)
+
+  onMounted(() => {
+    loadLookups()
+  })
+
+  const handleSearch = (params) => {
+    replaceSearchParams(buildUserSearchParams(params))
+    getData()
+  }
+
+  const handleReset = () => {
+    Object.assign(searchForm.value, createUserSearchState())
+    resetSearchParams()
+  }
+
+  const showDialog = (type, row) => {
+    dialogType.value = type
+    currentUserData.value = type === 'edit' ? buildUserDialogModel(row) : buildUserDialogModel()
+    dialogVisible.value = true
+  }
+
+  const loadUserDetail = async (id) => {
+    detailLoading.value = true
+    try {
+      return await fetchGetUserDetail(id)
+    } finally {
+      detailLoading.value = false
+    }
+  }
+
+  const openEditDialog = async (row) => {
+    try {
+      const detail = await loadUserDetail(row.id)
+      currentUserData.value = buildUserDialogModel(mergeUserDetailRecord(detail, row))
+      dialogType.value = 'edit'
+      dialogVisible.value = true
+    } catch (error) {
+      ElMessage.error(error?.message || '鑾峰彇鐢ㄦ埛璇︽儏澶辫触')
+    }
+  }
+
+  const openDetail = async (row) => {
+    detailDrawerVisible.value = true
+    try {
+      detailUserData.value = mergeUserDetailRecord(await loadUserDetail(row.id), row)
+    } catch (error) {
+      detailDrawerVisible.value = false
+      detailUserData.value = {}
+      ElMessage.error(error?.message || '鑾峰彇鐢ㄦ埛璇︽儏澶辫触')
+    }
+  }
+
+  const handleDialogSubmit = async (formData) => {
+    const payload = buildUserSavePayload(formData)
+    try {
+      if (dialogType.value === 'edit') {
+        await fetchUpdateUser(payload)
+        ElMessage.success('淇敼鎴愬姛')
+        dialogVisible.value = false
+        currentUserData.value = buildUserDialogModel()
+        await refreshUpdate()
+        return
+      }
+      await fetchSaveUser(payload)
+      ElMessage.success('鏂板鎴愬姛')
+      dialogVisible.value = false
+      currentUserData.value = buildUserDialogModel()
+      await refreshCreate()
+    } catch (error) {
+      ElMessage.error(error?.message || '鎻愪氦澶辫触')
+    }
+  }
+
+  const handleDelete = async (row) => {
+    try {
+      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 handleResetPassword = async (row) => {
+    try {
+      await ElMessageBox.confirm(
+        `纭畾灏嗙敤鎴枫��${row.username || row.nickname || row.id}銆嶇殑瀵嗙爜閲嶇疆涓� ${RESET_PASSWORD} 鍚楋紵`,
+        '閲嶇疆瀵嗙爜',
+        {
+          confirmButtonText: '纭畾',
+          cancelButtonText: '鍙栨秷',
+          type: 'warning'
+        }
+      )
+      await fetchResetUserPassword({
+        id: row.id,
+        password: RESET_PASSWORD
+      })
+      ElMessage.success(`瀵嗙爜宸查噸缃负 ${RESET_PASSWORD}`)
+      await refreshUpdate()
+    } catch (error) {
+      if (error !== 'cancel') {
+        ElMessage.error(error?.message || '閲嶇疆瀵嗙爜澶辫触')
+      }
+    }
+  }
+
+  const handleStatusChange = async (row, checked) => {
+    const previousStatus = row.status
+    const previousStatusBool = row.statusBool
+    const nextStatus = checked ? 1 : 0
+    row._statusLoading = true
+    row.status = nextStatus
+    row.statusBool = checked
+
+    try {
+      await fetchUpdateUserStatus({
+        id: row.id,
+        status: nextStatus
+      })
+      ElMessage.success('鐘舵�佸凡鏇存柊')
+      await refreshUpdate()
+    } catch (error) {
+      row.status = previousStatus
+      row.statusBool = previousStatusBool
+      ElMessage.error(error?.message || '鐘舵�佹洿鏂板け璐�')
+    } finally {
+      row._statusLoading = 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
new file mode 100644
index 0000000..9bf4210
--- /dev/null
+++ b/rsf-design/src/views/system/user/modules/user-detail-drawer.vue
@@ -0,0 +1,56 @@
+<template>
+  <ElDrawer
+    :model-value="visible"
+    title="鐢ㄦ埛璇︽儏"
+    size="520px"
+    @update:model-value="handleVisibleChange"
+  >
+    <ElSkeleton :loading="loading" animated :rows="10">
+      <ElDescriptions :column="1" border>
+        <ElDescriptionsItem label="鐢ㄦ埛鍚�">{{ displayData.username || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="鏄电О">{{ displayData.nickname || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="閮ㄩ棬">{{ displayData.deptLabel || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="瑙掕壊">{{ displayData.roleNames || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="鐘舵��">{{ statusLabel }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="鎵嬫満鍙�">{{ displayData.phone || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="閭">{{ displayData.email || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="鐪熷疄濮撳悕">{{ displayData.realName || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="韬唤璇佸彿">{{ displayData.idCard || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="宸ュ彿">{{ displayData.code || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="鎬у埆">{{ sexLabel }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="鍒涘缓鏃堕棿">{{ displayData.createTimeText || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="鏇存柊鏃堕棿">{{ displayData.updateTimeText || '--' }}</ElDescriptionsItem>
+        <ElDescriptionsItem label="澶囨敞">{{ displayData.memo || '--' }}</ElDescriptionsItem>
+      </ElDescriptions>
+    </ElSkeleton>
+  </ElDrawer>
+</template>
+
+<script setup>
+  import { getUserStatusMeta, normalizeUserListRow } from '../userPage.helpers'
+
+  const props = defineProps({
+    visible: { required: false, default: false },
+    loading: { required: false, default: false },
+    userData: { required: false, default: () => ({}) }
+  })
+
+  const emit = defineEmits(['update:visible'])
+
+  const displayData = computed(() => normalizeUserListRow(props.userData))
+  const statusLabel = computed(() => getUserStatusMeta(displayData.value.statusBool ?? displayData.value.status).text)
+  const sexLabel = computed(() => {
+    switch (displayData.value.sex) {
+      case 1:
+        return '鐢�'
+      case 2:
+        return '濂�'
+      default:
+        return '鏈煡'
+    }
+  })
+
+  const handleVisibleChange = (visible) => {
+    emit('update:visible', visible)
+  }
+</script>
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 706c5c3..72725a3 100644
--- a/rsf-design/src/views/system/user/modules/user-dialog.vue
+++ b/rsf-design/src/views/system/user/modules/user-dialog.vue
@@ -1,106 +1,301 @@
 <template>
   <ElDialog
-    v-model="dialogVisible"
-    :title="dialogType === 'add' ? '娣诲姞鐢ㄦ埛' : '缂栬緫鐢ㄦ埛'"
-    width="30%"
+    :title="dialogTitle"
+    :model-value="visible"
+    @update:model-value="handleCancel"
+    width="960px"
     align-center
+    class="user-dialog"
+    @closed="handleClosed"
   >
-    <ElForm ref="formRef" :model="formData" :rules="rules" label-width="80px">
-      <ElFormItem label="鐢ㄦ埛鍚�" prop="username">
-        <ElInput v-model="formData.username" placeholder="璇疯緭鍏ョ敤鎴峰悕" />
-      </ElFormItem>
-      <ElFormItem label="鎵嬫満鍙�" prop="phone">
-        <ElInput v-model="formData.phone" placeholder="璇疯緭鍏ユ墜鏈哄彿" />
-      </ElFormItem>
-      <ElFormItem label="鎬у埆" prop="gender">
-        <ElSelect v-model="formData.gender">
-          <ElOption label="鐢�" value="鐢�" />
-          <ElOption label="濂�" value="濂�" />
-        </ElSelect>
-      </ElFormItem>
-      <ElFormItem label="瑙掕壊" prop="role">
-        <ElSelect v-model="formData.role" multiple>
-          <ElOption
-            v-for="role in roleList"
-            :key="role.roleCode"
-            :value="role.roleCode"
-            :label="role.roleName"
-          />
-        </ElSelect>
-      </ElFormItem>
-    </ElForm>
+    <ArtForm
+      ref="formRef"
+      v-model="form"
+      :items="formItems"
+      :rules="rules"
+      :span="12"
+      :gutter="20"
+      label-width="110px"
+      :show-reset="false"
+      :show-submit="false"
+    />
+
     <template #footer>
-      <div class="dialog-footer">
-        <ElButton @click="dialogVisible = false">鍙栨秷</ElButton>
-        <ElButton type="primary" @click="handleSubmit">鎻愪氦</ElButton>
-      </div>
+      <span class="dialog-footer">
+        <ElButton @click="handleCancel">鍙栨秷</ElButton>
+        <ElButton type="primary" @click="handleSubmit">纭畾</ElButton>
+      </span>
     </template>
   </ElDialog>
 </template>
 
 <script setup>
-  import { ROLE_LIST_DATA } from '@/mock/temp/formData'
+  import ArtForm from '@/components/core/forms/art-form/index.vue'
+  import { buildUserDialogModel, createUserFormState } from '../userPage.helpers'
+
   const props = defineProps({
-    visible: { required: true },
-    type: { required: true },
-    userData: { required: false }
+    visible: { required: false, default: false },
+    type: { required: false, default: 'add' },
+    userData: { required: false, default: () => ({}) },
+    roleOptions: { required: false, default: () => [] },
+    deptTreeOptions: { required: false, default: () => [] }
   })
+
   const emit = defineEmits(['update:visible', 'submit'])
-  const roleList = ref(ROLE_LIST_DATA)
-  const dialogVisible = computed({
-    get: () => props.visible,
-    set: (value) => emit('update:visible', value)
-  })
-  const dialogType = computed(() => props.type)
   const formRef = ref()
-  const formData = reactive({
-    username: '',
-    phone: '',
-    gender: '鐢�',
-    role: []
-  })
-  const rules = {
-    username: [
-      { required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' },
-      { min: 2, max: 20, message: '闀垮害鍦� 2 鍒� 20 涓瓧绗�', trigger: 'blur' }
-    ],
-    phone: [
-      { required: true, message: '璇疯緭鍏ユ墜鏈哄彿', trigger: 'blur' },
-      { pattern: /^1[3-9]\d{9}$/, message: '璇疯緭鍏ユ纭殑鎵嬫満鍙锋牸寮�', trigger: 'blur' }
-    ],
-    gender: [{ required: true, message: '璇烽�夋嫨鎬у埆', trigger: 'blur' }],
-    role: [{ required: true, message: '璇烽�夋嫨瑙掕壊', trigger: 'blur' }]
+  const form = reactive(createUserFormState())
+
+  const isEdit = computed(() => props.type === 'edit')
+  const dialogTitle = computed(() => (isEdit.value ? '缂栬緫鐢ㄦ埛' : '鏂板鐢ㄦ埛'))
+
+  const validatePassword = (_rule, value, callback) => {
+    if (!isEdit.value && !value) {
+      callback(new Error('璇疯緭鍏ュ瘑鐮�'))
+      return
+    }
+    if (value && String(value).length < 6) {
+      callback(new Error('瀵嗙爜闀垮害鑷冲皯 6 浣�'))
+      return
+    }
+    callback()
   }
-  const initFormData = () => {
-    const isEdit = props.type === 'edit' && props.userData
-    const row = props.userData
-    Object.assign(formData, {
-      username: isEdit && row ? row.userName || '' : '',
-      phone: isEdit && row ? row.userPhone || '' : '',
-      gender: isEdit && row ? row.userGender || '鐢�' : '鐢�',
-      role: isEdit && row ? (Array.isArray(row.userRoles) ? row.userRoles : []) : []
-    })
+
+  const validateConfirmPassword = (_rule, value, callback) => {
+    if (!form.password && !value && isEdit.value) {
+      callback()
+      return
+    }
+    if (!value) {
+      callback(new Error('璇峰啀娆¤緭鍏ュ瘑鐮�'))
+      return
+    }
+    if (value !== form.password) {
+      callback(new Error('涓ゆ杈撳叆鐨勫瘑鐮佷笉涓�鑷�'))
+      return
+    }
+    callback()
   }
+
+  const rules = computed(() => ({
+    username: [{ required: true, message: '璇疯緭鍏ョ敤鎴峰悕', trigger: 'blur' }],
+    nickname: [{ required: true, message: '璇疯緭鍏ユ樀绉�', trigger: 'blur' }],
+    roleIds: [{ type: 'array', required: true, message: '璇烽�夋嫨瑙掕壊', trigger: 'change' }],
+    password: [{ validator: validatePassword, trigger: 'blur' }],
+    confirmPassword: [{ validator: validateConfirmPassword, trigger: 'blur' }]
+  }))
+
+  const formItems = computed(() => [
+    {
+      label: '鐢ㄦ埛鍚�',
+      key: 'username',
+      type: 'input',
+      props: {
+        placeholder: '璇疯緭鍏ョ敤鎴峰悕',
+        clearable: true
+      }
+    },
+    {
+      label: '鏄电О',
+      key: 'nickname',
+      type: 'input',
+      props: {
+        placeholder: '璇疯緭鍏ユ樀绉�',
+        clearable: true
+      }
+    },
+    {
+      label: '閮ㄩ棬',
+      key: 'deptId',
+      type: 'treeselect',
+      props: {
+        data: props.deptTreeOptions,
+        props: {
+          label: 'label',
+          value: 'value',
+          children: 'children'
+        },
+        placeholder: '璇烽�夋嫨閮ㄩ棬',
+        clearable: true,
+        checkStrictly: true
+      }
+    },
+    {
+      label: '瑙掕壊',
+      key: 'roleIds',
+      type: 'select',
+      props: {
+        placeholder: '璇烽�夋嫨瑙掕壊',
+        clearable: true,
+        multiple: true,
+        collapseTags: true,
+        filterable: true,
+        options: props.roleOptions
+      }
+    },
+    {
+      label: '瀵嗙爜',
+      key: 'password',
+      type: 'input',
+      props: {
+        type: 'password',
+        showPassword: true,
+        placeholder: isEdit.value ? '涓嶄慨鏀瑰垯鐣欑┖' : '璇疯緭鍏ュ瘑鐮�',
+        clearable: true
+      }
+    },
+    {
+      label: '纭瀵嗙爜',
+      key: 'confirmPassword',
+      type: 'input',
+      props: {
+        type: 'password',
+        showPassword: true,
+        placeholder: '璇峰啀娆¤緭鍏ュ瘑鐮�',
+        clearable: true
+      }
+    },
+    {
+      label: '鎬у埆',
+      key: 'sex',
+      type: 'select',
+      props: {
+        placeholder: '璇烽�夋嫨鎬у埆',
+        clearable: true,
+        options: [
+          { label: '鏈煡', value: 0 },
+          { label: '鐢�', value: 1 },
+          { label: '濂�', value: 2 }
+        ]
+      }
+    },
+    {
+      label: '宸ュ彿',
+      key: 'code',
+      type: 'input',
+      props: {
+        placeholder: '璇疯緭鍏ュ伐鍙�',
+        clearable: true
+      }
+    },
+    {
+      label: '鎵嬫満鍙�',
+      key: 'phone',
+      type: 'input',
+      props: {
+        placeholder: '璇疯緭鍏ユ墜鏈哄彿',
+        clearable: true
+      }
+    },
+    {
+      label: '閭',
+      key: 'email',
+      type: 'input',
+      props: {
+        placeholder: '璇疯緭鍏ラ偖绠�',
+        clearable: true
+      }
+    },
+    {
+      label: '鐪熷疄濮撳悕',
+      key: 'realName',
+      type: 'input',
+      props: {
+        placeholder: '璇疯緭鍏ョ湡瀹炲鍚�',
+        clearable: true
+      }
+    },
+    {
+      label: '韬唤璇佸彿',
+      key: 'idCard',
+      type: 'input',
+      props: {
+        placeholder: '璇疯緭鍏ヨ韩浠借瘉鍙�',
+        clearable: true
+      }
+    },
+    {
+      label: '鐘舵��',
+      key: 'status',
+      type: 'select',
+      props: {
+        placeholder: '璇烽�夋嫨鐘舵��',
+        clearable: true,
+        options: [
+          { label: '姝e父', value: 1 },
+          { label: '绂佺敤', value: 0 }
+        ]
+      }
+    },
+    {
+      label: '澶囨敞',
+      key: 'memo',
+      type: 'input',
+      props: {
+        type: 'textarea',
+        rows: 3,
+        placeholder: '璇疯緭鍏ュ娉�',
+        clearable: true
+      },
+      span: 24
+    }
+  ])
+
+  const resetForm = () => {
+    Object.assign(form, createUserFormState())
+    formRef.value?.clearValidate?.()
+  }
+
+  const loadFormData = () => {
+    Object.assign(form, buildUserDialogModel(props.userData))
+  }
+
+  const handleSubmit = async () => {
+    if (!formRef.value) return
+    try {
+      await formRef.value.validate()
+      emit('submit', { ...form })
+    } catch {
+      return
+    }
+  }
+
+  const handleCancel = () => {
+    emit('update:visible', false)
+  }
+
+  const handleClosed = () => {
+    resetForm()
+  }
+
   watch(
-    () => [props.visible, props.type, props.userData],
-    ([visible]) => {
+    () => props.visible,
+    (visible) => {
       if (visible) {
-        initFormData()
+        loadFormData()
         nextTick(() => {
-          formRef.value?.clearValidate()
+          formRef.value?.clearValidate?.()
         })
       }
     },
     { immediate: true }
   )
-  const handleSubmit = async () => {
-    if (!formRef.value) return
-    await formRef.value.validate((valid) => {
-      if (valid) {
-        ElMessage.success(dialogType.value === 'add' ? '娣诲姞鎴愬姛' : '鏇存柊鎴愬姛')
-        dialogVisible.value = false
-        emit('submit')
+
+  watch(
+    () => props.userData,
+    () => {
+      if (props.visible) {
+        loadFormData()
       }
-    })
-  }
+    },
+    { deep: true }
+  )
+
+  watch(
+    () => props.type,
+    () => {
+      if (props.visible) {
+        loadFormData()
+      }
+    }
+  )
 </script>
diff --git a/rsf-design/tests/auth-contract.test.mjs b/rsf-design/tests/auth-contract.test.mjs
new file mode 100644
index 0000000..6dd0d4e
--- /dev/null
+++ b/rsf-design/tests/auth-contract.test.mjs
@@ -0,0 +1,123 @@
+import assert from 'node:assert/strict'
+import { register } from 'node:module'
+import test from 'node:test'
+
+register(
+  'data:text/javascript,export function resolve(specifier, context, nextResolve){ if(specifier===\'@/utils/http\'){ return { shortCircuit:true, url:\'data:text/javascript,export default { post(options){ globalThis.__authHttpCalls.push({ method:"post", options }); return Promise.resolve({ accessToken:"abc", refreshToken:"ref", user:{ id:1 } }) }, get(options){ globalThis.__authHttpCalls.push({ method:"get", options }); return Promise.resolve({ userId:1, roles:[{ code:"R_ADMIN", name:"绠$悊鍛�" }], authorities:[{ authority:"system:menu:save" },{ authority:"system:menu:update" }] }) } }\' } } return nextResolve(specifier, context) }',
+  import.meta.url
+)
+
+globalThis.__authHttpCalls = []
+
+const {
+  buildLoginPayload,
+  fetchGetUserInfo,
+  fetchLogin,
+  normalizeLoginResponse,
+  normalizeUserInfo
+} = await import('../src/api/auth.js')
+
+test('buildLoginPayload matches the rsf-server login contract', () => {
+  assert.deepEqual(buildLoginPayload({ username: 'demo', password: '123456' }), {
+    username: 'demo',
+    password: '123456'
+  })
+})
+
+test('normalizeLoginResponse extracts the real token fields', () => {
+  assert.deepEqual(
+    normalizeLoginResponse({
+      code: 200,
+      data: { accessToken: 'abc', user: { id: 1 } }
+    }),
+    { accessToken: 'abc', refreshToken: '', user: { id: 1 } }
+  )
+})
+
+test('normalizeLoginResponse also handles an inner data object', () => {
+  assert.deepEqual(
+    normalizeLoginResponse({
+      accessToken: 'abc',
+      refreshToken: 'ref',
+      user: { id: 1 }
+    }),
+    { accessToken: 'abc', refreshToken: 'ref', user: { id: 1 } }
+  )
+})
+
+test('normalizeLoginResponse accepts the old backend accessToken shape', () => {
+  const result = normalizeLoginResponse({
+    code: 200,
+    data: { accessToken: 'token-1', refreshToken: '', user: { username: 'admin' } }
+  })
+
+  assert.equal(result.accessToken, 'token-1')
+})
+
+test('normalizeLoginResponse keeps missing accessToken empty', () => {
+  const result = normalizeLoginResponse({
+    code: 200,
+    data: { refreshToken: 'ref', user: { username: 'admin' } }
+  })
+
+  assert.equal(result.accessToken, '')
+})
+
+test('normalizeUserInfo returns roles and buttons arrays safely', () => {
+  assert.deepEqual(normalizeUserInfo({ userId: 1, roles: ['R_ADMIN'], buttons: ['add'] }), {
+    userId: 1,
+    roles: ['R_ADMIN'],
+    buttons: ['add']
+  })
+})
+
+test('normalizeUserInfo derives role codes and button aliases from rsf-server auth payloads', () => {
+  assert.deepEqual(
+    normalizeUserInfo({
+      userId: 1,
+      roles: [{ code: 'R_SUPER', name: '瓒呯骇绠$悊鍛�' }],
+      authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }]
+    }),
+    {
+      userId: 1,
+      roles: ['R_SUPER'],
+      authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }],
+      buttons: ['system:menu:save', 'system:menu:update']
+    }
+  )
+})
+
+test('fetchLogin keeps supporting legacy userName input at the boundary', async () => {
+  const result = await fetchLogin({ userName: 'demo', password: '123456' })
+
+  assert.deepEqual(globalThis.__authHttpCalls.at(-1), {
+    method: 'post',
+    options: {
+      url: '/login',
+      params: { username: 'demo', password: '123456' }
+    }
+  })
+  assert.deepEqual(result, {
+    token: 'abc',
+    accessToken: 'abc',
+    refreshToken: 'ref',
+    user: { id: 1 }
+  })
+})
+
+test('fetchGetUserInfo normalizes the returned user payload', async () => {
+  const result = await fetchGetUserInfo()
+
+  assert.deepEqual(globalThis.__authHttpCalls.at(-1), {
+    method: 'get',
+    options: {
+      url: '/auth/user'
+    }
+  })
+  assert.deepEqual(result, {
+    userId: 1,
+    roles: ['R_ADMIN'],
+    authorities: [{ authority: 'system:menu:save' }, { authority: 'system:menu:update' }],
+    buttons: ['system:menu:save', 'system:menu:update']
+  })
+})
diff --git a/rsf-design/tests/list-export-print-contract.test.mjs b/rsf-design/tests/list-export-print-contract.test.mjs
new file mode 100644
index 0000000..d7a2f50
--- /dev/null
+++ b/rsf-design/tests/list-export-print-contract.test.mjs
@@ -0,0 +1,103 @@
+import assert from 'node:assert/strict'
+import test from 'node:test'
+
+import {
+  buildListExportPayload,
+  buildPrintPageQuery,
+  toExportColumns
+} from '../src/components/biz/list-export-print/list-export-print.helpers.js'
+
+test('selected rows keep export payload on ids only instead of flat query params', () => {
+  const payload = buildListExportPayload({
+    reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
+    selectedRows: [{ id: 7 }, { id: 9 }],
+    queryParams: { current: 1, pageSize: 20, name: '绠$悊鍛�', code: 'R_ADMIN' },
+    columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О' }]
+  })
+
+  assert.deepEqual(payload.ids, [7, 9])
+  assert.deepEqual(payload.columns, [{ source: 'name', label: '瑙掕壊鍚嶇О' }])
+  assert.equal(payload.meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
+  assert.deepEqual(
+    Object.keys(payload).filter((key) => ['current', 'pageSize', 'name', 'code', 'queryParams'].includes(key)),
+    []
+  )
+})
+
+test('export columns use ListExportService source/label contract only', () => {
+  assert.deepEqual(
+    toExportColumns([
+      { prop: 'name', label: '瑙掕壊鍚嶇О' },
+      { prop: 'operation', label: '鎿嶄綔' },
+      { prop: 'selection', label: '鍕鹃��' }
+    ]),
+    [{ source: 'name', label: '瑙掕壊鍚嶇О' }]
+  )
+})
+
+test('export payload keeps no-filter searches legal as flat params', () => {
+  const payload = buildListExportPayload({
+    reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
+    selectedRows: [],
+    queryParams: { current: 1, pageSize: 20 },
+    columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О' }]
+  })
+
+  assert.equal(payload.current, 1)
+  assert.equal(payload.pageSize, 20)
+  assert.equal(payload.meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
+  assert.deepEqual(payload.ids, [])
+  assert.equal(Object.prototype.hasOwnProperty.call(payload, 'queryParams'), false)
+})
+
+test('export payload preserves report style meta and column align without top-level reportStyle', () => {
+  const payload = buildListExportPayload({
+    reportTitle: '瑙掕壊绠$悊鎶ヨ〃',
+    meta: {
+      reportStyle: {
+        orientation: 'landscape',
+        density: 'comfortable'
+      }
+    },
+    columns: [{ prop: 'name', label: '瑙掕壊鍚嶇О', align: 'right' }]
+  })
+
+  assert.deepEqual(payload.meta.reportStyle, {
+    orientation: 'landscape',
+    density: 'comfortable'
+  })
+  assert.equal(payload.columns[0].align, 'right')
+  assert.equal(Object.prototype.hasOwnProperty.call(payload, 'reportStyle'), false)
+})
+
+test('print query expands to the full result set instead of the current page size', () => {
+  assert.deepEqual(
+    buildPrintPageQuery({
+      queryParams: { current: 3, pageSize: 20, orderBy: 'createTime desc', name: '绠$悊鍛�' },
+      total: 86,
+      maxResults: 1000
+    }),
+    {
+      current: 1,
+      pageSize: 86,
+      orderBy: 'createTime desc',
+      name: '绠$悊鍛�'
+    }
+  )
+})
+
+test('print query caps pageSize at maxResults when total is larger', () => {
+  assert.deepEqual(
+    buildPrintPageQuery({
+      queryParams: { current: 5, pageSize: 20, orderBy: 'createTime desc', code: 'R_ADMIN' },
+      total: 1500,
+      maxResults: 1000
+    }),
+    {
+      current: 1,
+      pageSize: 1000,
+      orderBy: 'createTime desc',
+      code: 'R_ADMIN'
+    }
+  )
+})
diff --git a/rsf-design/tests/system-role-print-export-page.test.mjs b/rsf-design/tests/system-role-print-export-page.test.mjs
new file mode 100644
index 0000000..3b557c6
--- /dev/null
+++ b/rsf-design/tests/system-role-print-export-page.test.mjs
@@ -0,0 +1,97 @@
+import assert from 'node:assert/strict'
+import { readFileSync } from 'node:fs'
+import test from 'node:test'
+
+import * as roleHelpers from '../src/views/system/role/rolePage.helpers.js'
+
+test('role report columns are the fixed business columns, not table utility columns', () => {
+  assert.deepEqual(
+    roleHelpers.getRoleReportColumns().map((column) => column.source),
+    ['name', 'code', 'statusText', 'memo', 'createTimeText', 'updateTimeText']
+  )
+})
+
+test('report columns keep visible order inside the role allowlist', () => {
+  assert.deepEqual(
+    roleHelpers.resolveRoleReportColumns([
+      { prop: 'selection', label: '鍕鹃��' },
+      { prop: 'status', label: '鐘舵��' },
+      { prop: 'name', label: '瑙掕壊鍚嶇О' },
+      { prop: 'deptName', label: '閮ㄩ棬鍚嶇О' },
+      { prop: 'operation', label: '鎿嶄綔' },
+      { prop: 'memo', label: '澶囨敞' }
+    ]),
+    [
+      { source: 'statusText', label: '鐘舵��' },
+      { source: 'name', label: '瑙掕壊鍚嶇О' },
+      { source: 'memo', label: '澶囨敞' }
+    ]
+  )
+})
+
+test('role print rows expose formatted status text', () => {
+  const rows = roleHelpers.buildRolePrintRows([
+    { name: '绠$悊鍛�', status: 1 },
+    { name: '璁垮', status: 0 }
+  ])
+
+  assert.equal(rows[0].statusText, '姝e父')
+  assert.equal(rows[1].statusText, '绂佺敤')
+})
+
+test('role report meta applies shared report style and title', () => {
+  const meta = roleHelpers.buildRoleReportMeta({
+    previewMeta: {
+      reportDate: '2026-03-29',
+      printedAt: '2026-03-29 10:57:25',
+      operator: 'ROOT'
+    },
+    count: 2,
+    titleAlign: 'left',
+    titleLevel: 'prominent'
+  })
+
+  assert.equal(meta.reportTitle, '瑙掕壊绠$悊鎶ヨ〃')
+  assert.equal(meta.reportDate, '2026-03-29')
+  assert.equal(meta.operator, 'ROOT')
+  assert.deepEqual(meta.reportStyle, {
+    titleAlign: 'left',
+    titleLevel: 'prominent',
+    orientation: 'portrait',
+    density: 'compact',
+    showSequence: true
+  })
+})
+
+test('role page uses helper report defaults as single source of truth', () => {
+  const indexSource = readFileSync(new URL('../src/views/system/role/index.vue', import.meta.url), 'utf8')
+  const meta = roleHelpers.buildRoleReportMeta()
+
+  assert.equal(roleHelpers.ROLE_REPORT_TITLE, '瑙掕壊绠$悊鎶ヨ〃')
+  assert.deepEqual(roleHelpers.ROLE_REPORT_STYLE, {
+    titleAlign: 'center',
+    titleLevel: 'strong'
+  })
+  assert.equal(meta.reportTitle, roleHelpers.ROLE_REPORT_TITLE)
+  assert.deepEqual(meta.reportStyle, {
+    ...roleHelpers.ROLE_REPORT_STYLE,
+    orientation: 'portrait',
+    density: 'compact',
+    showSequence: true
+  })
+  assert.match(
+    indexSource,
+    /import\s*\{[\s\S]*\bROLE_REPORT_STYLE\b,[\s\S]*\bROLE_REPORT_TITLE\b[\s\S]*\}\s*from '\.\/rolePage\.helpers'/
+  )
+  assert.match(
+    indexSource,
+    /<ListExportPrint[\s\S]*:report-title="reportTitle"[\s\S]*:preview-meta="resolvedPreviewMeta"[\s\S]*\/>/
+  )
+  assert.match(indexSource, /const reportTitle = ROLE_REPORT_TITLE/)
+  assert.doesNotMatch(indexSource, /const reportTitle = '瑙掕壊绠$悊鎶ヨ〃'/)
+  assert.doesNotMatch(indexSource, /const ROLE_REPORT_STYLE = \{/)
+  assert.match(
+    indexSource,
+    /const resolvedPreviewMeta = computed\(\(\) =>\s*buildRoleReportMeta\(\{\s*previewMeta: previewMeta\.value,\s*count: previewRows\.value\.length,\s*titleAlign: ROLE_REPORT_STYLE\.titleAlign,\s*titleLevel: ROLE_REPORT_STYLE\.titleLevel\s*\}\)\s*\)/
+  )
+})

--
Gitblit v1.9.1