From 78e263faa457ad449b449101aeab8cc07728828d Mon Sep 17 00:00:00 2001 From: anghunk Date: Tue, 20 Jan 2026 14:17:45 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E6=95=B0=E6=8D=AE=E5=AF=BC=E5=85=A5):=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0URL=E5=89=8D=E7=BC=80=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E5=BC=B9=E7=AA=97=E5=8F=8A=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 检测到导入数据中URL缺少http/https前缀时,显示弹窗让用户确认是否添加前缀。包含以下改进: - 添加前缀确认弹窗组件及样式 - 实现前缀拼接逻辑 - 优化导入流程,支持中断等待用户确认 - 添加相关状态管理和样式 --- cwd-comments-admin/src/views/DataView.vue | 292 +++++++++++++++++++--- 1 file changed, 251 insertions(+), 41 deletions(-) diff --git a/cwd-comments-admin/src/views/DataView.vue b/cwd-comments-admin/src/views/DataView.vue index 4d81183..a2a834c 100644 --- a/cwd-comments-admin/src/views/DataView.vue +++ b/cwd-comments-admin/src/views/DataView.vue @@ -8,12 +8,10 @@ > {{ toastMessage }} - +

数据导出

-

- 将所有评论数据导出为 JSON 格式。 -

+

将所有评论数据导出为 JSON 格式。

