【element-tiptap】如何实现查找替换功能?

这是一个稍微复杂的功能了,因为 element-tiptap 中没有查找替换功能,需要从零开始开发。但是,在万能的github上有一个开源的库,我们可以借用一下 tiptap-search-and-replace
不过这个库是没有UI的,只有一个扩展的方法。但是这个关键的方法只要有了,剩下的就简单多了
searchAndReplace.ts
我的项目的目录名文件名都已经从首字母大写改成了全部小写的写法,不影响大家阅读哦
我们首先把这个源码放到 scr/extensions 目录下面
然后UI我们可以参考在线WPS的UI
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
先分析一下需求。我发现开发之前的需求分析真的很重要,可能这就是所谓的“慢即是快”,看似有些繁琐并且浪费时间,但是可以在开发的时候少走很多弯路,可以让自己开发的思路更加的清晰,效率也会更高。

  • 新建一个扩展
  • 需要一个下拉框组件,点击按钮出现查找和替换两个菜单项;点击查找、替换菜单项的时候,都需要弹出弹出框组件,并且把状态传递给弹出框组件
  • 需要一个弹出框组件,有两种状态,表示当前是查找还是替换;弹出框组件分为两个tab栏;查找可以找上一个、下一个;替换可以找上一个、下一个、全部替换、替换当前
  • 查找可以用快捷键⌘F唤醒,替换可以用快捷键⌘H唤醒
    更多选项里面的功能就先不做了

1、新建一个扩展

① 新建一个扩展,src/extensions/search-replace.ts,然后把上面的文件的源码放进去,当然,我们后续还需要对它进行稍微的改造,这里先不管
② 在 src/extensions/index.ts 文件中模仿其他扩展,也导出这个扩展

export { default as SearchAndReplace } from './search-replace';

③ 在 src/components/xkw-editor.vue 文件中的扩展列表 extensions 增加一项,根据官网的提示,需要增加 configure 配置项

import {
  SearchAndReplace
} from '../extensions';
SearchAndReplace.configure({
  searchResultClass: "search-result", // class to give to found items. default 'search-result'
  caseSensitive: false, // no need to explain
  disableRegex: false, // also no need to explain
}),

2、下拉框组件

① 创建下拉框组件 src/components/menu-commands/search-replace/search-replace.dropdown.vue
ps:我这里的文件命名自己有根据项目需要修改过,大家自行修改哈
代码说明:

  • 下拉框菜单有两个:查找、替换
  • 点击查找或者替换的时候,会将对应的标识字符串传递到回调函数中,以此标识当前的操作类型
  • 一个弹出框组件,当点击查找或替换的时候都会被激活,并且接受表示操作类型的参数
<template>
  <el-dropdown placement="bottom" trigger="click" @command="handleCommand" popper-class="my-dropdown"
    :popper-options="{ modifiers: [{ name: 'computeStyles', options: { adaptive: false } }] }">
    <div>
      <command-button :enable-tooltip="enableTooltip" :tooltip="t('editor.extensions.searchAndReplace.tooltip')"
        icon="search" :button-icon="buttonIcon" />
    </div>
    <template #dropdown>
      <el-dropdown-menu class="el-tiptap-dropdown-menu">
        <el-dropdown-item command="search">
          <span>查找</span>
        </el-dropdown-item>
        <el-dropdown-item command="replace">
          <span>替换</span>
        </el-dropdown-item>
      </el-dropdown-menu>
    </template>
  </el-dropdown>
  <search-replace-popup v-if="showPopup" :mode="popupMode" :editor="editor" @close="showPopup = false" />
</template>

<script lang="ts">
import { defineComponent, inject, ref } from 'vue';
import { Editor } from '@tiptap/vue-3';
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus';
import CommandButton from '../command.button.vue';
import searchReplacePopup from './search-replace.popup.vue';

