后台管理系统通用页面抽离=>高阶组件+配置文件+hooks

news2025/2/4 5:42:03

目录结构

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

配置文件和通用页面组件

content.config.ts

const contentConfig = {
  pageName: "role",
  header: {
    title: "角色列表",
    btnText: "新建角色"
  },
  propsList: [
    { type: "selection", label: "选择", width: "80px" },
    { type: "index", label: "序号", width: "80px" },
    { type: "normal", prop: "name", label: "角色名称", width: "180px" },
    { type: "normal", prop: "intro", label: "角色权限", width: "180px" },
    { type: "timer", prop: "createAt", label: "创建时间" },
    { type: "timer", prop: "updateAt", label: "更新时间" },
    { type: "handler", label: "操作", width: "180px" }
  ]
};

export default contentConfig;

modal.config.ts

const modalConfig = {
  pageName: "role",
  header: {
    newTitle: "新建角色",
    editTitle: "编辑角色"
  },
  formItems: [
    {
      type: "input",
      label: "角色名称",
      prop: "name",
      placeholder: "请输入角色名称"
    },
    {
      type: "input",
      label: "权限介绍",
      prop: "intro",
      placeholder: "请输入权限介绍"
    },
    {
      type: "custom",
      slotName: "menuList"
    }
  ]
};

export default modalConfig;

search.config.ts

const searchConfig = {
  formItems: [
    {
      type: 'input',
      prop: 'name',
      label: '角色名称',
      placeholder: '请输入查询的角色名称'
    },
    {
      type: 'input',
      prop: 'intro',
      label: '角色权限',
      placeholder: '请输入查询的角色权限'
    },
    {
      type: 'date-picker',
      prop: 'createAt',
      label: '创建时间'
    }
  ]
}
export default searchConfig;

role.vue

<script setup lang="ts">
import PageSearch from "@/components/page-search/page-search.vue";
import PageModal from "@/components/page-modal/page-modal.vue";
import PageContent from "@/components/page-content/page-content.vue";
import searchConfig from "@/views/main/system/role/config/search.config";
import contentConfig from "@/views/main/system/role/config/content.config";
import modalConfig from "@/views/main/system/role/config/modal.config";
import usePageContent from "@/hooks/usePageContent";
import usePageModal from "@/hooks/usePageModal";
import useSystemStore from "@/stores/modules/main/system/system";
import { ref, useTemplateRef, nextTick } from "vue";
import { ElTree } from "element-plus";
import {mapMenuListToIds} from "@/utils/mapMenus";

/**
 * 新增角色时,清空菜单列表
 */
const newCallback = () => {
  nextTick(() => {
    treeRef.value?.setCheckedKeys([]);
  })
}

/**
 * 编辑角色时,回显角色所拥有的菜单列表
 * @param itemData 当前编辑的角色信息
 */
const editCallback = (itemData: any) => {
  nextTick(() => {
    const menuIds = mapMenuListToIds(itemData.menuList)
    treeRef.value?.setCheckedKeys(menuIds);
  })
}

const { contentRef, handleQueryClick, handleResetClick } = usePageContent();
const { modalRef, handleNewClick, handleEditClick } = usePageModal(newCallback, editCallback); // editCallback 必须在 usePageModal() 方法前初始化

const systemStore = useSystemStore();
const menuList = systemStore.menuList;

const treeRef = useTemplateRef<InstanceType<typeof ElTree>>("treeRef");
const treeInfo = ref({});

/**
 * 选择某菜单节点的回调函数
 * @param node 传递给 data 属性的数组中该节点所对应的对象
 * @param checked 树目前的选中状态对象
 */
const handleElTreeCheck = (node: any, checked: any) => {
  const menuList = [...checked.checkedKeys, ...checked.halfCheckedKeys];
  treeInfo.value = { menuList };
};

</script>

<template>
  <div class="role">
    <page-search
      :searchConfig="searchConfig"
      :query-click="handleQueryClick"
      @reset-click="handleResetClick"
    />
    <page-content
      ref="contentRef"
      :content-config="contentConfig"
      @new-data-click="handleNewClick"
      @edit-data-click="handleEditClick"
    />
    <page-modal ref="modalRef" :modal-config="modalConfig" :treeInfo="treeInfo">
      <template #menuList>
        <el-tree
          ref="treeRef"
          :data="menuList"
          show-checkbox
          node-key="id"
          :props="{ children: 'children', label: 'name' }"
          @check="handleElTreeCheck"
        />
      </template>
    </page-modal>
  </div>
