This commit is contained in:
Your Name
2026-03-11 09:49:47 +08:00
parent 02ae537b4c
commit 38ad60f4bb
290 changed files with 36917 additions and 123 deletions
@@ -0,0 +1,149 @@
import { ref } from 'vue';
import type { Ref } from 'vue';
import { StoreName, TUIChatService, TUIStore } from '../chat-uikit-engine-lite';
import { MessageContentType } from './type';
import { convertInputContentToEditorNode } from './utils';
import type { InputContent } from './type';
/**
* Message Input Store
*
* This store manages the state and operations related to the chat message input, including:
* - Raw input value (text or structured content)
* - Editor instance management
* - Content manipulation (insertion, updating, etc.)
* - Message sending functionality
*/
interface MessageInputState {
inputRawValue: Ref<string | InputContent[]>;
isPeerTyping: Ref<boolean>;
}
interface MessageInputAction {
updateRawValue: (value: string | InputContent[]) => void;
setEditorInstance: (editor) => void;
setContent: (value: string | InputContent[]) => void;
insertContent: (value: string | InputContent[], focus?: boolean) => void;
focusEditor: () => void;
blurEditor: () => void;
sendMessage: (msg?: string | InputContent[]) => void;
}
const editor = ref(null);
const inputRawValue = ref<string | InputContent[]>('');
const isPeerTyping = ref(false);
/* =====================================================
* MessageInputActions begin
* ===================================================== */
const updateRawValue = (value: string | InputContent[]) => {
if (typeof value !== 'string' && !Array.isArray(value)) {
console.warn('Invalid input type for updateRawValue');
return;
}
if (typeof value === 'string') {
inputRawValue.value = value.trim();
} else {
inputRawValue.value = value?.length > 0 ? value : '';
}
TUIChatService.enterTypingState();
setTimeout(() => {
TUIChatService.leaveTypingState();
}, 3000);
};
const setEditorInstance = (instance) => {
if (editor.value) {
editor.value.destroy();
}
editor.value = instance;
};
const setContent = (content: string | InputContent[]) => {
if (!editor.value) {
return;
}
if (typeof content === 'string') {
editor.value.commands.setContent(content, true);
} else {
const editorContent = content.map(convertInputContentToEditorNode);
editor.value.commands.setContent(editorContent, true);
}
editor.value.commands.focus();
};
const insertContent = (content: string | InputContent[], focus = true) => {
if (!editor.value) {
return;
}
if (typeof content === 'string') {
editor.value.commands.insertContent(content);
} else {
const editorContent = content.map(convertInputContentToEditorNode);
editor.value.commands.insertContent(editorContent);
}
if (focus) {
editor.value.commands.focus();
}
};
const focusEditor = () => {
editor.value?.commands.focus();
};
const blurEditor = () => {
editor.value?.commands.blur();
};
const sendMessage = async (options) => {
const { type, content } = options
if (type === MessageContentType.TEXT) {
await TUIChatService.sendTextMessage({
payload: { text: content },
})
}
if (type === MessageContentType.IMAGE) {
await TUIChatService.sendImageMessage({
payload: { file: content },
});
}
if (type === MessageContentType.VIDEO) {
await TUIChatService.sendVideoMessage({
payload: { file: content },
});
}
};
/* =====================================================
* MessageInputActions end
* ===================================================== */
function useMessageInputState(): MessageInputState & MessageInputAction {
return {
inputRawValue,
isPeerTyping,
updateRawValue,
setEditorInstance,
setContent,
insertContent,
focusEditor,
blurEditor,
sendMessage,
};
}
function initWatcher() {
TUIStore.watch(StoreName.CHAT, {
typingStatus: (typingStatus) => {
isPeerTyping.value = typingStatus;
},
});
}
initWatcher();
export { useMessageInputState, MessageContentType };
export type { InputContent };
@@ -0,0 +1 @@
export * from './MessageInputState';
@@ -0,0 +1,38 @@
enum MessageContentType {
TEXT = 'text',
IMAGE = 'image',
VIDEO = 'video',
FILE = 'file',
MENTION = 'mention',
EMOJI = 'emoji',
}
type ContentTypeMap = {
[key in MessageContentType]: key extends MessageContentType.TEXT
? string
: key extends MessageContentType.IMAGE
? File
: key extends MessageContentType.VIDEO
? File
: key extends MessageContentType.FILE
? File
: key extends MessageContentType.MENTION
? string[]
: key extends MessageContentType.EMOJI
? { url: string; key: string; text: string }
: never;
};
interface InputContent<T extends MessageContentType = MessageContentType> {
type: T;
content: ContentTypeMap[T];
}
export {
MessageContentType,
};
export type {
ContentTypeMap,
InputContent,
};
@@ -0,0 +1,45 @@
import { MessageContentType } from './type';
import type { InputContent } from './type';
function convertInputContentToEditorNode(item: InputContent) {
switch (item.type) {
case MessageContentType.TEXT:
return {
type: 'text',
text: item.content,
};
case MessageContentType.IMAGE: {
const imageFile = item.content as File;
const imageUrl = URL.createObjectURL(imageFile);
return {
type: MessageContentType.IMAGE,
attrs: {
src: imageUrl,
alt: imageFile?.name,
fileData: imageFile,
title: imageFile?.name,
},
};
}
case MessageContentType.EMOJI: {
const emoticonContent = item.content as { url: string; key: string; text: string };
return {
type: MessageContentType.EMOJI,
attrs: {
src: emoticonContent.url,
alt: emoticonContent.key,
title: emoticonContent.text,
},
};
}
default:
return {
type: 'text',
text: String(item.content),
};
}
}
export {
convertInputContentToEditorNode,
};