feat: 会话窗口; +tailwindcss

dev/mobile
Lei OT 1 year ago
parent f2768df484
commit e600eae98f

@ -16,5 +16,7 @@ module.exports = {
'warn',
{ allowConstantExport: true },
],
'no-unused-vars': ['warn', { args: 'after-used', vars: 'all' }],
"react/prop-types": "off",
},
}

@ -0,0 +1,9 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"baseUrl": "./",
"paths": {
"@/*": ["src/*"]
}
}
}

@ -12,20 +12,29 @@
"dependencies": {
"ahooks": "^3.7.8",
"antd": "^5.12.8",
"crypto-js": "^4.2.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.0",
"react": "^18.2.0",
"react-chat-elements": "^12.0.11",
"react-dom": "^18.2.0",
"react-router-dom": "^6.21.1"
"react-router-dom": "^6.21.1",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@types/react": "^18.2.15",
"@types/react-dom": "^18.2.7",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"eslint": "^8.45.0",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"vite": "^4.5.1"
"postcss": "^8.4.33",
"tailwindcss": "^3.4.1",
"vite": "^4.5.1",
"vite-plugin-css-modules": "^0.0.1",
"vite-plugin-windicss": "^1.9.3",
"windicss": "^3.5.6"
}
}

@ -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;

@ -1,22 +1,24 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { configure } from 'mobx'
import {
createBrowserRouter,
RouterProvider,
} from 'react-router-dom'
import { AuthContext } from '@/stores/AuthContext'
import { ThemeContext } from '@/stores/ThemeContext'
import Auth from '@/stores/Auth'
import App from '@/views/App'
import Standlone from '@/views/Standlone'
import OrderFollow from '@/views/OrderFollow'
import ChatHistory from '@/views/ChatHistory'
import SalesManagement from '@/views/SalesManagement'
import DingdingQRCode from '@/views/DingdingQRCode'
import DingdingCallbak from '@/views/DingdingCallbak'
import AccountProfile from '@/views/AccountProfile'
import ErrorPage from '@/views/ErrorPage'
import React from 'react';
import ReactDOM from 'react-dom/client';
import { configure } from 'mobx';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { AuthContext } from '@/stores/AuthContext';
import { ThemeContext } from '@/stores/ThemeContext';
import ConversationProvider from '@/views/Conversations/ConversationProvider';
import Auth from '@/stores/Auth';
import App from '@/views/App';
import Standlone from '@/views/Standlone';
import OrderFollow from '@/views/OrderFollow';
import ChatHistory from '@/views/ChatHistory';
import SalesManagement from '@/views/SalesManagement';
import DingdingQRCode from '@/views/DingdingQRCode';
import DingdingCallbak from '@/views/DingdingCallbak';
import AccountProfile from '@/views/AccountProfile';
import ErrorPage from '@/views/ErrorPage';
import Conversations from '@/views/Conversations/ChatWindow';
// import Conversations from '@/views/Conversations/ChatApp';
import '@/assets/index.css';
configure({
useProxies: 'ifavailable',
@ -24,21 +26,22 @@ configure({
computedRequiresReaction: true,
observableRequiresReaction: false,
reactionRequiresObservable: true,
disableErrorBoundaries: process.env.NODE_ENV == 'production'
})
disableErrorBoundaries: process.env.NODE_ENV == 'production',
});
const router = createBrowserRouter([
{
path: '/',
element: <App />,
errorElement: <ErrorPage />,
children: [
children: [
{ index: true, element: <OrderFollow /> },
{ path: 'order/follow', element: <OrderFollow />},
{ path: 'chat/history', element: <ChatHistory />},
{ path: 'sales/management', element: <SalesManagement />},
{ path: 'account/profile', element: <AccountProfile />},
]
{ path: 'order/follow', element: <OrderFollow /> },
{ path: 'chat/history', element: <ChatHistory /> },
{ path: 'sales/management', element: <SalesManagement /> },
{ path: 'order/chat', element: <Conversations /> },
{ path: 'account/profile', element: <AccountProfile /> },
],
},
{
path: '/p',
@ -46,20 +49,18 @@ const router = createBrowserRouter([
children: [
{ path: 'dingding/qrcode', element: <DingdingQRCode /> },
{ path: 'dingding/callback', element: <DingdingCallbak /> },
]
}
])
],
},
]);
ReactDOM.createRoot(document.getElementById('root')).render(
// <React.StrictMode>
<ThemeContext.Provider value={{colorPrimary: '#1ba784', borderRadius: 4}}>
<AuthContext.Provider value={{loginUser: {userId: 1, openId: '123456789'}, permissionList: ['view_chat', 'send_msg']}}>
<RouterProvider
router={router}
fallbackElement={() => <div>Loading...</div>}
/>
<ThemeContext.Provider value={{ colorPrimary: '#1ba784', borderRadius: 4 }}>
<AuthContext.Provider value={{ loginUser: { userId: 1, openId: '123456789' }, permissionList: ['view_chat', 'send_msg'] }}>
<ConversationProvider>
<RouterProvider router={router} fallbackElement={() => <div>Loading...</div>} />
</ConversationProvider>
</AuthContext.Provider>
</ThemeContext.Provider>
// </React.StrictMode>
)
);

