Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
corporateCulture-qd
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
王立鹏
corporateCulture-qd
Commits
c23750c0
Commit
c23750c0
authored
Feb 05, 2026
by
lijiabin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【需求 20331】 perf: 优化关于评论相关的组件
parent
bed64a7f
Hide whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
369 additions
and
551 deletions
+369
-551
ReplyBox.vue
src/components/common/Comment/components/ReplyBox.vue
+0
-176
index.vue
src/components/common/Comment/index.vue
+26
-57
index.vue
src/components/common/CommentBox/index.vue
+72
-0
index.vue
src/components/common/CommentDialog/index.vue
+18
-130
index.vue
src/components/common/CommentListDialog/index.vue
+70
-71
index.vue
src/components/common/PublishBox/index.vue
+18
-117
index.vue
src/components/common/RichTextarea/index.vue
+85
-0
index.vue
src/components/common/UploadEmojiIcon/index.vue
+37
-0
index.vue
src/components/common/UploadImgIcon/index.vue
+43
-0
No files found.
src/components/common/Comment/components/ReplyBox.vue
deleted
100644 → 0
View file @
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
>
src/components/common/Comment/index.vue
View file @
c23750c0
...
...
@@ -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=
"!comment
ToOther.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
,
...
...
src/components/common/CommentBox/index.vue
0 → 100644
View file @
c23750c0
// 评论框的组件 用于评论的输入 以及 表情 图片的输入 以及 发表按钮
<
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
>
src/components/common/CommentDialog/index.vue
View file @
c23750c0
...
...
@@ -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
>
src/components/common/CommentListDialog/index.vue
View file @
c23750c0
...
...
@@ -2,7 +2,7 @@
<el-dialog
v-model=
"visible"
:title=
"dialogTitle"
width=
"
70
0px"
width=
"
65
0px"
class=
"rounded-2xl overflow-hidden"
:show-close=
"false"
top=
"5vh"
...
...
@@ -25,9 +25,9 @@
</div>
</
template
>
<div
class=
"flex flex-col h-[
80
vh]"
>
<div
class=
"flex flex-col h-[
75
vh]"
>
<!-- 中间滚动区域 -->
<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
}
}
}
...
...
src/components/common/PublishBox/index.vue
View file @
c23750c0
...
...
@@ -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
)
...
...
src/components/common/RichTextarea/index.vue
0 → 100644
View file @
c23750c0
<
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
>
src/components/common/UploadEmojiIcon/index.vue
0 → 100644
View file @
c23750c0
<
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
>
src/components/common/UploadImgIcon/index.vue
0 → 100644
View file @
c23750c0
<
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
>
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment