-
{{ t("data.prefixModal.title") }}
+
{{ t("data.siteIdModal.title") }}
- {{ t("data.prefixModal.descPart1") }}
- {{ missingPrefixCount }}
- {{ t("data.prefixModal.descPart2") }}
- {{ t("data.prefixModal.descPart3") }}
+ {{ t("data.siteIdModal.desc") }}
-
+
+
+
+
+
-
@@ -245,6 +251,7 @@ import {
saveS3Settings,
triggerS3Backup,
S3SettingsResponse,
+ fetchSiteList,
} from "../../api/admin";
import S3BackupModal from "./components/S3BackupModal.vue";
import { useSite } from "../../composables/useSite";
@@ -264,10 +271,11 @@ const { currentSiteId } = useSite();
// 当前导入模式: comments | config | stats | backup
const currentImportMode = ref
("comments");
-// 前缀处理相关状态
-const showPrefixModal = ref(false);
-const urlPrefix = ref("");
-const missingPrefixCount = ref(0);
+// site_id 选择相关状态
+const showSiteIdModal = ref(false);
+const siteOptions = ref<{ label: string; value: string }[]>([]);
+const selectedSiteId = ref("");
+const customSiteId = ref("");
const pendingJson = ref([]);
const s3Config = ref({
endpoint: "",
@@ -323,8 +331,27 @@ function handleS3BackupModalClose() {
onMounted(() => {
loadS3Config();
+ loadSiteOptions();
});
+async function loadSiteOptions() {
+ try {
+ const res = await fetchSiteList();
+ const sites = Array.isArray(res.sites) ? res.sites : [];
+ const unique = Array.from(new Set(sites));
+ siteOptions.value = unique
+ .filter((s) => s && s !== "default")
+ .map((s) => ({
+ label: s,
+ value: s,
+ }));
+ // 默认选择当前站点
+ selectedSiteId.value = currentSiteId.value || "default";
+ } catch {
+ siteOptions.value = [];
+ }
+}
+
function showToast(msg: string, type: "success" | "error" = "success") {
toastMessage.value = msg;
toastType.value = type;
@@ -395,13 +422,19 @@ async function handleFileChange(event: Event) {
try {
const content = e.target?.result as string;
let json;
- try {
- json = JSON.parse(content);
- } catch (parseError) {
- throw new Error(t("data.messages.jsonParseFailed"));
- }
- addLog(t("data.messages.fileParseSuccess"));
+ // Valine 使用 NDJSON 格式,需要特殊处理
+ if (currentImportMode.value === "comments" && importSource.value === "valine") {
+ json = parseNDJSON(content);
+ addLog(t("data.messages.fileParseSuccess"));
+ } else {
+ try {
+ json = JSON.parse(content);
+ } catch (parseError) {
+ throw new Error(t("data.messages.jsonParseFailed"));
+ }
+ addLog(t("data.messages.fileParseSuccess"));
+ }
switch (currentImportMode.value) {
case "comments":
@@ -434,6 +467,29 @@ async function handleFileChange(event: Event) {
reader.readAsText(file);
}
+// 解析 NDJSON (JSON-streaming) 格式,用于 Valine/LeanCloud 导出数据
+function parseNDJSON(content: string): any[] {
+ const lines = content.split("\n");
+ const results: any[] = [];
+
+ for (let i = 0; i < lines.length; i++) {
+ const line = lines[i].trim();
+ // 跳过空行和文件类型声明行
+ if (!line || line.startsWith("#filetype:")) {
+ continue;
+ }
+ try {
+ const obj = JSON.parse(line);
+ results.push(obj);
+ } catch (e) {
+ // 忽略解析失败的行,继续处理下一行
+ console.warn(`Failed to parse line ${i + 1}: ${line.substring(0, 50)}...`);
+ }
+ }
+
+ return results;
+}
+
// 导入处理逻辑
async function processImportConfig(data: any) {
const res = await importConfig(data);
@@ -460,31 +516,21 @@ async function processImportComments(json: any) {
const comments = Array.isArray(json) ? json : [json];
addLog(t("data.messages.parsedCommentsCount", { count: comments.length }));
- // 检查 URL 前缀 (仅针对评论导入)
- let missingCount = 0;
- for (const item of comments) {
- const url = item.href || item.page_key || item.post_slug;
- if (url && typeof url === "string") {
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
- missingCount++;
- }
- }
- }
-
- if (missingCount > 0) {
- addLog(t("data.messages.detectMissingPrefix", { count: missingCount }));
- missingPrefixCount.value = missingCount;
- pendingJson.value = comments;
- showPrefixModal.value = true;
- // 暂停,等待 Modal 操作
- } else {
- await executeImportComments(comments);
- }
+ // 弹出 site_id 选择弹窗
+ pendingJson.value = comments;
+ selectedSiteId.value = currentSiteId.value || "default";
+ customSiteId.value = "";
+ showSiteIdModal.value = true;
}
-async function executeImportComments(comments: any[]) {
+async function executeImportComments(comments: any[], siteId?: string) {
try {
- const res = await importComments(comments);
+ // 为所有评论设置 site_id
+ const commentsWithSiteId = comments.map((item) => ({
+ ...item,
+ site_id: siteId || currentSiteId.value || "default",
+ }));
+ const res = await importComments(commentsWithSiteId);
addLog(t("data.messages.importCommentsDone", { message: res.message }));
showToast(t("data.messages.importCommentsSuccess"));
} catch (err: any) {
@@ -495,59 +541,19 @@ async function executeImportComments(comments: any[]) {
}
}
-// 前缀确认逻辑
-async function confirmPrefix() {
- if (!urlPrefix.value) {
- showToast(t("data.messages.prefixRequired"), "error");
- return;
- }
-
- let prefix = urlPrefix.value.trim();
- 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);
- }
- }
- // Artalk
- if (newItem.page_key && typeof newItem.page_key === "string") {
- if (
- !newItem.page_key.startsWith("http://") &&
- !newItem.page_key.startsWith("https://")
- ) {
- newItem.page_key = joinUrl(prefix, newItem.page_key);
- }
- }
- // 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(t("data.messages.prefixAdded"));
- await executeImportComments(comments);
+// site_id 确认逻辑
+async function confirmSiteId() {
+ const siteId = customSiteId.value.trim() || selectedSiteId.value || "default";
+ addLog(t("data.messages.siteIdSelected", { siteId }));
+ showSiteIdModal.value = false;
+ await executeImportComments(pendingJson.value, siteId);
}
-function cancelPrefix() {
- showPrefixModal.value = false;
- addLog(t("data.messages.skipPrefix"));
- executeImportComments(pendingJson.value);
-}
-
-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 cancelSiteId() {
+ showSiteIdModal.value = false;
+ addLog(t("data.messages.importCancelled"));
+ importing.value = false;
+ pendingJson.value = [];
}
diff --git a/cwd-api/src/api/admin/importComments.ts b/cwd-api/src/api/admin/importComments.ts
index 07df044..1713331 100644
--- a/cwd-api/src/api/admin/importComments.ts
+++ b/cwd-api/src/api/admin/importComments.ts
@@ -10,14 +10,49 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
return c.json({ message: '导入数据为空' }, 400);
}
- // 映射 Twikoo / Artalk 数据结构到 CWD 结构
+ // 映射 Twikoo / Artalk / Valine 数据结构到 CWD 结构
const comments = rawComments.map((item: any) => {
+ // Valine/LeanCloud 特征检测
+ const isValine =
+ item.objectId !== undefined &&
+ item.insertedAt !== undefined &&
+ item.nick !== undefined;
// Twikoo 特征检测
const isTwikoo =
- item.href !== undefined || item.nick !== undefined || item.comment !== undefined;
+ item.href !== undefined || item.comment !== undefined;
// Artalk 特征检测 (page_key 是 Artalk 特有的)
const isArtalk = item.page_key !== undefined && item.content !== undefined;
+ if (isValine) {
+ // Valine/LeanCloud 映射逻辑
+ // 处理时间: insertedAt 是嵌套对象 { __type: "Date", iso: "ISO字符串" }
+ let created = Date.now();
+ if (item.insertedAt && item.insertedAt.iso) {
+ created = new Date(item.insertedAt.iso).getTime();
+ } else if (item.createdAt) {
+ created = new Date(item.createdAt).getTime();
+ }
+
+ return {
+ created, // >>> insertedAt.iso 转为时间戳
+ post_slug: item.url || '', // >>> url
+ post_url: item.url || '', // >>> url
+ name: item.nick || 'Anonymous', // >>> nick
+ email: item.mail || '', // >>> mail
+ url: item.link || null, // >>> link
+ ip_address: item.ip || null, // >>> ip
+ device: null, // >>> 保持空
+ os: null, // >>> 保持空
+ browser: null, // >>> 保持空
+ ua: item.ua || null, // >>> ua
+ content_text: item.comment || '', // >>> comment
+ content_html: item.comment || '', // >>> comment
+ parent_id: null, // >>> 保持 null
+ status: 'approved', // >>> 保持 "approved"
+ site_id: item.site_id
+ };
+ }
+
if (isArtalk) {
// Artalk 映射逻辑
// 处理 ID: Artalk ID 通常是数字
@@ -49,7 +84,8 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
content_text: item.content || '', // >>> content
content_html: item.content || '', // >>> content
parent_id: null, // >>> 保持 null
- status: 'approved' // >>> 保持 "approved"
+ status: 'approved', // >>> 保持 "approved"
+ site_id: item.site_id
};
}
@@ -86,7 +122,8 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
content_text: item.comment || "", // >>> comment
content_html: item.comment || "", // >>> comment
parent_id: null, // >>> 保持 null
- status: "approved" // >>> approved
+ status: "approved", // >>> approved
+ site_id: item.site_id
};
}