Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
foreign
/
customer-service
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Settings
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Commit
cf20b3e5
authored
Sep 28, 2021
by
Sixong.Zhu
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
客服支持批量接待,支持2min内撤回功能
parent
ef8432c0
Show whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
141 additions
and
16 deletions
components/chat-container.vue
components/chat-title.vue
components/controller/index.ts
components/message-list.vue
components/message.vue
database/index.ts
model/index.ts
store/index.ts
store/model.ts
xim/xim.ts
components/chat-container.vue
View file @
cf20b3e5
...
...
@@ -131,7 +131,7 @@ export default class ChatContainer extends Vue {
.chat-content-wrap
{
display
:
inline-block
;
width
:
75%
;
height
:
calc
(
100%
-
60px
)
;
height
:
100%
;
box-sizing
:
border-box
;
vertical-align
:
top
;
}
...
...
components/chat-title.vue
View file @
cf20b3e5
...
...
@@ -21,15 +21,11 @@
round
size=
"small"
v-if=
"!isChatMember"
type=
"primary"
>
我要接待
</el-button
>
<el-button
class=
"button"
@
click=
"exitChat"
round
size=
"small"
v-if=
"isChatMember"
>
退出会话
</el-button
<el-button
class=
"button"
@
click=
"showAddMember"
round
size=
"small"
>
添加客服
</el-button
>
<el-button
class=
"button"
...
...
@@ -37,10 +33,17 @@
round
size=
"small"
v-if=
"isChatMember && operatorType > 25"
type=
"warning"
>
结束接待
</el-button
>
<el-button
class=
"button"
@
click=
"showAddMember"
round
size=
"small"
>
添加客服
</el-button
<el-button
class=
"button"
@
click=
"exitChat"
round
size=
"small"
v-if=
"isChatMember"
type=
"danger"
>
退出会话
</el-button
>
<i
v-if=
"close && isSingleChat"
...
...
@@ -133,7 +136,13 @@ export default class ChatTitle extends Vue {
}
}
private
noop
()
{
return
1
;
}
private
async
exitChat
()
{
this
.
$confirm
(
"确认要退出此会话?"
)
.
then
(
async
()
=>
{
try
{
if
(
+
this
.
operatorType
===
ChatRole
.
Default
)
{
await
this
.
_userExitChat
();
...
...
@@ -144,6 +153,8 @@ export default class ChatTitle extends Vue {
}
catch
(
error
)
{
console
.
error
(
error
);
}
})
.
catch
(
this
.
noop
);
}
private
async
startReception
()
{
...
...
components/controller/index.ts
View file @
cf20b3e5
import
{
MessageType
}
from
"@/customer-service/model"
;
export
function
parserMessage
(
type
:
string
,
rawMsg
:
string
)
{
if
(
!
type
)
return
""
;
if
(
!
rawMsg
)
return
""
;
const
msg
=
JSON
.
parse
(
rawMsg
);
if
(
type
===
"text"
)
{
if
(
type
===
MessageType
.
Text
)
{
return
msg
.
text
;
}
else
if
(
type
===
"image"
)
{
}
else
if
(
type
===
MessageType
.
Image
)
{
return
`[图片]`
;
}
else
if
(
type
===
"file"
)
{
}
else
if
(
type
===
MessageType
.
File
)
{
return
`[文件]`
;
}
else
if
(
type
===
MessageType
.
Withdraw
)
{
return
`[您撤回了一条消息]`
;
}
else
{
return
`[系统自动回复]`
;
}
...
...
components/message-list.vue
View file @
cf20b3e5
...
...
@@ -28,6 +28,7 @@
:data=
"item"
:shape=
"shape"
@
open=
"open"
@
withdraw=
"refresh"
/>
</div>
</
template
>
...
...
@@ -353,6 +354,10 @@ export default class MessageList extends Vue {
}
return
v
;
}
private
refresh
(
msg
:
number
)
{
this
.
fetchNewMsg
();
}
}
</
script
>
...
...
components/message.vue
View file @
cf20b3e5
<
template
>
<div
class=
"message-con d-flex align-items-center"
:class=
"isMyMessage ? 'my-message flex-row-reverse' : ''"
:class=
"
{
'my-message flex-row-reverse': isMyMessage,
'justify-content-center': isWithdrawMessage,
}"
>
<div
class=
"msg-content"
:class=
"
{ 'algin-left': !isMyMessage }">
<div
class=
"msg-name no-selection"
:class=
"
{ 'algin-left': !isMyMessage }"
v-if="!isWithdrawMessage"
>
{{
userName
}}
</div>
...
...
@@ -85,6 +89,12 @@
>
<video-player-icon
@
click
.
native=
"openFile"
></video-player-icon>
</div>
<div
class=
"msg-detail withdraw-message"
v-else-if=
"messageType === 'withdraw'"
>
您撤回了一条消息
</div>
<!-- Text -->
<div
class=
"msg-detail inline-text"
...
...
@@ -115,7 +125,7 @@
></i>
<i
class=
"el-icon-loading"
v-else-if=
"isSendingMessage"
></i>
<template
v-if=
"showReadSummary"
>
<template
v-if=
"showReadSummary
&& !isWithdrawMessage
"
>
<div
v-if=
"isMyMessage"
class=
"msg-read pos-rel"
>
<span
@
click=
"readListVisibility = true"
...
...
@@ -135,6 +145,13 @@
/>
</div>
</template>
<span
class=
"withdraw"
v-if=
"isMyMessage && canWithdraw && !isWithdrawMessage"
@
click=
"withdraw"
>
撤回此消息
</span
>
</div>
</template>
...
...
@@ -163,6 +180,10 @@ import WhoReadList from "./who-read-list.vue";
import
avatar
from
"@/customer-service/components/avatar.vue"
;
import
{
chatStore
,
ChatStore
}
from
"@/customer-service/store/model"
;
import
ximInstance
from
"../xim/xim"
;
import
{
dbController
}
from
"../database"
;
const
twoMinutes
=
2
*
60
*
1000
;
@
Component
({
components
:
{
FileIcon
,
VoiceIcon
,
WhoReadList
,
VideoPlayerIcon
,
avatar
},
...
...
@@ -180,6 +201,9 @@ export default class Message extends Mixins(Filters) {
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_WITHDRAW
)
private
readonly
executeWithDraw
:
ChatStore
.
MUTATION_WITHDRAW
;
/**
* tbd: 文件消息所在的域名的url,逻辑需要补充
*/
...
...
@@ -213,6 +237,17 @@ export default class Message extends Mixins(Filters) {
private
org
=
""
;
private
get
canWithdraw
()
{
if
(
this
.
data
)
{
return
new
Date
().
valueOf
()
-
this
.
data
.
ts
*
1000
<
twoMinutes
;
}
return
false
;
}
private
get
isWithdrawMessage
()
{
return
this
.
data
.
type
===
dto
.
MessageType
.
Withdraw
;
}
private
get
isAllRead
()
{
return
this
.
data
.
read_count
>=
this
.
data
.
total_read_count
;
}
...
...
@@ -431,6 +466,16 @@ export default class Message extends Mixins(Filters) {
this
.
fileFailed2Load
=
true
;
this
.
messageRealUrl
=
""
;
}
private
withdraw
()
{
ximInstance
.
withdraw
(
this
.
chatId
,
this
.
data
.
id
)
.
then
(()
=>
{
this
.
executeWithDraw
(
this
.
data
.
id
);
dbController
.
removeMessage
(
this
.
chatId
,
this
.
data
.
id
);
})
.
finally
(()
=>
this
.
$emit
(
"withdraw"
,
this
.
data
.
id
));
}
}
</
script
>
...
...
@@ -438,6 +483,7 @@ export default class Message extends Mixins(Filters) {
.message-con
{
margin
:
20px
0
;
margin-right
:
15px
;
position
:
relative
;
&.my-message
{
.msg-avatar
{
...
...
@@ -471,6 +517,13 @@ export default class Message extends Mixins(Filters) {
background-color
:
#000
;
border-radius
:
0
;
}
&
.withdraw-message
{
background-color
:
transparent
;
padding
:
0
4px
;
font-size
:
12px
;
color
:
#999
;
}
}
.msg-read
{
...
...
@@ -487,6 +540,22 @@ export default class Message extends Mixins(Filters) {
margin-left
:
0
;
margin-top
:
0
;
}
.withdraw
{
color
:
#999
;
position
:
absolute
;
bottom
:
-18px
;
right
:
0
;
font-size
:
12px
;
display
:
none
;
cursor
:
pointer
;
}
&
:hover
{
.withdraw
{
display
:
inline-block
;
}
}
}
>
i
{
...
...
database/index.ts
View file @
cf20b3e5
...
...
@@ -175,6 +175,11 @@ class ChatCacheDatabaseController {
});
}
public
removeMessage
(
chat
:
number
,
msg
:
number
)
{
const
store
=
this
.
buildChatMessageStore
(
chat
);
store
.
delete
(
msg
);
}
public
mergeChatList
(
source1
:
Chat
[],
source2
:
Chat
[])
{
for
(
const
item
of
source2
)
{
const
t
=
source1
.
find
((
i
)
=>
i
.
id
===
item
.
id
);
...
...
model/index.ts
View file @
cf20b3e5
...
...
@@ -58,6 +58,16 @@ export type ChatListRequestList = {
total
:
number
;
};
export
const
enum
MessageType
{
Text
=
"text"
,
Image
=
"image"
,
File
=
"file"
,
Video
=
"video"
,
Voice
=
"voice"
,
GeneralOrderMsg
=
"general_order_msg"
,
Withdraw
=
"withdraw"
,
}
export
interface
Message
{
at_id
:
string
;
chat_id
:
number
;
...
...
@@ -75,7 +85,7 @@ export interface Message {
status
:
number
;
total_read_count
:
number
;
ts
:
number
;
type
:
"text"
|
"image"
|
"file"
|
"video"
|
"voice"
|
"general_order_msg"
;
type
:
MessageType
;
update_time
:
number
;
url
:
string
;
}
...
...
store/index.ts
View file @
cf20b3e5
...
...
@@ -347,6 +347,14 @@ export default {
)
=>
{
Vue
.
set
(
state
[
ChatStore
.
STATE_CHAT_USERNAME
],
param
.
id
,
param
.
name
);
},
[
ChatStore
.
MUTATION_WITHDRAW
]:
(
state
,
id
:
number
)
=>
{
const
old
=
state
[
ChatStore
.
STATE_CHAT_MSG_HISTORY
]
||
[];
const
chatid
=
state
[
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
];
if
(
chatid
==
null
)
return
;
state
[
ChatStore
.
STATE_CHAT_MSG_HISTORY
]
=
old
.
filter
(
(
i
)
=>
i
.
id
!==
id
);
},
},
actions
:
{
async
[
ChatStore
.
ACTION_GET_MY_CHAT_LIST
]({
commit
})
{
...
...
store/model.ts
View file @
cf20b3e5
...
...
@@ -170,6 +170,9 @@ export namespace ChatStore {
export
const
MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID
=
"清空chat uniplat id"
;
export
type
MUTATION_CLEAR_CURRENT_CHAT_UNIPLAT_ID
=
()
=>
void
;
export
const
MUTATION_WITHDRAW
=
"撤回"
;
export
type
MUTATION_WITHDRAW
=
(
id
:
number
)
=>
void
;
export
const
MUTATION_SAVE_MYSELF_ID
=
"保存我的id:聊天窗口显示在右边那个人的id"
;
export
type
MUTATION_SAVE_MYSELF_ID
=
()
=>
void
;
...
...
xim/xim.ts
View file @
cf20b3e5
...
...
@@ -291,6 +291,16 @@ export class Xim {
return
this
;
}
public
async
withdraw
(
chat
:
number
,
msg
:
number
)
{
this
.
checkConnected
();
if
(
this
.
client
==
null
)
{
throw
new
Error
(
"client shouldn't undefined"
);
}
return
this
.
client
.
withdrawMsg
(
chatType
,
chat
,
msg
)
.
then
((
r
)
=>
r
.
args
[
0
]);
}
/**
* 移除会话(用户端/移动端使用)
*/
...
...
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