625 lines
16 KiB
Vue
625 lines
16 KiB
Vue
<template>
|
||
<DefaultLayout>
|
||
<div class="user-management-page">
|
||
<div class="container">
|
||
<div class="settings-section">
|
||
<div class="section-header">
|
||
<div>
|
||
<h2 class="section-title">用户管理</h2>
|
||
<p class="section-subtitle">管理系统用户,分配角色和部门</p>
|
||
</div>
|
||
<a-button type="primary" @click="showUserModal()">
|
||
<template #icon><Icon name="plus" size="sm" /></template>
|
||
添加用户
|
||
</a-button>
|
||
</div>
|
||
|
||
<!-- 搜索筛选 -->
|
||
<div class="search-bar">
|
||
<a-input-search
|
||
v-model:value="searchKeyword"
|
||
placeholder="搜索用户名、姓名、手机号、邮箱"
|
||
style="width: 300px"
|
||
@search="loadUsers"
|
||
>
|
||
<template #prefix>
|
||
<Icon name="search" size="sm" />
|
||
</template>
|
||
</a-input-search>
|
||
<a-select
|
||
v-model:value="searchDeptId"
|
||
placeholder="选择部门"
|
||
style="width: 200px"
|
||
allow-clear
|
||
@change="loadUsers"
|
||
>
|
||
<a-select-option
|
||
v-for="dept in departments"
|
||
:key="dept.id"
|
||
:value="dept.id"
|
||
>
|
||
{{ dept.deptName }}
|
||
</a-select-option>
|
||
</a-select>
|
||
<a-select
|
||
v-model:value="searchStatus"
|
||
placeholder="选择状态"
|
||
style="width: 120px"
|
||
allow-clear
|
||
@change="loadUsers"
|
||
>
|
||
<a-select-option :value="1">启用</a-select-option>
|
||
<a-select-option :value="0">禁用</a-select-option>
|
||
</a-select>
|
||
</div>
|
||
|
||
<!-- 用户表格 -->
|
||
<a-table
|
||
:columns="columns"
|
||
:data-source="users"
|
||
:loading="loading"
|
||
:pagination="pagination"
|
||
row-key="id"
|
||
@change="handleTableChange"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'userInfo'">
|
||
<div class="user-info-cell">
|
||
<div class="user-info-main">
|
||
<span class="user-name-text">{{ record.realName || record.username }}</span>
|
||
<span v-if="record.realName" class="user-username">@{{ record.username }}</span>
|
||
</div>
|
||
<div class="user-info-sub">
|
||
<span v-if="record.phone">
|
||
<Icon name="phone" size="xs" style="margin-right: 4px;" />
|
||
{{ record.phone }}
|
||
</span>
|
||
<span v-if="record.email" class="user-email">
|
||
<Icon name="mail" size="xs" style="margin-right: 4px;" />
|
||
{{ record.email }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'department'">
|
||
<a-tag v-if="record.deptName" color="blue" size="small">
|
||
{{ record.deptName }}
|
||
</a-tag>
|
||
<span v-else class="empty-value">未分配</span>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'status'">
|
||
<div class="status-switch">
|
||
<a-switch
|
||
:checked="record.status === 1"
|
||
checked-children="启用"
|
||
un-checked-children="禁用"
|
||
@change="(checked: boolean) => toggleStatus(record.id, checked)"
|
||
/>
|
||
</div>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'createTime'">
|
||
<span class="time-value">{{ formatTime(record.createTime) }}</span>
|
||
</template>
|
||
|
||
<template v-else-if="column.key === 'actions'">
|
||
<div class="table-actions">
|
||
<a-button type="text" size="small" @click="showUserModal(record)">
|
||
<Icon name="edit" size="sm" style="margin-right: 4px;" />
|
||
编辑
|
||
</a-button>
|
||
<a-button type="text" size="small" @click="showRoleModal(record)">
|
||
<Icon name="shield" size="sm" style="margin-right: 4px;" />
|
||
角色
|
||
</a-button>
|
||
<a-popconfirm title="确定重置密码为123456?" @confirm="resetPwd(record.id)">
|
||
<a-button type="text" size="small">
|
||
<Icon name="key" size="sm" style="margin-right: 4px;" />
|
||
重置密码
|
||
</a-button>
|
||
</a-popconfirm>
|
||
<a-popconfirm title="确定删除此用户?" @confirm="deleteUserItem(record.id)">
|
||
<a-button type="text" size="small" danger>
|
||
<Icon name="trash" size="sm" style="margin-right: 4px;" />
|
||
删除
|
||
</a-button>
|
||
</a-popconfirm>
|
||
</div>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
|
||
<!-- 用户编辑弹窗 -->
|
||
<a-modal
|
||
v-model:open="userModalVisible"
|
||
:title="editingUser ? '编辑用户' : '添加用户'"
|
||
width="600px"
|
||
@ok="saveUser"
|
||
>
|
||
<a-form :model="userForm" layout="vertical">
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="用户名" required>
|
||
<a-input v-model:value="userForm.username" placeholder="请输入用户名" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="真实姓名">
|
||
<a-input v-model:value="userForm.realName" placeholder="请输入真实姓名" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-row :gutter="16">
|
||
<a-col :span="12">
|
||
<a-form-item label="手机号">
|
||
<a-input v-model:value="userForm.phone" placeholder="请输入手机号" />
|
||
</a-form-item>
|
||
</a-col>
|
||
<a-col :span="12">
|
||
<a-form-item label="邮箱">
|
||
<a-input v-model:value="userForm.email" placeholder="请输入邮箱" />
|
||
</a-form-item>
|
||
</a-col>
|
||
</a-row>
|
||
<a-form-item label="所属部门">
|
||
<a-select v-model:value="userForm.deptId" placeholder="请选择部门">
|
||
<a-select-option
|
||
v-for="dept in departments"
|
||
:key="dept.id"
|
||
:value="dept.id"
|
||
>
|
||
{{ dept.deptName }}
|
||
</a-select-option>
|
||
</a-select>
|
||
</a-form-item>
|
||
<a-form-item v-if="!editingUser" label="初始密码">
|
||
<a-input-password
|
||
v-model:value="userForm.password"
|
||
placeholder="默认为123456"
|
||
/>
|
||
</a-form-item>
|
||
</a-form>
|
||
</a-modal>
|
||
|
||
<!-- 角色分配弹窗 -->
|
||
<a-modal
|
||
v-model:open="roleModalVisible"
|
||
title="分配角色"
|
||
@ok="saveRoles"
|
||
>
|
||
<a-checkbox-group v-model:value="selectedRoleIds" style="width: 100%">
|
||
<a-row>
|
||
<a-col v-for="role in roles" :key="role.id" :span="24" style="margin-bottom: 8px">
|
||
<a-checkbox :value="role.id">
|
||
{{ role.roleName }}
|
||
<span class="text-tertiary role-description">{{ role.description }}</span>
|
||
</a-checkbox>
|
||
</a-col>
|
||
</a-row>
|
||
</a-checkbox-group>
|
||
</a-modal>
|
||
</div>
|
||
</div>
|
||
</DefaultLayout>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, reactive, onMounted } from 'vue'
|
||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||
import { message } from 'ant-design-vue'
|
||
import Icon from '@/components/Icon.vue'
|
||
import {
|
||
getUserList,
|
||
createUser,
|
||
updateUser,
|
||
deleteUser,
|
||
resetPassword,
|
||
toggleUserStatus,
|
||
assignUserRoles,
|
||
getUserRoleIds,
|
||
type User,
|
||
type PageResult
|
||
} from '@/api/user'
|
||
import { getAllRoles, type Role } from '@/api/role'
|
||
import axios from 'axios'
|
||
|
||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8080'
|
||
|
||
interface Department {
|
||
id: number
|
||
deptName: string
|
||
}
|
||
|
||
const loading = ref(false)
|
||
const users = ref<User[]>([])
|
||
const roles = ref<Role[]>([])
|
||
const departments = ref<Department[]>([])
|
||
|
||
const searchKeyword = ref('')
|
||
const searchDeptId = ref<number>()
|
||
const searchStatus = ref<number>()
|
||
|
||
const pagination = reactive({
|
||
current: 1,
|
||
pageSize: 10,
|
||
total: 0
|
||
})
|
||
|
||
const userModalVisible = ref(false)
|
||
const roleModalVisible = ref(false)
|
||
const editingUser = ref<User | null>(null)
|
||
const currentUserId = ref<number>()
|
||
|
||
const userForm = reactive<Partial<User>>({
|
||
username: '',
|
||
realName: '',
|
||
phone: '',
|
||
email: '',
|
||
deptId: undefined,
|
||
password: '',
|
||
status: 1
|
||
})
|
||
|
||
const selectedRoleIds = ref<number[]>([])
|
||
|
||
// 表格列定义
|
||
const columns = [
|
||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||
{ title: '用户信息', key: 'userInfo', width: 280 },
|
||
{ title: '所属部门', key: 'department', width: 140 },
|
||
{ title: '状态', key: 'status', width: 120 },
|
||
{ title: '创建时间', key: 'createTime', width: 160 },
|
||
{ title: '操作', key: 'actions', width: 360, fixed: 'right' }
|
||
]
|
||
|
||
// 格式化时间
|
||
const formatTime = (time: string | undefined) => {
|
||
if (!time) return '--'
|
||
return time.replace('T', ' ').substring(0, 16)
|
||
}
|
||
|
||
// 加载用户列表
|
||
const loadUsers = async () => {
|
||
loading.value = true
|
||
try {
|
||
const result: PageResult<User> = await getUserList(
|
||
pagination.current,
|
||
pagination.pageSize,
|
||
searchKeyword.value,
|
||
searchDeptId.value,
|
||
searchStatus.value
|
||
)
|
||
users.value = result.records
|
||
pagination.total = result.total
|
||
} catch (error) {
|
||
message.error('加载用户列表失败')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
// 加载角色列表
|
||
const loadRoles = async () => {
|
||
try {
|
||
roles.value = await getAllRoles()
|
||
} catch (error) {
|
||
message.error('加载角色列表失败')
|
||
}
|
||
}
|
||
|
||
// 加载部门列表
|
||
const loadDepartments = async () => {
|
||
try {
|
||
const response = await axios.get(`${API_BASE_URL}/api/departments`)
|
||
departments.value = response.data.data
|
||
} catch (error) {
|
||
message.error('加载部门列表失败')
|
||
}
|
||
}
|
||
|
||
// 表格分页变化
|
||
const handleTableChange = (pag: any) => {
|
||
pagination.current = pag.current
|
||
pagination.pageSize = pag.pageSize
|
||
loadUsers()
|
||
}
|
||
|
||
// 显示用户编辑弹窗
|
||
const showUserModal = (user?: User) => {
|
||
editingUser.value = user || null
|
||
if (user) {
|
||
Object.assign(userForm, user)
|
||
} else {
|
||
Object.keys(userForm).forEach(key => {
|
||
userForm[key as keyof typeof userForm] = key === 'status' ? 1 : undefined
|
||
})
|
||
}
|
||
userModalVisible.value = true
|
||
}
|
||
|
||
// 保存用户
|
||
const saveUser = async () => {
|
||
try {
|
||
if (editingUser.value) {
|
||
await updateUser(editingUser.value.id!, userForm)
|
||
message.success('更新成功')
|
||
} else {
|
||
await createUser(userForm as User)
|
||
message.success('创建成功')
|
||
}
|
||
userModalVisible.value = false
|
||
await loadUsers()
|
||
} catch (error) {
|
||
message.error('保存失败')
|
||
}
|
||
}
|
||
|
||
// 显示角色分配弹窗
|
||
const showRoleModal = async (user: User) => {
|
||
currentUserId.value = user.id
|
||
try {
|
||
selectedRoleIds.value = await getUserRoleIds(user.id!)
|
||
roleModalVisible.value = true
|
||
} catch (error) {
|
||
message.error('加载用户角色失败')
|
||
}
|
||
}
|
||
|
||
// 保存角色分配
|
||
const saveRoles = async () => {
|
||
if (!currentUserId.value) return
|
||
try {
|
||
await assignUserRoles(currentUserId.value, selectedRoleIds.value)
|
||
message.success('角色分配成功')
|
||
roleModalVisible.value = false
|
||
} catch (error) {
|
||
message.error('角色分配失败')
|
||
}
|
||
}
|
||
|
||
// 切换状态
|
||
const toggleStatus = async (id: number, checked: boolean) => {
|
||
try {
|
||
await toggleUserStatus(id, checked ? 1 : 0)
|
||
message.success('状态更新成功')
|
||
await loadUsers()
|
||
} catch (error) {
|
||
message.error('状态更新失败')
|
||
}
|
||
}
|
||
|
||
// 重置密码
|
||
const resetPwd = async (id: number) => {
|
||
try {
|
||
await resetPassword(id)
|
||
message.success('密码已重置为123456')
|
||
} catch (error) {
|
||
message.error('重置密码失败')
|
||
}
|
||
}
|
||
|
||
// 删除用户
|
||
const deleteUserItem = async (id: number) => {
|
||
try {
|
||
await deleteUser(id)
|
||
message.success('删除成功')
|
||
await loadUsers()
|
||
} catch (error) {
|
||
message.error('删除失败')
|
||
}
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadUsers()
|
||
loadRoles()
|
||
loadDepartments()
|
||
})
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
@import '@/styles/variables.scss';
|
||
|
||
.user-management-page {
|
||
min-height: 100vh;
|
||
padding: $space-xl 0;
|
||
background: $bg-primary;
|
||
}
|
||
|
||
.container {
|
||
max-width: $container-max-width;
|
||
margin: 0 auto;
|
||
padding: 0 $space-xl;
|
||
|
||
@media (max-width: $breakpoint-tablet) {
|
||
padding: 0 $space-lg;
|
||
}
|
||
|
||
@media (max-width: $breakpoint-mobile) {
|
||
padding: 0 $space-md;
|
||
}
|
||
}
|
||
|
||
.settings-section {
|
||
background: $bg-card;
|
||
border: 1px solid $border-primary;
|
||
border-radius: $radius-lg;
|
||
padding: $space-lg;
|
||
transition: all $transition-base $ease-out;
|
||
|
||
.section-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: $space-lg;
|
||
|
||
.section-title {
|
||
font-size: $font-size-xl;
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
margin: 0 0 4px 0;
|
||
}
|
||
|
||
.section-subtitle {
|
||
font-size: $font-size-sm;
|
||
color: $text-tertiary;
|
||
margin: 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
.search-bar {
|
||
display: flex;
|
||
gap: $space-md;
|
||
margin-bottom: $space-lg;
|
||
padding: $space-lg;
|
||
background: $bg-tertiary;
|
||
border: 1px solid $border-color;
|
||
border-radius: $radius-md;
|
||
}
|
||
|
||
// 用户信息单元格
|
||
.user-info-cell {
|
||
.user-info-main {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 4px;
|
||
|
||
.user-name-text {
|
||
font-weight: $font-weight-semibold;
|
||
color: $text-primary;
|
||
font-size: $font-size-base;
|
||
}
|
||
|
||
.user-username {
|
||
font-size: $font-size-sm;
|
||
color: $text-tertiary;
|
||
font-family: monospace;
|
||
}
|
||
}
|
||
|
||
.user-info-sub {
|
||
display: flex;
|
||
gap: 12px;
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
|
||
.user-email {
|
||
color: $text-tertiary;
|
||
}
|
||
|
||
span {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 状态开关
|
||
.status-switch {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
// 时间值
|
||
.time-value {
|
||
font-size: $font-size-sm;
|
||
color: $text-secondary;
|
||
font-family: monospace;
|
||
}
|
||
|
||
// 空值
|
||
.empty-value {
|
||
color: $text-tertiary;
|
||
font-size: $font-size-sm;
|
||
}
|
||
|
||
// 操作按钮
|
||
.table-actions {
|
||
display: flex;
|
||
gap: 4px;
|
||
flex-wrap: wrap;
|
||
|
||
:deep(.ant-btn-text) {
|
||
color: $text-secondary;
|
||
padding: 4px 8px;
|
||
height: auto;
|
||
font-size: $font-size-sm;
|
||
transition: all 0.2s ease;
|
||
|
||
&:hover {
|
||
color: $brand-primary;
|
||
background: rgba($brand-primary, 0.08);
|
||
}
|
||
|
||
&.ant-btn-dangerous {
|
||
&:hover {
|
||
color: $color-error;
|
||
background: rgba($color-error, 0.08);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.role-description {
|
||
font-size: $font-size-sm;
|
||
}
|
||
|
||
// 全局表格样式
|
||
:deep(.ant-table) {
|
||
background: transparent;
|
||
|
||
.ant-table-thead > tr > th {
|
||
background: $bg-tertiary;
|
||
color: $text-secondary;
|
||
font-weight: $font-weight-semibold;
|
||
border-bottom: 1px solid $border-color;
|
||
padding: 12px 16px;
|
||
|
||
&::before {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
.ant-table-tbody > tr {
|
||
&:hover > td {
|
||
background: rgba($brand-primary, 0.03);
|
||
}
|
||
|
||
> td {
|
||
border-bottom: 1px solid $border-color;
|
||
padding: 16px;
|
||
color: $text-primary;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Switch 样式
|
||
:deep(.ant-switch) {
|
||
&.ant-switch-checked {
|
||
background: $brand-primary !important;
|
||
|
||
.ant-switch-inner {
|
||
color: #000000 !important;
|
||
}
|
||
|
||
.ant-switch-handle::before {
|
||
background: #000000 !important;
|
||
}
|
||
}
|
||
|
||
&:not(.ant-switch-checked) {
|
||
background: $bg-tertiary;
|
||
|
||
.ant-switch-inner {
|
||
color: $text-tertiary !important;
|
||
}
|
||
}
|
||
}
|
||
</style>
|
||
|