BCVP.VUE3系列第十三课:框架底座已写完,欢迎加入我们!

科技   科技   2024-10-14 07:17   北京  
BCVP 开发者社区出品

BCVP V3开发

数字化
服务化
绿色化



周末学习不停歇,最近新开一个VUE3全新系列,这一系列会从0开始学习VUE3,使用Vite、TS、Pinia、Element-Plus、mittBus等新知识点,既是查漏补缺,也是知识分享。

目前项目的登录、鉴权、动态菜单、权限按钮、页面布局、标签页、数据增删改查案例等基本功能都已经写完,整体效果如动图,欢迎各位小伙伴可以加入到这个项目,可以提交PR,早期参与贡献的,可以作为核心成员。不仅可以锻炼自己的VUE3的功底,也能和社区其他伙伴讨论,如果有需要讨论,可以创建一个群二维码。

代码地址:

https://github.com/anjoy8/bcvp.vue3.git

这是每篇文章一节课一个分支,方便大家学习,会慢慢的将blog.admin项目进行翻新,使用的后端接口还是BlogCore,将动态权限、动态菜单和动态按钮通过vue3+ts的方式完美升级。

系列文章:

第一课:项目初始化与核心知识点说明

第二课:基于泛型基类封装Axios请求

第三课:封装Axios拦截器

第四课:登录页设计

第五课:获取用户信息

第六课:获取动态菜单接口

第七课:基于布局模式实现动态菜单渲染

第八课:丰富面包屑组件

第九课:实现tabs标签栏

第十课:个人中心模块

第十一课:基于总线实现框架多种布局样式

第十二课:渲染动态权限按钮



0、本文介绍



本文参考的是开源项目

https://gitee.com/HalseySpicy/Geeker-Admin/tree/template

今天主要是完成了部门管理的新增、编辑和删除的标准写法,当然可能不是最简洁的,欢迎投稿:



1、部门数据的API对接


在每一个页面中,都需要定义一个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>>,这种的进行渲染,不仅有很好的可读性,代码更健壮和规范。


2、页面内的function事件


在当前页面中,可以把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-formexport 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数据并指定其类型为Departmentexport 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数据并指定其类型为Departmentexport 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还是差不多的,主要就是在变量的定义上,需要有一定的调整,写习惯了就好了。




3、业务页面的渲染



页面渲染包括顶部的操作按钮工具条、表格和新增编辑弹窗的渲染,以及数据的控制。

页面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';// 定义 filtersconst 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 参数而已

基本写一个页面,就是上边这三个文件就可以搞定全部业务逻辑,也是比较标准化操作。

核心的逻辑除了表格+弹窗以外,就是动态按钮的渲染,整体看起来还是比较清晰的。


到目前为止,项目框架已经基本完成了,欢迎大家一起维护这个新项目,可以把其他页面帮忙一起写一写,过一两个月参与代码贡献的粉丝,做活动可以抽奖哟!

BCVP代码创新社
专注于 NetCore 相关技术栈的推广,致力于前后端之间的完全分离,从壹开始,让每一个程序员都能从这里学有所成。
 最新文章