feat(客户数据): 添加一键导出功能并集成xlsx库

- 在ProblemRanking组件中添加导出按钮和功能
- 新增导出API接口并修改axios基础URL
- 添加xlsx依赖用于Excel文件生成
- 实现客户数据展平处理和Excel导出逻辑
This commit is contained in:
2025-09-01 11:36:26 +08:00
parent c10b514779
commit fa2754e124
6 changed files with 316 additions and 21 deletions

View File

@@ -19,9 +19,6 @@
历史
</button>
<button v-if="isViewingHistory" @click="returnToCurrentPeriod" class="current-btn">
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,6A6,6 0 0,1 18,12A6,6 0 0,1 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6M12,8A4,4 0 0,0 8,12A4,4 0 0,0 12,16A4,4 0 0,0 16,12A4,4 0 0,0 12,8Z"/>
</svg>
返回当前
</button>
<button @click="nextMonth" class="nav-btn">

View File

@@ -2,6 +2,7 @@
<div class="chart-container">
<div class="chart-header">
<h3>客户迫切解决的问题排行榜</h3>
<button @click="exportData">一键导出</button>
</div>
<div class="chart-content">
<div v-if="sortedData.length > 0" class="problem-ranking">
@@ -33,7 +34,13 @@
</template>
<script setup>
import { computed } from 'vue';
import { computed,onMounted } from 'vue';
import { exportCustomers } from '@/api/secondTop';
import { useUserStore } from "@/stores/user";
import { ElMessage } from 'element-plus';
import * as XLSX from 'xlsx';
// 用户store实例
const userStore = useUserStore();
// 定义Props接收一个包含 { name: string, value: string | number } 的数组
const props = defineProps({
@@ -73,6 +80,111 @@ const getRankingClass = (index) => {
const getRankBadgeClass = (index) => {
return ['badge-gold', 'badge-silver', 'badge-bronze'][index] || 'badge-default';
};
async function exportData() {
const params = {
user_name: userStore.userInfo.username,
user_level: userStore.userInfo.user_level.toString(),
}
try {
ElMessage.info('正在导出数据,请稍候...')
const res = await exportCustomers(params)
if (res.code === 200 && res.data && res.data.length > 0) {
// 处理数据,将复杂的嵌套对象展平
const exportData = res.data.map(customer => {
const flatData = {
'昵称': customer.nickname || '',
'性别': customer.gender || '',
'跟进人': customer.follow_up_name || '',
'手机号': customer.phone || '',
'是否入群': customer.is_in_group || '',
'用户ID': customer.mantis_user_id || '',
}
// 处理微信表单信息
if (customer.wechat_form) {
flatData['家长姓名'] = customer.wechat_form.name || ''
flatData['孩子姓名'] = customer.wechat_form.child_name || ''
flatData['孩子性别'] = customer.wechat_form.child_gender || ''
flatData['职业'] = customer.wechat_form.occupation || ''
flatData['孩子教育阶段'] = customer.wechat_form.child_education || ''
flatData['与孩子关系'] = customer.wechat_form.child_relation || ''
flatData['联系电话'] = customer.wechat_form.mobile || ''
flatData['地区'] = customer.wechat_form.territory || ''
flatData['创建时间'] = customer.wechat_form.created_at ? new Date(customer.wechat_form.created_at).toLocaleString() : ''
flatData['更新时间'] = customer.wechat_form.updated_at ? new Date(customer.wechat_form.updated_at).toLocaleString() : ''
}
// 处理到课情况
if (customer.live) {
flatData['课一到课情况'] = customer.live['课一'] || ''
flatData['课二到课情况'] = customer.live['课二'] || ''
flatData['课三到课情况'] = customer.live['课三'] || ''
flatData['课四到课情况'] = customer.live['课四'] || ''
}
// 处理问卷调查信息
if (customer.wechat_form && customer.wechat_form.additional_info) {
customer.wechat_form.additional_info.forEach((item, index) => {
flatData[`问题${index + 1}`] = item.topic || ''
flatData[`答案${index + 1}`] = item.answer || ''
})
}
return flatData
})
// 创建工作簿
const wb = XLSX.utils.book_new()
const ws = XLSX.utils.json_to_sheet(exportData)
// 设置列宽
const colWidths = [
{ wch: 10 }, // 昵称
{ wch: 6 }, // 性别
{ wch: 12 }, // 跟进人
{ wch: 15 }, // 手机号
{ wch: 10 }, // 是否入群
{ wch: 20 }, // 用户ID
{ wch: 12 }, // 家长姓名
{ wch: 12 }, // 孩子姓名
{ wch: 8 }, // 孩子性别
{ wch: 12 }, // 职业
{ wch: 12 }, // 孩子教育阶段
{ wch: 15 }, // 与孩子关系
{ wch: 15 }, // 联系电话
{ wch: 20 }, // 地区
{ wch: 20 }, // 创建时间
{ wch: 20 }, // 更新时间
]
ws['!cols'] = colWidths
// 添加工作表到工作簿
XLSX.utils.book_append_sheet(wb, ws, '客户数据')
// 生成文件名(包含当前时间)
const now = new Date()
const fileName = `客户数据导出_${now.getFullYear()}${(now.getMonth() + 1).toString().padStart(2, '0')}${now.getDate().toString().padStart(2, '0')}_${now.getHours().toString().padStart(2, '0')}${now.getMinutes().toString().padStart(2, '0')}.xlsx`
// 导出文件
XLSX.writeFile(wb, fileName)
ElMessage.success(`导出成功!共导出 ${exportData.length} 条数据`)
} else {
ElMessage.warning('暂无数据可导出')
}
} catch (error) {
console.error('导出失败:', error)
ElMessage.error('导出失败,请稍后重试')
}
}
onMounted(async ()=>{
await exportData()
})
</script>
<style lang="scss" scoped>
@@ -89,12 +201,40 @@ const getRankBadgeClass = (index) => {
.chart-header {
padding: 20px 20px 16px;
border-bottom: 1px solid #ebeef5;
display: flex;
justify-content: space-between;
align-items: center;
h3 {
margin: 0;
color: #303133;
font-size: 18px;
font-weight: 600;
}
button {
padding: 8px 16px;
background: linear-gradient(135deg, #409eff, #3a8ee6);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
&:hover {
background: linear-gradient(135deg, #3a8ee6, #337ecc);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(64, 158, 255, 0.4);
}
&:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(64, 158, 255, 0.3);
}
}
}
.chart-content {