| | |
| | | import { PageContainer, ProTable } from '@ant-design/pro-components'; |
| | | import { DatePicker, Select, Tag } from 'antd'; |
| | | import { useRef } from 'react'; |
| | | import { maskPhoneNumber } from '@/utils/utils'; |
| | | import { PageContainer, ProTable } from '@ant-design/pro-components'; |
| | | import { DatePicker, Input, message, Select, Tag, Typography } from 'antd'; |
| | | import moment from 'moment'; |
| | | import { useRef } from 'react'; |
| | | import { getCallRecords } from './service'; |
| | | |
| | | const { RangePicker } = DatePicker; |
| | | |
| | | const intentionOptions = ['A', 'B', 'C', 'D', 'E', 'F', 'G'].map((item) => ({ |
| | | label: item, |
| | | value: item, |
| | | })); |
| | | const callTypeOptions = [ |
| | | { label: '呼入', value: 'callin' }, |
| | | { label: '外呼', value: 'callout' }, |
| | | { label: '双向呼叫', value: 'twoWayCall' }, |
| | | { label: '智能外呼', value: 'autoCallout' }, |
| | | { label: '外呼任务', value: 'callTask' }, |
| | | ]; |
| | | |
| | | const callTypeLabels = Object.fromEntries( |
| | | callTypeOptions.map(({ label, value }) => [value, label]), |
| | | ); |
| | | |
| | | const formatDateTime = (value) => { |
| | | if (!value) { |
| | | return ''; |
| | | } |
| | | |
| | | if (typeof value === 'string') { |
| | | return value; |
| | | } |
| | | |
| | | if (typeof value.format === 'function') { |
| | | return value.format('YYYY-MM-DD HH:mm:ss'); |
| | | } |
| | | |
| | | return ''; |
| | | if (!value) return ''; |
| | | return typeof value.format === 'function' ? value.format('YYYY-MM-DD HH:mm:ss') : value; |
| | | }; |
| | | |
| | | const normalizeSearchParams = (params) => { |
| | | const { current, pageSize, created_time, intention, ...rest } = params; |
| | | const [startTime, endTime] = Array.isArray(created_time) ? created_time : []; |
| | | const { current, pageSize, timeRange, ...rest } = params; |
| | | const [startTime, endTime] = Array.isArray(timeRange) ? timeRange : []; |
| | | |
| | | return { |
| | | ...rest, |
| | | created_time: startTime && endTime ? `${formatDateTime(startTime)}~${formatDateTime(endTime)}` : '', |
| | | intention: Array.isArray(intention) ? intention.join(',') : intention || '', |
| | | page: current || 1, |
| | | pagesize: pageSize || 10, |
| | | startTime: formatDateTime(startTime), |
| | | endTime: formatDateTime(endTime), |
| | | page: current > 0 ? current : 1, |
| | | pageSize: pageSize > 0 ? pageSize : 1000, |
| | | }; |
| | | }; |
| | | |
| | | const renderText = (text) => text || '-'; |
| | | |
| | | const CallRecords = () => { |
| | | const actionRef = useRef(); |
| | | const defaultTimeRange = [moment().subtract(6, 'days').startOf('day'), moment().endOf('day')]; |
| | | |
| | | const columns = [ |
| | | { |
| | | title: '呼叫时间', |
| | | dataIndex: 'created_time', |
| | | width: 180, |
| | | title: '查询时间', |
| | | dataIndex: 'timeRange', |
| | | width: 260, |
| | | renderFormItem: () => ( |
| | | <RangePicker |
| | | showTime |
| | | format="YYYY-MM-DD HH:mm:ss" |
| | | style={{ width: 380 }} |
| | | /> |
| | | <RangePicker showTime format="YYYY-MM-DD HH:mm:ss" style={{ width: '100%' }} /> |
| | | ), |
| | | }, |
| | | { |
| | | title: '任务ID', |
| | | dataIndex: 'task_id', |
| | | width: 100, |
| | | }, |
| | | { |
| | | title: '任务名称', |
| | | dataIndex: 'task_name', |
| | | width: 160, |
| | | hideInSearch: true, |
| | | }, |
| | | { |
| | | title: '手机号', |
| | | dataIndex: 'phone', |
| | | width: 140, |
| | | render: (_, record) => maskPhoneNumber(record.phone), |
| | | }, |
| | | { |
| | | title: '关键词', |
| | | dataIndex: 'key_word', |
| | | formItemProps: { rules: [{ required: true, message: '请选择查询时间' }] }, |
| | | hideInTable: true, |
| | | }, |
| | | { |
| | | title: '意向等级', |
| | | dataIndex: 'intention', |
| | | width: 100, |
| | | title: '呼叫类型', |
| | | dataIndex: 'callType', |
| | | width: 130, |
| | | renderFormItem: () => ( |
| | | <Select |
| | | mode="multiple" |
| | | allowClear |
| | | placeholder="请选择意向等级" |
| | | options={intentionOptions} |
| | | /> |
| | | <Select mode="multiple" allowClear options={callTypeOptions} placeholder="请选择呼叫类型" /> |
| | | ), |
| | | render: (_, record) => record.intention ? <Tag color="blue">{record.intention}</Tag> : '-', |
| | | render: (text) => <Tag color="blue">{callTypeLabels[text] || text || '-'}</Tag>, |
| | | }, |
| | | { |
| | | title: '类型', |
| | | dataIndex: 'sys_type', |
| | | title: '客户号码', |
| | | dataIndex: 'customerNumber', |
| | | width: 140, |
| | | render: (text) => maskPhoneNumber(text) || '-', |
| | | }, |
| | | { title: '外显号码', dataIndex: 'disNumber', width: 140, render: renderText }, |
| | | { title: '服务号码', dataIndex: 'serviceNumber', width: 140, render: renderText }, |
| | | { |
| | | title: '通话状态', |
| | | dataIndex: 'status', |
| | | width: 140, |
| | | renderFormItem: () => <Input placeholder="如 ANSWER" />, |
| | | render: renderText, |
| | | }, |
| | | { |
| | | title: '通话 ID', |
| | | dataIndex: 'callId', |
| | | width: 220, |
| | | hideInSearch: true, |
| | | ellipsis: true, |
| | | render: renderText, |
| | | }, |
| | | { |
| | | title: '座席名称', |
| | | dataIndex: 'agentName', |
| | | width: 120, |
| | | hideInSearch: true, |
| | | valueEnum: { |
| | | 1: { text: 'AI外呼记录' }, |
| | | 2: { text: '群呼记录' }, |
| | | }, |
| | | render: renderText, |
| | | }, |
| | | { |
| | | title: '座席工号', |
| | | dataIndex: 'agentNumber', |
| | | width: 120, |
| | | hideInSearch: true, |
| | | render: renderText, |
| | | }, |
| | | { title: '服务组', dataIndex: 'queueName', width: 140, hideInSearch: true, render: renderText }, |
| | | { |
| | | title: '开始时间', |
| | | dataIndex: 'startTime', |
| | | width: 180, |
| | | hideInSearch: true, |
| | | render: renderText, |
| | | }, |
| | | { |
| | | title: '接通时间', |
| | | dataIndex: 'answered_time', |
| | | dataIndex: 'answerTime', |
| | | width: 180, |
| | | hideInSearch: true, |
| | | render: (text) => text || '-', |
| | | render: renderText, |
| | | }, |
| | | { title: '结束时间', dataIndex: 'endTime', width: 180, hideInSearch: true, render: renderText }, |
| | | { |
| | | title: '挂机时间', |
| | | dataIndex: 'hungup_time', |
| | | width: 180, |
| | | hideInSearch: true, |
| | | render: (text) => text || '-', |
| | | }, |
| | | { |
| | | title: '通话时长(秒)', |
| | | dataIndex: 'billsec', |
| | | width: 120, |
| | | hideInSearch: true, |
| | | render: (text) => text || '-', |
| | | }, |
| | | { |
| | | title: '呼叫总时间(秒)', |
| | | dataIndex: 'duration', |
| | | title: '通话时长(秒)', |
| | | dataIndex: 'callDuration', |
| | | width: 130, |
| | | hideInSearch: true, |
| | | render: (text) => text || '-', |
| | | render: renderText, |
| | | }, |
| | | { |
| | | title: '通话轮次', |
| | | dataIndex: 'cti_asr_call_count', |
| | | width: 100, |
| | | title: '排队时长(秒)', |
| | | dataIndex: 'queueDuration', |
| | | width: 130, |
| | | hideInSearch: true, |
| | | render: (text) => text || '-', |
| | | render: renderText, |
| | | }, |
| | | { |
| | | title: '是否转人工', |
| | | dataIndex: 'bridge_uuid', |
| | | title: '挂机方', |
| | | dataIndex: 'hangUpParty', |
| | | width: 110, |
| | | hideInSearch: true, |
| | | render: (text) => text ? <Tag color="green">是</Tag> : <Tag>否</Tag>, |
| | | render: renderText, |
| | | }, |
| | | { |
| | | title: '是否查看', |
| | | dataIndex: 'is_eyes', |
| | | width: 100, |
| | | hideInSearch: true, |
| | | valueEnum: { |
| | | 1: { text: '未查看', status: 'Warning' }, |
| | | 2: { text: '已查看', status: 'Success' }, |
| | | }, |
| | | }, |
| | | { |
| | | title: '挂断原因', |
| | | dataIndex: 'hangup_cause', |
| | | width: 160, |
| | | hideInSearch: true, |
| | | ellipsis: true, |
| | | render: (text) => text || '-', |
| | | }, |
| | | { |
| | | title: '录音文件', |
| | | dataIndex: 'record_filename', |
| | | title: '失败原因', |
| | | dataIndex: 'failReasonMsg', |
| | | width: 180, |
| | | hideInSearch: true, |
| | | ellipsis: true, |
| | | render: (text) => text || '-', |
| | | render: renderText, |
| | | }, |
| | | { |
| | | title: '通话UUID', |
| | | dataIndex: 'uuid', |
| | | width: 240, |
| | | title: '通话录音', |
| | | dataIndex: 'recordUrl', |
| | | width: 120, |
| | | hideInSearch: true, |
| | | ellipsis: true, |
| | | render: (url) => |
| | | url ? ( |
| | | <Typography.Link href={url} target="_blank"> |
| | | 播放录音 |
| | | </Typography.Link> |
| | | ) : ( |
| | | '-' |
| | | ), |
| | | }, |
| | | { |
| | | title: '留言录音', |
| | | dataIndex: 'leaveMsgUrl', |
| | | width: 120, |
| | | hideInSearch: true, |
| | | render: (url) => |
| | | url ? ( |
| | | <Typography.Link href={url} target="_blank"> |
| | | 播放录音 |
| | | </Typography.Link> |
| | | ) : ( |
| | | '-' |
| | | ), |
| | | }, |
| | | ]; |
| | | |
| | | return ( |
| | | <PageContainer title="通话记录"> |
| | | <ProTable |
| | | rowKey="id" |
| | | rowKey="callId" |
| | | actionRef={actionRef} |
| | | columns={columns} |
| | | form={{ initialValues: { timeRange: defaultTimeRange } }} |
| | | search={{ defaultCollapsed: false }} |
| | | scroll={{ x: 2000 }} |
| | | pagination={{ |
| | | showSizeChanger: true, |
| | | showQuickJumper: true, |
| | | defaultPageSize: 10, |
| | | }} |
| | | scroll={{ x: 2600 }} |
| | | pagination={{ showSizeChanger: true, showQuickJumper: true, defaultPageSize: 20 }} |
| | | request={async (params) => { |
| | | const response = await getCallRecords(normalizeSearchParams(params)); |
| | | const requestData = normalizeSearchParams(params); |
| | | const start = moment(requestData.startTime, 'YYYY-MM-DD HH:mm:ss', true); |
| | | const end = moment(requestData.endTime, 'YYYY-MM-DD HH:mm:ss', true); |
| | | |
| | | if (!start.isValid() || !end.isValid() || end.isBefore(start)) { |
| | | message.error('请选择有效的查询时间范围'); |
| | | return { data: [], total: 0, success: false }; |
| | | } |
| | | |
| | | if (end.diff(start, 'seconds') > 7 * 24 * 60 * 60) { |
| | | message.error('查询时间跨度不能超过 7 天'); |
| | | return { data: [], total: 0, success: false }; |
| | | } |
| | | |
| | | const response = await getCallRecords(requestData); |
| | | return { |
| | | data: response?.data?.list || [], |
| | | total: response?.data?.total || 0, |
| | |
| | | import { request } from '@umijs/max'; |
| | | |
| | | // 呼大侠通话记录列表 |
| | | // Useasy 通话记录列表 |
| | | export const getCallRecords = async (data) => { |
| | | return request('/hudaxia/callRecords', { |
| | | return request('/admin/useasy/callRecords', { |
| | | method: 'POST', |
| | | data, |
| | | }); |