对话窗口; 发送文本; 解析WhatsApp信息各类型

dev/chat
Lei OT 2 years ago
parent ab124d9be3
commit 70fbcae715

@ -13,6 +13,8 @@
"ahooks": "^3.7.8",
"antd": "^5.12.8",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.10",
"dayjs-plugin-utc": "^0.1.2",
"mobx": "^6.12.0",
"mobx-react": "^9.1.0",
"react": "^18.2.0",

@ -0,0 +1,111 @@
export const sentMsgTypeMapped = {
text: {
type: 'text',
contentToSend: (msg) => ({ type: 'text', from: '+8617607730395', to: '', text: { body: msg.text } }),
contentToRender: (msg) => ({...msg}),
},
whatsappTemplate: {
type: 'template',
contentToSend: (msg) => ({
template: {
namespace: msg.whatsappTemplate.namespace,
language: msg.whatsappTemplate.language,
type: msg.whatsappTemplate.type,
components: msg.whatsappTemplate.components,
},
}),
},
};
export const whatsappMsgMapped = {
'whatsapp.inbound_message.received': {
getMsg: (result) => {
console.log('whatsapp.inbound_message.received', result);
return result?.whatsappInboundMessage || null;
},
contentToRender: (result) => {
console.log( 'whatsapp.inbound_message.received', result);
const contentObj = result?.whatsappInboundMessage || result; // debug:
return parseRenderMessageItem(contentObj);
},
},
};
export const whatsappMsgTypeMapped = {
text: { type: 'text', data: (msg) => ({ text: msg.text.body }) },
image: {
type: 'photo',
data: (msg) => ({
data: {
uri: msg.image.link,
width: 200,
height: 200,
alt: '',
},
onOpen: () => {
console.log('Open image', msg.image.link);
},
}),
},
sticker: {
type: 'photo',
data: (msg) => ({
data: {
uri: msg.sticker.link,
width: 150,
height: 120,
alt: '',
},
}),
},
video: {
type: 'video',
data: (msg) => ({
data: {
videoURL: msg.video.link,
status: {
click: true,
loading: 0,
download: true,
},
},
}),
},
audio: {
type: 'audio',
data: (msg) => ({
data: {
audioURL: msg.audio.link,
},
}),
},
'unsupported': { type: 'system', data: (msg) => ({ text: 'Message type is currently not supported.' }) },
// 'unsupported': { type: 'text', data: (msg) => ({ text: 'Message type is currently not supported.' }) }
// file: 'file',
// location: 'location',
// contact: 'contact',
// 'contact-card': 'contact-card',
// 'contact-card-with-photo': 'contact-card-with-photo',
// 'contact-card-with-photo-and-label': 'contact-card-with-photo-and-label',
};
export const parseRenderMessageItem = (msg) => {
console.log(msg, '[[[[');
return {
...(whatsappMsgTypeMapped?.[msg.type]?.data(msg) || {}),
id: msg.id,
sender: msg.from,
type: whatsappMsgTypeMapped?.[msg.type]?.type || 'text',
// title: msg.customerProfile.name,
date: msg.sendTime,
};
};
export const parseRenderMessageList = (messages) => {
return messages.map((msg) => {
return {
...(whatsappMsgTypeMapped?.[msg.type]?.data(msg) || {}),
id: msg.id,
sender: msg.from,
type: whatsappMsgTypeMapped?.[msg.type]?.type || 'text',
// title: msg.customerProfile.name,
date: msg.sendTime,
};
});
};

