2026-10-22 16:49:25 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="migration-center">
|
|
|
|
|
|
<div class="heading"><h2>CMS 迁移与数据核对</h2><p>导入前预览,保留原 URL;复杂关系和 Elementor 布局不会被静默丢弃。</p></div>
|
|
|
|
|
|
<el-card shadow="never"><el-form label-position="top">
|
|
|
|
|
|
<el-row :gutter="20"><el-col :span="8"><el-form-item label="目标网站"><el-select v-model="siteId" style="width:100%"><el-option v-for="s in sites" :key="s.id" :label="s.name" :value="s.id" /></el-select></el-form-item></el-col>
|
|
|
|
|
|
<el-col :span="8"><el-form-item label="原站系统"><el-select v-model="platform" style="width:100%"><el-option label="WordPress REST JSON" value="wordpress-rest"/><el-option label="WooCommerce JSON" value="woocommerce"/><el-option label="Drupal JSON:API" value="drupal-jsonapi"/><el-option label="Shopify Products JSON" value="shopify"/></el-select></el-form-item></el-col>
|
|
|
|
|
|
<el-col :span="8"><el-form-item label="导入文件"><input type="file" accept="application/json,.json" @change="setFile" /></el-form-item></el-col></el-row>
|
|
|
|
|
|
<el-space><el-button type="primary" :loading="loading" @click="preview">检查字段及差异</el-button><el-button :disabled="!report || report.errors.length>0" type="success" :loading="loading" @click="commit">确认导入</el-button></el-space>
|
|
|
|
|
|
</el-form></el-card>
|
|
|
|
|
|
<el-row v-if="report" :gutter="14" style="margin-top:15px"><el-col :span="8"><el-card>原记录:{{report.input}}</el-card></el-col><el-col :span="8"><el-card>可映射:{{report.mapped}}</el-card></el-col><el-col :span="8"><el-card>需人工检查:{{report.warnings.length}}</el-card></el-col></el-row>
|
|
|
|
|
|
<el-card v-if="report" shadow="never" style="margin-top:16px"><template #header>数据预览与检查结果</template>
|
|
|
|
|
|
<el-alert v-if="report.errors.length" type="error" :title="`存在 ${report.errors.length} 个错误,修复后才能导入`" :closable="false" />
|
|
|
|
|
|
<el-table :data="report.preview" max-height="300"><el-table-column prop="title" label="标题"/><el-table-column prop="type" label="类型" width="110"/><el-table-column prop="route" label="原路径"/><el-table-column prop="seo_title" label="SEO 标题"/></el-table>
|
|
|
|
|
|
<h4>字段与组件人工复核提示</h4><el-table :data="report.warnings" max-height="260"><el-table-column prop="row" label="记录" width="90"/><el-table-column prop="issue" label="问题" /></el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
<el-card shadow="never" style="margin-top:16px"><template #header><el-space>迁移人工复核 <el-tag type="warning">{{reviews.length}} 待处理</el-tag><el-button text @click="loadReviews">刷新</el-button></el-space></template>
|
|
|
|
|
|
<el-table :data="reviews" max-height="320"><el-table-column prop="platform" label="来源" width="150"/><el-table-column prop="row_number" label="行" width="75"/><el-table-column prop="issue" label="待处理问题"/><el-table-column label="复核" width="130"><template #default="{row}"><el-button link type="primary" @click="resolve(row.id)">处理并确认</el-button></template></el-table-column></el-table>
|
|
|
|
|
|
</el-card>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
2026-10-23 12:31:03 +08:00
|
|
|
|
import {watch,ref,onMounted} from 'vue'
|
2026-10-22 16:49:25 +08:00
|
|
|
|
import {ElMessage,ElMessageBox} from 'element-plus'
|
2026-10-23 12:31:03 +08:00
|
|
|
|
import {preferredSite,rememberSite,sfApi} from './_shared'
|
2026-10-22 16:49:25 +08:00
|
|
|
|
interface Site{id:string;name:string}
|
|
|
|
|
|
interface Report{input:number;mapped:number;errors:Array<{row:number;error:string}>;warnings:Array<{row:number;issue:string}>;preview:Array<{title:string;type:string;route:string;seo_title:string}>}
|
|
|
|
|
|
const sites=ref<Site[]>([]),siteId=ref(''),platform=ref('wordpress-rest'),file=ref<File|null>(null),report=ref<Report|null>(null),loading=ref(false),csrf=ref(''),reviews=ref<Array<{id:string;issue:string;platform:string;row_number:number}>>([])
|
|
|
|
|
|
function setFile(event:Event){file.value=(event.target as HTMLInputElement).files?.[0]||null;report.value=null}
|
2026-10-23 12:31:03 +08:00
|
|
|
|
async function request(url:string,init:RequestInit={}){return sfApi<any>(url,init)}
|
2026-10-22 16:49:25 +08:00
|
|
|
|
async function submit(mode:'preview'|'import'){if(!siteId.value||!file.value)return ElMessage.warning('请选网站和 JSON 文件');const body=new FormData();body.append('file',file.value);body.append('platform',platform.value);if(mode==='import')body.append('conflict','skip');loading.value=true;try{const data=await request(`/api/migrations/sites/${siteId.value}/${mode}`,{method:'POST',headers:{'X-SiteForge-CSRF':csrf.value},body});if(mode==='preview')report.value=data as Report;else{ElMessage.success(`导入完成:${data.result.imported} 条`);report.value=null;await loadReviews()}}catch(e:any){ElMessage.error(e.message)}finally{loading.value=false}}
|
|
|
|
|
|
async function preview(){await submit('preview')}
|
|
|
|
|
|
async function commit(){if(!report.value||report.value.errors.length)return;try{await ElMessageBox.confirm('导入后会写入网站数据库。复杂字段保留人工核对标记,是否继续?','确认导入');await submit('import')}catch{}}
|
|
|
|
|
|
async function loadReviews(){if(!siteId.value)return;try{reviews.value=(await request(`/api/migrations/sites/${siteId.value}/reviews`)).items}catch(e:any){ElMessage.error(e.message)}}
|
|
|
|
|
|
async function resolve(id:string){try{const result=await ElMessageBox.prompt('请输入实际核对结果或修正措施,至少12字','迁移复核',{inputValidator:(v:string)=>v.trim().length>=12||'请说明具体处理情况'});await request(`/api/migrations/sites/${siteId.value}/reviews/${id}/resolve`,{method:'POST',headers:{'Content-Type':'application/json','X-SiteForge-CSRF':csrf.value},body:JSON.stringify({resolution:result.value})});ElMessage.success('已保存复核记录');await loadReviews()}catch(e:any){if(e!=='cancel')ElMessage.error(e.message)}}
|
2026-10-23 12:31:03 +08:00
|
|
|
|
watch(siteId,(id)=>rememberSite(id))
|
|
|
|
|
|
onMounted(async()=>{try{csrf.value=(await request('/api/auth/me')).csrf;sites.value=await request('/api/sites');siteId.value=preferredSite(sites.value);await loadReviews()}catch(e:any){ElMessage.error(e.message)}})
|
2026-10-22 16:49:25 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
<style scoped>.heading{margin-bottom:20px}.heading h2{margin:0;font-size:24px}.heading p{color:var(--el-text-color-secondary)}</style>
|