| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150 |
- import type { NormalizedCandidate } from './recommendation.service.ts';
- function escapeHtml(value: string): string {
- return value
- .replaceAll('&', '&')
- .replaceAll('<', '<')
- .replaceAll('>', '>')
- .replaceAll('"', '"');
- }
- /**
- * 生成媒体资源推荐表 Excel (HTML table format compatible with MS Excel)
- * 按模板生成:账号名称、ID、账号类型、地区、链接、粉丝数、赞藏数、
- * 图文/视频报价、推荐等级、推荐理由等字段
- */
- export function generateExcelBuffer(candidates: NormalizedCandidate[], taskFileName?: string): Buffer {
- const title = taskFileName ? taskFileName.replace(/\.[^.]+$/, '') : '达人推荐表';
- const now = new Date().toLocaleDateString('zh-CN');
- const headerRow = `
- <tr>
- <th>序号</th>
- <th>平台</th>
- <th>账号名称</th>
- <th>账号ID</th>
- <th>地区</th>
- <th>主页链接</th>
- <th>粉丝数</th>
- <th>图文报价</th>
- <th>视频报价</th>
- <th>最低报价</th>
- <th>内容标签</th>
- <th>综合评分</th>
- <th>风格匹配</th>
- <th>推荐等级</th>
- <th>推荐理由</th>
- <th>风险提示</th>
- <th>数据来源</th>
- <th>昵称</th>
- <th>性别</th>
- <th>粉丝数(万)</th>
- <th>赞藏数(万)</th>
- <th>内容类型</th>
- <th>城市</th>
- <th>地理位置</th>
- <th>小红书主页</th>
- <th>合作方式</th>
- <th>图文笔记报价(含平台服务费)</th>
- <th>视频笔记报价(含平台服务费)</th>
- </tr>`;
- const rows = candidates.map((c, index) => `
- <tr>
- <td>${index + 1}</td>
- <td>${escapeHtml(mapPlatformDisplay(c.platform))}</td>
- <td>${escapeHtml(c.displayName)}</td>
- <td>${escapeHtml(c.platformUserId)}</td>
- <td>${escapeHtml(c.location || '-')}</td>
- <td>${escapeHtml(c.profileUrl)}</td>
- <td>${formatFans(c.fansCount)}</td>
- <td>${c.imagePrice > 0 ? `¥${c.imagePrice}` : '-'}</td>
- <td>${c.videoPrice > 0 ? `¥${c.videoPrice}` : '-'}</td>
- <td>${c.minPrice > 0 ? `¥${c.minPrice}` : '-'}</td>
- <td>${escapeHtml(c.contentTags.join('、') || '-')}</td>
- <td>${c.score}</td>
- <td>${c.styleMatch}%</td>
- <td>${escapeHtml(c.recommendStatus)}</td>
- <td>${escapeHtml(c.recommendReason || '-')}</td>
- <td>${escapeHtml(c.riskNote || '-')}</td>
- <td>${escapeHtml(c.sourceProvider)}</td>
- <td>${escapeHtml(c.displayName)}</td>
- <td>${escapeHtml(c.gender || '-')}</td>
- <td>${formatWan(c.fansCount)}</td>
- <td>${formatWan(c.likedCollectCount || 0)}</td>
- <td>${escapeHtml(c.contentType || c.contentTags.join('、') || '-')}</td>
- <td>${escapeHtml(c.city || c.location || '-')}</td>
- <td>${escapeHtml(c.geoLocation || c.location || '-')}</td>
- <td>${escapeHtml(c.xiaohongshuUrl || (c.platform === 'xiaohongshu' ? c.profileUrl : '-'))}</td>
- <td>${escapeHtml(c.cooperationMethod || '-')}</td>
- <td>${c.imagePrice > 0 ? `¥${c.imagePrice}` : '-'}</td>
- <td>${c.videoPrice > 0 ? `¥${c.videoPrice}` : '-'}</td>
- </tr>`
- ).join('');
- const html = `
- <html xmlns:o="urn:schemas-microsoft-com:office:office"
- xmlns:x="urn:schemas-microsoft-com:office:excel"
- xmlns="http://www.w3.org/TR/REC-html40">
- <head>
- <meta charset="UTF-8">
- <meta name="ProgId" content="Excel.Sheet">
- <!--[if gte mso 9]>
- <xml>
- <x:ExcelWorkbook>
- <x:ExcelWorksheets>
- <x:ExcelWorksheet>
- <x:Name>推荐名单</x:Name>
- <x:WorksheetOptions>
- <x:DisplayGridlines/>
- </x:WorksheetOptions>
- </x:ExcelWorksheet>
- </x:ExcelWorksheets>
- </x:ExcelWorkbook>
- </xml>
- <![endif]-->
- <style>
- table { border-collapse: collapse; font-family: "Microsoft YaHei", Arial, sans-serif; font-size: 11pt; }
- th { background: #1f8a70; color: #fff; font-weight: bold; text-align: center; }
- th, td { border: 1px solid #cfd7d1; padding: 6px 10px; white-space: nowrap; }
- .title-row td { font-size: 14pt; font-weight: bold; border: none; padding: 10px 0; }
- .meta-row td { font-size: 9pt; color: #666; border: none; padding: 2px 0; }
- td:nth-child(12), td:nth-child(13) { background: #eef7f3; font-weight: 700; }
- </style>
- </head>
- <body>
- <table>
- <tr class="title-row"><td colspan="28">${escapeHtml(title)} - 媒体资源推荐表</td></tr>
- <tr class="meta-row"><td colspan="28">生成时间:${now} | 候选总数:${candidates.length} | 强推荐:${candidates.filter(c => c.recommendStatus === '强推荐').length} | 备选:${candidates.filter(c => c.recommendStatus === '备选').length}</td></tr>
- <tr><td colspan="28"></td></tr>
- ${headerRow}
- ${rows}
- </table>
- </body>
- </html>`;
- return Buffer.from(html, 'utf-8');
- }
- function mapPlatformDisplay(platform: string): string {
- const map: Record<string, string> = {
- xiaohongshu: '小红书',
- douyin: '抖音',
- bilibili: 'B站',
- weibo: '微博',
- weixin: '微信/公众号',
- };
- return map[platform] || platform;
- }
- function formatFans(count: number): string {
- if (count === 0) return '-';
- if (count >= 10000) return `${(count / 10000).toFixed(1)}万`;
- return String(count);
- }
- function formatWan(count: number): string {
- if (!count) return '-';
- const value = count >= 10000 ? count / 10000 : count;
- return Number.isInteger(value) ? String(value) : value.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
- }
|