feat: 初始化智能物联网设备管理系统前后端基础项目
完成项目整体结构搭建,包含: 1. 前端Vue3+TS+Vite+Antd项目基础配置与核心依赖 2. 后端MidwayJS+TypeORM+JWT权限认证框架 3. 完整的用户、角色、权限、日志数据库表结构 4. 核心业务模块API与基础服务实现 5. 全局样式、路由、状态管理配置
This commit is contained in:
commit
3d7eb12103
89 files changed
+16085
No files matched your search
@@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<div class="role-management">
|
||||
<a-card title="角色管理">
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增角色
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div class="search-bar">
|
||||
<a-input-search
|
||||
v-model:value="keyword"
|
||||
placeholder="搜索角色名称/编码"
|
||||
allow-clear
|
||||
style="width: 300px"
|
||||
@search="handleSearch"
|
||||
@pressEnter="handleSearch"
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input-search>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="roleList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'name'">
|
||||
<span>{{ record.name }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'code'">
|
||||
<a-tag color="blue">{{ record.code }}</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<a-badge :status="record.status === 1 ? 'success' : 'error'" :text="record.status === 1 ? '启用' : '禁用'" />
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'userCount'">
|
||||
<a @click="handleViewUsers(record)">{{ record.userCount }}</a>
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'created_at'">
|
||||
{{ formatDateTime(record.created_at) }}
|
||||
</template>
|
||||
|
||||
<template v-if="column.dataIndex === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handlePermission(record)">
|
||||
<KeyOutlined />权限配置
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
<EditOutlined />编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定删除该角色?"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
@confirm="handleDelete(record)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
<DeleteOutlined />删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<a-modal
|
||||
v-model:open="formModalVisible"
|
||||
:title="formModalTitle"
|
||||
:confirm-loading="formLoading"
|
||||
@ok="handleSubmitForm"
|
||||
@cancel="resetForm"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 5 }"
|
||||
:wrapper-col="{ span: 17 }"
|
||||
>
|
||||
<a-form-item label="角色名称" name="name">
|
||||
<a-input v-model:value="formData.name" placeholder="请输入角色名称" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色编码" name="code">
|
||||
<a-input
|
||||
v-model:value="formData.code"
|
||||
placeholder="请输入角色编码"
|
||||
:disabled="isEdit"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="描述" name="description">
|
||||
<a-textarea v-model:value="formData.description" placeholder="请输入角色描述" :rows="3" />
|
||||
</a-form-item>
|
||||
<a-form-item label="排序" name="sortOrder">
|
||||
<a-input-number v-model:value="formData.sortOrder" :min="0" :max="999" style="width: 100%" />
|
||||
</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>
|
||||
|
||||
<a-modal
|
||||
v-model:open="permissionModalVisible"
|
||||
title="权限配置"
|
||||
width="600px"
|
||||
:confirm-loading="permissionLoading"
|
||||
@ok="handleSavePermissions"
|
||||
>
|
||||
<a-spin :spinning="permissionTreeLoading">
|
||||
<a-tree
|
||||
v-if="!permissionTreeLoading"
|
||||
v-model:checkedKeys="checkedKeys"
|
||||
:tree-data="permissionTree"
|
||||
checkable
|
||||
:field-names="{ title: 'name', key: 'id', children: 'children' }"
|
||||
:check-strictly="false"
|
||||
:default-expand-all="true"
|
||||
style="max-height: 500px; overflow-y: auto;"
|
||||
>
|
||||
<template #title="{ name, icon }">
|
||||
<span>
|
||||
<component v-if="icon" :is="icon" style="margin-right: 8px" />
|
||||
{{ name }}
|
||||
</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
PlusOutlined,
|
||||
SearchOutlined,
|
||||
KeyOutlined,
|
||||
EditOutlined,
|
||||
DeleteOutlined,
|
||||
} from '@ant-design/icons-vue';
|
||||
import { roleApi, type RoleInfo, type PermissionTreeNode } from '@/api/role';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const loading = ref(false);
|
||||
const roleList = ref<RoleInfo[]>([]);
|
||||
const keyword = ref('');
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total: number) => `共 ${total} 条`,
|
||||
});
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
},
|
||||
{
|
||||
title: '角色编码',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '关联用户数',
|
||||
dataIndex: 'userCount',
|
||||
key: 'userCount',
|
||||
width: 120,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 280,
|
||||
fixed: 'right' as const,
|
||||
},
|
||||
];
|
||||
|
||||
async function fetchRoleList() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res: any = await roleApi.getRoleList({
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
keyword: keyword.value || undefined,
|
||||
});
|
||||
if (res.code === 200) {
|
||||
roleList.value = res.data.list;
|
||||
pagination.total = res.data.total;
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取角色列表失败');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
pagination.current = 1;
|
||||
fetchRoleList();
|
||||
}
|
||||
|
||||
function handleTableChange(pag: any) {
|
||||
pagination.current = pag.current;
|
||||
pagination.pageSize = pag.pageSize;
|
||||
fetchRoleList();
|
||||
}
|
||||
|
||||
function formatDateTime(dateStr: string) {
|
||||
if (!dateStr) return '-';
|
||||
return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
const formModalVisible = ref(false);
|
||||
const formLoading = ref(false);
|
||||
const formRef = ref();
|
||||
const isEdit = ref(false);
|
||||
const currentRoleId = ref<number | null>(null);
|
||||
|
||||
const formModalTitle = computed(() => (isEdit.value ? '编辑角色' : '新增角色'));
|
||||
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
description: '',
|
||||
sortOrder: 0,
|
||||
status: 1,
|
||||
});
|
||||
|
||||
const formRules = {
|
||||
name: [{ required: true, message: '请输入角色名称', trigger: 'blur' }],
|
||||
code: [{ required: true, message: '请输入角色编码', trigger: 'blur' }],
|
||||
};
|
||||
|
||||
function handleAdd() {
|
||||
isEdit.value = false;
|
||||
currentRoleId.value = null;
|
||||
resetForm();
|
||||
formModalVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEdit(record: RoleInfo) {
|
||||
isEdit.value = true;
|
||||
currentRoleId.value = record.id;
|
||||
formData.name = record.name;
|
||||
formData.code = record.code;
|
||||
formData.description = record.description || '';
|
||||
formData.sortOrder = record.sortOrder;
|
||||
formData.status = record.status;
|
||||
formModalVisible.value = true;
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
formData.name = '';
|
||||
formData.code = '';
|
||||
formData.description = '';
|
||||
formData.sortOrder = 0;
|
||||
formData.status = 1;
|
||||
formRef.value?.resetFields();
|
||||
}
|
||||
|
||||
async function handleSubmitForm() {
|
||||
try {
|
||||
await formRef.value?.validate();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
formLoading.value = true;
|
||||
try {
|
||||
let res: any;
|
||||
if (isEdit.value && currentRoleId.value) {
|
||||
res = await roleApi.updateRole(currentRoleId.value, {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
sortOrder: formData.sortOrder,
|
||||
status: formData.status,
|
||||
});
|
||||
} else {
|
||||
res = await roleApi.createRole({
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
description: formData.description,
|
||||
sortOrder: formData.sortOrder,
|
||||
status: formData.status,
|
||||
});
|
||||
}
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功');
|
||||
formModalVisible.value = false;
|
||||
fetchRoleList();
|
||||
} else {
|
||||
message.error(res.message || '操作失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
} finally {
|
||||
formLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDelete(record: RoleInfo) {
|
||||
try {
|
||||
const res: any = await roleApi.deleteRole(record.id);
|
||||
if (res.code === 200) {
|
||||
message.success('删除成功');
|
||||
fetchRoleList();
|
||||
} else {
|
||||
message.error(res.message || '删除失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
}
|
||||
}
|
||||
|
||||
function handleViewUsers(_record: RoleInfo) {
|
||||
message.info('查看用户功能开发中');
|
||||
}
|
||||
|
||||
const permissionModalVisible = ref(false);
|
||||
const permissionLoading = ref(false);
|
||||
const permissionTreeLoading = ref(false);
|
||||
const permissionTree = ref<PermissionTreeNode[]>([]);
|
||||
const checkedKeys = ref<number[]>([]);
|
||||
|
||||
async function handlePermission(record: RoleInfo) {
|
||||
currentRoleId.value = record.id;
|
||||
permissionModalVisible.value = true;
|
||||
checkedKeys.value = [];
|
||||
await loadPermissionTree();
|
||||
}
|
||||
|
||||
async function loadPermissionTree() {
|
||||
permissionTreeLoading.value = true;
|
||||
try {
|
||||
if (!currentRoleId.value) return;
|
||||
|
||||
const res: any = await roleApi.getRolePermissions(currentRoleId.value);
|
||||
if (res.code === 200) {
|
||||
permissionTree.value = res.data;
|
||||
extractCheckedKeys(res.data);
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('获取权限数据失败');
|
||||
} finally {
|
||||
permissionTreeLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function extractCheckedKeys(tree: PermissionTreeNode[]) {
|
||||
const keys: number[] = [];
|
||||
|
||||
function traverse(nodes: PermissionTreeNode[]) {
|
||||
for (const node of nodes) {
|
||||
if (node.checked) {
|
||||
keys.push(node.id);
|
||||
}
|
||||
if (node.children?.length) {
|
||||
traverse(node.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(tree);
|
||||
checkedKeys.value = keys;
|
||||
}
|
||||
|
||||
async function handleSavePermissions() {
|
||||
if (!currentRoleId.value) return;
|
||||
|
||||
permissionLoading.value = true;
|
||||
try {
|
||||
const res: any = await roleApi.updateRolePermissions(
|
||||
currentRoleId.value,
|
||||
checkedKeys.value as unknown as number[]
|
||||
);
|
||||
|
||||
if (res.code === 200) {
|
||||
message.success('权限保存成功');
|
||||
permissionModalVisible.value = false;
|
||||
} else {
|
||||
message.error(res.message || '保存失败');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('保存失败');
|
||||
} finally {
|
||||
permissionLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchRoleList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-management {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-link) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,499 @@
|
||||
<template>
|
||||
<div class="user-management">
|
||||
<a-card :bordered="false">
|
||||
<a-form layout="inline" :model="searchForm" class="search-form">
|
||||
<a-form-item label="关键词">
|
||||
<a-input v-model:value="searchForm.keyword" placeholder="用户名/昵称/手机号" allow-clear style="width: 220px" @pressEnter="handleSearch" />
|
||||
</a-form-item>
|
||||
<a-form-item label="状态">
|
||||
<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-form-item>
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="handleSearch">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<div class="table-operations">
|
||||
<a-space>
|
||||
<a-button type="primary" v-permission="'user:create'" @click="handleAdd">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
新增用户
|
||||
</a-button>
|
||||
<a-button danger v-permission="'user:delete'" :disabled="!selectedRowKeys.length" @click="handleBatchDelete">
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
批量删除 ({{ selectedRowKeys.length }})
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-card :bordered="false">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="dataSource"
|
||||
:row-selection="rowSelection"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:scroll="{ x: 1200 }"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'avatar'">
|
||||
<a-avatar :src="record.avatar" :size="40">{{ record.nickname?.charAt(0) }}</a-avatar>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'roles'">
|
||||
<a-space>
|
||||
<a-tag v-for="role in record.userRoles" :key="role.roleId" :color="getRoleColor(role.role?.code)">
|
||||
{{ role.role?.name }}
|
||||
</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)"
|
||||
v-permission="'user:update'"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" v-permission="'user:update'" @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 v-permission="'user:delete'">
|
||||
<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="nickname">
|
||||
<a-input v-model:value="formData.nickname" 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';
|
||||
|
||||
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: '',
|
||||
nickname: '',
|
||||
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' }],
|
||||
nickname: [{ 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', width: 120 },
|
||||
{ title: '昵称', dataIndex: 'nickname', key: 'nickname', width: 120 },
|
||||
{ title: '手机号', dataIndex: 'phone', key: 'phone', width: 130 },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email', width: 180 },
|
||||
{ title: '角色', key: 'roles', width: 200 },
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
align: 'center' as const,
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 180 },
|
||||
{ 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.name,
|
||||
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.nickname = record.nickname;
|
||||
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.nickname = '';
|
||||
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 = {
|
||||
nickname: formData.nickname,
|
||||
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,
|
||||
nickname: formData.nickname,
|
||||
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';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.user-management {
|
||||
padding: 24px;
|
||||
|
||||
.search-form {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.table-operations {
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
.ant-table-thead > tr > th {
|
||||
background-color: #fafafa;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background-color: #e6f7ff;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.ant-avatar) {
|
||||
background-color: #1890ff;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user