| | |
| | | <!-- 用户管理页面 --> |
| | | <!-- 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, |
| | |
| | | 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> |