qiangdanxitong/frontend/src/views/UserManagement.vue

625 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>