- +
导入日志
-
+
{{ log }} -
+
+
+
+
+ + + @@ -74,6 +97,12 @@ const toastType = ref<"success" | "error">("success"); const toastVisible = ref(false); const importLogs = ref([]); +// 前缀处理相关状态 +const showPrefixModal = ref(false); +const urlPrefix = ref(""); +const missingPrefixCount = ref(0); +const pendingJson = ref([]); + function showToast(msg: string, type: "success" | "error" = "success") { toastMessage.value = msg; toastType.value = type; @@ -86,11 +115,11 @@ function showToast(msg: string, type: "success" | "error" = "success") { function addLog(msg: string) { const now = new Date(); const y = now.getFullYear(); - const m = String(now.getMonth() + 1).padStart(2, '0'); - const d = String(now.getDate()).padStart(2, '0'); - const h = String(now.getHours()).padStart(2, '0'); - const min = String(now.getMinutes()).padStart(2, '0'); - const s = String(now.getSeconds()).padStart(2, '0'); + const m = String(now.getMonth() + 1).padStart(2, "0"); + const d = String(now.getDate()).padStart(2, "0"); + const h = String(now.getHours()).padStart(2, "0"); + const min = String(now.getMinutes()).padStart(2, "0"); + const s = String(now.getSeconds()).padStart(2, "0"); const timeStr = `${y}.${m}.${d} ${h}:${min}:${s}`; importLogs.value.push(`${timeStr} ${msg}`); } @@ -99,11 +128,11 @@ async function handleExport() { exporting.value = true; try { const data = await exportComments(); - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); const url = window.URL.createObjectURL(blob); - const a = document.createElement('a'); + const a = document.createElement("a"); a.href = url; - a.download = `comments-export-${new Date().toISOString().split('T')[0]}.json`; + a.download = `comments-export-${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); @@ -126,10 +155,13 @@ async function handleFileChange(event: Event) { if (!file) return; // 重置 input,允许重复选择同一文件 - target.value = ''; - + target.value = ""; + // 清空之前的日志 importLogs.value = []; + showPrefixModal.value = false; + urlPrefix.value = ""; + pendingJson.value = []; importing.value = true; addLog(`开始导入文件 ${file.name}`); @@ -141,29 +173,44 @@ async function handleFileChange(event: Event) { try { const content = e.target?.result as string; addLog("文件读取完成,正在解析数据..."); - + let json; try { json = JSON.parse(content); } catch (parseError) { throw new Error("JSON 解析失败,请检查文件格式"); } - - const count = Array.isArray(json) ? json.length : 1; + + const comments = Array.isArray(json) ? json : [json]; + const count = comments.length; addLog(`解析成功,共 ${count} 条数据`); - addLog("正在上传并导入数据库..."); - - // 这里可以根据 importSource 做一些预处理,目前只有 twikoo/通用 - // 假设 json 已经是我们要的格式 - - const res = await importComments(json); - addLog(`导入完成: ${res.message || '成功'}`); - showToast(res.message || "导入成功", "success"); + + // 检测前缀逻辑 + let missingCount = 0; + for (const item of comments) { + // 优先检查 Twikoo 字段 href,其次检查 CWD 字段 post_slug + const url = item.href || item.post_slug; + if (url && typeof url === "string") { + if (!url.startsWith("http://") && !url.startsWith("https://")) { + missingCount++; + } + } + } + + if (missingCount > 0) { + addLog(`检测到 ${missingCount} 条评论 URL 缺少前缀,等待确认...`); + missingPrefixCount.value = missingCount; + pendingJson.value = comments; + showPrefixModal.value = true; + // 暂停在这里,等待用户操作 modal + } else { + // 无需处理,直接导入 + await executeImport(comments); + } } catch (err: any) { console.error(err); - addLog(`导入失败: ${err.message || '未知错误'}`); + addLog(`导入失败: ${err.message || "未知错误"}`); showToast(err.message || "导入失败,文件格式错误", "error"); - } finally { importing.value = false; } }; @@ -176,9 +223,171 @@ async function handleFileChange(event: Event) { reader.readAsText(file); } + +async function confirmPrefix() { + if (!urlPrefix.value) { + showToast("请输入域名前缀", "error"); + return; + } + + // 处理前缀,确保以 / 结尾或拼接正确 + let prefix = urlPrefix.value.trim(); + // 简单处理:如果 prefix 不以 / 结尾,且 url 不以 / 开头,可能需要补 /。 + // 但通常用户输入的域名可能带也可能不带。 + // 这里假设用户输入的 prefix 是 "https://example.com",而 href 是 "/posts/1" 或 "posts/1" + // 为了安全,如果 prefix 结尾没有 /,且 target 不以 / 开头,中间加个 /。 + // 但更简单的策略是直接拼,让用户自己负责输入正确的。 + // 考虑到用户习惯,我们做个简单的优化:如果 prefix 没 / 且 url 没 /,加一个。 + + const comments = pendingJson.value.map((item) => { + // 浅拷贝 + const newItem = { ...item }; + + // Twikoo + if (newItem.href && typeof newItem.href === "string") { + if (!newItem.href.startsWith("http://") && !newItem.href.startsWith("https://")) { + newItem.href = joinUrl(prefix, newItem.href); + } + } + + // CWD + if (newItem.post_slug && typeof newItem.post_slug === "string") { + if ( + !newItem.post_slug.startsWith("http://") && + !newItem.post_slug.startsWith("https://") + ) { + newItem.post_slug = joinUrl(prefix, newItem.post_slug); + } + } + + return newItem; + }); + + showPrefixModal.value = false; + addLog(`已为 ${missingPrefixCount.value} 条数据添加前缀`); + await executeImport(comments); +} + +function joinUrl(prefix: string, path: string): string { + if (prefix.endsWith("/") && path.startsWith("/")) { + return prefix + path.substring(1); + } + if (!prefix.endsWith("/") && !path.startsWith("/")) { + return prefix + "/" + path; + } + return prefix + path; +} + +function cancelPrefix() { + showPrefixModal.value = false; + addLog("用户选择跳过添加前缀,直接导入"); + executeImport(pendingJson.value); +} + +async function executeImport(comments: any[]) { + addLog("正在上传并导入数据库..."); + try { + const res = await importComments(comments); + addLog(`导入完成: ${res.message || "成功"}`); + showToast(res.message || "导入成功", "success"); + } catch (err: any) { + console.error(err); + addLog(`导入失败: ${err.message || "未知错误"}`); + showToast(err.message || "导入失败", "error"); + } finally { + importing.value = false; + pendingJson.value = []; + } +}