Commit c23750c0 by lijiabin

【需求 20331】 perf: 优化关于评论相关的组件

parent bed64a7f
<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,39 +66,25 @@
class="w-10 h-10 rounded-full object-cover cursor-pointer"
@click="jumpToUserHomePage({ userId: userInfo.userId, isReal: 0 })"
/>
<!-- <div class="flex-1">
<div ref="commentInputRef">
<el-input
v-model="myComment"
type="textarea"
placeholder="写下你的评论..."
:rows="3"
></el-input>
</div>
<div class="flex justify-between items-center mt-3">
<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>
<CommentBox
v-model:inputText="myComment"
v-model:inputImg="myCommentImgStr"
class="flex-1"
>
<template #submit>
<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="!myComment.trim() || loading"
@click="handleMyComment()"
:disabled="!myComment?.trim() || loading"
@click="handleMyComment"
>
发表
<div v-show="myCommentLoading" class="flex items-center gap-2">
<el-icon><IEpLoading /></el-icon>
<span>发表中...</span>
</div>
<div v-show="!myCommentLoading">发表</div>
</button>
</div>
</div> -->
<ReplyBox
v-model="myComment"
v-model:commentImgStr="myCommentImgStr"
@submit="handleMyComment"
:loading="myCommentLoading"
/>
</template>
</CommentBox>
</div>
</div>
<!-- 分割线 -->
......@@ -301,39 +287,22 @@
class="w-10 h-10 rounded-full object-cover cursor-pointer"
@click="jumpToUserHomePage({ userId: userInfo.userId, isReal: isReal })"
/>
<!-- <div class="flex-1">
<el-input
v-model="comment"
type="textarea"
:placeholder="replyPlaceholder"
:rows="3"
></el-input>
<div class="flex justify-between items-center mt-3">
<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>
<CommentBox
v-model:inputText="commentToOther"
v-model:inputImg="commentToOtherImgStr"
class="flex-1"
:ref="(el) => (replyToOtherBoxRefList[index] = el as HTMLElement)"
>
<template #submit>
<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="!comment.trim()"
:disabled="!commentToOther.trim() || commentToOtherLoading"
@click="handleComment(index)"
>
发表
</button>
</div>
</div> -->
<ReplyBox
v-model="commentToOther"
v-model:commentImgStr="commentToOtherImgStr"
:loading="commentToOtherLoading"
:placeholder="replyPlaceholder"
@submit="handleComment(index)"
:ref="(el) => (replyToOtherBoxRefList[index] = el as HTMLElement)"
/>
</template>
</CommentBox>
</div>
</transition>
</div>
......@@ -380,8 +349,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'
import CommentBox from '../CommentBox/index.vue'
const {
id,
defaultSize = 10,
......
// 评论框的组件 用于评论的输入 以及 表情 图片的输入 以及 发表按钮
<script setup lang="ts">
import RichTextarea from '../RichTextarea/index.vue'
import UploadImgIcon from '../UploadImgIcon/index.vue'
import UploadEmojiIcon from '../UploadEmojiIcon/index.vue'
import { useUploadImg } from '@/hooks'
import type { IEmoji } from '@/utils/emoji/type'
interface CommentBoxProps {
textAreaHeight?: number
placeholder?: string
}
const { textAreaHeight = 55, placeholder = '请输入内容' } = defineProps<CommentBoxProps>()
const inputStr = defineModel<string>('inputText', { required: true })
const imgStrs = defineModel<string>('inputImg', { required: true })
const { uploadPercent, imgList, handleFileChange, handleDeleteImg } = useUploadImg(imgStrs)
const richTextareaRef = useTemplateRef<InstanceType<typeof RichTextarea>>('richTextareaRef')
const handleSelectEmoji = async (emoji: IEmoji) => {
const textarea = richTextareaRef.value?.getTextarea()
if (!textarea) return
// 当选中一段文本时 这俩值是不一样
const start = textarea.selectionStart
const end = textarea.selectionEnd
const value = inputStr.value
// 插入内容(你可以是 [微笑],也可以是 😀)
inputStr.value = value.slice(0, start) + emoji.name + value.slice(end)
// 插入后把光标放到表情后面
await nextTick()
textarea.focus()
textarea.selectionStart = textarea.selectionEnd = start + emoji.name.length
}
defineExpose({
focus: async () => {
await nextTick()
richTextareaRef.value?.getTextarea()?.focus()
},
})
</script>
<template>
<div>
<RichTextarea
ref="richTextareaRef"
v-model="inputStr"
:imgList="imgList"
:uploadPercent="uploadPercent"
@deleteImg="handleDeleteImg"
:height="textAreaHeight"
:placeholder="placeholder"
/>
<div class="flex justify-between items-center mt-3">
<div class="flex items-center gap-2">
<UploadImgIcon @fileChange="handleFileChange" />
<UploadEmojiIcon @selectEmoji="handleSelectEmoji" />
</div>
<div>
<!-- 插槽 用于插入 发表按钮 -->
<slot name="submit" />
</div>
</div>
</div>
</template>
<style scoped></style>
......@@ -9,110 +9,25 @@
<div class="flex gap-3">
<!-- 用户头像 -->
<el-avatar :size="40" :src="userInfo.hiddenAvatar" />
<!-- 评论输入框 -->
<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"
<!-- 删除按钮 -->
<CommentBox
class="flex-1"
:textAreaHeight="100"
v-model:inputText="commentStr"
v-model:inputImg="commentImgStr"
>
<!-- 文本输入区 -->
<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 + '%'"
<template #submit>
<el-button
:disabled="isDisabled"
: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"
>发表</el-button
>
<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>
</template>
</CommentBox>
</div>
<template #footer>
<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"
>发表</el-button
>
</div>
</template>
</el-dialog>
</template>
......@@ -120,9 +35,7 @@
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'
import CommentBox from '../CommentBox/index.vue'
const emit = defineEmits<{
(e: 'commentSuccess'): void
}>()
......@@ -133,13 +46,10 @@ const visible = ref(false)
const commentStr = ref('')
const commentImgStr = ref('')
const loading = ref(false)
const isDisabled = computed(() => !commentStr.value.trim())
const isDisabled = computed(() => !commentStr.value.trim() || loading.value)
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
......@@ -158,24 +68,6 @@ const handleClose = () => {
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 () => {
loading.value = true
......@@ -202,7 +94,3 @@ defineExpose({
open,
})
</script>
<style scoped>
/* 如果需要额外样式可以在这里添加 */
</style>
......@@ -2,7 +2,7 @@
<el-dialog
v-model="visible"
:title="dialogTitle"
width="700px"
width="650px"
class="rounded-2xl overflow-hidden"
:show-close="false"
top="5vh"
......@@ -25,9 +25,9 @@
</div>
</template>
<div class="flex flex-col h-[80vh]">
<div class="flex flex-col h-[75vh]">
<!-- 中间滚动区域 -->
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 pt-0" ref="scrollContainer">
<div class="flex-1 overflow-y-auto custom-scrollbar p-6 pt-0">
<!-- 1. 顶部:父级评论展示 -->
<div v-if="parentComment" class="flex gap-4 bg-gray-50 p-5 rounded-xl">
<img
......@@ -84,7 +84,7 @@
<!-- 2. 下方:回复列表 -->
<div v-loading="loading" class="space-y-6">
<div v-for="item in list" :key="item.id" class="flex gap-4 relative group">
<div v-for="(item, index) in list" :key="item.id" class="flex gap-4 relative group">
<img
:src="item.avatar"
class="w-10 h-10 rounded-full object-cover cursor-pointer hover:opacity-80 transition-opacity flex-shrink-0"
......@@ -94,10 +94,8 @@
<div class="flex items-center justify-between mb-2">
<div class="flex items-center gap-2">
<span class="font-semibold text-gray-900 text-base">{{ item.replyUser }}</span>
<span
v-if="item.replyName && item.replyName !== parentComment?.replyUser"
class="text-gray-400 text-sm flex items-center"
>
<!-- v-if="item.replyName && item.replyName !== parentComment?.replyUser" -->
<span class="text-gray-400 text-sm flex items-center">
<el-icon class="mx-1"><CaretRight /></el-icon>
{{ item.replyName }}
</span>
......@@ -107,7 +105,7 @@
<div class="flex items-center gap-4">
<div
class="flex items-center gap-1.5 cursor-pointer text-gray-400 hover:text-blue-500 transition-colors px-3 py-1.5 rounded-full hover:bg-blue-50"
@click="handleReplyInline(item)"
@click="handleReplyInline(item, index)"
>
<span class="text-sm font-medium">回复</span>
</div>
......@@ -151,36 +149,28 @@
<!-- 内嵌回复框 -->
<div
v-if="currentInlineReplyId === item.id"
v-show="currentInlineReplyId === item.id"
class="mt-4 bg-gray-50 p-4 rounded-xl border border-gray-200 animate-fade-in animate-fade-out"
>
<el-input
v-model="inlineCommentContent"
type="textarea"
:rows="3"
<!-- 评论box -->
<CommentBox
v-model:inputText="commentStr"
v-model:inputImg="imgUrl"
:textAreaHeight="60"
:placeholder="`回复 ${item.replyUser}`"
class="bg-white mb-3 text-base"
resize="none"
/>
<div class="flex justify-between items-center">
<div class="flex gap-3 text-gray-400 text-xl">
<i
class="cursor-pointer i-carbon-face-satisfied hover:text-yellow-500 transition-colors"
></i>
<i
class="cursor-pointer i-carbon-image hover:text-blue-500 transition-colors"
></i>
</div>
<el-button
type="primary"
size="default"
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"
:disabled="!inlineCommentContent.trim()"
@click="submitReply(item.id)"
>
发布
</el-button>
</div>
:ref="(el) => (replyToOtherBoxRefList[index] = el as HTMLElement)"
>
<template #submit>
<el-button
:disabled="isDisabled"
:loading="loadingBtn"
type="primary"
@click="submitReply(item.id)"
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"
>发表</el-button
>
</template>
</CommentBox>
</div>
</div>
</div>
......@@ -203,28 +193,23 @@
<!-- 3. 底部固定回复框 -->
<div
class="border-t border-gray-100 p-5 bg-white flex items-center gap-4 z-10 shadow-[0_-4px_16px_rgba(0,0,0,0.04)]"
class="border-t border-gray-100 p-5 bg-white flex items-start gap-4 z-10 shadow-[0_-4px_16px_rgba(0,0,0,0.04)] pb-0"
>
<img :src="currentUserAvatar" class="w-10 h-10 rounded-full object-cover flex-shrink-0" />
<div
class="flex-1 bg-gray-100 rounded-full px-5 py-3 flex items-center cursor-text hover:bg-gray-200 transition-colors"
@click="focusBottomInput"
>
<input
ref="bottomInputRef"
v-model="bottomCommentContent"
type="text"
class="bg-transparent w-full outline-none text-base text-gray-700 placeholder-gray-400"
:placeholder="`回复 ${parentComment?.replyUser || '...'}`"
@keyup.enter="submitReply(parentComment?.id)"
/>
</div>
<CommentBox
class="flex-1"
v-model:inputText="bottomCommentContent"
v-model:inputImg="bottomImgUrl"
:textAreaHeight="20"
:placeholder="`回复 ${parentComment?.replyUser}`"
/>
<el-button
type="primary"
size="large"
circle
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"
:disabled="!bottomCommentContent.trim()"
:disabled="!bottomCommentContent.trim() || bottomLoadingBtn"
@click="submitReply(parentComment?.id)"
>
<el-icon :size="20"><Position /></el-icon>
......@@ -237,7 +222,6 @@
<script setup lang="ts">
import { ArrowLeft, CaretRight, Position } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import dayjs from 'dayjs'
import { useUserStore } from '@/stores'
import { storeToRefs } from 'pinia'
import {
......@@ -248,10 +232,11 @@ import {
} from '@/api'
import type { CommentItemDto } from '@/api'
import { BooleanFlag } from '@/constants'
import { usePageSearch } from '@/hooks' // 假设你有这个hook
import { usePageSearch } from '@/hooks'
import { parseEmoji } from '@/utils/emoji'
import CommentBox from '../CommentBox/index.vue'
import dayjs from 'dayjs'
// Props
const { articleId, pid } = defineProps<{
articleId: number
pid: number
......@@ -273,13 +258,17 @@ const dialogTitle = ref('详情')
// Inline Reply State
const currentInlineReplyId = ref<number | null>(null)
const inlineCommentContent = ref('')
const bottomCommentContent = ref('')
const bottomInputRef = ref<HTMLInputElement>()
const scrollContainer = ref<HTMLElement>()
const bottomImgUrl = ref('')
const bottomLoadingBtn = ref(false)
const replyToOtherBoxRefList = ref<HTMLElement[]>([])
// --- Actions ---
const commentStr = ref('')
const imgUrl = ref('')
const loadingBtn = ref(false)
const isDisabled = computed(() => !commentStr.value.trim() || loadingBtn.value)
// --- Actions ---
const { list, total, search, searchParams, goToPage, changePageSize, refresh, loading } =
usePageSearch(getSecondCommentChildren, {
defaultParams: {
......@@ -302,7 +291,7 @@ const open = async () => {
// // Reset state
// currentInlineReplyId.value = null
// inlineCommentContent.value = ''
// commentStr.value = ''
// bottomCommentContent.value = ''
console.log('pid', pid)
await nextTick()
......@@ -319,20 +308,19 @@ const formatDate = (time: number) => {
}
// 点击列表中的“回复”
const handleReplyInline = (item: CommentItemDto) => {
const handleReplyInline = (item: CommentItemDto, index: number) => {
if (currentInlineReplyId.value === item.id) {
currentInlineReplyId.value = null // Toggle off
} else {
currentInlineReplyId.value = item.id
inlineCommentContent.value = '' // Clear previous
commentStr.value = '' // Clear previous
imgUrl.value = ''
// 聚焦到输入框
console.log('replyToOtherBoxRefList', replyToOtherBoxRefList.value[index]?.focus)
replyToOtherBoxRefList.value[index]?.focus()
}
}
// 聚焦底部输入框
const focusBottomInput = () => {
bottomInputRef.value?.focus()
}
// 提交评论 (共用逻辑)
// targetId: 如果是回复父评论,传 parentComment.id;如果是回复子评论,传 item.id
const submitReply = async (targetId: number | undefined) => {
......@@ -340,15 +328,19 @@ const submitReply = async (targetId: number | undefined) => {
// 判断使用的是哪个输入框的内容
const isBottom = targetId === parentComment.value?.id
const content = isBottom ? bottomCommentContent.value : inlineCommentContent.value
if (!content.trim()) return
const content = isBottom ? bottomCommentContent.value : commentStr.value
const imgStr = isBottom ? bottomImgUrl.value : imgUrl.value
try {
if (isBottom) {
bottomLoadingBtn.value = true
} else {
loadingBtn.value = true
}
await addComment({
articleId: articleId,
content: content,
pid: targetId, // 这里的pid逻辑根据您的后端接口来,通常回复子评论也是传该子评论ID作为pid
imgUrl: imgStr,
})
ElMessage.success('回复成功')
......@@ -356,8 +348,9 @@ const submitReply = async (targetId: number | undefined) => {
// 清空输入框
if (isBottom) {
bottomCommentContent.value = ''
bottomImgUrl.value = ''
} else {
inlineCommentContent.value = ''
commentStr.value = ''
currentInlineReplyId.value = null
}
......@@ -367,6 +360,12 @@ const submitReply = async (targetId: number | undefined) => {
emit('refresh')
} catch (error) {
console.error(error)
} finally {
if (isBottom) {
bottomLoadingBtn.value = false
} else {
loadingBtn.value = false
}
}
}
......
......@@ -2,6 +2,7 @@
<div class="bg-white p-6 mb-6 rounded-lg shadow-sm">
<div class="flex-1 bg-white rounded-lg border border-gray-200">
<!-- 主输入区域 -->
<div class="flex gap-3 mb-2 items-start">
<!-- 用户头像 -->
<el-avatar
......@@ -28,64 +29,15 @@
</div>
<!-- 主要内容输入 -->
<div class="relative mb-3">
<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"
<RichTextarea
:placeholder="textMap[type].content"
:rows="6"
:maxlength="maxLength"
show-word-limit
resize="none"
:imgList="imgList"
:uploadPercent="uploadPercent"
v-model="form.content"
/> -->
@deleteImg="handleDeleteImg"
:height="100"
/>
</div>
<!-- 标签内容 -->
<div class="mb-2">
......@@ -101,33 +53,6 @@
</span>
</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"
: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>
......@@ -150,46 +75,20 @@
</el-button>
</el-tooltip>
<!-- 隐藏上传文件的input -->
<input
type="file"
class="hidden"
ref="fileInputRef"
@change="handleFileChange"
accept="image/*"
multiple
/>
<el-tooltip content="添加图片" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
@click="fileInputRef?.click()"
:disabled="uploadPercent > 0"
>
<el-icon size="18">
<span v-if="!imgList.length && uploadPercent > 0"> <IEpLoading /></span>
<span v-else> <IEpPicture /></span>
</el-icon>
</el-button>
</el-tooltip>
<!-- <el-tooltip content="添加视频" placement="top">
<UploadImgIcon @fileChange="handleFileChange">
<el-tooltip content="添加图片" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
:disabled="uploadPercent > 0"
>
<el-icon size="18"><VideoPlay /></el-icon>
<el-icon size="18">
<span v-if="!imgList.length && uploadPercent > 0"> <IEpLoading /></span>
<span v-else> <IEpPicture /></span>
</el-icon>
</el-button>
</el-tooltip>
<el-tooltip content="添加附件" placement="top">
<el-button
text
class="w-10 h-10 text-gray-500 hover:bg-gray-100 hover:text-gray-700 rounded-lg"
>
<el-icon size="18"><Paperclip /></el-icon>
</el-button>
</el-tooltip> -->
</UploadImgIcon>
</div>
<!-- 右侧操作按钮 -->
......@@ -237,6 +136,8 @@ import type { AddOrUpdatePracticeDto } from '@/api'
import type { BooleanFlag } from '@/constants'
import type { ElButton } from 'element-plus'
import { useAnimate } from '@vueuse/core'
import RichTextarea from '../RichTextarea/index.vue'
import UploadImgIcon from '../UploadImgIcon/index.vue'
type ArticleType = ArticleTypeEnum.QUESTION | ArticleTypeEnum.PRACTICE
......@@ -278,7 +179,7 @@ const userAvatar = computed(() => (isReal ? userInfo.value.avatar : userInfo.val
const selectTagsDialogRef =
useTemplateRef<InstanceType<typeof SelectTagsDialog>>('selectTagsDialogRef')
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
// const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
const openTour = ref(false)
......
<script setup lang="ts">
// 展示一个textarea 里面可展示图片等(暂时只加入了 图片 后续若有其他的 再添加)
// 暂时用到了快捷发布问吧 和 发布实践 以及 评论相关的内容
interface RichTextareaProps {
placeholder?: string
maxlength?: number
imgList: string[]
uploadPercent: number
height?: number
}
interface Emits {
deleteImg: [img: string]
}
const {
placeholder = '请输入内容',
maxlength,
imgList,
uploadPercent,
height = 55,
} = defineProps<RichTextareaProps>()
const emit = defineEmits<Emits>()
const inputStr = defineModel<string>({ required: true })
const textareaRef = useTemplateRef<HTMLTextAreaElement>('textareaRef')
defineExpose({
getTextarea: () => textareaRef.value,
})
</script>
<template>
<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="textareaRef"
v-model="inputStr"
:placeholder="placeholder"
class="w-full resize-none border-none outline-none text-sm leading-5 text-gray-800"
:style="{ height: height + 'px' }"
:maxlength="maxlength"
/>
<!-- 定位到右边 -->
<span v-if="maxlength" class="flex justify-end text-xs text-gray-400">
{{ inputStr?.length }} / {{ maxlength }}
</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="emit('deleteImg', 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>
</template>
<style scoped lang="scss"></style>
<script setup lang="ts">
import emojis from '@/utils/emoji/face.json'
import type { IEmoji } from '@/utils/emoji/type'
const emit = defineEmits<{
selectEmoji: [emoji: IEmoji]
}>()
</script>
<template>
<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="emit('selectEmoji', item)"
>
<img :src="item.url" alt="" class="w-6 h-6" />
</span>
</div>
</el-scrollbar>
</el-popover>
</div>
</template>
<style scoped></style>
<script lang="tsx">
import type { SetupContext } from 'vue'
interface Emits {
fileChange: [e: Event]
}
export default defineComponent({
setup(_, { slots, emit }: SetupContext<Emits>) {
const fileInputRef = useTemplateRef<HTMLInputElement>('fileInputRef')
const handleFileChange = (e: Event) => {
emit('fileChange', e)
}
return () => {
const UploadImgIcon = slots.default ? (
slots.default()
) : (
<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-icon size="30" class="cursor-pointer">
<svg-icon name="icon_picture" />
</el-icon>
</div>
)
return (
<div>
<input
type="file"
class="hidden"
ref="fileInputRef"
onChange={handleFileChange}
accept="image/*"
multiple
/>
<div onClick={() => fileInputRef?.value?.click()}>{UploadImgIcon}</div>
</div>
)
}
},
})
</script>
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