overview统计
This commit is contained in:
@@ -72,6 +72,14 @@ public interface ChatRoomService {
|
|||||||
*/
|
*/
|
||||||
ResultDomain<ChatRoomVO> getChatRoomPage(PageRequest<TbChatRoomDTO> pageRequest, String userId);
|
ResultDomain<ChatRoomVO> getChatRoomPage(PageRequest<TbChatRoomDTO> pageRequest, String userId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 统计聊天室数量
|
||||||
|
* @param filter 筛选条件
|
||||||
|
* @author yslg
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
ResultDomain<Long> countChatRooms(TbChatRoomDTO filter);
|
||||||
|
|
||||||
// ========================= 聊天室成员管理 ==========================
|
// ========================= 聊天室成员管理 ==========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.xyzh.api.workcase.service;
|
||||||
|
|
||||||
|
import org.xyzh.api.workcase.dto.TbWordCloudDTO;
|
||||||
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
|
import org.xyzh.common.core.page.PageRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 词云服务接口
|
||||||
|
* @filename WordCloudService.java
|
||||||
|
* @author yslg
|
||||||
|
* @copyright xyzh
|
||||||
|
* @since 2025-12-19
|
||||||
|
*/
|
||||||
|
public interface WordCloudService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询词云列表
|
||||||
|
* @param filter 筛选条件
|
||||||
|
* @return 词云列表
|
||||||
|
*/
|
||||||
|
ResultDomain<TbWordCloudDTO> getWordCloudList(TbWordCloudDTO filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页查询词云
|
||||||
|
* @param pageRequest 分页请求
|
||||||
|
* @return 词云分页数据
|
||||||
|
*/
|
||||||
|
ResultDomain<TbWordCloudDTO> getWordCloudPage(PageRequest<TbWordCloudDTO> pageRequest);
|
||||||
|
}
|
||||||
@@ -55,6 +55,22 @@ public interface WorkcaseService {
|
|||||||
*/
|
*/
|
||||||
ResultDomain<TbWorkcaseDTO> getWorkcasePage(PageRequest<TbWorkcaseDTO> pageRequest);
|
ResultDomain<TbWorkcaseDTO> getWorkcasePage(PageRequest<TbWorkcaseDTO> pageRequest);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 统计各个类型的工单数量
|
||||||
|
* @param filter
|
||||||
|
* @author yslg
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
ResultDomain<TbWorkcaseDTO> countWorkcasesByType(TbWorkcaseDTO filter);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description 统计工单数量
|
||||||
|
* @param filter
|
||||||
|
* @author yslg
|
||||||
|
* @since 2026-01-01
|
||||||
|
*/
|
||||||
|
ResultDomain<Long> countWorkcases(TbWorkcaseDTO filter);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description 获取工单详情
|
* @description 获取工单详情
|
||||||
* @param workcaseId
|
* @param workcaseId
|
||||||
|
|||||||
@@ -50,6 +50,9 @@ public class BaseDTO implements Serializable {
|
|||||||
@Schema(description = "数量限制")
|
@Schema(description = "数量限制")
|
||||||
private Integer limit;
|
private Integer limit;
|
||||||
|
|
||||||
|
@Schema(description = "统计数量")
|
||||||
|
private Integer count;
|
||||||
|
|
||||||
@Schema(description = "开始时间")
|
@Schema(description = "开始时间")
|
||||||
private Date startTime;
|
private Date startTime;
|
||||||
|
|
||||||
|
|||||||
@@ -171,6 +171,13 @@ public class WorkcaseChatController {
|
|||||||
return chatRoomService.getChatRoomPage(pageRequest, userId);
|
return chatRoomService.getChatRoomPage(pageRequest, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "统计聊天室数量")
|
||||||
|
@PreAuthorize("hasAuthority('workcase:room:view')")
|
||||||
|
@PostMapping("/room/count")
|
||||||
|
public ResultDomain<Long> countChatRooms(@RequestBody TbChatRoomDTO filter) {
|
||||||
|
return chatRoomService.countChatRooms(filter);
|
||||||
|
}
|
||||||
|
|
||||||
// ========================= ChatRoom成员管理 =========================
|
// ========================= ChatRoom成员管理 =========================
|
||||||
|
|
||||||
@Operation(summary = "添加聊天室成员")
|
@Operation(summary = "添加聊天室成员")
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import org.xyzh.common.auth.utils.LoginUtil;
|
|||||||
import org.xyzh.common.core.domain.LoginDomain;
|
import org.xyzh.common.core.domain.LoginDomain;
|
||||||
import org.xyzh.common.core.domain.ResultDomain;
|
import org.xyzh.common.core.domain.ResultDomain;
|
||||||
import org.xyzh.common.core.page.PageRequest;
|
import org.xyzh.common.core.page.PageRequest;
|
||||||
|
import org.xyzh.common.utils.validation.ValidationParam;
|
||||||
import org.xyzh.common.utils.validation.ValidationResult;
|
import org.xyzh.common.utils.validation.ValidationResult;
|
||||||
import org.xyzh.common.utils.validation.ValidationUtils;
|
import org.xyzh.common.utils.validation.ValidationUtils;
|
||||||
|
|
||||||
@@ -26,6 +27,8 @@ import com.alibaba.fastjson2.JSONObject;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,6 +119,49 @@ public class WorkcaseController {
|
|||||||
return workcaseService.getWorkcasePage(pageRequest);
|
return workcaseService.getWorkcasePage(pageRequest);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询工单问题统计")
|
||||||
|
@PreAuthorize("hasAuthority('workcase:ticket:view')")
|
||||||
|
@PostMapping("/category/count")
|
||||||
|
public ResultDomain<TbWorkcaseDTO> countWorkcasesByType(@RequestBody TbWorkcaseDTO workcase) {
|
||||||
|
ValidationResult vr = ValidationUtils.validate(workcase, Arrays.asList(
|
||||||
|
ValidationParam.builder()
|
||||||
|
.fieldName("startTime")
|
||||||
|
.fieldLabel("统计开始时间")
|
||||||
|
.required()
|
||||||
|
.build(),
|
||||||
|
// 校验结束时间不为空
|
||||||
|
ValidationParam.builder()
|
||||||
|
.fieldName("endTime")
|
||||||
|
.fieldLabel("统计结束时间")
|
||||||
|
.required()
|
||||||
|
.build(),
|
||||||
|
// 校验开始时间小于结束时间(使用 fieldCompare 比较两个字段)
|
||||||
|
ValidationUtils.fieldCompare(
|
||||||
|
"startTime",
|
||||||
|
"endTime",
|
||||||
|
"统计时间",
|
||||||
|
(startTime, endTime) -> {
|
||||||
|
if (startTime instanceof Date && endTime instanceof Date) {
|
||||||
|
return ((Date) startTime).before((Date) endTime);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
"统计开始时间不能晚于结束时间"
|
||||||
|
)
|
||||||
|
));
|
||||||
|
if (!vr.isValid()) {
|
||||||
|
return ResultDomain.failure(vr.getAllErrors());
|
||||||
|
}
|
||||||
|
return workcaseService.countWorkcasesByType(workcase);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "统计工单数量")
|
||||||
|
@PreAuthorize("hasAuthority('workcase:ticket:view')")
|
||||||
|
@PostMapping("/count")
|
||||||
|
public ResultDomain<Long> countWorkcases(@RequestBody TbWorkcaseDTO workcase) {
|
||||||
|
return workcaseService.countWorkcases(workcase);
|
||||||
|
}
|
||||||
|
|
||||||
// ========================= CRM同步接口 =========================
|
// ========================= CRM同步接口 =========================
|
||||||
|
|
||||||
@Operation(summary = "同步工单到CRM")
|
@Operation(summary = "同步工单到CRM")
|
||||||
|
|||||||
@@ -52,4 +52,6 @@ public interface TbWorkcaseMapper {
|
|||||||
*/
|
*/
|
||||||
long countWorkcases(@Param("filter") TbWorkcaseDTO filter);
|
long countWorkcases(@Param("filter") TbWorkcaseDTO filter);
|
||||||
|
|
||||||
|
List<TbWorkcaseDTO> countWorkcasesByType(@Param("filter") TbWorkcaseDTO filter);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,12 @@ public class ChatRoomServiceImpl implements ChatRoomService {
|
|||||||
return ResultDomain.success("查询聊天室成功", pageDomain);
|
return ResultDomain.success("查询聊天室成功", pageDomain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<Long> countChatRooms(TbChatRoomDTO filter) {
|
||||||
|
long count = chatRoomMapper.countChatRooms(filter);
|
||||||
|
return ResultDomain.success("查询成功", count);
|
||||||
|
}
|
||||||
|
|
||||||
// ========================= 聊天室成员管理 ==========================
|
// ========================= 聊天室成员管理 ==========================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -227,6 +227,18 @@ public class WorkcaseServiceImpl implements WorkcaseService {
|
|||||||
return ResultDomain.success("查询成功", pageDomain);
|
return ResultDomain.success("查询成功", pageDomain);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<TbWorkcaseDTO> countWorkcasesByType(TbWorkcaseDTO filter) {
|
||||||
|
List<TbWorkcaseDTO> workcases = workcaseMapper.countWorkcasesByType(filter);
|
||||||
|
return ResultDomain.success("查询成功", workcases);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ResultDomain<Long> countWorkcases(TbWorkcaseDTO filter) {
|
||||||
|
long count = workcaseMapper.countWorkcases(filter);
|
||||||
|
return ResultDomain.success("查询成功", count);
|
||||||
|
}
|
||||||
|
|
||||||
// ====================== 同步到CRM和接收 ===================
|
// ====================== 同步到CRM和接收 ===================
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -158,6 +158,9 @@
|
|||||||
<if test="filter.status != null and filter.status != ''">AND status = #{filter.status}</if>
|
<if test="filter.status != null and filter.status != ''">AND status = #{filter.status}</if>
|
||||||
<if test="filter.guestId != null and filter.guestId != ''">AND guest_id = #{filter.guestId}</if>
|
<if test="filter.guestId != null and filter.guestId != ''">AND guest_id = #{filter.guestId}</if>
|
||||||
<if test="filter.guestName != null and filter.guestName != ''">AND guest_name LIKE CONCAT('%', #{filter.guestName}, '%')</if>
|
<if test="filter.guestName != null and filter.guestName != ''">AND guest_name LIKE CONCAT('%', #{filter.guestName}, '%')</if>
|
||||||
|
<if test="filter.startTime != null and filter.endTime != null">
|
||||||
|
AND create_time BETWEEN #{filter.startTime} AND #{filter.endTime}
|
||||||
|
</if>
|
||||||
AND deleted = false
|
AND deleted = false
|
||||||
</where>
|
</where>
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
@@ -83,6 +83,9 @@
|
|||||||
</if>
|
</if>
|
||||||
</where>
|
</where>
|
||||||
ORDER BY frequency DESC, create_time DESC
|
ORDER BY frequency DESC, create_time DESC
|
||||||
|
<if test="filter.limit != null and filter.limit > 0">
|
||||||
|
LIMIT #{filter.limit}
|
||||||
|
</if>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<select id="selectWordCloudPage" resultMap="BaseResultMap">
|
<select id="selectWordCloudPage" resultMap="BaseResultMap">
|
||||||
|
|||||||
@@ -207,4 +207,13 @@
|
|||||||
</where>
|
</where>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<select id="countWorkcasesByType" resultType="org.xyzh.api.workcase.dto.TbWorkcaseDTO">
|
||||||
|
SELECT type, COUNT(*) as count
|
||||||
|
FROM workcase.tb_workcase
|
||||||
|
WHERE create_time BETWEEN #{filter.startTime} AND #{filter.endTime}
|
||||||
|
AND deleted = false
|
||||||
|
GROUP BY type
|
||||||
|
ORDER BY count DESC
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -181,5 +181,25 @@ export const workcaseAPI = {
|
|||||||
async getWorkcaseDevicePage(pageRequest: PageRequest<TbWorkcaseDeviceDTO>): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
|
async getWorkcaseDevicePage(pageRequest: PageRequest<TbWorkcaseDeviceDTO>): Promise<ResultDomain<TbWorkcaseDeviceDTO>> {
|
||||||
const response = await api.post<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device/page`, pageRequest)
|
const response = await api.post<TbWorkcaseDeviceDTO>(`${this.baseUrl}/device/page`, pageRequest)
|
||||||
return response.data
|
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 : {}
|
Object.keys(body).length > 0 ? body : {}
|
||||||
)
|
)
|
||||||
return response.data
|
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 {
|
.product-cloud {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px 12px;
|
gap: 8px 12px;
|
||||||
padding: 8px 0;
|
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>
|
<el-icon><ChatDotRound /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-info">
|
<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-label">咨询次数</div>
|
||||||
<div class="stat-trend up">
|
<div class="stat-trend" :class="dashboardData.consultTrend >= 0 ? 'up' : 'down'">
|
||||||
<el-icon><Top /></el-icon>
|
<el-icon v-if="dashboardData.consultTrend >= 0"><Top /></el-icon>
|
||||||
<span>较昨日 +12.5%</span>
|
<el-icon v-else><Top style="transform: rotate(180deg);" /></el-icon>
|
||||||
|
<span>较昨日 {{ dashboardData.consultTrend >= 0 ? '+' : '' }}{{ dashboardData.consultTrend }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -60,15 +61,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="question-stats">
|
<div class="question-stats">
|
||||||
<div v-for="item in questionCategories" :key="item.name" class="question-stat-item clickable">
|
<div v-if="questionCategories.length > 0">
|
||||||
<div class="stat-bar-header">
|
<div v-for="item in questionCategories" :key="item.name" class="question-stat-item clickable">
|
||||||
<span class="stat-name">{{ item.name }}</span>
|
<div class="stat-bar-header">
|
||||||
<span class="stat-count">{{ item.count }} 次</span>
|
<span class="stat-name">{{ item.name }}</span>
|
||||||
</div>
|
<span class="stat-count">{{ item.count }} 次</span>
|
||||||
<div class="stat-bar">
|
</div>
|
||||||
<div class="stat-bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
<div class="stat-bar">
|
||||||
|
<div class="stat-bar-fill" :style="{ width: item.percent + '%', background: item.color }"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<el-icon :size="48" style="color: #dcdfe6;"><Document /></el-icon>
|
||||||
|
<p>暂无数据</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@@ -83,10 +90,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div class="product-cloud">
|
<div class="product-cloud">
|
||||||
<span v-for="(product, index) in productCloudData" :key="index" class="cloud-tag"
|
<div v-if="productCloudData.length > 0">
|
||||||
:style="{ fontSize: product.size + 'px', color: product.color, opacity: 0.7 + product.weight * 0.3 }">
|
<span v-for="(product, index) in productCloudData" :key="index" class="cloud-tag"
|
||||||
{{ product.name }}
|
:style="{ fontSize: product.size + 'px', color: product.color, opacity: 0.7 + product.weight * 0.3 }">
|
||||||
</span>
|
{{ product.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="empty-state">
|
||||||
|
<el-icon :size="48" style="color: #dcdfe6;"><Document /></el-icon>
|
||||||
|
<p>暂无数据</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,49 +133,215 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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 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 { 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 questionStatPeriod = ref('today')
|
||||||
|
|
||||||
|
// 核心数据统计
|
||||||
const dashboardData = ref({
|
const dashboardData = ref({
|
||||||
pendingTickets: 24,
|
consultCount: 0, // 咨询次数(昨日聊天室数量)
|
||||||
completedTickets: 856
|
consultTrend: 0, // 较前日的增减百分比
|
||||||
|
pendingTickets: 0, // 待处理工单总量
|
||||||
|
completedTickets: 0 // 已处理工单总量
|
||||||
})
|
})
|
||||||
|
|
||||||
const questionCategories = ref([
|
// 问题分类统计数据
|
||||||
{ name: '设备故障', count: 156, percent: 85, color: '#409eff' },
|
const questionCategories = ref<Array<{ name: string; count: number; percent: number; color: string }>>([])
|
||||||
{ 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' },
|
const productCloudData = ref<Array<{ name: string; size: number; weight: number; color: string }>>([])
|
||||||
{ 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' },
|
const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399']
|
||||||
{ 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 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 = () => {
|
const goToChat = () => {
|
||||||
console.log('跳转到对话数据')
|
router.push('/admin/customerChat')
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToWorkcase = (status?: string) => {
|
const goToWorkcase = (status?: string) => {
|
||||||
console.log('跳转到工单管理', status)
|
router.push('/admin/workcase')
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToKnowledge = () => {
|
const goToKnowledge = () => {
|
||||||
console.log('跳转到知识库管理')
|
router.push('/admin/knowledge')
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToAgent = () => {
|
const goToAgent = () => {
|
||||||
console.log('跳转到智能体管理')
|
router.push('/admin/agent')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user