export default defineComponent({
  name: 'searchAndReplaceDropdown',

  components: {
    ElDropdown,
    ElDropdownMenu,
    ElDropdownItem,
    CommandButton,
    searchReplacePopup,
  },

  props: {
    editor: {
      type: Object as () => Editor,
      required: true,
    },
    buttonIcon: {
      default: '',
      type: String
    }
  },

  setup(props) {
    const t = inject('t') as (key: string) => string;
    const enableTooltip = inject('enableTooltip', true);
    const showPopup = ref(false);
    const popupMode = ref<'search' | 'replace'>('search');

    const handleCommand = (command: string) => {
      popupMode.value = command as 'search' | 'replace';
      showPopup.value = true;
    };

    return {
      t,
      enableTooltip,
      showPopup,
      popupMode,
      handleCommand
    };
  },
});
</script>

<style scoped>
.dropdown-title {
  font-size: 14px;
  font-weight: 500;
  margin: 5px;
}

.el-tiptap-dropdown-menu__item {
  margin-left: 5px;
}
</style>

② 这里的图标 search 需要我们自己添加
src/icons/search.svg

<svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke-width="1.5"><g id="group-0" stroke="#333333" fill="#333333"><path d="M11.2426 11.2426L14.5 14.5M13 7C13 10.3137 10.3137 13 7 13C3.68629 13 1 10.3137 1 7C1 3.68629 3.68629 1 7 1C10.3137 1 13 3.68629 13 7Z" stroke-linecap="round" stroke-linejoin="miter" fill="none" vector-effect="non-scaling-stroke"></path></g></svg>

③ 扩展文件修改,应用下拉框组件
主要是 addOptions 方法

import searchAndReplaceDropdown from "@/components/menu-commands/search-replace/search-replace.dropdown.vue";
addOptions() {
  return {
    // 保留父扩展的所有选项
    ...this.parent?.(),
    button({ editor, t }: { editor: Editor; t: (...args: any[]) => string }) {
      return {
        component: searchAndReplaceDropdown,
        componentProps: {
          editor,
        },
      };
    },
  };
},

3、弹出框组件

src/components/menu-commands/search-replace/search-replace.popup.vue
① 创建弹出框组件
组件说明:

  • UI 参考WPS编辑器的效果
  • 查找、替换、上一个、下一个、替换全部 这些功能已经被我们的扩展文件添加到了 editor.commands 上,所以直接通过命令调用即可
  • 输入框中文字改变的时候,就需要执行查找
  • 点击右上角的叉号关闭弹出框的时候,要去除所有的选中状态
<template>
    <div v-if="drawerVisible" class="search-replace-container" :style="containerStyle">
        <span class="search-replace-close" type="text" @click="handleClose">
            ×
        </span>
        <el-tabs v-model="activeTab">
            <el-tab-pane label="查找" name="search">
                <div class="search-replace-title">
                    查找
                </div>
                <el-input v-model="searchTerm"
                    @input="onSearchTermChange"
                    size="default"
                    :placeholder="t('editor.extensions.searchAndReplace.searchPlaceholder')"></el-input>
                <div class="search-replace-actions">
                    <el-button size="default" @click="findPrevious">上一个</el-button>
                    <el-button size="default" @click="findNext">下一个</el-button>
                </div>
            </el-tab-pane>
            <el-tab-pane label="替换" name="replace">
                <div class="search-replace-title">
                    查找
                </div>
                <el-input v-model="searchTerm"
                    @input="onSearchTermChange"
                    size="default"
                    :placeholder="t('editor.extensions.searchAndReplace.searchPlaceholder')"></el-input>
                <div class="search-replace-title">
                    替换为
                </div>
                <el-input v-model="replaceTerm"
                size="default"
                :placeholder="t('editor.extensions.searchAndReplace.replacePlaceholder')"></el-input>
                <div class="search-replace-actions">
                    <el-button size="default" @click="findPrevious">上一个</el-button>
                    <el-button size="default" @click="findNext">下一个</el-button>
                    <el-button size="default" @click="replaceCurrent">替换</el-button>
                    <el-button size="default" @click="replaceAll">替换全部</el-button>
                </div>
            </el-tab-pane>
            
        </el-tabs>
    </div>
</template>

<script lang="ts">
import { defineComponent, ref, watch, inject, computed } from 'vue';
import { Editor } from '@tiptap/vue-3';
import { ElTabs, ElTabPane, ElInput, ElButton } from 'element-plus';

