feat: 会话窗口; +tailwindcss
parent
f2768df484
commit
e600eae98f
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"baseUrl": "./",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
@import 'tailwindcss/base';
|
||||||
|
@import 'tailwindcss/components';
|
||||||
|
@import 'tailwindcss/utilities';
|
@ -0,0 +1,97 @@
|
|||||||
|
import { webSocket } from 'rxjs/webSocket';
|
||||||
|
import { filter, buffer, map, tap } from 'rxjs/operators';
|
||||||
|
// import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
|
export class RealTimeAPI {
|
||||||
|
constructor(param) {
|
||||||
|
this.webSocket = webSocket(param);
|
||||||
|
}
|
||||||
|
|
||||||
|
getObservable() {
|
||||||
|
return this.webSocket;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
return this.webSocket.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMessage(messageHandler) {
|
||||||
|
this.subscribe(messageHandler, undefined, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
onError(errorHandler) {
|
||||||
|
this.subscribe(undefined, errorHandler, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCompletion(completionHandler) {
|
||||||
|
this.subscribe(undefined, undefined, completionHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribe(messageHandler, errorHandler, completionHandler) {
|
||||||
|
// this.getObservable().subscribe(
|
||||||
|
// messageHandler,
|
||||||
|
// errorHandler,
|
||||||
|
// completionHandler
|
||||||
|
// );
|
||||||
|
this.getObservable().subscribe({
|
||||||
|
next: messageHandler,
|
||||||
|
error: errorHandler,
|
||||||
|
complete: completionHandler,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(messageObject) {
|
||||||
|
this.webSocket.next(messageObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
getObservableFilteredByMessageType(messageType) {
|
||||||
|
return this.getObservable().pipe(filter((message) => message.msg === messageType));
|
||||||
|
}
|
||||||
|
|
||||||
|
getObservableFilteredByID(id) {
|
||||||
|
return this.getObservable().pipe(filter((message) => message.id === id));
|
||||||
|
}
|
||||||
|
|
||||||
|
connectToServer() {
|
||||||
|
this.sendMessage({
|
||||||
|
msg: 'connect',
|
||||||
|
version: '1',
|
||||||
|
support: ['1', 'pre2', 'pre1'],
|
||||||
|
});
|
||||||
|
return this.getObservableFilteredByMessageType('connected');
|
||||||
|
}
|
||||||
|
|
||||||
|
keepAlive() {
|
||||||
|
return this.getObservableFilteredByMessageType('ping').pipe(tap(() => this.sendMessage({ msg: 'pong' })));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
callMethod(method, ...params) {
|
||||||
|
let id = 'uuid()';
|
||||||
|
this.sendMessage({
|
||||||
|
msg: 'method',
|
||||||
|
method,
|
||||||
|
id,
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return this.getObservableFilteredByID(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSubscription(streamName, streamParam, addEvent) {
|
||||||
|
let id = 'uuid()';
|
||||||
|
let subscription = this.webSocket.multiplex(
|
||||||
|
() => ({
|
||||||
|
msg: 'sub',
|
||||||
|
id: id,
|
||||||
|
name: streamName,
|
||||||
|
params: [streamParam, addEvent],
|
||||||
|
}),
|
||||||
|
() => ({
|
||||||
|
msg: 'unsub',
|
||||||
|
id: id,
|
||||||
|
}),
|
||||||
|
(message) => typeof message.collection === 'string' && message.collection === streamName && message.fields.eventName === streamParam
|
||||||
|
);
|
||||||
|
return subscription;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
// websocketLib.js
|
||||||
|
import { WebSocketSubject } from 'rxjs/webSocket';
|
||||||
|
import { retryWhen, delay, take, catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
|
class WebSocketLib {
|
||||||
|
constructor(url, authToken, protocol) {
|
||||||
|
this.url = url;
|
||||||
|
this.authToken = authToken;
|
||||||
|
this.protocol = protocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this.socket$ = new WebSocketSubject({
|
||||||
|
url: this.url,
|
||||||
|
protocol: this.protocol, // Use protocol for message type
|
||||||
|
openObserver: {
|
||||||
|
next: () => {
|
||||||
|
console.log('Connection established');
|
||||||
|
// Send authentication message as soon as connection is established
|
||||||
|
this.socket$.next({ event: 'authenticate', token: this.authToken });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
closeObserver: {
|
||||||
|
next: () => {
|
||||||
|
console.log('Connection closed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.socket$
|
||||||
|
.pipe(
|
||||||
|
retryWhen(errors =>
|
||||||
|
errors.pipe(
|
||||||
|
delay(1000), // Retry connection every 1 second
|
||||||
|
take(10), // Maximum of 10 retries
|
||||||
|
catchError(error => new Error(`Failed to reconnect: ${error}`)) // Throw error after 10 failed retries
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribe(
|
||||||
|
msg => console.log('Received message:', msg),
|
||||||
|
err => console.error('Received error:', err),
|
||||||
|
() => console.log('Connection closed')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage(message) {
|
||||||
|
if (!this.socket$) {
|
||||||
|
console.error('Must connect to WebSocket server before sending a message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket$.next({ type: 'message', content: message });
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (!this.socket$) {
|
||||||
|
console.error('Must connect to WebSocket server before disconnecting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.socket$.complete();
|
||||||
|
this.socket$ = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WebSocketLib;
|
@ -0,0 +1,112 @@
|
|||||||
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import { RealTimeAPI } from '@/lib/realTimeAPI';
|
||||||
|
import { useGetJson } from '@/hooks/userFetch';
|
||||||
|
|
||||||
|
export const ConversationContext = createContext();
|
||||||
|
|
||||||
|
const API_HOST = 'http://127.0.0.1:4523/m2/3888351-0-default'; // local mock
|
||||||
|
const URL = {
|
||||||
|
conversationList: `${API_HOST}/142426823`,
|
||||||
|
templates: `${API_HOST}/142952738`,
|
||||||
|
};
|
||||||
|
// const WS_URL = 'ws://202.103.68.144:8888/whatever/';
|
||||||
|
const WS_URL = 'ws://202.103.68.157:8888/whatever/';
|
||||||
|
// let realtimeAPI = new RealTimeAPI({ url: URL, protocol: 'aaa' });
|
||||||
|
let realtimeAPI = new RealTimeAPI({ url: WS_URL, protocol: 'WhatsApp' });
|
||||||
|
|
||||||
|
export const useConversations = () => {
|
||||||
|
const [errors, setErrors] = useState([]);
|
||||||
|
const [messages, setMessages] = useState([]); // 页面上激活的对话
|
||||||
|
const [conversations, setConversations] = useState({}); // 所有对话
|
||||||
|
const [currentID, setCurrentID] = useState();
|
||||||
|
const [conversationsList, setConversationsList] = useState([]); // 对话列表
|
||||||
|
const [currentConversation, setCurrentConversation] = useState({
|
||||||
|
id: '', name: ''
|
||||||
|
});
|
||||||
|
const [templates, setTemplates] = useState([]);
|
||||||
|
|
||||||
|
const [url, setUrl] = useState(URL.conversationList);
|
||||||
|
const data = useGetJson(url);
|
||||||
|
const fetchConversations = () => {
|
||||||
|
setUrl(null); // reset url
|
||||||
|
setUrl(URL.conversationList);
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
setConversationsList(data);
|
||||||
|
if (data && data.length) {
|
||||||
|
switchConversation(data[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
|
||||||
|
const getTemplates = () => {
|
||||||
|
setUrl(null); // reset url
|
||||||
|
setUrl(URL.templates);
|
||||||
|
}
|
||||||
|
|
||||||
|
const switchConversation = (cc) => {
|
||||||
|
console.log('switch to ', cc.id, cc);
|
||||||
|
setCurrentID(cc.id);
|
||||||
|
setCurrentConversation(cc);
|
||||||
|
setMessages(conversations[cc.id] || []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const addError = (reason) => {
|
||||||
|
setErrors(prevErrors => [...prevErrors, { reason }]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const addMessageToConversations = (customerId, message) => {
|
||||||
|
setConversations((prevList) => ({
|
||||||
|
...prevList,
|
||||||
|
[customerId]: [...(prevList[customerId] || []), message],
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMessage = (message) => {
|
||||||
|
setMessages((prevMessages) => [...prevMessages, message]);
|
||||||
|
addMessageToConversations(currentConversation.id, message);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMessage = (data) => {
|
||||||
|
const { errmsg, result: msgObj } = data;
|
||||||
|
|
||||||
|
const msg = data.result;
|
||||||
|
if (!msg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof msg.type === 'string' && msg.type === 'error') {
|
||||||
|
addError('Error Connecting to Server');
|
||||||
|
}
|
||||||
|
addMessage({ ...msg.message, sender: 'other', id: Date.now().toString(16) });
|
||||||
|
};
|
||||||
|
|
||||||
|
const sendMessage = (msg) => {
|
||||||
|
const msgObj = {
|
||||||
|
type: 'message',
|
||||||
|
message: msg,
|
||||||
|
};
|
||||||
|
realtimeAPI.sendMessage(msgObj);
|
||||||
|
addMessage(msgObj.message);
|
||||||
|
// debug:
|
||||||
|
// const msgObjR = {
|
||||||
|
// type: 'message',
|
||||||
|
// message: { type: 'text', text: { body: 'Received: ' + msg.text.body,} },
|
||||||
|
// };
|
||||||
|
// addMessage({ ...msgObjR.message, sender: 'other', id: Date.now().toString(16) });
|
||||||
|
};
|
||||||
|
|
||||||
|
realtimeAPI.onError(addError.bind(null, 'Error'));
|
||||||
|
realtimeAPI.onMessage(handleMessage);
|
||||||
|
realtimeAPI.onCompletion(addError.bind(null, 'Not Connected to Server'));
|
||||||
|
realtimeAPI.keepAlive(); // Ping Server
|
||||||
|
|
||||||
|
return {
|
||||||
|
errors, messages, conversationsList, currentConversation, sendMessage,
|
||||||
|
fetchConversations, switchConversation,
|
||||||
|
templates, setTemplates, getTemplates,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useConversationContext = () => useContext(ConversationContext);
|
@ -0,0 +1,59 @@
|
|||||||
|
import { makeAutoObservable, runInAction, toJS } from 'mobx';
|
||||||
|
import { RealTimeAPI } from '@/lib/realTimeAPI';
|
||||||
|
|
||||||
|
const URL = 'ws://202.103.68.144:8888/whatever/';
|
||||||
|
let realtimeAPI = new RealTimeAPI({ url: URL, protocol: 'aaa' });
|
||||||
|
|
||||||
|
class Conversations {
|
||||||
|
constructor() {
|
||||||
|
makeAutoObservable(this, { rootStore: false });
|
||||||
|
// this.sendMessage = this.sendMessage.bind(this);
|
||||||
|
|
||||||
|
realtimeAPI.onError(this.addError.bind(this, 'Error'));
|
||||||
|
realtimeAPI.onMessage(this.handleMessage.bind(this));
|
||||||
|
realtimeAPI.onCompletion(this.addError.bind(this, 'Not Connected to Server'));
|
||||||
|
realtimeAPI.keepAlive(); // Ping Server
|
||||||
|
// realtimeAPI.onMessage().subscribe(message => {
|
||||||
|
// this.addMessage(message);
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
|
||||||
|
addError = (reason) => {
|
||||||
|
// this.errors.push({ reason });
|
||||||
|
this.errors = [...this.errors, { reason }];
|
||||||
|
}
|
||||||
|
|
||||||
|
addMessage = (message) => {
|
||||||
|
// this.messages.push(msg.message); // Mobx will not work
|
||||||
|
this.messages = [...this.messages, message];
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMessage = (data) => {
|
||||||
|
const msg = data.result;
|
||||||
|
if (!msg) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (typeof msg.type === 'string' && msg.type === 'error') {
|
||||||
|
this.addError('Error Connecting to Server');
|
||||||
|
}
|
||||||
|
// todo: handle message
|
||||||
|
runInAction(() => {
|
||||||
|
this.addMessage({ ...msg.message, sender: 'other', id: Date.now().toString(16), });
|
||||||
|
// console.log(toJS(this.messages), 'messages');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMessage = (msg) => {
|
||||||
|
const msgObj = {
|
||||||
|
type: 'message',
|
||||||
|
message: msg,
|
||||||
|
};
|
||||||
|
realtimeAPI.sendMessage(msgObj);
|
||||||
|
this.addMessage(msgObj.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = [];
|
||||||
|
messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Conversations;
|
@ -0,0 +1,12 @@
|
|||||||
|
import Chat from './Index';
|
||||||
|
// import 'antd/dist/reset.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<Chat />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
@ -0,0 +1,57 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { List, Input, Avatar, Button } from 'antd';
|
||||||
|
|
||||||
|
const messages = [
|
||||||
|
{
|
||||||
|
sender: 'John Doe',
|
||||||
|
message: 'Hey!',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
sender: 'Jane Doe',
|
||||||
|
message: 'Hi John!',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
const sendMessage = () => {
|
||||||
|
// Update messages with new message data
|
||||||
|
const newMessage = {
|
||||||
|
sender: 'You',
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
messages.push(newMessage);
|
||||||
|
setMessage('');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
<List
|
||||||
|
itemLayout="horizontal"
|
||||||
|
dataSource={messages}
|
||||||
|
renderItem={(message) => (
|
||||||
|
<List.Item>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={<Avatar icon="user" />}
|
||||||
|
title={message.sender}
|
||||||
|
description={message.message}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Input.Group compact>
|
||||||
|
<Input
|
||||||
|
placeholder="Type your message"
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button type="primary" onClick={sendMessage}>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</Input.Group>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
@ -0,0 +1,28 @@
|
|||||||
|
.chat-layout {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-sider {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-content {
|
||||||
|
padding: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0;
|
||||||
|
width: calc(100% - 300px);
|
||||||
|
display: flex;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-input .ant-input {
|
||||||
|
margin-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-button {
|
||||||
|
height: 40px;
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
// App.js
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { Layout, Menu } from 'antd';
|
||||||
|
// import './App.css';
|
||||||
|
|
||||||
|
const { Header, Sider, Content } = Layout;
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const channels = ['General', 'Random'];
|
||||||
|
const messages = [
|
||||||
|
{ user: 'User1', text: 'Hello!' },
|
||||||
|
{ user: 'User2', text: 'Hi!' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout style={{ minHeight: '100vh' }}>
|
||||||
|
<Sider width={200}>
|
||||||
|
<Menu mode="inline" style={{ height: '100%', borderRight: 0 }}>
|
||||||
|
{channels.map(channel => (
|
||||||
|
<Menu.Item key={channel}>{channel}</Menu.Item>
|
||||||
|
))}
|
||||||
|
</Menu>
|
||||||
|
</Sider>
|
||||||
|
<Layout>
|
||||||
|
<Header style={{ background: '#fff', padding: 0 }} />
|
||||||
|
<Content style={{ margin: '24px 16px 0', overflow: 'initial' }}>
|
||||||
|
<div style={{ padding: 24, background: '#fff', textAlign: 'center' }}>
|
||||||
|
{messages.map((message, index) => (
|
||||||
|
<p key={index}>
|
||||||
|
<strong>{message.user}</strong>: {message.text}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
@ -0,0 +1,44 @@
|
|||||||
|
import { Layout, Menu, List, Timeline, Input } from 'antd';
|
||||||
|
|
||||||
|
const { Sider, Content } = Layout;
|
||||||
|
function ChatApp() {
|
||||||
|
return (
|
||||||
|
<Layout>
|
||||||
|
<Sider theme="light" width={300}>
|
||||||
|
<Menu>
|
||||||
|
<Menu.Item>Unread</Menu.Item>
|
||||||
|
<Menu.Item>Mentions</Menu.Item>
|
||||||
|
<Menu.Item>Favorites</Menu.Item>
|
||||||
|
<Menu.Item>Channel List</Menu.Item>
|
||||||
|
<Menu.Item>Direct Messages</Menu.Item>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<List>
|
||||||
|
{/* Show channels and DMs */}
|
||||||
|
</List>
|
||||||
|
</Sider>
|
||||||
|
|
||||||
|
<Content>
|
||||||
|
<Layout>
|
||||||
|
<Sider theme="light">
|
||||||
|
<List>
|
||||||
|
{/* Show user profile cards */}
|
||||||
|
</List>
|
||||||
|
</Sider>
|
||||||
|
|
||||||
|
<Content>
|
||||||
|
<Timeline>
|
||||||
|
{/* Show messages */}
|
||||||
|
</Timeline>
|
||||||
|
|
||||||
|
<Input.Search
|
||||||
|
enterButton="Send"
|
||||||
|
/>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChatApp;
|
@ -0,0 +1,82 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Layout, List, Avatar, Flex, Typography } from 'antd';
|
||||||
|
import Messages from './Components/Messages';
|
||||||
|
import InputBox from './Components/InputBox';
|
||||||
|
import ConversationsList from './Components/ConversationsList';
|
||||||
|
import CustomerProfile from './Components/CustomerProfile';
|
||||||
|
|
||||||
|
import { useConversationContext } from '@/stores/ConversationContext';
|
||||||
|
|
||||||
|
import './Conversations.css';
|
||||||
|
import { useAuthContext } from '@/stores/AuthContext.js';
|
||||||
|
|
||||||
|
const { Sider, Content, Header, Footer } = Layout;
|
||||||
|
|
||||||
|
const CList = [
|
||||||
|
{ name: 'Customer_1', label: 'Customer_1', key: 'Customer_1', value: 'Customer_1' },
|
||||||
|
{ name: 'Customer_2', label: 'Customer_2', key: 'Customer_2', value: 'Customer_2' },
|
||||||
|
{ name: 'Customer_3', label: 'Customer_3', key: 'Customer_3', value: 'Customer_3' },
|
||||||
|
{ name: 'Customer_4', label: 'Customer_4', key: 'Customer_4', value: 'Customer_4' },
|
||||||
|
];
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const ChatWindow = observer(() => {
|
||||||
|
const { loginUser: currentUser } = useAuthContext();
|
||||||
|
const { errors, messages, sendMessage, currentConversation } = useConversationContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout className='full-height' style={{ maxHeight: 'calc(100% - 150px)', height: 'calc(100% - 150px)' }}>
|
||||||
|
<Sider width={240} theme={'light'} className='scrollable-column' style={{ height: '70vh' }}>
|
||||||
|
<ConversationsList />
|
||||||
|
</Sider>
|
||||||
|
|
||||||
|
<Content className='h70' style={{ maxHeight: '70vh', height: '70vh' }}>
|
||||||
|
<Layout style={{ height: '100%' }}>
|
||||||
|
<Header className='ant-layout-sider-light ant-card' style={{ padding: '10px', height: 'auto' }}>
|
||||||
|
<Flex gap={16}>
|
||||||
|
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${currentConversation.name}`}>{currentConversation.name}</Avatar>
|
||||||
|
<Flex vertical={true} justify='space-between'>
|
||||||
|
<Typography.Text strong>{currentConversation.name}</Typography.Text>
|
||||||
|
{/* <div> HXY231119017</div> */}
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
{/* <List
|
||||||
|
dataSource={[]}
|
||||||
|
renderItem={(item, ii) => (
|
||||||
|
<List.Item actions={[<a key='list-loadmore-edit'>mark</a>]}>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={
|
||||||
|
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.name}`}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Avatar>
|
||||||
|
}
|
||||||
|
title={item.name}
|
||||||
|
description='{最近的消息}'
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
</Header>
|
||||||
|
<Content style={{ maxHeight: '70vh', height: '70vh' }}>
|
||||||
|
<div className='scrollable-column'>
|
||||||
|
<Messages />
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
<Footer className='ant-layout-sider-light' style={{ padding: '10px' }}>
|
||||||
|
<InputBox onSend={(v) => sendMessage(v)} />
|
||||||
|
</Footer>
|
||||||
|
</Layout>
|
||||||
|
{/* <InputBox onSend={(v) => sendMessage(v)} /> */}
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
<Sider width={300} theme={'light'} className='scrollable-column' style={{ maxHeight: '70vh', height: '70vh' }}>
|
||||||
|
<CustomerProfile customer={{}} />
|
||||||
|
</Sider>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default ChatWindow;
|
@ -0,0 +1,13 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { stores_Context } from '../config';
|
||||||
|
import { Table } from 'antd';
|
||||||
|
|
||||||
|
const ContactInfo = observer((props) => {
|
||||||
|
// const { } = useContext(stores_Context);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
export default ContactInfo;
|
@ -0,0 +1,13 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { stores_Context } from '../config';
|
||||||
|
import { Table } from 'antd';
|
||||||
|
|
||||||
|
const ContactPanel = observer((props) => {
|
||||||
|
// const { } = useContext(stores_Context);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
export default ContactPanel;
|
@ -0,0 +1,48 @@
|
|||||||
|
import { useRef, useEffect, useState } from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { List, Avatar, Flex } from 'antd';
|
||||||
|
import { useConversationContext } from '@/stores/ConversationContext';
|
||||||
|
import { ChatItem, ChatList } from 'react-chat-elements';
|
||||||
|
/**
|
||||||
|
* []
|
||||||
|
*/
|
||||||
|
const Conversations = observer(({ conversations }) => {
|
||||||
|
const { switchConversation, conversationsList } = useConversationContext();
|
||||||
|
console.log(conversationsList);
|
||||||
|
const [chatlist, setChatlist] = useState([]);
|
||||||
|
useEffect(() => {
|
||||||
|
setChatlist(
|
||||||
|
(conversationsList || []).map((item) => ({
|
||||||
|
...item,
|
||||||
|
avatar: `https://api.dicebear.com/7.x/avataaars/svg?seed=${item.name}`,
|
||||||
|
alt: item.name,
|
||||||
|
title: item.name,
|
||||||
|
subtitle: item.lastMessage,
|
||||||
|
date: item.last_time,
|
||||||
|
unread: item.new_msgs,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {};
|
||||||
|
}, [conversationsList]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ChatList className='chat-list' dataSource={chatlist} onClick={(item) => switchConversation(item)} />
|
||||||
|
{/* <List
|
||||||
|
dataSource={conversationsList || []}
|
||||||
|
renderItem={(item, ii) => (
|
||||||
|
// actions={[<a key='list-loadmore-edit'>mark</a>]}
|
||||||
|
<List.Item onClick={() => switchConversation(item)}>
|
||||||
|
<List.Item.Meta
|
||||||
|
avatar={<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${item.name}`}>{item.name}</Avatar>}
|
||||||
|
title={item.name}
|
||||||
|
// description='{最近的消息}'
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
export default Conversations;
|
@ -0,0 +1,69 @@
|
|||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Card, Flex, Avatar, Typography, Radio, Button } from 'antd';
|
||||||
|
import { useAuthContext } from '@/stores/AuthContext.js';
|
||||||
|
import { useConversationContext } from '@/stores/ConversationContext';
|
||||||
|
import { useGetJson } from '@/hooks/userFetch';
|
||||||
|
|
||||||
|
const orderTags = [
|
||||||
|
{ value: 'potential', label: '潜力' },
|
||||||
|
{ value: 'important', label: '重点' },
|
||||||
|
{ label: '休眠', value: 'snooze' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const orderStatus = [
|
||||||
|
{ value: 'pending', label: '报价中' },
|
||||||
|
// { value: 'in-progress', label: '处理中' },
|
||||||
|
{ value: 'lost', label: '丢失' },
|
||||||
|
{ value: 'later', label: '以后联系' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const { Meta } = Card;
|
||||||
|
|
||||||
|
const CustomerProfile = observer(({ customer }) => {
|
||||||
|
const { errors } = useConversationContext();
|
||||||
|
const { loginUser: currentUser } = useAuthContext();
|
||||||
|
const orderInfo = useGetJson('http://127.0.0.1:4523/m2/3888351-0-default/144062941');
|
||||||
|
const { quotes, contact, last_contact, ...order } = orderInfo || {};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className=' divide-x-0 divide-y divide-dotted divide-slate-400/[.24]'>
|
||||||
|
<Card className='p-2'
|
||||||
|
bordered={false}
|
||||||
|
title={order?.order_no}
|
||||||
|
extra={<Radio.Group size={'small'} options={orderTags} value={'important'} onChange={({ target: { value } }) => {}} optionType='button' buttonStyle={'solid'} />}>
|
||||||
|
<Meta
|
||||||
|
title={<Radio.Group size={'small'} options={orderStatus} value={'pending'} onChange={({ target: { value } }) => {}} optionType='button' buttonStyle={'solid'} />}
|
||||||
|
description={' '}
|
||||||
|
/>
|
||||||
|
<Flex gap={16}>
|
||||||
|
<Avatar src={`https://api.dicebear.com/7.x/avataaars/svg?seed=${contact?.name}`} />
|
||||||
|
<Flex vertical={true} justify='space-between'>
|
||||||
|
<Typography.Text strong>{contact?.name}</Typography.Text>
|
||||||
|
<div>
|
||||||
|
{contact?.phone} <span>{contact?.email}</span>{' '}
|
||||||
|
</div>
|
||||||
|
{/* <div>{order?.order_no}</div> */}
|
||||||
|
<div>
|
||||||
|
{order?.location} <span>{order?.local_datetime}</span>
|
||||||
|
</div>
|
||||||
|
<div></div>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Card>
|
||||||
|
<Flex vertical={true} className='p-2 '>
|
||||||
|
<div>最新报价</div>
|
||||||
|
<p className='m-0 py-2 '>{quotes?.[0]?.name}</p>
|
||||||
|
<Flex justify={'space-between'} >
|
||||||
|
<Button size={'small'}>Book</Button>
|
||||||
|
<Button size={'small'}>报价历史</Button>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
<p className='p-2 overflow-auto h-40 '>{order?.order_detail}</p>
|
||||||
|
<Flex vertical={true} className='p-2 '>
|
||||||
|
<div>沟通记录</div>
|
||||||
|
<p className='m-0 py-2 '>{quotes?.[0]?.name}</p>
|
||||||
|
</Flex>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
export default CustomerProfile;
|
@ -0,0 +1,47 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Input, Button } from 'antd';
|
||||||
|
// import { Input } from 'react-chat-elements';
|
||||||
|
|
||||||
|
const InputBox = observer(({ onSend }) => {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
|
||||||
|
const onOK = () => {
|
||||||
|
// console.log(message);
|
||||||
|
if (typeof onSend === 'function' && message.trim() !== '') {
|
||||||
|
const msgObj = {
|
||||||
|
type: 'text',
|
||||||
|
text: {
|
||||||
|
body: message,
|
||||||
|
},
|
||||||
|
// contentType: 'text/markdown',
|
||||||
|
sender: 'me',
|
||||||
|
id: Date.now().toString(16),
|
||||||
|
readState: false,
|
||||||
|
};
|
||||||
|
onSend(msgObj);
|
||||||
|
setMessage('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Input.Search placeholder='Type message here' enterButton='Send' size='large' onSearch={onOK} value={message} onChange={(e) => setMessage(e.target.value)} />
|
||||||
|
|
||||||
|
{/* <Input
|
||||||
|
placeholder='Type a message'
|
||||||
|
multiline={true}
|
||||||
|
value={message}
|
||||||
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
if (e.shiftKey && e.charCode === 13) {
|
||||||
|
onOK();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
rightButtons={<button onClick={onOK}>Send</button>}
|
||||||
|
/> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default InputBox;
|
@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { List, Avatar, Timeline } from 'antd';
|
||||||
|
import { MessageBox } from 'react-chat-elements';
|
||||||
|
import { useConversationContext } from '@/stores/ConversationContext';
|
||||||
|
|
||||||
|
const messagesTemplate = [
|
||||||
|
{
|
||||||
|
id: Date.now().toString(16),
|
||||||
|
sender: 'Customer_1',
|
||||||
|
type: 'text',
|
||||||
|
text: { body: 'Hello, how can I help you today?' } ,
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const Messages = observer(() => {
|
||||||
|
const { messages } = useConversationContext()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{messages.map((message, index) => (
|
||||||
|
<MessageBox
|
||||||
|
key={message.id}
|
||||||
|
position={ message.sender === 'me' ? 'right' : 'left' }
|
||||||
|
type={'text'}
|
||||||
|
text={message.text.body}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{/* <List
|
||||||
|
dataSource={conversationsStore.messages}
|
||||||
|
style={{ flex: '1 1' }}
|
||||||
|
renderItem={(message) => (
|
||||||
|
<List.Item>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={message.sender !== 'me' ? message.sender : ''}
|
||||||
|
description={message.sender !== 'me' ? `(${message.id}) ${message.content}` : ''}
|
||||||
|
/>
|
||||||
|
{message.sender === 'me' && <div>{message.content} ({message.id})</div>}
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/> */}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default Messages;
|
@ -0,0 +1,9 @@
|
|||||||
|
import { ConversationContext, useConversations } from '@/stores/ConversationContext';
|
||||||
|
|
||||||
|
export const ConversationProvider = ({ children }) => {
|
||||||
|
|
||||||
|
const conversations = useConversations();
|
||||||
|
return <ConversationContext.Provider value={conversations}>{children}</ConversationContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConversationProvider;
|
@ -0,0 +1,12 @@
|
|||||||
|
.full-height {
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scrollable-column {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.column {
|
||||||
|
height: 100%;
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
import { Layout, List, Input, Button } from 'antd';
|
||||||
|
import './Chat.css';
|
||||||
|
|
||||||
|
const { Sider, Content } = Layout;
|
||||||
|
const { TextArea } = Input;
|
||||||
|
|
||||||
|
const Chat = () => {
|
||||||
|
const channels = ['Channel 1', 'Channel 2'];
|
||||||
|
const messages = [
|
||||||
|
{ user: 'User 1', text: 'Hello!' },
|
||||||
|
{ user: 'User 2', text: 'Hi!' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Layout className="chat-layout">
|
||||||
|
<Sider theme="light" width={300} className="chat-sider">
|
||||||
|
<List
|
||||||
|
header={<div>Channels</div>}
|
||||||
|
bordered
|
||||||
|
dataSource={channels}
|
||||||
|
renderItem={item => (
|
||||||
|
<List.Item>
|
||||||
|
{item}
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Sider>
|
||||||
|
<Layout>
|
||||||
|
<Content className="chat-content">
|
||||||
|
<List
|
||||||
|
itemLayout="horizontal"
|
||||||
|
dataSource={messages}
|
||||||
|
renderItem={item => (
|
||||||
|
<List.Item>
|
||||||
|
<List.Item.Meta
|
||||||
|
title={item.user}
|
||||||
|
description={item.text}
|
||||||
|
/>
|
||||||
|
</List.Item>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="chat-input">
|
||||||
|
<TextArea rows={4} />
|
||||||
|
<Button type="primary" className="chat-button">
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Layout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chat;
|
@ -0,0 +1,35 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { observer } from 'mobx-react';
|
||||||
|
import { Table } from 'antd';
|
||||||
|
import WebSocketLib from '@/lib/websocketLib2';
|
||||||
|
|
||||||
|
const CList = [
|
||||||
|
{ name: 'Customer_1', label: 'Customer_1', key: 'Customer_1', value: 'Customer_1' },
|
||||||
|
{ name: 'Customer_2', label: 'Customer_2', key: 'Customer_2', value: 'Customer_2' },
|
||||||
|
{ name: 'Customer_3', label: 'Customer_3', key: 'Customer_3', value: 'Customer_3' },
|
||||||
|
{ name: 'Customer_4', label: 'Customer_4', key: 'Customer_4', value: 'Customer_4' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Assume we have an array of customer URLs and their corresponding auth tokens
|
||||||
|
const customers = [
|
||||||
|
{ url: 'ws://202.103.68.144:8888/whatever/', authToken: 'customer1Token' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Create a WebSocketLib instance for each customer
|
||||||
|
const connections = customers.map(customer => {
|
||||||
|
const wsLib = new WebSocketLib(customer.url, customer.authToken, 'WhatApp');
|
||||||
|
wsLib.connect();
|
||||||
|
return wsLib;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Now, the agent can send a message to a specific customer like this:
|
||||||
|
connections[0].sendMessage('Hello, customer 1! '+ Date.now().toString(36));
|
||||||
|
|
||||||
|
// Or broadcast a message to all customers like this:
|
||||||
|
// connections.forEach(conn => conn.sendMessage('Hello, all customers!'));
|
||||||
|
|
||||||
|
export default observer((props) => {
|
||||||
|
// const { } = useContext(stores_Context);
|
||||||
|
return <></>;
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,17 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
|
purge: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx}'],
|
||||||
|
darkMode: false,
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
// gridTemplateColumns: {
|
||||||
|
// 'responsive':repeat(autofill,minmax('300px',1fr))
|
||||||
|
// }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
corePlugins: {
|
||||||
|
preflight: false
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue