接入模板迁移前端预览
This commit is contained in:
parent
0fcbd3bcb3
commit
7b9260d8d7
@ -39,6 +39,47 @@ const STATUS_OPTIONS = [
|
||||
|
||||
const MATERIAL_TYPE_OPTIONS = ['3DMAX', 'PSD', '音频', '视频', '模特', '其他']
|
||||
|
||||
type MigrationPreviewField = {
|
||||
key: string
|
||||
label: string
|
||||
type?: string
|
||||
source: 'template' | 'standard'
|
||||
affectedCount?: number
|
||||
}
|
||||
|
||||
type MigrationPreviewResponse = {
|
||||
success: boolean
|
||||
previewOnly: true
|
||||
migratableCount: number
|
||||
message: string
|
||||
sourceTemplate: { id: string; name: string }
|
||||
targetTemplate: { id: string; name: string }
|
||||
migrationBackup: {
|
||||
materialsWithDroppedFields: number
|
||||
droppedTemplateFieldCount: number
|
||||
droppedStandardFieldCount: number
|
||||
}
|
||||
migrationPreview: {
|
||||
totalMaterials: number
|
||||
targetCurrentFields: Array<{
|
||||
key: string
|
||||
label: string
|
||||
type: string
|
||||
required: boolean
|
||||
}>
|
||||
backupFields: {
|
||||
templateFields: MigrationPreviewField[]
|
||||
standardFields: MigrationPreviewField[]
|
||||
}
|
||||
samples: Array<{
|
||||
materialId: string
|
||||
productModel: string | null
|
||||
droppedTemplateFields: MigrationPreviewField[]
|
||||
droppedStandardFields: MigrationPreviewField[]
|
||||
}>
|
||||
}
|
||||
}
|
||||
|
||||
// 默认需要进入分类管理的字段集合,可被后台实时配置覆盖
|
||||
const DEFAULT_CATEGORY_FIELDS = [
|
||||
'categoryName',
|
||||
@ -484,6 +525,9 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
const [migrationSourceTemplate, setMigrationSourceTemplate] = useState<{ id: string; name: string; materialCount: number } | null>(null)
|
||||
const [migrationTargetTemplateId, setMigrationTargetTemplateId] = useState<string>('')
|
||||
const [migrating, setMigrating] = useState(false)
|
||||
const [migrationPreview, setMigrationPreview] = useState<MigrationPreviewResponse | null>(null)
|
||||
const [migrationPreviewLoading, setMigrationPreviewLoading] = useState(false)
|
||||
const [migrationPreviewError, setMigrationPreviewError] = useState<string | null>(null)
|
||||
|
||||
const syncCategoryFieldSet = useCallback((keys: string[]) => {
|
||||
GLOBAL_CATEGORY_FIELDS.clear()
|
||||
@ -1649,6 +1693,8 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
materialCount: templateDetail.materialCount,
|
||||
})
|
||||
setMigrationTargetTemplateId('')
|
||||
setMigrationPreview(null)
|
||||
setMigrationPreviewError(null)
|
||||
setMigrationDialogOpen(true)
|
||||
return
|
||||
}
|
||||
@ -1673,10 +1719,46 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
setDeletingId(null)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMigrationTargetChange = (targetTemplateId: string) => {
|
||||
setMigrationTargetTemplateId(targetTemplateId)
|
||||
setMigrationPreview(null)
|
||||
setMigrationPreviewError(null)
|
||||
}
|
||||
|
||||
const loadMigrationPreview = async () => {
|
||||
if (!migrationSourceTemplate || !migrationTargetTemplateId || migrationPreviewLoading) return
|
||||
|
||||
setMigrationPreviewLoading(true)
|
||||
setMigrationPreviewError(null)
|
||||
try {
|
||||
const preview = await requestJson<MigrationPreviewResponse>(
|
||||
`/api/admin/upload-templates/${migrationSourceTemplate.id}/migrate`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
targetTemplateId: migrationTargetTemplateId,
|
||||
previewOnly: true,
|
||||
}),
|
||||
}
|
||||
)
|
||||
setMigrationPreview(preview)
|
||||
} catch (error: any) {
|
||||
setMigrationPreview(null)
|
||||
setMigrationPreviewError(error.message || '生成迁移预览失败')
|
||||
} finally {
|
||||
setMigrationPreviewLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 执行素材迁移并删除模板
|
||||
const handleMigrateAndDelete = async () => {
|
||||
if (!migrationSourceTemplate || !migrationTargetTemplateId) return
|
||||
if (!migrationPreview || migrationPreview.targetTemplate.id !== migrationTargetTemplateId) {
|
||||
setMigrationPreviewError('请先生成迁移预览,确认字段影响后再执行迁移。')
|
||||
return
|
||||
}
|
||||
|
||||
setMigrating(true)
|
||||
try {
|
||||
@ -1701,6 +1783,8 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
setMigrationDialogOpen(false)
|
||||
setMigrationSourceTemplate(null)
|
||||
setMigrationTargetTemplateId('')
|
||||
setMigrationPreview(null)
|
||||
setMigrationPreviewError(null)
|
||||
|
||||
// 通知首页刷新模板列表
|
||||
notifyTemplatesUpdate()
|
||||
@ -1713,10 +1797,12 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
|
||||
// 关闭迁移弹窗
|
||||
const closeMigrationDialog = () => {
|
||||
if (migrating) return
|
||||
if (migrating || migrationPreviewLoading) return
|
||||
setMigrationDialogOpen(false)
|
||||
setMigrationSourceTemplate(null)
|
||||
setMigrationTargetTemplateId('')
|
||||
setMigrationPreview(null)
|
||||
setMigrationPreviewError(null)
|
||||
}
|
||||
|
||||
// 获取可选的目标模板列表(排除源模板)
|
||||
@ -1933,12 +2019,12 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
<Modal
|
||||
isOpen={migrationDialogOpen}
|
||||
onClose={closeMigrationDialog}
|
||||
size="medium"
|
||||
size="large"
|
||||
showCloseButton={true}
|
||||
closeOnOverlayClick={!migrating}
|
||||
closeOnEscape={!migrating}
|
||||
closeOnOverlayClick={!migrating && !migrationPreviewLoading}
|
||||
closeOnEscape={!migrating && !migrationPreviewLoading}
|
||||
>
|
||||
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-lg p-6 max-w-lg w-full">
|
||||
<div className="bg-white dark:bg-slate-900 rounded-lg shadow-lg p-6 max-h-[84vh] overflow-y-auto">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 rounded-full bg-amber-100 dark:bg-amber-900/30 flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@ -1972,8 +2058,8 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
</label>
|
||||
<select
|
||||
value={migrationTargetTemplateId}
|
||||
onChange={(e) => setMigrationTargetTemplateId(e.target.value)}
|
||||
disabled={migrating}
|
||||
onChange={(e) => handleMigrationTargetChange(e.target.value)}
|
||||
disabled={migrating || migrationPreviewLoading}
|
||||
className={cn(
|
||||
'w-full px-3 py-2 rounded-lg border transition-colors',
|
||||
'bg-white dark:bg-slate-800',
|
||||
@ -2013,11 +2099,123 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-slate-700 dark:text-slate-300">
|
||||
迁移预览
|
||||
</p>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-1">
|
||||
预览不会修改数据,确认字段影响后再执行迁移。
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={loadMigrationPreview}
|
||||
disabled={migrating || migrationPreviewLoading || !migrationTargetTemplateId}
|
||||
className={cn(
|
||||
'px-3 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
'border border-slate-300 dark:border-slate-600',
|
||||
'bg-white dark:bg-slate-800 text-slate-700 dark:text-slate-200',
|
||||
'hover:bg-slate-50 dark:hover:bg-slate-700',
|
||||
'disabled:opacity-50 disabled:cursor-not-allowed'
|
||||
)}
|
||||
>
|
||||
{migrationPreviewLoading ? '生成中...' : migrationPreview ? '刷新预览' : '生成预览'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{migrationPreviewError && (
|
||||
<p className="mt-3 text-sm text-red-600 dark:text-red-400">
|
||||
{migrationPreviewError}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{migrationPreview && (
|
||||
<div className="mt-3 rounded-lg border border-slate-200 dark:border-slate-700 p-4 space-y-4">
|
||||
<div className="grid grid-cols-3 gap-3 text-sm">
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">可迁移素材</p>
|
||||
<p className="mt-1 font-semibold text-slate-900 dark:text-white">
|
||||
{migrationPreview.migratableCount}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">需备份素材</p>
|
||||
<p className="mt-1 font-semibold text-slate-900 dark:text-white">
|
||||
{migrationPreview.migrationBackup.materialsWithDroppedFields}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400">备份字段</p>
|
||||
<p className="mt-1 font-semibold text-slate-900 dark:text-white">
|
||||
{migrationPreview.migrationBackup.droppedTemplateFieldCount + migrationPreview.migrationBackup.droppedStandardFieldCount}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2">
|
||||
迁移后当前字段
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{migrationPreview.migrationPreview.targetCurrentFields.slice(0, 12).map((field) => (
|
||||
<span
|
||||
key={field.key}
|
||||
className="rounded-md bg-slate-100 px-2 py-1 text-xs text-slate-700 dark:bg-slate-800 dark:text-slate-300"
|
||||
>
|
||||
{field.label}{field.required ? ' *' : ''}
|
||||
</span>
|
||||
))}
|
||||
{migrationPreview.migrationPreview.targetCurrentFields.length > 12 && (
|
||||
<span className="rounded-md bg-slate-100 px-2 py-1 text-xs text-slate-500 dark:bg-slate-800 dark:text-slate-400">
|
||||
另有 {migrationPreview.migrationPreview.targetCurrentFields.length - 12} 个
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{(migrationPreview.migrationPreview.backupFields.templateFields.length > 0 ||
|
||||
migrationPreview.migrationPreview.backupFields.standardFields.length > 0) ? (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-slate-500 dark:text-slate-400 mb-2">
|
||||
会进入备份区的旧字段
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{[
|
||||
...migrationPreview.migrationPreview.backupFields.templateFields,
|
||||
...migrationPreview.migrationPreview.backupFields.standardFields,
|
||||
].slice(0, 12).map((field) => (
|
||||
<span
|
||||
key={`${field.source}-${field.key}`}
|
||||
className="rounded-md bg-amber-50 px-2 py-1 text-xs text-amber-800 dark:bg-amber-900/20 dark:text-amber-200"
|
||||
>
|
||||
{field.label}({field.affectedCount || 0})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{migrationPreview.migrationPreview.samples.length > 0 && (
|
||||
<p className="mt-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
示例素材:{migrationPreview.migrationPreview.samples
|
||||
.map((sample) => sample.productModel || sample.materialId)
|
||||
.join('、')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-500 dark:text-slate-400">
|
||||
目标模板已接收当前字段,本次迁移不会产生旧字段备份。
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 justify-end">
|
||||
<button
|
||||
onClick={closeMigrationDialog}
|
||||
disabled={migrating}
|
||||
disabled={migrating || migrationPreviewLoading}
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
'bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300',
|
||||
@ -2029,7 +2227,7 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
</button>
|
||||
<button
|
||||
onClick={handleMigrateAndDelete}
|
||||
disabled={migrating || !migrationTargetTemplateId}
|
||||
disabled={migrating || migrationPreviewLoading || !migrationTargetTemplateId || !migrationPreview || migrationPreview.targetTemplate.id !== migrationTargetTemplateId}
|
||||
className={cn(
|
||||
'px-4 py-2 rounded-lg text-sm font-medium transition-colors',
|
||||
'bg-red-600 text-white',
|
||||
@ -2047,7 +2245,7 @@ export default function TemplateManager({ showToast }: { showToast: ToastFn }) {
|
||||
迁移中...
|
||||
</>
|
||||
) : (
|
||||
<>迁移并删除</>
|
||||
<>确认迁移并删除</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -676,7 +676,7 @@ NAS 断电、容器重启、升级重启后,正在处理的视频可能永久
|
||||
|
||||
- 已完成:默认保留旧字段到备份命名空间,避免历史字段无声丢失。
|
||||
- 已完成:迁移前可通过 `previewOnly: true` 获取预览报告,列出哪些字段会进入备份区,哪些字段会成为目标模板当前字段。
|
||||
- 仍需继续:前端模板管理页还需要把迁移预览接入正式确认流程,避免管理员直接盲点迁移按钮。
|
||||
- 已完成:前端模板管理页的迁移删除弹窗已接入预览确认流程;管理员必须先生成当前目标模板的迁移预览,确认字段影响后才能点击“确认迁移并删除”。
|
||||
|
||||
### P1-14 上传会截断 metadata,可能截坏 JSON(已修复上传、编辑和模板迁移入口)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user