todo: 会话右键菜单; 标签筛选; 订单标记筛选; 会话列表显示

2.0/feat
Lei OT 10 months ago
parent f54c1e790a
commit 305502920d

@ -103,6 +103,14 @@ export const fetchConversationItemUnread = async (body) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/set_state_unread`, body); const { errcode, result } = await fetchJSON(`${API_HOST}/set_state_unread`, body);
return errcode !== 0 ? {} : result; return errcode !== 0 ? {} : result;
}; };
/**
* 设置置顶
* @param {object} body { conversationid, top_state }
*/
export const fetchConversationItemTop = async (body) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/set_state_top`, body);
return errcode !== 0 ? {} : result;
};
/** /**
* ------------------------------------------------------------------------------------------------ * ------------------------------------------------------------------------------------------------
@ -190,3 +198,52 @@ export const postAssignConversation = async (params) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/assign_conversation`, params); const { errcode, result } = await fetchJSON(`${API_HOST}/assign_conversation`, params);
return errcode !== 0 ? {} : result; return errcode !== 0 ? {} : result;
} }
/**
* ------------------------------------------------------------------------------------------------
*
*/
/**
* 顾问的自定义标签
* @param {object} params { opisn, }
*/
export const fetchTags = async (params) => {
return [
{ label: '已付款', key: 'p1', value: 'p1', },
{ label: '地接', key: 'p2', value: 'p2', },
]; // test:
const { errcode, result } = await fetchJSON(`${API_HOST}/opi_tags`, params);
return errcode !== 0 ? {} : result;
}
/**
* 会话设置标签
* @param {object} body { opisn, conversationid, tag_label, tag_id }
*/
export const postConversationTags = async (body) => {
const formData = new FormData();
Object.keys(body).forEach(function (key) {
formData.append(key, body[key]);
});
const { errcode, result } = await fetchJSON(`${API_HOST}/set_conversation_tags_add`, formData);
return errcode !== 0 ? {} : result;
}
/**
* 会话删除标签
* @param {object} params { opisn, conversationid, tag_id }
*/
export const deleteConversationTags = async (params) => {
const { errcode, result } = await fetchJSON(`${API_HOST}/delete_conversation_tags`, params);
return errcode !== 0 ? {} : result;
}
/**
* 附加备注
* @param {object} body { opisn, conversationid, memo }
*/
export const postConversationMemo = async (body) => {
const formData = new FormData();
Object.keys(body).forEach(function (key) {
formData.append(key, body[key]);
});
const { errcode, result } = await fetchJSON(`${API_HOST}/set_conversation_Memo`, formData);
return errcode !== 0 ? {} : result;
}

@ -0,0 +1,29 @@
import Icon from '@ant-design/icons';
const WABSvg = () => (
<svg viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' width='16' height='16'>
<path
d='M16.065 29.045h-.005a13.27 13.27 0 01-6.74-1.836l-.484-.287-5.012 1.31 1.338-4.865-.315-.498a13.102 13.102 0 01-2.025-7.014C2.825 8.588 8.766 2.676 16.071 2.676a13.185 13.185 0 019.362 3.866 13.068 13.068 0 013.875 9.324c-.003 7.267-5.943 13.18-13.243 13.18zM27.336 4.65A15.868 15.868 0 0016.066 0C7.281-.002.135 7.111.131 15.853a15.771 15.771 0 002.127 7.927l-2.26 8.217 8.446-2.205a15.982 15.982 0 007.614 1.93h.006c8.781 0 15.93-7.114 15.933-15.856a15.724 15.724 0 00-4.663-11.219z'
fill='#2ba84a'
/>
<path
d='M10.273 23.549c-.18-.105-.356-.197-.356-.769.004-2.836.009-9.394 0-11.82-.005-1.527-.209-2.515 1.14-2.515 3.65 0 8.983-.677 10.225 2.31 1.253 3.02-.774 4.483-1.219 5.26 3.042.842 3.208 7.593-3.293 7.593-1.391 0-3.348.005-5.615.01-.533 0-.77 0-.882-.07zm2.816-2.475h3.301c1.406-.004 2.657-.649 2.625-2.031-.023-1.3-.9-1.729-2.12-1.848-1.154.014-2.48.014-3.806.014v3.865zm0-6.476c2.443-.033 3.385.095 4.72-.234.918-.512 1.317-2.42.005-3.064-.909-.45-3.608-.297-4.725-.252v3.55z'
fill='#2ba84a'
/>
</svg>
);
export const WABIcon = (props) => <Icon component={WABSvg} {...props} />;
const Read = () => (
<svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" color="#4fc3f7" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z" stroke="none"/><path d="M18 7l-1.41-1.41-6.34 6.34 1.41 1.41L18 7zm4.24-1.41L11.66 16.17 7.48 12l-1.41 1.41L11.66 19l12-12-1.42-1.41zM.41 13.41L6 19l1.41-1.41L1.83 12 .41 13.41z" stroke="none"/></svg>
)
export const ReadIcon = (props) => <Icon component={Read} {...props} />;
const Deliver = () => (
<svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 512 512" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path d="M317.5 210.3c1.7-1.8 1.8-4.7 0-6.5l-19.8-21c-.8-.9-2-1.4-3.2-1.4-1.2 0-2.4.5-3.2 1.4l-66.5 69.1 26.4 27.1 66.3-68.7zm-193.7 42.8c-.9-.9-2-1.4-3.2-1.4-1.2 0-2.3.5-3.2 1.4l-20.1 20.7c-1.8 1.8-1.8 4.8 0 6.6l63.2 65c4 4.2 9 6.6 13.2 6.6 6 0 11.1-4.5 13.1-6.4l.1-.1 13.4-13.8-76.5-78.6z" stroke="none"/><path d="M414.7 182.4l-19.8-21c-.8-.9-2-1.4-3.2-1.4-1.2 0-2.4.5-3.2 1.4L250.7 304.1l-50.1-51.6c-.9-.9-2-1.4-3.2-1.4-1.2 0-2.3.5-3.2 1.4l-20.1 20.7c-1.8 1.8-1.8 4.8 0 6.6l63.2 65c4 4.2 9 6.6 13.2 6.6 6 0 11.1-4.5 13.1-6.4l.1-.1 151-156.1c1.7-1.7 1.7-4.6 0-6.4z" stroke="none"/></svg>
)
export const DeliverIcon = (props) => <Icon component={Deliver} {...props} />;
const Sent = () => (
<svg stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path fill="none" d="M0 0h24v24H0z" stroke="none"/><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z" stroke="none"/></svg>
)
export const SentIcon = (props) => <Icon component={Sent} {...props} />;

