客服后台结构

This commit is contained in:
2025-12-15 10:47:01 +08:00
parent 5667bab5c9
commit 56f398e4ca
11 changed files with 1903 additions and 264 deletions

View File

@@ -0,0 +1,5 @@
package org.xyzh.agent.client;
public class DifyClient {
}

View File

@@ -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));
}
}

View File

@@ -1,39 +1,80 @@
<template> <template>
<div> <div class="admin-layout">
<!-- 统一Admin下的基本样式 --> <!-- 头部 -->
<div> <div class="admin-header">
<!-- 头部 --> <div class="header-left">
<div> <h1 v-if="title" class="header-title">{{ title }}</h1>
<!-- 左侧信息 --> <p v-if="info" class="header-subtitle">{{ info }}</p>
<span>
<!-- title -->
</span>
<span>
<!-- info -->
</span>
</div> </div>
<div> <div class="header-right">
<!-- 右侧按钮 或者文本--> <slot name="action"></slot>
<slot name="action">
</slot>
</div> </div>
</div> </div>
<div> <!-- 内容区 -->
<!-- div控制margin等基本样式 --> <div class="admin-content">
<slot> <slot></slot>
</slot>
</div> </div>
</div> </div>
</template> </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> </script>
<style lang="scss" scoped> <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> </style>

View File

@@ -1,30 +1,221 @@
<template> <template>
<AdminLayout <AdminLayout title="智能体管理" info="配置和管理AI智能客服">
:title="智能体管理" <div class="agent-container">
:info="配置和管理AI智能客服" <el-card>
> <template #header>
<template default> <div class="card-header">
<div class="agent"> <span>AI客服配置</span>
<!-- 智能体管理视图 --> <el-button type="primary" size="small" @click="saveConfig">保存配置</el-button>
<div> </div>
<!-- head部分 --> </template>
</div>
<div>
<!-- 表单部分 -->
</div>
<div>
<!-- 按钮部分 -->
</div>
</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> </AdminLayout>
</template> </template>
<script setup lang="ts"> <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> </script>
<style lang="scss" scoped> <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> </style>

View File

@@ -1,35 +1,245 @@
<template> <template>
<AdminLayout <AdminLayout title="对话数据管理" info="管理和分析客服对话数据">
:title="对话数据管理" <template #action>
:info="管理和分析客服对话数据" <el-button type="primary" @click="exportData">
> <el-icon><Download /></el-icon>
<template action> 导出数据
<ElButton></ElButton> </el-button>
</template> </template>
<template default>
<div class="customer-chat"> <div class="chat-container">
<!-- 客服对话管理视图 --> <!-- 筛选区域 -->
<div> <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> </el-card>
<div>
<!-- 数据列表 --> <!-- 对话列表 -->
</div> <el-card>
<div> <el-table :data="filteredChats" style="width: 100%">
<!-- EL分页组件 全局样式 --> <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>
<div class="message-content">{{ msg.content }}</div>
</div> </div>
</div> </div>
</template> </el-dialog>
</AdminLayout> </AdminLayout>
</template> </template>
<script setup lang="ts"> <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> </script>
<style lang="scss" scoped> <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> </style>

View File

@@ -1,57 +1,247 @@
<template> <template>
<AdminLayout <AdminLayout title="知识库管理" info="管理外部和内部知识库文档">
:title="知识库管理" <template #action>
:info="管理外部和内部知识库文档" <el-button type="primary" @click="showUploadDialog = true">
> <el-icon><Upload /></el-icon>
<template action> 上传文档
<ElButton></ElButton> </el-button>
</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>
</template> </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> </AdminLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ElButton } from 'element-plus'; import { ref, computed } from 'vue'
import AdminLayout from '@/views/admin/AdminLayout.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> </script>
<style lang="scss" scoped> <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> </style>

View File

@@ -1,35 +1,166 @@
<template> <template>
<AdminLayout title="知识库日志" info="查看知识库操作记录">
<AdminLayout <template #action>
:title="知识库日志" <el-button type="primary" @click="exportLogs">
:info="查看知识库操作记录" <el-icon><Download /></el-icon>
> 导出日志
<template action> </el-button>
<ElButton></ElButton>
</template>
<template default>
<div class="knowledge-log">
<!-- 知识库日志管理视图 -->
<div>
<!-- 条件筛选区域 -->
</div>
<div>
<!-- 数据列表区域 -->
</div>
<div>
<!-- EL分页组件 全局样式 -->
</div>
</div>
</template> </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> </AdminLayout>
</template> </template>
<script setup lang="ts"> <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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import url("./KnowledgeLogView.scss"); @import url("./KnowledgeLogView.scss");
.log-container {
display: flex;
flex-direction: column;
gap: 16px;
}
</style> </style>

