Compare commits

...

2 Commits

Author SHA1 Message Date
fa2754e124 feat(客户数据): 添加一键导出功能并集成xlsx库
- 在ProblemRanking组件中添加导出按钮和功能
- 新增导出API接口并修改axios基础URL
- 添加xlsx依赖用于Excel文件生成
- 实现客户数据展平处理和Excel导出逻辑
2025-09-01 11:36:26 +08:00
c10b514779 feat(销售时间轴): 添加子时间轴阶段选择功能
实现子时间轴各阶段的点击选择功能,将筛选后的客户数据转换为统一格式并传递给父组件
2025-08-30 17:19:53 +08:00
8 changed files with 423 additions and 40 deletions

View File

@@ -9,6 +9,7 @@
"version": "0.0.0",
"dependencies": {
"@element-plus/icons-vue": "^2.3.1",
"@fullcalendar/core": "^6.1.19",
"axios": "^1.10.0",
"chart.js": "^4.5.0",
"dompurify": "^3.2.6",
@@ -17,11 +18,12 @@
"markdown-it": "^14.1.0",
"marked": "^16.1.1",
"pinia": "^3.0.2",
"pnpm": "^10.15.0",
"pinia-plugin-persistedstate": "^4.5.0",
"vue": "^3.5.17",
"vue-chartjs": "^5.3.2",
"vue-echarts": "^7.0.3",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.1",
@@ -1211,6 +1213,15 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@fullcalendar/core": {
"version": "6.1.19",
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.19.tgz",
"integrity": "sha512-z0aVlO5e4Wah6p6mouM0UEqtRf1MZZPt4mwzEyU6kusaNL+dlWQgAasF2cK23hwT4cmxkEmr4inULXgpyeExdQ==",
"license": "MIT",
"dependencies": {
"preact": "~10.12.1"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
@@ -2823,6 +2834,15 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz",
@@ -3031,6 +3051,19 @@
],
"license": "CC-BY-4.0"
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
@@ -3076,6 +3109,15 @@
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
@@ -3137,6 +3179,18 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3209,6 +3263,12 @@
"dev": true,
"license": "MIT"
},
"node_modules/deep-pick-omit": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/deep-pick-omit/-/deep-pick-omit-1.2.1.tgz",
"integrity": "sha512-2J6Kc/m3irCeqVG42T+SaUMesaK7oGWaedGnQQK/+O0gYc+2SP5bKh/KKTE7d7SJ+GCA9UUE1GRzh6oDe0EnGw==",
"license": "MIT"
},
"node_modules/default-browser": {
"version": "5.2.1",
"resolved": "https://registry.npmmirror.com/default-browser/-/default-browser-5.2.1.tgz",
@@ -3252,6 +3312,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
"integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==",
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -3261,6 +3327,12 @@
"node": ">=0.4.0"
}
},
"node_modules/destr": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz",
"integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==",
"license": "MIT"
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -3970,6 +4042,15 @@
"node": ">= 6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fs-extra": {
"version": "11.3.1",
"resolved": "https://registry.npmmirror.com/fs-extra/-/fs-extra-11.3.1.tgz",
@@ -5084,20 +5165,31 @@
}
}
},
"node_modules/pnpm": {
"version": "10.15.0",
"resolved": "https://registry.npmmirror.com/pnpm/-/pnpm-10.15.0.tgz",
"integrity": "sha512-SG68JZ0+mZpOhpHOA7XKxKccvso5Nyqbdiy1AM/fCHPiyxar49lRse4s8BJQPwJ7mLZYTk3yJSTgx0UNnseqew==",
"node_modules/pinia-plugin-persistedstate": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/pinia-plugin-persistedstate/-/pinia-plugin-persistedstate-4.5.0.tgz",
"integrity": "sha512-QTkP1xJVyCdr2I2p3AKUZM84/e+IS+HktRxKGAIuDzkyaKKV48mQcYkJFVVDuvTxlI5j6X3oZObpqoVB8JnWpw==",
"license": "MIT",
"bin": {
"pnpm": "bin/pnpm.cjs",
"pnpx": "bin/pnpx.cjs"
"dependencies": {
"deep-pick-omit": "^1.2.1",
"defu": "^6.1.4",
"destr": "^2.0.5"
},
"engines": {
"node": ">=18.12"
"peerDependencies": {
"@nuxt/kit": ">=3.0.0",
"@pinia/nuxt": ">=0.10.0",
"pinia": ">=3.0.0"
},
"funding": {
"url": "https://opencollective.com/pnpm"
"peerDependenciesMeta": {
"@nuxt/kit": {
"optional": true
},
"@pinia/nuxt": {
"optional": true
},
"pinia": {
"optional": true
}
}
},
"node_modules/postcss": {
@@ -5142,6 +5234,16 @@
"node": ">=4"
}
},
"node_modules/preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -5491,6 +5593,18 @@
"node": ">=0.10.0"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/strip-final-newline": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz",
@@ -6151,6 +6265,24 @@
"node": ">= 8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
@@ -6177,6 +6309,27 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/xml-name-validator": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz",

View File

@@ -23,7 +23,8 @@
"vue": "^3.5.17",
"vue-chartjs": "^5.3.2",
"vue-echarts": "^7.0.3",
"vue-router": "^4.5.0"
"vue-router": "^4.5.0",
"xlsx": "^0.18.5"
},
"devDependencies": {
"@tsconfig/node22": "^22.0.1",

View File

@@ -85,6 +85,10 @@ export const cancelSwitchHistoryCampPeriod = (params) => {
return https.post('/api/v1/level_four/overview/cancel_switch_history_camp_period', params)
}
// 一键导出 api/v1/level_four/overview/export_customers
export const exportCustomers = (params) => {
return https.post('http://192.168.15.56:8890/api/v1/level_four/overview/export_customers', params)
}

View File

@@ -5,8 +5,8 @@ import { useUserStore } from '@/stores/user'
// 创建axios实例
const service = axios.create({
baseURL: 'https://mldash.nycjy.cn/' || '', // API基础路径支持完整URL
// baseURL: 'http://192.168.15.121:8890' || '', // API基础路径支持完整URL
// baseURL: 'https://mldash.nycjy.cn/' || '', // API基础路径支持完整URL
baseURL: 'http://192.168.15.121:8890' || '', // API基础路径支持完整URL
timeout: 100000, // 请求超时时间
headers: {
'Content-Type': 'application/json;charset=UTF-8'

View File

@@ -46,14 +46,14 @@
<span class="course-conversion">转化率: {{ getCourseConversionRate(1) }}%</span>
</div>
<div class="mini-timeline">
<div class="mini-stage" :class="{ active: getCourseStageCount(1, '课1') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(1, '课1') > 0 }" @click="selectCourseStage(1, '课1')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">课1</span>
<span class="mini-count">{{ getCourseStageCount(1, '课1') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(1, '付定金') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(1, '付定金') > 0 }" @click="selectCourseStage(1, '付定金')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">付定金</span>
@@ -69,35 +69,35 @@
<span class="course-title">课2</span>
</div>
<div class="mini-timeline">
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '课2') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '课2') > 0 }" @click="selectCourseStage(2, '课2')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">课2</span>
<span class="mini-count">{{ getCourseStageCount(2, '课2') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '点击未支付') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '点击未支付') > 0 }" @click="selectCourseStage(2, '点击未支付')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">点击支付</span>
<span class="mini-count">{{ getCourseStageCount(2, '点击未支付') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '付定金') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '付定金') > 0 }" @click="selectCourseStage(2, '付定金')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">付定金</span>
<span class="mini-count">{{ getCourseStageCount(2, '付定金') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '定金转化') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '定金转化') > 0 }" @click="selectCourseStage(2, '定金转化')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">定金转化</span>
<span class="mini-count">{{ getCourseStageCount(2, '定金转化') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '成交') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '成交') > 0 }" @click="selectCourseStage(2, '成交')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">成交</span>
@@ -113,35 +113,35 @@
<span class="course-title">课3</span>
</div>
<div class="mini-timeline">
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '课3') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '课3') > 0 }" @click="selectCourseStage(3, '课3')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">课3</span>
<span class="mini-count">{{ getCourseStageCount(3, '课3') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '点击未支付') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '点击未支付') > 0 }" @click="selectCourseStage(3, '点击未支付')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">点击未支付</span>
<span class="mini-count">{{ getCourseStageCount(3, '点击未支付') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '付定金') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '付定金') > 0 }" @click="selectCourseStage(3, '付定金')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">付定金</span>
<span class="mini-count">{{ getCourseStageCount(3, '付定金') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '定金转化') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '定金转化') > 0 }" @click="selectCourseStage(3, '定金转化')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">定金转化</span>
<span class="mini-count">{{ getCourseStageCount(3, '定金转化') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '成交') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '成交') > 0 }" @click="selectCourseStage(3, '成交')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">成交</span>
@@ -157,35 +157,35 @@
<span class="course-title">课4</span>
</div>
<div class="mini-timeline">
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '课4') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '课4') > 0 }" @click="selectCourseStage(4, '课4')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">课4</span>
<span class="mini-count">{{ getCourseStageCount(4, '课4') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '点击未支付') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '点击未支付') > 0 }" @click="selectCourseStage(4, '点击未支付')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">点击未付</span>
<span class="mini-count">{{ getCourseStageCount(4, '点击未支付') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '付定金') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '付定金') > 0 }" @click="selectCourseStage(4, '付定金')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">付定金</span>
<span class="mini-count">{{ getCourseStageCount(4, '付定金') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '定金转化') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '定金转化') > 0 }" @click="selectCourseStage(4, '定金转化')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">定金转化</span>
<span class="mini-count">{{ getCourseStageCount(4, '定金转化') }}</span>
</div>
</div>
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '成交') > 0 }">
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '成交') > 0 }" @click="selectCourseStage(4, '成交')">
<div class="mini-marker"></div>
<div class="mini-content">
<span class="mini-title">成交</span>
@@ -768,6 +768,55 @@ const getHealthClass = (health) => {
return 'health-danger';
}
};
// 选择课程阶段
const selectCourseStage = (courseNumber, stageType) => {
let filteredCustomers = [];
if (stageType === `${courseNumber}`) {
// 课程阶段从customersList中筛选
filteredCustomers = props.customersList.filter(customer => {
const classNum = customer.class_num;
const classSituation = customer.class_situation;
// 检查class_num字段
if (classNum && Array.isArray(classNum)) {
return classNum.includes(courseNumber);
}
// 检查class_situation字段
if (classSituation) {
if (Array.isArray(classSituation)) {
return classSituation.includes(courseNumber);
}
if (typeof classSituation === 'object') {
return classSituation.hasOwnProperty(courseNumber.toString());
}
}
return false;
});
} else {
// 其他阶段从courseCustomers中筛选
if (props.courseCustomers?.['课1-4']) {
filteredCustomers = props.courseCustomers['课1-4'].filter(customer => {
// 检查客户是否参加了指定课程并且类型匹配
const hasAttendedCourse = customer.class_num && customer.class_num.includes(courseNumber);
return hasAttendedCourse && customer.type === stageType;
});
}
}
// 发送子时间轴选择事件给父组件,使用不同的事件名称避免与主轴冲突
emit('sub-stage-select', {
filteredCustomers,
stageType: `${courseNumber}-${stageType}`,
customerCount: filteredCustomers.length,
courseNumber,
originalStageType: stageType,
keepSubTimeline: true // 标识保持子时间轴显示
});
};
</script>
<style lang="scss" scoped>

