diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 4dcb439..bdbae1d 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -16,5 +16,7 @@ module.exports = {
'warn',
{ allowConstantExport: true },
],
+ 'no-unused-vars': ['warn', { args: 'after-used', vars: 'all' }],
+ "react/prop-types": "off",
},
}
diff --git a/jsconfig.json b/jsconfig.json
new file mode 100644
index 0000000..130459b
--- /dev/null
+++ b/jsconfig.json
@@ -0,0 +1,9 @@
+{
+ "compilerOptions": {
+ "experimentalDecorators": true,
+ "baseUrl": "./",
+ "paths": {
+ "@/*": ["src/*"]
+ }
+ }
+}
diff --git a/package.json b/package.json
index e532711..6f5d71e 100644
--- a/package.json
+++ b/package.json
@@ -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"
}
}
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..2e7af2b
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
diff --git a/src/assets/index.css b/src/assets/index.css
new file mode 100644
index 0000000..fafd6c4
--- /dev/null
+++ b/src/assets/index.css
@@ -0,0 +1,3 @@
+@import 'tailwindcss/base';
+@import 'tailwindcss/components';
+@import 'tailwindcss/utilities';
diff --git a/src/lib/realTimeAPI.js b/src/lib/realTimeAPI.js
new file mode 100644
index 0000000..f0067c8
--- /dev/null
+++ b/src/lib/realTimeAPI.js
@@ -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;
+ }
+}
diff --git a/src/lib/websocketLib2.js b/src/lib/websocketLib2.js
new file mode 100644
index 0000000..7f1db5b
--- /dev/null
+++ b/src/lib/websocketLib2.js
@@ -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;
diff --git a/src/main.jsx b/src/main.jsx
index 6f5f87f..38a7f0c 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -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: ,
errorElement: ,
- children: [
+ children: [
{ index: true, element: },
- { path: 'order/follow', element: },
- { path: 'chat/history', element: },
- { path: 'sales/management', element: },
- { path: 'account/profile', element: },
- ]
+ { path: 'order/follow', element: },
+ { path: 'chat/history', element: },
+ { path: 'sales/management', element: },
+ { path: 'order/chat', element: },
+ { path: 'account/profile', element: },
+ ],
},
{
path: '/p',
@@ -46,20 +49,18 @@ const router = createBrowserRouter([
children: [
{ path: 'dingding/qrcode', element: },
{ path: 'dingding/callback', element: },
- ]
- }
-])
+ ],
+ },
+]);
ReactDOM.createRoot(document.getElementById('root')).render(
-
//
-
-
- Loading...
}
- />
+
+
+
+ Loading...
} />
+
//
-)
+);
diff --git a/src/stores/ConversationContext.js b/src/stores/ConversationContext.js
new file mode 100644
index 0000000..28345f8
--- /dev/null
+++ b/src/stores/ConversationContext.js
@@ -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);
diff --git a/src/stores/Conversations.js b/src/stores/Conversations.js
new file mode 100644
index 0000000..23d8aa8
--- /dev/null
+++ b/src/stores/Conversations.js
@@ -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;
diff --git a/src/stores/Root.js b/src/stores/Root.js
index 0046fa5..76ff0a4 100644
--- a/src/stores/Root.js
+++ b/src/stores/Root.js
@@ -9,7 +9,7 @@ class Root {
makeAutoObservable(this)
}
- clearSession() {
+ clearSession() {
if (window.sessionStorage) {
const sessionStorage = window.sessionStorage
sessionStorage.clear()
diff --git a/src/utils/utils.js b/src/utils/utils.js
new file mode 100644
index 0000000..fd3c567
--- /dev/null
+++ b/src/utils/utils.js
@@ -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;
+};
diff --git a/src/views/App.jsx b/src/views/App.jsx
index 68db2bb..e0086ff 100644
--- a/src/views/App.jsx
+++ b/src/views/App.jsx
@@ -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';
diff --git a/src/views/Conversations/App.jsx b/src/views/Conversations/App.jsx
new file mode 100644
index 0000000..e54cd21
--- /dev/null
+++ b/src/views/Conversations/App.jsx
@@ -0,0 +1,12 @@
+import Chat from './Index';
+// import 'antd/dist/reset.css';
+
+function App() {
+ return (
+
+
+
+ );
+}
+
+export default App;
diff --git a/src/views/Conversations/App1.jsx b/src/views/Conversations/App1.jsx
new file mode 100644
index 0000000..b1a985d
--- /dev/null
+++ b/src/views/Conversations/App1.jsx
@@ -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 (
+
+ (
+
+ }
+ title={message.sender}
+ description={message.message}
+ />
+
+ )}
+ />
+
+ setMessage(e.target.value)}
+ />
+
+
+
+ );
+}
+
+export default App;
diff --git a/src/views/Conversations/Chat.css b/src/views/Conversations/Chat.css
new file mode 100644
index 0000000..7d22a0c
--- /dev/null
+++ b/src/views/Conversations/Chat.css
@@ -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;
+}
\ No newline at end of file
diff --git a/src/views/Conversations/ChatApp.jsx b/src/views/Conversations/ChatApp.jsx
new file mode 100644
index 0000000..73d043f
--- /dev/null
+++ b/src/views/Conversations/ChatApp.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+ {messages.map((message, index) => (
+
+ {message.user}: {message.text}
+
+ ))}
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/src/views/Conversations/ChatApp2.jsx b/src/views/Conversations/ChatApp2.jsx
new file mode 100644
index 0000000..e3e8114
--- /dev/null
+++ b/src/views/Conversations/ChatApp2.jsx
@@ -0,0 +1,44 @@
+import { Layout, Menu, List, Timeline, Input } from 'antd';
+
+const { Sider, Content } = Layout;
+function ChatApp() {
+ return (
+
+
+
+
+
+ {/* Show channels and DMs */}
+
+
+
+
+
+
+
+ {/* Show user profile cards */}
+
+
+
+
+
+ {/* Show messages */}
+
+
+
+
+
+
+
+ );
+}
+
+export default ChatApp;
diff --git a/src/views/Conversations/ChatWindow.jsx b/src/views/Conversations/ChatWindow.jsx
new file mode 100644
index 0000000..db34d65
--- /dev/null
+++ b/src/views/Conversations/ChatWindow.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+ {currentConversation.name}
+
+ {currentConversation.name}
+ {/* HXY231119017
*/}
+
+
+ {/* (
+ mark]}>
+
+ {item.name}
+
+ }
+ title={item.name}
+ description='{最近的消息}'
+ />
+
+ )}
+ /> */}
+
+
+
+
+
+
+
+
+ {/* sendMessage(v)} /> */}
+
+
+
+
+
+
+ );
+});
+
+export default ChatWindow;
diff --git a/src/views/Conversations/Components/ContactInfo.jsx b/src/views/Conversations/Components/ContactInfo.jsx
new file mode 100644
index 0000000..142f7e4
--- /dev/null
+++ b/src/views/Conversations/Components/ContactInfo.jsx
@@ -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;
diff --git a/src/views/Conversations/Components/ContactPanel.jsx b/src/views/Conversations/Components/ContactPanel.jsx
new file mode 100644
index 0000000..ba7a6c8
--- /dev/null
+++ b/src/views/Conversations/Components/ContactPanel.jsx
@@ -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;
diff --git a/src/views/Conversations/Components/ConversationsList.jsx b/src/views/Conversations/Components/ConversationsList.jsx
new file mode 100644
index 0000000..5d0633f
--- /dev/null
+++ b/src/views/Conversations/Components/ConversationsList.jsx
@@ -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 (
+ <>
+ switchConversation(item)} />
+ {/* (
+ // actions={[mark]}
+ switchConversation(item)}>
+ {item.name}}
+ title={item.name}
+ // description='{最近的消息}'
+ />
+
+ )}
+ /> */}
+ >
+ );
+});
+export default Conversations;
diff --git a/src/views/Conversations/Components/CustomerProfile.jsx b/src/views/Conversations/Components/CustomerProfile.jsx
new file mode 100644
index 0000000..2fcad26
--- /dev/null
+++ b/src/views/Conversations/Components/CustomerProfile.jsx
@@ -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 (
+
+
{}} optionType='button' buttonStyle={'solid'} />}>
+ {}} optionType='button' buttonStyle={'solid'} />}
+ description={' '}
+ />
+
+
+
+ {contact?.name}
+
+ {contact?.phone} {contact?.email}{' '}
+
+ {/* {order?.order_no}
*/}
+
+ {order?.location} {order?.local_datetime}
+
+
+
+
+
+
+ 最新报价
+ {quotes?.[0]?.name}
+
+
+
+
+
+
{order?.order_detail}
+
+ 沟通记录
+ {quotes?.[0]?.name}
+
+
+ );
+});
+export default CustomerProfile;
diff --git a/src/views/Conversations/Components/InputBox.jsx b/src/views/Conversations/Components/InputBox.jsx
new file mode 100644
index 0000000..aca129c
--- /dev/null
+++ b/src/views/Conversations/Components/InputBox.jsx
@@ -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 (
+
+ setMessage(e.target.value)} />
+
+ {/* setMessage(e.target.value)}
+ onKeyPress={(e) => {
+ if (e.shiftKey && e.charCode === 13) {
+ onOK();
+ }
+ }}
+ rightButtons={}
+ /> */}
+
+ );
+});
+
+export default InputBox;
diff --git a/src/views/Conversations/Components/Messages.jsx b/src/views/Conversations/Components/Messages.jsx
new file mode 100644
index 0000000..a004456
--- /dev/null
+++ b/src/views/Conversations/Components/Messages.jsx
@@ -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) => (
+
+ ))}
+ {/* (
+
+
+ {message.sender === 'me' && {message.content} ({message.id})
}
+
+ )}
+ /> */}
+ >
+ );
+});
+
+export default Messages;
diff --git a/src/views/Conversations/ConversationProvider.jsx b/src/views/Conversations/ConversationProvider.jsx
new file mode 100644
index 0000000..dd108ea
--- /dev/null
+++ b/src/views/Conversations/ConversationProvider.jsx
@@ -0,0 +1,9 @@
+import { ConversationContext, useConversations } from '@/stores/ConversationContext';
+
+export const ConversationProvider = ({ children }) => {
+
+ const conversations = useConversations();
+ return {children};
+};
+
+export default ConversationProvider;
diff --git a/src/views/Conversations/Conversations.css b/src/views/Conversations/Conversations.css
new file mode 100644
index 0000000..ef6d80f
--- /dev/null
+++ b/src/views/Conversations/Conversations.css
@@ -0,0 +1,12 @@
+.full-height {
+ height: 100vh;
+}
+
+.scrollable-column {
+ height: 100%;
+ overflow-y: auto;
+}
+
+.column {
+ height: 100%;
+}
diff --git a/src/views/Conversations/Index.jsx b/src/views/Conversations/Index.jsx
new file mode 100644
index 0000000..92b3c60
--- /dev/null
+++ b/src/views/Conversations/Index.jsx
@@ -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 (
+
+
+ Channels}
+ bordered
+ dataSource={channels}
+ renderItem={item => (
+
+ {item}
+
+ )}
+ />
+
+
+
+ (
+
+
+
+ )}
+ />
+
+
+
+
+
+
+
+ );
+}
+
+export default Chat;
diff --git a/src/views/Conversations/Index2.jsx b/src/views/Conversations/Index2.jsx
new file mode 100644
index 0000000..19273fb
--- /dev/null
+++ b/src/views/Conversations/Index2.jsx
@@ -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 <>>;
+});
+
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..7a4ab33
--- /dev/null
+++ b/tailwind.config.js
@@ -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
+ }
+};
diff --git a/vite.config.js b/vite.config.js
index 6bc93ac..505e794 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -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-')) {