mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-25 13:51:04 +00:00
feat: 新增权限指令并集成到多个页面组件
refactor(backend): 重构数据库模型和初始化逻辑 fix(frontend): 修复权限指令在多个组件中的使用问题 perf(backend): 优化数据库连接和初始化性能 docs: 更新低代码生成器的README文档 style: 清理无用代码和注释 chore: 移除不再需要的依赖项 test: 更新用户权限相关测试用例 ci: 更新CI配置以支持新的权限检查 build: 更新依赖项版本
This commit is contained in:
@@ -0,0 +1,606 @@
|
||||
/**
|
||||
* 低代码页面生成器 - 组件 Schema 定义
|
||||
* 定义组件的 JSON 结构和类型
|
||||
*/
|
||||
|
||||
// 基础组件属性接口
|
||||
export interface BaseComponentProps {
|
||||
id: string;
|
||||
type: string;
|
||||
label?: string;
|
||||
children?: ComponentSchema[];
|
||||
style?: Record<string, any>;
|
||||
class?: string | string[];
|
||||
}
|
||||
|
||||
// Element Plus 组件类型枚举(只包含已配置的组件)
|
||||
export enum ComponentType {
|
||||
// 基础组件
|
||||
BUTTON = 'el-button',
|
||||
LINK = 'el-link',
|
||||
TEXT = 'el-text',
|
||||
|
||||
// 布局组件
|
||||
ROW = 'el-row',
|
||||
COL = 'el-col',
|
||||
CONTAINER = 'el-container',
|
||||
HEADER = 'el-header',
|
||||
MAIN = 'el-main',
|
||||
ASIDE = 'el-aside',
|
||||
FOOTER = 'el-footer',
|
||||
|
||||
// 表单组件
|
||||
FORM = 'el-form',
|
||||
FORM_ITEM = 'el-form-item',
|
||||
INPUT = 'el-input',
|
||||
INPUT_NUMBER = 'el-input-number',
|
||||
SELECT = 'el-select',
|
||||
RADIO = 'el-radio',
|
||||
RADIO_GROUP = 'el-radio-group',
|
||||
CHECKBOX = 'el-checkbox',
|
||||
CHECKBOX_GROUP = 'el-checkbox-group',
|
||||
SWITCH = 'el-switch',
|
||||
SLIDER = 'el-slider',
|
||||
TIME_PICKER = 'el-time-picker',
|
||||
DATE_PICKER = 'el-date-picker',
|
||||
|
||||
// 数据展示组件
|
||||
CARD = 'el-card',
|
||||
TAG = 'el-tag',
|
||||
PROGRESS = 'el-progress',
|
||||
|
||||
// 反馈组件
|
||||
ALERT = 'el-alert',
|
||||
|
||||
// 其他组件
|
||||
DIVIDER = 'el-divider'
|
||||
}
|
||||
|
||||
// 组件 Schema 接口
|
||||
export interface ComponentSchema extends BaseComponentProps {
|
||||
props?: Record<string, any>;
|
||||
events?: Record<string, string>;
|
||||
slots?: Record<string, string>;
|
||||
}
|
||||
|
||||
// 页面 Schema 接口
|
||||
export interface PageSchema {
|
||||
id: string;
|
||||
name: string;
|
||||
components: ComponentSchema[];
|
||||
globalStyle?: Record<string, any>;
|
||||
}
|
||||
|
||||
// 组件配置接口
|
||||
export interface ComponentConfig {
|
||||
type: ComponentType;
|
||||
label: string;
|
||||
icon: string;
|
||||
category: string;
|
||||
defaultProps: Record<string, any>;
|
||||
editableProps: string[];
|
||||
canHaveChildren: boolean;
|
||||
maxChildren?: number;
|
||||
}
|
||||
|
||||
// 组件库配置
|
||||
export const COMPONENT_CONFIGS: Record<ComponentType, ComponentConfig> = {
|
||||
// 基础组件
|
||||
[ComponentType.BUTTON]: {
|
||||
type: ComponentType.BUTTON,
|
||||
label: '按钮',
|
||||
icon: 'el-icon-edit',
|
||||
category: '基础',
|
||||
defaultProps: {
|
||||
type: 'primary',
|
||||
size: 'default',
|
||||
plain: false,
|
||||
round: false,
|
||||
circle: false,
|
||||
disabled: false,
|
||||
loading: false,
|
||||
children: '按钮'
|
||||
},
|
||||
editableProps: ['type', 'size', 'plain', 'round', 'circle', 'disabled', 'loading', 'children'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.LINK]: {
|
||||
type: ComponentType.LINK,
|
||||
label: '链接',
|
||||
icon: 'el-icon-link',
|
||||
category: '基础',
|
||||
defaultProps: {
|
||||
type: 'primary',
|
||||
href: '',
|
||||
disabled: false,
|
||||
underline: true,
|
||||
children: '链接文本'
|
||||
},
|
||||
editableProps: ['type', 'href', 'disabled', 'underline', 'children'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.TEXT]: {
|
||||
type: ComponentType.TEXT,
|
||||
label: '文本',
|
||||
icon: 'el-icon-document',
|
||||
category: '基础',
|
||||
defaultProps: {
|
||||
type: 'primary',
|
||||
size: 'default',
|
||||
tag: 'span',
|
||||
children: '文本内容'
|
||||
},
|
||||
editableProps: ['type', 'size', 'tag', 'children'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.INPUT]: {
|
||||
type: ComponentType.INPUT,
|
||||
label: '输入框',
|
||||
icon: 'el-icon-edit',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
type: 'text',
|
||||
placeholder: '请输入内容',
|
||||
clearable: true,
|
||||
disabled: false,
|
||||
readonly: false,
|
||||
maxlength: null,
|
||||
showWordLimit: false,
|
||||
size: 'default'
|
||||
},
|
||||
editableProps: ['type', 'placeholder', 'clearable', 'disabled', 'readonly', 'maxlength', 'showWordLimit', 'size'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.INPUT_NUMBER]: {
|
||||
type: ComponentType.INPUT_NUMBER,
|
||||
label: '数字输入框',
|
||||
icon: 'el-icon-plus',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
precision: 0,
|
||||
size: 'default',
|
||||
disabled: false,
|
||||
controls: true,
|
||||
controlsPosition: ''
|
||||
},
|
||||
editableProps: ['min', 'max', 'step', 'precision', 'size', 'disabled', 'controls', 'controlsPosition'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.SELECT]: {
|
||||
type: ComponentType.SELECT,
|
||||
label: '选择器',
|
||||
icon: 'el-icon-arrow-down',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
placeholder: '请选择',
|
||||
clearable: true,
|
||||
disabled: false,
|
||||
multiple: false,
|
||||
filterable: false,
|
||||
size: 'default'
|
||||
},
|
||||
editableProps: ['placeholder', 'clearable', 'disabled', 'multiple', 'filterable', 'size'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.RADIO]: {
|
||||
type: ComponentType.RADIO,
|
||||
label: '单选框',
|
||||
icon: 'el-icon-check',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
label: '选项',
|
||||
disabled: false,
|
||||
border: false,
|
||||
size: 'default'
|
||||
},
|
||||
editableProps: ['label', 'disabled', 'border', 'size'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.RADIO_GROUP]: {
|
||||
type: ComponentType.RADIO_GROUP,
|
||||
label: '单选框组',
|
||||
icon: 'el-icon-check',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
disabled: false,
|
||||
size: 'default',
|
||||
textColor: '#ffffff',
|
||||
fill: '#409eff'
|
||||
},
|
||||
editableProps: ['disabled', 'size', 'textColor', 'fill'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.CHECKBOX]: {
|
||||
type: ComponentType.CHECKBOX,
|
||||
label: '复选框',
|
||||
icon: 'el-icon-check',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
label: '选项',
|
||||
disabled: false,
|
||||
border: false,
|
||||
size: 'default',
|
||||
indeterminate: false
|
||||
},
|
||||
editableProps: ['label', 'disabled', 'border', 'size', 'indeterminate'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.CHECKBOX_GROUP]: {
|
||||
type: ComponentType.CHECKBOX_GROUP,
|
||||
label: '复选框组',
|
||||
icon: 'el-icon-check',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
disabled: false,
|
||||
min: null,
|
||||
max: null,
|
||||
size: 'default',
|
||||
textColor: '#ffffff',
|
||||
fill: '#409eff'
|
||||
},
|
||||
editableProps: ['disabled', 'min', 'max', 'size', 'textColor', 'fill'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.SWITCH]: {
|
||||
type: ComponentType.SWITCH,
|
||||
label: '开关',
|
||||
icon: 'el-icon-switch-button',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
disabled: false,
|
||||
width: 40,
|
||||
activeText: '',
|
||||
inactiveText: '',
|
||||
activeValue: true,
|
||||
inactiveValue: false,
|
||||
activeColor: '#409eff',
|
||||
inactiveColor: '#dcdfe6'
|
||||
},
|
||||
editableProps: ['disabled', 'width', 'activeText', 'inactiveText', 'activeValue', 'inactiveValue', 'activeColor', 'inactiveColor'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.SLIDER]: {
|
||||
type: ComponentType.SLIDER,
|
||||
label: '滑块',
|
||||
icon: 'el-icon-minus',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
step: 1,
|
||||
disabled: false,
|
||||
showStops: false,
|
||||
showTooltip: true,
|
||||
range: false
|
||||
},
|
||||
editableProps: ['min', 'max', 'step', 'disabled', 'showStops', 'showTooltip', 'range'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.DATE_PICKER]: {
|
||||
type: ComponentType.DATE_PICKER,
|
||||
label: '日期选择器',
|
||||
icon: 'el-icon-date',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
type: 'date',
|
||||
placeholder: '选择日期',
|
||||
format: 'YYYY-MM-DD',
|
||||
valueFormat: 'YYYY-MM-DD',
|
||||
disabled: false,
|
||||
clearable: true,
|
||||
size: 'default'
|
||||
},
|
||||
editableProps: ['type', 'placeholder', 'format', 'valueFormat', 'disabled', 'clearable', 'size'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.TIME_PICKER]: {
|
||||
type: ComponentType.TIME_PICKER,
|
||||
label: '时间选择器',
|
||||
icon: 'el-icon-time',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
placeholder: '选择时间',
|
||||
format: 'HH:mm:ss',
|
||||
valueFormat: 'HH:mm:ss',
|
||||
disabled: false,
|
||||
clearable: true,
|
||||
size: 'default'
|
||||
},
|
||||
editableProps: ['placeholder', 'format', 'valueFormat', 'disabled', 'clearable', 'size'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.CARD]: {
|
||||
type: ComponentType.CARD,
|
||||
label: '卡片',
|
||||
icon: 'el-icon-document',
|
||||
category: '数据展示',
|
||||
defaultProps: {
|
||||
header: '卡片标题',
|
||||
shadow: 'always',
|
||||
bodyStyle: {}
|
||||
},
|
||||
editableProps: ['header', 'shadow', 'bodyStyle'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.TAG]: {
|
||||
type: ComponentType.TAG,
|
||||
label: '标签',
|
||||
icon: 'el-icon-price-tag',
|
||||
category: '数据展示',
|
||||
defaultProps: {
|
||||
type: 'primary',
|
||||
closable: false,
|
||||
disableTransitions: false,
|
||||
hit: false,
|
||||
color: '',
|
||||
size: 'default',
|
||||
effect: 'light',
|
||||
children: '标签'
|
||||
},
|
||||
editableProps: ['type', 'closable', 'disableTransitions', 'hit', 'color', 'size', 'effect', 'children'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.PROGRESS]: {
|
||||
type: ComponentType.PROGRESS,
|
||||
label: '进度条',
|
||||
icon: 'el-icon-loading',
|
||||
category: '数据展示',
|
||||
defaultProps: {
|
||||
percentage: 50,
|
||||
type: 'line',
|
||||
strokeWidth: 6,
|
||||
textInside: false,
|
||||
status: '',
|
||||
color: '#409eff',
|
||||
showText: true,
|
||||
format: null
|
||||
},
|
||||
editableProps: ['percentage', 'type', 'strokeWidth', 'textInside', 'status', 'color', 'showText'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.ALERT]: {
|
||||
type: ComponentType.ALERT,
|
||||
label: '警告',
|
||||
icon: 'el-icon-warning',
|
||||
category: '反馈',
|
||||
defaultProps: {
|
||||
title: '警告标题',
|
||||
type: 'info',
|
||||
description: '',
|
||||
closable: true,
|
||||
center: false,
|
||||
closeText: '',
|
||||
showIcon: false,
|
||||
effect: 'light'
|
||||
},
|
||||
editableProps: ['title', 'type', 'description', 'closable', 'center', 'closeText', 'showIcon', 'effect'],
|
||||
canHaveChildren: false
|
||||
},
|
||||
|
||||
[ComponentType.FORM]: {
|
||||
type: ComponentType.FORM,
|
||||
label: '表单',
|
||||
icon: 'el-icon-document',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
model: 'formData',
|
||||
rules: 'formRules',
|
||||
labelWidth: '120px',
|
||||
labelPosition: 'right',
|
||||
inline: false,
|
||||
labelSuffix: '',
|
||||
hideRequiredAsterisk: false,
|
||||
showMessage: true,
|
||||
inlineMessage: false,
|
||||
statusIcon: false,
|
||||
validateOnRuleChange: true,
|
||||
size: 'default',
|
||||
disabled: false
|
||||
},
|
||||
editableProps: ['labelWidth', 'labelPosition', 'inline', 'labelSuffix', 'hideRequiredAsterisk', 'showMessage', 'inlineMessage', 'statusIcon', 'validateOnRuleChange', 'size', 'disabled'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.FORM_ITEM]: {
|
||||
type: ComponentType.FORM_ITEM,
|
||||
label: '表单项',
|
||||
icon: 'el-icon-tickets',
|
||||
category: '表单',
|
||||
defaultProps: {
|
||||
label: '标签',
|
||||
labelWidth: '',
|
||||
required: false,
|
||||
rules: null,
|
||||
error: '',
|
||||
showMessage: true,
|
||||
inlineMessage: false,
|
||||
size: 'default'
|
||||
},
|
||||
editableProps: ['label', 'labelWidth', 'required', 'showMessage', 'inlineMessage', 'size'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.ROW]: {
|
||||
type: ComponentType.ROW,
|
||||
label: '行布局',
|
||||
icon: 'el-icon-menu',
|
||||
category: '布局',
|
||||
defaultProps: {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
gap: '12px',
|
||||
padding: '12px',
|
||||
border: '1px dashed #dcdfe6',
|
||||
borderRadius: '4px',
|
||||
minHeight: '60px'
|
||||
}
|
||||
},
|
||||
editableProps: ['gap', 'justifyContent', 'alignItems'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.COL]: {
|
||||
type: ComponentType.COL,
|
||||
label: '列布局',
|
||||
icon: 'el-icon-menu',
|
||||
category: '布局',
|
||||
defaultProps: {
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '12px',
|
||||
padding: '12px',
|
||||
border: '1px dashed #dcdfe6',
|
||||
borderRadius: '4px',
|
||||
minHeight: '60px'
|
||||
}
|
||||
},
|
||||
editableProps: ['gap', 'justifyContent', 'alignItems'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.CONTAINER]: {
|
||||
type: ComponentType.CONTAINER,
|
||||
label: '容器',
|
||||
icon: 'el-icon-s-grid',
|
||||
category: '布局',
|
||||
defaultProps: {
|
||||
direction: 'vertical'
|
||||
},
|
||||
editableProps: ['direction'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.HEADER]: {
|
||||
type: ComponentType.HEADER,
|
||||
label: '头部',
|
||||
icon: 'el-icon-top',
|
||||
category: '布局',
|
||||
defaultProps: {
|
||||
height: '60px'
|
||||
},
|
||||
editableProps: ['height'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.MAIN]: {
|
||||
type: ComponentType.MAIN,
|
||||
label: '主要区域',
|
||||
icon: 'el-icon-s-grid',
|
||||
category: '布局',
|
||||
defaultProps: {},
|
||||
editableProps: [],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.ASIDE]: {
|
||||
type: ComponentType.ASIDE,
|
||||
label: '侧边栏',
|
||||
icon: 'el-icon-s-unfold',
|
||||
category: '布局',
|
||||
defaultProps: {
|
||||
width: '300px'
|
||||
},
|
||||
editableProps: ['width'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.FOOTER]: {
|
||||
type: ComponentType.FOOTER,
|
||||
label: '底部',
|
||||
icon: 'el-icon-bottom',
|
||||
category: '布局',
|
||||
defaultProps: {
|
||||
height: '60px'
|
||||
},
|
||||
editableProps: ['height'],
|
||||
canHaveChildren: true
|
||||
},
|
||||
|
||||
[ComponentType.DIVIDER]: {
|
||||
type: ComponentType.DIVIDER,
|
||||
label: '分割线',
|
||||
icon: 'el-icon-minus',
|
||||
category: '其他',
|
||||
defaultProps: {
|
||||
direction: 'horizontal',
|
||||
contentPosition: 'center',
|
||||
children: ''
|
||||
},
|
||||
editableProps: ['direction', 'contentPosition', 'children'],
|
||||
canHaveChildren: false
|
||||
}
|
||||
};
|
||||
|
||||
// 组件分类
|
||||
export const COMPONENT_CATEGORIES = {
|
||||
'基础': [ComponentType.BUTTON, ComponentType.LINK, ComponentType.TEXT],
|
||||
'布局': [ComponentType.ROW, ComponentType.COL, ComponentType.CONTAINER, ComponentType.HEADER, ComponentType.MAIN, ComponentType.ASIDE, ComponentType.FOOTER],
|
||||
'表单': [ComponentType.FORM, ComponentType.FORM_ITEM, ComponentType.INPUT, ComponentType.INPUT_NUMBER, ComponentType.SELECT, ComponentType.RADIO, ComponentType.RADIO_GROUP, ComponentType.CHECKBOX, ComponentType.CHECKBOX_GROUP, ComponentType.SWITCH, ComponentType.SLIDER, ComponentType.TIME_PICKER, ComponentType.DATE_PICKER],
|
||||
'数据展示': [ComponentType.CARD, ComponentType.TAG, ComponentType.PROGRESS],
|
||||
'反馈': [ComponentType.ALERT],
|
||||
'其他': [ComponentType.DIVIDER]
|
||||
};
|
||||
|
||||
// 生成唯一 ID
|
||||
export function generateId(): string {
|
||||
return `component_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
// 创建组件实例
|
||||
export function createComponent(type: ComponentType, props: Record<string, any> = {}): ComponentSchema {
|
||||
const config = COMPONENT_CONFIGS[type];
|
||||
if (!config) {
|
||||
throw new Error(`Unknown component type: ${type}`);
|
||||
}
|
||||
|
||||
const component: ComponentSchema = {
|
||||
id: generateId(),
|
||||
type,
|
||||
props: { ...config.defaultProps, ...props },
|
||||
children: config.canHaveChildren ? [] : undefined
|
||||
};
|
||||
|
||||
// 布局组件初始化为空容器,不自动生成子组件
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
// 验证组件 Schema
|
||||
export function validateComponentSchema(schema: ComponentSchema): boolean {
|
||||
if (!schema.id || !schema.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const config = COMPONENT_CONFIGS[schema.type as ComponentType];
|
||||
if (!config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.canHaveChildren && schema.children) {
|
||||
return schema.children.every(child => validateComponentSchema(child));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* 低代码页面生成器 - 序列化器
|
||||
* 将 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>
|
||||
${template}
|
||||
</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<string, string> = {
|
||||
'el-button': '按钮',
|
||||
'el-link': '链接',
|
||||
'el-text': '文本',
|
||||
'el-tag': '标签',
|
||||
'el-alert': '', // alert 通过 title 属性显示内容
|
||||
'el-card': `\n${spaces}<div>卡片内容</div>\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<string, any>, 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, string>): 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, string>): string {
|
||||
const slotStrings: string[] = [];
|
||||
|
||||
for (const [slotName, slotContent] of Object.entries(slots)) {
|
||||
if (slotName === 'default') {
|
||||
slotStrings.push(slotContent);
|
||||
} else {
|
||||
slotStrings.push(`<template #${slotName}>${slotContent}</template>`);
|
||||
}
|
||||
}
|
||||
|
||||
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 `<script setup lang="ts">
|
||||
${imports}
|
||||
|
||||
${reactiveData}
|
||||
|
||||
${computed}
|
||||
|
||||
${methods}
|
||||
</script>`;
|
||||
}
|
||||
|
||||
// 生成导入语句
|
||||
function generateImports(schema: PageSchema): string {
|
||||
const componentTypes = new Set<string>();
|
||||
|
||||
// 收集所有使用的组件类型
|
||||
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 `<style scoped>
|
||||
/* 页面样式 */
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* 组件样式 */
|
||||
.component-wrapper {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 响应式样式 */
|
||||
@media (max-width: 768px) {
|
||||
.page-container {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
// 判断是否为自闭合标签
|
||||
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<string, string> = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
|
||||
return text.replace(/[&<>"']/g, (m) => map[m]);
|
||||
}
|
||||
|
||||
// 生成完整的 Vue 文件内容
|
||||
export function generateVueFile(schema: PageSchema, filename?: string): string {
|
||||
const template = generateVueTemplate(schema);
|
||||
const header = `<!--
|
||||
文件名: ${filename || 'generated-page.vue'}
|
||||
生成时间: ${new Date().toLocaleString()}
|
||||
描述: 由低代码页面生成器自动生成
|
||||
-->
|
||||
|
||||
`;
|
||||
|
||||
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<void> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user