Commit bed64a7f by lijiabin

【需求 20331】 feat: 关于评论相关的加入表情和图片

parent c137a089
......@@ -326,6 +326,7 @@ export interface AddCommentDto {
articleId: number | string
content: string
pid?: number | string
imgUrl?: string
}
/**
......@@ -359,6 +360,7 @@ export interface CommentItemDto {
showComment: boolean
isExpand: boolean
childNum: number
imgUrl: string
}
/**
......
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1653463274928" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="23443" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M514.286299 2.505864h3.565417V895.785974h-3.565417z" fill="#EDA639" p-id="23444"></path><path d="M69.430967 447.365225h893.276081v3.55736H69.430967z" fill="#EDA639" p-id="23445"></path><path d="M517.811429 452.578389m-430.932074 0a430.932074 430.932074 0 1 0 861.864148 0 430.932074 430.932074 0 1 0-861.864148 0Z" fill="#FFE770" p-id="23446"></path><path d="M520.027225 493.522274v160.145663" fill="#FFE770" p-id="23447"></path><path d="M520.027225 675.024184a21.352218 21.352218 0 0 1-21.352218-21.356247v-160.145663a21.352218 21.352218 0 0 1 42.708465 0v160.145663a21.352218 21.352218 0 0 1-21.356247 21.356247z" fill="#6E6E96" p-id="23448"></path><path d="M300.917211 313.051716l62.279988-62.279988 62.279988 62.279988" fill="#FFE770" p-id="23449"></path><path d="M425.477187 334.403934a21.299845 21.299845 0 0 1-15.099644-6.252574L363.197199 280.971016 316.016855 328.15136a21.352218 21.352218 0 1 1-30.199288-30.199288l62.279988-62.279988a21.352218 21.352218 0 0 1 30.199288 0l62.279988 62.279988a21.352218 21.352218 0 0 1-15.099644 36.451862z" fill="#6E6E96" p-id="23450"></path><path d="M610.536457 313.051716l62.284017-62.279988 62.279988 62.279988" fill="#FFE770" p-id="23451"></path><path d="M735.100462 334.403934a21.299845 21.299845 0 0 1-15.099644-6.252574l-47.180344-47.180344-47.184373 47.180344a21.356247 21.356247 0 0 1-30.195259-30.199288l62.279988-62.279988a21.352218 21.352218 0 0 1 30.195259 0l62.279989 62.279988a21.352218 21.352218 0 0 1-15.095616 36.451862z" fill="#6E6E96" p-id="23452"></path><path d="M519.624353 45.947556c-3.831313 0-7.65054 0.068488-11.449623 0.173235 203.095851 5.785242 365.960901 172.231825 365.960901 376.72967 0 208.159953-168.746981 376.898877-376.906934 376.898877-208.155924 0-376.902905-168.738924-376.902906-376.898877a377.615989 377.615989 0 0 1 12.061989-94.924709 406.368966 406.368966 0 0 0-19.523179 124.777527c0 224.645477 182.114276 406.755723 406.759752 406.755724s406.755723-182.110247 406.755724-406.755724c0-224.645477-182.110247-406.755723-406.755724-406.755723z" fill="#FF9900" opacity=".24" p-id="23453"></path><path d="M517.811429 901.305321c-103.779837 0-204.98935-36.262512-284.979593-102.095831-78.906517-64.947001-133.733373-155.508606-154.380564-255.005914a451.740415 451.740415 0 0 1-9.366775-91.629216C69.084497 205.14647 270.383539 3.847428 517.811429 3.847428c247.42789 0 448.726932 201.299042 448.726932 448.726932 0 247.42789-201.299042 448.730961-448.726932 448.730961z m0-861.864149c-227.803993 0-413.137216 185.333223-413.137216 413.137217 0 28.43068 2.900679 56.821072 8.621461 84.397663 19.003474 91.576843 69.487369 174.951211 142.153398 234.757565 73.636951 60.604041 166.813196 93.981989 262.366386 93.981988 227.808022 0 413.137216-185.333223 413.137216-413.137216S745.619451 39.441172 517.811429 39.441172z" fill="#6E6E96" p-id="23454"></path><path d="M809.635818 752.069432m-135.542268 0a135.542268 135.542268 0 1 0 271.084536 0 135.542268 135.542268 0 1 0-271.084536 0Z" fill="#FFE770" p-id="23455"></path><path d="M809.635818 905.406558c-84.546726 0-153.337126-68.786372-153.337126-153.341154 0-84.546726 68.786372-153.337126 153.337126-153.337126s153.341155 68.786372 153.341155 153.337126c-0.004029 84.554783-68.7904 153.341155-153.341155 153.341154z m0-271.084536c-64.926857 0-117.74741 52.816524-117.74741 117.74741s52.816524 117.751439 117.74741 117.751439 117.751439-52.816524 117.751439-117.751439c-0.004029-64.930886-52.820553-117.74741-117.751439-117.74741z" fill="#6E6E96" p-id="23456"></path><path d="M218.864263 752.069432m-135.542268 0a135.542268 135.542268 0 1 0 271.084536 0 135.542268 135.542268 0 1 0-271.084536 0Z" fill="#FFE770" p-id="23457"></path><path d="M218.864263 905.406558c-84.550754 0-153.337126-68.786372-153.337126-153.341154 0-84.546726 68.786372-153.337126 153.337126-153.337126s153.337126 68.786372 153.337126 153.337126c0 84.554783-68.786372 153.341155-153.337126 153.341154z m0-271.084536c-64.926857 0-117.74741 52.816524-117.74741 117.74741s52.820553 117.751439 117.74741 117.751439 117.751439-52.816524 117.751439-117.751439c-0.004029-64.930886-52.824581-117.74741-117.751439-117.74741z" fill="#6E6E96" p-id="23458"></path><path d="M305.248085 455.213172a61.824743 28.845638 0 1 0 123.649486 0 61.824743 28.845638 0 1 0-123.649486 0Z" fill="#FF0000" opacity=".18" p-id="23459"></path><path d="M615.403151 455.213172a61.820714 28.845638 0 1 0 123.641429 0 61.820714 28.845638 0 1 0-123.641429 0Z" fill="#FF0000" opacity=".18" p-id="23460"></path><path d="M410.079418 993.43007h193.378577a16.114881 16.114881 0 0 0 0-32.229763h-193.378577a16.114881 16.114881 0 0 0 0 32.229763z" fill="#6E6E96" opacity=".29" p-id="23461"></path><path d="M675.435114 993.43007h56.402085a16.114881 16.114881 0 0 0 0-32.229763h-56.402085a16.114881 16.114881 0 0 0 0 32.229763z" fill="#6E6E96" opacity=".17" p-id="23462"></path><path d="M332.993882 961.200307h-44.315924a16.114881 16.114881 0 0 0 0 32.229763h44.315924a16.114881 16.114881 0 0 0 0-32.229763z" fill="#6E6E96" opacity=".17" p-id="23463"></path></svg>
\ No newline at end of file
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1770017871834" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1986" xmlns:xlink="http://www.w3.org/1999/xlink" width="256" height="256"><path d="M515.9424 512.768m-450.816 0a450.816 450.816 0 1 0 901.632 0 450.816 450.816 0 1 0-901.632 0Z" fill="#FFF1DC" p-id="1987"></path><path d="M516.4544 251.1872c69.4784 0 138.9568-0.1024 208.4352 0.0512 50.4832 0.1024 74.5472 23.9616 74.5472 73.8816 0.0512 124.9792 0.0512 250.0096 0 374.9888 0 49.8176-24.2176 74.0864-74.5984 74.1888-138.9568 0.1536-277.9136 0.1536-416.8192 0-52.0192-0.0512-75.4176-23.4496-75.4688-75.1616-0.1536-124.3648-0.1536-248.7296 0-373.0944 0.0512-51.3536 23.7568-74.7008 75.52-74.8032 69.4272-0.1536 138.9056-0.0512 208.384-0.0512z m-110.5408 281.5488c-36.864 48.896-72.2944 95.8976-109.6704 145.4592h439.6544c-48.0256-65.2288-94.0032-127.7952-141.2096-191.8976-37.632 48.1792-73.472 94.0544-110.0288 140.9024-26.9824-32.3584-51.968-62.4128-78.7456-94.464zM371.5072 439.296c32.768 0.1536 57.7024-24.4736 57.5488-56.9344-0.1536-30.976-25.4976-56.7808-56.1664-57.1904-31.3856-0.4608-58.4192 25.6512-58.7264 56.7296-0.3584 32.2048 24.6272 57.1904 57.344 57.3952z" fill="#FC7032" p-id="1988"></path><path d="M516.4544 251.1872c-69.4784 0-138.9568-0.1024-208.4352 0.0512-51.7632 0.1024-75.4688 23.4496-75.52 74.8032-0.1536 124.3648-0.1536 248.7296 0 373.0944 0.0512 51.712 23.4496 75.1104 75.4688 75.1616 85.0432 0.1024 170.1376 0 255.1808 0a452.77184 452.77184 0 0 0 98.7136-96.0512H296.192c37.376-49.5616 72.8064-96.5632 109.6704-145.4592 26.7264 32.0512 51.7632 62.1056 78.7456 94.464 36.608-46.848 72.448-92.7232 110.0288-140.9024 34.2016 46.4384 67.7376 92.1088 101.888 138.496 35.7888-64.6144 56.1664-138.9568 56.1664-218.0096 0-54.6816-9.728-107.0592-27.5456-155.5456h-0.3584c-69.376-0.2048-138.8544-0.1024-208.3328-0.1024zM371.5072 439.296c-32.7168-0.1536-57.7024-25.1904-57.3952-57.3952 0.3072-31.0784 27.3408-57.1904 58.7264-56.7296 30.6688 0.4608 56.0128 26.2144 56.1664 57.1904 0.2048 32.4096-24.7296 57.088-57.4976 56.9344z" fill="#FF7E3E" p-id="1989"></path><path d="M309.4528 678.1952h-13.1584c37.376-49.5616 72.8064-96.5632 109.6704-145.4592 18.6368 22.3744 36.5056 43.776 54.7328 65.6384 101.8368-81.7664 167.3728-206.7968 168.7552-347.2384-37.632 0-75.3152 0.0512-112.9472 0.0512-69.4784 0-138.9568-0.1024-208.4352 0.0512-51.7632 0.1024-75.4688 23.4496-75.52 74.8032-0.1536 122.7776-0.1024 245.504 0 368.2816a448.6656 448.6656 0 0 0 76.9024-16.128z m63.4368-353.024c30.6688 0.4608 56.0128 26.2144 56.1664 57.1904 0.1536 32.4096-24.7808 57.088-57.5488 56.9344-32.7168-0.1536-57.7024-25.1904-57.3952-57.3952 0.3072-31.0784 27.392-57.1904 58.7776-56.7296z" fill="#FF9552" p-id="1990"></path><path d="M351.232 435.6608c-22.1696-7.9872-37.376-28.672-37.12-53.8112 0.3072-31.0784 27.3408-57.1904 58.7264-56.7296 22.784 0.3072 42.5984 14.6432 51.3536 34.7136a451.1744 451.1744 0 0 0 61.0816-108.6976c-59.0848 0-118.1696-0.0512-177.2544 0.0512-51.7632 0.1024-75.4688 23.4496-75.52 74.8032-0.0512 59.9552 0 119.9104 0 179.8656A448.06656 448.06656 0 0 0 351.232 435.6608z" fill="#FFA56A" p-id="1991"></path></svg>
\ No newline at end of file
<template>
<div class="flex-1">
<div>
<!-- <el-input
v-model="commentStr"
type="textarea"
:placeholder="placeholder"
:rows="3"
></el-input> -->
<div
style="border: 1px solid rgb(229, 231, 235)"
class="relative w-full rounded-lg border border-gray-200 px-3 py-2 transition focus-within:border-[var(--el-color-primary)] focus-within:ring-1 focus-within:ring-[var(--el-color-primary)] bg-white"
>
<!-- 文本输入区 -->
<textarea
ref="commentInputRef"
v-model="commentStr"
class="w-full resize-none border-none outline-none text-sm leading-5 text-gray-800 min-h-14"
:placeholder="placeholder"
/>
<!-- 定位到右边 -->
<!-- <span class="flex justify-end text-xs text-gray-400">
{{ form.content.length }} / 500
</span> -->
<div class="flex justify-between items-center mt-2">
<div
v-if="imgList.length"
class="flex flex-wrap gap-2"
v-loading="uploadPercent > 0"
:element-loading-text="uploadPercent + '%'"
>
<div
class="relative w-20 h-20 rounded-lg overflow-hidden group"
v-for="img in imgList"
:key="img"
>
<!-- 删除按钮 -->
<div
class="absolute top-1 right-1 z-10 w-5 h-5 flex items-center justify-center bg-black/60 rounded-full cursor-pointer opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-black/80 hover:scale-110"
@click="handleDeleteImg(img)"
>
<el-icon class="text-white text-xs">
<IEpClose />
</el-icon>
</div>
<el-image
:src="img"
class="w-full h-full rounded-lg border border-gray-200"
fit="cover"
/>
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-between items-center mt-3">
<div>
<!-- 操作栏 添加图片 表情包等 -->
<div class="flex items-center gap-2">
<div
class="w-8 h-8 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg flex items-center justify-center"
>
<input
accept="image/*"
@change="handleFileChange"
type="file"
class="hidden"
ref="fileInputRef"
/>
<el-icon size="30" class="cursor-pointer" @click="fileInputRef?.click()">
<svg-icon name="icon_picture" />
</el-icon>
</div>
<div
class="w-8 h-8 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg flex items-center justify-center"
>
<el-popover placement="bottom" trigger="click" width="384">
<template #reference>
<el-icon size="20" @mousedown.prevent>
<svg-icon name="icon_face" class="cursor-pointer"
/></el-icon>
</template>
<!-- 表情面板 -->
<el-scrollbar class="h-50">
<div class="flex flex-wrap">
<span
v-for="item in emojis"
:key="item.name"
class="cursor-pointer hover:bg-gray-100 rounded p-1 flex items-center justify-center"
@click="selectEmoji(item)"
>
<img :src="item.url" alt="" class="w-6 h-6" />
</span>
</div>
</el-scrollbar>
</el-popover>
</div>
</div>
</div>
<div class="flex items-center gap-2 text-sm text-gray-500">
<button class="hover:text-blue-500 transition-colors">
<i class="i-carbon-face-satisfied"></i>
</button>
<button class="hover:text-blue-500 transition-colors">
<i class="i-carbon-image"></i>
</button>
</div>
<button
class="cursor-pointer disabled:opacity-50 px-6 py-2 bg-gradient-to-r from-blue-500 to-purple-500 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled="!displayComment || loading"
@click="emit('submit')"
>
<div v-show="loading" class="flex items-center gap-2">
<el-icon><IEpLoading /></el-icon>
<span>发表中...</span>
</div>
<div v-show="!loading">发表</div>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import emojis from '@/utils/emoji/face.json'
import { useUploadImg } from '@/hooks'
import type { IEmoji } from '@/utils/emoji/type'
const commentStr = defineModel<string>('modelValue', {
required: true,
})
const commentImgStr = defineModel<string>('commentImgStr', {
required: true,
})
const { placeholder = '写下你的评论...', loading } = defineProps<{
placeholder?: string
loading: boolean
}>()
const emit = defineEmits<{
(e: 'submit'): void
}>()
const displayComment = computed(() => commentStr.value?.trim())
const fileInputRef = useTemplateRef('fileInputRef')
const commentInputRef = useTemplateRef<HTMLTextAreaElement>('commentInputRef')
const { handleFileChange, handleDeleteImg, uploadPercent, imgList } = useUploadImg(commentImgStr)
const selectEmoji = async (item: IEmoji) => {
const textarea = commentInputRef.value
if (!textarea) return
// 当选中一段文本时 这俩值是不一样
const start = textarea.selectionStart
const end = textarea.selectionEnd
const value = commentStr.value
// 插入内容(你可以是 [微笑],也可以是 😀)
commentStr.value = value.slice(0, start) + item.name + value.slice(end)
// 插入后把光标放到表情后面
await nextTick()
textarea.focus()
textarea.selectionStart = textarea.selectionEnd = start + item.name.length
}
defineExpose({
focus: async () => {
await nextTick()
commentInputRef.value?.focus()
},
})
</script>
......@@ -66,7 +66,7 @@
class="w-10 h-10 rounded-full object-cover cursor-pointer"
@click="jumpToUserHomePage({ userId: userInfo.userId, isReal: 0 })"
/>
<div class="flex-1">
<!-- <div class="flex-1">
<div ref="commentInputRef">
<el-input
v-model="myComment"
......@@ -92,9 +92,16 @@
发表
</button>
</div>
</div>
</div> -->
<ReplyBox
v-model="myComment"
v-model:commentImgStr="myCommentImgStr"
@submit="handleMyComment"
:loading="myCommentLoading"
/>
</div>
</div>
<!-- 分割线 -->
<!-- 评论列表 -->
<div v-loading="loading" class="divide-y divide-gray-100" v-if="list.length">
......@@ -122,9 +129,27 @@
<!-- <span class="px-2 py-0.5 text-xs bg-red-100 text-red-600 rounded-full">置顶</span> -->
</div>
<!-- 换行 -->
<p class="text-gray-800 my-2 break-all whitespace-pre-wrap">
{{ item.content }}
</p>
<p
class="text-gray-800 my-2 break-all whitespace-pre-wrap"
v-html="parseEmoji(item.content)"
></p>
<!-- 评论图片列表 -->
<div class="flex flex-wrap gap-2">
<div
v-for="(img, imgIndex) in item.imgUrl.split(',').filter(Boolean)"
:key="imgIndex"
class="w-20 h-20 rounded-lg overflow-hidden mb-2 cursor-pointer"
>
<el-image
:src="img"
:preview-teleported="true"
class="w-full h-full object-cover"
:preview-src-list="item.imgUrl.split(',').filter(Boolean)"
:initial-index="imgIndex"
fit="cover"
/>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-8 text-sm text-gray-500">
<span>{{ dayjs(item.createTime * 1000).format('YYYY-MM-DD HH:mm:ss') }}</span>
......@@ -141,7 +166,7 @@
</div>
<button
class="cursor-pointer hover:text-blue-500 transition-colors"
@click="handleReply(item)"
@click="handleReply(item, index)"
>
回复
</button>
......@@ -171,9 +196,27 @@
<span>{{ child.replyName }}</span></span
>
</div>
<p class="text-gray-800 my-2 break-all whitespace-pre-wrap">
{{ child.content }}
</p>
<p
class="text-gray-800 my-2 break-all whitespace-pre-wrap"
v-html="parseEmoji(child.content)"
></p>
<!-- 评论图片列表 -->
<div class="flex flex-wrap gap-2">
<div
v-for="(img, imgIndex) in child.imgUrl.split(',').filter(Boolean)"
:key="imgIndex"
class="w-20 h-20 rounded-lg overflow-hidden mb-2"
>
<el-image
:src="img"
:preview-teleported="true"
class="w-full h-full object-cover"
:preview-src-list="child.imgUrl.split(',').filter(Boolean)"
:initial-index="imgIndex"
fit="cover"
/>
</div>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-4 text-sm text-gray-500">
<span>{{
......@@ -193,7 +236,7 @@
</div>
</div>
<button
@click="handleReply(child)"
@click="handleReply(child, index)"
class="cursor-pointer hover:text-blue-500 transition-colors"
>
回复
......@@ -250,7 +293,7 @@
</div>
</div>
<!-- 展示 回复评论的输入框 -->
<transition name="fadeToComment" mode="out-in">
<transition name="fadeToComment">
<div v-show="showCommentBox(item)" class="flex gap-3 mt-4">
<img
:src="userAvatar"
......@@ -258,7 +301,7 @@
class="w-10 h-10 rounded-full object-cover cursor-pointer"
@click="jumpToUserHomePage({ userId: userInfo.userId, isReal: isReal })"
/>
<div class="flex-1">
<!-- <div class="flex-1">
<el-input
v-model="comment"
type="textarea"
......@@ -282,7 +325,15 @@
发表
</button>
</div>
</div>
</div> -->
<ReplyBox
v-model="commentToOther"
v-model:commentImgStr="commentToOtherImgStr"
:loading="commentToOtherLoading"
:placeholder="replyPlaceholder"
@submit="handleComment(index)"
:ref="(el) => (replyToOtherBoxRefList[index] = el as HTMLElement)"
/>
</div>
</transition>
</div>
......@@ -329,6 +380,8 @@ import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import CommentListDialog from '../CommentListDialog/index.vue'
import { jumpToUserHomePage } from '@/utils'
import ReplyBox from './components/ReplyBox.vue'
import { parseEmoji } from '@/utils/emoji'
const {
id,
defaultSize = 10,
......@@ -356,7 +409,7 @@ const { userInfo } = storeToRefs(userStore)
const userAvatar = computed(() => (isReal ? userInfo.value.avatar : userInfo.value.hiddenAvatar))
const commentRef = useTemplateRef<HTMLElement | null>('commentRef')
const commentListDialogRef = useTemplateRef<typeof CommentListDialog>('commentListDialogRef')
const commentInputRef = useTemplateRef<HTMLElement | null>('commentInputRef')
const replyToOtherBoxRefList = ref<HTMLElement[]>([])
const commentItemRefList = ref<HTMLElement[]>([])
// 回滚到评论框
const { handleBackTop } = useScrollTop(commentRef)
......@@ -413,12 +466,16 @@ const handleCurrentChange = async (e: number) => {
// 自己发出的评论
const myComment = ref('')
const myCommentLoading = ref(false)
// 回复别人的
const comment = ref('')
const commentToOther = ref('')
const commentToOtherLoading = ref(false)
// 回复别人placeholder
const replyPlaceholder = ref('回复@')
// 自己发出的评论图片
const myCommentImgStr = ref('')
// 回复别人的图片
const commentToOtherImgStr = ref('')
const currentCommentId = ref(-1)
const handleLickComment = async (item: CommentItemDto) => {
......@@ -434,12 +491,11 @@ const handleLickComment = async (item: CommentItemDto) => {
}
}
const handleReply = (item: CommentItemDto) => {
console.log('回复', item)
const handleReply = (item: CommentItemDto, index: number) => {
replyPlaceholder.value = `回复@${item.replyUser}:`
comment.value = ''
commentToOther.value = ''
currentCommentId.value = item.id
console.log(currentCommentId.value)
replyToOtherBoxRefList.value[index]?.focus()
}
// 是否展示子评论回复框
......@@ -451,31 +507,49 @@ const showCommentBox = (item: CommentItemDto) => {
}
const handleMyComment = async () => {
await addComment({
articleId: id,
content: myComment.value,
...(commentId ? { pid: commentId } : {}),
})
ElMessage.success('发表评论成功')
refresh()
myComment.value = ''
total.value++
emit('commentSuccess')
try {
myCommentLoading.value = true
await addComment({
articleId: id,
content: myComment.value,
...(commentId ? { pid: commentId } : {}),
imgUrl: myCommentImgStr.value,
})
ElMessage.success('发表评论成功')
refresh()
myComment.value = ''
myCommentImgStr.value = ''
total.value++
emit('commentSuccess')
} catch (error) {
console.error(error)
} finally {
myCommentLoading.value = false
}
}
const handleComment = async (index: number) => {
await addComment({
articleId: id,
content: comment.value,
...(currentCommentId.value ? { pid: currentCommentId.value } : {}),
})
ElMessage.success('发表评论成功')
comment.value = ''
total.value++
handleBackTopChildren(index)
// 只需要刷新当前的评论
search()
emit('commentSuccess')
try {
commentToOtherLoading.value = true
await addComment({
articleId: id,
content: commentToOther.value,
...(currentCommentId.value ? { pid: currentCommentId.value } : {}),
imgUrl: commentToOtherImgStr.value,
})
ElMessage.success('发表评论成功')
commentToOther.value = ''
commentToOtherImgStr.value = ''
total.value++
handleBackTopChildren(index)
// 只需要刷新当前的评论
search()
emit('commentSuccess')
} catch (error) {
console.error(error)
} finally {
commentToOtherLoading.value = false
}
}
// 展开回复 获取子评论列表
......@@ -538,12 +612,26 @@ defineExpose({
})
</script>
<style scoped lang="scss">
.fadeToComment-enter-from {
/* 进入 & 离开公共属性 */
.fadeToComment-enter-active,
.fadeToComment-leave-active {
transition:
opacity 0.25s ease,
transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
will-change: opacity, transform;
}
/* 进入前 & 离开后 */
.fadeToComment-enter-from,
.fadeToComment-leave-to {
opacity: 0;
transform: translateY(-10px);
transform: translateY(8px);
}
.fadeToComment-enter-active {
transition: all 0.5s ease-out;
/* 进入后 & 离开前 */
.fadeToComment-enter-to,
.fadeToComment-leave-from {
opacity: 1;
transform: translateY(0);
}
</style>
......@@ -11,21 +11,101 @@
<el-avatar :size="40" :src="userInfo.hiddenAvatar" />
<!-- 评论输入框 -->
<el-input
v-model="commentContent"
type="textarea"
:rows="4"
placeholder="写下你的评论..."
maxlength="500"
show-word-limit
class="flex-1"
/>
<div
style="border: 1px solid rgb(229, 231, 235)"
class="relative flex-1 rounded-lg border border-gray-200 px-3 py-2 transition focus-within:border-[var(--el-color-primary)] focus-within:ring-1 focus-within:ring-[var(--el-color-primary)] bg-white"
>
<!-- 文本输入区 -->
<textarea
ref="commentInputRef"
v-model="commentStr"
class="w-full resize-none border-none outline-none text-sm leading-5 text-gray-800 min-h-30"
placeholder="写下你的评论..."
/>
<!-- 定位到右边 -->
<!-- <span class="flex justify-end text-xs text-gray-400">
{{ form.content.length }} / 500
</span> -->
<!-- 底部工具栏 -->
<div class="flex justify-between items-center mt-2">
<div
v-if="imgList.length"
class="flex flex-wrap gap-2"
v-loading="uploadPercent > 0"
:element-loading-text="uploadPercent + '%'"
>
<div
class="relative w-20 h-20 rounded-lg overflow-hidden group"
v-for="img in imgList"
:key="img"
>
<!-- 删除按钮 -->
<div
class="absolute top-1 right-1 z-10 w-5 h-5 flex items-center justify-center bg-black/60 rounded-full cursor-pointer opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-black/80 hover:scale-110"
@click="handleDeleteImg(img)"
>
<el-icon class="text-white text-xs">
<IEpClose />
</el-icon>
</div>
<el-image
:src="img"
class="w-full h-full rounded-lg border border-gray-200"
fit="cover"
/>
</div>
</div>
</div>
</div>
</div>
<template #footer>
<div class="flex justify-end gap-2">
<el-button @click="handleClose" class="rounded-lg">取消</el-button>
<div class="flex justify-end gap-2 items-center">
<!-- 上传文件 和 表情 -->
<div
class="w-8 h-8 cursor-pointer text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg flex items-center justify-center"
>
<input
accept="image/*"
@change="handleFileChange"
type="file"
class="hidden"
ref="fileInputRef"
/>
<el-icon size="30" @click="fileInputRef?.click()">
<svg-icon name="icon_picture" />
</el-icon>
</div>
<div
class="w-8 h-8 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg flex items-center justify-center"
>
<el-popover placement="bottom" trigger="click" width="384">
<template #reference>
<el-icon size="20" @mousedown.prevent>
<svg-icon name="icon_face" class="cursor-pointer"
/></el-icon>
</template>
<!-- 表情面板 -->
<el-scrollbar class="h-50">
<div class="flex flex-wrap">
<span
v-for="item in emojis"
:key="item.name"
class="cursor-pointer hover:bg-gray-100 rounded p-1 flex items-center justify-center"
@click="selectEmoji(item)"
>
<img :src="item.url" alt="" class="w-6 h-6" />
</span>
</div>
</el-scrollbar>
</el-popover>
</div>
<el-button
:disabled="isDisabled || loading"
:loading="loading"
type="primary"
@click="handleSubmit"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
......@@ -40,48 +120,81 @@
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import { addComment } from '@/api'
import { useUploadImg } from '@/hooks'
import emojis from '@/utils/emoji/face.json'
import type { IEmoji } from '@/utils/emoji/type'
const emit = defineEmits<{
(e: 'commentSuccess'): void
}>()
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
// 弹窗显示状态
const visible = ref(false)
// 评论内容
const commentContent = ref('')
const commentStr = ref('')
const commentImgStr = ref('')
const loading = ref(false)
const isDisabled = computed(() => !commentStr.value.trim())
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
const { handleFileChange, handleDeleteImg, uploadPercent, imgList } = useUploadImg(commentImgStr)
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
const commentInputRef = useTemplateRef<HTMLTextAreaElement>('commentInputRef')
let articleId = 0
// 暴露 open 方法
const open = (id: number) => {
articleId = id
visible.value = true
commentContent.value = ''
commentStr.value = ''
commentImgStr.value = ''
}
// 关闭弹窗
const handleClose = () => {
visible.value = false
commentContent.value = ''
commentStr.value = ''
commentImgStr.value = ''
}
const selectEmoji = async (item: IEmoji) => {
const textarea = commentInputRef.value
if (!textarea) return
// 当选中一段文本时 这俩值是不一样
const start = textarea.selectionStart
const end = textarea.selectionEnd
const value = commentStr.value
// 插入内容(你可以是 [微笑],也可以是 😀)
commentStr.value = value.slice(0, start) + item.name + value.slice(end)
// 插入后把光标放到表情后面
await nextTick()
textarea.focus()
textarea.selectionStart = textarea.selectionEnd = start + item.name.length
}
// 提交评论
const handleSubmit = async () => {
if (!commentContent.value.trim()) {
ElMessage.warning('请输入评论内容')
return
}
loading.value = true
// TODO: 这里处理提交逻辑
await addComment({
articleId: articleId,
content: commentContent.value,
})
console.log('评论内容:', commentContent.value)
ElMessage.success('评论发表成功')
handleClose()
emit('commentSuccess')
try {
await addComment({
articleId: articleId,
content: commentStr.value,
imgUrl: commentImgStr.value,
})
console.log('评论内容:', commentStr.value)
ElMessage.success('评论发表成功')
handleClose()
emit('commentSuccess')
} catch (error) {
console.log(error)
} finally {
loading.value = false
}
}
// 暴露方法给父组件
......
......@@ -49,7 +49,28 @@
</div>
</div>
<p class="text-gray-800 text-base leading-relaxed mb-3">{{ parentComment.content }}</p>
<p
class="text-gray-800 text-base leading-relaxed mb-3"
v-html="parseEmoji(parentComment.content)"
></p>
<!-- 下方图片 -->
<div class="flex flex-wrap gap-2" v-if="parentComment.imgUrl">
<div
v-for="(img, imgIndex) in parentComment.imgUrl.split(',').filter(Boolean)"
:key="imgIndex"
class="w-20 h-20 rounded-lg overflow-hidden mb-2 cursor-pointer"
>
<el-image
:src="img"
:preview-teleported="true"
class="w-full h-full object-cover"
:preview-src-list="parentComment.imgUrl.split(',').filter(Boolean)"
:initial-index="imgIndex"
fit="cover"
/>
</div>
</div>
<div class="text-sm text-gray-400 flex items-center gap-4">
<span>{{ dayjs(parentComment.createTime * 1000).format('MM-DD HH:mm') }}</span>
......@@ -102,9 +123,27 @@
</div>
</div>
<p class="text-gray-700 text-base mb-3 break-all leading-relaxed">
{{ item.content }}
</p>
<p
class="text-gray-700 text-base mb-3 break-all leading-relaxed"
v-html="parseEmoji(item.content)"
></p>
<!-- 下方图片 -->
<div class="flex flex-wrap gap-2" v-if="item.imgUrl">
<div
v-for="(img, imgIndex) in item.imgUrl.split(',').filter(Boolean)"
:key="imgIndex"
class="w-20 h-20 rounded-lg overflow-hidden mb-2 cursor-pointer"
>
<el-image
:src="img"
:preview-teleported="true"
class="w-full h-full object-cover"
:preview-src-list="item.imgUrl.split(',').filter(Boolean)"
:initial-index="imgIndex"
fit="cover"
/>
</div>
</div>
<div class="text-sm text-gray-400">
<span>{{ formatDate(item.createTime) }}</span>
......@@ -210,6 +249,7 @@ import {
import type { CommentItemDto } from '@/api'
import { BooleanFlag } from '@/constants'
import { usePageSearch } from '@/hooks' // 假设你有这个hook
import { parseEmoji } from '@/utils/emoji'
// Props
const { articleId, pid } = defineProps<{
......
<template>
<el-dialog v-model="dialogVisible" title="选择标签" width="500px" :close-on-click-modal="false">
<el-dialog
v-model="dialogVisible"
title="选择标签"
width="500px"
:close-on-click-modal="false"
top="30vh"
>
<div class="space-y-6 px-2">
<div class="flex items-start gap-4">
<div class="text-sm text-gray-700 w-16 flex-shrink-0">主标签</div>
......
......@@ -28,18 +28,64 @@
</div>
<!-- 主要内容输入 -->
<div class="relative mb-3">
<el-input
<div
style="border: 1px solid rgb(229, 231, 235)"
class="relative w-full rounded-lg border border-gray-200 px-3 py-2 transition focus-within:border-[var(--el-color-primary)] focus-within:ring-1 focus-within:ring-[var(--el-color-primary)] bg-white"
>
<!-- 文本输入区 -->
<textarea
v-model="form.content"
:placeholder="textMap[type].content"
class="w-full resize-none border-none outline-none text-sm leading-5 text-gray-800 min-h-30"
:maxlength="maxLength"
/>
<!-- 定位到右边 -->
<span class="flex justify-end text-xs text-gray-400">
{{ form.content.length }} / 500
</span>
<!-- 底部工具栏 -->
<div class="flex justify-between items-center mt-2">
<!-- 图片列表 -->
<div
v-if="imgList.length"
class="flex flex-wrap gap-2"
v-loading="uploadPercent > 0"
:element-loading-text="uploadPercent + '%'"
>
<div
class="relative w-20 h-20 rounded-lg overflow-hidden group"
v-for="img in imgList"
:key="img"
>
<!-- 删除按钮 -->
<div
class="absolute top-1 right-1 z-10 w-5 h-5 flex items-center justify-center bg-black/60 rounded-full cursor-pointer opacity-0 group-hover:opacity-100 transition-all duration-200 hover:bg-black/80 hover:scale-110"
@click="handleDeleteImg(img)"
>
<el-icon class="text-white text-xs">
<IEpClose />
</el-icon>
</div>
<el-image
:src="img"
class="w-full h-full rounded-lg border border-gray-200"
fit="cover"
/>
</div>
</div>
</div>
</div>
<!-- <el-input
type="textarea"
:placeholder="textMap[type].content"
:rows="6"
:maxlength="maxLength"
show-word-limit
resize="none"
class="main-textarea"
v-model="form.content"
/>
<!-- 字符计数 -->
<!-- <div class="absolute bottom-3 right-3 text-xs text-gray-400">1/30</div> -->
/> -->
</div>
<!-- 标签内容 -->
<div class="mb-2">
......@@ -56,13 +102,12 @@
</div>
</div>
<!-- 图片相关 -->
<div
<!-- <div
v-if="form.imgUrl.length"
class="flex flex-wrap gap-2 w-fit"
v-loading="uploadPercent > 0"
:element-loading-text="uploadPercent + '%'"
>
<!-- 删除图片 -->
<div
class="relative w-20 h-20 rounded-lg overflow-hidden group"
v-for="img in form.imgUrl"
......@@ -82,7 +127,7 @@
fit="cover"
/>
</div>
</div>
</div> -->
</div>
</div>
......@@ -122,7 +167,7 @@
:disabled="uploadPercent > 0"
>
<el-icon size="18">
<span v-if="!form.imgUrl.length && uploadPercent > 0"> <IEpLoading /></span>
<span v-if="!imgList.length && uploadPercent > 0"> <IEpLoading /></span>
<span v-else> <IEpPicture /></span>
</el-icon>
</el-button>
......@@ -159,6 +204,7 @@
<el-button
type="primary"
:disabled="disabledSubmit"
:loading="loading"
class="px-6 py-2 bg-blue-500 hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg text-white text-sm font-medium shadow-sm hover:shadow-md transition-all duration-200"
@click="handlePublish(ReleaseStatusTypeEnum.PUBLISH)"
>
......@@ -204,6 +250,8 @@ const {
maxLength?: number
}>()
const loading = ref(false)
const textMap: Record<
ArticleType,
{ title: string; content: string; api: (data: any) => Promise<any> }
......@@ -260,7 +308,7 @@ const { play } = useAnimate(
const [form, resetForm] = useResetData({
title: '',
content: '',
imgUrl: [] as string[],
imgUrl: '',
releaseStatus: ReleaseStatusTypeEnum.PUBLISH,
mainTagId: '',
tagList: [],
......@@ -268,7 +316,7 @@ const [form, resetForm] = useResetData({
sendTime: '',
})
const { imgsStr, handleFileChange, handleDeleteImg, uploadPercent } = useUploadImg(
const { imgList, handleFileChange, handleDeleteImg, uploadPercent } = useUploadImg(
toRef(form.value, 'imgUrl'),
)
......@@ -302,8 +350,8 @@ const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdatePractic
return {
...form.value,
releaseStatus,
faceUrl: form.value.imgUrl[0] || '',
imgUrl: imgsStr.value,
faceUrl: imgList.value[0] || '',
imgUrl: form.value.imgUrl,
tagList: [form.value.mainTagId, ...form.value.tagList].map((item, index) => ({
sort: index,
tagId: Number(item),
......@@ -314,9 +362,17 @@ const transformForm = (releaseStatus: ReleaseStatusTypeEnum): AddOrUpdatePractic
const handlePublish = async (releaseStatus: ReleaseStatusTypeEnum) => {
if (!validateForm()) return
await textMap[type].api(transformForm(releaseStatus))
ElMessage.success(releaseStatus === ReleaseStatusTypeEnum.PUBLISH ? '发布成功' : '存草稿成功')
resetForm()
loading.value = true
try {
await textMap[type].api(transformForm(releaseStatus))
loading.value = false
ElMessage.success(releaseStatus === ReleaseStatusTypeEnum.PUBLISH ? '发布成功' : '存草稿成功')
resetForm()
} catch (error) {
console.error(error)
} finally {
loading.value = false
}
}
</script>
......
import { uploadFile } from '@/api'
// 默认参数
export const useUploadImg = (imgList: Ref<string[]> = ref([])) => {
const uploadPercent = ref(0)
// 字符串拼
const imgsStr = computed(() => imgList.value.join(','))
// 类型定义
type BaseReturn = {
handleFileChange: (e: Event) => Promise<void>
uploadPercent: Ref<number>
handleDeleteImg: (urlStr: string) => void
}
// 传单字符串时多返回 imgList
type UseUploadImgReturnString = BaseReturn & {
imgList: ComputedRef<string[]>
}
// 传字符串数组时只返回基础
type UseUploadImgReturnArray = BaseReturn
// 直接传ref('imgs1,imgs2') 或者 ref(['img1','img2]) 传字符串的时候 会多返回一个imgList数组 便于模板使用遍历等
export function useUploadImg(imgs: Ref<string>): UseUploadImgReturnString
export function useUploadImg(imgs: Ref<string[]>): UseUploadImgReturnArray
export function useUploadImg(imgs: Ref<string> | Ref<string[]>) {
const uploadPercent = ref(0)
// 上传图片的change事件
const handleFileChange = async (e: Event) => {
try {
......@@ -18,7 +31,11 @@ export const useUploadImg = (imgList: Ref<string[]> = ref([])) => {
},
})
const data = await promise
imgList.value = [...imgList.value, data.filePath]
if (Array.isArray(imgs.value)) {
imgs.value = [...imgs.value, data.filePath]
} else {
imgs.value = [...imgs.value.split(',').filter(Boolean), data.filePath].join(',')
}
} catch (error) {
console.error('上传失败:', error)
} finally {
......@@ -30,14 +47,37 @@ export const useUploadImg = (imgList: Ref<string[]> = ref([])) => {
// 删除图片
const handleDeleteImg = (urlStr: string) => {
imgList.value = imgList.value.filter((item) => item !== urlStr)
if (Array.isArray(imgs.value)) {
imgs.value = imgs.value.filter((item) => item !== urlStr)
} else {
imgs.value =
imgs.value
.split(',')
.filter((item) => item !== urlStr)
.join(',') || ''
}
}
return {
imgsStr,
imgList,
handleFileChange,
uploadPercent,
handleDeleteImg,
const imgList = computed(() => {
if (Array.isArray(imgs.value)) {
return imgs.value
} else {
return imgs.value.split(',').filter(Boolean)
}
})
if (Array.isArray(imgs.value)) {
return {
handleFileChange,
uploadPercent,
handleDeleteImg,
}
} else {
return {
handleFileChange,
uploadPercent,
handleDeleteImg,
imgList,
}
}
}
[
{
"url": "https://www.emojiall.com/images/60/skype/1f600.png",
"name": "face_嘿嘿",
"name": "[face_嘿嘿]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f603.png",
"name": "face_哈哈",
"name": "[face_哈哈]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f604.png",
"name": "face_大笑",
"name": "[face_大笑]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f601.png",
"name": "face_嘻嘻",
"name": "[face_嘻嘻]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f606.png",
"name": "face_斜眼笑",
"name": "[face_斜眼笑]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f605.png",
"name": "face_苦笑",
"name": "[face_苦笑]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f923.png",
"name": "face_笑得满地打滚",
"name": "[face_笑得满地打滚]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f602.png",
"name": "face_笑哭了",
"name": "[face_笑哭了]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f642.png",
"name": "face_呵呵",
"name": "[face_呵呵]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f643.png",
"name": "face_倒脸",
"name": "[face_倒脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f609.png",
"name": "face_眨眼",
"name": "[face_眨眼]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f60a.png",
"name": "face_羞涩微笑",
"name": "[face_羞涩微笑]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f607.png",
"name": "face_微笑天使",
"name": "[face_微笑天使]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f970.png",
"name": "face_喜笑颜开",
"name": "[face_喜笑颜开]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f60d.png",
"name": "face_花痴",
"name": "[face_花痴]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f929.png",
"name": "face_好崇拜哦",
"name": "[face_好崇拜哦]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f617.png",
"name": "face_亲亲",
"name": "[face_亲亲]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f61a.png",
"name": "face_羞涩亲亲",
"name": "[face_羞涩亲亲]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f619.png",
"name": "face_微笑亲亲",
"name": "[face_微笑亲亲]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f60b.png",
"name": "face_好吃",
"name": "[face_好吃]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f61b.png",
"name": "face_吐舌",
"name": "[face_吐舌]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f61c.png",
"name": "face_单眼吐舌",
"name": "[face_单眼吐舌]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f92a.png",
"name": "face_滑稽",
"name": "[face_滑稽]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f911.png",
"name": "face_发财",
"name": "[face_发财]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f917.png",
"name": "face_抱抱",
"name": "[face_抱抱]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f92d.png",
"name": "face_不说",
"name": "[face_不说]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f92b.png",
"name": "face_安静的脸",
"name": "[face_安静的脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f914.png",
"name": "face_想一想",
"name": "[face_想一想]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f910.png",
"name": "face_闭嘴",
"name": "[face_闭嘴]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f928.png",
"name": "face_挑眉",
"name": "[face_挑眉]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f610.png",
"name": "face_冷漠",
"name": "[face_冷漠]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f611.png",
"name": "face_无语",
"name": "[face_无语]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f636.png",
"name": "face_沉默",
"name": "[face_沉默]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f60f.png",
"name": "face_得意",
"name": "[face_得意]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f612.png",
"name": "face_不高兴",
"name": "[face_不高兴]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f644.png",
"name": "face_翻白眼",
"name": "[face_翻白眼]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f62c.png",
"name": "face_龇牙咧嘴",
"name": "[face_龇牙咧嘴]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f925.png",
"name": "face_说谎",
"name": "[face_说谎]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f60c.png",
"name": "face_松了口气",
"name": "[face_松了口气]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f614.png",
"name": "face_沉思",
"name": "[face_沉思]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f62a.png",
"name": "face_困",
"name": "[face_困]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f634.png",
"name": "face_睡着了",
"name": "[face_睡着了]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f637.png",
"name": "face_感冒",
"name": "[face_感冒]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f912.png",
"name": "face_发烧",
"name": "[face_发烧]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f915.png",
"name": "face_受伤",
"name": "[face_受伤]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f922.png",
"name": "face_恶心",
"name": "[face_恶心]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f92e.png",
"name": "face_呕吐",
"name": "[face_呕吐]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f975.png",
"name": "face_脸发烧",
"name": "[face_脸发烧]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f976.png",
"name": "face_冷脸",
"name": "[face_冷脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f974.png",
"name": "face_头昏眼花",
"name": "[face_头昏眼花]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f92f.png",
"name": "face_爆炸头",
"name": "[face_爆炸头]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f920.png",
"name": "face_牛仔帽脸",
"name": "[face_牛仔帽脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f973.png",
"name": "face_聚会笑脸",
"name": "[face_聚会笑脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f60e.png",
"name": "face_墨镜笑脸",
"name": "[face_墨镜笑脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f913.png",
"name": "face_书呆子脸",
"name": "[face_书呆子脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f9d0.png",
"name": "face_带单片眼镜的脸",
"name": "[face_带单片眼镜的脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f615.png",
"name": "face_困扰",
"name": "[face_困扰]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f641.png",
"name": "face_微微不满",
"name": "[face_微微不满]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/2639-fe0f.png",
"name": "face_不满",
"name": "[face_不满]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f62f.png",
"name": "face_缄默",
"name": "[face_缄默]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f632.png",
"name": "face_震惊",
"name": "[face_震惊]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f633.png",
"name": "face_脸红",
"name": "[face_脸红]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f97a.png",
"name": "face_恳求的脸",
"name": "[face_恳求的脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f626.png",
"name": "face_啊",
"name": "[face_啊]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f627.png",
"name": "face_极度痛苦",
"name": "[face_极度痛苦]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f628.png",
"name": "face_害怕",
"name": "[face_害怕]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f630.png",
"name": "face_冷汗",
"name": "[face_冷汗]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f625.png",
"name": "face_失望但如释重负",
"name": "[face_失望但如释重负]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f622.png",
"name": "face_哭",
"name": "[face_哭]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f62d.png",
"name": "face_放声大哭",
"name": "[face_放声大哭]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f631.png",
"name": "face_吓死了",
"name": "[face_吓死了]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f616.png",
"name": "face_困惑",
"name": "[face_困惑]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f61e.png",
"name": "face_失望",
"name": "[face_失望]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f613.png",
"name": "face_汗",
"name": "[face_汗]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f629.png",
"name": "face_累死了",
"name": "[face_累死了]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f62b.png",
"name": "face_累",
"name": "[face_累]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f971.png",
"name": "face_打呵欠",
"name": "[face_打呵欠]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f624.png",
"name": "face_傲慢",
"name": "[face_傲慢]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f621.png",
"name": "face_怒火中烧",
"name": "[face_怒火中烧]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f620.png",
"name": "face_生气",
"name": "[face_生气]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f92c.png",
"name": "face_嘴上有符号的脸",
"name": "[face_嘴上有符号的脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f608.png",
"name": "face_恶魔微笑",
"name": "[face_恶魔微笑]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f47f.png",
"name": "face_生气的恶魔",
"name": "[face_生气的恶魔]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f480.png",
"name": "face_头骨",
"name": "[face_头骨]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/2620-fe0f.png",
"name": "face_骷髅",
"name": "[face_骷髅]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f4a9.png",
"name": "face_大便",
"name": "[face_大便]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f921.png",
"name": "face_小丑脸",
"name": "[face_小丑脸]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f479.png",
"name": "face_食人魔",
"name": "[face_食人魔]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f47a.png",
"name": "face_小妖精",
"name": "[face_小妖精]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f47b.png",
"name": "face_鬼",
"name": "[face_鬼]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f47d.png",
"name": "face_外星人",
"name": "[face_外星人]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f47e.png",
"name": "face_外星怪物",
"name": "[face_外星怪物]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f916.png",
"name": "face_机器人",
"name": "[face_机器人]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f63a.png",
"name": "face_大笑的猫",
"name": "[face_大笑的猫]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f639.png",
"name": "face_笑出眼泪的猫",
"name": "[face_笑出眼泪的猫]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f63b.png",
"name": "face_花痴的猫",
"name": "[face_花痴的猫]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f63c.png",
"name": "face_奸笑的猫",
"name": "[face_奸笑的猫]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f63d.png",
"name": "face_亲亲猫",
"name": "[face_亲亲猫]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f640.png",
"name": "face_疲倦的猫",
"name": "[face_疲倦的猫]",
"group": "face",
"className": "emoji_small"
},
{
"url": "https://www.emojiall.com/images/60/skype/1f63f.png",
"name": "face_哭泣的猫",
"name": "[face_哭泣的猫]",
"group": "face",
"className": "emoji_small"
}
......
import emojis from './face.json'
function escapeHTML(str: string) {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}
export const parseEmoji = (content: string) => {
if (!content) return ''
let html = escapeHTML(content)
emojis.forEach((item) => {
const escapedName = item.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
const reg = new RegExp(escapedName, 'g')
html = html.replace(
reg,
`<img
src="${item.url}"
alt="${item.name}"
class="${item.className || 'emoji_small'} inline-block align-text-bottom w-6 h-6"
/>`,
)
})
return html
}
......@@ -11,7 +11,7 @@
<span
v-for="tag in questionDetail.tagNameList"
:key="tag"
class="px-2.5 py-0.5 rounded-full bg-blue-50 text-blue-500 text-xs font-semibold hover:bg-blue-100 transition-colors cursor-pointer"
class="px-2.5 py-0.5 rounded-full bg-blue-50 text-blue-500 text-xs font-semibold hover:bg-blue-100 transition-colors cursor-pointer mr-2"
>
#{{ tag }}
</span>
......@@ -70,7 +70,21 @@
/></el-icon>
</button>
</div>
<div
v-if="questionDetail.imgUrl"
class="mt-3 flex gap-2 flex-wrap items-center justify-start"
>
<el-image
v-for="(item, i) in questionDetail.imgUrl.split(',')"
:key="item"
:src="item"
fit="cover"
class="rounded-lg w-24 h-24 hover:scale-105 transition-transform cursor-pointer"
:preview-src-list="questionDetail.imgUrl.split(',')"
:initial-index="i"
:preview-teleported="true"
/>
</div>
<!-- 底部操作栏 -->
<div class="flex items-center justify-between mt-4">
<div class="flex gap-3">
......@@ -84,7 +98,7 @@
</div>
<!-- 右侧数据 -->
<div class="flex items-center gap-6 text-slate-400 text-sm select-none">
<div class="flex items-center gap-6 text-slate-500 text-sm select-none">
<span
@click="handleLikeArticle"
class="hover:text-slate-600 cursor-pointer transition-colors flex items-center gap-1"
......@@ -112,17 +126,6 @@
</div>
</div>
<!-- 展示图片相关 -->
<div v-if="questionDetail.imgUrl" class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
<el-image
v-for="item in questionDetail.imgUrl.split(',')"
:key="item"
:src="item"
fit="cover"
class="rounded-lg w-full h-64 hover:scale-105 transition-transform cursor-pointer"
:preview-src-list="questionDetail.imgUrl.split(',')"
:preview-teleported="true"
/>
</div>
</div>
<!-- 2. 列表控制栏 -->
......@@ -187,24 +190,41 @@
>优秀回答</span
> -->
</div>
<!-- <div class="text-xs text-slate-400 mt-0.5 max-w-md truncate">
<!-- <div class="text-xs text-slate-500 mt-0.5 max-w-md truncate">
{{ answer.description || '暂无简介' }}
</div> -->
</div>
</div>
<!-- 赞同票数 (微小的灰色文字,增加信息密度) -->
<div v-if="answer.postPriseCount" class="text-xs text-slate-400 mb-2">
<div v-if="answer.postPriseCount" class="text-xs text-slate-500 mb-2">
{{ answer.postPriseCount || 0 }} 人赞同了该回答
</div>
<!-- 正文 换行 -->
<div
class="text-slate-800 leading-7 text-[15px] mb-4 rich-text-content break-all"
v-html="answer.content"
v-html="parseEmoji(answer.content)"
></div>
<!-- 评论图片列表 -->
<div class="flex flex-wrap gap-3 mb-3">
<div
v-for="(img, imgIndex) in answer.imgUrl.split(',').filter(Boolean)"
:key="imgIndex"
class="w-24 h-24 rounded-lg overflow-hidden mb-2"
>
<el-image
:src="img"
:preview-teleported="true"
class="w-full h-full object-cover"
:preview-src-list="answer.imgUrl.split(',').filter(Boolean)"
:initial-index="imgIndex"
fit="cover"
/>
</div>
</div>
<div class="text-xs text-slate-400 mb-4">
<div class="text-xs text-slate-500 mb-3">
发布于 {{ dayjs(answer.createTime * 1000).format('YYYY-MM-DD HH:mm') }}
</div>
......@@ -294,6 +314,7 @@ import dayjs from 'dayjs'
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
import { jumpToUserHomePage } from '@/utils'
import { parseEmoji } from '@/utils/emoji'
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment