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
5d676393
Commit
5d676393
authored
Apr 16, 2026
by
lijiabin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【需求 21402】 feat: 完成私信相关的功能
parent
674789ad
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
166 additions
and
45 deletions
+166
-45
index.ts
src/api/selfMessage/index.ts
+10
-0
types.ts
src/api/selfMessage/types.ts
+10
-0
index.vue
src/views/selfMessage/index.vue
+117
-44
index.vue
src/views/userPage/index.vue
+29
-1
No files found.
src/api/selfMessage/index.ts
View file @
5d676393
...
@@ -5,6 +5,7 @@ import type {
...
@@ -5,6 +5,7 @@ import type {
ConversationResponseDto
,
ConversationResponseDto
,
GetMessageDetailListDto
,
GetMessageDetailListDto
,
MessageDetailListItem
,
MessageDetailListItem
,
DeleteMessageDto
,
}
from
'./types'
}
from
'./types'
// 关于私信的相关接口
// 关于私信的相关接口
...
@@ -37,3 +38,12 @@ export const getMessageDetailList = (data: GetMessageDetailListDto) => {
...
@@ -37,3 +38,12 @@ export const getMessageDetailList = (data: GetMessageDetailListDto) => {
data
,
data
,
})
})
}
}
// 删除某个私信
export
const
deleteMessage
=
(
data
:
DeleteMessageDto
)
=>
{
return
service
.
request
({
url
:
'/api/cultureDialog/deleteMessage'
,
method
:
'POST'
,
data
,
})
}
src/api/selfMessage/types.ts
View file @
5d676393
...
@@ -78,3 +78,13 @@ export interface MessageDetailListItem {
...
@@ -78,3 +78,13 @@ export interface MessageDetailListItem {
is_self
:
boolean
is_self
:
boolean
image_list
:
string
[]
image_list
:
string
[]
}
}
/** 删除私信:会话或单条消息 */
export
type
DeleteCultureMessageKind
=
'user'
|
'message'
export
interface
DeleteMessageDto
{
/** user:按会话删除;message:按消息删除 */
type
:
DeleteCultureMessageKind
/** 会话 id 列表或消息 id 列表 */
idList
:
number
[]
}
src/views/selfMessage/index.vue
View file @
5d676393
...
@@ -4,13 +4,17 @@ import { useUserStore } from '@/stores'
...
@@ -4,13 +4,17 @@ import { useUserStore } from '@/stores'
import
{
storeToRefs
}
from
'pinia'
import
{
storeToRefs
}
from
'pinia'
import
BackButton
from
'@/components/common/BackButton/index.vue'
import
BackButton
from
'@/components/common/BackButton/index.vue'
import
type
{
ScrollbarInstance
}
from
'element-plus'
import
type
{
ScrollbarInstance
}
from
'element-plus'
import
{
getMessageList
,
sendMessage
,
getMessageDetailList
}
from
'@/api'
import
{
getMessageList
,
sendMessage
,
getMessageDetailList
,
deleteMessage
}
from
'@/api'
import
type
{
ConversationItem
,
MessageDetailListItem
}
from
'@/api/selfMessage/types'
import
type
{
ConversationItem
,
MessageDetailListItem
}
from
'@/api/selfMessage/types'
import
{
parseEmoji
}
from
'@/utils/emoji'
import
{
parseEmoji
}
from
'@/utils/emoji'
import
{
push
}
from
'notivue'
import
{
BooleanFlag
}
from
'@/constants'
import
{
BooleanFlag
}
from
'@/constants'
import
{
useMessageBox
}
from
'@/hooks'
const
userStore
=
useUserStore
()
const
userStore
=
useUserStore
()
const
{
userInfo
}
=
storeToRefs
(
userStore
)
const
{
userInfo
}
=
storeToRefs
(
userStore
)
const
route
=
useRoute
()
const
route
=
useRoute
()
const
{
confirm
}
=
useMessageBox
()
const
categoryTabs
=
[
const
categoryTabs
=
[
{
{
...
@@ -71,10 +75,9 @@ const filteredConversationList = computed(() => {
...
@@ -71,10 +75,9 @@ const filteredConversationList = computed(() => {
})
})
const
activeConversation
=
computed
(()
=>
{
const
activeConversation
=
computed
(()
=>
{
const
current
=
filteredConversationList
.
value
.
find
(
return
(
(
item
)
=>
item
.
id
===
activeConversationId
.
value
,
filteredConversationList
.
value
.
find
((
item
)
=>
item
.
id
===
activeConversationId
.
value
)
??
null
)
)
return
current
||
filteredConversationList
.
value
[
0
]
||
null
})
})
const
conversationStats
=
computed
(()
=>
({
const
conversationStats
=
computed
(()
=>
({
...
@@ -98,47 +101,45 @@ watch(filteredConversationList, (list) => {
...
@@ -98,47 +101,45 @@ watch(filteredConversationList, (list) => {
return
return
}
}
if
(
!
list
.
some
((
item
)
=>
item
.
id
===
activeConversationId
.
value
)
&&
list
[
0
])
{
// 仅当当前选中 id 已不在列表中时才清空,避免进入页面时自动选中第一项
activeConversationId
.
value
=
list
[
0
].
id
if
(
activeConversationId
.
value
!==
0
&&
!
list
.
some
((
item
)
=>
item
.
id
===
activeConversationId
.
value
)
)
{
activeConversationId
.
value
=
0
messageDetailList
.
value
=
[]
}
}
})
})
watch
(
()
=>
activeConversation
.
value
?.
id
,
async
(
id
)
=>
{
if
(
!
id
)
return
const
current
=
conversationList
.
value
.
find
((
item
)
=>
item
.
id
===
id
)
if
(
current
)
current
.
un_read_count
=
0
if
(
!
messageDetailList
.
value
.
length
&&
current
)
{
await
selectConversation
(
current
)
}
await
nextTick
()
scrollToBottom
()
},
)
const
messageDetailList
=
ref
<
MessageDetailListItem
[]
>
([])
const
messageDetailList
=
ref
<
MessageDetailListItem
[]
>
([])
const
selectConversation
=
async
(
item
:
ConversationItem
)
=>
{
/** 拉取会话详情并滚动到底;不要在 watch(activeConversation) 里再调,否则会与点击逻辑并发请求两次 */
const
selectConversation
=
async
(
item
:
ConversationItem
,
shouldScrollToBottom
=
true
)
=>
{
activeConversationId
.
value
=
item
.
id
activeConversationId
.
value
=
item
.
id
const
{
data
}
=
await
getMessageDetailList
({
const
{
data
}
=
await
getMessageDetailList
({
receiverId
:
item
.
other_user_id
,
receiverId
:
item
.
other_user_id
,
dialogId
:
item
.
id
,
dialogId
:
item
.
id
,
chatType
:
activeCategory
.
value
,
chatType
:
activeCategory
.
value
,
})
})
messageDetailList
.
value
=
(
data
||
[]).
map
((
message
)
=>
{
messageDetailList
.
value
=
data
||
[]
const
imageList
=
Array
.
isArray
(
message
.
image_list
)
if
(
shouldScrollToBottom
)
{
?
message
.
image_list
await
nextTick
()
:
message
.
images
scrollToBottom
()
?
message
.
images
.
split
(
','
).
filter
(
Boolean
)
:
[]
return
{
...
message
,
image_list
:
imageList
,
}
}
})
}
}
watch
(
()
=>
activeConversation
.
value
?.
id
,
(
id
)
=>
{
if
(
!
id
)
return
const
current
=
conversationList
.
value
.
find
((
item
)
=>
item
.
id
===
id
)
if
(
current
)
{
current
.
un_read_count
=
0
current
.
had_read
=
BooleanFlag
.
YES
}
},
)
const
switchCategory
=
(
category
:
BooleanFlag
)
=>
{
const
switchCategory
=
(
category
:
BooleanFlag
)
=>
{
if
(
activeCategory
.
value
===
category
)
return
if
(
activeCategory
.
value
===
category
)
return
activeCategory
.
value
=
category
activeCategory
.
value
=
category
...
@@ -179,8 +180,6 @@ const handleSend = async () => {
...
@@ -179,8 +180,6 @@ const handleSend = async () => {
draftText
.
value
=
''
draftText
.
value
=
''
draftImages
.
value
=
''
draftImages
.
value
=
''
await
selectConversation
(
activeConversation
.
value
)
await
selectConversation
(
activeConversation
.
value
)
await
nextTick
()
scrollToBottom
()
}
finally
{
}
finally
{
sending
.
value
=
false
sending
.
value
=
false
}
}
...
@@ -193,6 +192,43 @@ const handleInputKeydown = (event: KeyboardEvent) => {
...
@@ -193,6 +192,43 @@ const handleInputKeydown = (event: KeyboardEvent) => {
}
}
}
}
const
onConversationMenu
=
async
(
cmd
:
string
|
number
,
item
:
ConversationItem
)
=>
{
if
(
cmd
!==
'delete'
)
return
await
confirm
({
title
:
'删除会话'
,
message
:
'确定删除该会话?删除后聊天记录将不可恢复。'
,
confirmText
:
'删除'
,
cancelText
:
'取消'
,
type
:
'warning'
,
})
try
{
await
deleteMessage
({
type
:
'user'
,
idList
:
[
item
.
id
]
})
conversationList
.
value
=
conversationList
.
value
.
filter
((
c
)
=>
c
.
id
!==
item
.
id
)
// 选中会话被删时,由 watch(filteredConversationList) 把 activeConversationId 置 0 并清空 messageDetailList
push
.
success
(
'已删除会话'
)
}
catch
{
/* 失败时请求封装已 toast */
}
}
const
onMessageMenu
=
async
(
cmd
:
string
|
number
,
message
:
MessageDetailListItem
)
=>
{
if
(
cmd
!==
'delete'
)
return
if
(
!
message
.
is_self
)
return
await
confirm
({
title
:
'删除消息'
,
message
:
'确定删除这条消息?'
,
confirmText
:
'删除'
,
cancelText
:
'取消'
,
type
:
'warning'
,
})
await
deleteMessage
({
type
:
'message'
,
idList
:
[
message
.
message_id
]
})
// await refreshConversationList()
if
(
activeConversation
.
value
)
await
selectConversation
(
activeConversation
.
value
,
false
)
push
.
success
(
'已删除消息'
)
}
onActivated
(
async
()
=>
{
onActivated
(
async
()
=>
{
applyCategoryFromRoute
()
applyCategoryFromRoute
()
await
refreshConversationList
()
await
refreshConversationList
()
...
@@ -257,9 +293,15 @@ onActivated(async () => {
...
@@ -257,9 +293,15 @@ onActivated(async () => {
<div
class=
"min-h-0 flex-1"
>
<div
class=
"min-h-0 flex-1"
>
<el-scrollbar>
<el-scrollbar>
<div
v-if=
"filteredConversationList.length"
class=
"space-y-3"
>
<div
v-if=
"filteredConversationList.length"
class=
"space-y-3"
>
<
butto
n
<
el-dropdow
n
v-for=
"item in filteredConversationList"
v-for=
"item in filteredConversationList"
:key=
"item.id"
:key=
"item.id"
class=
"block w-full"
trigger=
"contextmenu"
@
command=
"(cmd) => onConversationMenu(cmd, item)"
>
<button
type=
"button"
class=
"cursor-pointer w-full rounded-lg border p-3 text-left transition-all"
class=
"cursor-pointer w-full rounded-lg border p-3 text-left transition-all"
:class=
"
:class=
"
activeConversation?.id === item.id
activeConversation?.id === item.id
...
@@ -271,6 +313,11 @@ onActivated(async () => {
...
@@ -271,6 +313,11 @@ onActivated(async () => {
<div
class=
"flex gap-3"
>
<div
class=
"flex gap-3"
>
<div
class=
"relative shrink-0"
>
<div
class=
"relative shrink-0"
>
<el-avatar
:size=
"46"
:src=
"item.other_user_avatar"
/>
<el-avatar
:size=
"46"
:src=
"item.other_user_avatar"
/>
<span
v-if=
"Number(item.had_read) === BooleanFlag.NO"
class=
"pointer-events-none absolute right-0 top-0 h-2.5 w-2.5 rounded-full border-2 border-white bg-[#f56c6c] shadow-[0_0_0_1px_rgba(245,108,108,0.35)]"
title=
"未读"
/>
</div>
</div>
<div
class=
"min-w-0 flex-1"
>
<div
class=
"min-w-0 flex-1"
>
...
@@ -280,7 +327,9 @@ onActivated(async () => {
...
@@ -280,7 +327,9 @@ onActivated(async () => {
{{ item.other_user_name }}
{{ item.other_user_name }}
</span>
</span>
</div>
</div>
<span
class=
"shrink-0 text-3 text-[#8ca0b8]"
>
{{ item.last_time_str }}
</span>
<span
class=
"shrink-0 text-3 text-[#8ca0b8]"
>
{{
item.last_time_str
}}
</span>
</div>
</div>
<div
class=
"mt-2 flex min-w-0 items-center gap-2"
>
<div
class=
"mt-2 flex min-w-0 items-center gap-2"
>
...
@@ -303,6 +352,12 @@ onActivated(async () => {
...
@@ -303,6 +352,12 @@ onActivated(async () => {
</div>
</div>
</div>
</div>
</button>
</button>
<
template
#
dropdown
>
<el-dropdown-menu>
<el-dropdown-item
command=
"delete"
>
删除会话
</el-dropdown-item>
</el-dropdown-menu>
</
template
>
</el-dropdown>
</div>
</div>
<el-empty
v-else
description=
"没有找到匹配的会话"
:image-size=
"90"
/>
<el-empty
v-else
description=
"没有找到匹配的会话"
:image-size=
"90"
/>
...
@@ -356,13 +411,10 @@ onActivated(async () => {
...
@@ -356,13 +411,10 @@ onActivated(async () => {
<el-empty
description=
"没有找到匹配的会话"
:image-size=
"90"
/>
<el-empty
description=
"没有找到匹配的会话"
:image-size=
"90"
/>
</div>
</div>
<el-scrollbar
v-show=
"messageDetailList.length"
ref=
"conversationBoxRef"
>
<el-scrollbar
v-show=
"messageDetailList.length"
ref=
"conversationBoxRef"
>
<div
<div
v-show=
"messageDetailList.length"
class=
"px-5 py-5"
>
v-show=
"messageDetailList.length"
class=
"bg-[linear-gradient(180deg,#fcfdff_0%,#f8fbff_100%)] px-5 py-5"
>
<div
v-for=
"message in messageDetailList"
:key=
"message.message_id"
class=
"pb-3"
>
<div
v-for=
"message in messageDetailList"
:key=
"message.message_id"
class=
"pb-3"
>
<div
<div
class=
"mb-4 flex items-start gap-3"
class=
"mb-4 flex
w-full min-w-0 cursor-default
items-start gap-3"
:class=
"
{ 'flex-row-reverse': message.is_self }"
:class=
"
{ 'flex-row-reverse': message.is_self }"
>
>
<el-avatar
:size=
"38"
:src=
"message.sender_avatar"
class=
"shrink-0"
/>
<el-avatar
:size=
"38"
:src=
"message.sender_avatar"
class=
"shrink-0"
/>
...
@@ -379,21 +431,42 @@ onActivated(async () => {
...
@@ -379,21 +431,42 @@ onActivated(async () => {
</div>
</div>
<div
<div
class=
"
rounded-lg border
px-3 py-2 shadow-[0_0.125rem_0.5rem_rgba(39,74,120,0.04)]"
class=
"
w-fit max-w-full rounded-lg
px-3 py-2 shadow-[0_0.125rem_0.5rem_rgba(39,74,120,0.04)]"
:class=
"
:class=
"
message.is_self
message.is_self
? 'rounded-tr-[0.375rem] border-[#d4e4ff] bg-[#dbeaff] text-[#1a3f73]'
? 'rounded-tr-[0.375rem] border
border
-[#d4e4ff] bg-[#dbeaff] text-[#1a3f73]'
: 'rounded-tl-[0.375rem] border
-[#e7edf5] bg-white text-[#23364c
]'
: 'rounded-tl-[0.375rem] border
border-[#d9e3ef] bg-[#f3f7fc] text-[#1f2f46
]'
"
"
>
>
<el-dropdown
v-if=
"message.is_self"
class=
"inline-block max-w-full"
trigger=
"contextmenu"
@
command=
"(cmd) => onMessageMenu(cmd, message)"
>
<p
<p
v-if=
"message.content"
v-if=
"message.content"
v-html=
"parseEmoji(message.content)"
v-html=
"parseEmoji(message.content)"
class=
"cursor-pointer m-0 whitespace-pre-wrap break-words text-3.5 leading-[1.625rem]"
></p>
<template
#
dropdown
>
<el-dropdown-menu>
<el-dropdown-item
command=
"delete"
>
删除消息
</el-dropdown-item>
</el-dropdown-menu>
</
template
>
</el-dropdown>
<p
v-else-if=
"message.content"
v-html=
"parseEmoji(message.content)"
class=
"m-0 whitespace-pre-wrap break-words text-3.5 leading-[1.625rem]"
class=
"m-0 whitespace-pre-wrap break-words text-3.5 leading-[1.625rem]"
></p>
></p>
<div
<div
v-if=
"message.image_list?.length"
v-if=
"message.image_list?.length"
class=
"mt-2 grid gap-2 sm:grid-cols-2"
class=
"mt-2 flex flex-wrap gap-2"
:class=
"'justify-start'"
:style=
"{
width: message.image_list.length > 1 ? '15.5rem' : '7.5rem',
}"
>
>
<el-image
<el-image
v-for=
"image in message.image_list"
v-for=
"image in message.image_list"
...
...
src/views/userPage/index.vue
View file @
5d676393
...
@@ -112,6 +112,12 @@
...
@@ -112,6 +112,12 @@
<component
:is=
"item.icon"
/>
<component
:is=
"item.icon"
/>
</el-icon>
</el-icon>
<span
class=
"text-sm"
>
{{
item
.
label
}}
</span>
<span
class=
"text-sm"
>
{{
item
.
label
}}
</span>
<el-badge
v-if=
"item.tab === '我的私信'"
:value=
"privateMessageUnreadDisplay > 99 ? '99+' : privateMessageUnreadDisplay"
:hidden=
"privateMessageUnreadDisplay
<
=
0
"
class=
"ml-auto"
/>
</div>
</div>
</div>
</div>
<!-- 左侧菜单 —— 官方账号菜单 审核操作等 -->
<!-- 左侧菜单 —— 官方账号菜单 审核操作等 -->
...
@@ -164,7 +170,7 @@
...
@@ -164,7 +170,7 @@
<
script
lang=
"tsx"
setup
>
<
script
lang=
"tsx"
setup
>
import
{
storeToRefs
}
from
'pinia'
import
{
storeToRefs
}
from
'pinia'
import
EditUserInfo
from
'./components/editUserInfo.vue'
import
EditUserInfo
from
'./components/editUserInfo.vue'
import
{
generateLoginKey
,
hasOfficialAccount
}
from
'@/api'
import
{
generateLoginKey
,
hasOfficialAccount
,
getMessageList
}
from
'@/api'
import
type
{
OfficialAccountItemDto
}
from
'@/api/user/types'
import
type
{
OfficialAccountItemDto
}
from
'@/api/user/types'
import
{
wxLogin
}
from
'@/utils/wxUtil'
import
{
wxLogin
}
from
'@/utils/wxUtil'
import
type
{
RouteLocationNormalizedLoadedGeneric
}
from
'vue-router'
import
type
{
RouteLocationNormalizedLoadedGeneric
}
from
'vue-router'
...
@@ -198,6 +204,8 @@ const componentRef = useTemplateRef<{
...
@@ -198,6 +204,8 @@ const componentRef = useTemplateRef<{
const
editUserInfoRef
=
useTemplateRef
<
InstanceType
<
typeof
EditUserInfo
>>
(
'editUserInfoRef'
)
const
editUserInfoRef
=
useTemplateRef
<
InstanceType
<
typeof
EditUserInfo
>>
(
'editUserInfoRef'
)
const
userStore
=
useUserStore
()
const
userStore
=
useUserStore
()
const
{
userInfo
}
=
storeToRefs
(
userStore
)
const
{
userInfo
}
=
storeToRefs
(
userStore
)
const
anonymousUnreadCount
=
ref
(
0
)
const
realUnreadCount
=
ref
(
0
)
// 左侧普通用户菜单
// 左侧普通用户菜单
const
menuUserItems
=
[
const
menuUserItems
=
[
...
@@ -281,6 +289,11 @@ const menuOfficialItems = [
...
@@ -281,6 +289,11 @@ const menuOfficialItems = [
const
isReal
=
ref
<
BooleanFlag
>
(
BooleanFlag
.
NO
)
const
isReal
=
ref
<
BooleanFlag
>
(
BooleanFlag
.
NO
)
/** 与私信页一致:实名 → realUnreadCount,匿名 → anonymousUnreadCount */
const
privateMessageUnreadDisplay
=
computed
(()
=>
isReal
.
value
===
BooleanFlag
.
YES
?
realUnreadCount
.
value
:
anonymousUnreadCount
.
value
,
)
provide
(
IS_REAL_KEY
,
isReal
)
provide
(
IS_REAL_KEY
,
isReal
)
watch
(
isReal
,
()
=>
{
watch
(
isReal
,
()
=>
{
...
@@ -321,6 +334,17 @@ const getIsOfficial = async () => {
...
@@ -321,6 +334,17 @@ const getIsOfficial = async () => {
officialAccountList
.
value
=
data
officialAccountList
.
value
=
data
}
}
const
getSelfMessageUnreadCount
=
async
()
=>
{
try
{
const
{
data
}
=
await
getMessageList
({})
anonymousUnreadCount
.
value
=
data
?.
anonymousUnreadCount
??
0
realUnreadCount
.
value
=
data
?.
realUnreadCount
??
0
}
catch
{
anonymousUnreadCount
.
value
=
0
realUnreadCount
.
value
=
0
}
}
const
handleSwitchAccount
=
async
()
=>
{
const
handleSwitchAccount
=
async
()
=>
{
const
selectedEmail
=
ref
(
''
)
const
selectedEmail
=
ref
(
''
)
ElMessageBox
({
ElMessageBox
({
...
@@ -429,6 +453,10 @@ const handleClearCache = async () => {
...
@@ -429,6 +453,10 @@ const handleClearCache = async () => {
onMounted
(()
=>
{
onMounted
(()
=>
{
getIsOfficial
()
getIsOfficial
()
})
})
onActivated
(()
=>
{
getSelfMessageUnreadCount
()
})
</
script
>
</
script
>
<
style
scoped
>
<
style
scoped
>
...
...
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