vue3 + mark.js | 實現文字標註功能

来源:https://www.cnblogs.com/yangyukeke/archive/2023/09/26/17730681.html
-Advertisement-
Play Games

頁面效果 具體實現 新增 1、監聽滑鼠抬起事件,通過window.getSelection()方法獲取滑鼠用戶選擇的文本範圍或游標的當前位置。 2、通過 選中的文字長度是否大於0或window.getSelection().isCollapsed (返回一個布爾值用於描述選區的起始點和終止點是否位於 ...


頁面效果

具體實現

新增

  • 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


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 前言 不想看可以跳過前言部分,教程在下幾章。 ​ 最新搬到新校園,寢室的校園網可使用網線連接。雖然撥號的寬頻賬號和密碼已經自動記錄,但啟動電腦並登入電腦時仍需要手動進入設置並點擊自動登錄,就像鞋子里的小石子,雖然腳不會出血,但就是難受。於是開始網上搜索教程win11自動撥號。結合了兩篇文章實現了開機 ...
  • MySQL 高級(進階) SQL 語句 use gy; create table location (Region char(20),Store_Name char(20)); insert into location values('East','Boston'); insert into loc ...
  • 一、背景 在預發環境中,由消息驅動最終觸發執行事務來寫庫存,但是導致MySQL發生死鎖,寫庫存失敗。 com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: rpc error: code = Aborted desc = ...
  • 為了標識一段數據,通常我們會為其指定一個唯一id,比如利用MySQL資料庫中的自增主鍵。 但是當數據量非常大時,僅靠資料庫的自增主鍵是遠遠不夠的,並且對於分散式資料庫只依賴MySQL的自增id無法滿足全局唯一的需求。因此,產生了多種解決方案,如UUID,SnowFlake等。下文將介紹Vitess是... ...
  • 一直以來,大數據量一直是爆炸性增長,每天幾十 TB 的數據增量已經非常常見,但雲存儲相對來說還是不便宜的。眾多雲上的大數據用戶特別希望可以非常簡單快速的將文件移動到更實惠的 S3、OSS 上進行保存,這篇文章就來介紹如何使用 SeaTunnel 來進行到 OSS 的數據同步。 首先簡要介紹一下 Ap ...
  • 奇富科技(原360數科)是人工智慧驅動的信貸科技服務平臺,致力於憑藉智能服務、AI研究及應用、安全科技,賦能金融機構提質增效,助推普惠金融高質量發展,讓更多人享受到安全便捷的金融科技服務。作為國內領先的信貸科技服務品牌,累計註冊用戶數2億多。 奇富科技之前使用的是自研的任務調度框架,基於Python ...
  • 本篇作為 OPPO主題組件調試與預覽 文檔的補充,因為它真的很簡單而且太老,一些命令已發生變化😪 此圖片來自官網 一、調試前準備 1. PC 端下載 adb命令工具 下載 下載地址 https://adbdownload.com/,或從其他地方下載也可 解壓,放在你想放的文件夾下 配置環境變數 右 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 前兩天我的新同事告訴我一個困擾著他的問題,就是低代碼平臺中存在很多模塊,這些模塊的渲染是由模塊自身處理的,簡言之就是組件請求了自己的數據,一個兩個模塊還好,要是一次請求了幾十個模塊,就會出現請求阻塞的問題,而且模塊的請求都特別大。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...