@ -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;

@ -9,7 +9,7 @@ class Root {
makeAutoObservable(this)
}
clearSession() {
clearSession() {
if (window.sessionStorage) {
const sessionStorage = window.sessionStorage
sessionStorage.clear()

@ -0,0 +1,311 @@
import crypto from 'crypto-js';
/**
* ! 不支持计算 Set Map
* @param {*} val
* @example
* true if: 0, [], {}, null, '', undefined
* false if: 'false', 'undefined'
*/
export function isEmpty(val) {
// return val === undefined || val === null || val === "";
return [Object, Array].includes((val || {}).constructor) && !Object.entries(val || {}).length;
}
/**
* 数组排序
*/
export const sortBy = (key) => {
return (a, b) => (a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0);
};
/**
* Object排序keys
*/
export const sortKeys = (obj) =>
Object.keys(obj)
.sort()
.reduce((a, k2) => ({ ...a, [k2]: obj[k2] }), {});
/**
* 数组排序, 给定排序数组
* @param {array} items 需要排序的数组
* @param {array} keyName 排序的key
* @param {array} keyOrder 给定排序
* @returns
*/
export const sortArrayByOrder = (items, keyName, keyOrder) => {
return items.sort((a, b) => {
return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]);
});
};
/**
* 合并Object, 递归地
*/
export function merge(...objects) {
const isDeep = objects.some((obj) => obj !== null && typeof obj === 'object');
const result = objects[0] || (isDeep ? {} : objects[0]);
for (let i = 1; i < objects.length; i++) {
const obj = objects[i];
if (!obj) continue;
Object.keys(obj).forEach((key) => {
const val = obj[key];
if (isDeep) {
if (Array.isArray(val)) {
result[key] = [].concat(Array.isArray(result[key]) ? result[key] : [result[key]], val);
} else if (typeof val === 'object') {
result[key] = merge(result[key], val);
} else {
result[key] = val;
}
} else {
result[key] = typeof val === 'boolean' ? val : result[key];
}
});
}
return result;
}
/**
* 数组分组
* - 相当于 lodash _.groupBy
* @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
*/
export function groupBy(array, callback) {
return array.reduce((groups, item) => {
const key = typeof callback === 'function' ? callback(item) : item[callback];
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {});
}
/**
* 创建一个从 object 中选中的属性的对象
* @param {*} object
* @param {array} keys
*/
export function pick(object, keys) {
return keys.reduce((obj, key) => {
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
obj[key] = object[key];
}
return obj;
}, {});
}
/**
* 返回对象的副本经过筛选以省略指定的键
* @param {*} object
* @param {string[]} keysToOmit
* @returns
*/
export function omit(object, keysToOmit) {
return Object.fromEntries(Object.entries(object).filter(([key]) => !keysToOmit.includes(key)));
}
/**
* 深拷贝
*/
export function cloneDeep(value) {
if (typeof value !== 'object' || value === null) {
return value;
}
const result = Array.isArray(value) ? [] : {};
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
result[key] = cloneDeep(value[key]);
}
}
return result;
}
/**
* 向零四舍五入, 固定精度设置
*/
function curriedFix(precision = 0) {
return function (number) {
// Shift number by precision places
const shift = Math.pow(10, precision);
const shiftedNumber = number * shift;
// Round to nearest integer
const roundedNumber = Math.round(shiftedNumber);
// Shift back decimal place
return roundedNumber / shift;
};
}
/**
* 向零四舍五入, 保留2位小数
*/
export const fixTo2Decimals = curriedFix(2);
/**
* 向零四舍五入, 保留4位小数
*/
export const fixTo4Decimals = curriedFix(4);
export const fixTo1Decimals = curriedFix(1);
export const fixToInt = curriedFix(0);
/**
* 映射
* @example
* const keyMap = {
a: [{key: 'a1'}, {key: 'a2', transform: v => v * 2}],
b: {key: 'b1'}
};
const result = objectMapper({a: 1, b: 3}, keyMap);
// result = {a1: 1, a2: 2, b1: 3}
*
*/
export function objectMapper(input, keyMap) {
// Loop through array mapping
if (Array.isArray(input)) {
return input.map((obj) => objectMapper(obj, keyMap));
}
if (typeof input === 'object') {
const mappedObj = {};
Object.keys(input).forEach((key) => {
// Keep original keys not in keyMap
if (!keyMap[key]) {
mappedObj[key] = input[key];
}
// Handle array of maps
if (Array.isArray(keyMap[key])) {
keyMap[key].forEach((map) => {
let value = input[key];
if (map.transform) value = map.transform(value);
mappedObj[map.key] = value;
});
// Handle single map
} else {
const map = keyMap[key];
if (map) {
let value = input[key];
if (map.transform) value = map.transform(value);
mappedObj[map.key || key] = value;
}
}
});
return mappedObj;
}
return input;
}
/**
* 创建一个对应于对象路径的值数组
*/
export function at(obj, path) {
let result;
if (Array.isArray(obj)) {
// array case
const indexes = path.split('.').map((i) => parseInt(i));
result = [];
for (let i = 0; i < indexes.length; i++) {
result.push(obj[indexes[i]]);
}
} else {
// object case
const indexes = path.split('.').map((i) => i);
result = [obj];
for (let i = 0; i < indexes.length; i++) {
result = [result[0][indexes[i]]];
}
}
return result;
}
/**
* 删除 null/undefined
*/
export function flush(collection) {
let result, len, i;
if (!collection) {
return undefined;
}
if (Array.isArray(collection)) {
result = [];
len = collection.length;
for (i = 0; i < len; i++) {
const elem = collection[i];
if (elem != null) {
result.push(elem);
}
}
return result;
}
if (typeof collection === 'object') {
result = {};
const keys = Object.keys(collection);
len = keys.length;
for (i = 0; i < len; i++) {
const key = keys[i];
const value = collection[key];
if (value != null) {
result[key] = value;
}
}
return result;
}
return undefined;
}
/**
* 千分位 格式化数字
*/
export const numberFormatter = (number) => {
return new Intl.NumberFormat().format(number);
};
/**
* @example
* const obj = { a: { b: 'c' } };
* const keyArr = ['a', 'b'];
* getNestedValue(obj, keyArr); // Returns: 'c'
*/
export const getNestedValue = (obj, keyArr) => {
return keyArr.reduce((acc, curr) => {
return acc && Object.prototype.hasOwnProperty.call(acc, curr) ? acc[curr] : undefined;
// return acc && acc[curr];
}, obj);
};
/**
* 计算笛卡尔积
*/
export const cartesianProductArray = (arr, sep = '_', index = 0, prefix = '') => {
let result = [];
if (index === arr.length) {
return [prefix];
}
arr[index].forEach((item) => {
result = result.concat(cartesianProductArray(arr, sep, index + 1, prefix ? `${prefix}${sep}${item}` : `${item}`));
});
return result;
};
export const stringToColour = (str) => {
// Hash the username using SHA256
const hash = crypto.SHA256(str);
// Convert the hash to a hexadecimal string
const hexString = hash.toString(crypto.enc.Hex);
// Use the first 6 characters of the hex string as a color
const color = '#' + hexString.substring(0, 6);
return color;
};

@ -7,6 +7,7 @@ import { useThemeContext } from '@/stores/ThemeContext';
import { useAuthContext } from '@/stores/AuthContext'
import 'dayjs/locale/zh-cn';
import 'react-chat-elements/dist/main.css'
import '@/assets/App.css';
import AppLogo from '@/assets/logo-gh.png';
import { isEmpty } from '@/utils/commons';

@ -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
}
};

@ -1,9 +1,10 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import WindiCSS from 'vite-plugin-windicss'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
plugins: [react(), WindiCSS()],
server: {
host: "0.0.0.0",
},
@ -19,7 +20,7 @@ export default defineConfig({
rollupOptions: {
output: {
manualChunks(id) {
// if (id.includes('antd')) {
// if (id.includes('antd')) {
// console.info('chunk: ' + id)
// return 'antd-component'
// } else if (id.includes('rc-')) {

Loading…
Cancel
Save