@ -2,7 +2,7 @@ import { create } from 'zustand';
import { RealTimeAPI } from '@/channel/realTimeAPI'; import { RealTimeAPI } from '@/channel/realTimeAPI';
import { olog, isEmpty } from '@/utils/commons'; import { olog, isEmpty } from '@/utils/commons';
import { receivedMsgTypeMapped, handleNotification } from '@/channel/whatsappUtils'; import { receivedMsgTypeMapped, handleNotification } from '@/channel/whatsappUtils';
import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK } from '@/actions/ConversationActions'; import { fetchConversationsList, fetchTemplates, fetchConversationsSearch, UNREAD_MARK, fetchTags } from '@/actions/ConversationActions';
import { devtools } from 'zustand/middleware'; import { devtools } from 'zustand/middleware';
import { WS_URL, DATETIME_FORMAT } from '@/config'; import { WS_URL, DATETIME_FORMAT } from '@/config';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
@ -47,6 +47,35 @@ const initialConversationState = {
}; };
// 顾问的自定义标签
const tagsSlice = (set) => ({
tags: [],
setTags: (tags) => set({ tags }),
addTag: (tag) => set((state) => ({ tags: [...state.tags, tag] })),
removeTag: (tag) => set((state) => ({ tags: state.tags.filter((t) => t.key !== tag.key) })),
updateTag: (tag) => set((state) => ({ tags: state.tags.map((t) => (t.key === tag.key ? tag : t)) })),
resetTags: () => set({ tags: [] }),
});
// 会话筛选
const filterObj = {
search: '',
otype: '',
tags: [],
status: [],
labels: [],
};
const filterSlice = (set) => ({
filter: structuredClone(filterObj),
setFilter: (filter) => set({ 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 } })),
resetFilter: () => set({ filter: structuredClone(filterObj) }),
})
// WABA 模板
const templatesSlice = (set) => ({ const templatesSlice = (set) => ({
templates: [], templates: [],
setTemplates: (templates) => set({ templates }), setTemplates: (templates) => set({ templates }),
@ -362,6 +391,8 @@ export const useConversationStore = create(
...messageSlice(set, get), ...messageSlice(set, get),
...referenceMsgSlice(set, get), ...referenceMsgSlice(set, get),
...complexMsgSlice(set, get), ...complexMsgSlice(set, get),
...tagsSlice(set, get),
...filterSlice(set, get),
// state actions // state actions
addError: (error) => set((state) => ({ errors: [...state.errors, error] })), addError: (error) => set((state) => ({ errors: [...state.errors, error] })),
@ -369,7 +400,7 @@ export const useConversationStore = create(
// side effects // side effects
fetchInitialData: async (userIds) => { fetchInitialData: async (userIds) => {
const { addToConversationList, setTemplates, setInitial, setClosedConversationList } = get(); const { addToConversationList, setTemplates, setInitial, setClosedConversationList, setTags } = get();
const conversationsList = await fetchConversationsList({ opisn: userIds }); const conversationsList = await fetchConversationsList({ opisn: userIds });
addToConversationList(conversationsList); addToConversationList(conversationsList);
@ -380,6 +411,9 @@ export const useConversationStore = create(
const closedList = await fetchConversationsSearch({ opisn: userIds, session_enable: 0 }); const closedList = await fetchConversationsSearch({ opisn: userIds, session_enable: 0 });
setClosedConversationList(closedList); setClosedConversationList(closedList);
const myTags = await fetchTags();
setTags(myTags);
setInitial(true); setInitial(true);
}, },

@ -395,12 +395,12 @@ export const cartesianProductArray = (arr, sep = '_', index = 0, prefix = '') =>
return result; return result;
}; };
export const stringToColour = (str) => { export const stringToColour = (str, withFlag = true) => {
var hash = 0 var hash = 0
for (let i = 0; i < str.length; i++) { for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash) hash = str.charCodeAt(i) + ((hash << 5) - hash)
} }
var colour = '#' var colour = withFlag ? '#' : ''
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
var value = (hash >> (i * 8)) & 0xff var value = (hash >> (i * 8)) & 0xff
value = (value % 150) + 50 value = (value % 150) + 50

@ -1,6 +1,7 @@
import { loadScript } from '@/utils/commons'; import { loadScript } from '@/utils/commons';
export const loadPageSpy = (title) => { export const loadPageSpy = (title) => {
if (import.meta.env.DEV) return false;
const PageSpySrc = [ const PageSpySrc = [
'https://page-spy.mycht.cn/page-spy/index.min.js', 'https://page-spy.mycht.cn/page-spy/index.min.js',
'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js', 'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js',

@ -102,6 +102,8 @@
} }
.chatwindow-wrapper .rce-citem { .chatwindow-wrapper .rce-citem {
background: transparent; background: transparent;
height: auto;
min-height: 72px;
} }
.chatwindow-wrapper .bg-transparent .rce-mbox{ .chatwindow-wrapper .bg-transparent .rce-mbox{
background: unset; background: unset;

@ -0,0 +1,17 @@
import React, { } from 'react';
import { WhatsAppOutlined, MailOutlined } from '@ant-design/icons';
import { WABIcon, } from '@/components/Icons';
const ChannelLogo = ({channel}) => {
switch (channel) {
case 'waba':
return <WABIcon key={channel} className='text-whatsapp' />;
case 'wa':
return <WhatsAppOutlined key={channel} className='text-whatsapp' />;
case 'email':
return <MailOutlined key={channel} className='text-violet-500' />
default:
return <MailOutlined key={'channel'} className='text-violet-500' />
}
}
export default ChannelLogo;

@ -0,0 +1,105 @@
import React, { useState } from 'react';
import { Button, Tag, Radio, Popover, Form } from 'antd';
import { FilterOutlined, FilterTwoTone } from '@ant-design/icons';
import { isEmpty, objectMapper, stringToColour } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore';
const otypes = [
{ label: 'All', value: '' },
{ label: '重点', value: 'zhongdian' },
{ label: '次重点', value: 'qianli' },
{ label: '成行', value: 'chengxing' },
{ label: '走团中', value: 'zoutuan' },
];
const otypesMapped = otypes.reduce((acc, cur) => ({ ...acc, [cur.value]: cur }), {});
const TagColorStyle = (tag, outerStyle = false) => {
const color = stringToColour(tag);
const outerStyleObj = outerStyle ? { borderColor: `${color}66`, backgroundColor: `${color}0D` } : {};
return { color: `${color}`, ...outerStyleObj };
};
const ChatListFilter = ({ ...props }) => {
const handleFilter = async (param) => {};
const [
{ tags: selectedTags, otype: selectedOType, ...filter },
setFilterTags, setFilterOtype, resetFilter
] = useConversationStore((state) => [
state.filter,
state.setFilterTags, state.setFilterOtype, state.resetFilter
]);
const [tags] = useConversationStore((state) => [state.tags]);
const [form] = Form.useForm();
const handleTagsChange = (tag, checked) => {
const nextSelectedTags = checked ? [...selectedTags, tag.key] : selectedTags.filter((t) => t !== tag.key);
setFilterTags(nextSelectedTags);
form.setFieldValue('tags', nextSelectedTags);
};
const onFinish = async (values) => {
const filterParam = objectMapper(values, { tags: {key:'tags', transform: (v) => v.join(',')} });
console.log('Received values of form[filter_form]: ', values, ' \n', filterParam);
await handleFilter(filterParam);
setOpenPopup(false);
};
const onReset = () => {
resetFilter();
form.resetFields();
}
const [openPopup, setOpenPopup] = useState(false);
return (
<>
<div className='my-1 flex justify-between items-center '>
<Radio.Group optionType={'button'} buttonStyle='solid' size='small' options={otypes} value={selectedOType} onChange={(e) => setFilterOtype(e.target.value)} />
<Popover destroyTooltipOnHide
placement='bottom' overlayClassName='max-w-80'
trigger={'click'}
open={openPopup}
onOpenChange={setOpenPopup}
title={
<div className='flex justify-between '>
<div>更多筛选</div>
<Button size='small' onClick={() => setOpenPopup(false)}>
&times;
</Button>
</div>
}
content={
<>
<Form form={form} name='conversation_filter_form' layout='vertical' size='small' initialValues={{}} onFinish={onFinish} className='*:mb-2'>
<Form.Item name={'otype'} label='订单'>
<Tag key={selectedOType} closeIcon={selectedOType!==''} onClose={() => setFilterOtype('')}>
{otypesMapped[selectedOType].label}
</Tag>
</Form.Item>
<Form.Item name={'tags'} label='标签' className='*.div:gap-1'>
{tags.map((tag, ti) => (
<Tag.CheckableTag className='mb-1'
key={tag.key}
checked={selectedTags.includes(tag.key)}
onChange={(checked) => handleTagsChange(tag, checked)}
style={TagColorStyle(tag.label, selectedTags.includes(tag.key))}>
{tag.label}
</Tag.CheckableTag>
))}
</Form.Item>
<Form.Item noStyle className='flex justify-center mb-0'>
<Button.Group>
<Button onClick={onReset} type='primary' ghost>
重置
</Button>
<Button htmlType='submit' type='primary'>
确定
</Button>
</Button.Group>
</Form.Item>
</Form>
</>
}>
<Button icon={isEmpty(selectedTags) ? <FilterOutlined /> : <FilterTwoTone />} type='text' size='middle' />
</Popover>
</div>
</>
);
};
export default ChatListFilter;

@ -0,0 +1,316 @@
import React, { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Dropdown, Input, Button, Empty, Tooltip, Tag, Select, Divider, Radio, Popover, theme, Form } from 'antd';
import { PlusOutlined, WhatsAppOutlined, LoadingOutlined, HistoryOutlined, FireOutlined,AudioTwoTone, FilterOutlined, TagsOutlined, TagsTwoTone, FilterTwoTone, MailOutlined, CloseOutlined, CloseCircleOutlined } from '@ant-design/icons';
import { fetchConversationsList, fetchOrderConversationsList, fetchConversationItemClose, fetchConversationsSearch, postNewConversationItem, fetchConversationItemUnread, fetchConversationItemTop, UNREAD_MARK, postConversationTags, deleteConversationTags } from '@/actions/ConversationActions';
import { ChatItem } from 'react-chat-elements';
// import ConversationsNewItem from './ConversationsNewItem';
import { isEmpty, olog, stringToColour } from '@/utils/commons';
import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore';
import { useVisibilityState } from '@/hooks/useVisibilityState';
import { OrderLabelDefaultOptions, OrderStatusDefaultOptions, RemindStateDefaultOptions } from '@/stores/OrderStore'
import ChannelLogo from './ChannelLogo';
import { DeliverIcon, ReadIcon, SentIcon, WABIcon } from '@/components/Icons';
const { Option, OptGroup } = Select;
const { useToken } = theme;
const TagColorStyle = (tag) => {
const color = stringToColour(tag);
return { color: `${color}`, borderColor: `${color}66`, backgroundColor: `${color}0D` }
}
const TagColorStyle_2 = (tag, outerStyle = false) => {
const color = stringToColour(tag);
const outerStyleObj = outerStyle ? { borderColor: `${color}66`, } : {};
return { color: `${color}`, ...outerStyleObj };
};
const NewTagForm = ({onSubmit,...props}) => {
const [form] = Form.useForm();
const [subLoding, setSubLoding] = useState(false);
const [tags, addTag] = useConversationStore(state => [state.tags, state.addTag]);
const onFinish = async (values) => {
console.log('Received values of form[new_tag]: ', values);
setSubLoding(true);
if (typeof onSubmit === 'function') {
onSubmit();
}
// debug:
setTimeout(() => {
setSubLoding(false);
addTag({ label: values.tag_label, key: values.tag_label, value: values.tag_label })
}, 2000);
form.resetFields();
}
return (
<Form
form={form}
name='new_tag_form'
layout='inline' size='small'
initialValues={{}}
onFinish={onFinish}>
<Form.Item name={'tag_label'} rules={[{ required: true, message: '请输入标签名' }]}>
<Input placeholder='新增并设置' />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit' loading={subLoding} >
确定
</Button>
</Form.Item>
</Form>
);
};
const EditChatMemoForm = ({onSubmit,...props}) => {
const [form] = Form.useForm();
const [subLoding, setSubLoding] = useState(false);
const onFinish = async (values) => {
console.log('Received values of form[chat_memo]: ', values);
setSubLoding(true);
// debug:
setTimeout(() => {
setSubLoding(false);
}, 2000);
if (typeof onSubmit === 'function') {
onSubmit();
}
form.resetFields();
}
return (
<Form
form={form}
name='chat_memo_form'
layout='inline' size='small'
initialValues={{}}
onFinish={onFinish}>
<Form.Item name={'memo'} rules={[{ required: true, message: '请输入备注' }]}>
<Input placeholder='输入备注' width={400} className='w-64' />
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit' loading={subLoding} >
确定
</Button>
</Form.Item>
</Form>
);
};
const ChatListItem = (({mobile, item, refreshConversationList,setListUpdateFlag,onSwitchConversation,tabSelectedConversation, ...props}) => {
const routerReplace = mobile === undefined ? true : false; // : true;
const routePrefix = mobile === undefined ? `/order/chat` : `/m/chat`;
const { state: orderRow } = useLocation();
const { coli_guest_WhatsApp } = orderRow || {};
const { order_sn } = useParams();
const navigate = useNavigate();
const userId = useAuthStore((state) => state.loginUser.userId);
const initialState = useConversationStore((state) => state.initialState);
const [currentConversation, setCurrentConversation] = useConversationStore((state) => [state.currentConversation, state.setCurrentConversation]);
const conversationsList = useConversationStore((state) => state.conversationsList);
const [conversationsListLoading, setConversationsListLoading] = useConversationStore((state) => [state.conversationsListLoading, state.setConversationsListLoading]);
const addToConversationList = useConversationStore((state) => state.addToConversationList);
const delConversationitem = useConversationStore((state) => state.delConversationitem);
const closedConversationsList = useConversationStore((state) => state.closedConversationsList);
const setClosedConversationList = useConversationStore((state) => state.setClosedConversationList);
const itemTagsKeys = (item.tags || []).map(t => t.key);
const [tags, addTag] = useConversationStore(state => [state.tags, state.addTag]);
const handleConversationItemClose = async (item) => {
await fetchConversationItemClose({ conversationid: item.sn, opisn: item.opi_sn });
delConversationitem(item);
if (String(order_sn) === String(item.coli_sn)) {
navigate(routePrefix, { replace: routerReplace });
}
const _clist = await fetchConversationsSearch({ opisn: userId, session_enable: 0 });
setClosedConversationList(_clist);
};
const handleConversationItemUnread = async (item) => {
await fetchConversationItemUnread({ conversationid: item.sn });
await refreshConversationList();
setListUpdateFlag(Math.random());
}
const handleConversationItemTop = async (item) => {
await fetchConversationItemTop({ conversationid: item.sn, top_state: item.top_state === 0 ? 1 : 0 });
await refreshConversationList();
setListUpdateFlag(Math.random());
}
const handleConversationItemTags = async (item, tagKey) => {
const _tags = item.tags || [];
if (_tags.includes(tagKey)) {
await deleteConversationTags({ conversationid: item.sn, tag_id: tagKey, opisn: userId })
} else {
await postConversationTags({ conversationid: item.sn, tag_id: tagKey, opisn: userId });
}
await refreshConversationList();
setListUpdateFlag(Math.random());
}
const [contextMenuOpen, setContextMenuOpen] = useState(false);
const handleContextMenuOpenChange = (nextOpen, info) => {
if (info.source === 'trigger' || nextOpen) {
setContextMenuOpen(nextOpen);
}
};
const [openTags, setOpenTags] = useState([]);
useEffect(() => {
if (contextMenuOpen === false) {
setOpenTags([]);
}
return () => {};
}, [contextMenuOpen])
return (
<>
<Dropdown
key={item.sn}
destroyPopupOnHide
trigger={['contextMenu']}
open={contextMenuOpen}
onOpenChange={handleContextMenuOpenChange}
menu={{
items: [
{ label: '置顶会话', key: 'top' },
// { label: '', key: 'no_top' },
{ label: '标记为未读', key: 'unread' },
{
label: '设置标签',
key: 'tags',
children: [
...tags.map((t) => ({
...t,
key: `tag_${t.key}`,
style: { color: stringToColour(t.label) },
icon: itemTagsKeys.includes(t.key) ? <CloseCircleOutlined /> : false,
})),
{
label: (
<>
<Popover content={<NewTagForm onSubmit={() => setContextMenuOpen(false)} />} placement='bottom' trigger={['click']}>
{/* todo: refresh list */}
<Button type='dashed' size='small' className='m-1'>
+新标签
</Button>
</Popover>
</>
),
key: 'new_tags',
},
],
onTitleClick: ({ key, domEvent }) => {
console.log(']]]', key);
},
},
{
label: (
<>
{/* todo: refresh list */}
<Popover overlayClassNam1e='w-80' content={<EditChatMemoForm onSubmit={() => setContextMenuOpen(false)} />} placement='bottom' trigger={['click']}>
{/* <Button type='text' size='small' className='m-1'> */}
编辑联系人
{/* </Button> */}
</Popover>
</>
),
key: 'remark',
},
{ type: 'divider' },
{ label: '隐藏会话', key: 'close', danger: true },
],
triggerSubMenuAction: 'click',
openKeys: openTags,
onOpenChange: (openKeys) => {
if (!isEmpty(openKeys) && contextMenuOpen) {
setOpenTags(openKeys);
}
},
onClick: ({ key, domEvent }) => {
domEvent.stopPropagation();
if (key.startsWith('tag_')) {
const tagKey = key.replace('tag_', '');
return handleConversationItemTags(item, tagKey);
}
switch (key) {
case 'top':
setContextMenuOpen(false);
return handleConversationItemTop(item);
case 'unread':
setContextMenuOpen(false);
return handleConversationItemUnread(item);
case 'remark':
setOpenTags([]);
return;
case 'close':
setContextMenuOpen(false);
return handleConversationItemClose(item);
default:
// setContextMenuOpen(false);
console.log('unknown key', key);
return;
}
},
}}>
<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' : '',
].join(' ')}>
{/* <div className='pl-4 pt-1 text-xs text-right'>
{tags.map((tag) => <Tag color={tag.color} key={tag.value}>{tag.label}</Tag>)}
</div> */}
<ChatItem
{...item}
key={item.sn}
id={item.sn}
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.whatsapp_name || item.whatsapp_phone_number}
// subtitle={item.coli_id}
subtitle={
<div>
{/* <ReadIcon /> */}
{/* <DeliverIcon /> */}
{/* <SentIcon /> */}
{/* todo: last message ⤴⤵↗️↖️↘✔️ */}
<span>{item.coli_id}</span>
<div className='text-sm'>
{[
{ label: '已付款', key: 'p1' },
{ label: '地接', key: 'p2' },
]?.map((tag) => (
<Tag key={tag.label} style={{ ...TagColorStyle(tag.label) }} className='text-xs px-0.5 me-0.5'>
{tag.label}
</Tag>
))}
<span title={'附加备注'}>附加备注</span>
</div>
</div>
}
date={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' : '',
// String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '',
// ].join(' ')}
// statusText={<WhatsAppOutlined key={'channel'} className='text-whatsapp' />}
statusText={<ChannelLogo channel={'waba'} />}
statusColor={'#fff'}
onClick={() => onSwitchConversation(item)}
customStatusComponents={[
...(item.unread_msg_count > 99 ? [() => <div className='w-4 h-4 bg-red-500 rounded-full' key={'unread'}></div>] : []),
// () => <span key={'tag'} className='self-end>💎💴👑💼🤝💤💔💨🕳🚫🎈🎊🎁📜</span>,
]}
/>
</div>
</Dropdown>
</>
);
});
export default ChatListItem;

@ -1,6 +1,6 @@
import { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { Dropdown, Input, Button, Empty, Tooltip, Tag, Select, Divider, Radio, Popover } from 'antd'; import { Dropdown, Input, Button, Empty, Tooltip, Tag, Select, Divider, Radio, Popover, theme } from 'antd';
import { PlusOutlined, WhatsAppOutlined, LoadingOutlined, HistoryOutlined, FireOutlined,AudioTwoTone, FilterOutlined, TagsOutlined, TagsTwoTone, FilterTwoTone } from '@ant-design/icons'; import { PlusOutlined, WhatsAppOutlined, LoadingOutlined, HistoryOutlined, FireOutlined,AudioTwoTone, FilterOutlined, TagsOutlined, TagsTwoTone, FilterTwoTone } from '@ant-design/icons';
import { fetchConversationsList, fetchOrderConversationsList, fetchConversationItemClose, fetchConversationsSearch, postNewConversationItem, fetchConversationItemUnread, UNREAD_MARK } from '@/actions/ConversationActions'; import { fetchConversationsList, fetchOrderConversationsList, fetchConversationItemClose, fetchConversationsSearch, postNewConversationItem, fetchConversationItemUnread, UNREAD_MARK } from '@/actions/ConversationActions';
import { ChatItem } from 'react-chat-elements'; import { ChatItem } from 'react-chat-elements';
@ -10,17 +10,26 @@ import useConversationStore from '@/stores/ConversationStore';
import useAuthStore from '@/stores/AuthStore'; import useAuthStore from '@/stores/AuthStore';
import { useVisibilityState } from '@/hooks/useVisibilityState'; import { useVisibilityState } from '@/hooks/useVisibilityState';
import { OrderLabelDefaultOptions, OrderStatusDefaultOptions, RemindStateDefaultOptions } from '@/stores/OrderStore' import { OrderLabelDefaultOptions, OrderStatusDefaultOptions, RemindStateDefaultOptions } from '@/stores/OrderStore'
import ChatListItem from './Components/ChatListItem';
import ChatListFilter from './Components/ChatListFilter';
const { Option, OptGroup } = Select; const { Option, OptGroup } = Select;
const { useToken } = theme;
const TagColorStyle = (tag) => {
const color = stringToColour(tag);
return { color: `${color}`, borderColor: `${color}66`, backgroundColor: `${color}0D` }
}
/** /**
* [] * []
*/ */
const Conversations = ({ mobile }) => { const Conversations = ({ mobile }) => {
const { token } = useToken();
const contentStyle = {
backgroundColor: token.colorBgElevated,
borderRadius: token.borderRadiusLG,
boxShadow: token.boxShadowSecondary,
};
const menuStyle = {
boxShadow: 'none',
};
const routerReplace = mobile === undefined ? true : false; // : true; const routerReplace = mobile === undefined ? true : false; // : true;
const routePrefix = mobile === undefined ? `/order/chat` : `/m/chat`; const routePrefix = mobile === undefined ? `/order/chat` : `/m/chat`;
const { state: orderRow } = useLocation(); const { state: orderRow } = useLocation();
@ -211,10 +220,22 @@ const Conversations = ({ mobile }) => {
</Select> </Select>
); );
const [newTagOpen, setNewTagOpen] = useState(false);
const [contextMenuOpen, setContextMenuOpen] = useState(false);
const handleContextMenuOpenChange = (nextOpen, info, ...x) => {
console.log(info);
console.log(nextOpen);
console.log(x);
if (info.source === 'trigger' || nextOpen) {
setContextMenuOpen(nextOpen);
}
};
return ( return (
<div className='flex flex-col h-inherit'> <div className='flex flex-col h-inherit'>
<div className='flex gap-1'> <div className='flex gap-1 items-center'>
<Button onClick={() => setNewChatModalVisible(true)} icon={<PlusOutlined />} type={'primary'} ghost shape={'circle'} /> <Button onClick={() => setNewChatModalVisible(true)} icon={<PlusOutlined />} type={'primary'} ghost shape={'circle'} size='small' />
<Input.Search <Input.Search
className='' className=''
ref={searchInputRef} ref={searchInputRef}
@ -265,63 +286,7 @@ const Conversations = ({ mobile }) => {
/> />
)} )}
</div> </div>
<div className='my-1 flex justify-between items-center '> <ChatListFilter />
<Radio.Group
optionType={'button'}
buttonStyle='solid'
size='small'
options={[
{ label: 'All', value: 'all' },
{ label: '重点', value: 'top' },
{ label: '次重点', value: 'second' },
{ label: '成行', value: 'go' },
{ label: '走团中', value: 'runing' },
]}
/>
{/* <Dropdown
trigger={'click'}
placement='bottom'
menu={{
items: [
{ label: '已付款', key: 'p1', value: 'p1', className: `!m-1 underline-offset-4 hover:underline focus:underline active:underline`, style: TagColorStyle('已付款') },
{ label: '地接', key: 'p2', value: 'p2', className: `!m-1 underline-offset-4 hover:underline focus:underline active:underline`, style: TagColorStyle('地接') },
],
}}>
<Button
onClick={() => {
// alert('1')
}}
icon={<TagsTwoTone />}
type='text'
size='middle'
/>
</Dropdown> */}
<Popover
placement='bottom'
trigger={'click'}
content={
<>
标签:
{[
{ label: '已付款', key: 'p1', value: 'p1', className: ``, style: TagColorStyle('已付款') },
{ label: '地接', key: 'p2', value: 'p2', className: ``, style: TagColorStyle('地接') },
].map((tag) => (
<Tag.CheckableTag key={tag.key} color={tag.color} style={tag.style} className={tag.className}>
{tag.label}
</Tag.CheckableTag>
))}
</>
}>
<Button
onClick={() => {
// alert('1')
}}
icon={<FilterTwoTone />}
type='text'
size='middle'
/>
</Popover>
</div>
<div className='flex-1 overflow-x-hidden overflow-y-auto relative'> <div className='flex-1 overflow-x-hidden overflow-y-auto relative'>
{conversationsListLoading && dataSource.length === 0 ? ( {conversationsListLoading && dataSource.length === 0 ? (
<div className='text-center py-2'> <div className='text-center py-2'>
@ -330,77 +295,90 @@ const Conversations = ({ mobile }) => {
) : null} ) : null}
{dataSource.map((item) => ( {dataSource.map((item) => (
<Dropdown <ChatListItem key={item.sn} {...{mobile, item, refreshConversationList,setListUpdateFlag,onSwitchConversation,tabSelectedConversation}} />
key={item.sn} // <Dropdown
menu={{ // key={item.sn}
items: [ // open={contextMenuOpen}
{ label: '置顶会话', key: 'top' }, // onOpenChange={handleContextMenuOpenChange}
{ label: '标记为未读', key: 'unread' }, // menu={{
{ label: '设置标签', key: 'tags' }, // selection // items: [
{ label: '编辑联系人', key: 'remark' }, // { label: '', key: 'top' },
{ label: <Divider className='my-0' />, key: 'd2' }, // // { label: '', key: 'no_top' },
{ label: '隐藏会话', key: 'close', danger: true }, // { label: '', key: 'unread' },
], // { label: '', key: 'tags', children: [...custom_tags.map((t) => ({ ...t, style: { color: stringToColour(t.label) } })),
onClick: ({ key, domEvent }) => { // { label: (<>
domEvent.stopPropagation(); // <Popover content={<Input placeholder='' />}>
switch (key) { // <Button type='dashed' size='small' className='m-1'>+</Button>
case 'close': // </Popover>
return handleConversationItemClose(item); // </>), key: 'new_tags' },
case 'unread': // ] }, // selection
return handleConversationItemUnread(item); // { label: '', key: 'remark' },
// { type: 'divider' },
// { label: '', key: 'close', danger: true },
// ],
// onClick: ({ key, domEvent }) => {
// domEvent.stopPropagation();
// switch (key) {
// case 'close':
// setContextMenuOpen(false);
// return handleConversationItemClose(item);
// case 'unread':
// setContextMenuOpen(false);
// return handleConversationItemUnread(item);
default: // default:
return; // setContextMenuOpen(false);
} // return;
}, // }
}} // },
trigger={['contextMenu']}> // }}
<div // trigger={['contextMenu']}>
className={[ // <div
'border-0 border-t1 border-solid border-neutral-200', // className={[
String(item.sn) === String(currentConversation.sn) ? '__active text-primary bg-whatsapp-bg' : '', // 'border-0 border-t1 border-solid border-neutral-200',
String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '', // String(item.sn) === String(currentConversation.sn) ? '__active text-primary bg-whatsapp-bg' : '',
].join(' ')}> // String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '',
<div className='pl-4 pt-1 text-xs text-right'>{/* {filterTags.map((tag) => <Tag color={tag.color} key={tag.value}>{tag.label}</Tag>)} */}</div> // ].join(' ')}>
<ChatItem // <div className='pl-4 pt-1 text-xs text-right'>{/* {filterTags.map((tag) => <Tag color={tag.color} key={tag.value}>{tag.label}</Tag>)} */}</div>
{...item} // <ChatItem
key={item.sn} // {...item}
id={item.sn} // key={item.sn}
letterItem={{ id: item.whatsapp_name || item.whatsapp_phone_number, letter: (item.whatsapp_name || item.whatsapp_phone_number).slice(0, 5) }} // id={item.sn}
alt={item.whatsapp_name} // letterItem={{ id: item.whatsapp_name || item.whatsapp_phone_number, letter: (item.whatsapp_name || item.whatsapp_phone_number).slice(0, 5) }}
title={item.whatsapp_name || item.whatsapp_phone_number} // alt={item.whatsapp_name}
// subtitle={item.coli_id} // title={item.whatsapp_name || item.whatsapp_phone_number}
subtitle={ // // subtitle={item.coli_id}
<div> // subtitle={
{item.coli_id} // <div>
<div> // {item.coli_id}
{[ // <div>
{ label: '已付款', key: 'p1' }, // {[
{ label: '地接', key: 'p2' }, // { label: '', key: 'p1' },
]?.map((tag) => ( // { label: '', key: 'p2' },
<Tag key={tag.label} style={{ ...TagColorStyle(tag.label) }} className='text-xs px-0.5 me-0.5'> // ]?.map((tag) => (
{tag.label} // <Tag key={tag.label} style={{ ...TagColorStyle(tag.label) }} className='text-xs px-0.5 me-0.5'>
</Tag> // {tag.label}
))} // </Tag>
</div> // ))}
</div> // </div>
} // </div>
date={item.last_received_time || item.last_send_time} // }
unread={item.unread_msg_count > 99 ? 0 : item.unread_msg_count} // date={item.last_received_time || item.last_send_time}
// className={[ // unread={item.unread_msg_count > 99 ? 0 : item.unread_msg_count}
// String(item.sn) === String(currentConversation.sn) ? '__active text-primary bg-whatsapp-bg' : '', // // className={[
// String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '', // // String(item.sn) === String(currentConversation.sn) ? '__active text-primary bg-whatsapp-bg' : '',
// ].join(' ')} // // String(item.sn) === String(tabSelectedConversation?.sn) ? ' bg-neutral-200' : '',
statusText={<WhatsAppOutlined key={'channel'} className='text-whatsapp' />} // // ].join(' ')}
statusColor={'#fff'} // statusText={<WhatsAppOutlined key={'channel'} className='text-whatsapp' />}
onClick={() => onSwitchConversation(item)} // statusColor={'#fff'}
customStatusComponents={[ // onClick={() => onSwitchConversation(item)}
...(item.unread_msg_count > 99 ? [() => <div className='w-4 h-4 bg-red-500 rounded-full' key={'unread'}></div>] : []), // customStatusComponents={[
// () => <span key={'tag'}>💎💴👑💼🤝💤💔💨🕳🚫🎈🎊🎁📜</span>, // ...(item.unread_msg_count > 99 ? [() => <div className='w-4 h-4 bg-red-500 rounded-full' key={'unread'}></div>] : []),
]} // // () => <span key={'tag'}>💎💴👑💼🤝💤💔💨🕳🚫🎈🎊🎁📜</span>,
/> // ]}
</div> // />
</Dropdown> // </div>
// </Dropdown>
))} ))}
{dataSource.length === 0 && <Empty description={'无数据'} />} {dataSource.length === 0 && <Empty description={'无数据'} />}
</div> </div>

Loading…
Cancel
Save