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 ( + + + + {channels.map(channel => ( + {channel} + ))} + + + +
+ +
+ {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 ( + + + + Unread + Mentions + Favorites + Channel List + Direct Messages + + + + {/* 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)} /> +
+
+ {/* 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} + + )} + /> + + + + ( + + + + )} + /> +
+