谷粒商城实战笔记-56~57-商品服务-API-三级分类-修改-拖拽功能完成

news2024/12/23 13:00:25

文章目录

  • 一,56-商品服务-API-三级分类-修改-拖拽功能完成
  • 二,57-商品服务-API-三级分类-修改-批量拖拽效果
    • 1,增加按钮
    • 2,多次拖拽一次保存
    • 完整代码

在构建商品服务API中的三级分类修改功能时,拖拽排序是一个直观且高效的管理方式。通过允许用户直接在界面上调整分类的顺序,可以显著提升用户体验和操作效率。在实现这一功能的过程中,我们不仅关注了单个分类的即时更新,还考虑到了多级分类同时变化的情况,以及如何批量处理这些变化。

在上一节中,我们已经完成了拖拽功能的基础部分,包括对分类顺序、级别和父节点的更新逻辑。现在,我们将进一步优化该功能,以提供更完善的用户体验。

首先,增加了一个可控制的开关按钮,用于启动或禁用拖拽功能。这不仅增强了系统的灵活性,也避免了在不需要进行分类调整时可能出现的误操作。用户可以根据实际需求选择是否启用拖拽模式,这为操作者提供了更大的自主权。

其次,为了提高效率,我们引入了“批量保存拖拽”按钮。当用户进行了多次拖拽操作后,不必逐一保存每个分类的变动,而是可以通过点击这个按钮一次性提交所有更改。这样不仅简化了工作流程,还减少了网络请求次数,提高了系统响应速度。

在实现批量保存功能时,我们使用了Vue.js框架中的$http方法来发起POST请求,将包含所有变动信息的数组nodesInfoAfterDrop发送给后端。后端接收到请求后,利用updateBatchById方法批量更新数据库中的分类信息。如果更新成功,前端会显示成功的消息,并自动重新加载分类列表,确保界面与数据库状态保持一致。

此外,为了保证数据的准确性,在批量保存后,我们还重置了一些关键变量,如expandedKeys和draggingNodeNewPId,确保下一次操作时从一个干净的状态开始。这些细节上的考虑,使得整个拖拽和保存流程更加健壮和可靠。

综上所述,通过增加控制开关和批量保存功能,我们的商品服务API中的三级分类修改功能得到了显著增强,不仅提升了用户体验,也提高了管理效率,是系统功能完善的重要一步。

一,56-商品服务-API-三级分类-修改-拖拽功能完成

上一节已经把需要更新的数据都封装好了,三种结点的数据需要更新。

  • ① 顺序发生变化,需要更新sort值

在这里插入图片描述

  • ② 分类级别发生变化,需要更新catLevel

在这里插入图片描述

  • ③ 父节点发生变化,需要更新parentCid

在这里插入图片描述

后端接口代码,根据cid批量更新。

    @RequestMapping("/update/sort")
    public R update(@RequestBody CategoryEntity[] categorys){
        categoryService.updateBatchById(Arrays.asList(categorys));

        return R.ok();
    }

二,57-商品服务-API-三级分类-修改-批量拖拽效果

基于上一节,做两个优化。

1,增加按钮

增加一个开关,控制拖拽功能的开启,只有开启了拖拽功能,才能拖拽。

在这里插入图片描述

2,多次拖拽一次保存

增加一个按钮用来批量保存拖拽后的节点信息。

 <el-button @click="batchSaveDrag" v-if="draggable"
        >批量保存拖拽</el-button
      >

并绑定一个click事件。

batchSaveDrag() {
      // 4,调用后端接口,将数组nodesInfoAfterDrop发送给后端
      this.$http({
        url: this.$http.adornUrl("/product/category/update/sort"),
        method: "post",
        data: this.$http.adornData(this.nodesInfoAfterDrop, false),
      }).then(({ data }) => {
        if (data && data.code === 0) {
          this.$message({
            message: "操作成功",
            type: "success",
            duration: 1500,
            onClose: () => {
              console.log("删除成功,关闭消息提示");
              this.getMenus(); // 重新获取数据
              this.expandedKeys = [this.draggingNodeNewPId]; // 重置展开节点
              this.draggingNodeNewPId = 0; // 重置����的新parentCid
              this.nodesInfoAfterDrop = [];

            },
          });
        } else {
          this.$message.error(data.msg);
        }
      });
    },

