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
86932012
authored
Jul 13, 2021
by
panjiangyi
Browse files
Options
_('Browse Files')
Download
Email Patches
Plain Diff
格式化代码
parent
03c9c8d6
Hide whitespace changes
Inline
Side-by-side
Showing
13 changed files
with
1922 additions
and
1961 deletions
chat-list.vue
chat-room.vue
chat.vue
components/message.vue
components/who-read-list.vue
create-chat.vue
hybrid-input/index.vue
message-input.vue
service/request.ts
store/index.ts
store/model.ts
xim/index.ts
xim/xim.ts
chat-list.vue
View file @
86932012
<
template
>
<div
class=
"chat-list-con h-100"
>
<div
class=
"chat-list h-100"
>
<el-input
class=
"keyword-input"
placeholder=
"昵称、手机、Email、备注"
prefix-icon=
"el-icon-search"
v-model=
"searchKeyword"
v-on:keyup
.
enter
.
native=
"search"
clearable
@
clear=
"search"
></el-input>
<div
class=
"chat-list-scroll"
>
<el-scrollbar
class=
"h-100 no-bottom-scrollbar"
>
<div
v-for=
"item in chatRooms"
:key=
"item.chat_id"
class=
"chat-item"
:class=
"
{ selected: isSelected(item) }"
@click="goToChatRoom(item)"
>
<div
class=
"chat-avatar pos-rel"
:class=
"
{ 'red-dot': item.unread_msg_count > 0 }"
>
<fs-avatar
shape=
"circle"
:size=
"36"
:src=
"item.customer_avatar_url"
/>
</div>
<div
class=
"chat-info"
>
<div
class=
"
<div
class=
"chat-list-con h-100"
>
<div
class=
"chat-list h-100"
>
<el-input
class=
"keyword-input"
placeholder=
"昵称、手机、Email、备注"
prefix-icon=
"el-icon-search"
v-model=
"searchKeyword"
v-on:keyup
.
enter
.
native=
"search"
clearable
@
clear=
"search"
></el-input>
<div
class=
"chat-list-scroll"
>
<el-scrollbar
class=
"h-100 no-bottom-scrollbar"
>
<div
v-for=
"item in chatRooms"
:key=
"item.chat_id"
class=
"chat-item"
:class=
"
{ selected: isSelected(item) }"
@click="goToChatRoom(item)"
>
<div
class=
"chat-avatar pos-rel"
:class=
"
{ 'red-dot': item.unread_msg_count > 0 }"
>
<fs-avatar
shape=
"circle"
:size=
"36"
:src=
"item.customer_avatar_url"
/>
</div>
<div
class=
"chat-info"
>
<div
class=
"
chat-info-left
d-flex
justify-content-between
align-items-center
"
>
<div
:title=
"item.customer_name"
class=
"chat-name flex-fill text-dot-dot-dot"
>
<!--
<span>
{{
>
<div
:title=
"item.customer_name"
class=
"chat-name flex-fill text-dot-dot-dot"
>
<!--
<span>
{{
item
.
customer_name
||
item
.
customer_mobile
||
item
.
customer_eid
}}
</span>
-->
<span>
{{
item
.
chat_id
}}
</span>
</div>
<div
v-if=
"item.last_msg_ts"
class=
"chat-time"
>
{{
formatTimestamp
(
item
.
last_msg_ts
)
}}
</div>
</div>
<div
class=
"chat-msg text-dot-dot-dot"
>
{{
parseMesage
(
item
)
}}
</div>
</div>
</div>
<div
class=
"empty"
v-if=
"chatRooms && chatRooms.length
<
=
0
"
>
{{
searchKeyword
?
"无相关接待"
:
"无接待"
}}
</div>
</el-scrollbar>
<span>
{{
item
.
chat_id
}}
</span>
</div>
<div
v-if=
"item.last_msg_ts"
class=
"chat-time"
>
{{
formatTimestamp
(
item
.
last_msg_ts
)
}}
</div>
</div>
<div
class=
"chat-msg text-dot-dot-dot"
>
{{
parseMesage
(
item
)
}}
</div>
</div>
</div>
</div>
<div
class=
"empty"
v-if=
"chatRooms && chatRooms.length
<
=
0
"
>
{{
searchKeyword
?
"无相关接待"
:
"无接待"
}}
</div>
</el-scrollbar>
</div>
</div>
</div>
</
template
>
<
script
lang=
"ts"
>
import
buttonThrottle
from
"@/utils/button-throttle"
;
//
import buttonThrottle from "@/utils/button-throttle";
import
{
Component
,
Prop
,
Vue
}
from
"vue-property-decorator"
;
import
{
chatStore
,
ChatStore
}
from
"@/customer-service/store/model"
;
...
...
@@ -80,225 +77,227 @@ import { formatTime, TimeFormatRule } from "@/customer-service/utils/time";
import
Chat
from
"@/customer-service/xim"
;
export
function
parserMessage
(
type
:
string
,
rawMsg
:
string
)
{
if
(
!
type
)
return
""
;
if
(
!
rawMsg
)
return
""
;
const
msg
=
JSON
.
parse
(
rawMsg
);
if
(
type
===
"text"
)
{
return
msg
.
text
;
}
else
if
(
type
===
"image"
)
{
return
`[图片]`
;
}
else
if
(
type
===
"file"
)
{
return
`[文件]`
;
}
else
{
return
`[不支持的消息格式]`
;
}
if
(
!
type
)
return
""
;
if
(
!
rawMsg
)
return
""
;
const
msg
=
JSON
.
parse
(
rawMsg
);
if
(
type
===
"text"
)
{
return
msg
.
text
;
}
else
if
(
type
===
"image"
)
{
return
`[图片]`
;
}
else
if
(
type
===
"file"
)
{
return
`[文件]`
;
}
else
{
return
`[不支持的消息格式]`
;
}
}
type
Chat
=
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
[
"list"
][
number
]
type
Chat
=
NonNullable
<
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
>
[
"list"
][
number
];
@
Component
({
components
:
{}
})
export
default
class
ChatList
extends
Vue
{
@
chatStore
.
Action
(
ChatStore
.
ACTION_GET_MY_CHAT_LIST
)
private
readonly
getMyChatList
!
:
ChatStore
.
ACTION_GET_MY_CHAT_LIST
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
)
private
readonly
currentChatUniplatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
@
chatStore
.
Action
(
ChatStore
.
ACTION_GET_MY_CHAT_LIST
)
private
readonly
getMyChatList
!
:
ChatStore
.
ACTION_GET_MY_CHAT_LIST
;
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_VERSION
)
private
readonly
uniplatVersion
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_VERSION
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
;
@
chatStore
.
State
(
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
)
private
readonly
chatList
!
:
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
)
private
readonly
currentChatUniplatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
;
@
chatStore
.
Action
(
ChatStore
.
ACTION_SAVE_CURRENT_CHAT_ID
_VERSION
)
private
readonly
saveChatId
!
:
ChatStore
.
ACTION_SAVE_CURRENT_CHAT_ID_VERSION
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT
_VERSION
)
private
readonly
uniplatVersion
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_VERSION
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_SAVE_MYSELF_ID
)
private
readonly
saveMyId
!
:
ChatStore
.
MUTATION_SAVE_MYSELF_ID
@
chatStore
.
State
(
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
)
private
readonly
chatList
!
:
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_SET_CHAT_SOURCE
)
private
readonly
setSource
!
:
ChatStore
.
MUTATION_SET_CHAT_SOURCE
@
chatStore
.
Action
(
ChatStore
.
ACTION_SAVE_CURRENT_CHAT_ID_VERSION
)
private
readonly
saveChatId
!
:
ChatStore
.
ACTION_SAVE_CURRENT_CHAT_ID_VERSION
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_SAVE_CHAT_TITLE
)
private
readonly
saveChatTitle
!
:
ChatStore
.
MUTATION_SAVE_CHAT_TITLE
@
Prop
({
type
:
String
,
default
:
"-1"
})
private
selected
!
:
string
private
searchKeyword
=
""
private
get
chatRooms
()
{
return
this
.
chatList
?.
list
||
[];
}
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_SAVE_MYSELF_ID
)
private
readonly
saveMyId
!
:
ChatStore
.
MUTATION_SAVE_MYSELF_ID
;
private
isSelected
(
item
:
Chat
)
{
if
(
this
.
currentChatUniplatId
)
{
return
item
.
uniplatId
===
this
.
currentChatUniplatId
;
}
return
this
.
selected
===
item
.
uniplatId
;
}
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_SET_CHAT_SOURCE
)
private
readonly
setSource
!
:
ChatStore
.
MUTATION_SET_CHAT_SOURCE
;
async
created
()
{
await
this
.
getMyChatList
();
this
.
setSource
(
ChatStore
.
StateChatSourceDirection
.
Server
);
this
.
selectFirstChat
();
}
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_SAVE_CHAT_TITLE
)
private
readonly
saveChatTitle
!
:
ChatStore
.
MUTATION_SAVE_CHAT_TITLE
;
mounted
()
{
this
.
saveMyId
();
this
.
goToOnlyRoom
();
}
@
Prop
({
type
:
String
,
default
:
"-1"
})
private
selected
!
:
string
;
private
goToOnlyRoom
()
{
if
(
this
.
chatRooms
.
length
===
1
)
{
const
wantedChat
=
this
.
chatRooms
[
0
];
this
.
goToChatRoom
(
wantedChat
);
}
}
private
searchKeyword
=
""
;
private
selectFirstChat
()
{
if
(
this
.
chatId
!=
null
)
return
;
if
(
!
this
.
chatRooms
.
length
)
return
;
const
{
chat_id
,
uniplat_version
,
uniplatId
}
=
this
.
chatRooms
[
0
];
this
.
saveChatId
({
chatId
:
chat_id
,
v
:
uniplat_version
,
uniplatId
,
});
}
private
get
chatRooms
()
{
return
this
.
chatList
?.
list
||
[];
}
@
buttonThrottle
()
private
async
search
()
{
this
.
searchKeyword
=
this
.
searchKeyword
.
trim
();
if
(
!
this
.
searchKeyword
)
{
await
this
.
getMyChatList
();
}
else
{
await
this
.
getMyChatList
(
this
.
searchKeyword
);
}
private
isSelected
(
item
:
Chat
)
{
if
(
this
.
currentChatUniplatId
)
{
return
item
.
uniplatId
===
this
.
currentChatUniplatId
;
}
private
goToChatRoom
(
data
:
Chat
)
{
if
(
this
.
currentChatUniplatId
===
data
.
uniplatId
)
{
return
;
}
this
.
saveChatId
({
chatId
:
this
.
chatRooms
.
find
((
k
)
=>
k
.
uniplatId
===
data
.
uniplatId
)
.
chat_id
,
v
:
data
.
uniplat_version
,
uniplatId
:
data
.
uniplatId
,
}).
finally
(
this
.
raiseChatIdChanged
);
this
.
saveChatTitle
(
data
.
uniplatId
);
return
this
.
selected
===
item
.
uniplatId
;
}
async
created
()
{
await
this
.
getMyChatList
();
this
.
setSource
(
ChatStore
.
StateChatSourceDirection
.
Server
);
this
.
selectFirstChat
();
}
mounted
()
{
this
.
saveMyId
();
this
.
goToOnlyRoom
();
}
private
goToOnlyRoom
()
{
if
(
this
.
chatRooms
.
length
===
1
)
{
const
wantedChat
=
this
.
chatRooms
[
0
];
this
.
goToChatRoom
(
wantedChat
);
}
private
raiseChatIdChanged
()
{
this
.
$emit
(
"change"
);
}
private
parseMesage
(
data
:
Chat
)
{
return
parserMessage
(
data
.
msg_type
,
data
.
msg
);
}
private
selectFirstChat
()
{
if
(
this
.
chatId
!=
null
)
return
;
if
(
!
this
.
chatRooms
.
length
)
return
;
const
{
chat_id
,
uniplat_version
,
uniplatId
}
=
this
.
chatRooms
[
0
];
this
.
saveChatId
({
chatId
:
chat_id
,
v
:
uniplat_version
,
uniplatId
,
});
}
// @buttonThrottle()
private
async
search
()
{
this
.
searchKeyword
=
this
.
searchKeyword
.
trim
();
if
(
!
this
.
searchKeyword
)
{
await
this
.
getMyChatList
();
}
else
{
await
this
.
getMyChatList
(
this
.
searchKeyword
);
}
}
private
formatTimestamp
(
v
:
number
)
{
return
formatTime
(
v
,
{
short
:
true
,
rule
:
TimeFormatRule
.
Hour12
});
private
goToChatRoom
(
data
:
Chat
)
{
if
(
this
.
currentChatUniplatId
===
data
.
uniplatId
)
{
return
;
}
const
wantedChatRoom
=
this
.
chatRooms
.
find
(
(
k
)
=>
k
.
uniplatId
===
data
.
uniplatId
);
if
(
wantedChatRoom
==
null
)
return
;
this
.
saveChatId
({
chatId
:
wantedChatRoom
.
chat_id
,
v
:
data
.
uniplat_version
,
uniplatId
:
data
.
uniplatId
,
}).
finally
(
this
.
raiseChatIdChanged
);
this
.
saveChatTitle
(
data
.
uniplatId
);
}
private
raiseChatIdChanged
()
{
this
.
$emit
(
"change"
);
}
private
parseMesage
(
data
:
Chat
)
{
return
parserMessage
(
data
.
msg_type
,
data
.
msg
);
}
private
formatTimestamp
(
v
:
number
)
{
return
formatTime
(
v
,
{
short
:
true
,
rule
:
TimeFormatRule
.
Hour12
});
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.chat-list-con
{
.title
{
padding-left
:
20px
;
line-height
:
59px
;
font-size
:
18px
;
border-bottom
:
1px
solid
#e1e1e1
;
}
.title
{
padding-left
:
20px
;
line-height
:
59px
;
font-size
:
18px
;
border-bottom
:
1px
solid
#e1e1e1
;
}
}
.chat-list
{
text-align
:
center
;
text-align
:
center
;
}
.chat-list-scroll
{
height
:
calc
(
100%
-
50px
);
height
:
calc
(
100%
-
50px
);
.empty
{
margin-top
:
100%
;
}
.empty
{
margin-top
:
100%
;
}
}
.keyword-input
{
width
:
200px
;
margin
:
15px
0
;
/deep/
.el-input__inner
{
font-size
:
13px
;
height
:
30px
;
line-height
:
30px
;
border-radius
:
15px
;
padding-right
:
15px
;
}
/
deep
/
.el-icon-time
{
background
:
transparent
;
}
width
:
200px
;
margin
:
15px
0
;
/deep/
.el-input__inner
{
font-size
:
13px
;
height
:
30px
;
line-height
:
30px
;
border-radius
:
15px
;
padding-right
:
15px
;
}
/
deep
/
.el-icon-time
{
background
:
transparent
;
}
}
.chat-list
{
.chat-item
{
cursor
:
pointer
;
padding
:
10px
15px
;
&:hover
{
background
:
#e4f0ff
;
}
&
.selected
{
background
:
#f0f0f0
;
}
.chat-avatar
{
display
:
inline-block
;
vertical-align
:
middle
;
width
:
36px
;
height
:
36px
;
margin-right
:
15px
;
&.
red-dot
::
before
{
content
:
""
;
position
:
absolute
;
width
:
8px
;
height
:
8px
;
background
:
#e87005
;
border-radius
:
50%
;
z-index
:
1
;
right
:
-4px
;
top
:
-4px
;
}
}
.chat-info
{
display
:
inline-block
;
vertical-align
:
middle
;
width
:
calc
(
100%
-
51px
);
}
.chat-info-left
{
text-align
:
start
;
font-size
:
14px
;
line-height
:
20px
;
color
:
#333333
;
margin-bottom
:
10px
;
.chat-time
{
text-align
:
end
;
flex
:
none
;
color
:
#999999
;
margin-left
:
10px
;
font-size
:
13px
;
line-height
:
1
;
}
}
.chat-msg
{
color
:
#888
;
text-align
:
start
;
height
:
16px
;
margin-top
:
-3px
;
}
.chat-item
{
cursor
:
pointer
;
padding
:
10px
15px
;
&:hover
{
background
:
#e4f0ff
;
}
&
.selected
{
background
:
#f0f0f0
;
}
.chat-avatar
{
display
:
inline-block
;
vertical-align
:
middle
;
width
:
36px
;
height
:
36px
;
margin-right
:
15px
;
&.
red-dot
::
before
{
content
:
""
;
position
:
absolute
;
width
:
8px
;
height
:
8px
;
background
:
#e87005
;
border-radius
:
50%
;
z-index
:
1
;
right
:
-4px
;
top
:
-4px
;
}
}
.chat-info
{
display
:
inline-block
;
vertical-align
:
middle
;
width
:
calc
(
100%
-
51px
);
}
.chat-info-left
{
text-align
:
start
;
font-size
:
14px
;
line-height
:
20px
;
color
:
#333333
;
margin-bottom
:
10px
;
.chat-time
{
text-align
:
end
;
flex
:
none
;
color
:
#999999
;
margin-left
:
10px
;
font-size
:
13px
;
line-height
:
1
;
}
}
.chat-msg
{
color
:
#888
;
text-align
:
start
;
height
:
16px
;
margin-top
:
-3px
;
}
}
}
</
style
>
chat-room.vue
View file @
86932012
<
template
>
<div
class=
"chat-room-con h-100"
>
<div
class=
"room-title d-flex justify-content-between align-items-center"
>
<div
class=
"title"
@
click=
"showMembers"
>
{{
chatTitle
}}
<template
v-if=
"chatMembers.length"
>
<span
class=
"members-count"
>
(成员
{{
chatMembers
.
length
}}
人)
</span
>
<i
v-if=
"membersPanelVisibility"
class=
"title-right-arrow fast-service-icon-arrow-up"
/>
<i
v-else
class=
"title-right-arrow fast-service-icon-arrow-down"
/>
</
template
>
<
template
v-if=
"!notOnlyCheck"
>
<div
v-if=
"currentChat.is_finish"
class=
"chat-status chat-done"
>
已完成
</div>
<div
v-else
class=
"chat-status"
>
接待中
</div>
</
template
>
</div>
<i
v-if=
"close"
@
click=
"close"
class=
"title-close el-icon-close"
/>
</div>
<div
v-if=
"membersPanelVisibility"
class=
"chat-members"
>
<div
class=
"chat-member"
v-for=
"item in chatMembers"
:key=
"item.id"
>
<fs-avatar
shape=
"circle"
:src=
"item.avatar"
:size=
"40"
/>
<div
class=
"member-name"
>
{{ item.name || item.eid }}
</div>
</div>
<div
class=
"chat-room-con h-100"
>
<div
class=
"room-title d-flex justify-content-between align-items-center"
>
<div
class=
"title"
@
click=
"showMembers"
>
{{
chatTitle
}}
<template
v-if=
"chatMembers.length"
>
<span
class=
"members-count"
>
(成员
{{
chatMembers
.
length
}}
人)
</span>
<i
v-if=
"membersPanelVisibility"
class=
"title-right-arrow fast-service-icon-arrow-up"
/>
<i
v-else
class=
"title-right-arrow fast-service-icon-arrow-down"
/>
</
template
>
<
template
v-if=
"!notOnlyCheck"
>
<div
v-if=
"currentChat.is_finish"
class=
"chat-status chat-done"
>
已完成
</div>
<div
v-else
class=
"chat-status"
>
接待中
</div>
</
template
>
</div>
<i
v-if=
"close"
@
click=
"close"
class=
"title-close el-icon-close"
/>
</div>
<div
v-if=
"membersPanelVisibility"
class=
"chat-members"
>
<div
class=
"chat-member"
v-for=
"item in chatMembers"
:key=
"item.id"
>
<fs-avatar
shape=
"circle"
:src=
"item.avatar"
:size=
"40"
/>
<div
class=
"member-name"
>
{{ item.name || item.eid }}
</div>
<div
class=
"chat-panel"
>
<div
class=
"chat-area h-100"
>
<
template
v-if=
"notOnlyCheck"
>
<div
class=
"chat-messages pos-rel"
>
<div
v-if=
"getCurrentInputingPeople.length"
class=
"someone-inputing"
>
{{
getCurrentInputingPeople
}}
正在输入
</div>
<messages
/>
</div>
<div
class=
"chat-input"
>
<message-input
@
error=
"onError"
/>
</div>
</
template
>
<
template
v-else
>
<messages
/>
</
template
>
</div>
</div>
<div
class=
"chat-panel"
>
<div
class=
"chat-area h-100"
>
<
template
v-if=
"notOnlyCheck"
>
<div
class=
"chat-messages pos-rel"
>
<div
v-if=
"getCurrentInputingPeople.length"
class=
"someone-inputing"
>
{{
getCurrentInputingPeople
}}
正在输入
</div>
<!-- <div class="chat-info h-100 pos-rel">
<messages
/>
</div>
<div
class=
"chat-input"
>
<message-input
@
error=
"onError"
/>
</div>
</
template
>
<
template
v-else
>
<messages
/>
</
template
>
</div>
<!-- <div class="chat-info h-100 pos-rel">
<div class="info-tabs">
<div
@click="activeTab = 'customer'"
...
...
@@ -75,22 +65,22 @@
订单
</div>
</div> -->
<!-- <cusomter-info v-if="customerInfoTabShow" /> -->
<!-- <div v-else class="order-info-con">
<!-- <cusomter-info v-if="customerInfoTabShow" /> -->
<!-- <div v-else class="order-info-con">
<order-info />
</div> -->
<!-- </div> -->
</div>
<!-- </div> -->
</div>
</div>
</template>
<
script
lang=
"ts"
>
import
{
Component
,
Prop
,
Provide
,
Ref
,
Vue
,
Watch
,
Component
,
Prop
,
Provide
,
Ref
,
Vue
,
Watch
,
}
from
"vue-property-decorator"
;
import
MessageInput
from
"@/customer-service/message-input.vue"
;
...
...
@@ -99,205 +89,206 @@ import messages from "@/customer-service/message-list.vue";
// import OrderInfo from "./order-info.vue"
import
{
ChatStore
,
chatStore
}
from
"@/customer-service/store/model"
;
type
RoomInfoTab
=
"customer"
|
"order"
type
RoomInfoTab
=
"customer"
|
"order"
;
@
Component
({
components
:
{
MessageInput
,
messages
,
// CusomterInfo,
// OrderInfo,
},
components
:
{
MessageInput
,
messages
,
// CusomterInfo,
// OrderInfo,
},
})
export
default
class
ChatRoom
extends
Vue
{
@
chatStore
.
Getter
(
ChatStore
.
GETTER_CURRENT_CHAT_PRESENT_MEMBERS
)
private
readonly
chatMembers
!
:
ChatStore
.
GETTER_CURRENT_CHAT_PRESENT_MEMBERS
@
chatStore
.
Getter
(
ChatStore
.
GETTER_CURRENT_CHAT_PRESENT_MEMBERS
)
private
readonly
chatMembers
!
:
ChatStore
.
GETTER_CURRENT_CHAT_PRESENT_MEMBERS
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_CLEAR_CURRENT_CHAT_MEMBERS
)
private
readonly
clearChatMembers
!
:
ChatStore
.
MUTATION_CLEAR_CURRENT_CHAT_MEMBERS
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_CLEAR_CURRENT_CHAT_MEMBERS
)
private
readonly
clearChatMembers
!
:
ChatStore
.
MUTATION_CLEAR_CURRENT_CHAT_MEMBERS
;
@
chatStore
.
State
(
ChatStore
.
STATE_CURRENT_CHAT_TITLE
)
private
readonly
chatTitle
!
:
ChatStore
.
STATE_CURRENT_CHAT_TITLE
@
chatStore
.
State
(
ChatStore
.
STATE_CURRENT_CHAT_TITLE
)
private
readonly
chatTitle
!
:
ChatStore
.
STATE_CURRENT_CHAT_TITLE
;
@
chatStore
.
State
(
ChatStore
.
STATE_CURRENT_CHAT_INPUTING
)
private
readonly
currentInputPeople
!
:
ChatStore
.
STATE_CURRENT_CHAT_INPUTING
@
chatStore
.
State
(
ChatStore
.
STATE_CURRENT_CHAT_INPUTING
)
private
readonly
currentInputPeople
!
:
ChatStore
.
STATE_CURRENT_CHAT_INPUTING
;
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
)
private
readonly
currentChatUniplatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
)
private
readonly
currentChatUniplatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
;
@
chatStore
.
State
(
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
)
private
readonly
myChatList
!
:
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
@
chatStore
.
State
(
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
)
private
readonly
myChatList
!
:
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
;
private
allChatList
=
{
list
:
[]
}
private
allChatList
=
{
list
:
[]
};
@
Prop
({
type
:
Function
})
private
close
?:
()
=>
void
@
Prop
({
type
:
Function
})
private
close
?:
()
=>
void
;
@
Provide
()
showReadSummary
=
true
@
Provide
()
showReadSummary
=
true
;
@
Watch
(
"currentChatUniplatId"
)
private
whenCurrentChatIdChanged
(
newValue
:
string
,
oldValue
:
string
)
{
if
(
Number
(
oldValue
)
===
Number
(
newValue
))
return
;
this
.
hideMembers
();
this
.
clearChatMembers
();
}
@
Watch
(
"currentChatUniplatId"
)
private
whenCurrentChatIdChanged
(
newValue
:
string
,
oldValue
:
string
)
{
if
(
Number
(
oldValue
)
===
Number
(
newValue
))
return
;
this
.
hideMembers
();
this
.
clearChatMembers
();
}
private
activeTab
:
RoomInfoTab
=
"customer"
private
membersPanelVisibility
=
false
private
activeTab
:
RoomInfoTab
=
"customer"
;
private
membersPanelVisibility
=
false
;
private
get
getCurrentInputingPeople
()
{
return
this
.
currentInputPeople
.
map
((
k
)
=>
""
/* this.userInfo[k].name */
)
.
join
(
"、"
);
}
private
get
getCurrentInputingPeople
()
{
return
this
.
currentInputPeople
.
map
((
k
)
=>
""
/* this.userInfo[k].name */
)
.
join
(
"、"
);
}
private
get
currentChat
()
{
const
chatId
=
this
.
currentChatUniplatId
;
let
result
=
this
.
myChatList
.
list
.
find
((
k
)
=>
k
.
uniplatId
===
chatId
);
if
(
result
)
return
result
;
result
=
this
.
allChatList
.
list
.
find
((
k
)
=>
k
.
uniplatId
===
chatId
);
return
result
??
{};
}
private
get
currentChat
()
{
const
chatId
=
this
.
currentChatUniplatId
;
if
(
this
.
myChatList
==
null
)
return
;
let
result
=
this
.
myChatList
.
list
.
find
((
k
)
=>
k
.
uniplatId
===
chatId
);
// if (result) return result;
// result = this.allChatList.list.find((k) => k.uniplatId === chatId);
return
result
??
{};
}
private
get
notOnlyCheck
():
boolean
{
return
true
;
}
private
get
notOnlyCheck
():
boolean
{
return
true
;
}
private
get
customerInfoTabShow
()
{
return
this
.
activeTab
===
"customer"
;
}
private
get
customerInfoTabShow
()
{
return
this
.
activeTab
===
"customer"
;
}
private
get
orderInfoTabShow
()
{
return
this
.
activeTab
===
"order"
;
}
private
get
orderInfoTabShow
()
{
return
this
.
activeTab
===
"order"
;
}
private
showMembers
()
{
this
.
membersPanelVisibility
=
!
this
.
membersPanelVisibility
;
}
private
showMembers
()
{
this
.
membersPanelVisibility
=
!
this
.
membersPanelVisibility
;
}
private
hideMembers
()
{
this
.
membersPanelVisibility
=
false
;
}
private
hideMembers
()
{
this
.
membersPanelVisibility
=
false
;
}
private
onError
(
msg
:
string
)
{
// eslint-disable-next-line no-console
console
.
error
(
msg
);
this
.
$message
.
error
(
msg
);
}
private
onError
(
msg
:
string
)
{
// eslint-disable-next-line no-console
console
.
error
(
msg
);
this
.
$message
.
error
(
msg
);
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.room-title
{
font-size
:
16px
;
padding
:
0
20px
;
height
:
60px
;
border-bottom
:
1px
solid
#e1e1e1
;
.title
{
cursor
:
pointer
;
}
.members-count
{
color
:
#666666
;
}
.title-right-arrow
{
font-size
:
10px
;
margin-left
:
10px
;
vertical-align
:
middle
;
color
:
#666
;
}
.title-close
{
color
:
#8d959d
;
cursor
:
pointer
;
}
font-size
:
16px
;
padding
:
0
20px
;
height
:
60px
;
border-bottom
:
1px
solid
#e1e1e1
;
.title
{
cursor
:
pointer
;
}
.members-count
{
color
:
#666666
;
}
.title-right-arrow
{
font-size
:
10px
;
margin-left
:
10px
;
vertical-align
:
middle
;
color
:
#666
;
}
.title-close
{
color
:
#8d959d
;
cursor
:
pointer
;
}
}
.chat-status
{
display
:
inline-block
;
width
:
46px
;
height
:
20px
;
line-height
:
20px
;
background
:
#22bd7a
;
font-size
:
13px
;
border-radius
:
2px
;
color
:
#ffffff
;
text-align
:
center
;
margin-left
:
10px
;
&.chat-done
{
background
:
#c5d4e5
;
}
display
:
inline-block
;
width
:
46px
;
height
:
20px
;
line-height
:
20px
;
background
:
#22bd7a
;
font-size
:
13px
;
border-radius
:
2px
;
color
:
#ffffff
;
text-align
:
center
;
margin-left
:
10px
;
&.chat-done
{
background
:
#c5d4e5
;
}
}
.chat-members
{
position
:
absolute
;
width
:
calc
(
100%
-
350px
);
padding
:
30px
;
padding-bottom
:
0
;
background
:
#fff
;
z-index
:
1
;
border-bottom
:
1px
solid
#f0f0f0
;
.chat-member
{
text-align
:
center
;
display
:
inline-block
;
vertical-align
:
top
;
margin-bottom
:
30px
;
margin-right
:
30px
;
}
.member-name
{
margin-top
:
10px
;
}
position
:
absolute
;
width
:
calc
(
100%
-
350px
);
padding
:
30px
;
padding-bottom
:
0
;
background
:
#fff
;
z-index
:
1
;
border-bottom
:
1px
solid
#f0f0f0
;
.chat-member
{
text-align
:
center
;
display
:
inline-block
;
vertical-align
:
top
;
margin-bottom
:
30px
;
margin-right
:
30px
;
}
.member-name
{
margin-top
:
10px
;
}
}
.chat-panel
{
height
:
calc
(
100%
-
60px
);
.chat-area,
.chat-info
{
display
:
inline-block
;
vertical-align
:
top
;
}
.chat-area
{
width
:
100%
;
}
.chat-info
{
width
:
349px
;
border-left
:
1px
solid
#e1e1e1
;
}
height
:
calc
(
100%
-
60px
);
.chat-area,
.chat-info
{
display
:
inline-block
;
vertical-align
:
top
;
}
.chat-area
{
width
:
100%
;
}
.chat-info
{
width
:
349px
;
border-left
:
1px
solid
#e1e1e1
;
}
}
.info-tabs
{
height
:
40px
;
line-height
:
40px
;
border-bottom
:
1px
solid
#f0f0f0
;
height
:
40px
;
line-height
:
40px
;
border-bottom
:
1px
solid
#f0f0f0
;
.info-tab
{
display
:
inline-block
;
vertical-align
:
top
;
width
:
50%
;
text-align
:
center
;
font-size
:
15px
;
color
:
#333
;
cursor
:
pointer
;
.info-tab
{
display
:
inline-block
;
vertical-align
:
top
;
width
:
50%
;
text-align
:
center
;
font-size
:
15px
;
color
:
#333
;
cursor
:
pointer
;
&.active
{
font-weight
:
600
;
}
&.active
{
font-weight
:
600
;
}
}
}
.chat-area
{
$
input-height
:
130px
;
$
input-height
:
130px
;
.chat-messages
{
height
:
calc
(
100%
-
130px
+
1px
);
border-bottom
:
1px
solid
#e1e1e1
;
}
.chat-messages
{
height
:
calc
(
100%
-
130px
+
1px
);
border-bottom
:
1px
solid
#e1e1e1
;
}
.chat-input
{
height
:
$
input-height
;
}
.chat-input
{
height
:
$
input-height
;
}
}
.order-info-con
{
height
:
calc
(
100%
-
40px
);
height
:
calc
(
100%
-
40px
);
}
.someone-inputing
{
position
:
absolute
;
left
:
20px
;
bottom
:
20px
;
z-index
:
1
;
color
:
#c2c2c2
;
position
:
absolute
;
left
:
20px
;
bottom
:
20px
;
z-index
:
1
;
color
:
#c2c2c2
;
}
</
style
>
chat.vue
View file @
86932012
<
template
>
<el-dialog
class=
"chat-dialog-con"
:close-on-click-modal=
"false"
:visible=
"visible"
@
close=
"hide"
>
<div
class=
"chat-con h-100"
>
<div
class=
"h-100 chat-list"
>
<chat-list
/>
</div>
<div
v-if=
"chatId != null"
class=
"h-100 chat-area"
>
<chat-room
:close=
"hide"
/>
</div>
<div
class=
"chat-panel h-100"
>
<el-button
@
click=
"terminate"
>
结束
</el-button>
</div>
</div>
</el-dialog>
<el-dialog
class=
"chat-dialog-con"
:close-on-click-modal=
"false"
:visible=
"visible"
@
close=
"hide"
>
<div
class=
"chat-con h-100"
>
<div
class=
"h-100 chat-list"
>
<chat-list
/>
</div>
<div
v-if=
"chatId != null"
class=
"h-100 chat-area"
>
<chat-room
:close=
"hide"
/>
</div>
<div
class=
"chat-panel h-100"
>
<el-button
@
click=
"terminate"
>
结束
</el-button>
</div>
</div>
</el-dialog>
</
template
>
<
script
lang=
"ts"
>
import
{
Component
,
Vue
}
from
"vue-property-decorator"
;
...
...
@@ -29,52 +29,52 @@ import { ChatStore, chatStore } from "@/customer-service/store/model";
@
Component
({
components
:
{
MessageList
,
ChatRoom
,
ChatList
}
})
export
default
class
Chat
extends
Vue
{
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
;
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_DIALOG_VISIBLE
)
private
readonly
visible
:
ChatStore
.
STATE_CHAT_DIALOG_VISIBLE
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_DIALOG_VISIBLE
)
private
readonly
visible
!
:
ChatStore
.
STATE_CHAT_DIALOG_VISIBLE
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_HIDE_CHAT
)
private
readonly
hide
:
ChatStore
.
MUTATION_HIDE_CHAT
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_HIDE_CHAT
)
private
readonly
hide
!
:
ChatStore
.
MUTATION_HIDE_CHAT
;
@
chatStore
.
Action
(
ChatStore
.
ACTION_TERINATE_CHAT
)
private
readonly
_terminate
:
ChatStore
.
ACTION_TERINATE_CHAT
@
chatStore
.
Action
(
ChatStore
.
ACTION_TERINATE_CHAT
)
private
readonly
_terminate
!
:
ChatStore
.
ACTION_TERINATE_CHAT
;
private
terminate
()
{
this
.
_terminate
();
}
private
terminate
()
{
this
.
_terminate
();
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.chat-dialog-con
{
/deep/
.el-dialog__header
{
display
:
none
;
}
/
deep
/
.el-dialog
{
width
:
80%
;
max-width
:
1200px
;
}
--chat-side-width
:
200px
;
/deep/
.el-dialog__header
{
display
:
none
;
}
/
deep
/
.el-dialog
{
width
:
80%
;
max-width
:
1200px
;
}
--chat-side-width
:
200px
;
}
.chat-con
{
height
:
500px
;
height
:
500px
;
}
.chat-list
,
.chat-area
,
.chat-panel
{
display
:
inline-block
;
vertical-align
:
top
;
display
:
inline-block
;
vertical-align
:
top
;
}
.chat-list
{
width
:
var
(
--chat-side-width
);
border-right
:
1px
solid
#e1e1e1
;
width
:
var
(
--chat-side-width
);
border-right
:
1px
solid
#e1e1e1
;
}
.chat-area
{
width
:
calc
(
100%
-
2
*
var
(
--chat-side-width
)
-
2px
);
width
:
calc
(
100%
-
2
*
var
(
--chat-side-width
)
-
2px
);
}
.chat-panel
{
width
:
var
(
--chat-side-width
);
border-left
:
1px
solid
#e1e1e1
;
width
:
var
(
--chat-side-width
);
border-left
:
1px
solid
#e1e1e1
;
}
</
style
>
components/message.vue
View file @
86932012
<
template
>
<div
class=
"message-con d-flex"
:class=
"
isMyMessage
? 'my-message align-items-start flex-row-reverse'
: 'align-items-start'
"
>
<span
class=
"no-selection"
>
<fs-avatar
:size=
"40"
class=
"msg-avatar"
:shape=
"shape"
:src=
"avatar"
/>
</span>
<div
class=
"message-con d-flex"
:class=
"
isMyMessage
? 'my-message align-items-start flex-row-reverse'
: 'align-items-start'
"
>
<span
class=
"no-selection"
>
<fs-avatar
:size=
"40"
class=
"msg-avatar"
:shape=
"shape"
:src=
"avatar"
/>
</span>
<div
class=
"msg-content"
>
<div
class=
"msg-name no-selection"
>
{{
username
}}
</div>
<!-- Image -->
<div
class=
"msg-detail image-message"
:class=
"
{ 'image-404': fileFailed2Load }"
v-if="messageType === 'image'"
@dblclick="openFile"
>
<img
v-if=
"messageRealUrl"
:src=
"messageRealUrl"
:title=
"messageBody.msg.name"
:alt=
"messageBody.msg.name"
/>
<file-icon
v-else-if=
"fileFailed2Load"
:value=
"image404"
></file-icon>
</div>
<!-- File -->
<div
class=
"msg-detail file-message d-flex"
v-else-if=
"messageType === 'file'"
@
dblclick=
"openFile"
>
<div
class=
"file-message-info"
>
<div
class=
"text-nowrap text-truncate file-message-name"
:title=
"messageBody.msg.name"
>
{{
messageBody
.
msg
.
name
}}
</div>
<div
class=
"text-hint"
>
{{
messageBody
.
msg
.
size
|
formatSize
}}
</div>
</div>
<file-icon
:value=
"fileIcon"
></file-icon>
</div>
<!-- Audio -->
<div
class=
"msg-detail voice-message d-flex align-items-center"
:class=
"
{ playing: playing, 'can-play': messageRealUrl }"
v-else-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div
class=
"d-flex align-items-center"
v-if=
"messageRealUrl"
>
<voice-icon
:loading=
"playing"
></voice-icon>
<audio
ref=
"audio"
@
play=
"onPlay"
@
pause=
"onPause"
>
<source
type=
"audio/aac"
:src=
"messageRealUrl"
/>
</audio>
<span
v-if=
"duration"
class=
"duration text-nowrap text-hint"
>
{{
durationInSecond
}}
s
</span
>
</div>
<div
class=
"msg-content"
>
<div
class=
"msg-name no-selection"
>
{{
username
}}
</div>
<!-- Image -->
<div
class=
"msg-detail image-message"
:class=
"
{ 'image-404': fileFailed2Load }"
v-if="messageType === 'image'"
@dblclick="openFile"
>
<img
v-if=
"messageRealUrl"
:src=
"messageRealUrl"
:title=
"messageBody.msg.name"
:alt=
"messageBody.msg.name"
/>
<file-icon
v-else-if=
"fileFailed2Load"
:value=
"image404"
></file-icon>
</div>
<!-- File -->
<div
class=
"msg-detail file-message d-flex"
v-else-if=
"messageType === 'file'"
@
dblclick=
"openFile"
>
<div
class=
"file-message-info"
>
<div
class=
"text-nowrap text-truncate file-message-name"
:title=
"messageBody.msg.name"
>
{{
messageBody
.
msg
.
name
}}
</div>
<div
class=
"text-hint"
>
{{
messageBody
.
msg
.
size
|
formatSize
}}
</div>
</div>
<file-icon
:value=
"fileIcon"
></file-icon>
</div>
<!-- Audio -->
<div
class=
"msg-detail voice-message d-flex align-items-center"
:class=
"
{ playing: playing, 'can-play': messageRealUrl }"
v-else-if="messageType === 'voice'"
@click.stop="play"
:style="{ width: getVoiceMessageWidth + 'px' }"
>
<div
class=
"d-flex align-items-center"
v-if=
"messageRealUrl"
>
<voice-icon
:loading=
"playing"
></voice-icon>
<audio
ref=
"audio"
@
play=
"onPlay"
@
pause=
"onPause"
>
<source
type=
"audio/aac"
:src=
"messageRealUrl"
/>
</audio>
<span
v-if=
"duration"
class=
"duration text-nowrap text-hint"
>
{{
durationInSecond
}}
s
</span
>
</div>
<i
class=
"el-icon-warning-outline"
v-else-if=
"fileFailed2Load"
title=
"[语音加载失败]"
></i>
</div>
<!-- Video -->
<div
class=
"
<i
class=
"el-icon-warning-outline"
v-else-if=
"fileFailed2Load"
title=
"[语音加载失败]"
></i>
</div>
<!-- Video -->
<div
class=
"
msg-detail
video-message
d-flex
align-items-center
justify-content-center
"
v-else-if=
"messageType === 'video'"
>
<video-player-icon
@
click
.
native=
"openFile"
></video-player-icon>
</div>
<!-- Text -->
<div
class=
"msg-detail inline-text"
v-else
v-html=
"format2Link(messageBody.msg.text || emptyText)"
></div>
</div>
v-else-if=
"messageType === 'video'"
>
<video-player-icon
@
click
.
native=
"openFile"
></video-player-icon>
</div>
<!-- Text -->
<div
class=
"msg-detail inline-text"
v-else
v-html=
"format2Link(messageBody.msg.text || emptyText)"
></div>
</div>
<a
v-if=
"!isSendingMessage && messageType === 'file'"
class=
"
<a
v-if=
"!isSendingMessage && messageType === 'file'"
class=
"
d-flex
align-items-center
justify-content-center
download-icon
"
:href=
"messageRealUrl | downloadUrl(getAttachment)"
:download=
"getAttachment"
title=
"下载文件"
>
<img
src=
"~@/customer-service/imgs/download.png"
alt=
"Download"
/>
</a>
<i
class=
"el-icon-warning text-danger"
v-if=
"failed"
title=
"发送失败"
></i>
<i
class=
"el-icon-loading"
v-else-if=
"isSendingMessage"
></i>
<template
v-if=
"showReadSummary"
>
<div
v-if=
"isMyMessage"
class=
"msg-read pos-rel"
>
<span
@
click=
"readListVisibility = true"
class=
"pointer"
>
<template
v-if=
"isAllRead"
>
全部已读
</
template
>
<
template
v-else-if=
"data.read_count > 0"
>
{{
data
.
read_count
}}
人已读
</
template
>
<
template
v-else
>
未读
</
template
>
</span>
<who-read-list
v-if=
"readListVisibility"
@
blur=
"readListVisibility = false"
:msgId=
"data.id"
/>
</div>
</template>
</div>
:href=
"messageRealUrl | downloadUrl(getAttachment)"
:download=
"getAttachment"
title=
"下载文件"
>
<img
src=
"~@/customer-service/imgs/download.png"
alt=
"Download"
/>
</a>
<i
class=
"el-icon-warning text-danger"
v-if=
"failed"
title=
"发送失败"
></i>
<i
class=
"el-icon-loading"
v-else-if=
"isSendingMessage"
></i>
<template
v-if=
"showReadSummary"
>
<div
v-if=
"isMyMessage"
class=
"msg-read pos-rel"
>
<span
@
click=
"readListVisibility = true"
class=
"pointer"
>
<template
v-if=
"isAllRead"
>
全部已读
</
template
>
<
template
v-else-if=
"data.read_count > 0"
>
{{
data
.
read_count
}}
人已读
</
template
>
<
template
v-else
>
未读
</
template
>
</span>
<who-read-list
v-if=
"readListVisibility"
@
blur=
"readListVisibility = false"
:msgId=
"data.id"
/>
</div>
</template>
</div>
</template>
<
script
lang=
"ts"
>
...
...
@@ -150,13 +138,13 @@ import { replaceText2Link } from "../utils";
import
chat
from
"./../xim"
;
import
{
FileType
,
getFileType
,
isAudio
,
isImage
,
isVideo
,
MAX_FILE_SIZE
,
MAX_IMAGE_SIZE
,
FileType
,
getFileType
,
isAudio
,
isImage
,
isVideo
,
MAX_FILE_SIZE
,
MAX_IMAGE_SIZE
,
}
from
"./file-controller"
;
import
FileIcon
from
"./file-icon.vue"
;
import
VideoPlayerIcon
from
"./video-player-icon.vue"
;
...
...
@@ -166,459 +154,458 @@ import WhoReadList from "./who-read-list.vue";
import
{
chatStore
,
ChatStore
}
from
"@/customer-service/store/model"
;
@
Component
({
components
:
{
FileIcon
,
VoiceIcon
,
WhoReadList
,
VideoPlayerIcon
},
components
:
{
FileIcon
,
VoiceIcon
,
WhoReadList
,
VideoPlayerIcon
},
})
export
default
class
Message
extends
Vue
{
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_MY_ID
)
private
readonly
chatMyId
!
:
ChatStore
.
STATE_CHAT_MY_ID
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_MY_ID
)
private
readonly
chatMyId
!
:
ChatStore
.
STATE_CHAT_MY_ID
;
@
chatStore
.
Getter
(
ChatStore
.
GETTER_CURRENT_CHAT_PRESENT_MEMBERS
)
private
readonly
chatMembers
!
:
ChatStore
.
GETTER_CURRENT_CHAT_PRESENT_MEMBERS
;
@
chatStore
.
Getter
(
ChatStore
.
GETTER_CURRENT_CHAT_PRESENT_MEMBERS
)
private
readonly
chatMembers
!
:
ChatStore
.
GETTER_CURRENT_CHAT_PRESENT_MEMBERS
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_SOURCE
)
private
readonly
chatSource
!
:
ChatStore
.
STATE_CHAT_SOURCE
;
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_SOURCE
)
private
readonly
chatSource
!
:
ChatStore
.
STATE_CHAT_SOURCE
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
;
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
/**
* tbd: 文件消息所在的域名的url,逻辑需要补充
*/
private
messageRealUrl
=
""
;
private
fileFailed2Load
=
false
;
private
image404
=
FileType
.
Image_404
;
private
loadingRealUrl
=
false
;
/**
* tbd: 文件消息所在的域名的url,逻辑需要补充
*/
private
messageRealUrl
=
""
private
fileFailed2Load
=
false
private
image404
=
FileType
.
Image_404
private
loadingRealUrl
=
false
@
Prop
({
type
:
Object
,
default
:
()
=>
Object
.
create
(
null
)
})
private
data
!
:
dto
.
Message
;
@
Prop
({
type
:
Object
,
default
:
()
=>
Object
.
create
(
null
)
}
)
private
data
!
:
dto
.
Message
@
Prop
(
)
private
isSendingMessage
!
:
boolean
;
@
Prop
()
private
isSendingMessage
!
:
boolean
@
Prop
()
private
failed
!
:
boolean
;
@
Prop
(
)
private
failed
!
:
boolean
@
Prop
({
default
:
"circle"
}
)
private
shape
!
:
string
;
@
Prop
({
default
:
"circle"
}
)
private
shape
!
:
string
@
Ref
(
"audio"
)
private
readonly
audioRef
!
:
HTMLAudioElement
;
@
Ref
(
"audio"
)
private
readonly
audioRef
!
:
HTMLAudioElement
private
playing
=
false
;
private
playing
=
false
@
Inject
({
default
:
false
})
readonly
showReadSummary
!
:
boolean
;
@
Inject
({
default
:
false
})
readonly
showReadSummary
!
:
boolean
private
emptyText
=
" "
;
private
emptyText
=
" "
private
readListVisibility
=
false
;
private
readListVisibility
=
false
private
org
=
""
;
private
org
=
""
private
get
isAllRead
()
{
return
this
.
data
.
read_count
>=
this
.
data
.
total_read_count
;
}
private
get
isAllRead
()
{
return
this
.
data
.
read_count
>=
this
.
data
.
total_read_count
;
private
get
messageBody
():
{
eid
?:
string
;
oid
?:
string
;
msg
:
any
}
{
if
(
this
.
data
)
{
try
{
return
{
...
this
.
data
,
msg
:
JSON
.
parse
(
this
.
data
.
msg
)
};
}
catch
{
return
{
...
this
.
data
,
msg
:
JSON
.
parse
(
this
.
data
.
msg
.
replace
(
/
\n
/g
,
"\\n"
)),
};
}
}
private
get
messageBody
():
{
eid
?:
string
;
oid
?:
string
;
msg
:
any
}
{
if
(
this
.
data
)
{
try
{
return
{
...
this
.
data
,
msg
:
JSON
.
parse
(
this
.
data
.
msg
)
};
}
catch
{
return
{
...
this
.
data
,
msg
:
JSON
.
parse
(
this
.
data
.
msg
.
replace
(
/
\n
/g
,
"\\n"
)),
};
}
}
return
{
msg
:
{
text
:
""
}
};
}
return
{
msg
:
{
text
:
""
}
};
private
get
getAttachment
()
{
if
(
this
.
messageBody
)
{
return
this
.
messageBody
.
msg
.
name
;
}
return
"文件下载"
;
}
private
get
getAttachment
()
{
if
(
this
.
messageBody
)
{
return
this
.
messageBody
.
msg
.
name
;
}
return
"文件下载"
;
private
get
isMyMessage
()
{
if
(
this
.
isSendingMessage
)
{
return
true
;
}
private
get
isMyMessage
()
{
if
(
this
.
isSendingMessage
)
{
return
true
;
if
(
this
.
messageBody
)
{
const
msg
=
this
.
messageBody
;
if
(
this
.
chatSource
)
{
const
source
=
msg
.
msg
.
source
;
if
(
source
)
{
return
(
source
===
this
.
chatSource
&&
this
.
messageBody
.
eid
===
this
.
chatMyId
);
}
if
(
this
.
messageBody
)
{
const
msg
=
this
.
messageBody
;
if
(
this
.
chatSource
)
{
const
source
=
msg
.
msg
.
source
;
if
(
source
)
{
return
(
source
===
this
.
chatSource
&&
this
.
messageBody
.
eid
===
this
.
chatMyId
);
}
if
(
this
.
org
&&
this
.
messageBody
.
oid
)
{
return
(
this
.
messageBody
.
oid
===
this
.
org
&&
this
.
messageBody
.
eid
===
this
.
chatMyId
);
}
return
false
;
}
return
this
.
messageBody
.
eid
===
this
.
chatMyId
;
if
(
this
.
org
&&
this
.
messageBody
.
oid
)
{
return
(
this
.
messageBody
.
oid
===
this
.
org
&&
this
.
messageBody
.
eid
===
this
.
chatMyId
);
}
return
false
;
}
}
private
get
username
()
{
const
avatar
=
chat
.
getUserMapping
();
if
(
this
.
data
==
null
)
return
""
;
if
(
avatar
==
null
)
return
this
.
data
.
id
;
const
value
=
avatar
[
this
.
data
.
eid
];
if
(
value
==
null
)
return
""
;
if
(
value
.
name
==
null
)
return
""
;
return
value
.
name
;
return
this
.
messageBody
.
eid
===
this
.
chatMyId
;
}
private
get
avatar
()
{
const
avatar
=
chat
.
getUserMapping
();
if
(
this
.
isSendingMessage
)
{
if
(
avatar
&&
this
.
chatMyId
)
{
const
user
=
avatar
[
this
.
chatMyId
];
if
(
user
&&
user
.
avatar
)
{
return
user
.
avatar
;
}
}
}
if
(
avatar
&&
this
.
data
)
{
const
value
=
avatar
[
this
.
data
.
eid
];
if
(
value
&&
value
.
avatar
)
{
return
value
.
avatar
;
}
return
false
;
}
private
get
username
()
{
const
avatar
:
any
=
chat
.
getUserMapping
();
if
(
this
.
data
==
null
)
return
""
;
if
(
avatar
==
null
)
return
this
.
data
.
id
;
const
value
=
avatar
[
this
.
data
.
eid
];
if
(
value
==
null
)
return
""
;
if
(
value
.
name
==
null
)
return
""
;
return
value
.
name
;
}
private
get
avatar
()
{
const
avatar
=
chat
.
getUserMapping
();
if
(
this
.
isSendingMessage
)
{
if
(
avatar
&&
this
.
chatMyId
)
{
const
user
=
avatar
[
this
.
chatMyId
];
if
(
user
&&
user
.
avatar
)
{
return
user
.
avatar
;
}
}
}
return
""
;
if
(
avatar
&&
this
.
data
)
{
const
value
=
avatar
[
this
.
data
.
eid
];
if
(
value
&&
value
.
avatar
)
{
return
value
.
avatar
;
}
}
private
get
fileIcon
()
{
if
(
this
.
data
)
{
return
getFileType
(
this
.
messageBody
.
msg
.
name
);
}
return
""
;
}
return
FileType
.
Others
;
private
get
fileIcon
()
{
if
(
this
.
data
)
{
return
getFileType
(
this
.
messageBody
.
msg
.
name
);
}
private
get
messageType
()
{
const
type
=
this
.
data
?.
type
;
if
(
type
===
"file"
)
{
const
name
=
this
.
messageBody
?.
msg
.
name
;
if
(
name
)
{
const
size
=
this
.
messageBody
?.
msg
.
size
;
if
(
size
)
{
const
outImageSize
=
size
>
MAX_IMAGE_SIZE
;
if
(
!
outImageSize
&&
isImage
(
name
))
{
return
"image"
;
}
const
outSize
=
size
>
MAX_FILE_SIZE
;
if
(
!
outSize
)
{
if
(
isAudio
(
name
))
{
return
"voice"
;
}
if
(
isVide
o
(
name
))
{
return
"video
"
;
}
}
}
return
FileType
.
Others
;
}
private
get
messageType
()
{
const
type
=
this
.
data
?.
type
;
if
(
type
===
"file"
)
{
const
name
=
this
.
messageBody
?.
msg
.
name
;
if
(
name
)
{
const
size
=
this
.
messageBody
?.
msg
.
size
;
if
(
size
)
{
const
outImageSize
=
size
>
MAX_IMAGE_SIZE
;
if
(
!
outImageSize
&&
isImage
(
name
))
{
return
"image"
;
}
const
outSize
=
size
>
MAX_FILE_SIZE
;
if
(
!
outSize
)
{
if
(
isAudi
o
(
name
))
{
return
"voice
"
;
}
if
(
isVideo
(
name
))
{
return
"video"
;
}
}
}
return
this
.
data
?.
type
;
}
}
// 图片的样式设置,通过js偶尔会有高度计算不准确, 直接通过css的处理目前看可以达到对应效果
// private get imageStyle() {
// if (this.messageBody == null) return {};
// if (this.messageBody.msg.w == null) return {};
// if (this.messageBody.msg.h == null) return {};
// const w = this.messageBody.msg.w;
// const h = this.messageBody.msg.h;
// const maxWidth = 300;
// const maxHeight = maxWidth * (h / w);
// return { width: `${w}px`, height: `${h}px`, maxHeight: `${maxHeight}px` };
// }
private
get
duration
()
{
const
v
=
this
.
messageBody
.
msg
.
duration
as
number
;
return
v
||
0
;
return
this
.
data
?.
type
;
}
// 图片的样式设置,通过js偶尔会有高度计算不准确, 直接通过css的处理目前看可以达到对应效果
// private get imageStyle() {
// if (this.messageBody == null) return {};
// if (this.messageBody.msg.w == null) return {};
// if (this.messageBody.msg.h == null) return {};
// const w = this.messageBody.msg.w;
// const h = this.messageBody.msg.h;
// const maxWidth = 300;
// const maxHeight = maxWidth * (h / w);
// return { width: `${w}px`, height: `${h}px`, maxHeight: `${maxHeight}px` };
// }
private
get
duration
()
{
const
v
=
this
.
messageBody
.
msg
.
duration
as
number
;
return
v
||
0
;
}
private
get
durationInSecond
()
{
return
Math
.
round
(
this
.
duration
/
1000
);
}
private
get
getVoiceMessageWidth
()
{
if
(
this
.
fileFailed2Load
)
{
return
35
;
}
const
d
=
this
.
duration
/
1000
;
if
(
d
<=
3
)
{
return
60
;
}
private
get
durationInSecond
(
)
{
return
Math
.
round
(
this
.
duration
/
1000
)
;
if
(
d
>=
60
)
{
return
200
;
}
private
get
getVoiceMessageWidth
()
{
if
(
this
.
fileFailed2Load
)
{
return
35
;
}
const
d
=
this
.
duration
/
1000
;
if
(
d
<=
3
)
{
return
60
;
}
return
60
+
d
;
}
if
(
d
>=
60
)
{
return
200
;
}
private
isCustomer
(
)
{
return
!
this
.
showReadSummary
;
}
return
60
+
d
;
private
buildMessageUrl
()
{
if
(
this
.
messageRealUrl
||
this
.
loadingRealUrl
)
{
return
;
}
private
isCustomer
()
{
return
!
this
.
showReadSummary
;
const
url
=
this
.
messageBody
.
msg
.
url
as
string
;
if
(
url
)
{
// const service = XimService.getInstance()
if
(
isAccessibleUrl
(
url
))
{
return
(
this
.
messageRealUrl
=
url
);
}
this
.
loadingRealUrl
=
true
;
// service
// .getFileUrlById(url, this.chatId || 0, this.isCustomer())
// .then((data) => {
// if (data && data.itemList) {
// this.messageRealUrl = data.itemList[0].url
// } else {
// this.fileFailed2Load = true
// }
// })
// .catch(() => (this.fileFailed2Load = true))
// .finally(() => (this.loadingRealUrl = false))
}
else
{
this
.
fileFailed2Load
=
true
;
}
}
private
buildMessageUrl
()
{
if
(
this
.
messageRealUrl
||
this
.
loadingRealUrl
)
{
return
;
}
const
url
=
this
.
messageBody
.
msg
.
url
as
string
;
if
(
url
)
{
// const service = XimService.getInstance()
if
(
isAccessibleUrl
(
url
))
{
return
(
this
.
messageRealUrl
=
url
);
}
this
.
loadingRealUrl
=
true
;
// service
// .getFileUrlById(url, this.chatId || 0, this.isCustomer())
// .then((data) => {
// if (data && data.itemList) {
// this.messageRealUrl = data.itemList[0].url
// } else {
// this.fileFailed2Load = true
// }
// })
// .catch(() => (this.fileFailed2Load = true))
// .finally(() => (this.loadingRealUrl = false))
}
else
{
this
.
fileFailed2Load
=
true
;
}
private
openFile
()
{
if
(
this
.
isSendingMessage
)
{
return
;
}
private
openFile
()
{
if
(
this
.
isSendingMessage
)
{
return
;
}
if
(
this
.
failed
||
this
.
fileFailed2Load
)
{
return
;
}
const
copy
=
{
...
this
.
messageBody
.
msg
};
if
(
this
.
messageRealUrl
)
{
copy
.
url
=
this
.
messageRealUrl
;
}
this
.
$emit
(
"open"
,
{
type
:
this
.
messageType
,
msg
:
copy
});
if
(
this
.
failed
||
this
.
fileFailed2Load
)
{
return
;
}
private
play
()
{
if
(
this
.
audioRef
?.
paused
)
{
this
.
audioRef
?.
load
();
this
.
audioRef
?.
play
();
}
else
{
this
.
audioRef
?.
pause
();
}
const
copy
=
{
...
this
.
messageBody
.
msg
};
if
(
this
.
messageRealUrl
)
{
copy
.
url
=
this
.
messageRealUrl
;
}
private
onPlay
()
{
this
.
playing
=
true
;
this
.
$emit
(
"open"
,
{
type
:
this
.
messageType
,
msg
:
copy
});
}
private
play
()
{
if
(
this
.
audioRef
?.
paused
)
{
this
.
audioRef
?.
load
();
this
.
audioRef
?.
play
();
}
else
{
this
.
audioRef
?.
pause
();
}
}
private
onPause
()
{
this
.
playing
=
fals
e
;
}
private
onPlay
()
{
this
.
playing
=
tru
e
;
}
private
format2Link
(
text
:
string
)
{
return
replaceText2Link
(
text
);
}
private
onPause
()
{
this
.
playing
=
false
;
}
private
format2Link
(
text
:
string
)
{
return
replaceText2Link
(
text
);
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.message-con
{
margin
:
20px
0
;
&.my-message
{
.msg-avatar
{
margin-right
:
0
;
margin-left
:
10px
;
}
margin
:
20px
0
;
.msg-name
{
display
:
none
;
}
.msg-detail
{
margin-top
:
0
;
background-color
:
#dbf2ff
;
border-radius
:
8px
0
8px
8px
;
&.my-message
{
.msg-avatar
{
margin-right
:
0
;
margin-left
:
10px
;
}
&.
image-message
:
not
(.
image-404
),
&
.
file-message
{
background-color
:
transparent
;
border-radius
:
4px
;
border
:
1px
solid
#c5d4e5
;
}
.msg-name
{
display
:
none
;
}
&
.voice-message
{
>
div
{
flex-flow
:
row-reverse
;
}
.msg-detail
{
margin-top
:
0
;
background-color
:
#dbf2ff
;
border-radius
:
8px
0
8px
8px
;
svg
{
transform
:
rotateY
(
180deg
);
}
}
&.
image-message
:
not
(.
image-404
),
&
.
file-message
{
background-color
:
transparent
;
border-radius
:
4px
;
border
:
1px
solid
#c5d4e5
;
}
&
.video-message
{
background-color
:
#000
;
border-radius
:
0
;
}
&
.voice-message
{
>
div
{
flex-flow
:
row-reverse
;
}
.msg-read
{
display
:
inline-block
;
color
:
#bfe1ff
;
margin-right
:
15px
;
user-select
:
none
;
flex
:
none
;
svg
{
transform
:
rotateY
(
180deg
);
}
}
.download-icon
{
margin-right
:
15px
;
margin-left
:
0
;
margin-top
:
0
;
}
&
.video-message
{
background-color
:
#000
;
border-radius
:
0
;
}
}
.msg-read
{
display
:
inline-block
;
color
:
#bfe1ff
;
margin-right
:
15px
;
user-select
:
none
;
flex
:
none
;
}
>
i
{
height
:
16
px
;
font-size
:
16px
;
margin-right
:
10px
;
.download-icon
{
margin-right
:
15
px
;
margin-left
:
0
;
margin-top
:
0
;
}
}
>
i
{
height
:
16px
;
font-size
:
16px
;
margin-right
:
10px
;
}
}
.msg-avatar
{
margin-right
:
10px
;
flex
:
40px
0
0
;
margin-right
:
10px
;
flex
:
40px
0
0
;
}
i
.msg-avatar
{
font-size
:
30px
;
background-color
:
#c0c4cc
;
border-radius
:
4px
;
width
:
40px
;
height
:
40px
;
&:before
{
position
:
relative
;
left
:
5px
;
top
:
5px
;
color
:
#fff
;
}
font-size
:
30px
;
background-color
:
#c0c4cc
;
border-radius
:
4px
;
width
:
40px
;
height
:
40px
;
&:before
{
position
:
relative
;
left
:
5px
;
top
:
5px
;
color
:
#fff
;
}
}
.msg-name
{
font-size
:
12px
;
color
:
#888
;
font-size
:
12px
;
color
:
#888
;
}
.msg-detail
{
margin-top
:
10px
;
font-size
:
14px
;
line-height
:
20px
;
background
:
#f5f6fa
;
border-radius
:
0px
8px
8px
;
padding
:
10px
;
word-break
:
break-word
;
&.image-message,
&.file-message
{
background-color
:
transparent
;
border-radius
:
4px
;
border
:
1px
solid
#c5d4e5
;
}
margin-top
:
10px
;
font-size
:
14px
;
line-height
:
20px
;
background
:
#f5f6fa
;
border-radius
:
0px
8px
8px
;
padding
:
10px
;
word-break
:
break-word
;
&.image-message,
&.file-message
{
background-color
:
transparent
;
border-radius
:
4px
;
border
:
1px
solid
#c5d4e5
;
}
&
.image-message
{
line-height
:
1
;
&
.image-message
{
line-height
:
1
;
&.image-404
{
background
:
#f7f8fa
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
&.image-404
{
background
:
#f7f8fa
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
.file-icon
{
margin
:
0
;
}
}
.file-icon
{
margin
:
0
;
}
}
}
&
.voice-message
{
height
:
40px
;
width
:
200px
;
&
.voice-message
{
height
:
40px
;
width
:
200px
;
&.can-play
{
cursor
:
pointer
;
}
&.can-play
{
cursor
:
pointer
;
}
i
{
font-size
:
16px
;
}
i
{
font-size
:
16px
;
}
}
&
.video-message
{
height
:
160px
;
width
:
200px
;
background-color
:
#000
;
border-radius
:
0
;
&
.video-message
{
height
:
160px
;
width
:
200px
;
background-color
:
#000
;
border-radius
:
0
;
svg
{
cursor
:
pointer
;
}
svg
{
cursor
:
pointer
;
}
}
&
.inline-text
{
display
:
inline-block
;
}
&
.inline-text
{
display
:
inline-block
;
}
.file-message-name
{
max-width
:
130px
;
}
.file-message-name
{
max-width
:
130px
;
}
img
{
max-width
:
300px
;
}
img
{
max-width
:
300px
;
}
}
.download-icon
{
cursor
:
pointer
;
text-decoration
:
none
;
margin-left
:
15px
;
margin-top
:
42px
;
cursor
:
pointer
;
text-decoration
:
none
;
margin-left
:
15px
;
margin-top
:
42px
;
i
{
color
:
#fff
;
font-size
:
14px
;
}
i
{
color
:
#fff
;
font-size
:
14px
;
}
}
.no-selection
{
user-select
:
none
;
user-select
:
none
;
}
.image-message
{
max-width
:
300px
;
box-sizing
:
content-box
;
img
{
width
:
100%
;
}
max-width
:
300px
;
box-sizing
:
content-box
;
img
{
width
:
100%
;
}
}
</
style
>
components/who-read-list.vue
View file @
86932012
<
template
>
<div
v-loading=
"loading"
ref=
"list-con"
@
blur=
"$emit('blur')"
class=
"who-read-list pos-rel"
:style=
"`left:$
{left}px;top:${top}px`"
>
<template
v-if=
"!loading"
>
<div
class=
"list-left"
>
<div
class=
"number-count"
>
已读
{{
readlist
.
length
}}
</div>
<div
class=
"member-item"
v-for=
"item in readlist"
:key=
"item.eid"
>
<fs-avatar
class=
"member-avatar"
:src=
"item.avatar"
:size=
"30"
/>
<span
class=
"member-name"
>
{{
item
.
name
}}
</span>
</div>
</div>
<div
class=
"list-right"
>
<div
class=
"number-count"
>
未读
{{
unreadlist
.
length
}}
</div>
<div
class=
"member-item"
v-for=
"item in unreadlist"
:key=
"item.eid"
>
<fs-avatar
class=
"member-avatar"
:src=
"item.avatar"
:size=
"30"
/>
<span
class=
"member-name"
>
{{
item
.
name
}}
</span>
</div>
</div>
</
template
>
</div>
<div
v-loading=
"loading"
ref=
"list-con"
@
blur=
"$emit('blur')"
class=
"who-read-list pos-rel"
:style=
"`left:$
{left}px;top:${top}px`"
>
<template
v-if=
"!loading"
>
<div
class=
"list-left"
>
<div
class=
"number-count"
>
已读
{{
readlist
.
length
}}
</div>
<div
class=
"member-item"
v-for=
"item in readlist"
:key=
"item.eid"
>
<fs-avatar
class=
"member-avatar"
:src=
"item.avatar"
:size=
"30"
/>
<span
class=
"member-name"
>
{{
item
.
name
}}
</span>
</div>
</div>
<div
class=
"list-right"
>
<div
class=
"number-count"
>
未读
{{
unreadlist
.
length
}}
</div>
<div
class=
"member-item"
v-for=
"item in unreadlist"
:key=
"item.eid"
>
<fs-avatar
class=
"member-avatar"
:src=
"item.avatar"
:size=
"30"
/>
<span
class=
"member-name"
>
{{
item
.
name
}}
</span>
</div>
</div>
</
template
>
</div>
</template>
<
script
lang=
"ts"
>
import
{
Component
,
Prop
,
Ref
,
Vue
}
from
"vue-property-decorator"
;
...
...
@@ -49,161 +33,166 @@ import { unique } from "../utils";
import
{
ChatStore
}
from
"@/customer-service/store/model"
;
import
xim
from
"@/customer-service/xim/xim"
;
import
chat
from
"@/customer-service/xim/index"
;
const
chatStoreNamespace
=
namespace
(
"chatStore"
);
@
Component
({
components
:
{}
})
export
default
class
WhoReadList
extends
Vue
{
@
chatStoreNamespace
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
@
chatStoreNamespace
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
;
@
chatStoreNamespace
.
State
(
ChatStore
.
STATE_CHAT_MY_ID
)
private
readonly
chatMyId
!
:
ChatStore
.
STATE_CHAT_MY_ID
@
chatStoreNamespace
.
State
(
ChatStore
.
STATE_CHAT_MY_ID
)
private
readonly
chatMyId
!
:
ChatStore
.
STATE_CHAT_MY_ID
;
@
Prop
({
type
:
Number
,
})
private
msgId
!
:
number
@
Prop
({
type
:
Number
,
})
private
msgId
!
:
number
;
@
Ref
(
"list-con"
)
private
listCon
!
:
HTMLElement
@
Ref
(
"list-con"
)
private
listCon
!
:
HTMLElement
;
private
top
=
0
private
left
=
0
private
readlist
:
{
name
:
string
;
avatar
:
string
}[]
=
[]
private
unreadlist
:
{
name
:
string
;
avatar
:
string
}[]
=
[]
private
loading
=
false
private
startLoading
()
{
this
.
loading
=
true
;
}
private
top
=
0
;
private
left
=
0
;
private
readlist
:
{
name
:
string
;
avatar
:
string
}[]
=
[];
private
unreadlist
:
{
name
:
string
;
avatar
:
string
}[]
=
[];
private
loading
=
false
;
private
startLoading
()
{
this
.
loading
=
true
;
}
private
endLoading
()
{
this
.
loading
=
false
;
}
private
endLoading
()
{
this
.
loading
=
false
;
}
public
async
created
()
{
this
.
startLoading
();
await
this
.
getReader
();
this
.
endLoading
();
}
public
async
created
()
{
this
.
startLoading
();
await
this
.
getReader
();
this
.
endLoading
();
}
public
mounted
()
{
this
.
enableBlur
();
const
{
top
,
left
}
=
(
this
.
listCon
.
parentNode
as
HTMLElement
).
getBoundingClientRect
();
this
.
top
=
top
;
this
.
left
=
left
;
}
public
mounted
()
{
this
.
enableBlur
();
const
{
top
,
left
}
=
(
this
.
listCon
.
parentNode
as
HTMLElement
).
getBoundingClientRect
();
this
.
top
=
top
;
this
.
left
=
left
;
}
private
enableBlur
()
{
this
.
listCon
.
setAttribute
(
"tabindex"
,
"-1"
);
this
.
listCon
.
focus
();
}
private
enableBlur
()
{
this
.
listCon
.
setAttribute
(
"tabindex"
,
"-1"
);
this
.
listCon
.
focus
();
}
private
async
getUserNameByid
(
eid
:
string
)
{
const
data
=
await
this
.
sdk
.
model
(
"user"
).
detail
(
eid
).
query
();
return
data
.
row
.
first_name
.
value
as
string
;
}
private
async
getUserNameByid
(
eid
:
string
)
{
const
data
=
await
chat
.
getSdk
()
.
model
(
"user"
)
.
detail
(
eid
)
.
query
();
return
data
.
row
.
first_name
.
value
as
string
;
}
private
async
getReader
()
{
if
(
this
.
chatId
==
null
)
return
;
if
(
this
.
msgId
==
null
)
return
;
const
data
=
await
xim
.
fetchMsgInBox
(
this
.
chatId
,
this
.
msgId
);
const
readerlist
=
this
.
uniqueReaderList
(
data
.
args
[
0
]
as
dto
.
OneWhoReadMessage
[]
);
this
.
readlist
=
await
Promise
.
all
(
readerlist
.
filter
((
k
)
=>
k
.
is_read
)
.
filter
((
k
)
=>
k
.
eid
!==
this
.
chatMyId
)
.
map
(
async
(
k
)
=>
{
const
eid
=
k
.
eid
;
const
name
=
await
this
.
getUserNameByid
(
eid
);
return
{
eid
,
name
,
avatar
:
""
,
};
})
);
this
.
unreadlist
=
await
Promise
.
all
(
readerlist
.
filter
((
k
)
=>
!
k
.
is_read
)
.
filter
((
k
)
=>
k
.
eid
!==
this
.
chatMyId
)
.
map
(
async
(
k
)
=>
{
const
eid
=
k
.
eid
;
const
name
=
await
this
.
getUserNameByid
(
eid
);
return
{
eid
,
name
,
avatar
:
""
,
};
})
);
}
private
async
getReader
()
{
if
(
this
.
chatId
==
null
)
return
;
if
(
this
.
msgId
==
null
)
return
;
const
data
=
await
xim
.
fetchMsgInBox
(
this
.
chatId
,
this
.
msgId
);
if
(
data
==
null
)
return
;
const
readerlist
=
this
.
uniqueReaderList
(
data
.
args
[
0
]
as
dto
.
OneWhoReadMessage
[]
);
this
.
readlist
=
await
Promise
.
all
(
readerlist
.
filter
((
k
)
=>
k
.
is_read
)
.
filter
((
k
)
=>
k
.
eid
!==
this
.
chatMyId
)
.
map
(
async
(
k
)
=>
{
const
eid
=
k
.
eid
;
const
name
=
await
this
.
getUserNameByid
(
eid
);
return
{
eid
,
name
,
avatar
:
""
,
};
})
);
this
.
unreadlist
=
await
Promise
.
all
(
readerlist
.
filter
((
k
)
=>
!
k
.
is_read
)
.
filter
((
k
)
=>
k
.
eid
!==
this
.
chatMyId
)
.
map
(
async
(
k
)
=>
{
const
eid
=
k
.
eid
;
const
name
=
await
this
.
getUserNameByid
(
eid
);
return
{
eid
,
name
,
avatar
:
""
,
};
})
);
}
private
uniqueReaderList
(
data
:
dto
.
OneWhoReadMessage
[])
{
return
unique
(
data
,
function
(
item
,
all
)
{
return
all
.
findIndex
((
k
)
=>
k
.
eid
===
item
.
eid
);
});
}
private
uniqueReaderList
(
data
:
dto
.
OneWhoReadMessage
[])
{
return
unique
(
data
,
function
(
item
,
all
)
{
return
all
.
findIndex
((
k
)
=>
k
.
eid
===
item
.
eid
);
});
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.who-read-list
{
::before
{
content
:
""
;
position
:
absolute
;
width
:
1px
;
top
:
15px
;
bottom
:
15px
;
left
:
0
;
right
:
0
;
margin
:
auto
;
background
:
#e1e2e2
;
}
&
:focus
{
outline
:
unset
;
}
padding
:
15px
30px
;
background
:
rgba
(
249
,
249
,
249
,
1
);
box-shadow
:
0px
0px
6px
0px
rgba
(
0
,
0
,
0
,
0
.2
);
border-radius
:
4px
;
border
:
1px
solid
rgba
(
183
,
191
,
199
,
1
);
width
:
455px
;
min-height
:
100px
;
position
:
fixed
;
margin-left
:
-200px
;
margin-top
:
20px
;
color
:
#000
;
z-index
:
2
;
.list-left
,
.list-right
{
display
:
inline-block
;
vertical-align
:
top
;
width
:
calc
(
50%
-
30px
);
}
.list-left
{
padding-right
:
30px
;
}
.list-right
{
padding-left
:
30px
;
}
.number-count
{
font-size
:
14px
;
color
:
#333333
;
margin-bottom
:
15px
;
}
::before
{
content
:
""
;
position
:
absolute
;
width
:
1px
;
top
:
15px
;
bottom
:
15px
;
left
:
0
;
right
:
0
;
margin
:
auto
;
background
:
#e1e2e2
;
}
&
:focus
{
outline
:
unset
;
}
padding
:
15px
30px
;
background
:
rgba
(
249
,
249
,
249
,
1
);
box-shadow
:
0px
0px
6px
0px
rgba
(
0
,
0
,
0
,
0
.2
);
border-radius
:
4px
;
border
:
1px
solid
rgba
(
183
,
191
,
199
,
1
);
width
:
455px
;
min-height
:
100px
;
position
:
fixed
;
margin-left
:
-200px
;
margin-top
:
20px
;
color
:
#000
;
z-index
:
2
;
.list-left
,
.list-right
{
display
:
inline-block
;
vertical-align
:
top
;
width
:
calc
(
50%
-
30px
);
}
.list-left
{
padding-right
:
30px
;
}
.list-right
{
padding-left
:
30px
;
}
.number-count
{
font-size
:
14px
;
color
:
#333333
;
margin-bottom
:
15px
;
}
}
.member-item
{
margin-top
:
10px
;
.member-avatar,
.member-name
{
display
:
inline-block
;
vertical-align
:
middle
;
}
.member-avatar
{
margin-right
:
10px
;
}
margin-top
:
10px
;
.member-avatar,
.member-name
{
display
:
inline-block
;
vertical-align
:
middle
;
}
.member-avatar
{
margin-right
:
10px
;
}
}
</
style
>
create-chat.vue
View file @
86932012
<
template
>
<el-dialog
title=
"创建会话"
:visible=
"visible"
@
close=
"hide"
>
选择聊天对象
{{
userList
.
length
}}
<el-table
v-loading=
"loading"
:data=
"userList"
style=
"width: 100%"
height=
"250"
@
selection-change=
"handleSelectionChange"
>
<el-table-column
type=
"selection"
width=
"55"
>
</el-table-column>
<el-table-column
prop=
"id"
label=
"id"
width=
"150"
>
</el-table-column>
<el-table-column
prop=
"name"
label=
"name"
width=
"150"
>
</el-table-column>
</el-table>
<el-button
@
click=
"nextPage"
>
加载下一页
</el-button>
<span
slot=
"footer"
class=
"dialog-footer"
>
<el-button
@
click=
"hide"
>
取 消
</el-button>
<el-button
type=
"primary"
@
click=
"createChat"
>
确 定
</el-button>
</span>
</el-dialog>
<el-dialog
title=
"创建会话"
:visible=
"visible"
@
close=
"hide"
>
选择聊天对象
{{
userList
.
length
}}
<el-table
v-loading=
"loading"
:data=
"userList"
style=
"width: 100%"
height=
"250"
@
selection-change=
"handleSelectionChange"
>
<el-table-column
type=
"selection"
width=
"55"
>
</el-table-column>
<el-table-column
prop=
"id"
label=
"id"
width=
"150"
>
</el-table-column>
<el-table-column
prop=
"name"
label=
"name"
width=
"150"
>
</el-table-column>
</el-table>
<el-button
@
click=
"nextPage"
>
加载下一页
</el-button>
<span
slot=
"footer"
class=
"dialog-footer"
>
<el-button
@
click=
"hide"
>
取 消
</el-button>
<el-button
type=
"primary"
@
click=
"createChat"
>
确 定
</el-button>
</span>
</el-dialog>
</
template
>
<
script
lang=
"ts"
>
import
{
Component
,
Vue
}
from
"vue-property-decorator"
import
{
ChatStore
,
chatStore
}
from
"@/customer-service/store/model"
import
{
List
,
ListEasy
,
ListTypes
}
from
"uniplat-sdk"
import
{
Component
,
Vue
}
from
"vue-property-decorator"
;
import
{
ChatStore
,
chatStore
}
from
"@/customer-service/store/model"
;
import
type
{
List
,
ListEasy
,
ListTypes
}
from
"uniplat-sdk"
;
import
chat
from
"@/customer-service/xim/index"
type
User
=
{
id
:
string
name
:
string
}
type
ThenArg
<
T
>
=
T
extends
PromiseLike
<
infer
U
>
?
U
:
T
id
:
string
;
name
:
string
;
}
;
type
ThenArg
<
T
>
=
T
extends
PromiseLike
<
infer
U
>
?
U
:
T
;
@
Component
({
components
:
{}
})
export
default
class
ChatCreator
extends
Vue
{
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CREATOR_VISIBLE
)
private
readonly
visible
:
ChatStore
.
STATE_CHAT_CREATOR_VISIBLE
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_HIDE_CHAT_CREATOR
)
private
readonly
hide
:
ChatStore
.
MUTATION_HIDE_CHAT_CREATOR
@
chatStore
.
Action
(
ChatStore
.
ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN
)
private
readonly
_createChat
:
ChatStore
.
ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CREATOR_VISIBLE
)
private
readonly
visible
!
:
ChatStore
.
STATE_CHAT_CREATOR_VISIBLE
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_HIDE_CHAT_CREATOR
)
private
readonly
hide
!
:
ChatStore
.
MUTATION_HIDE_CHAT_CREATOR
;
@
chatStore
.
Action
(
ChatStore
.
ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN
)
private
readonly
_createChat
!
:
ChatStore
.
ACTION_CREATE_NEW_CHAT_BY_SERVICE_MAN
;
private
currentPage
=
1
private
userList
=
[]
private
getList
:
ThenArg
<
ReturnType
<
ListEasy
[
"query"
]
>>
[
"getList"
]
private
currentPage
=
1
;
private
userList
:
{
id
:
any
;
name
:
any
;
}[]
=
[];
private
getList
!
:
ThenArg
<
ReturnType
<
ListEasy
[
"query"
]
>>
[
"getList"
];
public
async
created
()
{
const
list
=
this
.
sdk
.
model
(
"user"
).
list
()
const
{
pageData
,
getList
}
=
await
list
.
query
({
pageIndex
:
this
.
currentPage
,
item_size
:
50
,
})
this
.
getList
=
getList
this
.
userList
=
this
.
exactUserList
(
pageData
.
rows
)
}
private
exactUserList
(
rows
:
any
[])
{
return
rows
.
map
((
k
)
=>
{
return
{
id
:
k
.
id
.
value
,
name
:
k
.
first_name
.
value
,
}
})
}
public
async
created
()
{
const
list
=
chat
.
getSdk
().
model
(
"user"
).
list
();
const
{
pageData
,
getList
}
=
await
list
.
query
({
pageIndex
:
this
.
currentPage
,
item_size
:
50
,
});
this
.
getList
=
getList
;
this
.
userList
=
this
.
exactUserList
(
pageData
.
rows
);
}
private
exactUserList
(
rows
:
any
[])
{
return
rows
.
map
((
k
)
=>
{
return
{
id
:
k
.
id
.
value
,
name
:
k
.
first_name
.
value
,
};
});
}
private
loading
=
false
private
async
nextPage
()
{
this
.
loading
=
true
this
.
currentPage
++
const
data
=
await
this
.
getList
(
this
.
currentPage
)
this
.
loading
=
false
this
.
userList
=
[...
this
.
userList
,
...
this
.
exactUserList
(
data
.
rows
)]
}
private
loading
=
false
;
private
async
nextPage
()
{
this
.
loading
=
true
;
this
.
currentPage
++
;
const
data
=
await
this
.
getList
(
this
.
currentPage
);
this
.
loading
=
false
;
this
.
userList
=
[...
this
.
userList
,
...
this
.
exactUserList
(
data
.
rows
)];
}
private
selectedRows
:
string
[]
=
[]
private
handleSelectionChange
(
selectedRows
:
User
[])
{
this
.
selectedRows
=
selectedRows
.
map
((
k
)
=>
String
(
k
.
id
))
}
private
selectedRows
:
string
[]
=
[];
private
handleSelectionChange
(
selectedRows
:
User
[])
{
this
.
selectedRows
=
selectedRows
.
map
((
k
)
=>
String
(
k
.
id
));
}
private
createChat
()
{
const
{
keyvalue
,
model_name
}
=
this
.
$route
.
params
this
.
_createChat
({
modelName
:
model_name
,
selectedListId
:
keyvalue
,
uids
:
this
.
selectedRows
,
})
}
private
createChat
()
{
const
{
keyvalue
,
model_name
}
=
this
.
$route
.
params
;
this
.
_createChat
({
modelName
:
model_name
,
selectedListId
:
keyvalue
,
uids
:
this
.
selectedRows
,
});
}
}
</
script
>
<
style
lang=
"scss"
scoped
></
style
>
hybrid-input/index.vue
View file @
86932012
<
template
>
<div
class=
"input-wrap h-100"
>
<div
class=
"tool-bar"
>
<img
class=
"tool-bar-icon"
title=
"发送表情"
@
click
.
stop=
"toggleEmoji"
src=
"@/customer-service/imgs/emoji.png"
/>
<label
for=
"chat-upload-file"
:title=
"tip4Image"
@
click=
"allowLoadImg"
class=
"offset"
>
<img
class=
"tool-bar-icon"
src=
"@/customer-service/imgs/pic.png"
/>
</label>
<label
for=
"chat-upload-file"
:title=
"tip4File"
@
click=
"allowLoadFile"
>
<img
class=
"tool-bar-icon"
src=
"@/customer-service/imgs/file.png"
/>
</label>
<input
@
change=
"onChange"
:value=
"file"
id=
"chat-upload-file"
type=
"file"
:accept=
"acceptType"
multiple
/>
</div>
<el-scrollbar
class=
"input-el-scrollbar"
>
<!-- contenteditable 只能设置为true,需要考虑浏览器兼容性问题,不能只考虑chrome -->
<div
class=
"input-container"
ref=
"input"
id=
"chat-input-box"
contenteditable=
"true"
@
input=
"$emit('input')"
@
keypress
.
enter=
"handleSendMsg"
></div>
</el-scrollbar>
<div
class=
"emoji-picker"
v-show=
"emojiPanelVisibility"
>
<el-scrollbar
class=
"overflow-x-hidden"
>
<span
class=
"emoji-item"
v-for=
"item in emoji"
:key=
"item.code"
:title=
"item.name"
@
click
.
stop=
"selectEmoji(item)"
>
{{
item
.
emoji_chars
}}
</span
>
</el-scrollbar>
</div>
<div
class=
"input-wrap h-100"
>
<div
class=
"tool-bar"
>
<img
class=
"tool-bar-icon"
title=
"发送表情"
@
click
.
stop=
"toggleEmoji"
src=
"@/customer-service/imgs/emoji.png"
/>
<label
for=
"chat-upload-file"
:title=
"tip4Image"
@
click=
"allowLoadImg"
class=
"offset"
>
<img
class=
"tool-bar-icon"
src=
"@/customer-service/imgs/pic.png"
/>
</label>
<label
for=
"chat-upload-file"
:title=
"tip4File"
@
click=
"allowLoadFile"
>
<img
class=
"tool-bar-icon"
src=
"@/customer-service/imgs/file.png"
/>
</label>
<input
@
change=
"onChange"
:value=
"file"
id=
"chat-upload-file"
type=
"file"
:accept=
"acceptType"
multiple
/>
</div>
<el-scrollbar
class=
"input-el-scrollbar"
>
<!-- contenteditable 只能设置为true,需要考虑浏览器兼容性问题,不能只考虑chrome -->
<div
class=
"input-container"
ref=
"input"
id=
"chat-input-box"
contenteditable=
"true"
@
input=
"$emit('input')"
@
keypress
.
enter=
"handleSendMsg"
></div>
</el-scrollbar>
<div
class=
"emoji-picker"
v-show=
"emojiPanelVisibility"
>
<el-scrollbar
class=
"overflow-x-hidden"
>
<span
class=
"emoji-item"
v-for=
"item in emoji"
:key=
"item.code"
:title=
"item.name"
@
click
.
stop=
"selectEmoji(item)"
>
{{
item
.
emoji_chars
}}
</span
>
</el-scrollbar>
</div>
</div>
</
template
>
<
script
lang=
"ts"
>
...
...
@@ -71,37 +61,37 @@ import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import
{
namespace
}
from
"vuex-class"
;
import
{
getFileType
,
getSvg
,
MAX_FILE_SIZE
,
MAX_FILE_SIZE_STRING
,
MAX_IMAGE_SIZE
,
MAX_IMAGE_SIZE_STRING
,
MESSAGE_FILE_EMPTY
,
MESSAGE_FILE_TOO_LARGE
,
MESSAGE_IMAGE_TOO_LARGE
,
getFileType
,
getSvg
,
MAX_FILE_SIZE
,
MAX_FILE_SIZE_STRING
,
MAX_IMAGE_SIZE
,
MAX_IMAGE_SIZE_STRING
,
MESSAGE_FILE_EMPTY
,
MESSAGE_FILE_TOO_LARGE
,
MESSAGE_IMAGE_TOO_LARGE
,
}
from
"../components/file-controller"
;
import
{
EmojiService
}
from
"../service/emoji"
;
import
{
ChatStore
}
from
"../store/model"
;
import
{
formatFileSize
}
from
"../utils"
;
export
const
enum
InputMessageType
{
Text
=
"text"
,
Image
=
"image"
,
File
=
"file"
,
Text
=
"text"
,
Image
=
"image"
,
File
=
"file"
,
}
export
interface
InputMessageBody
{
text
?:
string
;
url
?:
string
;
name
?:
string
;
size
?:
number
;
text
?:
string
;
url
?:
string
;
name
?:
string
;
size
?:
number
;
}
export
interface
InputMessage
{
type
:
InputMessageType
;
body
:
InputMessageBody
;
file
?:
File
|
null
;
type
:
InputMessageType
;
body
:
InputMessageBody
;
file
?:
File
|
null
;
}
const
chatStoreNamespace
=
namespace
(
"chatStore"
);
...
...
@@ -112,544 +102,539 @@ export const IMAGE_INFO_CLASS = "img-info";
export
const
FILE_INFO_CLASS
=
"file-info"
;
export
function
isImageOrFile
(
node
:
ChildNode
)
{
const
e
=
node
as
HTMLElement
;
return
(
e
.
classList
&&
(
e
.
classList
.
contains
(
IMAGE_INFO_CLASS
)
||
e
.
classList
.
contains
(
FILE_INFO_CLASS
))
);
const
e
=
node
as
HTMLElement
;
return
(
e
.
classList
&&
(
e
.
classList
.
contains
(
IMAGE_INFO_CLASS
)
||
e
.
classList
.
contains
(
FILE_INFO_CLASS
))
);
}
@
Component
({
components
:
{}
})
export
default
class
Input
extends
Vue
{
@
chatStoreNamespace
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
@
chatStoreNamespace
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
;
@
Ref
(
"input"
)
private
readonly
messageInputBox
!
:
HTMLDivElement
@
Ref
(
"input"
)
private
readonly
messageInputBox
!
:
HTMLDivElement
;
private
file
=
""
private
acceptType
=
"image/*"
private
emojiPanelVisibility
=
false
private
file
=
""
;
private
acceptType
=
"image/*"
;
private
emojiPanelVisibility
=
false
;
private
tip4Image
=
`发送图片(最大
${
MAX_IMAGE_SIZE_STRING
}
)`
private
tip4File
=
`发送文件(最大
${
MAX_FILE_SIZE_STRING
}
)`
private
tip4Image
=
`发送图片(最大
${
MAX_IMAGE_SIZE_STRING
}
)`
;
private
tip4File
=
`发送文件(最大
${
MAX_FILE_SIZE_STRING
}
)`
;
private
emoji
:
{
name
:
string
;
emoji_chars
:
string
;
code
:
string
}[]
=
[]
private
emoji
:
{
name
:
string
;
emoji_chars
:
string
;
code
:
string
}[]
=
[];
@
Watch
(
"chatId"
)
private
onChatIdChanged
(
v
:
number
,
old
:
number
)
{
if
(
old
)
{
const
current
=
this
.
getNodeListFromInputBox
();
if
(
current
&&
current
.
length
)
{
chatCache
[
old
]
=
current
;
}
}
this
.
clearInput
();
if
(
v
)
{
const
cache
=
chatCache
[
v
];
if
(
cache
)
{
const
e
=
document
.
querySelector
(
"#chat-input-box"
)
as
HTMLElement
;
if
(
e
)
{
for
(
const
node
of
cache
as
ChildNode
[])
{
e
.
appendChild
(
node
);
}
}
}
}
}
public
mounted
()
{
this
.
messageInputBox
.
addEventListener
(
"paste"
,
this
.
handlePasteEvent
);
document
.
addEventListener
(
"click"
,
this
.
hideEmoji
);
document
.
addEventListener
(
"selectionchange"
,
this
.
handleSaveRange
);
this
.
$onBeforeDestroy
(()
=>
{
document
.
removeEventListener
(
"click"
,
this
.
hideEmoji
);
document
.
removeEventListener
(
"selectionchange"
,
this
.
handleSaveRange
);
});
this
.
setupEmoji
();
this
.
focus
();
@
Watch
(
"chatId"
)
private
onChatIdChanged
(
v
:
number
,
old
:
number
)
{
if
(
old
)
{
const
current
=
this
.
getNodeListFromInputBox
();
if
(
current
&&
current
.
length
)
{
chatCache
[
old
]
=
current
;
}
}
public
focus
()
{
this
.
messageInputBox
.
focus
();
}
this
.
clearInput
();
private
clearInput
()
{
this
.
messageInputBox
.
innerHTML
=
""
;
const
input
=
document
.
getElementById
(
"chat-upload-file"
)
as
HTMLInputElement
;
if
(
input
)
{
input
.
value
=
""
;
if
(
v
)
{
const
cache
=
chatCache
[
v
];
if
(
cache
)
{
const
e
=
document
.
querySelector
(
"#chat-input-box"
)
as
HTMLElement
;
if
(
e
)
{
for
(
const
node
of
cache
as
ChildNode
[])
{
e
.
appendChild
(
node
);
}
}
}
}
private
allowLoadImg
()
{
this
.
acceptType
=
"image/*"
;
}
private
allowLoadFile
()
{
this
.
acceptType
=
"*"
;
}
public
mounted
()
{
this
.
messageInputBox
.
addEventListener
(
"paste"
,
this
.
handlePasteEvent
);
document
.
addEventListener
(
"click"
,
this
.
hideEmoji
);
document
.
addEventListener
(
"selectionchange"
,
this
.
handleSaveRange
);
this
.
setupEmoji
();
this
.
focus
();
}
public
beforeDestroy
()
{
document
.
removeEventListener
(
"click"
,
this
.
hideEmoji
);
document
.
removeEventListener
(
"selectionchange"
,
this
.
handleSaveRange
);
}
public
focus
()
{
this
.
messageInputBox
.
focus
();
}
private
clearInput
()
{
this
.
messageInputBox
.
innerHTML
=
""
;
const
input
=
document
.
getElementById
(
"chat-upload-file"
)
as
HTMLInputElement
;
if
(
input
)
{
input
.
value
=
""
;
}
private
async
handlePasteEvent
(
event
:
ClipboardEvent
)
{
/*
* 组织默认行为原因
* 1、浏览器自带复制粘贴图片到输入框的功能,与js加工后的图片重复了,
* 2、默认复制粘贴功能会粘贴dom结构
* */
event
.
preventDefault
();
const
items
=
event
.
clipboardData
&&
event
.
clipboardData
.
items
;
let
html
=
""
;
const
promiseArr
=
[];
if
(
items
&&
items
.
length
)
{
// 检索剪切板items中类型带有image的
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
if
(
items
[
i
].
kind
===
"file"
)
{
const
file
=
items
[
i
].
getAsFile
();
if
(
file
)
{
if
(
file
.
size
<=
0
)
{
this
.
$emit
(
"error"
,
MESSAGE_FILE_EMPTY
);
return
;
}
if
(
this
.
isImage
(
file
))
{
if
(
file
.
size
>=
MAX_IMAGE_SIZE
)
{
this
.
$emit
(
"error"
,
MESSAGE_IMAGE_TOO_LARGE
);
return
;
}
html
+=
this
.
buildImageHtml
(
file
);
}
else
{
if
(
file
.
size
>=
MAX_FILE_SIZE
)
{
this
.
$emit
(
"error"
,
MESSAGE_FILE_TOO_LARGE
);
return
;
}
html
+=
this
.
buildFileHtml
(
file
);
}
private
allowLoadImg
()
{
this
.
acceptType
=
"image/*"
;
}
private
allowLoadFile
()
{
this
.
acceptType
=
"*"
;
}
private
async
handlePasteEvent
(
event
:
ClipboardEvent
)
{
/*
* 组织默认行为原因
* 1、浏览器自带复制粘贴图片到输入框的功能,与js加工后的图片重复了,
* 2、默认复制粘贴功能会粘贴dom结构
* */
event
.
preventDefault
();
const
items
=
event
.
clipboardData
&&
event
.
clipboardData
.
items
;
let
html
=
""
;
const
promiseArr
=
[];
if
(
items
&&
items
.
length
)
{
// 检索剪切板items中类型带有image的
for
(
let
i
=
0
;
i
<
items
.
length
;
i
++
)
{
if
(
items
[
i
].
kind
===
"file"
)
{
const
file
=
items
[
i
].
getAsFile
();
if
(
file
)
{
if
(
file
.
size
<=
0
)
{
this
.
$emit
(
"error"
,
MESSAGE_FILE_EMPTY
);
return
;
}
if
(
this
.
isImage
(
file
))
{
if
(
file
.
size
>=
MAX_IMAGE_SIZE
)
{
this
.
$emit
(
"error"
,
MESSAGE_IMAGE_TOO_LARGE
);
return
;
}
html
+=
this
.
buildImageHtml
(
file
);
}
else
{
if
(
file
.
size
>=
MAX_FILE_SIZE
)
{
this
.
$emit
(
"error"
,
MESSAGE_FILE_TOO_LARGE
);
return
;
}
html
+=
this
.
buildFileHtml
(
file
);
}
break
;
}
}
else
{
promiseArr
.
push
(
new
Promise
<
void
>
((
resolve
)
=>
{
const
contentType
=
items
[
i
].
type
;
items
[
i
].
getAsString
((
k
)
=>
{
/*
* items第一项是文本
* 第二项是带有dom结构的文本(包含格式)
* 第三项写明了数据是从哪里复制来的
*/
if
(
i
===
0
)
{
if
(
contentType
===
"text/plain"
)
{
html
+=
k
;
}
}
else
if
(
i
===
1
)
{
const
srcRegex
=
/<img
[^
>
]
+src="
([^
">
]
+
)
"
\s
+title="
([^
">
]
+
)
"/g
;
let
result
;
do
{
result
=
srcRegex
.
exec
(
k
);
if
(
result
)
{
const
[,
src
,
name
]
=
result
;
html
+=
`<img tabindex="-1" src="
${
src
}
" data-image='
${
JSON
.
stringify
(
{
url
:
src
,
name
,
}
break
;
)}
'>`
;
html
=
html
.
replace
(
name
,
""
);
}
}
else
{
promiseArr
.
push
(
new
Promise
<
void
>
((
resolve
)
=>
{
const
contentType
=
items
[
i
].
type
;
items
[
i
].
getAsString
((
k
)
=>
{
/*
* items第一项是文本
* 第二项是带有dom结构的文本(包含格式)
* 第三项写明了数据是从哪里复制来的
*/
if
(
i
===
0
)
{
if
(
contentType
===
"text/plain"
)
{
html
+=
k
;
}
}
else
if
(
i
===
1
)
{
const
srcRegex
=
/<img
[^
>
]
+src="
([^
">
]
+
)
"
\s
+title="
([^
">
]
+
)
"/g
;
let
result
;
do
{
result
=
srcRegex
.
exec
(
k
);
if
(
result
)
{
const
[,
src
,
name
]
=
result
;
html
+=
`<img tabindex="-1" src="
${
src
}
" data-image='
${
JSON
.
stringify
(
{
url
:
src
,
name
,
}
)}
'>`
;
html
=
html
.
replace
(
name
,
""
);
}
}
while
(
result
);
}
resolve
();
});
})
);
}
while
(
result
);
}
}
}
await
Promise
.
all
(
promiseArr
);
if
(
html
)
{
this
.
insertHtmlAtCaret
(
html
);
resolve
();
});
})
);
}
}
}
private
async
handleSendMsg
(
e
:
Event
)
{
// 防止换行
e
.
preventDefault
();
return
new
Promise
((
resolve
,
reject
)
=>
{
try
{
const
data
=
this
.
getNodeListFromInputBox
();
this
.
$emit
(
"send"
,
data
,
resolve
);
}
catch
(
e
)
{
this
.
$emit
(
"error"
,
e
);
reject
(
e
);
}
}).
then
(()
=>
{
this
.
clearInput
();
if
(
this
.
chatId
)
{
chatCache
[
this
.
chatId
]
=
[];
}
});
}
/**
* 获取输入框中的内容
* @returns 返回的是节点数组
*/
private
getNodeListFromInputBox
()
{
this
.
messageInputBox
.
normalize
();
const
nodes
=
Array
.
from
(
this
.
messageInputBox
.
childNodes
);
return
this
.
combine
(
nodes
);
await
Promise
.
all
(
promiseArr
);
if
(
html
)
{
this
.
insertHtmlAtCaret
(
html
);
}
/**
* 文本,链接等需要合并成纯文本发送
*/
private
combine
(
nodes
:
ChildNode
[])
{
const
sendingNodes
:
ChildNode
[]
=
[];
let
needCreateNewNode
=
false
;
let
text
=
""
;
for
(
const
item
of
nodes
)
{
if
(
!
isImageOrFile
(
item
)
&&
item
.
textContent
)
{
if
(
needCreateNewNode
)
{
text
=
""
;
needCreateNewNode
=
false
;
}
text
+=
item
.
textContent
;
}
else
{
needCreateNewNode
=
true
;
if
(
text
)
{
this
.
checkTextLength
(
text
);
const
node
=
document
.
createTextNode
(
text
);
sendingNodes
.
push
(
node
);
}
sendingNodes
.
push
(
item
);
}
}
private
async
handleSendMsg
(
e
:
Event
)
{
// 防止换行
e
.
preventDefault
();
return
new
Promise
((
resolve
,
reject
)
=>
{
try
{
const
data
=
this
.
getNodeListFromInputBox
();
this
.
$emit
(
"send"
,
data
,
resolve
);
}
catch
(
e
)
{
this
.
$emit
(
"error"
,
e
);
reject
(
e
);
}
}).
then
(()
=>
{
this
.
clearInput
();
if
(
this
.
chatId
)
{
chatCache
[
this
.
chatId
]
=
[];
}
});
}
/**
* 获取输入框中的内容
* @returns 返回的是节点数组
*/
private
getNodeListFromInputBox
()
{
this
.
messageInputBox
.
normalize
();
const
nodes
=
Array
.
from
(
this
.
messageInputBox
.
childNodes
);
return
this
.
combine
(
nodes
);
}
/**
* 文本,链接等需要合并成纯文本发送
*/
private
combine
(
nodes
:
ChildNode
[])
{
const
sendingNodes
:
ChildNode
[]
=
[];
let
needCreateNewNode
=
false
;
let
text
=
""
;
for
(
const
item
of
nodes
)
{
if
(
!
isImageOrFile
(
item
)
&&
item
.
textContent
)
{
if
(
needCreateNewNode
)
{
text
=
""
;
needCreateNewNode
=
false
;
}
text
+=
item
.
textContent
;
}
else
{
needCreateNewNode
=
true
;
if
(
text
)
{
this
.
checkTextLength
(
text
);
const
node
=
document
.
createTextNode
(
text
);
sendingNodes
.
push
(
node
);
}
return
sendingNodes
;
}
private
checkTextLength
(
text
:
string
)
{
if
(
text
.
length
>=
4000
)
{
throw
new
Error
(
"消息不能超过4000个字"
);
this
.
checkTextLength
(
text
);
const
node
=
document
.
createTextNode
(
text
);
sendingNodes
.
push
(
node
);
}
sendingNodes
.
push
(
item
);
}
}
private
handleSaveRange
()
{
const
sel
=
window
.
getSelection
();
if
(
sel
&&
sel
.
rangeCount
)
{
const
range
=
sel
.
getRangeAt
(
0
);
const
oldRange
=
this
.
getRange
&&
this
.
getRange
();
if
(
this
.
messageInputBox
&&
this
.
messageInputBox
.
contains
(
range
.
endContainer
)
)
{
if
(
oldRange
&&
range
.
collapsed
&&
range
.
endContainer
===
oldRange
.
endContainer
&&
range
.
endOffset
===
oldRange
.
endOffset
)
{
return
;
}
this
.
saveRange
&&
this
.
saveRange
(
range
);
}
}
}
private
saveRange
?:
(
param
:
Range
)
=>
void
private
getRange
?:
()
=>
Range
|
null
/**
* 在光标处插入元素
*/
public
insertHtmlAtCaret
=
(
function
()
{
let
range
:
Range
|
null
=
null
;
return
function
(
this
:
Input
,
html
:
string
)
{
if
(
this
.
saveRange
==
null
)
{
this
.
saveRange
=
(
alternate
)
=>
(
range
=
alternate
);
}
if
(
this
.
getRange
==
null
)
{
this
.
getRange
=
()
=>
range
;
}
const
sel
=
window
.
getSelection
();
if
(
sel
)
{
if
(
range
)
{
range
.
deleteContents
();
sel
.
removeAllRanges
();
sel
.
addRange
(
range
);
}
else
{
this
.
focus
();
range
=
sel
.
getRangeAt
(
0
);
document
.
execCommand
(
"selectAll"
);
window
.
getSelection
()?.
collapseToEnd
();
}
}
const
el
=
document
.
createElement
(
"div"
);
el
.
innerHTML
=
html
;
const
frag
=
document
.
createDocumentFragment
();
let
node
=
null
;
let
lastNode
=
null
;
while
((
node
=
el
.
firstChild
))
{
lastNode
=
frag
.
appendChild
(
node
);
}
range
?.
insertNode
(
frag
);
// Preserve the selection
if
(
lastNode
)
{
if
(
range
)
{
range
=
range
.
cloneRange
();
range
.
setStartAfter
(
lastNode
);
range
.
collapse
(
true
);
sel
?.
removeAllRanges
();
sel
?.
addRange
(
range
);
}
}
};
})()
private
selectEmoji
(
emoji
:
any
)
{
this
.
insertHtmlAtCaret
(
emoji
.
emoji_chars
);
this
.
hideEmoji
();
}
private
isImage
(
file
:
File
)
{
return
file
.
type
.
startsWith
(
"image"
);
if
(
text
)
{
this
.
checkTextLength
(
text
);
const
node
=
document
.
createTextNode
(
text
);
sendingNodes
.
push
(
node
);
}
private
buildImageHtml
(
file
:
File
)
{
const
url
=
URL
.
createObjectURL
(
file
);
return
`<img class="
${
IMAGE_INFO_CLASS
}
" tabindex="-1" src="
${
url
}
" data-image='
${
JSON
.
stringify
(
{
url
,
name
:
file
.
name
,
size
:
file
.
size
,
}
)}
'>`
;
}
return
sendingNodes
;
}
private
buildFileHtml
(
file
:
File
)
{
const
extension
=
file
.
name
.
split
(
"."
);
const
type
=
getFileType
(
file
.
name
);
return
`<div class="
${
FILE_INFO_CLASS
}
" tabindex="-1" title="
${
extension
[
extension
.
length
-
1
]
}
" data-file='
${
JSON
.
stringify
({
url
:
URL
.
createObjectURL
(
file
),
name
:
file
.
name
,
size
:
file
.
size
,
})}
'><div style="display: inline-block"><div class="file-name text-truncate text-nowrap">
${
file
.
name
}
</div><div class="file-size">
${
formatFileSize
(
file
.
size
)}
</div></div><span class="file-icon" title="
${
type
}
">
${
getSvg
(
type
)}
</span></div> `
;
// 注意
<
/div>最后面有个空
白
private
checkTextLength
(
text
:
string
)
{
if
(
text
.
length
>=
4000
)
{
throw
new
Error
(
"消息不能超过4000个字"
);
}
private
onChange
(
e
:
Event
)
{
const
target
=
e
.
target
as
HTMLElement
;
if
(
target
)
{
const
input
=
document
.
getElementById
(
target
.
id
as
string
)
as
HTMLInputElement
;
if
(
input
)
{
const
files
=
input
.
files
;
if
(
files
&&
files
.
length
)
{
let
html
=
""
;
for
(
let
index
=
0
;
index
<
files
.
length
;
index
++
)
{
const
file
=
files
[
index
];
if
(
file
.
size
<=
0
)
{
this
.
$emit
(
"error"
,
MESSAGE_FILE_EMPTY
);
return
;
}
if
(
this
.
isImage
(
file
))
{
if
(
file
.
size
>=
MAX_IMAGE_SIZE
)
{
this
.
$emit
(
"error"
,
MESSAGE_IMAGE_TOO_LARGE
);
return
;
}
html
+=
this
.
buildImageHtml
(
file
);
}
else
{
if
(
file
.
size
>=
MAX_FILE_SIZE
)
{
this
.
$emit
(
"error"
,
MESSAGE_FILE_TOO_LARGE
);
return
;
}
html
+=
this
.
buildFileHtml
(
file
);
}
}
this
.
insertHtmlAtCaret
(
html
);
}
}
}
private
handleSaveRange
()
{
const
sel
=
window
.
getSelection
();
if
(
sel
&&
sel
.
rangeCount
)
{
const
range
=
sel
.
getRangeAt
(
0
);
const
oldRange
=
this
.
getRange
&&
this
.
getRange
();
if
(
this
.
messageInputBox
&&
this
.
messageInputBox
.
contains
(
range
.
endContainer
)
)
{
if
(
oldRange
&&
range
.
collapsed
&&
range
.
endContainer
===
oldRange
.
endContainer
&&
range
.
endOffset
===
oldRange
.
endOffset
)
{
return
;
}
}
private
toggleEmoji
()
{
this
.
emojiPanelVisibility
=
!
this
.
emojiPanelVisibility
;
this
.
saveRange
&&
this
.
saveRange
(
range
);
}
}
private
hideEmoji
(
e
?:
Event
)
{
if
(
e
&&
e
.
target
)
{
const
target
=
e
.
target
as
HTMLElement
;
if
(
target
.
closest
(
".emoji-picker"
))
{
}
private
saveRange
?:
(
param
:
Range
)
=>
void
;
private
getRange
?:
()
=>
Range
|
null
;
/**
* 在光标处插入元素
*/
public
insertHtmlAtCaret
=
(
function
()
{
let
range
:
Range
|
null
=
null
;
return
function
(
this
:
Input
,
html
:
string
)
{
if
(
this
.
saveRange
==
null
)
{
this
.
saveRange
=
(
alternate
)
=>
(
range
=
alternate
);
}
if
(
this
.
getRange
==
null
)
{
this
.
getRange
=
()
=>
range
;
}
const
sel
=
window
.
getSelection
();
if
(
sel
)
{
if
(
range
)
{
range
.
deleteContents
();
sel
.
removeAllRanges
();
sel
.
addRange
(
range
);
}
else
{
this
.
focus
();
range
=
sel
.
getRangeAt
(
0
);
document
.
execCommand
(
"selectAll"
);
window
.
getSelection
()?.
collapseToEnd
();
}
}
const
el
=
document
.
createElement
(
"div"
);
el
.
innerHTML
=
html
;
const
frag
=
document
.
createDocumentFragment
();
let
node
=
null
;
let
lastNode
=
null
;
while
((
node
=
el
.
firstChild
))
{
lastNode
=
frag
.
appendChild
(
node
);
}
range
?.
insertNode
(
frag
);
// Preserve the selection
if
(
lastNode
)
{
if
(
range
)
{
range
=
range
.
cloneRange
();
range
.
setStartAfter
(
lastNode
);
range
.
collapse
(
true
);
sel
?.
removeAllRanges
();
sel
?.
addRange
(
range
);
}
}
};
})();
private
selectEmoji
(
emoji
:
any
)
{
this
.
insertHtmlAtCaret
(
emoji
.
emoji_chars
);
this
.
hideEmoji
();
}
private
isImage
(
file
:
File
)
{
return
file
.
type
.
startsWith
(
"image"
);
}
private
buildImageHtml
(
file
:
File
)
{
const
url
=
URL
.
createObjectURL
(
file
);
return
`<img class="
${
IMAGE_INFO_CLASS
}
" tabindex="-1" src="
${
url
}
" data-image='
${
JSON
.
stringify
(
{
url
,
name
:
file
.
name
,
size
:
file
.
size
,
}
)}
'>`
;
}
private
buildFileHtml
(
file
:
File
)
{
const
extension
=
file
.
name
.
split
(
"."
);
const
type
=
getFileType
(
file
.
name
);
return
`<div class="
${
FILE_INFO_CLASS
}
" tabindex="-1" title="
${
extension
[
extension
.
length
-
1
]
}
" data-file='
${
JSON
.
stringify
({
url
:
URL
.
createObjectURL
(
file
),
name
:
file
.
name
,
size
:
file
.
size
,
})}
'><div style="display: inline-block"><div class="file-name text-truncate text-nowrap">
${
file
.
name
}
</div><div class="file-size">
${
formatFileSize
(
file
.
size
)}
</div></div><span class="file-icon" title="
${
type
}
">
${
getSvg
(
type
)}
</span></div> `
;
// 注意
<
/div>最后面有个空
白
}
private
onChange
(
e
:
Event
)
{
const
target
=
e
.
target
as
HTMLElement
;
if
(
target
)
{
const
input
=
document
.
getElementById
(
target
.
id
as
string
)
as
HTMLInputElement
;
if
(
input
)
{
const
files
=
input
.
files
;
if
(
files
&&
files
.
length
)
{
let
html
=
""
;
for
(
let
index
=
0
;
index
<
files
.
length
;
index
++
)
{
const
file
=
files
[
index
];
if
(
file
.
size
<=
0
)
{
this
.
$emit
(
"error"
,
MESSAGE_FILE_EMPTY
);
return
;
}
if
(
this
.
isImage
(
file
))
{
if
(
file
.
size
>=
MAX_IMAGE_SIZE
)
{
this
.
$emit
(
"error"
,
MESSAGE_IMAGE_TOO_LARGE
);
return
;
}
html
+=
this
.
buildImageHtml
(
file
);
}
else
{
if
(
file
.
size
>=
MAX_FILE_SIZE
)
{
this
.
$emit
(
"error"
,
MESSAGE_FILE_TOO_LARGE
);
return
;
}
html
+=
this
.
buildFileHtml
(
file
);
}
}
this
.
insertHtmlAtCaret
(
html
);
}
this
.
emojiPanelVisibility
=
false
;
}
}
private
setupEmoji
()
{
EmojiService
.
onReady
(()
=>
{
const
service
=
new
EmojiService
();
service
.
getEmoji
().
then
((
r
)
=>
{
if
(
r
)
{
this
.
emoji
=
r
.
list
;
}
});
});
}
private
toggleEmoji
()
{
this
.
emojiPanelVisibility
=
!
this
.
emojiPanelVisibility
;
}
private
hideEmoji
(
e
?:
Event
)
{
if
(
e
&&
e
.
target
)
{
const
target
=
e
.
target
as
HTMLElement
;
if
(
target
.
closest
(
".emoji-picker"
))
{
return
;
}
}
this
.
emojiPanelVisibility
=
false
;
}
private
setupEmoji
()
{
EmojiService
.
onReady
(()
=>
{
const
service
=
new
EmojiService
();
service
.
getEmoji
().
then
((
r
)
=>
{
if
(
r
)
{
this
.
emoji
=
r
.
list
;
}
});
});
}
}
</
script
>
<
style
lang=
"scss"
scoped
>
.input-wrap
{
position
:
relative
;
padding-left
:
20px
;
position
:
relative
;
padding-left
:
20px
;
/deep/.input-el-scrollbar.el-scrollbar
{
//
28px
:
tool-bar
的高度
height
:
calc
(
100%
-
28px
);
/deep/.input-el-scrollbar.el-scrollbar
{
//
28px
:
tool-bar
的高度
height
:
calc
(
100%
-
28px
);
>
.el-scrollbar__wrap
{
overflow-x
:
hidden
;
}
>
.el-scrollbar__wrap
{
overflow-x
:
hidden
;
}
>
.el-scrollbar__wrap
>
.el-scrollbar__view
{
min-height
:
100%
;
display
:
flex
;
//
输入框
>
.input-container
{
width
:
100%
;
font-size
:
14px
;
padding
:
10px
20px
10px
0
;
outline
:
0
;
white-space
:
pre-wrap
;
user-select
:
text
;
&::selection
{
background-color
:
#cce6fc
;
}
>
.el-scrollbar__wrap
>
.el-scrollbar__view
{
min-height
:
100%
;
display
:
flex
;
img
{
max-width
:
300px
;
font-size
:
50px
;
vertical-align
:
bottom
;
border
:
1px
solid
transparent
;
//
输入框
>
.input-container
{
width
:
100%
;
font-size
:
14px
;
padding
:
10px
20px
10px
0
;
outline
:
0
;
white-space
:
pre-wrap
;
user-select
:
text
;
&:focus
{
border-color
:
var
(
--main-color
)
;
}
&::selection
{
background-color
:
#cce6fc
;
}
&
::selection
{
background-color
:
#cce6fc
;
}
}
img
{
max-width
:
300px
;
font-size
:
50px
;
vertical-align
:
bottom
;
border
:
1px
solid
transparent
;
.file-info
{
padding
:
10px
;
border
:
1px
solid
#c5d4e5
;
border-radius
:
4px
;
display
:
inline-block
;
margin-right
:
10px
;
-webkit-user-modify
:
read-only
;
user-select
:
none
;
.file-name
{
color
:
#000
;
font-size
:
14px
;
max-width
:
130px
;
}
&:focus
{
border-color
:
var
(
--main-color
);
}
.file-size
{
margin-top
:
10px
;
}
}
}
&
::selection
{
background-color
:
#cce6fc
;
}
}
}
.input-emoji
{
position
:
absolute
;
left
:
0
;
top
:
-225px
;
outline
:
0
;
.file-info
{
padding
:
10px
;
border
:
1px
solid
#c5d4e5
;
border-radius
:
4px
;
display
:
inline-block
;
margin-right
:
10px
;
-webkit-user-modify
:
read-only
;
user-select
:
none
;
.file-name
{
color
:
#000
;
font-size
:
14px
;
max-width
:
130px
;
}
.file-size
{
margin-top
:
10px
;
}
}
}
}
}
.input-emoji
{
position
:
absolute
;
left
:
0
;
top
:
-225px
;
outline
:
0
;
}
}
.tool-bar
{
padding-top
:
10px
;
user-select
:
none
;
.tool-bar-icon
{
height
:
16px
;
width
:
16px
;
cursor
:
pointer
;
}
.offset
{
margin
:
0
22px
;
}
padding-top
:
10px
;
user-select
:
none
;
.tool-bar-icon
{
height
:
16px
;
width
:
16px
;
cursor
:
pointer
;
}
.offset
{
margin
:
0
22px
;
}
}
.emoji-picker
{
position
:
absolute
;
z-index
:
2
;
bottom
:
99px
;
left
:
-1px
;
background-color
:
#fff
;
padding
:
20px
;
padding-right
:
0
;
padding-bottom
:
10px
;
border
:
1px
solid
#f0f0f0
;
right
:
45px
;
overflow
:
hidden
;
.el-scrollbar
{
height
:
200px
;
}
.emoji-item
{
display
:
inline-flex
;
cursor
:
pointer
;
min-width
:
35px
;
min-height
:
25px
;
font-size
:
20px
;
vertical-align
:
top
;
margin-top
:
5px
;
justify-content
:
center
;
align-items
:
center
;
}
position
:
absolute
;
z-index
:
2
;
bottom
:
99px
;
left
:
-1px
;
background-color
:
#fff
;
padding
:
20px
;
padding-right
:
0
;
padding-bottom
:
10px
;
border
:
1px
solid
#f0f0f0
;
right
:
45px
;
overflow
:
hidden
;
.el-scrollbar
{
height
:
200px
;
}
.emoji-item
{
display
:
inline-flex
;
cursor
:
pointer
;
min-width
:
35px
;
min-height
:
25px
;
font-size
:
20px
;
vertical-align
:
top
;
margin-top
:
5px
;
justify-content
:
center
;
align-items
:
center
;
}
}
#chat-upload-file
{
visibility
:
hidden
;
position
:
absolute
;
visibility
:
hidden
;
position
:
absolute
;
}
</
style
>
message-input.vue
View file @
86932012
<
template
>
<div
class=
"h-100"
>
<chat-input
ref=
"chat-input"
@
input=
"onInput"
@
send=
"sendMessage"
@
error=
"onError"
/>
</div>
<div
class=
"h-100"
>
<chat-input
ref=
"chat-input"
@
input=
"onInput"
@
send=
"sendMessage"
@
error=
"onError"
/>
</div>
</
template
>
<
script
lang=
"ts"
>
import
{
Component
,
Ref
,
Vue
,
Watch
}
from
"vue-property-decorator"
;
import
ChatInput
,
{
FILE_INFO_CLASS
,
isImageOrFile
,
FILE_INFO_CLASS
,
isImageOrFile
,
}
from
"./hybrid-input/index.vue"
;
import
{
Message
}
from
"./model"
;
import
{
uploadFile
}
from
"./service/upload"
;
...
...
@@ -26,169 +26,167 @@ let sendingMessageIndex = 1;
@
Component
({
components
:
{
ChatInput
}
})
export
default
class
MessageInput
extends
Vue
{
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_DIALOG_VISIBLE
)
private
readonly
chatRoomVisible
:
ChatStore
.
STATE_CHAT_DIALOG_VISIBLE
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_DIALOG_VISIBLE
)
private
readonly
chatRoomVisible
!
:
ChatStore
.
STATE_CHAT_DIALOG_VISIBLE
;
@
chatStore
.
Action
(
ChatStore
.
ACTION_SEND_MESSAGE
)
private
readonly
sendMsg
!
:
ChatStore
.
ACTION_SEND_MESSAGE
@
chatStore
.
Action
(
ChatStore
.
ACTION_SEND_MESSAGE
)
private
readonly
sendMsg
!
:
ChatStore
.
ACTION_SEND_MESSAGE
;
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
)
private
readonly
chatId
!
:
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
;
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_MY_ID
)
private
readonly
chatMyId
!
:
ChatStore
.
STATE_CHAT_MY_ID
@
chatStore
.
State
(
ChatStore
.
STATE_CHAT_MY_ID
)
private
readonly
chatMyId
!
:
ChatStore
.
STATE_CHAT_MY_ID
;
@
chatStore
.
State
(
ChatStore
.
STATE_CURRENT_CHAT_INITING
)
private
readonly
chatIniting
!
:
ChatStore
.
STATE_CURRENT_CHAT_INITING
@
chatStore
.
State
(
ChatStore
.
STATE_CURRENT_CHAT_INITING
)
private
readonly
chatIniting
!
:
ChatStore
.
STATE_CURRENT_CHAT_INITING
;
@
chatStore
.
Getter
(
ChatStore
.
STATE_CHAT_SOURCE
)
private
readonly
source
!
:
ChatStore
.
STATE_CHAT_SOURCE
@
chatStore
.
Getter
(
ChatStore
.
STATE_CHAT_SOURCE
)
private
readonly
source
!
:
ChatStore
.
STATE_CHAT_SOURCE
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_APPEND_SENDING_MESSAGE
)
private
readonly
appendSendingMessages
!
:
ChatStore
.
MUTATION_APPEND_SENDING_MESSAGE
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_APPEND_SENDING_MESSAGE
)
private
readonly
appendSendingMessages
!
:
ChatStore
.
MUTATION_APPEND_SENDING_MESSAGE
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_FAILED_SENDING_MESSAGE
)
private
readonly
failedSendingMessage
!
:
ChatStore
.
MUTATION_FAILED_SENDING_MESSAGE
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_FAILED_SENDING_MESSAGE
)
private
readonly
failedSendingMessage
!
:
ChatStore
.
MUTATION_FAILED_SENDING_MESSAGE
;
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_REMOVE_SENDING_MESSAGE
)
private
readonly
removeSendingMessages
!
:
ChatStore
.
MUTATION_REMOVE_SENDING_MESSAGE
@
chatStore
.
Mutation
(
ChatStore
.
MUTATION_REMOVE_SENDING_MESSAGE
)
private
readonly
removeSendingMessages
!
:
ChatStore
.
MUTATION_REMOVE_SENDING_MESSAGE
;
@
Ref
(
"chat-input"
)
private
readonly
chatInput
!
:
ChatInput
@
Ref
(
"chat-input"
)
private
readonly
chatInput
!
:
ChatInput
;
@
Watch
(
"chatRoomVisible"
)
private
whenChatRoomShow
()
{
if
(
!
this
.
chatRoomVisible
)
return
;
this
.
chatInput
.
focus
();
}
@
Watch
(
"chatRoomVisible"
)
private
whenChatRoomShow
()
{
if
(
!
this
.
chatRoomVisible
)
return
;
this
.
chatInput
.
focus
();
}
private
async
sendMessage
(
msg
:
ChildNode
[],
done
:
()
=>
void
)
{
if
(
this
.
chatIniting
)
{
return
;
private
async
sendMessage
(
msg
:
ChildNode
[],
done
:
()
=>
void
)
{
if
(
this
.
chatIniting
)
{
return
;
}
for
(
const
item
of
msg
)
{
if
(
isImageOrFile
(
item
))
{
if
((
item
as
Element
).
classList
.
contains
(
FILE_INFO_CLASS
))
{
this
.
sendFile
(
item
,
"file"
);
}
else
{
this
.
sendFile
(
item
,
"image"
);
}
for
(
const
item
of
msg
)
{
if
(
isImageOrFile
(
item
))
{
if
((
item
as
Element
).
classList
.
contains
(
FILE_INFO_CLASS
))
{
this
.
sendFile
(
item
,
"file"
);
}
else
{
this
.
sendFile
(
item
,
"image"
);
}
continue
;
}
continue
;
}
if
(
item
.
textContent
)
{
this
.
sendText
(
item
.
textContent
);
}
}
ChatLoggerService
.
logger
?.
debug
(
"all messages sent"
);
done
();
this
.
$emit
(
"sent"
);
if
(
item
.
textContent
)
{
this
.
sendText
(
item
.
textContent
);
}
}
private
async
onInput
()
{
if
(
this
.
chatId
==
null
)
return
;
await
xim
.
inputing
(
this
.
chatId
);
ChatLoggerService
.
logger
?.
debug
(
"all messages sent"
);
done
();
this
.
$emit
(
"sent"
);
}
private
async
onInput
()
{
if
(
this
.
chatId
==
null
)
return
;
await
xim
.
inputing
(
this
.
chatId
);
}
private
sendText
(
text
:
string
)
{
if
(
text
&&
text
.
trim
())
{
const
msg
=
{
text
:
text
.
trim
()
};
if
(
this
.
source
)
{
Object
.
assign
(
msg
,
{
source
:
this
.
source
});
}
this
.
sendMsg
({
msgType
:
"text"
,
msg
:
JSON
.
stringify
(
msg
)
});
}
private
sendText
(
text
:
string
)
{
if
(
text
&&
text
.
trim
())
{
const
msg
=
{
text
:
text
.
trim
()
};
if
(
this
.
source
)
{
Object
.
assign
(
msg
,
{
source
:
this
.
source
});
}
this
.
sendMsg
({
msgType
:
"text"
,
msg
:
JSON
.
stringify
(
msg
)
});
}
private
async
sendFile
(
file
:
any
,
type
:
"image"
|
"file"
)
{
const
src
=
JSON
.
parse
(
file
.
attributes
[
`data-
${
type
}
`
]?.
value
||
""
)
as
{
url
:
string
;
name
:
string
;
size
:
number
;
};
if
(
src
)
{
const
index
=
this
.
sendSendingMessage
(
type
,
src
);
const
file
=
await
this
.
readBlobUrl2Base64
(
src
.
url
,
src
.
name
);
if
(
file
)
{
let
w
=
0
;
let
h
=
0
;
if
(
type
===
"image"
)
{
const
img
=
new
Image
();
img
.
src
=
src
.
url
;
img
.
onload
=
function
()
{
w
=
img
.
naturalWidth
;
h
=
img
.
naturalHeight
;
};
img
.
remove
();
}
}
private
async
sendFile
(
file
:
any
,
type
:
"image"
|
"file"
)
{
const
src
=
JSON
.
parse
(
file
.
attributes
[
`data-
${
type
}
`
]?.
value
||
""
)
as
{
url
:
string
;
name
:
string
;
size
:
number
;
};
if
(
src
)
{
const
index
=
this
.
sendSendingMessage
(
type
,
src
);
const
file
=
await
this
.
readBlobUrl2Base64
(
src
.
url
,
src
.
name
);
if
(
file
)
{
let
w
=
0
;
let
h
=
0
;
if
(
type
===
"image"
)
{
const
img
=
new
Image
();
img
.
src
=
src
.
url
;
img
.
onload
=
function
()
{
w
=
img
.
naturalWidth
;
h
=
img
.
naturalHeight
;
};
img
.
remove
();
}
uploadFile
(
file
,
this
.
chatId
||
0
,
w
,
h
)
.
then
((
r
)
=>
{
if
(
r
)
{
const
msg
=
{
url
:
r
,
name
:
file
.
name
,
size
:
file
.
size
,
};
if
(
this
.
source
)
{
Object
.
assign
(
msg
,
{
source
:
this
.
source
});
}
if
(
w
&&
h
)
{
Object
.
assign
(
msg
,
{
w
,
h
});
}
this
.
sendMsg
({
msgType
:
type
,
msg
:
JSON
.
stringify
(
msg
),
});
this
.
removeSendingMessages
(
index
);
URL
.
revokeObjectURL
(
src
.
url
);
}
else
{
this
.
setMsg2Failed
(
index
);
}
})
.
catch
((
e
)
=>
{
// eslint-disable-next-line no-console
console
.
error
(
e
);
this
.
setMsg2Failed
(
index
);
});
}
}
}
uploadFile
(
file
,
this
.
chatId
||
0
,
w
,
h
)
.
then
((
r
)
=>
{
if
(
r
)
{
const
msg
=
{
url
:
r
,
name
:
file
.
name
,
size
:
file
.
size
,
};
if
(
this
.
source
)
{
Object
.
assign
(
msg
,
{
source
:
this
.
source
});
}
private
setMsg2Failed
(
index
:
number
)
{
this
.
failedSendingMessage
(
index
);
}
if
(
w
&&
h
)
{
Object
.
assign
(
msg
,
{
w
,
h
}
);
}
private
sendSendingMessage
(
type
:
string
,
msg
:
any
)
{
const
index
=
sendingMessageIndex
++
;
if
(
this
.
source
)
{
Object
.
assign
(
msg
,
{
source
:
this
.
source
,
eid
:
this
.
chatMyId
});
}
if
(
this
.
chatId
)
{
this
.
appendSendingMessages
({
id
:
-
index
,
chat_id
:
this
.
chatId
,
ts
:
Date
.
now
(),
type
,
this
.
sendMsg
({
msgType
:
type
,
msg
:
JSON
.
stringify
(
msg
),
}
as
Message
);
return
-
index
;
}
return
0
;
});
this
.
removeSendingMessages
(
index
);
URL
.
revokeObjectURL
(
src
.
url
);
}
else
{
this
.
setMsg2Failed
(
index
);
}
})
.
catch
((
e
)
=>
{
// eslint-disable-next-line no-console
console
.
error
(
e
);
this
.
setMsg2Failed
(
index
);
});
}
}
}
private
readBlobUrl2Base64
(
url
:
string
,
name
:
string
)
{
return
fetch
(
url
)
.
then
((
r
)
=>
r
.
blob
())
.
then
((
blob
)
=>
new
File
([
blob
],
name
));
}
private
setMsg2Failed
(
index
:
number
)
{
this
.
failedSendingMessage
(
index
);
}
private
onError
(
e
:
any
)
{
this
.
$emit
(
"error"
,
e
.
message
||
e
);
private
sendSendingMessage
(
type
:
string
,
msg
:
any
)
{
const
index
=
sendingMessageIndex
++
;
if
(
this
.
source
)
{
Object
.
assign
(
msg
,
{
source
:
this
.
source
,
eid
:
this
.
chatMyId
});
}
if
(
this
.
chatId
)
{
this
.
appendSendingMessages
({
id
:
-
index
,
chat_id
:
this
.
chatId
,
ts
:
Date
.
now
(),
type
,
msg
:
JSON
.
stringify
(
msg
),
}
as
Message
);
return
-
index
;
}
return
0
;
}
private
readBlobUrl2Base64
(
url
:
string
,
name
:
string
)
{
return
fetch
(
url
)
.
then
((
r
)
=>
r
.
blob
())
.
then
((
blob
)
=>
new
File
([
blob
],
name
));
}
private
onError
(
e
:
any
)
{
this
.
$emit
(
"error"
,
e
.
message
||
e
);
}
}
</
script
>
service/request.ts
View file @
86932012
import
Axios
from
"axios"
;
import
qs
from
"qs"
;
import
chat
from
"../xim/index"
;
export
function
buildConfig
(
token
:
string
,
url
:
string
)
{
if
(
url
&&
url
.
includes
(
"/general"
))
{
return
{
headers
:
{
Authorization
:
token
,
CurrentOrg
:
this
.
station
}
};
return
{
headers
:
{
Authorization
:
token
,
CurrentOrg
:
chat
.
getOrgId
()
}
};
}
return
{
headers
:
{
Authorization
:
token
}
};
}
...
...
store/index.ts
View file @
86932012
...
...
@@ -29,7 +29,7 @@ function uniqueMessages(
messages
:
NonNullable
<
ChatStore
.
STATE_CHAT_MSG_HISTORY
>
)
{
const
arr
=
[...
messages
];
return
unique
(
arr
,
function
(
item
,
all
)
{
return
unique
(
arr
,
function
(
item
,
all
)
{
return
all
.
findIndex
((
k
)
=>
k
.
id
===
item
.
id
);
});
}
...
...
@@ -254,7 +254,7 @@ export default {
state
[
ChatStore
.
STATE_CHAT_SENDING_MESSAGES
]
=
[...
current
];
}
},
[
ChatStore
.
MUTATION_SAVE_CURRENT_CHAT_INPUTING
]:
(
function
()
{
[
ChatStore
.
MUTATION_SAVE_CURRENT_CHAT_INPUTING
]:
(
function
()
{
const
setTimeoutId
:
{
[
key
:
string
]:
number
}
=
{};
return
(
state
:
ChatStoreState
,
...
...
@@ -432,7 +432,7 @@ export default {
detailManager
.
done
();
const
{
id
}
=
await
action
.
dryExecute
();
// 无法得到chat id
await
sdk
()
await
sdk
()
.
model
(
UniplatChatModelName
)
.
action
(
"createXimChat"
)
.
updateInitialParams
({
...
...
@@ -441,9 +441,12 @@ export default {
.
dryExecute
();
commit
(
ChatStore
.
MUTATION_HIDE_CHAT_CREATOR
);
await
dispatch
(
ChatStore
.
ACTION_GET_MY_CHAT_LIST
);
const
newChat
=
state
[
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
].
list
.
find
(
const
roomList
=
state
[
ChatStore
.
STATE_MY_CHAT_ROOM_LIST
];
if
(
roomList
==
null
)
return
const
newChat
=
roomList
.
list
.
find
(
(
k
)
=>
k
.
uniplatId
===
id
);
if
(
newChat
==
null
)
return
commit
(
ChatStore
.
MUTATION_SHOW_CHAT
);
await
dispatch
(
ChatStore
.
ACTION_SAVE_CURRENT_CHAT_ID_VERSION
,
{
chatId
:
newChat
.
chat_id
,
...
...
@@ -538,10 +541,11 @@ export default {
const
chatId
=
state
[
ChatStore
.
STATE_CHAT_CURRENT_CHAT_ID
];
if
(
chatId
==
null
)
return
;
const
getChatMembersResult
=
await
xim
.
fetchChatMembers
(
chatId
);
if
(
getChatMembersResult
==
null
)
return
const
chatMembers
=
getChatMembersResult
.
args
[
0
]
as
ChatMember
[];
const
newChatMembers
=
await
Promise
.
all
(
chatMembers
.
map
(
async
(
member
)
=>
{
let
result
:
ChatStore
.
STATE_CURRENT_CHAT_MEMBERS
[
number
];
let
result
:
NonNullable
<
ChatStore
.
STATE_CURRENT_CHAT_MEMBERS
>
[
number
];
try
{
const
info
=
await
sdk
()
.
model
(
"user"
)
...
...
@@ -562,13 +566,14 @@ export default {
);
commit
(
ChatStore
.
MUTATION_SAVE_CURRENT_CHAT_MEMBERS
,
unique
(
newChatMembers
,
function
(
item
,
all
)
{
unique
(
newChatMembers
,
function
(
item
,
all
)
{
return
all
.
findIndex
((
k
)
=>
k
.
eid
===
item
.
eid
);
})
);
},
async
[
ChatStore
.
ACTION_TERINATE_CHAT
]({
state
,
dispatch
})
{
const
v
=
state
[
ChatStore
.
STATE_CHAT_CURRENT_CHAT_VERSION
];
if
(
v
==
null
)
return
const
id
=
Number
(
state
[
ChatStore
.
STATE_CHAT_CURRENT_CHAT_UNIPLAT_ID
]
);
...
...
store/model.ts
View file @
86932012
...
...
@@ -30,7 +30,7 @@ export namespace ChatStore {
is_finish
:
0
|
1
;
}[];
total
:
number
;
}
}
|
null
export
const
STATE_CHAT_MSG_HISTORY
=
"某个会话聊天记录"
;
export
type
STATE_CHAT_MSG_HISTORY
=
dto
.
MessageRequestResult
|
null
...
...
xim/index.ts
View file @
86932012
...
...
@@ -44,7 +44,9 @@ class Chat {
}
public
getSdk
=
()
=>
{
if
(
this
.
_sdk
==
null
)
return
;
if
(
this
.
_sdk
==
null
)
{
throw
new
Error
(
"sdk shouldn't undefined"
)
};
return
this
.
_sdk
();
};
...
...
@@ -90,7 +92,7 @@ class Chat {
}
public
getUserMapping
()
{
return
{};
return
{}
as
any
;
}
private
debug
(
message
:
string
)
{
...
...
xim/xim.ts
View file @
86932012
...
...
@@ -145,7 +145,9 @@ export class Xim {
desc
=
true
):
Promise
<
Message
[]
>
{
this
.
checkConnected
();
if
(
this
.
client
==
null
)
return
;
if
(
this
.
client
==
null
)
{
throw
new
Error
(
"client shouldn't undefined"
)
};
const
res
=
await
this
.
client
.
fetchChatMsgs
(
chatType
,
chatId
,
{
lid
,
rid
,
...
...
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