周末学习不停歇,最近新开一个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四种谓词方式。
参考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>,这种的进行渲染,不仅有很好的可读性,代码更健壮和规范。
在当前页面中,可以把vue代码和ts的脚本代码合理的拆开,定义一个function.ts来将所有的事件封装,这样不仅有可读性,更重要的就是要配合按钮权限来使用。
在srcviewsDepartmentdepartmentFunctions.ts文件中
// departmentFunctions.tsimport { 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还是差不多的,主要就是在变量的定义上,需要有一定的调整,写习惯了就好了。
页面渲染包括顶部的操作按钮工具条、表格和新增编辑弹窗的渲染,以及数据的控制。
页面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 = refconst 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 参数而已。

核心的逻辑除了表格+弹窗以外,就是动态按钮的渲染,整体看起来还是比较清晰的。
到目前为止,项目框架已经基本完成了,欢迎大家一起维护这个新项目,可以把其他页面帮忙一起写一写,过一两个月参与代码贡献的粉丝,做活动可以抽奖哟!