From 0a658f5b3be75ef4ae5b929c9c2a3c33428ae4c3 Mon Sep 17 00:00:00 2001 From: Lei OT Date: Mon, 3 Jun 2024 14:47:04 +0800 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=E8=AF=AD=E9=9F=B3=20=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=20=E5=B1=9E=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channel/whatsappUtils.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/channel/whatsappUtils.js b/src/channel/whatsappUtils.js index d23f2d5..68d034b 100644 --- a/src/channel/whatsappUtils.js +++ b/src/channel/whatsappUtils.js @@ -411,9 +411,11 @@ export const whatsappMsgTypeMapped = { type: 'audio', data: (msg) => ({ id: msg.wamid, + audioProps: { preload: 'auto' }, data: { audioURL: msg.audio.link, audioType: msg.audio?.mime_type || 'audio/ogg', + controlsList: 'nofullscreen', }, }), renderForReply: (msg) => ({ From ec2cfbbf6c132e2c2c8cd61376ef24e4f986a43e Mon Sep 17 00:00:00 2001 From: Lei OT Date: Wed, 5 Jun 2024 09:34:34 +0800 Subject: [PATCH 2/4] =?UTF-8?q?perf:=20=E4=BF=AE=E6=94=B9=E5=8F=91?= =?UTF-8?q?=E9=80=81=E5=A4=B1=E8=B4=A5=E6=8F=90=E7=A4=BA.=20FORBIDDEN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channel/whatsappUtils.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/channel/whatsappUtils.js b/src/channel/whatsappUtils.js index 68d034b..a785876 100644 --- a/src/channel/whatsappUtils.js +++ b/src/channel/whatsappUtils.js @@ -609,10 +609,12 @@ export const parseRenderMessageList = (messages) => { export const whatsappError = { 'BAD_REQUEST': ' ', 'PARAM_INVALID': '参数错误, 请联系技术组', - 'INTERNAL_SERVER_ERROR': '无法连接WhatsApp.\n请稍候重试', - '2': '无法连接WhatsApp.\n请稍候重试', // (#2) Service temporarily unavailable + 'INTERNAL_SERVER_ERROR': '无法连接WhatsApp.\n请稍后重试', + '2': '[2] 无法连接WhatsApp.\n请稍后重试', // (#2) Service temporarily unavailable 'INVALID_PHONE_NUMBER': '无效号码, 请修正号码后重新从订单进入会话', '100': '参数错误, 请联系技术组', + 'FORBIDDEN': '[FORBIDDEN] ', + '4': '[4] 无法连接WhatsApp.\n请稍后重试', // (#4) Application request limit reached '131026': '[131026] 消息无法投递(未同意WhatsApp 的隐私政策).\n请使用邮件联系', '131047': '[131047] 会话未激活. \n请使用模板消息💬发送', '131053': '[131053] 文件上传失败.', From 6ecfee241515ee3bd0f33eca888e9b5cf68418ea Mon Sep 17 00:00:00 2001 From: YCC Date: Wed, 5 Jun 2024 17:43:10 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E8=AF=AD=E9=9F=B3=E9=80=9A=E8=AF=9D?= =?UTF-8?q?=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 + package.json | 2 + src/config.js | 1 + src/main.jsx | 5 + src/stores/CallCenterStore.js | 104 +++++ src/views/CallCenter.jsx | 59 +++ .../Online/ConversationsList.jsx | 3 +- .../Online/order/CustomerProfile.jsx | 401 ++++++++++-------- src/views/DesktopApp.jsx | 3 +- 9 files changed, 399 insertions(+), 182 deletions(-) create mode 100644 src/stores/CallCenterStore.js create mode 100644 src/views/CallCenter.jsx diff --git a/README.md b/README.md index 27219c3..ecb5b9a 100644 --- a/README.md +++ b/README.md @@ -19,3 +19,6 @@ npm version minor [聊天式销售平台需求文档](https://www.kdocs.cn/l/calaUjgmCmDA?from=docs&reqtype=kdocs&startTime=1703645330177&createDirect=true&newFile=true) +## vonage语音视频 +安装模块 npm i @vonage/client-sdk + diff --git a/package.json b/package.json index de2adca..53093e2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "preview": "vite preview" }, "dependencies": { + "@vonage/client-sdk": "^1.6.0", "antd": "^5.14.0", "crypto-js": "^4.2.0", "dayjs": "^1.11.10", @@ -35,6 +36,7 @@ "eslint-plugin-react-refresh": "^0.4.3", "postcss": "^8.4.33", "tailwindcss": "^3.4.1", + "@vonage/client-sdk": "^1.6.0", "vite": "^4.5.1", "vite-plugin-css-modules": "^0.0.1", "vite-plugin-windicss": "^1.9.3", diff --git a/src/config.js b/src/config.js index 5c531f9..241f8bc 100644 --- a/src/config.js +++ b/src/config.js @@ -6,6 +6,7 @@ export const API_HOST = 'https://p9axztuwd7x8a7.mycht.cn/whatsapp_server'; export const WS_URL = 'wss://p9axztuwd7x8a7.mycht.cn/whatsapp_server'; // prod: +export const VONAGE_URL = 'https://p9axztuwd7x8a7.mycht.cn/vonage-server'; // 语音和视频接口: export const DATE_FORMAT = 'YYYY-MM-DD'; export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; diff --git a/src/main.jsx b/src/main.jsx index 3c63d25..3680e42 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -16,6 +16,7 @@ import ErrorPage from '@/components/ErrorPage' import ChatWindow from '@/views/ChatWindow' import MobileConversation from '@/views/mobile/Conversation' import MobileChat from '@/views/mobile/Chat' +import CallCenter from '@/views/CallCenter' import MobileSecondHeader from '@/views/mobile/SecondHeaderWrapper'; import CustomerProfile from '@/views/Conversations/Online/order/CustomerProfile'; @@ -52,6 +53,8 @@ const router = createBrowserRouter([ element: , children: [ { path: 'm/order', element: }, + { path: 'callcenter/call', element: }, + { path: 'callcenter/call/:phonenumber', element: }, ], }, ] @@ -68,6 +71,8 @@ const router = createBrowserRouter([ { path: 'account/profile', element: }, { path: 'chat/unassign/:whatsappid', element: }, { path: 'chat/unassign', element: }, + { path: 'callcenter/call', element: }, + { path: 'callcenter/call/:phonenumber', element: }, ], }, ], diff --git a/src/stores/CallCenterStore.js b/src/stores/CallCenterStore.js new file mode 100644 index 0000000..19a33e1 --- /dev/null +++ b/src/stores/CallCenterStore.js @@ -0,0 +1,104 @@ +import { create } from "zustand"; +import { VonageClient } from "@vonage/client-sdk"; +import { fetchJSON } from "@/utils/request"; +import { prepareUrl, isNotEmpty } from "@/utils/commons"; +import { VONAGE_URL, DATETIME_FORMAT } from "@/config"; +import dayjs from "dayjs"; + +const callCenterStore = create((set, get) => ({ + client: new VonageClient({ apiUrl: "https://api-ap-3.vonage.com", websocketUrl: "wss://ws-ap-3.vonage.com" }), + call_id: 0, + loading: false, + logs: "", + + //初始化 Vonage + init_vonage: user_id => { + const { client, log } = get(); + set({ loading: true }); + const fetchUrl = prepareUrl(VONAGE_URL + "/jwt") + .append("user_id", user_id) + .build(); + return fetchJSON(fetchUrl).then(json => { + if (json.status === 200) { + let jwt = json.token; + + client + .createSession(jwt) + .then(sessionId => { + log("Id of created session: ", sessionId); + }) + .catch(error => { + log("Error creating session: ", error); + }); + + client.on("sessionError", reason => { + // After creating a session + log("Session error reason: ", reason); + }); + + client.on("legStatusUpdate", (callId, legId, status) => { + // After creating a session + log({ callId, legId, status }); + }); + + client.on("callInvite", (callId, from, channelType) => { + log({ callId, from, channelType }); // Answer / Reject Call + }); + + client.on("callHangup", (callId, callQuality, reason) => { + log(`Call ${callId} has hung up, callQuality:${callQuality}, reason:${reason}`); + set({ call_id: 0 }); + }); + + client.on("sessionError", error => { + log({ error }); + }); + } else { + throw new Error("请求jwt失败"); + } + set({ loading: false }); + }); + }, + + log: (...message) => { + const { logs } = get(); + console.log(message); + set({ logs: [...logs, dayjs().format(DATETIME_FORMAT) + " : " + JSON.stringify(message)] }); + }, + + // 创建一个语音通话 + make_call: phone_number => { + const { client, log } = get(); + if (!isNotEmpty(phone_number)) { + log("请输入电话号码"); + return; + } + log("开始拨号:" + phone_number); + if (client) { + set({ loading: true }); + client + .serverCall({ to: phone_number }) + .then(callId => { + log("Id of created call: ", callId); + set({ call_id: callId }); + set({ loading: false }); + }) + .catch(error => { + log("Error making call: ", error); + set({ loading: false }); + }); + } + }, + + // 挂断语音通话 + hang_up: () => { + const { client, call_id, log } = get(); + log("挂断电话"); + if (call_id) { + client.hangup(call_id); + set({ call_id: 0 }); + } + }, +})); + +export default callCenterStore; diff --git a/src/views/CallCenter.jsx b/src/views/CallCenter.jsx new file mode 100644 index 0000000..1664d2a --- /dev/null +++ b/src/views/CallCenter.jsx @@ -0,0 +1,59 @@ +import { useCallback, useState, useEffect } from "react"; +import { Grid, Divider, Layout, Flex, Spin, Input, Col, Row, List, Typography } from "antd"; +import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined } from "@ant-design/icons"; +import { useParams, useHref, useNavigate } from "react-router-dom"; +import { isEmpty } from "@/utils/commons"; + +import callCenterStore from "@/stores/CallCenterStore"; +import useAuthStore from "@/stores/AuthStore"; + +const CallCenter = props => { + const href = useHref(); + const navigate = useNavigate(); + const { phonenumber } = useParams(); + const [init_vonage, make_call, hang_up, logs, call_id, loading] = callCenterStore(state => [state.init_vonage, state.make_call, state.hang_up, state.logs, state.call_id, state.loading]); + const [loginUser] = useAuthStore(state => [state.loginUser]); + const [phone_number, setPhone_number] = useState(phonenumber); + + useEffect(() => { + if (loginUser.userId === -1 && href.indexOf("/p/") === -1) { + navigate("/p/dingding/login?origin_url=" + href); + } else { + init_vonage(loginUser.userId); + } + }, [href, navigate, init_vonage, loginUser]); + + const oncall = () => { + if (isEmpty(call_id)) { + make_call(phone_number); + } else { + hang_up(); + } + }; + + return ( + <>
+ + + + } + suffix={loading ? : ""} + enterButton={call_id ? "挂断" : "拨号"} + onSearch={oncall} + onChange={e => { + setPhone_number(e.target.value); + }}> + + + + + + Console Logs} bordered dataSource={logs} renderItem={item => {item}} /> + + ); +}; +export default CallCenter; diff --git a/src/views/Conversations/Online/ConversationsList.jsx b/src/views/Conversations/Online/ConversationsList.jsx index 61ee65f..0b238a7 100644 --- a/src/views/Conversations/Online/ConversationsList.jsx +++ b/src/views/Conversations/Online/ConversationsList.jsx @@ -1,7 +1,7 @@ import { useEffect, useState, useRef } from 'react'; import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { Dropdown, Input, Button, Empty, Tooltip, Tag, Select } from 'antd'; -import { PlusOutlined, WhatsAppOutlined, LoadingOutlined, HistoryOutlined, FireOutlined, } from '@ant-design/icons'; +import { PlusOutlined, WhatsAppOutlined, LoadingOutlined, HistoryOutlined, FireOutlined,AudioTwoTone } from '@ant-design/icons'; import { fetchConversationsList, fetchOrderConversationsList, fetchConversationItemClose, fetchConversationsSearch, postNewConversationItem, fetchConversationItemUnread, UNREAD_MARK } from '@/actions/ConversationActions'; import { ChatItem } from 'react-chat-elements'; import ConversationsNewItem from './ConversationsNewItem'; @@ -263,6 +263,7 @@ const Conversations = ({ mobile }) => { type='text' /> + { mobile === undefined?'':{navigate(`/callcenter/call`)}} />}
{conversationsListLoading && dataSource.length === 0 ? ( diff --git a/src/views/Conversations/Online/order/CustomerProfile.jsx b/src/views/Conversations/Online/order/CustomerProfile.jsx index b349837..05411e1 100644 --- a/src/views/Conversations/Online/order/CustomerProfile.jsx +++ b/src/views/Conversations/Online/order/CustomerProfile.jsx @@ -1,190 +1,231 @@ -import { LinkOutlined, MailOutlined, PhoneOutlined, UserOutlined, WhatsAppOutlined } from '@ant-design/icons' -import { App, Button, Card, Empty, Flex, Select, Spin, Typography, Divider, Modal } from 'antd' -import { useEffect, useState, useRef } from 'react' +import { LinkOutlined, MailOutlined, PhoneOutlined, UserOutlined, WhatsAppOutlined } from "@ant-design/icons"; +import { App, Button, Card, Empty, Flex, Select, Spin, Typography, Divider, Modal } from "antd"; +import { useEffect, useState, useRef } from "react"; +import { useNavigate } from "react-router-dom"; -import { copy, isEmpty } from '@/utils/commons' -import { Conditional } from '@/components/Conditional' -import useConversationStore from '@/stores/ConversationStore' -import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions } from '@/stores/OrderStore' -import useAuthStore from '@/stores/AuthStore' -import QuotesHistory from './QuotesHistory' -import ConversationBind from './../ConversationBind'; -import ConversationsNewItem from './../ConversationsNewItem'; -import { useConversationNewItem } from '@/hooks/useConversation'; +import { copy, isEmpty } from "@/utils/commons"; +import { Conditional } from "@/components/Conditional"; +import useConversationStore from "@/stores/ConversationStore"; +import { useOrderStore, OrderLabelDefaultOptions, OrderStatusDefaultOptions } from "@/stores/OrderStore"; +import useAuthStore from "@/stores/AuthStore"; +import QuotesHistory from "./QuotesHistory"; +import ConversationBind from "./../ConversationBind"; +import ConversationsNewItem from "./../ConversationsNewItem"; +import { useConversationNewItem } from "@/hooks/useConversation"; -const CustomerProfile = (() => { - const { notification, message } = App.useApp() - const [loading, setLoading] = useState(false) - const [isModalOpen, setIsModalOpen] = useState(false) - const orderCommentRef = useRef(null) - const currentOrder = useConversationStore((state) => state.currentConversation?.coli_sn || '') - const currentConversationID = useConversationStore((state) => state.currentConversation?.sn || '') - const [updateCurrentConversation] = useConversationStore(((state) => [state.updateCurrentConversation])); - const loginUser = useAuthStore((state) => state.loginUser) - const { orderDetail, customerDetail, lastQuotation, quotationList, - fetchOrderDetail, setOrderPropValue, appendOrderComment - } = useOrderStore() +const CustomerProfile = () => { + const { notification, message } = App.useApp(); + const [loading, setLoading] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const orderCommentRef = useRef(null); + const currentOrder = useConversationStore(state => state.currentConversation?.coli_sn || ""); + const currentConversationID = useConversationStore(state => state.currentConversation?.sn || ""); + const [updateCurrentConversation] = useConversationStore(state => [state.updateCurrentConversation]); + const loginUser = useAuthStore(state => state.loginUser); + const { orderDetail, customerDetail, lastQuotation, quotationList, fetchOrderDetail, setOrderPropValue, appendOrderComment } = useOrderStore(); - const orderLabelOptions = copy(OrderLabelDefaultOptions) - orderLabelOptions.unshift({ value: 0, label: '未设置', disabled: true, }) + const navigate = useNavigate(); + const orderLabelOptions = copy(OrderLabelDefaultOptions); + orderLabelOptions.unshift({ value: 0, label: "未设置", disabled: true }); - const orderStatusOptions = copy(OrderStatusDefaultOptions) + const orderStatusOptions = copy(OrderStatusDefaultOptions); - useEffect(() => { - if (currentOrder) { - setLoading(true) - fetchOrderDetail(currentOrder) - .finally(() => setLoading(false)) - .catch(reason => { - notification.error({ - message: '查询出错', - description: reason.message, - placement: 'top', - duration: 60, - }) - }) - } - }, [currentOrder]) + useEffect(() => { + if (currentOrder) { + setLoading(true); + fetchOrderDetail(currentOrder) + .finally(() => setLoading(false)) + .catch(reason => { + notification.error({ + message: "查询出错", + description: reason.message, + placement: "top", + duration: 60, + }); + }); + } + }, [currentOrder]); - let regularText = '' - if (orderDetail.buytime > 0) regularText = '(R' + orderDetail.buytime + ')' + let regularText = ""; + if (orderDetail.buytime > 0) regularText = "(R" + orderDetail.buytime + ")"; - const { openOrderContactConversation } = useConversationNewItem(); - const [newChatModalVisible, setNewChatModalVisible] = useState(false); - const [newChatFormValues, setNewChatFormValues] = useState({}); - const handleNewChat = async (values) => { - const newContact = { wa_id: values.wa_id }; - openOrderContactConversation(newContact.wa_id); - setNewChatModalVisible(false); - } + const { openOrderContactConversation } = useConversationNewItem(); + const [newChatModalVisible, setNewChatModalVisible] = useState(false); + const [newChatFormValues, setNewChatFormValues] = useState({}); + const handleNewChat = async values => { + const newContact = { wa_id: values.wa_id }; + openOrderContactConversation(newContact.wa_id); + setNewChatModalVisible(false); + }; - if (currentOrder) { - return ( -
- - { - setOrderPropValue(currentOrder, 'orderlabel', value) - .then(() => { - message.success('设置成功') - }) - .catch(reason => { - notification.error({ - message: '设置出错', - description: reason.message, - placement: 'top', - duration: 60, - }) - }) - }} - value={orderDetail.tags} - options={orderLabelOptions} - />, - { + setOrderPropValue(currentOrder, "orderstatus", value) + .then(() => { + message.success("设置成功"); + }) + .catch(reason => { + notification.error({ + message: "设置出错", + description: reason.message, + placement: "top", + duration: 60, + }); + }); + }} + value={orderDetail.states} + options={orderStatusOptions} + />, + ]}> + + + + + {customerDetail.name + regularText} + + + + + + + + {customerDetail.email} + + + + + + + + + + 最新报价 + + + 0} + whenFalse={暂无报价}>} + whenTrue={ + <> +

+ + +  {lastQuotation.lettertitle} + +

+ + + + + } + /> +
- 表单信息 -

- { - const orderCommnet = orderCommentRef.current.value - if (isEmpty(orderCommnet)) { - message.warning('请输入备注后再提交。') - } else { - appendOrderComment(loginUser.userId, currentOrder, orderCommnet) - .then(() => { - message.success('添加成功') - setIsModalOpen(false) - }) - .catch(reason => { - notification.error({ - message: '添加出错', - description: reason.message, - placement: 'top', - duration: 60, - }) - }) - } - orderCommentRef.current.value = '' - }} - onCancel={() => {setIsModalOpen(false)}}> - - - -
- setNewChatModalVisible(false)} /> -
- ) - } else { - return ( - 暂无相关订单} - > - updateCurrentConversation({coli_sn})} /> - - ) - } -}) + + 表单信息 + +

