feat: 初始化智能物联网设备管理系统前后端基础项目

完成项目整体结构搭建,包含:
1. 前端Vue3+TS+Vite+Antd项目基础配置与核心依赖
2. 后端MidwayJS+TypeORM+JWT权限认证框架
3. 完整的用户、角色、权限、日志数据库表结构
4. 核心业务模块API与基础服务实现
5. 全局样式、路由、状态管理配置
This commit is contained in:
andyesfly committed 2026-05-28 16:23:15 +08:00
commit 3d7eb12103
89 files changed
+16085

No files matched your search

+499
View File
@@ -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>