重构 UI 编辑器工作流校验逻辑

抽取工作流步骤的前置与后置校验逻辑至独立模块 `WorkflowChecks`,替换原有冗余实现并优化状态传递。
This commit is contained in:
2026-08-18 19:13:21 +08:00
parent b21b252429
commit 98ff635db3
5 changed files with 113 additions and 67 deletions
@@ -142,29 +142,40 @@ export function validateLayoutReviewPrerequisites(
return validateLayoutGenerationPrerequisites(state);
}
function statusIssue(
function layoutStatusIssue(
status: StageStatus,
stage: 'layout' | 'components',
): UiEditorPrerequisiteIssue | null {
if (status === 'Pending') {
return {
code: `${stage}-pending`,
message:
stage === 'layout'
? '存在尚未识别的节点布局'
: '存在尚未绑定的节点组件',
};
switch (status) {
case 'Pending':
return { code: 'layout-pending', message: '存在尚未识别的节点布局' };
case 'Blocked':
return { code: 'layout-blocked', message: '存在被阻塞的节点结果' };
case 'Passed':
return null;
default:
return {
code: 'layout-needs-review',
message: status.NeedReview || '存在需要人工审阅的节点结果',
};
}
if (status === 'Blocked') {
return { code: `${stage}-blocked`, message: '存在被阻塞的节点结果' };
}
function componentStatusIssue(
status: StageStatus,
): UiEditorPrerequisiteIssue | null {
switch (status) {
case 'Pending':
return { code: 'components-pending', message: '存在尚未绑定的节点组件' };
case 'Blocked':
return { code: 'components-blocked', message: '存在被阻塞的节点结果' };
case 'Passed':
return null;
default:
return {
code: 'components-needs-review',
message: status.NeedReview || '存在需要人工审阅的节点结果',
};
}
if (typeof status === 'object' && 'NeedReview' in status) {
return {
code: `${stage}-needs-review`,
message: status.NeedReview || '存在需要人工审阅的节点结果',
};
}
return null;
}
function visitNodes(
@@ -205,7 +216,7 @@ export function validateStructureRecognitionResult(
const issues: UiEditorPrerequisiteIssue[] = [];
for (const tree of state.ui_trees) {
visitNodes([tree.root], (node) => {
const issue = statusIssue(node.metadata.layout_status, 'layout');
const issue = layoutStatusIssue(node.metadata.layout_status);
if (issue) issues.push(issue);
});
}
@@ -218,7 +229,10 @@ export function validateVisualBindingResult(
const issues: UiEditorPrerequisiteIssue[] = [];
for (const tree of state.ui_trees) {
visitNodes([tree.root], (node) => {
const issue = statusIssue(node.metadata.components_status, 'components');
if (node.components.length == 0) {
return;
}
const issue = componentStatusIssue(node.metadata.components_status);
if (issue) issues.push(issue);
});
}
@@ -1,4 +1,4 @@
import type { UiEditorPrerequisiteIssue } from '../../../features/ui-editor/prerequisites';
import type { UiEditorPrerequisiteIssue } from '../../../features/ui-editor/requisites';
export function PrerequisiteIssues({
issues,
@@ -0,0 +1,65 @@
import {
type UiEditorPrerequisiteIssue,
validateAssetRecognitionPrerequisites,
validateComponentRecognitionPrerequisites,
validateLayoutReviewPrerequisites,
validateReferenceAnalysisResult,
validateStructureRecognitionResult,
validateVisualBindingResult,
} from '../../../features/ui-editor/requisites';
import type { State } from '../../../features/ui-editor/types/State';
import type { UiEditorStepId } from '../model';
export type { UiEditorPrerequisiteIssue };
export function prerequisiteIssuesForStep(
state: State,
step: UiEditorStepId,
): UiEditorPrerequisiteIssue[] {
switch (step) {
case 'reference-analysis':
return [];
case 'structure-recognition':
return validateComponentRecognitionPrerequisites(state);
case 'visual-binding':
return validateAssetRecognitionPrerequisites(state);
}
}
export function postCheckIssuesForStep(
state: State,
step: UiEditorStepId,
): UiEditorPrerequisiteIssue[] {
switch (step) {
case 'reference-analysis':
return validateReferenceAnalysisResult(state);
case 'structure-recognition':
return validateStructureRecognitionResult(state);
case 'visual-binding':
return validateVisualBindingResult(state);
}
}
export function postCheckIssuesForSave(
state: State,
): UiEditorPrerequisiteIssue[] {
return [
...validateReferenceAnalysisResult(state),
...validateStructureRecognitionResult(state),
...validateVisualBindingResult(state),
];
}
export function activeStepPrerequisiteIssues(
state: State,
step: UiEditorStepId,
): UiEditorPrerequisiteIssue[] {
switch (step) {
case 'reference-analysis':
return validateComponentRecognitionPrerequisites(state);
case 'structure-recognition':
return validateAssetRecognitionPrerequisites(state);
case 'visual-binding':
return validateLayoutReviewPrerequisites(state);
}
}
@@ -9,15 +9,6 @@ import {
prepareSpriteAssetBatch,
} from '../../features/ui-editor/importAdapter';
import { applyMergeResult } from '../../features/ui-editor/merge';
import {
type UiEditorPrerequisiteIssue,
validateAssetRecognitionPrerequisites,
validateComponentRecognitionPrerequisites,
validateLayoutReviewPrerequisites,
validateReferenceAnalysisResult,
validateStructureRecognitionResult,
validateVisualBindingResult,
} from '../../features/ui-editor/prerequisites';
import { applyRecognitionResult } from '../../features/ui-editor/recognition';
import { collectUiNodeIds } from '../../features/ui-editor/treeUtils';
import type { BindingDTO } from '../../features/ui-editor/types/BindingDTO';
@@ -50,6 +41,13 @@ import {
createProjectResourcePreviewRequestId,
createProjectResourcePreviewScopeId,
} from '../../services/projectResourcePreviewTransport';
import {
activeStepPrerequisiteIssues,
postCheckIssuesForSave,
postCheckIssuesForStep,
prerequisiteIssuesForStep,
type UiEditorPrerequisiteIssue,
} from './components/WorkflowChecks';
import {
type PendingResourceRemoval,
removalHasDownstreamReferences,
@@ -562,24 +560,6 @@ export function useUiEditorPage(
setPendingRemoval(null);
}
function prerequisiteIssuesForStep(step: UiEditorStepId) {
if (step === 'reference-analysis') return [];
if (step === 'structure-recognition') {
return validateComponentRecognitionPrerequisites(editor.state);
}
return validateAssetRecognitionPrerequisites(editor.state);
}
function postCheckIssuesForStep(step: UiEditorStepId) {
if (step === 'reference-analysis') {
return validateReferenceAnalysisResult(editor.state);
}
if (step === 'structure-recognition') {
return validateStructureRecognitionResult(editor.state);
}
return validateVisualBindingResult(editor.state);
}
function enterStep(step: UiEditorStepId) {
setActiveStep(step);
const index =
@@ -593,8 +573,8 @@ export function useUiEditorPage(
function requestStepChange(step: UiEditorStepId) {
if (step === activeStep || isAiRunning) return;
const leavingIssues = postCheckIssuesForStep(activeStep);
const enteringIssues = prerequisiteIssuesForStep(step);
const leavingIssues = postCheckIssuesForStep(editor.state, activeStep);
const enteringIssues = prerequisiteIssuesForStep(editor.state, step);
if (leavingIssues.length === 0 && enteringIssues.length === 0) {
enterStep(step);
return;
@@ -613,21 +593,8 @@ export function useUiEditorPage(
setPendingWorkflowStepChange(null);
}
function postCheckIssuesForSave() {
return [
...validateReferenceAnalysisResult(editor.state),
...validateStructureRecognitionResult(editor.state),
...validateVisualBindingResult(editor.state),
];
}
function checkPrerequisites() {
const issues =
activeStep === 'reference-analysis'
? validateComponentRecognitionPrerequisites(editor.state)
: activeStep === 'structure-recognition'
? validateAssetRecognitionPrerequisites(editor.state)
: validateLayoutReviewPrerequisites(editor.state);
const issues = activeStepPrerequisiteIssues(editor.state, activeStep);
setStatus(
issues.length === 0
? '前置数据检查通过。'
@@ -988,7 +955,7 @@ export function useUiEditorPage(
},
confirmStepChange,
cancelStepChange: () => setPendingWorkflowStepChange(null),
postCheckIssuesForSave,
postCheckIssuesForSave: () => postCheckIssuesForSave(editor.state),
checkPrerequisites,
selectDesignImage,
selectSprite,
@@ -3,7 +3,7 @@
import { act, renderHook } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { validateComponentRecognitionPrerequisites } from '../src/features/ui-editor/prerequisites';
import { validateComponentRecognitionPrerequisites } from '../src/features/ui-editor/requisites';
import type { FontAsset } from '../src/features/ui-editor/types/FontAsset';
import type { Node } from '../src/features/ui-editor/types/Node';
import type { SpriteAsset } from '../src/features/ui-editor/types/SpriteAsset';