Commit 474ad190 by Sixong.Zhu

cs

parent 9e5832f9
......@@ -7,184 +7,193 @@
/>
</template>
<script lang="ts">
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import ChatInput, {
FILE_INFO_CLASS,
isImageOrFile,
} from "../hybrid-input/index.vue";
import { Message, MessageType } from "../model";
import { uploadFile } from "../service/upload";
import { ChatLoggerService } from "../xim/logger";
import xim from "../xim/xim";
import { ChatStore, chatStore } from "@/customer-service/store/model";
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import ChatInput, {
FILE_INFO_CLASS,
isImageOrFile,
} from "../hybrid-input/index.vue";
import { Message, MessageType } from "../model";
import { uploadFile } from "../service/upload";
import xim from "../xim/xim";
import { ChatStore, chatStore } from "@/customer-service/store/model";
let sendingMessageIndex = 1;
let sendingMessageIndex = 1;
@Component({ components: { ChatInput } })
export default class MessageInput extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_DIALOG_VISIBLE)
private readonly chatRoomVisible!: ChatStore.STATE_CHAT_DIALOG_VISIBLE;
@Component({ components: { ChatInput } })
export default class MessageInput extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_DIALOG_VISIBLE)
private readonly chatRoomVisible!: ChatStore.STATE_CHAT_DIALOG_VISIBLE;
@chatStore.Action(ChatStore.ACTION_SEND_MESSAGE)
private readonly sendMsg!: ChatStore.ACTION_SEND_MESSAGE;
@chatStore.Action(ChatStore.ACTION_SEND_MESSAGE)
private readonly sendMsg!: ChatStore.ACTION_SEND_MESSAGE;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.State(ChatStore.STATE_CHAT_MY_ID)
private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID;
@chatStore.State(ChatStore.STATE_CHAT_MY_ID)
private readonly chatMyId!: ChatStore.STATE_CHAT_MY_ID;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_INITING)
private readonly chatIniting!: ChatStore.STATE_CURRENT_CHAT_INITING;
@chatStore.State(ChatStore.STATE_CURRENT_CHAT_INITING)
private readonly chatIniting!: ChatStore.STATE_CURRENT_CHAT_INITING;
@chatStore.Getter(ChatStore.STATE_CHAT_SOURCE)
private readonly source!: ChatStore.STATE_CHAT_SOURCE;
@chatStore.Getter(ChatStore.STATE_CHAT_SOURCE)
private readonly source!: ChatStore.STATE_CHAT_SOURCE;
@chatStore.Mutation(ChatStore.MUTATION_APPEND_SENDING_MESSAGE)
private readonly appendSendingMessages!: ChatStore.MUTATION_APPEND_SENDING_MESSAGE;
@chatStore.Mutation(ChatStore.MUTATION_APPEND_SENDING_MESSAGE)
private readonly appendSendingMessages!: ChatStore.MUTATION_APPEND_SENDING_MESSAGE;
@chatStore.Mutation(ChatStore.MUTATION_FAILED_SENDING_MESSAGE)
private readonly failedSendingMessage!: ChatStore.MUTATION_FAILED_SENDING_MESSAGE;
@chatStore.Mutation(ChatStore.MUTATION_FAILED_SENDING_MESSAGE)
private readonly failedSendingMessage!: ChatStore.MUTATION_FAILED_SENDING_MESSAGE;
@chatStore.Mutation(ChatStore.MUTATION_REMOVE_SENDING_MESSAGE)
private readonly removeSendingMessages!: ChatStore.MUTATION_REMOVE_SENDING_MESSAGE;
@chatStore.Mutation(ChatStore.MUTATION_REMOVE_SENDING_MESSAGE)
private readonly removeSendingMessages!: ChatStore.MUTATION_REMOVE_SENDING_MESSAGE;
@Ref("chat-input")
private readonly chatInput!: ChatInput;
@Ref("chat-input")
private readonly chatInput!: ChatInput;
@Watch("chatRoomVisible")
private whenChatRoomShow() {
if (!this.chatRoomVisible) return;
this.chatInput.focus();
}
private async sendMessage(msg: ChildNode[], done: () => void) {
if (this.chatIniting) {
return;
@Watch("chatRoomVisible")
private whenChatRoomShow() {
if (!this.chatRoomVisible) return;
this.chatInput.focus();
}
for (const item of msg) {
if (isImageOrFile(item)) {
if ((item as Element).classList.contains(FILE_INFO_CLASS)) {
await this.sendFile(item, MessageType.File);
} else {
await this.sendFile(item, MessageType.Image);
}
continue;
private async sendMessage(msg: ChildNode[]) {
if (this.chatIniting) {
return;
}
for (const item of msg) {
if (isImageOrFile(item)) {
if ((item as Element).classList.contains(FILE_INFO_CLASS)) {
await this.sendFile(item, MessageType.File).catch((e) =>
this.onError(e)
);
} else {
await this.sendFile(item, MessageType.Image).catch((e) =>
this.onError(e)
);
}
continue;
}
if (item.textContent) {
await this.sendText(item.textContent);
if (item.textContent) {
await this.sendText(item.textContent).catch((e) =>
this.onError(e)
);
}
}
this.$emit("sent");
}
ChatLoggerService.logger?.debug("all messages sent");
done();
this.$emit("sent");
}
private async onInput() {
if (this.chatId == null) return;
await xim.inputing(this.chatId);
}
private async sendText(text: string) {
if (text && text.trim()) {
const msg = { text: text.trim() };
if (this.source) {
Object.assign(msg, { source: this.source });
}
return this.sendMsg({ msgType: MessageType.Text, msg: JSON.stringify(msg) });
private async onInput() {
if (this.chatId == null) return;
await xim.inputing(this.chatId);
}
}
private async sendFile(file: any, type: MessageType.Image | MessageType.File) {
const src = JSON.parse(
file.attributes[`data-${type}`]?.value || ""
) as {
url: string;
name: string;
size: number;
};
if (src) {
const index = this.sendSendingMessage(type, src);
const file = await this.readBlobUrl2Base64(src.url, src.name);
if (file) {
let w = 0;
let h = 0;
if (type === MessageType.Image) {
const img = new Image();
img.src = src.url;
img.onload = function () {
w = img.naturalWidth;
h = img.naturalHeight;
};
img.remove();
private async sendText(text: string) {
if (text && text.trim()) {
const msg = { text: text.trim() };
if (this.source) {
Object.assign(msg, { source: this.source });
}
uploadFile(file)
.then((r) => {
if (r) {
const msg = {
url: r,
name: file.name,
size: file.size,
};
if (this.source) {
Object.assign(msg, { source: this.source });
}
return this.sendMsg({
msgType: MessageType.Text,
msg: JSON.stringify(msg),
});
}
}
if (w && h) {
Object.assign(msg, { w, h });
private async sendFile(
file: any,
type: MessageType.Image | MessageType.File
) {
const src = JSON.parse(
file.attributes[`data-${type}`]?.value || ""
) as {
url: string;
name: string;
size: number;
};
if (src) {
const index = this.sendSendingMessage(type, src);
const file = await this.readBlobUrl2Base64(src.url, src.name);
if (file) {
let w = 0;
let h = 0;
if (type === MessageType.Image) {
const img = new Image();
img.src = src.url;
img.onload = function () {
w = img.naturalWidth;
h = img.naturalHeight;
};
img.remove();
}
uploadFile(file)
.then((r) => {
if (r) {
const msg = {
url: r,
name: file.name,
size: file.size,
};
if (this.source) {
Object.assign(msg, { source: this.source });
}
if (w && h) {
Object.assign(msg, { w, h });
}
this.sendMsg({
msgType: type,
msg: JSON.stringify(msg),
});
this.removeSendingMessages(index);
URL.revokeObjectURL(src.url);
} else {
this.setMsg2Failed(index);
}
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.sendMsg({
msgType: type,
msg: JSON.stringify(msg),
});
this.removeSendingMessages(index);
URL.revokeObjectURL(src.url);
} else {
this.setMsg2Failed(index);
}
})
.catch((e) => {
// eslint-disable-next-line no-console
console.error(e);
this.setMsg2Failed(index);
});
});
}
}
}
}
private setMsg2Failed(index: number) {
this.failedSendingMessage(index);
}
private sendSendingMessage(type: string, msg: any) {
const index = sendingMessageIndex++;
if (this.source) {
Object.assign(msg, { source: this.source, eid: this.chatMyId });
private setMsg2Failed(index: number) {
this.failedSendingMessage(index);
}
if (this.chatId) {
this.appendSendingMessages({
id: -index,
chat_id: this.chatId,
ts: Date.now(),
type,
msg: JSON.stringify(msg),
} as Message);
return -index;
private sendSendingMessage(type: string, msg: any) {
const index = sendingMessageIndex++;
if (this.source) {
Object.assign(msg, { source: this.source, eid: this.chatMyId });
}
if (this.chatId) {
this.appendSendingMessages({
id: -index,
chat_id: this.chatId,
ts: Date.now(),
type,
msg: JSON.stringify(msg),
} as Message);
return -index;
}
return 0;
}
return 0;
}
private readBlobUrl2Base64(url: string, name: string) {
return fetch(url)
.then((r) => r.blob())
.then((blob) => new File([blob], name));
}
private readBlobUrl2Base64(url: string, name: string) {
return fetch(url)
.then((r) => r.blob())
.then((blob) => new File([blob], name));
}
private onError(e: any) {
this.$emit("error", e.message || e);
private onError(e: any) {
this.$emit("error", e.message || e);
}
}
}
</script>
......@@ -60,614 +60,605 @@
</template>
<script lang="ts">
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import { namespace } from "vuex-class";
import {
getFileType,
getSvg,
MAX_FILE_SIZE,
MAX_FILE_SIZE_STRING,
MAX_IMAGE_SIZE,
MAX_IMAGE_SIZE_STRING,
MESSAGE_FILE_EMPTY,
MESSAGE_FILE_TOO_LARGE,
MESSAGE_IMAGE_TOO_LARGE,
} from "../components/message-item/file-controller";
import { EmojiItem, EmojiService } from "../service/emoji";
import { ChatStore } from "../store/model";
import { formatFileSize } from "../utils";
export const enum InputMessageType {
Text = "text",
Image = "image",
File = "file",
}
export interface InputMessageBody {
text?: string;
url?: string;
name?: string;
size?: number;
}
export interface InputMessage {
type: InputMessageType;
body: InputMessageBody;
file?: File | null;
}
const chatStore = namespace("chatStore");
const chatCache: { [key: number]: any } = {};
export const IMAGE_INFO_CLASS = "img-info";
export const FILE_INFO_CLASS = "file-info";
export function isImageOrFile(node: ChildNode) {
const e = node as HTMLElement;
return (
e.classList &&
(e.classList.contains(IMAGE_INFO_CLASS) ||
e.classList.contains(FILE_INFO_CLASS))
);
}
@Component({ components: {} })
export default class Input extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.Action(ChatStore.ACTION_GET_MY_CHAT_LIST)
protected readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST;
@Ref("input")
private readonly messageInputBox!: HTMLDivElement;
private file = "";
private acceptType = "image/*";
private emojiPanelVisibility = false;
private tip4Image = `发送图片(最大${MAX_IMAGE_SIZE_STRING})`;
private tip4File = `发送文件(最大${MAX_FILE_SIZE_STRING})`;
private emoji: EmojiItem[] = [];
@Watch("chatId")
private onChatIdChanged(v: number, old: number) {
if (old) {
const current = this.getNodeListFromInputBox();
if (current && current.length) {
chatCache[old] = current;
import { Component, Ref, Vue, Watch } from "vue-property-decorator";
import { namespace } from "vuex-class";
import {
getFileType,
getSvg,
MAX_FILE_SIZE,
MAX_FILE_SIZE_STRING,
MAX_IMAGE_SIZE,
MAX_IMAGE_SIZE_STRING,
MESSAGE_FILE_EMPTY,
MESSAGE_FILE_TOO_LARGE,
MESSAGE_IMAGE_TOO_LARGE,
} from "../components/message-item/file-controller";
import { EmojiItem, EmojiService } from "../service/emoji";
import { ChatStore } from "../store/model";
import { formatFileSize } from "../utils";
export const enum InputMessageType {
Text = "text",
Image = "image",
File = "file",
}
export interface InputMessageBody {
text?: string;
url?: string;
name?: string;
size?: number;
}
export interface InputMessage {
type: InputMessageType;
body: InputMessageBody;
file?: File | null;
}
const chatStore = namespace("chatStore");
const chatCache: { [key: number]: any } = {};
export const IMAGE_INFO_CLASS = "img-info";
export const FILE_INFO_CLASS = "file-info";
export function isImageOrFile(node: ChildNode) {
const e = node as HTMLElement;
return (
e.classList &&
(e.classList.contains(IMAGE_INFO_CLASS) ||
e.classList.contains(FILE_INFO_CLASS))
);
}
@Component({ components: {} })
export default class Input extends Vue {
@chatStore.State(ChatStore.STATE_CHAT_CURRENT_CHAT_ID)
private readonly chatId!: ChatStore.STATE_CHAT_CURRENT_CHAT_ID;
@chatStore.Action(ChatStore.ACTION_GET_MY_CHAT_LIST)
protected readonly getMyChatList!: ChatStore.ACTION_GET_MY_CHAT_LIST;
@Ref("input")
private readonly messageInputBox!: HTMLDivElement;
private file = "";
private acceptType = "image/*";
private emojiPanelVisibility = false;
private tip4Image = `发送图片(最大${MAX_IMAGE_SIZE_STRING})`;
private tip4File = `发送文件(最大${MAX_FILE_SIZE_STRING})`;
private emoji: EmojiItem[] = [];
@Watch("chatId")
private onChatIdChanged(v: number, old: number) {
if (old) {
const current = this.getNodeListFromInputBox();
if (current && current.length) {
chatCache[old] = current;
}
}
}
this.clearInput();
if (v) {
const cache = chatCache[v];
if (cache) {
const e = document.querySelector(
"#chat-input-box"
) as HTMLElement;
if (e) {
for (const node of cache as ChildNode[]) {
e.appendChild(node);
this.clearInput();
if (v) {
const cache = chatCache[v];
if (cache) {
const e = document.querySelector(
"#chat-input-box"
) as HTMLElement;
if (e) {
for (const node of cache as ChildNode[]) {
e.appendChild(node);
}
}
}
}
}
}
public mounted() {
this.messageInputBox.addEventListener("paste", this.handlePasteEvent);
document.addEventListener("click", this.hideEmoji);
document.addEventListener("selectionchange", this.handleSaveRange);
this.setupEmoji();
this.focus();
}
public mounted() {
this.messageInputBox.addEventListener("paste", this.handlePasteEvent);
document.addEventListener("click", this.hideEmoji);
document.addEventListener("selectionchange", this.handleSaveRange);
this.setupEmoji();
this.focus();
}
public beforeDestroy() {
document.removeEventListener("click", this.hideEmoji);
document.removeEventListener("selectionchange", this.handleSaveRange);
}
public beforeDestroy() {
document.removeEventListener("click", this.hideEmoji);
document.removeEventListener("selectionchange", this.handleSaveRange);
}
public focus() {
this.messageInputBox.focus();
}
public focus() {
this.messageInputBox.focus();
}
private clearInput() {
this.messageInputBox.innerHTML = "";
const input = document.getElementById(
"chat-upload-file"
) as HTMLInputElement;
if (input) {
input.value = "";
private clearInput() {
this.messageInputBox.innerHTML = "";
const input = document.getElementById(
"chat-upload-file"
) as HTMLInputElement;
if (input) {
input.value = "";
}
}
}
private allowLoadImg() {
this.acceptType = "image/*";
}
private allowLoadImg() {
this.acceptType = "image/*";
}
private allowLoadFile() {
this.acceptType = "*";
}
private allowLoadFile() {
this.acceptType = "*";
}
private async handlePasteEvent(event: ClipboardEvent) {
/*
* 组织默认行为原因
* 1、浏览器自带复制粘贴图片到输入框的功能,与js加工后的图片重复了,
* 2、默认复制粘贴功能会粘贴dom结构
* */
event.preventDefault();
const items = event.clipboardData && event.clipboardData.items;
let html = "";
const promiseArr = [];
if (items && items.length) {
// 检索剪切板items中类型带有image的
for (let i = 0; i < items.length; i++) {
if (items[i].kind === "file") {
const file = items[i].getAsFile();
if (file) {
if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY);
return;
}
if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
private async handlePasteEvent(event: ClipboardEvent) {
/*
* 组织默认行为原因
* 1、浏览器自带复制粘贴图片到输入框的功能,与js加工后的图片重复了,
* 2、默认复制粘贴功能会粘贴dom结构
* */
event.preventDefault();
const items = event.clipboardData && event.clipboardData.items;
let html = "";
const promiseArr = [];
if (items && items.length) {
// 检索剪切板items中类型带有image的
for (let i = 0; i < items.length; i++) {
if (items[i].kind === "file") {
const file = items[i].getAsFile();
if (file) {
if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY);
return;
}
html += this.buildImageHtml(file);
} else {
if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
return;
}
html += this.buildImageHtml(file);
} else {
if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
}
html += this.buildFileHtml(file);
}
html += this.buildFileHtml(file);
break;
}
break;
}
} else {
promiseArr.push(
new Promise<void>((resolve) => {
const contentType = items[i].type;
items[i].getAsString((k) => {
/*
* items第一项是文本
* 第二项是带有dom结构的文本(包含格式)
* 第三项写明了数据是从哪里复制来的
*/
if (i === 0) {
if (contentType === "text/plain") {
html += k;
}
} else if (i === 1) {
const srcRegex =
/<img[^>]+src="([^">]+)"\s+title="([^">]+)"/g;
let result;
do {
result = srcRegex.exec(k);
if (result) {
const [, src, name] = result;
html += `<img tabindex="-1" src="${src}" data-image='${JSON.stringify(
{
url: src,
name,
}
)}'>`;
html = html.replace(name, "");
} else {
promiseArr.push(
new Promise<void>((resolve) => {
const contentType = items[i].type;
items[i].getAsString((k) => {
/*
* items第一项是文本
* 第二项是带有dom结构的文本(包含格式)
* 第三项写明了数据是从哪里复制来的
*/
if (i === 0) {
if (contentType === "text/plain") {
html += k;
}
} while (result);
}
resolve();
});
})
);
} else if (i === 1) {
const srcRegex =
/<img[^>]+src="([^">]+)"\s+title="([^">]+)"/g;
let result;
do {
result = srcRegex.exec(k);
if (result) {
const [, src, name] = result;
html += `<img tabindex="-1" src="${src}" data-image='${JSON.stringify(
{
url: src,
name,
}
)}'>`;
html = html.replace(name, "");
}
} while (result);
}
resolve();
});
})
);
}
}
}
await Promise.all(promiseArr);
if (html) {
this.insertHtmlAtCaret(html);
}
}
await Promise.all(promiseArr);
if (html) {
this.insertHtmlAtCaret(html);
}
}
private handleReturn(e: KeyboardEvent) {
if (e.altKey || e.ctrlKey) {
const el = this.messageInputBox;
const range = document.createRange();
const sel = window.getSelection();
const offset = sel!.focusOffset;
const content = el.innerHTML;
el.innerHTML =
content.slice(0, offset) +
"\n" +
(content.slice(offset) || "\n");
range.setStart(el.childNodes[0], offset + 1);
range.collapse(true);
sel!.removeAllRanges();
sel!.addRange(range);
return;
private handleReturn(e: KeyboardEvent) {
if (e.altKey || e.ctrlKey) {
const el = this.messageInputBox;
const range = document.createRange();
const sel = window.getSelection();
const offset = sel!.focusOffset;
const content = el.innerHTML;
el.innerHTML =
content.slice(0, offset) +
"\n" +
(content.slice(offset) || "\n");
range.setStart(el.childNodes[0], offset + 1);
range.collapse(true);
sel!.removeAllRanges();
sel!.addRange(range);
return;
}
if (e.shiftKey) {
e.preventDefault();
}
}
if (e.shiftKey) {
private async handleSendMsg(e: KeyboardEvent) {
e.preventDefault();
if (e.shiftKey || e.ctrlKey || e.altKey) {
return;
}
const data = this.getNodeListFromInputBox();
this.$emit("send", data);
this.clearInput();
if (this.chatId) {
chatCache[this.chatId] = [];
}
setTimeout(() => this.getMyChatList(), 120);
}
}
private async handleSendMsg(e: KeyboardEvent) {
e.preventDefault();
if (e.shiftKey || e.ctrlKey || e.altKey) {
return;
/**
* 获取输入框中的内容
* @returns 返回的是节点数组
*/
private getNodeListFromInputBox() {
this.messageInputBox.normalize();
const nodes = Array.from(this.messageInputBox.childNodes);
return this.combine(nodes);
}
return new Promise((resolve, reject) => {
try {
const data = this.getNodeListFromInputBox();
this.$emit("send", data, resolve);
} catch (e) {
this.$emit("error", e);
reject(e);
}
})
.then(() => {
this.clearInput();
if (this.chatId) {
chatCache[this.chatId] = [];
}
})
.finally(() => setTimeout(() => this.getMyChatList(), 120));
}
/**
* 获取输入框中的内容
* @returns 返回的是节点数组
*/
private getNodeListFromInputBox() {
this.messageInputBox.normalize();
const nodes = Array.from(this.messageInputBox.childNodes);
return this.combine(nodes);
}
/**
* 文本,链接等需要合并成纯文本发送
*/
private combine(nodes: ChildNode[]) {
const sendingNodes: ChildNode[] = [];
let needCreateNewNode = false;
let text = "";
for (const item of nodes) {
if (!isImageOrFile(item) && item.textContent) {
if (needCreateNewNode) {
text = "";
needCreateNewNode = false;
}
text += item.textContent;
} else {
needCreateNewNode = true;
if (text) {
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
/**
* 文本,链接等需要合并成纯文本发送
*/
private combine(nodes: ChildNode[]) {
const sendingNodes: ChildNode[] = [];
let needCreateNewNode = false;
let text = "";
for (const item of nodes) {
if (!isImageOrFile(item) && item.textContent) {
if (needCreateNewNode) {
text = "";
needCreateNewNode = false;
}
text += item.textContent;
} else {
needCreateNewNode = true;
if (text) {
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
}
sendingNodes.push(item);
}
sendingNodes.push(item);
}
}
if (text) {
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
}
if (text) {
this.checkTextLength(text);
const node = document.createTextNode(text);
sendingNodes.push(node);
}
return sendingNodes;
}
return sendingNodes;
}
private checkTextLength(text: string) {
if (text.length >= 4000) {
throw new Error("消息不能超过4000个字");
private checkTextLength(text: string) {
if (text.length >= 4000) {
throw new Error("消息不能超过4000个字");
}
}
}
private handleSaveRange() {
const sel = window.getSelection();
private handleSaveRange() {
const sel = window.getSelection();
if (sel && sel.rangeCount) {
const range = sel.getRangeAt(0);
const oldRange = this.getRange && this.getRange();
if (
this.messageInputBox &&
this.messageInputBox.contains(range.endContainer)
) {
if (sel && sel.rangeCount) {
const range = sel.getRangeAt(0);
const oldRange = this.getRange && this.getRange();
if (
oldRange &&
range.collapsed &&
range.endContainer === oldRange.endContainer &&
range.endOffset === oldRange.endOffset
this.messageInputBox &&
this.messageInputBox.contains(range.endContainer)
) {
return;
}
if (
oldRange &&
range.collapsed &&
range.endContainer === oldRange.endContainer &&
range.endOffset === oldRange.endOffset
) {
return;
}
this.saveRange && this.saveRange(range);
this.saveRange && this.saveRange(range);
}
}
}
}
private saveRange?: (param: Range) => void;
private getRange?: () => Range | null;
/**
* 在光标处插入元素
*/
public insertHtmlAtCaret = (function () {
let range: Range | null = null;
return function (this: Input, html: string) {
if (this.saveRange == null) {
this.saveRange = (alternate) => (range = alternate);
}
if (this.getRange == null) {
this.getRange = () => range;
}
const sel = window.getSelection();
if (sel) {
if (range) {
range.deleteContents();
sel.removeAllRanges();
sel.addRange(range);
} else {
this.focus();
range = sel.getRangeAt(0);
document.execCommand("selectAll");
window.getSelection()?.collapseToEnd();
private saveRange?: (param: Range) => void;
private getRange?: () => Range | null;
/**
* 在光标处插入元素
*/
public insertHtmlAtCaret = (function () {
let range: Range | null = null;
return function (this: Input, html: string) {
if (this.saveRange == null) {
this.saveRange = (alternate) => (range = alternate);
}
if (this.getRange == null) {
this.getRange = () => range;
}
const sel = window.getSelection();
if (sel) {
if (range) {
range.deleteContents();
sel.removeAllRanges();
sel.addRange(range);
} else {
this.focus();
range = sel.getRangeAt(0);
document.execCommand("selectAll");
window.getSelection()?.collapseToEnd();
}
}
}
const el = document.createElement("div");
el.innerHTML = html;
const frag = document.createDocumentFragment();
let node = null;
let lastNode = null;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
range?.insertNode(frag);
// Preserve the selection
if (lastNode) {
if (range) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
const el = document.createElement("div");
el.innerHTML = html;
const frag = document.createDocumentFragment();
let node = null;
let lastNode = null;
while ((node = el.firstChild)) {
lastNode = frag.appendChild(node);
}
}
};
})();
range?.insertNode(frag);
// Preserve the selection
if (lastNode) {
if (range) {
range = range.cloneRange();
range.setStartAfter(lastNode);
range.collapse(true);
sel?.removeAllRanges();
sel?.addRange(range);
}
}
};
})();
private selectEmoji(emoji: any) {
this.insertHtmlAtCaret(emoji.emoji_chars);
this.hideEmoji();
}
private selectEmoji(emoji: any) {
this.insertHtmlAtCaret(emoji.emoji_chars);
this.hideEmoji();
}
private isImage(file: File) {
return file.type.startsWith("image");
}
private isImage(file: File) {
return file.type.startsWith("image");
}
private buildImageHtml(file: File) {
const url = URL.createObjectURL(file);
return `<img class="${IMAGE_INFO_CLASS}" tabindex="-1" src="${url}" data-image='${JSON.stringify(
{
url,
private buildImageHtml(file: File) {
const url = URL.createObjectURL(file);
return `<img class="${IMAGE_INFO_CLASS}" tabindex="-1" src="${url}" data-image='${JSON.stringify(
{
url,
name: file.name,
size: file.size,
}
)}'>`;
}
private buildFileHtml(file: File) {
const extension = file.name.split(".");
const type = getFileType(file.name);
return `<div class="${FILE_INFO_CLASS}" tabindex="-1" title="${
extension[extension.length - 1]
}" data-file='${JSON.stringify({
url: URL.createObjectURL(file),
name: file.name,
size: file.size,
}
)}'>`;
}
private buildFileHtml(file: File) {
const extension = file.name.split(".");
const type = getFileType(file.name);
return `<div class="${FILE_INFO_CLASS}" tabindex="-1" title="${
extension[extension.length - 1]
}" data-file='${JSON.stringify({
url: URL.createObjectURL(file),
name: file.name,
size: file.size,
})}'><div style="display: inline-block"><div class="file-name text-truncate text-nowrap">${
file.name
}</div><div class="file-size">${formatFileSize(
file.size
)}</div></div><span class="file-icon" title="${type}">${getSvg(
type
)}</span></div> `; // 注意</div>最后面有个空
}
})}'><div style="display: inline-block"><div class="file-name text-truncate text-nowrap">${
file.name
}</div><div class="file-size">${formatFileSize(
file.size
)}</div></div><span class="file-icon" title="${type}">${getSvg(
type
)}</span></div> `; // 注意</div>最后面有个空
}
private onChange(e: Event) {
const target = e.target as HTMLElement;
if (target) {
const input = document.getElementById(
target.id as string
) as HTMLInputElement;
if (input) {
const files = input.files;
if (files && files.length) {
let html = "";
for (let index = 0; index < files.length; index++) {
const file = files[index];
if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY);
return;
}
if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
private onChange(e: Event) {
const target = e.target as HTMLElement;
if (target) {
const input = document.getElementById(
target.id as string
) as HTMLInputElement;
if (input) {
const files = input.files;
if (files && files.length) {
let html = "";
for (let index = 0; index < files.length; index++) {
const file = files[index];
if (file.size <= 0) {
this.$emit("error", MESSAGE_FILE_EMPTY);
return;
}
html += this.buildImageHtml(file);
} else {
if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
if (this.isImage(file)) {
if (file.size >= MAX_IMAGE_SIZE) {
this.$emit("error", MESSAGE_IMAGE_TOO_LARGE);
return;
}
html += this.buildImageHtml(file);
} else {
if (file.size >= MAX_FILE_SIZE) {
this.$emit("error", MESSAGE_FILE_TOO_LARGE);
return;
}
html += this.buildFileHtml(file);
}
html += this.buildFileHtml(file);
}
}
this.insertHtmlAtCaret(html);
this.insertHtmlAtCaret(html);
}
}
}
}
}
private toggleEmoji() {
this.emojiPanelVisibility = !this.emojiPanelVisibility;
}
private toggleEmoji() {
this.emojiPanelVisibility = !this.emojiPanelVisibility;
}
private hideEmoji(e?: Event) {
if (e && e.target) {
const target = e.target as HTMLElement;
if (target.closest(".emoji-picker")) {
return;
private hideEmoji(e?: Event) {
if (e && e.target) {
const target = e.target as HTMLElement;
if (target.closest(".emoji-picker")) {
return;
}
}
this.emojiPanelVisibility = false;
}
this.emojiPanelVisibility = false;
}
private setupEmoji() {
EmojiService.onReady(() => {
const service = new EmojiService();
service.getEmoji().then((r) => {
if (r) {
this.emoji = r.list;
}
private setupEmoji() {
EmojiService.onReady(() => {
const service = new EmojiService();
service.getEmoji().then((r) => {
if (r) {
this.emoji = r.list;
}
});
});
});
}
}
}
</script>
<style lang="less" scoped>
.input-wrap {
position: relative;
padding-left: 20px;
.input-wrap {
position: relative;
padding-left: 20px;
/deep/.input-el-scrollbar.el-scrollbar {
height: calc(100% - 35px);
/deep/.input-el-scrollbar.el-scrollbar {
height: calc(100% - 35px);
> .el-scrollbar__wrap {
overflow-x: hidden;
}
> .el-scrollbar__wrap {
overflow-x: hidden;
}
> .el-scrollbar__wrap > .el-scrollbar__view {
min-height: 100%;
display: flex;
// 输入框
> .input-container {
width: 100%;
font-size: 14px;
padding: 10px 20px 20px 0;
outline: 0;
white-space: pre-wrap;
user-select: text;
&::selection {
background-color: #cce6fc;
}
> .el-scrollbar__wrap > .el-scrollbar__view {
min-height: 100%;
display: flex;
img {
max-width: 300px;
font-size: 50px;
vertical-align: bottom;
border: 1px solid transparent;
&:focus {
border-color: var(--main-color);
}
// 输入框
> .input-container {
width: 100%;
font-size: 14px;
padding: 10px 20px 20px 0;
outline: 0;
white-space: pre-wrap;
user-select: text;
&::selection {
background-color: #cce6fc;
}
}
.file-info {
padding: 10px;
border: 1px solid #c5d4e5;
border-radius: 4px;
display: inline-block;
margin-right: 10px;
-webkit-user-modify: read-only;
user-select: none;
.file-name {
color: #000;
font-size: 14px;
max-width: 130px;
img {
max-width: 300px;
font-size: 50px;
vertical-align: bottom;
border: 1px solid transparent;
&:focus {
border-color: var(--main-color);
}
&::selection {
background-color: #cce6fc;
}
}
.file-size {
margin-top: 10px;
.file-info {
padding: 10px;
border: 1px solid #c5d4e5;
border-radius: 4px;
display: inline-block;
margin-right: 10px;
-webkit-user-modify: read-only;
user-select: none;
.file-name {
color: #000;
font-size: 14px;
max-width: 130px;
}
.file-size {
margin-top: 10px;
}
}
}
}
}
}
.input-emoji {
position: absolute;
left: 0;
top: -225px;
outline: 0;
.input-emoji {
position: absolute;
left: 0;
top: -225px;
outline: 0;
}
}
}
.tool-bar {
padding-top: 10px;
user-select: none;
.tool-bar {
padding-top: 10px;
user-select: none;
.tool-bar-icon {
height: 16px;
width: 16px;
cursor: pointer;
}
.tool-bar-icon {
height: 16px;
width: 16px;
cursor: pointer;
}
.offset {
margin: 0 22px;
}
}
.emoji-picker {
position: absolute;
z-index: 2;
top: -232px;
left: -1px;
background-color: #fff;
padding: 20px;
padding-right: 0;
padding-bottom: 10px;
border: 1px solid #f0f0f0;
right: 45px;
overflow: hidden;
.el-scrollbar {
height: 200px;
.offset {
margin: 0 22px;
}
}
.emoji-item {
display: inline-flex;
cursor: pointer;
min-width: 35px;
min-height: 25px;
font-size: 20px;
vertical-align: top;
margin-top: 5px;
justify-content: center;
align-items: center;
.emoji-picker {
position: absolute;
z-index: 2;
top: -232px;
left: -1px;
background-color: #fff;
padding: 20px;
padding-right: 0;
padding-bottom: 10px;
border: 1px solid #f0f0f0;
right: 45px;
overflow: hidden;
.el-scrollbar {
height: 200px;
}
.emoji-item {
display: inline-flex;
cursor: pointer;
min-width: 35px;
min-height: 25px;
font-size: 20px;
vertical-align: top;
margin-top: 5px;
justify-content: center;
align-items: center;
}
}
}
#chat-upload-file {
visibility: hidden;
position: absolute;
}
#chat-upload-file {
visibility: hidden;
position: absolute;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment