提取进度
+等待提交请求。
+diff --git a/app.js b/app.js new file mode 100644 index 0000000..a7f69a3 --- /dev/null +++ b/app.js @@ -0,0 +1,178 @@ +const form = document.getElementById("comment-form"); +const statusText = document.getElementById("statusText"); +const statusDetails = document.getElementById("statusDetails"); +const submitBtn = document.getElementById("submitBtn"); +const apiKeyPreview = document.getElementById("api-key-preview"); + +const setStatus = (message, details = [], tone = "idle") => { + statusText.textContent = message; + statusDetails.innerHTML = ""; + details.forEach((detail) => { + const pill = document.createElement("span"); + pill.textContent = detail; + statusDetails.appendChild(pill); + }); + + const dot = document.querySelector(".status-dot"); + const colors = { + idle: "#94a3b8", + working: "#2563eb", + success: "#16a34a", + error: "#dc2626", + }; + dot.style.background = colors[tone] || colors.idle; +}; + +const escapeCsv = (value) => { + if (value === null || value === undefined) { + return ""; + } + const stringValue = String(value).replace(/\r?\n/g, " "); + if (stringValue.includes(",") || stringValue.includes('"')) { + return `"${stringValue.replace(/"/g, '""')}"`; + } + return stringValue; +}; + +const createCsv = (rows) => { + const header = [ + "评论ID", + "用户昵称", + "评论内容", + "点赞数", + "发布时间", + "IP/城市", + "原始链接", + ]; + const lines = [header.join(",")]; + rows.forEach((row) => { + lines.push( + [ + escapeCsv(row.id), + escapeCsv(row.nickname), + escapeCsv(row.content), + escapeCsv(row.likes), + escapeCsv(row.createdAt), + escapeCsv(row.city), + escapeCsv(row.sourceUrl), + ].join(",") + ); + }); + return lines.join("\n"); +}; + +const downloadCsv = (csv, filename) => { + const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(url); +}; + +const normalizeComments = (payload) => { + const list = + payload?.data?.comments || + payload?.data?.items || + payload?.comments || + payload?.items || + payload || + []; + + if (!Array.isArray(list)) { + return []; + } + + return list.map((item) => ({ + id: item.id || item.comment_id || item.cid || "", + nickname: item.nickname || item.user_name || item.author || "", + content: item.content || item.text || "", + likes: item.likes || item.like_count || item.digg_count || 0, + createdAt: item.created_at || item.time || item.publish_time || "", + city: item.city || item.ip_location || item.region || "", + sourceUrl: item.source_url || item.url || "", + })); +}; + +form.addEventListener("submit", async (event) => { + event.preventDefault(); + submitBtn.disabled = true; + + const apiKey = document.getElementById("apiKey").value.trim(); + const apiEndpoint = document.getElementById("apiEndpoint").value.trim(); + const videoUrl = document.getElementById("videoUrl").value.trim(); + const limit = Number(document.getElementById("limit").value); + const order = document.getElementById("order").value; + const minLikes = Number(document.getElementById("minLikes").value || 0); + const keywords = document.getElementById("keywords").value.trim(); + const requirements = document.getElementById("requirements").value.trim(); + + apiKeyPreview.textContent = apiKey ? apiKey : "未填写"; + + setStatus("正在提交 API 请求...", ["准备中"], "working"); + + try { + const response = await fetch(apiEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${apiKey}`, + }, + body: JSON.stringify({ + url: videoUrl, + limit, + order, + min_likes: minLikes, + keywords: keywords ? keywords.split(/\s+/) : [], + requirements, + }), + }); + + if (!response.ok) { + throw new Error(`API 请求失败:${response.status}`); + } + + const payload = await response.json(); + let comments = normalizeComments(payload); + + if (minLikes > 0) { + comments = comments.filter((comment) => Number(comment.likes) >= minLikes); + } + + if (keywords) { + const keywordList = keywords.split(/\s+/).filter(Boolean); + comments = comments.filter((comment) => + keywordList.every((keyword) => comment.content.includes(keyword)) + ); + } + + if (order === "likes_desc") { + comments.sort((a, b) => Number(b.likes) - Number(a.likes)); + } else if (order === "time_desc") { + comments.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt))); + } else if (order === "time_asc") { + comments.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt))); + } + + const limitedComments = comments.slice(0, limit); + const csv = createCsv(limitedComments); + downloadCsv(csv, "douyin-comments.csv"); + + setStatus( + `已成功导出 ${limitedComments.length} 条评论。`, + ["已下载 CSV", `排序:${order}`, `最少点赞:${minLikes}`], + "success" + ); + } catch (error) { + setStatus( + "提取失败,请检查 API 配置或稍后重试。", + [error.message || "未知错误"], + "error" + ); + } finally { + submitBtn.disabled = false; + } +}); diff --git a/index.html b/index.html new file mode 100644 index 0000000..e975603 --- /dev/null +++ b/index.html @@ -0,0 +1,122 @@ + + +
+ + +DataMiner
++ 输入抖音视频链接、提取数量与要求,自动调用 API 拉取评论并导出 Excel(CSV)。 +
+sd_56d36aa0c0533600bb8a0ebde9c6c8cc
+等待提交请求。
+