【RuoYi】框架中使用wangdietor富文本编辑器

一、前言

  在上篇博客中,介绍了RuoYi中如何实现文件的上传与下载,那么这篇博客讲一下如何在RuoYi中使用富文本编辑器,这部分的内容是向B站程序员青戈学习的,当然我这里就会把学到的内容做一个总结,当然也会说一下在RuoYi中使用富文本编辑器会出现的一些问题。希望对大家有所帮助!

二、准备工作

1、安装wangdietor

npm install @wangeditor/editor --save

2、导入相关的包

b641e9bfbc0d4e4e98f456cebdc7d4fa.png

三、Vue代码

  首先我想说一下使用这个wangdietor富文本编辑器的主要思路:先对需要使用wangdietor的字段绑定标签id=''editor",然后使用我们自己写的setRichText()函数对编辑器进行初始化和对一些图片、视频上传的接口进行自定义;最后要将富文本编辑器的内容存在数据中。当然数据显示的时候可能会有一些标签,此时我们可以通过按钮,出现弹窗来规避这种情况。

核心代码如下:

    setRichText(){
      this.$nextTick( () => {
        this.editor=new E('#editor')//创建对象
        this.editor.highlight = hljs//代码高亮
        this.editor.config.uploadImgServer = process.env.VUE_APP_BASE_API+'/file/editor/upload'//图片上传的接口
        this.editor.config.uploadFileName = 'file'
        this.editor.config.uploadImgParams = {
          type:"img"
        }
        this.editor.config.uploadVideoServer = process.env.VUE_APP_BASE_API+"/file/editor/upload"//视频上传的接口
        this.editor.config.uploadVideoName = 'file'
        this.editor.config.uploadVideoParams = {
          type:"video"
        }
        this.editor.create()
      })
    }

四、Java代码

后端代码主要就是在图片和视频的上传了,下面是wangdietor规定的图片和视频的上传的格式

04122df7a4e3464dae283163aebec9e4.png

d5df27bfc7354a20926dfa74794a5214.png

核心代码如下:

@RestController
@Api(tags = "文件上传与下载模块")
@RequestMapping("/file")
public class FileController extends BaseController {
    @Value("${ip:localhost}")
    String ip;//通过配置文件拿到ip

    @Value("${server.port}")
    String port;

    private static final String ROOT_PATH = System.getProperty("user.dir")+ File.separator+"files";//文件根目录

    @PostMapping("/editor/upload")
    public Dict editorUpload(MultipartFile file,@RequestParam String type) throws IOException {
        System.out.println(6767);
        String originalFilename = file.getOriginalFilename();
        String mainName = FileUtil.mainName(originalFilename);
        String extName = FileUtil.extName(originalFilename);
        if(!FileUtil.exist(ROOT_PATH)){
            FileUtil.mkdir(ROOT_PATH);
        }
        if(FileUtil.exist(ROOT_PATH+File.separator+originalFilename)){
            originalFilename=System.currentTimeMillis()+"_"+mainName+"."+extName;
        }
        File saveFile = new File(ROOT_PATH + File.separator + originalFilename);
        file.transferTo(saveFile);
        String url ="http://"+ip+":"+port+"/file/download/"+originalFilename;
        if("img".equals(type)){//上传图片
            return Dict.create().set("errno",0).set("data", CollUtil.newArrayList(Dict.create().set("url",url)));
        }else if("video".equals(type)){
            return Dict.create().set("errno",0).set("data", Dict.create().set("url",url));
        }
        return Dict.create().set("errno",0);
    }
    @ApiOperation(value = "文件下载")
    @GetMapping("/download/{fileName}")
    public void download(@PathVariable String fileName, HttpServletResponse response) throws IOException {
//        response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName,"UTF-8"));//文件下载
//        response.addHeader("Content-Disposition", "inline;filename=" + URLEncoder.encode(fileName,"UTF-8"));//文件预览
        String filePath = ROOT_PATH+File.separator+fileName;//拿到文件的路径
        if(!FileUtil.exist(filePath)){//文件不存在,就不用管,因为没有东西可写
            return;
        }
        byte[] bytes = FileUtil.readBytes(filePath);//使用FileUtil从filePath中去读取文件
        ServletOutputStream outputStream = response.getOutputStream();
        outputStream.write(bytes);//把文件的字节数写出去
        outputStream.flush();//刷新一下
        outputStream.close();//一定要关闭文件流
    }
}