View File

@@ -42,6 +42,7 @@
v-else
:data="timelineData"
@stage-select="handleStageSelect"
@sub-stage-select="handleSubStageSelect"
:selected-stage="selectedStage"
:contacts="filteredContacts"
:selected-contact-id="selectedContactId"
@@ -772,6 +773,44 @@ const handleStageSelect = (stage, extraData = null) => {
currentFilteredCustomers.value = [];
}
};
// 处理子时间轴阶段选择
const handleSubStageSelect = (eventData) => {
console.log('子时间轴选择事件:', eventData);
// 将筛选后的客户数据转换为contacts格式
const filteredContacts = eventData.filteredCustomers.map(customer => ({
id: customer.customer_name || customer.id,
name: customer.customer_name || customer.name,
phone: customer.phone,
profession: customer.customer_occupation || customer.profession,
education: customer.customer_child_education || customer.education,
lastMessageTime: customer.latest_message_time || customer.time,
avatarUrl: customer.customer_avatar_url || customer.avatar,
avatar: customer.customer_avatar_url || customer.avatar || '/default-avatar.svg',
type: customer.type || eventData.originalStageType,
classNum: customer.class_num,
class_num: customer.class_num,
salesStage: eventData.stageType,
priority: customer.type === '待联系' ? 'high' : 'normal',
time: customer.latest_message_time || customer.time || '未知',
health: customer.health || 75,
// 保留原始数据
customer_name: customer.customer_name,
customer_occupation: customer.customer_occupation,
customer_child_education: customer.customer_child_education,
scrm_user_main_code: customer.scrm_user_main_code,
weChat_avatar: customer.weChat_avatar,
class_situation: customer.class_situation,
records: customer.records
}));
// 更新当前筛选的客户数据但保持selectedStage不变保持子时间轴显示
currentFilteredCustomers.value = filteredContacts;
console.log(`已筛选出${eventData.originalStageType}阶段的${filteredContacts.length}位客户`);
};
const handleViewFormData = async (contact) => {
// 获取客户表单数据
await getCustomerForm();

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 {