zhou zhou
7 小时以前 e9283ffe6822b12ec5dd2ccf4dc13a369b227a61
rsf-design/src/views/system/user/index.vue
@@ -1,81 +1,104 @@
<!-- 用户管理页面 -->
<!-- art-full-height 自动计算出页面剩余高度 -->
<!-- art-table-card 一个符合系统样式的 class,同时自动撑满剩余高度 -->
<!-- 更多 useTable 使用示例请移步至 功能示例 下面的高级表格示例或者查看官方文档 -->
<!-- useTable 文档:https://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 元素上,用于解决表格内部图片预览样式异常
                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>