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

艺帆风顺 发布于 2025-04-03 29 次阅读


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四种谓词方式。

参考srcapidepartmentApi.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> => { try { const response = await get>('/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> => { try { const response = await get>('/api/department/getDepartmentTree', { pid: pid }); return response; } catch (error) { throw new Error('请求失败'); }};
    // 新增部门数据export const addDepartment = async (params: Department): Promisestring>> => { try { const response = await poststring>>('/api/department/post', params); return response; } catch (error) { throw new Error('请求失败'); }};
    // 编辑部门数据export const editDepartment = async (params: Department): Promisestring>> => { try { const response = await putstring>>('/api/department/put', params); return response; } catch (error) { throw new Error('请求失败'); }};
    // 删除部门数据export const removeDepartment = async (id: string): Promisestring>> => { try { const response = await delstring>>('/api/department/delete', { id: id }); return response; } catch (error) { throw new Error('请求失败'); }};

    根据需要,尽量定义丰富的interface类,比如多种泛型,甚至可以和后端保持一致,比如分页模式下,可以使用Base>,这种的进行渲染,不仅有很好的可读性,代码更健壮和规范。

    2、页面内的function事件

    在当前页面中,可以把vue代码和ts的脚本代码合理的拆开,定义一个function.ts来将所有的事件封装,这样不仅有可读性,更重要的就是要配合按钮权限来使用。

    srcviewsDepartmentdepartmentFunctions.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([]);export const listLoading = ref(false);export const page = ref(1);
      export const options = ref([]);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 = reftypeof ElForm> | null>(null);export const editFormRef = reftypeof ElForm> | null>(null);export const currentRow = refnull>(null);
      // ↓↓↓↓↓ 查询 ↓↓↓↓↓export const handleQuery = async (filters: { name: string }) => { currentRow.value = null;
      const para = ref({ 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({ 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({ 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、业务页面的渲染

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

      页面srcviewsDepartmentDepartment.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 = refname: string }>({ name: '' });// 加载按钮const buttonList = ref([]);

        const addFormRules = { Name: [{ required: true, message: "请输入部门名称", trigger: "blur" }], PidArr: [{ required: true, message: "请选择父节点", trigger: "blur" }],};
        // 创建函数映射表const functionMap: RecordFunction> = { 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({ 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 = reftypeof 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 参数而已。

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

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

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