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
Commit
a4dd4773
authored
Nov 25, 2024
by
YunaiV
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'dev' of
https://gitee.com/yudaocode/yudao-ui-admin-vue3
parents
22199c64
6dee7415
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
609 additions
and
340 deletions
+609
-340
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
+0
-0
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
+48
-136
src/views/mall/promotion/kefu/components/tools/emoji.ts
+1
-1
src/views/mall/promotion/kefu/index.vue
+28
-36
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 @
a4dd4773
...
@@ -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 @
a4dd4773
...
@@ -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 @
a4dd4773
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 @
a4dd4773
...
@@ -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 @
a4dd4773
<
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 @
a4dd4773
This diff is collapsed.
Click to expand it.
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 @
a4dd4773
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 @
a4dd4773
<!-- 右侧信息:会员信息 + 最近浏览 + 交易订单 -->
<
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 @
a4dd4773
File moved
src/views/mall/promotion/kefu/components/
history
/ProductBrowsingHistory.vue
→
src/views/mall/promotion/kefu/components/
member
/ProductBrowsingHistory.vue
View file @
a4dd4773
<
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 @
a4dd4773
...
@@ -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 @
a4dd4773
<
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"
>
</div>
<div
:style=
"[
{ borderRadius: radius + 'px', marginBottom: marginBottom + 'px' }]"
class="ss-order-card-warp flex items-stretch justify-between bg-white"
>
<div
class=
"img-box mr-24px"
>
<el-image
<el-image
:initial-index=
"0"
:initial-index=
"0"
:preview-src-list=
"[picUrl]"
:preview-src-list=
"[picUrl]"
:src=
"picUrl"
:src=
"picUrl"
class=
"order
-img"
class=
"product-warp-left
-img"
fit=
"contain"
fit=
"contain"
preview-teleported
preview-teleported
@
click
.
stop
@
click
.
stop
/>
/>
</div>
</div>
<div
<!-- 右侧商品信息 -->
:style=
"[
{ width: titleWidth ? titleWidth + 'px' : '' }]"
<div
class=
"product-warp-right"
>
class="box-right flex flex-col justify-between"
<div
class=
"description"
>
{{
title
}}
</div>
>
<div
class=
"my-5px"
>
<div
v-if=
"title"
class=
"title-text ss-line-2"
>
{{
title
}}
</div>
<span
class=
"mr-20px"
>
库存:
{{
stock
||
0
}}
</span>
<div
v-if=
"skuString"
class=
"spec-text mt-8px mb-12px"
>
{{
skuString
}}
</div>
<span>
销量:
{{
salesCount
||
0
}}
</span>
<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
class=
"flex justify-between items-center"
>
<span
class=
"price"
>
¥
{{
fenToYuan
(
price
)
}}
</span>
<el-button
size=
"small"
text
type=
"primary"
>
详情
</el-button>
</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
],
default
:
0
},
score
:
{
type
:
[
String
,
Number
],
type
:
[
String
,
Number
],
default
:
''
default
:
''
},
},
radius
:
{
stock
:
{
type
:
[
String
],
type
:
[
String
,
Number
],
default
:
''
},
marginBottom
:
{
type
:
[
String
],
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
;
}
}
.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
;
display
:
-webkit-box
;
-webkit-line-clamp
:
1
;
-webkit-line-clamp
:
1
;
/* 显示一行 */
-webkit-box-orient
:
vertical
;
-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
{
min-width
:
0
;
overflow
:
hidden
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
text-overflow
:
ellipsis
;
display
:
-webkit-box
;
-webkit-box-orient
:
vertical
;
&-1
{
-webkit-line-clamp
:
1
;
}
}
&
-2
{
.price
{
-webkit-line-clamp
:
2
;
color
:
#ff3000
;
}
}
}
}
}
</
style
>
</
style
>
src/views/mall/promotion/kefu/components/tools/emoji.ts
View file @
a4dd4773
...
@@ -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 @
a4dd4773
<
template
>
<
template
>
<el-
row
:gutter=
"10
"
>
<el-
container
class=
"kefu-layout
"
>
<!-- 会话列表 -->
<!-- 会话列表 -->
<el-col
:span=
"6"
>
<ContentWrap>
<KeFuConversationList
ref=
"keFuConversationRef"
@
change=
"handleChange"
/>
<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 @
a4dd4773
<
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 @
a4dd4773
...
@@ -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