View File

@@ -1,33 +1,165 @@
<template> <template>
<AdminLayout <AdminLayout title="系统日志" info="查看系统配置变更与异常信息">
:title="系统日志" <template #action>
:info="查看系统配置变更与异常信息" <el-button type="primary" @click="exportLogs">
> <el-icon><Download /></el-icon>
<template action> 导出日志
<ElButton></ElButton> </el-button>
</template>
<template default>
<div class="system-log">
<!-- 系统日志管理视图 -->
<div>
<!-- 条件筛选区域 -->
</div>
<div>
<!-- 数据列表区域 -->
</div>
<div>
<!-- EL分页组件 全局样式 -->
</div>
</div>
</template> </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> </AdminLayout>
</template> </template>
<script setup lang="ts"> <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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import url("./SystemLogView.scss"); @import url("./SystemLogView.scss");
.log-container {
display: flex;
flex-direction: column;
gap: 16px;
}
</style> </style>

View File

@@ -1,34 +1,165 @@
<template> <template>
<AdminLayout <AdminLayout title="工单日志" info="查看工单操作记录">
:title="工单日志" <template #action>
:info="查看工单操作记录" <el-button type="primary" @click="exportLogs">
> <el-icon><Download /></el-icon>
<template action> 导出日志
<ElButton></ElButton> </el-button>
</template>
<template default>
<div class="workcase-log">
<!-- 工单日志管理视图 -->
<div>
<!-- 条件筛选区域 -->
</div>
<div>
<!-- 数据列表区域 -->
</div>
<div>
<!-- EL分页组件 全局样式 -->
</div>
</div>
</template> </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> </AdminLayout>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import AdminLayout from '@/views/admin/AdminLayout.vue'; import { ref, computed } from 'vue'
import { ElInput, ElSelect } from 'element-plus'; 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> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import url("./WorkcaseLogView.scss"); @import url("./WorkcaseLogView.scss");
.log-container {
display: flex;
flex-direction: column;
gap: 16px;
}
</style> </style>

View File

@@ -1,69 +1,189 @@
<template> <template>
<AdminLayout <AdminLayout title="数据概览" info="泰豪小电智能客服运营数据">
:title="数据概览" <div class="overview-container">
> <!-- 核心指标卡片 -->
<template action> <div class="dashboard-stats">
<span>泰豪小电智能客服运营数据</span> <div class="stat-card clickable" @click="goToChat">
</template> <div class="stat-icon" style="background: linear-gradient(135deg, #409eff, #66b1ff);">
<template default> <el-icon><ChatDotRound /></el-icon>
<div>
<!-- 中间区域 -->
<div>
<!-- 信息卡 -->
<div>
<!-- 咨询次数 点击跳转 对话数据 -->
</div> </div>
<div> <div class="stat-info">
<!-- 待处理工单 点击跳转 工单管理选择待处理的选项 --> <div class="stat-value">1,258</div>
</div> <div class="stat-label">咨询次数</div>
<div> <div class="stat-trend up">
<!-- 已处理工单 点击跳转 工单管理选择已处理的选项--> <el-icon><Top /></el-icon>
</div> <span>较昨日 +12.5%</span>
</div>
<div>
<div>
<!-- 问题分类统计 -->
<span></span>
<div>
<!-- 时间范围选择 -->
</div>
</div>
<div>
<!-- 词云 -->
<span></span>
<div>
<!-- 词云图 -->
</div> </div>
</div> </div>
</div> </div>
<div> <div class="stat-card clickable" @click="goToWorkcase('pending')">
<!-- 快捷入口固定在底部 --> <div class="stat-icon" style="background: linear-gradient(135deg, #e6a23c, #f5c06a);">
<span></span> <el-icon><Clock /></el-icon>
<div> </div>
<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 class="stat-card clickable" @click="goToWorkcase('completed')">
<!-- 对话数据 点击跳转 对话数据管理 --> <div class="stat-icon" style="background: linear-gradient(135deg, #67c23a, #95d475);">
</div> <el-icon><Select /></el-icon>
<div> </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>
</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> </AdminLayout>
</template> </template>
<script setup lang="ts"> <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> </script>
<style lang="scss" scoped> <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> </style>

View File

@@ -1,43 +1,250 @@
<template> <template>
<AdminLayout <AdminLayout title="工单管理" info="查看和处理客户服务工单">
:title="工单管理" <template #action>
:info="查看和处理客户服务工单" <el-button type="primary" @click="showCreateDialog = true">
> <el-icon><Plus /></el-icon>
<template action> 创建工单
<ElButton></ElButton> </el-button>
</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>
</template> </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> </AdminLayout>
</template> </template>
<script setup lang="ts"> <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> </script>
<style lang="scss" scoped> <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> </style>