diff --git a/admin/src/views/fans/yeji.vue b/admin/src/views/fans/yeji.vue index 9d231ead..1688c747 100644 --- a/admin/src/views/fans/yeji.vue +++ b/admin/src/views/fans/yeji.vue @@ -1156,9 +1156,19 @@ class="yeji-leadlines-dialog" >

{{ leadLinesApiNote }}

@@ -1630,6 +1640,7 @@ const revisitBreakdownMeta = ref<{ start: string; end: string; revisitSlot: numb const leadLinesDialogVisible = ref(false) const leadLinesLoading = ref(false) +const leadLinesExporting = ref(false) const leadLinesSubtitle = ref('') const leadLinesApiNote = ref('') const leadLinesRows = ref([]) @@ -3128,28 +3139,79 @@ function onLeadCountCellClick(ev: MouseEvent, tb: YejiTable, row: YejiRow) { void openLeadLinesDialog(tb, row) } +const LEAD_LINES_EXPORT_COLUMNS: { key: keyof YejiLeadLineRow; label: string }[] = [ + { key: 'event_time_text', label: '进线时间' }, + { key: 'reception_admin_name', label: '接待' }, + { key: 'external_contact_name', label: '客户' }, + { key: 'external_userid', label: '外部联系人ID' }, + { key: 'user_id', label: '企微成员ID' }, + { key: 'state', label: '渠道参数' }, +] + +function buildLeadLinesRequestParams( + ctx: { tb: YejiTable; row: YejiRow }, + page: number, + pageSize: number +): Record { + const { tb, row } = ctx + const p: Record = { + start_date: tb.start_date, + end_date: tb.end_date, + dept_id: row.dept_id, + page, + page_size: pageSize, + } + if (selectedDeptIds.value.length > 0) { + p.dept_ids = selectedDeptIds.value.join(',') + } + if (selectedChannel.value) { + p.channel_code = selectedChannel.value + } + return p +} + +function escapeLeadLinesCsvCell(value: unknown): string { + const s = value == null ? '' : String(value) + if (/[",\n\r]/.test(s)) { + return `"${s.replace(/"/g, '""')}"` + } + return s +} + +function buildLeadLinesCsv(rows: YejiLeadLineRow[]): string { + const header = LEAD_LINES_EXPORT_COLUMNS.map((c) => escapeLeadLinesCsvCell(c.label)).join(',') + const body = rows + .map((row) => LEAD_LINES_EXPORT_COLUMNS.map((c) => escapeLeadLinesCsvCell(row[c.key])).join(',')) + .join('\n') + return `\uFEFF${header}\n${body}` +} + +function downloadLeadLinesCsv(filename: string, content: string) { + const blob = new Blob([content], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const anchor = document.createElement('a') + anchor.href = url + anchor.download = filename + anchor.click() + URL.revokeObjectURL(url) +} + +function buildLeadLinesExportFilename(ctx: { tb: YejiTable; row: YejiRow }): string { + const { tb, row } = ctx + const safeDept = (row.dept_name || `dept${row.dept_id}`).replace(/[\\/:*?"<>|]/g, '_') + return `进线数据明细_${safeDept}_${tb.start_date}_${tb.end_date}.csv` +} + async function fetchLeadLinesPage() { const ctx = leadLinesContext.value if (!ctx) { return } - const { tb, row } = ctx leadLinesLoading.value = true try { - const p: Record = { - start_date: tb.start_date, - end_date: tb.end_date, - dept_id: row.dept_id, - page: leadLinesPage.value, - page_size: leadLinesPageSize.value, - } - if (selectedDeptIds.value.length > 0) { - p.dept_ids = selectedDeptIds.value.join(',') - } - if (selectedChannel.value) { - p.channel_code = selectedChannel.value - } - const res: any = await yejiStatsLeadLines(p as any) + const res: any = await yejiStatsLeadLines( + buildLeadLinesRequestParams(ctx, leadLinesPage.value, leadLinesPageSize.value) as any + ) leadLinesRows.value = Array.isArray(res?.lists) ? res.lists : [] leadLinesCount.value = Number(res?.count ?? 0) leadLinesApiNote.value = typeof res?.note === 'string' ? res.note : '' @@ -3188,6 +3250,40 @@ function onLeadLinesPageSizeChange(size: number) { void fetchLeadLinesPage() } +async function exportLeadLines() { + const ctx = leadLinesContext.value + if (!ctx || leadLinesExporting.value) { + return + } + leadLinesExporting.value = true + try { + const pageSize = 100 + const firstRes: any = await yejiStatsLeadLines(buildLeadLinesRequestParams(ctx, 1, pageSize) as any) + const total = Number(firstRes?.count ?? 0) + if (total <= 0) { + ElMessage.warning('暂无进线明细可导出') + return + } + const allRows: YejiLeadLineRow[] = Array.isArray(firstRes?.lists) ? [...firstRes.lists] : [] + const totalPages = Math.ceil(total / pageSize) + for (let page = 2; page <= totalPages; page++) { + const res: any = await yejiStatsLeadLines(buildLeadLinesRequestParams(ctx, page, pageSize) as any) + const lists = Array.isArray(res?.lists) ? res.lists : [] + allRows.push(...lists) + } + downloadLeadLinesCsv(buildLeadLinesExportFilename(ctx), buildLeadLinesCsv(allRows)) + ElMessage.success(`已导出 ${allRows.length} 条进线明细`) + } catch (e: unknown) { + if (axios.isCancel(e)) { + return + } + const any = e as any + ElMessage.error(any?.msg || any?.message || '导出进线明细失败') + } finally { + leadLinesExporting.value = false + } +} + function appointmentLinesIndexMethod(index: number) { return (appointmentLinesPage.value - 1) * appointmentLinesPageSize.value + index + 1 } @@ -4914,6 +5010,18 @@ tbody tr.total-row:hover .yeji-lead-cell--link { color: color-mix(in srgb, var(--yj-brand, #2563eb) 88%, #000); } +.yeji-leadlines-dialog__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; +} + +.yeji-leadlines-dialog__head-main { + flex: 1; + min-width: 0; +} + .yeji-leadlines-dialog__pager { margin-top: 14px; display: flex;