</template>

<style scoped></style>

高阶组件

page-search.vue

<template>
  <div class="search">
    <!-- 1.1.表单输入 -->
    <el-form :model="searchForm" ref="formRef" label-width="120px" size="large">
      <el-row :gutter="20">
        <template v-for="item in searchConfig.formItems" :key="item.prop">
          <el-col :span="8">
            <el-form-item :label="item.label" :prop="item.prop">
              <template v-if="item.type === 'input'">
                <el-input v-model="searchForm[item.prop]" :placeholder="item.placeholder" />
              </template>
              <template v-if="item.type === 'date-picker'">
                <el-date-picker
                  v-model="searchForm[item.prop]"
                  type="daterange"
                  range-separator="至"
                  start-placeholder="开始日期"
                  end-placeholder="结束日期"
                />
              </template>
            </el-form-item>
          </el-col>
        </template>
      </el-row>
    </el-form>
    <!-- 1.2.搜索按钮  -->
    <div class="btns">
      <el-button size="large" icon="Refresh" @click="handleResetClick">重置</el-button>
      <el-button size="large" icon="Search" type="primary" @click="handleQueryClick">
        查询
      </el-button>
    </div>
  </div>
</template>

<script setup lang="ts" name="page-search">
import type {ElForm} from 'element-plus'
import {reactive, ref} from 'vue'

const emit = defineEmits(['queryClick', 'resetClick'])

// 根据配置初始化表单数据
const {searchConfig} = defineProps(['searchConfig'])
const initialForm: any = {}
for (const item of searchConfig.formItems) {
  initialForm[item.prop] = ""
}
// console.log('初始化表单数据', initialForm)
// 1.创建表单的数据
const searchForm = reactive(initialForm)

// 2.监听按钮的点击
const formRef = ref<InstanceType<typeof ElForm>>()

function handleResetClick() {
  formRef.value?.resetFields()
  emit('resetClick')
}

function handleQueryClick() {
  emit('queryClick', searchForm)
}
</script>

<style scoped lang="less">
.search {
  background-color: #fff;
  padding: 20px;
  border-radius: 5px;

  .el-form-item {
    padding: 20px 40px;
    margin-bottom: 0;
  }
}

.btns {
  text-align: right;
  padding: 0 50px 10px 0;
}
</style>

page-content.vue

  1. header
  2. propList
    1. 插槽(定制化)=> 作用域插槽
  3. pageName
<template>
  <div class="content">
    <div class="header">
      <h3 class="title">{{ contentConfig?.header?.title ?? "数据列表" }}</h3>
      <el-button v-if="isCreate" type="primary" @click="handleNewData">{{
        contentConfig?.header?.btnText ?? "新建数据"
      }}</el-button>
    </div>
    <div class="table">
      <el-table
        :data="pageList"
        :border="true"
        :row-key="contentConfig?.childrenTree?.rowKey"
        style="width: 100%"
      >
        <template v-for="item in contentConfig.propsList" :key="item.prop">
          <!-- <el-table-column align="center" :label="item.label" :prop="item.prop" :width="item.width ?? '150px'"></el-table-column>-->
          <el-table-column
            v-if="item.type === 'index' || item.type === 'selection'"
            align="center"
            v-bind="item"
          />
          <el-table-column v-else-if="item.type === 'custom'" align="center" v-bind="item">
            <template #default="scope">
              <slot :name="item.slotName" v-bind="scope" :prop="item.prop" :leaderRange="10" />
            </template>
          </el-table-column>
          <el-table-column v-else align="center" v-bind="item">
            <template #default="scope">
              <span v-if="item.type === 'timer'">{{ formatUTC(scope.row[item.prop]) }}</span>
              <span v-else-if="item.type === 'handler'">
                <el-button
                  v-if="isUpdate"
                  type="primary"
                  size="small"
                  icon="EditPen"
                  link
                  @click="handleEditClick(scope.row)"
                >
                  编辑
                </el-button>
                <el-button
                  v-if="isDelete"
                  type="danger"
                  size="small"
                  icon="Delete"
                  link
                  @click="handleDeleteClick(scope.row.id)"
                >
                  删除
                </el-button>
              </span>
              <span v-else>{{ scope.row[item.prop] }}</span>
            </template>
          </el-table-column>
        </template>
      </el-table>
    </div>
    <div class="footer">
      <el-pagination
        v-model:currentPage="currentPage"
        v-model:pageSize="pageSize"
        :page-sizes="[10, 20, 30]"
        layout="total, sizes, prev, pager, next, jumper"
        :total="pageTotalCount"
        @update:currentPage="handleCurrentChange"
        @update:pageSize="handlePageSizeChange"
      />
    </div>
  </div>
