<!-- * @FileDescription: 评论输入框 * @Date: 2023-06-06 * @LastEditTime: 2023-06-06 --> <script setup lang="ts"> import { ref, reactive, nextTick } from 'vue'; const inputRef = ref<any>(null); const showEmoji = ref(false); const emojiList = [ { title: '分组1', children: [ { name: 'strawberry', label: '草莓', url: require('./emojis/strawberry.png'), }, { name: 'watermelon', label: '西瓜', url: require('./emojis/watermelon.png'), }, ], }, { title: '分组2', children: [ { name: 'kiss', label: '亲亲', url: require('./emojis/kiss.png'), }, { name: 'se', label: '色', url: require('./emojis/se.png'), }, { name: 'angel', label: '天使', url: require('./emojis/angel.png'), }, { name: 'reserved', label: '矜持', url: require('./emojis/reserved.png'), }, { name: 'smile', label: '微笑', url: require('./emojis/smile.png'), }, { name: 'oh', label: '哦', url: require('./emojis/oh.png'), }, ], }, { title: '应用', children: [ { name: 'QQ', label: 'QQ', url: require('./emojis/QQ.png'), }, { name: 'WeChat', label: '微信', url: require('./emojis/WeChat.png'), }, ], }, ]; const state = reactive({ commentContent: '', commentContentByUI: '', canClickIcon: false, }); function willShow() { showEmoji.value = !showEmoji.value; } // 点击表情 function clikeEmoji(item: any) { // 编辑框设置焦点 const inputDom = inputRef.value; inputDom.focus(); const selection = document.getSelection() as Selection; const rangeCount = selection.rangeCount; if (rangeCount > 0) { // 设置最后光标对象 let range = selection.getRangeAt(0); range.deleteContents(); // 从 Document 中移除 Range 内容 // 创建标签 const img = document.createElement('img'); img.setAttribute('class', 'input-img'); img.src = item.url; img.alt = item.label; img.style.height = '20px'; img.style.width = '20px'; img.style.transform = 'translateY(5px)'; // 在 Range 开头插入一个节点 range.insertNode(img); range = range.cloneRange(); range.setStartAfter(img); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } showEmoji.value = false; } // 发表评论 function sendComment() { const re = formatWord(getDomValue(inputRef.value)); state.commentContent = re; const strlist = textJoin(re, []); // console.log('NB', strlist); const div = document.createElement('div'); div.style.lineHeight = '22px'; for (const ite of strlist) { if (ite.isHtml) { div.appendChild(ite.html); } else { const list = lineFeed(ite.text, []); for (const n of list) { if (n.isHtml) { div.appendChild(n.html); } else { const node = document.createTextNode(n.text); div.appendChild(node); } } } } nextTick(() => { const outHtml = div.outerHTML; state.commentContentByUI = outHtml; }); } function textJoin(str: string, resArr: any[]) { const startIndex = str.indexOf('['); if (startIndex > -1) { let afterStr = str.substring(startIndex + 1); let endIndex = afterStr.indexOf(']'); if (endIndex > -1) { const imgKey = afterStr.substring(0, endIndex); const imgData = findImg(imgKey); if (imgData) { const img = document.createElement('img'); img.setAttribute('class', 'input-img'); img.src = imgData.url; img.alt = imgData.label; img.style.height = '26px'; img.style.width = '26px'; img.style.transform = 'translateY(5px)'; resArr.push({ text: str.substring(0, startIndex), isHtml: false, }); resArr.push({ html: img, isHtml: true, }); textJoin(afterStr.substring(endIndex + 1), resArr); } else { resArr.push({ text: str.substring(0, endIndex + 1), isHtml: false, }); textJoin(str.substring(endIndex + 1), resArr); } } else { resArr.push({ text: str, isHtml: false, }); } } else { resArr.push({ text: str, isHtml: false, }); } return resArr; } function findImg(imgk: string) { let data = null as any; let imgLsit = [] as any[]; for (const i of emojiList) { imgLsit.push(...i.children); } for (const i of imgLsit) { if (i.label === imgk) { data = i; break; } } return data; } function inputFocus() { // console.log('111'); state.canClickIcon = true; } function inputBlur() { // state.canClickIcon = false; } // 获取纯文本内容 function getDomValue(elem: any) { let res = ''; Array.from(elem.childNodes).forEach((child: any) => { if (child.nodeName === '#text') { res += child.nodeValue; } else if (child.nodeName === 'BR') { // res += '\n'; res += '<br>'; } else if (child.nodeName === 'BUTTON') { res += getDomValue(child); } else if (child.nodeName === 'IMG') { res += '[' + child.alt + ']'; } else if (child.nodeName === 'DIV') { // res += '\n' + getDomValue(child); res += '<br>' + getDomValue(child); } }); return res; } // 换行标签转换 function lineFeed(val: string, arr: any[]) { const index = val.indexOf('<br>'); if (index > -1) { const br = document.createElement('br'); arr.push({ isHtml: false, text: val.substring(0, index), }); arr.push({ isHtml: true, html: br, }); lineFeed(val.substring(index + 4), arr); } else { arr.push({ isHtml: false, text: val, }); } return arr; } // 文本换行 function formatWord(str: string) { return str.replace(/\\n/g, '<br>'); } </script> <template> <div class="column justify-center items-center container-height"> <div style="width: 460px"> <div class="avatar-box"> <q-avatar size="38px"> <img src="https://cdn.quasar.dev/img/avatar.png" /> </q-avatar> <span class="name">皮皮虾</span> </div> <div class="input-box"> <div class="input-sty" contenteditable="true" ref="inputRef" placeholder="输入评论" @focus="inputFocus" @blur="inputBlur" ></div> <div class="operation"> <div class="icon-box"> <q-icon name="mood" class="_icon" size="22px" @click="willShow" v-if="state.canClickIcon" /> <div class="emoji-box-fa" v-show="showEmoji" id="_emoji"> <div class="emoji-box"> <div class="group-item" v-for="i in emojiList" :key="i.title"> <div class="group-item-title">{{ i.title }}</div> <div class="childre-box"> <div class="item-box" v-for="chi in i.children" :key="chi.name" :title="chi.label" @click="clikeEmoji(chi)" > <img :src="chi.url" :alt="chi.label" /> </div> </div> </div> </div> </div> </div> <q-btn unelevated color="primary" label="发表评论" @click="sendComment" /> </div> </div> </div> <div style="width: 460px"> <div>评论内容:</div> <div v-html="state.commentContentByUI" class="comment-sty"></div> <div class="q-mt-lg">评论内容:</div> <div>{{ state.commentContent }}</div> </div> </div> </template> <style lang="scss" scoped> .input-sty { min-width: 200px; min-height: 80px; font-size: 14px; line-height: 22px; padding: 5px 8px; border: 1px solid #ddd; color: $gray-text; border-radius: 0.25rem; transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; // transform: translateY(-10px) scale(0.9); &:empty::before { content: attr(placeholder); color: $gray-light-text; } &:focus { color: #212529; background-color: #fff; outline: 0; border-color: $primary; box-shadow: 0 0 0 0.25rem $primary-focus-shadow; } } .name { color: rgba(0, 0, 0, 0.55); padding-left: 6px; } .avatar-box { margin-bottom: 4px; } .input-box { margin-left: 40px; } .operation { padding: 6px 0; display: flex; flex-direction: row; justify-content: space-between; } .icon-box { position: relative; color: rgba(0, 0, 0, 0.55); ._icon { color: inherit; &:hover { cursor: pointer; color: $primary; } } .emoji-box-fa { position: absolute; padding: 8px; border-radius: 6px; top: 26px; left: 0; width: 400px; height: 200px; z-index: 10; background: #fff; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 12px; } .emoji-box { width: 100%; height: 100%; overflow: auto; color: black; &:hover { cursor: default; } .group-item-title { font-size: 12px; color: $gray-text; } .childre-box { display: flex; flex-direction: row; flex-wrap: wrap; } .item-box { width: 40px; height: 40px; box-sizing: border-box; transition: all 0.25s; > img { width: 100%; height: 100%; } &:hover { cursor: pointer; transform: scale(1.2); } } } } .img { width: 40px; height: 40px; } .comment-sty { min-height: 80px; background: rgba(0, 0, 0, 0.05); border-radius: 0.25rem; padding: 5px 8px; color: $gray-text; } </style>