vue3 + mark.js | 实现文字标注功能

news2024/11/26 13:52:38

页面效果

mark-demo.gif

具体实现

新增

  • 1、监听鼠标抬起事件,通过window.getSelection()方法获取鼠标用户选择的文本范围或光标的当前位置。
  • 2、通过 选中的文字长度是否大于0window.getSelection().isCollapsed (返回一个布尔值用于描述选区的起始点和终止点是否位于一个位置,即是否框选了)来判断是否展示标签选择的弹窗。
  • 3、标签选择的弹窗采用 子绝父相 的定位方式,通过鼠标抬起的位置确认弹窗的 topleft 值。
    const TAG_WIDTH = 280 //自定义最大范围,以保证不超过内容的最大宽度
    const tagInfo = ref({
     visible: false,
     top: 0,
     left: 0,
    })
    const el = document.getElementById('text-container')
    //鼠标抬起
    el?.addEventListener('mouseup', (e) => {
      const text = window?.getSelection()?.toString() || ''
      if (text.length > 0) {
        const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300
        tagInfo.value = {
          visible: true,
          top: e.offsetY + 40,
          left: left,
        }
        getSelectedTextData()
      } else {
        tagInfo.value.visible = false
      }
      //清空重选/取消数据
      resetEditTag()
  const selectedText = reactive({
    start: 0,
    end: 0,
    content: '',
  })
  //获取选取的文字数据
  const getSelectedTextData = () => {
    const select = window?.getSelection() as any
    console.log('selectselectselectselect', select)
    const nodeValue = select.focusNode?.nodeValue
    const anchorOffset = select.anchorOffset
    const focusOffset = select.focusOffset
    const nodeValueSatrtIndex = markContent.value?.indexOf(nodeValue)
    selectedText.content = select.toString()
    if (anchorOffset < focusOffset) {
      //从左到右标注
      selectedText.start = nodeValueSatrtIndex + anchorOffset
      selectedText.end = nodeValueSatrtIndex + focusOffset
    } else {
      //从右到左
      selectedText.start = nodeValueSatrtIndex + focusOffset
      selectedText.end = nodeValueSatrtIndex + anchorOffset
    }
  }

javascript操作光标和选区详情可参考文档:https://blog.51cto.com/u_14524391/3712814

  • 4、选中标签后,采用markjs的markRanges()方式去创建一个选中的元素并为其添加样式和绑定事件。
  • 5、定义一个响应式的文字列表,专门记录标记的内容,添加完元素后可追加一条已标记的数据。
import Mark from 'mark.js'
import {ref} from 'vue
import { nanoid } from 'nanoid'


const selectedTextList = ref([])

const handleSelectLabel = (t) => {
  const marker = new Mark(document.getElementById('text-container'))
  const { tag_color, tag_name, tag_id } = t
  const markId = nanoid(10)
  marker.markRanges(
      [
        {
          start: selectedText.start, //必填
          length: selectedText.content.length, //必填
        },
      ],
      {
        className: 'text-selected',
        element: 'span',
        each: (element: any) => {
          //为元素添加样式和属性
          element.setAttribute('id', markId)
          element.style.borderBottom = `2px solid ${t.tag_color}` //添加下划线
          element.style.color = t.tag_color
          //绑定事件
          element.onclick = function (e: any) {
            //
          }
        },
      }
    )
    selectedTextList.value.push({
      tag_color,
      tag_name,
      tag_id,
      start: selectedText.start,
      end: selectedText.end,
      mark_content:selectedText.content,
      mark_id: markId,
    })
}

删除

image.png

点击已进行标记的文字————>重选/取消弹窗显示————>点击取消

如何判断点击的文字是否已标记,通过在创建的标记元素中绑定点击事件,触发则表示已标记。

  1. 在点击事件中记录该标记的相关内容,如颜色,文字,起始位置,以及唯一标识id(新建时给元素添加一个id属性,点击时即可通过e.target.id获取)
      import { nanoid } from 'nanoid'
      
      //选择标签后
      const markId = nanoid(10)
      marker.markRanges(
      [
        {
          start: isReset ? editTag.value.start : selectedText.start,
          length: isReset ? editTag.value.content.length : selectedText.content.length,
        },
      ],
      {
        className: 'text-selected',
        element: 'span',
        each: (element: any) => {
          element.setAttribute('id', markId)
          //绑定事件
          element.onclick = function (e: any) {
            e.preventDefault()
            if (!e.target.id) return
            const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300
            const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any
            const { mark_content, tag_id, start, end } = item || {}
            editTag.value = {
              visible: true,
              top: e.offsetY + 40,
              left: e.offsetX,
              mark_id: e.target.id,
              content: mark_content || '',
              tag_id: tag_id || '',
              start: start,
              end: end,
            }
            tagInfo.value = {
              visible: false,
              top: e.offsetY + 40,
              left: left,
            }
          }
        },
      }
    )
  1. 点击取消后,获取在此前记录的id,根据id查询相关的标记元素
  • 使用markjs.unmark()方法即可删除此元素。
  • 绑定的响应式数据,可使用findIndexsplice()删除
  1. 编辑弹窗隐藏
const handleCancel = () => {
    if (!editTag.value.mark_id) return
    const markEl = new Mark(document.getElementById(editTag.value.mark_id))
    markEl.unmark()
    selectedTextList.value.splice(
      selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id),
      1
    )
    tagInfo.value = {
      visible: false,
      top: 0,
      left: 0,
    }
    resetEditTag()
  }

const resetEditTag = () => {
    editTag.value = {
      visible: false,
      top: 0,
      left: 0,
      mark_id: '',
      content: '',
      tag_id: '',
      start: 0,
      end: 0,
    }
  }

重选

image.png

和取消的步骤一样,只不过在点击重选后,先弹出标签弹窗,选择标签后,需要先删除选中的元素,然后再新增一个标记元素。由于在标签选择,在标签选择中判断一下是否是重选,是重选的话就需删除后再创建元素,不是的话就代表是新增,直接新增标记元素(综上所述)。

  const handleSelectLabel = (t: TTag) => {
    tagInfo.value.visible = false
    const { tag_color, tag_name, tag_id } = t
    const marker = new Mark(document.getElementById('text-container'))
    const markId = nanoid(10)
    const isReset = selectedTextList.value?.map((j) => j.mark_id).includes(editTag.value.mark_id)
      ? 1
      : 0 // 1:重选 0:新增
    if (isReset) {
      //如若重选,则删除后再新增标签
      const markEl = new Mark(document.getElementById(editTag.value.mark_id))
      markEl.unmark()
      selectedTextList.value.splice(
        selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id),
        1
      )
    }
    marker.markRanges(
      [
        {
          start: isReset ? editTag.value.start : selectedText.start,
          length: isReset ? editTag.value.content.length : selectedText.content.length,
        },
      ],
      {
        className: 'text-selected',
        element: 'span',
        each: (element: any) => {
          element.setAttribute('id', markId)
          element.style.borderBottom = `2px solid ${t.tag_color}`
          element.style.color = t.tag_color
          element.style.userSelect = 'none'
          element.style.paddingBottom = '6px'
          element.onclick = function (e: any) {
            e.preventDefault()
            if (!e.target.id) return
            const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300
            const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any
            const { mark_content, tag_id, start, end } = item || {}
            editTag.value = {
              visible: true,
              top: e.offsetY + 40,
              left: e.offsetX,
              mark_id: e.target.id,
              content: mark_content || '',
              tag_id: tag_id || '',
              start: start,
              end: end,
            }
            tagInfo.value = {
              visible: false,
              top: e.offsetY + 40,
              left: left,
            }
          }
        },
      }
    )
    selectedTextList.value.push({
      tag_color,
      tag_name,
      tag_id,
      start: isReset ? editTag.value.start : selectedText.start,
      end: isReset ? editTag.value.end : selectedText.end,
      mark_content: isReset ? editTag.value.content : selectedText.content,
      mark_id: markId,
    })
  }

