404 lines
11 KiB
Vue
404 lines
11 KiB
Vue
<template>
|
|
<!-- ================================================================== -->
|
|
<!-- 会话审计页面 — 历史会话查看与筛选 -->
|
|
<!-- ================================================================== -->
|
|
<div class="session-audit">
|
|
<!-- 筛选栏 -->
|
|
<div class="filter-bar">
|
|
<el-input
|
|
v-model="filters.keyword"
|
|
placeholder="搜索员工姓名/消息摘要"
|
|
:prefix-icon="Search"
|
|
clearable
|
|
style="width: 240px"
|
|
@keyup.enter="handleSearch"
|
|
/>
|
|
<el-select v-model="filters.status" placeholder="会话状态" clearable style="width: 140px">
|
|
<el-option label="全部" value="" />
|
|
<el-option label="AI处理中" value="ai_handling" />
|
|
<el-option label="排队中" value="queued" />
|
|
<el-option label="服务中" value="serving" />
|
|
<el-option label="已结单" value="resolved" />
|
|
</el-select>
|
|
<el-date-picker
|
|
v-model="dateRange"
|
|
type="daterange"
|
|
range-separator="至"
|
|
start-placeholder="开始日期"
|
|
end-placeholder="结束日期"
|
|
value-format="YYYY-MM-DD"
|
|
style="width: 280px"
|
|
@change="handleSearch"
|
|
/>
|
|
<el-button type="primary" :icon="Search" @click="handleSearch">搜索</el-button>
|
|
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
|
</div>
|
|
|
|
<!-- 会话列表 -->
|
|
<el-table
|
|
:data="conversations"
|
|
v-loading="loading"
|
|
stripe
|
|
style="width: 100%"
|
|
@row-click="handleRowClick"
|
|
row-class-name="clickable-row"
|
|
>
|
|
<el-table-column prop="employee_name" label="员工姓名" width="120" />
|
|
<el-table-column prop="department" label="部门" width="150" show-overflow-tooltip />
|
|
<el-table-column prop="status" label="状态" width="100">
|
|
<template #default="{ row }">
|
|
<el-tag :type="statusTagType(row.status)" size="small">
|
|
{{ statusLabel(row.status) }}
|
|
</el-tag>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="assigned_agent_name" label="负责坐席" width="100">
|
|
<template #default="{ row }">
|
|
{{ row.assigned_agent_name || '—' }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="urgency_score" label="紧急度" width="80" align="center">
|
|
<template #default="{ row }">
|
|
<span :style="{ color: urgencyColor(row.urgency_score) }">
|
|
{{ row.urgency_score }}/5
|
|
</span>
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column prop="last_message_summary" label="最近消息" min-width="200" show-overflow-tooltip />
|
|
<el-table-column prop="created_at" label="创建时间" width="170">
|
|
<template #default="{ row }">
|
|
{{ formatTime(row.created_at) }}
|
|
</template>
|
|
</el-table-column>
|
|
<el-table-column label="操作" width="80" fixed="right">
|
|
<template #default="{ row }">
|
|
<el-button type="primary" link size="small" @click.stop="showDetail(row)">
|
|
详情
|
|
</el-button>
|
|
</template>
|
|
</el-table-column>
|
|
</el-table>
|
|
|
|
<!-- 分页 -->
|
|
<div class="pagination-bar">
|
|
<el-pagination
|
|
v-model:current-page="pagination.page"
|
|
v-model:page-size="pagination.pageSize"
|
|
:total="pagination.total"
|
|
:page-sizes="[10, 20, 50]"
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
@current-change="loadConversations"
|
|
@size-change="handleSizeChange"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 详情抽屉 -->
|
|
<el-drawer
|
|
v-model="drawerVisible"
|
|
:title="`会话详情 — ${selectedConversation?.employee_name || ''}`"
|
|
size="500px"
|
|
direction="rtl"
|
|
>
|
|
<div v-if="detailLoading" v-loading="true" style="height: 200px" />
|
|
<div v-else-if="conversationDetail" class="detail-content">
|
|
<!-- 基本信息 -->
|
|
<div class="detail-section">
|
|
<h4>基本信息</h4>
|
|
<el-descriptions :column="2" border size="small">
|
|
<el-descriptions-item label="员工姓名">{{ conversationDetail.employee_name }}</el-descriptions-item>
|
|
<el-descriptions-item label="员工ID">{{ conversationDetail.employee_id }}</el-descriptions-item>
|
|
<el-descriptions-item label="部门">{{ conversationDetail.department || '—' }}</el-descriptions-item>
|
|
<el-descriptions-item label="岗位">{{ conversationDetail.position || '—' }}</el-descriptions-item>
|
|
<el-descriptions-item label="状态">
|
|
<el-tag :type="statusTagType(conversationDetail.status)" size="small">
|
|
{{ statusLabel(conversationDetail.status) }}
|
|
</el-tag>
|
|
</el-descriptions-item>
|
|
<el-descriptions-item label="负责坐席">{{ conversationDetail.assigned_agent_name || '—' }}</el-descriptions-item>
|
|
<el-descriptions-item label="创建时间" :span="2">{{ formatTime(conversationDetail.created_at) }}</el-descriptions-item>
|
|
</el-descriptions>
|
|
</div>
|
|
|
|
<!-- 消息列表 -->
|
|
<div class="detail-section">
|
|
<h4>消息记录 ({{ conversationDetail.messages?.length || 0 }})</h4>
|
|
<div class="message-list">
|
|
<div
|
|
v-for="msg in conversationDetail.messages"
|
|
:key="msg.id"
|
|
class="message-item"
|
|
:class="`sender-${msg.sender_type}`"
|
|
>
|
|
<div class="message-header">
|
|
<el-tag :type="senderTagType(msg.sender_type)" size="small">
|
|
{{ senderLabel(msg.sender_type) }}
|
|
</el-tag>
|
|
<span class="sender-name">{{ msg.sender_name }}</span>
|
|
<span class="message-time">{{ formatTime(msg.created_at) }}</span>
|
|
</div>
|
|
<div class="message-content">{{ msg.content }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</el-drawer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
// =============================================================================
|
|
// 会话审计页面逻辑
|
|
// =============================================================================
|
|
import { ref, reactive, onMounted } from 'vue'
|
|
import { Search, Refresh } from '@element-plus/icons-vue'
|
|
import { getAuditConversations, getAuditConversationDetail } from '@/api/admin'
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
// ---------- 状态 ----------
|
|
const loading = ref(false)
|
|
const detailLoading = ref(false)
|
|
const drawerVisible = ref(false)
|
|
const conversations = ref<any[]>([])
|
|
const selectedConversation = ref<any>(null)
|
|
const conversationDetail = ref<any>(null)
|
|
const dateRange = ref<[string, string] | null>(null)
|
|
|
|
const filters = reactive({
|
|
keyword: '',
|
|
status: '',
|
|
})
|
|
|
|
const pagination = reactive({
|
|
page: 1,
|
|
pageSize: 20,
|
|
total: 0,
|
|
})
|
|
|
|
// ---------- 数据加载 ----------
|
|
async function loadConversations() {
|
|
loading.value = true
|
|
try {
|
|
const params: any = {
|
|
page: pagination.page,
|
|
page_size: pagination.pageSize,
|
|
}
|
|
if (filters.keyword) params.keyword = filters.keyword
|
|
if (filters.status) params.status = filters.status
|
|
if (dateRange.value) {
|
|
params.date_from = dateRange.value[0]
|
|
params.date_to = dateRange.value[1]
|
|
}
|
|
|
|
const { data } = await getAuditConversations(params)
|
|
if (data.code === 0) {
|
|
conversations.value = data.data.items || []
|
|
pagination.total = data.data.total || 0
|
|
}
|
|
} catch (err: any) {
|
|
ElMessage.error('加载会话列表失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
async function showDetail(row: any) {
|
|
selectedConversation.value = row
|
|
drawerVisible.value = true
|
|
detailLoading.value = true
|
|
try {
|
|
const { data } = await getAuditConversationDetail(row.id)
|
|
if (data.code === 0) {
|
|
conversationDetail.value = data.data
|
|
}
|
|
} catch (err: any) {
|
|
ElMessage.error('加载会话详情失败')
|
|
} finally {
|
|
detailLoading.value = false
|
|
}
|
|
}
|
|
|
|
// ---------- 事件处理 ----------
|
|
function handleSearch() {
|
|
pagination.page = 1
|
|
loadConversations()
|
|
}
|
|
|
|
function handleReset() {
|
|
filters.keyword = ''
|
|
filters.status = ''
|
|
dateRange.value = null
|
|
pagination.page = 1
|
|
loadConversations()
|
|
}
|
|
|
|
function handleSizeChange() {
|
|
pagination.page = 1
|
|
loadConversations()
|
|
}
|
|
|
|
function handleRowClick(row: any) {
|
|
showDetail(row)
|
|
}
|
|
|
|
// ---------- 工具函数 ----------
|
|
function statusTagType(status: string): '' | 'success' | 'warning' | 'info' | 'danger' {
|
|
const map: Record<string, '' | 'success' | 'warning' | 'info' | 'danger'> = {
|
|
ai_handling: 'info',
|
|
queued: 'warning',
|
|
serving: '',
|
|
resolved: 'success',
|
|
}
|
|
return map[status] || 'info'
|
|
}
|
|
|
|
function statusLabel(status: string): string {
|
|
const map: Record<string, string> = {
|
|
ai_handling: 'AI处理中',
|
|
queued: '排队中',
|
|
serving: '服务中',
|
|
resolved: '已结单',
|
|
}
|
|
return map[status] || status
|
|
}
|
|
|
|
function urgencyColor(score: number): string {
|
|
if (score >= 4) return '#f56c6c'
|
|
if (score >= 3) return '#e6a23c'
|
|
return '#67c23a'
|
|
}
|
|
|
|
function senderTagType(type: string): '' | 'success' | 'warning' | 'info' | 'danger' {
|
|
const map: Record<string, '' | 'success' | 'warning' | 'info' | 'danger'> = {
|
|
employee: 'info',
|
|
agent: 'success',
|
|
ai: 'warning',
|
|
system: 'danger',
|
|
}
|
|
return map[type] || 'info'
|
|
}
|
|
|
|
function senderLabel(type: string): string {
|
|
const map: Record<string, string> = {
|
|
employee: '员工',
|
|
agent: '坐席',
|
|
ai: 'AI',
|
|
system: '系统',
|
|
}
|
|
return map[type] || type
|
|
}
|
|
|
|
function formatTime(iso: string): string {
|
|
if (!iso) return '—'
|
|
const d = new Date(iso)
|
|
return d.toLocaleString('zh-CN', {
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
})
|
|
}
|
|
|
|
// ---------- 生命周期 ----------
|
|
onMounted(() => loadConversations())
|
|
</script>
|
|
|
|
<style scoped>
|
|
.session-audit {
|
|
padding: 0;
|
|
}
|
|
|
|
.filter-bar {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.pagination-bar {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: 16px;
|
|
}
|
|
|
|
.clickable-row {
|
|
cursor: pointer;
|
|
}
|
|
|
|
.detail-content {
|
|
padding: 0 4px;
|
|
}
|
|
|
|
.detail-section {
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.detail-section h4 {
|
|
margin: 0 0 12px 0;
|
|
font-size: 15px;
|
|
color: #303133;
|
|
border-bottom: 1px solid #ebeef5;
|
|
padding-bottom: 8px;
|
|
}
|
|
|
|
.message-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.message-item {
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
background: #f5f7fa;
|
|
}
|
|
|
|
.message-item.sender-employee {
|
|
background: #ecf5ff;
|
|
border-left: 3px solid #409eff;
|
|
}
|
|
|
|
.message-item.sender-agent {
|
|
background: #f0f9eb;
|
|
border-left: 3px solid #67c23a;
|
|
}
|
|
|
|
.message-item.sender-ai {
|
|
background: #fdf6ec;
|
|
border-left: 3px solid #e6a23c;
|
|
}
|
|
|
|
.message-item.sender-system {
|
|
background: #f4f4f5;
|
|
border-left: 3px solid #909399;
|
|
}
|
|
|
|
.message-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 6px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.sender-name {
|
|
color: #606266;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.message-time {
|
|
color: #909399;
|
|
margin-left: auto;
|
|
}
|
|
|
|
.message-content {
|
|
font-size: 14px;
|
|
color: #303133;
|
|
line-height: 1.5;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
}
|
|
</style>
|