diff --git a/src/actions/ConversationActions.js b/src/actions/ConversationActions.js index c11044c..b457df3 100644 --- a/src/actions/ConversationActions.js +++ b/src/actions/ConversationActions.js @@ -17,6 +17,7 @@ export const fetchTemplates = async (params) => { 'say_hello_again', 'order_updated_specialist_assigned_christy', 'order_resumed_specialist_followup_schedule_sharon', + 'travel_service_update_v2', 'travel_service_update_v1', 'order_updated_specialist_assigned_sharon', 'first_message_for_not_reply', @@ -27,9 +28,17 @@ export const fetchTemplates = async (params) => { const scNames = ['trip_planner_showcase', 'showcase_different', 'order_status_updated']; // 客运 const crNames = [ + // 'notification_of_following_up_by_cr_v3', 'notification_of_one_day_before_ending_the_trip_by_cr_v2', - 'post_booking_confirmation_welcome', + 'one_day_after_payment_by_yuni', 'notification_of_status_changed', + 'notification_of_one_day_before_ending_the_trip_by_cr','one_day_after_payment_by_customer_relations', + 'one_day_before_ending_the_trip_contacted_by_yuni','one_day_before_ending_the_trip_first_time_by_yuni', + 'post_booking_confirmation_welcome', + ]; + const crNamesOmit = [ + 'birthday_greetings_by_marketing','one_day_before_ending_the_trip_by_marketing', + 'introduce_the_voucher_one_day_before_ending_the_trip_by_marketing', 'birthday_greetings_by_customer_relations_0', 'post_trip_voucher_issued', 'account_updated_order_ref', @@ -40,13 +49,8 @@ export const fetchTemplates = async (params) => { 'birthday_greetings_by_customer_relations_2', 'birthday_greetings_by_customer_relations_1', 'notification_of_account_updated_by_cr', - 'notification_of_one_day_before_ending_the_trip_by_cr', 'birthday_greetings_by_customer_relations', 'one_day_before_ending_the_trip_by_customer_relations', - 'one_day_after_payment_by_customer_relations', - 'birthday_greetings_by_marketing', 'one_day_before_ending_the_trip_by_marketing', - 'one_day_after_payment_by_yuni', 'introduce_the_voucher_one_day_before_ending_the_trip_by_marketing', - 'one_day_before_ending_the_trip_contacted_by_yuni', 'one_day_before_ending_the_trip_first_time_by_yuni' ] const canUseTemplates = (data?.result?.items || []) .filter((_t) => _t.status === 'APPROVED' && !['say_hello_from_trip_advisor', 'free_style_7', 'free_style_1', 'free_style_2'].includes(_t.name)) @@ -57,7 +61,7 @@ export const fetchTemplates = async (params) => { key: ele.name, // displayName: ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated']+`_${i}` : templatesDisplayNameMap?.[ele.name] || ele.name, displayName: templatesDisplayNameMap?.[ele.name] || (ele.name.startsWith('order_updated') ? templatesDisplayNameMap['order_updated']+`_${i}` : ele.name), - displayLanguage: crNames.includes(ele.name) ? ele.language + '-客运' : scNames.includes(ele.name) ? ele.language + '-示例' : ele.language, + displayLanguage: crNamesOmit.includes(ele.name) ? '客运-' : (crNames.includes(ele.name) || ele.name.includes('by_cr')) ? ele.language + '-客运' : scNames.includes(ele.name) ? ele.language + '-示例' : ele.language, })) const top2Name = topName.concat(canUseTemplates.filter(_t => _t.name.startsWith('order_updated')).map(_tem => _tem.name)); @@ -66,7 +70,9 @@ export const fetchTemplates = async (params) => { const second = canUseTemplates.filter(_t => _t.name.includes('free_style')); const secondS = second.sort(sortBy('name')); const raw = canUseTemplates.filter((_t) => !top2Name.includes(_t.name) && !_t.name.includes('free_style')); - return [...top, ...secondS, ...raw]; + // 剩下的排序 + const rawS = sortArrayByOrder(raw, 'name', [...crNames, ...scNames, ...crNamesOmit ]); + return [...top, ...secondS, ...rawS]; }; /** * ↑上面的模板名称bak diff --git a/src/channel/bubbleMsgUtils.js b/src/channel/bubbleMsgUtils.js index e71ad39..40b0a51 100644 --- a/src/channel/bubbleMsgUtils.js +++ b/src/channel/bubbleMsgUtils.js @@ -74,6 +74,11 @@ export const replaceTemplateString = (str, replacements) => { return result; } +export const whatsappTemplateBtnParamTypesMapped = { + 'copy_code': 'coupon_code', + // 'quick_reply': 'payload', +}; + /** * @deprecated 在渲染时处理 */ @@ -226,18 +231,18 @@ export const sentMsgTypeMapped = { ...msg.template, components: [ ...msg.template.components.filter((com) => !['footer', 'buttons'].includes(com.type.toLowerCase())), - ...(msg.template.components.filter((com) => 'buttons' === com.type.toLowerCase()).length > 0 - ? msg.template.components - .filter((com) => 'buttons' === com.type.toLowerCase())[0] - // .buttons.filter((btns) => ! ['phone_number', 'url'].includes( btns.type.toLowerCase())) - .buttons.filter((btns) => !isEmpty(btns.example)) // 静态按钮不发 - .map((btn, btnI) => ({ - type: 'button', - sub_type: btn.type.toLowerCase(), - index: btnI, - // parameters: [{ text: 'lq1FTtA8', type: 'text' }] - })) - : []), + // ...(msg.template.components.filter((com) => 'buttons' === com.type.toLowerCase()).length > 0 + // ? msg.template.components + // .filter((com) => 'buttons' === com.type.toLowerCase())[0] + // // .buttons.filter((btns) => ! ['phone_number', 'url'].includes( btns.type.toLowerCase())) + // .buttons.filter((btns) => !isEmpty(btns.example)) // 静态按钮不发 + // .map((btn, btnI) => ({ + // type: 'button', + // sub_type: btn.type.toLowerCase(), + // index: btnI, + // // parameters: [{ text: 'lq1FTtA8', type: 'text' }] + // })) + // : []), ], }, }), @@ -697,6 +702,11 @@ export const whatsappMsgTypeMapped = { data: (msg) => ({ id: msg.wamid, text: msg.reaction?.emoji || '' }), renderForReply: (msg) => ({ id: msg.wamid, message: msg.reaction?.emoji || '' }), }, + button: { + type: 'text', // todo: 后端返回 type='button' button: { payload, text } + data: (msg) => ({ id: msg.wamid, text: msg.button?.payload || msg.button?.text || '' }), + renderForReply: (msg) => ({ id: msg.wamid, message: msg.button?.payload || msg.button?.text || '' }), + }, document: { type: 'file', data: (msg) => ({ diff --git a/src/views/Conversations/Online/Components/BubbleIM.jsx b/src/views/Conversations/Online/Components/BubbleIM.jsx index 42b2205..1380c06 100644 --- a/src/views/Conversations/Online/Components/BubbleIM.jsx +++ b/src/views/Conversations/Online/Components/BubbleIM.jsx @@ -1,11 +1,11 @@ -import { createContext, useEffect, useState, memo } from 'react'; +import { memo } from 'react'; import { App, Button } from 'antd'; -import { MailFilled, MailOutlined, WhatsAppOutlined } from '@ant-design/icons'; +import { ExportOutlined, CopyOutlined, PhoneOutlined } from '@ant-design/icons'; import { MessageBox } from 'react-chat-elements'; import { groupBy, isEmpty } from '@/utils/commons'; import useConversationStore from '@/stores/ConversationStore'; import { useShallow } from 'zustand/react/shallow'; -import { WABIcon } from '@/components/Icons'; +import { ReplyIcon } from '@/components/Icons'; import ChannelLogo from './ChannelLogo'; const outboundStyle = { @@ -25,10 +25,10 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s const RenderText = memo(function renderText({ str, className, template, message }) { let headerObj, footerObj, buttonsArr; if (!isEmpty(template) && !isEmpty(template.components)) { - const componentsObj = groupBy(template.components, (item) => item.type); + const componentsObj = groupBy(template.components.concat(template?.components_omit || []), (item) => item.type); headerObj = componentsObj?.header?.[0]; footerObj = componentsObj?.footer?.[0]; - buttonsArr = componentsObj?.buttons?.reduce((r, c) => r.concat(c.buttons), []); + buttonsArr = componentsObj?.button; // ?.reduce((r, c) => r.concat(c.buttons), []); } const parts = str.split(/(https?:\/\/[^\s()]+|\p{Emoji_Presentation}|\d{4,})/gmu).filter((s) => s !== ''); @@ -84,17 +84,17 @@ const BubbleIM = ({ handlePreview, handleContactClick, setNewChatModalVisible, s {buttonsArr && buttonsArr.length > 0 ? (
{buttonsArr.map((btn, index) => - btn.type.toLowerCase() === 'url' ? ( - - ) : btn.type.toLowerCase() === 'phone_number' ? ( - ) : ( - ), )} diff --git a/src/views/Conversations/Online/Input/Template.jsx b/src/views/Conversations/Online/Input/Template.jsx index 27b0b58..15572bd 100644 --- a/src/views/Conversations/Online/Input/Template.jsx +++ b/src/views/Conversations/Online/Input/Template.jsx @@ -3,8 +3,8 @@ import { App, Popover, Flex, Button, List, Input, Tabs, Tag, Alert, Divider } fr import { MessageOutlined, SendOutlined } from '@ant-design/icons'; import useAuthStore from '@/stores/AuthStore'; import useConversationStore from '@/stores/ConversationStore'; -import { cloneDeep, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, sortObjectsByKeysMap, TagColorStyle } from '@/utils/commons'; -import { replaceTemplateString } from '@/channel/bubbleMsgUtils'; +import { cloneDeep, flush, getNestedValue, groupBy, objectMapper, removeFormattingChars, sortArrayByOrder, sortObjectsByKeysMap, TagColorStyle } from '@/utils/commons'; +import { replaceTemplateString, whatsappTemplateBtnParamTypesMapped } from '@/channel/bubbleMsgUtils'; import { isEmpty } from '@/utils/commons'; import useStyleStore from '@/stores/StyleStore'; @@ -36,16 +36,33 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac const keys = (templateText.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, '')) const paramsVal = keys.reduce((r, k) => ({ ...r, [k]: getNestedValue(valueMapped, [k]) }), {}) + if (key === 'header' && tempItem.components?.header?.[0]?.example?.header_url) { + const headerImg = { key: 'header_url', placeholder: '头图地址' }; + tempArr.unshift(headerImg); + } + if (key === 'buttons' && (tempItem.components?.buttons?.[0]?.buttons || []).findIndex(btn => btn.type === 'COPY_CODE') !== -1) { + const btnCode = { key: 'copy_code', placeholder: '复制条码' }; + tempArr.push(btnCode); + } + if (key === 'buttons' ) { + (tempItem.components?.buttons?.[0]?.buttons || []).filter(btn0 => btn0.type === 'URL').forEach((btn) => { + const hasParam = Object.prototype.hasOwnProperty.call(btn, "example"); + const templateUrl = btn.url || '' + const urlParamKeys = (templateUrl.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, '')).map(key => ({ key })) + hasParam ? tempArr.push(...urlParamKeys) : false; + }); + } + return ( <> - {tempArr.map((ele) => + {tempArr.map((ele, i) => typeof ele === 'string' ? ( {ele.replace(/\n+/g, '\n')} ) : ele.key.includes('free') || ele.key.includes('detail') ? ( { onInput(tempItem, ele.key, e.target.value, paramsVal) @@ -59,14 +76,14 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac /> ) : ( { onInput(tempItem, ele.key, e.target.value, paramsVal) }} className={ele.key.includes('free') || ele.key.includes('detail') ? `w-full block ` : `w-auto ${paramsVal[ele.key] ? 'max-w-24' : 'max-w-60'}`} size={'small'} title={ele.key} - placeholder={`${paramsVal[ele.key] || ele.key} 按Tab键跳到下一个空格`} + placeholder={`${paramsVal[ele.key] || ele.key} ${ele?.placeholder || '按Tab键跳到下一个空格'}`} value={activeInput[tempItem.name]?.[ele.key] || paramsVal[ele.key] || ''} // onPressEnter={() => handleSendTemplate(tempItem)} /> @@ -83,14 +100,19 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac return (
{'text' === headerObj.format.toLowerCase() &&
{renderForm({ tempItem }, 'header')}
} - {'image' === headerObj.format.toLowerCase() && } + {'image' === headerObj.format.toLowerCase() && ( +
+ + {renderForm({ tempItem }, 'header')} +
+ )} {['document', 'video'].includes(headerObj.format.toLowerCase()) && ( [ {headerObj.format} ]({headerObj.example.header_url}) )}
- ); + ) } const renderButtons = ({ tempItem }) => { if (isEmpty(tempItem.components.buttons)) { @@ -100,7 +122,8 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac return (
{buttons.map((btn, index) => - btn.type.toLowerCase() === 'url' ? ( + <> + {btn.type.toLowerCase() === 'url' ? ( @@ -108,11 +131,18 @@ const CategoryList = ({ dataSource, handleSendTemplate, valueMapped, onInput, ac - ) : ( - - ) + )} + {Object.prototype.hasOwnProperty.call(btn, "example") ? ({renderForm({ tempItem }, 'buttons')}) : null} + )}
); @@ -213,6 +243,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => { const handleSendTemplate = (fromTemplate) => { const mergeInput = { ...cloneDeep(valueMapped), ...activeInput[fromTemplate.name] }; + // console.log('----------------------------------------------', mergeInput) let valid = true; const msgObj = { type: 'whatsappTemplate', @@ -220,29 +251,82 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => { template: { name: fromTemplate.name, language: { code: fromTemplate.language }, - components: sortArrayByOrder(fromTemplate.components_origin.map((citem) => { + components: sortArrayByOrder(fromTemplate.components_origin.reduce((r, citem) => { const keys = ((citem?.text || '').match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, '')); const params = keys.map((v) => ({ type: 'text', text: getNestedValue(mergeInput, [v]) })); + const notTextKeys = []; + const paramNotText = []; if (citem.type.toLowerCase() === 'header' && (citem?.format || 'text').toLowerCase() !== 'text') { - params[0] = { type: citem.format.toLowerCase(), [citem.format.toLowerCase()]: { link: citem.example.header_url[0] } }; + params[0] = { type: citem.format.toLowerCase(), [citem.format.toLowerCase()]: { link: mergeInput?.header_url || citem.example.header_url[0] } }; + + // 头图可以不替换 + // notTextKeys.push('header_url'); + // mergeInput?.header_url ? paramNotText.push(mergeInput?.header_url || '') : false; } + let buttonsComponents; + if (citem.type.toLowerCase() === 'buttons' ) { + buttonsComponents = citem.buttons.map((btn, i) => { + const hasParam = Object.prototype.hasOwnProperty.call(btn, "example"); // whatsappTemplateBtnParamTypesMapped + let fillBtn = {}; + const paramKey = whatsappTemplateBtnParamTypesMapped[btn.type.toLowerCase()]; + if (paramKey) { + params[0] = { type: paramKey, [paramKey]: mergeInput?.[btn.type.toLowerCase()] || '' }; + notTextKeys.push(paramKey); + mergeInput?.[btn.type.toLowerCase()] ? paramNotText.push(mergeInput?.[btn.type.toLowerCase()] || '') : false; + fillBtn = { text: btn.text || `${btn.type}:${mergeInput?.[btn.type.toLowerCase()] || ''}` }; + } + if (btn.type.toLowerCase() === 'url') { + const templateUrl = btn.url || ''; + const urlParamKeys = (templateUrl.match(/{{(.*?)}}/g) || []).map((key) => key.replace(/{{|}}/g, '')); + const urlParams = urlParamKeys.map(key => ({ type: 'text', text: getNestedValue(mergeInput, [key]) })) + params.push(...urlParams); + notTextKeys.push(...urlParamKeys); + urlParamKeys.forEach(key => { + if (getNestedValue(mergeInput, [key])) { + paramNotText.push(getNestedValue(mergeInput, [key])); + } + }); + fillBtn = hasParam ? { text: btn.text, url: replaceTemplateString(templateUrl, paramNotText) } : {}; + } + + // if (hasParam) { + // notTextKeys.push(paramKey); + // mergeInput?.[btn.type.toLowerCase()] ? paramNotText.push(mergeInput?.[btn.type.toLowerCase()] || '') : false; + // } + + return hasParam ? { type: 'button', index: i, sub_type: btn.type.toLowerCase(), parameters: params, ...fillBtn } : null; + }) + buttonsComponents = flush(buttonsComponents); + } + // console.log('******', buttonsComponents, '\n', notTextKeys, '\n', paramNotText) + const paramText = keys.length ? params.map((p) => p.text) : []; const fillTemplate = paramText.length ? replaceTemplateString(citem?.text || '', paramText) : citem?.text || ''; valid = keys.length !== paramText.filter((s) => s).length ? false : valid; - return ['body', 'header'].includes(citem.type.toLowerCase()) ? { + valid = notTextKeys.length !== paramNotText.filter((s) => s).length ? false : valid; + const _components = ['body', 'header'].includes(citem.type.toLowerCase()) ? [{ type: citem.type.toLowerCase(), parameters: params, text: fillTemplate, - } : {...citem, type: citem.type.toLowerCase(),}; - }), 'type', ['header', 'body', 'footer', 'buttons'] ), + }] : ['buttons'].includes(citem.type.toLowerCase()) ? buttonsComponents : [{...citem, type: citem.type.toLowerCase(),}]; + return r.concat(_components); + }, []), 'type', ['body', 'header', 'footer', 'button', 'buttons'] ), + components_omit: fromTemplate.components_origin.reduce((r, citem) => { + const _componentItems = + citem.type.toLowerCase() === 'buttons' + ? citem.buttons.map((btn, index) => ({ ...btn, index, type: 'button', sub_type: btn.type.toLowerCase() })) + : [{ ...citem, type: citem.type.toLowerCase() }] + const staticComponents = _componentItems.filter((item) => !item.example && item.type.toLowerCase() !== 'body') + return r.concat(staticComponents) + }, []), }, template_origin: fromTemplate, }; const plainTextMsgObj = { type: 'text', - text: msgObj.template.components.map((citem) => citem.text).join(''), + text: msgObj.template.components.filter(com => com.type.toLowerCase() === 'body').map((citem) => citem.text).join(''), }; if (valid !== true) { notification.warning({ @@ -254,6 +338,7 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => { }); return false; } + // console.log('------------------------------------------------------------------------------', msgObj ); invokeSendMessage(channel === 'waba' ? msgObj : plainTextMsgObj); setOpenTemplates(false); setActiveInput({}); @@ -267,10 +352,6 @@ const InputTemplate = ({ disabled = false, invokeSendMessage, channel }) => { }); }; - - - - return ( <> { ...(Object.keys(templateLangMapped).map(lang => ({ key: lang, label: lang.toUpperCase(), children: }))) - ]} defaultActiveKey='utility' tabBarExtraContent={{right: , }} size='small' /> + ]} defaultActiveKey='utility' tabBarExtraContent={{right: , }} size='small' /> ) : ( // Search result