清空标记

image.png

const handleAllDelete = () => {
    selectedTextList.value = []
    const marker = new Mark(document.getElementById('text-container'))
    marker.unmark()
  }

完整代码

<script setup lang="ts">
  import { ref, onMounted, reactive } from 'vue'
  import Mark from 'mark.js'
  import { nanoid } from 'nanoid'

  type TTag = {
    tag_name: string
    tag_id: string
    tag_color: string
  }

  type TSelectText = {
    tag_id: string
    tag_name: string
    tag_color: string
    start: number
    end: number
    mark_content: string
    mark_id: string
  }

  const TAG_WIDTH = 280

  const selectedTextList = ref<TSelectText[]>([])

  const selectedText = reactive({
    start: 0,
    end: 0,
    content: '',
  })

  const markContent = ref(
    '这是标注的内容有业绩还是我我很快就很快就开完如突然好几个地方各级很大功夫数据库二极管捍卫国家和我回家很晚十九世纪俄国激活工具和丈母娘环境和颠覆国家的高房价奥苏爱哦因为i以太网图的还是觉得好看啊空间函数调用加快速度还是饥渴的发货可是磕碰日俄和那那么会就开始开会的数据库和也会觉得讲故事的而黄金九二额呵呵三角函数的吧合乎实际的和尽快核实当升科技看交互的接口和送二ui为人开朗少女都被你们进货金额麦当娜表面上的'
  )

  const tagInfo = ref({
    visible: false,
    top: 0,
    left: 0,
  })

  const editTag = ref({
    visible: false,
    top: 0,
    left: 0,
    mark_id: '',
    content: '',
    tag_id: '',
    start: 0,
    end: 0,
  })

  const tagList: TTag[] = [
    {
      tag_name: '标签一',
      tag_color: `#DE050CFF`,
      tag_id: 'tag_id1',
    },
    {
      tag_name: '标签二',
      tag_color: `#6ADE05FF`,
      tag_id: 'tag_id2',
    },
    {
      tag_name: '标签三',
      tag_color: `#DE058BFF`,
      tag_id: 'tag_id3',
    },
    {
      tag_name: '标签四',
      tag_color: `#9205DEFF`,
      tag_id: 'tag_id4',
    },
    {
      tag_name: '标签五',
      tag_color: `#DE5F05FF`,
      tag_id: 'tag_id5',
    },
  ]

  const handleAllDelete = () => {
    selectedTextList.value = []
    const marker = new Mark(document.getElementById('text-container'))
    marker.unmark()
  }

  const handleCancel = () => {
    if (!editTag.value.mark_id) return
    const markEl = new Mark(document.getElementById(editTag.value.mark_id))
    markEl.unmark()
    selectedTextList.value.splice(
      selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id),
      1
    )
    tagInfo.value = {
      visible: false,
      top: 0,
      left: 0,
    }
    resetEditTag()
  }

  const handleReset = () => {
    editTag.value.visible = false
    tagInfo.value.visible = true
  }

  const handleSave = () => {
    console.log('标注的数据', selectedTextList.value)
  }

  const handleSelectLabel = (t: TTag) => {
    const { tag_color, tag_name, tag_id } = t
    tagInfo.value.visible = false
    const marker = new Mark(document.getElementById('text-container'))
    const markId = nanoid(10)
    const isReset = selectedTextList.value?.map((j) => j.mark_id).includes(editTag.value.mark_id)
      ? 1
      : 0 // 1:重选 0:新增
    if (isReset) {
      //如若重选,则删除后再新增标签
      const markEl = new Mark(document.getElementById(editTag.value.mark_id))
      markEl.unmark()
      selectedTextList.value.splice(
        selectedTextList.value?.findIndex((t) => t.mark_id == editTag.value.mark_id),
        1
      )
    }
    marker.markRanges(
      [
        {
          start: isReset ? editTag.value.start : selectedText.start,
          length: isReset ? editTag.value.content.length : selectedText.content.length,
        },
      ],
      {
        className: 'text-selected',
        element: 'span',
        each: (element: any) => {
          element.setAttribute('id', markId)
          element.style.borderBottom = `2px solid ${t.tag_color}`
          element.style.color = t.tag_color
          element.style.userSelect = 'none'
          element.style.paddingBottom = '6px'
          element.onclick = function (e: any) {
            e.preventDefault()
            if (!e.target.id) return
            const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300
            const item = selectedTextList.value?.find?.((t) => t.mark_id == e.target.id) as any
            const { mark_content, tag_id, start, end } = item || {}
            editTag.value = {
              visible: true,
              top: e.offsetY + 40,
              left: e.offsetX,
              mark_id: e.target.id,
              content: mark_content || '',
              tag_id: tag_id || '',
              start: start,
              end: end,
            }
            tagInfo.value = {
              visible: false,
              top: e.offsetY + 40,
              left: left,
            }
          }
        },
      }
    )
    selectedTextList.value.push({
      tag_color,
      tag_name,
      tag_id,
      start: isReset ? editTag.value.start : selectedText.start,
      end: isReset ? editTag.value.end : selectedText.end,
      mark_content: isReset ? editTag.value.content : selectedText.content,
      mark_id: markId,
    })
  }

  /**
   * 获取选取的文字数据
   */
  const getSelectedTextData = () => {
    const select = window?.getSelection() as any
    const nodeValue = select.focusNode?.nodeValue
    const anchorOffset = select.anchorOffset
    const focusOffset = select.focusOffset
    const nodeValueSatrtIndex = markContent.value?.indexOf(nodeValue)
    selectedText.content = select.toString()
    if (anchorOffset < focusOffset) {
      //从左到右标注
      selectedText.start = nodeValueSatrtIndex + anchorOffset
      selectedText.end = nodeValueSatrtIndex + focusOffset
    } else {
      //从右到左
      selectedText.start = nodeValueSatrtIndex + focusOffset
      selectedText.end = nodeValueSatrtIndex + anchorOffset
    }
  }

  const resetEditTag = () => {
    editTag.value = {
      visible: false,
      top: 0,
      left: 0,
      mark_id: '',
      content: '',
      tag_id: '',
      start: 0,
      end: 0,
    }
  }

  const drawMark = () => {
    //模拟后端返回的数据
    const res = [
      {
        start: 2, //必备
        end: 6,
        tag_color: '#DE050CFF',
        tag_id: 'tag_id1',
        tag_name: '标签一',
        mark_content: '标注的内容',
        mark_id: 'mark_id1',
      },
      {
        start: 39,
        end: 41,
        tag_color: '#6ADE05FF',
        tag_id: 'tag_id2',
        tag_name: '标签二',
        mark_content: '二极管',
        mark_id: 'mark_id2',
      },
      {
        start: 58,
        end: 61,
        tag_color: '#DE058BFF',
        tag_id: 'tag_id3',
        tag_name: '标签三',
        mark_content: '激活工具',
        mark_id: 'mark_id3',
      },
    ]
    selectedTextList.value = res?.map((t) => ({
      tag_id: t.tag_id,
      tag_name: t.tag_name,
      tag_color: t.tag_color,
      start: t.start,
      end: t.end,
      mark_content: t.mark_content,
      mark_id: t.mark_id,
    }))
    const markList =
      selectedTextList.value?.map((j) => ({
        ...j,
        start: j.start, //必备
        length: j.end - j.start + 1, //必备
      })) || []
    const marker = new Mark(document.getElementById('text-container'))
    markList?.forEach?.(function (m: any) {
      marker.markRanges([m], {
        element: 'span',
        className: 'text-selected',
        each: (element: any) => {
          element.setAttribute('id', m.mark_id)
          element.style.borderBottom = `2px solid ${m.tag_color}`
          element.style.color = m.tag_color
          element.style.userSelect = 'none'
          element.style.paddingBottom = '6px'
          element.onclick = function (e: any) {
            console.log('cccccc', m)
            const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300
            editTag.value = {
              visible: true,
              top: e.offsetY + 40,
              left: e.offsetX,
              mark_id: m.mark_id,
              content: m.mark_content,
              tag_id: m.tag_id,
              start: m.start,
              end: m.end,
            }
            tagInfo.value = {
              visible: false,
              top: e.offsetY + 40,
              left: left,
            }
          }
        },
      })
    })
  }

  //页面初始化
  onMounted(() => {
    const el = document.getElementById('text-container')
    //鼠标抬起
    el?.addEventListener('mouseup', (e) => {
      const text = window?.getSelection()?.toString() || ''
      if (text.length > 0) {
        const left = e.offsetX < TAG_WIDTH ? 0 : e.offsetX - 300
        tagInfo.value = {
          visible: true,
          top: e.offsetY + 40,
          left: left,
        }
        getSelectedTextData()
      } else {
        tagInfo.value.visible = false
      }
      //清空重选/取消数据
      resetEditTag()
    })
    //从后端获取标注数据,进行初始化标注
    drawMark()
  })
