Files
vue3-element-admin/src/views/system/user/index.vue

533 lines
15 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>
<div class="page-container">
<el-row :gutter="20">
<!-- 部门树 -->
<el-col :lg="4" :xs="24" class="page-aside">
<UserDeptTree v-model="queryParams.deptId" @node-click="handleQuery" />
</el-col>
<!-- 用户列表 -->
<el-col :lg="20" :xs="24" class="page-main">
<!-- 搜索区域 -->
<el-card shadow="never" class="page-search">
<el-form ref="queryFormRef" :model="queryParams" :inline="true" label-width="auto">
<el-form-item label="关键字" prop="keywords">
<el-input
v-model="queryParams.keywords"
placeholder="用户名/昵称/手机号"
clearable
@keyup.enter="handleQuery"
/>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select
v-model="queryParams.status"
placeholder="全部"
clearable
style="width: 100px"
>
<el-option label="正常" :value="1" />
<el-option label="禁用" :value="0" />
</el-select>
</el-form-item>
<el-form-item label="创建时间">
<el-date-picker
v-model="queryParams.createTime"
:editable="false"
type="daterange"
range-separator="~"
start-placeholder="开始时间"
end-placeholder="截止时间"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="search" @click="handleQuery">搜索</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card shadow="never" class="page-content">
<div class="page-toolbar">
<div class="page-toolbar__left">
<el-button
v-hasPerm="['sys:user:create']"
type="success"
icon="plus"
@click="handleCreateClick"
>
新增
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
:disabled="!hasSelection"
@click="handleDelete()"
>
删除
</el-button>
</div>
<div class="page-toolbar__right">
<el-button v-hasPerm="'sys:user:import'" icon="upload" @click="openImportDialog">
导入
</el-button>
<el-button v-hasPerm="'sys:user:export'" icon="download" @click="exportUsers">
导出
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="userList"
border
stripe
highlight-current-row
row-key="id"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="50" align="center" />
<el-table-column label="用户名" prop="username" min-width="120" show-overflow-tooltip />
<el-table-column label="昵称" width="200" align="center" prop="nickname" />
<el-table-column label="性别" width="100" align="center">
<template #default="scope">
<DictTag v-model="scope.row.gender" code="gender" />
</template>
</el-table-column>
<el-table-column label="部门" width="120" align="center" prop="deptName" />
<el-table-column label="角色" align="center" prop="roleNames" min-width="160" />
<el-table-column label="手机号码" align="center" prop="mobile" width="120" />
<el-table-column label="邮箱" align="center" prop="email" width="160" />
<el-table-column label="状态" align="center" prop="status" width="80">
<template #default="scope">
<el-tag :type="scope.row.status === CommonStatus.ENABLED ? 'success' : 'info'">
{{ scope.row.status === CommonStatus.ENABLED ? "正常" : "禁用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="创建时间" align="center" prop="createTime" width="180" />
<el-table-column label="操作" fixed="right" width="220">
<template #default="scope">
<el-button
v-hasPerm="'sys:user:reset-password'"
type="primary"
icon="RefreshLeft"
size="small"
link
@click="handleResetPassword(scope.row)"
>
重置密码
</el-button>
<el-button
v-hasPerm="'sys:user:update'"
type="primary"
icon="edit"
link
size="small"
@click="handleEditClick(scope.row.id)"
>
编辑
</el-button>
<el-button
v-hasPerm="'sys:user:delete'"
type="danger"
icon="delete"
link
size="small"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination
v-if="total > 0"
v-model:total="total"
v-model:page="queryParams.pageNum"
v-model:limit="queryParams.pageSize"
@pagination="fetchList"
/>
</el-card>
</el-col>
</el-row>
<!-- 用户表单 -->
<el-drawer
v-model="dialogState.visible"
:title="dialogState.title"
append-to-body
:size="drawerSize"
@close="closeDialog"
>
<el-form ref="userFormRef" :model="formData" :rules="rules" label-width="80px">
<el-form-item label="用户名" prop="username">
<el-input
v-model="formData.username"
:readonly="!!formData.id"
placeholder="请输入用户名"
/>
</el-form-item>
<el-form-item label="用户昵称" prop="nickname">
<el-input v-model="formData.nickname" placeholder="请输入用户昵称" />
</el-form-item>
<el-form-item label="所属部门" prop="deptId">
<el-tree-select
v-model="formData.deptId"
placeholder="请选择所属部门"
:data="deptOptions"
filterable
check-strictly
:render-after-expand="false"
/>
</el-form-item>
<el-form-item label="性别" prop="gender">
<DictSelect v-model="formData.gender" code="gender" />
</el-form-item>
<el-form-item label="角色" prop="roleIds">
<el-select v-model="formData.roleIds" multiple placeholder="请选择">
<el-option
v-for="item in roleOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="手机号码" prop="mobile">
<el-input v-model="formData.mobile" placeholder="请输入手机号码" maxlength="11" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="formData.email" placeholder="请输入邮箱" maxlength="50" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-switch
v-model="formData.status"
inline-prompt
active-text="正常"
inactive-text="禁用"
:active-value="CommonStatus.ENABLED"
:inactive-value="CommonStatus.DISABLED"
/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit"> </el-button>
<el-button @click="closeDialog"> </el-button>
</div>
</template>
</el-drawer>
<!-- 用户导入 -->
<UserImportDialog v-model="importDialogVisible" @import-success="handleQuery()" />
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { useDebounceFn } from "@vueuse/core";
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
import type { UserForm, UserQueryParams, UserItem } from "@/api/system/user";
import { downloadFile } from "@/utils";
import UserAPI from "@/api/system/user";
import DeptAPI from "@/api/system/dept";
import RoleAPI from "@/api/system/role";
import { useUserStore, useAppStore } from "@/store";
import { DeviceEnum, DialogMode, CommonStatus } from "@/enums";
import { useTableSelection } from "@/composables";
import UserDeptTree from "./components/UserDeptTree.vue";
import UserImportDialog from "./components/UserImportDialog.vue";
defineOptions({
name: "User",
inheritAttrs: false,
});
const appStore = useAppStore();
const userStore = useUserStore();
// 表单引用
const queryFormRef = ref<FormInstance>();
const userFormRef = ref<FormInstance>();
// 查询参数
const queryParams = reactive<UserQueryParams>({
pageNum: 1,
pageSize: 10,
});
// 列表数据
const userList = ref<UserItem[]>([]);
const total = ref(0);
const loading = ref(false);
// 弹窗状态
const dialogState = reactive({
visible: false,
title: "新增用户",
mode: DialogMode.CREATE,
});
// 导入弹窗状态
const importDialogVisible = ref(false);
// 表单初始数据
const initialFormData: UserForm = {
status: CommonStatus.ENABLED,
};
// 表单数据
const formData = reactive<UserForm>({ ...initialFormData });
// 下拉选项
const deptOptions = ref<OptionItem[]>();
const roleOptions = ref<OptionItem[]>();
const drawerSize = computed(() => (appStore.device === DeviceEnum.DESKTOP ? "600px" : "90%"));
const rules: FormRules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
nickname: [{ required: true, message: "请输入用户昵称", trigger: "blur" }],
deptId: [{ required: true, message: "请选择所属部门", trigger: "change" }],
roleIds: [{ required: true, message: "请选择用户角色", trigger: "change" }],
email: [{ type: "email", message: "请输入正确的邮箱地址", trigger: "blur" }],
mobile: [{ pattern: /^1[3-9]\d{9}$/, message: "请输入正确的手机号码", trigger: "blur" }],
};
/**
* 加载用户列表数据
*/
async function fetchList(): Promise<void> {
loading.value = true;
try {
const data = await UserAPI.getPage(queryParams);
userList.value = data.list;
total.value = data.total ?? 0;
} finally {
loading.value = false;
}
}
/**
* 加载表单下拉选项数据
*/
async function loadFormOptions(): Promise<void> {
[roleOptions.value, deptOptions.value] = await Promise.all([
RoleAPI.getOptions(),
DeptAPI.getOptions(),
]);
}
const { selectedIds, hasSelection, handleSelectionChange } = useTableSelection<UserItem>();
/**
* 执行查询(重置页码)
*/
function handleQuery(): void {
queryParams.pageNum = 1;
fetchList();
}
/**
* 重置查询条件
*/
function resetQuery(): void {
queryFormRef.value?.resetFields();
queryParams.deptId = undefined;
queryParams.createTime = undefined;
}
/**
* 重置查询条件并重新查询
*/
function handleResetQuery(): void {
resetQuery();
handleQuery();
}
/**
* 重置用户密码
* @param userId 用户ID
* @param password 新密码
*/
async function resetPassword(userId: string, password: string): Promise<void> {
await UserAPI.resetPassword(userId, password);
ElMessage.success("密码重置成功");
}
/**
* 删除用户
* @param userIds 用户ID列表多个ID用逗号分隔
*/
async function deleteUsers(userIds: string): Promise<void> {
await UserAPI.deleteByIds(userIds);
ElMessage.success("删除成功");
handleQuery();
}
/**
* 打开表单弹窗
*/
function openDialog(): void {
dialogState.visible = true;
}
/**
* 关闭表单弹窗
*/
function closeDialog(): void {
dialogState.visible = false;
resetForm();
}
/**
* 重置表单数据和验证状态
*/
function resetForm(): void {
userFormRef.value?.resetFields();
userFormRef.value?.clearValidate();
Object.assign(formData, initialFormData);
}
/**
* 重置密码按钮点击事件
* @param row 用户数据
*/
function handleResetPassword(row: UserItem): void {
ElMessageBox.prompt(`请输入用户【${row.username}】的新密码`, "重置密码", {
confirmButtonText: "确定",
cancelButtonText: "取消",
inputPattern: /.{6,}/,
inputErrorMessage: "密码至少需要6位字符",
}).then(
(result: any) => resetPassword(row.id, result.value),
() => {
/* 用户取消 */
}
);
}
/**
* 新增按钮点击事件
*/
async function handleCreateClick(): Promise<void> {
dialogState.title = "新增用户";
dialogState.mode = DialogMode.CREATE;
await loadFormOptions();
openDialog();
}
/**
* 编辑按钮点击事件
* @param id 用户ID
*/
async function handleEditClick(id: string): Promise<void> {
dialogState.title = "修改用户";
dialogState.mode = DialogMode.EDIT;
await loadFormOptions();
const data = await UserAPI.getFormData(id);
Object.assign(formData, data);
openDialog();
}
/**
* 提交表单(防抖处理)
*/
const handleSubmit = useDebounceFn(async () => {
const valid = await userFormRef.value?.validate().then(
() => true,
() => false
);
if (!valid) return;
loading.value = true;
try {
if (formData.id) {
await UserAPI.update(formData.id, formData);
ElMessage.success("修改用户成功");
} else {
await UserAPI.create(formData);
ElMessage.success("新增用户成功");
}
closeDialog();
handleQuery();
} finally {
loading.value = false;
}
}, 300);
/**
* 删除按钮点击事件
* @param id 用户ID不传则删除选中的用户
*/
function handleDelete(id?: string): void {
const userIds = id ?? selectedIds.value.join(",");
if (!userIds) {
ElMessage.warning("请勾选删除项");
return;
}
// 安全检查:防止删除当前登录用户
const currentUserId = userStore.userInfo?.userId;
if (currentUserId) {
const isCurrentUserInList = id
? id === currentUserId
: selectedIds.value.some((selectedId) => String(selectedId) === currentUserId);
if (isCurrentUserInList) {
ElMessage.error("不能删除当前登录用户");
return;
}
}
ElMessageBox.confirm("确认删除选中的用户吗?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(
() => deleteUsers(userIds),
() => {
/* 用户取消 */
}
);
}
/**
* 导出用户列表
*/
async function exportUsers(): Promise<void> {
const response = await UserAPI.export(queryParams);
downloadFile(response);
ElMessage.success("导出成功");
}
/**
* 打开导入弹窗
*/
function openImportDialog(): void {
importDialogVisible.value = true;
}
onMounted(() => {
handleQuery();
});
</script>
<style scoped lang="scss"></style>