周末学习不停歇,最近新开一个VUE3全新系列,这一系列会从0开始学习VUE3,使用Vite、TS、Pinia、Element-Plus、mittBus等新知识点,既是查漏补缺,也是知识分享。
目前项目的登录、鉴权、动态菜单、权限按钮、页面布局、标签页、数据增删改查案例等基本功能都已经写完,整体效果如动图,欢迎各位小伙伴可以加入到这个项目,可以提交PR,早期参与贡献的,可以作为核心成员。不仅可以锻炼自己的VUE3的功底,也能和社区其他伙伴讨论,如果有需要讨论,可以创建一个群二维码。
代码地址:
https://github.com/anjoy8/bcvp.vue3.git
这是每篇文章一节课一个分支,方便大家学习,会慢慢的将blog.admin项目进行翻新,使用的后端接口还是BlogCore,将动态权限、动态菜单和动态按钮通过vue3+ts的方式完美升级。
系列文章:
本文参考的是开源项目
https://gitee.com/HalseySpicy/Geeker-Admin/tree/template
今天主要是完成了部门管理的新增、编辑和删除的标准写法,当然可能不是最简洁的,欢迎投稿:
在每一个页面中,都需要定义一个api的ts文件,定义所有的类,有请求类和响应类,以及各个接口,并设计了Get、Post、Put、Delete四种谓词方式。
参考src\api\departmentApi.ts
import { get, post, put, del, type BaseResponse } from '@/utils/axiosInstance';
/**
* 请求的入参接口
* @interface DepartmentRequest
*/
export interface DepartmentRequest {
page: number;
key: string;
f: string;
}
/**
* 部门响应接口
* @interface Department
*/
export interface Department {
CodeRelationship: string;
Name: string;
Leader: string;
OrderSort: number;
Enabled: boolean;
Status: boolean;
IsDeleted: boolean;
CreateBy: string;
CreateTime: string;
ModifyBy: string | null;
ModifyTime: string;
hasChildren: boolean;
Pid: string;
PidArr: string[];
Id: string;
}
// 获取菜单列表
export const getDepartmentListApi = async (params: DepartmentRequest): Promise<BaseResponse<Department[]>> => {
try {
const response = await get<BaseResponse<Department[]>>('/api/department/getTreeTable', params);
return response;
} catch (error) {
throw new Error('请求失败');
}
};
/**
* 机构树节点接口
* @interface DepartmentNode
*/
export interface DepartmentNode {
value: string;
Pid: string;
label: string;
order: number;
disabled: boolean;
children: DepartmentNode[] | null;
}
// 获取部门全量树
export const getDepartmentTree = async (pid: string): Promise<BaseResponse<DepartmentNode>> => {
try {
const response = await get<BaseResponse<DepartmentNode>>('/api/department/getDepartmentTree', { pid: pid });
return response;
} catch (error) {
throw new Error('请求失败');
}
};
// 新增部门数据
export const addDepartment = async (params: Department): Promise<BaseResponse<string>> => {
try {
const response = await post<BaseResponse<string>>('/api/department/post', params);
return response;
} catch (error) {
throw new Error('请求失败');
}
};
// 编辑部门数据
export const editDepartment = async (params: Department): Promise<BaseResponse<string>> => {
try {
const response = await put<BaseResponse<string>>('/api/department/put', params);
return response;
} catch (error) {
throw new Error('请求失败');
}
};
// 删除部门数据
export const removeDepartment = async (id: string): Promise<BaseResponse<string>> => {
try {
const response = await del<BaseResponse<string>>('/api/department/delete', { id: id });
return response;
} catch (error) {
throw new Error('请求失败');
}
};
根据需要,尽量定义丰富的interface类,比如多种泛型,甚至可以和后端保持一致,比如分页模式下,可以使用Base<Page<T>>,这种的进行渲染,不仅有很好的可读性,代码更健壮和规范。
在当前页面中,可以把vue代码和ts的脚本代码合理的拆开,定义一个function.ts来将所有的事件封装,这样不仅有可读性,更重要的就是要配合按钮权限来使用。
在src\views\Department\departmentFunctions.ts文件中
// departmentFunctions.ts
import { reactive, toRaw, ref } from 'vue';
import { getDepartmentListApi, addDepartment, editDepartment, removeDepartment, getDepartmentTree } from '@/api/departmentApi';
import type { DepartmentRequest, Department, DepartmentNode } from '@/api/departmentApi';
import { ElMessage, ElForm, ElMessageBox } from "element-plus";
import { formatDate } from "@/utils";
export const departments = ref<Department[]>([]);
export const listLoading = ref<boolean>(false);
export const page = ref<number>(1);
export const options = ref<DepartmentNode[]>([]);
export const addFormVisible = ref(false);
export const addLoading = ref(false);
export const editFormVisible = ref(false);
export const editLoading = ref(false);
export const isResouceShow = ref(0);
// 创建一个 ref 引用 el-form
export const addFormRef = ref<InstanceType<typeof ElForm> | null>(null);
export const editFormRef = ref<InstanceType<typeof ElForm> | null>(null);
export const currentRow = ref<Department | null>(null);
// ↓↓↓↓↓ 查询 ↓↓↓↓↓
export const handleQuery = async (filters: { name: string }) => {
currentRow.value = null;
const para = ref<DepartmentRequest>({
page: page.value,
f: '0',
key: filters.name,
});
listLoading.value = true;
try {
const { response } = await getDepartmentListApi(para.value);
departments.value = response ?? [];
} finally {
listLoading.value = false;
}
};
// ↑↑↑↑↑ 查询 ↑↑↑↑↑
// ↓↓↓↓↓ 新增 ↓↓↓↓↓
// 定义addForm数据并指定其类型为Department
export const addForm = reactive<Department>({
CodeRelationship: "",
Name: "",
Leader: "",
OrderSort: 0,
Enabled: true, // 可以根据需要设置初始值
Status: false,
IsDeleted: false, // 默认为false表示未删除
CreateBy: "",
CreateTime: "",
ModifyBy: null,
ModifyTime: "",
hasChildren: false, // 初始设为false,可以根据上下文修改
Pid: "",
PidArr: [],
Id: "" // 或使用字符串初始化如“0”以符号根节点
});
export const handleAdd = async () => {
options.value = [];
addFormVisible.value = true;
addLoading.value = true;
// 使用引用重置表单
if (addFormRef.value) {
addFormRef.value.resetFields();
}
try {
const { response } = await getDepartmentTree('0');
isResouceShow.value++;
options.value.push(response);
addLoading.value = false;
} catch (error) {
ElMessage.error("加载机构树失败");
} finally {
addLoading.value = false;
}
};
// 新增提交表单
export const addSubmit = async () => {
const formEl = addFormRef.value; // 获取表单实例
if (!formEl) return;
await formEl.validate(async (isValid) => {
if (isValid) {
const postData = toRaw(addForm);
postData.CodeRelationship = postData.PidArr.join() + ",";
postData.CreateTime = formatDate(new Date(), "yyyy-MM-dd hh:mm:ss");
postData.ModifyTime = postData.CreateTime;
postData.IsDeleted = false;
postData.Pid = postData.PidArr.pop() ?? '';
console.log(postData);
const { success, msg } = await addDepartment(postData);
if (success) {
ElMessage.success('提交成功');
await handleQuery({ name: '' });
} else {
ElMessage.error('提交失败' + msg);
}
addFormVisible.value = false;
} else {
ElMessage.error('验证失败,请检查输入项');
}
});
};
// ↑↑↑↑↑ 新增 ↑↑↑↑↑
// ↓↓↓↓↓ 编辑 ↓↓↓↓↓
// 定义addForm数据并指定其类型为Department
export const editForm = reactive<Department>({
CodeRelationship: "",
Name: "",
Leader: "",
OrderSort: 0,
Enabled: true, // 可以根据需要设置初始值
Status: false,
IsDeleted: false, // 默认为false表示未删除
CreateBy: "",
CreateTime: "",
ModifyBy: null,
ModifyTime: "",
hasChildren: false, // 初始设为false,可以根据上下文修改
Pid: "",
PidArr: [],
Id: "" // 或使用字符串初始化如“0”以符号根节点
});
export const handleEdit = async () => {
if (!(currentRow.value && currentRow.value?.Id)) {
ElMessage.error('请选择要编辑的一行数据!');
return;
}
options.value = [];
editFormVisible.value = true;
editLoading.value = true;
try {
const { response } = await getDepartmentTree(currentRow.value?.Id);
if (currentRow.value) {
Object.assign(editForm, currentRow.value);
}
isResouceShow.value++;
options.value.push(response);
editLoading.value = false;
} catch (error) {
ElMessage.error("加载机构树失败");
} finally {
editLoading.value = false;
}
};
// 编辑提交表单
export const editSubmit = async () => {
const formEl = editFormRef.value; // 获取表单实例
if (!formEl) return;
await formEl.validate(async (isValid) => {
if (isValid) {
ElMessageBox.confirm("确认提交吗?", "温馨提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const postData = toRaw(editForm);
postData.CodeRelationship = postData.PidArr.join() + ",";
postData.ModifyTime = formatDate(new Date(), "yyyy-MM-dd hh:mm:ss");
postData.Pid = postData.PidArr.pop() ?? '';
console.log(postData);
const { success, msg } = await editDepartment(postData);
if (success) {
ElMessage.success('提交成功');
await handleQuery({ name: '' });
} else {
ElMessage.error('提交失败' + msg);
}
});
editFormVisible.value = false;
} else {
ElMessage.error('验证失败,请检查输入项');
}
});
};
// ↑↑↑↑↑ 编辑 ↑↑↑↑↑
// ↓↓↓↓↓ 删除 ↓↓↓↓↓
// 删除数据
export const handleDel = async () => {
if (!(currentRow.value && currentRow.value?.Id)) {
ElMessage.error('请选择要删除的一行数据!');
return;
}
ElMessageBox.confirm("确认删除该记录吗?", "温馨提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
}).then(async () => {
const { success, msg } = await removeDepartment(currentRow.value?.Id || '0');
if (success) {
ElMessage.success('删除成功');
await handleQuery({ name: '' });
} else {
ElMessage.error('提交失败' + msg);
}
});
};
// ↑↑↑↑↑ 删除 ↑↑↑↑↑
可以看到,核心的写法和vue2还是差不多的,主要就是在变量的定义上,需要有一定的调整,写习惯了就好了。
页面渲染包括顶部的操作按钮工具条、表格和新增编辑弹窗的渲染,以及数据的控制。
页面src\views\Department\Department.vue
<template>
<section>
<!--工具条-->
<toolbar :button-list="buttonList"></toolbar>
<!-- 列表 -->
<el-table :data="departments" ref="tableRef" v-loading="listLoading" @select="dialogCheck"
@row-click="selectCurrentRow" row-key="Id" :load="load"
:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" border lazy style="width: 100%">
<el-table-column type="selection" width="50"></el-table-column>
<el-table-column prop="Name" label="部门" width="200"></el-table-column>
<el-table-column prop="Id" label="Id" width="80"></el-table-column>
<el-table-column prop="CodeRelationship" label="上级关系"></el-table-column>
<el-table-column prop="Leader" label="负责人"></el-table-column>
<el-table-column prop="OrderSort" label="Sort"></el-table-column>
<el-table-column prop="Status" label="是否有效" width="100">
<template #default="{ row }">
<el-tag :type="row.Status ? 'success' : 'danger'">{{ row.Status ? '是' : '否' }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="CreateTime" label="创建时间" :formatter="formatCreateTime" width="250"
sortable></el-table-column>
<el-table-column prop="ModifyTime" label="更新时间" :formatter="formatModifyTime" width="250"
sortable></el-table-column>
</el-table>
<!-- 新增 -->
<el-dialog title="新增" v-model="addFormVisible" :close-on-click-modal="false">
<el-form :model="addForm" :rules="addFormRules" ref="addFormRef" label-width="80px">
<el-form-item label="部门名称" prop="Name">
<el-input v-model="addForm.Name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="上级关系" prop="CodeRelationship">
<el-tooltip content="以','号结尾,方便下属部门统一查询" placement="top">
<el-input v-model="addForm.CodeRelationship" disabled auto-complete="off"></el-input>
</el-tooltip>
</el-form-item>
<el-form-item label="负责人" prop="Leader">
<el-input v-model="addForm.Leader" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="排序" prop="OrderSort">
<el-input v-model="addForm.OrderSort" type="number" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="是否有效" prop="Status">
<el-switch v-model="addForm.Status"></el-switch>
</el-form-item>
<el-form-item v-if="options.length > 0" label="父级部门" prop="PidArr">
<el-cascader v-model="addForm.PidArr" :options="options" filterable placeholder="请选择,支持搜索功能"
style="width: 400px" :props="{ checkStrictly: true, expandTrigger: 'hover' }"></el-cascader>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="addFormVisible = false">取消</el-button>
<el-button type="primary" @click="addSubmit" :loading="addLoading">提交</el-button>
</template>
</el-dialog>
<!-- 编辑 -->
<el-dialog title="编辑" v-model="editFormVisible" :close-on-click-modal="false">
<el-form :model="editForm" :rules="addFormRules" ref="editFormRef" label-width="80px">
<el-form-item label="部门名称" prop="Name">
<el-input v-model="editForm.Name" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="上级关系" prop="CodeRelationship">
<el-tooltip content="以','号结尾,方便下属部门统一查询" placement="top">
<el-input v-model="editForm.CodeRelationship" disabled auto-complete="off"></el-input>
</el-tooltip>
</el-form-item>
<el-form-item label="负责人" prop="Leader">
<el-input v-model="editForm.Leader" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="排序" prop="OrderSort">
<el-input type="number" v-model="editForm.OrderSort" auto-complete="off"></el-input>
</el-form-item>
<el-form-item label="是否有效" prop="Status">
<el-switch v-model="editForm.Status"></el-switch>
</el-form-item>
<el-form-item prop="PidArr" v-if="options && options.length > 0" label="父级部门">
<el-cascader v-if="!editLoading" placeholder="请选择,支持搜索功能" style="width: 400px"
v-model="editForm.PidArr" :options="options" filterable :key="isResouceShow"
:props="{ checkStrictly: true, expandTrigger: 'hover' }"></el-cascader>
<el-cascader v-if="editLoading" placeholder="加载中..." style="width: 400px"></el-cascader>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="editFormVisible = false">取消</el-button>
<el-button type="primary" @click="editSubmit" :loading="editLoading">提交</el-button>
</div>
</template>
</el-dialog>
</section>
</template>
<script setup lang="ts" name="department">
import { ref, onMounted, onUnmounted } from 'vue';
import { ElForm, ElTable } from 'element-plus';
import Toolbar from "@/components/toolbar.vue";
import mittBusT from "@/utils/mittBusT";
import { getButtonList } from "@/utils";
import { useAuthMenuStore } from "@/stores/modules/authMenu";
import { getDepartmentListApi, type Department, type DepartmentRequest } from '@/api/departmentApi';
// 从 departmentFunctions.ts 导入
import {
handleQuery, handleAdd, handleEdit, handleDel, departments, listLoading, isResouceShow,
page, addFormVisible, options, addLoading, addFormRef, addSubmit, addForm,
currentRow, editFormVisible, editForm, editLoading, editFormRef, editSubmit
} from './departmentFunctions';
// 定义 filters
const filters = ref<{ name: string }>({ name: '' });
// 加载按钮
const buttonList = ref<Menu.MenuOptions[]>([]);
const addFormRules = {
Name: [{ required: true, message: "请输入部门名称", trigger: "blur" }],
PidArr: [{ required: true, message: "请选择父节点", trigger: "blur" }],
};
// 创建函数映射表
const functionMap: Record<string, Function> = {
handleQuery,
handleAdd,
handleEdit,
handleDel,
// 可以在此添加其他需要调用的函数
};
const callFunction = (item: Menu.MenuOptions) => {
const filters = {
name: item.search,
};
if (item.Func && typeof item.Func === 'string') {
// 假设所有可用函数都在 functionMap 中定义
const func = functionMap[item.Func];
if (typeof func === 'function') {
func(filters);
} else {
console.error(`Function ${item.Func} is not defined in functionMap.`);
}
} else {
console.error('Func property is not a valid string.');
}
};
// 实现懒加载数据功能
const load = async (tree: Department, treeNode: any, resolve: (data: Department[]) => void) => {
const para = ref<DepartmentRequest>({
page: page.value,
f: tree.Id,
key: filters.value.name,
});
try {
const { response } = await getDepartmentListApi(para.value);
resolve(response);
} catch (error) {
console.error('Error loading data:', error);
resolve([]); // 在错误情况下返回空数据以继续渲染
}
};
// 格式化时间
const formatCreateTime = (row: Department) => row.CreateTime;
const formatModifyTime = (row: Department) => row.ModifyTime;
// 定义表格数据的类型
const tableRef = ref<InstanceType<typeof ElTable> | null>(null);
// 选中当前行
const dialogCheck = async (selection: Department[], row?: Department) => {
currentRow.value = null;
if (tableRef.value) {
tableRef.value.clearSelection();
}
if (selection.length === 0) {
return;
}
if (row) {
selectCurrentRow(row);
}
};
const selectCurrentRow = (val: Department) => {
if (!val) return;
currentRow.value = val;
if (tableRef.value) {
tableRef.value.clearSelection();
tableRef.value.toggleRowSelection(val, true);
}
};
// 钩子函数
onMounted(async () => {
const authStore = useAuthMenuStore();
const routers = authStore.authMenuListGet;
buttonList.value = getButtonList(window.location.pathname, routers);
// 监听事件
mittBusT.on('callFunction', callFunction);
// 获取数据
await handleQuery(filters.value);
});
// 在组件卸载时移除监听
onUnmounted(() => {
mittBusT.off('callFunction', callFunction);
});
</script>
在业务页面进行渲染,直接用el-table的形式,目前本页面没用到分页,因为是一个树形结构,分页也很简单,就是多了一个 page 参数而已。
核心的逻辑除了表格+弹窗以外,就是动态按钮的渲染,整体看起来还是比较清晰的。
到目前为止,项目框架已经基本完成了,欢迎大家一起维护这个新项目,可以把其他页面帮忙一起写一写,过一两个月参与代码贡献的粉丝,做活动可以抽奖哟!