refactor(gencode): 重构代码生成模块,优化字段初始化和类型处理

fix(setting): 确保日志目录存在

perf(logger): 改进日志文件轮转处理,优化异常处理

feat(gencode): 添加子表关联字段支持

style(constant): 更新字段常量命名规范

chore: 移除前端低代码生成器相关文件

fix(gencode): 修复CRUD操作中的字段过滤逻辑

refactor(gen_util): 重构字段初始化逻辑,增强类型安全

docs: 更新代码注释和文档

test: 移除无效测试文件

build: 更新依赖版本

ci: 优化CI配置
This commit is contained in:
zhangtao
2025-11-06 00:31:21 +08:00
parent 451a978d1d
commit 6383c53580
36 changed files with 297 additions and 8123 deletions
@@ -81,12 +81,7 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
- Sequence[GenTableModel]: 业务表列表信息。 - Sequence[GenTableModel]: 业务表列表信息。
""" """
# 使用基础CRUD的list与like检索 # 使用基础CRUD的list与like检索
search_dict: Dict = {} return await self.list(search=search.__dict__, order_by=[{"created_at": "desc"}], preload=preload)
if search and search.table_name:
search_dict["table_name"] = ("like", search.table_name)
if search and search.table_comment:
search_dict["table_comment"] = ("like", search.table_comment)
return await self.list(search=search_dict, order_by=[{"created_at": "desc"}], preload=preload)
async def add_gen_table(self, add_model: GenTableSchema) -> GenTableModel: async def add_gen_table(self, add_model: GenTableSchema) -> GenTableModel:
""" """
@@ -111,7 +106,9 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
返回: 返回:
- GenTableSchema: 修改后的业务表信息模型。 - GenTableSchema: 修改后的业务表信息模型。
""" """
obj = await self.update(id=table_id, data=edit_model) # 排除嵌套对象字段,避免SQLAlchemy尝试直接将字典设置到模型实例上
data_dict = edit_model.model_dump(exclude_unset=True, exclude={"columns", "pk_column", "sub_table", "sub"})
obj = await self.update(id=table_id, data=data_dict)
return GenTableSchema.model_validate(obj) return GenTableSchema.model_validate(obj)
async def delete_gen_table(self, ids: List[int]) -> None: async def delete_gen_table(self, ids: List[int]) -> None:
@@ -520,7 +517,9 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen
返回: 返回:
- Optional[GenTableColumnModel]: 业务表字段列表信息对象。 - Optional[GenTableColumnModel]: 业务表字段列表信息对象。
""" """
return await self.update(id=id, data=data) # 将对象转换为字典,避免SQLAlchemy直接操作对象时出现的状态问题
data_dict = data.model_dump(exclude_unset=True)
return await self.update(id=id, data=data_dict)
async def delete_gen_table_column_by_table_id_dao(self, table_ids: List[int]) -> None: async def delete_gen_table_column_by_table_id_dao(self, table_ids: List[int]) -> None:
"""根据业务表ID批量删除业务表字段。 """根据业务表ID批量删除业务表字段。
@@ -4,7 +4,9 @@ from typing import Optional, List
from sqlalchemy import String, Integer, ForeignKey from sqlalchemy import String, Integer, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.config.setting import settings
from app.core.base_model import CreatorMixin from app.core.base_model import CreatorMixin
from app.utils.common_util import SqlalchemyUtil
class GenTableModel(CreatorMixin): class GenTableModel(CreatorMixin):
@@ -17,6 +19,8 @@ class GenTableModel(CreatorMixin):
table_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='', comment='表名称') table_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='', comment='表名称')
table_comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, default='', comment='表描述') table_comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, default='', comment='表描述')
sub_table_name : Mapped[Optional[str]] = mapped_column(String(64), nullable=True, server_default=SqlalchemyUtil.get_server_default_null(settings.DATABASE_TYPE), comment='关联子表的表名',)
sub_table_fk_name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, server_default=SqlalchemyUtil.get_server_default_null(settings.DATABASE_TYPE), comment='子表关联的外键名',)
class_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, default='', comment='实体类名称') class_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, default='', comment='实体类名称')
package_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='生成包路径') package_name: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment='生成包路径')
module_name: Mapped[Optional[str]] = mapped_column(String(30), nullable=True, comment='生成模块名') module_name: Mapped[Optional[str]] = mapped_column(String(30), nullable=True, comment='生成模块名')
@@ -37,20 +37,19 @@ class GenTableBaseSchema(BaseModel):
""" """
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
table_id: Optional[int] = Field(default=None, description='编号')
table_name: str= Field(..., description='表名称') table_name: str= Field(..., description='表名称')
table_comment: Optional[str] = Field(default=None, description='表描述') table_comment: Optional[str] = Field(default=None, description='表描述')
sub_table_name: Optional[str] = Field(default=None, description='关联子表的表名')
sub_table_fk_name: Optional[str] = Field(default=None, description='子表关联的外键名')
class_name: Optional[str] = Field(default=None, description='实体类名称') class_name: Optional[str] = Field(default=None, description='实体类名称')
package_name: Optional[str] = Field(default=None, description='生成包路径') package_name: Optional[str] = Field(default=None, description='生成包路径')
module_name: Optional[str] = Field(default=None, description='生成模块名') module_name: Optional[str] = Field(default=None, description='生成模块名')
business_name: Optional[str] = Field(default=None, description='生成业务名') business_name: Optional[str] = Field(default=None, description='生成业务名')
function_name: Optional[str] = Field(default=None, description='生成功能名') function_name: Optional[str] = Field(default=None, description='生成功能名')
gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1生成项目路径)') gen_type: Optional[Literal['0', '1']] = Field(default=None, description='生成代码方式(0zip压缩包 1生成项目路径)')
options: Optional[str] = Field(default=None, description='其它生成选项') options: Optional[str] = Field(default=None, description='其它生成选项JSON字符串)')
description: Optional[str] = Field(default=None, description='功能描述') description: Optional[str] = Field(default=None, description='功能描述')
params: Optional[GenTableOptionSchema] = Field(default=None, description='前端传递过来的表附加信息,转换成json字符串后放到options')
class GenTableSchema(GenTableBaseSchema): class GenTableSchema(GenTableBaseSchema):
"""代码生成业务表更新模型(扩展聚合字段)。 """代码生成业务表更新模型(扩展聚合字段)。
@@ -62,6 +61,7 @@ class GenTableSchema(GenTableBaseSchema):
columns: Optional[List['GenTableColumnOutSchema']] = Field(default=None, description='表列信息') columns: Optional[List['GenTableColumnOutSchema']] = Field(default=None, description='表列信息')
parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段')
parent_menu_name: Optional[str] = Field(default=None, description='上级菜单名称字段') parent_menu_name: Optional[str] = Field(default=None, description='上级菜单名称字段')
sub: Optional[bool] = Field(default=None, description='是否为子表')
class GenTableOutSchema(GenTableSchema, BaseSchema): class GenTableOutSchema(GenTableSchema, BaseSchema):
@@ -70,9 +70,6 @@ class GenTableOutSchema(GenTableSchema, BaseSchema):
- 兼容:既支持传入ORM对象,也支持字典输入。 - 兼容:既支持传入ORM对象,也支持字典输入。
""" """
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
# 修复:确保columns字段默认为空列表而不是None
columns: Optional[List['GenTableColumnOutSchema']] = Field(default_factory=list, description='表列信息')
class GenTableColumnSchema(BaseModel): class GenTableColumnSchema(BaseModel):
@@ -11,11 +11,10 @@ from sqlglot import parse as sqlglot_parse
from app.config.setting import settings from app.config.setting import settings
from app.core.logger import logger from app.core.logger import logger
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.common.constant import GenConstant
from app.api.v1.module_system.auth.schema import AuthSchema from app.api.v1.module_system.auth.schema import AuthSchema
from app.utils.gen_util import GenUtils from app.utils.gen_util import GenUtils
from app.utils.jinja2_template_util import Jinja2TemplateUtil from app.utils.jinja2_template_util import Jinja2TemplateUtil
from .schema import GenTableOptionSchema, GenTableSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema from .schema import GenTableSchema, GenTableOutSchema, GenTableColumnSchema, GenTableColumnOutSchema
from .param import GenTableQueryParam from .param import GenTableQueryParam
from .crud import GenTableColumnCRUD, GenTableCRUD from .crud import GenTableColumnCRUD, GenTableCRUD
@@ -43,13 +42,6 @@ class GenTableService:
gen_table = await cls.get_gen_table_by_id_service(auth, table_id) gen_table = await cls.get_gen_table_by_id_service(auth, table_id)
gen_tables = await cls.get_gen_table_all_service(auth) gen_tables = await cls.get_gen_table_all_service(auth)
gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_service(auth, table_id) gen_columns = await GenTableColumnService.get_gen_table_column_list_by_table_id_service(auth, table_id)
# 修复:确保options不为None再解析
if gen_table.options:
try:
table_options = GenTableOptionSchema(**json.loads(gen_table.options))
gen_table.parent_menu_id = table_options.parent_menu_id
except Exception as e:
logger.warning(f"解析表选项时出错: {str(e)}")
gen_table.columns = gen_columns gen_table.columns = gen_columns
return dict(info=gen_table, rows=gen_columns, tables=gen_tables) return dict(info=gen_table, rows=gen_columns, tables=gen_tables)
@@ -71,7 +63,7 @@ class GenTableService:
@classmethod @classmethod
@handle_service_exception @handle_service_exception
async def get_gen_db_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam, order_by: Optional[List[Dict[str, str]]] = None) -> list[Any]: async def get_gen_db_table_list_service(cls, auth: AuthSchema, search: GenTableQueryParam) -> list[Any]:
"""获取数据库表列表(跨方言)。 """获取数据库表列表(跨方言)。
- 备注:返回已转换为字典的结构,适用于前端直接展示;排序参数保留扩展位但当前未使用。 - 备注:返回已转换为字典的结构,适用于前端直接展示;排序参数保留扩展位但当前未使用。
""" """
@@ -89,12 +81,6 @@ class GenTableService:
raise CustomException(msg="表名列表不能为空") raise CustomException(msg="表名列表不能为空")
gen_db_table_list_result = await GenTableCRUD(auth).get_db_table_list_by_names(table_names) gen_db_table_list_result = await GenTableCRUD(auth).get_db_table_list_by_names(table_names)
# 检查是否有未找到的表
found_table_names = [table.table_name for table in gen_db_table_list_result]
missing_tables = [name for name in table_names if name not in found_table_names]
if missing_tables:
raise CustomException(msg=f"以下数据表不存在: {', '.join(missing_tables)}")
# 修复:将GenDBTableSchema对象转换为字典后再传递给GenTableOutSchema # 修复:将GenDBTableSchema对象转换为字典后再传递给GenTableOutSchema
result = [] result = []
@@ -119,9 +105,6 @@ class GenTableService:
existing_tables = [] existing_tables = []
for table in gen_table_list: for table in gen_table_list:
table_name = table.table_name table_name = table.table_name
# 确保table_name不为None
if table_name is None:
raise CustomException(msg="表名不能为空")
# 检查表是否已存在 # 检查表是否已存在
existing_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name) existing_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
if existing_table: if existing_table:
@@ -137,21 +120,22 @@ class GenTableService:
GenUtils.init_table(table) GenUtils.init_table(table)
add_gen_table = await GenTableCRUD(auth).add_gen_table(table) add_gen_table = await GenTableCRUD(auth).add_gen_table(table)
if add_gen_table: if add_gen_table:
table.table_id = add_gen_table.id table.id = add_gen_table.id
# 获取数据库表的字段信息 # 获取数据库表的字段信息
gen_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name) gen_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name)
# 为每个字段初始化并保存到数据库 # 为每个字段初始化并保存到数据库
for column in gen_table_columns: for column in gen_table_columns:
# 将GenTableColumnOutSchema转换为GenTableColumnSchema # 将GenTableColumnOutSchema转换为GenTableColumnSchema,确保is_*字段为字符串格式
column_schema = GenTableColumnSchema( column_schema = GenTableColumnSchema(
table_id=table.table_id, table_id=table.id,
column_name=column.column_name, column_name=column.column_name,
column_comment=column.column_comment, column_comment=column.column_comment,
column_type=column.column_type, column_type=column.column_type,
is_pk=column.is_pk, # 确保这些字段为字符串格式,'1'表示true'0'表示false
is_increment=column.is_increment, is_pk=str(column.is_pk) if column.is_pk is not None else '0',
is_required=column.is_required, is_increment=str(column.is_increment) if column.is_increment is not None else '0',
is_required=str(column.is_required) if column.is_required is not None else '0',
sort=column.sort sort=column.sort
) )
# 初始化字段属性 # 初始化字段属性
@@ -235,25 +219,21 @@ class GenTableService:
async def update_gen_table_service(cls, auth: AuthSchema, data: GenTableSchema, table_id: int) -> Dict[str, Any]: async def update_gen_table_service(cls, auth: AuthSchema, data: GenTableSchema, table_id: int) -> Dict[str, Any]:
"""编辑业务表信息(含选项与字段)。 """编辑业务表信息(含选项与字段)。
- 备注:将`params`序列化写入`options`以持久化;仅更新存在`id`的列,避免误创建。 - 备注:将`params`序列化写入`options`以持久化;仅更新存在`id`的列,避免误创建。
""" """
# 处理params为None的情况
gen_table_info = await cls.get_gen_table_by_id_service(auth, table_id) gen_table_info = await cls.get_gen_table_by_id_service(auth, table_id)
if gen_table_info.id: if gen_table_info.id:
try: try:
# 处理params为None的情况 # 直接调用edit_gen_table方法,它会在内部处理排除嵌套字段的逻辑
edit_gen_table = data.model_dump(exclude_unset=True, by_alias=True) result = await GenTableCRUD(auth).edit_gen_table(table_id, data)
params = edit_gen_table.get('params')
if params:
edit_gen_table['options'] = json.dumps(params)
# 将字典转换为GenTableSchema对象
gen_table_schema = GenTableSchema(**edit_gen_table)
result = await GenTableCRUD(auth).edit_gen_table(table_id, gen_table_schema)
# 处理data.columns为None的情况 # 处理data.columns为None的情况
if data.columns: if data.columns:
for gen_table_column in data.columns: for gen_table_column in data.columns:
# 确保column有id字段 # 确保column有id字段
if hasattr(gen_table_column, 'id') and gen_table_column.id: if hasattr(gen_table_column, 'id') and gen_table_column.id:
await GenTableColumnCRUD(auth).update_gen_table_column_crud(gen_table_column.id, gen_table_column) column_schema = GenTableColumnSchema(**gen_table_column.model_dump())
await GenTableColumnCRUD(auth).update_gen_table_column_crud(gen_table_column.id, column_schema)
return result.model_dump() return result.model_dump()
except Exception as e: except Exception as e:
raise CustomException(msg=str(e)) raise CustomException(msg=str(e))
@@ -287,12 +267,8 @@ class GenTableService:
raise CustomException(msg='业务表不存在') raise CustomException(msg='业务表不存在')
result = GenTableOutSchema.model_validate(gen_table) result = GenTableOutSchema.model_validate(gen_table)
# 确保columns字段为列表,即使为None
if result.columns is None:
result.columns = []
return result return result
@classmethod @classmethod
@handle_service_exception @handle_service_exception
@@ -412,10 +388,7 @@ class GenTableService:
gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name) gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name)
if not gen_table: if not gen_table:
raise CustomException(msg='业务表不存在') raise CustomException(msg='业务表不存在')
table = GenTableSchema.model_validate(gen_table) table = GenTableOutSchema.model_validate(gen_table)
# 关键修复:确保 table.table_id 正确设置为持久化的表ID,否则列无法关联到该表
if getattr(table, 'table_id', None) is None:
table.table_id = getattr(gen_table, 'id', None)
table_columns = table.columns or [] table_columns = table.columns or []
table_column_map = {column.column_name: column for column in table_columns} table_column_map = {column.column_name: column for column in table_columns}
db_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name) db_table_columns = await GenTableColumnCRUD(auth).get_gen_db_table_columns_by_name(table_name)
@@ -457,7 +430,7 @@ class GenTableService:
await GenTableColumnCRUD(auth).create_gen_table_column_crud(column) await GenTableColumnCRUD(auth).create_gen_table_column_crud(column)
else: else:
# 设置table_id以确保新字段能正确关联到表 # 设置table_id以确保新字段能正确关联到表
column.table_id = table.table_id column.table_id = table.id
await GenTableColumnCRUD(auth).create_gen_table_column_crud(column) await GenTableColumnCRUD(auth).create_gen_table_column_crud(column)
del_columns = [column for column in table_columns if column.column_name not in db_table_column_names] del_columns = [column for column in table_columns if column.column_name not in db_table_column_names]
if del_columns: if del_columns:
@@ -542,8 +515,38 @@ class GenTableColumnService:
result = [] result = []
for gen_table_column in gen_table_column_list_result: for gen_table_column in gen_table_column_list_result:
try: try:
# 转换为输出模型前确保必要字段正确设置
# 确保is_*字段为字符串格式
if hasattr(gen_table_column, 'is_pk') and gen_table_column.is_pk is not None and not isinstance(gen_table_column.is_pk, str):
gen_table_column.is_pk = str(gen_table_column.is_pk)
if hasattr(gen_table_column, 'is_increment') and gen_table_column.is_increment is not None and not isinstance(gen_table_column.is_increment, str):
gen_table_column.is_increment = str(gen_table_column.is_increment)
if hasattr(gen_table_column, 'is_required') and gen_table_column.is_required is not None and not isinstance(gen_table_column.is_required, str):
gen_table_column.is_required = str(gen_table_column.is_required)
if hasattr(gen_table_column, 'is_unique') and gen_table_column.is_unique is not None and not isinstance(gen_table_column.is_unique, str):
gen_table_column.is_unique = str(gen_table_column.is_unique)
if hasattr(gen_table_column, 'is_insert') and gen_table_column.is_insert is not None and not isinstance(gen_table_column.is_insert, str):
gen_table_column.is_insert = str(gen_table_column.is_insert)
if hasattr(gen_table_column, 'is_edit') and gen_table_column.is_edit is not None and not isinstance(gen_table_column.is_edit, str):
gen_table_column.is_edit = str(gen_table_column.is_edit)
if hasattr(gen_table_column, 'is_list') and gen_table_column.is_list is not None and not isinstance(gen_table_column.is_list, str):
gen_table_column.is_list = str(gen_table_column.is_list)
if hasattr(gen_table_column, 'is_query') and gen_table_column.is_query is not None and not isinstance(gen_table_column.is_query, str):
gen_table_column.is_query = str(gen_table_column.is_query)
# 转换为输出模型 # 转换为输出模型
column_out = GenTableColumnOutSchema.model_validate(gen_table_column) column_out = GenTableColumnOutSchema.model_validate(gen_table_column)
# 确保输出模型中的布尔字段正确设置
column_out.pk = column_out.is_pk == '1'
column_out.increment = column_out.is_increment == '1'
column_out.required = column_out.is_required == '1'
column_out.unique = column_out.is_unique == '1'
column_out.insert = column_out.is_insert == '1'
column_out.edit = column_out.is_edit == '1'
column_out.list = column_out.is_list == '1'
column_out.query = column_out.is_query == '1'
result.append(column_out) result.append(column_out)
except Exception as e: except Exception as e:
logger.warning(f"转换字段模型时出错: {str(e)}") logger.warning(f"转换字段模型时出错: {str(e)}")
+7 -7
View File
@@ -413,28 +413,28 @@ class GenConstant:
'decimal', 'decimal',
] ]
# 页面不需要显示的添加字段 # 页面不需要显示的添加字段
COLUMNNAME_NOT_ADD_SHOW = ['create_by', 'create_time'] COLUMNNAME_NOT_ADD_SHOW = ['created_at', 'updated_at']
# 页面不需要显示的编辑字段 # 页面不需要显示的编辑字段
COLUMNNAME_NOT_EDIT_SHOW = ['updated_at'] COLUMNNAME_NOT_EDIT_SHOW = ['updated_at']
# 页面不需要编辑字段 # 页面不需要编辑字段
COLUMNNAME_NOT_EDIT = ["id", "create_by", "dept_id", "create_time", "del_flag", "update_time"] COLUMNNAME_NOT_EDIT = ["id", "description", "created_at", "updated_at"]
# 页面不需要显示的列表字段 # 页面不需要显示的列表字段
COLUMNNAME_NOT_LIST = ["id", "create_by", "dept_id", "create_time", "del_flag", "update_time"] COLUMNNAME_NOT_LIST = ["id", "description", "created_at", "updated_at"]
# 页面不需要查询字段 # 页面不需要查询字段
COLUMNNAME_NOT_QUERY = ["id", "create_by", "dept_id", "create_time", "del_flag", "update_by", "update_time", "remark"] COLUMNNAME_NOT_QUERY = ["id", "description", "created_at", "updated_at"]
# Crud基类字段 # Crud基类字段
CRUD_COLUMN_NOT_EDIT = ["create_by", "dept_id", "create_time", "del_flag", "update_time"] CRUD_COLUMN_NOT_EDIT = ["create_by", "description", "created_at", "updated_at"]
# 实体基类字段 # 实体基类字段
BASE_ENTITY = ['id', 'create_time', 'update_time', "create_by", "dept_id", 'del_flag'] BASE_ENTITY = ['id', 'created_at', 'updated_at', "description"]
# Tree基类字段 # Tree基类字段
TREE_ENTITY = ['parentName', 'parentId', 'orderNum', 'ancestors', 'children'] TREE_ENTITY = ['parent_name', 'parent_id', 'order', 'ancestors', 'children']
# 文本框 # 文本框
HTML_INPUT = 'input' HTML_INPUT = 'input'
+3
View File
@@ -286,6 +286,9 @@ class Settings(BaseSettings):
@property @property
def UVICORN_CONFIG(self) -> Dict[str, Any]: def UVICORN_CONFIG(self) -> Dict[str, Any]:
"""获取Uvicorn配置""" """获取Uvicorn配置"""
# 确保日志目录存在
self.LOGGER_DIR.mkdir(parents=True, exist_ok=True)
return { return {
"host": self.SERVER_HOST, "host": self.SERVER_HOST,
"port": self.SERVER_PORT, "port": self.SERVER_PORT,
+19 -17
View File
@@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import sys
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from pathlib import Path from pathlib import Path
import re
from app.config.setting import settings from app.config.setting import settings
@@ -26,26 +26,31 @@ class AppLogger:
) )
handler.setLevel(level) handler.setLevel(level)
handler.setFormatter(formatter) handler.setFormatter(formatter)
handler.suffix = "%Y-%m-%d.log" handler.suffix = "_%Y-%m-%d.log" # 设置正确的后缀格式
def namer(default_name: str) -> str: def namer(default_name: str) -> str:
parts = Path(default_name).name.split(".") # 统一处理轮转后的文件名,确保格式为stem_YYYY-MM-DD.log
if len(parts) >= 3 and parts[-1] == "log": file_name = Path(default_name).name
ts = parts[-2] # 提取日期部分
return str(Path(default_name).with_name(f"{stem}_{ts}.log")) base_name = file_name.split('.')[0] # 获取基本名称(不包含扩展名)
date_match = re.search(r'(\d{4}-\d{2}-\d{2})', base_name)
if date_match:
date_part = date_match.group(1)
return f"{stem}_{date_part}.log"
# 如果没有找到日期部分,返回原始名称
return default_name return default_name
def rotator(source: str, dest: str) -> None:
# 确保目录存在
Path(dest).parent.mkdir(parents=True, exist_ok=True)
# 重命名文件
Path(source).rename(dest)
handler.namer = namer handler.namer = namer
handler.rotator = rotator
return handler return handler
def _install_excepthook(self) -> None:
def excepthook(exc_type, exc_value, exc_tb):
if issubclass(exc_type, KeyboardInterrupt):
return
self._logger.error("未捕获的异常", exc_info=(exc_type, exc_value, exc_tb))
sys.excepthook = excepthook
def configure(self) -> logging.Logger: def configure(self) -> logging.Logger:
if self._configured: if self._configured:
return self._logger return self._logger
@@ -70,9 +75,6 @@ class AppLogger:
console.setFormatter(formatter) console.setFormatter(formatter)
self._logger.addHandler(console) self._logger.addHandler(console)
# 全局异常钩子
self._install_excepthook()
self._configured = True self._configured = True
return self._logger return self._logger
-19
View File
@@ -1939,25 +1939,6 @@
} }
] ]
}, },
{
"name": "表单构建",
"type": 2,
"icon": "el-icon-Wallet",
"order": 2,
"permission": "gencode:gencode:query",
"route_name": "webcode",
"route_path": "/gencode/webcode",
"component_path": "gencode/webcode/index",
"status": true,
"keep_alive": true,
"hidden": false,
"always_show": false,
"title": "表单构建",
"params": null,
"affix": false,
"redirect": null,
"description": "表单构建"
},
{ {
"name": "示例管理", "name": "示例管理",
"type": 2, "type": 2,
+76 -55
View File
@@ -6,7 +6,7 @@ from typing import List
from app.common.constant import GenConstant from app.common.constant import GenConstant
from app.config.setting import settings from app.config.setting import settings
from app.utils.string_util import StringUtil from app.utils.string_util import StringUtil
from app.api.v1.module_generator.gencode.schema import GenTableSchema, GenTableColumnSchema from app.api.v1.module_generator.gencode.schema import GenTableOutSchema, GenTableSchema, GenTableColumnSchema
class GenUtils: class GenUtils:
@@ -24,46 +24,47 @@ class GenUtils:
- None - None
""" """
# 只有当字段为None时才设置默认值 # 只有当字段为None时才设置默认值
if gen_table.class_name is None: gen_table.class_name = cls.convert_class_name(gen_table.table_name or "")
gen_table.class_name = cls.convert_class_name(gen_table.table_name or "") gen_table.package_name = settings.package_name
if gen_table.package_name is None: gen_table.module_name = settings.package_name.split('.')[-1]
gen_table.package_name = settings.package_name gen_table.business_name = gen_table.table_name.split('_')[-1]
if gen_table.module_name is None: gen_table.function_name = re.sub(r'(?:表|测试)', '', gen_table.table_comment or "")
gen_table.module_name = settings.package_name.split('.')[-1]
if gen_table.business_name is None:
gen_table.business_name = gen_table.table_name.split('_')[-1]
if gen_table.function_name is None:
gen_table.function_name = re.sub(r'(?:表|测试)', '', gen_table.table_comment or "")
@classmethod @classmethod
def init_column_field(cls, column: GenTableColumnSchema, table: GenTableSchema) -> None: def init_column_field(cls, column: GenTableColumnSchema, table: GenTableOutSchema) -> None:
""" """
初始化列属性字段 初始化列属性字段
参数: 参数:
- column (GenTableColumnSchema): 业务表字段对象 - column (GenTableColumnSchema): 业务表字段对象
- table (GenTableSchema): 业务表对象 - table (GenTableOutSchema): 业务表对象
返回: 返回:
- None - None
""" """
data_type = cls.get_db_type(column.column_type or "") data_type = cls.get_db_type(column.column_type or "")
column_name = column.column_name or "" column_name = column.column_name or ""
# 只有当table_id为None时才设置 column.table_id = table.id
if column.table_id is None: column.python_field = cls.to_camel_case(column_name)
column.table_id = table.table_id
# 只有当python_field为None时才设置
if column.python_field is None:
column.python_field = column_name
# 只有当python_type为None时才设置默认类型 # 只有当python_type为None时才设置默认类型
if column.python_type is None: column.python_type = StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_PYTHON, data_type)
# 根据数据库类型映射到Python类型(统一使用大写键以兼容MySQL)
column.python_type = GenConstant.DB_TO_PYTHON.get(data_type.upper(), "Any")
# 查询类型:优先根据字段语义(如以name结尾走LIKE),否则默认EQ # 查询类型:优先根据字段语义(如以name结尾走LIKE),否则默认EQ
if column.query_type is None: column.query_type = GenConstant.QUERY_LIKE
column.query_type = GenConstant.QUERY_LIKE if column_name.lower().endswith("name") else GenConstant.QUERY_EQ
# 确保is_pk等字段为字符串格式
# 将布尔值或其他类型转换为字符串'1'或'0'
if column.is_pk is not None and not isinstance(column.is_pk, str):
column.is_pk = '1' if bool(column.is_pk) else '0'
if column.is_increment is not None and not isinstance(column.is_increment, str):
column.is_increment = '1' if bool(column.is_increment) else '0'
if column.is_required is not None and not isinstance(column.is_required, str):
column.is_required = '1' if bool(column.is_required) else '0'
# 确保None值默认为'0'
column.is_pk = column.is_pk or '0'
column.is_increment = column.is_increment or '0'
column.is_required = column.is_required or '0'
# HTML类型:根据数据库类型设置基础控件,再根据字段语义进行覆写
if column.html_type is None: if column.html_type is None:
if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains( if cls.arrays_contains(GenConstant.COLUMNTYPE_STR, data_type) or cls.arrays_contains(
GenConstant.COLUMNTYPE_TEXT, data_type GenConstant.COLUMNTYPE_TEXT, data_type
@@ -96,37 +97,50 @@ class GenUtils:
# 只有当is_insert为None时才设置插入字段(默认所有字段都需要插入) # 只有当is_insert为None时才设置插入字段(默认所有字段都需要插入)
if column.is_insert is None: if column.is_insert is None:
column.is_insert = GenConstant.REQUIRE column.is_insert = GenConstant.REQUIRE
else:
# 确保is_insert为字符串格式
column.is_insert = str(column.is_insert) if column.is_insert is not None else '0'
# 只有当is_edit为None时才设置编辑字段 # 只有当is_edit为None时才设置编辑字段
if column.is_edit is None and not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and not (column.is_pk is not None and column.is_pk == GenConstant.REQUIRE): if column.is_edit is None:
column.is_edit = GenConstant.REQUIRE if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_EDIT, column_name) and column.is_pk != '1':
column.is_edit = GenConstant.REQUIRE
else:
column.is_edit = '0'
else:
# 确保is_edit为字符串格式
column.is_edit = str(column.is_edit) if column.is_edit is not None else '0'
# 只有当is_list为None时才设置列表字段 # 只有当is_list为None时才设置列表字段
if column.is_list is None and not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and not (column.is_pk is not None and column.is_pk == GenConstant.REQUIRE): if column.is_list is None:
column.is_list = GenConstant.REQUIRE if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_LIST, column_name) and column.is_pk != '1':
column.is_list = GenConstant.REQUIRE
else:
column.is_list = '0'
else:
# 确保is_list为字符串格式
column.is_list = str(column.is_list) if column.is_list is not None else '0'
# 只有当is_query为None时才设置查询字段 # 只有当is_query为None时才设置查询字段
if column.is_query is None and not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and not (column.is_pk is not None and column.is_pk == GenConstant.REQUIRE): if column.is_query is None:
column.is_query = GenConstant.REQUIRE if not cls.arrays_contains(GenConstant.COLUMNNAME_NOT_QUERY, column_name) and column.is_pk != '1':
column.is_query = GenConstant.REQUIRE
# 只有当query_type为None时才设置查询字段类型 else:
if column.query_type is None: column.is_query = '0'
if column_name.lower().endswith('name'): else:
column.query_type = GenConstant.QUERY_LIKE # 确保is_query为字符串格式
column.is_query = str(column.is_query) if column.is_query is not None else '0'
@classmethod @classmethod
def arrays_contains(cls, arr: List[str], target_value: str) -> bool: def arrays_contains(cls, arr: List[str], target_value: str) -> bool:
""" """
校验数组是否包含指定值忽略大小写 校验数组是否包含指定值
参数: param arr: 数组
- arr (List[str]): 数组 param target_value: 需要校验的值
- target_value (str): 需要校验的值 :return: 校验结果
返回:
- bool: 校验结果
""" """
if target_value is None: return target_value in arr
return False
return any(item.lower() == target_value.lower() for item in arr)
@classmethod @classmethod
def convert_class_name(cls, table_name: str) -> str: def convert_class_name(cls, table_name: str) -> str:
@@ -189,13 +203,9 @@ class GenUtils:
返回: 返回:
- int: 字段长度优先取第一个长度值无法解析时返回0 - int: 字段长度优先取第一个长度值无法解析时返回0
""" """
if '(' in column_type and ')' in column_type: if '(' in column_type:
try: length = len(column_type.split('(')[1].split(')')[0])
inner = column_type.split('(')[1].split(')')[0] return length
first = inner.split(',')[0].strip()
return int(first)
except Exception:
return 0
return 0 return 0
@classmethod @classmethod
@@ -211,4 +221,15 @@ class GenUtils:
""" """
if '(' in column_type and ')' in column_type: if '(' in column_type and ')' in column_type:
return column_type.split('(')[1].split(')')[0].split(',') return column_type.split('(')[1].split(')')[0].split(',')
return [] return []
@classmethod
def to_camel_case(cls, text: str) -> str:
"""
将字符串转换为驼峰命名
param text: 需要转换的字符串
:return: 驼峰命名
"""
parts = text.split('_')
return parts[0] + ''.join(word.capitalize() for word in parts[1:])
+99 -75
View File
@@ -39,28 +39,31 @@ class Jinja2TemplateUtil:
返回: 返回:
- Environment: Jinja2 环境对象 - Environment: Jinja2 环境对象
""" """
if cls._env is None: try:
# 确保模板目录存在 if cls._env is None:
template_dir = settings.TEMPLATE_DIR # 确保模板目录存在
if not os.path.exists(template_dir): template_dir = settings.TEMPLATE_DIR
raise RuntimeError(f'模板目录不存在: {template_dir}') if not os.path.exists(template_dir):
raise RuntimeError(f'模板目录不存在: {template_dir}')
cls._env = Environment( cls._env = Environment(
loader=FileSystemLoader(settings.TEMPLATE_DIR), loader=FileSystemLoader(settings.TEMPLATE_DIR),
autoescape=select_autoescape(['html', 'xml', 'jinja']), # 自动转义HTML autoescape=select_autoescape(['html', 'xml', 'jinja', 'j2']), # 自动转义HTML
trim_blocks=True, # 删除多余的空行 trim_blocks=True, # 删除多余的空行
lstrip_blocks=True, # 删除行首空格 lstrip_blocks=True, # 删除行首空格
keep_trailing_newline=True, # 保留行尾换行符 keep_trailing_newline=True, # 保留行尾换行符
enable_async=True, # 开启异步支持 enable_async=True, # 开启异步支持
) )
cls._env.filters.update( cls._env.filters.update(
{ {
'camel_to_snake': SnakeCaseUtil.camel_to_snake, 'camel_to_snake': SnakeCaseUtil.camel_to_snake,
'snake_to_camel': CamelCaseUtil.snake_to_camel, 'snake_to_camel': CamelCaseUtil.snake_to_camel,
'get_sqlalchemy_type': cls.get_sqlalchemy_type, 'get_sqlalchemy_type': cls.get_sqlalchemy_type,
} }
) )
return cls._env return cls._env
except Exception as e:
raise RuntimeError(f'初始化Jinja2模板引擎失败: {e}')
@classmethod @classmethod
def get_template(cls, template_path: str) -> Template: def get_template(cls, template_path: str) -> Template:
@@ -90,36 +93,27 @@ class Jinja2TemplateUtil:
- Dict[str, Any]: 模板上下文字典 - Dict[str, Any]: 模板上下文字典
""" """
# 处理options为None的情况 # 处理options为None的情况
options = gen_table.options or '{}' # if not gen_table.options:
try: # raise ValueError('请先完善生成配置信息')
params_obj = json.loads(options)
except json.JSONDecodeError:
params_obj = {}
class_name = gen_table.class_name or '' class_name = gen_table.class_name or ''
module_name = gen_table.module_name or '' module_name = gen_table.module_name or ''
business_name = gen_table.business_name or '' business_name = gen_table.business_name or ''
package_name = gen_table.package_name or '' package_name = gen_table.package_name or ''
function_name = gen_table.function_name or '' function_name = gen_table.function_name or ''
# 确保pk_column不为None
pk_column = gen_table.pk_column
if pk_column is None and gen_table.columns:
# 如果没有明确的主键列,使用第一个列作为主键
pk_column = gen_table.columns[0]
context = { context = {
'table_name': gen_table.table_name or '', 'table_name': gen_table.table_name or '',
'table_comment': gen_table.table_comment or '',
'function_name': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】', 'function_name': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】',
'class_name': class_name, 'class_name': class_name,
'module_name': module_name, 'module_name': module_name,
'business_name': business_name.capitalize() if business_name else '', 'business_name': business_name.capitalize(),
'base_package': cls.get_package_prefix(package_name) if package_name else '', 'base_package': cls.get_package_prefix(package_name),
'package_name': package_name, 'package_name': package_name,
'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'datetime': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'pk_column': pk_column, 'pk_column': gen_table.pk_column,
'do_import_list': cls.get_import_list(gen_table, "model"), 'model_import_list': cls.get_model_import_list(gen_table),
'vo_import_list': cls.get_import_list(gen_table, "schema"), 'schema_import_list': cls.get_schema_import_list(gen_table),
'permission_prefix': cls.get_permission_prefix(module_name, business_name), 'permission_prefix': cls.get_permission_prefix(module_name, business_name),
'columns': gen_table.columns or [], 'columns': gen_table.columns or [],
'table': gen_table, 'table': gen_table,
@@ -127,12 +121,10 @@ class Jinja2TemplateUtil:
'db_type': settings.DATABASE_TYPE, 'db_type': settings.DATABASE_TYPE,
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW, 'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW, 'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
'primary_key': pk_column.python_field if pk_column else ''
} }
return context return context
@classmethod @classmethod
def get_template_list(cls): def get_template_list(cls):
""" """
@@ -156,6 +148,7 @@ class Jinja2TemplateUtil:
] ]
return templates return templates
@classmethod @classmethod
def get_file_name(cls, template: str, gen_table: GenTableOutSchema): def get_file_name(cls, template: str, gen_table: GenTableOutSchema):
""" """
@@ -215,42 +208,60 @@ class Jinja2TemplateUtil:
return package_name[: package_name.rfind('.')] if '.' in package_name else package_name return package_name[: package_name.rfind('.')] if '.' in package_name else package_name
@classmethod @classmethod
def get_import_list(cls, gen_table: GenTableOutSchema, model_type: str): def get_schema_import_list(cls, gen_table: GenTableOutSchema):
""" """
获取导入包列表 获取schema模板导入包列表
参数: :param gen_table: 生成表的配置信息
- gen_table (GenTableOutSchema): 生成表的配置信息 :return: 导入包列表
- model_type (str): 模型类型 ("model" "schema")
返回:
- List[str]: 导入包列表
""" """
columns = gen_table.columns or [] columns = gen_table.columns or []
import_list = set() import_list = set()
if model_type == "model":
import_list.add('from sqlalchemy import Column')
for column in columns: for column in columns:
column_type = column.column_type or '' if column.python_type in GenConstant.TYPE_DATE:
if model_type == "schema": import_list.add(f'from datetime import {column.python_type}')
# Schema特定导入逻辑 elif column.python_type == GenConstant.TYPE_DECIMAL:
if column_type in GenConstant.TYPE_DATE: import_list.add('from decimal import Decimal')
import_list.add(f'from datetime import {column_type}') if gen_table.sub:
elif column_type == GenConstant.TYPE_DECIMAL: if gen_table.sub_table and gen_table.sub_table.columns:
import_list.add('from decimal import Decimal') sub_columns = gen_table.sub_table.columns or []
else: # model for sub_column in sub_columns:
# Model特定导入逻辑 if sub_column.python_type in GenConstant.TYPE_DATE:
data_type = cls.get_db_type(column_type) import_list.add(f'from datetime import {sub_column.python_type}')
elif sub_column.python_type == GenConstant.TYPE_DECIMAL:
import_list.add('from decimal import Decimal')
return cls.merge_same_imports(list(import_list), 'from datetime import')
@classmethod
def get_model_import_list(cls, gen_table: GenTableOutSchema):
"""
获取do模板导入包列表
:param gen_table: 生成表的配置信息
:return: 导入包列表
"""
columns = gen_table.columns or []
import_list = set()
import_list.add('from sqlalchemy import Column')
for column in columns:
if column.column_type:
data_type = cls.get_db_type(column.column_type)
if data_type in GenConstant.COLUMNTYPE_GEOMETRY: if data_type in GenConstant.COLUMNTYPE_GEOMETRY:
import_list.add('from geoalchemy2 import Geometry') import_list.add('from geoalchemy2 import Geometry')
sqlalchemy_type = GenConstant.DB_TO_SQLALCHEMY.get(data_type) import_list.add(
if sqlalchemy_type: f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}'
import_list.add(f'from sqlalchemy import {sqlalchemy_type}') )
if gen_table.sub:
return cls.merge_same_imports(list(import_list), import_list.add('from sqlalchemy import ForeignKey')
'from datetime import' if model_type == "schema" else 'from sqlalchemy import') if gen_table.sub_table and gen_table.sub_table.columns:
sub_columns = gen_table.sub_table.columns or []
for sub_column in sub_columns:
if sub_column.column_type:
data_type = cls.get_db_type(sub_column.column_type)
import_list.add(
f'from sqlalchemy import {StringUtil.get_mapping_value_by_key_ignore_case(GenConstant.DB_TO_SQLALCHEMY, data_type)}'
)
return cls.merge_same_imports(list(import_list), 'from sqlalchemy import')
@classmethod @classmethod
def get_db_type(cls, column_type: str) -> str: def get_db_type(cls, column_type: str) -> str:
@@ -367,10 +378,23 @@ class Jinja2TemplateUtil:
返回: 返回:
- str: SQLAlchemy 类型字符串 - str: SQLAlchemy 类型字符串
""" """
column_type = getattr(column, 'column_type', str(column)) if '(' in column:
if not column_type: column_type_list = column.split('(')
return "String" if column_type_list[0] in GenConstant.COLUMNTYPE_STR:
sqlalchemy_type = (
# 直接映射,简化逻辑 StringUtil.get_mapping_value_by_key_ignore_case(
base_type = column_type.split('(')[0] if '(' in column_type else column_type GenConstant.DB_TO_SQLALCHEMY, column_type_list[0]
return GenConstant.DB_TO_SQLALCHEMY.get(base_type, "String") )
+ '('
+ column_type_list[1]
)
else:
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_SQLALCHEMY, column_type_list[0]
)
else:
sqlalchemy_type = StringUtil.get_mapping_value_by_key_ignore_case(
GenConstant.DB_TO_SQLALCHEMY, column
)
return sqlalchemy_type
+2 -2
View File
@@ -22,7 +22,7 @@ pydantic-settings==2.5.2 # 配置设置
psutil==6.1.0 # 系统信息 psutil==6.1.0 # 系统信息
python-multipart==0.0.9 # request.form() 对表单进行「解析」时安装 python-multipart==0.0.9 # request.form() 对表单进行「解析」时安装
greenlet==3.1.1 # 协程框架 greenlet==3.1.1 # 协程框架
bcrypt==4.3.0 # 密码加密解析 bcrypt==4.0.1 # 密码加密解析,切勿升级,如果升级,请同时升级python版本
itsdangerous==2.2.0 # 用于安全处理各种数据,如密码、密钥等 itsdangerous==2.2.0 # 用于安全处理各种数据,如密码、密钥等
aiofiles==24.1.0 # 文件操作 aiofiles==24.1.0 # 文件操作
redis==5.2.1 # redis 同步操作数据库(用户celery配套使用)redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本 redis==5.2.1 # redis 同步操作数据库(用户celery配套使用)redis 异步操作数据库 redis已经完全具备了aioredis的功能,无需重复安全,且aioredis已经不再维护也不兼容3.10+的版本
@@ -35,5 +35,5 @@ PyMySQL==1.1.2 # mysql 异步操作数据库基于 pymysqlaiomys
cryptography==45.0.2 # mysql8 密码加密 cryptography==45.0.2 # mysql8 密码加密
openai==1.55.2 # ai 大模型 openai==1.55.2 # ai 大模型
oss2==2.18.4 # 阿里云对象存储 oss2==2.18.4 # 阿里云对象存储
rich==13.9.4 rich==13.9.4 # 终端打印美化
sqlglot[rs]==27.8.0 # sql 解析 sqlglot[rs]==27.8.0 # sql 解析
+14 -18
View File
@@ -153,6 +153,10 @@ export interface GenTableOutVO {
table_name?: string; table_name?: string;
/** 表描述 */ /** 表描述 */
table_comment?: string; table_comment?: string;
/** 关联子表的表名 */
sub_table_name?: string;
/** 关联子表的外键名 */
sub_table_fk_name?: string;
/** 实体类名称 */ /** 实体类名称 */
class_name?: string; class_name?: string;
/** 生成包路径 */ /** 生成包路径 */
@@ -166,23 +170,7 @@ export interface GenTableOutVO {
/** 生成代码方式(0zip压缩包 1自定义路径) */ /** 生成代码方式(0zip压缩包 1自定义路径) */
gen_type?: string; gen_type?: string;
/** 其它生成选项 */ /** 其它生成选项 */
options?: string; options?: GenTableOptionModel;
/** 上级菜单ID字段 */
parent_menu_id?: number;
/** 上级菜单名称字段 */
parent_menu_name?: string;
/** 是否为子表 */
sub?: boolean;
/** 是否为树表 */
tree?: boolean;
/** 是否为单表 */
crud?: boolean;
/** 表描述 */
description?: string;
/** 列列表 */
columns?: GenTableColumnOutSchema[];
/** 参数选项 */
params?: GenTableOptionModel;
} }
/** 表选项模型 */ /** 表选项模型 */
@@ -193,12 +181,20 @@ export interface GenTableOptionModel {
/** 代码生成业务表模型 */ /** 代码生成业务表模型 */
export interface GenTableSchema extends GenTableOutVO { export interface GenTableSchema extends GenTableOutVO {
/** 表描述 */
description?: string;
/** 上级菜单ID字段 */
parent_menu_id?: number;
/** 上级菜单名称字段 */
parent_menu_name?: string;
/** 主键信息 */ /** 主键信息 */
pk_column?: GenTableColumnOutSchema; pk_column?: GenTableColumnOutSchema;
/** 子表信息 */ /** 子表信息 */
sub_table?: GenTableSchema; sub_table?: GenTableSchema;
/** 表列信息 */ /** 表列信息 */
columns: GenTableColumnOutSchema[]; columns: GenTableColumnOutSchema[];
/** 是否为子表 */
sub?: boolean;
} }
/** 代码生成业务表列模型 */ /** 代码生成业务表列模型 */
@@ -325,5 +321,5 @@ export interface BasicInfoFormData {
table_name?: string; table_name?: string;
table_comment?: string; table_comment?: string;
class_name?: string; class_name?: string;
remark?: string; description?: string;
} }
-600
View File
@@ -1,600 +0,0 @@
// TypeScript interfaces for form configuration
export interface FormConf {
formRef: string;
formModel: string;
size: string;
labelPosition: string;
labelWidth: number;
formRules: string;
gutter: number;
disabled: boolean;
span: number;
formBtns: boolean;
}
export interface BaseComponent {
label: string;
tag: string;
tagIcon: string;
defaultValue?: any;
span?: number;
labelWidth?: number | null;
style?: Record<string, any>;
disabled?: boolean;
required?: boolean;
regList?: Array<{
pattern: string;
message: string;
}>;
changeTag?: boolean;
document?: string;
}
export interface ComponentOption {
label: string;
value: any;
disabled?: boolean;
id?: number;
children?: ComponentOption[];
}
export interface FormComponent extends BaseComponent {
// Input related
type?: string;
placeholder?: string;
clearable?: boolean;
prepend?: string;
append?: string;
'prefix-icon'?: string;
'suffix-icon'?: string;
maxlength?: number | null;
'show-word-limit'?: boolean;
readonly?: boolean;
autosize?: {
minRows: number;
maxRows: number;
};
'show-password'?: boolean;
// Number input related
min?: number;
max?: number;
step?: number;
'step-strictly'?: boolean;
precision?: number;
'controls-position'?: string;
// Select related
filterable?: boolean;
multiple?: boolean;
options?: ComponentOption[];
// Cascader related
props?: Record<string, any>;
'show-all-levels'?: boolean;
dataType?: string;
labelKey?: string;
valueKey?: string;
childrenKey?: string;
separator?: string;
// Radio/Checkbox related
optionType?: string;
border?: boolean;
size?: string;
// Switch related
'active-text'?: string;
'inactive-text'?: string;
'active-color'?: string | null;
'inactive-color'?: string | null;
'active-value'?: any;
'inactive-value'?: any;
// Slider related
'show-stops'?: boolean;
range?: boolean;
// Date/Time picker related
format?: string;
'value-format'?: string;
'is-range'?: boolean;
'range-separator'?: string;
'start-placeholder'?: string;
'end-placeholder'?: string;
'picker-options'?: any;
// Rate related
'allow-half'?: boolean;
'show-text'?: boolean;
'show-score'?: boolean;
// Color picker related
'color-format'?: string;
'show-alpha'?: boolean;
// Upload related
action?: string;
accept?: string;
name?: string;
'auto-upload'?: boolean;
showTip?: boolean;
buttonText?: string;
fileSize?: number;
sizeUnit?: string;
'list-type'?: string;
tip?: string;
}
export interface LayoutComponent {
layout: string;
tagIcon: string;
type?: string;
justify?: string;
align?: string;
label: string;
layoutTree?: boolean;
children?: any[];
tag?: string;
default?: string;
icon?: string;
size?: string;
labelWidth?: number | null;
changeTag?: boolean;
span?: number;
disabled?: boolean;
document?: string;
}
export const formConf: FormConf = {
formRef: 'formRef',
formModel: 'formData',
size: 'default',
labelPosition: 'right',
labelWidth: 100,
formRules: 'rules',
gutter: 15,
disabled: false,
span: 24,
formBtns: true,
}
export const inputComponents: FormComponent[] = [
{
label: '单行文本',
tag: 'el-input',
tagIcon: 'input',
type: 'text',
placeholder: '请输入',
defaultValue: undefined,
span: 24,
labelWidth: null,
style: { width: '100%' },
clearable: true,
prepend: '',
append: '',
'prefix-icon': '',
'suffix-icon': '',
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/input',
},
{
label: '多行文本',
tag: 'el-input',
tagIcon: 'textarea',
type: 'textarea',
placeholder: '请输入',
defaultValue: undefined,
span: 24,
labelWidth: null,
autosize: {
minRows: 4,
maxRows: 4,
},
style: { width: '100%' },
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/input',
},
{
label: '密码',
tag: 'el-input',
tagIcon: 'password',
type: 'password',
placeholder: '请输入',
defaultValue: undefined,
span: 24,
'show-password': true,
labelWidth: null,
style: { width: '100%' },
clearable: true,
prepend: '',
append: '',
'prefix-icon': '',
'suffix-icon': '',
maxlength: null,
'show-word-limit': false,
readonly: false,
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/input',
},
{
label: '计数器',
tag: 'el-input-number',
tagIcon: 'number',
placeholder: '',
defaultValue: undefined,
span: 24,
labelWidth: null,
min: undefined,
max: undefined,
step: undefined,
'step-strictly': false,
precision: undefined,
'controls-position': '',
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/input-number',
},
]
export const selectComponents: FormComponent[] = [
{
label: '下拉选择',
tag: 'el-select',
tagIcon: 'select',
placeholder: '请选择',
defaultValue: undefined,
span: 24,
labelWidth: null,
style: { width: '100%' },
clearable: true,
disabled: false,
required: true,
filterable: false,
multiple: false,
options: [
{
label: '选项一',
value: 1,
},
{
label: '选项二',
value: 2,
},
],
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/select',
},
{
label: '级联选择',
tag: 'el-cascader',
tagIcon: 'cascader',
placeholder: '请选择',
defaultValue: [],
span: 24,
labelWidth: null,
style: { width: '100%' },
props: {
props: {
multiple: false,
},
},
'show-all-levels': true,
disabled: false,
clearable: true,
filterable: false,
required: true,
options: [
{
id: 1,
value: 1,
label: '选项1',
children: [
{
id: 2,
value: 2,
label: '选项1-1',
},
],
},
],
dataType: 'dynamic',
labelKey: 'label',
valueKey: 'value',
childrenKey: 'children',
separator: '/',
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/cascader',
},
{
label: '单选框组',
tag: 'el-radio-group',
tagIcon: 'radio',
defaultValue: 0,
span: 24,
labelWidth: null,
style: {},
optionType: 'default',
border: false,
size: 'default',
disabled: false,
required: true,
options: [
{
label: '选项一',
value: 1,
},
{
label: '选项二',
value: 2,
},
],
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/radio',
},
{
label: '多选框组',
tag: 'el-checkbox-group',
tagIcon: 'checkbox',
defaultValue: [],
span: 24,
labelWidth: null,
style: {},
optionType: 'default',
border: false,
size: 'default',
disabled: false,
required: true,
options: [
{
label: '选项一',
value: 1,
},
{
label: '选项二',
value: 2,
},
],
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/checkbox',
},
{
label: '开关',
tag: 'el-switch',
tagIcon: 'switch',
defaultValue: false,
span: 24,
labelWidth: null,
style: {},
disabled: false,
required: true,
'active-text': '',
'inactive-text': '',
'active-color': null,
'inactive-color': null,
'active-value': true,
'inactive-value': false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/switch',
},
{
label: '滑块',
tag: 'el-slider',
tagIcon: 'slider',
defaultValue: null,
span: 24,
labelWidth: null,
disabled: false,
required: true,
min: 0,
max: 100,
step: 1,
'show-stops': false,
range: false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/slider',
},
{
label: '时间选择',
tag: 'el-time-picker',
tagIcon: 'time',
placeholder: '请选择',
defaultValue: '',
span: 24,
labelWidth: null,
style: { width: '100%' },
disabled: false,
clearable: true,
required: true,
format: 'HH:mm:ss',
'value-format': 'HH:mm:ss',
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/time-picker',
},
{
label: '时间范围',
tag: 'el-time-picker',
tagIcon: 'time-range',
defaultValue: null,
span: 24,
labelWidth: null,
style: { width: '100%' },
disabled: false,
clearable: true,
required: true,
'is-range': true,
'range-separator': '至',
'start-placeholder': '开始时间',
'end-placeholder': '结束时间',
format: 'HH:mm:ss',
'value-format': 'HH:mm:ss',
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/time-picker',
},
{
label: '日期选择',
tag: 'el-date-picker',
tagIcon: 'date',
placeholder: '请选择',
defaultValue: null,
type: 'date',
span: 24,
labelWidth: null,
style: { width: '100%' },
disabled: false,
clearable: true,
required: true,
format: 'YYYY-MM-DD',
'value-format': 'YYYY-MM-DD',
readonly: false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/date-picker',
},
{
label: '日期范围',
tag: 'el-date-picker',
tagIcon: 'date-range',
defaultValue: null,
span: 24,
labelWidth: null,
style: { width: '100%' },
type: 'daterange',
'range-separator': '至',
'start-placeholder': '开始日期',
'end-placeholder': '结束日期',
disabled: false,
clearable: true,
required: true,
format: 'YYYY-MM-DD',
'value-format': 'YYYY-MM-DD',
readonly: false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/date-picker',
},
{
label: '评分',
tag: 'el-rate',
tagIcon: 'rate',
defaultValue: 0,
span: 24,
labelWidth: null,
style: {},
max: 5,
'allow-half': false,
'show-text': false,
'show-score': false,
disabled: false,
required: true,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/rate',
},
{
label: '颜色选择',
tag: 'el-color-picker',
tagIcon: 'color',
defaultValue: null,
labelWidth: null,
'show-alpha': false,
'color-format': '',
disabled: false,
required: true,
size: 'default',
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/color-picker',
},
{
label: '上传',
tag: 'el-upload',
tagIcon: 'upload',
action: 'https://jsonplaceholder.typicode.com/posts/',
defaultValue: null,
labelWidth: null,
disabled: false,
required: true,
accept: '',
name: 'file',
'auto-upload': true,
showTip: false,
buttonText: '点击上传',
fileSize: 2,
sizeUnit: 'MB',
'list-type': 'text',
multiple: false,
regList: [],
changeTag: true,
document: 'https://element-plus.org/zh-CN/component/upload',
tip: '只能上传不超过 2MB 的文件',
style: { width: '100%' },
},
]
export const layoutComponents: LayoutComponent[] = [
{
layout: 'rowFormItem',
tagIcon: 'row',
type: 'default',
justify: 'start',
align: 'top',
label: '行容器',
layoutTree: true,
children: [],
document: 'https://element-plus.org/zh-CN/component/layout',
},
{
layout: 'colFormItem',
label: '按钮',
changeTag: true,
labelWidth: null,
tag: 'el-button',
tagIcon: 'button',
span: 24,
default: '主要按钮',
type: 'primary',
icon: 'Search',
size: 'default',
disabled: false,
document: 'https://element-plus.org/zh-CN/component/button',
},
]
// 组件rule的触发方式,无触发方式的组件不生成rule
export const trigger: Record<string, string> = {
'el-input': 'blur',
'el-input-number': 'blur',
'el-select': 'change',
'el-radio-group': 'change',
'el-checkbox-group': 'change',
'el-cascader': 'change',
'el-time-picker': 'change',
'el-date-picker': 'change',
'el-rate': 'change',
}
-27
View File
@@ -1,27 +0,0 @@
interface FormConfig {
fields: FormElement[]
}
interface FormElement {
tag: string
children?: FormElement[]
}
const styles: Record<string, string> = {
'el-rate': '.el-rate{display: inline-block; vertical-align: text-top;}',
'el-upload': '.el-upload__tip{line-height: 1.2;}'
}
function addCss(cssList: string[], el: FormElement): void {
const css = styles[el.tag]
css && cssList.indexOf(css) === -1 && cssList.push(css)
if (el.children) {
el.children.forEach(el2 => addCss(cssList, el2))
}
}
export function makeUpCss(conf: FormConfig): string {
const cssList: string[] = []
conf.fields.forEach(el => addCss(cssList, el))
return cssList.join('\n')
}
@@ -1,59 +0,0 @@
export interface DrawingItem {
layout: string;
tagIcon: string;
label: string;
vModel: string;
formId: number;
tag: string;
placeholder: string;
defaultValue: string;
span: number;
style: Record<string, any>;
clearable: boolean;
prepend: string;
append: string;
'prefix-icon': string;
'suffix-icon': string;
maxlength: number;
'show-word-limit': boolean;
readonly: boolean;
disabled: boolean;
required: boolean;
changeTag: boolean;
regList: Array<{
pattern: string;
message: string;
}>;
}
const drawingDefault: DrawingItem[] = [
{
layout: 'colFormItem',
tagIcon: 'input',
label: '手机号',
vModel: 'mobile',
formId: 6,
tag: 'el-input',
placeholder: '请输入手机号',
defaultValue: '',
span: 24,
style: { width: '100%' },
clearable: true,
prepend: '',
append: '',
'prefix-icon': 'Cellphone',
'suffix-icon': '',
maxlength: 11,
'show-word-limit': true,
readonly: false,
disabled: false,
required: true,
changeTag: true,
regList: [{
pattern: '/^1(3|4|5|7|8|9)\\d{9}$/',
message: '手机号格式错误'
}]
}
]
export default drawingDefault;
-455
View File
@@ -1,455 +0,0 @@
/* eslint-disable max-len */
import { trigger } from './config'
interface FormConfig {
formRef: string
formModel: string
formRules: string
size: string
disabled?: boolean
labelWidth: number
labelPosition: string
gutter: number
formBtns: boolean
fields: FormElement[]
}
interface FormElement {
tag: string
layout: string
label?: string
vModel?: string
span: number
labelWidth?: number | null
required?: boolean
disabled?: boolean
type?: string
placeholder?: string
maxlength?: number | null
'show-word-limit'?: boolean
readonly?: boolean
clearable?: boolean
'prefix-icon'?: string
'suffix-icon'?: string
'show-password'?: boolean
autosize?: { minRows: number; maxRows: number }
'controls-position'?: string
min?: number | undefined
max?: number | undefined
step?: number | undefined
'step-strictly'?: boolean
precision?: number | undefined
multiple?: boolean
filterable?: boolean
options?: Array<{ label: string; value: any; disabled?: boolean }>
size?: string
optionType?: string
border?: boolean
'active-text'?: string
'inactive-text'?: string
'active-color'?: string | null
'inactive-color'?: string | null
'active-value'?: any
'inactive-value'?: any
'show-stops'?: boolean
range?: boolean
'is-range'?: boolean
'range-separator'?: string
'start-placeholder'?: string
'end-placeholder'?: string
format?: string
'value-format'?: string
'picker-options'?: any
'allow-half'?: boolean
'show-text'?: boolean
'show-score'?: boolean
'show-alpha'?: boolean
'color-format'?: string
action?: string
'list-type'?: string
accept?: string
name?: string
'auto-upload'?: boolean
showTip?: boolean
buttonText?: string
fileSize?: number
sizeUnit?: string
'show-all-levels'?: boolean
props?: any
separator?: string
icon?: string
default?: string
justify?: string
align?: string
gutter?: number
children?: FormElement[]
style?: Record<string, any>
prepend?: string
append?: string
}
let confGlobal: FormConfig
let someSpanIsNot24: boolean
export function dialogWrapper(str: string): string {
return `<el-dialog v-model="dialogVisible" @open="onOpen" @close="onClose" title="Dialog Titile">
${str}
<template #footer>
<el-button @click="close"></el-button>
<el-button type="primary" @click="handelConfirm"></el-button>
</template>
</el-dialog>`
}
export function vueTemplate(str: string): string {
return `<template>
<div class="app-container">
${str}
</div>
</template>`
}
export function vueScript(str: string): string {
return `<script setup>
${str}
</script>`
}
export function cssStyle(cssStr: string): string {
return `<style>
${cssStr}
</style>`
}
function buildFormTemplate(conf: FormConfig, child: string, type: string): string {
let labelPosition = ''
if (conf.labelPosition !== 'right') {
labelPosition = `label-position="${conf.labelPosition}"`
}
const disabled = conf.disabled ? `:disabled="${conf.disabled}"` : ''
let str = `<el-form ref="${conf.formRef}" :model="${conf.formModel}" :rules="${conf.formRules}" size="${conf.size}" ${disabled} label-width="${conf.labelWidth}px" ${labelPosition}>
${child}
${buildFromBtns(conf, type)}
</el-form>`
if (someSpanIsNot24) {
str = `<el-row :gutter="${conf.gutter}">
${str}
</el-row>`
}
return str
}
function buildFromBtns(conf: FormConfig, type: string): string {
let str = ''
if (conf.formBtns && type === 'file') {
str = `<el-form-item>
<el-button type="primary" @click="submitForm"></el-button>
<el-button @click="resetForm"></el-button>
</el-form-item>`
if (someSpanIsNot24) {
str = `<el-col :span="24">
${str}
</el-col>`
}
}
return str
}
// span不为24的用el-col包裹
function colWrapper(element: FormElement, str: string): string {
if (someSpanIsNot24 || element.span !== 24) {
return `<el-col :span="${element.span}">
${str}
</el-col>`
}
return str
}
const layouts: Record<string, (element: FormElement) => string> = {
colFormItem(element: FormElement): string {
let labelWidth = ''
if (element.labelWidth && element.labelWidth !== confGlobal.labelWidth) {
labelWidth = `label-width="${element.labelWidth}px"`
}
const required = !trigger[element.tag as keyof typeof trigger] && element.required ? 'required' : ''
const tagDom = tags[element.tag as keyof typeof tags] ? tags[element.tag as keyof typeof tags](element) : null
let str = `<el-form-item ${labelWidth} label="${element.label}" prop="${element.vModel}" ${required}>
${tagDom}
</el-form-item>`
str = colWrapper(element, str)
return str
},
rowFormItem(element: FormElement): string {
const type = element.type === 'default' ? '' : `type="${element.type}"`
const justify = element.type === 'default' ? '' : `justify="${element.justify}"`
const align = element.type === 'default' ? '' : `align="${element.align}"`
const gutter = element.gutter ? `gutter="${element.gutter}"` : ''
const children = element.children?.map(el => layouts[el.layout](el)) || []
let str = `<el-row ${type} ${justify} ${align} ${gutter}>
${children.join('\n')}
</el-row>`
str = colWrapper(element, str)
return str
}
}
const tags: Record<string, (el: FormElement) => string> = {
'el-button': (el: FormElement): string => {
const {
tag, disabled
} = attrBuilder(el)
const type = el.type ? `type="${el.type}"` : ''
const icon = el.icon ? `icon="${el.icon}"` : ''
const size = el.size ? `size="${el.size}"` : ''
let child = buildElButtonChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${type} ${icon} ${size} ${disabled}>${child}</${el.tag}>`
},
'el-input': (el: FormElement): string => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const maxlength = el.maxlength ? `:maxlength="${el.maxlength}"` : ''
const showWordLimit = el['show-word-limit'] ? 'show-word-limit' : ''
const readonly = el.readonly ? 'readonly' : ''
const prefixIcon = el['prefix-icon'] ? `prefix-icon='${el['prefix-icon']}'` : ''
const suffixIcon = el['suffix-icon'] ? `suffix-icon='${el['suffix-icon']}'` : ''
const showPassword = el['show-password'] ? 'show-password' : ''
const type = el.type ? `type="${el.type}"` : ''
const autosize = el.autosize && el.autosize.minRows
? `:autosize="{minRows: ${el.autosize.minRows}, maxRows: ${el.autosize.maxRows}}"`
: ''
let child = buildElInputChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${vModel} ${type} ${placeholder} ${maxlength} ${showWordLimit} ${readonly} ${disabled} ${clearable} ${prefixIcon} ${suffixIcon} ${showPassword} ${autosize} ${width}>${child}</${el.tag}>`
},
'el-input-number': (el: FormElement): string => {
const { disabled, vModel, placeholder } = attrBuilder(el)
const controlsPosition = el['controls-position'] ? `controls-position=${el['controls-position']}` : ''
const min = el.min ? `:min='${el.min}'` : ''
const max = el.max ? `:max='${el.max}'` : ''
const step = el.step ? `:step='${el.step}'` : ''
const stepStrictly = el['step-strictly'] ? 'step-strictly' : ''
const precision = el.precision ? `:precision='${el.precision}'` : ''
return `<${el.tag} ${vModel} ${placeholder} ${step} ${stepStrictly} ${precision} ${controlsPosition} ${min} ${max} ${disabled}></${el.tag}>`
},
'el-select': (el: FormElement): string => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const filterable = el.filterable ? 'filterable' : ''
const multiple = el.multiple ? 'multiple' : ''
let child = buildElSelectChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${vModel} ${placeholder} ${disabled} ${multiple} ${filterable} ${clearable} ${width}>${child}</${el.tag}>`
},
'el-radio-group': (el: FormElement): string => {
const { disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
let child = buildElRadioGroupChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${vModel} ${size} ${disabled}>${child}</${el.tag}>`
},
'el-checkbox-group': (el: FormElement): string => {
const { disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
const min = el.min ? `:min="${el.min}"` : ''
const max = el.max ? `:max="${el.max}"` : ''
let child = buildElCheckboxGroupChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${vModel} ${min} ${max} ${size} ${disabled}>${child}</${el.tag}>`
},
'el-switch': (el: FormElement): string => {
const { disabled, vModel } = attrBuilder(el)
const activeText = el['active-text'] ? `active-text="${el['active-text']}"` : ''
const inactiveText = el['inactive-text'] ? `inactive-text="${el['inactive-text']}"` : ''
const activeColor = el['active-color'] ? `active-color="${el['active-color']}"` : ''
const inactiveColor = el['inactive-color'] ? `inactive-color="${el['inactive-color']}"` : ''
const activeValue = el['active-value'] !== true ? `:active-value='${JSON.stringify(el['active-value'])}'` : ''
const inactiveValue = el['inactive-value'] !== false ? `:inactive-value='${JSON.stringify(el['inactive-value'])}'` : ''
return `<${el.tag} ${vModel} ${activeText} ${inactiveText} ${activeColor} ${inactiveColor} ${activeValue} ${inactiveValue} ${disabled}></${el.tag}>`
},
'el-cascader': (el: FormElement): string => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const options = el.options ? `:options="${el.vModel}Options"` : ''
const props = el.props ? `:props="${el.vModel}Props"` : ''
const showAllLevels = el['show-all-levels'] ? '' : ':show-all-levels="false"'
const filterable = el.filterable ? 'filterable' : ''
const separator = el.separator === '/' ? '' : `separator="${el.separator}"`
return `<${el.tag} ${vModel} ${options} ${props} ${width} ${showAllLevels} ${placeholder} ${separator} ${filterable} ${clearable} ${disabled}></${el.tag}>`
},
'el-slider': (el: FormElement): string => {
const { disabled, vModel } = attrBuilder(el)
const min = el.min ? `:min='${el.min}'` : ''
const max = el.max ? `:max='${el.max}'` : ''
const step = el.step ? `:step='${el.step}'` : ''
const range = el.range ? 'range' : ''
const showStops = el['show-stops'] ? `:show-stops="${el['show-stops']}"` : ''
return `<${el.tag} ${min} ${max} ${step} ${vModel} ${range} ${showStops} ${disabled}></${el.tag}>`
},
'el-time-picker': (el: FormElement): string => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
const isRange = el['is-range'] ? 'is-range' : ''
const format = el.format ? `format="${el.format}"` : ''
const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
const pickerOptions = el['picker-options'] ? `:picker-options='${JSON.stringify(el['picker-options'])}'` : ''
return `<${el.tag} ${vModel} ${isRange} ${format} ${valueFormat} ${pickerOptions} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${disabled}></${el.tag}>`
},
'el-date-picker': (el: FormElement): string => {
const {
disabled, vModel, clearable, placeholder, width
} = attrBuilder(el)
const startPlaceholder = el['start-placeholder'] ? `start-placeholder="${el['start-placeholder']}"` : ''
const endPlaceholder = el['end-placeholder'] ? `end-placeholder="${el['end-placeholder']}"` : ''
const rangeSeparator = el['range-separator'] ? `range-separator="${el['range-separator']}"` : ''
const format = el.format ? `format="${el.format}"` : ''
const valueFormat = el['value-format'] ? `value-format="${el['value-format']}"` : ''
const type = el.type === 'date' ? '' : `type="${el.type}"`
const readonly = el.readonly ? 'readonly' : ''
return `<${el.tag} ${type} ${vModel} ${format} ${valueFormat} ${width} ${placeholder} ${startPlaceholder} ${endPlaceholder} ${rangeSeparator} ${clearable} ${readonly} ${disabled}></${el.tag}>`
},
'el-rate': (el: FormElement): string => {
const { disabled, vModel } = attrBuilder(el)
const max = el.max ? `:max='${el.max}'` : ''
const allowHalf = el['allow-half'] ? 'allow-half' : ''
const showText = el['show-text'] ? 'show-text' : ''
const showScore = el['show-score'] ? 'show-score' : ''
return `<${el.tag} ${vModel} ${allowHalf} ${showText} ${showScore} ${disabled}></${el.tag}>`
},
'el-color-picker': (el: FormElement): string => {
const { disabled, vModel } = attrBuilder(el)
const size = `size="${el.size}"`
const showAlpha = el['show-alpha'] ? 'show-alpha' : ''
const colorFormat = el['color-format'] ? `color-format="${el['color-format']}"` : ''
return `<${el.tag} ${vModel} ${size} ${showAlpha} ${colorFormat} ${disabled}></${el.tag}>`
},
'el-upload': (el: FormElement): string => {
const disabled = el.disabled ? ':disabled=\'true\'' : ''
const action = el.action ? `:action="${el.vModel}Action"` : ''
const multiple = el.multiple ? 'multiple' : ''
const listType = el['list-type'] !== 'text' ? `list-type="${el['list-type']}"` : ''
const accept = el.accept ? `accept="${el.accept}"` : ''
const name = el.name !== 'file' ? `name="${el.name}"` : ''
const autoUpload = el['auto-upload'] === false ? ':auto-upload="false"' : ''
const beforeUpload = `:before-upload="${el.vModel}BeforeUpload"`
const fileList = `:file-list="${el.vModel}fileList"`
const ref = `ref="${el.vModel}"`
let child = buildElUploadChild(el)
if (child) child = `\n${child}\n` // 换行
return `<${el.tag} ${ref} ${fileList} ${action} ${autoUpload} ${multiple} ${beforeUpload} ${listType} ${accept} ${name} ${disabled}>${child}</${el.tag}>`
}
}
interface AttrBuilderResult {
vModel: string
clearable: string
placeholder: string
width: string
disabled: string
tag?: string
}
function attrBuilder(el: FormElement): AttrBuilderResult {
return {
vModel: `v-model="${confGlobal.formModel}.${el.vModel}"`,
clearable: el.clearable ? 'clearable' : '',
placeholder: el.placeholder ? `placeholder="${el.placeholder}"` : '',
width: el.style && el.style.width ? ':style="{width: \'100%\'}"' : '',
disabled: el.disabled ? ':disabled=\'true\'' : ''
}
}
// el-buttin 子级
function buildElButtonChild(conf: FormElement): string {
const children: string[] = []
if (conf.default) {
children.push(conf.default)
}
return children.join('\n')
}
// el-input innerHTML
function buildElInputChild(conf: FormElement): string {
const children: string[] = []
if (conf.prepend) {
children.push(`<template slot="prepend">${conf.prepend}</template>`)
}
if (conf.append) {
children.push(`<template slot="append">${conf.append}</template>`)
}
return children.join('\n')
}
function buildElSelectChild(conf: FormElement): string {
const children: string[] = []
if (conf.options && conf.options.length) {
children.push(`<el-option v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.label" :value="item.value" :disabled="item.disabled"></el-option>`)
}
return children.join('\n')
}
function buildElRadioGroupChild(conf: FormElement): string {
const children: string[] = []
if (conf.options && conf.options.length) {
const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio'
const border = conf.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :value="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
}
return children.join('\n')
}
function buildElCheckboxGroupChild(conf: FormElement): string {
const children: string[] = []
if (conf.options && conf.options.length) {
const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
const border = conf.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :value="item.label" :disabled="item.disabled" ${border} />`)
}
return children.join('\n')
}
function buildElUploadChild(conf: FormElement): string {
const list: string[] = []
if (conf['list-type'] === 'picture-card') list.push('<i class="el-icon-plus"></i>')
else list.push(`<el-button size="small" type="primary" icon="el-icon-upload">${conf.buttonText}</el-button>`)
if (conf.showTip) list.push(`<div slot="tip" class="el-upload__tip">只能上传不超过 ${conf.fileSize}${conf.sizeUnit}${conf.accept}文件</div>`)
return list.join('\n')
}
export function makeUpHtml(conf: FormConfig, type: string): string {
const htmlList: string[] = []
confGlobal = conf
someSpanIsNot24 = conf.fields.some(item => item.span !== 24)
conf.fields.forEach(el => {
htmlList.push(layouts[el.layout](el))
})
const htmlStr = htmlList.join('\n')
let temp = buildFormTemplate(conf, htmlStr, type)
if (type === 'dialog') {
temp = dialogWrapper(temp)
}
confGlobal = null as any
return temp
}
-1
View File
@@ -1 +0,0 @@
["platform-eleme","eleme","delete-solid","delete","s-tools","setting","user-solid","user","phone","phone-outline","more","more-outline","star-on","star-off","s-goods","goods","warning","warning-outline","question","info","remove","circle-plus","success","error","zoom-in","zoom-out","remove-outline","circle-plus-outline","circle-check","circle-close","s-help","help","minus","plus","check","close","picture","picture-outline","picture-outline-round","upload","upload2","download","camera-solid","camera","video-camera-solid","video-camera","message-solid","bell","s-cooperation","s-order","s-platform","s-fold","s-unfold","s-operation","s-promotion","s-home","s-release","s-ticket","s-management","s-open","s-shop","s-marketing","s-flag","s-comment","s-finance","s-claim","s-custom","s-opportunity","s-data","s-check","s-grid","menu","share","d-caret","caret-left","caret-right","caret-bottom","caret-top","bottom-left","bottom-right","back","right","bottom","top","top-left","top-right","arrow-left","arrow-right","arrow-down","arrow-up","d-arrow-left","d-arrow-right","video-pause","video-play","refresh","refresh-right","refresh-left","finished","sort","sort-up","sort-down","rank","loading","view","c-scale-to-original","date","edit","edit-outline","folder","folder-opened","folder-add","folder-remove","folder-delete","folder-checked","tickets","document-remove","document-delete","document-copy","document-checked","document","document-add","printer","paperclip","takeaway-box","search","monitor","attract","mobile","scissors","umbrella","headset","brush","mouse","coordinate","magic-stick","reading","data-line","data-board","pie-chart","data-analysis","collection-tag","film","suitcase","suitcase-1","receiving","collection","files","notebook-1","notebook-2","toilet-paper","office-building","school","table-lamp","house","no-smoking","smoking","shopping-cart-full","shopping-cart-1","shopping-cart-2","shopping-bag-1","shopping-bag-2","sold-out","sell","present","box","bank-card","money","coin","wallet","discount","price-tag","news","guide","male","female","thumb","cpu","link","connection","open","turn-off","set-up","chat-round","chat-line-round","chat-square","chat-dot-round","chat-dot-square","chat-line-square","message","postcard","position","turn-off-microphone","microphone","close-notification","bangzhu","time","odometer","crop","aim","switch-button","full-screen","copy-document","mic","stopwatch","medal-1","medal","trophy","trophy-1","first-aid-kit","discover","place","location","location-outline","location-information","add-location","delete-location","map-location","alarm-clock","timer","watch-1","watch","lock","unlock","key","service","mobile-phone","bicycle","truck","ship","basketball","football","soccer","baseball","wind-power","light-rain","lightning","heavy-rain","sunrise","sunrise-1","sunset","sunny","cloudy","partly-cloudy","cloudy-and-sunny","moon","moon-night","dish","dish-1","food","chicken","fork-spoon","knife-fork","burger","tableware","sugar","dessert","ice-cream","hot-water","water-cup","coffee-cup","cold-drink","goblet","goblet-full","goblet-square","goblet-square-full","refrigerator","grape","watermelon","cherry","apple","pear","orange","coffee","ice-tea","ice-drink","milk-tea","potato-strips","lollipop","ice-cream-square","ice-cream-round"]
-415
View File
@@ -1,415 +0,0 @@
import { titleCase } from '@/utils/index'
import { trigger } from './config'
// 文件大小设置
const units: Record<string, string> = {
KB: '1024',
MB: '1024 / 1024',
GB: '1024 / 1024 / 1024',
}
interface FormElement {
vModel?: string
defaultValue?: any
multiple?: boolean
regList?: Array<{ pattern: string; message: string }>
required?: boolean
placeholder?: string
label?: string
tag: string
options?: Array<{ label: string; value: any }>
dataType?: string
props?: { props: any }
action?: string
'auto-upload'?: boolean
fileSize?: number
sizeUnit?: string
accept?: string
children?: FormElement[]
}
interface FormConfig {
formRef: string
formModel: string
formRules: string
formBtns?: boolean
fields: FormElement[]
}
/**
* @name: js需要的数据
* @description: js需要的数据
* @param {*} conf
* @param {*} type
* @return {*}
*/
export function makeUpJs(conf: FormConfig, type: string): string {
conf = JSON.parse(JSON.stringify(conf))
const dataList: string[] = []
const ruleList: string[] = []
const optionsList: string[] = []
const propsList: string[] = []
const methodList: string[] = []
const uploadVarList: string[] = []
conf.fields.forEach((el) => {
buildAttributes(
el,
dataList,
ruleList,
optionsList,
methodList,
propsList,
uploadVarList
)
})
const script = buildexport(
conf,
type,
dataList.join('\n'),
ruleList.join('\n'),
optionsList.join('\n'),
uploadVarList.join('\n'),
propsList.join('\n'),
methodList.join('\n')
)
return script
}
/**
* @name:
* @description:
* @return {*}
*/
function buildAttributes(
el: FormElement,
dataList: string[],
ruleList: string[],
optionsList: string[],
methodList: string[],
propsList: string[],
uploadVarList: string[]
): void {
buildData(el, dataList)
buildRules(el, ruleList)
if (el.options && el.options.length) {
buildOptions(el, optionsList)
if (el.dataType === 'dynamic') {
const model = `${el.vModel}Options`
const options = titleCase(model)
buildOptionMethod(`get${options}`, model, methodList)
}
}
if (el.props && el.props.props) {
buildProps(el, propsList)
}
if (el.action && el.tag === 'el-upload') {
uploadVarList.push(
`
// 上传请求路径
const ${el.vModel}Action = ref('${el.action}')
// 上传文件列表
const ${el.vModel}fileList = ref([])`
)
methodList.push(buildBeforeUpload(el))
if (!el['auto-upload']) {
methodList.push(buildSubmitUpload(el))
}
}
if (el.children) {
el.children.forEach((el2) => {
buildAttributes(
el2,
dataList,
ruleList,
optionsList,
methodList,
propsList,
uploadVarList
)
})
}
}
/**
* @name: formData
* @description: formData
* @param {*} conf
* @param {*} dataList
* @return {*}
*/
function buildData(conf: FormElement, dataList: string[]): void {
if (conf.vModel === undefined) return
let defaultValue: string
if (typeof conf.defaultValue === 'string' && !conf.multiple) {
defaultValue = `'${conf.defaultValue}'`
} else {
defaultValue = `${JSON.stringify(conf.defaultValue)}`
}
dataList.push(`${conf.vModel}: ${defaultValue},`)
}
/**
* @name: rule
* @description: rule
* @param {*} conf
* @param {*} ruleList
* @return {*}
*/
function buildRules(conf: FormElement, ruleList: string[]): void {
if (conf.vModel === undefined) return
const rules: string[] = []
if (trigger[conf.tag as keyof typeof trigger]) {
if (conf.required) {
const type = Array.isArray(conf.defaultValue) ? "type: 'array'," : ''
let message = Array.isArray(conf.defaultValue)
? `请至少选择一个${conf.vModel}`
: conf.placeholder
if (message === undefined) message = `${conf.label}不能为空`
rules.push(
`{ required: true, ${type} message: '${message}', trigger: '${
trigger[conf.tag as keyof typeof trigger]
}' }`
)
}
if (conf.regList && Array.isArray(conf.regList)) {
conf.regList.forEach((item) => {
if (item.pattern) {
rules.push(
`{ pattern: new RegExp(${item.pattern}), message: '${
item.message
}', trigger: '${trigger[conf.tag as keyof typeof trigger]}' }`
)
}
})
}
ruleList.push(`${conf.vModel}: [${rules.join(',')}],`)
}
}
/**
* @name:
* @description:
* @param {*} conf
* @param {*} optionsList
* @return {*}
*/
function buildOptions(conf: FormElement, optionsList: string[]): void {
if (conf.vModel === undefined) return
if (conf.dataType === 'dynamic') {
conf.options = []
}
const str = `const ${conf.vModel}Options = ref(${JSON.stringify(conf.options)})`
optionsList.push(str)
}
/**
* @name:
* @description:
* @param {*} methodName
* @param {*} model
* @param {*} methodList
* @return {*}
*/
function buildOptionMethod(methodName: string, model: string, methodList: string[]): void {
const str = `function ${methodName}() {
// TODO 发起请求获取数据
${model}.value
}`
methodList.push(str)
}
/**
* @name: props设置
* @description: props设置
* @param {*} conf
* @param {*} propsList
* @return {*}
*/
function buildProps(conf: FormElement, propsList: string[]): void {
if (conf.dataType === 'dynamic') {
const valueKey = (conf as any).valueKey
const labelKey = (conf as any).labelKey
const childrenKey = (conf as any).childrenKey
valueKey !== 'value' && (conf.props!.props.value = valueKey)
labelKey !== 'label' && (conf.props!.props.label = labelKey)
childrenKey !== 'children' && (conf.props!.props.children = childrenKey)
}
const str = `
// props设置
const ${conf.vModel}Props = ref(${JSON.stringify(conf.props!.props)})`
propsList.push(str)
}
/**
* @name:
* @description:
* @param {*} conf
* @return {*}
*/
function buildBeforeUpload(conf: FormElement): string {
const unitNum = units[conf.sizeUnit!]
let rightSizeCode = ''
let acceptCode = ''
const returnList: string[] = []
if (conf.fileSize) {
rightSizeCode = `let isRightSize = file.size / ${unitNum} < ${conf.fileSize}
if(!isRightSize){
proxy.$modal.msgError('文件大小超过 ${conf.fileSize}${conf.sizeUnit}')
}`
returnList.push('isRightSize')
}
if (conf.accept) {
acceptCode = `let isAccept = new RegExp('${conf.accept}').test(file.type)
if(!isAccept){
proxy.$modal.msgError('应该选择${conf.accept}类型的文件')
}`
returnList.push('isAccept')
}
const str = `
/**
* @name:
* @description:
* @param {*} file
* @return {*}
*/
function ${conf.vModel}BeforeUpload(file) {
${rightSizeCode}
${acceptCode}
return ${returnList.join('&&')}
}`
return returnList.length ? str : ''
}
/**
* @name:
* @description:
* @param {Object} conf vModel ref
* @return {*}
*/
function buildSubmitUpload(conf: FormElement): string {
const str = `function submitUpload() {
this.$refs['${conf.vModel}'].submit()
}`
return str
}
/**
* @name: js代码
* @description: js代码方法
* @return {*}
*/
function buildexport(
conf: FormConfig,
type: string,
data: string,
rules: string,
selectOptions: string,
uploadVar: string,
props: string,
methods: string
): string {
let str = `
const { proxy } = getCurrentInstance()
const ${conf.formRef} = ref()
const data = reactive({
${conf.formModel}: {
${data}
},
${conf.formRules}: {
${rules}
}
})
const {${conf.formModel}, ${conf.formRules}} = toRefs(data)
${selectOptions}
${uploadVar}
${props}
${methods}
`
if(type === 'dialog') {
str += `
// 弹窗设置
const dialogVisible = defineModel()
// 弹窗确认回调
const emit = defineEmits(['confirm'])
/**
* @name:
* @description:
* @return {*}
*/
function onOpen(){
}
/**
* @name:
* @description:
* @return {*}
*/
function onClose(){
${conf.formRef}.value.resetFields()
}
/**
* @name:
* @description:
* @return {*}
*/
function close(){
dialogVisible.value = false
}
/**
* @name:
* @description:
* @return {*}
*/
function handelConfirm(){
${conf.formRef}.value.validate((valid) => {
if (!valid) return
// TODO 提交表单
close()
// 回调父级组件
emit('confirm')
})
}
`
} else {
str += `
/**
* @name:
* @description:
* @return {*}
*/
function submitForm() {
${conf.formRef}.value.validate((valid) => {
if (!valid) return
// TODO 提交表单
})
}
/**
* @name:
* @description:
* @return {*}
*/
function resetForm() {
${conf.formRef}.value.resetFields()
}
`
}
return str
}
-186
View File
@@ -1,186 +0,0 @@
import { defineComponent, h, resolveComponent } from 'vue'
import { makeMap } from '@/utils/index'
const isAttr = makeMap(
'accept,accept-charset,accesskey,action,align,alt,async,autocomplete,' +
'autofocus,autoplay,autosave,bgcolor,border,buffered,challenge,charset,' +
'checked,cite,class,code,codebase,color,cols,colspan,content,http-equiv,' +
'name,contenteditable,contextmenu,controls,coords,data,datetime,default,' +
'defer,dir,dirname,disabled,download,draggable,dropzone,enctype,method,for,' +
'form,formaction,headers,height,hidden,high,href,hreflang,http-equiv,' +
'icon,id,ismap,itemprop,keytype,kind,label,lang,language,list,loop,low,' +
'manifest,max,maxlength,media,method,GET,POST,min,multiple,email,file,' +
'muted,name,novalidate,open,optimum,pattern,ping,placeholder,poster,' +
'preload,radiogroup,readonly,rel,required,reversed,rows,rowspan,sandbox,' +
'scope,scoped,seamless,selected,shape,size,type,text,password,sizes,span,' +
'spellcheck,src,srcdoc,srclang,srcset,start,step,style,summary,tabindex,' +
'target,title,type,usemap,value,width,wrap' + 'prefix-icon'
)
const isNotProps = makeMap(
'layout,prepend,regList,tag,document,changeTag,defaultValue'
)
interface ComponentConfig {
tag: string
options?: Array<{ label: string; value: any }>
optionType?: string
border?: boolean
'list-type'?: string
showTip?: boolean
fileSize?: number
sizeUnit?: string
accept?: string
buttonText?: string
default?: string
[key: string]: any
}
interface ChildFunction {
(h: any, conf: ComponentConfig, key: string): any
}
interface SlotFunction {
(h: any, conf: ComponentConfig, key: string): () => any
}
function useVModel(props: any, emit: any) {
return {
modelValue: props.defaultValue,
'onUpdate:modelValue': (val: any) => emit('update:modelValue', val),
}
}
const componentChild: Record<string, Record<string, ChildFunction>> = {
'el-button': {
default(h, conf, key) {
return conf[key]
},
},
'el-select': {
options(h, conf, key) {
return conf.options!.map(item => h(resolveComponent('el-option'), {
label: item.label,
value: item.value,
}))
}
},
'el-radio-group': {
options(h, conf, key) {
return conf.optionType === 'button' ? conf.options!.map(item => h(resolveComponent('el-radio-button'), {
label: item.value,
}, () => item.label)) : conf.options!.map(item => h(resolveComponent('el-radio'), {
label: item.value,
border: conf.border,
}, () => item.label))
}
},
'el-checkbox-group': {
options(h, conf, key) {
return conf.optionType === 'button' ? conf.options!.map(item => h(resolveComponent('el-checkbox-button'), {
label: item.value,
}, () => item.label)) : conf.options!.map(item => h(resolveComponent('el-checkbox'), {
label: item.value,
border: conf.border,
}, () => item.label))
}
},
'el-upload': {
'list-type': (h, conf, key) => {
const option: Record<string, any> = {}
// if (conf.showTip) {
// tip = h('div', {
// class: "el-upload__tip"
// }, () => '只能上传不超过' + conf.fileSize + conf.sizeUnit + '的' + conf.accept + '文件')
// }
if (conf['list-type'] === 'picture-card') {
return h(resolveComponent('el-icon'), option, () => h(resolveComponent('Plus')))
} else {
// option.size = "small"
option.type = "primary"
option.icon = "Upload"
return h(resolveComponent('el-button'), option, () => conf.buttonText)
}
},
}
}
const componentSlot: Record<string, Record<string, SlotFunction>> = {
'el-upload': {
'tip': (h, conf, key) => {
if (conf.showTip) {
return () => h('div', {
class: "el-upload__tip"
}, '只能上传不超过' + conf.fileSize + conf.sizeUnit + '的' + conf.accept + '文件')
}
return () => null
},
}
}
export default defineComponent({
// 使用 render 函数
render() {
const dataObject: Record<string, any> = {
attrs: {} as Record<string, any>,
props: {} as Record<string, any>,
on: {} as Record<string, any>,
style: {} as Record<string, any>
}
const confClone = JSON.parse(JSON.stringify(this.conf))
const children: any[] = []
const slot: Record<string, any> = {}
const childObjs = componentChild[confClone.tag]
if (childObjs) {
Object.keys(childObjs).forEach(key => {
const childFunc = childObjs[key]
if (confClone[key]) {
children.push(childFunc(h, confClone, key))
}
})
}
const slotObjs = componentSlot[confClone.tag]
if (slotObjs) {
Object.keys(slotObjs).forEach(key => {
const childFunc = slotObjs[key]
if (confClone[key]) {
slot[key] = childFunc(h, confClone, key)
}
})
}
Object.keys(confClone).forEach(key => {
const val = confClone[key]
if (dataObject[key]) {
dataObject[key] = val
} else if (isAttr(key)) {
dataObject.attrs[key] = val
} else if (!isNotProps(key)) {
dataObject.props[key] = val
}
})
if(children.length > 0){
slot.default = () => children
}
return h(resolveComponent(this.conf.tag),
{
modelValue: (this as any).$attrs.modelValue,
...dataObject.props,
...dataObject.attrs,
style: {
...dataObject.style
},
}
, slot ?? null)
},
props: {
conf: {
type: Object,
required: true,
},
}
})
+14 -65
View File
@@ -9,29 +9,9 @@
<el-form-item label="表描述" prop="table_comment"> <el-form-item label="表描述" prop="table_comment">
<el-input v-model="queryFormData.table_comment" placeholder="请输入表描述" clearable style="width: 200px" @keyup.enter="handleQuery"/> <el-input v-model="queryFormData.table_comment" placeholder="请输入表描述" clearable style="width: 200px" @keyup.enter="handleQuery"/>
</el-form-item> </el-form-item>
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
<DatePicker
v-model="dateRange"
@update:model-value="handleDateRangeChange"
/>
</el-form-item>
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['generator:gencode:query']" type="primary" icon="search" native-type="submit">查询</el-button> <el-button v-hasPerm="['generator:gencode:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['generator:gencode:query']" icon="refresh" @click="handleRefresh">重置</el-button> <el-button v-hasPerm="['generator:gencode:query']" icon="refresh" @click="handleRefresh">重置</el-button>
<!-- 展开/收起 -->
<template v-if="isExpandable">
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
{{ isExpand ? "收起" : "展开" }}
<el-icon>
<template v-if="isExpand">
<ArrowUp />
</template>
<template v-else>
<ArrowDown />
</template>
</el-icon>
</el-link>
</template>
</el-form-item> </el-form-item>
</el-form> </el-form>
</div> </div>
@@ -288,7 +268,7 @@
v-loading="loading" v-loading="loading"
:data="columns" :data="columns"
row-key="id" row-key="id"
:max-height="tableHeight" max-height="680"
highlight--currentrow highlight--currentrow
class="data-table__content" class="data-table__content"
border border
@@ -555,7 +535,7 @@
<!-- 第二步上一步下一步 --> <!-- 第二步上一步下一步 -->
<el-button v-if="activeStep === 1" type="success" :icon="Back" @click="prevStep">上一步基础配置</el-button> <el-button v-if="activeStep === 1" type="success" :icon="Back" @click="prevStep">上一步基础配置</el-button>
<el-button v-if="activeStep === 1" type="warning" :icon="Edit" @click="submitForm">保存字段配置</el-button> <el-button v-if="activeStep === 1" v-hasPerm="['generator:gencode:update']" type="warning" :icon="Edit" @click="submitForm">保存字段配置</el-button>
<el-button v-if="activeStep === 1" type="primary" @click="nextStep"> <el-button v-if="activeStep === 1" type="primary" @click="nextStep">
下一步预览代码<el-icon class="el-icon--right"><View /></el-icon> 下一步预览代码<el-icon class="el-icon--right"><View /></el-icon>
</el-button> </el-button>
@@ -591,7 +571,6 @@ import type { CmComponentRef } from "codemirror-editor-vue3";
import { ElMessage, ElMessageBox, type FormInstance, type TableInstance } from 'element-plus'; import { ElMessage, ElMessageBox, type FormInstance, type TableInstance } from 'element-plus';
import { QuestionFilled, MagicStick, View, CopyDocument, Close, Right, FolderOpened, Back, Download, Edit } from '@element-plus/icons-vue'; import { QuestionFilled, MagicStick, View, CopyDocument, Close, Right, FolderOpened, Back, Download, Edit } from '@element-plus/icons-vue';
import GencodeAPI, { type GenTableOutVO, type DatabaseTable, type GenTableQueryParam, type GenTableColumnOutSchema, type GenTableSchema } from "@/api/generator/gencode"; import GencodeAPI, { type GenTableOutVO, type DatabaseTable, type GenTableQueryParam, type GenTableColumnOutSchema, type GenTableSchema } from "@/api/generator/gencode";
import { formatToDateTime } from "@/utils/dateUtil";
import MenuAPI, { MenuTable } from "@/api/system/menu"; import MenuAPI, { MenuTable } from "@/api/system/menu";
import DictAPI, { DictTable } from "@/api/system/dict"; import DictAPI, { DictTable } from "@/api/system/dict";
import { formatTree } from "@/utils/common"; import { formatTree } from "@/utils/common";
@@ -633,10 +612,7 @@ const loading = ref(false);
const total = ref<number>(0); const total = ref<number>(0);
const uniqueId = ref(""); const uniqueId = ref("");
const editVisible = ref(false); const editVisible = ref(false);
const tableHeight = ref<number>(0);
const activeStep = ref(2); const activeStep = ref(2);
const isExpandable = ref(true);
const isExpand = ref(false);
// UI // UI
const createTableVisible = ref(false); const createTableVisible = ref(false);
@@ -724,17 +700,6 @@ const cmOptions: EditorConfiguration = {
readOnly: true readOnly: true
}; };
//
function handleDateRangeChange(range: [Date, Date]) {
dateRange.value = range;
if (range && range.length === 2) {
queryFormData.start_time = formatToDateTime(range[0]);
queryFormData.end_time = formatToDateTime(range[1]);
} else {
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
}
}
// //
const { copy } = useClipboard(); const { copy } = useClipboard();
@@ -1032,7 +997,7 @@ function handleImportTableSelectionChange(selection: DatabaseTable[]): void {
function calculateTableHeight() { function calculateTableHeight() {
// //
// 使400px // 使400px
tableHeight.value = 680;
} }
// //
@@ -1204,25 +1169,26 @@ onActivated(async () => {
}); });
// //
const info = reactive<GenTableOutVO>({ const info = reactive<GenTableSchema>({
id: undefined, id: undefined,
table_name: '', table_name: '',
table_comment: '', table_comment: '',
sub_table_name: '',
sub_table_fk_name: '',
class_name: '', class_name: '',
package_name: '', package_name: '',
module_name: '', module_name: '',
business_name: '', business_name: '',
function_name: '', function_name: '',
gen_type: '0', gen_type: '0',
parent_menu_id: undefined, options: {parent_menu_id: undefined,},
description: '', description: '',
parent_menu_id: undefined,
parent_menu_name: '',
pk_column: undefined,
sub_table: undefined,
columns: [], columns: [],
sub: false, sub: false,
tree: false,
crud: true,
params: {
parent_menu_id: undefined,
}
}); });
// //
@@ -1240,15 +1206,9 @@ const rules = {
/** 提交表单 - 保存配置 */ /** 提交表单 - 保存配置 */
async function submitForm() { async function submitForm() {
// // ID
const basicValid = await basicInfo.value?.validate() || false; if (!info.id) {
if (!basicValid) { ElMessage.error('无效的表ID');
return;
}
//
const genValid = await genInfo.value?.validate() || false;
if (!genValid) {
return; return;
} }
@@ -1261,22 +1221,12 @@ async function submitForm() {
return; return;
} }
// params
if (info.parent_menu_id) {
info.params = {
parent_menu_id: info.parent_menu_id
};
}
// columns // columns
const tableData = { const tableData = {
...info, ...info,
columns: info.columns || [] // columns columns: info.columns || [] // columns
}; };
//
delete (tableData as any).params;
delete (tableData as any).parent_menu_id;
const response = await GencodeAPI.updateTable(tableData as GenTableSchema, info.id || 0); const response = await GencodeAPI.updateTable(tableData as GenTableSchema, info.id || 0);
if (response?.data?.code === 200) { if (response?.data?.code === 200) {
@@ -1288,7 +1238,6 @@ async function submitForm() {
} finally { } finally {
loading.value = false; loading.value = false;
} }
return false;
} }
@@ -1,153 +0,0 @@
# 低代码页面生成器
一个基于 Vue3 + TypeScript + Element Plus 的可视化页面生成器,支持拖拽式组件设计和代码生成。
## 功能特性
- 🎨 **可视化设计**: 拖拽式组件设计,所见即所得
- 📦 **丰富组件**: 支持所有 Element Plus 组件
- ⚙️ **属性编辑**: 实时编辑组件属性
- 💾 **模板管理**: 保存和加载页面模板
- 🔧 **代码生成**: 生成完整的 Vue3 + TypeScript 代码
- 📱 **响应式**: 支持移动端适配
## 文件结构
```
src/views/gencode/backcode/
├── index.vue # 主界面
├── components/
│ ├── Palette.vue # 左侧组件库
│ ├── Canvas.vue # 中间画布
│ ├── CanvasComponent.vue # 画布中的组件
│ ├── Inspector.vue # 右侧属性面板
│ └── PropertyEditor.vue # 属性编辑器
└── utils/
├── schema.ts # 组件 Schema 定义
└── serializer.ts # 代码生成器
```
## 使用方法
### 1. 添加组件
- 从左侧组件库拖拽组件到中间画布
- 或点击组件快速添加
### 2. 编辑组件
- 点击画布中的组件选中
- 在右侧属性面板编辑属性
- 支持样式、事件、插槽等设置
### 3. 生成代码
- 点击顶部"生成代码"按钮
- 代码会自动复制到剪贴板
- 可选择下载为 .vue 文件
### 4. 保存模板
- 点击"保存模板"按钮
- 输入模板名称和描述
- 模板会保存到本地存储
## 支持的组件类型
### 基础组件
- 按钮 (el-button)
- 链接 (el-link)
- 文本 (el-text)
- 图标 (el-icon)
### 布局组件
- 行 (el-row)
- 列 (el-col)
- 容器 (el-container)
### 表单组件
- 输入框 (el-input)
- 选择器 (el-select)
- 单选框 (el-radio)
- 复选框 (el-checkbox)
- 开关 (el-switch)
- 滑块 (el-slider)
- 日期选择器 (el-date-picker)
- 时间选择器 (el-time-picker)
- 上传 (el-upload)
- 评分 (el-rate)
- 颜色选择器 (el-color-picker)
### 数据展示组件
- 卡片 (el-card)
- 表格 (el-table)
- 轮播图 (el-carousel)
- 折叠面板 (el-collapse)
- 描述列表 (el-descriptions)
- 空状态 (el-empty)
- 图片 (el-image)
- 分页 (el-pagination)
- 进度条 (el-progress)
- 结果页 (el-result)
- 骨架屏 (el-skeleton)
- 统计数值 (el-statistic)
- 标签 (el-tag)
- 时间线 (el-timeline)
- 树形控件 (el-tree)
### 导航组件
- 固钉 (el-affix)
- 面包屑 (el-breadcrumb)
- 下拉菜单 (el-dropdown)
- 菜单 (el-menu)
- 页面头部 (el-page-header)
- 步骤条 (el-steps)
- 标签页 (el-tabs)
### 反馈组件
- 警告提示 (el-alert)
- 抽屉 (el-drawer)
- 加载 (el-loading)
- 消息提示 (el-message)
- 消息确认框 (el-message-box)
- 通知 (el-notification)
- 气泡确认框 (el-popconfirm)
- 弹出框 (el-popover)
- 文字提示 (el-tooltip)
### 其他组件
- 回到顶部 (el-backtop)
- 分割线 (el-divider)
- 水印 (el-watermark)
## 技术栈
- **Vue 3**: 使用 Composition API
- **TypeScript**: 完整的类型支持
- **Element Plus**: UI 组件库
- **Vite**: 构建工具
- **SCSS**: 样式预处理器
## 开发说明
### 添加新组件
1. 在 `utils/schema.ts` 中添加组件类型
2. 在 `COMPONENT_CONFIGS` 中配置组件属性
3. 在 `COMPONENT_CATEGORIES` 中分类组件
### 自定义属性编辑器
1. 在 `PropertyEditor.vue` 中添加新的属性类型
2. 实现对应的编辑器组件
3. 更新属性验证逻辑
### 扩展代码生成
1. 在 `serializer.ts` 中添加新的生成逻辑
2. 支持自定义模板和样式
3. 添加代码格式化功能
## 注意事项
- 组件 ID 必须唯一
- 属性值需要符合 Element Plus 组件规范
- 生成的代码需要手动验证和测试
- 复杂布局建议使用栅格系统
## 许可证
MIT License
@@ -1,407 +0,0 @@
<!--
低代码页面生成器 - 画布组件
中间可放置和编辑组件的画布区域
-->
<template>
<div class="center-board">
<div class="action-bar">
<el-button icon="Download" type="primary" text @click="generateCode">
导出vue文件
</el-button>
<el-button class="copy-btn-main" icon="DocumentCopy" type="primary" text @click="copyCode">
复制代码
</el-button>
<el-button class="delete-btn" icon="Delete" text @click="clearCanvas" type="danger">
清空
</el-button>
</div>
<el-scrollbar class="center-scrollbar">
<el-row class="center-board-row" :gutter="20">
<div class="drawing-board-container">
<draggable
class="drawing-board"
:list="components"
:animation="340"
group="componentsGroup"
item-key="id"
@add="handleAdd"
@change="handleChange"
>
<template #item="{ element, index }">
<CanvasComponent
:key="element.id"
:component="element"
:index="index"
:selected-id="selectedComponentId"
:drawing-list="components"
@select="handleSelectComponent"
@update="handleUpdateComponent"
@delete="handleDeleteComponent"
@copy="handleCopyComponent"
/>
</template>
</draggable>
<div v-show="!components.length" class="empty-info">
从左侧拖入或点选组件进行页面设计
</div>
</div>
</el-row>
</el-scrollbar>
<!-- 预览对话框 -->
<el-dialog
v-model="previewVisible"
title="页面预览"
width="80%"
:before-close="handlePreviewClose"
>
<div class="preview-content">
<iframe
v-if="previewUrl"
:src="previewUrl"
frameborder="0"
class="preview-iframe"
/>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Delete, View, Download, Plus, DocumentCopy } from '@element-plus/icons-vue';
import draggable from 'vuedraggable';
import CanvasComponent from './CanvasComponent.vue';
import { ComponentSchema, generateId } from '../utils/schema';
import { generateVueFile, exportAsFile, copyToClipboard } from '../utils/serializer';
// Props
interface Props {
components: ComponentSchema[];
selectedComponentId?: string;
}
// Emits
interface Emits {
updateComponents: [components: ComponentSchema[]];
selectComponent: [component: ComponentSchema | null];
updateComponent: [component: ComponentSchema];
deleteComponent: [componentId: string];
moveComponent: [fromIndex: number, toIndex: number];
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
//
const previewVisible = ref(false);
const previewUrl = ref('');
//
function handleAdd(event: any) {
console.log('添加组件:', event);
}
//
function handleChange(event: any) {
console.log('组件变化:', event);
emit('updateComponents', props.components);
}
//
function handleSelectComponent(component: ComponentSchema) {
emit('selectComponent', component);
}
//
function handleUpdateComponent(component: ComponentSchema) {
emit('updateComponent', component);
}
//
function handleDeleteComponent(componentId: string) {
emit('deleteComponent', componentId);
}
//
function handleCopyComponent(component: ComponentSchema) {
const newComponent = JSON.parse(JSON.stringify(component));
newComponent.id = generateId();
const newComponents = [...props.components, newComponent];
emit('updateComponents', newComponents);
ElMessage.success('组件已复制');
}
//
function clearCanvas() {
ElMessageBox.confirm('确定要清空所有组件吗?', '确认清空', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
emit('updateComponents', []);
emit('selectComponent', null);
ElMessage.success('画布已清空');
}).catch(() => {
//
});
}
//
async function copyCode() {
if (props.components.length === 0) {
ElMessage.warning('请先添加组件');
return;
}
const pageSchema = {
id: 'generated-page',
name: '生成的页面',
components: props.components
};
try {
await copyToClipboard(pageSchema);
ElMessage.success('代码已复制到剪贴板');
} catch (error) {
console.error('复制代码失败:', error);
ElMessage.error('复制代码失败');
}
}
//
function previewPage() {
if (props.components.length === 0) {
ElMessage.warning('请先添加组件');
return;
}
//
const pageSchema = {
id: 'preview-page',
name: '预览页面',
components: props.components
};
const vueCode = generateVueFile(pageSchema);
// URL
const blob = new Blob([vueCode], { type: 'text/html' });
previewUrl.value = URL.createObjectURL(blob);
previewVisible.value = true;
}
//
function handlePreviewClose() {
previewVisible.value = false;
if (previewUrl.value) {
URL.revokeObjectURL(previewUrl.value);
previewUrl.value = '';
}
}
//
async function generateCode() {
if (props.components.length === 0) {
ElMessage.warning('请先添加组件');
return;
}
const pageSchema = {
id: 'generated-page',
name: '生成的页面',
components: props.components
};
try {
//
await copyToClipboard(pageSchema);
ElMessage.success('代码已复制到剪贴板');
//
ElMessageBox.confirm('代码已复制到剪贴板,是否同时下载文件?', '生成完成', {
confirmButtonText: '下载',
cancelButtonText: '取消',
type: 'success'
}).then(() => {
exportAsFile(pageSchema, 'generated-page.vue');
}).catch(() => {
//
});
} catch (error) {
console.error('生成代码失败:', error);
ElMessage.error('生成代码失败');
}
}
//
function getComponentLabel(componentType: string): string {
//
const typeMap: Record<string, string> = {
'el-button': '按钮',
'el-input': '输入框',
'el-select': '选择器',
'el-card': '卡片',
'el-form': '表单',
'el-row': '行',
'el-col': '列'
};
return typeMap[componentType] || componentType;
}
//
watch(() => props.components, (newComponents) => {
try {
localStorage.setItem('lowcode-canvas-components', JSON.stringify(newComponents));
} catch (error) {
console.warn('保存到本地存储失败:', error);
}
}, { deep: true });
</script>
<style scoped>
.center-board {
height: calc(100vh - 50px - 40px);
width: auto;
margin: 0 350px 0 260px;
box-sizing: border-box;
background: var(--el-bg-color);
.action-bar {
position: relative;
height: 42px;
padding: 0 15px;
box-sizing: border-box;
border: 1px solid var(--el-border-color-extra-light);
border-top: none;
border-left: none;
display: flex;
align-items: center;
justify-content: flex-end;
background: var(--el-bg-color);
gap: 8px;
.delete-btn {
color: var(--el-color-danger);
}
}
.center-scrollbar {
height: calc(100vh - 50px - 40px - 42px);
overflow: hidden;
border-left: 1px solid var(--el-border-color-extra-light);
border-right: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
background: var(--el-bg-color-page);
.el-scrollbar__view {
overflow-x: hidden;
}
.center-board-row {
padding: 12px 12px 15px 12px;
box-sizing: border-box;
.drawing-board-container {
width: 100%;
min-height: calc(100vh - 50px - 40px - 69px);
position: relative;
.drawing-board {
min-height: calc(100vh - 50px - 40px - 69px);
position: relative;
padding: 20px;
background: var(--el-bg-color);
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
.sortable-ghost {
position: relative;
display: block;
overflow: hidden;
&::before {
content: " ";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: var(--el-color-primary);
z-index: 2;
}
}
}
.empty-info {
position: absolute;
top: 46%;
left: 0;
right: 0;
text-align: center;
font-size: 18px;
color: var(--el-text-color-placeholder);
letter-spacing: 4px;
pointer-events: none;
}
}
}
}
}
.preview-content {
height: 70vh;
border: 1px solid var(--el-border-color);
border-radius: 4px;
overflow: hidden;
}
.preview-iframe {
width: 100%;
height: 100%;
border: none;
}
/* 滚动条样式 */
.center-scrollbar::-webkit-scrollbar {
width: 6px;
}
.center-scrollbar::-webkit-scrollbar-track {
background: var(--el-fill-color-light);
border-radius: 3px;
}
.center-scrollbar::-webkit-scrollbar-thumb {
background: var(--el-border-color);
border-radius: 3px;
}
.center-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--el-border-color-dark);
}
/* 响应式设计 */
@media (max-width: 1200px) {
.center-board {
margin: 0 300px 0 240px;
}
}
@media (max-width: 768px) {
.center-board {
margin: 0;
width: 100%;
}
.action-bar {
padding: 0 8px;
flex-wrap: wrap;
gap: 4px;
}
}
</style>
@@ -1,378 +0,0 @@
<!--
低代码页面生成器 - 画布中的单个组件
渲染和编辑画布中的组件
-->
<template>
<div
class="drawing-item"
:class="{
'active-from-item': isSelected,
'unfocus-bordered': !isSelected
}"
@click.stop="handleSelect"
>
<!-- 组件操作按钮 -->
<div class="drawing-item-copy" title="复制" @click.stop="handleCopy">
<el-icon><CopyDocument /></el-icon>
</div>
<div class="drawing-item-delete" title="删除" @click.stop="handleDelete">
<el-icon><Delete /></el-icon>
</div>
<!-- 组件内容区域 -->
<div class="component-wrapper">
<ComponentRenderer
:component="component"
@click.stop="handleSelect"
>
<template #children>
<!-- 容器组件的拖拽区域 -->
<draggable
v-if="isContainer()"
class="drag-wrapper"
:list="component.children || []"
group="componentsGroup"
:animation="340"
item-key="id"
@add="handleAdd"
@change="handleChange"
>
<template #item="{ element }">
<CanvasComponent
:key="element.id"
:component="element"
:selected-id="selectedId"
:drawing-list="component.children"
@select="$emit('select', $event)"
@update="$emit('update', $event)"
@delete="$emit('delete', $event)"
@copy="$emit('copy', $event)"
/>
</template>
</draggable>
<!-- 非容器组件的子组件 -->
<template v-else>
<CanvasComponent
v-for="child in component.children"
:key="child.id"
:component="child"
:selected-id="selectedId"
:drawing-list="drawingList"
@select="$emit('select', $event)"
@update="$emit('update', $event)"
@delete="$emit('delete', $event)"
@copy="$emit('copy', $event)"
/>
</template>
<!-- 容器占位符 -->
<div v-if="needsPlaceholder()" class="component-placeholder">
{{ getPlaceholderText() }}
</div>
</template>
</ComponentRenderer>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import { ElMessage } from 'element-plus';
import { Delete, CopyDocument } from '@element-plus/icons-vue';
import draggable from 'vuedraggable';
import ComponentRenderer from './ComponentRenderer.vue';
import { ComponentSchema, generateId } from '../utils/schema';
// Props
interface Props {
component: ComponentSchema;
selectedId?: string;
drawingList?: ComponentSchema[];
index?: number;
}
// Emits
interface Emits {
select: [component: ComponentSchema];
update: [component: ComponentSchema];
delete: [componentId: string];
copy: [component: ComponentSchema];
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
//
const isSelected = computed(() => props.selectedId === props.component.id);
//
function handleSelect() {
emit('select', props.component);
}
//
function handleDelete() {
emit('delete', props.component.id);
}
//
function handleCopy() {
emit('copy', props.component);
}
//
function isContainer(): boolean {
const containerTypes = [
'el-card', 'el-form', 'el-form-item', 'el-row', 'el-col',
'el-container', 'el-header', 'el-main', 'el-aside', 'el-footer'
];
return containerTypes.includes(props.component.type);
}
//
function handleAdd(event: any) {
console.log('子组件拖拽添加:', event);
emit('update', props.component);
}
//
function handleChange(event: any) {
console.log('子组件拖拽变化:', event);
emit('update', props.component);
}
//
function needsPlaceholder() {
const hasChildren = props.component.children && props.component.children.length > 0;
const containerComponents = ['el-card', 'el-form', 'el-form-item', 'el-row', 'el-col', 'el-container', 'el-header', 'el-main', 'el-aside', 'el-footer'];
const isContainer = containerComponents.includes(props.component.type);
return isContainer && !hasChildren;
}
//
function getPlaceholderText(): string {
const typeMap: Record<string, string> = {
'el-card': '拖拽组件到此处',
'el-form': '拖拽表单项到此处',
'el-form-item': '拖拽表单控件到此处',
'el-row': '拖拽列到此处',
'el-col': '拖拽组件到此处',
'el-container': '拖拽容器组件到此处',
'el-header': '头部区域',
'el-main': '主要内容区域',
'el-aside': '侧边栏区域',
'el-footer': '底部区域'
};
return typeMap[props.component.type] || '空容器';
}
</script>
<style scoped>
.drawing-item {
position: relative;
cursor: move;
margin-bottom: 15px;
transition: all 0.2s ease;
&.unfocus-bordered:not(.active-from-item) {
border: 1px dashed var(--el-border-color);
border-radius: 4px;
padding: 8px;
}
.component-wrapper {
position: relative;
min-height: 40px;
}
&.active-from-item {
.component-wrapper {
background: var(--el-fill-color-light);
border-radius: 6px;
padding: 8px;
}
.drawing-item-copy,
.drawing-item-delete {
display: initial;
}
}
&:hover {
.component-wrapper {
background: var(--el-fill-color-light);
border-radius: 6px;
padding: 8px;
}
.drawing-item-copy,
.drawing-item-delete {
display: initial;
}
}
.drawing-item-copy,
.drawing-item-delete {
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 10;
background: var(--el-bg-color);
.el-icon {
font-size: 12px;
}
}
.drawing-item-copy {
right: 56px;
border-color: var(--el-color-primary);
color: var(--el-color-primary);
&:hover {
background: var(--el-color-primary);
color: var(--el-color-white);
}
}
.drawing-item-delete {
right: 24px;
border-color: var(--el-color-danger);
color: var(--el-color-danger);
&:hover {
background: var(--el-color-danger);
color: var(--el-color-white);
}
}
}
.component-placeholder {
color: var(--el-text-color-placeholder);
font-size: 12px;
font-style: italic;
display: flex;
align-items: center;
justify-content: center;
min-height: 40px;
border: 1px dashed var(--el-border-color);
border-radius: 4px;
background: var(--el-fill-color-lighter);
margin: 8px 0;
}
/* 组件特定样式 */
:deep(.el-button) {
margin: 4px;
}
:deep(.el-input) {
margin: 4px 0;
}
:deep(.el-select) {
margin: 4px 0;
}
:deep(.el-card) {
margin: 8px 0;
.el-card__body {
min-height: 60px;
}
}
:deep(.el-form) {
margin: 8px 0;
padding: 16px;
border: 1px dashed var(--el-border-color);
border-radius: 4px;
min-height: 80px;
}
:deep(.el-row) {
margin: 8px 0;
padding: 12px;
border: 1px dashed var(--el-border-color);
border-radius: 4px;
min-height: 60px;
}
:deep(.el-col) {
padding: 8px;
border: 1px dashed var(--el-border-color-lighter);
border-radius: 4px;
min-height: 40px;
margin: 4px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.drawing-item-copy {
right: 40px;
top: -8px;
}
.drawing-item-delete {
right: 12px;
top: -8px;
}
}
/* 拖拽区域样式 */
.drag-wrapper {
min-height: 40px;
width: 100%;
border-radius: 4px;
transition: all 0.2s ease;
}
.drag-wrapper:empty {
min-height: 60px;
border: 1px dashed var(--el-border-color);
background: var(--el-fill-color-lighter);
display: flex;
align-items: center;
justify-content: center;
}
.drag-wrapper:empty::after {
content: '拖拽组件到此处';
color: var(--el-text-color-placeholder);
font-size: 12px;
}
/* 拖拽排序样式 */
.sortable-ghost {
opacity: 0.5;
background: var(--el-color-primary-light-9);
border: 2px dashed var(--el-color-primary);
}
.sortable-chosen {
transform: scale(1.02);
box-shadow: 0 4px 12px var(--el-color-primary-light-3);
}
/* 容器组件特殊样式 */
.drawing-item .drag-wrapper {
padding: 8px;
}
.drawing-item:hover .drag-wrapper {
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
</style>
@@ -1,628 +0,0 @@
<!--
组件渲染器 - 根据组件类型渲染不同的 Element Plus 组件
确保每个组件都能正确显示和交互
-->
<template>
<div class="component-renderer">
<!-- 按钮组件 -->
<el-button
v-if="component.type === 'el-button'"
v-bind="component.props"
@click="handleClick"
>
{{ component.props?.children || '按钮' }}
</el-button>
<!-- 链接组件 -->
<el-link
v-else-if="component.type === 'el-link'"
v-bind="component.props"
@click="handleClick"
>
{{ component.props?.children || '链接' }}
</el-link>
<!-- 文本组件 -->
<el-text
v-else-if="component.type === 'el-text'"
v-bind="component.props"
>
{{ component.props?.children || '文本内容' }}
</el-text>
<!-- 输入框组件 -->
<el-input
v-else-if="component.type === 'el-input'"
v-bind="component.props"
:model-value="getInputValue()"
@input="handleInput"
/>
<!-- 数字输入框 -->
<el-input-number
v-else-if="component.type === 'el-input-number'"
v-bind="component.props"
:model-value="getNumberValue()"
@change="handleNumberChange"
/>
<!-- 选择器 -->
<el-select
v-else-if="component.type === 'el-select'"
v-bind="component.props"
:model-value="getSelectValue()"
@change="handleSelectChange"
>
<el-option
v-for="option in getSelectOptions()"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<!-- 单选框 -->
<el-radio
v-else-if="component.type === 'el-radio'"
v-bind="component.props"
:model-value="getRadioValue()"
@change="handleRadioChange"
>
{{ component.props?.label || '选项' }}
</el-radio>
<!-- 单选框组 -->
<el-radio-group
v-else-if="component.type === 'el-radio-group'"
v-bind="component.props"
:model-value="getRadioGroupValue()"
@change="handleRadioGroupChange"
>
<el-radio
v-for="option in getRadioOptions()"
:key="option.value"
:label="option.value"
>
{{ option.label }}
</el-radio>
</el-radio-group>
<!-- 复选框 -->
<el-checkbox
v-else-if="component.type === 'el-checkbox'"
v-bind="component.props"
:model-value="getCheckboxValue()"
@change="handleCheckboxChange"
>
{{ component.props?.label || '选项' }}
</el-checkbox>
<!-- 复选框组 -->
<el-checkbox-group
v-else-if="component.type === 'el-checkbox-group'"
v-bind="component.props"
:model-value="getCheckboxGroupValue()"
@change="handleCheckboxGroupChange"
>
<el-checkbox
v-for="option in getCheckboxOptions()"
:key="option.value"
:label="option.value"
>
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
<!-- 开关 -->
<el-switch
v-else-if="component.type === 'el-switch'"
v-bind="component.props"
:model-value="getSwitchValue()"
@change="handleSwitchChange"
/>
<!-- 滑块 -->
<el-slider
v-else-if="component.type === 'el-slider'"
v-bind="component.props"
:model-value="getSliderValue()"
@change="handleSliderChange"
/>
<!-- 日期选择器 -->
<el-date-picker
v-else-if="component.type === 'el-date-picker'"
v-bind="component.props"
:model-value="getDateValue()"
@change="handleDateChange"
/>
<!-- 时间选择器 -->
<el-time-picker
v-else-if="component.type === 'el-time-picker'"
v-bind="component.props"
:model-value="getTimeValue()"
@change="handleTimeChange"
/>
<!-- 卡片 -->
<el-card
v-else-if="component.type === 'el-card'"
v-bind="component.props"
>
<slot name="children">
<div class="card-placeholder">
{{ component.props?.children || '卡片内容区域' }}
</div>
</slot>
</el-card>
<!-- 标签 -->
<el-tag
v-else-if="component.type === 'el-tag'"
v-bind="component.props"
>
{{ component.props?.children || '标签' }}
</el-tag>
<!-- 进度条 -->
<el-progress
v-else-if="component.type === 'el-progress'"
v-bind="component.props"
/>
<!-- 警告 -->
<el-alert
v-else-if="component.type === 'el-alert'"
v-bind="component.props"
/>
<!-- 分割线 -->
<el-divider
v-else-if="component.type === 'el-divider'"
v-bind="component.props"
>
{{ component.props?.children }}
</el-divider>
<!-- 表单 -->
<el-form
v-else-if="component.type === 'el-form'"
v-bind="component.props"
class="form-container"
>
<slot name="children">
<div class="form-placeholder">拖拽表单项到此处</div>
</slot>
</el-form>
<!-- 表单项 -->
<el-form-item
v-else-if="component.type === 'el-form-item'"
v-bind="component.props"
>
<slot name="children">
<div class="form-item-placeholder">拖拽表单控件到此处</div>
</slot>
</el-form-item>
<!-- 行布局 -->
<div
v-else-if="component.type === 'el-row'"
class="row-layout"
:style="getRowStyle()"
>
<slot name="children">
<div class="layout-placeholder">
<el-icon><Grid /></el-icon>
<span>行布局 - 水平排列容器</span>
<small>拖拽组件到此处它们将水平排列</small>
</div>
</slot>
</div>
<!-- 列布局 -->
<div
v-else-if="component.type === 'el-col'"
class="col-layout"
:style="getColStyle()"
>
<slot name="children">
<div class="layout-placeholder">
<el-icon><Grid /></el-icon>
<span>列布局 - 垂直排列容器</span>
<small>拖拽组件到此处它们将垂直排列</small>
</div>
</slot>
</div>
<!-- 容器布局 -->
<el-container
v-else-if="component.type === 'el-container'"
v-bind="component.props"
class="container-layout"
>
<slot name="children">
<div class="container-placeholder">拖拽容器组件到此处</div>
</slot>
</el-container>
<!-- 头部 -->
<el-header
v-else-if="component.type === 'el-header'"
v-bind="component.props"
class="header-container"
>
<slot name="children">
<div class="header-placeholder">头部内容</div>
</slot>
</el-header>
<!-- 主要区域 -->
<el-main
v-else-if="component.type === 'el-main'"
v-bind="component.props"
class="main-container"
>
<slot name="children">
<div class="main-placeholder">主要内容区域</div>
</slot>
</el-main>
<!-- 侧边栏 -->
<el-aside
v-else-if="component.type === 'el-aside'"
v-bind="component.props"
class="aside-container"
>
<slot name="children">
<div class="aside-placeholder">侧边栏内容</div>
</slot>
</el-aside>
<!-- 底部 -->
<el-footer
v-else-if="component.type === 'el-footer'"
v-bind="component.props"
class="footer-container"
>
<slot name="children">
<div class="footer-placeholder">底部内容</div>
</slot>
</el-footer>
<!-- 未知组件 -->
<div v-else class="unknown-component">
未知组件: {{ component.type }}
</div>
</div>
</template>
<script setup lang="ts">
import { Grid, Plus } from '@element-plus/icons-vue';
import { ComponentSchema } from '../utils/schema';
// Props
interface Props {
component: ComponentSchema;
}
const props = defineProps<Props>();
//
function handleClick() {
console.log('按钮点击');
}
function handleInput(value: string) {
console.log('输入值:', value);
}
function handleNumberChange(value: number | undefined) {
console.log('数字变化:', value);
}
function handleSelectChange(value: any) {
console.log('选择变化:', value);
}
function handleRadioChange(value: any) {
console.log('单选变化:', value);
}
function handleRadioGroupChange(value: any) {
console.log('单选组变化:', value);
}
function handleCheckboxChange(value: any) {
console.log('复选框变化:', value);
}
function handleCheckboxGroupChange(value: any[]) {
console.log('复选框组变化:', value);
}
function handleSwitchChange(value: any) {
console.log('开关变化:', value);
}
function handleSliderChange(value: any) {
console.log('滑块变化:', value);
}
function handleDateChange(value: any) {
console.log('日期变化:', value);
}
function handleTimeChange(value: any) {
console.log('时间变化:', value);
}
//
function getInputValue() {
return props.component.props?.modelValue || '';
}
function getNumberValue() {
return props.component.props?.modelValue || 0;
}
function getSelectValue() {
return props.component.props?.modelValue || '';
}
function getRadioValue() {
return props.component.props?.modelValue || false;
}
function getRadioGroupValue() {
return props.component.props?.modelValue || '';
}
function getCheckboxValue() {
return props.component.props?.modelValue || false;
}
function getCheckboxGroupValue() {
return props.component.props?.modelValue || [];
}
function getSwitchValue() {
return props.component.props?.modelValue || false;
}
function getSliderValue() {
return props.component.props?.modelValue || 0;
}
function getDateValue() {
return props.component.props?.modelValue || null;
}
function getTimeValue() {
return props.component.props?.modelValue || null;
}
//
function getSelectOptions() {
return [
{ label: '选项一', value: 'option1' },
{ label: '选项二', value: 'option2' },
{ label: '选项三', value: 'option3' }
];
}
function getRadioOptions() {
return [
{ label: '选项一', value: 'radio1' },
{ label: '选项二', value: 'radio2' }
];
}
function getCheckboxOptions() {
return [
{ label: '选项一', value: 'checkbox1' },
{ label: '选项二', value: 'checkbox2' }
];
}
//
function getRowStyle() {
const defaultStyle = {
display: 'flex',
flexDirection: 'row',
gap: '12px',
padding: '12px',
border: '1px dashed var(--el-border-color)',
borderRadius: '4px',
minHeight: '60px',
background: 'var(--el-fill-color-lighter)'
};
const customStyle = props.component.props?.style || {};
const gap = props.component.props?.gap || '12px';
const justifyContent = props.component.props?.justifyContent || 'flex-start';
const alignItems = props.component.props?.alignItems || 'flex-start';
return {
...defaultStyle,
...customStyle,
gap,
justifyContent,
alignItems
};
}
//
function getColStyle() {
const defaultStyle = {
display: 'flex',
flexDirection: 'column',
gap: '12px',
padding: '12px',
border: '1px dashed var(--el-border-color)',
borderRadius: '4px',
minHeight: '60px',
background: 'var(--el-fill-color-lighter)'
};
const customStyle = props.component.props?.style || {};
const gap = props.component.props?.gap || '12px';
const justifyContent = props.component.props?.justifyContent || 'flex-start';
const alignItems = props.component.props?.alignItems || 'flex-start';
return {
...defaultStyle,
...customStyle,
gap,
justifyContent,
alignItems
};
}
</script>
<style scoped>
.component-renderer {
width: 100%;
}
/* 占位符样式 */
.card-placeholder,
.form-placeholder,
.form-item-placeholder,
.row-placeholder,
.col-placeholder,
.container-placeholder,
.header-placeholder,
.main-placeholder,
.aside-placeholder,
.footer-placeholder {
padding: 20px;
text-align: center;
color: var(--el-text-color-placeholder);
background: var(--el-fill-color-lighter);
border: 1px dashed var(--el-border-color);
border-radius: 4px;
min-height: 60px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
/* 容器组件样式 */
.form-container {
border: 1px dashed var(--el-border-color);
border-radius: 4px;
padding: 20px;
min-height: 120px;
background: var(--el-fill-color-lighter);
position: relative;
}
.row-layout,
.col-layout {
position: relative;
transition: all 0.2s ease;
margin: 8px 0;
}
.row-layout:hover,
.col-layout:hover {
border-color: var(--el-color-primary) !important;
background: var(--el-color-primary-light-9) !important;
}
.layout-placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
color: var(--el-text-color-placeholder);
text-align: center;
padding: 20px;
}
.layout-placeholder .el-icon {
font-size: 24px;
}
.layout-placeholder span {
font-size: 14px;
font-weight: 500;
}
.layout-placeholder small {
font-size: 12px;
opacity: 0.8;
}
.container-layout {
border: 1px dashed var(--el-border-color);
border-radius: 4px;
min-height: 200px;
background: var(--el-fill-color-lighter);
padding: 16px;
}
.header-container,
.footer-container {
border: 1px dashed var(--el-border-color);
border-radius: 4px;
background: var(--el-fill-color-lighter);
}
.main-container {
border: 1px dashed var(--el-border-color);
border-radius: 4px;
background: var(--el-fill-color-lighter);
min-height: 200px;
}
.aside-container {
border: 1px dashed var(--el-border-color);
border-radius: 4px;
background: var(--el-fill-color-lighter);
}
.unknown-component {
padding: 16px;
background: var(--el-color-danger-light-9);
border: 1px dashed var(--el-color-danger);
border-radius: 4px;
color: var(--el-color-danger);
text-align: center;
font-size: 14px;
}
/* 组件间距 */
.component-renderer > * {
margin: 4px 0;
}
/* 响应式设计 */
@media (max-width: 768px) {
.card-placeholder,
.form-placeholder,
.form-item-placeholder,
.row-placeholder,
.col-placeholder,
.container-placeholder,
.header-placeholder,
.main-placeholder,
.aside-placeholder,
.footer-placeholder {
padding: 12px;
min-height: 40px;
font-size: 12px;
}
.form-container,
.row-container,
.col-container {
padding: 8px;
min-height: 60px;
}
}
</style>
@@ -1,473 +0,0 @@
<!--
低代码页面生成器 - 属性检查器
右侧编辑选中组件属性的面板
-->
<template>
<div class="right-board">
<el-tabs v-model="currentTab" stretch class="center-tabs">
<el-tab-pane label="组件属性" name="field" />
<el-tab-pane label="样式设置" name="style" />
</el-tabs>
<div class="field-box">
<!-- 文档链接 -->
<a
v-if="selectedComponent && getDocumentLink()"
class="document-link"
target="_blank"
:href="getDocumentLink()"
title="查看组件文档"
>
<el-icon><Link /></el-icon>
</a>
<el-scrollbar class="right-scrollbar">
<!-- 未选中组件 -->
<div v-if="!selectedComponent" class="empty-inspector">
<el-icon class="empty-icon"><Setting /></el-icon>
<p>请选择一个组件来编辑属性</p>
</div>
<!-- 组件属性 -->
<el-form
v-show="currentTab === 'field' && selectedComponent"
size="small"
label-width="90px"
label-position="top"
>
<!-- 基本信息 -->
<el-form-item label="组件类型">
<el-input :value="getComponentLabel(selectedComponent?.type || '')" disabled />
</el-form-item>
<el-form-item v-if="selectedComponent?.label !== undefined" label="标签">
<el-input v-model="selectedComponent.label" placeholder="请输入标签" />
</el-form-item>
<!-- 布局组件特殊配置 -->
<template v-if="selectedComponent?.type === 'el-row' || selectedComponent?.type === 'el-col'">
<el-form-item label="间隔">
<el-input
:model-value="selectedComponent?.props?.gap || '12px'"
placeholder="如: 12px, 1rem"
@update:model-value="(value) => handlePropertyUpdate('gap', value)"
/>
</el-form-item>
<el-form-item label="主轴对齐">
<el-select
:model-value="selectedComponent?.props?.justifyContent || 'flex-start'"
@update:model-value="(value) => handlePropertyUpdate('justifyContent', value)"
>
<el-option label="起始对齐" value="flex-start" />
<el-option label="居中对齐" value="center" />
<el-option label="末尾对齐" value="flex-end" />
<el-option label="两端对齐" value="space-between" />
<el-option label="环绕对齐" value="space-around" />
<el-option label="平均对齐" value="space-evenly" />
</el-select>
</el-form-item>
<el-form-item label="交叉轴对齐">
<el-select
:model-value="selectedComponent?.props?.alignItems || 'flex-start'"
@update:model-value="(value) => handlePropertyUpdate('alignItems', value)"
>
<el-option label="起始对齐" value="flex-start" />
<el-option label="居中对齐" value="center" />
<el-option label="末尾对齐" value="flex-end" />
<el-option label="拉伸对齐" value="stretch" />
</el-select>
</el-form-item>
</template>
<!-- 组件属性 -->
<template v-for="propKey in getEditableProps()" :key="propKey">
<el-form-item v-if="propKey !== 'columnCount'" :label="getPropertyLabel(propKey)">
<PropertyEditor
:prop-key="propKey"
:prop-value="selectedComponent?.props?.[propKey]"
:component-type="selectedComponent?.type || ''"
@update="handlePropertyUpdate"
/>
</el-form-item>
</template>
</el-form>
<!-- 样式设置 -->
<el-form
v-show="currentTab === 'style' && selectedComponent"
size="small"
label-width="90px"
label-position="top"
>
<el-form-item label="宽度">
<el-input v-model="styleProps.width" placeholder="如: 100px, 50%, auto" />
</el-form-item>
<el-form-item label="高度">
<el-input v-model="styleProps.height" placeholder="如: 100px, 50vh, auto" />
</el-form-item>
<el-form-item label="外边距">
<el-input v-model="styleProps.margin" placeholder="如: 10px, 10px 20px" />
</el-form-item>
<el-form-item label="内边距">
<el-input v-model="styleProps.padding" placeholder="如: 10px, 10px 20px" />
</el-form-item>
<el-form-item label="背景色">
<el-color-picker v-model="styleProps.backgroundColor" show-alpha />
</el-form-item>
<el-form-item label="边框">
<el-input v-model="styleProps.border" placeholder="如: 1px solid #ccc" />
</el-form-item>
<el-form-item label="圆角">
<el-input v-model="styleProps.borderRadius" placeholder="如: 4px, 50%" />
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import { Setting, Link } from '@element-plus/icons-vue';
import PropertyEditor from './PropertyEditor.vue';
import { ComponentSchema, COMPONENT_CONFIGS, generateId } from '../utils/schema';
// Props
interface Props {
selectedComponent: ComponentSchema | null;
}
// Emits
interface Emits {
updateComponent: [component: ComponentSchema];
deleteComponent: [componentId: string];
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
//
const currentTab = ref('field');
// -
const styleProps = computed({
get() {
return props.selectedComponent?.style || {};
},
set(value) {
if (props.selectedComponent) {
props.selectedComponent.style = { ...props.selectedComponent.style, ...value };
emit('updateComponent', props.selectedComponent);
}
}
});
//
function handlePropertyUpdate(key: string, value: any) {
if (props.selectedComponent) {
if (!props.selectedComponent.props) {
props.selectedComponent.props = {};
}
props.selectedComponent.props[key] = value;
emit('updateComponent', props.selectedComponent);
}
}
//
function getDocumentLink(): string {
if (!props.selectedComponent) return '';
const config = COMPONENT_CONFIGS[props.selectedComponent.type as keyof typeof COMPONENT_CONFIGS];
return `https://element-plus.org/zh-CN/component/${props.selectedComponent.type.replace('el-', '')}`;
}
//
function getEditableProps(): string[] {
if (!props.selectedComponent) return [];
const config = COMPONENT_CONFIGS[props.selectedComponent.type as keyof typeof COMPONENT_CONFIGS];
return config?.editableProps || [];
}
//
function getComponentLabel(componentType: string): string {
const config = COMPONENT_CONFIGS[componentType as keyof typeof COMPONENT_CONFIGS];
return config?.label || componentType;
}
//
function getPropertyLabel(key: string): string {
const labelMap: Record<string, string> = {
//
type: '类型',
size: '尺寸',
disabled: '禁用',
children: '内容',
//
plain: '朴素按钮',
round: '圆角',
circle: '圆形',
loading: '加载中',
//
placeholder: '占位符',
clearable: '可清空',
readonly: '只读',
maxlength: '最大长度',
showWordLimit: '显示字数统计',
//
min: '最小值',
max: '最大值',
step: '步长',
precision: '精度',
controls: '显示控制按钮',
controlsPosition: '控制按钮位置',
//
multiple: '多选',
filterable: '可筛选',
// /
border: '带边框',
textColor: '文字颜色',
fill: '填充色',
indeterminate: '半选状态',
//
width: '开关宽度',
activeText: '打开文字',
inactiveText: '关闭文字',
activeValue: '打开值',
inactiveValue: '关闭值',
activeColor: '打开颜色',
inactiveColor: '关闭颜色',
//
showStops: '显示间断点',
showTooltip: '显示提示',
range: '范围选择',
// /
format: '显示格式',
valueFormat: '绑定值格式',
//
model: '表单数据对象',
rules: '表单验证规则',
labelWidth: '标签宽度',
labelPosition: '标签位置',
inline: '行内表单',
labelSuffix: '标签后缀',
hideRequiredAsterisk: '隐藏必填星号',
showMessage: '显示错误信息',
inlineMessage: '行内显示错误信息',
statusIcon: '显示状态图标',
validateOnRuleChange: '规则改变时验证',
//
required: '必填',
error: '错误信息',
//
gap: '间隔',
justifyContent: '主轴对齐',
alignItems: '交叉轴对齐',
gutter: '栅格间隔',
justify: '水平排列',
align: '垂直排列',
span: '栅格占据列数',
offset: '栅格左侧间隔',
push: '栅格向右移动',
pull: '栅格向左移动',
tag: '自定义元素标签',
direction: '排列方向',
height: '高度',
//
header: '卡片标题',
shadow: '阴影显示时机',
bodyStyle: '内容区域样式',
//
closable: '可关闭',
disableTransitions: '禁用渐变动画',
hit: '是否有边框描边',
color: '背景色',
effect: '主题',
//
percentage: '百分比',
strokeWidth: '进度条宽度',
textInside: '文字内显',
status: '状态',
showText: '显示文字',
//
title: '标题',
description: '描述',
center: '文字居中',
closeText: '关闭按钮文字',
showIcon: '显示图标',
// 线
contentPosition: '内容位置',
//
href: '链接地址',
underline: '下划线'
};
return labelMap[key] || key;
}
</script>
<style scoped>
.right-board {
width: 350px;
position: absolute;
right: 0;
top: 0;
padding-top: 3px;
height: calc(100vh - 50px - 40px);
background: var(--el-bg-color);
border-left: 1px solid var(--el-border-color-light);
:deep(.el-tabs__header) {
margin: 0;
background: var(--el-bg-color);
}
:deep(.el-input-group__append .el-button) {
display: inline-flex;
}
.field-box {
position: relative;
height: calc(100vh - 50px - 40px - 42px);
box-sizing: border-box;
overflow: hidden;
}
.right-scrollbar {
height: 100%;
:deep(.el-scrollbar__view) {
padding: 20px;
}
}
}
.empty-inspector {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 300px;
color: var(--el-text-color-placeholder);
text-align: center;
.empty-icon {
font-size: 48px;
margin-bottom: 16px;
color: var(--el-border-color);
}
p {
margin: 8px 0;
font-size: 14px;
}
}
.document-link {
position: absolute;
display: flex;
width: 26px;
height: 26px;
top: 0;
left: 0;
cursor: pointer;
background: var(--el-color-primary);
z-index: 10;
border-radius: 0 0 6px 0;
justify-content: center;
align-items: center;
color: var(--el-color-white);
font-size: 14px;
&:hover {
background: var(--el-color-primary-dark-2);
}
}
/* 表单样式 */
:deep(.el-form-item) {
margin-bottom: 18px;
}
:deep(.el-form-item__label) {
font-weight: 500;
color: var(--el-text-color-regular);
margin-bottom: 6px;
}
:deep(.el-input) {
width: 100%;
}
:deep(.el-select) {
width: 100%;
}
:deep(.el-color-picker) {
width: 100%;
}
:deep(.el-slider) {
width: 100%;
}
/* 提示文字样式 */
.form-item-tip {
font-size: 12px;
color: var(--el-text-color-placeholder);
margin-top: 4px;
line-height: 1.4;
}
/* 滚动条样式 */
.right-scrollbar::-webkit-scrollbar {
width: 6px;
}
.right-scrollbar::-webkit-scrollbar-track {
background: var(--el-fill-color-light);
border-radius: 3px;
}
.right-scrollbar::-webkit-scrollbar-thumb {
background: var(--el-border-color);
border-radius: 3px;
}
.right-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--el-border-color-dark);
}
/* 响应式设计 */
@media (max-width: 768px) {
.right-board {
width: 300px;
}
.right-scrollbar {
:deep(.el-scrollbar__view) {
padding: 16px;
}
}
}
</style>
@@ -1,355 +0,0 @@
<!--
低代码页面生成器 - 组件库面板
左侧可拖拽的 Element Plus 组件库
-->
<template>
<div class="left-board">
<div class="logo-wrapper">
<div class="logo">
<el-icon class="logo-icon"><Tools /></el-icon>
低代码生成器
</div>
</div>
<el-scrollbar class="left-scrollbar">
<div class="components-list">
<div
v-for="(components, category) in filteredCategories"
:key="category"
class="component-category"
>
<div class="components-title">
<el-icon><Grid /></el-icon>
{{ category }}
</div>
<draggable
class="components-draggable"
:list="getComponentList(components)"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }"
:clone="cloneComponent"
draggable=".components-item"
:sort="false"
@end="onEnd"
item-key="type"
>
<template #item="{ element }">
<div class="components-item" @click="addComponent(element)">
<div class="components-body">
<el-icon class="component-icon">
<component :is="getComponentIcon(element.type)" />
</el-icon>
<span class="component-label">{{ element.label }}</span>
</div>
</div>
</template>
</draggable>
</div>
</div>
</el-scrollbar>
<!-- 搜索框 -->
<div class="search-wrapper">
<el-input
v-model="searchText"
placeholder="搜索组件"
clearable
size="small"
prefix-icon="Search"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
import draggable from 'vuedraggable';
import { Tools, Grid, Search } from '@element-plus/icons-vue';
import {
ComponentType,
COMPONENT_CONFIGS,
COMPONENT_CATEGORIES,
createComponent
} from '../utils/schema';
//
const emit = defineEmits<{
addComponent: [component: any];
}>();
//
const searchText = ref('');
// -
const filteredCategories = computed(() => {
if (!searchText.value) {
return COMPONENT_CATEGORIES;
}
const filtered: Record<string, ComponentType[]> = {};
for (const [category, components] of Object.entries(COMPONENT_CATEGORIES)) {
const filteredComponents = components.filter(componentType => {
const config = COMPONENT_CONFIGS[componentType];
return config?.label?.toLowerCase().includes(searchText.value.toLowerCase());
});
if (filteredComponents.length > 0) {
filtered[category] = filteredComponents;
}
}
return filtered;
});
//
function getComponentList(components: ComponentType[]) {
return components.map(type => {
const config = COMPONENT_CONFIGS[type];
return {
type,
label: config?.label || type,
icon: config?.icon || 'Grid'
};
});
}
//
function cloneComponent(element: any) {
const component = createComponent(element.type);
return component;
}
//
function onEnd(event: any) {
//
}
//
function addComponent(element: any) {
const component = createComponent(element.type);
emit('addComponent', component);
}
//
function getComponentIcon(componentType: ComponentType): string {
const iconMap: Record<string, string> = {
'el-button': 'Connection',
'el-link': 'Link',
'el-text': 'Document',
'el-input': 'Edit',
'el-input-number': 'Plus',
'el-select': 'ArrowDown',
'el-radio': 'CircleCheck',
'el-radio-group': 'CircleCheck',
'el-checkbox': 'Check',
'el-checkbox-group': 'Check',
'el-switch': 'Switch',
'el-slider': 'Minus',
'el-date-picker': 'Calendar',
'el-time-picker': 'Clock',
'el-card': 'Document',
'el-tag': 'PriceTag',
'el-progress': 'Loading',
'el-alert': 'Warning',
'el-form': 'List',
'el-form-item': 'Tickets',
'el-row': 'Right',
'el-col': 'Bottom',
'el-container': 'Grid',
'el-header': 'Top',
'el-main': 'Grid',
'el-aside': 'Operation',
'el-footer': 'Bottom',
'el-divider': 'Minus'
};
return iconMap[componentType] || 'Grid';
}
</script>
<style scoped>
.left-board {
width: 260px;
position: absolute;
left: 0;
top: 0;
height: calc(100vh - 50px - 40px);
background: var(--el-bg-color);
border-right: 1px solid var(--el-border-color-light);
.logo-wrapper {
position: relative;
height: 42px;
border-bottom: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
background: var(--el-bg-color);
.logo {
position: absolute;
left: 12px;
top: 6px;
line-height: 30px;
color: var(--el-color-primary);
font-weight: 600;
font-size: 16px;
white-space: nowrap;
display: flex;
align-items: center;
gap: 8px;
.logo-icon {
font-size: 20px;
}
}
}
.left-scrollbar {
height: calc(100% - 42px - 60px);
.el-scrollbar__wrap {
box-sizing: border-box;
overflow-x: hidden !important;
margin-bottom: 0 !important;
}
.components-list {
padding: 8px;
box-sizing: border-box;
height: 100%;
.component-category {
margin-bottom: 16px;
}
.components-title {
font-size: 14px;
color: var(--el-text-color-regular);
margin: 6px 2px 8px 2px;
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
.el-icon {
font-size: 16px;
color: var(--el-color-primary);
}
}
.components-draggable {
padding-bottom: 8px;
.components-item {
display: inline-block;
width: 48%;
margin: 1%;
transition: transform 0.2s ease;
cursor: grab;
.components-body {
padding: 10px 8px;
background: var(--el-fill-color-light);
font-size: 12px;
border: 1px solid var(--el-border-color-light);
border-radius: 4px;
text-align: center;
transition: all 0.2s ease;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
min-height: 60px;
justify-content: center;
.component-icon {
font-size: 18px;
color: var(--el-text-color-regular);
}
.component-label {
color: var(--el-text-color-regular);
line-height: 1.2;
word-break: break-all;
}
&:hover {
border: 1px solid var(--el-color-primary);
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
transform: translateY(-1px);
.component-icon,
.component-label {
color: var(--el-color-primary);
}
}
}
&:active {
cursor: grabbing;
.components-body {
transform: scale(0.95);
}
}
}
}
}
}
.search-wrapper {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 12px;
background: var(--el-bg-color);
border-top: 1px solid var(--el-border-color-extra-light);
}
}
/* 滚动条样式 */
.left-scrollbar::-webkit-scrollbar {
width: 6px;
}
.left-scrollbar::-webkit-scrollbar-track {
background: var(--el-fill-color-light);
border-radius: 3px;
}
.left-scrollbar::-webkit-scrollbar-thumb {
background: var(--el-border-color);
border-radius: 3px;
}
.left-scrollbar::-webkit-scrollbar-thumb:hover {
background: var(--el-border-color-dark);
}
/* 响应式设计 */
@media (max-width: 768px) {
.left-board {
width: 240px;
}
.components-item {
width: 98% !important;
margin: 1% !important;
}
}
/* 空状态 */
.components-draggable:empty::after {
content: '暂无组件';
display: flex;
align-items: center;
justify-content: center;
height: 60px;
color: var(--el-text-color-placeholder);
font-size: 12px;
border: 1px dashed var(--el-border-color);
border-radius: 4px;
margin: 1%;
width: 48%;
}
</style>
@@ -1,466 +0,0 @@
<!--
低代码页面生成器 - 属性编辑器
根据属性类型渲染不同的编辑器
-->
<template>
<div class="property-editor">
<!-- 字符串输入 -->
<el-input
v-if="isStringType"
:model-value="propValue"
:placeholder="getPlaceholder()"
@update:model-value="handleUpdate"
/>
<!-- 数字输入 -->
<el-input-number
v-else-if="isNumberType"
:model-value="propValue"
:min="getMinValue()"
:max="getMaxValue()"
:step="getStepValue()"
:precision="getPrecision()"
@update:model-value="handleUpdate"
/>
<!-- 布尔开关 -->
<el-switch
v-else-if="isBooleanType"
:model-value="propValue"
@update:model-value="handleUpdate"
/>
<!-- 选择器 -->
<el-select
v-else-if="isSelectType"
:model-value="propValue"
:placeholder="getPlaceholder()"
@update:model-value="handleUpdate"
>
<el-option
v-for="option in getSelectOptions()"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-select>
<!-- 颜色选择器 -->
<el-color-picker
v-else-if="isColorType"
:model-value="propValue"
:show-alpha="getShowAlpha()"
:color-format="getColorFormat()"
@update:model-value="handleUpdate"
/>
<!-- 滑块 -->
<el-slider
v-else-if="isSliderType"
:model-value="propValue"
:min="getMinValue()"
:max="getMaxValue()"
:step="getStepValue()"
:show-stops="getShowStops()"
:range="getRange()"
@update:model-value="handleUpdate"
/>
<!-- 文本域 -->
<el-input
v-else-if="isTextareaType"
:model-value="propValue"
type="textarea"
:rows="getTextareaRows()"
:placeholder="getPlaceholder()"
@update:model-value="handleUpdate"
/>
<!-- 对象编辑器 -->
<div v-else-if="isObjectType" class="object-editor">
<el-input
:model-value="JSON.stringify(propValue, null, 2)"
type="textarea"
:rows="4"
placeholder="JSON 对象"
@update:model-value="handleObjectUpdate"
/>
</div>
<!-- 数组编辑器 -->
<div v-else-if="isArrayType" class="array-editor">
<div v-for="(item, index) in propValue" :key="index" class="array-item">
<el-input
:model-value="item"
:placeholder="`项目 ${index + 1}`"
@update:model-value="handleArrayItemUpdate(index, $event)"
/>
<el-button
size="small"
type="danger"
@click="removeArrayItem(index)"
>
<el-icon><Delete /></el-icon>
</el-button>
</div>
<el-button size="small" @click="addArrayItem">
<el-icon><Plus /></el-icon>
添加项目
</el-button>
</div>
<!-- 默认输入框 -->
<el-input
v-else
:model-value="String(propValue)"
:placeholder="getPlaceholder()"
@update:model-value="handleUpdate"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import { Delete, Plus } from '@element-plus/icons-vue';
// Props
interface Props {
propKey: string;
propValue: any;
componentType: string;
}
// Emits
interface Emits {
update: [key: string, value: any];
}
const props = defineProps<Props>();
const emit = defineEmits<Emits>();
// -
const isStringType = computed(() => {
return typeof props.propValue === 'string' && !isSelectType.value && !isTextareaType.value;
});
const isNumberType = computed(() => {
return typeof props.propValue === 'number' ||
(typeof props.propValue === 'string' && !isNaN(Number(props.propValue)));
});
const isBooleanType = computed(() => {
return typeof props.propValue === 'boolean';
});
const isSelectType = computed(() => {
const selectProps = ['type', 'size', 'status', 'position', 'placement', 'trigger'];
return selectProps.includes(props.propKey);
});
const isColorType = computed(() => {
return props.propKey.includes('color') || props.propKey.includes('Color');
});
const isSliderType = computed(() => {
return ['min', 'max', 'step', 'value'].includes(props.propKey);
});
const isTextareaType = computed(() => {
return props.propKey.includes('text') ||
props.propKey.includes('content') ||
props.propKey.includes('description');
});
const isObjectType = computed(() => {
return typeof props.propValue === 'object' && !Array.isArray(props.propValue);
});
const isArrayType = computed(() => {
return Array.isArray(props.propValue);
});
//
function handleUpdate(value: any) {
emit('update', props.propKey, value);
}
//
function handleObjectUpdate(value: string) {
try {
const obj = JSON.parse(value);
emit('update', props.propKey, obj);
} catch (error) {
console.error('JSON 解析错误:', error);
}
}
//
function handleArrayItemUpdate(index: number, value: any) {
const newArray = [...props.propValue];
newArray[index] = value;
emit('update', props.propKey, newArray);
}
//
function addArrayItem() {
const newArray = [...props.propValue, ''];
emit('update', props.propKey, newArray);
}
//
function removeArrayItem(index: number) {
const newArray = props.propValue.filter((_: any, i: number) => i !== index);
emit('update', props.propKey, newArray);
}
//
function getPlaceholder(): string {
const placeholders: Record<string, string> = {
placeholder: '请输入占位符',
label: '请输入标签',
text: '请输入文本',
value: '请输入值',
name: '请输入名称',
id: '请输入ID',
class: '请输入CSS类名',
style: '请输入样式',
title: '请输入标题',
content: '请输入内容',
description: '请输入描述'
};
return placeholders[props.propKey] || '请输入值';
}
//
function getSelectOptions() {
const optionMap: Record<string, Array<{ label: string; value: any }>> = {
type: getTypeOptions(),
size: [
{ label: '大', value: 'large' },
{ label: '默认', value: 'default' },
{ label: '小', value: 'small' }
],
shadow: [
{ label: '总是显示', value: 'always' },
{ label: '悬停时显示', value: 'hover' },
{ label: '从不显示', value: 'never' }
],
justify: [
{ label: '左对齐', value: 'start' },
{ label: '居中', value: 'center' },
{ label: '右对齐', value: 'end' },
{ label: '两端对齐', value: 'space-between' },
{ label: '环绕对齐', value: 'space-around' },
{ label: '平均对齐', value: 'space-evenly' }
],
align: [
{ label: '顶部', value: 'top' },
{ label: '中间', value: 'middle' },
{ label: '底部', value: 'bottom' }
],
labelPosition: [
{ label: '右侧', value: 'right' },
{ label: '左侧', value: 'left' },
{ label: '顶部', value: 'top' }
],
direction: [
{ label: '垂直', value: 'vertical' },
{ label: '水平', value: 'horizontal' }
],
contentPosition: [
{ label: '左侧', value: 'left' },
{ label: '居中', value: 'center' },
{ label: '右侧', value: 'right' }
],
effect: [
{ label: '浅色', value: 'light' },
{ label: '深色', value: 'dark' }
]
};
return optionMap[props.propKey] || [];
}
// type
function getTypeOptions() {
const componentTypeOptions: Record<string, Array<{ label: string; value: any }>> = {
'el-button': [
{ label: '默认', value: 'default' },
{ label: '主要', value: 'primary' },
{ label: '成功', value: 'success' },
{ label: '信息', value: 'info' },
{ label: '警告', value: 'warning' },
{ label: '危险', value: 'danger' },
{ label: '文本', value: 'text' }
],
'el-input': [
{ label: '文本', value: 'text' },
{ label: '密码', value: 'password' },
{ label: '数字', value: 'number' },
{ label: '邮箱', value: 'email' },
{ label: '电话', value: 'tel' },
{ label: 'URL', value: 'url' }
],
'el-alert': [
{ label: '成功', value: 'success' },
{ label: '信息', value: 'info' },
{ label: '警告', value: 'warning' },
{ label: '错误', value: 'error' }
],
'el-tag': [
{ label: '默认', value: '' },
{ label: '成功', value: 'success' },
{ label: '信息', value: 'info' },
{ label: '警告', value: 'warning' },
{ label: '危险', value: 'danger' }
],
'el-link': [
{ label: '默认', value: 'default' },
{ label: '主要', value: 'primary' },
{ label: '成功', value: 'success' },
{ label: '信息', value: 'info' },
{ label: '警告', value: 'warning' },
{ label: '危险', value: 'danger' }
],
'el-text': [
{ label: '主要', value: 'primary' },
{ label: '常规', value: 'regular' },
{ label: '次要', value: 'secondary' },
{ label: '占位符', value: 'placeholder' },
{ label: '禁用', value: 'disabled' }
],
'el-progress': [
{ label: '线形', value: 'line' },
{ label: '环形', value: 'circle' },
{ label: '仪表盘', value: 'dashboard' }
],
'el-date-picker': [
{ label: '日期', value: 'date' },
{ label: '周', value: 'week' },
{ label: '月', value: 'month' },
{ label: '年', value: 'year' },
{ label: '日期时间', value: 'datetime' },
{ label: '日期范围', value: 'daterange' },
{ label: '月范围', value: 'monthrange' },
{ label: '日期时间范围', value: 'datetimerange' }
]
};
return componentTypeOptions[props.componentType] || [
{ label: '默认', value: 'default' },
{ label: '主要', value: 'primary' },
{ label: '成功', value: 'success' },
{ label: '信息', value: 'info' },
{ label: '警告', value: 'warning' },
{ label: '危险', value: 'danger' }
];
}
//
function getMinValue(): number {
const minMap: Record<string, number> = {
min: 0,
max: 0,
step: 0,
value: 0,
span: 1,
offset: 0,
push: 0,
pull: 0,
columnCount: 1,
percentage: 0,
width: 0
};
return minMap[props.propKey] ?? -Infinity;
}
//
function getMaxValue(): number {
const maxMap: Record<string, number> = {
span: 24,
offset: 23,
push: 23,
pull: 23,
maxlength: 1000,
columnCount: 6,
percentage: 100,
width: 200
};
return maxMap[props.propKey] ?? Infinity;
}
//
function getStepValue(): number {
return props.propKey === 'step' ? 1 : 0.1;
}
//
function getPrecision(): number {
return props.propKey === 'precision' ? 0 : 2;
}
//
function getShowAlpha(): boolean {
return props.propKey.includes('alpha') || props.propKey === 'backgroundColor';
}
//
function getColorFormat(): string {
return props.propKey.includes('alpha') ? 'rgba' : 'hex';
}
//
function getShowStops(): boolean {
return props.propKey === 'step' || props.propKey === 'value';
}
//
function getRange(): boolean {
return props.propKey === 'value' && props.componentType === 'el-slider';
}
//
function getTextareaRows(): number {
return props.propKey.includes('description') ? 4 : 2;
}
</script>
<style scoped>
.property-editor {
width: 100%;
}
.object-editor {
width: 100%;
}
.array-editor {
width: 100%;
}
.array-item {
display: flex;
gap: 8px;
margin-bottom: 8px;
align-items: center;
}
.array-item .el-input {
flex: 1;
}
/* 响应式设计 */
@media (max-width: 768px) {
.array-item {
flex-direction: column;
align-items: stretch;
}
.array-item .el-button {
width: 100%;
}
}
</style>
@@ -1,71 +0,0 @@
<template>
<el-dialog v-model="open" width="500px" title="选择生成类型" @open="onOpen" @close="onClose">
<el-form ref="codeTypeForm" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="生成类型" prop="type">
<el-radio-group v-model="formData.type">
<el-radio-button v-for="(item, index) in typeOptions" :key="index" :label="item.value">
{{ item.label }}
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="showFileName" label="文件名" prop="fileName">
<el-input v-model="formData.fileName" placeholder="请输入文件名" clearable />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="onClose">取消</el-button>
<el-button type="primary" @click="handelConfirm">确定</el-button>
</template>
</el-dialog>
</template>
<script setup>
const open = defineModel()
const props = defineProps({
showFileName: Boolean
})
const emit = defineEmits(['confirm'])
const formData = ref({
fileName: undefined,
type: 'file'
})
const codeTypeForm = ref()
const rules = {
fileName: [{
required: true,
message: '请输入文件名',
trigger: 'blur'
}],
type: [{
required: true,
message: '生成类型不能为空',
trigger: 'change'
}]
}
const typeOptions = ref([
{
label: '页面',
value: 'file'
},
{
label: '弹窗',
value: 'dialog'
}
])
function onOpen() {
if (props.showFileName) {
formData.value.fileName = `${+new Date()}.vue`
}
}
function onClose() {
open.value = false
}
function handelConfirm() {
codeTypeForm.value.validate(valid => {
if (!valid) return
emit('confirm', { ...formData.value })
onClose()
})
}
</script>
@@ -1,68 +0,0 @@
<template>
<el-col :span="element.span" :class="className" @click.stop="activeItem(element)">
<el-form-item :label="element.label" :label-width="element.labelWidth ? element.labelWidth + 'px' : null"
:required="element.required" v-if="element.layout === 'colFormItem'">
<render :key="element.tag" :conf="element" v-model="element.defaultValue" />
</el-form-item>
<el-row :gutter="element.gutter" :class="element.class" @click.stop="activeItem(element)" v-else>
<span class="component-name"> {{ element.componentName }} </span>
<draggable group="componentsGroup" :animation="340" :list="element.children" class="drag-wrapper" item-key="label"
ref="draggableItemRef" :component-data="getComponentData()">
<template #item="scoped">
<draggable-item :key="scoped.element.renderKey" :drawing-list="element.children" :element="scoped.element"
:index="index" :active-id="activeId" :form-conf="formConf" @activeItem="activeItem(scoped.element)"
@copyItem="copyItem(scoped.element, element.children)"
@deleteItem="deleteItem(scoped.index, element.children)" />
</template>
</draggable>
</el-row>
<span class="drawing-item-copy" title="复制" @click.stop="copyItem(element)">
<el-icon><CopyDocument /></el-icon>
</span>
<span class="drawing-item-delete" title="删除" @click.stop="deleteItem(index)">
<el-icon><Delete /></el-icon>
</span>
</el-col>
</template>
<script setup name="DraggableItem">
import draggable from "vuedraggable"
import render from '@/utils/generator/render'
const props = defineProps({
element: Object,
index: Number,
drawingList: Array,
activeId: {
type: [String, Number]
},
formConf: Object
})
const className = ref('')
const draggableItemRef = ref(null)
const emits = defineEmits(['activeItem', 'copyItem', 'deleteItem'])
function activeItem(item) {
emits('activeItem', item)
}
function copyItem(item, parent) {
emits('copyItem', item, parent ?? props.drawingList)
}
function deleteItem(item, parent) {
emits('deleteItem', item, parent ?? props.drawingList)
}
function getComponentData() {
return {
gutter: props.element.gutter,
justify: props.element.justify,
align: props.element.align
}
}
watch(() => props.activeId, (val) => {
className.value = (props.element.layout === 'rowFormItem' ? 'drawing-row-item' : 'drawing-item') + (val === props.element.formId ? ' active-from-item' : '')
if (props.formConf.unFocusedComponentBorder) {
className.value += ' unfocus-bordered'
}
}, { immediate: true })
</script>
@@ -1,115 +0,0 @@
<template>
<div class="icon-dialog">
<el-dialog v-model="value" width="980px" :close-on-click-modal="false" :modal-append-to-body="false" @open="onOpen"
@close="onClose">
<template #header="{ close, titleId, titleClass }">
选择图标
<el-input v-model="key" size="small" :style="{ width: '260px' }" placeholder="请输入图标名称" prefix-icon="Search"
clearable />
</template>
<ul class="icon-ul">
<li v-for="icon in iconList" :key="icon" :class="active === icon ? 'active-item' : ''" @click="onSelect(icon)">
<div>
<el-icon :size="30">
<component :is="icon" />
</el-icon>
<div>{{ icon }}</div>
</div>
</li>
</ul>
</el-dialog>
</div>
</template>
<script setup>
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import { watch } from 'vue'
const iconList = ref([])
const originList = []
const key = ref('')
const active = ref('')
const emit = defineEmits(['select'])
const value = defineModel()
for (const [key] of Object.entries(ElementPlusIconsVue)) {
iconList.value.push(key)
originList.push(key)
}
function onOpen() { }
function onClose() { }
function onSelect(icon) {
active.value = icon
emit('select', icon)
value.value = false
}
watch(key, (val) => {
if (val) {
iconList.value = originList.filter(name => name.indexOf(val) > -1)
} else {
iconList.value = originList
}
})
</script>
<style lang="scss" scoped>
.icon-ul {
margin: 0;
padding: 0;
font-size: 0;
li {
list-style-type: none;
text-align: center;
font-size: 14px;
display: inline-flex;
width: 16.66%;
box-sizing: border-box;
height: 108px;
padding: 6px 6px 6px 6px;
cursor: pointer;
overflow: hidden;
align-items: center;
justify-content: center;
&:hover {
background: var(--el-fill-color-light);
}
&.active-item {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary)
}
i {
font-size: 30px;
line-height: 50px;
margin-bottom: 10px;
}
}
}
.icon-dialog {
:deep() {
.el-dialog {
border-radius: 8px;
margin-bottom: 0;
margin-top: 4vh !important;
display: flex;
flex-direction: column;
max-height: 92vh;
overflow: hidden;
box-sizing: border-box;
.el-dialog__header {
padding-top: 14px;
}
.el-dialog__body {
margin: 0 20px 20px 20px;
padding: 0;
overflow: auto;
}
}
}
}
</style>
@@ -1,996 +0,0 @@
<template>
<div class="right-board">
<el-tabs v-model="currentTab" stretch class="center-tabs">
<el-tab-pane label="组件属性" name="field" />
<el-tab-pane label="表单属性" name="form" />
</el-tabs>
<div class="field-box">
<a class="document-link" target="_blank" :href="documentLink" title="查看组件文档">
<el-icon>
<Link />
</el-icon>
</a>
<el-scrollbar class="right-scrollbar">
<!-- 组件属性 -->
<el-form v-show="currentTab === 'field' && showField" size="default" label-width="90px" label-position="top"
style="">
<el-form-item v-if="activeData.changeTag" label="组件类型">
<el-select v-model="activeData.tagIcon" placeholder="请选择组件类型" :style="{ width: '100%' }" @change="tagChange">
<el-option-group v-for="group in tagList" :key="group.label" :label="group.label">
<el-option v-for="item in group.options" :key="item.label" :label="item.label" :value="item.tagIcon">
<svg-icon class="node-icon" :icon-class="item.tagIcon" style="margin-right: 10px;" />
<span> {{ item.label }}</span>
</el-option>
</el-option-group>
</el-select>
</el-form-item>
<el-form-item v-if="activeData.vModel !== undefined" label="字段名">
<el-input v-model="activeData.vModel" placeholder="请输入字段名(v-model" />
</el-form-item>
<el-form-item v-if="activeData.componentName !== undefined" label="组件名">
{{ activeData.componentName }}
</el-form-item>
<el-form-item v-if="activeData.label !== undefined" label="标题">
<el-input v-model="activeData.label" placeholder="请输入标题" />
</el-form-item>
<el-form-item v-if="activeData.placeholder !== undefined" label="占位提示">
<el-input v-model="activeData.placeholder" placeholder="请输入占位提示" />
</el-form-item>
<el-form-item v-if="activeData['start-placeholder'] !== undefined" label="开始占位">
<el-input v-model="activeData['start-placeholder']" placeholder="请输入占位提示" />
</el-form-item>
<el-form-item v-if="activeData['end-placeholder'] !== undefined" label="结束占位">
<el-input v-model="activeData['end-placeholder']" placeholder="请输入占位提示" />
</el-form-item>
<el-form-item v-if="activeData.span !== undefined" label="表单栅格">
<el-slider v-model="activeData.span" :max="24" :min="1" :marks="{ 12: '' }" @change="spanChange" />
</el-form-item>
<el-form-item v-if="activeData.layout === 'rowFormItem'" label="栅格间隔">
<el-input-number v-model="activeData.gutter" :min="0" placeholder="栅格间隔" />
</el-form-item>
<el-form-item v-if="activeData.justify !== undefined">
<template #label>
<span>水平排列</span>
<el-tooltip content="设置子元素在主轴上的对齐方式" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-select v-model="activeData.justify" placeholder="请选择水平排列" :style="{ width: '100%' }">
<el-option v-for="(item, index) in justifyOptions" :key="index" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item v-if="activeData.align !== undefined">
<template #label>
<span>垂直排列</span>
<el-tooltip content="设置子元素在交叉轴上的对齐方式" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-radio-group v-model="activeData.align">
<el-radio-button label="top" />
<el-radio-button label="middle" />
<el-radio-button label="bottom" />
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData.labelWidth !== undefined">
<template #label>
<span>标签宽度</span>
<el-tooltip content="设置标签的宽度,单位为px" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input v-model.number="activeData.labelWidth" type="number" placeholder="请输入标签宽度" />
</el-form-item>
<el-form-item v-if="activeData.style && activeData.style.width !== undefined">
<template #label>
<span>组件宽度</span>
<el-tooltip content="设置组件的宽度,支持px、%、auto等" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input v-model="activeData.style.width" placeholder="请输入组件宽度" clearable />
</el-form-item>
<el-form-item v-if="activeData.vModel !== undefined">
<template #label>
<span>默认值</span>
<el-tooltip content="设置组件的初始值" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input :value="setDefaultValue(activeData.defaultValue)" placeholder="请输入默认值"
@input="onDefaultValueInput" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-checkbox-group'" label="至少应选">
<el-input-number :value="activeData.min" :min="0" placeholder="至少应选"
@input="$set(activeData, 'min', $event ? $event : undefined)" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-checkbox-group'" label="最多可选">
<el-input-number :value="activeData.max" :min="0" placeholder="最多可选"
@input="$set(activeData, 'max', $event ? $event : undefined)" />
</el-form-item>
<el-form-item v-if="activeData.prepend !== undefined">
<template #label>
<span>前缀</span>
<el-tooltip content="在输入框前面添加的内容,如单位、固定前缀等" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input v-model="activeData.prepend" placeholder="请输入前缀" />
</el-form-item>
<el-form-item v-if="activeData.append !== undefined">
<template #label>
<span>后缀</span>
<el-tooltip content="在输入框后面添加的内容,如单位、固定前缀等" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input v-model="activeData.append" placeholder="请输入后缀" />
</el-form-item>
<el-form-item v-if="activeData['prefix-icon'] !== undefined" label="前图标">
<el-input v-model="activeData['prefix-icon']" placeholder="请输入前图标名称">
<template #append>
<el-button icon="Pointer" @click="openIconsDialog('prefix-icon')">
选择
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="activeData['suffix-icon'] !== undefined" label="后图标">
<el-input v-model="activeData['suffix-icon']" placeholder="请输入后图标名称">
<template #append>
<el-button icon="Pointer" @click="openIconsDialog('suffix-icon')">
选择
</el-button>
</template>
</el-input>
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-cascader'" label="选项分隔符">
<el-input v-model="activeData.separator" placeholder="请输入选项分隔符" />
</el-form-item>
<el-form-item v-if="activeData.autosize !== undefined" label="最小行数">
<el-input-number v-model="activeData.autosize.minRows" :min="1" placeholder="最小行数" />
</el-form-item>
<el-form-item v-if="activeData.autosize !== undefined" label="最大行数">
<el-input-number v-model="activeData.autosize.maxRows" :min="1" placeholder="最大行数" />
</el-form-item>
<el-form-item v-if="activeData.min !== undefined" label="最小值">
<el-input-number v-model="activeData.min" placeholder="最小值" />
</el-form-item>
<el-form-item v-if="activeData.max !== undefined" label="最大值">
<el-input-number v-model="activeData.max" placeholder="最大值" />
</el-form-item>
<el-form-item v-if="activeData.step !== undefined" label="步长">
<el-input-number v-model="activeData.step" placeholder="步数" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-input-number'" label="精度">
<el-input-number v-model="activeData.precision" :min="0" placeholder="精度" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-input-number'" label="按钮位置">
<el-radio-group v-model="activeData['controls-position']">
<el-radio-button label="">
默认
</el-radio-button>
<el-radio-button label="right">
右侧
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData.maxlength !== undefined" label="最多输入">
<el-input v-model="activeData.maxlength" placeholder="请输入字符长度">
<template slot="append">
个字符
</template>
</el-input>
</el-form-item>
<el-form-item v-if="activeData['active-text'] !== undefined" label="开启提示">
<el-input v-model="activeData['active-text']" placeholder="请输入开启提示" />
</el-form-item>
<el-form-item v-if="activeData['inactive-text'] !== undefined" label="关闭提示">
<el-input v-model="activeData['inactive-text']" placeholder="请输入关闭提示" />
</el-form-item>
<el-form-item v-if="activeData['active-value'] !== undefined" label="开启值">
<el-input :value="setDefaultValue(activeData['active-value'])" placeholder="请输入开启值"
@input="onSwitchValueInput($event, 'active-value')" />
</el-form-item>
<el-form-item v-if="activeData['inactive-value'] !== undefined" label="关闭值">
<el-input :value="setDefaultValue(activeData['inactive-value'])" placeholder="请输入关闭值"
@input="onSwitchValueInput($event, 'inactive-value')" />
</el-form-item>
<el-form-item v-if="activeData.type !== undefined && 'el-date-picker' === activeData.tag" label="时间类型">
<el-select v-model="activeData.type" placeholder="请选择时间类型" :style="{ width: '100%' }"
@change="dateTypeChange">
<el-option v-for="(item, index) in dateOptions" :key="index" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item v-if="activeData.name !== undefined" label="文件字段名">
<el-input v-model="activeData.name" placeholder="请输入上传文件字段名" />
</el-form-item>
<el-form-item v-if="activeData.accept !== undefined" label="文件类型">
<el-select v-model="activeData.accept" placeholder="请选择文件类型" :style="{ width: '100%' }" clearable>
<el-option label="图片" value="image/*" />
<el-option label="视频" value="video/*" />
<el-option label="音频" value="audio/*" />
<el-option label="excel" value=".xls,.xlsx" />
<el-option label="word" value=".doc,.docx" />
<el-option label="pdf" value=".pdf" />
<el-option label="txt" value=".txt" />
</el-select>
</el-form-item>
<el-form-item v-if="activeData.fileSize !== undefined" label="文件大小">
<el-input v-model.number="activeData.fileSize" placeholder="请输入文件大小">
<el-select slot="append" v-model="activeData.sizeUnit" :style="{ width: '66px' }">
<el-option label="KB" value="KB" />
<el-option label="MB" value="MB" />
<el-option label="GB" value="GB" />
</el-select>
</el-input>
</el-form-item>
<el-form-item v-if="activeData.action !== undefined" label="上传地址">
<el-input v-model="activeData.action" placeholder="请输入上传地址" clearable />
</el-form-item>
<el-form-item v-if="activeData['list-type'] !== undefined" label="列表类型">
<el-radio-group v-model="activeData['list-type']" size="small">
<el-radio-button label="text">
text
</el-radio-button>
<el-radio-button label="picture">
picture
</el-radio-button>
<el-radio-button label="picture-card">
picture-card
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData.buttonText !== undefined" v-show="'picture-card' !== activeData['list-type']"
label="按钮文字">
<el-input v-model="activeData.buttonText" placeholder="请输入按钮文字" />
</el-form-item>
<el-form-item v-if="activeData['range-separator'] !== undefined" label="分隔符">
<el-input v-model="activeData['range-separator']" placeholder="请输入分隔符" />
</el-form-item>
<el-form-item v-if="activeData['picker-options'] !== undefined" label="时间段">
<el-input v-model="activeData['picker-options'].selectableRange" placeholder="请输入时间段" />
</el-form-item>
<el-form-item v-if="activeData.format !== undefined" label="时间格式">
<el-input :value="activeData.format" placeholder="请输入时间格式" @input="setTimeValue($event)" />
</el-form-item>
<template v-if="['el-checkbox-group', 'el-radio-group', 'el-select'].indexOf(activeData.tag) > -1">
<el-divider>选项</el-divider>
<draggable :list="activeData.options" :animation="340" group="selectItem" handle=".option-drag"
item-key="label">
<template #item="{ element, index }">
<div :key="index" class="select-item">
<div class="select-line-icon option-drag">
<i class="el-icon-s-operation" />
</div>
<el-input v-model="element.label" placeholder="选项名" size="small" />
<el-input placeholder="选项值" size="small" :value="element.value"
@input="setOptionValue(element, $event)" />
<div class="close-btn select-line-icon" @click="activeData.options.splice(index, 1)">
<el-icon>
<Remove />
</el-icon>
</div>
</div>
</template>
</draggable>
<div>
<el-button icon="CirclePlus" style="margin-left: 8px; margin-top: 10px;" text bg type="primary"
@click="addSelectItem">
添加选项
</el-button>
</div>
<el-divider />
</template>
<template v-if="['el-cascader'].indexOf(activeData.tag) > -1">
<el-divider>选项</el-divider>
<el-form-item label="数据类型">
<el-radio-group v-model="activeData.dataType" size="small">
<el-radio-button label="dynamic">
动态数据
</el-radio-button>
<el-radio-button label="static">
静态数据
</el-radio-button>
</el-radio-group>
</el-form-item>
<template v-if="activeData.dataType === 'dynamic'">
<el-form-item label="标签键名">
<el-input v-model="activeData.labelKey" placeholder="请输入标签键名" />
</el-form-item>
<el-form-item label="值键名">
<el-input v-model="activeData.valueKey" placeholder="请输入值键名" />
</el-form-item>
<el-form-item label="子级键名">
<el-input v-model="activeData.childrenKey" placeholder="请输入子级键名" />
</el-form-item>
</template>
<el-tree v-if="activeData.dataType === 'static'" draggable :data="activeData.options" node-key="id"
:expand-on-click-node="false" :render-content="renderContent" />
<div v-if="activeData.dataType === 'static'">
<el-button icon="CirclePlus" style="margin-left: 0; margin-top: 10px;" type="primary" text bg
@click="addTreeItem">
添加父级
</el-button>
</div>
<el-divider />
</template>
<el-form-item v-if="activeData.optionType !== undefined" label="选项样式">
<el-radio-group v-model="activeData.optionType">
<el-radio-button label="default">
默认
</el-radio-button>
<el-radio-button label="button">
按钮
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData['active-color'] !== undefined" label="开启颜色">
<el-color-picker v-model="activeData['active-color']" />
</el-form-item>
<el-form-item v-if="activeData['inactive-color'] !== undefined" label="关闭颜色">
<el-color-picker v-model="activeData['inactive-color']" />
</el-form-item>
<el-form-item v-if="activeData['allow-half'] !== undefined" label="允许半选">
<el-switch v-model="activeData['allow-half']" />
</el-form-item>
<el-form-item v-if="activeData['show-text'] !== undefined" label="辅助文字">
<el-switch v-model="activeData['show-text']" @change="rateTextChange" />
</el-form-item>
<el-form-item v-if="activeData['show-score'] !== undefined" label="显示分数">
<el-switch v-model="activeData['show-score']" @change="rateScoreChange" />
</el-form-item>
<el-form-item v-if="activeData['show-stops'] !== undefined" label="显示间断点">
<el-switch v-model="activeData['show-stops']" />
</el-form-item>
<el-form-item v-if="activeData.range !== undefined" label="范围选择">
<el-switch v-model="activeData.range" @change="rangeChange" />
</el-form-item>
<el-form-item v-if="activeData.border !== undefined && activeData.optionType === 'default'" label="是否带边框">
<el-switch v-model="activeData.border" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-color-picker'" label="颜色格式">
<el-select v-model="activeData['color-format']" placeholder="请选择颜色格式" :style="{ width: '100%' }"
@change="colorFormatChange">
<el-option v-for="(item, index) in colorFormatOptions" :key="index" :label="item.label"
:value="item.value" />
</el-select>
</el-form-item>
<el-form-item v-if="activeData.size !== undefined &&
(activeData.optionType === 'button' ||
activeData.border ||
activeData.tag === 'el-color-picker')" label="选项尺寸">
<el-radio-group v-model="activeData.size">
<el-radio-button label="large">
较大
</el-radio-button>
<el-radio-button label="default">
默认
</el-radio-button>
<el-radio-button label="small">
较小
</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="activeData['show-word-limit'] !== undefined" label="输入统计">
<el-switch v-model="activeData['show-word-limit']" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-input-number'" label="严格步数">
<el-switch v-model="activeData['step-strictly']" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-cascader'" label="是否多选">
<el-switch v-model="activeData.props.props.multiple" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-cascader'" label="展示全路径">
<el-switch v-model="activeData['show-all-levels']" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-cascader'" label="可否筛选">
<el-switch v-model="activeData.filterable" />
</el-form-item>
<el-form-item v-if="activeData.clearable !== undefined" label="能否清空">
<el-switch v-model="activeData.clearable" />
</el-form-item>
<el-form-item v-if="activeData.showTip !== undefined" label="显示提示">
<el-switch v-model="activeData.showTip" />
</el-form-item>
<el-form-item v-if="activeData.multiple !== undefined" label="多选文件">
<el-switch v-model="activeData.multiple" />
</el-form-item>
<el-form-item v-if="activeData['auto-upload'] !== undefined" label="自动上传">
<el-switch v-model="activeData['auto-upload']" />
</el-form-item>
<el-form-item v-if="activeData.readonly !== undefined" label="是否只读">
<el-switch v-model="activeData.readonly" />
</el-form-item>
<el-form-item v-if="activeData.disabled !== undefined" label="是否禁用">
<el-switch v-model="activeData.disabled" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-select'" label="是否可搜索">
<el-switch v-model="activeData.filterable" />
</el-form-item>
<el-form-item v-if="activeData.tag === 'el-select'" label="是否多选">
<el-switch v-model="activeData.multiple" @change="multipleChange" />
</el-form-item>
<el-form-item v-if="activeData.required !== undefined">
<template #label>
<span>是否必填</span>
<el-tooltip content="设置该字段是否为必填项,影响表单验证" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-switch v-model="activeData.required" />
</el-form-item>
<template v-if="activeData.layoutTree">
<el-divider>布局结构树</el-divider>
<el-tree :data="[activeData]" :props="layoutTreeProps" node-key="renderKey" default-expand-all draggable>
<template #default="{ node, data }">
<span class="node-label">
<svg-icon class="node-icon" :icon-class="data.tagIcon" style="margin-right: 5px;" />
{{ node.label }}
</span>
</template>
</el-tree>
</template>
<template v-if="activeData.layout === 'colFormItem' && activeData.tag !== 'el-button'">
<el-divider>
<span>正则校验</span>
<el-tooltip content="添加自定义正则表达式验证规则,用于表单字段验证" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</el-divider>
<div v-for="(item, index) in activeData.regList" :key="index" class="reg-item">
<span class="close-btn" @click="activeData.regList.splice(index, 1)">
<el-icon>
<Close />
</el-icon>
</span>
<el-form-item>
<template #label>
<span>表达式</span>
<el-tooltip content="输入正则表达式,用于验证输入内容格式" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input v-model="item.pattern" placeholder="请输入正则" />
</el-form-item>
<el-form-item style="margin-bottom:0">
<template #label>
<span>错误提示</span>
<el-tooltip content="当验证失败时显示的错误信息" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input v-model="item.message" placeholder="请输入错误提示" />
</el-form-item>
</div>
<div>
<el-button icon="CirclePlus" style="margin-left: 0; margin-top: 10px;" type="primary" text bg
@click="addReg">
添加规则
</el-button>
</div>
</template>
</el-form>
<!-- 表单属性 -->
<el-form v-show="currentTab === 'form'" label-width="90px" label-position="top">
<el-form-item label="表单名">
<el-input v-model="formConf.formRef" placeholder="请输入表单名(ref" />
</el-form-item>
<el-form-item label="表单模型">
<el-input v-model="formConf.formModel" placeholder="请输入数据模型" />
</el-form-item>
<el-form-item label="校验模型">
<el-input v-model="formConf.formRules" placeholder="请输入校验模型" />
</el-form-item>
<el-form-item>
<template #label>
<span>表单尺寸</span>
<el-tooltip content="设置表单组件的整体尺寸" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-radio-group v-model="formConf.size">
<el-radio-button label="large">较大</el-radio-button>
<el-radio-button label="default">默认</el-radio-button>
<el-radio-button label="small">较小</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item>
<template #label>
<span>标签对齐</span>
<el-tooltip content="设置标签文本的对齐方式" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-radio-group v-model="formConf.labelPosition">
<el-radio-button label="left">左对齐</el-radio-button>
<el-radio-button label="right">右对齐</el-radio-button>
<el-radio-button label="top">顶部对齐</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="标签宽度">
<el-input-number v-model="formConf.labelWidth" placeholder="标签宽度" />
</el-form-item>
<el-form-item>
<template #label>
<span>栅格间隔</span>
<el-tooltip content="设置栅格列之间的间隔,单位为px" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-input-number v-model="formConf.gutter" :min="0" placeholder="栅格间隔" />
</el-form-item>
<el-form-item label="禁用表单">
<el-switch v-model="formConf.disabled" />
</el-form-item>
<el-form-item>
<template #label>
<span>表单按钮</span>
<el-tooltip content="是否在生成的表单中包含提交、重置等按钮" placement="top">
<el-icon style="margin-left: 4px; cursor: help;"><QuestionFilled /></el-icon>
</el-tooltip>
</template>
<el-switch v-model="formConf.formBtns" />
</el-form-item>
<el-form-item label="显示未选中组件边框">
<el-switch v-model="formConf.unFocusedComponentBorder" />
</el-form-item>
</el-form>
</el-scrollbar>
</div>
<icons-dialog v-model="iconsVisible" :current="activeData[currentIconModel]" @select="setIcon" />
<treeNode-dialog v-model="dialogVisible" @commit="addNode" />
</div>
</template>
<script setup>
import draggable from "vuedraggable"
import { isNumberStr } from '@/utils/index'
import IconsDialog from './IconsDialog.vue'
import TreeNodeDialog from './TreeNodeDialog.vue'
import { inputComponents, selectComponents } from '@/utils/generator/config'
import { QuestionFilled } from '@element-plus/icons-vue'
const { proxy } = getCurrentInstance()
const dateTimeFormat = {
date: 'YYYY-MM-DD',
week: 'YYYY 第 ww 周',
month: 'YYYY-MM',
year: 'YYYY',
datetime: 'YYYY-MM-DD HH:mm:ss',
daterange: 'YYYY-MM-DD',
monthrange: 'YYYY-MM',
datetimerange: 'YYYY-MM-DD HH:mm:ss'
}
const props = defineProps({
showField: Boolean,
activeData: Object,
formConf: Object
})
const data = reactive({
currentTab: 'field',
currentNode: null,
dialogVisible: false,
iconsVisible: false,
currentIconModel: null,
dateTypeOptions: [
{
label: '日(date)',
value: 'date'
},
{
label: '周(week)',
value: 'week'
},
{
label: '月(month)',
value: 'month'
},
{
label: '年(year)',
value: 'year'
},
{
label: '日期时间(datetime)',
value: 'datetime'
}
],
dateRangeTypeOptions: [
{
label: '日期范围(daterange)',
value: 'daterange'
},
{
label: '月范围(monthrange)',
value: 'monthrange'
},
{
label: '日期时间范围(datetimerange)',
value: 'datetimerange'
}
],
colorFormatOptions: [
{
label: 'hex',
value: 'hex'
},
{
label: 'rgb',
value: 'rgb'
},
{
label: 'rgba',
value: 'rgba'
},
{
label: 'hsv',
value: 'hsv'
},
{
label: 'hsl',
value: 'hsl'
}
],
justifyOptions: [
{
label: 'start',
value: 'start'
},
{
label: 'end',
value: 'end'
},
{
label: 'center',
value: 'center'
},
{
label: 'space-around',
value: 'space-around'
},
{
label: 'space-between',
value: 'space-between'
}
],
layoutTreeProps: {
label(data, node) {
return data.componentName || `${data.label}: ${data.vModel}`
}
}
})
const { currentTab, currentNode, dialogVisible, iconsVisible, currentIconModel, dateTypeOptions, dateRangeTypeOptions, colorFormatOptions, justifyOptions, layoutTreeProps } = toRefs(data)
const documentLink = computed(() => props.activeData.document || 'https://element-plus.org/zh-CN/guide/installation')
const dateOptions = computed(() => {
if (props.activeData.type !== undefined && props.activeData.tag === 'el-date-picker') {
if (props.activeData['start-placeholder'] === undefined) {
return dateTypeOptions.value
}
return dateRangeTypeOptions.value
}
return []
})
const tagList = ref([
{
label: '输入型组件',
options: inputComponents
},
{
label: '选择型组件',
options: selectComponents
}
])
const emit = defineEmits(['tag-change'])
function addReg() {
props.activeData.regList.push({
pattern: '',
message: ''
})
}
function addSelectItem() {
props.activeData.options.push({
label: '',
value: ''
})
}
function addTreeItem() {
++proxy.idGlobal
dialogVisible.value = true
currentNode.value = props.activeData.options
}
function renderContent(h, { node, data, store }) {
return h('div', {
class: "custom-tree-node"
}, [
h('span', node.label),
h('span', {
class: "node-operation"
}, [
h(resolveComponent('el-link'), {
type: "primary",
icon: "Plus",
underline: false,
onClick: () => {
append(data)
}
}),
h(resolveComponent('el-link'), {
type: "danger",
icon: "Delete",
underline: false,
style: "margin-left: 5px;",
onClick: () => {
remove(node, data)
}
})
])
])
}
function append(data) {
if (!data.children) {
data.children = []
}
dialogVisible.value = true
currentNode.value = data.children
}
function remove(node, data) {
const { parent } = node
const children = parent.data.children || parent.data
const index = children.findIndex(d => d.id === data.id)
children.splice(index, 1)
}
function addNode(data) {
currentNode.value.push(data)
}
function setOptionValue(item, val) {
item.value = isNumberStr(val) ? +val : val
}
function setDefaultValue(val) {
if (Array.isArray(val)) {
return val.join(',')
}
if (['string', 'number'].indexOf(val) > -1) {
return val
}
if (typeof val === 'boolean') {
return `${val}`
}
return val
}
function onDefaultValueInput(str) {
if (Array.isArray(props.activeData.defaultValue)) {
//
props.activeData.defaultValue = str.split(',').map(val => (isNumberStr(val) ? +val : val))
} else if (['true', 'false'].indexOf(str) > -1) {
//
props.activeData.defaultValue = JSON.parse(str)
} else {
//
props.activeData.defaultValue = isNumberStr(str) ? +str : str
}
}
function onSwitchValueInput(val, name) {
if (['true', 'false'].indexOf(val) > -1) {
props.activeData[name] = JSON.parse(val)
} else {
props.activeData[name] = isNumberStr(val) ? +val : val
}
}
function setTimeValue(val, type) {
const valueFormat = type === 'week' ? dateTimeFormat.date : val
props.activeData.defaultValue = null
props.activeData['value-format'] = valueFormat
props.activeData.format = val
}
function spanChange(val) {
props.formConf.span = val
}
function multipleChange(val) {
props.activeData.defaultValue = val ? [] : ''
}
function dateTypeChange(val) {
setTimeValue(dateTimeFormat[val], val)
}
function rangeChange(val) {
props.activeData.defaultValue = val ? [props.activeData.min, props.activeData.max] : props.activeData.min
}
function rateTextChange(val) {
if (val) props.activeData['show-score'] = false
}
function rateScoreChange(val) {
if (val) props.activeData['show-text'] = false
}
function colorFormatChange(val) {
props.activeData.defaultValue = null
props.activeData['show-alpha'] = val.indexOf('a') > -1
props.activeData.renderKey = +new Date() // renderKey,
}
function openIconsDialog(model) {
iconsVisible.value = true
currentIconModel.value = model
}
function setIcon(val) {
props.activeData[currentIconModel.value] = val
}
function tagChange(tagIcon) {
let target = inputComponents.find(item => item.tagIcon === tagIcon)
if (!target) target = selectComponents.find(item => item.tagIcon === tagIcon)
emit('tag-change', target)
}
</script>
<style lang="scss" scoped>
.right-board {
width: 350px;
position: absolute;
right: 0;
top: 0;
padding-top: 3px;
&:deep() {
.el-tabs__header {
margin: 0;
}
.el-input-group__append .el-button {
display: inline-flex;
}
}
.field-box {
position: relative;
height: calc(100vh - 50px - 40px - 42px);
box-sizing: border-box;
overflow: hidden;
}
.el-scrollbar {
height: 100%;
&:deep() {
.el-scrollbar__view {
padding: 30px 20px;
}
}
}
}
.reg-item {
padding: 12px 6px;
background: var(--el-border-color-extra-light);
position: relative;
border-radius: 4px;
.close-btn {
position: absolute;
right: -6px;
top: -6px;
display: flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
line-height: 16px;
background: rgba(0, 0, 0, .2);
border-radius: 50%;
color: var(--el-color-white);
z-index: 1;
cursor: pointer;
font-size: 12px;
}
}
.select-item {
display: flex;
border: 1px dashed var(--el-color-white);
box-sizing: border-box;
& .close-btn {
cursor: pointer;
color: var(--el-color-danger);
}
& .el-input+.el-input {
margin-left: 4px;
}
}
.select-item+.select-item {
margin-top: 4px;
}
.select-item.sortable-chosen {
border: 1px dashed var(--el-color-primary);
}
.select-line-icon {
line-height: 32px;
font-size: 22px;
padding: 0 4px;
color: var(--el-text-color-regular);
}
.option-drag {
cursor: move;
}
.time-range {
.el-date-editor {
width: 227px;
}
:deep() {
.el-icon-time {
display: none;
}
}
}
.document-link {
position: absolute;
display: flex;
width: 26px;
height: 26px;
top: 0;
left: 0;
cursor: pointer;
background: var(--el-color-primary);
z-index: 1;
border-radius: 0 0 6px 0;
justify-content: center;
align-items: center;
color: var(--el-color-white);
font-size: 18px;
}
.node-label {
font-size: 14px;
}
.node-icon {
color: var(--el-text-color-placeholder);
}
.custom-tree-node {
flex: 1;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
}
</style>
@@ -1,93 +0,0 @@
<template>
<div>
<el-dialog title="添加选项" v-model="open" width="800px" :close-on-click-modal="false" :modal-append-to-body="false"
@open="onOpen" @close="onClose">
<el-form ref="treeNodeForm" :model="formData" :rules="rules" label-width="100px">
<el-col :span="24">
<el-form-item label="选项名" prop="label">
<el-input v-model="formData.label" placeholder="请输入选项名" clearable />
</el-form-item>
</el-col>
<el-col :span="24">
<el-form-item label="选项值" prop="value">
<el-input v-model="formData.value" placeholder="请输入选项值" clearable>
<template #append>
<el-select v-model="dataType" :style="{ width: '100px' }">
<el-option v-for="(item, index) in dataTypeOptions" :key="index" :label="item.label" :value="item.value"
:disabled="item.disabled" />
</el-select>
</template>
</el-input>
</el-form-item>
</el-col>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handelConfirm"> </el-button>
<el-button @click="onClose"> </el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup>
const open = defineModel()
const emit = defineEmits(['confirm'])
const formData = ref({
label: undefined,
value: undefined
})
const rules = {
label: [
{
required: true,
message: '请输入选项名',
trigger: 'blur'
}
],
value: [
{
required: true,
message: '请输入选项值',
trigger: 'blur'
}
]
}
const dataType = ref('string')
const dataTypeOptions = ref([
{
label: '字符串',
value: 'string'
},
{
label: '数字',
value: 'number'
}
])
const id = ref(100)
const treeNodeForm = ref()
function onOpen() {
formData.value = {
label: undefined,
value: undefined
}
}
function onClose() {
open.value = false
}
function handelConfirm() {
treeNodeForm.value.validate(valid => {
if (!valid) return
if (dataType.value === 'number') {
formData.value.value = parseFloat(formData.value.value)
}
formData.value.id = id.value++
emit('commit', formData.value)
onClose()
})
}
</script>
@@ -1,172 +0,0 @@
<!--
低代码页面生成器 - 主界面
整合 PaletteCanvasInspector 三个核心组件
-->
<template>
<div class="container">
<!-- 左侧组件库 -->
<Palette @add-component="handleAddComponent" />
<!-- 中间画布 -->
<Canvas
:components="components"
:selected-component-id="selectedComponentId || undefined"
@update-components="handleUpdateComponents"
@select-component="handleSelectComponent"
@update-component="handleUpdateComponent"
@delete-component="handleDeleteComponent"
@move-component="handleMoveComponent"
/>
<!-- 右侧属性面板 -->
<Inspector
:selected-component="selectedComponent"
@update-component="handleUpdateComponent"
@delete-component="handleDeleteComponent"
/>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue';
import { ElMessage } from 'element-plus';
import Palette from './components/Palette.vue';
import Canvas from './components/Canvas.vue';
import Inspector from './components/Inspector.vue';
import { ComponentSchema } from './utils/schema';
//
const components = ref<ComponentSchema[]>([]);
const selectedComponentId = ref<string | null>(null);
//
const selectedComponent = computed(() => {
if (!selectedComponentId.value) return null;
return findComponentById(components.value, selectedComponentId.value);
});
//
function findComponentById(components: ComponentSchema[], id: string): ComponentSchema | null {
for (const component of components) {
if (component.id === id) {
return component;
}
if (component.children) {
const found = findComponentById(component.children, id);
if (found) return found;
}
}
return null;
}
//
function handleAddComponent(component: ComponentSchema) {
components.value.push(component);
selectedComponentId.value = component.id;
ElMessage.success(`已添加 ${component.type} 组件`);
}
//
function handleUpdateComponents(newComponents: ComponentSchema[]) {
components.value = newComponents;
}
//
function handleSelectComponent(component: ComponentSchema | null) {
selectedComponentId.value = component?.id || null;
}
//
function handleUpdateComponent(component: ComponentSchema) {
updateComponentById(components.value, component);
}
//
function updateComponentById(components: ComponentSchema[], updatedComponent: ComponentSchema) {
for (let i = 0; i < components.length; i++) {
if (components[i].id === updatedComponent.id) {
components[i] = updatedComponent;
return;
}
if (components[i].children) {
updateComponentById(components[i].children!, updatedComponent);
}
}
}
//
function handleDeleteComponent(componentId: string) {
deleteComponentById(components.value, componentId);
if (selectedComponentId.value === componentId) {
selectedComponentId.value = null;
}
}
//
function deleteComponentById(components: ComponentSchema[], id: string): boolean {
for (let i = 0; i < components.length; i++) {
if (components[i].id === id) {
components.splice(i, 1);
return true;
}
if (components[i].children) {
if (deleteComponentById(components[i].children!, id)) {
return true;
}
}
}
return false;
}
//
function handleMoveComponent(fromIndex: number, toIndex: number) {
const component = components.value.splice(fromIndex, 1)[0];
components.value.splice(toIndex, 0, component);
}
//
onMounted(() => {
try {
const savedComponents = localStorage.getItem('lowcode-components');
if (savedComponents) {
components.value = JSON.parse(savedComponents);
}
} catch (error) {
console.warn('加载本地数据失败:', error);
}
});
//
watch(() => components.value, (newComponents) => {
try {
localStorage.setItem('lowcode-components', JSON.stringify(newComponents));
} catch (error) {
console.warn('保存到本地存储失败:', error);
}
}, { deep: true });
</script>
<style scoped>
.container {
position: relative;
width: 100%;
background-color: var(--el-bg-color-overlay);
height: calc(100vh - 50px - 40px);
overflow: hidden;
}
/* 响应式设计 */
@media (max-width: 1200px) {
.container {
height: calc(100vh - 40px);
}
}
@media (max-width: 768px) {
.container {
height: calc(100vh - 30px);
}
}
</style>
@@ -1,669 +0,0 @@
<template>
<div class="app-container">
<div class="container">
<div class="left-board">
<div class="logo-wrapper">
<div class="logo">
<img :src="logo" alt="logo"> Form Generator
</div>
</div>
<el-scrollbar class="left-scrollbar">
<div class="components-list">
<div class="components-title">
<svg-icon icon-class="component" />输入型组件
</div>
<draggable
class="components-draggable" :list="inputComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
<div class="components-title">
<svg-icon icon-class="component" />选择型组件
</div>
<draggable
class="components-draggable" :list="selectComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
<div class="components-title">
<svg-icon icon-class="component" /> 布局型组件
</div>
<draggable
class="components-draggable" :list="layoutComponents"
:group="{ name: 'componentsGroup', pull: 'clone', put: false }" :clone="cloneComponent"
draggable=".components-item" :sort="false" item-key="label" @end="onEnd">
<template #item="{ element, index }">
<div :key="index" class="components-item" @click="addComponent(element)">
<div class="components-body">
<svg-icon :icon-class="element.tagIcon" />
{{ element.label }}
</div>
</div>
</template>
</draggable>
</div>
</el-scrollbar>
</div>
<div class="center-board">
<div class="action-bar">
<el-button icon="Download" type="primary" text @click="download" v-hasPermi="['generator:webcode:download']">
导出vue文件
</el-button>
<el-button class="copy-btn-main" icon="DocumentCopy" type="primary" text @click="copy" v-hasPermi="['generator:webcode:copy']">
复制代码
</el-button>
<el-button class="delete-btn" icon="Delete" text type="danger" @click="empty" v-hasPermi="['generator:webcode:empty']">
清空
</el-button>
</div>
<el-scrollbar class="center-scrollbar">
<el-row class="center-board-row" :gutter="formConf.gutter">
<el-form :size="formConf.size" :label-position="formConf.labelPosition" :disabled="formConf.disabled"
:label-width="formConf.labelWidth + 'px'">
<draggable
class="drawing-board" :list="drawingList" :animation="340" group="componentsGroup"
item-key="label">
<template #item="{ element, index }">
<DraggableItem
:key="element.renderKey" :drawing-list="drawingList" :element="element" :index="index"
:active-id="activeId" :form-conf="formConf" @active-item="activeFormItem" @copy-item="drawingItemCopy"
@delete-item="drawingItemDelete" />
</template>
</draggable>
<div v-show="!drawingList.length" class="empty-info">
从左侧拖入或点选组件进行表单设计
</div>
</el-form>
</el-row>
</el-scrollbar>
</div>
</div>
<RightPanel
:active-data="activeData" :form-conf="formConf" :show-field="!!drawingList.length"
@tag-change="tagChange" />
<CodeTypeDialog v-model="dialogVisible" title="选择生成类型" :show-file-name="showFileName" @confirm="generate" />
<input id="copyNode" type="hidden">
</div>
</template>
<script setup>
import draggable from "vuedraggable"
import ClipboardJS from 'clipboard'
import beautifier from 'js-beautify'
import logo from '@/assets/logo/logo.png'
import { inputComponents, selectComponents, layoutComponents, formConf as formConfData } from '@/utils/generator/config'
import { beautifierConf } from '@/utils/index'
import drawingDefalut from '@/utils/generator/drawingDefalut'
import { makeUpHtml, vueTemplate, vueScript, cssStyle } from '@/utils/generator/html'
import { makeUpJs } from '@/utils/generator/js'
import { makeUpCss } from '@/utils/generator/css'
import Download from '@/plugins/download.ts'
import { ElNotification, ElMessageBox, ElMessage } from 'element-plus'
import DraggableItem from './components/DraggableItem.vue'
import RightPanel from './components/RightPanel.vue'
import CodeTypeDialog from './components/CodeTypeDialog.vue'
import { onMounted, watch, ref, nextTick } from 'vue'
const drawingList = ref(drawingDefalut)
const dialogVisible = ref(false)
const showFileName = ref(false)
const operationType = ref('')
const idGlobal = ref(100)
const activeData = ref(drawingDefalut[0])
const activeId = ref(drawingDefalut[0].formId)
const generateConf = ref(null)
const formData = ref({})
const formConf = ref(formConfData)
let oldActiveId
let tempActiveData
function activeFormItem(element) {
activeData.value = element
activeId.value = element.formId
}
function copy() {
dialogVisible.value = true
showFileName.value = false
operationType.value = 'copy'
}
function download() {
dialogVisible.value = true
showFileName.value = true
operationType.value = 'download'
}
function empty() {
ElMessageBox.confirm('确定要清空所有组件吗?', '提示', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
}).then(() => {
idGlobal.value = 100
drawingList.value = []
}
).catch(() => {
// 用户取消操作
})
}
function onEnd(obj, a) {
if (obj.from !== obj.to) {
activeData.value = tempActiveData
activeId.value = idGlobal.value
}
}
function addComponent(item) {
const clone = cloneComponent(item)
drawingList.value.push(clone)
activeFormItem(clone)
}
function cloneComponent(origin) {
const clone = JSON.parse(JSON.stringify(origin))
clone.formId = ++idGlobal.value
clone.span = formConf.value.span
clone.renderKey = +new Date() // 改变renderKey后可以实现强制更新组件
if (!clone.layout) clone.layout = 'colFormItem'
if (clone.layout === 'colFormItem') {
clone.vModel = `field${idGlobal.value}`
clone.placeholder !== undefined && (clone.placeholder += clone.label)
tempActiveData = clone
} else if (clone.layout === 'rowFormItem') {
delete clone.label
clone.componentName = `row${idGlobal.value}`
clone.gutter = formConf.value.gutter
tempActiveData = clone
}
return tempActiveData
}
function drawingItemCopy(item, parent) {
let clone = JSON.parse(JSON.stringify(item))
clone = createIdAndKey(clone)
parent.push(clone)
activeFormItem(clone)
}
function createIdAndKey(item) {
item.formId = ++idGlobal.value
item.renderKey = +new Date()
if (item.layout === 'colFormItem') {
item.vModel = `field${idGlobal.value}`
} else if (item.layout === 'rowFormItem') {
item.componentName = `row${idGlobal.value}`
}
if (Array.isArray(item.children)) {
item.children = item.children.map(childItem => createIdAndKey(childItem))
}
return item
}
function drawingItemDelete(index, parent) {
parent.splice(index, 1)
nextTick(() => {
const len = drawingList.value.length
if (len) {
activeFormItem(drawingList.value[len - 1])
}
})
}
function tagChange(newTag) {
newTag = cloneComponent(newTag)
newTag.vModel = activeData.value.vModel
newTag.formId = activeId.value
newTag.span = activeData.value.span
delete activeData.value.tag
delete activeData.value.tagIcon
delete activeData.value.document
Object.keys(newTag).forEach(key => {
if (activeData.value[key] !== undefined
&& typeof activeData.value[key] === typeof newTag[key]) {
newTag[key] = activeData.value[key]
}
})
activeData.value = newTag
updateDrawingList(newTag, drawingList.value)
}
function updateDrawingList(newTag, list) {
const index = list.findIndex(item => item.formId === activeId.value)
if (index > -1) {
list.splice(index, 1, newTag)
} else {
list.forEach(item => {
if (Array.isArray(item.children)) updateDrawingList(newTag, item.children)
})
}
}
function generate(data) {
generateConf.value = data
nextTick(() => {
switch (operationType.value) {
case 'copy':
execCopy(data)
break
case 'download':
execDownload(data)
break
default:
break
}
})
}
function execDownload(data) {
const codeStr = generateCode()
const blob = new Blob([codeStr], { type: 'text/plain;charset=utf-8' })
Download.saveAs(blob, data.fileName)
}
function execCopy(data) {
document.getElementById('copyNode').click()
}
function AssembleFormData() {
formData.value = { fields: JSON.parse(JSON.stringify(drawingList.value)), ...formConf.value }
}
function generateCode() {
const { type } = generateConf.value
AssembleFormData()
const script = vueScript(makeUpJs(formData.value, type))
const html = vueTemplate(makeUpHtml(formData.value, type))
const css = cssStyle(makeUpCss(formData.value))
return beautifier.html(html + script + css, beautifierConf.html)
}
watch(() => activeData.value.label, (val, oldVal) => {
if (
activeData.value.placeholder === undefined
|| !activeData.value.tag
|| oldActiveId !== activeId.value
) {
return
}
activeData.value.placeholder = activeData.value.placeholder.replace(oldVal, '') + val
})
watch(activeId, (val) => {
oldActiveId = val
}, { immediate: true })
onMounted(() => {
const clipboard = new ClipboardJS('#copyNode', {
text: trigger => {
const codeStr = generateCode()
ElNotification({ title: '成功', message: '代码已复制到剪切板,可粘贴。', type: 'success' })
return codeStr
}
})
clipboard.on('error', e => {
ElMessage.error('代码复制失败')
})
})
</script>
<style lang='scss'>
// 使用Element Plus系统颜色变量
.container {
position: relative;
width: 100%;
background-color: var(--el-bg-color-overlay);
height: calc(100vh - 50px - 40px);
overflow: hidden;
.left-board {
width: 260px;
position: absolute;
left: 0;
top: 0;
height: calc(100vh - 50px - 40px);
.logo-wrapper {
position: relative;
height: 42px;
border-bottom: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
.logo {
position: absolute;
left: 12px;
top: 6px;
line-height: 30px;
color: var(--el-color-primary);
font-weight: 600;
font-size: 17px;
white-space: nowrap;
>img {
width: 30px;
height: 30px;
vertical-align: top;
}
.github {
display: inline-block;
vertical-align: sub;
margin-left: 15px;
>img {
height: 22px;
}
}
}
}
.left-scrollbar {
.el-scrollbar__wrap {
box-sizing: border-box;
overflow-x: hidden !important;
margin-bottom: 0 !important;
.components-list {
padding: 8px;
box-sizing: border-box;
height: 100%;
.components-title {
font-size: 14px;
// color: #222;
margin: 6px 2px;
.svg-icon {
// color: #666;
font-size: 18px;
margin-right: 5px;
}
}
.components-draggable {
padding-bottom: 20px;
.components-item {
display: inline-block;
width: 48%;
margin: 1%;
transition: transform 0ms !important;
.components-body {
padding: 8px 10px;
background: var(--el-border-color-extra-light);
font-size: 12px;
cursor: move;
border: 1px dashed var(--el-border-color-extra-light);
border-radius: 3px;
.svg-icon {
// color: #777;
font-size: 15px;
margin-right: 5px;
}
&:hover {
border: 1px dashed var(--el-color-primary);
color: var(--el-color-primary);
.svg-icon {
color: var(--el-color-primary);
}
}
}
}
}
}
}
}
}
.center-board {
height: calc(100vh - 50px - 40px);
width: auto;
margin: 0 350px 0 260px;
box-sizing: border-box;
.action-bar {
position: relative;
height: 42px;
padding: 0 15px;
box-sizing: border-box;
;
border: 1px solid var(--el-border-color-extra-light);
border-top: none;
border-left: none;
display: flex;
align-items: center;
justify-content: flex-end;
.delete-btn {
color: var(--el-color-danger);
}
}
.center-scrollbar {
height: calc(100vh - 50px - 40px - 42px);
overflow: hidden;
border-left: 1px solid var(--el-border-color-extra-light);
border-right: 1px solid var(--el-border-color-extra-light);
box-sizing: border-box;
.el-scrollbar__view {
overflow-x: hidden;
}
.center-board-row {
padding: 12px 12px 15px 12px;
box-sizing: border-box;
&>.el-form {
// 69 = 12+15+42
height: calc(100vh - 50px - 40px - 69px);
flex: 1;
.drawing-board {
height: 100%;
position: relative;
.components-body {
padding: 0;
margin: 0;
font-size: 0;
}
.sortable-ghost {
position: relative;
display: block;
overflow: hidden;
&::before {
content: " ";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: var(--el-color-primary);
z-index: 2;
}
}
.components-item.sortable-ghost {
width: 100%;
height: 60px;
background: var(--el-border-color-extra-light);
}
.active-from-item {
&>.el-form-item {
background: var(--el-border-color-extra-light);
border-radius: 6px;
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: initial;
}
&>.component-name {
color: var(--el-color-primary);
}
.el-input__wrapper {
box-shadow: 0 0 0 1px var(--el-input-hover-border-color) inset;
}
}
.el-form-item {
margin-bottom: 15px;
}
}
.drawing-item {
position: relative;
cursor: move;
&.unfocus-bordered:not(.activeFromItem)>div:first-child {
border: 1px dashed var(--el-border-color);
}
.el-form-item {
padding: 12px 10px;
}
}
.drawing-row-item {
position: relative;
cursor: move;
box-sizing: border-box;
border: 1px dashed var(--el-border-color);
border-radius: 3px;
padding: 0 2px;
margin-bottom: 15px;
.drawing-row-item {
margin-bottom: 2px;
}
.el-col {
margin-top: 22px;
}
.el-form-item {
margin-bottom: 0;
}
.drag-wrapper {
min-height: 80px;
flex: 1;
display: flex;
flex-wrap: wrap;
}
&.active-from-item {
border: 1px dashed var(--el-color-primary);
}
.component-name {
position: absolute;
top: 0;
left: 0;
font-size: 12px;
color: var(--el-text-color-placeholder);
display: inline-block;
padding: 0 6px;
}
}
.drawing-item,
.drawing-row-item {
&:hover {
&>.el-form-item {
background: var(--el-border-color-extra-light);
border-radius: 6px;
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: initial;
}
}
&>.drawing-item-copy,
&>.drawing-item-delete {
display: none;
position: absolute;
top: -10px;
width: 22px;
height: 22px;
line-height: 22px;
text-align: center;
border-radius: 50%;
font-size: 12px;
border: 1px solid;
cursor: pointer;
z-index: 1;
}
&>.drawing-item-copy {
right: 56px;
border-color: var(--el-color-primary);
color: var(--el-color-primary);
background: var(--el-bg-color);
&:hover {
background: var(--el-color-primary);
color: var(--el-color-white);
}
}
&>.drawing-item-delete {
right: 24px;
border-color: var(--el-color-danger);
color: var(--el-color-danger);
background: var(--el-bg-color);
&:hover {
background: var(--el-color-danger);
color: var(--el-color-white);
}
}
}
.empty-info {
position: absolute;
top: 46%;
left: 0;
right: 0;
text-align: center;
font-size: 18px;
color: var(--el-text-color-placeholder);
letter-spacing: 4px;
}
}
}
}
}
}
</style>
@@ -1,606 +0,0 @@
/**
* - 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;
}
@@ -1,412 +0,0 @@
/**
* -
* 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> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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);
}
}