Files
wecom_it_smart_desk/frontend-admin/src/views/SessionAudit.vue
T

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>