message-input-at.vue 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362
  1. <template>
  2. <div class="message-input-at" :class="[isH5 && 'message-input-at-h5']" v-if="showAtList" ref="MessageInputAt">
  3. <div class="memberList" ref="dialog">
  4. <header class="memberList-title" v-if="isH5">
  5. <span class="title">{{ $t("TUIChat.选择提醒的人") }}</span>
  6. <i class="icon icon-close close" @click="closeAt"></i>
  7. </header>
  8. <ul class="memberList-box">
  9. <li class="memberList-box-body" :class="[index === selectedIndex && 'selected']" v-for="(item, index) in showMemberList" :key="index" @click="selectItem(index)" ref="memberListItems">
  10. <img :src="(item as any)?.avatar || 'https://oss.dayaedu.com/news-info/07/1690787574969.png'" />
  11. <span>
  12. {{ (item as any)?.nick ? (item as any)?.nick : (item as any)?.userID }}
  13. </span>
  14. </li>
  15. </ul>
  16. </div>
  17. </div>
  18. </template>
  19. <script lang="ts">
  20. import { defineComponent, ref, toRefs, watchEffect, watch } from "vue";
  21. import { SuggestionProps, SuggestionKeyDownProps } from "@tiptap/suggestion";
  22. import atIcon from "../../../assets/icon/at.svg";
  23. import TIM from "../../../../TUICore/tim";
  24. import { onClickOutside } from "@vueuse/core";
  25. const MessageInputAt = ref();
  26. const showAtList = ref(false);
  27. const allMemberList = ref<Array<any>>();
  28. const showMemberList = ref<Array<any>>();
  29. const position = ref({
  30. left: 0,
  31. top: 0,
  32. });
  33. const command = ref();
  34. const selectedIndex = ref(0);
  35. const memberListItems = ref();
  36. const isH5 = ref(false);
  37. const MessageInputAtSuggestion = () => {
  38. return {
  39. allowedPrefixes: null,
  40. items: (props: { query: string }) => {
  41. const queryResult = allMemberList?.value?.filter((item) => item?.nick?.toLowerCase()?.startsWith(props?.query?.toLowerCase()) || item?.userID?.toLowerCase()?.startsWith(props?.query?.toLowerCase()));
  42. showMemberList.value = queryResult?.length ? queryResult : allMemberList.value;
  43. return showMemberList.value;
  44. },
  45. render: () => {
  46. return {
  47. onStart: (
  48. props: SuggestionProps<{
  49. id?: string;
  50. userID?: string;
  51. isAll?: boolean;
  52. }>
  53. ) => {
  54. showAtList.value = true;
  55. if (!props?.clientRect) {
  56. return;
  57. }
  58. const rect = props?.clientRect();
  59. if (rect?.left && rect?.top && !isH5.value) {
  60. position.value = {
  61. left: rect?.left,
  62. top: rect?.top,
  63. };
  64. }
  65. command.value = props.command;
  66. },
  67. onUpdate(props: SuggestionProps<any>) {
  68. if (!props?.clientRect) {
  69. return;
  70. }
  71. const rect = props?.clientRect();
  72. if (rect?.left && rect?.top && !isH5.value) {
  73. position.value = {
  74. left: rect?.left,
  75. top: rect?.top,
  76. };
  77. }
  78. },
  79. onKeyDown(props: SuggestionKeyDownProps) {
  80. if (props.event.key === "Enter") {
  81. props.event?.stopPropagation();
  82. props.event?.preventDefault();
  83. }
  84. if (props.event.key === "Escape") {
  85. showAtList.value = false;
  86. showMemberList.value = allMemberList.value;
  87. return true;
  88. }
  89. if (props?.event.key === "ArrowUp") {
  90. upHandler();
  91. return true;
  92. }
  93. if (props?.event.key === "ArrowDown") {
  94. downHandler();
  95. return true;
  96. }
  97. if (props?.event.key === "Enter") {
  98. enterHandler();
  99. return true;
  100. }
  101. return false;
  102. },
  103. onExit(props: SuggestionProps<any>) {
  104. showAtList.value = false;
  105. showMemberList.value = allMemberList.value;
  106. position.value = {
  107. left: 0,
  108. top: 0,
  109. };
  110. },
  111. };
  112. },
  113. };
  114. };
  115. const upHandler = () => {
  116. if (!showMemberList?.value?.length) return;
  117. selectedIndex.value = (selectedIndex.value + showMemberList?.value?.length - 1) % showMemberList?.value?.length;
  118. memberListItems?.value[selectedIndex.value]?.scrollIntoView(false);
  119. };
  120. const downHandler = () => {
  121. if (!showMemberList?.value?.length) return;
  122. selectedIndex.value = (selectedIndex.value + 1) % showMemberList?.value?.length;
  123. memberListItems?.value[selectedIndex.value]?.scrollIntoView(false);
  124. };
  125. const enterHandler = () => {
  126. selectItem(selectedIndex.value);
  127. };
  128. const selectItem = (index: number) => {
  129. if (!showMemberList?.value?.length) return;
  130. const item = showMemberList?.value[index];
  131. if (item) {
  132. command.value &&
  133. command.value({
  134. id: (item as any)?.userID,
  135. label: (item as any)?.nick || (item as any)?.userID,
  136. });
  137. }
  138. };
  139. const MessageInputAtComponent = defineComponent({
  140. props: {
  141. memberList: {
  142. type: Array,
  143. default: () => [],
  144. },
  145. isGroup: {
  146. type: Boolean,
  147. default: false,
  148. },
  149. selfInfo: {
  150. type: Object,
  151. default: () => ({}),
  152. },
  153. isH5: {
  154. type: Boolean,
  155. default: false,
  156. },
  157. },
  158. setup(props) {
  159. const { memberList, isGroup, selfInfo } = toRefs(props);
  160. const all = {
  161. userID: TIM.TYPES.MSG_AT_ALL,
  162. nick: "所有人",
  163. isAll: true,
  164. avatar: atIcon,
  165. };
  166. const dialog = ref();
  167. watchEffect(() => {
  168. showAtList.value = showAtList.value && isGroup.value;
  169. isH5.value = props.isH5;
  170. });
  171. watch(
  172. () => memberList.value,
  173. () => {
  174. // add all
  175. if (!(memberList?.value[0] as any)?.isAll) {
  176. memberList?.value?.unshift(all);
  177. }
  178. // delete self in @ list
  179. const list = memberList?.value?.filter((item: any) => {
  180. return item?.userID !== selfInfo?.value?.userID;
  181. });
  182. allMemberList.value = list;
  183. showMemberList.value = list;
  184. },
  185. {
  186. deep: true,
  187. immediate: true,
  188. }
  189. );
  190. watch(
  191. () => [position.value, MessageInputAt?.value],
  192. () => {
  193. if (isH5.value || !MessageInputAt?.value || !MessageInputAt?.value?.style) {
  194. return;
  195. }
  196. MessageInputAt.value.style.left = position.value.left + "Px";
  197. MessageInputAt.value.style.top = position.value.top - MessageInputAt.value.clientHeight + "Px";
  198. },
  199. {
  200. deep: true,
  201. immediate: true,
  202. }
  203. );
  204. const closeAt = () => {
  205. showAtList.value = false;
  206. showMemberList.value = allMemberList.value;
  207. position.value = {
  208. left: 0,
  209. top: 0,
  210. };
  211. };
  212. onClickOutside(dialog, () => {
  213. closeAt();
  214. });
  215. return {
  216. selectedIndex,
  217. selectItem,
  218. showAtList,
  219. closeAt,
  220. showMemberList,
  221. allMemberList,
  222. MessageInputAt,
  223. memberListItems,
  224. dialog,
  225. };
  226. },
  227. });
  228. export default MessageInputAtComponent;
  229. export { MessageInputAtSuggestion, MessageInputAtComponent };
  230. </script>
  231. <style scoped lang="scss">
  232. @import url("../../../styles/common.scss");
  233. @import url("../../../styles/icon.scss");
  234. .message-input-at {
  235. position: fixed;
  236. max-width: 15rem;
  237. max-height: 10rem;
  238. overflow-x: hidden;
  239. overflow-y: auto;
  240. background: #ffffff;
  241. box-shadow: 0 0.06rem 0.63rem 0 rgba(2, 16, 43, 0.15);
  242. border-radius: 0.13rem;
  243. }
  244. .memberList-box {
  245. &-header {
  246. height: 2.5rem;
  247. padding-top: 5px;
  248. cursor: pointer;
  249. &:hover {
  250. background: rgba(0, 110, 255, 0.1);
  251. }
  252. }
  253. span {
  254. font-family: PingFangSC-Regular;
  255. font-weight: 400;
  256. font-size: 0.88rem;
  257. color: #000000;
  258. letter-spacing: 0;
  259. padding: 5px;
  260. }
  261. &-body {
  262. height: 2.5rem;
  263. cursor: pointer;
  264. display: flex;
  265. align-items: center;
  266. .selected,
  267. &:hover {
  268. background: rgba(0, 110, 255, 0.1);
  269. }
  270. span {
  271. overflow: hidden;
  272. white-space: nowrap;
  273. word-wrap: break-word;
  274. word-break: break-all;
  275. text-overflow: ellipsis;
  276. }
  277. }
  278. img {
  279. width: 1.5rem;
  280. height: 1.5rem;
  281. padding-left: 10px;
  282. }
  283. .selected {
  284. background: rgba(0, 110, 255, 0.1);
  285. }
  286. }
  287. .message-input-at-h5 {
  288. position: fixed;
  289. bottom: 0;
  290. left: 0;
  291. width: 100%;
  292. height: 100%;
  293. max-width: 100%;
  294. max-height: 100%;
  295. overflow: hidden;
  296. background: rgba(0, 0, 0, 0.5);
  297. z-index: 10;
  298. display: flex;
  299. align-items: flex-end;
  300. .memberList {
  301. height: auto;
  302. max-height: 50%;
  303. width: 100%;
  304. max-width: 100%;
  305. background: white;
  306. border-radius: 12px 12px 0 0;
  307. display: flex;
  308. flex-direction: column;
  309. overflow: hidden;
  310. &-title {
  311. height: fit-content;
  312. width: calc(100% - 30px);
  313. text-align: center;
  314. vertical-align: middle;
  315. padding: 15px;
  316. .title {
  317. vertical-align: middle;
  318. display: inline-block;
  319. font-size: 16px;
  320. }
  321. .close {
  322. vertical-align: middle;
  323. position: absolute;
  324. right: 10px;
  325. display: inline-block;
  326. }
  327. }
  328. &-box {
  329. flex: 1;
  330. overflow-y: scroll;
  331. &-body {
  332. padding: 10px;
  333. img {
  334. width: 26px;
  335. height: 26px;
  336. }
  337. span {
  338. font-size: 14px;
  339. }
  340. }
  341. }
  342. }
  343. }
  344. </style>