MessageSend.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. <template>
  2. <div class="flex radius-base overflow-hidden message-list-modal">
  3. <template v-if="!!replyUserId">
  4. <slot></slot>
  5. </template>
  6. <div class="flex direction-column p-base fill-lighter tree-box">
  7. <el-input v-model="filterText" placeholder="输入评卷员账号或名称筛选" clearable></el-input>
  8. <div class="flex-1 m-t-base scroll-y-auto">
  9. <el-tree
  10. ref="treeRef"
  11. show-checkbox
  12. node-key="id"
  13. :filter-node-method="filterTree"
  14. :data="markerTree"
  15. :props="treeProp"
  16. @check-change="onCheckChange"
  17. ></el-tree>
  18. </div>
  19. </div>
  20. <div class="message-info-container" :class="{ big: !showCheckUser && !replyUserId }">
  21. <div class="flex direction-column message-info">
  22. <div class="flex items-center p-base message-info-header">
  23. <div class="flex flex-1 items-center overflow-hidden send-user">
  24. <span class="m-r-mini">收件人</span>
  25. <span class="flex flex-1 overflow-hidden items-center justify-between radius-base m-r-base user-name">
  26. <p class="flex-1 checked-users flex" :class="{ big: showCheckUser }">
  27. <span class="split-names">{{ viewCheckedUser }}</span>
  28. <span v-if="checkedUsers.length > 3">共计{{ checkedUsers.length }}个人员</span>
  29. </p>
  30. <!-- <el-button v-if="!replyUserId" class="m-l-base" size="small" type="primary" @click="toggleCheckUser">
  31. 选择收件人
  32. </el-button> -->
  33. </span>
  34. </div>
  35. <div class="grid pointer m-l-auto close-icon" @click="$emit('close')">
  36. <el-icon><close /></el-icon>
  37. </div>
  38. </div>
  39. <div class="flex-1 p-base overflow-hidden">
  40. <div class="full-h radius-base p-base message-info-content">
  41. <div style="height: calc(100% - 18px)" class="scroll-y-auto">
  42. <content-edit-able
  43. v-model="messageContent"
  44. class="full content-edit-able"
  45. @click="onContentClick"
  46. ></content-edit-able>
  47. </div>
  48. <div class="limit-tip">{{ messageContent.length }} / 2000</div>
  49. </div>
  50. </div>
  51. <div class="p-base flex items-center justify-end">
  52. <el-button v-show="showSendPaper" size="small" plain @click="sendCurrentPaper">发送当前试卷</el-button>
  53. <el-button size="small" type="primary" :disabled="!allowSend" :loading="loading" @click="onSendMessage">
  54. 发送
  55. </el-button>
  56. <el-button v-if="checkedUsers.length == 1" size="small" plain @click="toggleHistory">历史消息</el-button>
  57. </div>
  58. </div>
  59. <message-history
  60. v-if="showHistory"
  61. :send-user-id="checkedUsers.length == 1 ? checkedUsers[0].id : undefined"
  62. ></message-history>
  63. </div>
  64. </div>
  65. <image-preview
  66. v-model="previewModalVisible"
  67. resize-key="can-resize22"
  68. :url="props.paperPath || ''"
  69. :is-big="true"
  70. ></image-preview>
  71. </template>
  72. <script setup lang="tsx" name="MessageSend">
  73. import { ref, computed, watch, nextTick } from 'vue'
  74. import { ElInput, ElButton, ElTree, ElIcon, ElMessage } from 'element-plus'
  75. import { Close } from '@element-plus/icons-vue'
  76. import ContentEditAble from '@/components/common/ContentEditAble.vue'
  77. import ImagePreview from '../ImagePreview.vue'
  78. import useFetch from '@/hooks/useFetch'
  79. import useVModel from '@/hooks/useVModel'
  80. import MessageHistory from '@/components/shared/message/MessageHistory.vue'
  81. import { transHtmlContent } from '@/utils/common'
  82. import type { ExtractApiResponse } from '@/api/api'
  83. type MarkerItem = ExtractArrayValue<ExtractApiResponse<'getUserGroup'>['chiffGroup']>
  84. type TreeNode = ExtractArrayValue<ExtractApiResponse<'getUserGroup'>['markerGroup']> & { label: string }
  85. const props = defineProps<{
  86. replyUserId?: number | null
  87. paperPath?: string | null
  88. replyUserName?: string | null
  89. }>()
  90. const customNodeClass = (data: any, node: any) => {
  91. if (data.online === true) {
  92. return 'is-online'
  93. } else if (data.online === false) {
  94. return 'is-offline'
  95. }
  96. return null
  97. }
  98. /** cleat warning */
  99. const emit = defineEmits(['close', 'change-type', 'reply'])
  100. const showHistory = ref<boolean>(false)
  101. /** 默认的收件人ID */
  102. const replyUserId = useVModel(props, 'replyUserId')
  103. const replyUserName = useVModel(props, 'replyUserName')
  104. /** 图片预览 */
  105. const previewModalVisible = ref<boolean>(false)
  106. /** 显示收件人tree */
  107. const showCheckUser = ref<boolean>(true)
  108. /** 消息内容 */
  109. const messageContent = ref<string>('')
  110. /** 选中的收件人 */
  111. const checkedUsers = ref<MarkerItem[]>([])
  112. /** 收件人tree 筛选关键字 */
  113. const filterText = ref<string>('')
  114. /** 是否已发送试卷 */
  115. const sendedPaper = ref<boolean>(false)
  116. /** 显示发送试卷按钮 */
  117. const showSendPaper = computed<boolean>(() => {
  118. return !!props.paperPath && !sendedPaper.value
  119. })
  120. const allowSend = computed<boolean>(() => {
  121. return !!(
  122. (messageContent.value && checkedUsers.value?.filter((v) => !!v.id)?.length) ||
  123. (replyUserId.value && messageContent.value)
  124. )
  125. })
  126. const treeRef = ref<InstanceType<typeof ElTree>>()
  127. const viewCheckedUser = computed(() => {
  128. let arr = checkedUsers?.value?.filter((v) => !!v.id)?.map((d) => `${d.loginName}-${d.name}`)
  129. if (!!replyUserName.value && !arr.includes(replyUserName.value)) {
  130. arr.unshift(replyUserName.value)
  131. }
  132. return arr.join(';')
  133. // return checkedUsers?.value
  134. // ?.filter((v) => !!v.id)
  135. // ?.map((d) => `${d.loginName}-${d.name}`)
  136. // ?.join(';')
  137. })
  138. const toggleCheckUser = () => {
  139. showCheckUser.value = !showCheckUser.value
  140. }
  141. function isMarker(x: any): x is MarkerItem {
  142. return !!x.loginName
  143. }
  144. const treeProp = {
  145. children: 'markers',
  146. label(treeNode: TreeNode) {
  147. if (isMarker(treeNode)) {
  148. // return treeNode.name || treeNode.loginName
  149. return `${treeNode.loginName}-${treeNode.name}`
  150. }
  151. return treeNode.label
  152. },
  153. class: customNodeClass,
  154. } as unknown as InstanceType<typeof ElTree>['props']
  155. const filterTree = ((value: string, data: MarkerItem) => {
  156. if (!value) return true
  157. return data.name?.includes(value) || data.loginName?.includes(value)
  158. }) as unknown as InstanceType<typeof ElTree>['filterNodeMethod']
  159. watch(filterText, () => {
  160. treeRef?.value?.filter(filterText.value)
  161. })
  162. const { fetch: getUserGroup, result: userGroup } = useFetch('getUserGroup')
  163. const markerTree = computed<TreeNode[]>(() => {
  164. if (!userGroup?.value) {
  165. return []
  166. }
  167. const { chiffGroup, deputyGroup, markerGroup } = userGroup.value
  168. return [
  169. chiffGroup?.length ? { label: '大组长', markingGroupNumber: -1, markers: chiffGroup } : null,
  170. deputyGroup?.length ? { label: '小组长', markingGroupNumber: -1, markers: deputyGroup } : null,
  171. markerGroup?.length
  172. ? { label: '评卷小组', markers: markerGroup.map((d) => ({ ...d, label: `第${d.markingGroupNumber}组` })) }
  173. : null,
  174. ].filter(Boolean) as TreeNode[]
  175. })
  176. watch(
  177. [replyUserId, markerTree],
  178. () => {
  179. nextTick(() => {
  180. replyUserId.value && markerTree.value.length && treeRef?.value?.setCheckedKeys([replyUserId.value])
  181. })
  182. },
  183. { immediate: true }
  184. )
  185. const onCheckChange = () => {
  186. checkedUsers.value = (treeRef?.value?.getCheckedNodes(true) as MarkerItem[]).filter((v) => !!v.id) || []
  187. if (checkedUsers.value.length != 1) {
  188. showHistory.value = false
  189. }
  190. }
  191. watch(messageContent, () => {
  192. // let reg = /<span class=\"pointer inline link-button\" contenteditable=\"false\" data-path=.*>查看试卷<\/span>/g
  193. // let arr = messageContent.value.match(reg)
  194. // let text = messageContent.value.replace(reg, '【查看试卷临时替换字符】')
  195. // text = transHtmlContent(text)
  196. // if (arr && arr.length) {
  197. // text = text.replace('【查看试卷临时替换字符】', arr[0])
  198. // }
  199. // messageContent.value = text
  200. nextTick(() => {
  201. const paperButton = document.querySelector(`.content-edit-able [data-path="${props.paperPath}"]`)
  202. sendedPaper.value = !!paperButton
  203. })
  204. })
  205. const onContentClick = (e: Event) => {
  206. const target = e.target as HTMLButtonElement
  207. const path = target.getAttribute('data-path')
  208. if (path) {
  209. previewModalVisible.value = true
  210. }
  211. }
  212. /** 发送当前试卷 */
  213. const sendCurrentPaper = () => {
  214. if (props.paperPath) {
  215. messageContent.value += `<span class="pointer inline link-button" contenteditable="false" data-path="${props.paperPath}">查看试卷</span>`
  216. }
  217. }
  218. const { fetch: sendMessage, loading } = useFetch('sendMessage')
  219. /** 发送消息 */
  220. const onSendMessage = async () => {
  221. try {
  222. if (messageContent.value.length > 2000) {
  223. return ElMessage.error('输入内容过长')
  224. }
  225. let ids: any = checkedUsers.value?.filter(Boolean)?.map((u) => u.id)
  226. if (replyUserId?.value && ids.indexOf(replyUserId?.value) == -1) {
  227. ids.unshift(replyUserId.value)
  228. }
  229. await sendMessage({
  230. content: messageContent.value,
  231. receiveUserIds: ids,
  232. })
  233. ElMessage.success('发送成功')
  234. messageContent.value = ''
  235. // emit('close')
  236. } catch (error) {
  237. console.error(error)
  238. }
  239. }
  240. getUserGroup()
  241. const toggleHistory = () => {
  242. showHistory.value = !showHistory.value
  243. }
  244. </script>
  245. <style scoped lang="scss">
  246. .message-list-modal {
  247. background-color: transparent;
  248. width: 750px;
  249. .tree-box {
  250. width: 260px;
  251. height: 446px;
  252. ::v-deep(.el-tree) {
  253. min-height: 100%;
  254. .is-online,
  255. .is-offline {
  256. .el-tree-node__label {
  257. padding-left: 14px;
  258. position: relative;
  259. &:before {
  260. content: '';
  261. display: block;
  262. position: absolute;
  263. left: 0;
  264. top: 5px;
  265. width: 8px;
  266. height: 8px;
  267. border-radius: 4px;
  268. background-color: #67c23a;
  269. z-index: 10;
  270. }
  271. }
  272. }
  273. .is-offline {
  274. .el-tree-node__label {
  275. &:before {
  276. background-color: #ccc !important;
  277. }
  278. }
  279. }
  280. }
  281. }
  282. // .message-list {
  283. // width: 260px;
  284. // height: 446px;
  285. // background: #fafafa;
  286. // box-shadow: 0px 6px 6px 0px rgba(0, 0, 0, 0.1);
  287. // }
  288. .message-info-container {
  289. // width: 600px;
  290. // flex: 1;
  291. width: calc(100% - 260px);
  292. &.big {
  293. width: 750px;
  294. }
  295. .message-info {
  296. height: 446px;
  297. background-color: $color--white;
  298. .message-info-header {
  299. border-bottom: $OnePixelLine;
  300. .send-user {
  301. font-size: $SmallFont;
  302. color: $RegularFontColor;
  303. .user-name {
  304. min-width: 320px;
  305. padding: 10px 12px;
  306. border: $OnePixelLine;
  307. .checked-users {
  308. // overflow: hidden;
  309. // white-space: nowrap;
  310. display: flex;
  311. width: 500px;
  312. &.big {
  313. width: 240px;
  314. }
  315. .split-names {
  316. flex: 1;
  317. overflow: hidden;
  318. white-space: nowrap;
  319. text-overflow: ellipsis;
  320. }
  321. }
  322. }
  323. }
  324. .close-icon {
  325. width: 20px;
  326. height: 20px;
  327. place-items: center;
  328. font-size: 18px;
  329. color: $RegularFontColor;
  330. &:hover {
  331. color: $NormalColor;
  332. }
  333. }
  334. }
  335. .message-info-content {
  336. border: 1px solid $color--primary;
  337. font-size: $SmallFont;
  338. ::v-deep(textarea.el-textarea__inner) {
  339. height: 100%;
  340. }
  341. .limit-tip {
  342. text-align: right;
  343. height: 18px;
  344. padding-top: 3px;
  345. }
  346. }
  347. }
  348. }
  349. }
  350. </style>