- 切换数据库为PostgreSQL,更新依赖与配置 - 重构权限体系,使用sys_user/sys_role/sys_menu等新实体替代旧模型 - 移除旧的用户角色/权限/系统设置相关代码与实体 - 新增字典、菜单管理模块与基础CRUD接口 - 优化鉴权逻辑,基于菜单权限标识进行接口校验 - 前端同步更新API、路由与权限控制逻辑 - 删除冗余的v-permission指令与相关代码
486 lines
13 KiB
Vue
486 lines
13 KiB
Vue
<template>
|
|
<div class="user-management">
|
|
<a-card title="用户管理">
|
|
<template #extra>
|
|
<a-space>
|
|
<a-button type="primary" @click="handleAdd">
|
|
<template #icon><PlusOutlined /></template>
|
|
新增用户
|
|
</a-button>
|
|
<a-button danger :disabled="!selectedRowKeys.length" @click="handleBatchDelete">
|
|
<template #icon><DeleteOutlined /></template>
|
|
批量删除 ({{ selectedRowKeys.length }})
|
|
</a-button>
|
|
</a-space>
|
|
</template>
|
|
|
|
<div class="search-bar">
|
|
<a-input
|
|
v-model:value="searchForm.keyword"
|
|
placeholder="用户名/姓名/手机号"
|
|
allow-clear
|
|
style="width: 220px"
|
|
@pressEnter="handleSearch"
|
|
>
|
|
<template #prefix><SearchOutlined /></template>
|
|
</a-input>
|
|
<a-select v-model:value="searchForm.status" placeholder="状态" allow-clear style="width: 120px">
|
|
<a-select-option :value="1">启用</a-select-option>
|
|
<a-select-option :value="0">禁用</a-select-option>
|
|
</a-select>
|
|
<a-button @click="handleReset">
|
|
<template #icon><ReloadOutlined /></template>
|
|
重置
|
|
</a-button>
|
|
</div>
|
|
|
|
<a-table
|
|
:columns="columns"
|
|
:data-source="dataSource"
|
|
:row-selection="rowSelection"
|
|
:loading="loading"
|
|
:pagination="pagination"
|
|
row-key="id"
|
|
@change="handleTableChange"
|
|
>
|
|
<template #bodyCell="{ column, record }">
|
|
<template v-if="column.key === 'roles'">
|
|
<a-space>
|
|
<a-tag v-for="role in record.userRoles" :key="role.roleId" :color="getRoleColor(role.role?.roleCode)">
|
|
{{ role.role?.roleName }}
|
|
</a-tag>
|
|
</a-space>
|
|
</template>
|
|
|
|
<template v-if="column.key === 'status'">
|
|
<a-switch
|
|
:checked="record.status === 1"
|
|
checked-children="启用"
|
|
un-checked-children="禁用"
|
|
@change="(checked: boolean) => handleStatusChange(record.id, checked ? 1 : 0)"
|
|
/>
|
|
</template>
|
|
|
|
<template v-if="column.key === 'created_at'">
|
|
{{ formatDateTime(record.created_at) }}
|
|
</template>
|
|
|
|
<template v-if="column.key === 'action'">
|
|
<a-space>
|
|
<a-button type="link" size="small" @click="handleEdit(record)">
|
|
<template #icon><EditOutlined /></template>
|
|
编辑
|
|
</a-button>
|
|
<a-popconfirm title="确定要删除该用户吗?" ok-text="确定" cancel-text="取消" @confirm="handleDelete(record.id)">
|
|
<a-button type="link" size="small" danger>
|
|
<template #icon><DeleteOutlined /></template>
|
|
删除
|
|
</a-button>
|
|
</a-popconfirm>
|
|
</a-space>
|
|
</template>
|
|
</template>
|
|
|
|
<template #emptyText>
|
|
<a-empty description="暂无用户数据" />
|
|
</template>
|
|
</a-table>
|
|
</a-card>
|
|
|
|
<a-modal
|
|
v-model:open="modalVisible"
|
|
:title="modalTitle"
|
|
:confirm-loading="modalLoading"
|
|
width="600px"
|
|
@ok="handleSubmit"
|
|
@cancel="handleCancel"
|
|
>
|
|
<a-form
|
|
ref="formRef"
|
|
:model="formData"
|
|
:rules="formRules"
|
|
:label-col="{ span: 6 }"
|
|
:wrapper-col="{ span: 16 }"
|
|
>
|
|
<a-form-item label="用户名" name="username">
|
|
<a-input v-model:value="formData.username" placeholder="请输入用户名" :disabled="isEdit" />
|
|
</a-form-item>
|
|
|
|
<a-form-item label="密码" name="password" v-if="!isEdit || showPassword">
|
|
<a-input-password v-model:value="formData.password" placeholder="请输入密码" />
|
|
</a-form-item>
|
|
|
|
<a-form-item label="确认密码" name="confirmPassword" v-if="!isEdit || showPassword">
|
|
<a-input-password v-model:value="formData.confirmPassword" placeholder="请再次输入密码" />
|
|
</a-form-item>
|
|
|
|
<a-form-item v-if="isEdit && !showPassword">
|
|
<a-button type="link" size="small" @click="showPassword = true">修改密码</a-button>
|
|
</a-form-item>
|
|
|
|
<a-form-item label="姓名" name="realName">
|
|
<a-input v-model:value="formData.realName" placeholder="请输入姓名" />
|
|
</a-form-item>
|
|
|
|
<a-form-item label="手机号" name="phone">
|
|
<a-input v-model:value="formData.phone" placeholder="请输入手机号" />
|
|
</a-form-item>
|
|
|
|
<a-form-item label="邮箱" name="email">
|
|
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
|
</a-form-item>
|
|
|
|
<a-form-item label="角色" name="roleIds">
|
|
<a-select
|
|
v-model:value="formData.roleIds"
|
|
mode="multiple"
|
|
placeholder="请选择角色"
|
|
:options="roleOptions"
|
|
/>
|
|
</a-form-item>
|
|
|
|
<a-form-item label="状态" name="status">
|
|
<a-radio-group v-model:value="formData.status">
|
|
<a-radio :value="1">启用</a-radio>
|
|
<a-radio :value="0">禁用</a-radio>
|
|
</a-radio-group>
|
|
</a-form-item>
|
|
</a-form>
|
|
</a-modal>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, onMounted, computed } from 'vue';
|
|
import { message, Modal } from 'ant-design-vue';
|
|
import {
|
|
SearchOutlined,
|
|
ReloadOutlined,
|
|
PlusOutlined,
|
|
DeleteOutlined,
|
|
EditOutlined,
|
|
} from '@ant-design/icons-vue';
|
|
import { userApi, type UserInfo, type UserQuery, type RoleInfo } from '@/api/user';
|
|
import dayjs from 'dayjs';
|
|
|
|
const loading = ref(false);
|
|
const dataSource = ref<UserInfo[]>([]);
|
|
const total = ref(0);
|
|
const searchForm = reactive<UserQuery>({
|
|
page: 1,
|
|
pageSize: 10,
|
|
keyword: '',
|
|
status: undefined as any,
|
|
});
|
|
const selectedRowKeys = ref<number[]>([]);
|
|
const modalVisible = ref(false);
|
|
const modalTitle = ref('');
|
|
const modalLoading = ref(false);
|
|
const formRef = ref();
|
|
const isEdit = ref(false);
|
|
const editId = ref<number | null>(null);
|
|
const showPassword = ref(false);
|
|
const roleOptions = ref<{ label: string; value: number }[]>([]);
|
|
|
|
const formData = reactive({
|
|
username: '',
|
|
password: '',
|
|
confirmPassword: '',
|
|
realName: '',
|
|
phone: '',
|
|
email: '',
|
|
roleIds: [] as number[],
|
|
status: 1,
|
|
});
|
|
|
|
const validateConfirmPassword = async (_rule: any, value: string) => {
|
|
if (value && value !== formData.password) {
|
|
return Promise.reject(new Error('两次输入的密码不一致'));
|
|
}
|
|
return Promise.resolve();
|
|
};
|
|
|
|
const formRules = {
|
|
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
|
password: [
|
|
{ required: true, message: '请输入密码', trigger: 'blur' },
|
|
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' },
|
|
],
|
|
confirmPassword: [{ required: true, message: '请确认密码', trigger: 'blur' }, { validator: validateConfirmPassword, trigger: 'blur' }],
|
|
realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
|
|
phone: [{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }],
|
|
email: [{ type: 'email' as const, message: '邮箱格式不正确', trigger: 'blur' }],
|
|
roleIds: [{ required: true, message: '请选择角色', trigger: 'change', type: 'array' as const }],
|
|
};
|
|
|
|
const columns = [
|
|
{ title: '用户名', dataIndex: 'username', key: 'username', ellipsis: true },
|
|
{ title: '姓名', dataIndex: 'realName', key: 'realName', ellipsis: true },
|
|
{ title: '手机号', dataIndex: 'phone', key: 'phone', ellipsis: true },
|
|
{ title: '邮箱', dataIndex: 'email', key: 'email', ellipsis: true },
|
|
{ title: '角色', key: 'roles' },
|
|
{
|
|
title: '状态',
|
|
key: 'status',
|
|
align: 'center' as const,
|
|
width: 80,
|
|
},
|
|
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', ellipsis: true },
|
|
{ title: '操作', key: 'action', width: 160, fixed: 'right' as const },
|
|
];
|
|
|
|
const pagination = computed(() => ({
|
|
current: searchForm.page,
|
|
pageSize: searchForm.pageSize,
|
|
total: total.value,
|
|
showSizeChanger: true,
|
|
showQuickJumper: true,
|
|
showTotal: (t: number) => `共 ${t} 条数据`,
|
|
}));
|
|
|
|
const rowSelection = computed(() => ({
|
|
selectedRowKeys: selectedRowKeys.value,
|
|
onChange: (keys: number[]) => {
|
|
selectedRowKeys.value = keys;
|
|
},
|
|
}));
|
|
|
|
onMounted(() => {
|
|
fetchUserList();
|
|
fetchRoleList();
|
|
});
|
|
|
|
async function fetchUserList() {
|
|
loading.value = true;
|
|
try {
|
|
const res: any = await userApi.getUserList(searchForm);
|
|
if (res.code === 200) {
|
|
dataSource.value = res.data.list;
|
|
total.value = res.data.total;
|
|
}
|
|
} catch (error) {
|
|
message.error('获取用户列表失败');
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
async function fetchRoleList() {
|
|
try {
|
|
const res: any = await userApi.getRoleList();
|
|
if (res.code === 200) {
|
|
roleOptions.value = res.data.map((item: RoleInfo) => ({
|
|
label: item.roleName,
|
|
value: item.id,
|
|
}));
|
|
}
|
|
} catch (error) {
|
|
console.error('获取角色列表失败:', error);
|
|
}
|
|
}
|
|
|
|
function handleSearch() {
|
|
searchForm.page = 1;
|
|
fetchUserList();
|
|
}
|
|
|
|
function handleReset() {
|
|
searchForm.keyword = '';
|
|
searchForm.status = undefined as any;
|
|
searchForm.page = 1;
|
|
fetchUserList();
|
|
}
|
|
|
|
function handleTableChange(pag: any) {
|
|
searchForm.page = pag.current;
|
|
searchForm.pageSize = pag.pageSize;
|
|
fetchUserList();
|
|
}
|
|
|
|
function handleAdd() {
|
|
isEdit.value = false;
|
|
editId.value = null;
|
|
showPassword.value = false;
|
|
modalTitle.value = '新增用户';
|
|
resetForm();
|
|
modalVisible.value = true;
|
|
}
|
|
|
|
function handleEdit(record: UserInfo) {
|
|
isEdit.value = true;
|
|
editId.value = record.id;
|
|
showPassword.value = false;
|
|
modalTitle.value = '编辑用户';
|
|
resetForm();
|
|
|
|
formData.username = record.username;
|
|
formData.realName = record.realName;
|
|
formData.phone = record.phone || '';
|
|
formData.email = record.email || '';
|
|
formData.status = record.status;
|
|
formData.roleIds = record.userRoles?.map((ur) => ur.roleId) || [];
|
|
|
|
modalVisible.value = true;
|
|
}
|
|
|
|
function resetForm() {
|
|
formData.username = '';
|
|
formData.password = '';
|
|
formData.confirmPassword = '';
|
|
formData.realName = '';
|
|
formData.phone = '';
|
|
formData.email = '';
|
|
formData.roleIds = [];
|
|
formData.status = 1;
|
|
|
|
if (formRef.value) {
|
|
formRef.value.resetFields();
|
|
}
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
try {
|
|
await formRef.value.validate();
|
|
} catch {
|
|
return;
|
|
}
|
|
|
|
modalLoading.value = true;
|
|
try {
|
|
let res: any;
|
|
if (isEdit.value && editId.value) {
|
|
const updateData: any = {
|
|
realName: formData.realName,
|
|
phone: formData.phone,
|
|
email: formData.email,
|
|
roleIds: formData.roleIds,
|
|
status: formData.status,
|
|
};
|
|
|
|
if (showPassword.value && formData.password) {
|
|
updateData.password = formData.password;
|
|
}
|
|
|
|
res = await userApi.updateUser(editId.value, updateData);
|
|
} else {
|
|
res = await userApi.createUser({
|
|
username: formData.username,
|
|
password: formData.password,
|
|
realName: formData.realName,
|
|
phone: formData.phone,
|
|
email: formData.email,
|
|
roleIds: formData.roleIds,
|
|
status: formData.status,
|
|
});
|
|
}
|
|
|
|
if (res.code === 200) {
|
|
message.success(isEdit.value ? '更新成功' : '创建成功');
|
|
modalVisible.value = false;
|
|
fetchUserList();
|
|
} else {
|
|
message.error(res.message || '操作失败');
|
|
}
|
|
} catch (error) {
|
|
message.error('操作失败');
|
|
} finally {
|
|
modalLoading.value = false;
|
|
}
|
|
}
|
|
|
|
function handleCancel() {
|
|
modalVisible.value = false;
|
|
resetForm();
|
|
}
|
|
|
|
async function handleDelete(id: number) {
|
|
try {
|
|
const res: any = await userApi.deleteUser(id);
|
|
if (res.code === 200) {
|
|
message.success('删除成功');
|
|
fetchUserList();
|
|
} else {
|
|
message.error(res.message || '删除失败');
|
|
}
|
|
} catch (error) {
|
|
message.error('删除失败');
|
|
}
|
|
}
|
|
|
|
function handleBatchDelete() {
|
|
if (!selectedRowKeys.value.length) return;
|
|
|
|
Modal.confirm({
|
|
title: '批量删除确认',
|
|
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个用户吗?此操作不可恢复!`,
|
|
okText: '确定',
|
|
cancelText: '取消',
|
|
okType: 'danger',
|
|
onOk: async () => {
|
|
try {
|
|
const res: any = await userApi.batchDeleteUsers(selectedRowKeys.value);
|
|
if (res.code === 200) {
|
|
message.success(res.message);
|
|
selectedRowKeys.value = [];
|
|
fetchUserList();
|
|
} else {
|
|
message.error(res.message || '批量删除失败');
|
|
}
|
|
} catch (error) {
|
|
message.error('批量删除失败');
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
async function handleStatusChange(id: number, status: number) {
|
|
try {
|
|
const res: any = await userApi.updateStatus(id, status);
|
|
if (res.code === 200) {
|
|
message.success(status === 1 ? '已启用' : '已禁用');
|
|
fetchUserList();
|
|
} else {
|
|
message.error(res.message || '状态更新失败');
|
|
}
|
|
} catch (error) {
|
|
message.error('状态更新失败');
|
|
}
|
|
}
|
|
|
|
function getRoleColor(code?: string): string {
|
|
const colorMap: Record<string, string> = {
|
|
admin: 'red',
|
|
manager: 'orange',
|
|
operator: 'blue',
|
|
user: 'green',
|
|
};
|
|
return colorMap[code || ''] || 'default';
|
|
}
|
|
|
|
function formatDateTime(dateStr: string) {
|
|
if (!dateStr) return '-';
|
|
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.user-management {
|
|
padding: 20px;
|
|
}
|
|
|
|
.search-bar {
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
}
|
|
|
|
:deep(.ant-table) {
|
|
font-size: 14px;
|
|
}
|
|
|
|
:deep(.ant-btn-link) {
|
|
padding: 0 4px;
|
|
}
|
|
</style>
|