feat: 会话列表搜索和分页

2.0/email-builder
Lei OT 11 months ago
parent 0a28fa4457
commit 921cb2e7b9

@ -32,10 +32,11 @@ export const fetchConversationsList = async (params) => {
keyword: '',
ostate: '',
intour: '',
lastactivetime: dayjs('2024-09-01').format('YYYY-MM-DD 00:00'),
session_enable: 1,
lastactivetime: '',
}
const combinedFilterStr = Object.values(pick(params, ['tags', 'olabel', 'intour', 'keyword', 'ostate'])).join()
if (isNotEmpty(combinedFilterStr)) {
const combinedFilterStr = Object.values(pick(params, ['keyword', 'tags', 'olabel', 'intour', 'ostate'])).join('')
if (isNotEmpty(combinedFilterStr) || params.session_enable === 0) {
params.lastactivetime = '';
}
const { errcode, result: data } = await fetchJSON(`${API_HOST}/getconversations`, { ...defaultParams, ...params })
@ -46,9 +47,7 @@ export const fetchConversationsList = async (params) => {
whatsapp_name: `${ele.whatsapp_name || ''}`.trim(),
coli_id: ele.COLI_ID,
}))
// return first 10 items
return list.slice(0, 30); // test: 0
// return list;
return list;
};
/**

@ -0,0 +1,5 @@
const RegionCodeEmoji = ({ regionCode }) => {
const countryCode = String.fromCodePoint(...[...regionCode.toUpperCase()].map((x) => 0x1f1a5 + x.charCodeAt()))
return <span title={regionCode}>{countryCode}</span>
}
export default RegionCodeEmoji

@ -7,6 +7,11 @@ import { devtools } from 'zustand/middleware';
import { WS_URL, DATETIME_FORMAT } from '@/config';
import dayjs from 'dayjs';
const replaceObjectsByKey = (arr1, arr2, key) => {
const map = new Map(arr2.map(ele => [ele[key], ele]));
return arr1.map(item => map.has(item[key]) ? map.get(item[key]) : item);
}
// const WS_URL = 'ws://202.103.68.144:8888/whatever/';
// const WS_URL = 'ws://120.79.9.217:10022/whatever/';
const conversationRow = {
@ -62,17 +67,17 @@ const filterObj = {
search: '',
otype: '',
tags: [],
status: [],
labels: [],
loadNextPage: true,
lastpagetime: '',
lastactivetime: '', // dayjs().subtract(30, "days").format('YYYY-MM-DD 00:00'), // 30 days
};
const filterSlice = (set) => ({
filter: structuredClone(filterObj),
setFilter: (filter) => set({ filter }),
setFilter: (filter) => set(state => ({ filter: { ...state.filter, ...filter } })),
setFilterSearch: (search) => set((state) => ({ filter: { ...state.filter, search } })),
setFilterOtype: (otype) => set((state) => ({ filter: {...state.filter, otype } })),
setFilterTags: (tags) => set((state) => ({ filter: {...state.filter, tags } })),
setFilterStatus: (status) => set((state) => ({ filter: {...state.filter, status } })),
setFilterLabels: (labels) => set((state) => ({ filter: {...state.filter, labels } })),
setFilterLoadNextPage: (loadNextPage) => set((state) => ({ filter: {...state.filter, loadNextPage } })),
resetFilter: () => set({ filter: structuredClone(filterObj) }),
})
// WABA 模板
@ -187,28 +192,32 @@ const conversationSlice = (set, get) => ({
setConversationsListLoading: (conversationsListLoading) => set({ conversationsListLoading }),
/**
* @deprecated
* 首次加载
* 搜索结果
*/
setConversationsList: (conversationsList) => {
const { activeConversations, } = get();
const conversationsMapped = conversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
return set({ conversationsList, activeConversations: conversationsMapped });
return set({ conversationsList, activeConversations: {...conversationsMapped, ...activeConversations,} });
},
setClosedConversationList: (closedConversationsList) => {
const { activeConversations, } = get();
const listMapped = closedConversationsList.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
return set({ closedConversationsList, activeConversations: { ...activeConversations, ...listMapped } });
},
addToConversationList: (newList) => {
addToConversationList: (newList, position='top') => {
const { activeConversations, conversationsList } = get();
const conversationsIds = Object.keys(activeConversations);
// const conversationsIds = conversationsList.map((chatItem) => `${chatItem.sn}`);
// const conversationsIds = Object.keys(activeConversations);
const conversationsIds = conversationsList.map((chatItem) => `${chatItem.sn}`);
const newConversations = newList.filter((conversation) => !conversationsIds.includes(`${conversation.sn}`));
const newConversationsMapped = newConversations.reduce((r, v) => ({ ...r, [`${v.sn}`]: [] }), {});
const newListIds = newList.map((chatItem) => `${chatItem.sn}`);
const withoutNew = conversationsList.filter((item) => !newListIds.includes(`${item.sn}`));
const mergedList = [...newList, ...withoutNew];
const updateList = replaceObjectsByKey(conversationsList, newList, 'sn');
const mergedList = position==='top' ? [...newList, ...withoutNew] : [...updateList, ...newConversations];
const refreshTotalNotify = mergedList.reduce((r, c) => r+(c.unread_msg_count === UNREAD_MARK ? 0 : c.unread_msg_count), 0);
return set((state) => ({
@ -408,8 +417,8 @@ export const useConversationStore = create(
const templates = await fetchTemplates();
setTemplates(templates);
const closedList = await fetchConversationsSearch({ opisn: userIds, session_enable: 0 });
setClosedConversationList(closedList);
// const closedList = await fetchConversationsList({ opisn: userIds, session_enable: 0 });
// setClosedConversationList(closedList);
const myTags = await fetchTags({ opisn: userIds});
setTags(myTags);

@ -1,7 +1,7 @@
import React, { useState, useEffect } from 'react';
import { Button, Tag, Radio, Popover, Form, Divider } from 'antd';
import { FilterOutlined, FilterTwoTone } from '@ant-design/icons';
import { isEmpty, isNotEmpty, objectMapper, stringToColour } from '@/utils/commons';
import { debounce, isEmpty, isNotEmpty, objectMapper, stringToColour } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore';
import { OrderLabelDefaultOptions, OrderStatusDefaultOptions } from '@/stores/OrderStore';
import { FilterIcon } from '@/components/Icons';
@ -32,6 +32,9 @@ const ChatListFilter = ({ onFilterChange, activeList, ...props }) => {
const conversationsList = useConversationStore((state) => state.conversationsList);
const closedConversationsList = useConversationStore((state) => state.closedConversationsList);
/**
* 前端搜索, 返回结果到列表
*/
const handleFilter = async () => {
const fromSource = activeList ? conversationsList : closedConversationsList;
@ -72,9 +75,9 @@ const ChatListFilter = ({ onFilterChange, activeList, ...props }) => {
return isEmpty(selectedTags) ? true : (selectedTags || []).some(tele => (itemTagKeys).includes(tele));
}
const filterWithParamRes = fromSource.filter(filterSearchFun).filter(filterOTypeFun).filter(filterTagFun);
// const filterWithParamRes = fromSource.filter(filterSearchFun).filter(filterOTypeFun).filter(filterTagFun);
if (typeof onFilterChange === 'function') {
onFilterChange(filterWithParamRes);
onFilterChange();
}
};
@ -85,6 +88,10 @@ const ChatListFilter = ({ onFilterChange, activeList, ...props }) => {
setFilterTags(nextSelectedTags);
form.setFieldValue('tags', nextSelectedTags);
};
/**
* @ignore
*/
const onFinish = async (values) => {
// const filterParam = objectMapper(values, { tags: {key:'tags', transform: (v) => v ? v.join(',') : ''} });
const filterParam = objectMapper(values, { tags: {key:'tags', } });
@ -98,16 +105,18 @@ const ChatListFilter = ({ onFilterChange, activeList, ...props }) => {
resetFilter();
form.resetFields();
if (typeof onFilterChange === 'function') {
const fromSource = activeList ? conversationsList : closedConversationsList;
onFilterChange(fromSource);
onFilterChange();
}
}
useEffect(() => {
handleFilter()
// handleFilter()
if (typeof onFilterChange === 'function') {
onFilterChange()
}
return () => {}
}, [selectedTags, selectedOType, filter ])
}, [selectedTags, selectedOType, search ])
const [openPopup, setOpenPopup] = useState(false);
return (
@ -121,7 +130,6 @@ const ChatListFilter = ({ onFilterChange, activeList, ...props }) => {
value={selectedOType}
onChange={(e) => {
setFilterOtype(e.target.value)
// handleFilter({ otype: e.target.value })
}}
/>
<Popover

@ -9,6 +9,7 @@ import { flush, isEmpty, isNotEmpty, stringToColour } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore';
import ChannelLogo from './ChannelLogo';
import RegionCodeEmoji from '@/components/RegionCodeEmoji'
import { ReadIcon } from '@/components/Icons';
import useStyleStore from '@/stores/StyleStore';
import { OrderLabelDefaultOptionsMapped, OrderStatusDefaultOptionsMapped } from '@/stores/OrderStore';
@ -28,15 +29,10 @@ const OrderSignEmoji = ({ item }) => (
<>
{OrderLabelDefaultOptionsMapped[String(item.order_label_id)]?.emoji}
{OrderStatusDefaultOptionsMapped[String(item.order_state_id)]?.emoji}
{item.intour === 1 ? '👣' : ''}
{item.intour === 1 && item.order_state_id===5 ? '👣' : ''}
</>
)
const RegionCodeEmoji = ({ regionCode }) => {
const countryCode = String.fromCodePoint(...[...regionCode.toUpperCase()].map((x) => 0x1f1a5 + x.charCodeAt()))
return <span title={regionCode}>{countryCode}</span>
}
const NewTagForm = ({onSubmit,...props}) => {
const [form] = Form.useForm();
const [subLoding, setSubLoding] = useState(false);
@ -130,13 +126,13 @@ const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitch
const handleConversationItemUnread = async (item) => {
await fetchConversationItemUnread({ conversationid: item.sn });
await refreshConversationList();
await refreshConversationList(item.last_received_time > item.last_send_time ? item.last_received_time : item.last_send_time);
setListUpdateFlag(Math.random());
}
const handleConversationItemTop = async (item) => {
await fetchConversationItemTop({ conversationid: item.sn, top_state: item.top_state === 0 ? 1 : 0 });
await refreshConversationList();
await refreshConversationList(item.last_received_time > item.last_send_time ? item.last_received_time : item.last_send_time);
setListUpdateFlag(Math.random());
}
@ -155,7 +151,7 @@ const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitch
addTag({ label: rtag.tag_label, key: rtag.tag_key, value: rtag.tag_key })
}
}
await refreshConversationList();
await refreshConversationList(item.last_received_time > item.last_send_time ? item.last_received_time : item.last_send_time);
setListUpdateFlag(Math.random());
}
@ -277,9 +273,16 @@ const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitch
}}>
<div
className={[
'border-0 border-t1 border-solid border-neutral-200',
String(item.sn) === String(currentConversation.sn) ? '__active text-primary bg-whatsapp-bg' : '',
String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '',
'border-0 border-solid border-neutral-200',
// item.top_state === 1 ? 'bg-stone-100' : '',
String(item.sn) === String(currentConversation.sn)
? '__active text-primary bg-whatsapp-bg'
: String(item.sn) === String(tabSelectedConversation?.sn)
? ' bg-neutral-200'
: item.top_state === 1
? 'bg-stone-100'
: '',
// String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '',
].join(' ')}>
{/* <div className='pl-4 pt-1 text-xs text-right'>
{tags.map((tag) => <Tag color={tag.color} key={tag.value}>{tag.label}</Tag>)}
@ -291,12 +294,12 @@ const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitch
letterItem={{ id: item.whatsapp_name || item.whatsapp_phone_number, letter: (item.whatsapp_name || item.whatsapp_phone_number).slice(0, 5) }}
alt={item.whatsapp_name}
title={
<>
<span>
{/* 🔝 */}
{/* <RegionCodeEmoji regionCode={item?.last_message?.regionCode || ''} /> */}
{(item.top_state === 1 ? '🔝' : '')
+ (item.conversation_memo || item.whatsapp_name || item.whatsapp_phone_number)}
</>
// item.conversation_memo ||
{item.conversation_memo || item.whatsapp_name || item.whatsapp_phone_number}
</span>
// item.conversation_memo ||
}
// subtitle={item.coli_id}
subtitle={
@ -306,7 +309,7 @@ const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitch
{/* <SentIcon /> */}
{/* <span>{item.coli_id}</span> */}
{/* <span><ReadIcon />最后一条消息</span> */}
<span>
<span className='text-sm'>
<RenderLastMsg {...item?.last_message} />
</span>
<div className='text-sm'>
@ -320,7 +323,7 @@ const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitch
</div>
</div>
}
date={item.last_received_time || item.last_send_time}
date={item.lasttime || item.last_received_time || item.last_send_time}
unread={item.unread_msg_count > 99 ? 0 : item.unread_msg_count}
// className={[
// String(item.sn) === String(currentConversation.sn) ? '__active text-primary bg-whatsapp-bg' : '',

@ -1,16 +1,18 @@
import React, { useEffect, useState, useRef } from 'react';
import React, { useEffect, useState, useRef, useCallback } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Input, Button, Empty, Tooltip } from 'antd';
import { Input, Button, Empty, Tooltip, List } from 'antd';
import { PlusOutlined, LoadingOutlined, HistoryOutlined, FireOutlined,AudioTwoTone } from '@ant-design/icons';
import { fetchConversationsList, fetchOrderConversationsList } from '@/actions/ConversationActions';
import { fetchConversationsList, fetchOrderConversationsList, CONVERSATION_PAGE_SIZE } from '@/actions/ConversationActions';
import ConversationsNewItem from './ConversationsNewItem';
import { isEmpty } from '@/utils/commons';
import { debounce, isEmpty, isNotEmpty, pick } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore';
import { useVisibilityState } from '@/hooks/useVisibilityState';
import ChatListItem from './Components/ChatListItem';
import ChatListFilter from './Components/ChatListFilter';
import useStyleStore from '@/stores/StyleStore';
import dayjs from 'dayjs';
import { DATETIME_FORMAT } from '@/config';
/**
* []
@ -29,19 +31,61 @@ const Conversations = () => {
const conversationsList = useConversationStore((state) => state.conversationsList);
const [conversationsListLoading, setConversationsListLoading] = useConversationStore((state) => [state.conversationsListLoading, state.setConversationsListLoading]);
const addToConversationList = useConversationStore((state) => state.addToConversationList);
const closedConversationsList = useConversationStore((state) => state.closedConversationsList);
const setConversationsList = useConversationStore((state) => state.setConversationsList);
const isVisible = useVisibilityState();
const [tabSelectedConversation, setTabSelectedConversation] = useState({});
const [tabCnt, setTabCnt] = useState(-1);
async function refreshConversationList() {
setConversationsListLoading(mobile !== undefined ? true : false);
const _list = await fetchConversationsList({ opisn: userId });
addToConversationList(_list);
const [{ search: searchContent, loadNextPage, ...filterState }, setSearchContent, setFilter] = useConversationStore((state) => [state.filter, state.setFilterSearch, state.setFilter])
const [currentLoading, setCurrentLoading] = useState(false);
/**
*
*/
async function refreshConversationList(current='', append=false) {
// setConversationsListLoading(mobile !== undefined ? true : false);
setConversationsListLoading(true);
setCurrentLoading(current==='');
const [otypeC, v] = filterState.otype ? filterState.otype.split('@') : [];
const otypeV = v ? parseInt(v) : '';
const searchParams = {
keyword: searchContent,
tags: filterState.tags.join(','),
olabel: otypeC === 'label' ? otypeV : '',
ostate: otypeC === 'state' ? otypeV : '',
intour: otypeC === 'intour' ? otypeV : '',
session_enable: activeList ? 1 : 0,
lastpagetime: current ?
dayjs(current).add(1, 'minutes').format('YYYY-MM-DDTHH:mm:ss')
: append
? filterState.lastpagetime
: '',
lastactivetime: filterState.lastactivetime,
}
const _list = await fetchConversationsList({ ...searchParams, opisn: userId });
// ,
if (current) {
addToConversationList(_list, 'next');
} else if (append === false) {
setConversationsList(_list)
} else {
addToConversationList(_list, 'next');
}
setFilter({
lastpagetime: _list.length > 0 ? _list[_list.length - 1].lasttime : '',
loadNextPage: !(_list.length === 0 || _list.length < CONVERSATION_PAGE_SIZE),
// ...((_list.length === 0 || _list.length < CONVERSATION_PAGE_SIZE) ? {
// lastactivetime: dayjs(filterState.lastactivetime).subtract(6, 'months').format(DATETIME_FORMAT),
// } : {}),
});
setConversationsListLoading(false);
setCurrentLoading(false);
}
useEffect(() => {
@ -51,21 +95,29 @@ const Conversations = () => {
return () => {};
}, []);
const [activeList, setActiveList] = useState(true);
useEffect(() => {
if (isVisible) {
refreshConversationList();
if (isVisible && initialState) {
refreshConversationList(new Date()); // , loading,
}
return () => {};
}, [isVisible]);
const [activeList, setActiveList] = useState(true);
useEffect(() => {
if (isVisible && initialState) {
refreshConversationList(); // loading
}
return () => {};
}, [activeList]);
const [dataSource, setDataSource] = useState(conversationsList);
const [listUpdateFlag, setListUpdateFlag] = useState();
useEffect(() => {
// setDataSource(conversationsList);
setDataSource(activeList ? conversationsList: closedConversationsList);
setDataSource(conversationsList);
// setDataSource(activeList ? conversationsList: closedConversationsList);
return () => {};
}, [conversationsList, listUpdateFlag, currentConversation.unread_msg_count]);
@ -147,9 +199,6 @@ const Conversations = () => {
// switchConversation(item);
};
const [{ search: searchContent, ...filter }, setSearchContent] =
useConversationStore((state) => [state.filter, state.setFilterSearch])
const searchInputRef = useRef(null);
const [newChatModalVisible, setNewChatModalVisible] = useState(false);
@ -157,12 +206,13 @@ const Conversations = () => {
// const closedVisible = closedConversationsList.length > 0;
const toggleClosedConversationsList = () => {
const _active = activeList;
setDataSource(_active ? closedConversationsList : conversationsList);
setActiveList(!activeList);
setCurrentConversation({});
};
const [searchInput, setSearchInput] = useState(searchContent);
return (
<div className='flex flex-col h-inherit'>
<div className='flex gap-1 items-center'>
@ -181,49 +231,36 @@ const Conversations = () => {
className=''
ref={searchInputRef}
allowClear
value={searchContent}
value={searchInput}
onChange={(e) => {
setSearchContent(e.target.value)
setTabCnt(-1)
setTabSelectedConversation({})
}}
onKeyDown={(e) => {
if (e.key === 'Tab') {
e.preventDefault()
const _this = tabCnt >= dataSource.length - 1 ? 0 : tabCnt + 1
setTabCnt(_this)
setTabSelectedConversation(dataSource[_this])
}
setSearchInput(e.target.value)
// setTabCnt(-1)
// setTabSelectedConversation({})
}}
// onKeyDown={(e) => {
// if (e.key === 'Tab') {
// e.preventDefault()
// const _this = tabCnt >= dataSource.length - 1 ? 0 : tabCnt + 1
// setTabCnt(_this)
// setTabSelectedConversation(dataSource[_this])
// }
// }}
onPressEnter={(e) => {
searchInputRef.current.blur()
onSwitchConversation(dataSource[tabCnt < 0 ? 0 : tabCnt])
setTabCnt(-1)
setTabSelectedConversation({})
// searchInputRef.current.blur()
setSearchContent(e.target.value)
// onSwitchConversation(dataSource[tabCnt < 0 ? 0 : tabCnt])
// setTabCnt(-1)
// setTabSelectedConversation({})
return false
}}
placeholder={`名称/号码/订单号${
conversationsListLoading ? '...' : ''
}`}
// onSearch={handleRichSearchConvs}
onSearch={(v, e, { source }) => setSearchContent(v)}
placeholder={`名称/号码/订单号${conversationsListLoading ? '...' : ''}`}
// addonBefore={filterTag}
// addonBefore={<FilterOutlined />}
// enterButton={'Filter'}
/>
<Tooltip
key={'conversation-list'}
title={activeList ? '历史会话' : '活跃会话'}>
<Button
onClick={toggleClosedConversationsList}
icon={
activeList ? (
<HistoryOutlined className='text-neutral-500' />
) : (
<FireOutlined className=' text-orange-500' />
)
}
type='text'
/>
<Tooltip key={'conversation-list'} title={activeList ? '隐藏会话' : '活跃会话'}>
<Button onClick={toggleClosedConversationsList} icon={activeList ? <HistoryOutlined className='text-neutral-500' /> : <FireOutlined className=' text-orange-500' />} type='text' />
</Tooltip>
{mobile && (
<AudioTwoTone
@ -233,29 +270,46 @@ const Conversations = () => {
/>
)}
</div>
<ChatListFilter onFilterChange={(list) => setDataSource(list)} activeList={activeList} />
<ChatListFilter
onFilterChange={(d) => {
refreshConversationList()
}}
activeList={activeList}
/>
<div className='flex-1 overflow-x-hidden overflow-y-auto relative'>
{conversationsListLoading && dataSource.length === 0 ? (
{/* {mobile && conversationsListLoading && dataSource.length === 0 ? ( */}
{conversationsListLoading && currentLoading ? (
<div className='text-center py-2'>
<LoadingOutlined className='text-primary ' />
</div>
) : null}
{dataSource.map((item) => (
<ChatListItem
key={item.sn}
{...{
item,
refreshConversationList,
setListUpdateFlag,
onSwitchConversation,
tabSelectedConversation,
setNewChatModalVisible,
setEditingChat,
}}
/>
))}
{dataSource.length === 0 && <Empty description={'无数据'} />}
<List
itemLayout='vertical'
dataSource={dataSource}
loadMore={
<div className='text-center pt-3 mb-3 h-8 leading-8 '>
{!conversationsListLoading && loadNextPage ? (
<Button onClick={() => refreshConversationList(false, true)} size='small'>load more</Button>
) : null}
{conversationsListLoading && <LoadingOutlined className='text-primary ' />}
</div>
}
renderItem={(item, index) => (
<ChatListItem
key={item.sn}
{...{
item,
refreshConversationList,
setListUpdateFlag,
onSwitchConversation,
tabSelectedConversation,
setNewChatModalVisible,
setEditingChat,
}}
/>
)}
/>
</div>
<ConversationsNewItem
initialValues={{ ...editingChat, is_current_order: false }}

Loading…
Cancel
Save