</template>

<script setup lang="ts" name="content">
import { storeToRefs } from "pinia";
import { ref } from "vue";
import useSystemStore from "@/stores/modules/main/system/system";
import { formatUTC } from "@/utils/format";
import usePermission from "@/hooks/usePermission";

const { contentConfig } = defineProps(["contentConfig"]);
const emit = defineEmits(["newDataClick", "editDataClick"]);

// 0.判断是否有增删改查的权限
const isCreate = usePermission(contentConfig.pageName, "create");
const isDelete = usePermission(contentConfig.pageName, "delete");
const isUpdate = usePermission(contentConfig.pageName, "update");
const isQuery = usePermission(contentConfig.pageName, "query");

// 1.请求数据
const systemStore = useSystemStore();
const currentPage = ref(1);
const pageSize = ref(10);

systemStore.$onAction(({ name, after }) => {
  after(() => {
    if (
        name === "deletePageByIdAction" ||
        name === "editPageDataAction" ||
        name === "newPageDataAction"
    ) {
      currentPage.value = 1;
    }
  })
});

function fetchPageListData(queryInfo: any = {}) {
  // 0.判断是否具有查询权限
  if (!isQuery) return;
  // 1.获取offset和size
  const size = pageSize.value;
  const offset = (currentPage.value - 1) * size;

  // 2.发生网络请求
  systemStore.postPageListAction(contentConfig.pageName, { offset, size, ...queryInfo });
}

fetchPageListData();

// 2.展示数据
const { pageList, pageTotalCount } = storeToRefs(systemStore);

// 3.绑定分页数据
function handleCurrentChange() {
  fetchPageListData();
}

function handlePageSizeChange(newPageSize: number) {
  pageSize.value = newPageSize;
  fetchPageListData();
}

function handleResetClick() {
  currentPage.value = 1;
  pageSize.value = 10;
  fetchPageListData();
}

// 4.新建数据的处理
function handleNewData() {
  emit("newDataClick");
}

// 5.删除和编辑操作
function handleDeleteClick(id: number) {
  systemStore.deletePageByIdAction(contentConfig.pageName, id);
}

function handleEditClick(data: any) {
  emit("editDataClick", data);
}

// 暴露函数
defineExpose({
  fetchPageListData,
  handleResetClick
});
</script>

<style scoped lang="less">
.content {
  margin-top: 20px;
  padding: 20px;
  background-color: #fff;

  .header {
    display: flex;
    height: 45px;
    padding: 0 5px;
    justify-content: space-between;
    align-items: center;

    .title {
      font-size: 20px;
      font-weight: 700;
    }

    .handler {
      align-items: center;
    }
  }

  .table {
    :deep(.el-table__cell) {
      padding: 14px 0;
    }
  }

  .footer {
    display: flex;
    justify-content: flex-end;
    margin-top: 15px;
  }
}
</style>

page-modal.vue

  1. header
    1. newTitle
    2. editTitle
  2. pageName
  3. formItems