完整代码

<template>
  <div>
    <el-switch
      style="display: block"
      v-model="draggable"
      active-color="#13ce66"
      inactive-color="#ff4949"
      active-text="开启拖拽"
      inactive-text="关闭拖拽"
    >
     
    </el-switch>

     <el-button @click="batchSaveDrag" v-if="draggable"
        >批量保存拖拽</el-button
      >

    <el-tree
      node-key="catId"
      :data="menus"
      :props="defaultProps"
      :expand-on-click-node="false"
      show-checkbox
      :default-expanded-keys="expandedKeys"
      :allow-drop="allowDrag"
      @node-drop="nodeDrop"
      :draggable="draggable"
    >
      <span class="custom-tree-node" slot-scope="{ node, data }">
        <span>{{ node.label }}</span>
        <span>
          <el-button
            v-if="node.level <= 2"
            size="mini"
            @click="() => append(data)"
          >
            Append
          </el-button>
          <el-button size="mini" @click="() => edit(data)"> Edit </el-button>
          <el-button
            v-if="node.childNodes.length == 0"
            type="text"
            size="mini"
            @click="() => remove(node, data)"
          >
            Delete
          </el-button>
        </span>
      </span>
    </el-tree>

    <el-dialog
      :title="dialogTitle"
      :visible.sync="dialogFormVisible"
      :close-on-click-modal="false"
    >
      <el-form :model="category">
        <el-form-item label="分类名称">
          <el-input v-model="category.name" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="图标">
          <el-input v-model="category.icon" autocomplete="off"></el-input>
        </el-form-item>
        <el-form-item label="计量单位">
          <el-input
            v-model="category.productUnit"
            autocomplete="off"
          ></el-input>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button @click="dialogFormVisible = false">取 消</el-button>
        <el-button type="primary" @click="submitCategory">确 定</el-button>
      </div>
    </el-dialog>
  </div>
</template>

