|
|
@@ -0,0 +1,672 @@
|
|
|
+<template>
|
|
|
+ <div class="app-container">
|
|
|
+ <!-- 搜索表单 -->
|
|
|
+ <el-form
|
|
|
+ v-show="showSearch"
|
|
|
+ ref="queryForm"
|
|
|
+ :inline="true"
|
|
|
+ :model="queryParams"
|
|
|
+ label-width="100px"
|
|
|
+ >
|
|
|
+ <el-form-item label="通话UUID" prop="uuid">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.uuid"
|
|
|
+ clearable
|
|
|
+ placeholder="请输入通话UUID"
|
|
|
+ size="small"
|
|
|
+ style="width: 200px"
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="主叫号码" prop="caller">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.caller"
|
|
|
+ clearable
|
|
|
+ placeholder="请输入主叫号码"
|
|
|
+ size="small"
|
|
|
+ style="width: 200px"
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="接听分机号" prop="extnum">
|
|
|
+ <el-input
|
|
|
+ v-model="queryParams.extnum"
|
|
|
+ clearable
|
|
|
+ placeholder="请输入接听分机号"
|
|
|
+ size="small"
|
|
|
+ style="width: 200px"
|
|
|
+ @keyup.enter.native="handleQuery"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="通话时长(秒)">
|
|
|
+ <el-input-number
|
|
|
+ v-model="queryParams.params.timeLenStart"
|
|
|
+ :min="0"
|
|
|
+ size="small"
|
|
|
+ style="width: 120px"
|
|
|
+ placeholder="最小值"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ <span class="range-separator">-</span>
|
|
|
+ <el-input-number
|
|
|
+ v-model="queryParams.params.timeLenEnd"
|
|
|
+ :min="0"
|
|
|
+ size="small"
|
|
|
+ style="width: 120px"
|
|
|
+ placeholder="最大值"
|
|
|
+ controls-position="right"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="呼入时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="inboundTimeRange"
|
|
|
+ type="datetimerange"
|
|
|
+ size="small"
|
|
|
+ style="width: 340px"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始时间"
|
|
|
+ end-placeholder="结束时间"
|
|
|
+ value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="接听时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="answeredTimeRange"
|
|
|
+ type="datetimerange"
|
|
|
+ size="small"
|
|
|
+ style="width: 340px"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始时间"
|
|
|
+ end-placeholder="结束时间"
|
|
|
+ value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="挂机时间">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="hangupTimeRange"
|
|
|
+ type="datetimerange"
|
|
|
+ size="small"
|
|
|
+ style="width: 340px"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始时间"
|
|
|
+ end-placeholder="结束时间"
|
|
|
+ value-format="yyyy-MM-dd HH:mm:ss"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
|
|
|
+ <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <el-row :gutter="10" class="mb8">
|
|
|
+ <right-toolbar :showSearch.sync="showSearch" @queryTable="getList" />
|
|
|
+ </el-row>
|
|
|
+
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <el-table
|
|
|
+ v-loading="loading"
|
|
|
+ :data="tableData"
|
|
|
+ border
|
|
|
+ style="width: 100%"
|
|
|
+ >
|
|
|
+ <el-table-column align="center" label="UUID" prop="uuid" min-width="120" show-overflow-tooltip />
|
|
|
+ <el-table-column align="center" label="主叫号码" prop="caller" min-width="110" show-overflow-tooltip />
|
|
|
+ <el-table-column align="center" label="被叫号码" prop="callee" min-width="110" show-overflow-tooltip />
|
|
|
+ <el-table-column align="center" label="录音文件" min-width="80">
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-tag v-if="getMediaType(scope.row.wavFile) === 'audio'" type="primary" size="small">音频</el-tag>
|
|
|
+ <el-tag v-else-if="getMediaType(scope.row.wavFile) === 'video'" type="success" size="small">视频</el-tag>
|
|
|
+ <span v-else>无</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column align="center" label="呼入时间" min-width="150">
|
|
|
+ <template slot-scope="scope">{{ formatTimestamp(scope.row.inboundTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column align="center" label="接听时间" min-width="150">
|
|
|
+ <template slot-scope="scope">{{ formatTimestamp(scope.row.answeredTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column align="center" label="接听分机" prop="extnum" min-width="90" />
|
|
|
+ <el-table-column align="center" label="接听坐席" prop="opnum" min-width="90" />
|
|
|
+ <el-table-column align="center" label="挂机时间" min-width="150">
|
|
|
+ <template slot-scope="scope">{{ formatTimestamp(scope.row.hangupTime) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column align="center" label="业务组" prop="groupName" min-width="100" show-overflow-tooltip />
|
|
|
+ <el-table-column align="center" label="通话时长" min-width="90">
|
|
|
+ <template slot-scope="scope">{{ formatDuration(scope.row.timeLen) }}</template>
|
|
|
+ </el-table-column>
|
|
|
+ <!-- <el-table-column align="center" label="挂机原因" min-width="120" show-overflow-tooltip>
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <span
|
|
|
+ class="hangup-cause-cell"
|
|
|
+ :title="scope.row.hangupCause"
|
|
|
+ @dblclick="copyText(formatHangupCause(scope.row.hangupCause))"
|
|
|
+ >{{ formatHangupCause(scope.row.hangupCause) }}</span>
|
|
|
+ </template>
|
|
|
+ </el-table-column> -->
|
|
|
+ <el-table-column
|
|
|
+ align="center"
|
|
|
+ class-name="small-padding fixed-width"
|
|
|
+ label="操作"
|
|
|
+ width="160"
|
|
|
+ >
|
|
|
+ <template slot-scope="scope">
|
|
|
+ <el-button
|
|
|
+ v-if="scope.row.wavFileUrl"
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-video-play"
|
|
|
+ @click="handlePlay(scope.row)"
|
|
|
+ >播放</el-button>
|
|
|
+ <el-button
|
|
|
+ v-if="scope.row.wavFileUrl"
|
|
|
+ size="mini"
|
|
|
+ type="text"
|
|
|
+ icon="el-icon-download"
|
|
|
+ @click="handleDownload(scope.row)"
|
|
|
+ >下载</el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <pagination
|
|
|
+ v-show="total > 0"
|
|
|
+ :limit.sync="queryParams.pageSize"
|
|
|
+ :page.sync="queryParams.pageNum"
|
|
|
+ :total="total"
|
|
|
+ @pagination="getList"
|
|
|
+ />
|
|
|
+
|
|
|
+ <!-- 播放弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ title="录音播放"
|
|
|
+ :visible.sync="playDialogVisible"
|
|
|
+ width="700px"
|
|
|
+ append-to-body
|
|
|
+ @close="handlePlayDialogClose"
|
|
|
+ >
|
|
|
+ <!-- 音频/视频播放器 -->
|
|
|
+ <div v-if="currentMediaType === 'audio'" class="player-wrapper">
|
|
|
+ <audio
|
|
|
+ ref="audioPlayer"
|
|
|
+ :src="currentWavFileUrl"
|
|
|
+ controls
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div v-else-if="currentMediaType === 'video'" class="player-wrapper">
|
|
|
+ <video
|
|
|
+ ref="videoPlayer"
|
|
|
+ :src="currentWavFileUrl"
|
|
|
+ controls
|
|
|
+ style="width: 100%"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- AI对话内容 -->
|
|
|
+ <div v-if="chatDialogList.length > 0" class="chat-container">
|
|
|
+ <div class="chat-title">AI对话内容</div>
|
|
|
+ <div class="chat-list">
|
|
|
+ <div
|
|
|
+ v-for="(item, index) in chatDialogList"
|
|
|
+ :key="index"
|
|
|
+ class="dialog-item"
|
|
|
+ :class="item.role"
|
|
|
+ >
|
|
|
+ <!-- 左侧角色(assistant / agent / kb) -->
|
|
|
+ <template v-if="item.role !== 'user'">
|
|
|
+ <span class="role-icon">
|
|
|
+ <i :class="getRoleIcon(item.role)" />
|
|
|
+ </span>
|
|
|
+ <span class="role-label">{{ getRoleLabel(item.role) }}</span>
|
|
|
+ <div class="bubble">
|
|
|
+ <span class="content-text">
|
|
|
+ {{ item.expanded ? item.content : getTruncatedContent(item.content) }}
|
|
|
+ <span
|
|
|
+ v-if="item.content.length > 200"
|
|
|
+ class="more-text"
|
|
|
+ @click="toggleExpand(index)"
|
|
|
+ >{{ item.expanded ? '[收起]' : '[更多]' }}</span>
|
|
|
+ </span>
|
|
|
+ <el-tag v-if="item.isKb" size="mini" type="warning" class="kb-tag">知识库来源</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <!-- 右侧角色(user) -->
|
|
|
+ <template v-else>
|
|
|
+ <div class="bubble">
|
|
|
+ <span class="content-text">
|
|
|
+ {{ item.expanded ? item.content : getTruncatedContent(item.content) }}
|
|
|
+ <span
|
|
|
+ v-if="item.content.length > 200"
|
|
|
+ class="more-text"
|
|
|
+ @click="toggleExpand(index)"
|
|
|
+ >{{ item.expanded ? '[收起]' : '[更多]' }}</span>
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <span class="role-icon">
|
|
|
+ <i class="el-icon-user" />
|
|
|
+ </span>
|
|
|
+ <span class="role-label">客户</span>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-dialog>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+import { listInboundCdr } from '@/api/company/inboundCallManage'
|
|
|
+
|
|
|
+export default {
|
|
|
+ name: 'InboundCallRecord',
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ // 遮罩层
|
|
|
+ loading: false,
|
|
|
+ // 显示搜索条件
|
|
|
+ showSearch: true,
|
|
|
+ // 总条数
|
|
|
+ total: 0,
|
|
|
+ // 表格数据
|
|
|
+ tableData: [],
|
|
|
+ // 查询参数
|
|
|
+ queryParams: {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ uuid: undefined,
|
|
|
+ caller: undefined,
|
|
|
+ extnum: undefined,
|
|
|
+ params: {
|
|
|
+ timeLenStart: undefined,
|
|
|
+ timeLenEnd: undefined,
|
|
|
+ inboundTimeStart: undefined,
|
|
|
+ inboundTimeEnd: undefined,
|
|
|
+ answeredTimeStart: undefined,
|
|
|
+ answeredTimeEnd: undefined,
|
|
|
+ hangupTimeStart: undefined,
|
|
|
+ hangupTimeEnd: undefined
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 时间范围
|
|
|
+ inboundTimeRange: [],
|
|
|
+ answeredTimeRange: [],
|
|
|
+ hangupTimeRange: [],
|
|
|
+ // 播放弹窗
|
|
|
+ playDialogVisible: false,
|
|
|
+ currentWavFileUrl: '',
|
|
|
+ currentMediaType: '',
|
|
|
+ // AI对话列表
|
|
|
+ chatDialogList: []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ created() {
|
|
|
+ this.getList()
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ /** 查询列表 */
|
|
|
+ getList() {
|
|
|
+ this.loading = true
|
|
|
+ // 处理时间范围参数(转为epoch毫秒时间戳,与后端Long类型字段匹配)
|
|
|
+ if (this.inboundTimeRange && this.inboundTimeRange.length === 2) {
|
|
|
+ this.queryParams.params.inboundTimeStart = new Date(this.inboundTimeRange[0]).getTime()
|
|
|
+ this.queryParams.params.inboundTimeEnd = new Date(this.inboundTimeRange[1]).getTime()
|
|
|
+ } else {
|
|
|
+ this.queryParams.params.inboundTimeStart = undefined
|
|
|
+ this.queryParams.params.inboundTimeEnd = undefined
|
|
|
+ }
|
|
|
+ if (this.answeredTimeRange && this.answeredTimeRange.length === 2) {
|
|
|
+ this.queryParams.params.answeredTimeStart = new Date(this.answeredTimeRange[0]).getTime()
|
|
|
+ this.queryParams.params.answeredTimeEnd = new Date(this.answeredTimeRange[1]).getTime()
|
|
|
+ } else {
|
|
|
+ this.queryParams.params.answeredTimeStart = undefined
|
|
|
+ this.queryParams.params.answeredTimeEnd = undefined
|
|
|
+ }
|
|
|
+ if (this.hangupTimeRange && this.hangupTimeRange.length === 2) {
|
|
|
+ this.queryParams.params.hangupTimeStart = new Date(this.hangupTimeRange[0]).getTime()
|
|
|
+ this.queryParams.params.hangupTimeEnd = new Date(this.hangupTimeRange[1]).getTime()
|
|
|
+ } else {
|
|
|
+ this.queryParams.params.hangupTimeStart = undefined
|
|
|
+ this.queryParams.params.hangupTimeEnd = undefined
|
|
|
+ }
|
|
|
+ // 通话时长:前端输入秒,数据库存毫秒,显示用Math.ceil(ms/1000)
|
|
|
+ // Math.ceil(v/1000)==N 等价于 (N-1)*1000 < v <= N*1000,即 v∈[(N-1)*1000+1, N*1000]
|
|
|
+ const params = { ...this.queryParams }
|
|
|
+ params.params = { ...this.queryParams.params }
|
|
|
+ if (params.params.timeLenStart != null && params.params.timeLenStart !== undefined) {
|
|
|
+ const n = params.params.timeLenStart
|
|
|
+ params.params.timeLenStart = n > 0 ? (n - 1) * 1000 + 1 : 0
|
|
|
+ }
|
|
|
+ if (params.params.timeLenEnd != null && params.params.timeLenEnd !== undefined) {
|
|
|
+ params.params.timeLenEnd = params.params.timeLenEnd * 1000
|
|
|
+ }
|
|
|
+ listInboundCdr(params).then(response => {
|
|
|
+ this.tableData = response.rows || []
|
|
|
+ this.total = response.total || 0
|
|
|
+ this.loading = false
|
|
|
+ }).catch(() => {
|
|
|
+ this.loading = false
|
|
|
+ })
|
|
|
+ },
|
|
|
+ /** 搜索按钮操作 */
|
|
|
+ handleQuery() {
|
|
|
+ this.queryParams.pageNum = 1
|
|
|
+ this.getList()
|
|
|
+ },
|
|
|
+ /** 重置按钮操作 */
|
|
|
+ resetQuery() {
|
|
|
+ this.inboundTimeRange = []
|
|
|
+ this.answeredTimeRange = []
|
|
|
+ this.hangupTimeRange = []
|
|
|
+ this.resetForm('queryForm')
|
|
|
+ this.queryParams = {
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ uuid: undefined,
|
|
|
+ caller: undefined,
|
|
|
+ extnum: undefined,
|
|
|
+ params: {
|
|
|
+ timeLenStart: undefined,
|
|
|
+ timeLenEnd: undefined,
|
|
|
+ inboundTimeStart: undefined,
|
|
|
+ inboundTimeEnd: undefined,
|
|
|
+ answeredTimeStart: undefined,
|
|
|
+ answeredTimeEnd: undefined,
|
|
|
+ hangupTimeStart: undefined,
|
|
|
+ hangupTimeEnd: undefined
|
|
|
+ }
|
|
|
+ }
|
|
|
+ this.handleQuery()
|
|
|
+ },
|
|
|
+ /** 格式化时间戳 - 毫秒级时间戳转 yyyy-MM-dd HH:mm:ss */
|
|
|
+ formatTimestamp(value) {
|
|
|
+ if (!value || value <= 0) return '-'
|
|
|
+ const date = new Date(Number(value))
|
|
|
+ const year = date.getFullYear()
|
|
|
+ const month = (date.getMonth() + 1).toString().padStart(2, '0')
|
|
|
+ const day = date.getDate().toString().padStart(2, '0')
|
|
|
+ const hour = date.getHours().toString().padStart(2, '0')
|
|
|
+ const minute = date.getMinutes().toString().padStart(2, '0')
|
|
|
+ const second = date.getSeconds().toString().padStart(2, '0')
|
|
|
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}`
|
|
|
+ },
|
|
|
+ /** 格式化通话时长 - 毫秒转 mm分ss秒 */
|
|
|
+ formatDuration(value) {
|
|
|
+ if (!value || value <= 0) return '0秒'
|
|
|
+ const totalSeconds = Math.ceil(value / 1000)
|
|
|
+ const minutes = Math.floor(totalSeconds / 60)
|
|
|
+ const seconds = totalSeconds % 60
|
|
|
+ if (minutes > 0) {
|
|
|
+ return `${minutes}分${seconds.toString().padStart(2, '0')}秒`
|
|
|
+ }
|
|
|
+ return `${seconds}秒`
|
|
|
+ },
|
|
|
+ /** 判断媒体类型 */
|
|
|
+ getMediaType(wavFile) {
|
|
|
+ if (!wavFile) return 'none'
|
|
|
+ const ext = wavFile.split('.').pop().toLowerCase()
|
|
|
+ if (['wav', 'mp3', 'aac'].includes(ext)) return 'audio'
|
|
|
+ if (['mp4', 'avi', 'mov'].includes(ext)) return 'video'
|
|
|
+ return 'none'
|
|
|
+ },
|
|
|
+ /** 格式化挂机原因 */
|
|
|
+ formatHangupCause(value) {
|
|
|
+ if (!value) return '-'
|
|
|
+ try {
|
|
|
+ const obj = JSON.parse(value)
|
|
|
+ if (obj && obj.code !== undefined) {
|
|
|
+ return `${obj.code}: ${obj.details || ''}`
|
|
|
+ }
|
|
|
+ return value
|
|
|
+ } catch (e) {
|
|
|
+ return value
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /** 双击复制文本 */
|
|
|
+ copyText(text) {
|
|
|
+ if (!text || text === '-') return
|
|
|
+ if (navigator.clipboard && window.isSecureContext) {
|
|
|
+ navigator.clipboard.writeText(text).then(() => {
|
|
|
+ this.$message.success('复制成功')
|
|
|
+ }).catch(() => {
|
|
|
+ this.fallbackCopy(text)
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ this.fallbackCopy(text)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /** 降级复制方案 */
|
|
|
+ fallbackCopy(text) {
|
|
|
+ const textArea = document.createElement('textarea')
|
|
|
+ textArea.value = text
|
|
|
+ textArea.style.position = 'fixed'
|
|
|
+ textArea.style.left = '-9999px'
|
|
|
+ document.body.appendChild(textArea)
|
|
|
+ textArea.focus()
|
|
|
+ textArea.select()
|
|
|
+ try {
|
|
|
+ document.execCommand('copy')
|
|
|
+ this.$message.success('复制成功')
|
|
|
+ } catch (err) {
|
|
|
+ this.$message.error('复制失败')
|
|
|
+ }
|
|
|
+ document.body.removeChild(textArea)
|
|
|
+ },
|
|
|
+ /** 播放按钮操作 */
|
|
|
+ handlePlay(row) {
|
|
|
+ this.currentWavFileUrl = row.wavFileUrl
|
|
|
+ this.currentMediaType = this.getMediaType(row.wavFileUrl || row.wavFile)
|
|
|
+ // 解析AI对话内容
|
|
|
+ this.chatDialogList = this.parseChatContent(row.chatContent)
|
|
|
+ this.playDialogVisible = true
|
|
|
+ },
|
|
|
+ /** 解析聊天内容 */
|
|
|
+ parseChatContent(chatContent) {
|
|
|
+ if (!chatContent) return []
|
|
|
+ try {
|
|
|
+ let items = chatContent
|
|
|
+ // 如果是字符串,尝试解析
|
|
|
+ if (typeof items === 'string') {
|
|
|
+ items = JSON.parse(items)
|
|
|
+ }
|
|
|
+ // 可能是双重JSON字符串
|
|
|
+ if (typeof items === 'string') {
|
|
|
+ items = JSON.parse(items)
|
|
|
+ }
|
|
|
+ if (!Array.isArray(items)) return []
|
|
|
+ return items.filter(item => ['user', 'assistant', 'agent'].includes(item.role)).map(item => {
|
|
|
+ let content = item.content || ''
|
|
|
+ let isKb = false
|
|
|
+ // 检查是否包含JSON(知识库返回结果)
|
|
|
+ if (this.containsJson(content)) {
|
|
|
+ isKb = true
|
|
|
+ // 移除JSON部分
|
|
|
+ let cleaned = content
|
|
|
+ while (this.containsJson(cleaned)) {
|
|
|
+ cleaned = cleaned.replace(/\{[^{}]*\}/s, '').trim()
|
|
|
+ }
|
|
|
+ content = cleaned || content
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ role: isKb ? 'kb' : item.role,
|
|
|
+ content: content,
|
|
|
+ isKb: isKb,
|
|
|
+ expanded: false
|
|
|
+ }
|
|
|
+ }).filter(item => item.content)
|
|
|
+ } catch (e) {
|
|
|
+ return []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /** 检查字符串是否包含JSON */
|
|
|
+ containsJson(input) {
|
|
|
+ if (!input) return false
|
|
|
+ return /\{.*?\}/s.test(input)
|
|
|
+ },
|
|
|
+ /** 获取截断内容 */
|
|
|
+ getTruncatedContent(content) {
|
|
|
+ if (!content || content.length <= 200) return content
|
|
|
+ return content.substring(0, 200) + '...'
|
|
|
+ },
|
|
|
+ /** 切换展开/收起 */
|
|
|
+ toggleExpand(index) {
|
|
|
+ this.$set(this.chatDialogList[index], 'expanded', !this.chatDialogList[index].expanded)
|
|
|
+ },
|
|
|
+ /** 获取角色图标 */
|
|
|
+ getRoleIcon(role) {
|
|
|
+ const iconMap = {
|
|
|
+ assistant: 'el-icon-monitor',
|
|
|
+ agent: 'el-icon-phone-outline',
|
|
|
+ kb: 'el-icon-folder-opened',
|
|
|
+ user: 'el-icon-user'
|
|
|
+ }
|
|
|
+ return iconMap[role] || 'el-icon-user'
|
|
|
+ },
|
|
|
+ /** 获取角色标签 */
|
|
|
+ getRoleLabel(role) {
|
|
|
+ const labelMap = {
|
|
|
+ assistant: 'AI',
|
|
|
+ agent: '坐席',
|
|
|
+ kb: '知识库',
|
|
|
+ user: '客户'
|
|
|
+ }
|
|
|
+ return labelMap[role] || role
|
|
|
+ },
|
|
|
+ /** 下载按钮操作 */
|
|
|
+ handleDownload(row) {
|
|
|
+ if (row.wavFileUrl) {
|
|
|
+ const link = document.createElement('a')
|
|
|
+ link.href = row.wavFileUrl
|
|
|
+ link.target = '_blank'
|
|
|
+ link.download = ''
|
|
|
+ document.body.appendChild(link)
|
|
|
+ link.click()
|
|
|
+ document.body.removeChild(link)
|
|
|
+ }
|
|
|
+ },
|
|
|
+ /** 播放弹窗关闭 */
|
|
|
+ handlePlayDialogClose() {
|
|
|
+ // 停止音频/视频播放
|
|
|
+ if (this.$refs.audioPlayer) {
|
|
|
+ this.$refs.audioPlayer.pause()
|
|
|
+ this.$refs.audioPlayer.currentTime = 0
|
|
|
+ }
|
|
|
+ if (this.$refs.videoPlayer) {
|
|
|
+ this.$refs.videoPlayer.pause()
|
|
|
+ this.$refs.videoPlayer.currentTime = 0
|
|
|
+ }
|
|
|
+ this.currentWavFileUrl = ''
|
|
|
+ this.currentMediaType = ''
|
|
|
+ this.chatDialogList = []
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.app-container {
|
|
|
+ padding: 20px;
|
|
|
+}
|
|
|
+.range-separator {
|
|
|
+ padding: 0 5px;
|
|
|
+}
|
|
|
+.player-wrapper {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+/* 对话容器 */
|
|
|
+.chat-container {
|
|
|
+ margin-top: 16px;
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
+ padding-top: 12px;
|
|
|
+}
|
|
|
+.chat-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: bold;
|
|
|
+ margin-bottom: 12px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+.chat-list {
|
|
|
+ max-height: 400px;
|
|
|
+ overflow-y: auto;
|
|
|
+ padding-right: 6px;
|
|
|
+}
|
|
|
+/* 对话气泡 */
|
|
|
+.dialog-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: flex-start;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+.dialog-item.user {
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+.dialog-item.assistant,
|
|
|
+.dialog-item.agent,
|
|
|
+.dialog-item.kb {
|
|
|
+ justify-content: flex-start;
|
|
|
+}
|
|
|
+.bubble {
|
|
|
+ display: inline-block;
|
|
|
+ max-width: 70%;
|
|
|
+ padding: 10px 14px;
|
|
|
+ border-radius: 12px;
|
|
|
+ word-break: break-word;
|
|
|
+ line-height: 1.6;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #263238;
|
|
|
+ box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+.dialog-item.user .bubble {
|
|
|
+ background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+.dialog-item.assistant .bubble {
|
|
|
+ background: linear-gradient(135deg, #f5f5f5 0%, #eeeeee 100%);
|
|
|
+}
|
|
|
+.dialog-item.agent .bubble {
|
|
|
+ background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
|
|
|
+}
|
|
|
+.dialog-item.kb .bubble {
|
|
|
+ background: linear-gradient(135deg, #fff8e1 0%, #ffecb3 100%);
|
|
|
+}
|
|
|
+.role-icon {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0 8px;
|
|
|
+ font-size: 18px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+.role-label {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ white-space: nowrap;
|
|
|
+ padding: 0 4px;
|
|
|
+ line-height: 32px;
|
|
|
+}
|
|
|
+.content-text {
|
|
|
+ word-break: break-word;
|
|
|
+}
|
|
|
+.more-text {
|
|
|
+ color: #1976d2;
|
|
|
+ cursor: pointer;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-left: 4px;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+.more-text:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+.kb-tag {
|
|
|
+ margin-top: 6px;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+.hangup-cause-cell {
|
|
|
+ cursor: pointer;
|
|
|
+ white-space: nowrap;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ max-width: 100%;
|
|
|
+ display: inline-block;
|
|
|
+}
|
|
|
+</style>
|