diff --git a/frontend/src/views/Analytics.vue b/frontend/src/views/Analytics.vue index af64971..ebd775b 100644 --- a/frontend/src/views/Analytics.vue +++ b/frontend/src/views/Analytics.vue @@ -1759,27 +1759,50 @@ const getWarningActionText = (type: string) => { // 导出数据为 CSV const exportData = () => { + if (loading.value) { + message.warning('数据正在加载中,请稍后再导出') + return + } + const monthStr = selectedMonth.value || dayjs().format('YYYY-MM') let csvContent = '\uFEFF' // BOM for UTF-8 encoding + + const csvEscape = (value: any) => { + const str = value === null || value === undefined ? '' : String(value) + const escaped = str.replace(/"/g, '""') + return /[",\n\r]/.test(escaped) ? `"${escaped}"` : escaped + } + + const appendRow = (fields: any[]) => { + csvContent += `${fields.map(csvEscape).join(',')}\n` + } // 1. 概览数据 csvContent += '【核心指标概览】\n' - csvContent += '指标,数值\n' - csvContent += `总任务数,${overview.value.totalDemands}\n` - csvContent += `待处理,${overview.value.pendingDemands}\n` - csvContent += `进行中,${inProgressCount.value}\n` - csvContent += `已完成,${overview.value.completedDemands}\n` - csvContent += `完成率,${completionRate.value}%\n` - csvContent += `准时率,${onTimeRate.value}%\n` + appendRow(['指标', '数值']) + appendRow(['总任务数', overview.value.totalDemands]) + appendRow(['待处理', overview.value.pendingDemands]) + appendRow(['进行中', inProgressCount.value]) + appendRow(['已完成', overview.value.completedDemands]) + appendRow(['完成率', `${completionRate.value}%`]) + appendRow(['准时率', `${onTimeRate.value}%`]) csvContent += '\n' // 2. 用户需求统计 if (statistics.userStats && statistics.userStats.length > 0) { csvContent += '【用户需求统计】\n' - csvContent += '排名,用户名,部门,发起数,完成数,总积分,完成率\n' + appendRow(['排名', '用户名', '部门', '发起数', '完成数', '总积分', '完成率']) statistics.userStats.forEach((user: any, index: number) => { const rate = user.totalDemands > 0 ? ((user.completedDemands / user.totalDemands) * 100).toFixed(1) : '0.0' - csvContent += `${index + 1},${user.userName || '-'},${user.deptName || '-'},${user.totalDemands},${user.completedDemands},${formatScore(user.totalScore)},${rate}%\n` + appendRow([ + index + 1, + user.realName || user.userName || '-', + user.deptName || '-', + user.totalDemands, + user.completedDemands, + formatScore(user.totalScore), + `${rate}%` + ]) }) csvContent += '\n' } @@ -1787,10 +1810,16 @@ const exportData = () => { // 3. 部门需求统计 if (statistics.deptStats && statistics.deptStats.length > 0) { csvContent += '【部门需求统计】\n' - csvContent += '排名,部门名称,发起数,完成数,完成率\n' + appendRow(['排名', '部门名称', '发起数', '完成数', '完成率']) statistics.deptStats.forEach((dept: any, index: number) => { const rate = dept.totalDemands > 0 ? ((dept.completedDemands / dept.totalDemands) * 100).toFixed(1) : '0.0' - csvContent += `${index + 1},${dept.deptName || '-'},${dept.totalDemands},${dept.completedDemands},${rate}%\n` + appendRow([ + index + 1, + dept.deptName || '-', + dept.totalDemands, + dept.completedDemands, + `${rate}%` + ]) }) csvContent += '\n' } @@ -1798,28 +1827,234 @@ const exportData = () => { // 4. 设计师绩效 if (designerPerformance.designerStats && designerPerformance.designerStats.length > 0) { csvContent += '【设计师绩效】\n' - csvContent += '排名,设计师,部门,接单量,完成量,获得积分,扣分,净积分,目标积分,目标完成率,准时率,评分\n' + appendRow(['排名', '设计师', '部门', '接单量', '完成量', '获得积分', '扣分', '净积分', '目标积分', '目标完成率', '准时率', '评分']) designerPerformance.designerStats.forEach((designer: any, index: number) => { - csvContent += `${index + 1},${designer.realName || '-'},${designer.deptName || '-'},${designer.receivedCount || 0},${designer.completedCount || 0},${formatScore(designer.totalScore)},${formatScore(designer.deductionScore)},${formatScore(designer.netScore)},${designer.targetScore ? formatScore(designer.targetScore) : '-'},${designer.targetScore > 0 ? (designer.targetCompletionRate || 0) + '%' : '-'},${designer.onTimeRate || 0}%,${designer.avgRating?.toFixed(1) || '-'}\n` + appendRow([ + index + 1, + designer.realName || '-', + designer.deptName || '-', + designer.receivedCount || 0, + designer.completedCount || 0, + formatScore(designer.totalScore), + formatScore(designer.deductionScore), + formatScore(designer.netScore), + designer.targetScore ? formatScore(designer.targetScore) : '-', + designer.targetScore > 0 ? `${designer.targetCompletionRate || 0}%` : '-', + `${designer.onTimeRate || 0}%`, + designer.avgRating?.toFixed(1) || '-' + ]) }) csvContent += '\n' } // 5. 流程效率指标 csvContent += '【流程效率指标】\n' - csvContent += '指标,本周,上周,本月,目标\n' + appendRow(['指标', '本周', '上周', '本月', '目标']) efficiencyTableData.value.forEach((row: any) => { - csvContent += `${row.metric},${row.week},${row.lastWeek},${row.month},${row.target}\n` + appendRow([row.metric, row.week, row.lastWeek, row.month, row.target]) }) csvContent += '\n' // 6. 预警任务 if (warningTasks.value && warningTasks.value.length > 0) { csvContent += '【预警任务】\n' - csvContent += '级别,任务名称,处理人,预警原因\n' + appendRow(['级别', '任务名称', '处理人', '预警原因']) warningTasks.value.forEach((task: any) => { const level = task.warningLevel === 'urgent' ? '紧急' : task.warningLevel === 'warning' ? '警告' : '正常' - csvContent += `${level},${task.taskTitle || '-'},${task.receiverName || '-'},${task.warningReason || '-'}\n` + appendRow([level, task.taskTitle || '-', task.receiverName || '-', task.warningReason || '-']) + }) + csvContent += '\n' + } + + // 7. 多维度分析 + if (dimensionData.categoryAnalysis?.length) { + csvContent += '【类目分析】\n' + appendRow(['类目名称', '任务数', '总数量', '已完成', '进行中', '完成率', '总积分']) + dimensionData.categoryAnalysis.forEach((item: any) => { + appendRow([ + item.categoryName || '-', + item.totalTasks || 0, + item.totalQuantity || 0, + item.completedTasks || 0, + item.inProgressTasks || 0, + `${formatScore(item.completionRate)}%`, + formatScore(item.totalScore) + ]) + }) + csvContent += '\n' + } + + if (dimensionData.brandAnalysis?.length) { + csvContent += '【品牌分析】\n' + appendRow(['品牌名称', '任务数', '总数量', '已完成', '进行中', '完成率', '总积分']) + dimensionData.brandAnalysis.forEach((item: any) => { + appendRow([ + item.brandName || '-', + item.totalTasks || 0, + item.totalQuantity || 0, + item.completedTasks || 0, + item.inProgressTasks || 0, + `${formatScore(item.completionRate)}%`, + formatScore(item.totalScore) + ]) + }) + csvContent += '\n' + } + + if (dimensionData.receivingDeptAnalysis?.length) { + csvContent += '【接单部门分析】\n' + appendRow(['部门名称', '部门人数', '任务数', '人均任务', '总数量', '已完成', '进行中', '完成率', '总积分', '平均耗时']) + dimensionData.receivingDeptAnalysis.forEach((item: any) => { + appendRow([ + item.deptName || '-', + item.memberCount || 0, + item.totalTasks || 0, + formatScore(item.avgTasksPerMember), + item.totalQuantity || 0, + item.completedTasks || 0, + item.inProgressTasks || 0, + `${formatScore(item.completionRate)}%`, + formatScore(item.totalScore), + item.avgCompletionHours ? `${formatScore(item.avgCompletionHours)}h` : '-' + ]) + }) + csvContent += '\n' + } + + if (dimensionData.demandDeptAnalysis?.length) { + csvContent += '【需求部门分析】\n' + appendRow(['部门名称', '部门人数', '任务数', '人均任务', '总数量', '已完成', '待处理', '完成率', '总积分']) + dimensionData.demandDeptAnalysis.forEach((item: any) => { + appendRow([ + item.deptName || '-', + item.memberCount || 0, + item.totalTasks || 0, + formatScore(item.avgTasksPerMember), + item.totalQuantity || 0, + item.completedTasks || 0, + item.pendingTasks || 0, + `${formatScore(item.completionRate)}%`, + formatScore(item.totalScore) + ]) + }) + csvContent += '\n' + } + + if (dimensionData.platformAnalysis?.length) { + csvContent += '【平台分析】\n' + appendRow(['平台名称', '任务数', '总数量', '已完成', '进行中', '完成率', '总积分']) + dimensionData.platformAnalysis.forEach((item: any) => { + appendRow([ + item.platformName || '-', + item.totalTasks || 0, + item.totalQuantity || 0, + item.completedTasks || 0, + item.inProgressTasks || 0, + `${formatScore(item.completionRate)}%`, + formatScore(item.totalScore) + ]) + }) + csvContent += '\n' + } + + const exportTaskTypeAnalysis = (title: string, data: any[]) => { + if (!data?.length) return + csvContent += `【${title}】\n` + appendRow(['父级分类', '类型名称', '任务数', '总数量', '已完成', '进行中', '待接单', '完成率', '准时率', '总积分', '平均积分', '单价分值', '预估工时', '平均完成时长']) + data.forEach((item: any) => { + appendRow([ + item.parentName || '-', + item.typeName || '-', + item.totalTasks || 0, + item.totalQuantity || 0, + item.completedTasks || 0, + item.inProgressTasks || 0, + item.pendingTasks || 0, + `${formatScore(item.completionRate)}%`, + `${formatScore(item.onTimeRate)}%`, + formatScore(item.totalScore), + formatScore(item.avgScore), + formatScore(item.unitScore), + item.estimatedHours ? `${formatScore(item.estimatedHours)}h` : '-', + item.avgCompletionHours ? `${formatScore(item.avgCompletionHours)}h` : '-' + ]) + }) + csvContent += '\n' + } + + exportTaskTypeAnalysis('任务类型分析(一级分类)', dimensionData.taskTypeAnalysis || []) + exportTaskTypeAnalysis('任务类型分析(二级分类)', (dimensionData.taskTypeLevel2Analysis || []).map((item: any) => ({ ...item, parentName: item.parentName || '-' }))) + exportTaskTypeAnalysis('任务类型分析(三级分类)', (dimensionData.taskTypeLevel3Analysis || []).map((item: any) => ({ ...item, parentName: item.parentName || '-' }))) + + // 8. 一级需求部门统计 + if (primaryDeptStats) { + csvContent += '【一级需求部门统计】\n' + appendRow(['指标', '数值']) + appendRow(['发起任务数', primaryDeptStats.totalTasks || 0]) + appendRow(['审核通过', primaryDeptStats.approvedTasks || 0]) + appendRow(['审核驳回', primaryDeptStats.rejectedTasks || 0]) + appendRow(['消耗积分', formatScore(primaryDeptStats.consumedScore)]) + appendRow(['待处理', primaryDeptStats.pendingTasks || 0]) + appendRow(['进行中', primaryDeptStats.inProgressTasks || 0]) + appendRow(['已完成', primaryDeptStats.completedTasks || 0]) + csvContent += '\n' + } + + if (primaryDeptRanking.value?.length) { + csvContent += '【一级需求部门排名】\n' + appendRow(['排名', '部门名称', '一级部门', '二级部门', '发起任务数', '审核通过率', '消耗积分', '平均响应时间', '已完成', '进行中']) + primaryDeptRanking.value.forEach((item: any, index: number) => { + appendRow([ + item.rank || index + 1, + item.deptName || '-', + item.firstLevelDeptName || '-', + item.secondLevelDeptName || '-', + item.taskCount || 0, + `${formatScore(item.approvalRate)}%`, + formatScore(item.consumedScore), + item.avgResponseHours ? `${formatScore(item.avgResponseHours)}h` : '-', + item.completedCount || 0, + item.inProgressCount || 0 + ]) + }) + csvContent += '\n' + } + + if (brandDistribution.value?.length) { + csvContent += '【品牌任务分布(一级需求)】\n' + appendRow(['品牌名称', '任务数', '占比', '负责部门', '消耗积分', '已完成']) + brandDistribution.value.forEach((item: any) => { + appendRow([ + item.brandName || '-', + item.taskCount || 0, + `${formatScore(item.percentage)}%`, + item.responsibleDeptName || '-', + formatScore(item.consumedScore), + item.completedCount || 0 + ]) + }) + csvContent += '\n' + } + + if (primaryDeptUserRanking.value?.length) { + csvContent += '【一级需求部门用户排名】\n' + appendRow(['排名', '用户姓名', '部门名称', '一级部门', '二级部门', '发起任务数', '审核通过数', '审核驳回数', '审核通过率', '消耗积分', '已完成', '进行中', '待审核']) + primaryDeptUserRanking.value.forEach((item: any, index: number) => { + appendRow([ + item.rank || index + 1, + item.userName || '-', + item.deptName || '-', + item.firstLevelDeptName || '-', + item.secondLevelDeptName || '-', + item.taskCount || 0, + item.approvedCount || 0, + item.rejectedCount || 0, + `${formatScore(item.approvalRate)}%`, + formatScore(item.consumedScore), + item.completedCount || 0, + item.inProgressCount || 0, + item.pendingCount || 0 + ]) }) csvContent += '\n' }