From 921cb2e7b93e75709cc9051765abbcbd00780cb4 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Thu, 31 Oct 2024 15:50:20 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=9A=E8=AF=9D=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=92=8C=E5=88=86=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/ConversationActions.js | 11 +- src/components/RegionCodeEmoji.jsx | 5 + src/stores/ConversationStore.js | 35 ++-- .../Online/Components/ChatListFilter.jsx | 24 ++- .../Online/Components/ChatListItem.jsx | 41 ++-- .../Online/ConversationsList.jsx | 196 +++++++++++------- 6 files changed, 195 insertions(+), 117 deletions(-) create mode 100644 src/components/RegionCodeEmoji.jsx diff --git a/src/actions/ConversationActions.js b/src/actions/ConversationActions.js index 39004a3..d5cd526 100644 --- a/src/actions/ConversationActions.js +++ b/src/actions/ConversationActions.js @@ -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; }; /** diff --git a/src/components/RegionCodeEmoji.jsx b/src/components/RegionCodeEmoji.jsx new file mode 100644 index 0000000..b401b9a --- /dev/null +++ b/src/components/RegionCodeEmoji.jsx @@ -0,0 +1,5 @@ +const RegionCodeEmoji = ({ regionCode }) => { + const countryCode = String.fromCodePoint(...[...regionCode.toUpperCase()].map((x) => 0x1f1a5 + x.charCodeAt())) + return {countryCode} +} +export default RegionCodeEmoji diff --git a/src/stores/ConversationStore.js b/src/stores/ConversationStore.js index fd32b2f..b835e22 100644 --- a/src/stores/ConversationStore.js +++ b/src/stores/ConversationStore.js @@ -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); diff --git a/src/views/Conversations/Online/Components/ChatListFilter.jsx b/src/views/Conversations/Online/Components/ChatListFilter.jsx index 78a68b9..7ae955f 100644 --- a/src/views/Conversations/Online/Components/ChatListFilter.jsx +++ b/src/views/Conversations/Online/Components/ChatListFilter.jsx @@ -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 }) }} /> ( <> {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 {countryCode} -} - 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 }}>
{/*
{tags.map((tag) => {tag.label})} @@ -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={ - <> + + {/* 🔝 */} {/* */} - {(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} + + // item.conversation_memo || } // subtitle={item.coli_id} subtitle={ @@ -306,7 +309,7 @@ const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitch {/* */} {/* {item.coli_id} */} {/* 最后一条消息 */} - +
@@ -320,7 +323,7 @@ const ChatListItem = (({item, refreshConversationList,setListUpdateFlag,onSwitch
} - 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' : '', diff --git a/src/views/Conversations/Online/ConversationsList.jsx b/src/views/Conversations/Online/ConversationsList.jsx index fa654de..b5fde52 100644 --- a/src/views/Conversations/Online/ConversationsList.jsx +++ b/src/views/Conversations/Online/ConversationsList.jsx @@ -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 (
@@ -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={} // enterButton={'Filter'} /> - -
- setDataSource(list)} activeList={activeList} /> + { + refreshConversationList() + }} + activeList={activeList} + />
- {conversationsListLoading && dataSource.length === 0 ? ( + {/* {mobile && conversationsListLoading && dataSource.length === 0 ? ( */} + {conversationsListLoading && currentLoading ? (
) : null} - {dataSource.map((item) => ( - - ))} - {dataSource.length === 0 && } + + {!conversationsListLoading && loadNextPage ? ( + + ) : null} + {conversationsListLoading && } +
+ } + renderItem={(item, index) => ( + + )} + />