<script>
export default {
  components: {},
  props: {},
  data() {
    return {
      draggingNodeNewPId: 0,
      draggable: false,
      nodesInfoAfterDrop: [],
      dialogTitle: "", // 编辑窗口标题,新增分类,修改分类
      dialogType: "", // 编辑窗口类型,create表示append,edit表示edit
      dialogFormVisible: false,
      menus: [],
      category: {
        name: "",
        parentCid: 0,
        catLevel: 0,
        sort: 0,
        showStatus: 1,
        icon: "",
        productUnit: "",
        catId: null,
      },
      expandedKeys: [],
      defaultProps: {
        children: "children",
        label: "name",
      },
    };
  },

  methods: {
    nodeDrop(draggingNode, dropNode, dropPosition) {
      console.log("draggingNode:", draggingNode, dropNode, dropPosition);
      // 1,更新draggingNode的parentCid,根据dropPosition的不同值,分为两种情况
      if (dropPosition === "before" || dropPosition === "after") {
        this.draggingNodeNewPId = dropNode.data.parentCid;
      } else {
        this.draggingNodeNewPId = dropNode.data.cid;
      }

      console.log("draggingNodeNewPId:", this.draggingNodeNewPId, dropNode.data.parentCid, dropNode.data.catId);

      // 2,更新draggingNode及其子节点的sort
      // 2.1如果draggingNode的parentCid没有更新,只需要重排draggingNode的兄弟结点的sort;
      // 2.2如果draggingNode的parentCid有更新,不仅需要重排draggingNode的原来的兄弟结点的sort,还需要重排draggingNode新的兄弟结点的sort
      var siblingNodesOld = [];
      var siblingNodesNew = [];
      // draggingNode.parent为空,所以要根据parentCid找到其parent。
      // 写一个根据cid从menus中查询结点的函数
      let draggingNodeOldParentNode = this.getNodeByCid(
        draggingNode.data.parentCid,
        this.getLevel1Nodes(dropNode)
      );

      console.log("draggingNodeOldParentNode:", draggingNodeOldParentNode);
      siblingNodesOld = draggingNodeOldParentNode.childNodes;
      console.log("siblingNodesOld:", siblingNodesOld);

      if (draggingNode.data.parentCid !== this.draggingNodeNewPId) {
        if (dropPosition === "before" || dropPosition === "after") {
          siblingNodesNew = dropNode.parent.childNodes;
        } else {
          siblingNodesNew = dropNode.childNodes;
        }
      }

      for (var i = 0; i < siblingNodesOld.length; i++) {
        this.nodesInfoAfterDrop.push({
          catId: siblingNodesOld[i].data.catId,
          sort: i,
        });
      }

      console.log(
        "update sortu0....",
        siblingNodesNew,
        siblingNodesOld,
        dropNode,
        dropPosition
      );

      for (var i = 0; i < siblingNodesNew.length; i++) {
        this.nodesInfoAfterDrop.push({
          catId: siblingNodesNew[i].data.catId,
          sort: i,
        });
      }

      console.log(
        "update sort....",
        siblingNodesNew,
        siblingNodesOld,
        dropNode,
        dropPosition
      );

      // 3,更新draggingNode及其子节点的catLevel
      var catLevelNeedUpdate = true;
      if (
        draggingNode.data.catLevel === dropNode.data.catLevel &&
        (dropPosition === "before" || dropPosition === "after")
      ) {
        catLevelNeedUpdate = false;
      }

      var draggingNodeAfterDrop = null;
      if (catLevelNeedUpdate) {
        var draggingParentNodeAfterDrop = {};
        if (dropPosition === "before" || dropPosition === "after") {
          draggingParentNodeAfterDrop = dropNode.parent;
        } else {
          draggingParentNodeAfterDrop = dropNode;
        }

        draggingParentNodeAfterDrop.childNodes.forEach((child) => {
          if (child.data.catId === draggingNode.data.catId) {
            draggingNodeAfterDrop = child;
          }
        });

        // 递归变量draggingNodeAfterDrop及其childNodes,将其data属性的cateLevel置为level属性值
        this.updateCatLevel(draggingNodeAfterDrop);
      }

      console.log("draggingNodeNewPId", this.draggingNodeNewPId)
      this.nodesInfoAfterDrop.push({
        catId: draggingNode.data.catId,
        parentCid: this.draggingNodeNewPId,
      });

      console.log(this.nodesInfoAfterDrop);
    },

    batchSaveDrag() {
      // 4,调用后端接口,将数组nodesInfoAfterDrop发送给后端
      this.$http({
        url: this.$http.adornUrl("/product/category/update/sort"),
        method: "post",
        data: this.$http.adornData(this.nodesInfoAfterDrop, false),
      }).then(({ data }) => {
        if (data && data.code === 0) {
          this.$message({
            message: "操作成功",
            type: "success",
            duration: 1500,
            onClose: () => {
              console.log("删除成功,关闭消息提示");
              this.getMenus(); // 重新获取数据
              this.expandedKeys = [this.draggingNodeNewPId]; // 重置展开节点
              this.draggingNodeNewPId = 0; // 重置����的新parentCid
              this.nodesInfoAfterDrop = [];

            },
          });
        } else {
          this.$message.error(data.msg);
        }
      });
    },

    getLevel1Nodes(node) {
      var tmpNode = node;
      while (tmpNode.parent) {
        tmpNode = tmpNode.parent;
      }

      return tmpNode.childNodes;
    },

    // 写一个根据cid从menus中查询结点的函数
    getNodeByCid(cid, nodes) {
      if (cid === 0) {
        return nodes[0].parent;
      }

      // 递归查询
      for (var i = 0; i < nodes.length; i++) {
        if (nodes[i].data.catId === cid) {
          return nodes[i];
        }

        if (nodes[i].childNodes && nodes[i].childNodes.length > 0) {
          var node = this.getNodeByCid(cid, nodes[i].childNodes);
          if (node) {
            return node;
          }
        }
      }
      return null;
    },

    // 递归更新draggingNode及其子节点的catLevel
    updateCatLevel(node) {
      if (!node) {
        return;
      }

      this.nodesInfoAfterDrop.push({
        catId: node.data.catId,
        catLevel: node.level,
      });

      if (node.childNodes && node.childNodes.length > 0) {
        node.childNodes.forEach((child) => {
          this.updateCatLevel(child);
        });
      }
    },

    allowDrag(draggingNode, dropNode, dropPosition) {
      var deep = this.countDraggingNodeDeep(draggingNode);

      // 根据dropPosition结合dropNode.catLevel来判断draggingNode新位置的位置是否合法
      if (dropPosition === "prev" || dropPosition === "next") {
        return dropNode.data.catLevel + deep - 1 <= 3;
      } else if (dropPosition === "inner") {
        return dropNode.data.catLevel + deep <= 3;
      }
    },

    // 递归计算draggingNode子树的深度
    countDraggingNodeDeep(draggingNode) {
      var deep = 0;
      if (draggingNode.childNodes && draggingNode.childNodes.length > 0) {
        debugger;
        draggingNode.childNodes.forEach((child) => {
          deep = Math.max(deep, this.countDraggingNodeDeep(child));
        });
      }
      return deep + 1;
    },
    append(data) {
      console.log(data);
      this.dialogType = "create";
      this.dialogTitle = "新增分类";
      this.dialogFormVisible = true;
      this.category = {
        name: "",
        parentCid: data.catId,
        catLevel: data.level + 1,
        sort: 0,
        showStatus: 1,
      };
    },
    edit(data) {
      console.log(data);
      this.dialogType = "edit";
      this.dialogTitle = "修改分类";
      this.dialogFormVisible = true;

      // 根据catId查询最新数据
      this.$http({
        url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
        method: "get",
        data: this.$http.adornData({ catId: data.catId }, false),
      }).then(({ data }) => {
        if (data && data.code === 0) {
          this.category = { ...data.data };
        } else {
          this.$message.error(data.msg);
        }
      });
    },
    submitCategory() {
      if (this.dialogType === "create") {
        this.addCategory();
      } else if (this.dialogType === "edit") {
        this.updateCategory();
      }
    },
    updateCategory() {
      var { catId, name, icon, productUnit } = this.category;
      console.log(this.category);
      this.$http({
        url: this.$http.adornUrl("/product/category/update"),
        method: "post",
        data: this.$http.adornData({ catId, name, icon, productUnit }, false),
      }).then(({ data }) => {
        if (data && data.code === 0) {
          this.$message({
            message: "修改成功",
            type: "success",
            duration: 1500,
            onClose: () => {
              console.log("修改成功,关闭消息提示");
              this.dialogFormVisible = false;
              this.getMenus(); // 重新获取数据
              this.expandedKeys = [
                this.category.parentCid == 0
                  ? this.category.catId
                  : this.category.parentCid,
              ]; // 重置展开节点
              console.log(this.expandedKeys);
            },
          });
        } else {
          this.$message.error(data.msg);
        }
      });
    },
    addCategory() {
      this.$http({
        url: this.$http.adornUrl("/product/category/save"),
        method: "post",
        data: this.$http.adornData(this.category, false),
      }).then(({ data }) => {
        if (data && data.code === 0) {
          this.$message({
            message: "添加成功",
            type: "success",
            duration: 1500,
            onClose: () => {
              console.log("添加成功,关闭消息提示");
              this.dialogFormVisible = false;
              this.getMenus(); // 重新获取数据
              this.expandedKeys = [this.category.parentCid]; // 重置展开节点
            },
          });
        } else {
          this.$message.error(data.msg);
        }
      });
    },
    remove(node, data) {
      console.log(node, data);
      var ids = [node.data.catId];

      this.$confirm(
        `确定对[id=${ids.join(",")}]进行[${
          ids.length == 1 ? "删除" : "批量删除"
        }]操作?`,
        "提示",
        {
          confirmButtonText: "确定",
          cancelButtonText: "取消",
          type: "warning",
        }
      )
        .then(() => {
          this.$http({
            url: this.$http.adornUrl("/product/category/delete"),
            method: "post",
            data: this.$http.adornData(ids, false),
          }).then(({ data }) => {
            if (data && data.code === 0) {
              this.$message({
                message: "操作成功",
                type: "success",
                duration: 1500,
                onClose: () => {
                  console.log("删除成功,关闭消息提示");
                  this.getMenus(); // 重新获取数据
                  this.expandedKeys = [node.parent.data.catId]; // 重置展开节点
                },
              });
            } else {
              this.$message.error(data.msg);
            }
          });
        })
        .catch(() => {});
    },
    // 获取分类数据
    getMenus() {
      this.dataListLoading = true;
      this.$http({
        url: this.$http.adornUrl("/product/category/list/tree"),
        method: "get",
      }).then(({ data }) => {
        console.log(data);
        this.dataListLoading = false;
        this.menus = data.data;
      });
    },
  },
  created() {
    this.getMenus(); // 获取分类数据
  },
};
</script>
<style scoped>
</style>

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

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