@ -1,58 +1,84 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { RealTimeAPI } from '@/lib/realTimeAPI';
import { useGetJson } from '@/hooks/userFetch';
import { whatsappMsgMapped, sentMsgTypeMapped, parseRenderMessageList } from '@/lib/msgUtils';
import { groupBy } from '@/utils/utils';
export const ConversationContext = createContext();
export const useConversationContext = () => useContext(ConversationContext);
export async function fetchJSON(url, data) {
let params = '';
let ifp = '';
if (data) {
params = new URLSearchParams(data).toString();
ifp = params ? '?' : ifp;
}
ifp = url.includes('?') ? '' : ifp;
const host = /^https?:\/\//i.test(url) ? '': ''; // HT_HOST;
const response = await fetch(`${host}${url}${ifp}${params}`);
return await response.json();
}
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/';
// const API_HOST = 'http://202.103.68.144:8888';
const API_HOST = 'http://202.103.68.144:8888';
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 [messages, setMessages] = useState([]); // active conversation
const [conversations, setConversations] = useState({}); // all conversation
const [currentID, setCurrentID] = useState();
const [conversationsList, setConversationsList] = useState([]); // 对话列表
const [conversationsList, setConversationsList] = useState([]); // open conversations
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(() => {
getConversations();
// getTemplates();
return () => {};
}, []);
const getConversations = async () => {
const data = await fetchJSON('http://127.0.0.1:4523/m2/3888351-0-default/142426823');
setConversationsList(data);
if (data && data.length) {
switchConversation(data[0]);
}
};
return () => {};
}, [data]);
const templates = AllTemplates.filter(_t => _t.status !== 'REJECTED').map( ele => ({...ele, components: groupBy(ele.components, _c => _c.type.toLowerCase())})); // test: 0
// const [templates, setTemplates] = useState([]);
const [templatesList, setTemplatesList] = useState([]);
const getTemplates = async () => {
const data = await fetchJSON(`${API_HOST}/listtemplates`);
const canUseTemplates = (data?.result?.items || []).filter(_t => _t.status !== 'REJECTED');
setTemplatesList(canUseTemplates);
};
const getTemplates = () => {
setUrl(null); // reset url
setUrl(URL.templates);
}
const [customerOrderProfile, setCustomerProfile] = useState({});
const getCustomerProfile = async (customerId) => {
console.log(customerId, 'getCustomerProfile');
const data = await fetchJSON(`http://127.0.0.1:4523/m2/3888351-0-default/144062941`);
setCustomerProfile(data);
};
const switchConversation = (cc) => {
console.log('switch to ', cc.id, cc);
setCurrentID(cc.id);
setCurrentConversation(cc);
setMessages(conversations[cc.id] || []);
// debug: 0
const _all = all.map((ele) => whatsappMsgMapped['whatsapp.inbound_message.received'].contentToRender(ele));
setMessages([..._all,...conversations[cc.id] || []]);
// Get customer profile when switching conversation
getCustomerProfile(cc.id);
};
/**
* websocket --------------------------------------------------------------------------------------------
*/
const addError = (reason) => {
setErrors(prevErrors => [...prevErrors, { reason }]);
}
@ -65,36 +91,30 @@ export const useConversations = () => {
};
const addMessage = (message) => {
setMessages((prevMessages) => [...prevMessages, message]);
setMessages((prevMessages) => [ ...prevMessages, message]);
addMessageToConversations(currentConversation.id, message);
};
const handleMessage = (data) => {
const { errmsg, result: msgObj } = data;
const { errcode, errmsg, result } = data;
const msg = data.result;
if (!msg) {
if (!result) {
return false;
}
if (typeof msg.type === 'string' && msg.type === 'error') {
if (typeof result.type === 'string' && result.type === 'error') {
addError('Error Connecting to Server');
}
addMessage({ ...msg.message, sender: 'other', id: Date.now().toString(16) });
console.log(result, 'handleMessage------------------');
const msgObj = whatsappMsgMapped[result.type].getMsg(result);
console.log(msgObj, 'msgObj');
addMessage(whatsappMsgMapped[result.type].contentToRender(msgObj));
};
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) });
const sendMessage = (msgObj) => {
const contentToSend = sentMsgTypeMapped[msgObj.type].contentToSend(msgObj);
realtimeAPI.sendMessage(contentToSend);
const contentToRender = sentMsgTypeMapped[msgObj.type].contentToRender(msgObj);
addMessage(contentToRender);
};
realtimeAPI.onError(addError.bind(null, 'Error'));
@ -104,9 +124,273 @@ export const useConversations = () => {
return {
errors, messages, conversationsList, currentConversation, sendMessage,
fetchConversations, switchConversation,
templates, setTemplates, getTemplates,
getConversations, switchConversation,
// templates: templatesList, // setTemplates, getTemplates,
templates, // debug: 0
customerOrderProfile,
};
}
export const useConversationContext = () => useContext(ConversationContext);
// test: 0
const all = [
{
'id': '65b06828619a1d82777eb4c6',
'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBQzNDNzBFNjFCREJBNDIyQjQ2AA==',
'wabaId': '190290134156880',
'from': '+8613317835586',
'customerProfile': {
'name': 'qqs',
},
'to': '+8617607730395',
'sendTime': '2024-01-24T01:30:14.000Z',
'type': 'image',
'image': {
'link':
'https://api.ycloud.com/v2/whatsapp/media/download/934379820978291?sig=t%3D1706059814%2Cs%3D91a79a0e4007ad2f6a044a28307affe663f7f81903b3537bd80e758d3c0d0563&payload=eyJpZCI6IjkzNDM3OTgyMDk3ODI5MSIsIndhYmFJZCI6IjE5MDI5MDEzNDE1Njg4MCIsImluYm91bmRNZXNzYWdlSWQiOiI2NWIwNjgyODYxOWExZDgyNzc3ZWI0YzYiLCJtaW1lVHlwZSI6ImltYWdlL2pwZWciLCJzaGEyNTYiOiJPVTJjdkN2eHplMUdMMmQ5NUxyTGVaNmpNb2ZscUZYM1RvcXdTTUNWZkxNPSJ9',
'id': '934379820978291',
'sha256': 'OU2cvCvxze1GL2d95LrLeZ6jMoflqFX3ToqwSMCVfLM=',
'mime_type': 'image/jpeg',
},
},
{
'id': '65b06ce6619a1d8277c97fc0',
'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBMUJBOUZCODY4NkNBMkM2NUEzAA==',
'wabaId': '190290134156880',
'from': '+8613317835586',
'customerProfile': {
'name': 'qqs',
},
'to': '+8617607730395',
'sendTime': '2024-01-24T01:50:29.000Z',
'type': 'text',
'text': {
'body': 'eeee',
},
},
{
'id': '65b06b2f619a1d8277b5ab06',
'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBRkU0RUZGRUI1OUQzQUFBMEExAA==',
'wabaId': '190290134156880',
'from': '+8613317835586',
'customerProfile': {
'name': 'qqs',
},
'to': '+8617607730395',
'sendTime': '2024-01-24T01:43:09.000Z',
'type': 'audio',
'audio': {
'link':
'https://api.ycloud.com/v2/whatsapp/media/download/901696271448320?sig=t%3D1706060589%2Cs%3Dca75dbd57e4867783390c913491263f07c9738d69c141d4ae622c76df9fa033b&payload=eyJpZCI6IjkwMTY5NjI3MTQ0ODMyMCIsIndhYmFJZCI6IjE5MDI5MDEzNDE1Njg4MCIsImluYm91bmRNZXNzYWdlSWQiOiI2NWIwNmIyZjYxOWExZDgyNzdiNWFiMDYiLCJtaW1lVHlwZSI6ImF1ZGlvL29nZzsgY29kZWNzPW9wdXMiLCJzaGEyNTYiOiJoZUNSUDdEMjM3bG9ydkZ4eFhSdHZpU1ZsNDR3Rlk4TytaMFhic2k5cy9rPSJ9',
'id': '901696271448320',
'sha256': 'heCRP7D237lorvFxxXRtviSVl44wFY8O+Z0Xbsi9s/k=',
'mime_type': 'audio/ogg; codecs=opus',
},
},
{
'id': '65b06b12619a1d8277b3c0c4',
'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBREZEMEM0MURDNjJGREVEQjY3AA==',
'wabaId': '190290134156880',
'from': '+8613317835586',
'customerProfile': {
'name': 'qqs',
},
'to': '+8617607730395',
'sendTime': '2024-01-24T01:42:40.000Z',
'type': 'video',
'video': {
'link':
'https://api.ycloud.com/v2/whatsapp/media/download/742404324517058?sig=t%3D1706060560%2Cs%3D53eeb1508c2103e310fb14a72563a8e07c5a84c7e6192a25f3608ac9bea32334&payload=eyJpZCI6Ijc0MjQwNDMyNDUxNzA1OCIsIndhYmFJZCI6IjE5MDI5MDEzNDE1Njg4MCIsImluYm91bmRNZXNzYWdlSWQiOiI2NWIwNmIxMjYxOWExZDgyNzdiM2MwYzQiLCJtaW1lVHlwZSI6InZpZGVvL21wNCIsInNoYTI1NiI6IlNJcjRlZFlPb1BDTGtETEgrVTY2d3dkMDgra2JndFV5OHRDd2RjQU5FaFU9In0',
'caption': 'and',
'id': '742404324517058',
'sha256': 'SIr4edYOoPCLkDLH+U66wwd08+kbgtUy8tCwdcANEhU=',
'mime_type': 'video/mp4',
},
},
{
'id': '65b06aa7619a1d8277ac806e',
'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBOTFBOTU5RDE2QjgxQTQ1MEE2AA==',
'wabaId': '190290134156880',
'from': '+8613317835586',
'customerProfile': {
'name': 'qqs',
},
'to': '+8617607730395',
'sendTime': '2024-01-24T01:40:53.000Z',
'type': 'sticker',
'sticker': {
'link':
'https://api.ycloud.com/v2/whatsapp/media/download/1156118002042289?sig=t%3D1706060453%2Cs%3Dfbd5f881856614e35715b1e3e1097b3bbe56f8a36aaa67bfbef25a37d9143d51&payload=eyJpZCI6IjExNTYxMTgwMDIwNDIyODkiLCJ3YWJhSWQiOiIxOTAyOTAxMzQxNTY4ODAiLCJpbmJvdW5kTWVzc2FnZUlkIjoiNjViMDZhYTc2MTlhMWQ4Mjc3YWM4MDZlIiwibWltZVR5cGUiOiJpbWFnZS93ZWJwIiwic2hhMjU2IjoibUNaLzdhNnNaNlRNYTE0WW9rUkNTZnVsdGpZNmFRRVZFNVoxMVRwanNQOD0ifQ',
'id': '1156118002042289',
'sha256': 'mCZ/7a6sZ6TMa14YokRCSfultjY6aQEVE5Z11TpjsP8=',
'mime_type': 'image/webp',
'animated': false,
},
},
{
'id': '65b06a91619a1d8277aaf05e',
'wamid': 'wamid.HBgNODYxMzMxNzgzNTU4NhUCABIYFDNBRjUxNzdCQ0FEOTlFQzc5MzQ1AA==',
'wabaId': '190290134156880',
'from': '+8613317835586',
'customerProfile': {
'name': 'qqs',
},
'to': '+8617607730395',
'sendTime': '2024-01-24T01:40:32.000Z',
'type': 'unsupported',
'errors': [
{
'code': '131051',
'title': 'Message type unknown',
'message': 'Message type unknown',
'error_data': {
'details': 'Message type is currently not supported.',
},
},
],
},
];
const AllTemplates = [
{
"wabaId": "190290134156880",
"name": "say_hi",
"language": "en",
"components": [
{
"type": "BODY",
"text": "Hi {{customer_name}} I'm {{your_name}} from Asia Highlights.\n\nWe provide *families* and *couples* with personalized and stress-free experiences, whether for _milestone trips_, _birthday trips_, _graduation trips_, or _bucketlist trips_.",
"example": {
"body_text": [
[
"Mike",
"Jimmy"
]
]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "Asia Highlights",
"url": "https://www.asiahighlights.com/"
}
]
}
],
"category": "UTILITY",
"status": "REJECTED",
"qualityRating": "UNKNOWN",
"reason": "INCORRECT_CATEGORY",
"createTime": "2024-01-23T02:26:33.012Z",
"updateTime": "2024-01-23T02:26:35.397Z",
"statusUpdateEvent": "REJECTED"
},
{
"wabaId": "190290134156880",
"name": "i_hope_this_message_finds_you_well",
"language": "en",
"components": [
{
"type": "BODY",
"text": "Hi {{customer_name}}, I hope this message finds you well. Did you see my previous message?",
"example": {
"body_text": [
[
"Mike"
]
]
}
}
],
"category": "UTILITY",
"status": "REJECTED",
"qualityRating": "UNKNOWN",
"reason": "INCORRECT_CATEGORY",
"createTime": "2024-01-23T02:22:20.232Z",
"updateTime": "2024-01-23T02:22:22.937Z",
"statusUpdateEvent": "REJECTED"
},
{
"wabaId": "190290134156880",
"name": "asia_highlights_has_receive_your_inquiry",
"language": "en",
"components": [
{
"type": "BODY",
"text": "Dear {{customer_name}},\n\nThank you for choosing Asia Highlights. Your inquiry has been submitted to Asia Highlights. One of our travel advisors will respond within 24 hours.",
"example": {
"body_text": [
[
"Jimmy Liow"
]
]
}
},
{
"type": "HEADER",
"format": "TEXT",
"text": "Asia highlights has receive your inquiry"
},
{
"type": "FOOTER",
"text": "Kind regards, Asia Highlights Team"
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "Asia Highlights",
"url": "https://www.asiahighlights.com/"
}
]
}
],
"category": "UTILITY",
"status": "APPROVED",
"qualityRating": "UNKNOWN",
"reason": "NONE",
"createTime": "2024-01-19T05:59:32.933Z",
"updateTime": "2024-01-19T05:59:55.581Z",
"statusUpdateEvent": "APPROVED"
},
{
"wabaId": "190290134156880",
"name": "hello",
"language": "zh_CN",
"components": [
{
"type": "BODY",
"text": "你好,这是一个测试程序"
},
{
"type": "HEADER",
"format": "TEXT",
"text": "Hello 同学"
},
{
"type": "FOOTER",
"text": "Global Highlights"
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "about us",
"url": "https://www.globalhighlights.com/"
}
]
}
],
"category": "MARKETING",
"status": "APPROVED",
"qualityRating": "UNKNOWN",
"reason": "NONE",
"createTime": "2023-11-17T03:26:10.961Z",
"updateTime": "2023-11-17T13:36:33.623Z",
"statusUpdateEvent": "APPROVED"
}
];

@ -1,78 +1,67 @@
import { useEffect } from 'react';
import { useEffect, useState } 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 LocalTimeClock from './Components/LocalTimeClock';
import { useConversationContext } from '@/stores/ConversationContext';
import './Conversations.css';
import { useAuthContext } from '@/stores/AuthContext.js';
import dayjs from 'dayjs';
import utc from 'dayjs-plugin-utc';
dayjs.extend(utc);
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();
const { sendMessage, currentConversation, customerOrderProfile: orderInfo } = useConversationContext();
const { quotes, contact, last_contact, ...order } = orderInfo;
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' }}>
<Layout className='full-height h-full chatwindow-wrapper' style={{ maxHeight: 'calc(100% - 198px)', height: 'calc(100% - 198px)' }}>
<Sider width={240} theme={'light'} className='scrollable-column' style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<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' }}>
<Content style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<Layout className='h-full'>
<Header className='ant-layout-sider-light ant-card p-1 h-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={'1'} justify='space-between'>
<Flex vertical={true} justify='space-between'>
<Typography.Text strong>{currentConversation.name}</Typography.Text>
{/* <Typography.Text>{contact?.phone}</Typography.Text> */}
</Flex>
<Flex vertical={true} justify='space-between'>
<Typography.Text ><LocalTimeClock /> <Typography.Text strong>{order?.location} </Typography.Text></Typography.Text>
{/* <Typography.Text>{customerDateTime}</Typography.Text> */}
</Flex>
</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' }}>
<Content style={{ maxHeight: '74vh', height: '74vh' }}>
<div className='scrollable-column'>
<Messages />
</div>
</Content>
<Footer className='ant-layout-sider-light' style={{ padding: '10px' }}>
<Footer className='ant-layout-sider-light p-1'>
<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' }}>
<Sider width={300} theme={'light'} className='scrollable-column' style={{ maxHeight: 'calc(100vh - 198px)', height: 'calc(100vh - 198px)' }}>
<CustomerProfile customer={{}} />
</Sider>
</Layout>

