feat: 新增部门页面

This commit is contained in:
valarchie 2023-07-20 22:52:22 +08:00
parent 51c1deab44
commit 5ae73339c0
6 changed files with 462 additions and 262 deletions

82
src/api/system/dept.ts Normal file
View File

@ -0,0 +1,82 @@
import { http } from "@/utils/http";
export interface DeptQuery extends BaseQuery {
deptId?: number;
parentId?: number;
}
/**
* DeptDTO
*/
export interface DeptDTO {
createTime?: Date;
id?: number;
deptName?: string;
email?: string;
leaderName?: string;
orderNum?: number;
parentId?: number;
phone?: string;
status?: number;
statusStr?: string;
}
/**
* AddDeptCommand
*/
export interface DeptRequest {
deptName: string;
email?: string;
leaderName?: string;
orderNum: number;
parentId: number;
phone?: string;
status: number;
}
export interface DeptTreeDTO {
id: number;
parentId: number;
label: string;
children: [DeptTreeDTO];
}
/** 获取部门列表 */
export const getDeptListApi = (params?: DeptQuery) => {
return http.request<ResponseData<Array<DeptDTO>>>("get", "/system/depts", {
params
});
};
/** 新增部门 */
export const addDeptApi = (data: DeptRequest) => {
console.log(data);
return http.request<ResponseData<void>>("post", "/system/dept", {
data
});
};
/** 部门详情 */
export const getDeptInfoApi = (deptId: string) => {
return http.request<ResponseData<DeptDTO>>("get", `/system/dept/${deptId}`);
};
/** 修改部门 */
export const updateDeptApi = (deptId: string, data: DeptRequest) => {
return http.request<ResponseData<void>>("put", `/system/dept/${deptId}`, {
data
});
};
/** 删除部门 */
export const deleteDeptApi = (deptId: string) => {
return http.request<ResponseData<void>>("delete", `/system/dept/${deptId}`);
};
/** 获取部门树级结构 */
export const getDeptTree = () => {
return http.request<ResponseData<DeptTreeDTO>>(
"get",
"/system/depts/dropdown"
);
};

View File

@ -0,0 +1,134 @@
<script setup lang="ts">
import { ref } from "vue";
import ReCol from "@/components/ReCol";
import { formRules } from "./utils/rule";
import { usePublicHooks } from "../hooks";
import { DeptRequest } from "@/api/system/dept";
interface FormProps {
formInline: DeptRequest;
higherDeptOptions: any[];
}
const props = withDefaults(defineProps<FormProps>(), {
formInline: () => ({
id: 0,
parentId: 0,
deptName: "",
leaderName: "",
phone: "",
email: "",
orderNum: 0,
status: 1
}),
higherDeptOptions: () => []
});
const ruleFormRef = ref();
const { switchStyle } = usePublicHooks();
const newFormInline = ref(props.formInline);
const deptOptions = ref(props.higherDeptOptions);
function getRef() {
return ruleFormRef.value;
}
defineExpose({ getRef });
</script>
<template>
<el-form
ref="ruleFormRef"
:model="newFormInline"
:rules="formRules"
label-width="82px"
>
<el-row :gutter="30">
<re-col>
<el-form-item label="上级部门">
<el-cascader
class="w-full"
v-model="newFormInline.parentId"
:options="deptOptions"
:props="{
value: 'id',
label: 'deptName',
emitPath: false,
checkStrictly: true
}"
clearable
placeholder="请选择上级部门"
/>
<!-- 这种写法可以自定义选项的内容 比如括号后面加上子节点的数字 -->
<!-- <template #default="{ node, data }">
<span>{{ data.deptName }}</span>
<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
</template> -->
<!-- </el-cascader> -->
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门名称" prop="deptName">
<el-input
v-model="newFormInline.deptName"
clearable
placeholder="请输入部门名称"
/>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门负责人">
<el-input
v-model="newFormInline.leaderName"
clearable
placeholder="请输入部门负责人"
/>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="手机号" prop="phone">
<el-input
v-model="newFormInline.phone"
clearable
placeholder="请输入手机号"
/>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="邮箱" prop="email">
<el-input
v-model="newFormInline.email"
clearable
placeholder="请输入邮箱"
/>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="排序">
<el-input-number
v-model="newFormInline.orderNum"
:min="0"
:max="9999"
controls-position="right"
/>
</el-form-item>
</re-col>
<re-col :value="12" :xs="24" :sm="24">
<el-form-item label="部门状态">
<el-switch
v-model="newFormInline.status"
inline-prompt
:active-value="1"
:inactive-value="0"
active-text="启用"
inactive-text="停用"
:style="switchStyle"
/>
</el-form-item>
</re-col>
</el-row>
</el-form>
</template>

