overview统计
This commit is contained in:
@@ -181,5 +181,25 @@ export const workcaseAPI = {
|
||||
async getWorkcaseDevicePage(pageRequest: PageRequest<TbWorkcaseDeviceDTO>): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
|
||||
const response = await api.post<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device/page`, pageRequest)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ========================= 工单统计 =========================
|
||||
|
||||
/**
|
||||
* 查询工单问题分类统计
|
||||
* @param filter 筛选条件(startTime, endTime)
|
||||
*/
|
||||
async countWorkcasesByType(filter: TbWorkcaseDTO): Promise<ResultDomain<TbWorkcaseDTO>> {
|
||||
const response = await api.post<TbWorkcaseDTO>(`${this.baseUrl}/category/count`, filter)
|
||||
return response.data
|
||||
},
|
||||
|
||||
/**
|
||||
* 统计工单数量
|
||||
* @param filter 筛选条件(status等)
|
||||
*/
|
||||
async countWorkcases(filter: TbWorkcaseDTO): Promise<ResultDomain<number>> {
|
||||
const response = await api.post<number>(`${this.baseUrl}/count`, filter)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,5 +312,16 @@ export const workcaseChatAPI = {
|
||||
Object.keys(body).length > 0 ? body : {}
|
||||
)
|
||||
return response.data
|
||||
},
|
||||
|
||||
// ====================== 统计接口 ======================
|
||||
|
||||
/**
|
||||
* 统计聊天室数量
|
||||
* @param filter 筛选条件(startTime, endTime, status等)
|
||||
*/
|
||||
async countChatRooms(filter: TbChatRoomDTO): Promise<ResultDomain<number>> {
|
||||
const response = await api.post<number>(`${this.baseUrl}/room/count`, filter)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +139,7 @@ $brand-color-hover: #004488;
|
||||
|
||||
.product-cloud {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 12px;
|
||||
padding: 8px 0;
|
||||
|
||||
@@ -1 +1,31 @@
|
||||
// OverviewView 样式占位符
|
||||
// OverviewView 样式
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 60px 20px;
|
||||
color: #909399;
|
||||
|
||||
p {
|
||||
margin-top: 16px;
|
||||
font-size: 14px;
|
||||
color: #909399;
|
||||
}
|
||||
}
|
||||
|
||||
.question-stats {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.product-cloud {
|
||||
min-height: 150px;
|
||||
}
|
||||
|
||||
// 趋势样式
|
||||
.stat-trend {
|
||||
&.down {
|
||||
color: #f56c6c !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@
|
||||
<el-icon><ChatDotRound /></el-icon>
|
||||
</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-value">1,258</div>
|
||||
<div class="stat-value">{{ dashboardData.consultCount }}</div>
|
||||
<div class="stat-label">咨询次数</div>
|
||||
<div class="stat-trend up">
|
||||
<el-icon><Top /></el-icon>
|
||||
<span>较昨日 +12.5%</span>
|
||||
<div class="stat-trend" :class="dashboardData.consultTrend >= 0 ? 'up' : 'down'">
|
||||
<el-icon v-if="dashboardData.consultTrend >= 0"><Top /></el-icon>
|
||||
<el-icon v-else><Top style="transform: rotate(180deg);" /></el-icon>
|
||||
<span>较昨日 {{ dashboardData.consultTrend >= 0 ? '+' : '' }}{{ dashboardData.consultTrend }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,15 +61,21 @@
|
||||
</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 v-if="questionCategories.length > 0">
|
||||
<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>
|
||||
<div v-else class="empty-state">
|
||||
<el-icon :size="48" style="color: #dcdfe6;"><Document /></el-icon>
|
||||
<p>暂无数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
@@ -83,10 +90,16 @@
|
||||
</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 v-if="productCloudData.length > 0">
|
||||
<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>
|
||||
<div v-else class="empty-state">
|
||||
<el-icon :size="48" style="color: #dcdfe6;"><Document /></el-icon>
|
||||
<p>暂无数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
@@ -120,49 +133,215 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import AdminLayout from '@/views/admin/AdminLayout.vue'
|
||||
import { MessageCircle as ChatDotRound, Clock, CheckSquare as Select, ArrowUp as Top, ArrowRight as Right, FileText as Document, Ticket as Tickets, Headphones as Service } from 'lucide-vue-next'
|
||||
import { workcaseAPI } from '@/api/workcase/workcase'
|
||||
import { workcaseChatAPI } from '@/api/workcase/workcaseChat'
|
||||
import type { TbWorkcaseDTO, TbWordCloudDTO, TbChatRoomDTO } from '@/types/workcase'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const questionStatPeriod = ref('today')
|
||||
|
||||
// 核心数据统计
|
||||
const dashboardData = ref({
|
||||
pendingTickets: 24,
|
||||
completedTickets: 856
|
||||
consultCount: 0, // 咨询次数(昨日聊天室数量)
|
||||
consultTrend: 0, // 较前日的增减百分比
|
||||
pendingTickets: 0, // 待处理工单总量
|
||||
completedTickets: 0 // 已处理工单总量
|
||||
})
|
||||
|
||||
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 questionCategories = ref<Array<{ name: string; count: number; percent: number; color: string }>>([])
|
||||
|
||||
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 productCloudData = ref<Array<{ name: string; size: number; weight: number; color: string }>>([])
|
||||
|
||||
// 颜色配置
|
||||
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399']
|
||||
|
||||
// 获取昨日和前日的时间范围
|
||||
const getYesterdayRange = () => {
|
||||
const today = new Date()
|
||||
today.setHours(0, 0, 0, 0)
|
||||
|
||||
const yesterday = new Date(today)
|
||||
yesterday.setDate(today.getDate() - 1)
|
||||
|
||||
const yesterdayEnd = new Date(today)
|
||||
yesterdayEnd.setMilliseconds(-1)
|
||||
|
||||
const dayBeforeYesterday = new Date(yesterday)
|
||||
dayBeforeYesterday.setDate(yesterday.getDate() - 1)
|
||||
|
||||
const dayBeforeYesterdayEnd = new Date(yesterday)
|
||||
dayBeforeYesterdayEnd.setMilliseconds(-1)
|
||||
|
||||
return {
|
||||
yesterday: { start: yesterday, end: yesterdayEnd },
|
||||
dayBeforeYesterday: { start: dayBeforeYesterday, end: dayBeforeYesterdayEnd }
|
||||
}
|
||||
}
|
||||
|
||||
// 根据时间周期计算开始和结束时间
|
||||
const getTimeRange = (period: string) => {
|
||||
const now = new Date()
|
||||
const endTime = now
|
||||
const startTime = new Date()
|
||||
|
||||
if (period === 'today') {
|
||||
startTime.setHours(0, 0, 0, 0)
|
||||
} else if (period === 'week') {
|
||||
startTime.setDate(now.getDate() - 7)
|
||||
} else if (period === 'month') {
|
||||
startTime.setMonth(now.getMonth() - 1)
|
||||
}
|
||||
|
||||
return { startTime, endTime }
|
||||
}
|
||||
|
||||
// 加载核心数据统计
|
||||
const loadDashboardData = async () => {
|
||||
try {
|
||||
// 1. 统计昨日和前日的咨询次数(使用count接口)
|
||||
const timeRanges = getYesterdayRange()
|
||||
|
||||
const yesterdayResult = await workcaseChatAPI.countChatRooms({
|
||||
startTime: timeRanges.yesterday.start,
|
||||
endTime: timeRanges.yesterday.end
|
||||
} as TbChatRoomDTO)
|
||||
|
||||
const dayBeforeResult = await workcaseChatAPI.countChatRooms({
|
||||
startTime: timeRanges.dayBeforeYesterday.start,
|
||||
endTime: timeRanges.dayBeforeYesterday.end
|
||||
} as TbChatRoomDTO)
|
||||
|
||||
if (yesterdayResult.success && dayBeforeResult.success) {
|
||||
const yesterdayCount = Number(yesterdayResult.data || 0)
|
||||
const dayBeforeCount = Number(dayBeforeResult.data || 0)
|
||||
|
||||
dashboardData.value.consultCount = yesterdayCount
|
||||
|
||||
// 计算增减百分比
|
||||
if (dayBeforeCount > 0) {
|
||||
const change = ((yesterdayCount - dayBeforeCount) / dayBeforeCount) * 100
|
||||
dashboardData.value.consultTrend = Math.round(change * 10) / 10 // 保留1位小数
|
||||
} else {
|
||||
dashboardData.value.consultTrend = yesterdayCount > 0 ? 100 : 0
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 统计待处理工单总量(使用count接口)
|
||||
const pendingResult = await workcaseAPI.countWorkcases({
|
||||
status: 'pending'
|
||||
} as TbWorkcaseDTO)
|
||||
|
||||
if (pendingResult.success && pendingResult.data) {
|
||||
dashboardData.value.pendingTickets = Number(pendingResult.data || 0)
|
||||
}
|
||||
|
||||
// 3. 统计已处理工单总量(使用count接口)
|
||||
const completedResult = await workcaseAPI.countWorkcases({
|
||||
status: 'done'
|
||||
} as TbWorkcaseDTO)
|
||||
|
||||
if (completedResult.success && completedResult.data) {
|
||||
dashboardData.value.completedTickets = Number(completedResult.data || 0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载核心数据统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载问题分类统计
|
||||
const loadQuestionStats = async () => {
|
||||
try {
|
||||
const { startTime, endTime } = getTimeRange(questionStatPeriod.value)
|
||||
|
||||
const result = await workcaseAPI.countWorkcasesByType({
|
||||
startTime,
|
||||
endTime
|
||||
} as TbWorkcaseDTO)
|
||||
|
||||
if (result.success && result.data) {
|
||||
const data = Array.isArray(result.data) ? result.data : [result.data]
|
||||
|
||||
// 计算总数
|
||||
const total = data.reduce((sum, item) => sum + (item.count || 0), 0)
|
||||
|
||||
// 转换数据格式并计算百分比
|
||||
questionCategories.value = data.map((item, index) => ({
|
||||
name: item.type || '未分类',
|
||||
count: Number(item.count || 0),
|
||||
percent: total > 0 ? Math.round((Number(item.count || 0) / total) * 100) : 0,
|
||||
color: colors[index % colors.length]
|
||||
}))
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载问题分类统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载词云数据
|
||||
const loadWordCloud = async () => {
|
||||
try {
|
||||
const result = await workcaseChatAPI.getWordCloudList({
|
||||
limit: 10,
|
||||
category: 'product'
|
||||
} as TbWordCloudDTO)
|
||||
|
||||
if (result.success && result.data) {
|
||||
const data = Array.isArray(result.data) ? result.data : [result.data]
|
||||
|
||||
// 找出最大词频用于归一化
|
||||
const maxFreq = Math.max(...data.map(item => Number(item.frequency || 0)))
|
||||
|
||||
// 转换数据格式
|
||||
productCloudData.value = data.map((item, index) => {
|
||||
const freq = Number(item.frequency || 0)
|
||||
const weight = maxFreq > 0 ? freq / maxFreq : 0
|
||||
|
||||
return {
|
||||
name: item.word || '',
|
||||
size: 16 + Math.round(weight * 10), // 16-26px
|
||||
weight,
|
||||
color: colors[index % colors.length]
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载词云数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听时间周期变化
|
||||
watch(questionStatPeriod, () => {
|
||||
loadQuestionStats()
|
||||
})
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
loadDashboardData()
|
||||
loadQuestionStats()
|
||||
loadWordCloud()
|
||||
})
|
||||
|
||||
const goToChat = () => {
|
||||
console.log('跳转到对话数据')
|
||||
router.push('/admin/customerChat')
|
||||
}
|
||||
|
||||
const goToWorkcase = (status?: string) => {
|
||||
console.log('跳转到工单管理', status)
|
||||
router.push('/admin/workcase')
|
||||
}
|
||||
|
||||
const goToKnowledge = () => {
|
||||
console.log('跳转到知识库管理')
|
||||
router.push('/admin/knowledge')
|
||||
}
|
||||
|
||||
const goToAgent = () => {
|
||||
console.log('跳转到智能体管理')
|
||||
router.push('/admin/agent')
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user