</script>

<template>
  <header>
    <n-button
      type="primary"
      :disabled="selectedTextList.length == 0 ? true : false"
      ghost
      @click="handleAllDelete"
    >
      清空标记
    </n-button>
    <n-button
      type="primary"
      :disabled="selectedTextList.length == 0 ? true : false"
      @click="handleSave"
    >
      保存
    </n-button>
  </header>
  <main>
    <div id="text-container" class="text">
      {{ markContent }}
    </div>
    <!-- 标签选择 -->
    <div
      v-if="tagInfo.visible && tagList.length > 0"
      :class="['tag-box p-4 ']"
      :style="{ top: tagInfo.top + 'px', left: tagInfo.left + 'px' }"
    >
      <div v-for="i in tagList" :key="i.tag_id" class="tag-name" @click="handleSelectLabel(i)">
        <n-space>
          <p>{{ i.tag_name }}</p>
          <n-button v-if="i.tag_id == editTag.tag_id" text type="primary"></n-button>
        </n-space>
        <div
          :class="['w-4 h-4']"
          :style="{
            background: i.tag_color,
          }"
        ></div>
      </div>
    </div>
    <!-- 重选/取消 -->
    <div
      v-if="editTag.visible"
      class="edit-tag"
      :style="{ top: editTag.top + 'px', left: editTag.left + 'px' }"
    >
      <div class="py-1 bg-gray-100 text-center" @click="handleCancel">取 消</div>
      <div class="py-1 bg-gray-100 mt-2 text-center" @click="handleReset">重 选</div>
    </div>
  </main>