export default defineComponent({
    name: 'searchAndReplacePopup',
    components: {
        ElTabs,
        ElTabPane,
        ElInput,
        ElButton,
    },
    props: {
        mode: {
            type: String as () => 'search' | 'replace',
            required: true,
        },
        editor: {
            type: Object as () => Editor,
            required: true,
        },
    },
    emits: ['close'],
    setup(props, { emit }) {
        const t = inject('t') as (key: string) => string;
        const drawerVisible = ref(true);
        const activeTab = ref(props.mode);
        const searchTerm = ref('');
        const replaceTerm = ref('');

        watch(() => props.mode, (newMode) => {
            activeTab.value = newMode;
        });

        const handleClose = () => {
            emit('close');
            searchTerm.value = '';
            replaceTerm.value = '';
            props.editor.commands.setSearchTerm('');
            props.editor.commands.resetIndex();
        };

        const findNext = () => {
            props.editor.commands.setSearchTerm(searchTerm.value);
            props.editor.commands.nextSearchResult();
        };

        const findPrevious = () => {
            props.editor.commands.setSearchTerm(searchTerm.value);
            props.editor.commands.previousSearchResult();
        };

        const replaceCurrent = () => {
            props.editor.commands.setReplaceTerm(replaceTerm.value);
            props.editor.commands.replace();
        };

        const replaceAll = () => {
            props.editor.commands.setReplaceTerm(replaceTerm.value);
            props.editor.commands.replaceAll();
        };

        const onSearchTermChange = () => {
            props.editor.commands.setSearchTerm(searchTerm.value);
        };

        // 动态计算容器宽度
        const containerStyle = computed(() => ({
            width: activeTab.value === 'search' ? '358px' : '478px',
        }));

        return {
            t,
            drawerVisible,
            activeTab,
            searchTerm,
            replaceTerm,
            handleClose,
            findNext,
            findPrevious,
            replaceCurrent,
            replaceAll,
            onSearchTermChange,
            containerStyle,
        };
    },
});
</script>

<style lang="scss">
@import '../../../styles/variables.scss';
.search-replace-container {
    position: absolute;
    top: 10%;
    right: 10%;
    width: 30%;
    background-color: white;
    border: 1px solid #ccc;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
    padding: 16px;
    z-index: 1000;
    border-radius: 5px;
    transition: width 0.3s ease; /* 添加过渡效果 */
}

.search-replace-container .el-tabs__item {
    font-size: 16px;
}
.search-replace-container .el-tabs__nav-wrap::after {
    background-color: transparent;
}
.search-replace-close {
    cursor: pointer;
    font-size: 18px;
    color: #60646c;
    font-weight: 600;
    position: absolute;
    width: 24px;
    height: 24px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
    top: 22px;
    right: 18px;
    z-index: 3;
}
.search-replace-close:hover {
    background-color: #f0f0f0;
}

.search-replace-title {
    font-weight: 600;
    font-size: 14px;
    line-height: 22px;
    color: hsla(0, 0%, 5%, .9);
    margin-top: 8px;
    margin-bottom: 3px;
}

.search-result {
	background: $lighter-primary-color;
}

.search-result.search-result-current {
	background: $tiptap-search-result-current-color;
}

.search-replace-actions {
    margin-top: 16px;
    display: flex;
    justify-content: flex-end;
}
</style>

② 背景颜色定义
在这里插入图片描述
如上图所示,查找的时候会查找到很多结果,所有的结果都会被添加类名 search-result,不过当前选中的结果还会被添加 search-result-current 类名。
上面的代码中,我们给这两个类添加了背景颜色,背景颜色是通过 @import '../../../styles/variables.scss'; 引入进来的,在这个文件中,还需要增加一个定义
src/styles/variables.scss

$tiptap-search-result-current-color:rgb(193, 243, 181);

这样就可以实现如下效果
在这里插入图片描述

4、替换方法改造

这里有一个小问题,就是替换功能总是会替换第一个查找结果,但是期望结果应该是替换我们选中的结果
在这里插入图片描述
此时选中的是第三个 Content,点击替换的时候,还是会替换第一个 Content
这其实是源码中的 replace 方法的问题,我们看一下这个方法的定义