<template>
  <div class="modal">
    <el-dialog v-model="dialogVisible" :title="modalConfig.header.newTitle" width="30%" center>
      <div class="form">
        <el-form :model="formData" label-width="80px" size="large">
          <template v-for="item in modalConfig.formItems" :key="item.prop">
            <el-form-item :label="item.label" :prop="item.prop">
              <template v-if="item.type === 'input'">
                <el-input v-model="formData[item.prop]" :placeholder="item.placeholder" />
              </template>
              <template v-if="item.type === 'password'">
                <el-input
                  show-password
                  v-model="formData[item.prop]"
                  :placeholder="item.placeholder"
                />
              </template>
              <template v-if="item.type === 'select'">
                <el-select
                  v-model="formData.parentId"
                  :placeholder="item.placeholder"
                  style="width: 100%"
                >
                  <template v-for="value in item.options" :key="value.value">
                    <el-option :value="value.value" :label="value.label" />
                  </template>
                </el-select>
              </template>
              <template v-if="item.type === 'date-picker'">
                <el-date-picker
                  type="daterange"
                  range-separator="-"
                  start-placeholder="开始时间"
                  end-placeholder="结束时间"
                  v-model="formData[item.prop]"
                />
              </template>
              <template v-if="item.type === 'custom'">
                <slot :name="item.slotName"></slot>
              </template>
            </el-form-item>
          </template>
        </el-form>
      </div>
      <template #footer>
        <span class="dialog-footer">
          <el-button @click="dialogVisible = false">取消</el-button>
          <el-button type="primary" @click="handleConfirmClick">确定</el-button>
        </span>
      </template>
    </el-dialog>
  </div>
</template>

<script setup lang="ts" name="modal">
import { storeToRefs } from "pinia";
import { reactive, ref } from "vue";
import useSystemStore from "@/stores/modules/main/system/system";

const dialogVisible = ref(false);
const isEdit = ref(false);
const editData = ref();

// 定义数据绑定
const formData = reactive<any>({
  name: "",
  leader: "",
  parentId: ""
});

const { modalConfig, treeInfo } = defineProps(["modalConfig", "treeInfo"]);
// 点击确定
const systemStore = useSystemStore();
const { departmentList } = storeToRefs(systemStore);

const initialData: any = {};
for (const item of modalConfig?.formItems) {
  initialData[item.prop] = item.initialValue ?? "";
}

function handleConfirmClick() {
  dialogVisible.value = false;
  // 判断是否存在含树形菜单权限的formData
  let treeFormData = { ...formData };
  if (treeInfo) {
    treeFormData = {
      ...treeFormData,
      ...treeInfo
    };
  }
  console.log(treeFormData);
  if (!isEdit.value) {
    systemStore.newPageDataAction(modalConfig.pageName, treeFormData);
  } else {
    systemStore.editPageDataAction(modalConfig.pageName, editData.value.id, treeFormData);
  }
}

// 新建或者编辑
function setDialogVisible(isNew: boolean = true, data: any = {}) {
  dialogVisible.value = true;
  isEdit.value = !isNew;
  editData.value = data;
  for (const key in formData) {
    if (isNew) {
      formData[key] = "";
    } else {
      formData[key] = data[key];
    }
  }
}

defineExpose({
  setDialogVisible
});
</script>

<style scoped lang="less">
.form {
  padding: 10px 30px;
}
</style>

hooks

usePageContent.ts

import { useTemplateRef } from "vue";
import PageContent from "@/components/page-content/page-content.vue";

function usePageContent() {
  const contentRef = useTemplateRef<InstanceType<typeof PageContent>>("contentRef");

  const handleQueryClick = (queryInfo: any) => {
    contentRef.value?.fetchPageListData(queryInfo);
  };
  const handleResetClick = () => {
    contentRef.value?.fetchPageListData();
  };

  return {
    contentRef,
    handleQueryClick,
    handleResetClick
  };
}

export default usePageContent;

usePageModal.ts

import { useTemplateRef } from "vue";
import PageModal from "@/components/page-modal/page-modal.vue";

function usePageModal(newCallback?: () => void, editCallback?: (itemData: any) => void) {
  const modalRef = useTemplateRef<InstanceType<typeof PageModal>>("modalRef");

  const handleNewClick = () => {
    modalRef.value?.setDialogVisible(true);
    if (newCallback) newCallback();
  };
  const handleEditClick = (itemData: any) => {
    modalRef.value?.setDialogVisible(false, itemData);
    if (editCallback) editCallback(itemData)
  };

  return {
    modalRef,
    handleNewClick,
    handleEditClick
  };
}

