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
d3614cbb
authored
Jul 01, 2024
by
puhui999
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
【新增】:mall 客服初始化
parent
bc8948ee
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
408 additions
and
2 deletions
+408
-2
.env.local
+3
-0
src/api/mall/promotion/kefu/conversation/index.ts
+71
-0
src/views/infra/webSocket/index.vue
+2
-2
src/views/mall/promotion/kefu/components/KeFuConversationBox.vue
+89
-0
src/views/mall/promotion/kefu/components/KefuChatBox.vue
+77
-0
src/views/mall/promotion/kefu/components/constants.ts
+14
-0
src/views/mall/promotion/kefu/components/emoji.ts
+91
-0
src/views/mall/promotion/kefu/components/index.ts
+5
-0
src/views/mall/promotion/kefu/index.vue
+55
-0
types/env.d.ts
+1
-0
No files found.
.env.local
View file @
d3614cbb
...
...
@@ -29,5 +29,8 @@ VITE_BASE_PATH=/
# 商城H5会员端域名
VITE_MALL_H5_DOMAIN='http://localhost:3000'
# 客户端静态资源地址 空=默认使用服务端指定的CDN资源地址前缀 | local=本地 | http(s)://xxx.xxx=自定义静态资源地址前缀
VITE_STATIC_URL = https://file.sheepjs.com
# 验证码的开关
VITE_APP_CAPTCHA_ENABLE=false
src/api/mall/promotion/kefu/conversation/index.ts
0 → 100644
View file @
d3614cbb
import
request
from
'@/config/axios'
export
interface
KeFuConversationRespVO
{
/**
* 编号
*/
id
:
number
/**
* 会话所属用户
*/
userId
:
number
/**
* 会话所属用户头像
*/
userAvatar
:
string
/**
* 会话所属用户昵称
*/
nickname
:
string
/**
* 最后聊天时间
*/
lastMessageTime
:
Date
/**
* 最后聊天内容
*/
lastMessageContent
:
string
/**
* 最后发送的消息类型
*/
lastMessageContentType
:
number
/**
* 管理端置顶
*/
adminPinned
:
boolean
/**
* 用户是否可见
*/
userDeleted
:
boolean
/**
* 管理员是否可见
*/
adminDeleted
:
boolean
/**
* 管理员未读消息数
*/
adminUnreadMessageCount
:
number
/**
* 创建时间
*/
createTime
?:
string
}
// 客服会话 API
export
const
KeFuConversationApi
=
{
// 获得客服会话列表
getConversationList
:
async
()
=>
{
return
await
request
.
get
({
url
:
'/promotion/kefu-conversation/list'
})
},
// 客服会话置顶
updateConversationPinned
:
async
(
data
:
any
)
=>
{
return
await
request
.
put
({
url
:
'/promotion/kefu-conversation/update-conversation-pinned'
,
data
})
},
// 删除客服会话
deleteConversation
:
async
(
id
:
number
)
=>
{
return
await
request
.
get
({
url
:
'/promotion/kefu-conversation/delete?id'
+
id
})
}
}
src/views/infra/webSocket/index.vue
View file @
d3614cbb
...
...
@@ -29,8 +29,8 @@
:autosize=
"{ minRows: 2, maxRows: 4 }"
:disabled=
"!getIsOpen"
clearable
type=
"textarea"
placeholder=
"请输入你要发送的消息"
type=
"textarea"
/>
<el-select
v-model=
"sendUserId"
class=
"mt-4"
placeholder=
"请选择发送人"
>
<el-option
key=
""
label=
"所有人"
value=
""
/>
...
...
@@ -71,7 +71,7 @@
<
script
lang=
"ts"
setup
>
import
{
formatDate
}
from
'@/utils/formatTime'
import
{
useWebSocket
}
from
'@vueuse/core'
import
{
getAccessToken
}
from
'@/utils/auth'
//
import { getAccessToken } from '@/utils/auth'
import
*
as
UserApi
from
'@/api/system/user'
defineOptions
({
name
:
'InfraWebSocket'
})
...
...
src/views/mall/promotion/kefu/components/KeFuConversationBox.vue
0 → 100644
View file @
d3614cbb
<
template
>
<div
class=
"kefu"
>
<div
v-for=
"(item, index) in conversationList"
:key=
"item.id"
:class=
"
{ active: index === activeConversationIndex }"
class="kefu-conversation flex justify-between items-center"
@click="openRightMessage(item, index)"
>
<div
class=
"kefu-conversation-left flex justify-center items-center"
>
<el-avatar
:src=
"item.userAvatar"
alt=
"avatar"
/>
<div
class=
"ml-10px"
>
<div
class=
"nickname"
>
{{
item
.
nickname
}}
</div>
<div
v-dompurify-html=
"replaceEmoji(item.lastMessageContent)"
class=
"last-message flex items-center color-[#989EA6]"
></div>
</div>
</div>
<div
class=
"kefu-conversation-right color-[#989EA6]"
>
{{
formatDate
(
item
.
lastMessageTime
)
}}
</div>
</div>
</div>
</
template
>
<
script
lang=
"ts"
setup
>
import
{
KeFuConversationApi
,
KeFuConversationRespVO
}
from
'@/api/mall/promotion/kefu/conversation'
import
{
replaceEmoji
}
from
'@/views/mall/promotion/kefu/components/emoji'
import
{
formatDate
,
getNowDateTime
}
from
'@/utils/formatTime'
defineOptions
({
name
:
'KeFuConversationBox'
})
const
activeConversationIndex
=
ref
(
0
)
// 默认激活第一个
const
conversationList
=
ref
<
KeFuConversationRespVO
[]
>
([])
// 会话列表
const
getConversationList
=
async
()
=>
{
conversationList
.
value
=
await
KeFuConversationApi
.
getConversationList
()
// 测试数据
for
(
let
i
=
0
;
i
<
5
;
i
++
)
{
conversationList
.
value
.
push
({
id
:
1
,
userId
:
283
,
userAvatar
:
'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTKMezSxtOImrC9lbhwHiazYwck3xwrEcO7VJfG6WQo260whaeVNoByE5RreiaGsGfOMlIiaDhSaA991w/132'
,
nickname
:
'辉辉鸭'
+
i
,
lastMessageTime
:
getNowDateTime
(),
lastMessageContent
:
'[爱心][爱心]你好哇'
,
lastMessageContentType
:
1
,
adminPinned
:
false
,
userDeleted
:
false
,
adminDeleted
:
false
,
adminUnreadMessageCount
:
19
})
}
}
defineExpose
({
getConversationList
})
const
emits
=
defineEmits
<
{
(
e
:
'change'
,
v
:
number
):
void
}
>
()
// 打开右侧消息
const
openRightMessage
=
(
item
:
KeFuConversationRespVO
,
index
:
number
)
=>
{
activeConversationIndex
.
value
=
index
emits
(
'change'
,
item
.
id
)
}
</
script
>
<
style
lang=
"scss"
scoped
>
.kefu
{
&-conversation
{
height
:
60px
;
padding
:
10px
;
background-color
:
#fff
;
transition
:
border-left
0.05s
ease-in-out
;
/* 设置过渡效果 */
&-left
{
.last-message
{
width
:
300px
;
overflow
:
hidden
;
//
隐藏超出的文本
white-space
:
nowrap
;
//
禁止换行
text-overflow
:
ellipsis
;
//
添加省略号
}
}
}
.active
{
border-left
:
5px
#3271ff
solid
;
background-color
:
#eff0f1
;
}
}
</
style
>
src/views/mall/promotion/kefu/components/KefuChatBox.vue
0 → 100644
View file @
d3614cbb
<
template
>
<el-container
class=
"kefu"
>
<el-header>
<div
class=
"kefu-title"
>
芋道
</div>
</el-header>
<el-main
class=
"kefu-content"
>
<div
v-for=
"item in 100"
:key=
"item"
:class=
"[item % 2 === 0 ? `ss-row-left` : `ss-row-right`]"
class=
"flex mb-20px w-[100%]"
>
<el-avatar
v-if=
"item % 2 === 0"
alt=
"avatar"
src=
"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"
/>
<div
class=
"ml-10px"
>
Lorem Ipsum,也称乱数假文或者哑元文本
</div>
<el-avatar
v-if=
"item % 2 !== 0"
alt=
"avatar"
src=
"https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"
/>
</div>
</el-main>
<el-footer
height=
"230px"
>
<div
class=
"h-[100%]"
>
<div
class=
"chat-tools"
>
<Icon
:size=
"30"
class=
"ml-10px"
icon=
"fa:frown-o"
/>
</div>
<el-input
v-model=
"message"
:autosize=
"
{ minRows: 6, maxRows: 6 }" type="textarea" />
<div
class=
"h-45px flex justify-end"
>
<el-button
class=
"mt-10px"
type=
"primary"
>
发送
</el-button>
</div>
</div>
</el-footer>
</el-container>
<!-- 没选择左侧会话时显示空界面 -->
</
template
>
<
script
lang=
"ts"
setup
>
defineOptions
({
name
:
'KeFuMessageBox'
})
const
message
=
ref
(
''
)
</
script
>
<
style
lang=
"scss"
scoped
>
.kefu
{
&-title
{
border-bottom
:
#e4e0e0
solid
1px
;
height
:
60px
;
line-height
:
60px
;
}
&
-content
{
}
.chat-tools
{
width
:
100%
;
border
:
#e4e0e0
solid
1px
;
height
:
44px
;
display
:
flex
;
align-items
:
center
;
}
.ss-row-left
{
justify-content
:
flex-start
;
}
.ss-row-right
{
justify-content
:
flex-end
;
}
::v-deep
(
textarea
)
{
resize
:
none
;
}
}
</
style
>
src/views/mall/promotion/kefu/components/constants.ts
0 → 100644
View file @
d3614cbb
export
const
KeFuMessageContentTypeEnum
=
{
TEXT
:
1
,
// 文本消息
IMAGE
:
2
,
// 图片消息
VOICE
:
3
,
// 语音消息
VIDEO
:
4
,
// 视频消息
SYSTEM
:
5
,
// 系统消息
// ========== 商城特殊消息 ==========
PRODUCT
:
10
,
// 商品消息
ORDER
:
11
// 订单消息"
}
export
const
UserTypeEnum
=
{
MEMBER
:
1
,
// 会员 面向 c 端,普通用户
ADMIN
:
2
// 管理员 面向 b 端,管理后台
}
src/views/mall/promotion/kefu/components/emoji.ts
0 → 100644
View file @
d3614cbb
export
const
emojiList
=
[
{
name
:
'[笑掉牙]'
,
file
:
'xiaodiaoya.png'
},
{
name
:
'[可爱]'
,
file
:
'keai.png'
},
{
name
:
'[冷酷]'
,
file
:
'lengku.png'
},
{
name
:
'[闭嘴]'
,
file
:
'bizui.png'
},
{
name
:
'[生气]'
,
file
:
'shengqi.png'
},
{
name
:
'[惊恐]'
,
file
:
'jingkong.png'
},
{
name
:
'[瞌睡]'
,
file
:
'keshui.png'
},
{
name
:
'[大笑]'
,
file
:
'daxiao.png'
},
{
name
:
'[爱心]'
,
file
:
'aixin.png'
},
{
name
:
'[坏笑]'
,
file
:
'huaixiao.png'
},
{
name
:
'[飞吻]'
,
file
:
'feiwen.png'
},
{
name
:
'[疑问]'
,
file
:
'yiwen.png'
},
{
name
:
'[开心]'
,
file
:
'kaixin.png'
},
{
name
:
'[发呆]'
,
file
:
'fadai.png'
},
{
name
:
'[流泪]'
,
file
:
'liulei.png'
},
{
name
:
'[汗颜]'
,
file
:
'hanyan.png'
},
{
name
:
'[惊悚]'
,
file
:
'jingshu.png'
},
{
name
:
'[困~]'
,
file
:
'kun.png'
},
{
name
:
'[心碎]'
,
file
:
'xinsui.png'
},
{
name
:
'[天使]'
,
file
:
'tianshi.png'
},
{
name
:
'[晕]'
,
file
:
'yun.png'
},
{
name
:
'[啊]'
,
file
:
'a.png'
},
{
name
:
'[愤怒]'
,
file
:
'fennu.png'
},
{
name
:
'[睡着]'
,
file
:
'shuizhuo.png'
},
{
name
:
'[面无表情]'
,
file
:
'mianwubiaoqing.png'
},
{
name
:
'[难过]'
,
file
:
'nanguo.png'
},
{
name
:
'[犯困]'
,
file
:
'fankun.png'
},
{
name
:
'[好吃]'
,
file
:
'haochi.png'
},
{
name
:
'[呕吐]'
,
file
:
'outu.png'
},
{
name
:
'[龇牙]'
,
file
:
'ziya.png'
},
{
name
:
'[懵比]'
,
file
:
'mengbi.png'
},
{
name
:
'[白眼]'
,
file
:
'baiyan.png'
},
{
name
:
'[饿死]'
,
file
:
'esi.png'
},
{
name
:
'[凶]'
,
file
:
'xiong.png'
},
{
name
:
'[感冒]'
,
file
:
'ganmao.png'
},
{
name
:
'[流汗]'
,
file
:
'liuhan.png'
},
{
name
:
'[笑哭]'
,
file
:
'xiaoku.png'
},
{
name
:
'[流口水]'
,
file
:
'liukoushui.png'
},
{
name
:
'[尴尬]'
,
file
:
'ganga.png'
},
{
name
:
'[惊讶]'
,
file
:
'jingya.png'
},
{
name
:
'[大惊]'
,
file
:
'dajing.png'
},
{
name
:
'[不好意思]'
,
file
:
'buhaoyisi.png'
},
{
name
:
'[大闹]'
,
file
:
'danao.png'
},
{
name
:
'[不可思议]'
,
file
:
'bukesiyi.png'
},
{
name
:
'[爱你]'
,
file
:
'aini.png'
},
{
name
:
'[红心]'
,
file
:
'hongxin.png'
},
{
name
:
'[点赞]'
,
file
:
'dianzan.png'
},
{
name
:
'[恶魔]'
,
file
:
'emo.png'
}
]
export
const
emojiPage
=
{}
emojiList
.
forEach
((
item
,
index
)
=>
{
if
(
!
emojiPage
[
Math
.
floor
(
index
/
30
)
+
1
])
{
emojiPage
[
Math
.
floor
(
index
/
30
)
+
1
]
=
[]
}
emojiPage
[
Math
.
floor
(
index
/
30
)
+
1
].
push
(
item
)
})
// 后端上传地址
const
staticUrl
=
import
.
meta
.
env
.
VITE_STATIC_URL
// 处理表情
export
function
replaceEmoji
(
data
:
string
)
{
let
newData
=
data
if
(
typeof
newData
!==
'object'
)
{
const
reg
=
/
\[(
.+
?)\]
/g
// [] 中括号
const
zhEmojiName
=
newData
.
match
(
reg
)
if
(
zhEmojiName
)
{
zhEmojiName
.
forEach
((
item
)
=>
{
const
emojiFile
=
selEmojiFile
(
item
)
newData
=
newData
.
replace
(
item
,
`<img class="chat-img" style="width: 24px;height: 24px;margin: 0 3px;" src="
${
staticUrl
+
'/static/img/chat/emoji/'
+
emojiFile
}
"/>`
)
})
}
}
return
newData
}
function
selEmojiFile
(
name
:
string
)
{
for
(
const
index
in
emojiList
)
{
if
(
emojiList
[
index
].
name
===
name
)
{
return
emojiList
[
index
].
file
}
}
return
false
}
src/views/mall/promotion/kefu/components/index.ts
0 → 100644
View file @
d3614cbb
import
KeFuConversationBox
from
'./KeFuConversationBox.vue'
import
KeFuChatBox
from
'./KefuChatBox.vue'
import
*
as
Constants
from
'./constants'
export
{
KeFuConversationBox
,
KeFuChatBox
,
Constants
}
src/views/mall/promotion/kefu/index.vue
0 → 100644
View file @
d3614cbb
<
template
>
<el-row
:gutter=
"10"
>
<el-col
:span=
"8"
>
<ContentWrap>
<KeFuConversationBox
ref=
"keFuConversationRef"
@
change=
"handleChange"
/>
</ContentWrap>
</el-col>
<el-col
:span=
"16"
>
<ContentWrap>
<KeFuChatBox
ref=
"keFuChatBoxRef"
/>
</ContentWrap>
</el-col>
</el-row>
</
template
>
<
script
lang=
"ts"
setup
>
import
{
KeFuChatBox
,
KeFuConversationBox
}
from
'./components'
defineOptions
({
name
:
'KeFu'
})
const
keFuChatBoxRef
=
ref
<
InstanceType
<
typeof
KeFuChatBox
>>
()
const
handleChange
=
()
=>
{}
const
keFuConversationRef
=
ref
<
InstanceType
<
typeof
KeFuConversationBox
>>
()
onMounted
(()
=>
{
keFuConversationRef
.
value
?.
getConversationList
()
})
</
script
>
<
style
lang=
"scss"
>
.kefu
{
height
:
calc
(
100vh
-
165px
);
overflow
:
auto
;
/* 确保内容可滚动 */
}
/* 定义滚动条样式 */
::-webkit-scrollbar
{
width
:
10px
;
height
:
6px
;
}
/*定义滚动条轨道 内阴影+圆角*/
::-webkit-scrollbar-track
{
box-shadow
:
inset
0
0
0px
rgba
(
240
,
240
,
240
,
0.5
);
border-radius
:
10px
;
background-color
:
#fff
;
}
/*定义滑块 内阴影+圆角*/
::-webkit-scrollbar-thumb
{
border-radius
:
10px
;
box-shadow
:
inset
0
0
0px
rgba
(
240
,
240
,
240
,
0.5
);
background-color
:
rgba
(
240
,
240
,
240
,
0.5
);
}
</
style
>
types/env.d.ts
View file @
d3614cbb
...
...
@@ -19,6 +19,7 @@ interface ImportMetaEnv {
readonly
VITE_UPLOAD_URL
:
string
readonly
VITE_API_URL
:
string
readonly
VITE_BASE_PATH
:
string
readonly
VITE_STATIC_URL
:
string
readonly
VITE_DROP_DEBUGGER
:
string
readonly
VITE_DROP_CONSOLE
:
string
readonly
VITE_SOURCEMAP
:
string
...
...
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