const replace = (
  replaceTerm: string,
  results: Range[],
  { state, dispatch }: { state: EditorState; dispatch: Dispatch },
) => {
  const firstResult = results[0];

  if (!firstResult) return;

  const { from, to } = results[0];

  if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
};

可以看到确实是进行的是结果列表中第一个元素的替换
这个方法的调用在 addCommands 里:

replace:
  () =>
    ({ editor, state, dispatch }) => {
      const { replaceTerm, results } = editor.storage.searchAndReplace;
      console.log(editor.storage.searchAndReplace);
      console.log(results);

      replace(replaceTerm, results, { state, dispatch });

      return false;
    },

控制台输出一下 editor.storage.searchAndReplace ,我们可以发现,有一个属性可以标识当前选中的是哪一个结果
在这里插入图片描述
索引为 2,也就是第三个。
那么我们可以改造一下 replace 函数,接收一个索引的参数,来指定替换哪一个

// 替换当前搜索结果
const replace = (
  replaceTerm: string,
  results: Range[],
  resultIndex: number,
  { state, dispatch }: { state: EditorState; dispatch: Dispatch },
) => {
  if (resultIndex < 0 || resultIndex >= results.length) return;

  const { from, to } = results[resultIndex];

  if (dispatch) dispatch(state.tr.insertText(replaceTerm, from, to));
};

这样就可以实现 替换当前选中的结果 了

5、快捷键

src/extensions/search-replace.ts 增加快捷键
如果当前鼠标选中的区域有文本的话,就需要获取到选中区域的文本,传递给 setSearchTerm 命令

addKeyboardShortcuts() {
  return {
    'Mod-f': () => {
      const { state } = this.editor;
      const { from, to } = state.selection;
      const selectedText = state.doc.textBetween(from, to);
      this.editor.commands.setSearchTerm(selectedText);
      this.editor.emit('openSearchReplacePopup', 'search');
      return true;
    },
    'Mod-h': () => {
      const { state } = this.editor;
      const { from, to } = state.selection;
      const selectedText = state.doc.textBetween(from, to);
      this.editor.commands.setSearchTerm(selectedText);
      this.editor.commands.setReplaceTerm('');
      this.editor.emit('openSearchReplacePopup', 'replace');
      return true;
    },
  };
},

src/components/menu-commands/search-replace/search-replace.dropdown.vue
我们知道,弹出框显示与否,是通过这个组件里面的 showPopup 属性控制的,快捷键按下的时候,会使用 emit 触发 openSearchReplacePopup 事件。那么在下拉框组件中,我们需要监听openSearchReplacePopup 事件,在回调函数中将showPopup 属性设置为 true

onMounted(() => {
    props.editor.on('openSearchReplacePopup', (mode: 'search' | 'replace') => {
        popupMode.value = mode;
        showPopup.value = true;
    });
});

src/components/menu-commands/search-replace/search-replace.popup.vue
在这个组件中,searchTerm 我们之前是初始化为''的,但是现在这个值,需要从编辑器的属性中获取

const searchTerm = ref(props.editor.storage.searchAndReplace.searchTerm);

另外,还有一个小细节,如图,我们选中了第三个 Content,并且按下了快捷键
在这里插入图片描述
此时会发生什么?会发现当前的查找结果是第一个 Content
在这里插入图片描述
这不是我想要的。
应该是,咱们鼠标选中的内容作为当前查找结果。
那么此时我们就需要在查找结果数组中,找到和我们鼠标选中区域的位置一样的查找结果,并且选中这个查找结果

const selection = props.editor.state.selection;
const results = props.editor.storage.searchAndReplace.results;
if(results.length > 0) {
    // 找到 results 中,from 和 selection.from 一样的结果,然后设置为当前结果
    for(let i = 0; i < results.length; i++) {
        if(results[i].from !== selection.from) {
            // 向后找
            props.editor.commands.nextSearchResult();
        }else{
            break;
        }
    }
}

然后你会发现,现在选中第三个 Content 按下快捷键是这种效果
在这里插入图片描述

其实不是bug了,是因为当前我们用鼠标选中了第三个 Content,并且鼠标选中的背景色跟我们设置的选中结果的背景色一样😂😂😂,就这个我以为是bug看了好久好久。。。。。。
不如改个颜色直观一些吧
src/styles/variables.scss

$tiptap-search-result-color:#FDFF00;
$tiptap-search-result-current-color:#F79632;

src/components/menu-commands/search-replace/search-replace.popup.vue

.search-result {
    display: inline-block;
	background: $tiptap-search-result-color;
}

.search-result.search-result-current {
	background: $tiptap-search-result-current-color;
}

在这里插入图片描述
随便点击一下页面
在这里插入图片描述
长呼一口气,这个还真是有点复杂。

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

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

相关文章

微信小程序-获取头像和昵称

一.获取头像 1.将button组件open-type的值设置为chooseAvatar 2.通过bindchooseavatar事件回调获取到头像信息的临时路径 wxml文件代码&#xff1a; <view> <button class"btn" open-type"chooseAvatar" bindchooseavatar"chooseavatar&qu…

Video-XL:面向小时级视频理解的超长视觉语言模型

在人工智能领域&#xff0c;视频理解一直是一个挑战性的任务&#xff0c;尤其是对于长时间视频内容的理解。现在&#xff0c;Video-XL的问世标志着我们在这一领域迈出了重要的一步。Video-XL是一个专为小时级视频理解设计的超长视觉语言模型&#xff0c;它能够处理超长视频序列…

Excel:vba实现筛选出有批注的单元格

实现的效果&#xff1a;代码&#xff1a; Sub test() Dim cell As RangeRange("F3:I10000").ClearlastRow Cells(Rows.Count, "f").End(xlUp).Row MsgBox lastrow For Each cell In Range("a1:a21")If Not cell.Comment Is Nothing ThenMsgBox…

MATLAB和Python电车电池制造性能度量分析

&#x1f3af;要点 分析全电池制造端开路电压曲线&#xff0c;解析电化学指标或特征&#xff0c;了解电池的热力学和动力学特性。测试相同活性材料&#xff08;正极和石墨负极&#xff09;的两批电池&#xff0c;列出模型提取的电化学特征并可视化分析结果。使用类似电压拟合方…

金仓数据库×武汉人社:共塑大数据应用智慧平台

数智人社 随着数字化和智能化的浪潮席卷全球&#xff0c;武汉市人力资源和社会保障局紧跟时代步伐&#xff0c;成功打造了以金仓数据库为承载底座的大数据应用平台。 这一平台的建立&#xff0c;不仅实现了与武汉市智慧城市大脑的高效数据对接和共享&#xff0c;更在治理体系和…

k8s部署Kafka集群超详细讲解

准备部署环境 Kubernetes集群信息 NAMEVERSIONk8s-masterv1.29.2k8s-node01v1.29.2k8s-node02v1.29.2 Kafka&#xff1a;3.7.1版本&#xff0c;apche版本 Zookeeper&#xff1a;3.6.3版本 准备StorageClass # kubectl get sc NAME PROVISIONER RECLA…

Spring容器详解:BeanFactory和ApplicationContext的不同

Spring容器 创建容器的几种方式 方式一&#xff1a;类路径加载配置文件 ApplicationContext context new ClassPathXmlApplicationContext("application.xml"); 方式二&#xff1a;文件路径加载配置文件 ApplicationContext context new FileSystemXmlApplicatio…

前端——选择器

基础选择器 1.标签选择器 p 例如p标签&#xff0c;可以更改所有此标签的格式 格式为 标签{ } Html文件 <link rel"stylesheet" href"./demo01.css"><body><p>hello css</p><p>hello html</p><p>hello js&…

Python 实现 excel 数据过滤

一、场景分析 假设有如下一份 excel 数据 shop.xlsx, 写一段 python 程序&#xff0c;实现对于车牌的分组数据过滤。 并以车牌为文件名&#xff0c;把店名输出到 车牌.txt 文件中。 比如 闽A.txt 文件内容为&#xff1a; 小林书店福州店1 小林书店福州店2 二、依赖安装 程序依…

SOLID 原则:编写可扩展且可维护的代码

有人告诉过你&#xff0c;你写的是“糟糕的代码”吗&#xff1f; 如果你有&#xff0c;那真的没什么可羞愧的。我们在学习的过程中都会写出有缺陷的代码。好消息是&#xff0c;改进起来相当简单——但前提是你愿意。 改进代码的最佳方法之一是学习一些编程设计原则。你可以将…

当贝F7Pro怎么样?一文看懂当贝秋季新品当贝F7Pro值不值得买?

当贝投影在今年的双11阶段发布了一款全新护眼三色激光投影当贝F7Pro 4K激光投影&#xff0c;这款被誉为“4K激光真旗舰”的激光投影主要是定位高端系列&#xff1b;不仅采用了全新的护眼三色激光技术&#xff0c;全面提升了投影画面的亮度、色彩和色准&#xff1b;在4K分辨率&a…

【Linux系统】Ubuntu的简单操作

什么是 Ubuntu&#xff1f; Ubuntu&#xff08;乌帮图&#xff09;是一个非洲词汇&#xff0c;它的意思是“人性对待他人”或“群在故我在”。Ubuntu发行版将Ubuntu精神带到软件世界之中。 目前已有大量各种各样基于GNU/Linux的操作系统&#xff0c;例如:Debian,SuSE,Gentoo,R…

猜数游戏(Fortran)

背景 学了两个月Fortran还没来一次正式练习 于是—— 代码 program gessnum! implicit none 不取消IN规则。integer::num,areal::Ncall random_seed()call random_number(N)aint(N*10)print*,"请输入您猜的数字&#xff1a;"read(*,*)numdo i1,3if (numa)thenpri…

【Next.js 项目实战系列】02-创建 Issue

原文链接 CSDN 的排版/样式可能有问题&#xff0c;去我的博客查看原文系列吧&#xff0c;觉得有用的话&#xff0c;给我的库点个star&#xff0c;关注一下吧 上一篇【Next.js 项目实战系列】01-创建项目 创建 Issue 配置 MySQL 与 Prisma​ 在数据库中可以找到相关内容&…

【Linux】【xmake】安装 + C/C++常用项目配置

文章目录 0. 环境准备1. 子命令create - 快速创建项目build - 构建程序config - 配置编译需要的参数show - 查看当前工程基本信息update - 程序自更新 2. C/C 项目常用配置2.1 项目目标类型2.2 添加宏定义2.3 头文件路径和链接库配置2.4 设置语言标准2.5 设置编译优化2.6 添加源…

《YOLO 目标检测》—— YOLO v3 详细介绍

&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;还未写完&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xf…

vscode插件live server无法在手机预览调试H5网页

环境 Window10、vscode&#xff1a;1.94.2、Live Server&#xff1a;v5.7.9、Live Server (Five Server)&#xff1a;v0.3.1 问题 PC端预览没有问题&#xff0c;但是在手机点击链接显示访问失败 排查 1. 是否同一局域网 意思就是电脑、手机是不是访问同一个网络。电脑插得…

【设计模式-原型】

**原型模式&#xff08;Prototype Pattern&#xff09;**是一种创建型设计模式&#xff0c;旨在通过复制现有对象的方式来创建新对象&#xff0c;而不是通过实例化类来创建对象。该模式允许对象通过克隆&#xff08;复制&#xff09;来创建新的实例&#xff0c;因此避免了重新创…

Git核心概念图例与最常用内容操作(reset、diff、restore、stash、reflog、cherry-pick)

文章目录 简介前置概念.git目录objects目录refs目录HEAD文件 resetreflog 与 reset --hardrevert(撤销指定提交)stashdiff工作区与暂存区差异暂存区与HEAD差异工作区与HEAD差异其他比较 restore、checkout(代码撤回)merge、rebase、cherry-pick 简介 本文将介绍Git几个核心概念…

赛氪提供专业技术支持,首届“天翼云息壤杯”高校AI大赛正式开启

2024年9月25日&#xff0c;在ICT中国2024高层论坛暨国际信息通信展主论坛上&#xff0c;首届“天翼云息壤杯”高校AI大赛正式拉开帷幕。中国电信总经理梁宝俊出席并发表了致辞。此次大赛由国务院国资委、工业和信息化部、教育部等部委指导&#xff0c;中国电信集团有限公司和华…