chat-app/src/pages/dialog/index.vue

577 lines
15 KiB
Vue
Raw Normal View History

2024-11-22 01:06:37 +00:00
<template>
<div class="outer-layer">
<div>
2024-11-22 09:00:03 +00:00
<tm-navbar :hideBack="false" hideHome title="" :leftWidth="220">
<div class="flex flex-col items-center justify-center">
<div class="text-[34rpx] font-bold">{{ talkParams.username }}</div>
<div v-if="true" class="text-[24rpx] text-[#999999]">公司群</div>
2024-11-22 01:06:37 +00:00
</div>
<template v-slot:right>
<div class="mr-[36rpx]">
<tm-icon color="rgb(51, 51, 51)" :font-size="36" name="tmicon-gengduo"></tm-icon>
</div>
2024-11-22 09:00:03 +00:00
</template>
2024-11-22 01:06:37 +00:00
</tm-navbar>
</div>
<div class="root">
2024-11-22 09:00:03 +00:00
<div class="dialogBox">
2024-11-26 08:51:36 +00:00
<ZPaging :fixed="false" use-chat-record-mode :use-page-scroll="false" :refresher-enabled="false"
:show-scrollbar="false" :loading-more-enabled="true" :hide-empty-view="true" height="100%" ref="pagingRef"
@scrolltolower="onRefreshLoad">
<!-- 数据加载状态栏 -->
<div class="message-item" v-for="(item, index) in reversedRecords" style="transform: scaleY(-1);"
:key="item.msg_id" :id="item.msg_id">
<!-- 系统消息 -->
<div v-if="item.msg_type >= 1000" class="message-box">
<component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item" />
</div>
<!-- 撤回消息 -->
<div v-else-if="item.is_revoke == 1" class="message-box">
<revoke-message :login_uid="uid" :user_id="item.user_id" :nickname="item.nickname"
:talk_type="item.talk_type" :datetime="item.created_at" />
</div>
<div v-else class="message-box record-box" :class="{
'direction-rt': item.float == 'right',
'multi-select': dialogueStore.isOpenMultiSelect,
'multi-select-check': item.isCheck
}">
<!-- 多选按钮 -->
<aside v-if="dialogueStore.isOpenMultiSelect" class="checkbox-column">
<n-checkbox size="small" :checked="item.isCheck" @update:checked="item.isCheck = !item.isCheck" />
</aside>
<!-- 头像信息 -->
<aside class="avatar-column">
<im-avatar class="pointer" :src="item.avatar" :size="80" :username="item.nickname"
@click="showUserInfoModal(item.user_id)" />
</aside>
<!-- 主体信息 -->
<main class="main-column">
<div class="talk-title">
<span class="nickname pointer" v-show="talk_type == 2 && item.float == 'left'"
@click="onClickNickname(item)">
<span class="at">@</span>{{ item.nickname }}
</span>
<span>{{ parseTime(item.created_at, '{m}/{d} {h}:{i}') }}</span>
</div>
<div class="talk-content" :class="{ pointer: dialogueStore.isOpenMultiSelect }">
<component :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra"
:data="item" :max-width="true" :source="'panel'"
@contextmenu.prevent="onContextMenu($event, item)" />
2024-11-22 09:00:03 +00:00
2024-11-26 08:51:36 +00:00
<div class="talk-tools">
<template v-if="talk_type == 1 && item.float == 'right'">
<loading theme="outline" size="19" fill="#000" :strokeWidth="1" class="icon-rotate"
v-show="item.send_status == 1" />
2024-11-22 09:00:03 +00:00
2024-11-26 08:51:36 +00:00
<span v-show="item.send_status == 1"> 正在发送... </span>
<!-- <span v-show="item.send_status != 1"> 已送达 </span> -->
</template>
</div>
2024-11-22 09:00:03 +00:00
</div>
2024-11-22 01:06:37 +00:00
2024-11-26 08:51:36 +00:00
<div v-if="item.extra.reply" class="talk-reply pointer"
@click="onJumpMessage(item.extra?.reply?.msg_id)">
<n-icon :component="ToTop" size="14" class="icon-top" />
<span class="ellipsis">
回复 {{ item.extra?.reply?.nickname }}:
{{ item.extra?.reply?.content }}
</span>
</div>
</main>
</div>
</div>
<div class="load-toolbar pointer" style="transform: scaleY(-1);">
<span v-if="loadConfig.status == 0"> 正在加载数据中 ... </span>
<span v-else-if="loadConfig.status == 1" @click="onRefreshLoad"> 查看更多消息 ... </span>
<span v-else class="no-more"> 没有更多消息了 </span>
</div>
</ZPaging>
2024-11-22 01:06:37 +00:00
</div>
</div>
2024-11-22 09:00:03 +00:00
<div class="footBox">
<div class="mt-[16rpx] ml-[32rpx] mr-[32rpx] flex items-center justify-between">
2024-11-26 08:51:36 +00:00
<div class="w-[534rpx] quillBox" >
<QuillEditor ref="editor" id="editor" :options="editorOption" @editorChange="onEditorChange"
style="height: 100%; border: none" />
<!-- <tm-input type=textarea autoHeight focusColor="#F9F9F9" color="#F9F9F9" :inputPadding="[12]"
placeholder=""></tm-input> -->
2024-11-22 01:06:37 +00:00
</div>
2024-11-26 08:51:36 +00:00
<tm-image @click="state.isOpenEmojiPanel = !state.isOpenEmojiPanel" :width="52" :height="52" :round="12"
:src="state.isOpenEmojiPanel ? keyboard : smile"></tm-image>
2024-11-22 01:06:37 +00:00
<tm-image :width="52" :height="52" :round="12" :src="addCircleGray"></tm-image>
</div>
2024-11-26 08:51:36 +00:00
<div v-if="state.isOpenEmojiPanel" class="mt-[50rpx]">
<emoji-panel />
</div>
<!--底部安全区-->
<div class="content-placeholder"></div>
2024-11-22 01:06:37 +00:00
</div>
</div>
</template>
<script setup>
2024-11-22 09:00:03 +00:00
import { ref, reactive, watch, computed, onMounted } from 'vue';
2024-11-26 08:51:36 +00:00
import { QuillEditor, Quill } from '@vueup/vue-quill'
2024-11-22 01:06:37 +00:00
import { useChatList } from "@/store/chatList/index.js";
2024-11-22 09:00:03 +00:00
import { useAuth } from "@/store/auth";
2024-11-26 08:51:36 +00:00
import { useUserStore, useDialogueStore, useUploadsStore, useEditorDraftStore } from '@/store'
2024-11-22 01:06:37 +00:00
import addCircleGray from "@/static/image/chatList/addCircleGray.png";
import { MessageComponents, ForwardableMessageType } from '@/constant/message'
2024-11-22 09:00:03 +00:00
import { formatTime, parseTime } from '@/utils/datetime'
2024-11-26 08:51:36 +00:00
import { deltaToMessage, deltaToString, isEmptyDelta } from './util'
2024-11-22 01:06:37 +00:00
import smile from "@/static/image/chatList/smile@2x.png";
2024-11-26 08:51:36 +00:00
import keyboard from "@/static/image/chatList/keyboard@2x.png";
2024-11-22 01:06:37 +00:00
import { useInject, useTalkRecord } from '@/hooks'
2024-11-26 08:51:36 +00:00
import ZPaging from "@/uni_modules/z-paging/components/z-paging/z-paging.vue";
import useZPaging from "@/uni_modules/z-paging/components/z-paging/js/hooks/useZPaging.js";
import emojiPanel from './components/emojiPanel.vue'
2024-11-22 01:06:37 +00:00
const userStore = useUserStore()
const dialogueStore = useDialogueStore()
2024-11-26 08:51:36 +00:00
const editorDraftStore = useEditorDraftStore()
const editor = ref()
const pagingRef = ref(null)
useZPaging(pagingRef)
const indexName = computed(() => dialogueStore.index_name)
2024-11-22 01:06:37 +00:00
const talkParams = reactive({
uid: computed(() => userStore.uid),
index_name: computed(() => dialogueStore.index_name),
type: computed(() => dialogueStore.talk.talk_type),
receiver_id: computed(() => dialogueStore.talk.receiver_id),
username: computed(() => dialogueStore.talk.username),
online: computed(() => dialogueStore.online),
keyboard: computed(() => dialogueStore.keyboard),
num: computed(() => dialogueStore.members.length)
})
2024-11-26 08:51:36 +00:00
const state = ref({
isOpenEmojiPanel: false
})
2024-11-22 01:06:37 +00:00
const { loadConfig, records, onLoad, onRefreshLoad, onJumpMessage } = useTalkRecord(talkParams.uid)
2024-11-26 08:51:36 +00:00
// 添加计算属性来反转records数组
const reversedRecords = computed(() => {
return [...records.value].reverse()
})
const getQuill = () => {
return editor.value?.getQuill()
}
const onEditorChange = () => {
debugger
let delta = getQuill().getContents()
let text = deltaToString(delta)
if (!isEmptyDelta(delta)) {
editorDraftStore.items[indexName.value || ''] = JSON.stringify({
text: text,
ops: delta.ops
})
} else {
// 删除 editorDraftStore.items 下的元素
delete editorDraftStore.items[indexName.value || '']
}
// emit('editor-event', emitCall('input_event', text))
}
const onSendMessage = () => {
let delta = getQuill().getContents()
let data = deltaToMessage(delta)
if (data.items.length === 0) {
return
}
switch (data.msgType) {
case 1: // 文字消息
if (data.items[0].content.length > 1024) {
return window['$message'].info('发送内容超长,请分条发送')
}
emit(
'editor-event',
emitCall('text_event', data, (ok) => {
ok && getQuill().setContents([], Quill.sources.USER)
})
)
break
case 3: // 图片消息
emit(
'editor-event',
emitCall(
'image_event',
{ ...getImageInfo(data.items[0].content), url: data.items[0].content, size: 10000 },
(ok) => {
ok && getQuill().setContents([])
}
)
)
break
case 12: // 图文消息
emit(
'editor-event',
emitCall('mixed_event', data, (ok) => {
ok && getQuill().setContents([])
})
)
break
}
}
const editorOption = {
debug: false,
modules: {
toolbar: false,
// clipboard: {
// // 粘贴版,处理粘贴时候的自带样式
// matchers: [[Node.ELEMENT_NODE, onClipboardMatcher]]
// },
keyboard: {
bindings: {
enter: {
key: 13,
handler: onSendMessage
}
}
},
// imageUploader: {
// upload: onEditorUpload
// },
// mention: {
// allowedChars: /^[\u4e00-\u9fa5]*$/,
// mentionDenotationChars: ['@'],
// positioningStrategy: 'fixed',
// renderItem: (data) => {
// const el = document.createElement('div')
// el.className = 'ed-member-item'
// el.innerHTML = `<img src="${data.avatar}" class="avator"/>`
// el.innerHTML += `<span class="nickname">${data.nickname}</span>`
// return el
// },
// source: function (searchTerm, renderList) {
// if (!props.members.length) {
// return renderList([])
// }
// let list = [
// { id: 0, nickname: '所有人', avatar: defAvatar, value: '所有人' },
// ...props.members
// ]
// const items = list.filter(
// (item) => item.nickname.toLowerCase().indexOf(searchTerm) !== -1
// )
// renderList(items)
// },
// mentionContainerClass: 'ql-mention-list-container me-scrollbar me-scrollbar-thumb'
// }
},
placeholder: '',
}
2024-11-22 01:06:37 +00:00
watch(() => records, (newValue, oldValue) => {
console.log(newValue);
2024-11-22 09:00:03 +00:00
}, { deep: true, immediate: true })
2024-11-22 01:06:37 +00:00
watch(() => talkParams.uid, (newValue, oldValue) => {
let objT = {
uid: talkParams.uid,
talk_type: talkParams.type,
receiver_id: talkParams.receiver_id,
index_name: talkParams.index_name
}
onLoad({ ...objT, limit: 30 })
2024-11-22 09:00:03 +00:00
}, { immediate: true })
2024-11-22 01:06:37 +00:00
onMounted(() => {
let objT = {
uid: talkParams.uid,
talk_type: talkParams.type,
receiver_id: talkParams.receiver_id,
index_name: talkParams.index_name
}
onLoad({ ...objT, limit: 30 })
})
</script>
2024-11-22 09:00:03 +00:00
<style scoped lang="less">
2024-11-26 08:51:36 +00:00
uni-page-body,
page {
height: 100%;
}
2024-11-22 01:06:37 +00:00
.outer-layer {
flex: 1;
background-image: url("@/static/image/clockIn/z3280@3x.png");
background-size: cover;
display: flex;
flex-direction: column;
2024-11-26 08:51:36 +00:00
overflow: hidden;
2024-11-22 01:06:37 +00:00
}
2024-11-22 09:00:03 +00:00
2024-11-22 01:06:37 +00:00
.root {
flex: 1;
padding: 20rpx 32rpx;
2024-11-26 08:51:36 +00:00
min-height: 0;
2024-11-22 01:06:37 +00:00
}
2024-11-22 09:00:03 +00:00
2024-11-22 01:06:37 +00:00
.searchRoot {
background-color: #fff;
padding: 22rpx 18rpx;
}
2024-11-22 09:00:03 +00:00
2024-11-22 01:06:37 +00:00
.contentRoot {
margin-top: 20rpx;
background-color: #fff;
}
2024-11-22 09:00:03 +00:00
2024-11-22 01:06:37 +00:00
.footBox {
2024-11-26 08:51:36 +00:00
min-height: 162rpx;
2024-11-22 01:06:37 +00:00
background-color: #fff;
}
2024-11-22 09:00:03 +00:00
.dialogBox {
2024-11-22 01:06:37 +00:00
height: 100%;
2024-11-26 08:51:36 +00:00
min-height: 0;
overflow: auto;
// 添加以下样式来隐藏滚动条
&::-webkit-scrollbar {
display: none;
}
-ms-overflow-style: none;
/* IE and Edge */
scrollbar-width: none;
/* Firefox */
2024-11-22 01:06:37 +00:00
}
2024-11-22 09:00:03 +00:00
.load-toolbar {
2024-11-26 08:51:36 +00:00
height: 50rpx;
2024-11-22 09:00:03 +00:00
color: #409eff;
text-align: center;
2024-11-26 08:51:36 +00:00
line-height: 50rpx;
font-size: 24rpx;
2024-11-22 09:00:03 +00:00
.no-more {
color: #b9b3b3;
}
}
.message-item {
&.border {
2024-11-26 08:51:36 +00:00
border-radius: 16rpx;
2024-11-22 09:00:03 +00:00
}
}
.message-box {
width: 100%;
2024-11-26 08:51:36 +00:00
min-height: 30rpx;
margin-bottom: 5rpx;
2024-11-22 09:00:03 +00:00
}
.record-box {
display: flex;
flex-direction: row;
align-items: flex-start;
2024-11-26 08:51:36 +00:00
2024-11-22 09:00:03 +00:00
.checkbox-column {
display: flex;
justify-content: center;
2024-11-26 08:51:36 +00:00
width: 35rpx;
2024-11-22 09:00:03 +00:00
order: 1;
user-select: none;
2024-11-26 08:51:36 +00:00
padding-top: 12rpx;
2024-11-22 09:00:03 +00:00
}
.avatar-column {
width: 80rpx;
display: flex;
align-items: center;
order: 2;
user-select: none;
margin-top: 16rpx;
flex-direction: column;
}
.main-column {
flex: 1 auto;
order: 3;
position: relative;
box-sizing: border-box;
padding: 16rpx 20rpx 14rpx 20rpx;
overflow: hidden;
min-height: 30px;
.talk-title {
display: flex;
align-items: center;
margin-bottom: 6rpx;
font-size: 24rpx;
user-select: none;
color: #BABABA;
opacity: 1;
&.show {
opacity: 1;
}
.nickname {
color: var(--im-text-color);
2024-11-26 08:51:36 +00:00
margin-right: 5rpx;
2024-11-22 09:00:03 +00:00
.at {
display: none;
}
&:hover {
color: var(--im-primary-color);
.at {
display: inline-block;
}
}
}
span {
transform: scale(0.88);
transform-origin: left center;
}
}
.talk-content {
display: flex;
justify-content: flex-start;
align-items: flex-end;
box-sizing: border-box;
width: 100%;
.talk-tools {
display: flex;
2024-11-26 08:51:36 +00:00
margin: 0 16rpx;
2024-11-22 09:00:03 +00:00
color: #a79e9e;
2024-11-26 08:51:36 +00:00
font-size: 24rpx;
2024-11-22 09:00:03 +00:00
user-select: none;
align-items: center;
justify-content: space-around;
.more-tools {
visibility: hidden;
2024-11-26 08:51:36 +00:00
margin-left: 5rpx;
2024-11-22 09:00:03 +00:00
}
}
}
.talk-reply {
display: flex;
align-items: flex-start;
align-items: center;
width: fit-content;
2024-11-26 08:51:36 +00:00
padding: 8rpx;
margin-top: 6rpx;
2024-11-22 09:00:03 +00:00
margin-right: auto;
2024-11-26 08:51:36 +00:00
font-size: 24rpx;
2024-11-22 09:00:03 +00:00
color: #8f8f8f;
word-break: break-all;
background-color: var(--im-message-left-bg-color);
2024-11-26 08:51:36 +00:00
border-radius: 10rpx;
max-width: 450rpx;
2024-11-22 09:00:03 +00:00
overflow: hidden;
user-select: none;
.icon-top {
2024-11-26 08:51:36 +00:00
margin-right: 6rpx;
2024-11-22 09:00:03 +00:00
}
.ellipsis {
display: -webkit-inline-box;
text-overflow: ellipsis;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
}
&:hover {
.talk-title {
opacity: 1;
}
.more-tools {
visibility: visible !important;
}
}
}
&.direction-rt {
.avatar-column {
order: 3;
}
.main-column {
order: 2;
.talk-title {
justify-content: flex-end;
span {
transform-origin: right center;
}
}
.talk-content {
flex-direction: row-reverse;
}
.talk-reply {
margin-right: 0;
margin-left: auto;
}
}
}
&.multi-select {
border-radius: 5px;
&:hover,
&.multi-select-check {
background-color: var(--im-active-bg-color);
}
}
}
2024-11-26 08:51:36 +00:00
.content-placeholder {
height: 58rpx;
}
.quillBox {
:deep(.ql-clipboard) {
width: 0;
height: 0;
}
:deep(.ql-editor) {
padding: 14rpx 22rpx;
background-color: #F9F9F9;
border-radius: 8rpx;
outline: none !important;
}
}
2024-11-22 01:06:37 +00:00
</style>