fix
Some checks failed
Check / lint (push) Has been cancelled
Check / typecheck (push) Has been cancelled
Check / build (build, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build, 18.x, windows-latest) (push) Has been cancelled
Check / build (build:app, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build:app, 18.x, windows-latest) (push) Has been cancelled
Check / build (build:mp-weixin, 18.x, ubuntu-latest) (push) Has been cancelled
Check / build (build:mp-weixin, 18.x, windows-latest) (push) Has been cancelled

This commit is contained in:
caiyx 2024-12-20 16:59:58 +08:00
parent 7101d1cd25
commit 1213ad9b98
16 changed files with 1053 additions and 119 deletions

View File

@ -0,0 +1,335 @@
<template>
<div id="popover" ref="bubbleRef" class="ui-popover">
<view
:id="popoverBoxId"
class="popover-box"
@touchend="onTouchend"
@touchstart="onTouchstart"
>
<slot></slot>
</view>
<!-- mode -->
<view
@click.stop="close(50)"
:id="popoverContentId"
class="popover-content"
:style="data.showStyle"
>
<div class="menu">
<div
class="w-full h-[132rpx] text-[24rpx] text-[#FFFFFF] flex items-center justify-around"
>
<div
v-if="props.isShowCopy"
@click="() => itemClick('actionCopy')"
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="copy07"></tm-image>
<div>复制</div>
</div>
<div
@click="() => itemClick('multipleChoose')"
class="flex flex-col items-center justify-center"
>
<tm-image
:width="40"
:height="40"
:src="multipleChoices"
></tm-image>
<div>多选</div>
</div>
<div
v-if="props.isShowCite"
@click="() => itemClick('actionCite')"
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="cite"></tm-image>
<div>引用</div>
</div>
<div
v-if="props.isShowWithdraw"
@click="() => itemClick('actionWithdraw')"
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="withdraw"></tm-image>
<div>撤回</div>
</div>
<div
@click="() => itemClick('actionDelete')"
class="flex flex-col items-center justify-center"
>
<tm-image :width="40" :height="40" :src="delete07"></tm-image>
<div>删除</div>
</div>
</div>
<div :style="data.iconStyle" class="icon"></div>
</div>
</view>
<view
v-show="data.popoverShow"
@touchstart="close"
@click="close"
class="popover-bg"
></view>
</div>
</template>
<script setup>
//
// uniapp & vue
import { onLoad, onReady } from "@dcloudio/uni-app";
import { defineEmits, defineProps } from "vue";
import {
reactive,
ref,
watch,
computed,
nextTick,
getCurrentInstance,
onMounted,
onBeforeUnmount
} from "vue";
import copy07 from "@/static/image/chatList/copy07@2x.png";
import multipleChoices from "@/static/image/chatList/multipleChoices@2x.png";
import cite from "@/static/image/chatList/cite@2x.png";
import withdraw from "@/static/image/chatList/withdraw@2x.png";
import delete07 from "@/static/image/chatList/delete@2x.png";
// pinia
const systemInfo = uni.getSystemInfoSync();
const bubbleRef = ref(null);
const props = defineProps({
isShowCopy: {
type: Boolean,
default: true,
},
isShowCite: {
type: Boolean,
default: true,
},
isShowWithdraw: {
type: Boolean,
default: true,
},
});
const emits = defineEmits(["clickMenu"]);
/**
* @name 生成UUID
*/
const uuid = () => {
const reg = /[xy]/g;
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
.replace(reg, function (c) {
var r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
})
.replace(/-/g, "");
};
const popoverBoxId = `ID${uuid()}`;
const popoverContentId = `ID${uuid()}`;
const instance = getCurrentInstance();
const data = reactive({
popoverShow: false,
defaultStyle: {},
showStyle: {
left: 0,
right: "",
transform: "",
},
iconStyle: {
left: "",
right: "",
transform: "",
},
});
/**
* @name 获取DOM
*/
const getDom = (dom) => {
return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery().in(instance);
let select = query.select(dom);
const boundingClientRect = select.boundingClientRect((data) => {
resolve(data);
});
boundingClientRect.exec();
});
};
const itemClick = (item) => {
emits("clickMenu", item);
};
// 5
let pressDownTime = 0;
let time = null;
const onTouchstart = () => {
time && clearTimeout(time);
time = setTimeout(open, 500);
};
const onTouchend = () => {
time && clearTimeout(time);
};
const open = async () => {
let popoverContent = await getDom(`#${popoverContentId}`);
let popoverBox = await getDom(`#${popoverBoxId}`);
//
let originX = popoverBox.width / 2;
//
let isTop = popoverBox.top - 50 > popoverContent.height;
//
data.defaultStyle = {
top: isTop ? "60rpx" : "auto",
bottom: !isTop ? "-20rpx" : "auto",
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(.8)`,
};
//
if (popoverBox.left > systemInfo.windowWidth - popoverBox.right) {
data.defaultStyle.right = 0;
//
data.defaultStyle["transform-origin"] = `${
popoverContent.width - originX
}px ${isTop ? "100%" : "0%"}`;
} else {
data.defaultStyle.left = 0;
//
data.defaultStyle["transform-origin"] = `${originX}px ${
isTop ? "100%" : "0%"
}`;
}
data.showStyle = { ...data.defaultStyle };
// icon
let iconDefsultStyle = {
transform: `translate(0%, ${isTop ? "20%" : "-20%"})`,
"border-top-color": isTop ? "#333333" : "",
"border-bottom-color": !isTop ? "#333333" : "",
top: !isTop ? "-20rpx" : "auto",
bottom: isTop ? "-20rpx" : "auto",
};
setTimeout(() => {
if (popoverBox.left > systemInfo.windowWidth - popoverBox.right) {
data.showStyle = {
//
...data.defaultStyle,
//
opacity: 1,
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(1)`,
"pointer-events": "auto",
};
data.iconStyle = {
right: `${originX}px`,
left: "auto",
...iconDefsultStyle,
};
} else {
data.showStyle = {
//
...data.defaultStyle,
//
opacity: 1,
transform: `translateY(${isTop ? "-100%" : "100%"}) scale(1)`,
"pointer-events": "auto",
};
data.iconStyle = {
left: `${originX}px`,
right: "auto",
...iconDefsultStyle,
};
}
if (!data.popoverShow) data.popoverShow = true;
}, 200);
};
const close = (time) => {
setTimeout(() => {
data.popoverShow = false;
data.showStyle = data.defaultStyle;
}, time || 0);
};
const handleClickOutside = (event) => {
if (data.popoverShow = false) {
return false
}
if (bubbleRef.value && !bubbleRef.value.contains(event.target)) {
close();
}
};
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside);
});
</script>
<style lang="scss" scoped>
.ui-popover {
position: relative;
.popover-box {
width: fit-content;
}
.popover-content {
width: fit-content;
transition: all 0.2s;
z-index: 5;
position: absolute;
top: 0;
opacity: 0;
pointer-events: none;
}
.popover-bg {
width: 100%;
height: 100%;
position: fixed;
left: 0;
top: 0;
z-index: 4;
}
.menu {
width: 526rpx;
height: 132rpx;
display: flex;
background-color: #333333;
display: flex;
color: #fff;
border-radius: 15rpx;
font-size: 26rpx;
padding: 0 10rpx;
box-sizing: border-box;
position: relative;
.icon {
border: 15rpx solid transparent;
position: absolute;
}
.item {
flex: 1;
height: 120rpx;
text-align: center;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
image {
width: 26rpx;
height: 26rpx;
margin-bottom: 8rpx;
}
}
}
</style>

View File

@ -63,7 +63,10 @@ const getFileTypeIMG = computed(() => {
</script> </script>
<template> <template>
<section class="file-message" :class="{ left: data.float === 'left' }"> <section
class="file-message"
:class="{ left: data.float === 'left', right: data.float === 'right' }"
>
<div class="flex justify-between"> <div class="flex justify-between">
<div class="w-[228rpx] text-[32rpx] text-[#1A1A1A] h-[88rpx] leading-[44rpx] textEllipsis"> <div class="w-[228rpx] text-[32rpx] text-[#1A1A1A] h-[88rpx] leading-[44rpx] textEllipsis">
{{ extra.name }} {{ extra.name }}
@ -109,13 +112,18 @@ const getFileTypeIMG = computed(() => {
padding: 28rpx 30rpx 22rpx 30rpx; padding: 28rpx 30rpx 22rpx 30rpx;
border-radius: 10px; border-radius: 10px;
background-color: #fff; background-color: #fff;
border-radius: 16rpx 0 16rpx 16rpx; border-radius: 0;
&.left { &.left {
background-color: #fff; background-color: #fff;
border-radius: 0 16rpx 16rpx 16rpx; border-radius: 0 16rpx 16rpx 16rpx;
} }
&.right {
background-color: #fff;
border-radius: 16rpx 0 16rpx 16rpx;
}
.main { .main {
height: 45px; height: 45px;
display: flex; display: flex;

View File

@ -16,11 +16,14 @@ const title = computed(() => {
}) })
const onClick = () => { const onClick = () => {
isShowRecord.value = true // isShowRecord.value = true
uni.navigateTo({
url: '/pages/forwardRecord/index?msgId=' + props.data.msg_id
})
} }
</script> </script>
<template> <template>
<section class="im-message-forward pointer" @click="onClick"> <section class="im-message-forward pointer" :class="{ left: data.float === 'left' }" @click="onClick">
<div class="title">{{ title }} 的会话记录</div> <div class="title">{{ title }} 的会话记录</div>
<div class="list" v-for="(record, index) in extra.records" :key="index"> <div class="list" v-for="(record, index) in extra.records" :key="index">
<p> <p>
@ -28,57 +31,70 @@ const onClick = () => {
<span>{{ record.text }}</span> <span>{{ record.text }}</span>
</p> </p>
</div> </div>
<div class="divider"></div>
<div class="tips"> <div class="tips">
<span>转发聊天会话记录 ({{ extra.msg_ids.length }})</span> <span>转发聊天会话记录 ({{ extra.msg_ids.length }})</span>
</div> </div>
<ForwardRecord v-if="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" /> <!-- <ForwardRecord v-if="isShowRecord" :msg-id="data.msg_id" @close="isShowRecord = false" /> -->
</section> </section>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.im-message-forward { .im-message-forward {
width: 250px; width: 486rpx;
min-height: 95px; min-height: 180rpx;
max-height: 150px; max-height: 274rpx;
border-radius: 10px; border-radius: 0;
padding: 8px 10px; background-color: #FFFFFF;
border: 1px solid var(--im-message-border-color); padding: 22rpx 28rpx 22rpx 30rpx;
user-select: none; &.left {
background-color: #fff;
border-radius: 0 16rpx 16rpx 16rpx;
}
&.right {
background-color: #fff;
border-radius: 16rpx 0 16rpx 16rpx;
}
.title { .title {
height: 30px; height: 44rpx;
line-height: 30px; line-height: 44rpx;
font-size: 15px; font-size: 32rpx;
color: #1A1A1A;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
font-weight: 400; font-weight: 400;
margin-bottom: 5px; margin-bottom: 8rpx;
} }
.list p { .list p {
height: 18px; height: 34rpx;
line-height: 18px; line-height: 34rpx;
font-size: 12px; font-size: 24rpx;
color: #a8a8a8; color: #B4B4B4;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
margin-bottom: 5px; margin-bottom: 10rpx;
} }
.tips { .tips {
height: 32px; height: 34rpx;
line-height: 35px; line-height: 34rpx;
color: #8a8888; color: #747474;
border-top: 1px solid var(--border-color); font-size: 24rpx;
font-size: 12px; margin-top: 10rpx;
margin-top: 12px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
} }
.divider {
height: 1rpx;
background-color: #E7E7E7;
}
</style> </style>

View File

@ -52,14 +52,12 @@ const img = computed(() => {
<template> <template>
<section <section
class="im-message-image" class="im-message-image"
:class="{ left: data.float === 'left' }" :class="{
left: data.float === 'left',
right: data.float === 'right'
}"
> >
<div class="image-container"> <div class="image-container">
<!-- <image
:src="extra.url"
mode="scaleToFill"
:width="img.width" :height="img.height"
/> -->
<tm-image preview :width="img.width" :height="img.height" :src="extra.url" /> <tm-image preview :width="img.width" :height="img.height" :src="extra.url" />
<wd-circle custom-class="circleProgress" v-if="props.data.uploadCurrent && props.data.uploadCurrent<100" v-model="props.data.uploadCurrent" color="#ffffff" layer-color="#E3E3E3"></wd-circle> <wd-circle custom-class="circleProgress" v-if="props.data.uploadCurrent && props.data.uploadCurrent<100" v-model="props.data.uploadCurrent" color="#ffffff" layer-color="#E3E3E3"></wd-circle>
</div> </div>
@ -69,15 +67,21 @@ const img = computed(() => {
.im-message-image { .im-message-image {
overflow: hidden; overflow: hidden;
padding: 20rpx 18rpx; padding: 20rpx 18rpx;
border-radius: 16rpx 0 16rpx 16rpx; border-radius: 0;
background-color: #46299D; background-color: #fff;
min-width: 40rpx; min-width: 40rpx;
min-height: 40rpx; min-height: 40rpx;
max-width: 532rpx;
&.left { &.left {
background-color: #fff; background-color: #fff;
border-radius: 0 16rpx 16rpx 16rpx; border-radius: 0 16rpx 16rpx 16rpx;
} }
&.right {
background-color: #46299D;
border-radius: 16rpx 0 16rpx 16rpx;
}
} }
.image-container { .image-container {
position: relative; position: relative;

View File

@ -78,7 +78,13 @@ const img = (src: string, width = 200) => {
padding: 20rpx 18rpx; padding: 20rpx 18rpx;
color: #000000; color: #000000;
background-color: #FFFFFF; background-color: #FFFFFF;
border-radius: 0;
&.left{
background-color: #FFFFFF;
color: #000000;
border-radius: 0 16rpx 16rpx 16rpx; border-radius: 0 16rpx 16rpx 16rpx;
}
&.right { &.right {
background-color: #46299D; background-color: #46299D;

View File

@ -2,12 +2,16 @@
import {ref,nextTick} from 'vue' import {ref,nextTick} from 'vue'
import WdPopup from "@/uni_modules/wot-design-uni/components/wd-popup/wd-popup.vue"; import WdPopup from "@/uni_modules/wot-design-uni/components/wd-popup/wd-popup.vue";
import TmButton from "@/uni_modules/tmui/components/tm-button/tm-button.vue"; import TmButton from "@/uni_modules/tmui/components/tm-button/tm-button.vue";
import {language} from "@/uni_modules/tmui/tool/lib/language"
const confirmState=ref(false) const confirmState=ref(false)
const cancel=ref(true) const cancel=ref(true)
let onConfirm=null let onConfirm=null
let onCancel=null let onCancel=null
const confirm=ref(true) const confirm=ref(true)
const contentText=ref('') const contentText=ref('')
const imageRef=ref('')
const confirmLabel=ref(language('components.confirm.confirmText'))
const cancelLabel=ref(language('components.confirm.cancelText'))
const sendCancel=()=>{ const sendCancel=()=>{
confirmState.value=false confirmState.value=false
if (typeof onCancel==='function'){ if (typeof onCancel==='function'){
@ -20,31 +24,70 @@ const sendConfirm=()=>{
onConfirm() onConfirm()
} }
} }
const showConfirm=({content,onConfirm:confirm,onCancel:cancel})=>{ const showConfirm=({content,image,onConfirm:confirm,onCancel:cancel,confirmText,cancelText})=>{
confirmState.value=true confirmState.value=true
contentText.value=content contentText.value=content
imageRef.value = image?image:''
onConfirm=confirm onConfirm=confirm
onCancel=cancel onCancel=cancel
confirmLabel.value = confirmText || confirmLabel.value
cancelLabel.value = cancelText || cancelLabel.value
} }
defineExpose({ defineExpose({
showConfirm showConfirm
}) })
</script> </script>
<template> <template>
<wd-popup custom-style="border-radius: 16rpx;" modal-style="background-color: rgba(0,0,0,0.3);" v-model="confirmState"> <wd-popup custom-style="border-radius: 16rpx;" modal-style="background-color: #000000;opacity: 0.6;" v-model="confirmState">
<div class="flex flex-col w-[640rpx] h-[402rpx]"> <div class="flex flex-col w-[640rpx]">
<div class="flex justify-center items-center h-[288rpx] text-[32rpx] font-bold text-[#1A1A1A]"> <div v-if="imageRef===''" class="flex justify-center items-center h-[288rpx] text-[32rpx] font-bold text-[#1A1A1A]">
{{contentText}} {{contentText}}
</div> </div>
<div v-else class="flex flex-col items-center h-[456rpx] text-[32rpx] font-bold text-[#1A1A1A]" >
<div class="wrap1 mt-[32rpx] mb-[44rpx]" >
<img :src="imageRef" alt="">
</div>
<div class="mb-[56rpx]" > {{contentText}} </div>
</div>
<div class="flex flex-grow border-t-solid border-[#E7E7E7] border-1rpx text-[32rpx]"> <div class="flex flex-grow border-t-solid border-[#E7E7E7] border-1rpx text-[32rpx]">
<div class="flex justify-center items-center text-[#1A1A1A]"> <div class="flex justify-center items-center text-[#1A1A1A]">
<tm-button @click="sendCancel" :width="319" @touchstart="cancel=false" @touchend="cancel=true" :fontSize="32" :height="112" :margin="[0]" :font-color="'#1A1A1A'" :transprent="cancel" text label="取消"></tm-button> <tm-button
@click="sendCancel"
:width="319"
@touchstart="cancel=false"
@touchend="cancel=true"
:fontSize="32"
:height="112"
:margin="[0]"
:font-color="'#1A1A1A'"
:transprent="cancel"
text
:label="cancelLabel"></tm-button>
</div> </div>
<div class="h-[112rpx] w-[1rpx] bg-[#E7E7E7]"></div> <div class="h-[112rpx] w-[1rpx] bg-[#E7E7E7]"></div>
<div class="flex justify-center items-center text-[#CF3050]"> <div class="flex justify-center items-center text-[#CF3050]">
<tm-button @click="sendConfirm" @touchstart="confirm=false" @touchend="confirm=true" :width="319" :fontSize="32" :transprent="confirm" :height="112" :margin="[0]" :font-color="'#46299D'" text label="确定"></tm-button> <tm-button
@click="sendConfirm"
@touchstart="confirm=false"
@touchend="confirm=true"
:width="319"
:fontSize="32"
:transprent="confirm"
:height="112"
:margin="[0]"
:font-color="'#46299D'"
text
:label="confirmLabel"></tm-button>
</div> </div>
</div> </div>
</div> </div>
</wd-popup> </wd-popup>
</template> </template>
<style scoped lang="scss">
.wrap1 {
img {
width: 381.59rpx;
height: 280.14rpx;
}
}
</style>

View File

@ -30,6 +30,14 @@
"enablePullDownRefresh":false "enablePullDownRefresh":false
} }
}, },
{
"path": "pages/forwardRecord/index",
"type": "page",
"style": {
"navigationStyle": "custom",
"enablePullDownRefresh":false
}
},
{ {
"path": "pages/login/index", "path": "pages/login/index",
"type": "page", "type": "page",

View File

@ -127,7 +127,7 @@ const handleOk = () => {
let user_ids = [] let user_ids = []
let group_ids = [] let group_ids = []
for (let o of selectItems.value) { for (let o of selectItemsModal.value) {
if (o.talk_type == 1) { if (o.talk_type == 1) {
user_ids.push(o.receiver_id) user_ids.push(o.receiver_id)
} else { } else {
@ -141,6 +141,7 @@ const handleOk = () => {
uids: user_ids, uids: user_ids,
gids: group_ids gids: group_ids
}) })
uni.navigateBack() uni.navigateBack()
} }

View File

@ -17,7 +17,7 @@
<div class="dialogBox"> <div class="dialogBox">
<ZPaging :fixed="false" use-chat-record-mode :use-page-scroll="false" :refresher-enabled="false" <ZPaging :fixed="false" use-chat-record-mode :use-page-scroll="false" :refresher-enabled="false"
:show-scrollbar="false" :loading-more-enabled="false" :hide-empty-view="true" height="100%" ref="zpagingRef" :show-scrollbar="false" :loading-more-enabled="false" :hide-empty-view="true" height="100%" ref="zpagingRef"
:use-virtual-list="true" :preload-page="1" cell-height-mode="dynamic" :use-virtual-list="true" :preload-page="1" cell-height-mode="dynamic" virtual-scroll-fps="80"
:loading-more-custom-style="{ display: 'none', height: '0' }" @virtualListChange="virtualListChange" :loading-more-custom-style="{ display: 'none', height: '0' }" @virtualListChange="virtualListChange"
@scrolltolower="onRefreshLoad"> @scrolltolower="onRefreshLoad">
<!-- <template #top> <!-- <template #top>
@ -70,35 +70,15 @@
</div> </div>
<div class="talk-content" :class="{ pointer: dialogueStore.isOpenMultiSelect }"> <div class="talk-content" :class="{ pointer: dialogueStore.isOpenMultiSelect }">
<tm-popover :position="item.float == 'left' ? 'tl' : 'tr'" :width="526" color="#333333"> <deepBubble
<component :key="item.zp_index" :is="MessageComponents[item.msg_type] || 'unknown-message'" @clickMenu="(menuType) => onContextMenu(menuType, item)"
:extra="item.extra" :data="item" :max-width="true" :source="'panel'" :isShowCopy="isShowCopy(item)"
@click="onContextMenu(item)" /> :isShowWithdraw="isRevoke(talkParams.uid,item)"
<template v-slot:label> >
<div class="w-full h-[132rpx] text-[24rpx] text-[#FFFFFF] flex items-center justify-around"> <component class="component-content" :key="item.zp_index"
<div @click="() => actionCopy(item)" class="flex flex-col items-center justify-center"> :is="MessageComponents[item.msg_type] || 'unknown-message'" :extra="item.extra" :data="item"
<tm-image :width="40" :height="40" :src="copy07"></tm-image> :max-width="true" :source="'panel'" />
<div>复制</div> </deepBubble>
</div>
<div @click="() => multipleChoose(item)" class="flex flex-col items-center justify-center">
<tm-image :width="40" :height="40" :src="multipleChoices"></tm-image>
<div>多选</div>
</div>
<div @click="() => actionCite(item)" class="flex flex-col items-center justify-center">
<tm-image :width="40" :height="40" :src="cite"></tm-image>
<div>引用</div>
</div>
<div @click="() => actionWithdraw(item)" class="flex flex-col items-center justify-center">
<tm-image :width="40" :height="40" :src="withdraw"></tm-image>
<div>撤回</div>
</div>
<div @click="() => actionDelete(item)" class="flex flex-col items-center justify-center">
<tm-image :width="40" :height="40" :src="delete07"></tm-image>
<div>删除</div>
</div>
</div>
</template>
</tm-popover>
<!-- <div class="talk-tools"> <!-- <div class="talk-tools">
<template v-if="talkParams.type == 1 && item.float == 'right'"> <template v-if="talkParams.type == 1 && item.float == 'right'">
<loading theme="outline" size="19" fill="#000" :sxtrokeWidth="1" class="icon-rotate" <loading theme="outline" size="19" fill="#000" :sxtrokeWidth="1" class="icon-rotate"
@ -192,7 +172,15 @@ import { QuillEditor, Quill } from '@vueup/vue-quill'
import EmojiBlot from './formats/emoji' import EmojiBlot from './formats/emoji'
import { useChatList } from "@/store/chatList/index.js"; import { useChatList } from "@/store/chatList/index.js";
import { useAuth } from "@/store/auth"; import { useAuth } from "@/store/auth";
import { useUserStore, useDialogueStore, useUploadsStore, useEditorDraftStore, useTalkStore, useSettingsStore, useDialogueListStore } from '@/store' import {
useUserStore,
useDialogueStore,
useUploadsStore,
useEditorDraftStore,
useTalkStore,
useSettingsStore,
useDialogueListStore
} from '@/store'
import addCircleGray from "@/static/image/chatList/addCircleGray.png"; import addCircleGray from "@/static/image/chatList/addCircleGray.png";
import { MessageComponents, ForwardableMessageType } from '@/constant/message' import { MessageComponents, ForwardableMessageType } from '@/constant/message'
import { formatTime, parseTime } from '@/utils/datetime' import { formatTime, parseTime } from '@/utils/datetime'
@ -216,6 +204,9 @@ import zu6050 from "@/static/image/chatList/zu6050@2x.png"
import zu6051 from "@/static/image/chatList/zu6051@2x.png" import zu6051 from "@/static/image/chatList/zu6051@2x.png"
import zu6052 from "@/static/image/chatList/zu6052@2x.png" import zu6052 from "@/static/image/chatList/zu6052@2x.png"
import zu6053 from "@/static/image/chatList/zu6053@2x.png" import zu6053 from "@/static/image/chatList/zu6053@2x.png"
import deepBubble from "@/components/deep-bubble/deep-bubble.vue"
import {isRevoke } from './menu'
Quill.register('formats/emoji', EmojiBlot) Quill.register('formats/emoji', EmojiBlot)
@ -349,6 +340,22 @@ const getQuill = () => {
return editor.value?.getQuill() return editor.value?.getQuill()
} }
const isShowCopy = (item) => {
switch (item.msg_type) {
case 1:
return true
case 3:
return true
case 5:
return true
case 6:
return true
default:
return false
}
}
const selectedMessage = computed(() => { const selectedMessage = computed(() => {
return virtualList.value.filter(item => item.isCheck) return virtualList.value.filter(item => item.isCheck)
}) })
@ -488,8 +495,28 @@ const virtualListChange = (vList) => {
virtualList.value = vList virtualList.value = vList
} }
const onContextMenu = (item) => { const onContextMenu = (menuType, item) => {
console.log(item, 'item'); console.log(menuType, item, 'item');
switch (menuType) {
case 'actionCopy':
actionCopy(item)
break;
case 'multipleChoose':
multipleChoose(item)
break;
case 'actionCite':
actionCite(item)
break;
case 'actionWithdraw':
actionWithdraw(item)
break;
case 'actionDelete':
actionDelete(item)
break;
default:
break;
}
} }
const actionCopy = (item) => { const actionCopy = (item) => {
@ -519,16 +546,19 @@ const multipleChoose = (item) => {
dialogueStore.setMultiSelect(true) dialogueStore.setMultiSelect(true)
} }
const actionCite = () => { const actionCite = (item) => {
console.log('引用'); console.log('引用');
} }
const actionWithdraw = () => { const actionWithdraw = (item) => {
console.log('撤回'); console.log('撤回');
dialogueStore.ApiRevokeRecord(item.msg_id)
} }
const actionDelete = () => { const actionDelete = (item) => {
console.log('删除'); console.log('删除');
item.isCheck = true
handleDelete()
} }
const handleMergeForward = () => { const handleMergeForward = () => {
@ -551,6 +581,14 @@ const handleSingleForward = () => {
return message.warning('未选择消息') return message.warning('未选择消息')
} }
console.log('逐条转发'); console.log('逐条转发');
dialogueStore.setForwardType(1)
dialogueStore.setForwardMessages(selectedMessage.value)
uni.navigateTo({
url: '/pages/chooseChat/index',
success: function (res) {
clearMultiSelect()
}
})
} }
const handleWechatForward = () => { const handleWechatForward = () => {
@ -565,6 +603,10 @@ const handleDelete = () => {
return message.warning('未选择消息') return message.warning('未选择消息')
} }
console.log('删除'); console.log('删除');
const msgIds = selectedMessage.value.map(item => item.msg_id)
virtualList.value = virtualList.value.filter(item => !msgIds.includes(item.msg_id))
dialogueStore.ApiDeleteRecord(msgIds)
clearMultiSelect()
} }
@ -898,4 +940,10 @@ page {
:deep(.wd-action-sheet__panel-title) { :deep(.wd-action-sheet__panel-title) {
color: #fff !important; color: #fff !important;
} }
.component-content {
position: relative;
z-index: 1;
/* 确保 z-index 低于 deepBubble */
}
</style> </style>

68
src/pages/dialog/menu.ts Normal file
View File

@ -0,0 +1,68 @@
import { reactive } from 'vue'
interface IDropdown {
options: any[]
show: boolean
x: number
y: number
item: any
}
export const isRevoke = (uid: any, item: any): boolean => {
if (uid != item.user_id) {
return false
}
const datetime = item.created_at.replace(/-/g, '/')
const time = new Date().getTime() - Date.parse(datetime)
return Math.floor(time / 1000 / 60) <= 2
}
export function useMenu() {
const dropdown: IDropdown = reactive({
options: [],
show: false,
x: 0,
y: 0,
item: {}
})
const showDropdownMenu = (e: any, uid: number, item: any) => {
dropdown.item = Object.assign({}, item)
dropdown.options = []
if ([1, 3].includes(item.msg_type)) {
dropdown.options.push({ label: '复制', key: 'copy' })
}
if (isRevoke(uid, item)) {
dropdown.options.push({ label: `撤回`, key: 'revoke' })
}
dropdown.options.push({ label: '回复', key: 'quote' })
dropdown.options.push({ label: '删除', key: 'delete' })
dropdown.options.push({ label: '多选', key: 'multiSelect' })
if ([3, 4, 5].includes(item.msg_type)) {
dropdown.options.push({ label: '下载', key: 'download' })
}
if ([3].includes(item.msg_type)) {
dropdown.options.push({ label: '收藏', key: 'collect' })
}
dropdown.x = e.clientX
dropdown.y = e.clientY
dropdown.show = true
}
const closeDropdownMenu = () => {
dropdown.show = false
dropdown.item = {}
}
return { dropdown, showDropdownMenu, closeDropdownMenu }
}

View File

@ -0,0 +1,129 @@
<script lang="ts" setup>
import { ref, onMounted } from 'vue'
import { ServeGetForwardRecords } from '@/api/chat'
import { MessageComponents } from '@/constant/message'
import { ITalkRecord } from '@/types/chat'
import WdLoading from "@/uni_modules/wot-design-uni/components/wd-loading/wd-loading.vue";
import { useInject } from '@/hooks'
const emit = defineEmits(['close'])
const msgId = ref(null)
const { showUserInfoModal } = useInject()
const isShow = ref(true)
const items = ref<ITalkRecord[]>([])
const title = ref('会话记录')
const onMaskClick = () => {
emit('close')
}
const onLoadData = () => {
ServeGetForwardRecords({
msg_id: msgId.value
}).then((res) => {
if (res.code == 200) {
items.value = res.data.items || []
title.value = [...new Set(items.value.map((v) => v.nickname))].join('、')
}
})
}
onMounted(() => {
const pages = getCurrentPages()
const page = pages[pages.length - 1]
const options = page.$page.options
msgId.value = options.msgId
console.log(msgId.value,'msgId.value');
onLoadData()
})
</script>
<template>
<div class="outer-layer">
<div>
<tm-navbar :hideBack="false" hideHome :title="`${title}的会话记录`" >
</tm-navbar>
</div>
<div class="main-box">
<div v-if="items.length === 0" class="flex justify-center items-center w-full mt-[200rpx]">
<wd-loading />
</div>
<div v-else v-for="item in items" :key="item.msg_id" class="message-item">
<div class="left-box" @click="showUserInfoModal(item.user_id)">
<tm-image v-if="item.avatar !==''" :width="80" :height="80" :round="12" :src="item.avatar"></tm-image>
<div v-else class="w-[80rpx] h-[80rpx] text-[28rpx] text-[#fff] bg-[#46299D] leading-[40rpx] rounded-[40rpx] flex justify-center items-center">{{ item.nickname.slice(-2) }}</div>
</div>
<div class="right-box">
<div class="msg-header">
<span class="name">{{ item.nickname }}</span>
<span class="time"> {{ item.created_at }}</span>
</div>
<component
:is="MessageComponents[item.msg_type] || 'unknown-message'"
:extra="item.extra"
:data="item"
/>
</div>
</div>
</div>
</div>
</template>
<style lang="less" scoped>
.outer-layer {
overflow-y: auto;
flex: 1;
background-image: url("@/static/image/clockIn/z3280@3x.png");
background-size: cover;
padding: 0 66rpx 20rpx 50rpx;
display: flex;
flex-direction: column;
}
.main-box {
flex:1;
width: 100%;
overflow-y: auto;
display: flex;
flex-direction: column;
overflow: auto;
padding-top: 28rpx;
}
.message-item {
min-height: 60rpx;
display: flex;
margin-bottom: 44rpx;
.left-box {
width: 80rpx;
display: flex;
user-select: none;
}
.right-box {
width: 100%;
overflow-x: auto;
padding-left: 22rpx;
box-sizing: border-box;
height: fit-content;
.msg-header {
height: 34rpx;
line-height: 34rpx;
width: 526rpx;
font-size: 24rpx;
color: #999999;
position: relative;
user-select: none;
display: flex;
justify-content: space-between;
margin-bottom: 6rpx;
}
}
}
</style>

View File

@ -0,0 +1,175 @@
import { Delta } from '@vueup/vue-quill'
interface Item {
type: number
content: string
}
interface AnalysisResp {
items: Item[]
mentions: any[]
mentionUids: number[]
msgType: number // 1 文本2图片3图文混合消息
quoteId: string // 引用的消息ID
}
function removeLeadingNewlines(str: string) {
return str.replace(/^[\n\s]+/, '')
}
function removeTrailingNewlines(str: string) {
return str.replace(/[\n\s]+$/, '')
}
export function deltaToMessage(delta: Delta): AnalysisResp {
const resp: AnalysisResp = {
items: [],
mentions: [],
mentionUids: [],
quoteId: '',
msgType: 1
}
for (const iterator of delta.ops) {
const insert: any = iterator.insert
let node: any = null
if (resp.items.length) {
node = resp.items[resp.items.length - 1]
}
if (typeof insert === 'string') {
if (!insert || insert == '\n') continue
if (node && node.type == 1) {
node.content = node.content + insert
continue
}
resp.items.push({
type: 1,
content: insert
})
continue
}
// @好友
if (insert && insert.mention) {
const mention = insert.mention
resp.mentions.push({
name: `${mention.denotationChar}${mention.value}`,
atid: parseInt(mention.id)
})
if (node && node.type == 1) {
node.content = node.content + ` ${mention.denotationChar}${mention.value}`
continue
}
resp.items.push({
type: 1,
content: `${mention.denotationChar}${mention.value}`
})
continue
}
// 图片
if (insert && insert.image) {
resp.items.push({
type: 3,
content: insert.image
})
continue
}
// 表情
if (insert && insert.emoji) {
const { emoji } = insert
if (node && node.type == 1) {
node.content = node.content + emoji.alt
continue
}
resp.items.push({
type: 1,
content: emoji.alt
})
continue
}
if (insert && insert.quote) {
resp.quoteId = insert.quote.id
continue
}
}
// 去除前后多余空格
if (resp.items.length) {
if (resp.items[0].type == 1) {
resp.items[0].content = removeLeadingNewlines(resp.items[0].content)
}
if (resp.items[resp.items.length - 1].type == 1) {
resp.items[resp.items.length - 1].content = removeTrailingNewlines(
resp.items[resp.items.length - 1].content
)
}
}
if (resp.items.length > 1) {
resp.msgType = 12
}
if (resp.items.length == 1) {
resp.msgType = resp.items[0].type
}
resp.mentionUids = resp.mentions.map((item) => item.atid)
return resp
}
export function deltaToString(delta: Delta): string {
let content = ''
for (const o of delta.ops) {
const insert: any = o.insert
if (typeof insert === 'string') {
if (!insert || insert == '\n') continue
content += insert
continue
}
// @好友
if (insert && insert.mention) {
const { mention } = insert
content += ` ${mention.denotationChar}${mention.value} `
continue
}
// 图片
if (insert && insert.image) {
content += '[图片]'
continue
}
// 表情
if (insert && insert.emoji) {
content += insert.emoji.alt
continue
}
}
return content
}
export function isEmptyDelta(delta: Delta): boolean {
return delta.ops.length == 1 && delta.ops[0].insert == '\n'
}

View File

@ -1,53 +1,90 @@
<template> <template>
<div> <div>
<wd-swipe-action> <wd-swipe-action>
<div @click="cellClick" :class="['chatItem', props.data.is_top === 1 ? 'isTop' : '']"> <div
@click="cellClick"
:class="['chatItem', props.data.is_top === 1 ? 'isTop' : '']"
>
<div class="avatarImg"> <div class="avatarImg">
<tm-badge :count="props.data.unread_num" :maxCount="99" color="#D03050"> <tm-badge
<tm-image :width="96" :height="96" :round="12" :src="props.data.avatar"></tm-image> :count="props.data.unread_num"
:maxCount="99"
color="#D03050"
>
<tm-image
:width="96"
:height="96"
:round="12"
:src="avatarCpt"
></tm-image>
</tm-badge> </tm-badge>
</div> </div>
<div class="chatInfo"> <div class="chatInfo">
<div class="chatInfo_1"> <div class="chatInfo_1">
<div class="flex items-center"> <div class="flex items-center">
<div class="text-[#000000] text-[32rpx] font-bold opacity-90">{{ props.data.name }}</div> <div class="text-[#000000] text-[32rpx] font-bold opacity-90">
{{ props.data.name }}
</div>
<div> <div>
<div class="companyTag">公司</div> <div v-if="props.data.group_type === 2" class="depTag">
部门
</div>
<div v-if="props.data.group_type === 3" class="projectTag">
项目
</div>
<div v-if="props.data.group_type === 4" class="companyTag">
公司
</div> </div>
</div> </div>
<div class="text-[#000000] text-[28rpx] font-medium opacity-26">{{ beautifyTime(props.data.updated_at) }} </div>
<div class="text-[#000000] text-[28rpx] font-medium opacity-26">
{{ beautifyTime(props.data.updated_at) }}
</div> </div>
</div> </div>
<div class="chatInfo_2 w-full mr-[6rpx]"> <div class="chatInfo_2 w-full mr-[6rpx]">
<div class="w-full chatInfo_2_1 textEllipsis">{{ props.data.msg_text }}</div> <div class="w-full chatInfo_2_1 textEllipsis">
{{ props.data.msg_text }}
</div>
</div> </div>
</div> </div>
</div> </div>
<template #right> <template #right>
<div class="flex flex-row flex-row-center-end"> <div class="flex flex-row flex-row-center-end">
<div @click="handleTop" <div
class="w-[156rpx] h-[154rpx] text-[#ffffff] bg-[#F09F1F] flex items-center justify-center"> @click="handleTop"
{{ props.data.is_top === 1 ? '取消置顶' : '置顶' }}</div> class="w-[156rpx] h-[154rpx] text-[#ffffff] bg-[#F09F1F] flex items-center justify-center"
<div class="w-[156rpx] h-[154rpx] text-[#ffffff] bg-[#CF3050] flex items-center justify-center">删除</div> >
{{ props.data.is_top === 1 ? "取消置顶" : "置顶" }}
</div>
<div
class="w-[156rpx] h-[154rpx] text-[#ffffff] bg-[#CF3050] flex items-center justify-center"
>
删除
</div>
</div> </div>
</template> </template>
</wd-swipe-action> </wd-swipe-action>
<div v-if="props.index !== talkStore.talkItems.length - 1" class="divider"></div> <div
v-if="props.index !== talkStore.talkItems.length - 1"
class="divider"
></div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, reactive, defineProps } from "vue" import { ref, reactive, defineProps,computed } from "vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { beautifyTime } from '@/utils/datetime' import { beautifyTime } from "@/utils/datetime";
import { ServeClearTalkUnreadNum } from '@/api/chat' import { ServeClearTalkUnreadNum } from "@/api/chat";
import { useTalkStore, useDialogueStore } from '@/store' import { useTalkStore, useDialogueStore } from "@/store";
import { useSessionMenu } from '@/hooks' import { useSessionMenu } from "@/hooks";
import zu4989 from "@/static/image/chatList/zu4989@2x.png";
import zu4991 from "@/static/image/chatList/zu4991@2x.png";
import zu4992 from "@/static/image/chatList/zu4992@2x.png";
import zu5296 from "@/static/image/chatList/zu5296@2x.png";
const talkStore = useTalkStore() const talkStore = useTalkStore();
const { const { onToTopTalk } = useSessionMenu();
onToTopTalk const dialogueStore = useDialogueStore();
} = useSessionMenu()
const dialogueStore = useDialogueStore()
const props = defineProps({ const props = defineProps({
data: { data: {
type: Object, type: Object,
@ -58,38 +95,58 @@ const props = defineProps({
type: Number, type: Number,
default: -1, default: -1,
required: true, required: true,
},
});
const avatarCpt = computed(() => {
let avatar = null;
if (props.data.avatar !== "") {
avatar = props.data.avatar;
} else {
switch (props.data.group_type) {
case 1:
avatar = zu4992;
break;
case 2:
avatar = zu4989;
break;
case 3:
avatar = zu4991;
break;
case 4:
avatar = zu5296;
break;
} }
}
return avatar;
}); });
const cellClick = () => { const cellClick = () => {
console.log(props.data); console.log(props.data);
// //
dialogueStore.setDialogue(props.data) dialogueStore.setDialogue(props.data);
// //
if (props.data.unread_num > 0) { if (props.data.unread_num > 0) {
ServeClearTalkUnreadNum({ ServeClearTalkUnreadNum({
talk_type: props.data.talk_type, talk_type: props.data.talk_type,
receiver_id: props.data.receiver_id receiver_id: props.data.receiver_id,
}).then(() => { }).then(() => {
talkStore.updateItem({ talkStore.updateItem({
index_name: props.data.index_name, index_name: props.data.index_name,
unread_num: 0 unread_num: 0,
}) });
}) });
} }
uni.navigateTo({ uni.navigateTo({
url: '/pages/dialog/index', url: "/pages/dialog/index",
}) });
} };
const handleTop = () => { const handleTop = () => {
console.log(props.data, 1); console.log(props.data, 1);
onToTopTalk(props.data) onToTopTalk(props.data);
} };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.chatItem { .chatItem {
@ -100,7 +157,7 @@ const handleTop = () => {
align-items: center; align-items: center;
&.isTop { &.isTop {
background-color: #F3F3F3; background-color: #f3f3f3;
} }
} }
@ -131,17 +188,43 @@ const handleTop = () => {
font-size: 28rpx; font-size: 28rpx;
color: #000000; color: #000000;
opacity: 40%; opacity: 40%;
} }
.companyTag { .companyTag {
width: 76rpx; width: 76rpx;
height: 38rpx; height: 38rpx;
border: 1px solid #7A58DE; line-height: 38rpx;
border: 1px solid #7a58de;
font-size: 24rpx; font-size: 24rpx;
text-align: center; text-align: center;
border-radius: 6rpx; border-radius: 6rpx;
color: #7A58DE; color: #7a58de;
font-weight: bold;
margin-left: 12rpx;
}
.depTag {
width: 76rpx;
height: 38rpx;
line-height: 38rpx;
border: 1px solid #377ec6;
font-size: 24rpx;
text-align: center;
border-radius: 6rpx;
color: #377ec6;
font-weight: bold;
margin-left: 12rpx;
}
.projectTag {
width: 76rpx;
height: 38rpx;
line-height: 38rpx;
border: 1px solid #c1681c;
font-size: 24rpx;
text-align: center;
border-radius: 6rpx;
color: #c1681c;
font-weight: bold; font-weight: bold;
margin-left: 12rpx; margin-left: 12rpx;
} }

View File

@ -4,7 +4,7 @@ import {uniStorage} from "@/utils/uniStorage.js"
import {ref} from 'vue' import {ref} from 'vue'
export const useAuth = createGlobalState(() => { export const useAuth = createGlobalState(() => {
// const token = useStorage('token', '', uniStorage) // const token = useStorage('token', '', uniStorage)
const token = ref('30119d9978a6f3321fb4779c0040e997df4dd0dd0cf6b71119657617d2249ed783f940b0050d5be7e758740ea467afdf3eeb4d28fb5ea234af60ebe51fb218ff6a0563074f3084b41c1bc8dc0733d06bfbb433a8d5a1d13eb6227adbf50a5da566a4cacdbf91899e563f10864fe2acfeee36e90ceb1aa03bbcca63f1bf5514d416827d0ea543bd8e02552fbd612d801b4b827977f6fe7d6201838456049083706a6e48004fc0ad99eddea3a8875f6583e959fd172a7cfb40f877bc0741a259520e9b6f524d04ee175bdc99c32eef4ef52c1ed38ed1034df8695c62ff44933644') const token = ref('30119d9978a6f3321fb4779c0040e997df4dd0dd0cf6b71119657617d2249ed783f940b0050d5be7e758740ea467afdf3eeb4d28fb5ea234af60ebe51fb218ff6a0563074f3084b41c1bc8dc0733d06bfbb433a8d5a1d13eb6227adbf50a5da566a4cacdbf91899e563f10864fe2acfe9526343f2c27f726448f6cf9ac172a9716f21a7079e22f4ded14f57418364bea07bfd33259ff97ca4c3c4a54cd90e529b1ac523444d29485f9ee637bb0ca5dfdd87125a1161291696084029bc0cc6bbcf690e3d99787d75eabd8906b6f79035c23391abf12bbabf10ab7b1a0ca998c38')
const refreshToken = useStorage('refreshToken', '', uniStorage) const refreshToken = useStorage('refreshToken', '', uniStorage)
const userInfo = useStorage('userInfo', {}, uniStorage) const userInfo = useStorage('userInfo', {}, uniStorage)
const leaderList = useStorage('leaderList', [], uniStorage) const leaderList = useStorage('leaderList', [], uniStorage)

View File

@ -7,6 +7,7 @@ import {
} from '@/api/chat/index' } from '@/api/chat/index'
import { ServeGetGroupMembers } from '@/api/group/index' import { ServeGetGroupMembers } from '@/api/group/index'
import { useEditorStore } from './editor' import { useEditorStore } from './editor'
import { useDialogueListStore } from './dialogueList'
// 键盘消息事件定时器 // 键盘消息事件定时器
// let keyboardTimeout = null // let keyboardTimeout = null
@ -186,6 +187,7 @@ export const useDialogueStore = defineStore('dialogue', {
// 删除聊天记录 // 删除聊天记录
ApiDeleteRecord(msgIds = []) { ApiDeleteRecord(msgIds = []) {
const { batchDelDialogueRecord } = useDialogueListStore()
ServeRemoveRecords({ ServeRemoveRecords({
talk_type: this.talk.talk_type, talk_type: this.talk.talk_type,
receiver_id: this.talk.receiver_id, receiver_id: this.talk.receiver_id,
@ -193,6 +195,7 @@ export const useDialogueStore = defineStore('dialogue', {
}).then((res) => { }).then((res) => {
if (res.code == 200) { if (res.code == 200) {
this.batchDelDialogueRecord(msgIds) this.batchDelDialogueRecord(msgIds)
batchDelDialogueRecord(msgIds)
} else { } else {
message.warning(res.message) message.warning(res.message)
} }

View File

@ -103,6 +103,12 @@ export const useDialogueListStore = createGlobalState(() => {
} }
} }
const batchDelDialogueRecord=(msgIds)=>{
const dialogue = lodash.cloneDeep(useDialogueStore())
const item = getDialogueList(dialogue.index_name)
item.records = item.records.filter(item=>!msgIds.includes(item.msg_id))
}
return { return {
dialogueList, dialogueList,
zpagingRef, zpagingRef,
@ -115,5 +121,6 @@ export const useDialogueListStore = createGlobalState(() => {
zpagingComplete, zpagingComplete,
addChatRecord, addChatRecord,
virtualList, virtualList,
batchDelDialogueRecord,
} }
}) })