overview统计

This commit is contained in:
2026-01-01 16:19:55 +08:00
parent eb15706ccc
commit b53faca120
17 changed files with 425 additions and 40 deletions

View File

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

View File

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

View File

@@ -139,6 +139,7 @@ $brand-color-hover: #004488;
.product-cloud {
display: flex;
justify-content: center;
flex-wrap: wrap;
gap: 8px 12px;
padding: 8px 0;

View File

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

View File

@@ -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>