客服后台结构
This commit is contained in:
@@ -1 +1,282 @@
|
||||
// 全局EL分页组件样式
|
||||
// 全局EL分页组件样式
|
||||
|
||||
.content-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.content-header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.content-header .subtitle {
|
||||
margin-top: 4px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.dashboard-stats,
|
||||
.dashboard-cards,
|
||||
.quick-entries {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 14px 16px;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.stat-card.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 10px;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.stat-trend {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
color: #909399;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-trend.up {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.question-stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.question-stat-item {
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.question-stat-item.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.stat-bar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-bar {
|
||||
height: 8px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stat-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.product-cloud {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.cloud-tag {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.quick-entry-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.quick-entry-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.quick-entry-item:hover {
|
||||
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.06);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.quick-entry-item .entry-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filter-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ticket-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.ticket-filters .filter-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.table-pagination {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.file-name-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.unassigned {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.section-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.kb-categories {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin: 8px 0 12px;
|
||||
}
|
||||
|
||||
.kb-category-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #ebeef5;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all .2s ease;
|
||||
}
|
||||
|
||||
.kb-category-card.active,
|
||||
.kb-category-card:hover {
|
||||
border-color: #409eff;
|
||||
box-shadow: 0 6px 18px rgba(64, 158, 255, 0.12);
|
||||
}
|
||||
|
||||
.kb-category-card .cat-name {
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.kb-category-card .cat-count {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.kb-files-section {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.dashboard-stats,
|
||||
.dashboard-cards,
|
||||
.quick-entries {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
.kb-categories {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.dashboard-stats,
|
||||
.dashboard-cards,
|
||||
.quick-entries {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.kb-categories {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -1,39 +1,80 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 统一Admin下的基本样式 -->
|
||||
<div>
|
||||
<!-- 头部 -->
|
||||
<div>
|
||||
<!-- 左侧信息 -->
|
||||
<span>
|
||||
<!-- title -->
|
||||
</span>
|
||||
<span>
|
||||
<!-- info -->
|
||||
</span>
|
||||
<div class="admin-layout">
|
||||
<!-- 头部 -->
|
||||
<div class="admin-header">
|
||||
<div class="header-left">
|
||||
<h1 v-if="title" class="header-title">{{ title }}</h1>
|
||||
<p v-if="info" class="header-subtitle">{{ info }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<!-- 右侧按钮 或者文本-->
|
||||
<slot name="action">
|
||||
|
||||
</slot>
|
||||
<div class="header-right">
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- div控制margin等基本样式 -->
|
||||
<slot>
|
||||
|
||||
</slot>
|
||||
<!-- 内容区 -->
|
||||
<div class="admin-content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ElButton } from 'element-plus';
|
||||
import { ref } from 'vue';
|
||||
defineEmits("clickButton")
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
info: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.admin-layout {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.admin-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 20px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-subtitle {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.admin-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +1,221 @@
|
||||
<template>
|
||||
<AdminLayout
|
||||
:title="智能体管理"
|
||||
:info="配置和管理AI智能客服"
|
||||
>
|
||||
<template default>
|
||||
<div class="agent">
|
||||
<!-- 智能体管理视图 -->
|
||||
<div>
|
||||
<!-- head部分 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 表单部分 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 按钮部分 -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<AdminLayout title="智能体管理" info="配置和管理AI智能客服">
|
||||
<div class="agent-container">
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>AI客服配置</span>
|
||||
<el-button type="primary" size="small" @click="saveConfig">保存配置</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="agentConfig" label-width="150px">
|
||||
<el-form-item label="智能体名称">
|
||||
<el-input v-model="agentConfig.name" placeholder="请输入智能体名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="AI模型">
|
||||
<el-select v-model="agentConfig.model" style="width: 100%;">
|
||||
<el-option label="GPT-4" value="gpt-4" />
|
||||
<el-option label="Claude-3" value="claude-3" />
|
||||
<el-option label="文心一言" value="ernie" />
|
||||
<el-option label="本地模型" value="local" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="温度(Temperature)">
|
||||
<el-slider v-model="agentConfig.temperature" :min="0" :max="1" :step="0.1" show-stops />
|
||||
<span style="margin-left: 12px; color: #909399;">{{ agentConfig.temperature }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="最大令牌数">
|
||||
<el-input-number v-model="agentConfig.maxTokens" :min="100" :max="4000" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="系统提示词">
|
||||
<el-input v-model="agentConfig.systemPrompt" type="textarea" rows="4" placeholder="输入系统提示词,用于指导AI的行为" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用知识库">
|
||||
<el-switch v-model="agentConfig.enableKnowledge" />
|
||||
<span style="margin-left: 12px; color: #909399;">{{ agentConfig.enableKnowledge ? '已启用' : '未启用' }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用情感分析">
|
||||
<el-switch v-model="agentConfig.enableSentiment" />
|
||||
<span style="margin-left: 12px; color: #909399;">{{ agentConfig.enableSentiment ? '已启用' : '未启用' }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="启用自动转工单">
|
||||
<el-switch v-model="agentConfig.enableAutoTicket" />
|
||||
<span style="margin-left: 12px; color: #909399;">{{ agentConfig.enableAutoTicket ? '已启用' : '未启用' }}</span>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="响应超时(秒)">
|
||||
<el-input-number v-model="agentConfig.timeout" :min="5" :max="60" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="日志级别">
|
||||
<el-select v-model="agentConfig.logLevel" style="width: 100%;">
|
||||
<el-option label="DEBUG" value="debug" />
|
||||
<el-option label="INFO" value="info" />
|
||||
<el-option label="WARN" value="warn" />
|
||||
<el-option label="ERROR" value="error" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<!-- 测试区域 -->
|
||||
<el-card style="margin-top: 16px;">
|
||||
<template #header>
|
||||
<span>测试智能体</span>
|
||||
</template>
|
||||
|
||||
<div class="test-section">
|
||||
<el-input v-model="testMessage" type="textarea" rows="3" placeholder="输入测试消息..." style="margin-bottom: 12px;" />
|
||||
<el-button type="primary" @click="testAgent">发送测试</el-button>
|
||||
|
||||
<div v-if="testResponse" class="test-response">
|
||||
<h4>AI响应:</h4>
|
||||
<p>{{ testResponse }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 性能统计 -->
|
||||
<el-card style="margin-top: 16px;">
|
||||
<template #header>
|
||||
<span>性能统计</span>
|
||||
</template>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{{ stats.totalChats }}</div>
|
||||
<div class="stat-label">总对话数</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{{ stats.avgResponseTime }}ms</div>
|
||||
<div class="stat-label">平均响应时间</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{{ stats.satisfaction }}</div>
|
||||
<div class="stat-label">平均满意度</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-box">
|
||||
<div class="stat-value">{{ stats.uptime }}%</div>
|
||||
<div class="stat-label">系统可用性</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const agentConfig = ref({
|
||||
name: '泰豪小电智能客服',
|
||||
model: 'gpt-4',
|
||||
temperature: 0.7,
|
||||
maxTokens: 2000,
|
||||
systemPrompt: '你是一个专业的客服助手,代表泰豪小电公司。你需要帮助客户解决设备相关的问题,提供友好、专业的服务。如果问题超出你的能力范围,请建议转接给人工客服。',
|
||||
enableKnowledge: true,
|
||||
enableSentiment: true,
|
||||
enableAutoTicket: true,
|
||||
timeout: 30,
|
||||
logLevel: 'info'
|
||||
})
|
||||
|
||||
const testMessage = ref('')
|
||||
const testResponse = ref('')
|
||||
|
||||
const stats = ref({
|
||||
totalChats: 1258,
|
||||
avgResponseTime: 1250,
|
||||
satisfaction: 4.5,
|
||||
uptime: 99.8
|
||||
})
|
||||
|
||||
const saveConfig = () => {
|
||||
ElMessage.success('配置保存成功')
|
||||
}
|
||||
|
||||
const testAgent = () => {
|
||||
if (!testMessage.value.trim()) {
|
||||
ElMessage.warning('请输入测试消息')
|
||||
return
|
||||
}
|
||||
testResponse.value = '感谢您的咨询。我已收到您的问题,正在为您处理。如果您的问题涉及设备故障,我可以帮您创建一个工单,由我们的技术团队为您服务。'
|
||||
ElMessage.success('测试消息已发送')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./AgentView.scss");
|
||||
@import url("./AgentView.scss");
|
||||
|
||||
.agent-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.test-response {
|
||||
margin-top: 12px;
|
||||
padding: 12px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #409eff;
|
||||
}
|
||||
|
||||
.test-response h4 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.test-response p {
|
||||
margin: 0;
|
||||
color: #606266;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.stat-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #409eff;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 13px;
|
||||
color: #909399;
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +1,245 @@
|
||||
<template>
|
||||
<AdminLayout
|
||||
:title="对话数据管理"
|
||||
:info="管理和分析客服对话数据"
|
||||
>
|
||||
<template action>
|
||||
<ElButton></ElButton>
|
||||
<AdminLayout title="对话数据管理" info="管理和分析客服对话数据">
|
||||
<template #action>
|
||||
<el-button type="primary" @click="exportData">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出数据
|
||||
</el-button>
|
||||
</template>
|
||||
<template default>
|
||||
<div class="customer-chat">
|
||||
<!-- 客服对话管理视图 -->
|
||||
<div>
|
||||
<!-- 条件筛选 -->
|
||||
|
||||
<div class="chat-container">
|
||||
<!-- 筛选区域 -->
|
||||
<el-card class="filter-card">
|
||||
<div class="ticket-filters">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 280px;" />
|
||||
|
||||
<div class="filter-right">
|
||||
<el-select v-model="statusFilter" placeholder="对话状态" clearable style="width: 120px;">
|
||||
<el-option label="进行中" value="ongoing" />
|
||||
<el-option label="已结束" value="ended" />
|
||||
<el-option label="已转工单" value="converted" />
|
||||
</el-select>
|
||||
<el-select v-model="satisfactionFilter" placeholder="满意度" clearable style="width: 120px;">
|
||||
<el-option label="满意" value="satisfied" />
|
||||
<el-option label="一般" value="normal" />
|
||||
<el-option label="不满意" value="unsatisfied" />
|
||||
</el-select>
|
||||
<el-input v-model="searchKeyword" placeholder="搜索客户/内容" style="width: 200px;" :prefix-icon="Search" clearable />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<!-- 数据列表 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- EL分页组件 全局样式 -->
|
||||
</el-card>
|
||||
|
||||
<!-- 对话列表 -->
|
||||
<el-card>
|
||||
<el-table :data="filteredChats" style="width: 100%">
|
||||
<el-table-column prop="chatId" label="对话ID" width="140">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #409eff; font-weight: 500;">{{ row.chatId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="customerName" label="客户信息" width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="customer-info">
|
||||
<span class="name">{{ row.customerName }}</span>
|
||||
<span class="phone">{{ row.customerPhone }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="agentName" label="客服人员" width="100" />
|
||||
<el-table-column prop="messageCount" label="消息数" width="80" align="center" />
|
||||
<el-table-column prop="duration" label="对话时长" width="100" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
{{ row.statusName }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="satisfaction" label="满意度" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-rate v-model="row.satisfaction" disabled allow-half />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startTime" label="开始时间" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewChat(row)">查看</el-button>
|
||||
<el-button type="success" link size="small" @click="downloadChat(row)">下载</el-button>
|
||||
<el-button v-if="row.status === 'ongoing'" type="warning" link size="small" @click="endChat(row)">结束</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination">
|
||||
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="chats.length" layout="total, prev, pager, next" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 查看对话详情弹窗 -->
|
||||
<el-dialog v-model="showChatDialog" title="对话详情" width="700px">
|
||||
<div class="chat-messages">
|
||||
<div v-for="(msg, idx) in currentChatMessages" :key="idx" class="message-item" :class="msg.type">
|
||||
<div class="message-header">
|
||||
<span class="sender">{{ msg.sender }}</span>
|
||||
<span class="time">{{ msg.time }}</span>
|
||||
</div>
|
||||
<div class="message-content">{{ msg.content }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Download, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const statusFilter = ref('')
|
||||
const satisfactionFilter = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const showChatDialog = ref(false)
|
||||
const currentChatMessages = ref<any[]>([])
|
||||
|
||||
const chats = ref([
|
||||
{ chatId: 'CH001', customerName: '张三', customerPhone: '13800138000', agentName: '王五', messageCount: 24, duration: '15分钟', status: 'ended', statusName: '已结束', satisfaction: 4, startTime: '2024-12-13 10:30' },
|
||||
{ chatId: 'CH002', customerName: '李四', customerPhone: '13800138001', agentName: '赵六', messageCount: 18, duration: '12分钟', status: 'ended', statusName: '已结束', satisfaction: 5, startTime: '2024-12-13 09:15' },
|
||||
{ chatId: 'CH003', customerName: '王五', customerPhone: '13800138002', agentName: '孙七', messageCount: 32, duration: '22分钟', status: 'converted', statusName: '已转工单', satisfaction: 3, startTime: '2024-12-12 14:20' },
|
||||
{ chatId: 'CH004', customerName: '赵六', customerPhone: '13800138003', agentName: '李四', messageCount: 15, duration: '10分钟', status: 'ongoing', statusName: '进行中', satisfaction: 0, startTime: '2024-12-13 11:00' },
|
||||
{ chatId: 'CH005', customerName: '孙七', customerPhone: '13800138004', agentName: '王五', messageCount: 28, duration: '18分钟', status: 'ended', statusName: '已结束', satisfaction: 4.5, startTime: '2024-12-13 08:45' }
|
||||
])
|
||||
|
||||
const filteredChats = computed(() => {
|
||||
let result = chats.value
|
||||
if (statusFilter.value) {
|
||||
result = result.filter(c => c.status === statusFilter.value)
|
||||
}
|
||||
if (satisfactionFilter.value) {
|
||||
const satisfaction = satisfactionFilter.value === 'satisfied' ? 4 : satisfactionFilter.value === 'normal' ? 2.5 : 1
|
||||
result = result.filter(c => {
|
||||
if (satisfactionFilter.value === 'satisfied') return c.satisfaction >= 4
|
||||
if (satisfactionFilter.value === 'normal') return c.satisfaction >= 2 && c.satisfaction < 4
|
||||
if (satisfactionFilter.value === 'unsatisfied') return c.satisfaction < 2
|
||||
return true
|
||||
})
|
||||
}
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(c =>
|
||||
c.customerName.toLowerCase().includes(keyword) ||
|
||||
c.customerPhone.includes(keyword)
|
||||
)
|
||||
}
|
||||
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
|
||||
})
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
ongoing: 'success',
|
||||
ended: 'info',
|
||||
converted: 'warning'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const viewChat = (row: any) => {
|
||||
currentChatMessages.value = [
|
||||
{ type: 'customer', sender: row.customerName, time: '10:30:15', content: '你好,我的设备出现了故障' },
|
||||
{ type: 'agent', sender: row.agentName, time: '10:30:45', content: '您好,感谢您的咨询。请问是什么故障呢?' },
|
||||
{ type: 'customer', sender: row.customerName, time: '10:31:20', content: '显示屏不亮了,但是有声音' },
|
||||
{ type: 'agent', sender: row.agentName, time: '10:31:50', content: '好的,这可能是显示屏的问题。请问您的设备型号是什么?' },
|
||||
{ type: 'customer', sender: row.customerName, time: '10:32:30', content: 'TH-500GF' },
|
||||
{ type: 'agent', sender: row.agentName, time: '10:33:00', content: '好的,我为您创建了一个工单,技术人员会尽快联系您' }
|
||||
]
|
||||
showChatDialog.value = true
|
||||
}
|
||||
|
||||
const downloadChat = (row: any) => {
|
||||
ElMessage.success(`下载对话: ${row.chatId}`)
|
||||
}
|
||||
|
||||
const endChat = (row: any) => {
|
||||
ElMessage.info(`结束对话: ${row.chatId}`)
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
ElMessage.success('数据导出成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./CustomerChatView.scss");
|
||||
@import url("./CustomerChatView.scss");
|
||||
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.customer-info .name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.customer-info .phone {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.chat-messages {
|
||||
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.customer {
|
||||
background: #e6f7ff;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.message-item.agent {
|
||||
background: #f0f9ff;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.message-header .sender {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.message-header .time {
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
color: #303133;
|
||||
line-height: 1.5;
|
||||
word-break: break-word;
|
||||
}
|
||||
</style>
|
||||
@@ -1,57 +1,247 @@
|
||||
<template>
|
||||
<AdminLayout
|
||||
:title="知识库管理"
|
||||
:info="管理外部和内部知识库文档"
|
||||
>
|
||||
<template action>
|
||||
<ElButton></ElButton>
|
||||
</template>
|
||||
<template default>
|
||||
<div class="knowledge">
|
||||
<!-- 知识库管理视图 -->
|
||||
<div>
|
||||
<!-- 内外部知识库选择器 -->
|
||||
</div>
|
||||
<span>
|
||||
<!-- 描述内外部知识库 -->
|
||||
</span>
|
||||
<div>
|
||||
<div>
|
||||
<!-- 知识库列表 -->
|
||||
<div v-for="value in source">
|
||||
<!-- 知识库封面、icon -->
|
||||
<img/>
|
||||
<span>
|
||||
<!-- 知识库名称 -->
|
||||
</span>
|
||||
<span>
|
||||
<!-- 文件数量 -->
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<!-- 知识库名称+搜索 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 文件列表 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- EL分页组件 全局样式 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AdminLayout title="知识库管理" info="管理外部和内部知识库文档">
|
||||
<template #action>
|
||||
<el-button type="primary" @click="showUploadDialog = true">
|
||||
<el-icon><Upload /></el-icon>
|
||||
上传文档
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<div class="knowledge-container">
|
||||
<el-card>
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="外部知识库" name="external">
|
||||
<p class="tab-desc">面向客户的知识库内容,包含设备操作指南、故障解决方案等</p>
|
||||
|
||||
<div class="kb-categories">
|
||||
<div v-for="cat in externalCategories" :key="cat.key" class="kb-category-card"
|
||||
:class="{ active: activeExternalCat === cat.key }" @click="activeExternalCat = cat.key">
|
||||
<el-icon :style="{ color: cat.color }"><component :is="cat.icon" /></el-icon>
|
||||
<span class="cat-name">{{ cat.name }}</span>
|
||||
<span class="cat-count">{{ cat.count }} 个文件</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kb-files-section">
|
||||
<div class="section-toolbar">
|
||||
<h3>{{ currentExternalCatName }}</h3>
|
||||
<el-input v-model="externalSearch" placeholder="搜索文件名" style="width: 240px;" :prefix-icon="Search" clearable />
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredExternalFiles" style="width: 100%">
|
||||
<el-table-column prop="name" label="文件名" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<div class="file-name-cell">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span>{{ row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploader" label="上传人员" width="120" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="180" />
|
||||
<el-table-column label="操作" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="previewFile(row)">
|
||||
<el-icon><View /></el-icon>预览
|
||||
</el-button>
|
||||
<el-button type="success" link size="small" @click="downloadFile(row)">
|
||||
<el-icon><Download /></el-icon>下载
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="deleteFile(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="内部知识库" name="internal">
|
||||
<p class="tab-desc">内部技术资料与服务规范,仅供内部员工使用</p>
|
||||
|
||||
<div class="kb-categories">
|
||||
<div v-for="cat in internalCategories" :key="cat.key" class="kb-category-card"
|
||||
:class="{ active: activeInternalCat === cat.key }" @click="activeInternalCat = cat.key">
|
||||
<el-icon :style="{ color: cat.color }"><component :is="cat.icon" /></el-icon>
|
||||
<span class="cat-name">{{ cat.name }}</span>
|
||||
<span class="cat-count">{{ cat.count }} 个文件</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kb-files-section">
|
||||
<div class="section-toolbar">
|
||||
<h3>{{ currentInternalCatName }}</h3>
|
||||
<el-input v-model="internalSearch" placeholder="搜索文件名" style="width: 240px;" :prefix-icon="Search" clearable />
|
||||
</div>
|
||||
|
||||
<el-table :data="filteredInternalFiles" style="width: 100%">
|
||||
<el-table-column prop="name" label="文件名" min-width="280">
|
||||
<template #default="{ row }">
|
||||
<div class="file-name-cell">
|
||||
<el-icon class="file-icon"><Document /></el-icon>
|
||||
<span>{{ row.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="uploader" label="上传人员" width="120" />
|
||||
<el-table-column prop="uploadTime" label="上传时间" width="180" />
|
||||
<el-table-column label="操作" width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="previewFile(row)">
|
||||
<el-icon><View /></el-icon>预览
|
||||
</el-button>
|
||||
<el-button type="success" link size="small" @click="downloadFile(row)">
|
||||
<el-icon><Download /></el-icon>下载
|
||||
</el-button>
|
||||
<el-button type="danger" link size="small" @click="deleteFile(row)">
|
||||
<el-icon><Delete /></el-icon>删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 上传文档弹窗 -->
|
||||
<el-dialog v-model="showUploadDialog" title="上传文档" width="500px">
|
||||
<el-form :model="uploadForm" label-width="80px">
|
||||
<el-form-item label="知识库" required>
|
||||
<el-select v-model="uploadForm.kbType" placeholder="请选择知识库">
|
||||
<el-option label="外部知识库" value="external" />
|
||||
<el-option label="内部知识库" value="internal" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" required>
|
||||
<el-select v-model="uploadForm.category" placeholder="请选择分类">
|
||||
<el-option label="操作指南" value="guide" />
|
||||
<el-option label="故障排查" value="troubleshoot" />
|
||||
<el-option label="技术规范" value="spec" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件" required>
|
||||
<el-upload action="#" :auto-upload="false" drag>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
拖拽文件到此或 <em>点击上传</em>
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showUploadDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="uploadFile">上传</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElButton } from 'element-plus';
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Upload, Search, Document, View, Download, Delete } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const activeTab = ref('external')
|
||||
const activeExternalCat = ref('guide')
|
||||
const activeInternalCat = ref('spec')
|
||||
const externalSearch = ref('')
|
||||
const internalSearch = ref('')
|
||||
const showUploadDialog = ref(false)
|
||||
|
||||
const uploadForm = ref({
|
||||
kbType: '',
|
||||
category: '',
|
||||
file: null
|
||||
})
|
||||
|
||||
const externalCategories = ref([
|
||||
{ key: 'guide', name: '操作指南', count: 12, color: '#409eff', icon: 'Document' },
|
||||
{ key: 'troubleshoot', name: '故障排查', count: 8, color: '#67c23a', icon: 'Document' },
|
||||
{ key: 'spec', name: '技术规范', count: 5, color: '#e6a23c', icon: 'Document' }
|
||||
])
|
||||
|
||||
const internalCategories = ref([
|
||||
{ key: 'spec', name: '技术规范', count: 15, color: '#409eff', icon: 'Document' },
|
||||
{ key: 'process', name: '流程文档', count: 10, color: '#67c23a', icon: 'Document' },
|
||||
{ key: 'training', name: '培训资料', count: 7, color: '#e6a23c', icon: 'Document' }
|
||||
])
|
||||
|
||||
const externalFiles = ref([
|
||||
{ name: 'TH-500GF操作手册.pdf', uploader: '张三', uploadTime: '2024-12-10 14:30', category: 'guide' },
|
||||
{ name: 'TH-300D故障排查指南.pdf', uploader: '李四', uploadTime: '2024-12-09 10:15', category: 'troubleshoot' },
|
||||
{ name: '设备安装规范.pdf', uploader: '王五', uploadTime: '2024-12-08 09:45', category: 'spec' }
|
||||
])
|
||||
|
||||
const internalFiles = ref([
|
||||
{ name: '内部技术规范v2.0.pdf', uploader: '赵六', uploadTime: '2024-12-11 16:20', category: 'spec' },
|
||||
{ name: '售后服务流程.pdf', uploader: '孙七', uploadTime: '2024-12-10 11:00', category: 'process' },
|
||||
{ name: '员工培训手册.pdf', uploader: '周八', uploadTime: '2024-12-09 15:30', category: 'training' }
|
||||
])
|
||||
|
||||
const currentExternalCatName = computed(() => {
|
||||
const cat = externalCategories.value.find(c => c.key === activeExternalCat.value)
|
||||
return cat?.name || '操作指南'
|
||||
})
|
||||
|
||||
const currentInternalCatName = computed(() => {
|
||||
const cat = internalCategories.value.find(c => c.key === activeInternalCat.value)
|
||||
return cat?.name || '技术规范'
|
||||
})
|
||||
|
||||
const filteredExternalFiles = computed(() => {
|
||||
return externalFiles.value.filter(f => {
|
||||
const matchCat = f.category === activeExternalCat.value
|
||||
const matchSearch = !externalSearch.value || f.name.toLowerCase().includes(externalSearch.value.toLowerCase())
|
||||
return matchCat && matchSearch
|
||||
})
|
||||
})
|
||||
|
||||
const filteredInternalFiles = computed(() => {
|
||||
return internalFiles.value.filter(f => {
|
||||
const matchCat = f.category === activeInternalCat.value
|
||||
const matchSearch = !internalSearch.value || f.name.toLowerCase().includes(internalSearch.value.toLowerCase())
|
||||
return matchCat && matchSearch
|
||||
})
|
||||
})
|
||||
|
||||
const previewFile = (row: any) => {
|
||||
ElMessage.info(`预览文件: ${row.name}`)
|
||||
}
|
||||
|
||||
const downloadFile = (row: any) => {
|
||||
ElMessage.success(`下载文件: ${row.name}`)
|
||||
}
|
||||
|
||||
const deleteFile = (row: any) => {
|
||||
ElMessage.warning(`删除文件: ${row.name}`)
|
||||
}
|
||||
|
||||
const uploadFile = () => {
|
||||
if (!uploadForm.value.kbType || !uploadForm.value.category) {
|
||||
ElMessage.error('请填写必填项')
|
||||
return
|
||||
}
|
||||
ElMessage.success('文件上传成功')
|
||||
showUploadDialog.value = false
|
||||
uploadForm.value = { kbType: '', category: '', file: null }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./KnowLedgeView.scss");
|
||||
@import url("./KnowLedgeView.scss");
|
||||
|
||||
.knowledge-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.tab-desc {
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,35 +1,166 @@
|
||||
<template>
|
||||
|
||||
<AdminLayout
|
||||
:title="知识库日志"
|
||||
:info="查看知识库操作记录"
|
||||
>
|
||||
<template action>
|
||||
<ElButton></ElButton>
|
||||
</template>
|
||||
<template default>
|
||||
<div class="knowledge-log">
|
||||
<!-- 知识库日志管理视图 -->
|
||||
<div>
|
||||
<!-- 条件筛选区域 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 数据列表区域 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- EL分页组件 全局样式 -->
|
||||
</div>
|
||||
</div>
|
||||
<AdminLayout title="知识库日志" info="查看知识库操作记录">
|
||||
<template #action>
|
||||
<el-button type="primary" @click="exportLogs">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出日志
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<div class="log-container">
|
||||
<!-- 筛选区域 -->
|
||||
<el-card class="filter-card">
|
||||
<div class="ticket-filters">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 280px;" />
|
||||
|
||||
<div class="filter-right">
|
||||
<el-select v-model="operationFilter" placeholder="操作类型" clearable style="width: 140px;">
|
||||
<el-option label="上传" value="upload" />
|
||||
<el-option label="下载" value="download" />
|
||||
<el-option label="删除" value="delete" />
|
||||
<el-option label="更新" value="update" />
|
||||
</el-select>
|
||||
<el-select v-model="kbTypeFilter" placeholder="知识库类型" clearable style="width: 140px;">
|
||||
<el-option label="外部知识库" value="external" />
|
||||
<el-option label="内部知识库" value="internal" />
|
||||
</el-select>
|
||||
<el-input v-model="searchKeyword" placeholder="搜索文件名/操作人" style="width: 200px;" :prefix-icon="Search" clearable />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<el-card>
|
||||
<el-table :data="filteredLogs" style="width: 100%">
|
||||
<el-table-column prop="logId" label="日志ID" width="120">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #409eff; font-weight: 500;">{{ row.logId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="fileName" label="文件名" min-width="200" />
|
||||
<el-table-column prop="operation" label="操作类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOperationType(row.operation)" size="small">
|
||||
{{ row.operationName }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="kbType" label="知识库" width="100">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.kbType === 'external' ? '外部' : '内部' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" width="100" />
|
||||
<el-table-column prop="operationTime" label="操作时间" width="160" />
|
||||
<el-table-column prop="fileSize" label="文件大小" width="100" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination">
|
||||
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="logs.length" layout="total, prev, pager, next" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情弹窗 -->
|
||||
<el-dialog v-model="showDetailDialog" title="日志详情" width="600px">
|
||||
<el-form v-if="selectedLog" label-width="100px">
|
||||
<el-form-item label="日志ID">
|
||||
<span>{{ selectedLog.logId }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件名">
|
||||
<span>{{ selectedLog.fileName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-tag :type="getOperationType(selectedLog.operation)">{{ selectedLog.operationName }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="知识库">
|
||||
<span>{{ selectedLog.kbType === 'external' ? '外部知识库' : '内部知识库' }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人">
|
||||
<span>{{ selectedLog.operator }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作时间">
|
||||
<span>{{ selectedLog.operationTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="文件大小">
|
||||
<span>{{ selectedLog.fileSize }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Download, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const operationFilter = ref('')
|
||||
const kbTypeFilter = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const showDetailDialog = ref(false)
|
||||
const selectedLog = ref<any>(null)
|
||||
|
||||
const logs = ref([
|
||||
{ logId: 'LOG001', fileName: 'TH-500GF操作手册.pdf', operation: 'upload', operationName: '上传', kbType: 'external', operator: '张三', operationTime: '2024-12-10 14:30', fileSize: '2.5MB' },
|
||||
{ logId: 'LOG002', fileName: 'TH-300D故障排查指南.pdf', operation: 'upload', operationName: '上传', kbType: 'external', operator: '李四', operationTime: '2024-12-09 10:15', fileSize: '1.8MB' },
|
||||
{ logId: 'LOG003', fileName: '内部技术规范v2.0.pdf', operation: 'update', operationName: '更新', kbType: 'internal', operator: '赵六', operationTime: '2024-12-11 16:20', fileSize: '3.2MB' },
|
||||
{ logId: 'LOG004', fileName: '售后服务流程.pdf', operation: 'download', operationName: '下载', kbType: 'internal', operator: '孙七', operationTime: '2024-12-10 11:00', fileSize: '1.1MB' },
|
||||
{ logId: 'LOG005', fileName: '员工培训手册.pdf', operation: 'delete', operationName: '删除', kbType: 'internal', operator: '周八', operationTime: '2024-12-09 15:30', fileSize: '2.0MB' }
|
||||
])
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logs.value
|
||||
if (operationFilter.value) {
|
||||
result = result.filter(l => l.operation === operationFilter.value)
|
||||
}
|
||||
if (kbTypeFilter.value) {
|
||||
result = result.filter(l => l.kbType === kbTypeFilter.value)
|
||||
}
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(l =>
|
||||
l.fileName.toLowerCase().includes(keyword) ||
|
||||
l.operator.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
|
||||
})
|
||||
|
||||
const getOperationType = (operation: string) => {
|
||||
const map: Record<string, string> = {
|
||||
upload: 'success',
|
||||
download: 'info',
|
||||
delete: 'danger',
|
||||
update: 'warning'
|
||||
}
|
||||
return map[operation] || 'info'
|
||||
}
|
||||
|
||||
const viewDetail = (row: any) => {
|
||||
selectedLog.value = row
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
const exportLogs = () => {
|
||||
ElMessage.success('日志导出成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./KnowledgeLogView.scss");
|
||||
@import url("./KnowledgeLogView.scss");
|
||||
|
||||
.log-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,33 +1,165 @@
|
||||
<template>
|
||||
<AdminLayout
|
||||
:title="系统日志"
|
||||
:info="查看系统配置变更与异常信息"
|
||||
>
|
||||
<template action>
|
||||
<ElButton></ElButton>
|
||||
</template>
|
||||
<template default>
|
||||
<div class="system-log">
|
||||
<!-- 系统日志管理视图 -->
|
||||
<div>
|
||||
<!-- 条件筛选区域 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 数据列表区域 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- EL分页组件 全局样式 -->
|
||||
</div>
|
||||
</div>
|
||||
<AdminLayout title="系统日志" info="查看系统配置变更与异常信息">
|
||||
<template #action>
|
||||
<el-button type="primary" @click="exportLogs">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出日志
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<div class="log-container">
|
||||
<!-- 筛选区域 -->
|
||||
<el-card class="filter-card">
|
||||
<div class="ticket-filters">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 280px;" />
|
||||
|
||||
<div class="filter-right">
|
||||
<el-select v-model="levelFilter" placeholder="日志级别" clearable style="width: 120px;">
|
||||
<el-option label="DEBUG" value="debug" />
|
||||
<el-option label="INFO" value="info" />
|
||||
<el-option label="WARN" value="warn" />
|
||||
<el-option label="ERROR" value="error" />
|
||||
</el-select>
|
||||
<el-select v-model="moduleFilter" placeholder="模块" clearable style="width: 120px;">
|
||||
<el-option label="认证" value="auth" />
|
||||
<el-option label="工单" value="workcase" />
|
||||
<el-option label="知识库" value="knowledge" />
|
||||
<el-option label="AI服务" value="ai" />
|
||||
</el-select>
|
||||
<el-input v-model="searchKeyword" placeholder="搜索日志内容" style="width: 200px;" :prefix-icon="Search" clearable />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<el-card>
|
||||
<el-table :data="filteredLogs" style="width: 100%">
|
||||
<el-table-column prop="logId" label="日志ID" width="120">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #409eff; font-weight: 500;">{{ row.logId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="level" label="级别" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getLevelType(row.level)" size="small">
|
||||
{{ row.level.toUpperCase() }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="module" label="模块" width="100" />
|
||||
<el-table-column prop="message" label="日志内容" min-width="250" />
|
||||
<el-table-column prop="timestamp" label="时间" width="160" />
|
||||
<el-table-column prop="source" label="来源" width="120" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination">
|
||||
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="logs.length" layout="total, prev, pager, next" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情弹窗 -->
|
||||
<el-dialog v-model="showDetailDialog" title="日志详情" width="700px">
|
||||
<el-form v-if="selectedLog" label-width="100px">
|
||||
<el-form-item label="日志ID">
|
||||
<span>{{ selectedLog.logId }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="级别">
|
||||
<el-tag :type="getLevelType(selectedLog.level)">{{ selectedLog.level.toUpperCase() }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="模块">
|
||||
<span>{{ selectedLog.module }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="日志内容">
|
||||
<span>{{ selectedLog.message }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="时间">
|
||||
<span>{{ selectedLog.timestamp }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="来源">
|
||||
<span>{{ selectedLog.source }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="selectedLog.stackTrace" label="堆栈跟踪">
|
||||
<div style="background: #f5f7fa; padding: 10px; border-radius: 4px; max-height: 200px; overflow-y: auto; font-family: monospace; font-size: 12px;">
|
||||
{{ selectedLog.stackTrace }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Download, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const levelFilter = ref('')
|
||||
const moduleFilter = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const showDetailDialog = ref(false)
|
||||
const selectedLog = ref<any>(null)
|
||||
|
||||
const logs = ref([
|
||||
{ logId: 'LOG001', level: 'info', module: '认证', message: '用户 admin 登录成功', timestamp: '2024-12-13 10:30:15', source: '192.168.1.100', stackTrace: null },
|
||||
{ logId: 'LOG002', level: 'info', module: '工单', message: '工单 TK001 创建成功', timestamp: '2024-12-13 10:31:20', source: '192.168.1.101', stackTrace: null },
|
||||
{ logId: 'LOG003', level: 'warn', module: '知识库', message: '文件上传超时,已重试', timestamp: '2024-12-13 10:32:45', source: '192.168.1.102', stackTrace: null },
|
||||
{ logId: 'LOG004', level: 'error', module: 'AI服务', message: '调用 GPT-4 API 失败:连接超时', timestamp: '2024-12-13 10:33:50', source: '192.168.1.103', stackTrace: 'java.net.SocketTimeoutException: Connection timeout\n at com.example.ai.GptClient.call(GptClient.java:45)\n at com.example.service.AiService.process(AiService.java:120)' },
|
||||
{ logId: 'LOG005', level: 'debug', module: '工单', message: '工单状态更新:pending -> processing', timestamp: '2024-12-13 10:34:30', source: '192.168.1.100', stackTrace: null }
|
||||
])
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logs.value
|
||||
if (levelFilter.value) {
|
||||
result = result.filter(l => l.level === levelFilter.value)
|
||||
}
|
||||
if (moduleFilter.value) {
|
||||
result = result.filter(l => l.module === moduleFilter.value)
|
||||
}
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(l =>
|
||||
l.message.toLowerCase().includes(keyword) ||
|
||||
l.module.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
|
||||
})
|
||||
|
||||
const getLevelType = (level: string) => {
|
||||
const map: Record<string, string> = {
|
||||
debug: 'info',
|
||||
info: 'success',
|
||||
warn: 'warning',
|
||||
error: 'danger'
|
||||
}
|
||||
return map[level] || 'info'
|
||||
}
|
||||
|
||||
const viewDetail = (row: any) => {
|
||||
selectedLog.value = row
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
const exportLogs = () => {
|
||||
ElMessage.success('日志导出成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./SystemLogView.scss");
|
||||
@import url("./SystemLogView.scss");
|
||||
|
||||
.log-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +1,165 @@
|
||||
<template>
|
||||
<AdminLayout
|
||||
:title="工单日志"
|
||||
:info="查看工单操作记录"
|
||||
>
|
||||
<template action>
|
||||
<ElButton></ElButton>
|
||||
</template>
|
||||
<template default>
|
||||
<div class="workcase-log">
|
||||
<!-- 工单日志管理视图 -->
|
||||
<div>
|
||||
<!-- 条件筛选区域 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 数据列表区域 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- EL分页组件 全局样式 -->
|
||||
</div>
|
||||
</div>
|
||||
<AdminLayout title="工单日志" info="查看工单操作记录">
|
||||
<template #action>
|
||||
<el-button type="primary" @click="exportLogs">
|
||||
<el-icon><Download /></el-icon>
|
||||
导出日志
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<div class="log-container">
|
||||
<!-- 筛选区域 -->
|
||||
<el-card class="filter-card">
|
||||
<div class="ticket-filters">
|
||||
<el-date-picker v-model="dateRange" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" style="width: 280px;" />
|
||||
|
||||
<div class="filter-right">
|
||||
<el-select v-model="operationFilter" placeholder="操作类型" clearable style="width: 140px;">
|
||||
<el-option label="创建" value="create" />
|
||||
<el-option label="更新" value="update" />
|
||||
<el-option label="指派" value="assign" />
|
||||
<el-option label="完成" value="complete" />
|
||||
<el-option label="关闭" value="close" />
|
||||
</el-select>
|
||||
<el-select v-model="operatorFilter" placeholder="操作人" clearable style="width: 120px;">
|
||||
<el-option label="王五" value="wangwu" />
|
||||
<el-option label="赵六" value="zhaoliu" />
|
||||
<el-option label="孙七" value="sunqi" />
|
||||
</el-select>
|
||||
<el-input v-model="searchKeyword" placeholder="搜索工单号/内容" style="width: 200px;" :prefix-icon="Search" clearable />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 日志列表 -->
|
||||
<el-card>
|
||||
<el-table :data="filteredLogs" style="width: 100%">
|
||||
<el-table-column prop="logId" label="日志ID" width="120">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #409eff; font-weight: 500;">{{ row.logId }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ticketNo" label="工单号" width="120" />
|
||||
<el-table-column prop="operation" label="操作类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getOperationType(row.operation)" size="small">
|
||||
{{ row.operationName }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="operator" label="操作人" width="100" />
|
||||
<el-table-column prop="content" label="操作内容" min-width="200" />
|
||||
<el-table-column prop="operationTime" label="操作时间" width="160" />
|
||||
<el-table-column prop="ipAddress" label="IP地址" width="130" />
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination">
|
||||
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="logs.length" layout="total, prev, pager, next" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 日志详情弹窗 -->
|
||||
<el-dialog v-model="showDetailDialog" title="日志详情" width="600px">
|
||||
<el-form v-if="selectedLog" label-width="100px">
|
||||
<el-form-item label="日志ID">
|
||||
<span>{{ selectedLog.logId }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="工单号">
|
||||
<span>{{ selectedLog.ticketNo }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型">
|
||||
<el-tag :type="getOperationType(selectedLog.operation)">{{ selectedLog.operationName }}</el-tag>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作人">
|
||||
<span>{{ selectedLog.operator }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作内容">
|
||||
<span>{{ selectedLog.content }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作时间">
|
||||
<span>{{ selectedLog.operationTime }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="IP地址">
|
||||
<span>{{ selectedLog.ipAddress }}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||
import { ElInput, ElSelect } from 'element-plus';
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Download, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const dateRange = ref<[Date, Date] | null>(null)
|
||||
const operationFilter = ref('')
|
||||
const operatorFilter = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const showDetailDialog = ref(false)
|
||||
const selectedLog = ref<any>(null)
|
||||
|
||||
const logs = ref([
|
||||
{ logId: 'LOG001', ticketNo: 'TK001', operation: 'create', operationName: '创建', operator: '王五', content: '创建工单,客户反映设备显示屏不亮', operationTime: '2024-12-13 10:30', ipAddress: '192.168.1.100' },
|
||||
{ logId: 'LOG002', ticketNo: 'TK001', operation: 'assign', operationName: '指派', operator: '赵六', content: '将工单指派给技术人员处理', operationTime: '2024-12-13 10:35', ipAddress: '192.168.1.101' },
|
||||
{ logId: 'LOG003', ticketNo: 'TK002', operation: 'create', operationName: '创建', operator: '孙七', content: '创建工单,客户反映机械故障', operationTime: '2024-12-13 09:15', ipAddress: '192.168.1.102' },
|
||||
{ logId: 'LOG004', ticketNo: 'TK002', operation: 'update', operationName: '更新', operator: '王五', content: '更新工单状态为处理中', operationTime: '2024-12-13 09:45', ipAddress: '192.168.1.100' },
|
||||
{ logId: 'LOG005', ticketNo: 'TK003', operation: 'complete', operationName: '完成', operator: '赵六', content: '工单处理完成,客户已确认', operationTime: '2024-12-12 14:20', ipAddress: '192.168.1.101' }
|
||||
])
|
||||
|
||||
const filteredLogs = computed(() => {
|
||||
let result = logs.value
|
||||
if (operationFilter.value) {
|
||||
result = result.filter(l => l.operation === operationFilter.value)
|
||||
}
|
||||
if (operatorFilter.value) {
|
||||
result = result.filter(l => l.operator === operatorFilter.value)
|
||||
}
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(l =>
|
||||
l.ticketNo.toLowerCase().includes(keyword) ||
|
||||
l.content.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
|
||||
})
|
||||
|
||||
const getOperationType = (operation: string) => {
|
||||
const map: Record<string, string> = {
|
||||
create: 'success',
|
||||
update: 'info',
|
||||
assign: 'warning',
|
||||
complete: 'success',
|
||||
close: 'danger'
|
||||
}
|
||||
return map[operation] || 'info'
|
||||
}
|
||||
|
||||
const viewDetail = (row: any) => {
|
||||
selectedLog.value = row
|
||||
showDetailDialog.value = true
|
||||
}
|
||||
|
||||
const exportLogs = () => {
|
||||
ElMessage.success('日志导出成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./WorkcaseLogView.scss");
|
||||
@import url("./WorkcaseLogView.scss");
|
||||
|
||||
.log-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,69 +1,189 @@
|
||||
<template>
|
||||
<AdminLayout
|
||||
:title="数据概览"
|
||||
>
|
||||
<template action>
|
||||
<span>泰豪小电智能客服运营数据</span>
|
||||
</template>
|
||||
<template default>
|
||||
<div>
|
||||
<!-- 中间区域 -->
|
||||
<div>
|
||||
<!-- 信息卡 -->
|
||||
<div>
|
||||
<!-- 咨询次数 点击跳转 对话数据 -->
|
||||
<AdminLayout title="数据概览" info="泰豪小电智能客服运营数据">
|
||||
<div class="overview-container">
|
||||
<!-- 核心指标卡片 -->
|
||||
<div class="dashboard-stats">
|
||||
<div class="stat-card clickable" @click="goToChat">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #409eff, #66b1ff);">
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<!-- 待处理工单 点击跳转 工单管理,选择待处理的选项 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 已处理工单 点击跳转 工单管理,选择已处理的选项-->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<!-- 问题分类统计 -->
|
||||
<span></span>
|
||||
<div>
|
||||
<!-- 时间范围选择 -->
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<!-- 词云 -->
|
||||
<span></span>
|
||||
<div>
|
||||
<!-- 词云图 -->
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">1,258</div>
|
||||
<div class="stat-label">咨询次数</div>
|
||||
<div class="stat-trend up">
|
||||
<el-icon><Top /></el-icon>
|
||||
<span>较昨日 +12.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<!-- 快捷入口,固定在底部 -->
|
||||
<span></span>
|
||||
<div>
|
||||
<div>
|
||||
<!-- 知识库管理 点击跳转 知识库管理 -->
|
||||
<div class="stat-card clickable" @click="goToWorkcase('pending')">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #e6a23c, #f5c06a);">
|
||||
<el-icon><Clock /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ dashboardData.pendingTickets }}</div>
|
||||
<div class="stat-label">待处理工单</div>
|
||||
<div class="stat-trend">
|
||||
<span>点击查看详情</span>
|
||||
<el-icon><Right /></el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<!-- 工单管理 点击跳转 工单管理 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 对话数据 点击跳转 对话数据管理 -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- 智能体管理 点击跳转 智能体管理 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stat-card clickable" @click="goToWorkcase('completed')">
|
||||
<div class="stat-icon" style="background: linear-gradient(135deg, #67c23a, #95d475);">
|
||||
<el-icon><Select /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">{{ dashboardData.completedTickets }}</div>
|
||||
<div class="stat-label">已处理工单</div>
|
||||
<div class="stat-trend">
|
||||
<span>点击查看详情</span>
|
||||
<el-icon><Right /></el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 问题分类统计和词云 -->
|
||||
<div class="dashboard-charts">
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>问题分类统计</span>
|
||||
<el-radio-group v-model="questionStatPeriod" size="small">
|
||||
<el-radio-button label="today">今日</el-radio-button>
|
||||
<el-radio-button label="week">本周</el-radio-button>
|
||||
<el-radio-button label="month">本月</el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</template>
|
||||
<div class="question-stats">
|
||||
<div v-for="item in questionCategories" :key="item.name" class="question-stat-item clickable">
|
||||
<div class="stat-bar-header">
|
||||
<span class="stat-name">{{ item.name }}</span>
|
||||
<span class="stat-count">{{ item.count }} 次</span>
|
||||
</div>
|
||||
<div class="stat-bar">
|
||||
<div class="stat-bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="chart-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>涉及产品词云</span>
|
||||
<el-button type="primary" link size="small" @click="goToChat">
|
||||
查看详情
|
||||
<el-icon><Right /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="product-cloud">
|
||||
<span v-for="(product, index) in productCloudData" :key="index" class="cloud-tag"
|
||||
:style="{ fontSize: product.size + 'px', color: product.color, opacity: 0.7 + product.weight * 0.3 }">
|
||||
{{ product.name }}
|
||||
</span>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 快捷入口 -->
|
||||
<el-card class="quick-entry-card">
|
||||
<template #header>
|
||||
<span>快捷入口</span>
|
||||
</template>
|
||||
<div class="quick-entries">
|
||||
<div class="quick-entry-item" @click="goToKnowledge">
|
||||
<el-icon class="entry-icon" style="background: #409eff;"><Document /></el-icon>
|
||||
<span>知识库管理</span>
|
||||
</div>
|
||||
<div class="quick-entry-item" @click="goToWorkcase()">
|
||||
<el-icon class="entry-icon" style="background: #e6a23c;"><Tickets /></el-icon>
|
||||
<span>工单管理</span>
|
||||
</div>
|
||||
<div class="quick-entry-item" @click="goToChat">
|
||||
<el-icon class="entry-icon" style="background: #67c23a;"><ChatDotRound /></el-icon>
|
||||
<span>对话数据</span>
|
||||
</div>
|
||||
<div class="quick-entry-item" @click="goToAgent">
|
||||
<el-icon class="entry-icon" style="background: #909399;"><Service /></el-icon>
|
||||
<span>智能体管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||
import { ref } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { ChatDotRound, Clock, Select, Top, Right, Document, Tickets, Service } from '@element-plus/icons-vue'
|
||||
|
||||
const questionStatPeriod = ref('today')
|
||||
|
||||
const dashboardData = ref({
|
||||
pendingTickets: 24,
|
||||
completedTickets: 856
|
||||
})
|
||||
|
||||
const questionCategories = ref([
|
||||
{ name: '设备故障', count: 156, percent: 85, color: '#409eff' },
|
||||
{ name: '使用咨询', count: 98, percent: 65, color: '#67c23a' },
|
||||
{ name: '配件更换', count: 45, percent: 35, color: '#e6a23c' },
|
||||
{ name: '安装调试', count: 32, percent: 25, color: '#f56c6c' },
|
||||
{ name: '其他问题', count: 28, percent: 18, color: '#909399' }
|
||||
])
|
||||
|
||||
const productCloudData = ref([
|
||||
{ name: 'TH-500GF', size: 24, weight: 0.9, color: '#409eff' },
|
||||
{ name: 'TH-300D', size: 20, weight: 0.7, color: '#67c23a' },
|
||||
{ name: 'TH-800GF', size: 22, weight: 0.8, color: '#e6a23c' },
|
||||
{ name: 'S-200X', size: 18, weight: 0.6, color: '#f56c6c' },
|
||||
{ name: 'S-150X', size: 16, weight: 0.5, color: '#909399' },
|
||||
{ name: 'G-100S', size: 19, weight: 0.65, color: '#409eff' },
|
||||
{ name: 'G-200S', size: 21, weight: 0.75, color: '#67c23a' }
|
||||
])
|
||||
|
||||
const goToChat = () => {
|
||||
console.log('跳转到对话数据')
|
||||
}
|
||||
|
||||
const goToWorkcase = (status?: string) => {
|
||||
console.log('跳转到工单管理', status)
|
||||
}
|
||||
|
||||
const goToKnowledge = () => {
|
||||
console.log('跳转到知识库管理')
|
||||
}
|
||||
|
||||
const goToAgent = () => {
|
||||
console.log('跳转到智能体管理')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./OverviewView.scss");
|
||||
@import url("./OverviewView.scss");
|
||||
|
||||
.overview-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.dashboard-charts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
.dashboard-charts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +1,250 @@
|
||||
<template>
|
||||
<AdminLayout
|
||||
:title="工单管理"
|
||||
:info="查看和处理客户服务工单"
|
||||
>
|
||||
<template action>
|
||||
<ElButton></ElButton>
|
||||
</template>
|
||||
<template default>
|
||||
<div class="workcase">
|
||||
<!-- 工单管理视图 -->
|
||||
<div>
|
||||
<!-- 筛选区域 -->
|
||||
<div>
|
||||
<!-- 工单状态筛选按钮 -->
|
||||
</div>
|
||||
<div>
|
||||
<ElSelect></ElSelect>
|
||||
<ElSelect></ElSelect>
|
||||
<ElInput></ElInput>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>
|
||||
<!-- ELTable -->
|
||||
</div>
|
||||
<div>
|
||||
<!-- EL分页组件 全局样式-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AdminLayout title="工单管理" info="查看和处理客户服务工单">
|
||||
<template #action>
|
||||
<el-button type="primary" @click="showCreateDialog = true">
|
||||
<el-icon><Plus /></el-icon>
|
||||
创建工单
|
||||
</el-button>
|
||||
</template>
|
||||
|
||||
<div class="workcase-container">
|
||||
<!-- 筛选区域 -->
|
||||
<el-card class="filter-card">
|
||||
<div class="ticket-filters">
|
||||
<el-radio-group v-model="statusFilter" @change="handleFilterChange">
|
||||
<el-radio-button label="all">全部</el-radio-button>
|
||||
<el-radio-button label="pending">待处理</el-radio-button>
|
||||
<el-radio-button label="processing">处理中</el-radio-button>
|
||||
<el-radio-button label="completed">已完成</el-radio-button>
|
||||
<el-radio-button label="closed">已关闭</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<div class="filter-right">
|
||||
<el-select v-model="typeFilter" placeholder="故障类型" clearable style="width: 140px;">
|
||||
<el-option label="电气系统故障" value="electrical" />
|
||||
<el-option label="机械故障" value="mechanical" />
|
||||
<el-option label="控制系统故障" value="control" />
|
||||
<el-option label="配件更换" value="parts" />
|
||||
<el-option label="安装调试" value="install" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
<el-select v-model="urgencyFilter" placeholder="紧急程度" clearable style="width: 120px;">
|
||||
<el-option label="紧急" value="urgent" />
|
||||
<el-option label="普通" value="normal" />
|
||||
<el-option label="低" value="low" />
|
||||
</el-select>
|
||||
<el-input v-model="searchKeyword" placeholder="搜索工单号/客户/设备" style="width: 200px;" :prefix-icon="Search" clearable />
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 工单列表 -->
|
||||
<el-card>
|
||||
<el-table :data="filteredTickets" style="width: 100%">
|
||||
<el-table-column prop="ticketNo" label="工单号" width="140">
|
||||
<template #default="{ row }">
|
||||
<span class="ticket-no">{{ row.ticketNo }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="customerName" label="客户信息" width="150">
|
||||
<template #default="{ row }">
|
||||
<div class="customer-info">
|
||||
<span class="name">{{ row.customerName }}</span>
|
||||
<span class="phone">{{ row.customerPhone }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="faultType" label="故障类型" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-tag size="small">{{ row.faultTypeName }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="deviceModel" label="设备型号" width="120" />
|
||||
<el-table-column prop="urgency" label="紧急程度" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.urgency === 'urgent' ? 'danger' : row.urgency === 'normal' ? 'warning' : 'info'" size="small">
|
||||
{{ row.urgencyName }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusType(row.status)" size="small">
|
||||
{{ row.statusName }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="assignee" label="处理人" width="100">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.assignee">{{ row.assignee }}</span>
|
||||
<span v-else class="unassigned">未指派</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createTime" label="创建时间" width="160" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link size="small" @click="viewDetail(row)">详情</el-button>
|
||||
<el-button v-if="row.status === 'pending'" type="warning" link size="small" @click="assignTicket(row)">指派</el-button>
|
||||
<el-button v-if="row.status === 'processing'" type="success" link size="small" @click="completeTicket(row)">完成</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="table-pagination">
|
||||
<el-pagination v-model:current-page="currentPage" :page-size="10" :total="tickets.length" layout="total, prev, pager, next" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<!-- 创建工单弹窗 -->
|
||||
<el-dialog v-model="showCreateDialog" title="创建工单" width="650px">
|
||||
<el-form :model="formData" label-width="90px">
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户姓名" required>
|
||||
<el-input v-model="formData.customerName" placeholder="请输入客户姓名" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="联系电话" required>
|
||||
<el-input v-model="formData.customerPhone" placeholder="请输入联系电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="设备型号" required>
|
||||
<el-select v-model="formData.deviceModel" placeholder="请选择设备型号" style="width: 100%;">
|
||||
<el-option label="TH-500GF" value="TH-500GF" />
|
||||
<el-option label="TH-300D" value="TH-300D" />
|
||||
<el-option label="TH-800GF" value="TH-800GF" />
|
||||
<el-option label="S-200X" value="S-200X" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="故障描述" required>
|
||||
<el-input v-model="formData.description" type="textarea" rows="4" placeholder="请输入故障描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="showCreateDialog = false">取消</el-button>
|
||||
<el-button type="primary" @click="createTicket">创建</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</AdminLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue';
|
||||
import { ref, computed } from 'vue'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { Plus, Search } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const statusFilter = ref('all')
|
||||
const typeFilter = ref('')
|
||||
const urgencyFilter = ref('')
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const showCreateDialog = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
customerName: '',
|
||||
customerPhone: '',
|
||||
deviceModel: '',
|
||||
description: ''
|
||||
})
|
||||
|
||||
const tickets = ref([
|
||||
{ ticketNo: 'TK001', customerName: '张三', customerPhone: '13800138000', faultType: 'electrical', faultTypeName: '电气系统故障', deviceModel: 'TH-500GF', urgency: 'urgent', urgencyName: '紧急', status: 'pending', statusName: '待处理', assignee: '', createTime: '2024-12-13 10:30' },
|
||||
{ ticketNo: 'TK002', customerName: '李四', customerPhone: '13800138001', faultType: 'mechanical', faultTypeName: '机械故障', deviceModel: 'TH-300D', urgency: 'normal', urgencyName: '普通', status: 'processing', statusName: '处理中', assignee: '王五', createTime: '2024-12-13 09:15' },
|
||||
{ ticketNo: 'TK003', customerName: '王五', customerPhone: '13800138002', faultType: 'control', faultTypeName: '控制系统故障', deviceModel: 'S-200X', urgency: 'low', urgencyName: '低', status: 'completed', statusName: '已完成', assignee: '赵六', createTime: '2024-12-12 14:20' },
|
||||
{ ticketNo: 'TK004', customerName: '赵六', customerPhone: '13800138003', faultType: 'parts', faultTypeName: '配件更换', deviceModel: 'TH-800GF', urgency: 'normal', urgencyName: '普通', status: 'pending', statusName: '待处理', assignee: '', createTime: '2024-12-13 11:00' },
|
||||
{ ticketNo: 'TK005', customerName: '孙七', customerPhone: '13800138004', faultType: 'install', faultTypeName: '安装调试', deviceModel: 'G-100S', urgency: 'urgent', urgencyName: '紧急', status: 'processing', statusName: '处理中', assignee: '李四', createTime: '2024-12-13 08:45' }
|
||||
])
|
||||
|
||||
const filteredTickets = computed(() => {
|
||||
let result = tickets.value
|
||||
if (statusFilter.value !== 'all') {
|
||||
result = result.filter(t => t.status === statusFilter.value)
|
||||
}
|
||||
if (typeFilter.value) {
|
||||
result = result.filter(t => t.faultType === typeFilter.value)
|
||||
}
|
||||
if (urgencyFilter.value) {
|
||||
result = result.filter(t => t.urgency === urgencyFilter.value)
|
||||
}
|
||||
if (searchKeyword.value) {
|
||||
const keyword = searchKeyword.value.toLowerCase()
|
||||
result = result.filter(t =>
|
||||
t.ticketNo.toLowerCase().includes(keyword) ||
|
||||
t.customerName.toLowerCase().includes(keyword) ||
|
||||
t.deviceModel.toLowerCase().includes(keyword)
|
||||
)
|
||||
}
|
||||
return result.slice((currentPage.value - 1) * 10, currentPage.value * 10)
|
||||
})
|
||||
|
||||
const getStatusType = (status: string) => {
|
||||
const map: Record<string, string> = {
|
||||
pending: 'warning',
|
||||
processing: 'info',
|
||||
completed: 'success',
|
||||
closed: 'danger'
|
||||
}
|
||||
return map[status] || 'info'
|
||||
}
|
||||
|
||||
const handleFilterChange = () => {
|
||||
currentPage.value = 1
|
||||
}
|
||||
|
||||
const viewDetail = (row: any) => {
|
||||
ElMessage.info(`查看工单详情: ${row.ticketNo}`)
|
||||
}
|
||||
|
||||
const assignTicket = (row: any) => {
|
||||
ElMessage.info(`指派工单: ${row.ticketNo}`)
|
||||
}
|
||||
|
||||
const completeTicket = (row: any) => {
|
||||
ElMessage.success(`完成工单: ${row.ticketNo}`)
|
||||
}
|
||||
|
||||
const createTicket = () => {
|
||||
if (!formData.value.customerName || !formData.value.customerPhone || !formData.value.deviceModel) {
|
||||
ElMessage.error('请填写必填项')
|
||||
return
|
||||
}
|
||||
ElMessage.success('工单创建成功')
|
||||
showCreateDialog.value = false
|
||||
formData.value = { customerName: '', customerPhone: '', deviceModel: '', description: '' }
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import url("./WorkcaseView.scss");
|
||||
@import url("./WorkcaseView.scss");
|
||||
|
||||
.workcase-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.customer-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.customer-info .name {
|
||||
font-weight: 500;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.customer-info .phone {
|
||||
font-size: 12px;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
.ticket-no {
|
||||
color: #409eff;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user