相关文章

Linux:Linux权限

目录 1. Linux权限的概念 2. Linux权限管理 2.1 文件访问者的分类 2.2 文件类型和访问权限 2.2.1 文件类型 2.2.2 基本权限 2.3 文件权限值的表示方法 2.4 文件访问权限的相关设置方法 2.4.1 chmod 2.4.2 chown 2.4.3 chgrp 2.4.4 umask 3. file指令 4. Linux目…

如何学习EMR:糙快猛的大数据之路(建立整体框架)

目录 初学EMREMR是什么&#xff1f;我的EMR学习故事糙快猛学习法则代码示例: 你的第一个EMR任务学习EMR的深入步骤EMR进阶技巧实用资源推荐常见挑战和解决方案 EMR生态EMR生态系统深度探索1. EMR上的Hadoop生态系统2. EMR Studio3. EMR on EKS 高级EMR配置和优化1. EMR实例集策…

音视频入门基础:PCM专题(3)——使用Audacity工具分析PCM音频文件

音视频入门基础&#xff1a;PCM专题系列文章&#xff1a; 音视频入门基础&#xff1a;PCM专题&#xff08;1&#xff09;——使用FFmpeg命令生成PCM音频文件并播放 音视频入门基础&#xff1a;PCM专题&#xff08;2&#xff09;——使用Qt播放PCM音频文件 音视频入门基础&am…