+ { + const orderCommnet = orderCommentRef.current.value; + if (isEmpty(orderCommnet)) { + message.warning("请输入备注后再提交。"); + } else { + appendOrderComment(loginUser.userId, currentOrder, orderCommnet) + .then(() => { + message.success("添加成功"); + setIsModalOpen(false); + }) + .catch(reason => { + notification.error({ + message: "添加出错", + description: reason.message, + placement: "top", + duration: 60, + }); + }); + } + orderCommentRef.current.value = ""; + }} + onCancel={() => { + setIsModalOpen(false); + }}> + + + + + setNewChatModalVisible(false)} /> +
+ ); + } else { + return ( + 暂无相关订单}> + updateCurrentConversation({ coli_sn })} /> + + ); + } +}; -export default CustomerProfile +export default CustomerProfile; diff --git a/src/views/DesktopApp.jsx b/src/views/DesktopApp.jsx index 005d90b..430d28b 100644 --- a/src/views/DesktopApp.jsx +++ b/src/views/DesktopApp.jsx @@ -31,7 +31,7 @@ function DesktopApp() { let defaultPath = '/order/follow' if (href !== '/') { - const splitPath = href.split('/') + const splitPath = href.split('/'); if (splitPath.length > 2) { defaultPath = '/' + splitPath[1] + '/' + splitPath[2] } @@ -94,6 +94,7 @@ function DesktopApp() { ), }, + { key: '/callcenter/call', label: 语音通话 }, { key: '/chat/history', label: 聊天记录 }, ]} /> From 01b8c2770e6e2411b717557dabc510747438a19b Mon Sep 17 00:00:00 2001 From: YCC Date: Wed, 5 Jun 2024 17:56:40 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E7=94=A8=E4=BA=8E=E8=BE=93=E5=85=A5?= =?UTF-8?q?=E7=94=B5=E8=AF=9D=E5=8F=B7=E7=A0=81=E7=9A=84=E6=8E=A7=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/views/CallCenter.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/views/CallCenter.jsx b/src/views/CallCenter.jsx index 1664d2a..28844e6 100644 --- a/src/views/CallCenter.jsx +++ b/src/views/CallCenter.jsx @@ -32,11 +32,13 @@ const CallCenter = props => { }; return ( - <>
+ <> +