feat(admin): 添加 Valine 评论导入支持并重构导入流程
This commit is contained in:
@@ -95,7 +95,8 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"cwd": "CWD (.json)",
|
"cwd": "CWD (.json)",
|
||||||
"twikoo": "Twikoo (.json)",
|
"twikoo": "Twikoo (.json)",
|
||||||
"artalk": "Artalk (.json)"
|
"artalk": "Artalk (.json)",
|
||||||
|
"valine": "Valine (.jsonl)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@@ -144,15 +145,14 @@
|
|||||||
"logs": {
|
"logs": {
|
||||||
"title": "Operation Logs"
|
"title": "Operation Logs"
|
||||||
},
|
},
|
||||||
"prefixModal": {
|
"siteIdModal": {
|
||||||
"title": "Missing URL Prefix Detected",
|
"title": "Select Target Site",
|
||||||
"descPart1": "Detected ",
|
"desc": "Select the site ID for imported comments, or enter a custom site ID.",
|
||||||
"descPart2": " comments with URL missing protocol prefix (http/https).",
|
"selectLabel": "Select Site",
|
||||||
"descPart3": "Add prefix during import?",
|
"customLabel": "Custom Site ID (Leave empty to use selected site)",
|
||||||
"label": "Domain Prefix (e.g. https://example.me)",
|
"customPlaceholder": "Enter custom site ID",
|
||||||
"placeholder": "Enter domain prefix",
|
"cancel": "Cancel",
|
||||||
"skip": "Skip (Don't Add)",
|
"confirm": "Import"
|
||||||
"confirm": "Add & Import"
|
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"exportSuccess": "Export Success",
|
"exportSuccess": "Export Success",
|
||||||
@@ -163,14 +163,12 @@
|
|||||||
"importStatsSuccess": "Stats Imported",
|
"importStatsSuccess": "Stats Imported",
|
||||||
"importBackupSuccess": "Full Restore Success",
|
"importBackupSuccess": "Full Restore Success",
|
||||||
"importCommentsSuccess": "Comments Imported",
|
"importCommentsSuccess": "Comments Imported",
|
||||||
"prefixRequired": "Please enter domain prefix",
|
|
||||||
"importStart": "Importing: {name} (Mode: {mode})",
|
"importStart": "Importing: {name} (Mode: {mode})",
|
||||||
"fileParseSuccess": "File parsed, processing...",
|
"fileParseSuccess": "File parsed, processing...",
|
||||||
"errorWithMessage": "Error: {msg}",
|
"errorWithMessage": "Error: {msg}",
|
||||||
"parsedCommentsCount": "Parsed {count} comments",
|
"parsedCommentsCount": "Parsed {count} comments",
|
||||||
"detectMissingPrefix": "Detected {count} URLs missing prefix, waiting for confirmation...",
|
"siteIdSelected": "Site selected: {siteId}",
|
||||||
"prefixAdded": "Prefix added, continuing...",
|
"importCancelled": "Import cancelled",
|
||||||
"skipPrefix": "Skipped prefix addition",
|
|
||||||
"readFileFailedLog": "Read file failed",
|
"readFileFailedLog": "Read file failed",
|
||||||
"importCommentsDone": "Import Done: {message}"
|
"importCommentsDone": "Import Done: {message}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,8 @@
|
|||||||
"source": {
|
"source": {
|
||||||
"cwd": "CWD (.json)",
|
"cwd": "CWD (.json)",
|
||||||
"twikoo": "Twikoo (.json)",
|
"twikoo": "Twikoo (.json)",
|
||||||
"artalk": "Artalk (.json)"
|
"artalk": "Artalk (.json)",
|
||||||
|
"valine": "Valine (.jsonl)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
@@ -144,15 +145,14 @@
|
|||||||
"logs": {
|
"logs": {
|
||||||
"title": "操作日志"
|
"title": "操作日志"
|
||||||
},
|
},
|
||||||
"prefixModal": {
|
"siteIdModal": {
|
||||||
"title": "检测到 URL 缺失前缀",
|
"title": "选择目标站点",
|
||||||
"descPart1": "检测到 ",
|
"desc": "请选择导入评论所属的站点 ID,或输入自定义站点 ID。",
|
||||||
"descPart2": " 条评论的 URL 不存在域名前缀(http/https)。",
|
"selectLabel": "选择站点",
|
||||||
"descPart3": "是否在导入时统一添加?",
|
"customLabel": "自定义站点 ID(留空则使用上方选择的站点)",
|
||||||
"label": "域名前缀 (例如 https://example.me)",
|
"customPlaceholder": "输入自定义站点 ID",
|
||||||
"placeholder": "请输入域名前缀",
|
"cancel": "取消",
|
||||||
"skip": "直接导入 (不添加)",
|
"confirm": "确认导入"
|
||||||
"confirm": "添加并导入"
|
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"exportSuccess": "导出成功",
|
"exportSuccess": "导出成功",
|
||||||
@@ -163,14 +163,12 @@
|
|||||||
"importStatsSuccess": "统计数据导入成功",
|
"importStatsSuccess": "统计数据导入成功",
|
||||||
"importBackupSuccess": "全量恢复成功",
|
"importBackupSuccess": "全量恢复成功",
|
||||||
"importCommentsSuccess": "评论导入成功",
|
"importCommentsSuccess": "评论导入成功",
|
||||||
"prefixRequired": "请输入域名前缀",
|
|
||||||
"importStart": "开始导入: {name} (模式: {mode})",
|
"importStart": "开始导入: {name} (模式: {mode})",
|
||||||
"fileParseSuccess": "文件解析成功,开始处理...",
|
"fileParseSuccess": "文件解析成功,开始处理...",
|
||||||
"errorWithMessage": "错误: {msg}",
|
"errorWithMessage": "错误: {msg}",
|
||||||
"parsedCommentsCount": "解析到 {count} 条评论数据",
|
"parsedCommentsCount": "解析到 {count} 条评论数据",
|
||||||
"detectMissingPrefix": "检测到 {count} 条 URL 缺失前缀,等待用户确认...",
|
"siteIdSelected": "已选择站点: {siteId}",
|
||||||
"prefixAdded": "已添加前缀,继续导入...",
|
"importCancelled": "导入已取消",
|
||||||
"skipPrefix": "用户跳过前缀添加",
|
|
||||||
"readFileFailedLog": "读取文件失败",
|
"readFileFailedLog": "读取文件失败",
|
||||||
"importCommentsDone": "导入完成: {message}"
|
"importCommentsDone": "导入完成: {message}"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.log-container {
|
.log-container {
|
||||||
margin-top: 16px;
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
background-color: var(--bg-sider);
|
background-color: var(--bg-sider);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
<option value="cwd">{{ t("data.sections.comments.source.cwd") }}</option>
|
<option value="cwd">{{ t("data.sections.comments.source.cwd") }}</option>
|
||||||
<option value="twikoo">{{ t("data.sections.comments.source.twikoo") }}</option>
|
<option value="twikoo">{{ t("data.sections.comments.source.twikoo") }}</option>
|
||||||
<option value="artalk">{{ t("data.sections.comments.source.artalk") }}</option>
|
<option value="artalk">{{ t("data.sections.comments.source.artalk") }}</option>
|
||||||
|
<option value="valine">{{ t("data.sections.comments.source.valine") }}</option>
|
||||||
</select>
|
</select>
|
||||||
<button
|
<button
|
||||||
class="card-button secondary"
|
class="card-button secondary"
|
||||||
@@ -43,6 +44,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 导入日志 -->
|
||||||
|
<div v-if="importLogs.length > 0" class="log-container">
|
||||||
|
<div class="log-title">{{ t("data.logs.title") }}</div>
|
||||||
|
<div class="log-list">
|
||||||
|
<div v-for="(log, index) in importLogs" :key="index" class="log-item">
|
||||||
|
{{ log }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 2. 系统配置 -->
|
<!-- 2. 系统配置 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3 class="card-title">{{ t("data.sections.config.title") }}</h3>
|
<h3 class="card-title">{{ t("data.sections.config.title") }}</h3>
|
||||||
@@ -176,46 +187,41 @@
|
|||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
ref="fileInput"
|
ref="fileInput"
|
||||||
accept=".json"
|
accept=".json,.jsonl"
|
||||||
style="display: none"
|
style="display: none"
|
||||||
@change="handleFileChange"
|
@change="handleFileChange"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 导入日志 -->
|
<!-- site_id 选择弹窗 -->
|
||||||
<div v-if="importLogs.length > 0" class="log-container">
|
<div v-if="showSiteIdModal" class="modal-overlay">
|
||||||
<div class="log-title">{{ t("data.logs.title") }}</div>
|
|
||||||
<div class="log-list">
|
|
||||||
<div v-for="(log, index) in importLogs" :key="index" class="log-item">
|
|
||||||
{{ log }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 前缀确认弹窗 -->
|
|
||||||
<div v-if="showPrefixModal" class="modal-overlay">
|
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
<h3 class="modal-title">{{ t("data.prefixModal.title") }}</h3>
|
<h3 class="modal-title">{{ t("data.siteIdModal.title") }}</h3>
|
||||||
<p class="modal-desc">
|
<p class="modal-desc">
|
||||||
{{ t("data.prefixModal.descPart1") }}
|
{{ t("data.siteIdModal.desc") }}
|
||||||
<strong>{{ missingPrefixCount }}</strong>
|
|
||||||
{{ t("data.prefixModal.descPart2") }}<br />
|
|
||||||
{{ t("data.prefixModal.descPart3") }}
|
|
||||||
</p>
|
</p>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">{{ t("data.prefixModal.label") }}</label>
|
<label class="form-label">{{ t("data.siteIdModal.selectLabel") }}</label>
|
||||||
|
<select v-model="selectedSiteId" class="form-select" style="width: 100%">
|
||||||
|
<option value="default">{{ t("layout.defaultSite") }}</option>
|
||||||
|
<option v-for="item in siteOptions" :key="item.value" :value="item.value">
|
||||||
|
{{ item.label }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">{{ t("data.siteIdModal.customLabel") }}</label>
|
||||||
<input
|
<input
|
||||||
v-model="urlPrefix"
|
v-model="customSiteId"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
:placeholder="t('data.prefixModal.placeholder')"
|
:placeholder="t('data.siteIdModal.customPlaceholder')"
|
||||||
@keyup.enter="confirmPrefix"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="modal-btn secondary" @click="cancelPrefix">
|
<button class="modal-btn secondary" @click="cancelSiteId">
|
||||||
{{ t("data.prefixModal.skip") }}
|
{{ t("data.siteIdModal.cancel") }}
|
||||||
</button>
|
</button>
|
||||||
<button class="modal-btn primary" @click="confirmPrefix">
|
<button class="modal-btn primary" @click="confirmSiteId">
|
||||||
{{ t("data.prefixModal.confirm") }}
|
{{ t("data.siteIdModal.confirm") }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -245,6 +251,7 @@ import {
|
|||||||
saveS3Settings,
|
saveS3Settings,
|
||||||
triggerS3Backup,
|
triggerS3Backup,
|
||||||
S3SettingsResponse,
|
S3SettingsResponse,
|
||||||
|
fetchSiteList,
|
||||||
} from "../../api/admin";
|
} from "../../api/admin";
|
||||||
import S3BackupModal from "./components/S3BackupModal.vue";
|
import S3BackupModal from "./components/S3BackupModal.vue";
|
||||||
import { useSite } from "../../composables/useSite";
|
import { useSite } from "../../composables/useSite";
|
||||||
@@ -264,10 +271,11 @@ const { currentSiteId } = useSite();
|
|||||||
// 当前导入模式: comments | config | stats | backup
|
// 当前导入模式: comments | config | stats | backup
|
||||||
const currentImportMode = ref<string>("comments");
|
const currentImportMode = ref<string>("comments");
|
||||||
|
|
||||||
// 前缀处理相关状态
|
// site_id 选择相关状态
|
||||||
const showPrefixModal = ref(false);
|
const showSiteIdModal = ref(false);
|
||||||
const urlPrefix = ref("");
|
const siteOptions = ref<{ label: string; value: string }[]>([]);
|
||||||
const missingPrefixCount = ref(0);
|
const selectedSiteId = ref("");
|
||||||
|
const customSiteId = ref("");
|
||||||
const pendingJson = ref<any[]>([]);
|
const pendingJson = ref<any[]>([]);
|
||||||
const s3Config = ref<S3SettingsResponse>({
|
const s3Config = ref<S3SettingsResponse>({
|
||||||
endpoint: "",
|
endpoint: "",
|
||||||
@@ -323,8 +331,27 @@ function handleS3BackupModalClose() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadS3Config();
|
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") {
|
function showToast(msg: string, type: "success" | "error" = "success") {
|
||||||
toastMessage.value = msg;
|
toastMessage.value = msg;
|
||||||
toastType.value = type;
|
toastType.value = type;
|
||||||
@@ -395,13 +422,19 @@ async function handleFileChange(event: Event) {
|
|||||||
try {
|
try {
|
||||||
const content = e.target?.result as string;
|
const content = e.target?.result as string;
|
||||||
let json;
|
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) {
|
switch (currentImportMode.value) {
|
||||||
case "comments":
|
case "comments":
|
||||||
@@ -434,6 +467,29 @@ async function handleFileChange(event: Event) {
|
|||||||
reader.readAsText(file);
|
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) {
|
async function processImportConfig(data: any) {
|
||||||
const res = await importConfig(data);
|
const res = await importConfig(data);
|
||||||
@@ -460,31 +516,21 @@ async function processImportComments(json: any) {
|
|||||||
const comments = Array.isArray(json) ? json : [json];
|
const comments = Array.isArray(json) ? json : [json];
|
||||||
addLog(t("data.messages.parsedCommentsCount", { count: comments.length }));
|
addLog(t("data.messages.parsedCommentsCount", { count: comments.length }));
|
||||||
|
|
||||||
// 检查 URL 前缀 (仅针对评论导入)
|
// 弹出 site_id 选择弹窗
|
||||||
let missingCount = 0;
|
pendingJson.value = comments;
|
||||||
for (const item of comments) {
|
selectedSiteId.value = currentSiteId.value || "default";
|
||||||
const url = item.href || item.page_key || item.post_slug;
|
customSiteId.value = "";
|
||||||
if (url && typeof url === "string") {
|
showSiteIdModal.value = true;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function executeImportComments(comments: any[]) {
|
async function executeImportComments(comments: any[], siteId?: string) {
|
||||||
try {
|
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 }));
|
addLog(t("data.messages.importCommentsDone", { message: res.message }));
|
||||||
showToast(t("data.messages.importCommentsSuccess"));
|
showToast(t("data.messages.importCommentsSuccess"));
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -495,59 +541,19 @@ async function executeImportComments(comments: any[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 前缀确认逻辑
|
// site_id 确认逻辑
|
||||||
async function confirmPrefix() {
|
async function confirmSiteId() {
|
||||||
if (!urlPrefix.value) {
|
const siteId = customSiteId.value.trim() || selectedSiteId.value || "default";
|
||||||
showToast(t("data.messages.prefixRequired"), "error");
|
addLog(t("data.messages.siteIdSelected", { siteId }));
|
||||||
return;
|
showSiteIdModal.value = false;
|
||||||
}
|
await executeImportComments(pendingJson.value, siteId);
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cancelPrefix() {
|
function cancelSiteId() {
|
||||||
showPrefixModal.value = false;
|
showSiteIdModal.value = false;
|
||||||
addLog(t("data.messages.skipPrefix"));
|
addLog(t("data.messages.importCancelled"));
|
||||||
executeImportComments(pendingJson.value);
|
importing.value = false;
|
||||||
}
|
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;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,49 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
return c.json({ message: '导入数据为空' }, 400);
|
return c.json({ message: '导入数据为空' }, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 映射 Twikoo / Artalk 数据结构到 CWD 结构
|
// 映射 Twikoo / Artalk / Valine 数据结构到 CWD 结构
|
||||||
const comments = rawComments.map((item: any) => {
|
const comments = rawComments.map((item: any) => {
|
||||||
|
// Valine/LeanCloud 特征检测
|
||||||
|
const isValine =
|
||||||
|
item.objectId !== undefined &&
|
||||||
|
item.insertedAt !== undefined &&
|
||||||
|
item.nick !== undefined;
|
||||||
// Twikoo 特征检测
|
// Twikoo 特征检测
|
||||||
const isTwikoo =
|
const isTwikoo =
|
||||||
item.href !== undefined || item.nick !== undefined || item.comment !== undefined;
|
item.href !== undefined || item.comment !== undefined;
|
||||||
// Artalk 特征检测 (page_key 是 Artalk 特有的)
|
// Artalk 特征检测 (page_key 是 Artalk 特有的)
|
||||||
const isArtalk = item.page_key !== undefined && item.content !== undefined;
|
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) {
|
if (isArtalk) {
|
||||||
// Artalk 映射逻辑
|
// Artalk 映射逻辑
|
||||||
// 处理 ID: Artalk ID 通常是数字
|
// 处理 ID: Artalk ID 通常是数字
|
||||||
@@ -49,7 +84,8 @@ export const importComments = async (c: Context<{ Bindings: Bindings }>) => {
|
|||||||
content_text: item.content || '', // >>> content
|
content_text: item.content || '', // >>> content
|
||||||
content_html: item.content || '', // >>> content
|
content_html: item.content || '', // >>> content
|
||||||
parent_id: null, // >>> 保持 null
|
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_text: item.comment || "", // >>> comment
|
||||||
content_html: item.comment || "", // >>> comment
|
content_html: item.comment || "", // >>> comment
|
||||||
parent_id: null, // >>> 保持 null
|
parent_id: null, // >>> 保持 null
|
||||||
status: "approved" // >>> approved
|
status: "approved", // >>> approved
|
||||||
|
site_id: item.site_id
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user