Taro react 160 行實現@別人功能

語言: CN / TW / HK

本篇文章實現一個簡單的提及功能,在輸入 @ 後會彈出動作面板選擇人員,選擇後顯示在正文裡,並在最後獲取正文和提及人員資訊。最終效果如下:

提及功能最終效果.gif

目前暫不包含刪除提及人的部分字元後直接刪除完整的提及人@的功能,有思路的歡迎交流。

需求分析

這個功能最讓人頭疼的地方在於小程式文字域的輸入回撥提供的資訊過少,沒辦法直接判斷輸入位置,在 Taro 文件中提到 TextArea 的 onInput 事件會返回文字內容 value、游標位置 cursor、按鍵型別 keyCode 三個值。但是在我的實際測試中,只拿到了 value

image.png

image.png

這是什麼人間疾苦。

所以說,我們需要先搞一個演算法來找到當前新輸入了什麼以及在哪輸入的。

之後的問題就好辦了,如果輸入的是 @,就拉起動作面板,選擇某個人後觸發回撥,在正文裡插入這個人的名字,然後把對應的資訊存到另一個 state 方便後續操作就可以了。

元件實現

這裡先貼出來完整程式碼,有需要的可以直接拿去用,元件庫是 taro-vant

MentionTextarea\index.tsx

```tsx import { useReady } from "@tarojs/taro"; import { FC, forwardRef, useImperativeHandle, useRef, Ref, useState } from "react"; import { View, Textarea } from "@tarojs/components"; import { ActionSheet, Search, Cell, Notify } from "@antmjs/vantui"; import { debounce, DebouncedFunc } from 'lodash'; import { getQuestionUser } from "@/services/engineerCarbonCopyProblem";

export interface Props { defaultContent?: string className?: string ref?: Ref }

/ * 抄送問題人員 */ export interface QuestionUser { id: number originId: number name: string tel: string / * 崗位名 */ type: string }

export interface MentionTextareaRef { / * 獲取填寫的內容 * @returns [正文內容,抄送人員列表] */ getContent: () => [string, QuestionUser[]] / * 設定填寫的內容 */ setContent: (content: string) => void }

/* * 找到新舊字串的變化 * * @param oldStr 老字串 * @param newStr 新字串 * @returns [變更的字元(刪除則為空), 變更的位置] / const findDiffChar = (oldStr: string, newStr: string): [string, number] => { const arr1 = oldStr.split(""); const arr2 = newStr.split("");

// 用較長的內容進行遍歷 const mapStr = arr1.length > arr2.length ? arr1 : arr2;

for (let i = 0; i < mapStr.length; i++) { if (arr1[i] === arr2[i]) continue; // 如果原始字串長的話,說明是刪除了字元 if (oldStr.length > newStr.length) return ['', i]; // 否則(原始字串短)說明是插入了字元 return [mapStr[i], i]; }

return ['', -1]; };

/* * 附帶提及功能的文字域 * 非受控元件,通過 ref 來獲取文字域的內容 * 用法見 src\pages\engineerReply\index.tsx / export const MentionTextarea: FC = forwardRef((props, ref) => { const { className, defaultContent= '' } = props;

// 輸入框引用 const textareaRef = useRef(null); // 上一個輸入的內容 const lastContentRef = useRef(""); // 當前編輯的位置 const editInfoRef = useRef<[string, number]>(['', 0]); // 是否展示動作面板 const [showActionPanel, setShowActionPanel] = useState(false); // 抄送人待選列表 const [userList, setUserList] = useState([]); // 當前選中的所有抄送人 const selectedUsersRef = useRef([]); // 正文內容 const [content, setContent] = useState(defaultContent); // 搜尋防抖 const searchDebounce = useRef<DebouncedFunc<(keywords: any) => Promise>>();

// 頁面就緒後註冊搜尋防抖 useReady(() => { const onSearch = async (keywords) => { const resp = await getQuestionUser(keywords) if (!resp?.success) { Notify.show({ type: 'danger', message: resp?.message || '無法載入抄送人列表' }); return; }

  setUserList(resp?.data || []);
}

searchDebounce.current = debounce(onSearch, 300)

})

// 內容變更時觸發的回撥 // 會檢查有沒有輸入 @ const onFieldInput = async (e) => { const [diffChar, diffIndex] = findDiffChar(lastContentRef.current, e.detail.value);

// 輸入了 @ 就開啟抄送人面板
if (diffChar === '@') {
  searchDebounce.current?.('');
  setShowActionPanel(true);
}

setContent(e.detail.value)
lastContentRef.current = e.detail.value
editInfoRef.current = [diffChar, diffIndex]

}

// 在抄送人面板裡選擇抄送人 const onSelectUser = (user: QuestionUser) => { console.log(editInfoRef.current)

// 把抄送人插入到當前游標位置
setContent(oldContent => {
  const diffIndex = editInfoRef.current[1];
  const newContent = oldContent.slice(0, diffIndex + 1) + user.name + oldContent.slice(diffIndex + 1);
  // 插入抄送人後要及時更新內容,防止下次輸入內容時輸入位置定位錯誤
  lastContentRef.current = newContent;
  return newContent;
});

// 把抄送人新增到選中列表
selectedUsersRef.current = [...new Set([...selectedUsersRef.current, user])];
setShowActionPanel(false);
(textareaRef.current?.firstChild as HTMLTextAreaElement).focus()

}

// 獲取抄送資訊 const getContent = (): [string, QuestionUser[]] => { const selectedUser = selectedUsersRef.current.filter(user => content.includes(@${user.name})); return [content, selectedUser] }

useImperativeHandle(ref, () => ({ setContent, getContent }))

// 渲染抄送人專案 const renderUser = (item: QuestionUser) => { return ( onSelectUser(item)} title={item.name} value={item.type} /> ) }

return (