Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
admin
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
6eb013b2
authored
Nov 25, 2024
by
芋道源码
Committed by
Gitee
Nov 25, 2024
Browse files
Options
Browse Files
Download
Plain Diff
!601 同步商城的客服优化、门店自提优化
Merge pull request !601 from 芋道源码/dev
parents
22199c64
6dee7415
Hide whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
790 additions
and
463 deletions
+790
-463
src/api/mall/promotion/kefu/conversation/index.ts
+5
-1
src/api/mall/promotion/kefu/message/index.ts
+3
-3
src/store/modules/mall/kefu.ts
+81
-0
src/views/mall/promotion/components/SpuSelect.vue
+8
-1
src/views/mall/promotion/kefu/components/KeFuConversationList.vue
+49
-33
src/views/mall/promotion/kefu/components/KeFuMessageList.vue
+168
-110
src/views/mall/promotion/kefu/components/history/MemberBrowsingHistory.vue
+0
-97
src/views/mall/promotion/kefu/components/index.ts
+2
-2
src/views/mall/promotion/kefu/components/member/MemberInfo.vue
+252
-0
src/views/mall/promotion/kefu/components/member/OrderBrowsingHistory.vue
+0
-0
src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue
+3
-4
src/views/mall/promotion/kefu/components/message/OrderItem.vue
+9
-7
src/views/mall/promotion/kefu/components/message/ProductItem.vue
+60
-148
src/views/mall/promotion/kefu/components/tools/emoji.ts
+1
-1
src/views/mall/promotion/kefu/index.vue
+29
-37
src/views/member/user/detail/UserAccountInfo.vue
+23
-2
src/views/member/user/detail/UserBasicInfo.vue
+97
-17
No files found.
src/api/mall/promotion/kefu/conversation/index.ts
View file @
6eb013b2
...
@@ -21,6 +21,10 @@ export const KeFuConversationApi = {
...
@@ -21,6 +21,10 @@ export const KeFuConversationApi = {
getConversationList
:
async
()
=>
{
getConversationList
:
async
()
=>
{
return
await
request
.
get
({
url
:
'/promotion/kefu-conversation/list'
})
return
await
request
.
get
({
url
:
'/promotion/kefu-conversation/list'
})
},
},
// 获得客服会话
getConversation
:
async
(
id
:
number
)
=>
{
return
await
request
.
get
({
url
:
`/promotion/kefu-conversation/get?id=`
+
id
})
},
// 客服会话置顶
// 客服会话置顶
updateConversationPinned
:
async
(
data
:
any
)
=>
{
updateConversationPinned
:
async
(
data
:
any
)
=>
{
return
await
request
.
put
({
return
await
request
.
put
({
...
@@ -30,6 +34,6 @@ export const KeFuConversationApi = {
...
@@ -30,6 +34,6 @@ export const KeFuConversationApi = {
},
},
// 删除客服会话
// 删除客服会话
deleteConversation
:
async
(
id
:
number
)
=>
{
deleteConversation
:
async
(
id
:
number
)
=>
{
return
await
request
.
delete
({
url
:
`/promotion/kefu-conversation/delete?id=
${
id
}
`
})
return
await
request
.
delete
({
url
:
`/promotion/kefu-conversation/delete?id=
${
id
}
`
})
}
}
}
}
src/api/mall/promotion/kefu/message/index.ts
View file @
6eb013b2
...
@@ -29,8 +29,8 @@ export const KeFuMessageApi = {
...
@@ -29,8 +29,8 @@ export const KeFuMessageApi = {
url
:
'/promotion/kefu-message/update-read-status?conversationId='
+
conversationId
url
:
'/promotion/kefu-message/update-read-status?conversationId='
+
conversationId
})
})
},
},
// 获得消息
分页数据
// 获得消息
列表(流式加载)
getKeFuMessage
Page
:
async
(
params
:
any
)
=>
{
getKeFuMessage
List
:
async
(
params
:
any
)
=>
{
return
await
request
.
get
({
url
:
'/promotion/kefu-message/
page
'
,
params
})
return
await
request
.
get
({
url
:
'/promotion/kefu-message/
list
'
,
params
})
}
}
}
}
src/store/modules/mall/kefu.ts
0 → 100644
View file @
6eb013b2
import
{
store
}
from
'@/store'
import
{
defineStore
}
from
'pinia'
import
{
KeFuConversationApi
,
KeFuConversationRespVO
}
from
'@/api/mall/promotion/kefu/conversation'
import
{
KeFuMessageRespVO
}
from
'@/api/mall/promotion/kefu/message'
import
{
isEmpty
}
from
'@/utils/is'
interface
MallKefuInfoVO
{
conversationList
:
KeFuConversationRespVO
[]
// 会话列表
conversationMessageList
:
Map
<
number
,
KeFuMessageRespVO
[]
>
// 会话消息
}
export
const
useMallKefuStore
=
defineStore
(
'mall-kefu'
,
{
state
:
():
MallKefuInfoVO
=>
({
conversationList
:
[],
conversationMessageList
:
new
Map
<
number
,
KeFuMessageRespVO
[]
>
()
// key 会话,value 会话消息列表
}),
getters
:
{
getConversationList
():
KeFuConversationRespVO
[]
{
return
this
.
conversationList
},
getConversationMessageList
():
(
conversationId
:
number
)
=>
KeFuMessageRespVO
[]
|
undefined
{
return
(
conversationId
:
number
)
=>
this
.
conversationMessageList
.
get
(
conversationId
)
}
},
actions
:
{
// ======================= 会话消息相关 =======================
/** 缓存历史消息 */
saveMessageList
(
conversationId
:
number
,
messageList
:
KeFuMessageRespVO
[])
{
this
.
conversationMessageList
.
set
(
conversationId
,
messageList
)
},
// ======================= 会话相关 =======================
/** 加载会话缓存列表 */
async
setConversationList
()
{
this
.
conversationList
=
await
KeFuConversationApi
.
getConversationList
()
this
.
conversationSort
()
},
/** 更新会话缓存已读 */
async
updateConversationStatus
(
conversationId
:
number
)
{
if
(
isEmpty
(
this
.
conversationList
))
{
return
}
const
conversation
=
this
.
conversationList
.
find
((
item
)
=>
item
.
id
===
conversationId
)
conversation
&&
(
conversation
.
adminUnreadMessageCount
=
0
)
},
/** 更新会话缓存 */
async
updateConversation
(
conversationId
:
number
)
{
if
(
isEmpty
(
this
.
conversationList
))
{
return
}
const
conversation
=
await
KeFuConversationApi
.
getConversation
(
conversationId
)
this
.
deleteConversation
(
conversationId
)
conversation
&&
this
.
conversationList
.
push
(
conversation
)
this
.
conversationSort
()
},
/** 删除会话缓存 */
deleteConversation
(
conversationId
:
number
)
{
const
index
=
this
.
conversationList
.
findIndex
((
item
)
=>
item
.
id
===
conversationId
)
// 存在则删除
if
(
index
>
-
1
)
{
this
.
conversationList
.
splice
(
index
,
1
)
}
},
conversationSort
()
{
// 按置顶属性和最后消息时间排序
this
.
conversationList
.
sort
((
a
,
b
)
=>
{
// 按照置顶排序,置顶的会在前面
if
(
a
.
adminPinned
!==
b
.
adminPinned
)
{
return
a
.
adminPinned
?
-
1
:
1
}
// 按照最后消息时间排序,最近的会在前面
return
(
b
.
lastMessageTime
as
unknown
as
number
)
-
(
a
.
lastMessageTime
as
unknown
as
number
)
})
}
}
})
export
const
useMallKefuStoreWithOut
=
()
=>
{
return
useMallKefuStore
(
store
)
}
src/views/mall/promotion/components/SpuSelect.vue
View file @
6eb013b2
...
@@ -115,7 +115,7 @@ import { getPropertyList, PropertyAndValues, SkuList } from '@/views/mall/produc
...
@@ -115,7 +115,7 @@ import { getPropertyList, PropertyAndValues, SkuList } from '@/views/mall/produc
import
{
ElTable
}
from
'element-plus'
import
{
ElTable
}
from
'element-plus'
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
{
dateFormatter
}
from
'@/utils/formatTime'
import
{
createImageViewer
}
from
'@/components/ImageViewer'
import
{
createImageViewer
}
from
'@/components/ImageViewer'
import
{
formatToFraction
}
from
'@/utils'
import
{
f
loatToFixed2
,
f
ormatToFraction
}
from
'@/utils'
import
{
defaultProps
,
handleTree
}
from
'@/utils/tree'
import
{
defaultProps
,
handleTree
}
from
'@/utils/tree'
import
*
as
ProductCategoryApi
from
'@/api/mall/product/category'
import
*
as
ProductCategoryApi
from
'@/api/mall/product/category'
...
@@ -228,6 +228,13 @@ const expandChange = async (row: ProductSpuApi.Spu, expandedRows?: ProductSpuApi
...
@@ -228,6 +228,13 @@ const expandChange = async (row: ProductSpuApi.Spu, expandedRows?: ProductSpuApi
}
}
// 获取 SPU 详情
// 获取 SPU 详情
const
res
=
(
await
ProductSpuApi
.
getSpu
(
row
.
id
as
number
))
as
ProductSpuApi
.
Spu
const
res
=
(
await
ProductSpuApi
.
getSpu
(
row
.
id
as
number
))
as
ProductSpuApi
.
Spu
res
.
skus
?.
forEach
((
item
)
=>
{
item
.
price
=
floatToFixed2
(
item
.
price
)
item
.
marketPrice
=
floatToFixed2
(
item
.
marketPrice
)
item
.
costPrice
=
floatToFixed2
(
item
.
costPrice
)
item
.
firstBrokeragePrice
=
floatToFixed2
(
item
.
firstBrokeragePrice
)
item
.
secondBrokeragePrice
=
floatToFixed2
(
item
.
secondBrokeragePrice
)
})
propertyList
.
value
=
getPropertyList
(
res
)
propertyList
.
value
=
getPropertyList
(
res
)
spuData
.
value
=
res
spuData
.
value
=
res
isExpand
.
value
=
true
isExpand
.
value
=
true
...
...
src/views/mall/promotion/kefu/components/KeFuConversationList.vue
View file @
6eb013b2
<
template
>
<
template
>
<div
class=
"kefu"
>
<el-aside
class=
"kefu pt-5px h-100%"
width=
"260px"
>
<div
class=
"color-[#999] font-bold my-10px"
>
会话记录(
{{
kefuStore
.
getConversationList
.
length
}}
)
</div>
<div
<div
v-for=
"item in
c
onversationList"
v-for=
"item in
kefuStore.getC
onversationList"
:key=
"item.id"
:key=
"item.id"
:class=
"
{ active: item.id === activeConversationId, pinned: item.adminPinned }"
:class=
"
{ active: item.id === activeConversationId, pinned: item.adminPinned }"
class="kefu-conversation flex items-center"
class="kefu-conversation
px-10px
flex items-center"
@click="openRightMessage(item)"
@click="openRightMessage(item)"
@contextmenu.prevent="rightClick($event as PointerEvent, item)"
@contextmenu.prevent="rightClick($event as PointerEvent, item)"
>
>
...
@@ -22,8 +25,8 @@
...
@@ -22,8 +25,8 @@
<div
class=
"ml-10px w-100%"
>
<div
class=
"ml-10px w-100%"
>
<div
class=
"flex justify-between items-center w-100%"
>
<div
class=
"flex justify-between items-center w-100%"
>
<span
class=
"username"
>
{{
item
.
userNickname
}}
</span>
<span
class=
"username"
>
{{
item
.
userNickname
}}
</span>
<span
class=
"color-[
var(--left-menu-text-color)
]"
style=
"font-size: 13px"
>
<span
class=
"color-[
#999
]"
style=
"font-size: 13px"
>
{{
formatPast
(
item
.
lastMessageTime
,
'YYYY-MM-DD'
)
}}
{{
lastMessageTimeMap
.
get
(
item
.
id
)
??
'计算中'
}}
</span>
</span>
</div>
</div>
<!-- 最后聊天内容 -->
<!-- 最后聊天内容 -->
...
@@ -31,7 +34,7 @@
...
@@ -31,7 +34,7 @@
v-dompurify-html=
"
v-dompurify-html=
"
getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
getConversationDisplayText(item.lastMessageContentType, item.lastMessageContent)
"
"
class=
"last-message flex items-center color-[
var(--left-menu-text-color)
]"
class=
"last-message flex items-center color-[
#999
]"
>
>
</div>
</div>
</div>
</div>
...
@@ -65,7 +68,7 @@
...
@@ -65,7 +68,7 @@
取消
取消
</li>
</li>
</ul>
</ul>
</
div
>
</
el-aside
>
</
template
>
</
template
>
<
script
lang=
"ts"
setup
>
<
script
lang=
"ts"
setup
>
...
@@ -74,29 +77,36 @@ import { useEmoji } from './tools/emoji'
...
@@ -74,29 +77,36 @@ import { useEmoji } from './tools/emoji'
import
{
formatPast
}
from
'@/utils/formatTime'
import
{
formatPast
}
from
'@/utils/formatTime'
import
{
KeFuMessageContentTypeEnum
}
from
'./tools/constants'
import
{
KeFuMessageContentTypeEnum
}
from
'./tools/constants'
import
{
useAppStore
}
from
'@/store/modules/app'
import
{
useAppStore
}
from
'@/store/modules/app'
import
{
useMallKefuStore
}
from
'@/store/modules/mall/kefu'
import
{
jsonParse
}
from
'@/utils'
defineOptions
({
name
:
'KeFuConversationList'
})
defineOptions
({
name
:
'KeFuConversationList'
})
const
message
=
useMessage
()
// 消息弹窗
const
message
=
useMessage
()
// 消息弹窗
const
appStore
=
useAppStore
()
const
appStore
=
useAppStore
()
const
kefuStore
=
useMallKefuStore
()
// 客服缓存
const
{
replaceEmoji
}
=
useEmoji
()
const
{
replaceEmoji
}
=
useEmoji
()
const
conversationList
=
ref
<
KeFuConversationRespVO
[]
>
([])
// 会话列表
const
activeConversationId
=
ref
(
-
1
)
// 选中的会话
const
activeConversationId
=
ref
(
-
1
)
// 选中的会话
const
collapse
=
computed
(()
=>
appStore
.
getCollapse
)
// 折叠菜单
const
collapse
=
computed
(()
=>
appStore
.
getCollapse
)
// 折叠菜单
/** 加载会话列表 */
/** 计算消息最后发送时间距离现在过去了多久 */
const
getConversationList
=
async
()
=>
{
const
lastMessageTimeMap
=
ref
<
Map
<
number
,
string
>>
(
new
Map
<
number
,
string
>
())
const
list
=
await
KeFuConversationApi
.
getConversationList
()
const
calculationLastMessageTime
=
()
=>
{
list
.
sort
((
a
:
KeFuConversationRespVO
,
_
)
=>
(
a
.
adminPinned
?
-
1
:
1
))
kefuStore
.
getConversationList
?.
forEach
((
item
)
=>
{
conversationList
.
value
=
list
lastMessageTimeMap
.
value
.
set
(
item
.
id
,
formatPast
(
item
.
lastMessageTime
,
'YYYY-MM-DD'
))
})
}
}
defineExpose
({
getConversationList
})
defineExpose
({
calculationLastMessageTime
})
/** 打开右侧的消息列表 */
/** 打开右侧的消息列表 */
const
emits
=
defineEmits
<
{
const
emits
=
defineEmits
<
{
(
e
:
'change'
,
v
:
KeFuConversationRespVO
):
void
(
e
:
'change'
,
v
:
KeFuConversationRespVO
):
void
}
>
()
}
>
()
const
openRightMessage
=
(
item
:
KeFuConversationRespVO
)
=>
{
const
openRightMessage
=
(
item
:
KeFuConversationRespVO
)
=>
{
// 同一个会话则不处理
if
(
activeConversationId
.
value
===
item
.
id
)
{
return
}
activeConversationId
.
value
=
item
.
id
activeConversationId
.
value
=
item
.
id
emits
(
'change'
,
item
)
emits
(
'change'
,
item
)
}
}
...
@@ -118,7 +128,7 @@ const getConversationDisplayText = computed(
...
@@ -118,7 +128,7 @@ const getConversationDisplayText = computed(
case
KeFuMessageContentTypeEnum
.
VOICE
:
case
KeFuMessageContentTypeEnum
.
VOICE
:
return
'[语音消息]'
return
'[语音消息]'
case
KeFuMessageContentTypeEnum
.
TEXT
:
case
KeFuMessageContentTypeEnum
.
TEXT
:
return
replaceEmoji
(
lastMessageContent
)
return
replaceEmoji
(
jsonParse
(
lastMessageContent
).
text
||
lastMessageContent
)
default
:
default
:
return
''
return
''
}
}
...
@@ -155,7 +165,7 @@ const updateConversationPinned = async (adminPinned: boolean) => {
...
@@ -155,7 +165,7 @@ const updateConversationPinned = async (adminPinned: boolean) => {
message
.
notifySuccess
(
adminPinned
?
'置顶成功'
:
'取消置顶成功'
)
message
.
notifySuccess
(
adminPinned
?
'置顶成功'
:
'取消置顶成功'
)
// 2. 关闭右键菜单,更新会话列表
// 2. 关闭右键菜单,更新会话列表
closeRightMenu
()
closeRightMenu
()
await
getConversationList
(
)
await
kefuStore
.
updateConversation
(
rightClickConversation
.
value
.
id
)
}
}
/** 删除会话 */
/** 删除会话 */
...
@@ -165,7 +175,7 @@ const deleteConversation = async () => {
...
@@ -165,7 +175,7 @@ const deleteConversation = async () => {
await
KeFuConversationApi
.
deleteConversation
(
rightClickConversation
.
value
.
id
)
await
KeFuConversationApi
.
deleteConversation
(
rightClickConversation
.
value
.
id
)
// 2. 关闭右键菜单,更新会话列表
// 2. 关闭右键菜单,更新会话列表
closeRightMenu
()
closeRightMenu
()
await
getConversationList
(
)
kefuStore
.
deleteConversation
(
rightClickConversation
.
value
.
id
)
}
}
/** 监听右键菜单的显示状态,添加点击事件监听器 */
/** 监听右键菜单的显示状态,添加点击事件监听器 */
...
@@ -176,42 +186,48 @@ watch(showRightMenu, (val) => {
...
@@ -176,42 +186,48 @@ watch(showRightMenu, (val) => {
document
.
body
.
removeEventListener
(
'click'
,
closeRightMenu
)
document
.
body
.
removeEventListener
(
'click'
,
closeRightMenu
)
}
}
})
})
const
timer
=
ref
<
any
>
()
/** 初始化 */
onMounted
(()
=>
{
timer
.
value
=
setInterval
(
calculationLastMessageTime
,
1000
*
10
)
// 十秒计算一次
})
/** 组件卸载前 */
onBeforeUnmount
(()
=>
{
clearInterval
(
timer
.
value
)
})
</
script
>
</
script
>
<
style
lang=
"scss"
scoped
>
<
style
lang=
"scss"
scoped
>
.kefu
{
.kefu
{
background-color
:
#e5e4e4
;
&-conversation
{
&-conversation
{
height
:
60px
;
height
:
60px
;
padding
:
10px
;
//
background-color
:
#fff
;
//
background-color
:
#fff
;
transition
:
border-left
0.05s
ease-in-out
;
/* 设置过渡效果 */
//
transition
:
border-left
0.05s
ease-in-out
;
/* 设置过渡效果 */
.username
{
.username
{
min-width
:
0
;
min-width
:
0
;
max-width
:
60%
;
max-width
:
60%
;
}
.last-message
{
font-size
:
13px
;
}
.last-message
,
.username
{
overflow
:
hidden
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
text-overflow
:
ellipsis
;
display
:
-webkit-box
;
display
:
-webkit-box
;
-webkit-box-orient
:
vertical
;
-webkit-box-orient
:
vertical
;
-webkit-line-clamp
:
1
;
-webkit-line-clamp
:
1
;
}
}
.last-message
{
font-size
:
13px
;
width
:
200px
;
overflow
:
hidden
;
//
隐藏超出的文本
white-space
:
nowrap
;
//
禁止换行
text-overflow
:
ellipsis
;
//
添加省略号
}
}
}
.active
{
.active
{
border-left
:
5px
#3271ff
solid
;
background-color
:
rgba
(
128
,
128
,
128
,
0.5
);
//
透明色,暗黑模式下也能体现
background-color
:
var
(
--login-bg-color
);
}
.pinned
{
background-color
:
var
(
--left-menu-bg-active-color
);
}
}
.right-menu-ul
{
.right-menu-ul
{
...
...
src/views/mall/promotion/kefu/components/KeFuMessageList.vue
View file @
6eb013b2
<
template
>
<
template
>
<el-container
v-if=
"showKeFuMessageList"
class=
"kefu"
>
<el-container
v-if=
"showKeFuMessageList"
class=
"kefu"
>
<el-header>
<el-header
class=
"kefu-header"
>
<div
class=
"kefu-title"
>
{{
conversation
.
userNickname
}}
</div>
<div
class=
"kefu-title"
>
{{
conversation
.
userNickname
}}
</div>
</el-header>
</el-header>
<el-main
class=
"kefu-content overflow-visible"
>
<el-main
class=
"kefu-content overflow-visible"
>
<el-scrollbar
ref=
"scrollbarRef"
always
height=
"calc(100vh - 495px)"
@
scroll=
"handleScroll"
>
<el-scrollbar
ref=
"scrollbarRef"
always
@
scroll=
"handleScroll"
>
<div
v-if=
"refreshContent"
ref=
"innerRef"
class=
"w-[100%] p
b-3
px"
>
<div
v-if=
"refreshContent"
ref=
"innerRef"
class=
"w-[100%] p
x-10
px"
>
<!-- 消息列表 -->
<!-- 消息列表 -->
<div
v-for=
"(item, index) in getMessageList0"
:key=
"item.id"
class=
"w-[100%]"
>
<div
v-for=
"(item, index) in getMessageList0"
:key=
"item.id"
class=
"w-[100%]"
>
<div
class=
"flex justify-center items-center mb-20px"
>
<div
class=
"flex justify-center items-center mb-20px"
>
...
@@ -43,15 +43,16 @@
...
@@ -43,15 +43,16 @@
class=
"w-60px h-60px"
class=
"w-60px h-60px"
/>
/>
<div
<div
:class=
"
{ 'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType }"
:class=
"
{
class="p-10px"
'kefu-message': KeFuMessageContentTypeEnum.TEXT === item.contentType
}"
>
>
<!-- 文本消息 -->
<!-- 文本消息 -->
<MessageItem
:message=
"item"
>
<MessageItem
:message=
"item"
>
<template
v-if=
"KeFuMessageContentTypeEnum.TEXT === item.contentType"
>
<template
v-if=
"KeFuMessageContentTypeEnum.TEXT === item.contentType"
>
<div
<div
v-dompurify-html=
"replaceEmoji(item.content)"
v-dompurify-html=
"replaceEmoji(
getMessageContent(item).text ||
item.content)"
class=
"
flex items-center
"
class=
"
line-height-normal text-justify h-1/1 w-full
"
></div>
></div>
</
template
>
</
template
>
</MessageItem>
</MessageItem>
...
@@ -60,9 +61,9 @@
...
@@ -60,9 +61,9 @@
<el-image
<el-image
v-if=
"KeFuMessageContentTypeEnum.IMAGE === item.contentType"
v-if=
"KeFuMessageContentTypeEnum.IMAGE === item.contentType"
:initial-index=
"0"
:initial-index=
"0"
:preview-src-list=
"[item.content]"
:preview-src-list=
"[
getMessageContent(item).picUrl ||
item.content]"
:src=
"item.content"
:src=
"
getMessageContent(item).picUrl ||
item.content"
class=
"w-200px"
class=
"w-200px
mx-10px
"
fit=
"contain"
fit=
"contain"
preview-teleported
preview-teleported
/>
/>
...
@@ -71,14 +72,13 @@
...
@@ -71,14 +72,13 @@
<MessageItem
:message=
"item"
>
<MessageItem
:message=
"item"
>
<ProductItem
<ProductItem
v-if=
"KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
v-if=
"KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
:spuId=
"getMessageContent(item).spuId"
:picUrl=
"getMessageContent(item).picUrl"
:picUrl=
"getMessageContent(item).picUrl"
:price=
"getMessageContent(item).price"
:price=
"getMessageContent(item).price"
:skuText=
"getMessageContent(item).introduction"
:sales-count=
"getMessageContent(item).salesCount"
:spuId=
"getMessageContent(item).spuId"
:stock=
"getMessageContent(item).stock"
:title=
"getMessageContent(item).spuName"
:title=
"getMessageContent(item).spuName"
:titleWidth=
"400"
class=
"max-w-300px mx-10px"
class=
"max-w-70%"
priceColor=
"#FF3000"
/>
/>
</MessageItem>
</MessageItem>
<!-- 订单消息 -->
<!-- 订单消息 -->
...
@@ -86,7 +86,7 @@
...
@@ -86,7 +86,7 @@
<OrderItem
<OrderItem
v-if=
"KeFuMessageContentTypeEnum.ORDER === item.contentType"
v-if=
"KeFuMessageContentTypeEnum.ORDER === item.contentType"
:message=
"item"
:message=
"item"
class=
"max-w-100%"
class=
"max-w-100%
mx-10px
"
/>
/>
</MessageItem>
</MessageItem>
</div>
</div>
...
@@ -108,23 +108,29 @@
...
@@ -108,23 +108,29 @@
<Icon
class=
"ml-5px"
icon=
"ep:bottom"
/>
<Icon
class=
"ml-5px"
icon=
"ep:bottom"
/>
</div>
</div>
</el-main>
</el-main>
<el-footer
height=
"230px"
>
<el-footer
class=
"kefu-footer"
>
<div
class=
"h-[100%]"
>
<div
class=
"chat-tools flex items-center"
>
<div
class=
"chat-tools flex items-center"
>
<EmojiSelectPopover
@
select-emoji=
"handleEmojiSelect"
/>
<EmojiSelectPopover
@
select-emoji=
"handleEmojiSelect"
/>
<PictureSelectUpload
<PictureSelectUpload
class=
"ml-15px mt-3px cursor-pointer"
class=
"ml-15px mt-3px cursor-pointer"
@
send-picture=
"handleSendPicture"
@
send-picture=
"handleSendPicture"
/>
/>
</div>
<el-input
v-model=
"message"
:rows=
"6"
style=
"border-style: none"
type=
"textarea"
/>
<div
class=
"h-45px flex justify-end"
>
<el-button
class=
"mt-10px"
type=
"primary"
@
click=
"handleSendMessage"
>
发送
</el-button>
</div>
</div>
</div>
<el-input
v-model=
"message"
:rows=
"6"
placeholder=
"输入消息,Enter发送,Shift+Enter换行"
style=
"border-style: none"
type=
"textarea"
@
keyup
.
enter
.
prevent=
"handleSendMessage"
/>
</el-footer>
</el-footer>
</el-container>
</el-container>
<el-empty
v-else
description=
"请选择左侧的一个会话后开始"
/>
<el-container
v-else
class=
"kefu"
>
<el-main>
<el-empty
description=
"请选择左侧的一个会话后开始"
/>
</el-main>
</el-container>
</template>
</template>
<
script
lang=
"ts"
setup
>
<
script
lang=
"ts"
setup
>
...
@@ -144,6 +150,7 @@ import dayjs from 'dayjs'
...
@@ -144,6 +150,7 @@ import dayjs from 'dayjs'
import
relativeTime
from
'dayjs/plugin/relativeTime'
import
relativeTime
from
'dayjs/plugin/relativeTime'
import
{
debounce
}
from
'lodash-es'
import
{
debounce
}
from
'lodash-es'
import
{
jsonParse
}
from
'@/utils'
import
{
jsonParse
}
from
'@/utils'
import
{
useMallKefuStore
}
from
'@/store/modules/mall/kefu'
dayjs
.
extend
(
relativeTime
)
dayjs
.
extend
(
relativeTime
)
...
@@ -156,25 +163,31 @@ const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
...
@@ -156,25 +163,31 @@ const messageList = ref<KeFuMessageRespVO[]>([]) // 消息列表
const
conversation
=
ref
<
KeFuConversationRespVO
>
({}
as
KeFuConversationRespVO
)
// 用户会话
const
conversation
=
ref
<
KeFuConversationRespVO
>
({}
as
KeFuConversationRespVO
)
// 用户会话
const
showNewMessageTip
=
ref
(
false
)
// 显示有新消息提示
const
showNewMessageTip
=
ref
(
false
)
// 显示有新消息提示
const
queryParams
=
reactive
({
const
queryParams
=
reactive
({
pageNo
:
1
,
conversationId
:
0
,
pageSize
:
10
,
createTime
:
undefined
conversationId
:
0
})
})
const
total
=
ref
(
0
)
// 消息总条数
const
total
=
ref
(
0
)
// 消息总条数
const
refreshContent
=
ref
(
false
)
// 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
const
refreshContent
=
ref
(
false
)
// 内容刷新,主要解决会话消息页面高度不一致导致的滚动功能精度失效
const
kefuStore
=
useMallKefuStore
()
// 客服缓存
/** 获悉消息内容 */
/** 获悉消息内容 */
const
getMessageContent
=
computed
(()
=>
(
item
:
any
)
=>
jsonParse
(
item
.
content
))
const
getMessageContent
=
computed
(()
=>
(
item
:
any
)
=>
jsonParse
(
item
.
content
))
/** 获得消息列表 */
/** 获得消息列表 */
const
getMessageList
=
async
()
=>
{
const
getMessageList
=
async
()
=>
{
const
res
=
await
KeFuMessageApi
.
getKeFuMessagePage
(
queryParams
)
const
res
=
await
KeFuMessageApi
.
getKeFuMessageList
(
queryParams
)
total
.
value
=
res
.
total
if
(
isEmpty
(
res
))
{
// 当返回的是空列表说明没有消息或者已经查询完了历史消息
skipGetMessageList
.
value
=
true
return
}
queryParams
.
createTime
=
formatDate
(
res
.
at
(
-
1
).
createTime
)
as
any
// 情况一:加载最新消息
// 情况一:加载最新消息
if
(
queryParams
.
pageNo
===
1
)
{
if
(
!
queryParams
.
createTime
)
{
messageList
.
value
=
res
.
list
messageList
.
value
=
res
}
else
{
}
else
{
// 情况二:加载历史消息
// 情况二:加载历史消息
for
(
const
item
of
res
.
list
)
{
for
(
const
item
of
res
)
{
pushMessage
(
item
)
pushMessage
(
item
)
}
}
}
}
...
@@ -208,8 +221,7 @@ const refreshMessageList = async (message?: any) => {
...
@@ -208,8 +221,7 @@ const refreshMessageList = async (message?: any) => {
}
}
pushMessage
(
message
)
pushMessage
(
message
)
}
else
{
}
else
{
// TODO @puhui999:不基于 page 做。而是流式分页;通过 createTime 排序查询;
queryParams
.
createTime
=
undefined
queryParams
.
pageNo
=
1
await
getMessageList
()
await
getMessageList
()
}
}
...
@@ -222,28 +234,27 @@ const refreshMessageList = async (message?: any) => {
...
@@ -222,28 +234,27 @@ const refreshMessageList = async (message?: any) => {
}
}
}
}
/** 获得新会话的消息列表 */
/** 获得新会话的消息列表, 点击切换时,读取缓存;然后异步获取新消息,merge 下; */
// TODO @puhui999:可优化:可以考虑本地做每个会话的消息 list 缓存;然后点击切换时,读取缓存;然后异步获取新消息,merge 下;
const
getNewMessageList
=
async
(
val
:
KeFuConversationRespVO
)
=>
{
const
getNewMessageList
=
async
(
val
:
KeFuConversationRespVO
)
=>
{
// 会话切换,重置相关参数
// 1. 缓存当前会话消息列表
queryParams
.
pageNo
=
1
kefuStore
.
saveMessageList
(
conversation
.
value
.
id
,
messageList
.
value
)
messageList
.
value
=
[]
// 2.1 会话切换,重置相关参数
total
.
value
=
0
messageList
.
value
=
kefuStore
.
getConversationMessageList
(
val
.
id
)
||
[]
total
.
value
=
messageList
.
value
.
length
||
0
loadHistory
.
value
=
false
loadHistory
.
value
=
false
refreshContent
.
value
=
false
refreshContent
.
value
=
false
// 设置会话相关属性
skipGetMessageList
.
value
=
false
// 2.2 设置会话相关属性
conversation
.
value
=
val
conversation
.
value
=
val
queryParams
.
conversationId
=
val
.
id
queryParams
.
conversationId
=
val
.
id
// 获取消息
queryParams
.
createTime
=
undefined
// 3. 获取消息
await
refreshMessageList
()
await
refreshMessageList
()
}
}
defineExpose
({
getNewMessageList
,
refreshMessageList
})
defineExpose
({
getNewMessageList
,
refreshMessageList
})
const
showKeFuMessageList
=
computed
(()
=>
!
isEmpty
(
conversation
.
value
))
// 是否显示聊天区域
const
showKeFuMessageList
=
computed
(()
=>
!
isEmpty
(
conversation
.
value
))
// 是否显示聊天区域
const
skipGetMessageList
=
computed
(()
=>
{
const
skipGetMessageList
=
ref
(
false
)
// 跳过消息获取
// 已加载到最后一页的话则不触发新的消息获取
return
total
.
value
>
0
&&
Math
.
ceil
(
total
.
value
/
queryParams
.
pageSize
)
===
queryParams
.
pageNo
})
// 跳过消息获取
/** 处理表情选择 */
/** 处理表情选择 */
const
handleEmojiSelect
=
(
item
:
Emoji
)
=>
{
const
handleEmojiSelect
=
(
item
:
Emoji
)
=>
{
...
@@ -256,13 +267,17 @@ const handleSendPicture = async (picUrl: string) => {
...
@@ -256,13 +267,17 @@ const handleSendPicture = async (picUrl: string) => {
const
msg
=
{
const
msg
=
{
conversationId
:
conversation
.
value
.
id
,
conversationId
:
conversation
.
value
.
id
,
contentType
:
KeFuMessageContentTypeEnum
.
IMAGE
,
contentType
:
KeFuMessageContentTypeEnum
.
IMAGE
,
content
:
picUrl
content
:
JSON
.
stringify
({
picUrl
})
}
}
await
sendMessage
(
msg
)
await
sendMessage
(
msg
)
}
}
/** 发送文本消息 */
/** 发送文本消息 */
const
handleSendMessage
=
async
()
=>
{
const
handleSendMessage
=
async
(
event
:
any
)
=>
{
// shift 不发送
if
(
event
.
shiftKey
)
{
return
}
// 1. 校验消息是否为空
// 1. 校验消息是否为空
if
(
isEmpty
(
unref
(
message
.
value
)))
{
if
(
isEmpty
(
unref
(
message
.
value
)))
{
messageTool
.
notifyWarning
(
'请输入消息后再发送哦!'
)
messageTool
.
notifyWarning
(
'请输入消息后再发送哦!'
)
...
@@ -272,7 +287,7 @@ const handleSendMessage = async () => {
...
@@ -272,7 +287,7 @@ const handleSendMessage = async () => {
const
msg
=
{
const
msg
=
{
conversationId
:
conversation
.
value
.
id
,
conversationId
:
conversation
.
value
.
id
,
contentType
:
KeFuMessageContentTypeEnum
.
TEXT
,
contentType
:
KeFuMessageContentTypeEnum
.
TEXT
,
content
:
message
.
value
content
:
JSON
.
stringify
({
text
:
message
.
value
})
}
}
await
sendMessage
(
msg
)
await
sendMessage
(
msg
)
}
}
...
@@ -284,6 +299,8 @@ const sendMessage = async (msg: any) => {
...
@@ -284,6 +299,8 @@ const sendMessage = async (msg: any) => {
message
.
value
=
''
message
.
value
=
''
// 加载消息列表
// 加载消息列表
await
refreshMessageList
()
await
refreshMessageList
()
// 更新会话缓存
await
kefuStore
.
updateConversation
(
conversation
.
value
.
id
)
}
}
/** 滚动到底部 */
/** 滚动到底部 */
...
@@ -333,8 +350,6 @@ const handleOldMessage = async () => {
...
@@ -333,8 +350,6 @@ const handleOldMessage = async () => {
return
return
}
}
loadHistory
.
value
=
true
loadHistory
.
value
=
true
// 加载消息列表
queryParams
.
pageNo
+=
1
await
getMessageList
()
await
getMessageList
()
// 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
// 等页面加载完后,获得上一页最后一条消息的位置,控制滚动到它所在位置
scrollbarRef
.
value
!
.
setScrollTop
(
innerRef
.
value
!
.
clientHeight
-
oldPageHeight
)
scrollbarRef
.
value
!
.
setScrollTop
(
innerRef
.
value
!
.
clientHeight
-
oldPageHeight
)
...
@@ -357,14 +372,51 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
...
@@ -357,14 +372,51 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
<
style
lang=
"scss"
scoped
>
<
style
lang=
"scss"
scoped
>
.kefu
{
.kefu
{
&-title
{
background-color
:
#f5f5f5
;
border-bottom
:
#e4e0e0
solid
1px
;
position
:
relative
;
height
:
60px
;
width
:
calc
(
100%
-
300px
-
260px
);
line-height
:
60px
;
&::after
{
content
:
''
;
position
:
absolute
;
top
:
0
;
left
:
0
;
width
:
1px
;
/* 实际宽度 */
height
:
100%
;
background-color
:
var
(
--el-border-color
);
transform
:
scaleX
(
0.3
);
/* 缩小宽度 */
}
.kefu-header
{
background-color
:
#f5f5f5
;
position
:
relative
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
&::before
{
content
:
''
;
position
:
absolute
;
bottom
:
0
;
left
:
0
;
width
:
100%
;
height
:
1px
;
/* 初始宽度 */
background-color
:
var
(
--el-border-color
);
transform
:
scaleY
(
0.3
);
/* 缩小视觉高度 */
}
&
-title
{
font-size
:
18px
;
font-weight
:
bold
;
}
}
}
&
-content
{
&
-content
{
margin
:
0
;
padding
:
10px
;
position
:
relative
;
position
:
relative
;
height
:
100%
;
width
:
100%
;
.newMessageTip
{
.newMessageTip
{
position
:
absolute
;
position
:
absolute
;
...
@@ -381,21 +433,12 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
...
@@ -381,21 +433,12 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
justify-content
:
flex-start
;
justify-content
:
flex-start
;
.kefu-message
{
.kefu-message
{
margin-left
:
20px
;
background-color
:
#fff
;
position
:
relative
;
margin-left
:
10px
;
margin-top
:
3px
;
&::before
{
border-top-right-radius
:
10px
;
content
:
''
;
border-bottom-right-radius
:
10px
;
width
:
10px
;
border-bottom-left-radius
:
10px
;
height
:
10px
;
left
:
-19px
;
top
:
calc
(
50%
-
10px
);
position
:
absolute
;
border-left
:
5px
solid
transparent
;
border-bottom
:
5px
solid
transparent
;
border-top
:
5px
solid
transparent
;
border-right
:
5px
solid
var
(
--app-content-bg-color
);
}
}
}
}
}
...
@@ -403,37 +446,25 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
...
@@ -403,37 +446,25 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
justify-content
:
flex-end
;
justify-content
:
flex-end
;
.kefu-message
{
.kefu-message
{
margin-right
:
20px
;
background-color
:
rgb
(
206
,
223
,
255
);
position
:
relative
;
margin-right
:
10px
;
margin-top
:
3px
;
&::after
{
border-top-left-radius
:
10px
;
content
:
''
;
border-bottom-right-radius
:
10px
;
width
:
10px
;
border-bottom-left-radius
:
10px
;
height
:
10px
;
right
:
-19px
;
top
:
calc
(
50%
-
10px
);
position
:
absolute
;
border-left
:
5px
solid
var
(
--app-content-bg-color
);
border-bottom
:
5px
solid
transparent
;
border-top
:
5px
solid
transparent
;
border-right
:
5px
solid
transparent
;
}
}
}
}
}
//
消息气泡
//
消息气泡
.kefu-message
{
.kefu-message
{
color
:
#a9a9a9
;
color
:
#414141
;
border-radius
:
5px
;
font-weight
:
500
;
box-shadow
:
3px
3px
5px
rgba
(
220
,
220
,
220
,
0.1
);
padding
:
5px
10px
;
padding
:
5px
10px
;
width
:
auto
;
width
:
auto
;
max-width
:
50%
;
max-width
:
50%
;
text-align
:
left
;
//
text-align
:
left
;
display
:
inline-block
!important
;
//
display
:
inline-block
!important
;
position
:
relative
;
//
word-break
:
break-all
;
word-break
:
break-all
;
background-color
:
var
(
--app-content-bg-color
);
transition
:
all
0.2s
;
transition
:
all
0.2s
;
&:hover
{
&:hover
{
...
@@ -444,24 +475,51 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
...
@@ -444,24 +475,51 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
.date-message
,
.date-message
,
.system-message
{
.system-message
{
width
:
fit-content
;
width
:
fit-content
;
border-radius
:
12
rpx
;
background-color
:
rgba
(
0
,
0
,
0
,
0.1
);
padding
:
8
rpx
16
rpx
;
border-radius
:
8px
;
margin-bottom
:
16
rpx
;
padding
:
0
5px
;
//
background-color
:
#e8e8e8
;
color
:
#fff
;
color
:
#999
;
font-size
:
10px
;
font-size
:
24
rpx
;
}
}
}
}
.chat-tools
{
.kefu-footer
{
width
:
100%
;
position
:
relative
;
border
:
var
(
--el-border-color
)
solid
1px
;
display
:
flex
;
border-radius
:
10px
;
flex-direction
:
column
;
height
:
44px
;
height
:
auto
;
margin
:
0
;
padding
:
0
;
&::before
{
content
:
''
;
position
:
absolute
;
top
:
0
;
left
:
0
;
width
:
100%
;
height
:
1px
;
/* 初始宽度 */
background-color
:
var
(
--el-border-color
);
transform
:
scaleY
(
0.3
);
/* 缩小视觉高度 */
}
.chat-tools
{
width
:
100%
;
height
:
44px
;
}
}
}
::v-deep
(
textarea
)
{
::v-deep
(
textarea
)
{
resize
:
none
;
resize
:
none
;
background-color
:
#f5f5f5
;
}
:deep
(
.el-input__wrapper
)
{
box-shadow
:
none
!important
;
border-radius
:
0
;
}
::v-deep
(
.el-textarea__inner
)
{
box-shadow
:
none
!important
;
}
}
}
}
</
style
>
</
style
>
src/views/mall/promotion/kefu/components/history/MemberBrowsingHistory.vue
deleted
100644 → 0
View file @
22199c64
<!-- 目录是不是叫 member 好点。然后这个组件是 MemberInfo,里面有浏览足迹 -->
<
template
>
<div
v-show=
"!isEmpty(conversation)"
class=
"kefu"
>
<div
class=
"header-title h-60px flex justify-center items-center"
>
他的足迹
</div>
<el-tabs
v-model=
"activeName"
class=
"demo-tabs"
@
tab-click=
"handleClick"
>
<el-tab-pane
label=
"最近浏览"
name=
"a"
/>
<el-tab-pane
label=
"订单列表"
name=
"b"
/>
</el-tabs>
<div>
<el-scrollbar
ref=
"scrollbarRef"
always
height=
"calc(115vh - 400px)"
@
scroll=
"handleScroll"
>
<!-- 最近浏览 -->
<ProductBrowsingHistory
v-if=
"activeName === 'a'"
ref=
"productBrowsingHistoryRef"
/>
<!-- 订单列表 -->
<OrderBrowsingHistory
v-if=
"activeName === 'b'"
ref=
"orderBrowsingHistoryRef"
/>
</el-scrollbar>
</div>
</div>
<el-empty
v-show=
"isEmpty(conversation)"
description=
"请选择左侧的一个会话后开始"
/>
</
template
>
<
script
lang=
"ts"
setup
>
import
type
{
TabsPaneContext
}
from
'element-plus'
import
ProductBrowsingHistory
from
'./ProductBrowsingHistory.vue'
import
OrderBrowsingHistory
from
'./OrderBrowsingHistory.vue'
import
{
KeFuConversationRespVO
}
from
'@/api/mall/promotion/kefu/conversation'
import
{
isEmpty
}
from
'@/utils/is'
import
{
debounce
}
from
'lodash-es'
import
{
ElScrollbar
as
ElScrollbarType
}
from
'element-plus/es/components/scrollbar/index'
defineOptions
({
name
:
'MemberBrowsingHistory'
})
const
activeName
=
ref
(
'a'
)
/** tab 切换 */
const
productBrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
ProductBrowsingHistory
>>
()
const
orderBrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
OrderBrowsingHistory
>>
()
const
handleClick
=
async
(
tab
:
TabsPaneContext
)
=>
{
activeName
.
value
=
tab
.
paneName
as
string
await
nextTick
()
await
getHistoryList
()
}
/** 获得历史数据 */
// TODO @puhui:不要用 a、b 哈。就订单列表、浏览列表这种噶
const
getHistoryList
=
async
()
=>
{
switch
(
activeName
.
value
)
{
case
'a'
:
await
productBrowsingHistoryRef
.
value
?.
getHistoryList
(
conversation
.
value
)
break
case
'b'
:
await
orderBrowsingHistoryRef
.
value
?.
getHistoryList
(
conversation
.
value
)
break
default
:
break
}
}
/** 加载下一页数据 */
const
loadMore
=
async
()
=>
{
switch
(
activeName
.
value
)
{
case
'a'
:
await
productBrowsingHistoryRef
.
value
?.
loadMore
()
break
case
'b'
:
await
orderBrowsingHistoryRef
.
value
?.
loadMore
()
break
default
:
break
}
}
/** 浏览历史初始化 */
const
conversation
=
ref
<
KeFuConversationRespVO
>
({}
as
KeFuConversationRespVO
)
// 用户会话
const
initHistory
=
async
(
val
:
KeFuConversationRespVO
)
=>
{
activeName
.
value
=
'a'
conversation
.
value
=
val
await
nextTick
()
await
getHistoryList
()
}
defineExpose
({
initHistory
})
/** 处理消息列表滚动事件(debounce 限流) */
const
scrollbarRef
=
ref
<
InstanceType
<
typeof
ElScrollbarType
>>
()
const
handleScroll
=
debounce
(()
=>
{
const
wrap
=
scrollbarRef
.
value
?.
wrapRef
// 触底重置
if
(
Math
.
abs
(
wrap
!
.
scrollHeight
-
wrap
!
.
clientHeight
-
wrap
!
.
scrollTop
)
<
1
)
{
loadMore
()
}
},
200
)
</
script
>
<
style
lang=
"scss"
scoped
>
.header-title
{
border-bottom
:
#e4e0e0
solid
1px
;
}
</
style
>
src/views/mall/promotion/kefu/components/index.ts
View file @
6eb013b2
import
KeFuConversationList
from
'./KeFuConversationList.vue'
import
KeFuConversationList
from
'./KeFuConversationList.vue'
import
KeFuMessageList
from
'./KeFuMessageList.vue'
import
KeFuMessageList
from
'./KeFuMessageList.vue'
import
Member
BrowsingHistory
from
'./history/MemberBrowsingHistory
.vue'
import
Member
Info
from
'./member/MemberInfo
.vue'
export
{
KeFuConversationList
,
KeFuMessageList
,
Member
BrowsingHistory
}
export
{
KeFuConversationList
,
KeFuMessageList
,
Member
Info
}
src/views/mall/promotion/kefu/components/member/MemberInfo.vue
0 → 100644
View file @
6eb013b2
<!-- 右侧信息:会员信息 + 最近浏览 + 交易订单 -->
<
template
>
<el-container
class=
"kefu"
>
<el-header
class=
"kefu-header"
>
<div
:class=
"
{ 'kefu-header-item-activation': tabActivation('会员信息') }"
class="kefu-header-item cursor-pointer flex items-center justify-center"
@click="handleClick('会员信息')"
>
会员信息
</div>
<div
:class=
"
{ 'kefu-header-item-activation': tabActivation('最近浏览') }"
class="kefu-header-item cursor-pointer flex items-center justify-center"
@click="handleClick('最近浏览')"
>
最近浏览
</div>
<div
:class=
"
{ 'kefu-header-item-activation': tabActivation('交易订单') }"
class="kefu-header-item cursor-pointer flex items-center justify-center"
@click="handleClick('交易订单')"
>
交易订单
</div>
</el-header>
<el-main
class=
"kefu-content p-10px!"
>
<div
v-if=
"!isEmpty(conversation)"
v-loading=
"loading"
>
<!-- 基本信息 -->
<UserBasicInfo
v-if=
"activeTab === '会员信息'"
:user=
"user"
mode=
"kefu"
>
<template
#
header
>
<CardTitle
title=
"基本信息"
/>
</
template
>
</UserBasicInfo>
<!-- 账户信息 -->
<el-card
v-if=
"activeTab === '会员信息'"
class=
"h-full mt-10px"
shadow=
"never"
>
<
template
#
header
>
<CardTitle
title=
"账户信息"
/>
</
template
>
<UserAccountInfo
:column=
"1"
:user=
"user"
:wallet=
"wallet"
/>
</el-card>
</div>
<div
v-show=
"!isEmpty(conversation)"
>
<el-scrollbar
ref=
"scrollbarRef"
always
@
scroll=
"handleScroll"
>
<!-- 最近浏览 -->
<ProductBrowsingHistory
v-if=
"activeTab === '最近浏览'"
ref=
"productBrowsingHistoryRef"
/>
<!-- 交易订单 -->
<OrderBrowsingHistory
v-if=
"activeTab === '交易订单'"
ref=
"orderBrowsingHistoryRef"
/>
</el-scrollbar>
</div>
<el-empty
v-show=
"isEmpty(conversation)"
description=
"请选择左侧的一个会话后开始"
/>
</el-main>
</el-container>
</template>
<
script
lang=
"ts"
setup
>
import
ProductBrowsingHistory
from
'./ProductBrowsingHistory.vue'
import
OrderBrowsingHistory
from
'./OrderBrowsingHistory.vue'
import
{
KeFuConversationRespVO
}
from
'@/api/mall/promotion/kefu/conversation'
import
{
isEmpty
}
from
'@/utils/is'
import
{
debounce
}
from
'lodash-es'
import
{
ElScrollbar
as
ElScrollbarType
}
from
'element-plus/es/components/scrollbar/index'
import
{
CardTitle
}
from
'@/components/Card'
import
UserBasicInfo
from
'@/views/member/user/detail/UserBasicInfo.vue'
import
UserAccountInfo
from
'@/views/member/user/detail/UserAccountInfo.vue'
import
*
as
UserApi
from
'@/api/member/user'
import
*
as
WalletApi
from
'@/api/pay/wallet/balance'
defineOptions
({
name
:
'MemberBrowsingHistory'
})
const
activeTab
=
ref
(
'会员信息'
)
const
tabActivation
=
computed
(()
=>
(
tab
:
string
)
=>
activeTab
.
value
===
tab
)
/** tab 切换 */
const
productBrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
ProductBrowsingHistory
>>
()
const
orderBrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
OrderBrowsingHistory
>>
()
const
handleClick
=
async
(
tab
:
string
)
=>
{
activeTab
.
value
=
tab
await
nextTick
()
await
getHistoryList
()
}
/** 获得历史数据 */
const
getHistoryList
=
async
()
=>
{
switch
(
activeTab
.
value
)
{
case
'会员信息'
:
await
getUserData
()
await
getUserWallet
()
break
case
'最近浏览'
:
await
productBrowsingHistoryRef
.
value
?.
getHistoryList
(
conversation
.
value
)
break
case
'交易订单'
:
await
orderBrowsingHistoryRef
.
value
?.
getHistoryList
(
conversation
.
value
)
break
default
:
break
}
}
/** 加载下一页数据 */
const
loadMore
=
async
()
=>
{
switch
(
activeTab
.
value
)
{
case
'会员信息'
:
break
case
'最近浏览'
:
await
productBrowsingHistoryRef
.
value
?.
loadMore
()
break
case
'交易订单'
:
await
orderBrowsingHistoryRef
.
value
?.
loadMore
()
break
default
:
break
}
}
/** 浏览历史初始化 */
const
conversation
=
ref
<
KeFuConversationRespVO
>
({}
as
KeFuConversationRespVO
)
// 用户会话
const
initHistory
=
async
(
val
:
KeFuConversationRespVO
)
=>
{
activeTab
.
value
=
'会员信息'
conversation
.
value
=
val
await
nextTick
()
await
getHistoryList
()
}
defineExpose
({
initHistory
})
/** 处理消息列表滚动事件(debounce 限流) */
const
scrollbarRef
=
ref
<
InstanceType
<
typeof
ElScrollbarType
>>
()
const
handleScroll
=
debounce
(()
=>
{
const
wrap
=
scrollbarRef
.
value
?.
wrapRef
// 触底重置
if
(
Math
.
abs
(
wrap
!
.
scrollHeight
-
wrap
!
.
clientHeight
-
wrap
!
.
scrollTop
)
<
1
)
{
loadMore
()
}
},
200
)
/** 查询用户钱包信息 */
const
WALLET_INIT_DATA
=
{
balance
:
0
,
totalExpense
:
0
,
totalRecharge
:
0
}
as
WalletApi
.
WalletVO
// 钱包初始化数据
const
wallet
=
ref
<
WalletApi
.
WalletVO
>
(
WALLET_INIT_DATA
)
// 钱包信息
const
getUserWallet
=
async
()
=>
{
if
(
!
conversation
.
value
.
userId
)
{
wallet
.
value
=
WALLET_INIT_DATA
return
}
wallet
.
value
=
(
await
WalletApi
.
getWallet
({
userId
:
conversation
.
value
.
userId
}))
||
WALLET_INIT_DATA
}
/** 获得用户 */
const
loading
=
ref
(
true
)
// 加载中
const
user
=
ref
<
UserApi
.
UserVO
>
({}
as
UserApi
.
UserVO
)
const
getUserData
=
async
()
=>
{
loading
.
value
=
true
try
{
user
.
value
=
await
UserApi
.
getUser
(
conversation
.
value
.
userId
)
}
finally
{
loading
.
value
=
false
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.kefu
{
position
:
relative
;
width
:
300px
!important
;
background-color
:
#f5f5f5
;
&::after
{
content
:
''
;
position
:
absolute
;
top
:
0
;
left
:
0
;
width
:
1px
;
/* 实际宽度 */
height
:
100%
;
background-color
:
var
(
--el-border-color
);
transform
:
scaleX
(
0.3
);
/* 缩小宽度 */
}
&
-header
{
background-color
:
#f5f5f5
;
position
:
relative
;
display
:
flex
;
align-items
:
center
;
justify-content
:
space-around
;
&::before
{
content
:
''
;
position
:
absolute
;
bottom
:
0
;
left
:
0
;
width
:
100%
;
height
:
1px
;
/* 初始宽度 */
background-color
:
var
(
--el-border-color
);
transform
:
scaleY
(
0.3
);
/* 缩小视觉高度 */
}
&
-title
{
font-size
:
18px
;
font-weight
:
bold
;
}
&
-item
{
height
:
100%
;
width
:
100%
;
position
:
relative
;
&
-activation
::
before
{
content
:
''
;
position
:
absolute
;
/* 绝对定位 */
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
/* 覆盖整个元素 */
border-bottom
:
2px
solid
rgba
(
128
,
128
,
128
,
0.5
);
/* 边框样式 */
pointer-events
:
none
;
/* 确保点击事件不会被伪元素拦截 */
}
&
:hover::before
{
content
:
''
;
position
:
absolute
;
/* 绝对定位 */
top
:
0
;
left
:
0
;
right
:
0
;
bottom
:
0
;
/* 覆盖整个元素 */
border-bottom
:
2px
solid
rgba
(
128
,
128
,
128
,
0.5
);
/* 边框样式 */
pointer-events
:
none
;
/* 确保点击事件不会被伪元素拦截 */
}
}
}
&
-content
{
margin
:
0
;
padding
:
0
;
position
:
relative
;
height
:
100%
;
width
:
100%
;
}
&
-tabs
{
height
:
100%
;
width
:
100%
;
}
}
.header-title
{
border-bottom
:
#e4e0e0
solid
1px
;
}
</
style
>
src/views/mall/promotion/kefu/components/
history
/OrderBrowsingHistory.vue
→
src/views/mall/promotion/kefu/components/
member
/OrderBrowsingHistory.vue
View file @
6eb013b2
File moved
src/views/mall/promotion/kefu/components/
history
/ProductBrowsingHistory.vue
→
src/views/mall/promotion/kefu/components/
member
/ProductBrowsingHistory.vue
View file @
6eb013b2
<
template
>
<
template
>
<ProductItem
<ProductItem
v-for=
"item in list"
v-for=
"item in list"
:spu-id=
"item.spuId"
:key=
"item.id"
:key=
"item.id"
:picUrl=
"item.picUrl"
:picUrl=
"item.picUrl"
:price=
"item.price"
:price=
"item.price"
:skuText=
"item.introduction"
:sales-count=
"item.salesCount"
:spu-id=
"item.spuId"
:stock=
"item.stock"
:title=
"item.spuName"
:title=
"item.spuName"
:titleWidth=
"400"
class=
"mb-10px"
class=
"mb-10px"
priceColor=
"#FF3000"
/>
/>
</
template
>
</
template
>
...
...
src/views/mall/promotion/kefu/components/message/OrderItem.vue
View file @
6eb013b2
...
@@ -14,11 +14,11 @@
...
@@ -14,11 +14,11 @@
</div>
</div>
<div
v-for=
"item in getMessageContent.items"
:key=
"item.id"
class=
"border-bottom"
>
<div
v-for=
"item in getMessageContent.items"
:key=
"item.id"
class=
"border-bottom"
>
<ProductItem
<ProductItem
:spu-id=
"item.spuId"
:num=
"item.count"
:num=
"item.count"
:picUrl=
"item.picUrl"
:picUrl=
"item.picUrl"
:price=
"item.price"
:price=
"item.price"
:skuText=
"item.properties.map((property: any) => property.valueName).join(' ')"
:skuText=
"item.properties.map((property: any) => property.valueName).join(' ')"
:spu-id=
"item.spuId"
:title=
"item.spuName"
:title=
"item.spuName"
/>
/>
</div>
</div>
...
@@ -112,14 +112,14 @@ function formatOrderStatus(order: any) {
...
@@ -112,14 +112,14 @@ function formatOrderStatus(order: any) {
border-radius
:
10px
;
border-radius
:
10px
;
padding
:
10px
;
padding
:
10px
;
border
:
1px
var
(
--el-border-color
)
solid
;
border
:
1px
var
(
--el-border-color
)
solid
;
background-color
:
var
(
--app-content-bg-color
);
background-color
:
#fff
;
//
透明色,暗黑模式下也能体现
.order-card-header
{
.order-card-header
{
height
:
28px
;
height
:
28px
;
font-weight
:
bold
;
.order-no
{
.order-no
{
font-size
:
12px
;
font-size
:
13px
;
font-weight
:
500
;
span
{
span
{
&:hover
{
&:hover
{
...
@@ -128,27 +128,29 @@ function formatOrderStatus(order: any) {
...
@@ -128,27 +128,29 @@ function formatOrderStatus(order: any) {
}
}
}
}
}
}
.order-state
{
font-size
:
13px
;
}
}
}
.pay-box
{
.pay-box
{
padding-top
:
10px
;
padding-top
:
10px
;
font-weight
:
bold
;
.discounts-title
{
.discounts-title
{
font-size
:
16px
;
font-size
:
16px
;
line-height
:
normal
;
line-height
:
normal
;
color
:
#999999
;
}
}
.discounts-money
{
.discounts-money
{
font-size
:
16px
;
font-size
:
16px
;
line-height
:
normal
;
line-height
:
normal
;
color
:
#999
;
font-family
:
OPPOSANS
;
font-family
:
OPPOSANS
;
}
}
.pay-color
{
.pay-color
{
font-size
:
13px
;
font-size
:
13px
;
color
:
var
(
--left-menu-text-color
);
}
}
}
}
}
}
...
...
src/views/mall/promotion/kefu/components/message/ProductItem.vue
View file @
6eb013b2
<
template
>
<
template
>
<div
@
click
.
stop=
"openDetail(props.spuId)"
style=
"cursor: pointer;"
>
<div
class=
"product-warp"
style=
"cursor: pointer"
@
click
.
stop=
"openDetail(spuId)"
>
<div>
<!-- 左侧商品图片-->
<slot
name=
"top"
></slot>
<div
class=
"product-warp-left mr-24px"
>
<el-image
:initial-index=
"0"
:preview-src-list=
"[picUrl]"
:src=
"picUrl"
class=
"product-warp-left-img"
fit=
"contain"
preview-teleported
@
click
.
stop
/>
</div>
</div>
<div
<!-- 右侧商品信息 -->
:style=
"[
{ borderRadius: radius + 'px', marginBottom: marginBottom + 'px' }]"
<div
class=
"product-warp-right"
>
class="ss-order-card-warp flex items-stretch justify-between bg-white"
<div
class=
"description"
>
{{
title
}}
</div>
>
<div
class=
"my-5px"
>
<div
class=
"img-box mr-24px"
>
<span
class=
"mr-20px"
>
库存:
{{
stock
||
0
}}
</span>
<el-image
<span>
销量:
{{
salesCount
||
0
}}
</span>
:initial-index=
"0"
:preview-src-list=
"[picUrl]"
:src=
"picUrl"
class=
"order-img"
fit=
"contain"
preview-teleported
@
click
.
stop
/>
</div>
</div>
<div
<div
class=
"flex justify-between items-center"
>
:style=
"[
{ width: titleWidth ? titleWidth + 'px' : '' }]"
<span
class=
"price"
>
¥
{{
fenToYuan
(
price
)
}}
</span>
class="box-right flex flex-col justify-between"
<el-button
size=
"small"
text
type=
"primary"
>
详情
</el-button>
>
<div
v-if=
"title"
class=
"title-text ss-line-2"
>
{{
title
}}
</div>
<div
v-if=
"skuString"
class=
"spec-text mt-8px mb-12px"
>
{{
skuString
}}
</div>
<div
class=
"groupon-box"
>
<slot
name=
"groupon"
></slot>
</div>
<div
class=
"flex"
>
<div
class=
"flex items-center"
>
<div
v-if=
"price && Number(price) > 0"
:style=
"[
{ color: priceColor }]"
class="price-text flex items-center"
>
¥
{{
fenToYuan
(
price
)
}}
</div>
<div
v-if=
"num"
class=
"total-text flex items-center"
>
x
{{
num
}}
</div>
<slot
name=
"priceSuffix"
></slot>
</div>
</div>
<div
class=
"tool-box"
>
<slot
name=
"tool"
></slot>
</div>
<div>
<slot
name=
"rightBottom"
></slot>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
...
@@ -57,7 +33,7 @@ import { fenToYuan } from '@/utils'
...
@@ -57,7 +33,7 @@ import { fenToYuan } from '@/utils'
const
{
push
}
=
useRouter
()
const
{
push
}
=
useRouter
()
defineOptions
({
name
:
'ProductItem'
})
defineOptions
({
name
:
'ProductItem'
})
const
props
=
defineProps
({
defineProps
({
spuId
:
{
spuId
:
{
type
:
Number
,
type
:
Number
,
default
:
0
default
:
0
...
@@ -70,134 +46,70 @@ const props = defineProps({
...
@@ -70,134 +46,70 @@ const props = defineProps({
type
:
String
,
type
:
String
,
default
:
''
default
:
''
},
},
titleWidth
:
{
type
:
Number
,
default
:
0
},
skuText
:
{
type
:
[
String
,
Array
],
default
:
''
},
price
:
{
price
:
{
type
:
[
String
,
Number
],
type
:
[
String
,
Number
],
default
:
''
default
:
''
},
},
priceColor
:
{
salesCount
:
{
type
:
[
String
],
default
:
''
},
num
:
{
type
:
[
String
,
Number
],
type
:
[
String
,
Number
],
default
:
0
},
score
:
{
type
:
[
String
,
Number
],
default
:
''
},
radius
:
{
type
:
[
String
],
default
:
''
default
:
''
},
},
marginBottom
:
{
stock
:
{
type
:
[
String
],
type
:
[
String
,
Number
],
default
:
''
default
:
''
}
}
})
})
/** SKU 展示字符串 */
const
skuString
=
computed
(()
=>
{
if
(
!
props
.
skuText
)
{
return
''
}
if
(
typeof
props
.
skuText
===
'object'
)
{
return
props
.
skuText
.
join
(
','
)
}
return
props
.
skuText
})
/** 查看商品详情 */
/** 查看商品详情 */
const
openDetail
=
(
spuId
:
number
)
=>
{
const
openDetail
=
(
spuId
:
number
)
=>
{
console
.
log
(
props
.
spuId
)
push
({
name
:
'ProductSpuDetail'
,
params
:
{
id
:
spuId
}
})
push
({
name
:
'ProductSpuDetail'
,
params
:
{
id
:
spuId
}
})
}
}
</
script
>
</
script
>
<
style
lang=
"scss"
scoped
>
<
style
lang=
"scss"
scoped
>
.ss-order-card-warp
{
.button
{
padding
:
20px
;
background-color
:
#007bff
;
border-radius
:
10px
;
color
:
white
;
border
:
1px
var
(
--el-border-color
)
solid
;
border
:
none
;
background-color
:
var
(
--app-content-bg-color
);
padding
:
5px
10px
;
cursor
:
pointer
;
.img-box
{
}
width
:
80px
;
height
:
80px
;
border-radius
:
10px
;
overflow
:
hidden
;
.order-img
{
.product-warp
{
width
:
80px
;
width
:
100%
;
height
:
80px
;
background-color
:
#fff
;
border-radius
:
8px
;
display
:
flex
;
align-items
:
center
;
padding
:
10px
;
&-left
{
width
:
70px
;
&-img
{
width
:
100%
;
height
:
100%
;
border-radius
:
8px
;
}
}
}
}
.box
-right
{
&
-right
{
flex
:
1
;
flex
:
1
;
position
:
relative
;
.tool-box
{
.description
{
position
:
absolute
;
width
:
100%
;
right
:
0
;
font-size
:
16px
;
bottom
:
-10px
;
font-weight
:
bold
;
display
:
-webkit-box
;
-webkit-line-clamp
:
1
;
/* 显示一行 */
-webkit-box-orient
:
vertical
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
}
}
.title-text
{
font-size
:
13px
;
font-weight
:
500
;
line-height
:
20px
;
}
.spec-text
{
font-size
:
10px
;
font-weight
:
400
;
color
:
#999999
;
min-width
:
0
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
display
:
-webkit-box
;
-webkit-line-clamp
:
1
;
-webkit-box-orient
:
vertical
;
}
.price-text
{
font-size
:
11px
;
font-weight
:
500
;
font-family
:
OPPOSANS
;
}
.total-text
{
font-size
:
10px
;
font-weight
:
400
;
line-height
:
16px
;
color
:
#999999
;
margin-left
:
8px
;
}
}
.ss-line
{
.price
{
min-width
:
0
;
color
:
#ff3000
;
overflow
:
hidden
;
}
text-overflow
:
ellipsis
;
display
:
-webkit-box
;
-webkit-box-orient
:
vertical
;
&-1
{
-webkit-line-clamp
:
1
;
}
&
-2
{
-webkit-line-clamp
:
2
;
}
}
}
}
</
style
>
</
style
>
src/views/mall/promotion/kefu/components/tools/emoji.ts
View file @
6eb013b2
...
@@ -93,7 +93,7 @@ export const useEmoji = () => {
...
@@ -93,7 +93,7 @@ export const useEmoji = () => {
const
emojiFile
=
getEmojiFileByName
(
item
)
const
emojiFile
=
getEmojiFileByName
(
item
)
newData
=
newData
.
replace
(
newData
=
newData
.
replace
(
item
,
item
,
`<img
class="chat-img" style="width: 24px;height: 24px;margin: 0 3px
;" src="
${
emojiFile
}
" alt=""/>`
`<img
style="width: 20px;height: 20px;margin:0 1px 3px 1px;vertical-align: middle
;" src="
${
emojiFile
}
" alt=""/>`
)
)
})
})
}
}
...
...
src/views/mall/promotion/kefu/index.vue
View file @
6eb013b2
<
template
>
<
template
>
<el-
row
:gutter=
"10
"
>
<el-
container
class=
"kefu-layout
"
>
<!-- 会话列表 -->
<!-- 会话列表 -->
<el-col
:span=
"6"
>
<KeFuConversationList
ref=
"keFuConversationRef"
@
change=
"handleChange"
/>
<ContentWrap>
<KeFuConversationList
ref=
"keFuConversationRef"
@
change=
"handleChange"
/>
</ContentWrap>
</el-col>
<!-- 会话详情(选中会话的消息列表) -->
<!-- 会话详情(选中会话的消息列表) -->
<el-col
:span=
"12"
>
<KeFuMessageList
ref=
"keFuChatBoxRef"
/>
<ContentWrap>
<!-- 会员信息(选中会话的会员信息) -->
<KeFuMessageList
ref=
"keFuChatBoxRef"
@
change=
"getConversationList"
/>
<MemberInfo
ref=
"memberInfoRef"
/>
</ContentWrap>
</el-container>
</el-col>
<!-- 会员足迹(选中会话的会员足迹) -->
<el-col
:span=
"6"
>
<ContentWrap>
<MemberBrowsingHistory
ref=
"memberBrowsingHistoryRef"
/>
</ContentWrap>
</el-col>
</el-row>
</
template
>
</
template
>
<
script
lang=
"ts"
setup
>
<
script
lang=
"ts"
setup
>
import
{
KeFuConversationList
,
KeFuMessageList
,
Member
BrowsingHistory
}
from
'./components'
import
{
KeFuConversationList
,
KeFuMessageList
,
Member
Info
}
from
'./components'
import
{
WebSocketMessageTypeConstants
}
from
'./components/tools/constants'
import
{
WebSocketMessageTypeConstants
}
from
'./components/tools/constants'
import
{
KeFuConversationRespVO
}
from
'@/api/mall/promotion/kefu/conversation'
import
{
KeFuConversationRespVO
}
from
'@/api/mall/promotion/kefu/conversation'
import
{
getRefreshToken
}
from
'@/utils/auth'
import
{
getRefreshToken
}
from
'@/utils/auth'
import
{
useWebSocket
}
from
'@vueuse/core'
import
{
useWebSocket
}
from
'@vueuse/core'
import
{
useMallKefuStore
}
from
'@/store/modules/mall/kefu'
import
{
jsonParse
}
from
'@/utils'
defineOptions
({
name
:
'KeFu'
})
defineOptions
({
name
:
'KeFu'
})
const
message
=
useMessage
()
// 消息弹窗
const
message
=
useMessage
()
// 消息弹窗
const
kefuStore
=
useMallKefuStore
()
// 客服缓存
// ======================= WebSocket start =======================
// ======================= WebSocket start =======================
const
server
=
ref
(
const
server
=
ref
(
...
@@ -55,7 +46,6 @@ watchEffect(() => {
...
@@ -55,7 +46,6 @@ watchEffect(() => {
if
(
data
.
value
===
'pong'
)
{
if
(
data
.
value
===
'pong'
)
{
return
return
}
}
// 2.1 解析 type 消息类型
// 2.1 解析 type 消息类型
const
jsonMessage
=
JSON
.
parse
(
data
.
value
)
const
jsonMessage
=
JSON
.
parse
(
data
.
value
)
const
type
=
jsonMessage
.
type
const
type
=
jsonMessage
.
type
...
@@ -65,41 +55,39 @@ watchEffect(() => {
...
@@ -65,41 +55,39 @@ watchEffect(() => {
}
}
// 2.2 消息类型:KEFU_MESSAGE_TYPE
// 2.2 消息类型:KEFU_MESSAGE_TYPE
if
(
type
===
WebSocketMessageTypeConstants
.
KEFU_MESSAGE_TYPE
)
{
if
(
type
===
WebSocketMessageTypeConstants
.
KEFU_MESSAGE_TYPE
)
{
const
message
=
JSON
.
parse
(
jsonMessage
.
content
)
// 刷新会话列表
// 刷新会话列表
// TODO @puhui999:不应该刷新列表,而是根据消息,本地 update 列表的数据;
kefuStore
.
updateConversation
(
message
.
conversationId
)
getConversationList
()
// 刷新消息列表
// 刷新消息列表
keFuChatBoxRef
.
value
?.
refreshMessageList
(
JSON
.
parse
(
jsonMessage
.
content
)
)
keFuChatBoxRef
.
value
?.
refreshMessageList
(
message
)
return
return
}
}
// 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
// 2.3 消息类型:KEFU_MESSAGE_ADMIN_READ
if
(
type
===
WebSocketMessageTypeConstants
.
KEFU_MESSAGE_ADMIN_READ
)
{
if
(
type
===
WebSocketMessageTypeConstants
.
KEFU_MESSAGE_ADMIN_READ
)
{
// 刷新会话列表
// 更新会话已读
// TODO @puhui999:不应该刷新列表,而是根据消息,本地 update 列表的数据;
kefuStore
.
updateConversationStatus
(
jsonParse
(
jsonMessage
.
content
))
getConversationList
()
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
error
)
console
.
error
(
error
)
}
}
})
})
// ======================= WebSocket end =======================
// ======================= WebSocket end =======================
/** 加载会话列表 */
const
keFuConversationRef
=
ref
<
InstanceType
<
typeof
KeFuConversationList
>>
()
const
getConversationList
=
()
=>
{
keFuConversationRef
.
value
?.
getConversationList
()
}
/** 加载指定会话的消息列表 */
/** 加载指定会话的消息列表 */
const
keFuChatBoxRef
=
ref
<
InstanceType
<
typeof
KeFuMessageList
>>
()
const
keFuChatBoxRef
=
ref
<
InstanceType
<
typeof
KeFuMessageList
>>
()
const
member
BrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
MemberBrowsingHistory
>>
()
const
member
InfoRef
=
ref
<
InstanceType
<
typeof
MemberInfo
>>
()
const
handleChange
=
(
conversation
:
KeFuConversationRespVO
)
=>
{
const
handleChange
=
(
conversation
:
KeFuConversationRespVO
)
=>
{
keFuChatBoxRef
.
value
?.
getNewMessageList
(
conversation
)
keFuChatBoxRef
.
value
?.
getNewMessageList
(
conversation
)
member
BrowsingHistory
Ref
.
value
?.
initHistory
(
conversation
)
member
Info
Ref
.
value
?.
initHistory
(
conversation
)
}
}
const
keFuConversationRef
=
ref
<
InstanceType
<
typeof
KeFuConversationList
>>
()
/** 初始化 */
/** 初始化 */
onMounted
(()
=>
{
onMounted
(()
=>
{
getConversationList
()
/** 加载会话列表 */
kefuStore
.
setConversationList
().
then
(()
=>
{
keFuConversationRef
.
value
?.
calculationLastMessageTime
()
})
// 打开 websocket 连接
// 打开 websocket 连接
open
()
open
()
})
})
...
@@ -112,9 +100,13 @@ onBeforeUnmount(() => {
...
@@ -112,9 +100,13 @@ onBeforeUnmount(() => {
</
script
>
</
script
>
<
style
lang=
"scss"
>
<
style
lang=
"scss"
>
.kefu
{
.kefu-layout
{
height
:
calc
(
100vh
-
165px
);
position
:
absolute
;
overflow
:
auto
;
/* 确保内容可滚动 */
flex
:
1
;
top
:
0
;
left
:
0
;
height
:
100%
;
width
:
100%
;
}
}
/* 定义滚动条样式 */
/* 定义滚动条样式 */
...
...
src/views/member/user/detail/UserAccountInfo.vue
View file @
6eb013b2
<
template
>
<
template
>
<el-descriptions
:c
olumn=
"2
"
>
<el-descriptions
:c
lass=
"
{ 'kefu-descriptions': column === 1 }" :column="column
">
<el-descriptions-item>
<el-descriptions-item>
<template
#
label
>
<template
#
label
>
<descriptions-item-label
icon=
"svg-icon:member_level"
label=
" 等级 "
/>
<descriptions-item-label
icon=
"svg-icon:member_level"
label=
" 等级 "
/>
...
@@ -50,7 +50,9 @@ import * as UserApi from '@/api/member/user'
...
@@ -50,7 +50,9 @@ import * as UserApi from '@/api/member/user'
import
*
as
WalletApi
from
'@/api/pay/wallet/balance'
import
*
as
WalletApi
from
'@/api/pay/wallet/balance'
import
{
fenToYuan
}
from
'@/utils'
import
{
fenToYuan
}
from
'@/utils'
defineProps
<
{
user
:
UserApi
.
UserVO
;
wallet
:
WalletApi
.
WalletVO
}
>
()
// 用户信息
withDefaults
(
defineProps
<
{
user
:
UserApi
.
UserVO
;
wallet
:
WalletApi
.
WalletVO
;
column
?:
number
}
>
(),
{
column
:
2
})
// 用户信息
</
script
>
</
script
>
<
style
lang=
"scss"
scoped
>
<
style
lang=
"scss"
scoped
>
.cell-item
{
.cell-item
{
...
@@ -60,4 +62,23 @@ defineProps<{ user: UserApi.UserVO; wallet: WalletApi.WalletVO }>() // 用户信
...
@@ -60,4 +62,23 @@ defineProps<{ user: UserApi.UserVO; wallet: WalletApi.WalletVO }>() // 用户信
.cell-item
::after
{
.cell-item
::after
{
content
:
':'
;
content
:
':'
;
}
}
.kefu-descriptions
{
::v-deep(.el-descriptions__cell)
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
.el-descriptions__label
{
width
:
120px
;
display
:
block
;
text-align
:
left
;
}
.el-descriptions__content
{
flex
:
1
;
text-align
:
end
;
}
}
}
</
style
>
</
style
>
src/views/member/user/detail/UserBasicInfo.vue
View file @
6eb013b2
...
@@ -3,83 +3,163 @@
...
@@ -3,83 +3,163 @@
<template
#
header
>
<template
#
header
>
<slot
name=
"header"
></slot>
<slot
name=
"header"
></slot>
</
template
>
</
template
>
<el-row>
<el-row
v-if=
"mode === 'member'"
>
<el-col
:span=
"4"
>
<el-col
:span=
"4"
>
<ElAvatar
shape=
"square"
:size=
"140"
:src=
"user.avatar || undefined
"
/>
<ElAvatar
:size=
"140"
:src=
"user.avatar || undefined"
shape=
"square
"
/>
</el-col>
</el-col>
<el-col
:span=
"20"
>
<el-col
:span=
"20"
>
<el-descriptions
:column=
"2"
>
<el-descriptions
:column=
"2"
>
<el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"用户名"
icon=
"ep:user
"
/>
<descriptions-item-label
icon=
"ep:user"
label=
"用户名
"
/>
</
template
>
</
template
>
{{ user.name || '空' }}
{{ user.name || '空' }}
</el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"昵称"
icon=
"ep:user
"
/>
<descriptions-item-label
icon=
"ep:user"
label=
"昵称
"
/>
</
template
>
</
template
>
{{ user.nickname }}
{{ user.nickname }}
</el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item
label=
"手机号"
>
<el-descriptions-item
label=
"手机号"
>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"手机号"
icon=
"ep:phone
"
/>
<descriptions-item-label
icon=
"ep:phone"
label=
"手机号
"
/>
</
template
>
</
template
>
{{ user.mobile }}
{{ user.mobile }}
</el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"性别"
icon=
"fa:mars-double
"
/>
<descriptions-item-label
icon=
"fa:mars-double"
label=
"性别
"
/>
</
template
>
</
template
>
<dict-tag
:type=
"DICT_TYPE.SYSTEM_USER_SEX"
:value=
"user.sex"
/>
<dict-tag
:type=
"DICT_TYPE.SYSTEM_USER_SEX"
:value=
"user.sex"
/>
</el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"所在地"
icon=
"ep:location
"
/>
<descriptions-item-label
icon=
"ep:location"
label=
"所在地
"
/>
</
template
>
</
template
>
{{ user.areaName }}
{{ user.areaName }}
</el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"注册 IP"
icon=
"ep:position
"
/>
<descriptions-item-label
icon=
"ep:position"
label=
"注册 IP
"
/>
</
template
>
</
template
>
{{ user.registerIp }}
{{ user.registerIp }}
</el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"生日"
icon=
"fa:birthday-cake
"
/>
<descriptions-item-label
icon=
"fa:birthday-cake"
label=
"生日
"
/>
</
template
>
</
template
>
{{ user.birthday ? formatDate(user.birthday) : '空' }}
{{ user.birthday ? formatDate(user.birthday
as any
) : '空' }}
</el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"注册时间"
icon=
"ep:calendar
"
/>
<descriptions-item-label
icon=
"ep:calendar"
label=
"注册时间
"
/>
</
template
>
</
template
>
{{ user.createTime ? formatDate(user.createTime) : '空' }}
{{ user.createTime ? formatDate(user.createTime
as any
) : '空' }}
</el-descriptions-item>
</el-descriptions-item>
<el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<
template
#
label
>
<descriptions-item-label
label=
"最后登录时间"
icon=
"ep:calendar
"
/>
<descriptions-item-label
icon=
"ep:calendar"
label=
"最后登录时间
"
/>
</
template
>
</
template
>
{{ user.loginDate ? formatDate(user.loginDate) : '空' }}
{{ user.loginDate ? formatDate(user.loginDate
as any
) : '空' }}
</el-descriptions-item>
</el-descriptions-item>
</el-descriptions>
</el-descriptions>
</el-col>
</el-col>
</el-row>
</el-row>
<
template
v-if=
"mode === 'kefu'"
>
<ElAvatar
:size=
"140"
:src=
"user.avatar || undefined"
shape=
"square"
/>
<el-descriptions
:column=
"1"
class=
"kefu-descriptions"
>
<el-descriptions-item>
<template
#
label
>
<descriptions-item-label
icon=
"ep:user"
label=
"用户名"
/>
</
template
>
{{ user.name || '空' }}
</el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<descriptions-item-label
icon=
"ep:user"
label=
"昵称"
/>
</
template
>
{{ user.nickname }}
</el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<descriptions-item-label
icon=
"ep:phone"
label=
"手机号"
/>
</
template
>
{{ user.mobile }}
</el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<descriptions-item-label
icon=
"fa:mars-double"
label=
"性别"
/>
</
template
>
<dict-tag
:type=
"DICT_TYPE.SYSTEM_USER_SEX"
:value=
"user.sex"
/>
</el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<descriptions-item-label
icon=
"ep:location"
label=
"所在地"
/>
</
template
>
{{ user.areaName }}
</el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<descriptions-item-label
icon=
"ep:position"
label=
"注册 IP"
/>
</
template
>
{{ user.registerIp }}
</el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<descriptions-item-label
icon=
"fa:birthday-cake"
label=
"生日"
/>
</
template
>
{{ user.birthday ? formatDate(user.birthday as any) : '空' }}
</el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<descriptions-item-label
icon=
"ep:calendar"
label=
"注册时间"
/>
</
template
>
{{ user.createTime ? formatDate(user.createTime as any) : '空' }}
</el-descriptions-item>
<el-descriptions-item>
<
template
#
label
>
<descriptions-item-label
icon=
"ep:calendar"
label=
"最后登录时间"
/>
</
template
>
{{ user.loginDate ? formatDate(user.loginDate as any) : '空' }}
</el-descriptions-item>
</el-descriptions>
</template>
</el-card>
</el-card>
</template>
</template>
<
script
setup
lang=
"ts"
>
<
script
lang=
"ts"
setup
>
import
{
DICT_TYPE
}
from
'@/utils/dict'
import
{
DICT_TYPE
}
from
'@/utils/dict'
import
{
formatDate
}
from
'@/utils/formatTime'
import
{
formatDate
}
from
'@/utils/formatTime'
import
*
as
UserApi
from
'@/api/member/user'
import
*
as
UserApi
from
'@/api/member/user'
import
{
DescriptionsItemLabel
}
from
'@/components/Descriptions/index'
import
{
DescriptionsItemLabel
}
from
'@/components/Descriptions/index'
const
{
user
}
=
defineProps
<
{
user
:
UserApi
.
UserVO
}
>
()
withDefaults
(
defineProps
<
{
user
:
UserApi
.
UserVO
;
mode
?:
string
}
>
(),
{
mode
:
'member'
})
</
script
>
</
script
>
<
style
scoped
lang=
"scss"
>
<
style
lang=
"scss"
scoped
>
.card-header
{
.card-header
{
display
:
flex
;
display
:
flex
;
justify-content
:
space-between
;
justify-content
:
space-between
;
align-items
:
center
;
align-items
:
center
;
}
}
::v-deep
(
.kefu-descriptions
)
{
.el-descriptions__cell
{
display
:
flex
;
align-items
:
center
;
justify-content
:
space-between
;
.el-descriptions__label
{
width
:
120px
;
display
:
block
;
text-align
:
left
;
}
.el-descriptions__content
{
flex
:
1
;
text-align
:
end
;
}
}
}
</
style
>
</
style
>
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