feat: 移动端

fix: 更新未读消息数;
style: 历史记录查询表单
perf: Emoji picker 使用native 避免下载图片
fix: 退出登录
perf: 历史记录: 会话显示Me的名字
dev/mobile
Lei OT 2 years ago
parent 6d3c40188c
commit 5b158f80e1

@ -87,6 +87,7 @@ export const fetchConversationsSearch = async (params) => {
...ele, ...ele,
customer_name: `${ele.whatsapp_name || ''}`.trim(), customer_name: `${ele.whatsapp_name || ''}`.trim(),
whatsapp_name: `${ele.whatsapp_name || ''}`.trim(), whatsapp_name: `${ele.whatsapp_name || ''}`.trim(),
OPI_Name: `${ele.OPI_Name || ''}`.trim(),
matchMsgList: parseRenderMessageList((ele.msglist_AsJOSN || [])), // .reverse()), matchMsgList: parseRenderMessageList((ele.msglist_AsJOSN || [])), // .reverse()),
})); }));
return list; return list;

@ -36,7 +36,7 @@ function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
optionFilterProp='label' optionFilterProp='label'
> >
{options.map((d) => ( {options.map((d) => (
<Select.Option key={d.value} title={d.label}> <Select.Option key={`${d.value}${d.label}`} title={d.label}>
{d.label} {d.label}
</Select.Option> </Select.Option>
))} ))}