View File

@ -1,71 +1,49 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from "vue"; import { ref } from "vue";
import { useLoginLogHook } from "./utils/hook"; import { useHook } from "./utils/hook";
import { PureTableBar } from "@/components/RePureTableBar"; import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks"; import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import Delete from "@iconify-icons/ep/delete"; import Delete from "@iconify-icons/ep/delete";
import EditPen from "@iconify-icons/ep/edit-pen";
import Search from "@iconify-icons/ep/search"; import Search from "@iconify-icons/ep/search";
import Refresh from "@iconify-icons/ep/refresh"; import Refresh from "@iconify-icons/ep/refresh";
import { useUserStoreHook } from "@/store/modules/user"; import AddFill from "@iconify-icons/ri/add-circle-line";
// TODO
import { CommonUtils } from "@/utils/common";
/** 组件name最好和菜单表中的router_name一致 */
defineOptions({ defineOptions({
name: "SystemOperationLog" name: "SystemDept"
}); });
const loginLogStatusList = const formRef = ref();
useUserStoreHook().dictionaryList["sysLoginLog.status"];
const tableRef = ref(); const tableRef = ref();
const searchFormRef = ref();
const { const {
searchFormParams, searchFormParams,
pageLoading, loading,
columns, columns,
dataList, dataList,
pagination,
timeRange,
defaultSort,
multipleSelection,
onSearch, onSearch,
resetForm, resetForm,
exportAllExcel, openDialog,
getLoginLogList, handleDelete
handleDelete, } = useHook();
handleBulkDelete
} = useLoginLogHook();
</script> </script>
<template> <template>
<div class="main"> <div class="main">
<!-- 搜索栏 -->
<el-form <el-form
ref="searchFormRef" ref="formRef"
:inline="true" :inline="true"
:model="searchFormParams" :model="searchFormParams"
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]" class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
> >
<el-form-item label="登录IP" prop="ipAddress"> <el-form-item label="部门名称:" prop="name">
<el-input <el-input
v-model="searchFormParams.ipAddress" v-model="searchFormParams.deptName"
placeholder="请输入IP地址" placeholder="请输入部门名称"
clearable clearable
class="!w-[200px]" class="!w-[200px]"
/> />
</el-form-item> </el-form-item>
<el-form-item label="用户名:" prop="username">
<el-input
v-model="searchFormParams.username"
placeholder="请选择用户名称"
clearable
class="!w-[200px]"
/>
</el-form-item>
<el-form-item label="状态:" prop="status"> <el-form-item label="状态:" prop="status">
<el-select <el-select
v-model="searchFormParams.status" v-model="searchFormParams.status"
@ -73,94 +51,74 @@ const {
clearable clearable
class="!w-[180px]" class="!w-[180px]"
> >
<el-option <el-option label="启用" :value="1" />
v-for="dict in loginLogStatusList" <el-option label="停用" :value="0" />
:key="dict.value"
:label="dict.label"
:value="dict.value"
/>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item>
<label class="el-form-item__label is-required font-bold"
>登录时间</label
>
<!-- TODO 如何消除这个v-model的warning -->
<el-date-picker
class="!w-[240px]"
v-model="timeRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
/>
</el-form-item>
<el-form-item> <el-form-item>
<el-button <el-button
type="primary" type="primary"
:icon="useRenderIcon(Search)" :icon="useRenderIcon(Search)"
:loading="pageLoading" :loading="loading"
@click="onSearch" @click="onSearch"
> >
搜索 搜索
</el-button> </el-button>
<el-button <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
:icon="useRenderIcon(Refresh)"
@click="resetForm(searchFormRef, tableRef)"
>
重置 重置
</el-button> </el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
<!-- table bar 包裹 table --> <PureTableBar
<PureTableBar title="登录日志列表" :columns="columns" @refresh="onSearch"> title="部门列表(仅演示,操作后不生效)"
<!-- 表格操作栏 --> :columns="columns"
:tableRef="tableRef?.getTableRef()"
@refresh="onSearch"
>
<template #buttons> <template #buttons>
<el-button
type="danger"
:icon="useRenderIcon(Delete)"
@click="handleBulkDelete(tableRef)"
>
批量删除
</el-button>
<el-button <el-button
type="primary" type="primary"
@click="CommonUtils.exportExcel(columns, dataList, '登录日志列表')" :icon="useRenderIcon(AddFill)"
>单页导出</el-button @click="openDialog()"
> >
<el-button type="primary" @click="exportAllExcel">全部导出</el-button> 新增部门
</el-button>
</template> </template>
<template v-slot="{ size, dynamicColumns }"> <template v-slot="{ size, dynamicColumns }">
<pure-table <pure-table
border
ref="tableRef" ref="tableRef"
border
adaptive
:adaptiveConfig="{ offsetBottom: 32 }"
align-whole="center" align-whole="center"
row-key="id"
showOverflowTooltip showOverflowTooltip
table-layout="auto" table-layout="auto"
:loading="pageLoading" default-expand-all
:loading="loading"
:size="size" :size="size"
adaptive
:data="dataList" :data="dataList"
:columns="dynamicColumns" :columns="dynamicColumns"
:default-sort="defaultSort"
:pagination="pagination"
:paginationSmall="size === 'small' ? true : false"
:header-cell-style="{ :header-cell-style="{
background: 'var(--el-table-row-hover-bg-color)', background: 'var(--el-table-row-hover-bg-color)',
color: 'var(--el-text-color-primary)' color: 'var(--el-text-color-primary)'
}" }"
@page-size-change="getLoginLogList"
@page-current-change="getLoginLogList"
@sort-change="getLoginLogList"
@selection-change="
rows => (multipleSelection = rows.map(item => item.logId))
"
> >
<template #operation="{ row }"> <template #operation="{ row }">
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('编辑', row)"
>
编辑
</el-button>
<el-popconfirm <el-popconfirm
:title="`是否确认删除编号为${row.logId}的这条日志`" :title="`是否确认删除部门名称为${row.deptName}的这条数据`"
@confirm="handleDelete(row)" @confirm="handleDelete(row)"
> >
<template #reference> <template #reference>
@ -182,11 +140,7 @@ const {
</div> </div>
</template> </template>
<style scoped lang="scss"> <style lang="scss" scoped>
:deep(.el-dropdown-menu__item i) {
margin: 0;
}
.search-form { .search-form {
:deep(.el-form-item) { :deep(.el-form-item) {
margin-bottom: 12px; margin-bottom: 12px;

View File

@ -1,234 +1,224 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import editForm from "../form.vue";
import { handleTree } from "@/utils/tree";
import { message } from "@/utils/message"; import { message } from "@/utils/message";
import { ElMessageBox, Sort } from "element-plus";
import { import {
getLoginLogListApi, DeptDTO,
deleteLoginLogApi, DeptRequest,
exportLoginLogExcelApi, addDeptApi,
LoginLogQuery deleteDeptApi,
} from "@/api/system/log"; getDeptInfoApi,
import { reactive, ref, onMounted, toRaw } from "vue"; getDeptListApi,
import { useUserStoreHook } from "@/store/modules/user"; updateDeptApi
import { CommonUtils } from "@/utils/common"; } from "@/api/system/dept";
import { PaginationProps } from "@pureadmin/table"; import { usePublicHooks } from "../../hooks";
import { addDialog } from "@/components/ReDialog";
import { reactive, ref, onMounted, h, computed } from "vue";
import { isAllEmpty } from "@pureadmin/utils";
const loginLogStatusMap = export function useHook() {
useUserStoreHook().dictionaryMap["sysLoginLog.status"]; const searchFormParams = reactive({
deptName: "",
export function useLoginLogHook() { status: null
const defaultSort: Sort = {
prop: "loginTime",
order: "descending"
};
const pagination: PaginationProps = {
total: 0,
pageSize: 10,
currentPage: 1,
background: true
};
const timeRange = ref([]);
const searchFormParams = reactive<LoginLogQuery>({
ipAddress: undefined,
username: undefined,
status: undefined,
beginTime: undefined,
endTime: undefined,
timeRangeColumn: defaultSort.prop
}); });
const dataList = ref([]); const formRef = ref();
const pageLoading = ref(true);
const multipleSelection = ref([]); const originalDataList = ref([]);
const dataList = computed(() => {
let filterDataList = [...originalDataList.value];
if (!isAllEmpty(searchFormParams.deptName)) {
// 前端搜索部门名称
filterDataList = filterDataList.filter(item =>
item.deptName.includes(searchFormParams.deptName)
);
}
if (!isAllEmpty(searchFormParams.status)) {
// 前端搜索状态
filterDataList = filterDataList.filter(
item => item.status === searchFormParams.status
);
}
// 处理成树结构
return [...handleTree(filterDataList)];
});
const loading = ref(true);
const { tagStyle } = usePublicHooks();
const columns: TableColumnList = [ const columns: TableColumnList = [
{ {
type: "selection", label: "部门名称",
prop: "deptName",
width: 240,
align: "left" align: "left"
}, },
{ {
label: "日志编号", label: "部门编号",
prop: "logId", prop: "id",
minWidth: 100 width: 100,
align: "center"
}, },
{ {
label: "用户名", label: "部门负责人",
prop: "username", prop: "leaderName",
minWidth: 120, minWidth: 70
sortable: "custom"
},
{
label: "IP地址",
prop: "ipAddress",
minWidth: 120
},
{
label: "登录地点",
prop: "loginLocation",
minWidth: 120
},
{
label: "操作系统",
prop: "operationSystem",
minWidth: 120
},
{
label: "浏览器",
prop: "browser",
minWidth: 120
}, },
{ {
label: "状态", label: "状态",
prop: "status", prop: "status",
minWidth: 120, minWidth: 100,
cellRenderer: ({ row, props }) => ( cellRenderer: ({ row, props }) => (
<el-tag <el-tag size={props.size} style={tagStyle.value(row.status)}>
size={props.size} {row.status === 1 ? "启用" : "停用"}
type={loginLogStatusMap[row.status].cssTag}
effect="plain"
>
{loginLogStatusMap[row.status].label}
</el-tag> </el-tag>
) )
}, },
{ {
label: "状态名", label: "排序",
prop: "statusStr", prop: "orderNum",
minWidth: 120, minWidth: 70
hide: true
}, },
{ {
label: "登录时间", label: "创建时间",
minWidth: 160, minWidth: 200,
prop: "loginTime", prop: "createTime",
sortable: "custom", formatter: ({ createTime }) =>
formatter: ({ loginTime }) => dayjs(createTime).format("YYYY-MM-DD HH:mm:ss")
dayjs(loginTime).format("YYYY-MM-DD HH:mm:ss")
}, },
{ {
label: "操作", label: "操作",
fixed: "right", fixed: "right",
width: 140, width: 240,
slot: "operation" slot: "operation"
} }
]; ];
async function onSearch() { function resetForm(formEl) {
// 点击搜索的时候 需要重置分页
pagination.currentPage = 1;
getLoginLogList();
}
function resetForm(formEl, tableRef) {
if (!formEl) return; if (!formEl) return;
// 清空查询参数
formEl.resetFields(); formEl.resetFields();
// 清空排序
searchFormParams.orderColumn = undefined;
searchFormParams.orderDirection = undefined;
// 清空时间查询 TODO 这块有点繁琐 有可以优化的地方吗?
// Form组件的resetFields方法无法清除datepicker里面的数据。
timeRange.value = [];
searchFormParams.beginTime = undefined;
searchFormParams.endTime = undefined;
tableRef.getTableRef().clearSort();
// 重置分页并查询
onSearch(); onSearch();
} }
async function getLoginLogList(sort: Sort = defaultSort) { async function onSearch() {
pageLoading.value = true; loading.value = true;
if (sort != null) { // 这里是返回一维数组结构前端自行处理成树结构返回格式要求唯一id加父节点parentIdparentId取父节点id
CommonUtils.fillSortParams(searchFormParams, sort); const { data } = await getDeptListApi().finally(() => {
} loading.value = false;
CommonUtils.fillPaginationParams(searchFormParams, pagination); });
CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value); originalDataList.value = data;
const { data } = await getLoginLogListApi(toRaw(searchFormParams)).finally(
() => {
pageLoading.value = false;
}
);
dataList.value = data.rows;
pagination.total = data.total;
} }
async function exportAllExcel(sort: Sort = defaultSort) { /**
if (sort != null) { * status字段值判断追加是否禁用disabled字段
CommonUtils.fillSortParams(searchFormParams, sort); *
* disabled disabled: 'status' (true/false)
* @param treeList
* @returns
*/
function setDisabledForTreeOptions(treeList) {
if (!treeList || !treeList.length) return;
const newTreeList = [];
for (let i = 0; i < treeList.length; i++) {
treeList[i].disabled = treeList[i].status === 0 ? true : false;
setDisabledForTreeOptions(treeList[i].children);
newTreeList.push(treeList[i]);
} }
CommonUtils.fillPaginationParams(searchFormParams, pagination); return newTreeList;
CommonUtils.fillTimeRangeParams(searchFormParams, timeRange.value);
exportLoginLogExcelApi(toRaw(searchFormParams), "登录日志.xls");
} }
async function handleDelete(row) { async function handleAdd(row, done) {
await deleteLoginLogApi([row.logId]).then(() => { await addDeptApi(row).then(() => {
message(`您删除了操作编号为${row.logId}的这条数据`, { message(`新增了部门:${row.deptName}`, {
type: "success" type: "success"
}); });
// 关闭弹框
done();
// 刷新列表 // 刷新列表
getLoginLogList(); onSearch();
}); });
} }
async function handleBulkDelete(tableRef) { async function handleUpdate(row, done) {
if (multipleSelection.value.length === 0) { await updateDeptApi(row.id, row).then(() => {
message("请选择需要删除的数据", { type: "warning" }); message(`您更新了部门${row.deptName}`, {
return; type: "success"
});
// 关闭弹框
done();
// 刷新列表
onSearch();
});
}
async function openDialog(title = "新增", row?: DeptDTO) {
const { data } = await getDeptListApi();
const treeList = setDisabledForTreeOptions(handleTree(data));
if (title === "编辑") {
row = (await getDeptInfoApi(row.id + "")).data;
} }
ElMessageBox.confirm( // TODO 为什么声明一个formInline变量,把变量填充进去, 再给props.formInline 结果就不生效
`确认要<strong>删除</strong>编号为<strong style='color:var(--el-color-primary)'>[ ${multipleSelection.value} ]</strong>的日志吗?`, addDialog({
"系统提示", title: `${title}部门`,
{ props: {
confirmButtonText: "确定", formInline: {
cancelButtonText: "取消", id: row?.id ?? 0,
type: "warning", parentId: row?.parentId ?? 0,
dangerouslyUseHTMLString: true, deptName: row?.deptName ?? "",
draggable: true leaderName: row?.leaderName ?? "",
phone: row?.phone ?? "",
email: row?.email ?? "",
orderNum: row?.orderNum ?? 0,
status: row?.status ?? 1
},
higherDeptOptions: [...treeList]
},
width: "40%",
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(editForm, { ref: formRef }),
beforeSure: (done, { options }) => {
const FormRef = formRef.value.getRef();
const curData = options.props.formInline as DeptRequest;
FormRef.validate(valid => {
if (valid) {
// 表单规则校验通过
if (title === "新增") {
handleAdd(curData, done);
} else {
// 实际开发先调用编辑接口,再进行下面操作
handleUpdate(curData, done);
}
}
});
} }
) });
.then(async () => { }
await deleteLoginLogApi(multipleSelection.value).then(() => {
message(`您删除了日志编号为[ ${multipleSelection.value} ]的数据`, { async function handleDelete(row) {
type: "success" await deleteDeptApi(row.id).then(() => {
}); message(`您删除了部门${row.deptName}`, { type: "success" });
// 刷新列表 // 刷新列表
getLoginLogList(); onSearch();
}); });
})
.catch(() => {
message("取消删除", {
type: "info"
});
// 清空checkbox选择的数据
tableRef.getTableRef().clearSelection();
});
} }
onMounted(() => { onMounted(() => {
getLoginLogList(); onSearch();
}); });
return { return {
searchFormParams, searchFormParams,
pageLoading, loading,
columns, columns,
dataList, dataList,
pagination,
defaultSort,
timeRange,
multipleSelection,
onSearch, onSearch,
exportAllExcel,
// exportExcel,
getLoginLogList,
resetForm, resetForm,
handleDelete, openDialog,
handleBulkDelete handleDelete
}; };
} }

View File

@ -0,0 +1,37 @@
import { reactive } from "vue";
import type { FormRules } from "element-plus";
import { isPhone, isEmail } from "@pureadmin/utils";
/** 自定义表单规则校验 */
export const formRules = reactive(<FormRules>{
name: [{ required: true, message: "部门名称为必填项", trigger: "blur" }],
phone: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback();
} else if (!isPhone(value)) {
callback(new Error("请输入正确的手机号码格式"));
} else {
callback();
}
},
trigger: "blur"
// trigger: "click" // 如果想在点击确定按钮时触发这个校验trigger 设置成 click 即可
}
],
email: [
{
validator: (rule, value, callback) => {
if (value === "") {
callback();
} else if (!isEmail(value)) {
callback(new Error("请输入正确的邮箱格式"));
} else {
callback();
}
},
trigger: "blur"
}
]
});

5
types/index.d.ts vendored
View File

@ -60,9 +60,12 @@ type PageDTO<T> = {
rows: Array<T>; rows: Array<T>;
}; };
interface BasePageQuery { interface BasePageQuery extends BaseQuery {
pageNum?: number; pageNum?: number;
pageSize?: number; pageSize?: number;
}
interface BaseQuery {
beginTime?: string; beginTime?: string;
endTime?: string; endTime?: string;
orderColumn?: string; orderColumn?: string;