const token = localStorage.getItem('token'); if (!token) { App.Toast.show('请先登录系统', 'error'); return; } fetch('/api/collect/bookmarklet?token=' + encodeURIComponent(token)) .then(function(r) { return r.text(); }) .then(function(code) { if (navigator.clipboard) { navigator.clipboard.writeText(code).then(function() { alert('✅ 书签代码已复制!\n\n设置步骤:\n1. 右键浏览器书签栏 → 添加网页\n2. 名称填「采集达人」\n3. URL栏粘贴(Ctrl+V)\n4. 保存\n\n在百应达人广场页面点击书签即可采集!'); }); } else { prompt('请复制书签代码(右键书签栏→添加网页→粘贴URL):', code); } App.Toast.show('书签代码已生成,请按提示设置', 'success'); }) .catch(function(e) { App.Toast.show('生成失败: ' + e.message, 'error'); }); }, renderCdpTab(container) { container.innerHTML = `
5页=约100条,最多20页=400条
`; }, async checkCdpStatus() { const box = document.getElementById('cdp-status-box'); if (box) box.innerHTML = '检测中...'; const data = await App.api.collect.cdpStatus(); this.cdpStatus = data; if (!box) return; if (!data) { box.innerHTML = '❌ 检测失败,后端服务异常'; return; } const pwOk = data.playwright_available ? '✅' : '❌'; const cookieOk = data.cookie_valid ? '✅' : '❌'; box.innerHTML = `
`; }, async startCdpCollect() { const keyword = document.getElementById('collect-keyword')?.value || ''; const maxPages = parseInt(document.getElementById('collect-pages')?.value) || 5; const autoImport = document.getElementById('collect-auto-import')?.checked || false; const resultBox = document.getElementById('cdp-result-box'); if (resultBox) resultBox.innerHTML = '
'; const data = await App.api.collect.cdpCollect({ keyword, max_pages: maxPages, auto_import: autoImport }); if (!data) return; this.collectResult = data; this.renderCollectResult(data, resultBox, autoImport); }, // ── Cookie 管理方法 ────────────────────────────────────────────────── switchCookieTab(tab) { this._cookieTab = tab; const jsonPanel = document.getElementById('cookie-json-panel'); const rawPanel = document.getElementById('cookie-raw-panel'); const jsonTab = document.getElementById('cookie-tab-json'); const rawTab = document.getElementById('cookie-tab-raw'); if (tab === 'json') { if(jsonPanel) jsonPanel.classList.remove('hidden'); if(rawPanel) rawPanel.classList.add('hidden'); if(jsonTab) jsonTab.className = 'px-3 py-1.5 text-xs font-medium border-b-2 border-indigo-500 text-indigo-600'; if(rawTab) rawTab.className = 'px-3 py-1.5 text-xs font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700'; } else { if(jsonPanel) jsonPanel.classList.add('hidden'); if(rawPanel) rawPanel.classList.remove('hidden'); if(jsonTab) jsonTab.className = 'px-3 py-1.5 text-xs font-medium border-b-2 border-transparent text-gray-500 hover:text-gray-700'; if(rawTab) rawTab.className = 'px-3 py-1.5 text-xs font-medium border-b-2 border-indigo-500 text-indigo-600'; } }, copyBookmarklet() { const code = "javascript:(function(){var c=document.cookie;if(!c){alert('请先登录百应');return;}fetch('https://shijingzhou.icu/api/collect/cookie/upload',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({platform:'buyin',raw_cookie:c})}).then(function(r){return r.json()}).then(function(d){alert(d.code===0?'✅ Cookie同步成功!':'❌ 失败:'+d.message);});})();"; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(code).then(function(){alert('✅ 书签代码已复制!\n\n在百应页面打开时,点击这个书签就自动同步Cookie了\n\n设置方法:右键浏览器书签栏 → 添加网页 → 名称填「同步Cookie」→ 粘贴URL → 保存');}); } else { prompt('请手动复制书签代码(右键书签栏→添加):', code); } }, async uploadCookie() { const tab = this._cookieTab || 'raw'; const jsonStr = document.getElementById('cookie-json-input')?.value || ''; const rawStr = document.getElementById('cookie-raw-input')?.value || ''; let body; if (tab === 'json' && jsonStr.trim()) { body = { platform: 'buyin', cookies_json: jsonStr }; } else if (tab === 'raw' && rawStr.trim()) { body = { platform: 'buyin', raw_cookie: rawStr }; } else if (jsonStr.trim()) { body = { platform: 'buyin', cookies_json: jsonStr }; } else if (rawStr.trim()) { body = { platform: 'buyin', raw_cookie: rawStr }; } else { App.Toast.show('请先粘贴Cookie内容', 'warning'); return; } const resp = await App.api.request('/api/collect/cookie/upload', { method: 'POST', body: JSON.stringify(body) }); if (resp && resp.code === 0) { App.Toast.show(resp.message || '上传成功', 'success'); const ji = document.getElementById('cookie-json-input'); const ri = document.getElementById('cookie-raw-input'); if(ji) ji.value = ''; if(ri) ri.value = ''; this.checkCookie(); } else { App.Toast.show((resp && resp.message) || '上传失败', 'error'); } }, async checkCookie() { const box = document.getElementById('cookie-status-box'); if (box) box.innerHTML = '检测中...'; const resp = await App.api.request('/api/collect/cookie/check?platform=buyin'); if (!box) return; if (resp && resp.code === 0) { const d = resp.data; if (d.valid) { const expStr = d.expires_in != null ? `(${Math.floor(d.expires_in / 3600)}小时后过期)` : ''; box.innerHTML = `✅ Cookie有效: ${d.count}条 ${expStr}`; } else { box.innerHTML = '❌ Cookie无效或已过期'; } } else { box.innerHTML = '❌ 检测失败'; } }, async clearCookie() { if (!confirm('确认清除 buyin 平台的所有Cookie?')) return; const resp = await App.api.request('/api/collect/cookie/clear?platform=buyin', { method: 'DELETE' }); if (resp && resp.code === 0) { App.Toast.show(resp.message || '已清除', 'success'); document.getElementById('cookie-status-box').innerHTML = '已清除'; } else { App.Toast.show((resp && resp.message) || '清除失败', 'error'); } }, renderCollectResult(data, container, autoImport) { if (!container) return; const preview = data.preview || []; const importResult = data.import_result; let html = `
| 昵称 | 抖音ID | 粉丝 | 30天GMV | 等级 |
|---|---|---|---|---|
| ${d.nickname||'-'} | ${d.platform_id||'-'} | ${App.formatFans(d.fans)} | ${App.formatGmvRange(d)} | ${App.levelBadge(d.level)} |
'; container.innerHTML = html; }, async importCollectedData() { if (!this.collectResult || (!this.collectResult.data && !this.collectResult.preview)) { App.showToast('没有可导入的数据', 'error'); return; } const rows = this.collectResult.data || this.collectResult.preview; const data = await App.api.collect.import(rows, true); if (!data) return; App.showToast(`导入成功:${data.success} 条新达人已入公海`, 'success'); if (this.collectResult) { this.collectResult.import_result = data; this.renderCollectResult(this.collectResult, document.getElementById('cdp-result-box'), true); } }, // ── 手动批量录入Tab ────────────────────────────────────────────────── renderManualTab(container) { container.innerHTML = `
每行一个达人,用 Tab 或逗号分隔各列。格式:昵称, 抖音ID, 粉丝数, 30天GMV, 联系方式
`; }, fillExample() { const area = document.getElementById('manual-paste-area'); if (area) area.value = `张三探店美食,dy10001,80000,45000,13800001111 李四户外vlog,dy10002,250000,180000,13800002222 王五家居生活,dy10003,15000,8000,13800003333 赵六美妆分享,dy10004,520000,620000,13800004444`; }, clearManual() { const area = document.getElementById('manual-paste-area'); if (area) area.value = ''; const result = document.getElementById('manual-result'); if (result) result.innerHTML = ''; const btn = document.getElementById('manual-import-btn'); if (btn) btn.classList.add('hidden'); this.pasteRows = []; }, parseManualData() { const text = document.getElementById('manual-paste-area')?.value || ''; const lines = text.trim().split('\n').filter(l => l.trim()); if (!lines.length) { App.showToast('请先填写数据', 'error'); return; } const rows = []; lines.forEach((line, i) => { const parts = line.split(/[\t,,]/).map(s => s.trim()); if (!parts[0]) return; const fans = parseInt(parts[2]) || 0; const gmv = parseFloat(parts[3]) || 0; let level = 'D'; if (gmv >= 500000 || fans >= 1000000) level = 'S'; else if (gmv >= 100000 || fans >= 100000) level = 'A'; else if (gmv >= 10000 || fans >= 10000) level = 'B'; else if (gmv > 0 || fans > 0) level = 'C'; rows.push({ nickname: parts[0], platform: 'douyin', platform_id: parts[1] || '', fans: fans, gmv_d30: gmv, contact: parts[4] || '', level: level, source: 'manual_batch', status: 'public_sea' }); }); this.pasteRows = rows; const resultBox = document.getElementById('manual-result'); const importBtn = document.getElementById('manual-import-btn'); if (!rows.length) { if (resultBox) resultBox.innerHTML = '
'; return; } let html = `
| # | 昵称 | 抖音ID | 粉丝 | 30天GMV | 推断等级 |
|---|---|---|---|---|---|
| ${i+1} | ${r.nickname} | ${r.platform_id||'-'} | ${App.formatFans(r.fans)} | ${App.formatGmvRange(r)} | ${App.levelBadge(r.level)} |
'; if (resultBox) resultBox.innerHTML = html; if (importBtn) importBtn.classList.remove('hidden'); }, async importManualData() { if (!this.pasteRows.length) { App.showToast('请先解析数据', 'error'); return; } const data = await App.api.collect.import(this.pasteRows, true); if (!data) return; App.showToast(`导入完成:成功 ${data.success} 条,跳过重复 ${data.skipped} 条`, 'success'); const resultBox = document.getElementById('manual-result'); if (resultBox) { resultBox.innerHTML = `
`; } const importBtn = document.getElementById('manual-import-btn'); if (importBtn) importBtn.classList.add('hidden'); this.pasteRows = []; }, // ── 采集统计Tab ────────────────────────────────────────────────────── renderStatsTab(container) { container.innerHTML = `
`; this.refreshStats(container); }, async refreshStats(container) { const data = await App.api.collect.stats(); const box = container || document.getElementById('collect-tab-content'); if (!box) return; if (!data) { box.innerHTML = '
'; return; } const sources = data.sources || []; const labelMap = { manual: '手动录入', cdp_buyin: 'Playwright采集(巨量百应)', import: '批量导入', manual_batch: '批量粘贴' }; let html = `
| 来源 | 达人数 | 平均粉丝 | 平均30天GMV |
|---|---|---|---|
| ${labelMap[s.source] || s.source} | ${s.cnt} | ${App.formatFans(Math.round(s.avg_fans || 0))} | ${App.formatMoney(Math.round(s.avg_gmv || 0))} |
'; box.innerHTML = html; }, }; // ==================== 抖音同步页面 ==================== App.pages.douyinSync = { title: '抖音同步', syncing: false, async render() { const container = document.getElementById('main-content'); if (!container) return; container.innerHTML = `
`; this.loadAuthStatus(); this.loadLogs(); }, async loadAuthStatus() { const data = await App.api.douyin.authStatus(); const badge = document.getElementById('auth-badge'); const text = document.getElementById('auth-status-text'); if (!data || !badge || !text) return; if (data.authorized) { badge.className = 'px-3 py-1 rounded-full text-xs font-medium bg-green-100 text-green-700'; badge.textContent = '已授权'; text.textContent = '授权正常,Token有效。抖音数据同步已就绪。'; } else { badge.className = 'px-3 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-700'; badge.textContent = '未授权'; text.textContent = '尚未完成OAuth授权,请在ICP备案通过后完成授权。'; } }, async syncProducts() { this._doSync('商品', () => App.api.douyin.syncProducts()); }, async syncDaren() { this._doSync('达人', () => App.api.douyin.syncDaren()); }, async syncOrders() { const end = new Date(); const start = new Date(end - 30*86400000); const sd = start.toISOString().slice(0,10); const ed = end.toISOString().slice(0,10); this._doSync('订单', () => App.api.douyin.syncOrders({ start_date: sd, end_date: ed })); }, async syncBrands() { this._doSync('品牌', () => App.api.douyin.syncBrands()); }, async syncAll() { this._doSync('全量', () => App.api.douyin.syncAll()); }, async _doSync(label, fn) { if (this.syncing) { App.showToast('同步进行中,请稍后'); return; } this.syncing = true; const btn = document.getElementById('btn-sync-all'); if (btn) btn.disabled = true; App.showToast(`开始同步${label}数据...`); try { const result = await fn(); if (result) { App.showToast(`${label}同步完成`); this.loadLogs(); } } catch(e) { App.showToast(`同步失败: ${e.message}`, 'error'); } this.syncing = false; if (btn) btn.disabled = false; }, async loadLogs() { const logs = await App.api.douyin.syncLogs(30); const tbody = document.getElementById('sync-log-body'); if (!tbody) return; const typeLabels = { product: '商品', daren: '达人', order: '订单', brand: '品牌' }; const statusColors = { success: 'text-green-600', running: 'text-blue-600', failed: 'text-red-600', pending: 'text-gray-400' }; if (!logs || logs.length === 0) { tbody.innerHTML = '
'; return; } tbody.innerHTML = logs.map(l => `
`).join(''); }, }; // ==================== 初始化 ==================== document.addEventListener('DOMContentLoaded', () => { initParticleEngine(); App.router.init(); });