@ -29,7 +29,7 @@ const router = createBrowserRouter([
element: <AuthApp />, element: <AuthApp />,
errorElement: <ErrorPage />, errorElement: <ErrorPage />,
children: [ children: [
{ {
element: <DesktopApp />, element: <DesktopApp />,
children: [ children: [
{ index: true, element: <OrderFollow /> }, { index: true, element: <OrderFollow /> },
@ -41,11 +41,12 @@ const router = createBrowserRouter([
{ path: 'account/profile', element: <AccountProfile /> }, { path: 'account/profile', element: <AccountProfile /> },
] ]
}, },
{ {
path: 'm', path: 'm',
element: <MobileApp />, element: <MobileApp />,
children: [ children: [
{ path: 'conversation', element: <MobileConversation /> }, { path: 'conversation', element: <MobileConversation /> },
{ path: 'chat/:order_sn', element: <MobileChat /> },
{ path: 'chat', element: <MobileChat /> }, { path: 'chat', element: <MobileChat /> },
] ]
}, },

@ -32,12 +32,16 @@ const initialConversationState = {
// templates: [], // templates: [],
// conversationsList: [], // 对话列表 conversationsList: [], // 对话列表
// currentConversation: {}, // 当前对话 currentConversation: {}, // 当前对话
// activeConversations: {}, // 激活的对话的消息列表: { [conversationId]: <messageItem>[] } activeConversations: {}, // 激活的对话的消息列表: { [conversationId]: <messageItem>[] }
// referenceMsg: {}, referenceMsg: {},
complexMsg: {},
totalNotify: 0,
msgListLoading: false,
}; };
@ -219,6 +223,7 @@ const messageSlice = (set, get) => ({
totalNotify: 0, totalNotify: 0,
msgListLoading: false, msgListLoading: false,
activeConversations: {}, activeConversations: {},
refreshTotalNotify: () => set((state) => ({ totalNotify: state.conversationsList.reduce((r, c) => r+c.unread_msg_count, 0) })),
setMsgLoading: (msgListLoading) => set({ msgListLoading }), setMsgLoading: (msgListLoading) => set({ msgListLoading }),
receivedMessageList: (conversationid, msgList) => receivedMessageList: (conversationid, msgList) =>
set((state) => ({ set((state) => ({
@ -343,15 +348,9 @@ export const useConversationStore = create(
setInitial(true); setInitial(true);
// const autoGetMsgs = conversationsList.length > 5 ? 5 : conversationsList.length;
// for (let index = 0; index < autoGetMsgs; index++) {
// const chatItem = conversationsList[index];
// const msgData = await fetchMessages({ opisn: chatItem.opi_sn, whatsappid: chatItem.whatsapp_phone_number });
// receivedMessageList(chatItem.sn, msgData);
// }
// for (const chatItem of conversationsList) {
// }
}, },
reset: () => set(initialConversationState),
})) }))
); );

@ -52,7 +52,8 @@ function AuthApp() {
theme={{ theme={{
token: { token: {
colorPrimary: colorPrimary, colorPrimary: colorPrimary,
borderRadius: borderRadius borderRadius: borderRadius,
fontFamily: "-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Noto Color Emoji','Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol'",
}, },
algorithm: theme.defaultAlgorithm, algorithm: theme.defaultAlgorithm,
}} }}

@ -35,32 +35,33 @@ const SearchForm = memo(function ({ initialValues, onSubmit }) {
}); });
} }
return ( return (
<Form <Form layout={'inline'} form={form} initialValues={initialValues} onFinish={handleSubmit} style={{}}>
layout={'inline'} <Flex className='w-full'>
form={form} <Flex flex={'auto'} wrap='wrap' gap={4}>
initialValues={initialValues} <Form.Item label='发送人' name='agent' style={{ width: '200px' }} rules={[{ required: false, message: '请选择发送人' }]}>
onFinish={handleSubmit} <SearchInput placeholder='搜索发送人' fetchOptions={fetchSalesAgent} mode={'tags'} maxTagCount={0} />
style={{}}> </Form.Item>
<Form.Item label='顾问' name='agent' style={{ width: '200px' }} rules={[{required: true, message: '请选择顾问'}]}> <Form.Item label='客人' name='customer' style={{ width: '200px' }}>
<SearchInput placeholder='搜索顾问' fetchOptions={fetchSalesAgent} mode={'tags'} maxTagCount={0} /> <SearchInput placeholder='搜索客人' fetchOptions={fetchCustomerList} mode={'tags'} maxTagCount={0} />
</Form.Item> </Form.Item>
<Form.Item label='客人' name='customer' style={{ width: '200px' }}> <Form.Item label='订单号' name='coli_id'>
<SearchInput placeholder='搜索客人' fetchOptions={fetchCustomerList} mode={'tags'} maxTagCount={0} /> <Input placeholder='订单号' allowClear />
</Form.Item> </Form.Item>
<Form.Item label='订单号' name='coli_id'> <Form.Item label='关键词' name='search'>
<Input placeholder='订单号' allowClear /> <Input placeholder='关键词' allowClear />
</Form.Item> </Form.Item>
<Form.Item label='关键词' name='search'> <Form.Item label='日期' name='msgDateRange'>
<Input placeholder='关键词' allowClear /> <RangePicker format={'YYYY-MM-DD'} />
</Form.Item> </Form.Item>
<Form.Item label='日期' name='msgDateRange'> </Flex>
<RangePicker format={'YYYY-MM-DD'} /> <div style={{flex: '0 1 64px'}}>
</Form.Item> <Form.Item>
<Form.Item> <Button type='primary' htmlType='submit'>
<Button type='primary' htmlType='submit'> 搜索
搜索 </Button>
</Button> </Form.Item>
</Form.Item> </div>
</Flex>
</Form> </Form>
); );
}); });
@ -89,7 +90,7 @@ function ChatHistory() {
const allEmpty = Object.values(cloneDeep(formValues)).every((val) => { const allEmpty = Object.values(cloneDeep(formValues)).every((val) => {
return val === null || val === '' || val === undefined; return val === null || val === '' || val === undefined;
}); });
if (allEmpty) return; // if (allEmpty) return;
setConversationsListLoading(true); setConversationsListLoading(true);
setChatItemMessages([]); setChatItemMessages([]);
setParamsForMsgList({}); setParamsForMsgList({});
@ -224,11 +225,11 @@ function ChatHistory() {
return prev; return prev;
}, []); }, []);
return ( return (
<span className={`text-base leading-5 emoji-text ${className} ${extraClass} `}> <span className={`text-sm leading-5 emoji-text ${className} ${extraClass} `}>
{(objArr || []).map((part, index) => { {(objArr || []).map((part, index) => {
if (part.type === 'link') { if (part.type === 'link') {
return ( return (
<a href={part.key} target='_blank' key={`${part.key}${index}`} rel='noreferrer' className='text-base'> <a href={part.key} target='_blank' key={`${part.key}${index}`} rel='noreferrer' className='text-sm'>
{part.key} {part.key}
</a> </a>
); );
@ -284,8 +285,9 @@ function ChatHistory() {
letterItem={{ id: item.whatsapp_name || item.whatsapp_phone_number, letter: (item.whatsapp_name || item.whatsapp_phone_number).split(" ")[0] }} letterItem={{ id: item.whatsapp_name || item.whatsapp_phone_number, letter: (item.whatsapp_name || item.whatsapp_phone_number).split(" ")[0] }}
alt={`${item.whatsapp_name}`} alt={`${item.whatsapp_name}`}
title={item.whatsapp_name || item.whatsapp_phone_number} title={item.whatsapp_name || item.whatsapp_phone_number}
subtitle={item.coli_id} subtitle={`${item.OPI_Name || ''} ${item.coli_id || ''}`}
date={item.last_received_time} date={item.last_received_time}
// dateString={item.last_received_time}
className={String(item.conversationid) === String(selectedConversation.conversationid) ? '__active text-primary bg-neutral-100' : ''} className={String(item.conversationid) === String(selectedConversation.conversationid) ? '__active text-primary bg-neutral-100' : ''}
onClick={() => setSelectedConversation(item)} onClick={() => setSelectedConversation(item)}
/> />
@ -300,7 +302,7 @@ function ChatHistory() {
{...item} {...item}
key={item.sn} key={item.sn}
id={item.sn} id={item.sn}
letterItem={{ id: item.senderName, letter: (item.senderName).split(" ")[0] }} letterItem={{ id: (item.sender === 'me' ? (selectedConversation.OPI_Name || item.senderName) : item.senderName), letter: (item.sender === 'me' ? (selectedConversation.OPI_Name || item.senderName) : item.senderName).split(" ")[0] }}
alt={`${item.senderName}`} alt={`${item.senderName}`}
title={item.senderName} title={item.senderName}
subtitle={item.originText} subtitle={item.originText}
@ -362,7 +364,7 @@ function ChatHistory() {
: {})} : {})}
renderAddCmp={ renderAddCmp={
<div className='border-dashed border-0 border-t border-slate-300 text-slate-600 space-x-2 emoji'> <div className='border-dashed border-0 border-t border-slate-300 text-slate-600 space-x-2 emoji'>
<span className={`p-1 rounded-b ${message.msg_direction === 'outbound' ? 'text-white' : ''} `} style={{backgroundColor: message.msg_direction === 'outbound' ? stringToColour(message.senderName) : 'unset'}}>{message.senderName}</span> <span className={`p-1 rounded-b ${message.msg_direction === 'outbound' ? 'text-white' : ''} `} style={{backgroundColor: message.msg_direction === 'outbound' ? stringToColour(message.senderName) : 'unset'}}>{selectedConversation.OPI_Name || message.senderName}</span>
<span>{message.dateString || message.localDate}</span> <span>{message.dateString || message.localDate}</span>
<span>{message.statusCN}</span> <span>{message.statusCN}</span>
</div> </div>

@ -10,7 +10,8 @@ import useAuthStore from '@/stores/AuthStore';
/** /**
* [] * []
*/ */
const Conversations = () => { const Conversations = ({ mobile }) => {
const routerReplace = mobile === undefined ? false : true;
const { state: orderRow } = useLocation(); const { state: orderRow } = useLocation();
const { coli_guest_WhatsApp } = orderRow || {}; const { coli_guest_WhatsApp } = orderRow || {};
const { order_sn } = useParams(); const { order_sn } = useParams();
@ -22,8 +23,6 @@ const Conversations = () => {
const conversationsList = useConversationStore((state) => state.conversationsList); const conversationsList = useConversationStore((state) => state.conversationsList);
const addToConversationList = useConversationStore((state) => state.addToConversationList); const addToConversationList = useConversationStore((state) => state.addToConversationList);
const delConversationitem = useConversationStore((state) => state.delConversationitem); const delConversationitem = useConversationStore((state) => state.delConversationitem);
const receivedMessageList = useConversationStore((state) => state.receivedMessageList);
const setMsgLoading = useConversationStore((state) => state.setMsgLoading);
const [tabSelectedConversation, setTabSelectedConversation] = useState({}); const [tabSelectedConversation, setTabSelectedConversation] = useState({});
const [tabCnt, setTabCnt] = useState(-1); const [tabCnt, setTabCnt] = useState(-1);
@ -70,34 +69,16 @@ const Conversations = () => {
return false; return false;
} }
}; };
const getMessages = async (item) => {
setMsgLoading(true);
const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number, lasttime: '' });
setMsgLoading(false);
receivedMessageList(item.sn, data);
const thisLastTime = data.length > 0 ? data[0].orgmsgtime : '';
const loadNextPage = !(data.length === 0 || data.length < MESSAGE_PAGE_SIZE);
updateCurrentConversation({ lasttime: thisLastTime, loadNextPage });
};
useEffect(() => {
const messagesList = activeConversations[`${currentConversation.sn}`] || [];
if (currentConversation.sn && messagesList.length < 20) {
getMessages(currentConversation);
}
return () => {};
}, [currentConversation.sn]);
const onSwitchConversation = async (item) => { const onSwitchConversation = async (item) => {
setCurrentConversation(item); setCurrentConversation(item);
const routePrefix = mobile === undefined ? `/order/chat` : `/m/chat`;
if (isEmpty(item.coli_sn)) { if (isEmpty(item.coli_sn)) {
navigate(`/order/chat`, { replace: true }); navigate(routePrefix, { replace: true });
} else { } else {
setSwitchToC(item); setSwitchToC(item);
setShouldFetchCList(false); setShouldFetchCList(false);
navigate(`/order/chat/${item.coli_sn}`, { replace: true }); navigate(`${routePrefix}/${item.coli_sn}`, { replace: routePrefix });
} }
// if (!isEmpty(item.coli_sn)) { // if (!isEmpty(item.coli_sn)) {
// setSwitchToC(item); // setSwitchToC(item);
@ -113,7 +94,7 @@ const Conversations = () => {
await fetchConversationItemClose({ conversationid: item.sn, opisn: item.opi_sn }); await fetchConversationItemClose({ conversationid: item.sn, opisn: item.opi_sn });
delConversationitem(item); delConversationitem(item);
if (String(order_sn) === String(item.coli_sn)) { if (String(order_sn) === String(item.coli_sn)) {
navigate(`/order/chat`, { replace: true }); navigate(`/order/chat`, { replace: routerReplace });
} }
}; };

@ -2,7 +2,7 @@ import { useState } from 'react';
import { Popover, Button } from 'antd'; import { Popover, Button } from 'antd';
import EmojiPicker from 'emoji-picker-react'; import EmojiPicker from 'emoji-picker-react';
const InputTemplate = ({ disabled = false, inputEmoji }) => { const InputTemplate = ({ mobile, disabled = false, inputEmoji }) => {
const [openPopup, setOpenPopup] = useState(false); const [openPopup, setOpenPopup] = useState(false);
const handlePickEmoji = (emojiData) => { const handlePickEmoji = (emojiData) => {
@ -13,16 +13,16 @@ const InputTemplate = ({ disabled = false, inputEmoji }) => {
<> <>
<Popover <Popover
overlayClassName='p-0' overlayClassName='p-0'
placement={'right'} placement={mobile === undefined ? 'right' : 'top'}
overlayInnerStyle={{ padding: 0, borderRadius: '8px' }} overlayInnerStyle={{ padding: 0, borderRadius: '8px' }}
forceRender={true} forceRender={true}
content={<EmojiPicker skinTonesDisabled={true} emojiStyle='google' onEmojiClick={handlePickEmoji} />} content={<EmojiPicker skinTonesDisabled={true} emojiStyle='native' onEmojiClick={handlePickEmoji} className='chatwindow-wrapper' />}
// title='😀' // title='😃'
trigger='click' trigger='click'
open={openPopup} open={openPopup}
onOpenChange={setOpenPopup}> onOpenChange={setOpenPopup}>
<Button type='text' className=' px-1' size={'middle'} disabled={disabled}> <Button type='text' className=' px-1' size={'middle'} disabled={disabled}>
😀 😃
</Button> </Button>
</Popover> </Popover>
</> </>

@ -20,7 +20,7 @@ const splitTemplate = (template) => {
}, []); }, []);
return obj; return obj;
}; };
const InputTemplate = ({ disabled = false, invokeSendMessage }) => { const InputTemplate = ({ mobile, disabled = false, invokeSendMessage }) => {
const searchInputRef = useRef(null); const searchInputRef = useRef(null);
const { notification } = App.useApp(); const { notification } = App.useApp();
const loginUser = useAuthStore((state) => state.loginUser); const loginUser = useAuthStore((state) => state.loginUser);
@ -126,7 +126,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage }) => {
return ( return (
<> <>
<Popover <Popover
overlayClassName='w-3/5' overlayClassName={[mobile === undefined ? 'w-3/5' : 'w-full'].join(' ')}
fresh fresh
forceRender forceRender
destroyTooltipOnHide={true} destroyTooltipOnHide={true}

@ -46,14 +46,14 @@ const fileTypesExt = {
audio: ['aac', 'mp4', 'm4a', 'mp3', 'amr', 'ogg'], audio: ['aac', 'mp4', 'm4a', 'mp3', 'amr', 'ogg'],
}; };
const InputComposer = () => { const InputComposer = ({ mobile }) => {
const userId = useAuthStore(state => state.loginUser.userId); const userId = useAuthStore((state) => state.loginUser.userId);
const websocket = useConversationStore(state => state.websocket); const websocket = useConversationStore((state) => state.websocket);
const websocketOpened = useConversationStore(state => state.websocketOpened); const websocketOpened = useConversationStore((state) => state.websocketOpened);
const currentConversation = useConversationStore(state => state.currentConversation); const currentConversation = useConversationStore((state) => state.currentConversation);
const [referenceMsg, setReferenceMsg] = useConversationStore(state => [state.referenceMsg, state.setReferenceMsg]); const [referenceMsg, setReferenceMsg] = useConversationStore((state) => [state.referenceMsg, state.setReferenceMsg]);
const [complexMsg, setComplexMsg] = useConversationStore(state => [state.complexMsg, state.setComplexMsg]); const [complexMsg, setComplexMsg] = useConversationStore((state) => [state.complexMsg, state.setComplexMsg]);
const sentOrReceivedNewMessage = useConversationStore(state => state.sentOrReceivedNewMessage); const sentOrReceivedNewMessage = useConversationStore((state) => state.sentOrReceivedNewMessage);
const talkabled = !isEmpty(currentConversation.sn) && websocketOpened; const talkabled = !isEmpty(currentConversation.sn) && websocketOpened;
const gt24h = currentConversation.last_received_time ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') > 24 : true; const gt24h = currentConversation.last_received_time ? dayjs().diff(dayjs(currentConversation.last_received_time), 'hour') > 24 : true;
@ -120,15 +120,14 @@ const InputComposer = () => {
const contentToSend = sentMsgTypeMapped[msgObjMerge.type].contentToSend(msgObjMerge); const contentToSend = sentMsgTypeMapped[msgObjMerge.type].contentToSend(msgObjMerge);
// olog('invoke upload send +++ ', contentToSend) // olog('invoke upload send +++ ', contentToSend)
websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn }); websocket.sendMessage({ ...contentToSend, opi_sn: userId, coli_sn: currentConversation.coli_sn });
} };
const { message } = App.useApp(); const { message } = App.useApp();
const [pastedUploading, setPastedUploading] = useState(false); const [pastedUploading, setPastedUploading] = useState(false);
const readPasted = async (file, rename = false) => { const readPasted = async (file, rename = false) => {
// 使 FileReader // 使 FileReader
const reader = new FileReader(); const reader = new FileReader();
const suffix = file.name.slice(file.name.lastIndexOf('.')+1); const suffix = file.name.slice(file.name.lastIndexOf('.') + 1);
const newName = `${uuid()}.${suffix}`; // rename ? `${uuid()}.${suffix}` : file.name; const newName = `${uuid()}.${suffix}`; // rename ? `${uuid()}.${suffix}` : file.name;
const type = Object.keys(fileTypesExt).find((type) => fileTypesExt[type].includes(suffix)); const type = Object.keys(fileTypesExt).find((type) => fileTypesExt[type].includes(suffix));
const dataUri = aliOSSHost + newName; const dataUri = aliOSSHost + newName;
@ -161,7 +160,7 @@ const InputComposer = () => {
let isNotFile = true; let isNotFile = true;
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
// if (items[i].type.indexOf("image") !== -1) { // if (items[i].type.indexOf("image") !== -1) {
if (items[i].kind.indexOf("file") !== -1) { if (items[i].kind.indexOf('file') !== -1) {
isNotFile = false; isNotFile = false;
tmpfile = items[i].getAsFile(); tmpfile = items[i].getAsFile();
break; break;
@ -180,17 +179,17 @@ const InputComposer = () => {
setComplexMsg(_tmpFile.msgData); setComplexMsg(_tmpFile.msgData);
setPastedUploading(true); setPastedUploading(true);
const { file_url } = await postUploadFileItem(tmpfile, _tmpFile.newName); const { file_url } = await postUploadFileItem(tmpfile, _tmpFile.newName);
setPastedUploading(false); setPastedUploading(false); // todo: data uri
setComplexMsg({..._tmpFile.msgData, uploadStatus: file_url ? 'done' : 'error'}); setComplexMsg({ ..._tmpFile.msgData, uploadStatus: file_url ? 'done' : 'error' });
return; return;
} };
const focusInput = () => { const focusInput = () => {
textInputRef.current.focus({ cursor: 'end', preventScroll: true, }); textInputRef.current.focus({ cursor: 'end', preventScroll: true });
}; };
const addEmoji = emoji => { const addEmoji = (emoji) => {
setTextContent(prevValue => { setTextContent((prevValue) => {
return prevValue + emoji; return prevValue + emoji;
}); });
}; };
@ -248,7 +247,9 @@ const InputComposer = () => {
<Input.TextArea <Input.TextArea
onPaste={handlePaste} onPaste={handlePaste}
ref={textInputRef} ref={textInputRef}
size='large' maxLength={2000} showCount={textabled} size='large'
maxLength={2000}
showCount={textabled}
placeholder={gt24h ? 'This session has expired. Please send a template message to activate the session' : 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'} placeholder={gt24h ? 'This session has expired. Please send a template message to activate the session' : 'Enter 发送, Shift+Enter 换行\n支持复制粘贴 [截图/文件] 以备发送'}
rows={2} rows={2}
disabled={!textabled} disabled={!textabled}
@ -265,8 +266,8 @@ const InputComposer = () => {
/> />
<Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b'> <Flex justify={'space-between'} className=' bg-gray-200 p-1 rounded-b'>
<Flex gap={4} className='*:text-primary *:rounded-none'> <Flex gap={4} className='*:text-primary *:rounded-none'>
<InputTemplate key='templates' disabled={!talkabled || textabled} invokeSendMessage={invokeSendMessage} /> <InputTemplate key='templates' disabled={!talkabled || textabled} invokeSendMessage={invokeSendMessage} {...{ mobile }} />
<InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} /> <InputEmoji key='emoji' disabled={!textabled} inputEmoji={addEmoji} {...{ mobile }} />
<InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{ invokeUploadFileMessage, invokeSendUploadMessage }} /> <InputMediaUpload key={'addNewMedia'} disabled={!textabled} {...{ invokeUploadFileMessage, invokeSendUploadMessage }} />
{/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} /> {/* <Button type='text' className='' icon={<YoutubeOutlined />} size={'middle'} />
<Button type='text' className='' icon={<AudioOutlined />} size={'middle'} /> <Button type='text' className='' icon={<AudioOutlined />} size={'middle'} />

@ -45,11 +45,11 @@ const MessagesList = ({ messages, handlePreview, reference, longListLoading, get
return prev; return prev;
}, []); }, []);
return ( return (
<span className={`text-base leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`}> <span className={`text-sm leading-5 emoji-text whitespace-pre-wrap ${className} ${extraClass}`}>
{(objArr || []).map((part, index) => { {(objArr || []).map((part, index) => {
if (part.type === 'link') { if (part.type === 'link') {
return ( return (
<a href={part.key} target='_blank' key={`${part.key}${index}`} rel='noreferrer' className='text-base'> <a href={part.key} target='_blank' key={`${part.key}${index}`} rel='noreferrer' className='text-sm'>
{part.key} {part.key}
</a> </a>
); );

@ -6,21 +6,33 @@ import MessagesList from './MessagesList';
import { fetchCleanUnreadMsgCount, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions'; import { fetchCleanUnreadMsgCount, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions';
import { fetchOrderConversationsList, } from '@/actions/ConversationActions'; import { fetchOrderConversationsList, } from '@/actions/ConversationActions';
import { isEmpty } from '@/utils/utils'; import { isEmpty } from '@/utils/utils';
import useAuthStore from '@/stores/AuthStore';
const MessagesWrapper = () => { const MessagesWrapper = () => {
const userId = useAuthStore((state) => state.loginUser.userId);
const [currentConversation, updateCurrentConversation, setCurrentConversation] = useConversationStore(useShallow((state) => [state.currentConversation, state.updateCurrentConversation, state.setCurrentConversation])); const [currentConversation, updateCurrentConversation, setCurrentConversation] = useConversationStore(useShallow((state) => [state.currentConversation, state.updateCurrentConversation, state.setCurrentConversation]));
const conversationsList = useConversationStore(useShallow((state) => state.conversationsList)); const conversationsList = useConversationStore(useShallow((state) => state.conversationsList));
const activeMessages = useConversationStore(useShallow((state) => (state.currentConversation.sn && state.activeConversations[state.currentConversation.sn] ? state.activeConversations[state.currentConversation.sn]: []))); const activeMessages = useConversationStore(useShallow((state) => (state.currentConversation.sn && state.activeConversations[state.currentConversation.sn] ? state.activeConversations[state.currentConversation.sn]: [])));
const addToConversationList = useConversationStore((state) => state.addToConversationList); const addToConversationList = useConversationStore((state) => state.addToConversationList);
const receivedMessageList = useConversationStore((state) => state.receivedMessageList);
const setMsgLoading = useConversationStore((state) => state.setMsgLoading);
const refreshTotalNotify = useConversationStore(useShallow((state) => state.refreshTotalNotify));
const [longList, setLongList] = useState([]); const [longList, setLongList] = useState([]);
const [longListLoading, setLongListLoading] = useState(false); const [longListLoading, setLongListLoading] = useState(false);
const [shouldScrollBottom, setShouldScrollBottom] = useState(true); const [shouldScrollBottom, setShouldScrollBottom] = useState(true);
useEffect(() => { useEffect(() => {
setLongList(activeMessages); setLongList(activeMessages);
setShouldScrollBottom(true); setShouldScrollBottom(true);
if (currentConversation.sn && activeMessages.length < 20) {
getFirstPageMessages(currentConversation);
}
if (currentConversation.opi_sn && currentConversation.whatsapp_phone_number && activeMessages.length > 0) { if (currentConversation.opi_sn && currentConversation.whatsapp_phone_number && activeMessages.length > 0) {
fetchCleanUnreadMsgCount({ opisn: currentConversation.opi_sn, whatsappid: currentConversation.whatsapp_phone_number }); fetchCleanUnreadMsgCount({ opisn: currentConversation.opi_sn, whatsappid: currentConversation.whatsapp_phone_number });
refreshTotalNotify();
} }
const thisLastTime = activeMessages.length > 0 ? activeMessages[0].orgmsgtime : ''; const thisLastTime = activeMessages.length > 0 ? activeMessages[0].orgmsgtime : '';
const loadNextPage = !(activeMessages.length === 0 || activeMessages.length < MESSAGE_PAGE_SIZE); const loadNextPage = !(activeMessages.length === 0 || activeMessages.length < MESSAGE_PAGE_SIZE);
@ -29,6 +41,17 @@ const MessagesWrapper = () => {
return () => {}; return () => {};
}, [activeMessages, currentConversation.sn]); }, [activeMessages, currentConversation.sn]);
const getFirstPageMessages = async (item) => {
setMsgLoading(true);
const data = await fetchMessages({ opisn: userId, whatsappid: item.whatsapp_phone_number, lasttime: '' });
setMsgLoading(false);
receivedMessageList(item.sn, data);
const thisLastTime = data.length > 0 ? data[0].orgmsgtime : '';
const loadNextPage = !(data.length === 0 || data.length < MESSAGE_PAGE_SIZE);
updateCurrentConversation({ lasttime: thisLastTime, loadNextPage });
};
const getMoreMessages = async () => { const getMoreMessages = async () => {
setShouldScrollBottom(false); setShouldScrollBottom(false);
setLongListLoading(true); setLongListLoading(true);

@ -122,9 +122,10 @@
.chatwindow-wrapper .referrer-msg, .chatwindow-wrapper .referrer-msg,
.chatwindow-wrapper .rce-mbox-reply-message, .chatwindow-wrapper .rce-mbox-reply-message,
.chatwindow-wrapper .emoji, .chatwindow-wrapper .emoji,
.chatwindow-wrapper .epr-emoji-native,
.chatwindow-wrapper .ant-input-textarea-affix-wrapper.ant-input-affix-wrapper >textarea.ant-input .chatwindow-wrapper .ant-input-textarea-affix-wrapper.ant-input-affix-wrapper >textarea.ant-input
{ {
font-family: 'Open Sans', 'Noto Sans',"Noto Color Emoji", 'Apple Color Emoji', 'Twemoji Mozilla', 'Segoe UI Emoji', 'Segoe UI Symbol', 'EmojiOne Color', 'Android Emoji', Arial, sans-serif; font-family: 'Open Sans', 'Noto Sans',"Noto Color Emoji", 'Apple Color Emoji', 'Twemoji Mozilla', 'Segoe UI Emoji', 'Segoe UI Symbol', 'EmojiOne Color', 'Android Emoji', Arial, sans-serif!important;
font-weight: 400; font-weight: 400;
} }
.chatwindow-wrapper .rce-mbox-text a{ .chatwindow-wrapper .rce-mbox-text a{
@ -182,8 +183,11 @@
{ {
display: inline-block; display: inline-block;
} }
.chatwindow-wrapper .rce-mbox-file>button{
width: 100%;
}
/** Chat history */ /** Chat history ------------------------------------------------------------------------------------ */
/* .chathistory-wrapper .rce-mbox-time { /* .chathistory-wrapper .rce-mbox-time {
user-select: auto; user-select: auto;
-webkit-user-select: auto; -webkit-user-select: auto;
@ -214,3 +218,15 @@
animation-name: message-box-default-focus-x; animation-name: message-box-default-focus-x;
animation-duration: 1s; animation-duration: 1s;
} }
/**
* Mobile chat ------------------------------------------------------------------------------------
*/
.mobilechat-wrapper .rce-container-mbox .rce-mbox{
max-width: 320px;
}
.mobilechat-wrapper .rce-mtlink,
.mobilechat-wrapper .rce-mtlink-item{
min-width: unset;
}

@ -41,7 +41,7 @@ function MobileApp() {
<Outlet /> <Outlet />
</Content> </Content>
</Layout> </Layout>
<Footer>桂林海纳国际旅行社有限公司</Footer> {/* <Footer>桂林海纳国际旅行社有限公司</Footer> */}
</Layout> </Layout>
) )
} }

@ -1,4 +1,5 @@
import useAuthStore from '@/stores/AuthStore' import useAuthStore from '@/stores/AuthStore'
import useConversationStore from '@/stores/ConversationStore'
import { Flex, Result, Spin, Typography } from 'antd' import { Flex, Result, Spin, Typography } from 'antd'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom' import { useNavigate } from 'react-router-dom'
@ -7,11 +8,13 @@ import { useNavigate } from 'react-router-dom'
function Logout() { function Logout() {
const navigate = useNavigate() const navigate = useNavigate()
const { logout } = useAuthStore() const logout = useAuthStore(state => state.logout)
const reset = useConversationStore((state) => state.reset);
useEffect(() => { useEffect(() => {
logout() logout()
reset();
navigate('/p/dingding/qrcode') navigate('/p/dingding/qrcode')
}, []) }, [])
@ -29,4 +32,4 @@ function Logout() {
) )
} }
export default Logout export default Logout

@ -1,68 +1,26 @@
import { memo, useCallback, useEffect, useRef, useState, forwardRef } from 'react' import { Layout } from 'antd';
import { App, Avatar, List, Layout, Input, DatePicker, Button, Spin } from 'antd' import MessagesHeader from '@/views/Conversations/Components/MessagesHeader';
import { ChatItem, MessageBox } from 'react-chat-elements' import MessagesWrapper from '@/views/Conversations/Components/MessagesWrapper';
import { fetchConversationsList, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions' import InputComposer from '@/views/Conversations/Components/InputComposer';
import { isEmpty } from '@/utils/utils'
import useFormStore from '@/stores/FormStore'
import { useShallow } from 'zustand/react/shallow'
const { Content, Header, Footer } = Layout;
import { RightOutlined } from '@ant-design/icons'
import { fetchSalesAgent, fetchCustomerList } from '@/actions/CommonActions'
import SearchInput from '@/components/SearchInput'
const { Sider, Content, Header, Footer } = Layout
const { TextArea } = Input
const { RangePicker } = DatePicker
const data = [
{
title: 'Tyler Dru Kelly',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'Hi Nazly Parlindungan Siregar, this is Sharon , travel advisor of Asia Highlights. We got your inquiry for your trip toJapan , are you available for a quick chat to discuss about your trip? I have some ideas to share with you . Looking forward to talking with you!',
},
{
title: 'Chhavi',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'Hi Sharon, thanks for reaching out. I am extremely busy person, please feel free to write down or send me any note and I will respond to you immediately. Cheers🙏',
},
{
title: 'Nathan Posey',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'It is too late to see cherry blossom from April 28, is it ok for you? For your 7 days tour, visit Tokyo and Kyoto is ok. I will suggest a tour with private car for you because your mother cannot walk too much.',
},
]
function Chat() { function Chat() {
const { notification } = App.useApp()
return ( return (
<> <>
<List <Layout className='h-full chatwindow-wrapper mobilechat-wrapper' style={{ maxHeight: 'calc(100vh - 84px)', height: 'calc(100vh - 84px)', minWidth: '360px' }}>
itemLayout='horizontal' <Header className='ant-layout-sider-light ant-card h-auto flex justify-between gap-1 items-center'>
dataSource={data} <MessagesHeader />
renderItem={(item, index) => ( </Header>
<List.Item> <Content className='flex-grow bg-whatsapp-bg relative'>
<List.Item.Meta <MessagesWrapper />
avatar={<Avatar src={item.avatarUrl} />} </Content>
title={<a href='https://ant.design'>{item.title}</a>} <Footer className='ant-layout-sider-light p-0'>
description={item.msgTime} <InputComposer mobile />
/> </Footer>
</List.Item> </Layout>
)}
/>
<TextArea rows={4} placeholder='聊天窗口' maxLength={2000} />
<Button type='primary' onClick={() => {
notification.info({
message: '温馨提示',
description: '功能还在开发中,敬请期待',
placement: 'top',
duration: 60,
})
}}>发送</Button>
</> </>
) );
} }
export default Chat export default Chat;

@ -1,90 +1,12 @@
import { memo, useCallback, useEffect, useRef, useState, forwardRef } from 'react' import ConversationsList from '@/views/Conversations/Components/ConversationsList';
import { Avatar, List, Button, Input, Layout, Select, DatePicker, Form, Spin } from 'antd'
import { ChatItem, MessageBox } from 'react-chat-elements'
import { fetchConversationsList, fetchMessages, MESSAGE_PAGE_SIZE } from '@/actions/ConversationActions'
import { isEmpty } from '@/utils/utils'
import useFormStore from '@/stores/FormStore'
import { useShallow } from 'zustand/react/shallow'
import { useNavigate, useHref } from 'react-router-dom'
function Conversation() {
import { RightOutlined } from '@ant-design/icons'
import { fetchSalesAgent, fetchCustomerList } from '@/actions/CommonActions'
import SearchInput from '@/components/SearchInput'
const { Sider, Content, Header, Footer } = Layout
const { Search } = Input
const { RangePicker } = DatePicker
const data = [
{
title: 'Tyler Dru Kelly',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: '15127570944',
},
{
title: 'Chhavi',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240311112',
},
{
title: 'Nathan Posey',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240311114',
},
{
title: 'Philip',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'WLJ240312062',
},
{
title: 'Jeanne',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240313032',
},
{
title: 'Susan Puls',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240311213',
},
{
title: 'Ana Beatriz',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'H240312073',
},
{
title: 'Kathleen Anne Workman',
avatarUrl: 'https://api.dicebear.com/7.x/miniavs/svg?seed=' + Math.random() * 10000,
msgTime: 'HXY240104171',
},
]
function Login() {
const navigate = useNavigate()
return ( return (
<> <div className='chatwindow-wrapper'>
<List <ConversationsList mobile={true} />
itemLayout='horizontal' </div>
dataSource={data}
renderItem={(item, index) => (
<List.Item
onClick={() => {
navigate('/m/chat')}}
actions={[<RightOutlined key='goto' />]}
>
<List.Item.Meta
avatar={<Avatar src={item.avatarUrl} />}
title={<a href='https://ant.design'>{item.title}</a>}
description={item.msgTime}
/>
</List.Item>
)}
/>
</>
) )
} }
export default Login export default Conversation

Loading…
Cancel
Save