@ -3,12 +3,13 @@ import { observer } from 'mobx-react';
import { List, Avatar, Flex } from 'antd';
import { useConversationContext } from '@/stores/ConversationContext';
import { ChatItem, ChatList } from 'react-chat-elements';
import { useGetJson } from '@/hooks/userFetch';
/**
* []
*/
const Conversations = observer(({ conversations }) => {
const { switchConversation, conversationsList } = useConversationContext();
console.log(conversationsList);
// console.log(conversationsList);
const [chatlist, setChatlist] = useState([]);
useEffect(() => {
setChatlist(
@ -26,9 +27,13 @@ const Conversations = observer(({ conversations }) => {
return () => {};
}, [conversationsList]);
const onSwitchConversation = (item) => {
switchConversation(item);
}
return (
<>
<ChatList className='chat-list' dataSource={chatlist} onClick={(item) => switchConversation(item)} />
<ChatList className='chat-list' dataSource={chatlist} onClick={(item) => onSwitchConversation(item)} />
{/* <List
dataSource={conversationsList || []}
renderItem={(item, ii) => (

@ -0,0 +1,33 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { observer } from "mobx-react";
import { Popover, Flex, Button } from 'antd';
const CreatePayment = observer((props) => {
const [open, setOpen] = useState(false);
const handleOpenChange = (newOpen) => {
setOpen(newOpen);
};
const onSend = () => {
setOpen(false);
// todo: send
}
return (
<>
<Popover
// content={<a onClick={hide}>Close</a>}
content={
<Flex></Flex>
}
title='🔗付款链接'
trigger='click'
open={open}
onOpenChange={handleOpenChange}>
{/* <Button type="primary">Click me</Button> */}
{/* <Button type='primary' shape='circle' icon={<></>} size={'large'} /> */}
<Button size={'small'}>Book</Button>
</Popover>
</>
);
});
export default CreatePayment;

@ -1,8 +1,17 @@
import { observer } from 'mobx-react';
import { Card, Flex, Avatar, Typography, Radio, Button } from 'antd';
import { Card, Flex, Avatar, Typography, Radio, Button, Table } from 'antd';
import { useAuthContext } from '@/stores/AuthContext.js';
import { useConversationContext } from '@/stores/ConversationContext';
import { useGetJson } from '@/hooks/userFetch';
import {
HomeOutlined,
LoadingOutlined,
SettingFilled,
SmileOutlined,
SyncOutlined,PhoneOutlined,MailOutlined
} from '@ant-design/icons';
import CreatePayment from './CreatePayment';
import QuotesHistory from './QuotesHistory';
const orderTags = [
{ value: 'potential', label: '潜力' },
@ -20,33 +29,34 @@ const orderStatus = [
const { Meta } = Card;
const CustomerProfile = observer(({ customer }) => {
const { errors } = useConversationContext();
const { errors, customerOrderProfile: orderInfo } = 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 || {};
const { quotes, contact, last_contact, ...order } = orderInfo;
return (
<div className=' divide-x-0 divide-y divide-dotted divide-slate-400/[.24]'>
<Flex vertical>
</Flex>
<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={' '}
<Meta className='py-2 text-right'
// title={<Radio.Group size={'small'} options={orderStatus} value={'pending'} onChange={({ target: { value } }) => {}} optionType='button' buttonStyle={'solid'} />}
description={<Radio.Group size={'small'} options={orderStatus} value={'pending'} onChange={({ target: { value } }) => {}} optionType='button' buttonStyle={'solid'} />}
/>
<Flex gap={16}>
<Flex gap={10}>
<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>
<Typography.Text ><PhoneOutlined className=' pr-1' />{contact?.phone}</Typography.Text>
<Typography.Text ><MailOutlined className=' pr-1' />{contact?.email}</Typography.Text>
{/* <div>{order?.order_no}</div> */}
<div>
{/* <div>
{order?.location} <span>{order?.local_datetime}</span>
</div>
<div></div>
<div></div> */}
</Flex>
</Flex>
</Card>
@ -54,14 +64,14 @@ const CustomerProfile = observer(({ customer }) => {
<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>
<CreatePayment />
<QuotesHistory />
</Flex>
</Flex>
<p className='p-2 overflow-auto h-40 '>{order?.order_detail}</p>
<p className='p-2 overflow-auto h-32 '>{order?.order_detail}</p>
<Flex vertical={true} className='p-2 '>
<div>沟通记录</div>
<p className='m-0 py-2 '>{quotes?.[0]?.name}</p>
<Table size={'small'} columns={[{ title: '进程', dataIndex: 'title' }, { title: '状态', dataIndex: 'title2' },]} />
</Flex>
</div>
);

@ -1,45 +1,80 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Input, Button } from 'antd';
import { Input, Button, Tabs, List, Space, Popover, Flex } from 'antd';
// import { Input } from 'react-chat-elements';
import { useConversationContext } from '@/stores/ConversationContext';
import { LikeOutlined, MessageOutlined, StarOutlined, SendOutlined, PlusOutlined, PlusCircleOutlined } from '@ant-design/icons';
const InputBox = observer(({ onSend }) => {
const [message, setMessage] = useState('');
const { templates } = useConversationContext();
const [textContent, setTextContent] = useState('');
console.log( 'ttt');
const onOK = () => {
// console.log(message);
if (typeof onSend === 'function' && message.trim() !== '') {
const handleSendText = () => {
// console.log(textContent);
if (typeof onSend === 'function' && textContent.trim() !== '') {
const msgObj = {
type: 'text',
text: {
body: message,
},
// contentType: 'text/markdown',
text: textContent,
sender: 'me',
from: '',
id: Date.now().toString(16),
date: new Date(),
readState: false,
};
onSend(msgObj);
setMessage('');
setTextContent('');
}
};
const handleSendTemplate = () => { };
const [openTemplates, setOpenTemplates] = useState(false);
const handleOpenChange = (newOpen) => {
setOpenTemplates(newOpen);
};
const onSendTemplates = () => {
setOpenTemplates(false);
// todo: send
}
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();
<Flex gap={8}>
<Popover
content={
<List className='w-96 h-4/6 overflow-y-auto text-slate-900'
itemLayout='horizontal'
dataSource={templates}
renderItem={(item, index) => (
<List.Item
actions={[
<Button onClick={onSendTemplates} size={'small'} type='link' key={'send'} icon={<SendOutlined />}>
Send
</Button>,
]}>
<List.Item.Meta className=' text-neutral-800' title={item.components.header?.[0]?.text || item.name} description={item.components.body?.[0]?.text} />
</List.Item>
)}
/>
}
}}
rightButtons={<button onClick={onOK}>Send</button>}
/> */}
title='📋模板消息'
trigger='click'
open={openTemplates}
onOpenChange={handleOpenChange}>
{/* <Button type="primary">Click me</Button> */}
<Button type='primary' shape='circle' icon={<MessageOutlined />} size={'large'} />
</Popover>
<Input.Search
placeholder='Type message here'
enterButton='Send'
size='large'
onSearch={handleSendText}
value={textContent}
onChange={(e) => setTextContent(e.target.value)}
/>
</Flex>
<div></div>
</div>
);
});

@ -0,0 +1,39 @@
import { useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Typography } from 'antd';
import { useConversationContext } from '@/stores/ConversationContext';
const LocalTimeClock = observer((props) => {
const { customerOrderProfile: orderInfo } = useConversationContext();
const [customerDateTime, setCustomerDateTime] = useState();
// todo: dayjs
useEffect(() => {
const intervalId = setInterval(() => {
// if (customerProfile && customerProfile.timezone) {
const date = new Date();
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
hour12: true,
};
const formatter = new Intl.DateTimeFormat('cn-ZH', options); // todo:
setCustomerDateTime(formatter.format(date));
// }
}, 1000); // Update every second
// Cleanup function to clear the interval when the component is unmounted
return () => clearInterval(intervalId);
}, []);
return (
<>
<Typography.Text>{customerDateTime}</Typography.Text>
</>
);
});
export default LocalTimeClock;

@ -1,45 +1,118 @@
import { useEffect } from 'react';
import { useEffect, useState, useRef } from 'react';
import { observer } from 'mobx-react';
import { List, Avatar, Timeline } from 'antd';
import { List, Avatar, Timeline, Image } 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 msgTypeMapped = {
text: { type: 'text', data: (msg) => ({ text: msg.text.body }) },
image: {
type: 'photo',
data: (msg) => ({
data: {
uri: msg.image.link,
width: 200,
height: 200,
alt: '',
},
onOpen: () => {
console.log('Open image', msg.image.link);
},
}),
},
sticker: {
type: 'photo',
data: (msg) => ({
data: {
uri: msg.sticker.link,
width: 150,
height: 120,
alt: '',
},
}),
},
video: {
type: 'video',
data: (msg) => ({
data: {
videoURL: msg.video.link,
status: {
click: true,
loading: 0,
download: true,
},
},
}),
},
audio: {
type: 'audio',
data: (msg) => ({
data: {
audioURL: msg.audio.link,
},
}),
},
'unsupported': { type: 'system', data: (msg) => ({ text: 'Message type is currently not supported.' }) },
// 'unsupported': { type: 'text', data: (msg) => ({ text: 'Message type is currently not supported.' }) }
// file: 'file',
// location: 'location',
// contact: 'contact',
// 'contact-card': 'contact-card',
// 'contact-card-with-photo': 'contact-card-with-photo',
// 'contact-card-with-photo-and-label': 'contact-card-with-photo-and-label',
};
const parseMessage = (messages) => {
return messages.map((msg) => {
return {
...(msgTypeMapped?.[msg.type]?.data(msg) || {}),
id: msg.id,
sender: msg.from,
type: msgTypeMapped?.[msg.type]?.type || 'text',
// title: msg.customerProfile.name,
date: msg.sendTime,
};
});
};
const Messages = observer(() => {
const { messages } = useConversationContext()
const { messages: messagesList } = useConversationContext();
// const messagesList = parseMessage(messages);
// console.log(messagesList);
const messagesEndRef = useRef(null);
useEffect(() => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}, [messagesList.length]);
const [previewVisible, setPreviewVisible] = useState(false);
const [previewSrc, setPreviewSrc] = useState();
const onPreviewClose = () => {
setPreviewSrc('');
setPreviewVisible(false);
};
const handlePreview = (msg) => {
if (msg.type !== 'photo') {
return false;
}
setPreviewVisible(true);
setPreviewSrc(msg.data.uri);
};
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>
)}
/> */}
</>
<div>
{messagesList.map((message, index) => (
<MessageBox
key={message.id}
position={message.sender === 'me' ? 'right' : 'left'}
{...message}
onOpen={() => handlePreview(message)}
status={message.sender === 'me' ? 'sent' : ''}
// read | 'waiting'| 'sent' | 'received' | 'read'
/>
))}
<Image src={previewSrc} preview={{ visible: previewVisible, src: previewSrc, onClose: onPreviewClose }} />
<div ref={messagesEndRef}></div>
</div>
);
});

@ -0,0 +1,63 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { observer } from 'mobx-react';
import { Popover, Flex, Button, List, Popconfirm } from 'antd';
const CreatePayment = observer((props) => {
const [open, setOpen] = useState(false);
const handleOpenChange = (newOpen) => {
setOpen(newOpen);
};
const onSend = () => {
setOpen(false);
// todo: send
};
return (
<>
<Popover
// content={<a onClick={hide}>Close</a>}
content={
<List
className='w-96 h-4/6 overflow-y-auto text-slate-900'
itemLayout='horizontal'
dataSource={[
{ key: 1, title: 'XXX' },
{ key: 11, title: 'ZZZZ' },
]}
renderItem={(item, index) => (
<List.Item className=''>
<List.Item.Meta
className=' text-neutral-800'
title={item.title}
description={
<Flex justify='space-between'>
<Button onClick={onSend} size={'small'} type='link' key={'send'}>
详细报价
</Button>
<Flex gap={8}>
<Popconfirm title='删除报价信' description='确认要删除报价信吗?' onConfirm={() => {}} onCancel={onSend} okText='Yes' cancelText='No'>
<Button size={'small'} type='link' danger key={'send'}>
删除
</Button>
</Popconfirm>
<Button onClick={onSend} size={'small'} type='link' key={'send'}>
复制
</Button>
</Flex>
</Flex>
}
/>
</List.Item>
)}
/>
}
title='📜报价信历史'
trigger='click'
placement={'left'}
open={open}
onOpenChange={handleOpenChange}>
<Button size={'small'}>报价历史</Button>
</Popover>
</>
);
});
export default CreatePayment;

@ -1,4 +1,4 @@
import { ConversationContext, useConversations } from '@/stores/ConversationContext';
import { ConversationContext, useConversations, } from '@/stores/ConversationContext';
export const ConversationProvider = ({ children }) => {

@ -10,3 +10,16 @@
.column {
height: 100%;
}
.ant-card .ant-card-head{
padding: 0 .5em .5em .5em;
min-height: unset;
}
.ant-card .ant-card-body{
padding: .5em;
}
.chatwindow-wrapper .rce-mbox-time-block{
background: linear-gradient(0deg,#00000014,#0000);
color: #00000073;
}

Loading…
Cancel
Save