export default usePageModal;

最终呈现

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2291629.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring Boot项目如何使用MyBatis实现分页查询

写在前面&#xff1a;大家好&#xff01;我是晴空๓。如果博客中有不足或者的错误的地方欢迎在评论区或者私信我指正&#xff0c;感谢大家的不吝赐教。我的唯一博客更新地址是&#xff1a;https://ac-fun.blog.csdn.net/。非常感谢大家的支持。一起加油&#xff0c;冲鸭&#x…

Intellij 插件开发-快速开始

目录 一、开发环境搭建以及创建action1. 安装 Plugin DevKit 插件2. 新建idea插件项目3. 创建 Action4. 向新的 Action 表单注册 Action5. Enabling Internal Mode 二、插件实战开发[不推荐]UI Designer 基础JBPanel类&#xff08;JPanel面板&#xff09;需求&#xff1a;插件设…

语言月赛 202412【题目名没活了】题解(AC)

》》》点我查看「视频」详解》》》 [语言月赛 202412] 题目名没活了 题目描述 在 XCPC 竞赛里&#xff0c;会有若干道题目&#xff0c;一支队伍可以对每道题目提交若干次。我们称一支队伍对一道题目的一次提交是有效的&#xff0c;当且仅当&#xff1a; 在本次提交以前&…

MySQL锁类型(详解)

锁的分类图&#xff0c;如下&#xff1a; 锁操作类型划分 读锁 : 也称为共享锁 、英文用S表示。针对同一份数据&#xff0c;多个事务的读操作可以同时进行而不会互相影响&#xff0c;相互不阻塞的。 写锁 : 也称为排他锁 、英文用X表示。当前写操作没有完成前&#xff0c;它会…

OSCP - Proving Grounds - Roquefort

主要知识点 githook 注入Linux path覆盖 具体步骤 依旧是nmap扫描开始&#xff0c;3000端口不是很熟悉&#xff0c;先看一下 Nmap scan report for 192.168.54.67 Host is up (0.00083s latency). Not shown: 65530 filtered tcp ports (no-response) PORT STATE SERV…

集合通讯概览

&#xff08;1&#xff09;通信的算法 是根据通讯的链路组成的 &#xff08;2&#xff09;因为通信链路 跟硬件强相关&#xff0c;所以每个CCL的库都不一样 芯片与芯片、不同U之间是怎么通信的&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01; 很重要…

【贪心算法篇】:“贪心”之旅--算法练习题中的智慧与策略(二)

✨感谢您阅读本篇文章&#xff0c;文章内容是个人学习笔记的整理&#xff0c;如果哪里有误的话还请您指正噢✨ ✨ 个人主页&#xff1a;余辉zmh–CSDN博客 ✨ 文章所属专栏&#xff1a;贪心算法篇–CSDN博客 文章目录 前言例题1.买卖股票的最佳时机2.买卖股票的最佳时机23.k次取…

oracle: 表分区>>范围分区,列表分区,散列分区/哈希分区,间隔分区,参考分区,组合分区,子分区/复合分区/组合分区

分区表 是将一个逻辑上的大表按照特定的规则划分为多个物理上的子表&#xff0c;这些子表称为分区。 分区可以基于不同的维度&#xff0c;如时间、数值范围、字符串值等&#xff0c;将数据分散存储在不同的分区 中&#xff0c;以提高数据管理的效率和查询性能&#xff0c;同时…

基于SpringBoot 前端接收中文显示解决方案

一. 问题 返回给前端的的中文值会变成“???” 二. 解决方案 1. 在application.yml修改字符编码 &#xff08;无效&#xff09; 在网上看到说修改servlet字符集编码&#xff0c;尝试了不行 server:port: 8083servlet:encoding:charset: UTF-8enabled: trueforce: true2. …

java练习(5)

ps:题目来自力扣 给你两个 非空 的链表&#xff0c;表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的&#xff0c;并且每个节点只能存储 一位 数字。 请你将两个数相加&#xff0c;并以相同形式返回一个表示和的链表。 你可以假设除了数字 0 之外&#xff0c;这…

python算法和数据结构刷题[3]:哈希表、滑动窗口、双指针、回溯算法、贪心算法

