/**
|
* AI 组件草稿 → 画布数据(语义层的逆运算)
|
* <p>
|
* 与 use-designer 的 collectSchema 构成往返:collectSchema 从画布收集 {type, props},
|
* 本模块把 {type, props} 装回画布。两者必须逐项对齐,否则「AI 生成 → 保存 → 再打开」
|
* 会掉组件或错位属性。
|
* <p>
|
* 刻意放在 core/ 而不是设计器里:它不需要 editor 实例,是纯函数,可单测,
|
* 也避免 components/ 反向依赖 #/api。
|
*/
|
import type { QualityComponentDefinition, QualityProps } from './types';
|
|
import { buildQualityContent } from './factory';
|
import { getQualityComponent } from './registry';
|
import { validateQualityProps } from './validator';
|
|
/**
|
* AI 草稿里的一个组件。
|
* <p>
|
* 结构完全扁平:没有 children。props 的 key 由前端积木清单定义,
|
* 所以这里是宽松的 Record 而不是强类型——合法 key 的权威在注册表。
|
*/
|
export interface QualityDraftComponent {
|
type?: string;
|
props?: Record<string, unknown>;
|
}
|
|
/** 被跳过的草稿组件 */
|
export interface QualityAssemblySkip {
|
type: string;
|
/** 展示名;未注册的 type 没有展示名,回退为 type 本身 */
|
label: string;
|
reason: string;
|
}
|
|
export interface QualityAssemblyResult {
|
/** 可直接交给 loadProjectData 的 GrapesJS 项目数据 */
|
grapes: Record<string, unknown>;
|
/** 识别到了但装不进画布的组件 */
|
skipped: QualityAssemblySkip[];
|
/** 装进去了、但属性不完整或不合法的组件,提示用 */
|
problems: string[];
|
}
|
|
/**
|
* 把草稿组件装配成画布数据。
|
* <p>
|
* 单个组件坏掉不影响其余:未知类型进 skipped,属性有问题的进 problems,
|
* 都不会让整批失败——AI 的产物本来就不可信,全有或全无只会让用户白等一次识别。
|
*/
|
export function assembleQualityCanvas(
|
components?: QualityDraftComponent[],
|
): QualityAssemblyResult {
|
const skipped: QualityAssemblySkip[] = [];
|
const problems: string[] = [];
|
const nodes: Record<string, unknown>[] = [];
|
|
(components ?? []).forEach((draft, index) => {
|
const position = `第 ${index + 1} 个组件`;
|
const type = draft?.type ?? '';
|
const definition = getQualityComponent(type);
|
if (!definition) {
|
skipped.push({
|
type,
|
label: type || '未命名组件',
|
reason: `${position}:类型「${type || '空'}」不在当前设计器的组件清单内,已跳过`,
|
});
|
return;
|
}
|
|
const normalized = normalizeDraftProps(definition, draft?.props);
|
normalized.problems.forEach((message) => problems.push(`${position}:${message}`));
|
|
// buildQualityContent 做的正是往返所需的四件事:defaults 打底、buildContent 生成结构、
|
// propsToAttributes 只输出声明过的 key 并字符串化、写入 data-quality-type。
|
const content = buildQualityContent(definition, normalized.props) as Record<string, unknown>;
|
// traits 不进画布数据:载入后由 restoreQualityTraits 按 data-quality-type 重建,
|
// 与「打开一份已保存的版本」走的是同一条路(traits 本来也不参与项目数据序列化)。
|
delete content.traits;
|
nodes.push(content);
|
|
validateQualityProps(definition, normalized.props).forEach((message) =>
|
problems.push(`${position}:${message}`),
|
);
|
});
|
|
return {
|
// pages 必须非空,否则 loadSchema 会走空白页兜底分支,装配好的组件全部丢掉
|
grapes: {
|
pages: [{ component: { style: { padding: '0' }, components: nodes } }],
|
},
|
skipped,
|
problems,
|
};
|
}
|
|
/**
|
* 把 AI 给的属性收敛成语义层能承载的原始值。
|
* <p>
|
* 只管一件事:值必须是 boolean / number / string。模型偶尔会把字段写成嵌套对象或数组
|
* (比如把检验项数据行整段塞进 itemsPath),这类值经 coerceFieldValue 的 String() 会变成
|
* "[object Object]" 写进画布,属性面板里显示一串乱码,还会静默盖掉默认值——所以在这里拦掉并说明。
|
* <p>
|
* 不检查 key 是否声明过:未声明的 key 由 propsToAttributes 丢弃,本来也进不了画布。
|
*/
|
function normalizeDraftProps(
|
definition: QualityComponentDefinition,
|
props?: Record<string, unknown>,
|
): { props: QualityProps; problems: string[] } {
|
const normalized: QualityProps = {};
|
const problems: string[] = [];
|
if (!props) {
|
return { props: normalized, problems };
|
}
|
definition.propertySchema.forEach((field) => {
|
const value = props[field.key];
|
// null 与 undefined 同义:模型没给值,交给 defaults 兜底
|
if (value === undefined || value === null) {
|
return;
|
}
|
if (
|
typeof value === 'boolean' ||
|
typeof value === 'number' ||
|
typeof value === 'string'
|
) {
|
normalized[field.key] = value;
|
return;
|
}
|
problems.push(
|
`${definition.label}的「${field.label}」收到的是结构化数据,语义层只承载单个值,已按默认值处理`,
|
);
|
});
|
return { props: normalized, problems };
|
}
|