feat(团队分析): 添加团队整体三阶分析报告功能

- 在api/manager.js中添加获取团队分析报告的接口
- 在TeamReport组件中添加分析按钮并触发事件
- 在manager.vue中实现团队分析弹窗及相关逻辑
- 添加样式美化分析报告展示
This commit is contained in:
2025-11-25 17:53:54 +08:00
parent 2ba88eff08
commit b48b7749f2
3 changed files with 405 additions and 6 deletions

View File

@@ -54,3 +54,8 @@ export const getMemberCallClassify = (params) => {
return https.post('/api/v1/manager/get_member_call_classify', params) return https.post('/api/v1/manager/get_member_call_classify', params)
} }
// 团队整体三阶分析报告 /api/v1/manager/group_entirety_third_report
export const getGroupEntiretyThirdReport = (params) => {
return https.post('/api/v1/manager/group_entirety_third_report', params)
}

View File

@@ -1,6 +1,9 @@
<template> <template>
<div class="team-report"> <div class="team-report">
<h2>今日团队实时战报</h2> <div class="header-container">
<h2>今日团队实时战报</h2>
<button class="analysis-button" @click="showTeamAnalysis">团队分析</button>
</div>
<div class="report-grid"> <div class="report-grid">
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
@@ -76,6 +79,9 @@ const props = defineProps({
} }
}) })
// 定义emit
const emit = defineEmits(['show-team-analysis'])
// 监听数据变化,用于调试 // 监听数据变化,用于调试
watch(() => props.weekTotalData, (newData) => { watch(() => props.weekTotalData, (newData) => {
console.log('TeamReport 收到的数据:', newData) console.log('TeamReport 收到的数据:', newData)
@@ -146,6 +152,11 @@ const showTooltip = (metricType, event) => {
const hideTooltip = () => { const hideTooltip = () => {
tooltip.visible = false tooltip.visible = false
} }
// 显示团队分析
const showTeamAnalysis = () => {
emit('show-team-analysis')
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -156,11 +167,33 @@ const hideTooltip = () => {
padding: 1.5rem; padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
h2 { h2 {
font-size: 1.2rem; font-size: 1.2rem;
font-weight: 600; font-weight: 600;
color: #1e293b; color: #1e293b;
margin: 0 0 1.5rem 0; margin: 0;
}
.analysis-button {
background: #409eff;
color: white;
border: none;
border-radius: 4px;
padding: 8px 16px;
font-size: 0.9rem;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background: #337ecc;
}
} }
.report-grid { .report-grid {

View File

@@ -38,8 +38,8 @@
<div class="top-section"> <div class="top-section">
<!-- Team Alerts --> <!-- Team Alerts -->
<TeamAlerts :abnormalData="groupAbnormalResponse" /> <TeamAlerts :abnormalData="groupAbnormalResponse" />
<!-- Today's Team Report --> <!-- Today's Team Report -->
<TeamReport :weekTotalData="weekTotalData" /> <TeamReport :weekTotalData="weekTotalData" @show-team-analysis="fetchTeamAnalysis" />
</div> </div>
<!-- Sales Funnel Section --> <!-- Sales Funnel Section -->
@@ -65,6 +65,24 @@
</div> </div>
</main> </main>
</div> </div>
<!-- 团队分析弹窗 -->
<div v-if="showTeamAnalysisModal" class="modal-overlay" @click="showTeamAnalysisModal = false">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>团队整体三阶分析报告</h3>
<button class="close-button" @click="showTeamAnalysisModal = false">×</button>
</div>
<div class="modal-body">
<div v-for="(report, index) in teamAnalysisData" :key="index" class="report-item">
<div class="report-meta">
<span class="time-range">{{ report.start_time }} {{ report.end_time }}</span>
<span class="created-at">生成时间: {{ report.created_at }}</span>
</div>
<div class="report-content" v-html="formatReportContent(report.report)"></div>
</div>
</div>
</div>
</div>
</template> </template>
<script setup> <script setup>
@@ -81,7 +99,7 @@ import CustomerDetail from "../person/components/CustomerDetail.vue";
import { useUserStore } from "@/stores/user"; import { useUserStore } from "@/stores/user";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import {getGroupAbnormalResponse, getWeekTotalCall, getWeekAddCustomerTotal, getWeekAddDealTotal, import {getGroupAbnormalResponse, getWeekTotalCall, getWeekAddCustomerTotal, getWeekAddDealTotal,
getWeekAddFeeTotal, getGroupFunnel,getPayDepositToMoneyRate,getGroupRanking, getGroupCallDuration,getGroupDetail} from "@/api/manager.js"; getWeekAddFeeTotal, getGroupFunnel,getPayDepositToMoneyRate,getGroupRanking, getGroupCallDuration,getGroupDetail, getGroupEntiretyThirdReport} from "@/api/manager.js";
// 团队成员数据 // 团队成员数据
const teamMembers = [ const teamMembers = [
@@ -108,9 +126,10 @@ const userStore = useUserStore();
// 获取通用请求参数的函数 // 获取通用请求参数的函数
const getRequestParams = () => { const getRequestParams = () => {
const params = {} const params = {}
// 从路由参数获取 // 从路由参数获取
const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level const routeUserLevel = router.currentRoute.value.query.user_level || router.currentRoute.value.params.user_level
const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name const routeUserName = router.currentRoute.value.query.user_name || router.currentRoute.value.params.user_name
// 如果路由有参数,使用路由参数 // 如果路由有参数,使用路由参数
if (routeUserLevel) { if (routeUserLevel) {
params.user_level = routeUserLevel.toString() params.user_level = routeUserLevel.toString()
@@ -119,6 +138,14 @@ const getRequestParams = () => {
params.user_name = routeUserName params.user_name = routeUserName
} }
// 如果没有路由参数,使用当前登录用户的信息
if (!params.user_level && userStore.userInfo?.user_level) {
params.user_level = userStore.userInfo.user_level.toString()
}
if (!params.user_name && userStore.userInfo?.username) {
params.user_name = userStore.userInfo.username
}
return params return params
} }
@@ -286,6 +313,10 @@ async function TeamGetGroupRanking() {
const memberDetails = ref({}) const memberDetails = ref({})
// 团队分析数据
const teamAnalysisData = ref([])
const showTeamAnalysisModal = ref(false)
// 当前选中的成员,默认为空 // 当前选中的成员,默认为空
const selectedMember = ref(null); const selectedMember = ref(null);
@@ -318,6 +349,74 @@ week_order_count
} }
} }
// 获取团队分析数据
const fetchTeamAnalysis = async () => {
try {
showTeamAnalysisModal.value = true
const params = {
...getRequestParams(),
part_count: 1
}
const response = await getGroupEntiretyThirdReport(params)
// 根据API响应结构调整数据处理逻辑
if (response.data) {
if (Array.isArray(response.data)) {
// 如果response.data本身就是数组
teamAnalysisData.value = response.data
} else if (response.data.data && Array.isArray(response.data.data)) {
// 如果response.data.data是数组
teamAnalysisData.value = response.data.data
} else {
// 其他情况,可能是单个对象
teamAnalysisData.value = [response.data]
}
}
} catch (error) {
console.error('获取团队分析数据失败:', error)
teamAnalysisData.value = []
}
}
// 格式化报告内容
const formatReportContent = (content) => {
if (!content || content === "None") {
return "<p>暂无分析报告内容</p>";
}
// 处理报告内容,保留换行和基本格式
let formattedContent = content
// 替换连续的换行符
.replace(/\n\s*\n/g, '</p><p>')
// 替换单个换行符为<br>
.replace(/\n/g, '<br>')
// 替换Markdown风格的标题为HTML标签
.replace(/^### (.*?)(<br>|$)/gim, '<h3>$1</h3>')
.replace(/^## (.*?)(<br>|$)/gim, '<h2>$1</h2>')
.replace(/^# (.*?)(<br>|$)/gim, '<h1>$1</h1>')
// 替换粗体
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
// 替换斜体
.replace(/\*(.*?)\*/g, '<em>$1</em>')
// 替换无序列表项
.replace(/^\* (.*?)(<br>|$)/gim, '<li>$1</li>');
// 包装列表项到<ul>标签中
formattedContent = formattedContent.replace(/(<li>.*?<\/li>)+/g, '<ul>$&</ul>');
// 处理段落
if (!formattedContent.startsWith('<p>')) {
formattedContent = '<p>' + formattedContent;
}
if (!formattedContent.endsWith('</p>')) {
formattedContent = formattedContent + '</p>';
}
// 清理多余的<br>标签
formattedContent = formattedContent.replace(/<br><\/p>/g, '</p>');
return formattedContent;
}
// 团队异常预警 // 团队异常预警
@@ -1850,5 +1949,267 @@ onMounted(async () => {
} }
} }
} }
/* 团队分析弹窗样式 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #999;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #333;
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
max-height: calc(90vh - 80px);
}
.report-item {
margin-bottom: 2rem;
}
.report-item:last-child {
margin-bottom: 0;
}
.report-meta {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
font-size: 0.9rem;
color: #666;
}
.report-content {
line-height: 1.6;
}
.report-content :deep(h1),
.report-content :deep(h2),
.report-content :deep(h3),
.report-content :deep(h4),
.report-content :deep(h5),
.report-content :deep(h6) {
margin: 1.5rem 0 1rem 0;
font-weight: 600;
}
.report-content :deep(h1) {
font-size: 1.75rem;
border-bottom: 2px solid #eee;
padding-bottom: 0.5rem;
}
.report-content :deep(h2) {
font-size: 1.5rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.report-content :deep(h3) {
font-size: 1.25rem;
}
.report-content :deep(p) {
margin: 0.75rem 0;
}
.report-content :deep(ul),
.report-content :deep(ol) {
margin: 0.75rem 0;
padding-left: 1.5rem;
}
.report-content :deep(li) {
margin: 0.25rem 0;
}
.report-content :deep(strong) {
font-weight: 600;
}
.report-content :deep(em) {
font-style: italic;
}
}
/* 团队分析弹窗 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
max-width: 90vw;
max-height: 90vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h3 {
margin: 0;
font-size: 1.25rem;
color: #333;
}
.close-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: #999;
padding: 0;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.close-button:hover {
color: #333;
}
.modal-body {
padding: 1.5rem;
overflow-y: auto;
max-height: calc(90vh - 80px);
}
.report-item {
margin-bottom: 2rem;
}
.report-item:last-child {
margin-bottom: 0;
}
.report-meta {
display: flex;
justify-content: space-between;
margin-bottom: 1rem;
font-size: 0.9rem;
color: #666;
}
.report-content {
line-height: 1.6;
}
.report-content :deep(h1),
.report-content :deep(h2),
.report-content :deep(h3),
.report-content :deep(h4),
.report-content :deep(h5),
.report-content :deep(h6) {
margin: 1.5rem 0 1rem 0;
font-weight: 600;
}
.report-content :deep(h1) {
font-size: 1.75rem;
border-bottom: 2px solid #eee;
padding-bottom: 0.5rem;
}
.report-content :deep(h2) {
font-size: 1.5rem;
border-bottom: 1px solid #eee;
padding-bottom: 0.5rem;
}
.report-content :deep(h3) {
font-size: 1.25rem;
}
.report-content :deep(p) {
margin: 0.75rem 0;
}
.report-content :deep(ul),
.report-content :deep(ol) {
margin: 0.75rem 0;
padding-left: 1.5rem;
}
.report-content :deep(li) {
margin: 0.25rem 0;
}
.report-content :deep(strong) {
font-weight: 600;
}
.report-content :deep(em) {
font-style: italic;
} }
</style> </style>