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