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/views/system/user/index.vue |  444 ++++++++++++++++++++++++++++++++++++++-----------------
 1 files changed, 307 insertions(+), 137 deletions(-)

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>

--
Gitblit v1.9.1