</template>

<style lang="less" scoped>
  header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 0 24px;
    height: 80px;
    border-bottom: 1px solid #e5e7eb;
    user-select: none;
    background: #fff;
  }

  main {
    background: #fff;
    margin: 24px;
    height: 80vh;
    padding: 24px;
    overflow-y: auto;
    position: relative;
    box-shadow: 0 3px 8px 0 rgb(0 0 0 / 13%);
    .text {
      color: #333;
      font-weight: 500;
      font-size: 16px;
      line-height: 50px;
    }
    .tag-box {
      position: absolute;
      z-index: 10;
      width: 280px;
      max-height: 40vh;
      overflow-y: auto;
      background: #fff;
      border-radius: 4px;
      box-shadow: 0 9px 28px 8px rgb(0 0 0 / 3%), 0 6px 16px 4px rgb(0 0 0 / 9%),
        0 3px 6px -2px rgb(0 0 0 / 20%);
      user-select: none;
      .tag-name {
        width: 100%;
        background: rgba(243, 244, 246, var(--tw-bg-opacity));
        font-size: 14px;
        cursor: pointer;
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 4px 8px;
        margin-top: 8px;
      }
      .tag-name:nth-of-type(1) {
        margin-top: 0;
      }
    }
    .edit-tag {
      position: absolute;
      z-index: 20;
      padding: 16px;
      cursor: pointer;
      width: 100px;
      background: #fff;
      border-radius: 4px;
      box-shadow: 0 9px 28px 8px rgb(0 0 0 / 3%), 0 6px 16px 4px rgb(0 0 0 / 9%),
        0 3px 6px -2px rgb(0 0 0 / 20%);
      user-select: none;
    }
    ::selection {
      background: rgb(51 51 51 / 20%);
    }
  }
