Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
corporate-culture-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
王立鹏
corporate-culture-qd
Commits
861e50b6
Commit
861e50b6
authored
Oct 19, 2026
by
lijiabin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【需求 25171】feat: 发布/编辑页新增预览功能,问吧问题卡片抽成共用组件
parent
82d62658
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
380 additions
and
267 deletions
+380
-267
index.vue
src/components/common/ArticleContent/index.vue
+4
-2
index.vue
src/components/common/QuestionCard/index.vue
+290
-0
index.vue
src/views/publishLongArticle/index.vue
+68
-3
index.vue
src/views/questionDetail/index.vue
+18
-262
No files found.
src/components/common/ArticleContent/index.vue
View file @
861e50b6
...
@@ -95,6 +95,7 @@
...
@@ -95,6 +95,7 @@
alt=
""
alt=
""
class=
"w-12 h-12 rounded-full object-cover cursor-pointer"
class=
"w-12 h-12 rounded-full object-cover cursor-pointer"
@
click=
"
@
click=
"
!isPreview &&
jumpToUserHomePage(
{
jumpToUserHomePage(
{
userId: articleDetail.createUserId,
userId: articleDetail.createUserId,
type: articleDetail.type,
type: articleDetail.type,
...
@@ -163,7 +164,7 @@
...
@@ -163,7 +164,7 @@
{{
articleType
}}
{{
articleType
}}
</span>
</span>
<ActionMore
:articleDetail=
"articleDetail"
/>
<ActionMore
v-if=
"!isPreview"
:articleDetail=
"articleDetail"
/>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -305,9 +306,10 @@ const userStore = useUserStore()
...
@@ -305,9 +306,10 @@ const userStore = useUserStore()
const
{
userInfo
}
=
storeToRefs
(
userStore
)
const
{
userInfo
}
=
storeToRefs
(
userStore
)
const
router
=
useRouter
()
const
router
=
useRouter
()
const
{
jumpToUserHomePage
}
=
useNavigation
()
const
{
jumpToUserHomePage
}
=
useNavigation
()
const
{
articleDetail
,
isAudit
}
=
defineProps
<
{
const
{
articleDetail
,
isAudit
,
isPreview
=
false
}
=
defineProps
<
{
articleDetail
:
ArticleItemDto
articleDetail
:
ArticleItemDto
isAudit
:
boolean
// 是否是审核页面
isAudit
:
boolean
// 是否是审核页面
isPreview
?:
boolean
// 是否是发布页的预览:数据是前端拼的,头像不跳转、不展示举报
}
>
()
}
>
()
const
sendMessageDialogRef
=
ref
<
InstanceType
<
typeof
SendMessageDialog
>
|
null
>
(
null
)
const
sendMessageDialogRef
=
ref
<
InstanceType
<
typeof
SendMessageDialog
>
|
null
>
(
null
)
...
...
src/components/common/QuestionCard/index.vue
0 → 100644
View file @
861e50b6
<
template
>
<div
class=
"bg-white rounded-lg p-6 shadow-[0_1px_3px_rgba(0,0,0,0.02)] border border-slate-100 relative"
>
<slot
/>
<el-skeleton
:rows=
"5"
animated
:loading=
"loading"
:throttle=
"
{ leading: 0, trailing: 300 }">
<template
#
template
>
<!-- 顶部标签行 -->
<div
class=
"flex flex-wrap gap-2 mb-4 justify-between"
>
<!-- 标签 -->
<div
class=
"flex gap-2"
>
<el-skeleton-item
v-for=
"i in 3"
:key=
"i"
variant=
"text"
style=
"width: 60px; height: 18px; border-radius: 9999px"
/>
</div>
<!-- 编辑按钮 -->
<el-skeleton-item
variant=
"text"
style=
"width: 40px; height: 18px"
/>
</div>
<!-- 发布人信息 -->
<div
class=
"flex items-center gap-3 pb-3 border-b border-slate-100"
>
<!-- 头像 -->
<el-skeleton-item
variant=
"circle"
style=
"width: 40px; height: 40px"
/>
<!-- 用户名 + 时间 -->
<div
class=
"flex flex-col gap-2"
>
<el-skeleton-item
variant=
"text"
style=
"width: 100px; height: 14px"
/>
<el-skeleton-item
variant=
"text"
style=
"width: 180px; height: 12px"
/>
</div>
</div>
<!-- 标题 -->
<div
class=
"mt-3 mb-3"
>
<el-skeleton-item
variant=
"text"
style=
"width: 90%; height: 28px"
/>
</div>
<!-- 内容描述 -->
<div
class=
"space-y-2"
>
<el-skeleton-item
variant=
"text"
style=
"width: 100%"
/>
<el-skeleton-item
variant=
"text"
style=
"width: 95%"
/>
<el-skeleton-item
variant=
"text"
style=
"width: 85%"
/>
</div>
<!-- 图片列表 -->
<!--
<div
class=
"mt-3 flex gap-2 flex-wrap"
>
<el-skeleton-item
v-for=
"i in 3"
:key=
"i"
variant=
"image"
style=
"width: 96px; height: 96px; border-radius: 8px"
/>
</div>
-->
<!-- 底部操作栏 -->
<div
class=
"flex items-center justify-between mt-4"
>
<!-- 写回答按钮 -->
<el-skeleton-item
variant=
"button"
style=
"width: 88px; height: 32px"
/>
<!-- 右侧点赞 / 收藏 -->
<div
class=
"flex items-center gap-6"
>
<el-skeleton-item
variant=
"text"
style=
"width: 48px; height: 16px"
/>
<el-skeleton-item
variant=
"text"
style=
"width: 48px; height: 16px"
/>
</div>
</div>
</
template
>
<
template
#
default
>
<!-- 顶部标签行 -->
<div
class=
"flex flex-wrap gap-2 mb-4 justify-between"
>
<div>
<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 mr-2"
>
#
{{
tag
}}
</span>
</div>
<!-- 后面加一个编辑 -->
<el-link
v-if=
"isAuthor"
type=
"primary"
:underline=
"false"
@
click=
"
router.push(`/publishLongArticle/$
{questionDetail.type}?id=${questionDetail.id}`)
"
class="text-sm"
>
编辑
</el-link>
</div>
<!-- 👇 新增:发布人信息 -->
<div
class=
"flex items-center gap-3 pb-3 border-b border-slate-100"
>
<el-avatar
:src=
"questionDetail.createUserAvatar"
:size=
"40"
class=
"flex-shrink-0 cursor-pointer"
@
click=
"
!isPreview &&
jumpToUserHomePage(
{
userId: questionDetail.createUserId,
type: ArticleTypeEnum.QUESTION,
})
"
>
{{
questionDetail
.
createUserName
}}
</el-avatar>
<div
class=
"flex flex-col flex-1 min-w-0"
>
<span
class=
"text-sm text-slate-900"
>
{{
questionDetail
.
createUserName
}}
</span>
<span
class=
"text-sm text-slate-500 flex items-center gap-1"
>
{{
dayjs
(
questionDetail
.
createTime
*
1000
).
format
(
'YYYY-MM-DD HH:mm'
)
}}
·
<span
v-if=
"questionDetail?.region"
class=
"text-sm text-slate-500 flex items-center gap-1"
>
{{
questionDetail
.
region
}}
·
</span>
{{
questionDetail
.
viewCount
}}
阅读
</span>
</div>
<button
v-if=
"!isAuthor && questionDetail.createUserId"
type=
"button"
class=
"shrink-0 inline-flex items-center px-3 py-1.5 text-xs font-semibold text-blue-600 bg-blue-50 rounded-lg border border-blue-100 hover:bg-blue-100 hover:border-blue-200/80 active:scale-[0.98] transition-all cursor-pointer"
@
click=
"emit('message')"
>
私信
</button>
</div>
<!-- 标题:主要信息,黑重粗 -->
<h1
class=
"text-2xl font-bold text-slate-900 mb-3 leading-snug tracking-tight"
>
{{
questionDetail
.
title
}}
</h1>
<!-- 描述:次要信息,深灰 -->
<div
class=
"transition-all duration-300"
>
<div
:class=
"
{ 'line-clamp-3': !isExpand }"
ref="questionContentRef"
class="html-content"
v-html="parseEmoji(questionDetail.content)"
v-image-preview
v-parse-comment
>
</div>
<button
v-if=
"isOverThreeLine"
class=
"text-blue-500 text-sm font-medium hover:text-blue-600 mt-1 flex items-center gap-0.5 cursor-pointer"
@
click=
"isExpand = !isExpand"
>
{{
isExpand
?
'收起'
:
'显示全部'
}}
<el-icon
:class=
"
{ 'rotate-180': isExpand }" class="transition-transform"
>
<IEpCaretBottom
/></el-icon>
</button>
</div>
<div
v-if=
"questionDetail.imgUrlList?.length"
class=
"mt-3 flex gap-2 flex-wrap items-center justify-start"
>
<el-image
v-for=
"(item, i) in questionDetail.imgUrlList"
:key=
"item"
:src=
"item"
fit=
"cover"
class=
"rounded-lg w-24 h-24 hover:scale-105 transition-transform cursor-pointer"
:preview-src-list=
"questionDetail.imgUrlList"
:initial-index=
"i"
:preview-teleported=
"true"
/>
</div>
<!-- 底部操作栏:预览时没有这些操作 -->
<div
v-if=
"!isPreview"
class=
"flex items-center justify-between mt-4"
>
<div
class=
"flex gap-3"
>
<button
class=
"px-5 py-1.5 border !bg-blue-500 !text-white rounded-[4px] text-sm font-medium transition-colors flex items-center gap-1 cursor-pointer"
@
click=
"emit('write')"
>
<el-icon><IEpEditPen
/></el-icon>
写回答
</button>
</div>
<!-- 右侧数据 -->
<div
class=
"flex items-center gap-1 text-slate-500 text-base select-none"
>
<el-button
text
class=
"!ml-0 flex items-center gap-1 transition-colors hover:text-blue-500"
:class=
"
{ '!text-blue-500': questionDetail?.hasPraised }"
@click="emit('like')"
>
<el-icon
size=
"17"
>
<svg-icon
:name=
"questionDetail?.hasPraised ? 'praise_fill' : 'praise'"
></svg-icon>
</el-icon>
<span
class=
"text-15px"
>
{{
questionDetail
?.
praiseCount
||
0
}}
</span>
</el-button>
<el-button
text
class=
"!ml-0 flex items-center gap-1 transition-colors hover:text-blue-500"
:class=
"
{ '!text-blue-500': questionDetail?.hasCollect }"
@click="emit('collect')"
>
<el-icon
size=
"17"
>
<svg-icon
:name=
"questionDetail?.hasCollect ? 'collection_fill' : 'collection'"
></svg-icon>
</el-icon>
<span
class=
"text-15px"
>
{{
questionDetail
?.
collectionCount
||
0
}}
</span>
</el-button>
<el-button
text
class=
"!ml-0 flex items-center gap-2 transition-colors hover:text-blue-500"
@
click=
"emit('share')"
>
<el-icon
size=
"17"
>
<svg-icon
name=
"share"
size=
"15"
></svg-icon>
</el-icon>
<span
class=
"text-15px"
>
分享
</span>
</el-button>
</div>
</div>
</
template
>
</el-skeleton>
<!-- 正文里的图片预览 -->
<el-image-viewer
v-if=
"showPreview"
:url-list=
"srcList"
show-progress
:teleported=
"true"
:initial-index=
"currentPreviewIndex"
@
close=
"showPreview = false"
/>
</div>
</template>
<
script
setup
lang=
"ts"
>
/**
* 问吧详情页顶部的问题卡片(标签、发布人、标题、正文、配图、点赞收藏分享)。
* questionDetail 页和发布页的预览共用这一个组件,保证预览跟真实页面长得一样;
* 预览时传 isPreview,头像不跳转、不展示底部操作栏。
* 点击这些操作只往外抛事件,具体怎么处理由页面决定。
*/
import
dayjs
from
'dayjs'
import
type
{
ArticleItemDto
}
from
'@/api'
import
{
ArticleTypeEnum
}
from
'@/constants'
import
{
useImagePreview
,
useNavigation
}
from
'@/hooks'
import
{
parseEmoji
}
from
'@/utils/emoji'
const
{
questionDetail
,
isAuthor
=
false
,
isPreview
=
false
,
}
=
defineProps
<
{
questionDetail
:
ArticleItemDto
isAuthor
?:
boolean
isPreview
?:
boolean
}
>
()
const
emit
=
defineEmits
<
{
write
:
[]
like
:
[]
collect
:
[]
share
:
[]
message
:
[]
}
>
()
const
router
=
useRouter
()
const
{
jumpToUserHomePage
}
=
useNavigation
()
const
{
showPreview
,
currentPreviewIndex
,
srcList
,
vImagePreview
}
=
useImagePreview
()
const
loading
=
computed
(()
=>
!
questionDetail
.
title
)
const
questionContentRef
=
useTemplateRef
<
HTMLElement
>
(
'questionContentRef'
)
const
isExpand
=
ref
(
false
)
// 检测当前是否超过三行 要用到具体的dom
const
isOverThreeLine
=
computed
(()
=>
{
if
(
!
questionContentRef
.
value
||
!
questionDetail
.
content
)
return
false
const
lineHeight
=
parseFloat
(
getComputedStyle
(
questionContentRef
.
value
).
lineHeight
)
const
height
=
questionContentRef
.
value
!
.
scrollHeight
const
maxHeight
=
lineHeight
*
3
return
height
>
maxHeight
})
</
script
>
src/views/publishLongArticle/index.vue
View file @
861e50b6
<
template
>
<
template
>
<div
class=
"min-h-screen bg-gradient-to-br pb-10 relative"
>
<div
class=
"min-h-screen bg-gradient-to-br pb-10 relative"
>
<BackButton
/>
<BackButton
/>
<!-- 主编辑区域 -->
<!-- 主编辑区域:预览时用 v-show 藏起来而不是销毁,返回编辑时编辑器里的内容和光标状态都还在 -->
<div
class=
"bg-white rounded-lg shadow-lg border border-gray-100/50 overflow-hidden"
>
<div
v-show=
"!previewing"
class=
"bg-white rounded-lg shadow-lg border border-gray-100/50 overflow-hidden"
>
<div
class=
"px-10 py-5"
>
<div
class=
"px-10 py-5"
>
<!-- 标题输入 -->
<!-- 标题输入 -->
<el-form-item
class=
"mb-8"
>
<el-form-item
class=
"mb-8"
>
...
@@ -41,11 +44,21 @@
...
@@ -41,11 +44,21 @@
</div>
</div>
<!-- 底部固定按钮栏 -->
<!-- 底部固定按钮栏 -->
<div
class=
"fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg"
>
<div
v-show=
"!previewing"
class=
"fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 shadow-lg"
>
<div
class=
"max-w-[1200px] mx-auto px-4 py-3"
>
<div
class=
"max-w-[1200px] mx-auto px-4 py-3"
>
<div
class=
"flex justify-center gap-4"
>
<div
class=
"flex justify-center gap-4"
>
<el-button
@
click=
"handleClosed"
class=
"rounded-lg min-w[120px]"
>
取消
</el-button>
<el-button
@
click=
"handleClosed"
class=
"rounded-lg min-w[120px]"
>
取消
</el-button>
<el-button
<el-button
:disabled=
"!sharedForm.content"
@
click=
"previewing = true"
class=
"rounded-lg min-w[120px]"
>
预览
</el-button>
<el-button
type=
"primary"
type=
"primary"
:disabled=
"!canPublish"
:disabled=
"!canPublish"
@
click=
"openDrawer"
@
click=
"openDrawer"
...
@@ -89,6 +102,7 @@
...
@@ -89,6 +102,7 @@
<
template
#
footer
>
<
template
#
footer
>
<div
class=
"flex gap-3 justify-end"
>
<div
class=
"flex gap-3 justify-end"
>
<el-button
@
click=
"handleDrawerClose"
class=
"rounded-lg"
>
取消
</el-button>
<el-button
@
click=
"handleDrawerClose"
class=
"rounded-lg"
>
取消
</el-button>
<el-button
@
click=
"handlePreviewFromDrawer"
class=
"rounded-lg"
>
预览
</el-button>
<el-button
<el-button
:loading=
"loading"
:loading=
"loading"
@
click=
"handleSubmit(ReleaseStatusTypeEnum.DRAFT)"
@
click=
"handleSubmit(ReleaseStatusTypeEnum.DRAFT)"
...
@@ -107,6 +121,19 @@
...
@@ -107,6 +121,19 @@
</div>
</div>
</
template
>
</
template
>
</el-drawer>
</el-drawer>
<!-- 预览:直接用详情页的 ArticleContent 渲染,所见即所得,不用先保存也不需要接口 -->
<div
v-if=
"previewing"
class=
"space-y-4"
>
<!-- 问吧详情页的样子跟其它图文类完全不同,用它自己的问题卡片;其它类型用 ArticleContent -->
<QuestionCard
v-if=
"type === ArticleTypeEnum.QUESTION"
:questionDetail=
"previewDetail"
isPreview
/>
<ArticleContent
v-else
:articleDetail=
"previewDetail"
:isAudit=
"true"
:isPreview=
"true"
/>
<div
class=
"flex justify-end gap-3"
>
<el-button
@
click=
"previewing = false"
>
返回编辑
</el-button>
<el-button
type=
"primary"
:disabled=
"!canPublish"
@
click=
"openDrawer"
>
{{ isEdit ? '编辑' : '发布' }}
</el-button>
</div>
</div>
</div>
</div>
</template>
</template>
...
@@ -132,6 +159,10 @@ import type { ArticleItemDto } from '@/api'
...
@@ -132,6 +159,10 @@ import type { ArticleItemDto } from '@/api'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
useRouter
,
useRoute
}
from
'vue-router'
import
{
push
}
from
'notivue'
import
{
push
}
from
'notivue'
import
BackButton
from
'@/components/common/BackButton/index.vue'
import
BackButton
from
'@/components/common/BackButton/index.vue'
import
ArticleContent
from
'@/components/common/ArticleContent/index.vue'
import
QuestionCard
from
'@/components/common/QuestionCard/index.vue'
import
{
useUserStore
}
from
'@/stores/user'
import
{
storeToRefs
}
from
'pinia'
import
type
{
ArticleTypeFormExpose
}
from
'./types'
import
type
{
ArticleTypeFormExpose
}
from
'./types'
import
PostForm
from
'./components/PostForm.vue'
import
PostForm
from
'./components/PostForm.vue'
import
ColumnForm
from
'./components/ColumnForm.vue'
import
ColumnForm
from
'./components/ColumnForm.vue'
...
@@ -183,6 +214,39 @@ const handleDrawerClose = () => {
...
@@ -183,6 +214,39 @@ const handleDrawerClose = () => {
drawerVisible
.
value
=
false
drawerVisible
.
value
=
false
}
}
// 预览:用当前编辑器里的标题/内容和自己的头像昵称拼一份假的文章详情,交给 ArticleContent 渲染。
// 标签、浏览量这些要后端数据的部分预览里没有,也不需要
const
previewing
=
ref
(
false
)
const
userStore
=
useUserStore
()
const
{
userInfo
}
=
storeToRefs
(
userStore
)
// 抽屉里也能预览:关掉抽屉进预览,抽屉里已填的内容不会丢(子表单没销毁),预览里点发布会再打开
const
handlePreviewFromDrawer
=
()
=>
{
drawerVisible
.
value
=
false
previewing
.
value
=
true
}
const
previewDetail
=
computed
(()
=>
{
// 实践/问吧的配图在抽屉子表单里,拿得到就带上
const
imgUrl
=
subFormRef
.
value
?.
getSubmitData
().
imgUrl
as
string
|
undefined
const
isReal
=
[
ArticleTypeEnum
.
PRACTICE
,
ArticleTypeEnum
.
INTERVIEW
,
ArticleTypeEnum
.
QUESTION
,
].
includes
(
type
)
return
{
type
,
title
:
sharedForm
.
value
.
title
,
content
:
sharedForm
.
value
.
content
,
imgUrl
,
imgUrlList
:
imgUrl
?.
split
(
','
).
filter
(
Boolean
)
??
[],
createTime
:
Math
.
floor
(
Date
.
now
()
/
1000
),
viewCount
:
0
,
createUserName
:
isReal
?
userInfo
.
value
.
name
:
userInfo
.
value
.
hiddenName
,
createUserAvatar
:
isReal
?
userInfo
.
value
.
avatar
:
userInfo
.
value
.
hiddenAvatar
,
tagNameList
:
[],
tagIdList
:
[],
}
as
unknown
as
ArticleItemDto
})
// 取消按钮
// 取消按钮
const
handleClosed
=
()
=>
{
const
handleClosed
=
()
=>
{
resetSharedForm
()
resetSharedForm
()
...
@@ -228,6 +292,7 @@ const editorKey = ref(0)
...
@@ -228,6 +292,7 @@ const editorKey = ref(0)
const
isEdit
=
computed
(()
=>
!!
route
.
query
.
id
)
const
isEdit
=
computed
(()
=>
!!
route
.
query
.
id
)
onActivated
(
async
()
=>
{
onActivated
(
async
()
=>
{
previewing
.
value
=
false
editorKey
.
value
++
editorKey
.
value
++
resetSharedForm
()
resetSharedForm
()
subFormRef
.
value
?.
reset
()
subFormRef
.
value
?.
reset
()
...
...
src/views/questionDetail/index.vue
View file @
861e50b6
...
@@ -2,246 +2,17 @@
...
@@ -2,246 +2,17 @@
<div
class=
"min-h-screen pb-10 text-slate-800 flex justify-center sm:px-20"
>
<div
class=
"min-h-screen pb-10 text-slate-800 flex justify-center sm:px-20"
>
<div
class=
"w-full space-y-4"
>
<div
class=
"w-full space-y-4"
>
<!-- 1. 问题卡片 -->
<!-- 1. 问题卡片 -->
<div
<QuestionCard
class=
"bg-white rounded-lg p-6 shadow-[0_1px_3px_rgba(0,0,0,0.02)] border border-slate-100 relative"
:questionDetail=
"questionDetail"
:isAuthor=
"isAuthor"
@
write=
"openCommentDialog"
@
like=
"handleLikeArticle"
@
collect=
"handleCollectArticle"
@
share=
"handleShareQuestion"
@
message=
"openSendMessage"
>
>
<BackButton
/>
<BackButton
/>
<el-skeleton
</QuestionCard>
:rows=
"5"
animated
:loading=
"loading"
:throttle=
"
{ leading: 0, trailing: 300 }"
>
<template
#
template
>
<!-- 顶部标签行 -->
<div
class=
"flex flex-wrap gap-2 mb-4 justify-between"
>
<!-- 标签 -->
<div
class=
"flex gap-2"
>
<el-skeleton-item
v-for=
"i in 3"
:key=
"i"
variant=
"text"
style=
"width: 60px; height: 18px; border-radius: 9999px"
/>
</div>
<!-- 编辑按钮 -->
<el-skeleton-item
variant=
"text"
style=
"width: 40px; height: 18px"
/>
</div>
<!-- 发布人信息 -->
<div
class=
"flex items-center gap-3 pb-3 border-b border-slate-100"
>
<!-- 头像 -->
<el-skeleton-item
variant=
"circle"
style=
"width: 40px; height: 40px"
/>
<!-- 用户名 + 时间 -->
<div
class=
"flex flex-col gap-2"
>
<el-skeleton-item
variant=
"text"
style=
"width: 100px; height: 14px"
/>
<el-skeleton-item
variant=
"text"
style=
"width: 180px; height: 12px"
/>
</div>
</div>
<!-- 标题 -->
<div
class=
"mt-3 mb-3"
>
<el-skeleton-item
variant=
"text"
style=
"width: 90%; height: 28px"
/>
</div>
<!-- 内容描述 -->
<div
class=
"space-y-2"
>
<el-skeleton-item
variant=
"text"
style=
"width: 100%"
/>
<el-skeleton-item
variant=
"text"
style=
"width: 95%"
/>
<el-skeleton-item
variant=
"text"
style=
"width: 85%"
/>
</div>
<!-- 图片列表 -->
<!--
<div
class=
"mt-3 flex gap-2 flex-wrap"
>
<el-skeleton-item
v-for=
"i in 3"
:key=
"i"
variant=
"image"
style=
"width: 96px; height: 96px; border-radius: 8px"
/>
</div>
-->
<!-- 底部操作栏 -->
<div
class=
"flex items-center justify-between mt-4"
>
<!-- 写回答按钮 -->
<el-skeleton-item
variant=
"button"
style=
"width: 88px; height: 32px"
/>
<!-- 右侧点赞 / 收藏 -->
<div
class=
"flex items-center gap-6"
>
<el-skeleton-item
variant=
"text"
style=
"width: 48px; height: 16px"
/>
<el-skeleton-item
variant=
"text"
style=
"width: 48px; height: 16px"
/>
</div>
</div>
</
template
>
<
template
#
default
>
<!-- 顶部标签行 -->
<div
class=
"flex flex-wrap gap-2 mb-4 justify-between"
>
<div>
<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 mr-2"
>
#
{{
tag
}}
</span>
</div>
<!-- 后面加一个编辑 -->
<el-link
v-if=
"isAuthor"
type=
"primary"
:underline=
"false"
@
click=
"
router.push(`/publishLongArticle/$
{questionDetail.type}?id=${questionDetail.id}`)
"
class="text-sm"
>
编辑
</el-link>
</div>
<!-- 👇 新增:发布人信息 -->
<div
class=
"flex items-center gap-3 pb-3 border-b border-slate-100"
>
<el-avatar
:src=
"questionDetail.createUserAvatar"
:size=
"40"
class=
"flex-shrink-0 cursor-pointer"
@
click=
"
jumpToUserHomePage(
{
userId: questionDetail.createUserId,
type: ArticleTypeEnum.QUESTION,
})
"
>
{{
questionDetail
.
createUserName
}}
</el-avatar>
<div
class=
"flex flex-col flex-1 min-w-0"
>
<span
class=
"text-sm text-slate-900"
>
{{
questionDetail
.
createUserName
}}
</span>
<span
class=
"text-sm text-slate-500 flex items-center gap-1"
>
{{
dayjs
(
questionDetail
.
createTime
*
1000
).
format
(
'YYYY-MM-DD HH:mm'
)
}}
·
<span
v-if=
"questionDetail?.region"
class=
"text-sm text-slate-500 flex items-center gap-1"
>
{{
questionDetail
.
region
}}
·
</span>
{{
questionDetail
.
viewCount
}}
阅读
</span>
</div>
<button
v-if=
"!isAuthor && questionDetail.createUserId"
type=
"button"
class=
"shrink-0 inline-flex items-center px-3 py-1.5 text-xs font-semibold text-blue-600 bg-blue-50 rounded-lg border border-blue-100 hover:bg-blue-100 hover:border-blue-200/80 active:scale-[0.98] transition-all cursor-pointer"
@
click=
"
sendMessageDialogRef?.open(
{
receiverId: String(questionDetail.createUserId),
isReal: BooleanFlag.YES,
})
"
>
私信
</button>
</div>
<!-- 标题:主要信息,黑重粗 -->
<h1
class=
"text-2xl font-bold text-slate-900 mb-3 leading-snug tracking-tight"
>
{{
questionDetail
.
title
}}
</h1>
<!-- 描述:次要信息,深灰 -->
<div
class=
"transition-all duration-300"
>
<div
:class=
"
{ 'line-clamp-3': !isExpand }"
ref="questionContentRef"
class="html-content"
v-html="parseEmoji(questionDetail.content)"
v-image-preview
v-parse-comment
>
</div>
<button
v-if=
"isOverThreeLine"
class=
"text-blue-500 text-sm font-medium hover:text-blue-600 mt-1 flex items-center gap-0.5 cursor-pointer"
@
click=
"isExpand = !isExpand"
>
{{
isExpand
?
'收起'
:
'显示全部'
}}
<el-icon
:class=
"
{ 'rotate-180': isExpand }" class="transition-transform"
>
<IEpCaretBottom
/></el-icon>
</button>
</div>
<div
v-if=
"questionDetail.imgUrlList?.length"
class=
"mt-3 flex gap-2 flex-wrap items-center justify-start"
>
<el-image
v-for=
"(item, i) in questionDetail.imgUrlList"
:key=
"item"
:src=
"item"
fit=
"cover"
class=
"rounded-lg w-24 h-24 hover:scale-105 transition-transform cursor-pointer"
:preview-src-list=
"questionDetail.imgUrlList"
:initial-index=
"i"
:preview-teleported=
"true"
/>
</div>
<!-- 底部操作栏 -->
<div
class=
"flex items-center justify-between mt-4"
>
<div
class=
"flex gap-3"
>
<button
class=
"px-5 py-1.5 border !bg-blue-500 !text-white rounded-[4px] text-sm font-medium transition-colors flex items-center gap-1 cursor-pointer"
@
click=
"openCommentDialog"
>
<el-icon><IEpEditPen
/></el-icon>
写回答
</button>
</div>
<!-- 右侧数据 -->
<div
class=
"flex items-center gap-1 text-slate-500 text-base select-none"
>
<el-button
text
class=
"!ml-0 flex items-center gap-1 transition-colors hover:text-blue-500"
:class=
"
{ '!text-blue-500': questionDetail?.hasPraised }"
@click="handleLikeArticle"
>
<el-icon
size=
"17"
>
<svg-icon
:name=
"questionDetail?.hasPraised ? 'praise_fill' : 'praise'"
></svg-icon>
</el-icon>
<span
class=
"text-15px"
>
{{
questionDetail
?.
praiseCount
||
0
}}
</span>
</el-button>
<el-button
text
class=
"!ml-0 flex items-center gap-1 transition-colors hover:text-blue-500"
:class=
"
{ '!text-blue-500': questionDetail?.hasCollect }"
@click="handleCollectArticle"
>
<el-icon
size=
"17"
>
<svg-icon
:name=
"questionDetail?.hasCollect ? 'collection_fill' : 'collection'"
></svg-icon>
</el-icon>
<span
class=
"text-15px"
>
{{
questionDetail
?.
collectionCount
||
0
}}
</span>
</el-button>
<el-button
text
class=
"!ml-0 flex items-center gap-2 transition-colors hover:text-blue-500"
@
click=
"handleShareQuestion"
>
<el-icon
size=
"17"
>
<svg-icon
name=
"share"
size=
"15"
></svg-icon>
</el-icon>
<span
class=
"text-15px"
>
分享
</span>
</el-button>
</div>
</div>
</
template
>
</el-skeleton>
</div>
<!-- 2. 列表控制栏 -->
<!-- 2. 列表控制栏 -->
<div
<div
...
@@ -452,14 +223,6 @@
...
@@ -452,14 +223,6 @@
<
/div
>
<
/div
>
<
CommentDialog
ref
=
"commentDialogRef"
@
commentSuccess
=
"refresh"
/>
<
CommentDialog
ref
=
"commentDialogRef"
@
commentSuccess
=
"refresh"
/>
<
SendMessageDialog
ref
=
"sendMessageDialogRef"
/>
<
SendMessageDialog
ref
=
"sendMessageDialogRef"
/>
<el-image-viewer
v-if=
"showPreview"
:url-list=
"srcList"
show-progress
:teleported=
"true"
:initial-index=
"currentPreviewIndex"
@
close=
"showPreview = false"
/>
<
/div
>
<
/div
>
<
/template
>
<
/template
>
<
script
setup
lang
=
"ts"
>
<
script
setup
lang
=
"ts"
>
...
@@ -480,10 +243,11 @@ import Comment from '@/components/common/Comment/index.vue'
...
@@ -480,10 +243,11 @@ import Comment from '@/components/common/Comment/index.vue'
import
CommentDialog
from
'@/components/common/CommentDialog/index.vue'
import
CommentDialog
from
'@/components/common/CommentDialog/index.vue'
import
SendMessageDialog
from
'@/components/common/SendMessageDialog/index.vue'
import
SendMessageDialog
from
'@/components/common/SendMessageDialog/index.vue'
import
BackButton
from
'@/components/common/BackButton/index.vue'
import
BackButton
from
'@/components/common/BackButton/index.vue'
import
QuestionCard
from
'@/components/common/QuestionCard/index.vue'
import
dayjs
from
'dayjs'
import
dayjs
from
'dayjs'
import
{
useUserStore
}
from
'@/stores/user'
import
{
useUserStore
}
from
'@/stores/user'
import
{
storeToRefs
}
from
'pinia'
import
{
storeToRefs
}
from
'pinia'
import
{
use
ImagePreview
,
use
MessageBox
,
useNavigation
,
useScrollTop
}
from
'@/hooks'
import
{
useMessageBox
,
useNavigation
,
useScrollTop
}
from
'@/hooks'
import
{
parseEmoji
}
from
'@/utils/emoji'
import
{
parseEmoji
}
from
'@/utils/emoji'
import
{
isWxShareCancel
,
wxShare
}
from
'@/utils/wxUtil'
import
{
isWxShareCancel
,
wxShare
}
from
'@/utils/wxUtil'
import
{
ArticleTypeEnum
,
BooleanFlag
}
from
'@/constants'
import
{
ArticleTypeEnum
,
BooleanFlag
}
from
'@/constants'
...
@@ -495,7 +259,6 @@ const { userInfo } = storeToRefs(userStore)
...
@@ -495,7 +259,6 @@ const { userInfo } = storeToRefs(userStore)
const
{
jumpToUserHomePage
}
=
useNavigation
()
const
{
jumpToUserHomePage
}
=
useNavigation
()
const
{
confirm
}
=
useMessageBox
()
const
{
confirm
}
=
useMessageBox
()
const
route
=
useRoute
()
const
route
=
useRoute
()
const
router
=
useRouter
()
const
questionId
=
Number
(
route
.
params
.
id
)
const
questionId
=
Number
(
route
.
params
.
id
)
const
commentRefList
=
ref
<
InstanceType
<
typeof
Comment
>
[]
>
([])
const
commentRefList
=
ref
<
InstanceType
<
typeof
Comment
>
[]
>
([])
...
@@ -506,31 +269,17 @@ const commentDialogRef = useTemplateRef<typeof CommentDialog>('commentDialogRef'
...
@@ -506,31 +269,17 @@ const commentDialogRef = useTemplateRef<typeof CommentDialog>('commentDialogRef'
const
sendMessageDialogRef
=
useTemplateRef
<
InstanceType
<
typeof
SendMessageDialog
>
|
null
>
(
const
sendMessageDialogRef
=
useTemplateRef
<
InstanceType
<
typeof
SendMessageDialog
>
|
null
>
(
'sendMessageDialogRef'
,
'sendMessageDialogRef'
,
)
)
const
{
showPreview
,
currentPreviewIndex
,
srcList
,
vImagePreview
}
=
useImagePreview
()
// 回滚到子评论框
// 回滚到子评论框
const
{
handleBackTop
:
handleBackTopChildren
}
=
useScrollTop
(
answerRefList
)
const
{
handleBackTop
:
handleBackTopChildren
}
=
useScrollTop
(
answerRefList
)
// 翻页时滚回到回答列表顶部
// 翻页时滚回到回答列表顶部
const
{
handleBackTop
:
handleBackTopAnswerList
}
=
useScrollTop
(
answerListRef
)
const
{
handleBackTop
:
handleBackTopAnswerList
}
=
useScrollTop
(
answerListRef
)
const
loading
=
computed
(()
=>
!
questionDetail
.
value
.
title
)
const
isSelfAnswer
=
(
answer
:
CommentItemDto
)
=>
const
isSelfAnswer
=
(
answer
:
CommentItemDto
)
=>
String
(
answer
.
userId
||
''
)
===
String
(
userInfo
.
value
.
userId
||
''
)
String
(
answer
.
userId
||
''
)
===
String
(
userInfo
.
value
.
userId
||
''
)
const
isAuthor
=
computed
(()
=>
{
const
isAuthor
=
computed
(()
=>
{
return
questionDetail
.
value
.
createUserId
===
userInfo
.
value
.
userId
return
questionDetail
.
value
.
createUserId
===
userInfo
.
value
.
userId
}
)
}
)
const
questionContentRef
=
useTemplateRef
<
HTMLElement
>
(
'questionContentRef'
)
const
isExpand
=
ref
(
false
)
// 检测当前是否超过三行 要用到具体的dom
const
isOverThreeLine
=
computed
(()
=>
{
if
(
!
questionContentRef
.
value
||
!
questionDetail
.
value
.
content
)
return
false
const
lineHeight
=
parseFloat
(
getComputedStyle
(
questionContentRef
.
value
).
lineHeight
)
const
height
=
questionContentRef
.
value
!
.
scrollHeight
const
maxHeight
=
lineHeight
*
3
return
height
>
maxHeight
})
const
getQuestionDetail
=
async
()
=>
{
const
getQuestionDetail
=
async
()
=>
{
const
{
data
}
=
await
getArticleDetail
(
questionId
)
const
{
data
}
=
await
getArticleDetail
(
questionId
)
questionDetail
.
value
=
{
questionDetail
.
value
=
{
...
@@ -688,6 +437,13 @@ const openCommentDialog = () => {
...
@@ -688,6 +437,13 @@ const openCommentDialog = () => {
commentDialogRef
.
value
?.
open
(
questionDetail
.
value
.
id
)
commentDialogRef
.
value
?.
open
(
questionDetail
.
value
.
id
)
}
}
const
openSendMessage
=
()
=>
{
sendMessageDialogRef
.
value
?.
open
({
receiverId
:
String
(
questionDetail
.
value
.
createUserId
),
isReal
:
BooleanFlag
.
YES
,
}
)
}
// ==================== 评论定位(消息通知带 commentId 跳进来) ====================
// ==================== 评论定位(消息通知带 commentId 跳进来) ====================
// 问吧是三层:回答 → 回答下的评论 → 评论下的回复。回答这层由页面自己定位,
// 问吧是三层:回答 → 回答下的评论 → 评论下的回复。回答这层由页面自己定位,
// 评论/回复那两层交给该回答内嵌的 Comment 组件(focusTarget),滚动只发生一次
// 评论/回复那两层交给该回答内嵌的 Comment 组件(focusTarget),滚动只发生一次
...
...
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