1. 程式人生 > 程式設計 >微信小程式純文字實現@功能

微信小程式純文字實現@功能

前言

大家肯定對@功能不陌生,在如今的各大社交軟體中它是一種不可或缺的功能。實現@人的功能並不複雜,只需將@人員的id傳給後端,後端下發通知即可。主要的複雜點在於一鍵刪除功能與變色功能,web端可以使用現成庫 caret.js 或者 At.js 來實現。但筆者需要在小程式中實現這個功能,而且在 textarea 標籤裡實現,當然@人名的變色功能自然而然就砍掉了。

準備工作

怎麼來實現一鍵刪除呢?首先想到對@人名前後用特殊符號標記+正則來實現,但結果不是很理想,擴充套件性也比較差,如果還要匹配話題之類的就得多寫一套程式碼,所以就試著找其他方法解決。發現 wx.getSelectedTextRange

可以獲取文字框聚焦時的游標,這樣就可以將@人員插入文字指定位置。文字框事件 @input 的可以獲取到變化的資料與位置,那就可以根據變化的位置與變化的資料來判斷是否命中@人員,@人員的位置可以通過計算獲取。

// bindinput事件返回值 
// value為變化後的值 cursor為變化的位置 keyCode為觸發的鍵值 
const {value,cursor,keyCode} = event.detail 
// 獲取游標位置,聚焦時生效 
wx.getSelectedTextRange({
  complete: res => {
    console.log('游標位置:',res.start,res.end)
  }
})

準備工作做好了就進入實踐環節,畢竟實踐是檢驗真理的唯一標準。設計圖呈現:通過點選@按鈕到人員列表頁面,選擇人員後返回,具體如下圖。這裡涉及頁面之間的通訊問題,可以通過狀態管理器、資料快取、獲取頁面棧設定資料等來實現,本例中使用資料快取。

微信小程式純文字實現@功能

資料組裝

從人員列表返回用 wx.navigateBack ,會觸發 onShow 這個生命週期,所以需要在 onShow 裡組裝@資料。獲取到的@人員根據游標位置對文字進行字串擷取組裝,若未獲取到游標位置則直接將@人員新增到文字末尾。然後對@人員資料、文字資料等進行備份,用於後續的計算。

initAtFn() {
    // 獲取@人員資料
    const me = this
    const initMemberList = wx.getStorageSync('atMemberList')
    const atMemberArr = initMemberList ? initMemberList : []
    // 賦值後清除@人員資料
    wx.removeStorageSync('atMemberList')
    // 獲取上一次游標的位置
    const preCursor = wx.getStorageSync('blurCursor') ? parseInt(wx.getStorageSync('blurCursor')) : me.content.length
    // 將 @人員資料 併入內容區域
    if (atMemberArr.length > 0) {
     // 獲取人員名稱
     const atMemberName = `@${atMemberArr[0].name}`
     // 如果上次游標有記錄 就根據游標分割字串 併入@人員名稱
     if (preCursor.toString().length !== me.content.length) {
      const start = me.content.substring(0,preCursor)
      const end = me.content.substring(preCursor)
      me.content = `${start}${atMemberName}${end}`
     } else {
      me.content += `${atMemberName}`
     }
     me.atArr = me.atArr.concat(atMemberArr) // 合併人員
     wx.setStorageSync('blurCursor',preCursor + atMemberName.length)
    }else {
     wx.setStorageSync('blurCursor',me.content.length)
    }
    me.focus = true
    me.copyContent = me.content
    me.executeArr = me.getAtMemberPosFn() // 獲取@人員位置
   }

計算@人員位置

對@人員陣列進行遍歷,計算@人員在文字中的位置區間。通過indexOf來獲取起點(這裡有一個缺陷,也是需要優化的點,當手動輸入的內容中有和@人員名字相同的欄位時,那麼位置靠前的那一個將會生效),終點為起點+名字長度。這裡有個問題:如果重複@相同的人員,刪除時怎麼區分呢?筆者想當然的使用了時間戳,結果發現在遍歷中使用時間戳並不準確,只有規規矩矩生成唯一值。

計算時收集了人員位置的最值區間,在這個範圍之外增減文字不會影響@人員的完整性。下面是程式碼:

getAtMemberPosFn() {
    const me = this
    const [tipArr,left,right] = [ [],[],[] ]
    // 根據@人員的陣列來匹配計算所處位置
    me.atArr.map(item => {
     const name = item.name
     const userId = item.userId
     // 此處有一個缺陷 如果手輸入的和獲取的@人名字相同 第一個會生效 第二個不會生效
     let start = me.copyContent.indexOf(name)
     
     if (tipArr.length > 0) {
      const _arr = tipArr.filter(v => v.name.includes(name))
      if (_arr.length > 0) {
       start = me.copyContent.indexOf(name,_arr[_arr.length - 1].end)
      }
     }
     
     const end = name.length + start // end
     left.push(start)
     right.push(end)
     // 獲取唯一標識 是用於重複@的區分
     const guid = me.createGuidFn()
     const tipObj = {
      start: start - 1,// @ - 1
      end,name,atName: `@${name}`,type: item.userId,userId: userId,code: guid
     }
     tipArr.push(tipObj)
    })
    
    // 獲取區間左右最值
    right.length > 0 ? me.maxAt = Math.max(...right) : me.maxAt = 0
    left.length > 0 ? me.minAt = Math.min(...left) : me.minAt = 0
    me.atArr = tipArr
    return tipArr
   }

一鍵刪除功能

@人員的位置區間已經計算出來了,接下來監聽輸入框的內容變化實現一鍵刪除功能,當輸入框文字內容變化,會觸發 @input 事件,它會返回變化後的值 value ,變化的位置 cursor ,我們將利用這兩個資料作為是否 命中@人員的判斷依據 。將情況分為以下幾種:

變化後的value為空,即清空了輸入框。

資料變化的游標位置大於@人員位置最值區間的最大值,即不影響@人員位置。

當資料變化影響@人員時,這裡對增加減少內容做了區分處理:

增加時,如果增加位置小於最值的最小值,則直接重新計算位置。如果增加值的位置命中@人員位置,則過濾掉失效人員,再重新計算。這裡需要注意,移動端輸入法會有一次性輸入多個字元,變化的位置不再是返回的游標位置,而是以游標位置減去變化前後資料的差值。

刪除時,獲取刪除的起始位置 (A,B) ,然後與@人員位置 (start,end) 作比較。 當 !(A < start || B > end) 時,則為命中,將命中的@人員過濾掉即可。

changeFn(txt) {
    const me = this
    const { value,keyCode } = txt.detail // 改變後的值,改變的位置,按鍵
     
    // 如果改變後的值為'',就直接返回
    if(!value) {
     me.content = value
     me.copyContent = value
     me.atArr = []
     return false
    }
    
    // 判斷值改變的增減
    const changeLen = value.length - me.copyContent.length
    // 值改變的游標位置 不影響@人員的則不管
    if (cursor > me.maxAt) {
     me.copyContent = me.content
     return false
    }
  
    // 判斷為 增加值
    if (changeLen > 0) {
     const addCursor = cursor - changeLen // 重新計算增加位置 防止移動端一次性貼上導致失效問題
     me.copyContent = me.content
     // 增加值的位置 小於左區間最值 則重新計算位置
     if(addCursor < me.minAt) {
      me.executeArr = me.getAtMemberPosFn()
      return false
     }
     
     me.executeArr.map(item => {
      const { start,end,code } = item
      if (addCursor < end && addCursor > start) {
       // 刪除命中人員,則該人員失效
       me.atArr = me.atArr.filter(v => v.code !== code)
      }
     })
     
     // 需要重新計算位置
     me.executeArr = me.getAtMemberPosFn()
    } else {
     let replaceStr = '' // 應被刪除的欄位
     const left = [] // 刪除左值集合
     const right = [] // 刪除右值集合
     const delLen = cursor - changeLen // 本身刪除的長度
     const deleteString = me.copyContent.substring(cursor,delLen) // 本身刪除的欄位 [cursor,changeLen)
     // 獲取應被刪除的左右位置
     function pushArrEvent(s,e) {
      left.push(s)
      right.push(e)
     }
  
     me.executeArr.map(item => {
      let { start,code } = item
      // D大 <= B小 || D小 >= B大
      // 命中部分為 刪除部分與@人員的交集
      if (!(delLen <= start || cursor >= end)) {
       // 命中判定,命中位置在名字區間 左邊/右邊/之間/或者多選中刪除的
       if (delLen <= end && cursor >= start) {
        pushArrEvent(start,end)
       } else {
        if (cursor > start) {
         if (delLen > end) {
          pushArrEvent(start,delLen)
         } else {
          pushArrEvent(start,end)
         }
        } else if (cursor < start) {
         if (delLen > end) {
          pushArrEvent(cursor,delLen)
         } else {
          pushArrEvent(cursor,end)
         }
        } else {
         pushArrEvent(cursor,delLen)
        }
       }
  
       // 獲取一鍵刪除區間 
       const del_left = Math.min(...left)
       const del_right = Math.max(...right)
       // 根據區間獲取一鍵刪除欄位
       replaceStr = me.copyContent.substring(del_left,del_right)
       // 刪除後的賦值
       me.content = me.copyContent.substring(0,del_left) + me.copyContent.substring(del_right)
       
       // @人員陣列生成
       me.atArr = me.atArr.filter(v => v.code !== code)
      }
     })
     // 執行完後 重新賦值計算
     me.copyContent = me.content
     me.executeArr = me.getAtMemberPosFn()
    }
   }

新增標籤

我們還差最後一步,那就是給@人名新增標籤,用於顯示時與一般文字做區分。這裡踩了一個坑,用正則替換時,如果名字與名字之間存在包含關係,則會失效,所以用記錄位置的方式來對文字進行擷取組裝。

submitTxtFn() {
    const copyTxt = this.content
    const arr = JSON.parse(JSON.stringify(this.atArr))
    const atUserIds = [...new Set(arr.map(v=>v.userId))] // 獲取@人員id
    let targetContent = ''
    let count = 0
    // 給@人員新增wxml標籤,此處用了jyf-Parser富文字解析外掛,href裡面的值用於點選傳參
    if(arr.length > 0) {
     arr.forEach((item,index)=>{
      let _tip = ''
      const txt = copyTxt.substring(count,item.start)
      // 加空格
      _tip = `${txt}<a class="link" href="${item.name}" rel="external nofollow" >${item.atName} </a>`
      targetContent += _tip
      // 處理最後一個標籤後面的文字
      if(index + 1 === arr.length) {
       if(item.end < copyTxt.length) {
        targetContent += copyTxt.substring(item.end)
       }
      }
      count = item.end
     })
    }else {
     targetContent = this.content
    }
    // 目標資料
    const targetObj = {
     content: targetContent,atIds: atUserIds
    }
    this.submitData = targetObj
    return targetObj
   }

以上就實現了純文字的@功能,通過計算位置來實現的優點是具有擴充套件性,比如一套程式碼可以實現#話題功能和@功能共存,只需加個type作為區分即可。缺點是一鍵刪除時體驗不是很好,並且刪除後不能控制游標位置,不能實現人員名稱變色等。雖然功能比較ZZ,但也比較有趣,所以就分享給大家,如果大家有更好的解決方案,評論區有請。

完整程式碼請移步 語雀

總結

到此這篇關於微信小程式純文字實現@功能的文章就介紹到這了,更多相關小程式@功能內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!