ICML 2024最佳论文开奖了!今年的热门投稿方向有这些

ICML 2024最近也放榜啦&#xff01;今年共有10篇论文夺得最佳论文奖&#xff0c;包括火爆的Stable Diffusion 3、谷歌VideoPoet以及世界模型Genie。 ICML是国际机器学习顶会&#xff0c;也是CCF-A类学术会议。今年这届顶会一共收到了9473篇论文&#xff0c;其中2610篇被录用&am…

昇思25天学习打卡营第22天|基于MindNLP+MusicGen生成自己的个性化音乐

文章目录 昇思MindSpore应用实践1、MusicGen模型简介残差矢量量化&#xff08;RVQ&#xff09;SoundStreamEncodec 2、生成音乐无提示生成文本提示生成音频提示生成 Reference 昇思MindSpore应用实践 本系列文章主要用于记录昇思25天学习打卡营的学习心得。 1、MusicGen模型简…

Qt基础 | Qt SQL模块介绍 | Qt SQL模块常用类及其常用函数介绍

文章目录 一、Qt SQL模块概述1.Qt sql 支持的数据库2.SQLite 数据库3.Qt SQL 模块的主要类 一、Qt SQL模块概述 Qt SQL 模块提供数据库编程的支持&#xff0c;Qt 支持多种常见的数据库&#xff0c;如MySQL、Oracle、MS SQL Server、SQLite 等。Qt SQL 模块包括多个类&#xff0…

phpstorm配置xdebug3

查看php路径相关信息 php --ini安装xdebug https://www.jetbrains.com/help/phpstorm/2024.1/configuring-xdebug.html?php.debugging.xdebug.configure php.ini 配置 在最后添加&#xff0c;以下是我的配置 [xdebug] zend_extension/opt/homebrew/Cellar/php8.1/8.1.29/p…

安装NVIDIA驱动

一、不升级内核安装NVIDIA驱动 说明: 1、安装NVIDIA驱动,是用来提升AI、图片等算法 2、本人是在centos7.9操作系统安装英伟达T4板卡驱动 操作系统Centos 7.9驱动版本NVIDIA-Linux-x86_64-525.89.02.run操作账号root1.1 关闭nouveau 1、查看nouveau是否关闭 lsmod |grep nouv…

Android 常用调试工具/方法解析

一、内存相关 参考Android内存分析命令_dumpsys meminfo 算出rss-CSDN博客 1、基本概念 1&#xff09;PSS & RSS & USS & VSS a、PSS 概念&#xff1a;全称Proportional Set Size&#xff0c;根据进程实际使用的内存量按照共享比例分配给进程的一种内存度量方…

MySql性能调优05-[sql实战演练]

sql实战演练 行列转换行列式转换第一题【列转行】第二题【列转行】 having的使用找到表中&#xff0c;名字重复的项有数据表employee&#xff0c;包含如下字段id、name、department、age&#xff0c;编写SQL&#xff0c;找到不与其他人同龄的年纪最大的员工的年龄有数据表emplo…

Nacos-2.4.0最新版本docker镜像,本人亲自制作,部署十分方便,兼容postgresql最新版本17和16,奉献给大家了

基于Postgresql数据库存储的nacos最新版本2.4.0,采用docker镜像安装方式 因业务需要,为了让nacos支持postgresql,特意花了两天时间修改了源码,然后制作了docker镜像,如果你也在找支持postgresql的nacos最新版本,恭喜你,你来的正好~ nacos-2.4.0 postgresql的数据库脚本…

C++学习笔记-C++11中的智能指针

1.智能指针介绍 智能指针是C的特性用法&#xff0c;是一个类似指针功能的类对象&#xff0c;其目的是为了更好的管理动态分配的内存&#xff0c;避免出现内存泄漏、悬空指针等问题。C11的标准库里提供了三种智能指针模板类&#xff0c;分别是std::unique_ptr、std::shared_ptr…

vue 两个页面切换, 再回到当前页,还是离开前的数据

1、要保证页面的name 和 建路由的大小写一致 2、页面不用生命周期--activated 调接口刷新

计算机网络八股文(三)

目录 41.为什么每次建立TCP连接时&#xff0c;初始化的序列号都不一样&#xff1f; 42.初始序列号ISN如何随机产生&#xff1f; 43.既然IP层会分片&#xff0c;为什么TCP层需要根据MSS分片呢&#xff1f; 44.TCP第一次握手丢失&#xff0c;会发生什么&#xff1f; 45.TCP第…

一个python脚本解决新版剪映导出字幕收费问题

如果你是希望我能完全解决剪映收费问题&#xff0c;我无法帮你&#xff1b; 两个文件&#xff0c;可生成不带时间线的纯文案&#xff0c;MD 格式&#xff0c;也可以生成带时间线的 SRT 文件。 因为剪映国内版对 JSON 文件进行了加密&#xff0c;所以请选择国际版 Cutcap&#x…

《javaEE篇》--阻塞队列详解

阻塞队列 阻塞队列概述 阻塞队列也是一种队列&#xff0c;和普通队列一样遵循先进先出的原则&#xff0c;但是阻塞队列相较于普通队列多了两项功能阻塞添加和阻塞移除&#xff0c;使得阻塞队列成为一种线程安全的数据结构 阻塞添加&#xff1a;当队列满的时候继续入队就会阻…

电脑虚拟摄像头软件分享|用手机打破电脑摄像头的极限

随着手机摄像头的不断更新迭代&#xff0c;手机已经接近专业电脑摄像头的画质。这让我们可以花费更低的成本获取优质的电脑录像画面。今天小编给大家详细讲解电脑虚拟摄像头的在我们日常生活中的妙用&#xff0c;以及分享几款口碑不错的电脑虚拟摄像头软件。有需要的小伙伴可以…

从业务到数据,大模型应用成功的再思考!

自2022年底OpenAI发布ChatGPT以来&#xff0c;大模型在企业的应用方兴未艾。 大模型必须要结合落地应用&#xff0c;才算是长出手跟脚&#xff0c;真正应用于实际业务场景的解决方案中&#xff0c;配合“大脑”完成任务。从医疗诊断到自动驾驶&#xff0c;从个性化营销到智能客…

数据结构重置版(概念篇)

本篇文章是对数据结构的重置&#xff0c;且只涉及概念 顺序表与链表的区别 不同点 顺序表 链表 存储空间上 物理上一定连续 逻辑上连续&#xff0c;但物理上不一定连续…

【办公软件】Office 2019以上版本PPT 做平滑切换

Office2019以上版本可以在切页面时做平滑切换&#xff0c;做到一些简单的动画效果。如下在快捷菜单栏中的切换里选择平滑。 比如&#xff0c;在两页PPT中&#xff0c;使用同一个形状对象&#xff0c;修改了大小和颜色。 选择切换为平滑后&#xff0c;可以完成如下的动画显示。 …