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
18c7693e
authored
Nov 10, 2024
by
芋道源码
Committed by
Gitee
Nov 10, 2024
Browse files
Options
Browse Files
Download
Plain Diff
!584 【功能完善】商城: 客服
Merge pull request !584 from puhui999/dev
parents
8b0778ca
5e7afae9
Show whitespace changes
Inline
Side-by-side
Showing
11 changed files
with
224 additions
and
225 deletions
+224
-225
src/store/modules/mall/kefu.ts
+7
-41
src/utils/index.ts
+1
-1
src/views/mall/promotion/kefu/components/KeFuConversationList.vue
+4
-3
src/views/mall/promotion/kefu/components/KeFuMessageList.vue
+10
-10
src/views/mall/promotion/kefu/components/member/MemberInfo.vue
+55
-1
src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue
+3
-4
src/views/mall/promotion/kefu/components/message/OrderItem.vue
+10
-7
src/views/mall/promotion/kefu/components/message/ProductItem.vue
+48
-136
src/views/mall/promotion/kefu/index.vue
+4
-3
src/views/member/user/detail/UserAccountInfo.vue
+4
-2
src/views/member/user/detail/UserBasicInfo.vue
+78
-17
No files found.
src/store/modules/mall/kefu.ts
View file @
18c7693e
...
@@ -63,48 +63,14 @@ export const useMallKefuStore = defineStore('mall-kefu', {
...
@@ -63,48 +63,14 @@ export const useMallKefuStore = defineStore('mall-kefu', {
}
}
},
},
conversationSort
()
{
conversationSort
()
{
//
TODO @puhui999:1)逻辑上,先按照置顶、再按照最后消息时间;2)感觉写的有一丢丢小复杂,发给大模型,看看有没可能简化哈。
//
按置顶属性和最后消息时间排序
this
.
conversationList
.
sort
((
obj1
,
obj2
)
=>
{
this
.
conversationList
.
sort
((
a
,
b
)
=>
{
//
如果 obj1.adminPinned 为 true,obj2.adminPinned 为 false,obj1 应该排
在前面
//
按照置顶排序,置顶的会
在前面
if
(
obj1
.
adminPinned
&&
!
obj2
.
adminPinned
)
{
if
(
a
.
adminPinned
!==
b
.
adminPinned
)
{
return
-
1
return
a
.
adminPinned
?
-
1
:
1
}
}
// 如果 obj1.adminPinned 为 false,obj2.adminPinned 为 true,obj2 应该排在前面
// 按照最后消息时间排序,最近的会在前面
if
(
!
obj1
.
adminPinned
&&
obj2
.
adminPinned
)
{
return
(
b
.
lastMessageTime
as
unknown
as
number
)
-
(
a
.
lastMessageTime
as
unknown
as
number
)
return
1
}
// 如果 obj1.adminPinned 和 obj2.adminPinned 都为 true,比较 adminUnreadMessageCount 的值
if
(
obj1
.
adminPinned
&&
obj2
.
adminPinned
)
{
return
obj1
.
adminUnreadMessageCount
-
obj2
.
adminUnreadMessageCount
}
// 如果 obj1.adminPinned 和 obj2.adminPinned 都为 false,比较 adminUnreadMessageCount 的值
if
(
!
obj1
.
adminPinned
&&
!
obj2
.
adminPinned
)
{
return
obj1
.
adminUnreadMessageCount
-
obj2
.
adminUnreadMessageCount
}
// 如果 obj1.adminPinned 为 true,obj2.adminPinned 为 true,且 b 都大于 0,比较 adminUnreadMessageCount 的值
if
(
obj1
.
adminPinned
&&
obj2
.
adminPinned
&&
obj1
.
adminUnreadMessageCount
>
0
&&
obj2
.
adminUnreadMessageCount
>
0
)
{
return
obj1
.
adminUnreadMessageCount
-
obj2
.
adminUnreadMessageCount
}
// 如果 obj1.adminPinned 为 false,obj2.adminPinned 为 false,且 b 都大于 0,比较 adminUnreadMessageCount 的值
if
(
!
obj1
.
adminPinned
&&
!
obj2
.
adminPinned
&&
obj1
.
adminUnreadMessageCount
>
0
&&
obj2
.
adminUnreadMessageCount
>
0
)
{
return
obj1
.
adminUnreadMessageCount
-
obj2
.
adminUnreadMessageCount
}
return
0
})
})
}
}
}
}
...
...
src/utils/index.ts
View file @
18c7693e
import
{
toNumber
}
from
'lodash-es'
import
{
toNumber
}
from
'lodash-es'
/**
/**
*
*
...
...
src/views/mall/promotion/kefu/components/KeFuConversationList.vue
View file @
18c7693e
<
template
>
<
template
>
<el-aside
class=
"kefu p-5px h-100%"
width=
"260px"
>
<el-aside
class=
"kefu p-5px h-100%"
width=
"260px"
>
<div
class=
"color-[#999] font-bold my-10px"
<div
class=
"color-[#999] font-bold my-10px"
>
>
会话记录(
{{
kefuStore
.
getConversationList
.
length
}}
)
会话记录(
{{
kefuStore
.
getConversationList
.
length
}}
)
</div>
</div>
<div
<div
v-for=
"item in kefuStore.getConversationList"
v-for=
"item in kefuStore.getConversationList"
...
@@ -78,6 +78,7 @@ import { formatPast } from '@/utils/formatTime'
...
@@ -78,6 +78,7 @@ 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
{
useMallKefuStore
}
from
'@/store/modules/mall/kefu'
import
{
jsonParse
}
from
'@/utils'
defineOptions
({
name
:
'KeFuConversationList'
})
defineOptions
({
name
:
'KeFuConversationList'
})
...
@@ -118,7 +119,7 @@ const getConversationDisplayText = computed(
...
@@ -118,7 +119,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
''
}
}
...
...
src/views/mall/promotion/kefu/components/KeFuMessageList.vue
View file @
18c7693e
...
@@ -52,7 +52,7 @@
...
@@ -52,7 +52,7 @@
<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=
"flex items-center"
></div>
></div>
</
template
>
</
template
>
...
@@ -62,8 +62,8 @@
...
@@ -62,8 +62,8 @@
<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"
fit=
"contain"
fit=
"contain"
preview-teleported
preview-teleported
...
@@ -75,12 +75,11 @@
...
@@ -75,12 +75,11 @@
v-if=
"KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
v-if=
"KeFuMessageContentTypeEnum.PRODUCT === item.contentType"
:picUrl=
"getMessageContent(item).picUrl"
:picUrl=
"getMessageContent(item).picUrl"
:price=
"getMessageContent(item).price"
:price=
"getMessageContent(item).price"
:s
kuText=
"getMessageContent(item).introduction
"
:s
ales-count=
"getMessageContent(item).salesCount
"
:spuId=
"getMessageContent(item).spuId"
:spuId=
"getMessageContent(item).spuId"
:stock=
"getMessageContent(item).stock"
:title=
"getMessageContent(item).spuName"
:title=
"getMessageContent(item).spuName"
:titleWidth=
"400"
class=
"max-w-300px"
class=
"max-w-70%"
priceColor=
"#FF3000"
/>
/>
</MessageItem>
</MessageItem>
<!-- 订单消息 -->
<!-- 订单消息 -->
...
@@ -245,6 +244,7 @@ const getNewMessageList = async (val: KeFuConversationRespVO) => {
...
@@ -245,6 +244,7 @@ const getNewMessageList = async (val: KeFuConversationRespVO) => {
total
.
value
=
messageList
.
value
.
length
||
0
total
.
value
=
messageList
.
value
.
length
||
0
loadHistory
.
value
=
false
loadHistory
.
value
=
false
refreshContent
.
value
=
false
refreshContent
.
value
=
false
skipGetMessageList
.
value
=
false
// 2.2 设置会话相关属性
// 2.2 设置会话相关属性
conversation
.
value
=
val
conversation
.
value
=
val
queryParams
.
conversationId
=
val
.
id
queryParams
.
conversationId
=
val
.
id
...
@@ -268,7 +268,7 @@ const handleSendPicture = async (picUrl: string) => {
...
@@ -268,7 +268,7 @@ 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
)
}
}
...
@@ -288,7 +288,7 @@ const handleSendMessage = async (event: any) => {
...
@@ -288,7 +288,7 @@ const handleSendMessage = async (event: any) => {
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
)
}
}
...
@@ -392,7 +392,7 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
...
@@ -392,7 +392,7 @@ const showTime = computed(() => (item: KeFuMessageRespVO, index: number) => {
&
-content
{
&
-content
{
margin
:
0
;
margin
:
0
;
padding
:
0
;
padding
:
10px
;
position
:
relative
;
position
:
relative
;
height
:
100%
;
height
:
100%
;
width
:
100%
;
width
:
100%
;
...
...
src/views/mall/promotion/kefu/components/member/MemberInfo.vue
View file @
18c7693e
...
@@ -24,7 +24,22 @@
...
@@ -24,7 +24,22 @@
交易订单
交易订单
</div>
</div>
</el-header>
</el-header>
<el-main
class=
"kefu-content"
>
<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)"
>
<div
v-show=
"!isEmpty(conversation)"
>
<el-scrollbar
ref=
"scrollbarRef"
always
@
scroll=
"handleScroll"
>
<el-scrollbar
ref=
"scrollbarRef"
always
@
scroll=
"handleScroll"
>
<!-- 最近浏览 -->
<!-- 最近浏览 -->
...
@@ -45,11 +60,17 @@ import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
...
@@ -45,11 +60,17 @@ import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
import
{
isEmpty
}
from
'@/utils/is'
import
{
isEmpty
}
from
'@/utils/is'
import
{
debounce
}
from
'lodash-es'
import
{
debounce
}
from
'lodash-es'
import
{
ElScrollbar
as
ElScrollbarType
}
from
'element-plus/es/components/scrollbar/index'
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'
})
defineOptions
({
name
:
'MemberBrowsingHistory'
})
const
activeTab
=
ref
(
'会员信息'
)
const
activeTab
=
ref
(
'会员信息'
)
const
tabActivation
=
computed
(()
=>
(
tab
:
string
)
=>
activeTab
.
value
===
tab
)
const
tabActivation
=
computed
(()
=>
(
tab
:
string
)
=>
activeTab
.
value
===
tab
)
/** tab 切换 */
/** tab 切换 */
const
productBrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
ProductBrowsingHistory
>>
()
const
productBrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
ProductBrowsingHistory
>>
()
const
orderBrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
OrderBrowsingHistory
>>
()
const
orderBrowsingHistoryRef
=
ref
<
InstanceType
<
typeof
OrderBrowsingHistory
>>
()
...
@@ -63,6 +84,8 @@ const handleClick = async (tab: string) => {
...
@@ -63,6 +84,8 @@ const handleClick = async (tab: string) => {
const
getHistoryList
=
async
()
=>
{
const
getHistoryList
=
async
()
=>
{
switch
(
activeTab
.
value
)
{
switch
(
activeTab
.
value
)
{
case
'会员信息'
:
case
'会员信息'
:
await
getUserData
()
await
getUserWallet
()
break
break
case
'最近浏览'
:
case
'最近浏览'
:
await
productBrowsingHistoryRef
.
value
?.
getHistoryList
(
conversation
.
value
)
await
productBrowsingHistoryRef
.
value
?.
getHistoryList
(
conversation
.
value
)
...
@@ -110,6 +133,37 @@ const handleScroll = debounce(() => {
...
@@ -110,6 +133,37 @@ const handleScroll = debounce(() => {
loadMore
()
loadMore
()
}
}
},
200
)
},
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
}
const
params
=
{
userId
:
conversation
.
value
.
userId
}
wallet
.
value
=
(
await
WalletApi
.
getWallet
(
params
))
||
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
>
</
script
>
<
style
lang=
"scss"
scoped
>
<
style
lang=
"scss"
scoped
>
...
...
src/views/mall/promotion/kefu/components/member/ProductBrowsingHistory.vue
View file @
18c7693e
<
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 @
18c7693e
...
@@ -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
:
rgba
(
128
,
128
,
128
,
0.5
);
//
透明色,暗黑模式下也能体现
.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,30 @@ function formatOrderStatus(order: any) {
...
@@ -128,27 +128,30 @@ function formatOrderStatus(order: any) {
}
}
}
}
}
}
.order-state
{
font-size
:
13px
;
}
}
}
.pay-box
{
.pay-box
{
padding-top
:
10px
;
padding-top
:
10px
;
color
:
#fff
;
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 @
18c7693e
<
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
:
rgba
(
128
,
128
,
128
,
0.5
);
//
透明色,暗黑模式下也能体现
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/index.vue
View file @
18c7693e
...
@@ -16,6 +16,7 @@ import { KeFuConversationRespVO } from '@/api/mall/promotion/kefu/conversation'
...
@@ -16,6 +16,7 @@ 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
{
useMallKefuStore
}
from
'@/store/modules/mall/kefu'
import
{
jsonParse
}
from
'@/utils'
defineOptions
({
name
:
'KeFu'
})
defineOptions
({
name
:
'KeFu'
})
...
@@ -30,6 +31,7 @@ const server = ref(
...
@@ -30,6 +31,7 @@ const server = ref(
)
// WebSocket 服务地址
)
// WebSocket 服务地址
/** 发起 WebSocket 连接 */
/** 发起 WebSocket 连接 */
// TODO puhui999: websocket 连接有点问题收不到消息 🤣
const
{
data
,
close
,
open
}
=
useWebSocket
(
server
.
value
,
{
const
{
data
,
close
,
open
}
=
useWebSocket
(
server
.
value
,
{
autoReconnect
:
true
,
autoReconnect
:
true
,
heartbeat
:
true
heartbeat
:
true
...
@@ -45,9 +47,9 @@ watchEffect(() => {
...
@@ -45,9 +47,9 @@ 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
)
console
.
log
(
jsonMessage
)
const
type
=
jsonMessage
.
type
const
type
=
jsonMessage
.
type
if
(
!
type
)
{
if
(
!
type
)
{
message
.
error
(
'未知的消息类型:'
+
data
.
value
)
message
.
error
(
'未知的消息类型:'
+
data
.
value
)
...
@@ -57,7 +59,6 @@ watchEffect(() => {
...
@@ -57,7 +59,6 @@ watchEffect(() => {
if
(
type
===
WebSocketMessageTypeConstants
.
KEFU_MESSAGE_TYPE
)
{
if
(
type
===
WebSocketMessageTypeConstants
.
KEFU_MESSAGE_TYPE
)
{
const
message
=
JSON
.
parse
(
jsonMessage
.
content
)
const
message
=
JSON
.
parse
(
jsonMessage
.
content
)
// 刷新会话列表
// 刷新会话列表
// TODO @puhui999:不应该刷新列表,而是根据消息,本地 update 列表的数据;
kefuStore
.
updateConversation
(
message
.
conversationId
)
kefuStore
.
updateConversation
(
message
.
conversationId
)
// 刷新消息列表
// 刷新消息列表
keFuChatBoxRef
.
value
?.
refreshMessageList
(
message
)
keFuChatBoxRef
.
value
?.
refreshMessageList
(
message
)
...
@@ -66,7 +67,7 @@ watchEffect(() => {
...
@@ -66,7 +67,7 @@ watchEffect(() => {
// 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
)
{
// 更新会话已读
// 更新会话已读
kefuStore
.
updateConversationStatus
(
JSON
.
parse
(
jsonMessage
.
content
)?.
id
)
kefuStore
.
updateConversationStatus
(
jsonParse
(
jsonMessage
.
content
)
)
}
}
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
error
)
console
.
error
(
error
)
...
...
src/views/member/user/detail/UserAccountInfo.vue
View file @
18c7693e
<
template
>
<
template
>
<el-descriptions
:column=
"
2
"
>
<el-descriptions
: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
{
...
...
src/views/member/user/detail/UserBasicInfo.vue
View file @
18c7693e
...
@@ -3,80 +3,141 @@
...
@@ -3,80 +3,141 @@
<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"
>
<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
label=
"手机号"
>
<
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
;
...
...
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