回溯算法 「所有可能的结果」&#xff0c;而不是「结果的个数」&#xff0c;一般情况下&#xff0c;我们就知道需要暴力搜索所有的可行解了&#xff0c;可以用「回溯法」。 回溯算法关键在于:不合适就退回上一步。在回溯算法中&#xff0c;递归用于深入到所有可能的分支&…

大数据数仓实战项目(离线数仓+实时数仓)1

目录 1.课程目标 2.电商行业与电商系统介绍 3.数仓项目整体技术架构介绍 4.数仓项目架构-kylin补充 5.数仓具体技术介绍与项目环境介绍 6.kettle的介绍与安装 7.kettle入门案例 8.kettle输入组件之JSON输入与表输入 9.kettle输入组件之生成记录组件 10.kettle输出组件…

【开源免费】基于Vue和SpringBoot的公寓报修管理系统(附论文)

本文项目编号 T 186 &#xff0c;文末自助获取源码 \color{red}{T186&#xff0c;文末自助获取源码} T186&#xff0c;文末自助获取源码 目录 一、系统介绍二、数据库设计三、配套教程3.1 启动教程3.2 讲解视频3.3 二次开发教程 四、功能截图五、文案资料5.1 选题背景5.2 国内…

【Windows7和Windows10下从零搭建Qt+Leaflet开发环境】

Windows7和Windows10下从零搭建QtLeaflet开发环境 本文开始编写于2025年1月27日星期一&#xff08;农历&#xff1a;腊月二十八&#xff0c;苦逼的人&#xff0c;过年了还在忙工作&#xff09;。 第一章 概述 整个开发环境搭建需要的资源&#xff1a; 操作系统 Windows7_x6…

关于MySQL InnoDB存储引擎的一些认识

文章目录 一、存储引擎1.MySQL中执行一条SQL语句的过程是怎样的&#xff1f;1.1 MySQL的存储引擎有哪些&#xff1f;1.2 MyIsam和InnoDB有什么区别&#xff1f; 2.MySQL表的结构是什么&#xff1f;2.1 行结构是什么样呢&#xff1f;2.1.1 NULL列表&#xff1f;2.1.2 char和varc…

WSL2中安装的ubuntu开启与关闭探讨

1. PC开机后&#xff0c;查询wsl状态 在cmd或者powersell中输入 wsl -l -vNAME STATE VERSION * Ubuntu Stopped 22. 从windows访问WSL2 wsl -l -vNAME STATE VERSION * Ubuntu Stopped 23. 在ubuntu中打开一个工作区后…

π0:仅有3B数据模型打通Franka等7种机器人形态适配,实现0样本的完全由模型自主控制方法

Chelsea Finn引领的Physical Intelligence公司&#xff0c;专注于打造先进的机器人大模型&#xff0c;近日迎来了一个令人振奋的里程碑。在短短不到一年的时间内&#xff0c;该公司成功推出了他们的首个演示版本。这一成就不仅展示了团队的卓越技术实力&#xff0c;也预示着机器…

pandas(二)读取数据

一、读取数据 示例代码 import pandaspeople pandas.read_excel(../002/People.xlsx) #读取People数据 print(people.shape) # 打印people表的行数、列数 print(people.head(3)) # 默认打印前5行,当前打印前3行 print("") print(people.tail(3)) # 默…

向上调整算法(详解)c++

算法流程&#xff1a; 与⽗结点的权值作⽐较&#xff0c;如果⽐它⼤&#xff0c;就与⽗亲交换&#xff1b; 交换完之后&#xff0c;重复 1 操作&#xff0c;直到⽐⽗亲⼩&#xff0c;或者换到根节点的位置 这里为什么插入85完后合法&#xff1f; 我们插入一个85&#xff0c;…

LabVIEW无线齿轮监测系统

本案例介绍了基于LabVIEW的无线齿轮监测系统设计。该系统利用LabVIEW编程语言和改进的天牛须算法优化支持向量机&#xff0c;实现了无线齿轮故障监测。通过LabVIEW软件和相关硬件&#xff0c;可以实现对齿轮箱振动信号的采集、传输和故障识别&#xff0c;集远程采集、数据库存储…