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
5d016c03
Commit
5d016c03
authored
Apr 09, 2026
by
lijiabin
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【需求 21402】 wip: 暂时加入私信页面
parent
d15d4d58
Show whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
1135 additions
and
16 deletions
+1135
-16
route.ts
src/router/route.ts
+6
-0
index.vue
src/views/selfMessage/index.vue
+1107
-0
index.vue
src/views/userPage/index.vue
+22
-16
No files found.
src/router/route.ts
View file @
5d016c03
...
...
@@ -189,6 +189,12 @@ export const constantsRoute = [
name
:
'Auction'
,
component
:
()
=>
import
(
'@/views/auction/index.vue'
),
},
// 我的私信
{
path
:
'selfMessage'
,
name
:
'CultureSelfMessage'
,
component
:
()
=>
import
(
'@/views/selfMessage/index.vue'
),
},
],
},
...
...
src/views/selfMessage/index.vue
0 → 100644
View file @
5d016c03
<
script
setup
lang=
"ts"
>
import
CommentBox
from
'@/components/common/CommentBox/index.vue'
import
{
useUserStore
}
from
'@/stores'
import
dayjs
from
'dayjs'
import
{
push
}
from
'notivue'
import
{
storeToRefs
}
from
'pinia'
import
BackButton
from
'@/components/common/BackButton/index.vue'
import
type
{
ScrollbarInstance
}
from
'element-plus'
type
MessageCategory
=
'anonymous'
|
'realname'
type
MessageSender
=
'self'
|
'other'
interface
ChatMessage
{
id
:
number
sender
:
MessageSender
text
:
string
images
:
string
[]
createdAt
:
string
}
interface
ConversationItem
{
id
:
number
category
:
MessageCategory
name
:
string
avatar
:
string
badge
:
string
subtitle
:
string
sourceTitle
:
string
sourceTypeLabel
:
string
onlineStatus
:
string
unread
:
number
isPinned
?:
boolean
messages
:
ChatMessage
[]
}
const
userStore
=
useUserStore
()
const
{
userInfo
}
=
storeToRefs
(
userStore
)
const
categoryTabs
=
[
{
key
:
'anonymous'
as
MessageCategory
,
label
:
'匿名私信'
,
description
:
'适用于除问吧、实践、专访文章以外的内容'
,
},
{
key
:
'realname'
as
MessageCategory
,
label
:
'实名私信'
,
description
:
'适用于问吧、实践、专访文章'
,
},
]
const
conversationList
=
ref
<
ConversationItem
[]
>
([
{
id
:
1
,
category
:
'realname'
,
name
:
'陈菲'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei'
,
badge
:
'实名'
,
subtitle
:
'你上次提到的培训案例已经补充好了'
,
sourceTitle
:
'实践 · 从复盘到落地的团队协作方法'
,
sourceTypeLabel
:
'实践'
,
onlineStatus
:
'29分钟前活跃'
,
unread
:
2
,
isPinned
:
true
,
messages
:
[
{
id
:
101
,
sender
:
'other'
,
text
:
'你好,这篇实践文章里提到的复盘模板能发我看一下吗?'
,
images
:
[],
createdAt
:
'2026-04-09 09:18'
,
},
{
id
:
102
,
sender
:
'self'
,
text
:
'可以,我整理完私信给你。'
,
images
:
[],
createdAt
:
'2026-04-09 09:22'
,
},
{
id
:
103
,
sender
:
'other'
,
text
:
'好的,我也很想看看你们怎么把共识拆成执行动作。'
,
images
:
[],
createdAt
:
'2026-04-09 09:24'
,
},
],
},
{
id
:
2
,
category
:
'anonymous'
,
name
:
'树洞来信'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA'
,
badge
:
'匿名'
,
subtitle
:
'谢谢你愿意认真回复我的困惑'
,
sourceTitle
:
'案例库 · 一次被看见的小改变'
,
sourceTypeLabel
:
'案例'
,
onlineStatus
:
'刚刚'
,
unread
:
0
,
messages
:
[
{
id
:
201
,
sender
:
'other'
,
text
:
'看到你的评论后想私信你,原来很多人也会经历类似阶段。'
,
images
:
[],
createdAt
:
'2026-04-09 08:31'
,
},
{
id
:
202
,
sender
:
'self'
,
text
:
'会的,所以别急着否定自己,先把能做的小事做起来。'
,
images
:
[],
createdAt
:
'2026-04-09 08:36'
,
},
],
},
{
id
:
3
,
category
:
'realname'
,
name
:
'王守勇'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong'
,
badge
:
'实名'
,
subtitle
:
'关于专访提到的组织演进,我补了两张图'
,
sourceTitle
:
'专访 · 从宏大叙事到实事求是'
,
sourceTypeLabel
:
'专访'
,
onlineStatus
:
'今天 08:45'
,
unread
:
1
,
messages
:
[
{
id
:
301
,
sender
:
'other'
,
text
:
'这两张图是给你补充的版本,可以一起带给同事看。'
,
images
:
[
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80'
,
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80'
,
],
createdAt
:
'2026-04-09 08:45'
,
},
],
},
{
id
:
4
,
category
:
'anonymous'
,
name
:
'未署名同事'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB'
,
badge
:
'匿名'
,
subtitle
:
'想继续请教你怎么带新人'
,
sourceTitle
:
'视频 · 一次普通晨会为什么能开得更好'
,
sourceTypeLabel
:
'视频'
,
onlineStatus
:
'昨天'
,
unread
:
0
,
messages
:
[
{
id
:
401
,
sender
:
'other'
,
text
:
'我发现自己总在说结论,带新人时对方不太能跟上。'
,
images
:
[],
createdAt
:
'2026-04-08 17:20'
,
},
{
id
:
402
,
sender
:
'self'
,
text
:
'可以先从为什么这么做开始讲,再拆成两个最关键动作。'
,
images
:
[],
createdAt
:
'2026-04-08 17:35'
,
},
],
},
{
id
:
11
,
category
:
'realname'
,
name
:
'陈菲'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei'
,
badge
:
'实名'
,
subtitle
:
'你上次提到的培训案例已经补充好了'
,
sourceTitle
:
'实践 · 从复盘到落地的团队协作方法'
,
sourceTypeLabel
:
'实践'
,
onlineStatus
:
'29分钟前活跃'
,
unread
:
2
,
isPinned
:
true
,
messages
:
[
{
id
:
101
,
sender
:
'other'
,
text
:
'你好,这篇实践文章里提到的复盘模板能发我看一下吗?'
,
images
:
[],
createdAt
:
'2026-04-09 09:18'
,
},
{
id
:
102
,
sender
:
'self'
,
text
:
'可以,我整理完私信给你。'
,
images
:
[],
createdAt
:
'2026-04-09 09:22'
,
},
{
id
:
103
,
sender
:
'other'
,
text
:
'好的,我也很想看看你们怎么把共识拆成执行动作。'
,
images
:
[],
createdAt
:
'2026-04-09 09:24'
,
},
],
},
{
id
:
21
,
category
:
'anonymous'
,
name
:
'树洞来信'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA'
,
badge
:
'匿名'
,
subtitle
:
'谢谢你愿意认真回复我的困惑'
,
sourceTitle
:
'案例库 · 一次被看见的小改变'
,
sourceTypeLabel
:
'案例'
,
onlineStatus
:
'刚刚'
,
unread
:
0
,
messages
:
[
{
id
:
201
,
sender
:
'other'
,
text
:
'看到你的评论后想私信你,原来很多人也会经历类似阶段。'
,
images
:
[],
createdAt
:
'2026-04-09 08:31'
,
},
{
id
:
202
,
sender
:
'self'
,
text
:
'会的,所以别急着否定自己,先把能做的小事做起来。'
,
images
:
[],
createdAt
:
'2026-04-09 08:36'
,
},
],
},
{
id
:
31
,
category
:
'realname'
,
name
:
'王守勇'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong'
,
badge
:
'实名'
,
subtitle
:
'关于专访提到的组织演进,我补了两张图'
,
sourceTitle
:
'专访 · 从宏大叙事到实事求是'
,
sourceTypeLabel
:
'专访'
,
onlineStatus
:
'今天 08:45'
,
unread
:
1
,
messages
:
[
{
id
:
301
,
sender
:
'other'
,
text
:
'这两张图是给你补充的版本,可以一起带给同事看。'
,
images
:
[
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80'
,
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80'
,
],
createdAt
:
'2026-04-09 08:45'
,
},
],
},
{
id
:
41
,
category
:
'anonymous'
,
name
:
'未署名同事'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB'
,
badge
:
'匿名'
,
subtitle
:
'想继续请教你怎么带新人'
,
sourceTitle
:
'视频 · 一次普通晨会为什么能开得更好'
,
sourceTypeLabel
:
'视频'
,
onlineStatus
:
'昨天'
,
unread
:
0
,
messages
:
[
{
id
:
401
,
sender
:
'other'
,
text
:
'我发现自己总在说结论,带新人时对方不太能跟上。'
,
images
:
[],
createdAt
:
'2026-04-08 17:20'
,
},
{
id
:
402
,
sender
:
'self'
,
text
:
'可以先从为什么这么做开始讲,再拆成两个最关键动作。'
,
images
:
[],
createdAt
:
'2026-04-08 17:35'
,
},
],
},
{
id
:
12
,
category
:
'realname'
,
name
:
'陈菲'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei'
,
badge
:
'实名'
,
subtitle
:
'你上次提到的培训案例已经补充好了'
,
sourceTitle
:
'实践 · 从复盘到落地的团队协作方法'
,
sourceTypeLabel
:
'实践'
,
onlineStatus
:
'29分钟前活跃'
,
unread
:
2
,
isPinned
:
true
,
messages
:
[
{
id
:
101
,
sender
:
'other'
,
text
:
'你好,这篇实践文章里提到的复盘模板能发我看一下吗?'
,
images
:
[],
createdAt
:
'2026-04-09 09:18'
,
},
{
id
:
102
,
sender
:
'self'
,
text
:
'可以,我整理完私信给你。'
,
images
:
[],
createdAt
:
'2026-04-09 09:22'
,
},
{
id
:
103
,
sender
:
'other'
,
text
:
'好的,我也很想看看你们怎么把共识拆成执行动作。'
,
images
:
[],
createdAt
:
'2026-04-09 09:24'
,
},
],
},
{
id
:
22
,
category
:
'anonymous'
,
name
:
'树洞来信'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA'
,
badge
:
'匿名'
,
subtitle
:
'谢谢你愿意认真回复我的困惑'
,
sourceTitle
:
'案例库 · 一次被看见的小改变'
,
sourceTypeLabel
:
'案例'
,
onlineStatus
:
'刚刚'
,
unread
:
0
,
messages
:
[
{
id
:
201
,
sender
:
'other'
,
text
:
'看到你的评论后想私信你,原来很多人也会经历类似阶段。'
,
images
:
[],
createdAt
:
'2026-04-09 08:31'
,
},
{
id
:
202
,
sender
:
'self'
,
text
:
'会的,所以别急着否定自己,先把能做的小事做起来。'
,
images
:
[],
createdAt
:
'2026-04-09 08:36'
,
},
],
},
{
id
:
32
,
category
:
'realname'
,
name
:
'王守勇'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong'
,
badge
:
'实名'
,
subtitle
:
'关于专访提到的组织演进,我补了两张图'
,
sourceTitle
:
'专访 · 从宏大叙事到实事求是'
,
sourceTypeLabel
:
'专访'
,
onlineStatus
:
'今天 08:45'
,
unread
:
1
,
messages
:
[
{
id
:
301
,
sender
:
'other'
,
text
:
'这两张图是给你补充的版本,可以一起带给同事看。'
,
images
:
[
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80'
,
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80'
,
],
createdAt
:
'2026-04-09 08:45'
,
},
],
},
{
id
:
4
,
category
:
'anonymous'
,
name
:
'未署名同事'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB'
,
badge
:
'匿名'
,
subtitle
:
'想继续请教你怎么带新人'
,
sourceTitle
:
'视频 · 一次普通晨会为什么能开得更好'
,
sourceTypeLabel
:
'视频'
,
onlineStatus
:
'昨天'
,
unread
:
0
,
messages
:
[
{
id
:
401
,
sender
:
'other'
,
text
:
'我发现自己总在说结论,带新人时对方不太能跟上。'
,
images
:
[],
createdAt
:
'2026-04-08 17:20'
,
},
{
id
:
402
,
sender
:
'self'
,
text
:
'可以先从为什么这么做开始讲,再拆成两个最关键动作。'
,
images
:
[],
createdAt
:
'2026-04-08 17:35'
,
},
],
},
{
id
:
13
,
category
:
'realname'
,
name
:
'陈菲'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei'
,
badge
:
'实名'
,
subtitle
:
'你上次提到的培训案例已经补充好了'
,
sourceTitle
:
'实践 · 从复盘到落地的团队协作方法'
,
sourceTypeLabel
:
'实践'
,
onlineStatus
:
'29分钟前活跃'
,
unread
:
2
,
isPinned
:
true
,
messages
:
[
{
id
:
101
,
sender
:
'other'
,
text
:
'你好,这篇实践文章里提到的复盘模板能发我看一下吗?'
,
images
:
[],
createdAt
:
'2026-04-09 09:18'
,
},
{
id
:
102
,
sender
:
'self'
,
text
:
'可以,我整理完私信给你。'
,
images
:
[],
createdAt
:
'2026-04-09 09:22'
,
},
{
id
:
103
,
sender
:
'other'
,
text
:
'好的,我也很想看看你们怎么把共识拆成执行动作。'
,
images
:
[],
createdAt
:
'2026-04-09 09:24'
,
},
],
},
{
id
:
2
,
category
:
'anonymous'
,
name
:
'树洞来信'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA'
,
badge
:
'匿名'
,
subtitle
:
'谢谢你愿意认真回复我的困惑'
,
sourceTitle
:
'案例库 · 一次被看见的小改变'
,
sourceTypeLabel
:
'案例'
,
onlineStatus
:
'刚刚'
,
unread
:
0
,
messages
:
[
{
id
:
201
,
sender
:
'other'
,
text
:
'看到你的评论后想私信你,原来很多人也会经历类似阶段。'
,
images
:
[],
createdAt
:
'2026-04-09 08:31'
,
},
{
id
:
202
,
sender
:
'self'
,
text
:
'会的,所以别急着否定自己,先把能做的小事做起来。'
,
images
:
[],
createdAt
:
'2026-04-09 08:36'
,
},
],
},
{
id
:
3
,
category
:
'realname'
,
name
:
'王守勇'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong'
,
badge
:
'实名'
,
subtitle
:
'关于专访提到的组织演进,我补了两张图'
,
sourceTitle
:
'专访 · 从宏大叙事到实事求是'
,
sourceTypeLabel
:
'专访'
,
onlineStatus
:
'今天 08:45'
,
unread
:
1
,
messages
:
[
{
id
:
301
,
sender
:
'other'
,
text
:
'这两张图是给你补充的版本,可以一起带给同事看。'
,
images
:
[
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80'
,
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80'
,
],
createdAt
:
'2026-04-09 08:45'
,
},
],
},
{
id
:
4
,
category
:
'anonymous'
,
name
:
'未署名同事'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB'
,
badge
:
'匿名'
,
subtitle
:
'想继续请教你怎么带新人'
,
sourceTitle
:
'视频 · 一次普通晨会为什么能开得更好'
,
sourceTypeLabel
:
'视频'
,
onlineStatus
:
'昨天'
,
unread
:
0
,
messages
:
[
{
id
:
401
,
sender
:
'other'
,
text
:
'我发现自己总在说结论,带新人时对方不太能跟上。'
,
images
:
[],
createdAt
:
'2026-04-08 17:20'
,
},
{
id
:
402
,
sender
:
'self'
,
text
:
'可以先从为什么这么做开始讲,再拆成两个最关键动作。'
,
images
:
[],
createdAt
:
'2026-04-08 17:35'
,
},
],
},
{
id
:
1
,
category
:
'realname'
,
name
:
'陈菲'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=ChengFei'
,
badge
:
'实名'
,
subtitle
:
'你上次提到的培训案例已经补充好了'
,
sourceTitle
:
'实践 · 从复盘到落地的团队协作方法'
,
sourceTypeLabel
:
'实践'
,
onlineStatus
:
'29分钟前活跃'
,
unread
:
2
,
isPinned
:
true
,
messages
:
[
{
id
:
101
,
sender
:
'other'
,
text
:
'你好,这篇实践文章里提到的复盘模板能发我看一下吗?'
,
images
:
[],
createdAt
:
'2026-04-09 09:18'
,
},
{
id
:
102
,
sender
:
'self'
,
text
:
'可以,我整理完私信给你。'
,
images
:
[],
createdAt
:
'2026-04-09 09:22'
,
},
{
id
:
103
,
sender
:
'other'
,
text
:
'好的,我也很想看看你们怎么把共识拆成执行动作。'
,
images
:
[],
createdAt
:
'2026-04-09 09:24'
,
},
],
},
{
id
:
23
,
category
:
'anonymous'
,
name
:
'树洞来信'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousA'
,
badge
:
'匿名'
,
subtitle
:
'谢谢你愿意认真回复我的困惑'
,
sourceTitle
:
'案例库 · 一次被看见的小改变'
,
sourceTypeLabel
:
'案例'
,
onlineStatus
:
'刚刚'
,
unread
:
0
,
messages
:
[
{
id
:
201
,
sender
:
'other'
,
text
:
'看到你的评论后想私信你,原来很多人也会经历类似阶段。'
,
images
:
[],
createdAt
:
'2026-04-09 08:31'
,
},
{
id
:
202
,
sender
:
'self'
,
text
:
'会的,所以别急着否定自己,先把能做的小事做起来。'
,
images
:
[],
createdAt
:
'2026-04-09 08:36'
,
},
],
},
{
id
:
33
,
category
:
'realname'
,
name
:
'王守勇'
,
avatar
:
'https://api.dicebear.com/7.x/adventurer/svg?seed=WangShouYong'
,
badge
:
'实名'
,
subtitle
:
'关于专访提到的组织演进,我补了两张图'
,
sourceTitle
:
'专访 · 从宏大叙事到实事求是'
,
sourceTypeLabel
:
'专访'
,
onlineStatus
:
'今天 08:45'
,
unread
:
1
,
messages
:
[
{
id
:
301
,
sender
:
'other'
,
text
:
'这两张图是给你补充的版本,可以一起带给同事看。'
,
images
:
[
'https://images.unsplash.com/photo-1516321318423-f06f85e504b3?auto=format&fit=crop&w=600&q=80'
,
'https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?auto=format&fit=crop&w=600&q=80'
,
],
createdAt
:
'2026-04-09 08:45'
,
},
],
},
{
id
:
42
,
category
:
'anonymous'
,
name
:
'未署名同事'
,
avatar
:
'https://api.dicebear.com/7.x/shapes/svg?seed=AnonymousB'
,
badge
:
'匿名'
,
subtitle
:
'想继续请教你怎么带新人'
,
sourceTitle
:
'视频 · 一次普通晨会为什么能开得更好'
,
sourceTypeLabel
:
'视频'
,
onlineStatus
:
'昨天'
,
unread
:
0
,
messages
:
[
{
id
:
401
,
sender
:
'other'
,
text
:
'我发现自己总在说结论,带新人时对方不太能跟上。'
,
images
:
[],
createdAt
:
'2026-04-08 17:20'
,
},
{
id
:
402
,
sender
:
'self'
,
text
:
'可以先从为什么这么做开始讲,再拆成两个最关键动作。'
,
images
:
[],
createdAt
:
'2026-04-08 17:35'
,
},
],
},
])
const
activeCategory
=
ref
<
MessageCategory
>
(
'anonymous'
)
const
activeConversationId
=
ref
<
number
>
(
2
)
const
keyword
=
ref
(
''
)
const
draftText
=
ref
(
''
)
const
draftImages
=
ref
(
''
)
const
sending
=
ref
(
false
)
const
messageListRef
=
useTemplateRef
<
HTMLDivElement
>
(
'messageListRef'
)
const
conversationBoxRef
=
useTemplateRef
<
ScrollbarInstance
>
(
'conversationBoxRef'
)
const
filteredConversationList
=
computed
(()
=>
{
const
lowerKeyword
=
keyword
.
value
.
trim
().
toLowerCase
()
return
conversationList
.
value
.
filter
((
item
)
=>
item
.
category
===
activeCategory
.
value
)
.
filter
((
item
)
=>
{
if
(
!
lowerKeyword
)
return
true
return
[
item
.
name
,
item
.
subtitle
,
item
.
sourceTitle
].
some
((
field
)
=>
field
.
toLowerCase
().
includes
(
lowerKeyword
),
)
})
.
sort
((
a
,
b
)
=>
{
if
(
!!
a
.
isPinned
!==
!!
b
.
isPinned
)
return
a
.
isPinned
?
-
1
:
1
return
(
dayjs
(
b
.
messages
.
at
(
-
1
)?.
createdAt
).
valueOf
()
-
dayjs
(
a
.
messages
.
at
(
-
1
)?.
createdAt
).
valueOf
()
)
})
})
const
activeConversation
=
computed
(()
=>
{
const
current
=
filteredConversationList
.
value
.
find
(
(
item
)
=>
item
.
id
===
activeConversationId
.
value
,
)
return
current
||
filteredConversationList
.
value
[
0
]
||
null
})
const
groupedMessages
=
computed
(()
=>
{
if
(
!
activeConversation
.
value
)
return
[]
const
groups
:
Array
<
{
label
:
string
;
items
:
ChatMessage
[]
}
>
=
[]
activeConversation
.
value
.
messages
.
forEach
((
message
)
=>
{
const
label
=
dayjs
(
message
.
createdAt
).
format
(
'YYYY-MM-DD HH:mm'
)
const
lastGroup
=
groups
.
at
(
-
1
)
if
(
!
lastGroup
||
lastGroup
.
label
!==
label
)
{
groups
.
push
({
label
,
items
:
[
message
]
})
return
}
lastGroup
.
items
.
push
(
message
)
})
return
groups
})
const
currentUserName
=
computed
(()
=>
userInfo
.
value
?.
hiddenName
||
userInfo
.
value
?.
name
||
'我'
)
const
currentUserAvatar
=
computed
(
()
=>
userInfo
.
value
?.
hiddenAvatar
||
userInfo
.
value
?.
avatar
||
'https://api.dicebear.com/7.x/adventurer/svg?seed=Me'
,
)
const
conversationStats
=
computed
(()
=>
({
anonymous
:
conversationList
.
value
.
filter
((
item
)
=>
item
.
category
===
'anonymous'
).
length
,
realname
:
conversationList
.
value
.
filter
((
item
)
=>
item
.
category
===
'realname'
).
length
,
}))
const
canSend
=
computed
(()
=>
{
return
(
!!
activeConversation
.
value
&&
(
!!
draftText
.
value
.
trim
()
||
!!
draftImages
.
value
.
trim
())
&&
!
sending
.
value
)
})
watch
(
filteredConversationList
,
(
list
)
=>
{
if
(
!
list
.
length
)
{
activeConversationId
.
value
=
0
return
}
if
(
!
list
.
some
((
item
)
=>
item
.
id
===
activeConversationId
.
value
)
&&
list
[
0
])
{
activeConversationId
.
value
=
list
[
0
].
id
}
},
{
immediate
:
true
},
)
watch
(
()
=>
activeConversation
.
value
?.
id
,
async
(
id
)
=>
{
if
(
!
id
)
return
const
current
=
conversationList
.
value
.
find
((
item
)
=>
item
.
id
===
id
)
if
(
current
)
current
.
unread
=
0
await
nextTick
()
scrollToBottom
()
},
{
immediate
:
true
},
)
const
getConversationPreviewTime
=
(
conversation
:
ConversationItem
)
=>
{
const
latest
=
conversation
.
messages
.
at
(
-
1
)?.
createdAt
if
(
!
latest
)
return
'--'
const
now
=
dayjs
()
const
target
=
dayjs
(
latest
)
if
(
target
.
isSame
(
now
,
'day'
))
return
target
.
format
(
'HH:mm'
)
if
(
target
.
isSame
(
now
.
subtract
(
1
,
'day'
),
'day'
))
return
'昨天'
return
target
.
format
(
'MM-DD'
)
}
const
getMessageText
=
(
message
:
ChatMessage
)
=>
{
if
(
message
.
text
.
trim
())
return
message
.
text
if
(
message
.
images
.
length
)
return
'[图片]'
return
'[新消息]'
}
const
selectConversation
=
(
id
:
number
)
=>
{
activeConversationId
.
value
=
id
}
const
switchCategory
=
(
category
:
MessageCategory
)
=>
{
activeCategory
.
value
=
category
keyword
.
value
=
''
}
const
scrollToBottom
=
()
=>
{
const
wrapper
=
conversationBoxRef
.
value
console
.
log
(
wrapper
,
'wrapper'
)
if
(
!
wrapper
)
return
console
.
log
(
wrapper
.
$el
.
scrollHeight
,
'wrapper.$el.scrollHeight'
)
wrapper
.
scrollTo
({
top
:
wrapper
.
$el
.
scrollHeight
,
behavior
:
'smooth'
,
})
}
const
handleSend
=
async
()
=>
{
if
(
!
canSend
.
value
||
!
activeConversation
.
value
)
return
sending
.
value
=
true
try
{
const
newMessage
:
ChatMessage
=
{
id
:
Date
.
now
(),
sender
:
'self'
,
text
:
draftText
.
value
.
trim
(),
images
:
draftImages
.
value
.
split
(
','
)
.
map
((
item
)
=>
item
.
trim
())
.
filter
(
Boolean
),
createdAt
:
dayjs
().
format
(
'YYYY-MM-DD HH:mm'
),
}
activeConversation
.
value
.
messages
.
push
(
newMessage
)
activeConversation
.
value
.
subtitle
=
getMessageText
(
newMessage
)
draftText
.
value
=
''
draftImages
.
value
=
''
await
nextTick
()
scrollToBottom
()
push
.
success
(
'私信已发送'
)
}
finally
{
sending
.
value
=
false
}
}
const
handleInputKeydown
=
(
event
:
KeyboardEvent
)
=>
{
if
(
event
.
key
===
'Enter'
&&
!
event
.
shiftKey
)
{
event
.
preventDefault
()
handleSend
()
}
}
</
script
>
<
template
>
<div
class=
"rounded-lg relative"
>
<BackButton
/>
<section
class=
"flex h-[calc(100vh-6.25rem)] gap-4 overflow-hidden"
>
<aside
class=
"flex-1/4 rounded-lg border border-[#d8e5f2] bg-[linear-gradient(180deg,#ffffff_0%,#f8fbff_100%)] p-4 shadow-[0_0.5rem_1.5rem_rgba(134,167,207,0.12)]"
>
<div
class=
"flex h-full flex-col"
>
<div>
<div
class=
"mb-4 flex items-start justify-between gap-3"
>
<div>
<h2
class=
"m-0 text-4.5 font-700 text-[#17305d]"
>
私信列表
</h2>
</div>
</div>
<div
class=
"mb-4 grid grid-cols-2 gap-2"
>
<button
v-for=
"item in categoryTabs"
:key=
"item.key"
class=
"cursor-pointer flex items-center justify-between rounded-lg border px-4 py-3 text-3.5 transition-all"
:class=
"
activeCategory === item.key
? 'border-[#8eadff] bg-[linear-gradient(180deg,#f4f8ff_0%,#eaf1ff_100%)] text-[#2f5ab7] shadow-[0_0.25rem_0.625rem_rgba(130,160,220,0.12)]'
: 'border-[#dfebf5] bg-[#f9fbfe] text-[#667b95]'
"
@
click=
"switchCategory(item.key)"
>
<span>
{{
item
.
label
}}
</span>
<span
class=
"inline-flex h-6 min-w-6 items-center justify-center rounded-full bg-[#e8eff8] px-1.5 text-3 font-700"
>
{{
item
.
key
===
'anonymous'
?
conversationStats
.
anonymous
:
conversationStats
.
realname
}}
</span>
</button>
</div>
<el-input
v-model=
"keyword"
size=
"large"
clearable
placeholder=
"搜索用户名"
class=
"mb-4"
>
<template
#
prefix
>
<el-icon><IEpSearch
/></el-icon>
</
template
>
</el-input>
</div>
<div
class=
"min-h-0 flex-1"
>
<el-scrollbar>
<div
v-if=
"filteredConversationList.length"
class=
"space-y-3"
>
<button
v-for=
"item in filteredConversationList"
:key=
"item.id"
class=
"w-full rounded-lg border p-3 text-left transition-all"
:class=
"
activeConversation?.id === item.id
? 'border-[#cbdcff] bg-[linear-gradient(180deg,#f6faff_0%,#edf5ff_100%)] shadow-[0_0.5rem_1.125rem_rgba(134,167,207,0.15)]'
: 'border-transparent bg-transparent hover:border-[#dde8f3] hover:bg-[#f9fbfe]'
"
@
click=
"selectConversation(item.id)"
>
<div
class=
"flex gap-3"
>
<div
class=
"relative shrink-0"
>
<el-avatar
:size=
"46"
:src=
"item.avatar"
/>
</div>
<div
class=
"min-w-0 flex-1"
>
<div
class=
"flex items-center justify-between gap-2"
>
<div
class=
"flex min-w-0 items-center gap-2"
>
<span
class=
"max-w-[6.875rem] truncate text-4 font-700 text-[#17305d]"
>
{{ item.name }}
</span>
</div>
<span
class=
"shrink-0 text-3 text-[#8ca0b8]"
>
{{
getConversationPreviewTime(item)
}}
</span>
</div>
<div
class=
"mt-2 flex min-w-0 items-center gap-2"
>
<span
class=
"inline-flex h-5 shrink-0 items-center rounded-full bg-[#fff3dc] px-2 text-3 text-[#9a6817]"
>
{{ item.sourceTypeLabel }}
</span>
<span
class=
"truncate text-3 text-[#7890aa]"
>
{{ item.sourceTitle }}
</span>
</div>
</div>
</div>
</button>
</div>
<el-empty
v-else
description=
"没有找到匹配的会话"
:image-size=
"90"
/>
</el-scrollbar>
</div>
</div>
</aside>
<main
class=
"overflow-hidden h-full flex-3/4 rounded-lg border border-[#d8e5f2] bg-[linear-gradient(180deg,#ffffff_0%,#f9fbff_100%)] shadow-[0_0.5rem_1.5rem_rgba(134,167,207,0.12)]"
>
<
template
v-if=
"activeConversation"
>
<div
class=
"flex h-full min-h-0 flex-col"
>
<header
class=
"flex flex-col gap-3 border-b border-[#e6eef6] bg-[linear-gradient(180deg,#f6faff_0%,#ffffff_100%)] px-5 py-4 md:flex-row md:items-center md:justify-between"
>
<div
class=
"flex min-w-0 items-center gap-3"
>
<el-avatar
:size=
"50"
:src=
"activeConversation.avatar"
/>
<div
class=
"min-w-0"
>
<div
class=
"flex items-center gap-2"
>
<strong
class=
"text-4.5 font-700 text-[#18305d]"
>
{{
activeConversation
.
name
}}
</strong>
<span
class=
"inline-flex h-6 items-center rounded-full px-2.5 text-3 font-700"
:class=
"
activeConversation.category === 'anonymous'
? 'bg-[#e7f7ed] text-[#2a7f61]'
: 'bg-[#e8f0ff] text-[#3762c7]'
"
>
{{
activeConversation
.
badge
}}
</span>
</div>
<p
class=
"mt-1 truncate text-[0.8125rem] leading-[1.375rem] text-[#7588a3]"
>
来源:
{{
activeConversation
.
sourceTitle
}}
·
{{
activeConversation
.
onlineStatus
}}
</p>
</div>
</div>
<div
class=
"text-[0.8125rem] text-[#6f84a4]"
>
当前对话按
<span
class=
"mx-1 font-700 text-[#4a6fd2]"
>
{{
activeConversation
.
badge
}}
</span>
方式展示
</div>
</header>
<div
class=
"min-h-0 flex-1 overflow-hidden"
>
<el-scrollbar
ref=
"conversationBoxRef"
>
<div
class=
"bg-[linear-gradient(180deg,#fcfdff_0%,#f8fbff_100%)] px-5 py-5"
>
<div
v-for=
"group in groupedMessages"
:key=
"group.label"
class=
"pb-3"
>
<div
class=
"mb-4 text-center text-3 text-[#96a6bb]"
>
<span
class=
"rounded-full bg-[#eef4fb] px-3 py-1"
>
{{
group
.
label
}}
</span>
</div>
<div
v-for=
"message in group.items"
:key=
"message.id"
class=
"mb-4 flex items-start gap-3"
:class=
"
{ 'flex-row-reverse': message.sender === 'self' }"
>
<el-avatar
:size=
"38"
:src=
"
message.sender === 'self' ? currentUserAvatar : activeConversation.avatar
"
class=
"shrink-0"
/>
<div
class=
"flex max-w-[min(72%,38.75rem)] flex-col gap-1"
:class=
"
{ 'items-end': message.sender === 'self' }"
>
<div
class=
"text-3 text-[#8c9bb0]"
>
{{
message
.
sender
===
'self'
?
currentUserName
:
activeConversation
.
name
}}
</div>
<div
class=
"rounded-lg border px-4 py-3 shadow-[0_0.125rem_0.5rem_rgba(39,74,120,0.04)]"
:class=
"
message.sender === 'self'
? 'rounded-tr-[0.375rem] border-[#d4e4ff] bg-[#dbeaff] text-[#1a3f73]'
: 'rounded-tl-[0.375rem] border-[#e7edf5] bg-white text-[#23364c]'
"
>
<p
v-if=
"message.text"
class=
"m-0 whitespace-pre-wrap break-words text-3.5 leading-[1.625rem]"
>
{{
message
.
text
}}
</p>
<div
v-if=
"message.images.length"
class=
"mt-2 grid gap-2 sm:grid-cols-2"
>
<el-image
v-for=
"image in message.images"
:key=
"image"
:src=
"image"
fit=
"cover"
:preview-src-list=
"message.images"
preview-teleported
class=
"h-30 w-full overflow-hidden rounded-[0.875rem]"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
<footer
class=
"border-t border-[#e6eef6] bg-white px-5 py-4 h-auto"
>
<div
class=
"mb-3 flex items-center justify-between gap-3 text-[0.8125rem] text-[#7690ac]"
>
<div
class=
"flex items-center gap-1.5"
>
<el-icon><IEpInfoFilled
/></el-icon>
<span>
按 Enter 发送,Shift + Enter 换行
</span>
</div>
<span>
{{
draftText
.
length
}}
/ 250
</span>
</div>
<div
class=
"self-message-commentbox"
@
keydown=
"handleInputKeydown"
>
<CommentBox
v-model:inputText=
"draftText"
v-model:inputImg=
"draftImages"
:textAreaHeight=
"72"
placeholder=
"请输入回复内容,回车发送"
>
<template
#
submit
>
<button
type=
"button"
class=
"comment-publish-btn cursor-pointer disabled:opacity-50 px-6 py-2 text-white rounded-full text-sm hover:shadow-lg transition-all"
:disabled=
"!draftText?.trim() || sending"
@
click=
"handleSend"
>
<div
v-show=
"sending"
class=
"flex items-center gap-2"
>
<el-icon><IEpLoading
/></el-icon>
<span>
发表中...
</span>
</div>
<div
v-show=
"!sending"
>
发表
</div>
</button>
</
template
>
</CommentBox>
</div>
</footer>
</div>
</template>
<div
v-else
class=
"flex h-full items-center justify-center"
>
<el-empty
description=
"选择一个会话开始查看私信内容"
:image-size=
"120"
/>
</div>
</main>
</section>
</div>
</template>
<
style
scoped
>
:deep
(
.self-message-commentbox
textarea
)
{
min-height
:
4.25rem
!important
;
}
:deep
(
.self-message-commentbox
.el-button--primary
)
{
border-radius
:
0.875rem
;
border-color
:
#85a7ff
;
background
:
linear-gradient
(
90deg
,
#71b6ff
0%
,
#7f92ff
100%
);
box-shadow
:
0
0.375rem
1rem
rgba
(
125
,
146
,
255
,
0.2
);
}
:deep
(
.self-message-commentbox
.el-button--large
)
{
min-width
:
5.75rem
;
}
@media
(
max-width
:
64rem
)
{
:deep
(
.self-message-commentbox
.el-button--large
)
{
width
:
100%
;
}
}
/**
* 发表按钮:不用纯 Uno 渐变工具类。
* 旧版内核 / 部分企业微信 WebView 对「依赖 CSS 变量的 linear-gradient」解析失败时,
* 背景会变成透明或无效,原生 button 呈白底,易出现「只剩阴影、几乎看不见按钮」。
* 这里用实色 background-color 作兜底,并写死 hex + -webkit- 前缀渐变。
*/
.comment-publish-btn
{
-webkit-appearance
:
none
;
appearance
:
none
;
margin
:
0
;
border
:
1px
solid
rgba
(
255
,
255
,
255
,
0.35
);
/* 与 Tailwind blue-500 / purple-500 接近的实色兜底 */
background-color
:
#6366f1
;
background-image
:
-webkit-linear-gradient
(
left
,
#3b82f6
,
#a855f7
);
background-image
:
linear-gradient
(
to
right
,
#3b82f6
,
#a855f7
);
color
:
#ffffff
;
}
.comment-publish-btn
:disabled
{
cursor
:
not-allowed
;
}
</
style
>
src/views/userPage/index.vue
View file @
5d016c03
...
...
@@ -168,7 +168,6 @@ import { generateLoginKey, hasOfficialAccount } from '@/api'
import
type
{
OfficialAccountItemDto
}
from
'@/api/user/types'
import
{
wxLogin
}
from
'@/utils/wxUtil'
import
type
{
RouteLocationNormalizedLoadedGeneric
}
from
'vue-router'
import
type
{
TabPaneName
}
from
'element-plus'
import
{
IS_REAL_KEY
}
from
'@/constants/symbolKey'
import
{
useOnlineTimeStore
,
useUserStore
}
from
'@/stores'
import
BackButton
from
'@/components/common/BackButton/index.vue'
...
...
@@ -182,10 +181,11 @@ const route = useRoute()
// 当前激活的菜单 用计算属性 好办法!
const
activeMenu
=
computed
(()
=>
{
const
path
=
route
.
path
console
.
log
(
path
,
'path'
)
if
(
path
.
includes
(
'userPage'
))
{
return
path
.
split
(
'/'
).
at
(
-
1
)
||
'
selfPublish'
return
path
||
'/userPage/
selfPublish'
}
return
'selfPublish'
return
'
/userPage/
selfPublish'
})
const
getThirdLevelKey
=
(
route
:
RouteLocationNormalizedLoadedGeneric
)
=>
{
...
...
@@ -202,71 +202,77 @@ const { userInfo } = storeToRefs(userStore)
// 左侧普通用户菜单
const
menuUserItems
=
[
{
path
:
'selfPublish'
,
path
:
'
/userPage/
selfPublish'
,
label
:
'我的帖子'
,
icon
:
()
=>
<
IEpUser
/>
,
tab
:
'发布'
,
},
{
path
:
'selfDraft'
,
path
:
'
/userPage/
selfDraft'
,
label
:
'我的草稿'
,
icon
:
()
=>
<
IEpDocument
/>
,
tab
:
'草稿'
,
},
{
path
:
'selfCollect'
,
path
:
'
/userPage/
selfCollect'
,
label
:
'我的收藏'
,
icon
:
()
=>
<
IEpStar
/>
,
tab
:
'收藏'
,
},
{
path
:
'selfPraise'
,
path
:
'
/userPage/
selfPraise'
,
label
:
'我的点赞'
,
icon
:
()
=>
<
IEpPointer
/>
,
tab
:
'点赞'
,
},
{
path
:
'selfCase'
,
path
:
'
/userPage/
selfCase'
,
label
:
'我的案例库'
,
icon
:
()
=>
<
IEpCollection
/>
,
tab
:
'案例库'
,
},
{
path
:
'selfTask'
,
path
:
'
/userPage/
selfTask'
,
label
:
'我的任务'
,
icon
:
()
=>
<
IEpFinished
/>
,
tab
:
'任务'
,
},
{
path
:
'selfActivity'
,
path
:
'
/userPage/
selfActivity'
,
label
:
'参与活动'
,
icon
:
()
=>
<
IEpTrophy
/>
,
tab
:
'活动'
,
},
{
path
:
'selfComment'
,
path
:
'
/userPage/
selfComment'
,
label
:
'评论回复'
,
icon
:
()
=>
<
IEpChatDotRound
/>
,
tab
:
'评论回复'
,
},
{
path
:
'selfAnswer'
,
path
:
'
/userPage/
selfAnswer'
,
label
:
'回答问题(问吧)'
,
icon
:
()
=>
<
IEpChatLineSquare
/>
,
tab
:
'回答问题'
,
},
{
path
:
'/selfMessage'
,
label
:
'我的私信'
,
icon
:
()
=>
<
IEpMessage
/>
,
tab
:
'我的私信'
,
},
]
// 左侧官方账号菜单
const
menuOfficialItems
=
[
{
path
:
'selfAudit'
,
path
:
'
/userPage/
selfAudit'
,
label
:
'审核列表'
,
icon
:
()
=>
<
IEpUser
/>
,
tab
:
'审核列表'
,
},
{
path
:
'selfComplaint'
,
path
:
'
/userPage/
selfComplaint'
,
label
:
'举报列表'
,
icon
:
()
=>
<
IEpWarning
/>
,
tab
:
'举报列表'
,
...
...
@@ -296,8 +302,8 @@ const currentUserInfo = computed(() =>
},
)
const
changeMenu
=
(
key
:
TabPaneName
)
=>
{
router
.
push
(
`/userPage/
${
key
}
`
)
const
changeMenu
=
(
key
:
string
)
=>
{
router
.
push
(
key
)
}
const
handleEdit
=
()
=>
{
...
...
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