</style>

结束语

目前功能实现比较简单,还有很多发挥的空间,先小小的记录一下,最后~,预祝大家,双节快乐!!

markjs

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

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

相关文章

TensorFlow入门(三、TensorFlow模型的运行机制)

TensorFlow通过"图"和会话的方式分离了计算的定义和执行,即它的运行机制是"定义"与"运行"相分离的。从操作层面可以把它抽象成两种:模型构建和模型运行。 TensorFlow模型中的几个概念: ①张量(tensor):数据,即某一类型的多维数组 ②变量(Vari…

Wi-Fi直连分享:Android设备间的高速连接

Wi-Fi直连分享&#xff1a;Android设备间的高速连接 引言 随着无线局域网&#xff08;Wi-Fi&#xff09;的普及和发展&#xff0c;使用Wi-Fi直连技术&#xff08;P2P&#xff09;在没有中间接入点的情况下实现设备间直接互联成为可能。通过Wi-Fi直连&#xff0c;具备相应硬件…

链动2+1模式:如何用二级分销打造高效团队,实现销量突破

你是否想要拥有一个高效的团队&#xff0c;让你的销量快速提升&#xff1f;你是否厌倦了传统的多层级分销模式&#xff0c;觉得它太复杂、太难管理、太不合规&#xff1f;你是否想要找到一种简单、合理、合法的商业模式&#xff0c;让你的收益稳定、可持续、可复制&#xff1f;…

【深度学习实验】卷积神经网络(三):自定义二维卷积神经网络:步长和填充、输入输出通道

目录 一、实验介绍 二、实验环境 1. 配置虚拟环境 2. 库版本介绍 三、实验内容 0. 导入必要的工具包 1. 步长、填充 a. 二维互相关运算&#xff08;corr2d&#xff09; b. 二维卷积层类&#xff08;Conv2D&#xff09; c. 模型测试 d. 代码整合 2. 输入输出通道 a…

前缀树-Trie树

前缀树—Trie树&#xff0c;也叫作“单词查找树”、“字典树” 它属于多叉树结构&#xff0c;典型应用是用于统计&#xff0c;排序和保存大量的字符串&#xff08;但不仅限于字符串&#xff09;&#xff0c;所以经常被搜索引擎系统用于文本词频统计。它的优点是&#xff1a;利…

【前段基础入门之】=>玩转【CSS】开篇章!

目录 CSS 的简介&#xff1a;CSS的编写位置行内样式内部样式外部样式 样式表的优先级CSS语法规范&#xff1a; 总结&#xff1a; CSS 的简介&#xff1a; 层叠样式表&#xff08;Cascading Style Sheets&#xff0c;缩写为 CSS&#xff09;是一种样式表语言&#xff0c;用来描述…

前端项目练习(练习-007-typescript-02)

学习前&#xff0c;首先&#xff0c;创建一个web-007项目&#xff0c;内容和web-006一样。&#xff08;注意将package.json中的name改为web-007&#xff09; 前面的例子&#xff0c;我们使用了nodejswebpack&#xff0c;成功创建了包含html&#xff0c;ts&#xff0c;css三个文…

【.net core】使用nssm发布WEB项目

nssm下载地址&#xff1a;NSSM - the Non-Sucking Service Manager 配置方式 修改服务在nssm工具下输入命令:nssm edit jntyjr 其中 jntyjr为添加服务时设置的Service name nssm可以设置任何以参数启动的应用程序以服务形式启动,通过设置参数内容启动服务 以上配置等同于执行…

ReferenceError: primordials is not defined错误解决

问题场景&#xff1a; 从github上拉了一个项目&#xff0c;想要学习一下&#xff0c;在起服务的时候出现了这个问题。 造成的原因&#xff1a; gulp 与 node 版本起冲突。 1&#xff09;首先&#xff0c;安装 gulp&#xff0c;查看版本&#xff1b; npm install gulp -g g…

如何设计科研问卷?

问卷研究法的最大特点在于能在较短时间内调查很多研究对象取得大量的资料&#xff0c;并能对资料进行数量化处理&#xff0c;经济省时&#xff0c;因此是教育研究中使用频率较高、用途较广泛的一种研究方法。问卷研究法的关键在于设计一份信度、效度较高&#xff0c;内容合理的…

二维码怎么分解成链接?线上快速解码教学

怎么分解二维码呢&#xff1f;有些时候我们需要将二维码图片分解成链接使用&#xff0c;所以想要使用解码功能一般都需要通过二维码生成器工具来完成。那么如何在线将二维码分解成链接呢&#xff0c;可能有些小伙伴还不知道怎么操作&#xff0c;下面就给大家分享一下免费二维码…

较真儿学源码系列-PowerJob时间轮源码分析

PowerJob版本&#xff1a;4.3.2-main。 之前分析过PowerJob的启动流程源码&#xff0c;感兴趣的可以查看《较真儿学源码系列-PowerJob启动流程源码分析》 1 简介 试想一下&#xff0c;如果此时有一个需要延迟3s执行的任务&#xff0c;你会怎么实现呢&#xff1f;一种常规的思路…

洗地机哪个牌子好用又实惠?口碑最好的洗地机推荐

智能技术飞速发展的时代&#xff0c;扫地机器人这类智能家电其实也在顺应潮流和用户需求&#xff0c;不断更新迭代。暂且不说市面上现有多少个洗地机品牌&#xff0c;单单一个洗地机品牌旗下&#xff0c;其实每年都会有多个系列的新品亮相&#xff0c;我们面对的选择多了&#…

Python交叉验证实现

目录 <font colorblue size4 face"楷体">HoldOut 交叉验证<font colorred size4 face"楷体">K 折交叉验证<font colorblue size4 face"楷体">分层 K 折交叉验证<font colorblue size4 face"楷体">Leave P Out…

融云 CallPlus + X,通话场景一站式解决方案

融云近期上线的 CallPlus SDK&#xff0c;针对音视频呼叫场景单独设计后端服务 Call Server&#xff0c;信令延时低至 150ms&#xff0c;确保各端计时准确、一致&#xff1b;上线了音视频通话互转、灵活的多人通话、通话记录管理能力等功能。关注【融云全球互联网通信云】了解更…

掌动智能兼容性测试有哪些优势

兼容性测试为企业带来市场竞争优势&#xff0c;并提高用户满意度。在软件开发过程中&#xff0c;将兼容性测试作为一个重要的环节&#xff0c;将为企业的成功和用户满意度打下坚实的基础。那么&#xff0c;掌动智能兼容性测试的具体优势是什么?下面&#xff0c;就来看看具体介…

【面试题】说说你对 async和await 理解

前端面试题库 &#xff08;面试必备&#xff09; 推荐&#xff1a;★★★★★ 地址&#xff1a;前端面试题库 表妹一键制作自己的五星红旗国庆头像&#xff0c;超好看 async await详解 原理&#xff1a; async声明该函数是异步的&#xff0c;且该函数会返回一个…

比例导引详解(Proportional navigation guidance,PNG)-及Python程序

模型算法推导 比例导引是一种制导算法&#xff0c;其经典程度相当于控制器中的PID&#xff0c;在本文中&#xff0c;只对其二维平面的情况做分析&#xff0c;考虑一个拦截弹拦截机动目标&#xff08;固定目标相当于目标速度为0&#xff09;&#xff0c;其运动如下图所示&#…

变配电智能化系统:提高效率与安全性

随着科技的发展&#xff0c;电力系统正在逐步向智能化、数字化方向转型。变配电系统作为电力系统的重要组成部分&#xff0c;其智能化水平直接影响着电力系统的运行效率和稳定性。 一、系统概述 力安科技变配电智能化系统是一种采用先进技术&#xff0c;实现对变配电设…

DD5 进制转换

目录 一、题目 二、分析 三、代码 一、题目 进制转换_牛客题霸_牛客网 二、分析 三、代码 #include <iostream> #include <vector> #include <string> using namespace std; string Greater_than_Ten(int digit)//余数大于等于10的时候转换成对应的字母…