/**
* 低代码页面生成器 - 序列化器
* 将 JSON Schema 转换为 Vue3 + TypeScript 单文件组件
*/
import { ComponentSchema, PageSchema, ComponentType, COMPONENT_CONFIGS } from './schema';
// 生成 Vue 组件的模板字符串
export function generateVueTemplate(schema: PageSchema): string {
const template = generateTemplateFromComponents(schema.components);
const script = generateScriptSetup(schema);
const style = generateStyle();
return `
${template}
${script}
${style}`;
}
// 递归生成组件模板
function generateTemplateFromComponents(components: ComponentSchema[], indent = 0): string {
const spaces = ' '.repeat(indent);
let template = '';
for (const component of components) {
template += generateComponentTemplate(component, indent);
}
return template;
}
// 生成单个组件的模板
function generateComponentTemplate(component: ComponentSchema, indent = 0): string {
const spaces = ' '.repeat(indent);
const { type, props = {}, children = [], events = {}, slots = {} } = component;
// 对于行列布局,生成为div
const actualType = (type === 'el-row' || type === 'el-col') ? 'div' : type;
// 生成属性字符串
const propsString = generatePropsString(props, type);
// 生成事件字符串
const eventsString = generateEventsString(events);
// 获取组件内容
const componentContent = getComponentContent(component, indent);
// 生成子组件模板
const childrenTemplate = children.length > 0
? `\n${generateTemplateFromComponents(children, indent + 1)}\n${spaces}`
: '';
// 自闭合标签处理
const isSelfClosing = isSelfClosingTag(actualType);
if (isSelfClosing) {
return `${spaces}<${actualType}${propsString}${eventsString} />\n`;
} else {
const content = componentContent || childrenTemplate;
return `${spaces}<${actualType}${propsString}${eventsString}>${content}${actualType}>\n`;
}
}
// 获取组件内容
function getComponentContent(component: ComponentSchema, indent = 0): string {
const spaces = ' '.repeat(indent + 1);
// 有文本内容的组件
if (component.props?.children && typeof component.props.children === 'string') {
return component.props.children;
}
// 特殊组件的默认内容
const defaultContent: Record = {
'el-button': '按钮',
'el-link': '链接',
'el-text': '文本',
'el-tag': '标签',
'el-alert': '', // alert 通过 title 属性显示内容
'el-card': `\n${spaces}卡片内容
\n${spaces.slice(2)}`,
'el-form': `\n${spaces}\n${spaces.slice(2)}`,
'el-form-item': `\n${spaces}\n${spaces.slice(2)}`,
'el-container': `\n${spaces}\n${spaces.slice(2)}`,
'el-header': `\n${spaces}\n${spaces.slice(2)}`,
'el-main': `\n${spaces}\n${spaces.slice(2)}`,
'el-aside': `\n${spaces}\n${spaces.slice(2)}`,
'el-footer': `\n${spaces}\n${spaces.slice(2)}`
};
return defaultContent[component.type] || '';
}
// 生成属性字符串
function generatePropsString(props: Record, componentType?: string): string {
const propStrings: string[] = [];
for (const [key, value] of Object.entries(props)) {
if (value === null || value === undefined) continue;
// 对于行列布局,将样式属性转换为style
if ((componentType === 'el-row' || componentType === 'el-col') && key === 'style') {
const styleString = Object.entries(value)
.map(([k, v]) => `${k.replace(/([A-Z])/g, '-$1').toLowerCase()}: ${v}`)
.join('; ');
propStrings.push(`style="${styleString}"`);
continue;
}
// 跳过布局组件的特殊属性
if ((componentType === 'el-row' || componentType === 'el-col') &&
['gap', 'justifyContent', 'alignItems'].includes(key)) {
continue;
}
if (typeof value === 'boolean') {
if (value) {
propStrings.push(key);
}
} else if (typeof value === 'string') {
propStrings.push(`${key}="${escapeHtml(value)}"`);
} else if (typeof value === 'number') {
propStrings.push(`${key}="${value}"`);
} else if (Array.isArray(value)) {
propStrings.push(`:${key}="[${value.map(v => JSON.stringify(v)).join(', ')}]"`);
} else if (typeof value === 'object') {
propStrings.push(`:${key}="${JSON.stringify(value)}"`);
} else {
propStrings.push(`:${key}="${value}"`);
}
}
return propStrings.length > 0 ? ` ${propStrings.join(' ')}` : '';
}
// 生成事件字符串
function generateEventsString(events: Record): string {
const eventStrings: string[] = [];
for (const [event, handler] of Object.entries(events)) {
eventStrings.push(`@${event}="${handler}"`);
}
return eventStrings.length > 0 ? ` ${eventStrings.join(' ')}` : '';
}
// 生成插槽字符串
function generateSlotsString(slots: Record): string {
const slotStrings: string[] = [];
for (const [slotName, slotContent] of Object.entries(slots)) {
if (slotName === 'default') {
slotStrings.push(slotContent);
} else {
slotStrings.push(`${slotContent}`);
}
}
return slotStrings.length > 0 ? ` ${slotStrings.join(' ')}` : '';
}
// 生成 Script Setup
function generateScriptSetup(schema: PageSchema): string {
const imports = generateImports(schema);
const reactiveData = generateReactiveData(schema);
const methods = generateMethods(schema);
const computed = generateComputed(schema);
return ``;
}
// 生成导入语句
function generateImports(schema: PageSchema): string {
const componentTypes = new Set();
// 收集所有使用的组件类型
function collectComponentTypes(components: ComponentSchema[]) {
for (const component of components) {
componentTypes.add(component.type);
if (component.children) {
collectComponentTypes(component.children);
}
}
}
collectComponentTypes(schema.components);
// 生成导入语句
const imports = [
"import { ref, reactive, computed, onMounted } from 'vue'",
"import { ElMessage } from 'element-plus'"
];
// 添加 Element Plus 组件导入
const elementComponents = Array.from(componentTypes).filter(type =>
type.startsWith('el-') && !['el-icon', 'el-text'].includes(type)
);
if (elementComponents.length > 0) {
imports.push(`import { ${elementComponents.join(', ')} } from 'element-plus'`);
}
return imports.join('\n');
}
// 生成响应式数据
function generateReactiveData(schema: PageSchema): string {
const dataProperties: string[] = [];
// 收集所有需要的数据属性
function collectDataProperties(components: ComponentSchema[]) {
for (const component of components) {
if (component.type === ComponentType.FORM) {
dataProperties.push('formData: {}');
dataProperties.push('formRules: {}');
} else if (component.type === ComponentType.INPUT ||
component.type === ComponentType.SELECT ||
component.type === ComponentType.INPUT_NUMBER) {
const fieldName = component.props?.model || `field_${component.id}`;
dataProperties.push(`${fieldName}: ""`);
}
if (component.children) {
collectDataProperties(component.children);
}
}
}
collectDataProperties(schema.components);
if (dataProperties.length === 0) {
return '// 响应式数据\nconst data = reactive({})';
}
return `// 响应式数据
const data = reactive({
${dataProperties.join(',\n ')}
})`;
}
// 生成计算属性
function generateComputed(schema: PageSchema): string {
return `// 计算属性
const computedValue = computed(() => {
// 在这里添加计算逻辑
return 'computed value'
})`;
}
// 生成方法
function generateMethods(schema: PageSchema): string {
const methods: string[] = [];
// 收集所有需要的方法
function collectMethods(components: ComponentSchema[]) {
for (const component of components) {
if (component.events) {
for (const [event, handler] of Object.entries(component.events)) {
if (!methods.includes(handler)) {
methods.push(handler);
}
}
}
if (component.children) {
collectMethods(component.children);
}
}
}
collectMethods(schema.components);
// 添加默认方法
methods.push('handleSubmit');
methods.push('handleReset');
const methodBodies = methods.map(method => {
switch (method) {
case 'handleSubmit':
return `function handleSubmit() {
ElMessage.success('提交成功')
}`;
case 'handleReset':
return `function handleReset() {
// 重置表单逻辑
}`;
default:
return `function ${method}() {
// ${method} 方法实现
}`;
}
});
return `// 方法
${methodBodies.join('\n\n')}
// 生命周期
onMounted(() => {
// 组件挂载后的逻辑
})`;
}
// 生成样式
function generateStyle(): string {
return ``;
}
// 判断是否为自闭合标签
function isSelfClosingTag(tag: string): boolean {
const selfClosingTags = [
'el-input',
'el-input-number',
'el-icon',
'el-image',
'el-progress',
'el-skeleton',
'el-empty',
'el-backtop'
];
return selfClosingTags.includes(tag);
}
// HTML 转义
function escapeHtml(text: string): string {
const map: Record = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, (m) => map[m]);
}
// 生成完整的 Vue 文件内容
export function generateVueFile(schema: PageSchema, filename?: string): string {
const template = generateVueTemplate(schema);
const header = `
`;
return header + template;
}
// 导出为文件
export function exportAsFile(schema: PageSchema, filename: string = 'generated-page.vue'): void {
const content = generateVueFile(schema, filename);
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
// 复制到剪贴板
export async function copyToClipboard(schema: PageSchema): Promise {
const content = generateVueFile(schema);
try {
await navigator.clipboard.writeText(content);
console.log('代码已复制到剪贴板');
} catch (err) {
console.error('复制失败:', err);
// 降级方案
const textArea = document.createElement('textarea');
textArea.value = content;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
}
}