1.图片拖拽
const editableDiv = document.getElementById('editable');
editableDiv.addEventListener('dragover', function(event) {
event.preventDefault(); // 阻止默认行为
});
editableDiv.addEventListener('drop', function(event) {
event.preventDefault(); // 阻止默认行为
const files = event.dataTransfer.files; // 获取拖放的文件
// 处理文件
handleFiles(files);
});
function handleFiles(files) {
[...files].forEach(file => {
// 检查文件类型
if (!file.type.startsWith('image/')) {
alert('请拖入图片文件');
return;
}
const reader = new FileReader();
reader.onload = function(event) {
// 创建图片元素并插入到div
const img = document.createElement('img');
img.src = event.target.result;
editableDiv.appendChild(img);
};
reader.readAsDataURL(file); // 读取文件
});
}
2.文件复制
document.getElementById('editable-div').addEventListener('paste', function(event) {
// 阻止默认粘贴行为
event.preventDefault();
// 获取剪贴板数据
const clipboardData = event.clipboardData || window.clipboardData;
const items = clipboardData.items;
for (let i = 0; i < items.length; i++) {
const item = items[i];
// 检查是否是文件
if (item.kind === 'file') {
const file = item.getAsFile();
// 根据文件类型处理
if (file.type === 'text/plain') {
// 对于.txt文件,直接插入文本
const reader = new FileReader();
reader.onload = function(e) {
const text = e.target.result;
document.getElementById('editable-div').textContent += text;
};
reader.readAsText(file);
} else if (file.type === 'application/pdf' || file.type === 'application/vnd.openxmlformats-officedocument.presentationml.presentation') {
// 对于PDF和PPTX文件,可以尝试将它们转换为图片或者提供预览链接
const fileURL = URL.createObjectURL(file);
// 这里可以插入一个iframe或者图片来预览文件
console.log('Preview URL:', fileURL);
} else {
console.log('Unsupported file type:', file.type);
}
}
}
});
3.根据视频图片地址获取其宽高
//视频:
getVideoMsg(file) {
return new Promise((resolve) => {
let videoElement = document.createElement("video")
videoElement.src = URL.createObjectURL(file)
videoElement.addEventListener("loadedmetadata", function () {
resolve({
duration: videoElement.duration,
height: videoElement.videoHeight,
width: videoElement.videoWidth,
})
})
})
},
//图片:
getPictureInfo(url){
const image = new Image();
image.src = url;
image.onload = function(){
const height = image.height;
const width = image.width;
}
}
4.将div封装成聊天发送框
import { Button, Flex, Input, Popover, message } from "antd";
import { FC, useState, useRef, useEffect } from "react";
import emojiIcon from "@/asisite/image/boshiweb_biaoqing.png";
import imgIcon from "@/asisite/image/tupian.png";
import FileIcon from "@/asisite/image/tupian copy.png";
import SendIcon from "@/asisite/image/fasong.svg";
import { Short } from "../Allert/short";
import { useSelector, useDispatch } from "react-redux";
import { RootState } from "@/store/store";
import {
setFocusToEnd,
insertAtCursor,
handleSendMessage,
SendMsgs,
} from "./operate";
import { ChangeRecords } from "@/store/reducer/UserInfoSlice";
import { ChangeCount } from "@/store/reducer/ListSlice";
import UploadPicture from "../UploadPicture";
import ShuCaiIcon from "@/asisite/image/tupian copy 3.png";
import shortIcon from "@/asisite/image/tupian copy 2.png";
import Emoji from "./Emoji";
const TextArea = Input.TextArea;
interface FooterProps {
daRef: any;
}
export const Footer: FC<FooterProps> = ({ daRef }) => {
const [valutOpen, setValutOpen] = useState<boolean>(false);
const dispatch = useDispatch();
const records = useSelector((state: RootState) => state.UserInfoSlice.record);
const chatInfo = useSelector(
(state: RootState) => state.UserInfoSlice.chatInfo
);
const userInfo = useSelector(
(state: RootState) => state.UserInfoSlice.userInfo
);
const ChangeValutOpen = (val: boolean) => {
setValutOpen(val);
};
const connectState = useSelector(
(state: RootState) => state.ListSlice.connectState
);
// 图片区分
const [pics, setPics] = useState<any>({});
const sendImg = (data: any) => {
if (connectState && chatInfo?.userID.length) {
SendMsgs(102, chatInfo?.userID, data, changemsg);
} else {
message.error("disconnect...");
}
};
const HandleKeyDown = (e: any) => {
if (e.key === "Enter" && !e.ctrlKey) {
e.preventDefault();
handleSendMessage(refs, chatInfo, pics, userInfo, dispatch, setPics,setFlag);
} else if (e.key === "Enter" && e.ctrlKey) {
const parser = new DOMParser();
const doc = parser.parseFromString("\n", "text/html");
const nodes = Array.from(doc.body.childNodes);
insertAtCursor(nodes);
}
};
const refs = useRef<any>(null);
// 监听粘贴事件=>粘贴时添加图片标识
const [pic, setPic] = useState<any>({});
useEffect(() => {
if (JSON.stringify(pic) !== "{}") {
setPics((pics: any) => {
if (JSON.stringify(pics) !== "{}") {
return { ...pics, ...pic };
} else {
return pic;
}
});
}
}, [pic]);
useEffect(() => {
const handlePaste = (e: any) => {
const clp = e.clipboardData;
e.preventDefault();
if (clp?.items[0].type.includes("text/html")) {
let text = clp.getData("text/html") || "";
const imgMeta = "<meta charset='utf-8'>";
if (text.includes(imgMeta)) {
text = text.replaceAll(imgMeta, "");
const parser = new DOMParser();
const doc = parser.parseFromString(text, "text/html");
const nodes = Array.from(doc.body.childNodes);
insertAtCursor(nodes);
} else {
text = text.replace(/<\/?[^>]+(>|$)/g, "");
insertAtCursor([document.createTextNode(text)]);
}
}
// 粘贴文件=》待开发
if (clp?.items[0].type.includes("text/plain")) {
const text = clp.getData("text/plain") || "";
const reader = new FileReader();
const blob = clp?.items[0].getAsFile();
console.log("file==", blob);
reader.onload = () => {
const parser = new DOMParser();
const doc = parser.parseFromString(
reader.result as string,
"text/html"
);
const nodes = Array.from(doc.body.childNodes);
insertAtCursor(nodes);
};
reader.readAsDataURL(blob!);
}
const images = [] as HTMLImageElement[];
const imageItems = [...clp.items].filter((item: any) =>
item.type.includes("image")
);
// const obj: any = {};
imageItems.map((item: DataTransferItem) => {
const blob = item.getAsFile();
// 文件流
console.log("blob===", blob);
const reader = new FileReader();
reader.onload = () => {
const obj: any = {};
const image = new Image();
image.src = reader.result as string;
const str = new Date().getTime().toString();
image.title = str;
obj[str] = blob;
setPic(obj);
images.push(image);
if (images.length === imageItems.length) {
insertAtCursor([...images]);
}
};
reader.readAsDataURL(blob!);
});
};
const editor = refs?.current;
editor?.addEventListener("paste", handlePaste);
return () => {
editor?.removeEventListener("paste", handlePaste);
};
}, []);
// emoji框
const [emojiOpen, setEmojiOpen] = useState<boolean>(false);
const handleOpenChange = (newOpen: boolean) => {
setEmojiOpen(newOpen);
};
useEffect(() => {
if (refs && refs.current) {
setFocusToEnd();
}
}, [chatInfo]);
// 消息单发
const changemsg = (val: any) => {
const arr: any = [...records];
arr.push(val);
dispatch(ChangeRecords(arr));
dispatch(ChangeCount(false));
};
useEffect(() => {
console.log("pics===", pics);
}, [pics]);
// 当发送内容为空时
const [flag, setFlag] = useState<boolean>(false);
useEffect(()=>{
if(flag){
setTimeout(()=>{
setFlag(false)
},1000)
}
},[flag])
return (
<>
<Flex className="box-head" align="center">
<Popover
content={<Emoji Hide={setEmojiOpen} />}
trigger="click"
overlayClassName="emoji"
open={emojiOpen}
onOpenChange={handleOpenChange}
>
<img className="mr4 point" src={emojiIcon} alt="" />
</Popover>
{/* <img className="mr4 point" src={imgIcon} alt="" /> */}
<UploadPicture SendMsg={sendImg} />
<img className="mr4 point" src={FileIcon} alt="" />
<img className="mr4 point" src={ShuCaiIcon} alt="" />
<Short
Open={valutOpen}
ChangeOpen={ChangeValutOpen}
daRef={daRef}
/>
</Flex>
<Flex align="center">
<div
className="input"
contentEditable="true"
ref={refs}
id="editable-div"
defaultValue="please input"
onKeyDown={HandleKeyDown}
/>
<Popover content={<div className="send-tootile">不能发送空白信息</div>} open={flag}>
<Flex
className="btn point"
onClick={() =>
handleSendMessage(
refs,
chatInfo,
pics,
userInfo,
dispatch,
setPics,
setFlag
)
}
>
<div className="mr5">Send</div>
<img src={SendIcon} alt="" width={10} />
</Flex>
</Popover>
</Flex>
</>
);
};
1.聊天发送框聚焦
export const setFocusToEnd = () => {
var editableDiv = document.getElementById("editable-div");
if (editableDiv) {
editableDiv.focus(); // 聚焦元素
if (
typeof window.getSelection != "undefined" &&
typeof document.createRange != "undefined"
) {
var range = document.createRange();
range.selectNodeContents(editableDiv);
range.collapse(false);
var sel: any = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
} else {
if ("createTextRange" in document.body) {
// 这里是针对IE的代码
const rage = (document.body as any).createTextRange();
// ... 使用range进行相关操作
if (rage != "undefined") {
rage.moveToElementText(editableDiv);
rage.collapse(false);
rage.select();
}
}
}
editableDiv.scrollTop = editableDiv.scrollHeight;
}
};
2.项发送框插入节点
export const insertAtCursor = (nodes: Node[], rangeIdx = 0) => {
const selection = window.getSelection();
const range = selection!.getRangeAt(rangeIdx);
range.deleteContents();
nodes.map((node) => {
range.insertNode(node);
range.setStartAfter(node);
});
range.setEndAfter(nodes[nodes.length - 1]);
selection!.removeAllRanges();
selection!.addRange(range);
// Move the inserted nodes to the input element if they are not already there
const inputElement = document.getElementById("editable-div");
nodes.forEach((node) => {
let parent = node.parentElement;
while (parent) {
if (parent === inputElement) return;
parent = parent.parentElement;
}
inputElement?.appendChild(node);
});
return range && setFocusToEnd();
};
3.混合消息发送处理函数
export const handleSendMessage = (
refs: any,
chatInfo: any,
pics: any,
userInfo: any,
dispatch: any,
setPics: any,
setFlag: any
) => {
if (refs && refs.current) {
let msg: any[] = [];
let idx: number[] = [];
for (let i = 0; i < refs.current.childNodes.length; i++) {
let node = refs.current.childNodes[i];
console.log("node===", node.nodeType);
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.length) {
msg[i] = { type: 101, msg: node.textContent };
}
} else if (node.nodeName === "IMG") {
msg[i] = {
type: 102,
msg: node.getAttribute("title"),
src: node.getAttribute("src"),
};
idx.push(i);
}
}
console.log("msg=====", msg);
idx.push(refs.current.childNodes.length);
const mesg: any[] = [];
let i = 0;
idx.forEach((item: any, idx: number) => {
let msgs: string = "";
msg.slice(i, item).forEach((itm: any) => {
msgs += itm.msg;
});
if (msgs.length) {
mesg.push({ type: 101, msg: msgs });
}
if (msg[item]?.msg?.length) {
mesg.push(msg[item]);
}
i = item + 1;
});
console.log("mesg=====", mesg);
if (mesg.length) {
if (chatInfo?.userID) {
const img: any[] = mesg?.filter((item: any) => item.type === 102);
const text: any[] = mesg?.filter((item: any) => item.type === 101);
text?.forEach((item: any) => {
dispatch(
SendMsg({
type: item?.type,
recvID: chatInfo?.userID,
body: { msg: item?.msg },
})
);
dispatch(ChangeCount(false));
});
for (let i = 0; i < img.length; i++) {
const id: any = new Date().getTime();
const image = new Image();
image.src = img[i]?.src;
image.onload = function () {
const height = image.height;
const width = image.width;
const msg: any = {
id,
sendID: userInfo?.im_user_id,
recvID: chatInfo?.userID,
status: 1,
contentType: 102,
pictureElem: {
sourcePicture: { url: img[i]?.src, width, height },
},
sendTime: id,
};
dispatch(PushRecords(msg));
};
api.user.UploadImage(id, { image: pics[img[i]?.msg] }).then((res) => {
console.log(res);
const image = new Image();
image.src = res?.data?.data?.url;
image.onload = function () {
const height = image.height;
const width = image.width;
dispatch(ChangeCount(false));
dispatch(
SendMsg({
type: 102,
recvID: chatInfo?.userID,
body: {
type: 102,
picBaseInfo: {
type: pics[img[i]?.msg]?.type,
size: pics[img[i]?.msg]?.size,
url: res?.data?.data?.url,
width,
height,
},
url: res?.data?.data?.url,
},
conversationID: chatInfo?.conversationID || null,
})
);
};
});
}
setTimeout(() => {
refs.current.innerHTML = "";
setPics({});
}, 200);
}
} else {
setFlag(true);
}
}
};
5.react-redux reduxjs/toolkit 实现同步异步方法 (读取initialState中变量的值JSON.parse(JSON.stringify(state)))
import { createSlice, PayloadAction, createAsyncThunk } from "@reduxjs/toolkit";
import { getSDK, CbEvents } from "open-im-sdk-wasm";
import store from "../store";
import { ChangeCount } from "./ListSlice";
const IMSDK = getSDK();
type initialStateType = {
userInfo: any;
// app 通道id
apps: any[];
channels_id: string;
staff_avatar: string;
connectState: number | null;
local: string;
// 未读消息徽标
badge: any;
// group
selected: string;
// chatRight
orderSel: string;
// record
recordSel: string;
// 放刷新聊条记录消失
record: any[];
// 当前聊天对象基础信息
chatInfo: any;
// 登录状态
loginState: boolean;
// 滚动获取聊天记录参数
startClientMsgID: string;
lastMinSeq: number;
// 页面加载状态
loading: boolean;
// chat loading
chatLoading: boolean;
};
const initialState: initialStateType = {
userInfo: {},
channels_id: "all",
staff_avatar: "",
connectState: 1,
local: "en-us",
badge: {},
selected: "group1",
orderSel: "order",
recordSel: "record",
record: [],
chatInfo: {},
loginState: false,
startClientMsgID: "",
lastMinSeq: 0,
apps: [],
loading: true,
chatLoading: false,
};
export const UserInfoLice = createSlice({
name: "UserInfoLice",
initialState: initialState,
reducers: {
setUserInfo: (state, action: PayloadAction<any>) => {
state.userInfo = action.payload;
},
ClearDate: (state, action: PayloadAction<boolean>) => {
if (action.payload) {
state.userInfo = {};
state.channels_id = "all";
state.staff_avatar = "";
state.connectState = 1;
state.local = "en-us";
state.badge = {};
state.selected = "group1";
state.orderSel = "order";
state.recordSel = "record";
state.record = [];
state.chatInfo = {};
state.loginState = false;
state.startClientMsgID = "";
state.lastMinSeq = 0;
state.apps = [];
state.loading = true;
state.chatLoading = false;
}
},
changeChannelsId: (state, action: PayloadAction<string>) => {
state.channels_id = action.payload;
},
setStaffAvatar: (state, action: PayloadAction<string>) => {
state.staff_avatar = action.payload;
},
setConnectStates: (state, action: PayloadAction<number | null>) => {
state.connectState = action.payload;
},
changeLocal: (state, action: PayloadAction<string>) => {
state.local = action.payload;
},
ChangeBadge: (state, action: PayloadAction<any>) => {
state.badge = action.payload;
},
ChangeSelected: (state, action: PayloadAction<string>) => {
state.selected = action.payload;
},
ChangeOrder: (state, action: PayloadAction<string>) => {
state.orderSel = action.payload;
},
ChangeRecord: (state, action: PayloadAction<string>) => {
state.recordSel = action.payload;
},
ChangeRecords: (state, action: PayloadAction<any[]>) => {
state.record = action.payload;
},
PushRecords:(state,action: PayloadAction<any>) => {
if(action.payload){
const arr = [...state.record];
arr.push(action.payload);
state.record = [...arr];
}
},
ChangeChatInfo: (state, action: PayloadAction<any>) => {
state.chatInfo = action.payload;
},
ShuaData: (state, action: PayloadAction<boolean>) => {
if (action.payload) {
state.record = [];
state.chatInfo = {};
state.apps = [];
}
},
ChangeConnectLoginState: (state, action: PayloadAction<boolean>) => {
state.loginState = action.payload;
},
ChangeStartClientMsgID: (state, action: PayloadAction<string>) => {
state.startClientMsgID = action.payload;
},
ChangeLastMinSeq: (state, action: PayloadAction<number>) => {
state.lastMinSeq = action.payload;
},
ChangeApps: (state, action: PayloadAction<any[]>) => {
state.apps = action.payload;
},
ChangeLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
ChangeChatLoading: (state, action: PayloadAction<boolean>) => {
state.chatLoading = action.payload;
},
},
extraReducers:(builder)=>{
builder.addCase(SendMsg.pending,(state)=>{});
builder.addCase(SendMsg.fulfilled,(state,action:PayloadAction<any>)=>{
const msg:any = action?.payload?.msg;
if (msg?.contentType === 102) {
const list:any[] = [...state.record];
const obj: any = { ...msg };
const id = Number(
obj?.pictureElem?.sourcePicture?.url
?.split("/")
[
obj?.pictureElem?.sourcePicture?.url?.split("/").length - 1
]?.split(".")[0]
);
const arr: any = [...list];
let idx: number | null = null;
arr?.forEach((item: any, index: number) => {
if (Number(item?.id) === id) {
console.log("msg=====", obj, arr, item);
idx = index;
// item = {...obj}
// item.status = obj?.status;
// item.pictureElem = obj?.pictureElem;
// item.sendTime = obj?.sendTime;
}
});
if (idx) {
arr[idx] = { ...obj };
console.log("arrs===", arr);
state.record = arr;
// dispatch(ChangeRecords(arr));
// dispatch(ChangeCount(false));
// store.dispatch(ChangeCount(false));
}
} else {
const list:any[] = state.record;
const arr: any = [...list];
console.log("==arr", arr);
arr.push(msg);
state.record = arr;
// dispatch(ChangeRecords(arr));
// dispatch(ChangeCount(false));
// store.dispatch(ChangeCount(false))
}
});
builder.addCase(SendMsg.rejected,(state,action)=>{
console.log(action.error.message);
})
}
});
// 导出actions
export const {
setUserInfo,
changeChannelsId,
ClearDate,
setStaffAvatar,
setConnectStates,
changeLocal,
ChangeBadge,
ChangeSelected,
ChangeOrder,
ChangeRecord,
ChangeRecords,
ChangeChatInfo,
ShuaData,
ChangeConnectLoginState,
ChangeLastMinSeq,
ChangeStartClientMsgID,
ChangeApps,
ChangeLoading,
ChangeChatLoading,
PushRecords,
} = UserInfoLice.actions;
// 创建异步方法修改聊天记录
export const SendMsg = createAsyncThunk("sendMsg", async (dat: any) => {
if (dat && Number(dat?.type) === 101) {
const { data: message } = await IMSDK.createTextMessage(dat?.body?.msg);
const res = await IMSDK.sendMessage({
recvID: dat?.recvID,
groupID: "",
message,
});
console.log('fsdafd00===',res)
return {msg:res?.data};
}else if(dat&&Number(dat?.type)===102){
if(dat?.conversationID){
const { data: messageList } = await IMSDK.getAdvancedHistoryMessageList({
lastMinSeq:0,
count:20,
startClientMsgID:"",
conversationID:dat.conversationID
})
const { data: message } = await IMSDK.createImageMessageByURL({
sourcePicture: dat?.body?.picBaseInfo,
bigPicture: dat?.body?.picBaseInfo,
snapshotPicture: dat?.body?.picBaseInfo,
sourcePath: dat?.body?.url,
});
const res = await IMSDK.sendMessageNotOss({
recvID:dat?.recvID,
groupID: "",
message,
})
console.log('fsdafd00===',messageList)
return {msg:res?.data,record:[...messageList.messageList],newMsg:dat?.newMsg};
}
}
});
// 导出reducer,在创建store中使用
export default UserInfoLice.reducer;
6.emoji普通表情和国旗表情转换成图片 .
export const convertToUnicode = (str: string) => {
return str?.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, function (match: any) {
return `<img src=${
process.env.REACT_APP_EMOJI_API +
match.codePointAt(0).toString(16) +
".png"
} alt='' width=${16} height=${16}/>`;
});
};
// 识别超链接
export const translateHtml = (msg: string) => {
let re = /(http[s]?:\/\/([\w-]+.)+([:\d+])?(\/[\w-\.\/\?%&=]*)?)/gi;
let s = msg?.replace(re, function (a) {
return ' <a href="' + a + '" target=_blank class="recordA">' + a + "</a>";
});
return s;
};
// 识别emoji国旗
export const findEmojiFlags = (str: string) => {
return str?.replace(
/[\uD83C][\uDDE6-\uDDFF][\uD83C][\uDDE6-\uDDFF]/g,
function (match: any) {
let str2: string = match?.replace(
/[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
function (match2: any) {
return match2.codePointAt(0).toString(16) + "-";
}
);
str2 = str2
.split("-")
?.filter((item: any) => item?.length)
.join("-");
return `<img src=${
process.env.REACT_APP_EMOJI_API + str2 + ".png"
} alt='' width=${16} height=${16}/>`;
}
);
};
//举例
<div dangerouslySetInnerHTML={{__html: convertToUnicode(findEmojiFlags(translateHtml(item?.textElem?.content?.toString())?.toString()))}}></div>