后端报错

36161883564d40b294ff46ed8744776d.png

如果出现这个错误,我们改一下对应的XSS中的内容即可

96af8f9a98a14da199f58646d2906f9f.png

效果图

8037f5046f5e4c18991e6aec5cee3a95.png

630e26f6c6ce4b90b7a63b872b4830c8.png

bd893b8b814346cdbc83cecae86c687f.png

e7f720e32f88481a955aa3734da9857a.png

全部代码

  以下的代码是包含了上述效果图中的所有代码,至于一些按钮、弹窗的设计、数据的请求与显示在该代码中都有,都是一些比较小的点,我就不详细介绍了,大家感兴趣的话就在评论区评论就好了,我看到了都会回复的。

<template>
  <div class="app-container">
    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="学号" prop="stuNum">
        <el-input
          v-model="queryParams.stuNum"
          placeholder="请输入学号"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item label="姓名" prop="stuName">
        <el-input
          v-model="queryParams.stuName"
          placeholder="请输入姓名"
          clearable
          @keyup.enter.native="handleQuery"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <el-button
          type="primary"
          plain
          icon="el-icon-plus"
          size="mini"
          @click="handleAdd"
          v-hasPermi="['system:student:add']"
        >新增</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="el-icon-edit"
          size="mini"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['system:student:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="el-icon-delete"
          size="mini"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['system:student:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="el-icon-download"
          size="mini"
          @click="handleExport"
          v-hasPermi="['system:student:export']"
        >导出</el-button>
      </el-col>
      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>

    <el-table v-loading="loading" :data="studentList" @selection-change="handleSelectionChange" @close="closeDialog()">
      <el-table-column type="selection" width="55" align="center" />
      <el-table-column label="表编号" align="center" prop="tableId" />
      <el-table-column label="学号" align="center" prop="stuNum" />
      <el-table-column label="姓名" align="center" prop="stuName" />
      <el-table-column label="性别" align="center" prop="stuSex" />
      <el-table-column label="爱好" align="center" prop="stuHobby" >
        <template v-slot="scope">
          <el-button type="primary" size="mini" @click="handleStuHobbyContent(scope.row)">显示</el-button>
        </template>
      </el-table-column>
      <el-table-column label="家庭信息">
        <template slot-scope="scope">
          <el-button type="success" size="mini" @click="handleJump(scope.row)">按钮</el-button>
        </template>
      </el-table-column>
      <el-table-column label="头像上传">
        <template v-slot="scope">
          <el-upload
            action="http://localhost:8080/file/upload"
            :show-file-list="false"
            :on-success="(row,file,filelist) => handleTableFileUpload(scope.row,file,filelist)"
          >
            <el-button slot="trigger" size="mini" type="primary">选取文件</el-button>
          </el-upload>
        </template>
      </el-table-column>
      <el-table-column label="头像" align="center" prop="stuAvatar" >
        <template v-slot="scope">
          <el-image v-if="scope.row.stuAvatar" :src="scope.row.stuAvatar" style="with:50px;height: 50px"></el-image>
          <div>
            <el-button type="success" size="mini" @click="preview(scope.row.stuAvatar)">预览</el-button>
          </div>
        </template>
      </el-table-column>
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template slot-scope="scope">
          <el-button
            size="mini"
            type="text"
            icon="el-icon-edit"
            @click="handleUpdate(scope.row)"
            v-hasPermi="['system:student:edit']"
          >修改</el-button>
          <el-button
            size="mini"
            type="text"
            icon="el-icon-delete"
            @click="handleDelete(scope.row)"
            v-hasPermi="['system:student:remove']"
          >删除</el-button>
        </template>
      </el-table-column>
    </el-table>

    <pagination
      v-show="total>0"
      :total="total"
      :page.sync="queryParams.pageNum"
      :limit.sync="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改学生信息列表对话框 -->
    <el-dialog :title="title" :visible.sync="open" width="60%" append-to-body>
      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="学号" prop="stuNum">
          <el-input v-model="form.stuNum" placeholder="请输入学号" />
        </el-form-item>
        <el-form-item label="姓名" prop="stuName">
          <el-input v-model="form.stuName" placeholder="请输入姓名" />
        </el-form-item>
        <el-form-item label="性别" prop="stuSex">
          <el-input v-model="form.stuSex" placeholder="请输入性别" />
        </el-form-item>
        <el-form-item label="爱好" prop="stuHobby" class="w-e-text">
          <div id="editor"></div>
        </el-form-item>
      </el-form>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="submitForm">确 定</el-button>
        <el-button @click="cancel">取 消</el-button>
      </div>
    </el-dialog>
    <div style="margin:10px 0">
      <el-upload
        class="upload-demo"
        drag
        action="http://localhost:8080/file/upload"
        :on-success="handleMultipleUpload"
        multiple>
        <i class="el-icon-upload"></i>
        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
        <div class="el-upload__tip" slot="tip">只能上传jpg/png文件,且不超过500kb</div>
      </el-upload>
    </div>

    <el-dialog title="爱好" :visible.sync="open1" width="60%" append-to-body :close-on-click-modal="false">
      <el-card class="w-e-text" >
        <div v-html="stuHobbyContent"></div>
      </el-card>
      <div slot="footer" class="dialog-footer">
        <el-button type="primary" @click="open1 = false">确 定</el-button>
      </div>
    </el-dialog>

  </div>
</template>

<script>
import { listStudent, getStudent, delStudent, addStudent, updateStudent } from "@/api/system/student";
import E from "wangeditor"
import hljs from "highlight.js"
export default {
  name: "Student",
  data() {
    return {
      // 遮罩层
      loading: true,
      // 选中数组
      ids: [],
      // 非单个禁用
      single: true,
      // 非多个禁用
      multiple: true,
      // 显示搜索条件
      showSearch: true,
      // 总条数
      total: 0,
      // 学生信息列表表格数据
      studentList: [],
      // 弹出层标题
      title: "",
      // 是否显示弹出层
      open: false,
      // 查询参数
      queryParams: {
        pageNum: 1,
        pageSize: 10,
        stuNum: null,
        stuName: null,
      },
      // 表单参数
      form: {},
      // 表单校验
      rules: {
        stuNum: [
          { required: true, message: "学号不能为空", trigger: "blur" }
        ],
        stuName: [
          { required: true, message: "姓名不能为空", trigger: "blur" }
        ],
      },
      urls:'',
      editor:null,
      open1:false,
      stuHobbyContent:''
    };
  },
  created() {
    this.getList();
  },
  methods: {
    handleStuHobbyContent(row){
      this.stuHobbyContent=row.stuHobby;
      this.open1=true;
    },
    setRichText(){
      this.$nextTick( () => {
        this.editor=new E('#editor')
        this.editor.highlight = hljs
        this.editor.config.uploadImgServer = process.env.VUE_APP_BASE_API+'/file/editor/upload'
        this.editor.config.uploadFileName = 'file'
        this.editor.config.uploadImgParams = {
          type:"img"
        }
        this.editor.config.uploadVideoServer = process.env.VUE_APP_BASE_API+"/file/editor/upload"
        this.editor.config.uploadVideoName = 'file'
        this.editor.config.uploadVideoParams = {
          type:"video"
        }
        this.editor.create()//
      })
    },
    closeDialog() {
      this.editor.destroy()
      this.editor = null
    },
    handleMultipleUpload(response,file,fileList){
      this.urls=fileList.map(v => v.response?.msg)
      console.log(this.urls)//拿到我们的图片的路径
    },
    preview(url){
      window.open(url)
    },
    handleTableFileUpload(row,file,fileList){
      console.log(row,file)
      row.stuAvatar=file.response.msg//注意我们使用的是RuoYi框架中的AjaxResult结果集,返回的数据是在response下的msg中
      //触发更新
      //使用RuoYi中封装好的updateStudent()函数,将当前对象更新
      updateStudent(row).then(response => {
        if(response.code=='200'){
          this.$message.success("上传成功")
        }else{
          this.$message.success("上传失败")
        }
        this.form = response.data;
      });
    },
    /** 查询学生信息列表列表 */
    getList() {
      this.loading = true;
      listStudent(this.queryParams).then(response => {
        this.studentList = response.rows;
        this.total = response.total;
        this.loading = false;
      });
    },
    // 取消按钮
    cancel() {
      this.open = false;
      this.reset();
    },
    // 表单重置
    reset() {
      this.form = {
        tableId: null,
        stuNum: null,
        stuName: null,
        stuSex: null,
        stuHobby: null,
        stuAvatar: null,
      };
      this.resetForm("form");
    },
    /** 搜索按钮操作 */
    handleQuery() {
      this.queryParams.pageNum = 1;
      this.getList();
    },
    /** 重置按钮操作 */
    resetQuery() {
      this.resetForm("queryForm");
      this.handleQuery();
    },
    // 多选框选中数据
    handleSelectionChange(selection) {
      this.ids = selection.map(item => item.tableId)
      this.single = selection.length!==1
      this.multiple = !selection.length
    },
    /** 新增按钮操作 */
    handleAdd() {
      this.reset();
      this.open = true;
      this.title = "添加学生信息列表";
      this.setRichText();
    },
    /** 修改按钮操作 */
    handleUpdate(row) {
      this.reset();
      this.setRichText();
      setTimeout(()=>{//延时加载
        this.editor.txt.html(row.stuHobby)
      },0);
      const tableId = row.tableId || this.ids
      getStudent(tableId).then(response => {
        this.form = response.data;
        this.open = true;
        this.title = "修改学生信息列表";
      });
    },
    /** 页面跳转按钮操作 */
    handleJump(row){
      const tableId = row.tableId || this.ids
      getStudent(tableId).then(response => {
        this.$router.push("/student/family-data/index/"+tableId)
      });
    },
    /** 提交按钮 */
    submitForm() {
      this.$refs["form"].validate(valid => {
        if (valid) {
          let content = this.editor.txt.html();
          this.form.stuHobby=content;
          if (this.form.tableId != null) {

            updateStudent(this.form).then(response => {
              this.$modal.msgSuccess("修改成功");
              this.open = false;
              this.getList();
            });
          } else {
            addStudent(this.form).then(response => {
              this.$modal.msgSuccess("新增成功");
              this.open = false;
              this.getList();
            });
          }
        }
      });
    },
    /** 删除按钮操作 */
    handleDelete(row) {
      const tableIds = row.tableId || this.ids;
      this.$modal.confirm('是否确认删除学生信息列表编号为"' + tableIds + '"的数据项?').then(function() {
        return delStudent(tableIds);
      }).then(() => {
        this.getList();
        this.$modal.msgSuccess("删除成功");
      }).catch(() => {});
    },
    /** 导出按钮操作 */
    handleExport() {
      this.download('system/student/export', {
        ...this.queryParams
      }, `student_${new Date().getTime()}.xlsx`)
    }
  }
};
</script>

五、总结

  学无止尽,大家加油哦!希望我们都能在顶峰想见!

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

信息系统项目管理师0147:工具与技术(9项目范围管理—9.3规划范围管理—9.3.2工具与技术)

点击查看专栏目录 文章目录 9.3.2 工具与技术 9.3.2 工具与技术 专家判断 规划范围管理过程中&#xff0c;应征求具备如下领域相关专业知识或接受过相关培训的个人或小组 的意见&#xff0c;涉及的领域包括&#xff1a;以往类似项目&#xff1b;特定行业、学科和应用领域的信息…

服务器远程连接工具有哪些?

【天联】是一款功能强大的服务器远程连接工具&#xff0c;它可以让用户通过网络远程连接到目标服务器&#xff0c;实现远程操作和管理。【天联】的使用场景非常广泛&#xff0c;特别适用于以下几个领域&#xff1a; 零售、收银软件应用的远程管理&#xff1a;【天联】可以结合医…

Stable Diffusion【应用篇】【图片修复】:模糊头像照片的高清修复

本文主要是回复一下后台小伙伴留言的问题。经小伙伴本人同意后&#xff0c;允许使用待修复的照片。 我们先看一下待修复的照片。 在向我咨询之前&#xff0c;小伙伴也自己进行了尝试&#xff0c;如果直接使用Stable Diffusion的后期处理功能&#xff0c;出来的图片效果是这样的…

Spring Security3.0.1版本

前言&#xff1a; 抽象Spring Security3.0上一篇 在上一篇中&#xff0c;我们完成了对Security导入&#xff0c;快速入门&#xff0c;和对自动配置的简单验证 对登录流程的分析和Security基本原理 补充&#xff1a; 先解决上一篇留下的问题&#xff0c;端口和端点的区别 端…

Javascript系统学习(三)

前端模块化前端模块化CommonJS、AMD、CMD、ES6_commonjs amd cmd es6模块化-CSDN博客 ES6: <script type"module" src"main.js"></script> //默认导出 export default function(ctx) {... } ----------------------------------- //模块命名…

每日题库:Huawe数通HCIA——全部【813道】

1.关于ARP报文的说法错误的是?单选 A.ARP报文不能被转发到其他广播域 B.ARP应答报文是单播方发送的 C.任何链路层协议都需要ARP协议辅助获取数据链路层标识 DARP请求报文是广播发送的 答案:C  解析: STP协议不需要ARP辅助 2.园区网络搭建时,使用以下哪种协议可以避免出现二层…

[AIGC] 详解Mockito - 简单易学的Java单元测试框架

在Java的世界中, 单元测试是一项非常重要的任务. Mockito作为一个强大灵活的mock框架&#xff0c;可以帮助我们有效的编写和管理我们的单元测试. 了解并掌握Mockito的使用对于提高我们的开发效率和保证我们的软件质量有着巨大的帮助. 文章目录 什么是Mockito?Mockito的核心API…

UE4_环境_材质函数

学习笔记&#xff0c;不喜勿喷&#xff0c;欢迎指正&#xff0c;侵权立删&#xff01; 1、建立材质函数Distance_Fun&#xff0c;勾选公开到库。 2、添加函数输入节点FunctionInput&#xff0c; 这个输入我们想作为混合材质属性BlendMaterialAttributes的alpha输入节点&#x…

苍穹外卖笔记-06-菜品管理-菜品分类,公共字段填充

菜品分类 1 菜品分类模块1.1 需求分析与设计1.1.1 产品原型1.1.2 接口设计1.1.3 表设计 1.3 代码实现1.4 测试分类分页查询启用禁用分类修改分类信息新增菜品分类删除菜品分类 2 公共字段自动填充2.1 问题分析2.2 实现思路自定义注解AutoFill自定义切面AutoFillAspectMapper接口…

C++ | Leetcode C++题解之第136题只出现一次的数字

题目&#xff1a; 题解&#xff1a; class Solution { public:int singleNumber(vector<int>& nums) {int ret 0;for (auto e: nums) ret ^ e;return ret;} };

如何在手机上恢复误删除的视频?

说到移动设备上的视频恢复&#xff0c;我们仍将揭开4种解决方案供您使用。希望它们对您的案件有所帮助。 众所周知&#xff0c;我们移动设备上的视频应用程序将创建一个缓存文件夹&#xff0c;以在它们永远消失之前临时存储已删除的项目。因此&#xff0c;有许多iPhone / Andr…

《系统架构设计师教程(第2版)》第11章-未来信息综合技术-02-人工智能技术概述

文章目录 1. 人工智能&#xff08;AI&#xff09;1.1 弱人工智能1.2 强人工智能 2. 人工智能的发展历程3. 人工智能关键技术31. 自然语言处理 (Natural Language Processing)3.2 计算机视觉 (Computer Vision)3.3 知识图谱 (Knowledge Graph)3.4 人机交互 (Human-Computer Inte…

2. keepalived结合LVS配合使用

keepalived结合LVS配合使用 1、后端nfs存储提供项目文件2、后端nfs上集中安装MySQL&#xff0c;共用数据库3、业务服务器通过LNMP正常部署wordpress博客&#xff0c;客户端通过DNS解析可正常访问4、所有业务服务器上修改arp参数、配置VIP5、配置keepalived实现LVS高可用5.1 kee…

来自工业界的知识库 RAG 服务(三),FinGLM 竞赛获奖项目详解

背景介绍 前面介绍过工业界的 RAG 服务 QAnything 和 RagFlow 的详细设计&#xff0c;也介绍过来自学术界的 一些优化手段。 前一阵子刚好看到智谱组织的一个金融大模型比赛 FinGLM&#xff0c;主要做就是 RAG 服务的竞赛&#xff0c;深入研究了其中的几个获奖作品&#xff…

docker安装rabbitmq详解

目录 1、安装 1-1.查看rabbitmq镜像 1-2.下载Rabbitmq的镜像 1-3.创建并运行rabbitmq容器 1-4.查看启动情况 1-5.启动web客户端 1-6.访问rabbitmq的客户端 2..遇到的问题 解决方法: 1、安装 1-1.查看rabbitmq镜像 docker search rabbitmq 1-2.下载Rabbitmq的镜像 拉…

php高级之框架源码、宏扩展原理与开发

在使用框架的时候我们经常会看到如下代码 类的方法不会显示地声明在代码里面&#xff0c;而是通过扩展的形式后续加进去&#xff0c;这么做的好处是可以降低代码的耦合度、保证源码的完整性。我自己看着框架源码实现了这个功能。 以下是结果: base代码 index.php <?php…

哈希表和二维矩阵的结合-2352. 相等行列对(新思路、新解法)

题目链接及描述 . - 力扣&#xff08;LeetCode&#xff09;. - 备战技术面试&#xff1f;力扣提供海量技术面试资源&#xff0c;帮助你高效提升编程技能,轻松拿下世界 IT 名企 Dream Offer。https://leetcode.cn/problems/equal-row-and-column-pairs/description/?envTypest…

Java | Leetcode Java题解之第135题分发糖果

题目&#xff1a; 题解&#xff1a; class Solution {public int candy(int[] ratings) {int n ratings.length;int ret 1;int inc 1, dec 0, pre 1;for (int i 1; i < n; i) {if (ratings[i] > ratings[i - 1]) {dec 0;pre ratings[i] ratings[i - 1] ? 1 : …

【Python报错】已解决AttributeError: list object has no attribute items ( Solved )

解决Python报错&#xff1a;AttributeError: list object has no attribute ‘items’ (Solved) 在Python中&#xff0c;AttributeError通常表示你试图访问的对象没有你请求的属性或方法。如果你遇到了AttributeError: list object has no attribute items的错误&#xff0c;这…

探索Adobe XD:高效UI设计软件的中文入门教程

在这个数字化世界里&#xff0c;创意设计不仅是为了吸引观众的注意&#xff0c;也是用户体验的核心部分。强大的设计工具可以帮助设计师创造出明亮的视觉效果&#xff0c;从而提高用户体验。 一、Adobe XD是什么&#xff1f; Adobe XD是一家知名软件公司 Adobe Systems 用户体…