mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-20 20:39:55 +00:00
feat(代码生成器): 重构代码生成模板并增强安全性
重构代码生成模板结构,将模板文件移动到标准目录 添加文件覆盖控制和安全路径检查,防止生成到项目外 优化生成逻辑,支持跳过已存在文件并返回统计信息 完善模板内容,增加分页查询和更多功能实现细节
This commit is contained in:
@@ -222,10 +222,6 @@ async def gen_code_local_controller(
|
||||
返回:
|
||||
- JSONResponse: 包含生成结果的JSON响应
|
||||
"""
|
||||
from app.config.setting import settings
|
||||
if not settings.allow_overwrite:
|
||||
logger.error('【系统预设】不允许生成文件覆盖到本地')
|
||||
return ErrorResponse(msg='【系统预设】不允许生成文件覆盖到本地')
|
||||
result = await GenTableService.generate_code_service(auth, table_name)
|
||||
logger.info('生成代码到指定路径成功')
|
||||
return SuccessResponse(msg="生成代码到指定路径成功", data=result)
|
||||
|
||||
@@ -408,18 +408,35 @@ class GenTableService:
|
||||
"""
|
||||
env = Jinja2TemplateInitializerUtil.init_jinja2()
|
||||
render_info = await cls.__get_gen_render_info(auth, table_name)
|
||||
gen_table_schema = render_info[3]
|
||||
skipped = 0
|
||||
for template in render_info[0]:
|
||||
try:
|
||||
render_content = await env.get_template(template).render_async(**render_info[2])
|
||||
gen_path = cls.__get_gen_path(render_info[3], template)
|
||||
gen_path = cls.__get_gen_path(gen_table_schema, template)
|
||||
if gen_path:
|
||||
# 只允许写入到项目根目录及其子目录
|
||||
project_root = os.path.realpath(str(settings.BASE_DIR.parent))
|
||||
target_path = os.path.realpath(gen_path)
|
||||
if not target_path.startswith(project_root):
|
||||
raise CustomException(msg='生成路径不允许,请选择项目目录内路径')
|
||||
|
||||
os.makedirs(os.path.dirname(gen_path), exist_ok=True)
|
||||
|
||||
# 覆盖控制:存在且不允许覆盖则跳过
|
||||
if os.path.exists(gen_path) and not settings.allow_overwrite:
|
||||
skipped += 1
|
||||
continue
|
||||
|
||||
with open(gen_path, 'w', encoding='utf-8') as f:
|
||||
f.write(render_content)
|
||||
except Exception as e:
|
||||
raise CustomException(msg=f'渲染模板失败,表名:{render_info[3].table_name},详细错误信息:{str(e)}')
|
||||
raise CustomException(msg=f'渲染模板失败,表名:{gen_table_schema.table_name},详细错误信息:{str(e)}')
|
||||
|
||||
return SuccessResponse(msg='生成代码成功')
|
||||
msg = '生成代码成功'
|
||||
if skipped:
|
||||
msg += f'(已跳过 {skipped} 个已存在文件)'
|
||||
return SuccessResponse(msg=msg)
|
||||
|
||||
@classmethod
|
||||
async def batch_gen_code_service(cls, auth: AuthSchema, table_names: List[str]) -> bytes:
|
||||
@@ -682,13 +699,14 @@ class GenTableService:
|
||||
- Optional[str]: 生成的文件路径,若失败则返回None。
|
||||
"""
|
||||
try:
|
||||
gen_path = gen_table.gen_path or ""
|
||||
gen_path = (gen_table.gen_path or '').strip()
|
||||
file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
|
||||
# 修复:检查文件名是否为空
|
||||
if not file_name:
|
||||
return None
|
||||
if gen_path == '/':
|
||||
return os.path.join(os.getcwd(), GEN_PATH, file_name)
|
||||
# 默认写入到项目根目录(backend的上一级)
|
||||
project_root = str(settings.BASE_DIR.parent)
|
||||
if gen_path in ['', '/']:
|
||||
return os.path.join(project_root, file_name)
|
||||
else:
|
||||
return os.path.join(gen_path, file_name)
|
||||
except Exception:
|
||||
|
||||
@@ -248,33 +248,35 @@ class Jinja2TemplateUtil:
|
||||
# 处理空值情况
|
||||
category = tpl_category or GenConstant.TPL_CRUD
|
||||
templates = [
|
||||
# Python相关模板
|
||||
'python/controller.py.j2',
|
||||
'python/service.py.j2',
|
||||
'python/crud.py.j2',
|
||||
'python/schema.py.j2',
|
||||
'python/param.py.j2',
|
||||
'python/model.py.j2',
|
||||
# Vue相关模板
|
||||
f'{use_web_type}/api.ts.j2',
|
||||
# SQL脚本模板
|
||||
'sql/sql.sql.j2',
|
||||
# Python相关模板(调整为实际目录)
|
||||
'backend/app/v1/module_demo/python/controller.py.j2',
|
||||
'backend/app/v1/module_demo/python/service.py.j2',
|
||||
'backend/app/v1/module_demo/python/crud.py.j2',
|
||||
'backend/app/v1/module_demo/python/schema.py.j2',
|
||||
'backend/app/v1/module_demo/python/param.py.j2',
|
||||
'backend/app/v1/module_demo/python/model.py.j2',
|
||||
'backend/app/v1/module_demo/python/__init__.py.j2',
|
||||
|
||||
# Vue相关模板(API)
|
||||
'frontend/src/api/api.ts.j2',
|
||||
# SQL脚本模板(调整为实际目录)
|
||||
'backend/sql/sql.sql.j2',
|
||||
]
|
||||
if category == GenConstant.TPL_CRUD:
|
||||
templates.append(f'{use_web_type}/index.vue.j2')
|
||||
templates.append(f'frontend/src/views/module_demo/{use_web_type}/index.vue.j2')
|
||||
elif category == GenConstant.TPL_TREE:
|
||||
templates.append(f'{use_web_type}/index-tree.vue.j2')
|
||||
templates.append(f'frontend/src/views/module_demo/{use_web_type}/index-tree.vue.j2')
|
||||
elif category == GenConstant.TPL_SUB:
|
||||
templates.append(f'{use_web_type}/index.vue.j2')
|
||||
templates.append(f'frontend/src/views/module_demo/{use_web_type}/index.vue.j2')
|
||||
return templates
|
||||
|
||||
@classmethod
|
||||
def get_file_name(cls, template: List[str], gen_table: GenTableOutSchema):
|
||||
def get_file_name(cls, template: str, gen_table: GenTableOutSchema):
|
||||
"""
|
||||
根据模板生成文件名。
|
||||
|
||||
参数:
|
||||
- template (List[str]): 模板列表。
|
||||
- template (str): 模板路径字符串。
|
||||
- gen_table (GenTableOutSchema): 生成表的配置信息。
|
||||
|
||||
返回:
|
||||
@@ -299,7 +301,9 @@ class Jinja2TemplateUtil:
|
||||
return f'{python_path}/app/api/v1/{module_name}/{business_name}/param.py'
|
||||
elif 'schema.py.j2' in template:
|
||||
return f'{python_path}/app/api/v1/{module_name}/{business_name}/schema.py'
|
||||
elif 'sql.j2' in template:
|
||||
elif '__init__.py.j2' in template:
|
||||
return f'{vue_path}/src/views/{module_name}/{business_name}/__init__.py'
|
||||
elif 'sql.sql.j2' in template:
|
||||
return f'{cls.BACKEND_PROJECT_PATH}/sql/{module_name}/{business_name}_menu.sql'
|
||||
elif 'api.ts.j2' in template:
|
||||
return f'{vue_path}/src/api/{module_name}/{business_name}.ts'
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
+8
-4
@@ -6,7 +6,6 @@ from app.common.response import SuccessResponse, StreamResponse
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.common.request import PaginationService
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.utils.common_util import bytes2file_response
|
||||
from app.core.logger import logger
|
||||
@@ -34,9 +33,14 @@ async def get_obj_list_controller(
|
||||
search: {{ table_name|snake_to_pascal_case }}QueryParam = Depends(),
|
||||
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
|
||||
) -> JSONResponse:
|
||||
"""查询{{ function_name }}列表接口"""
|
||||
result_dict_list = await {{ table_name|snake_to_pascal_case }}Service.list_service(auth=auth, search=search, order_by=page.order_by)
|
||||
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size)
|
||||
"""查询{{ function_name }}列表接口(数据库分页)"""
|
||||
result_dict = await {{ table_name|snake_to_pascal_case }}Service.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no if page.page_no is not None else 1,
|
||||
page_size=page.page_size if page.page_size is not None else 10,
|
||||
search=search,
|
||||
order_by=page.order_by
|
||||
)
|
||||
logger.info("查询{{ function_name }}列表成功")
|
||||
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
|
||||
|
||||
+20
-7
@@ -1,11 +1,11 @@
|
||||
# -*- coding:utf-8 -*-
|
||||
|
||||
from typing import Dict, List, Optional, Sequence
|
||||
from typing import Dict, List, Optional, Sequence, Union, Any
|
||||
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from .model import {{ table_name|snake_to_pascal_case }}Model
|
||||
from .schema import {{ table_name|snake_to_pascal_case }}CreateSchema, {{ table_name|snake_to_pascal_case }}UpdateSchema
|
||||
from .schema import {{ table_name|snake_to_pascal_case }}CreateSchema, {{ table_name|snake_to_pascal_case }}UpdateSchema, {{ table_name|snake_to_pascal_case }}OutSchema
|
||||
|
||||
|
||||
class {{ table_name|snake_to_pascal_case }}CRUD(CRUDBase[{{ table_name|snake_to_pascal_case }}Model, {{ table_name|snake_to_pascal_case }}CreateSchema, {{ table_name|snake_to_pascal_case }}UpdateSchema]):
|
||||
@@ -15,13 +15,13 @@ class {{ table_name|snake_to_pascal_case }}CRUD(CRUDBase[{{ table_name|snake_to_
|
||||
"""初始化CRUD"""
|
||||
super().__init__(model={{ table_name|snake_to_pascal_case }}Model, auth=auth)
|
||||
|
||||
async def get_by_id_crud(self, id: int) -> Optional[{{ table_name|snake_to_pascal_case }}Model]:
|
||||
async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[{{ table_name|snake_to_pascal_case }}Model]:
|
||||
"""详情"""
|
||||
return await self.get(id=id)
|
||||
return await self.get(id=id, preload=preload)
|
||||
|
||||
async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[{{ table_name|snake_to_pascal_case }}Model]:
|
||||
async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[{{ table_name|snake_to_pascal_case }}Model]:
|
||||
"""列表查询"""
|
||||
return await self.list(search=search, order_by=order_by)
|
||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||
|
||||
async def create_crud(self, data: {{ table_name|snake_to_pascal_case }}CreateSchema) -> Optional[{{ table_name|snake_to_pascal_case }}Model]:
|
||||
"""创建"""
|
||||
@@ -37,4 +37,17 @@ class {{ table_name|snake_to_pascal_case }}CRUD(CRUDBase[{{ table_name|snake_to_
|
||||
|
||||
async def set_available_crud(self, ids: List[int], status: bool) -> None:
|
||||
"""批量设置可用状态"""
|
||||
return await self.set(ids=ids, status=status)
|
||||
return await self.set(ids=ids, status=status)
|
||||
|
||||
async def page_crud(self, offset: int, limit: int, order_by: Optional[List[Dict[str, str]]] = None, search: Optional[Dict] = None, preload: Optional[List[Union[str, Any]]] = None) -> Dict:
|
||||
"""分页查询"""
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
search_dict = search or {}
|
||||
return await self.page(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
order_by=order_by_list,
|
||||
search=search_dict,
|
||||
out_schema={{ table_name|snake_to_pascal_case }}OutSchema,
|
||||
preload=preload
|
||||
)
|
||||
+3
-2
@@ -2,8 +2,8 @@
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from sqlalchemy import String, Integer, Text, DateTime
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy import String, Integer, Text, DateTime, Boolean, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import CreatorMixin
|
||||
|
||||
@@ -15,6 +15,7 @@ class {{ table_name|snake_to_pascal_case }}Model(CreatorMixin):
|
||||
|
||||
__tablename__ = '{{ table_name }}'
|
||||
__table_args__ = {'comment': '{{ function_name }}'}
|
||||
__loader_options__ = ["creator"]
|
||||
|
||||
{% for column in columns %}
|
||||
{{ column.column_name }}: Mapped[Optional[{{ column.python_type }}]] = mapped_column({{ column.column_type|get_sqlalchemy_type }}, nullable=True, comment='{{ column.column_comment }}')
|
||||
+28
-1
@@ -34,6 +34,20 @@ class {{ table_name|snake_to_pascal_case }}Service:
|
||||
search_dict = search.__dict__ if search else None
|
||||
obj_list = await {{ table_name|snake_to_pascal_case }}CRUD(auth).list_crud(search=search_dict, order_by=order_by)
|
||||
return [{{ table_name|snake_to_pascal_case }}OutSchema.model_validate(obj).model_dump() for obj in obj_list]
|
||||
|
||||
@classmethod
|
||||
async def page_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: Optional[{{ table_name|snake_to_pascal_case }}QueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Dict:
|
||||
"""分页查询(数据库分页)"""
|
||||
search_dict = search.__dict__ if search else {}
|
||||
order_by_list = order_by or [{'id': 'asc'}]
|
||||
offset = (page_no - 1) * page_size
|
||||
result = await {{ table_name|snake_to_pascal_case }}CRUD(auth).page_crud(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by_list,
|
||||
search=search_dict
|
||||
)
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: {{ table_name|snake_to_pascal_case }}CreateSchema) -> Dict:
|
||||
@@ -80,6 +94,17 @@ class {{ table_name|snake_to_pascal_case }}Service:
|
||||
}
|
||||
|
||||
data = obj_list.copy()
|
||||
for item in data:
|
||||
# 状态转换
|
||||
if 'status' in item:
|
||||
item['status'] = '正常' if item.get('status') else '停用'
|
||||
# 创建者转换
|
||||
creator_info = item.get('creator')
|
||||
if isinstance(creator_info, dict):
|
||||
item['creator'] = creator_info.get('name', '未知')
|
||||
elif creator_info is None:
|
||||
item['creator'] = '未知'
|
||||
|
||||
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||
|
||||
@classmethod
|
||||
@@ -117,7 +142,9 @@ class {{ table_name|snake_to_pascal_case }}Service:
|
||||
"{{ column.column_name }}": row['{{ column.column_name }}'],
|
||||
{% endfor %}
|
||||
}
|
||||
await {{ table_name|snake_to_pascal_case }}CRUD(auth).create(data=data)
|
||||
# 使用CreateSchema做校验后入库
|
||||
create_schema = {{ table_name|snake_to_pascal_case }}CreateSchema.model_validate(data)
|
||||
await {{ table_name|snake_to_pascal_case }}CRUD(auth).create_crud(data=create_schema)
|
||||
success_count += 1
|
||||
except Exception as e:
|
||||
error_msgs.append(f"第{count}行: {str(e)}")
|
||||
@@ -1,6 +1,6 @@
|
||||
import request from "@/utils/request";
|
||||
|
||||
const API_PATH = "/{{ module_name }}/{{ business_name }}";
|
||||
const API_PATH = "/{{ module_name }}/{{ business_name|lower }}";
|
||||
|
||||
// 参考 demo.ts 的风格,提供标准的 CRUD 与导入/导出 API(TypeScript)
|
||||
const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = {
|
||||
@@ -60,7 +60,7 @@ const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = {
|
||||
|
||||
// 下载导入模板
|
||||
downloadTemplate() {
|
||||
return request<ApiResponse>({
|
||||
return request<Blob>({
|
||||
url: `${API_PATH}/download/template`,
|
||||
method: "post",
|
||||
responseType: "blob",
|
||||
@@ -68,7 +68,7 @@ const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = {
|
||||
},
|
||||
|
||||
// 导入
|
||||
import(data: any) {
|
||||
import(data: FormData) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/import`,
|
||||
method: "post",
|
||||
@@ -76,6 +76,14 @@ const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
},
|
||||
// 批量启用/停用
|
||||
batchAvailable(body: { ids: number[]; status: boolean }) {
|
||||
return request<ApiResponse>({
|
||||
url: `${API_PATH}/available/setting`,
|
||||
method: "patch",
|
||||
data: body,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export default {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API;
|
||||
@@ -86,13 +94,13 @@ export default {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API;
|
||||
|
||||
export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}PageQuery extends PageQuery {
|
||||
{% for column in columns %}
|
||||
{% if column.is_query == "1" and column.query_type != "BETWEEN" %}
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% if column.is_query == "1" and column.query_type != "BETWEEN" %}
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
// 时间范围查询(按示例统一字段名,如需按字段拆分可在页面层处理)
|
||||
start_time?: string;
|
||||
@@ -101,11 +109,11 @@ export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Pag
|
||||
|
||||
export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Table {
|
||||
{% for column in columns %}
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{% endfor %}
|
||||
creator?: creatorType;
|
||||
}
|
||||
@@ -113,12 +121,12 @@ export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Tab
|
||||
export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Form {
|
||||
id?: number;
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{{ column.python_field }}?: {{
|
||||
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
|
||||
else 'number' if column.is_pk == '1'
|
||||
else 'string'
|
||||
}};
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
+283
-93
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="app-container">
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-container">
|
||||
<el-form :model="queryFormData" ref="queryFormRef" :inline="true" label-suffix=":">
|
||||
<div v-show="visible" class="search-container">
|
||||
<el-form ref="queryFormRef" :model="queryFormData" label-suffix=":" :inline="true" @submit.prevent="handleQuery">
|
||||
{% for column in columns %}
|
||||
{% if column.is_query == "1" %}
|
||||
{% set dict_type = column.dict_type %}
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
{% if column.html_type == "input" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
<el-input v-model="queryFormData.{{ column.python_field }}" placeholder="请输入{{ comment }}" clearable @keyup.enter="handleQuery" />
|
||||
<el-input v-model="queryFormData.{{ column.python_field }}" placeholder="请输入{{ comment }}" clearable />
|
||||
</el-form-item>
|
||||
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
@@ -30,16 +30,32 @@
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
|
||||
<el-date-picker v-model="queryFormData.{{ column.python_field }}" type="date" value-format="YYYY-MM-DD" clearable placeholder="请选择{{ comment }}" />
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "datetime" and column.query_type == "BETWEEN" %}
|
||||
<el-form-item label="{{ comment }}" style="width: 308px">
|
||||
<el-date-picker v-model="dateRangeMap.{{ column.python_field }}" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" />
|
||||
</el-form-item>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<el-form-item class="search-buttons">
|
||||
<el-button type="primary" icon="search" @click="handleQuery">查询</el-button>
|
||||
<el-button icon="refresh" @click="handleResetQuery">重置</el-button>
|
||||
<!-- 可选:创建人选择与统一日期范围(展开后显示) -->
|
||||
<el-form-item v-if="isExpand" prop="creator" label="创建人">
|
||||
<UserTableSelect v-model="queryFormData.creator" @confirm-click="handleConfirm" @clear-click="handleQuery" />
|
||||
</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>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:query']" type="primary" icon="search" @click="handleQuery">查询</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name|lower }}:query']" icon="refresh" @click="handleResetQuery">重置</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>
|
||||
</div>
|
||||
@@ -59,23 +75,61 @@
|
||||
|
||||
<!-- 功能区域 -->
|
||||
<div class="data-table__toolbar">
|
||||
<div class="data-table__toolbar--actions">
|
||||
<div class="data-table__toolbar--left">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-button type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:create']" type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:delete']" type="danger" icon="delete" :disabled="selectIds.length === 0" @click="handleDelete(selectIds)">批量删除</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-dropdown v-hasPerm="['{{ module_name }}:{{ business_name }}:batch']" trigger="click">
|
||||
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el按钮>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
|
||||
<el-dropdown-item icon="CircleClose" @click="handleMoreClick(false)">批量停用</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
<div class="data-table__toolbar--tools">
|
||||
<div class="data-table__toolbar--right">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button type="primary" icon="refresh" circle @click="handleRefresh" />
|
||||
<el-tooltip content="导入">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:import']" type="success" icon="upload" circle @click="handleOpenImportDialog" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="导出">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:export']" type="warning" icon="download" circle @click="handleOpenExportsModal" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="搜索显示/隐藏">
|
||||
<el-button v-hasPerm="['*:*:*']" type="info" icon="search" circle @click="visible = !visible" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-tooltip content="刷新">
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:refresh']" type="primary" icon="refresh" circle @click="handleRefresh" />
|
||||
</el-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-popover placement="bottom" trigger="click">
|
||||
<template #reference>
|
||||
<el-button type="danger" icon="operation" circle></el-button>
|
||||
</template>
|
||||
<el-scrollbar max-height="350px">
|
||||
<template v-for="column in tableColumns" :key="column.prop">
|
||||
<el-checkbox v-if="column.prop" v-model="column.show" :label="column.label" />
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-popover>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
@@ -95,10 +149,10 @@
|
||||
<template #empty>
|
||||
<el-empty :image-size="80" description="暂无数据" />
|
||||
</template>
|
||||
<el-table-column type="selection" min-width="55" align="center" />
|
||||
<el-table-column fixed label="序号" min-width="60">
|
||||
<el-table-column v-if="tableColumns.find((col) => col.prop === 'selection')?.show" type="selection" min-width="55" align="center" />
|
||||
<el-table-column v-if="tableColumns.find((col) => col.prop === 'index')?.show" fixed label="序号" min-width="60">
|
||||
<template #default="scope">
|
||||
{{ '{' }}{{ '{' }} (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 {{ '}' }}{{ '}' }}
|
||||
{{ '{{' }} (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 {{ '}}' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
{% for column in columns %}
|
||||
@@ -107,20 +161,24 @@
|
||||
{% set parentheseIndex = column_comment.find("(") %}
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
{% if column.is_list == "1" %}
|
||||
<el-table-column label="{{ comment }}" prop="{{ python_field }}" min-width="140">
|
||||
{% if column.html_type == "datetime" %}
|
||||
<el-table-column v-if="tableColumns.find((col) => col.prop === '{{ python_field }}')?.show" label="{{ comment }}" prop="{{ python_field }}" min-width="140">
|
||||
{% if python_field == "status" %}
|
||||
<template #default="scope">
|
||||
<span>{{ '{' }}{{ '{' }} scope.row.{{ python_field }} {{ '}' }}{{ '}' }}</span>
|
||||
<el-tag :type="scope.row.status ? 'success' : 'info'">{{ '{{' }} scope.row.status ? '启用' : '停用' {{ '}}' }}</el-tag>
|
||||
</template>
|
||||
{% elif python_field == "creator" %}
|
||||
<template #default="scope">
|
||||
<el-tag>{{ '{{' }} scope.row.creator?.name {{ '}}' }}</el-tag>
|
||||
</template>
|
||||
{% endif %}
|
||||
</el-table-column>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
<el-table-column fixed="right" label="操作" align="center" min-width="200">
|
||||
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right" label="操作" align="center" min-width="180">
|
||||
<template #default="scope">
|
||||
<el-button type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:detail']" type="info" size="small" link icon="document" @click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:update']" type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
|
||||
<el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:delete']" type="danger" size="small" link icon="delete" @click="handleDelete([scope.row.id])">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -158,7 +216,14 @@
|
||||
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
|
||||
{% set required = 'true' if column.is_required == '1' else 'false' %}
|
||||
|
||||
{% if column.html_type == "input" %}
|
||||
{% if column.python_field == "status" %}
|
||||
<el-form-item label="状态" prop="status" :required="true">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :value="true">启用</el-radio>
|
||||
<el-radio :value="false">停用</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
{% elif column.html_type == "input" %}
|
||||
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
|
||||
<el-input v-model="formData.{{ column.python_field }}" placeholder="请输入{{ comment }}" />
|
||||
</el-form-item>
|
||||
@@ -203,72 +268,177 @@
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="handleCloseDialog">取消</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
<el-button v-if="dialogVisible.type !== 'detail'" v-hasPerm="['{{ module_name }}:{{ business_name|lower }}:submit']" type="primary" @click="handleSubmit">确定</el-button>
|
||||
<el-button v-else v-hasPerm="['{{ module_name }}:{{ business_name|lower }}:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 导入弹窗 -->
|
||||
<ImportModal
|
||||
v-model="importDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
@upload="handleUpload"
|
||||
/>
|
||||
|
||||
<!-- 导出弹窗 -->
|
||||
<ExportModal
|
||||
v-model="exportsDialogVisible"
|
||||
:content-config="curdContentConfig"
|
||||
:query-params="queryFormData"
|
||||
:page-data="pageTableData"
|
||||
:selection-data="selectionRows"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ResultEnum } from '@/enums/api/result.enum'
|
||||
import { QuestionFilled, ArrowUp, ArrowDown, Check, CircleClose } from '@element-plus/icons-vue'
|
||||
import { formatToDateTime } from '@/utils/dateUtil'
|
||||
import {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API from '@/api/{{ module_name }}/{{ business_name }}'
|
||||
import { useDictStore } from '@/store/index'
|
||||
import SingleImageUpload from '@/components/Upload/SingleImageUpload.vue'
|
||||
import ImportModal from '@/components/CURD/ImportModal.vue'
|
||||
import ExportModal from '@/components/CURD/ExportModal.vue'
|
||||
import DatePicker from '@/components/DatePicker/index.vue'
|
||||
import type { IContentConfig } from '@/components/CURD/types'
|
||||
|
||||
const visible = ref(true)
|
||||
const isExpand = ref(false)
|
||||
const isExpandable = ref(true)
|
||||
|
||||
const queryFormRef = ref()
|
||||
const dataFormRef = ref()
|
||||
const total = ref(0)
|
||||
const selectIds = ref([])
|
||||
const selectIds = ref<number[]>([])
|
||||
const selectionRows = ref<any[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
// 字典仓库与需要加载的字典类型
|
||||
const dictStore = useDictStore()
|
||||
const dictTypes = [
|
||||
{% for column in columns %}
|
||||
{% if column.dict_type %}
|
||||
'{{ column.dict_type }}',
|
||||
{% endif %}
|
||||
{% if column.dict_type %}
|
||||
'{{ column.dict_type }}',
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
// 表格数据
|
||||
const pageTableData = ref([])
|
||||
const pageTableData = ref<any[]>([])
|
||||
|
||||
// 表格列配置(根据列生成,可显隐)
|
||||
const tableColumns = ref([
|
||||
{ prop: 'selection', label: '选择框', show: true },
|
||||
{ prop: 'index', label: '序号', show: true },
|
||||
{% for column in columns %}
|
||||
{% if column.is_list == "1" %}
|
||||
{ prop: '{{ column.python_field }}', label: '{{ column.column_comment or column.python_field }}', show: true },
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{ prop: 'operation', label: '操作', show: true }
|
||||
])
|
||||
|
||||
// 导出列(不含选择/序号/操作)
|
||||
const exportColumns = [
|
||||
{% for column in columns %}
|
||||
{% if column.is_list == "1" %}
|
||||
{ prop: '{{ column.python_field }}', label: '{{ column.column_comment or column.python_field }}' },
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
]
|
||||
|
||||
// 导入/导出配置
|
||||
const curdContentConfig = {
|
||||
permPrefix: '{{ module_name }}:{{ business_name }}',
|
||||
cols: exportColumns as any,
|
||||
importTemplate: () => {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.downloadTemplate(),
|
||||
exportsAction: async (params: any) => {
|
||||
const query: any = { ...params };
|
||||
if (typeof query.status === 'string') {
|
||||
query.status = query.status === 'true'
|
||||
}
|
||||
query.page_no = 1
|
||||
query.page_size = 9999
|
||||
const all: any[] = []
|
||||
while (true) {
|
||||
const res = await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.list(query)
|
||||
const items = res.data?.data?.items || []
|
||||
const total = res.data?.data?.total || 0
|
||||
all.push(...items)
|
||||
if (all.length >= total || items.length === 0) break
|
||||
query.page_no += 1
|
||||
}
|
||||
return all
|
||||
},
|
||||
} as unknown as IContentConfig
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: '',
|
||||
visible: false,
|
||||
type: 'create', // 'create' | 'update' | 'detail'
|
||||
})
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive({
|
||||
id: undefined,
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{{ column.python_field }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
})
|
||||
|
||||
// 表单验证规则(必填项按 is_required 生成)
|
||||
const rules = reactive({
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{% set required = 'true' if column.is_required == '1' else 'false' %}
|
||||
{{ column.python_field }}: [
|
||||
{ required: {{ required }}, message: '请输入{{ column.column_comment or column.python_field }}', trigger: 'blur' },
|
||||
],
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
})
|
||||
|
||||
// 详情表单
|
||||
const detailFormData = ref({})
|
||||
const detailFormData = ref<any>({})
|
||||
|
||||
// 日期范围映射(支持多个 BETWEEN 字段)
|
||||
const dateRangeMap = reactive({})
|
||||
// 统一日期范围
|
||||
const dateRange = ref<[Date, Date] | []>([])
|
||||
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 queryFormData = reactive({
|
||||
page_no: 1,
|
||||
page_size: 10,
|
||||
{% for column in columns %}
|
||||
{% if column.is_query == "1" and column.query_type != "BETWEEN" %}
|
||||
{{ column.python_field }}: undefined,
|
||||
{% endif %}
|
||||
{% if column.is_query == "1" and column.query_type != "BETWEEN" %}
|
||||
{{ column.python_field }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
start_time: undefined,
|
||||
end_time: undefined,
|
||||
creator: undefined,
|
||||
})
|
||||
|
||||
// 加载表格数据
|
||||
async function loadingData() {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
...queryFormData,
|
||||
{% for column in columns %}
|
||||
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %}
|
||||
{{ column.python_field }}_start: dateRangeMap.{{ column.python_field }} && dateRangeMap.{{ column.python_field }}.length > 0 ? dateRangeMap.{{ column.python_field }}[0] : undefined,
|
||||
{{ column.python_field }}_end: dateRangeMap.{{ column.python_field }} && dateRangeMap.{{ column.python_field }}.length > 0 ? dateRangeMap.{{ column.python_field }}[1] : undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
const response = await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.list(params)
|
||||
// 参考 demo.ts 返回结构
|
||||
const response = await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.list(queryFormData)
|
||||
pageTableData.value = response.data.data.items
|
||||
total.value = response.data.data.total
|
||||
} catch (error) {
|
||||
@@ -284,22 +454,25 @@ async function handleQuery() {
|
||||
loadingData()
|
||||
}
|
||||
|
||||
// 选择创建人后触发查询
|
||||
function handleConfirm() {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
// 重置查询
|
||||
async function handleResetQuery() {
|
||||
queryFormRef.value.resetFields()
|
||||
queryFormData.page_no = 1
|
||||
// 重置所有日期范围选择器
|
||||
{% for column in columns %}
|
||||
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %}
|
||||
dateRangeMap.{{ column.python_field }} = []
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
dateRange.value = []
|
||||
queryFormData.start_time = undefined
|
||||
queryFormData.end_time = undefined
|
||||
loadingData()
|
||||
}
|
||||
|
||||
// 行复选框选中项变化
|
||||
function handleSelectionChange(selection) {
|
||||
selectIds.value = selection.map((item) => item.id)
|
||||
function handleSelectionChange(selection: any[]) {
|
||||
selectIds.value = selection.map((item: any) => item.id)
|
||||
selectionRows.value = selection
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
@@ -308,7 +481,7 @@ function handleCloseDialog() {
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
async function handleOpenDialog(type, id) {
|
||||
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
|
||||
dialogVisible.type = type
|
||||
if (id) {
|
||||
const response = await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.detail(id)
|
||||
@@ -328,23 +501,20 @@ async function handleOpenDialog(type, id) {
|
||||
|
||||
// 提交表单
|
||||
async function handleSubmit() {
|
||||
dataFormRef.value.validate(async (valid) => {
|
||||
dataFormRef.value.validate(async (valid: any) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
try {
|
||||
const id = formData.id
|
||||
if (id) {
|
||||
await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.update(id, { id, ...formData })
|
||||
ElMessage.success('修改成功')
|
||||
} else {
|
||||
await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.create(formData)
|
||||
ElMessage.success('新增成功')
|
||||
}
|
||||
dialogVisible.visible = false
|
||||
handleResetQuery()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('操作失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -353,7 +523,7 @@ async function handleSubmit() {
|
||||
}
|
||||
|
||||
// 删除、批量删除
|
||||
async function handleDelete(ids) {
|
||||
async function handleDelete(ids: number[]) {
|
||||
ElMessageBox.confirm('确认删除该项数据?', '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
@@ -363,11 +533,9 @@ async function handleDelete(ids) {
|
||||
try {
|
||||
loading.value = true
|
||||
await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.delete(ids)
|
||||
ElMessage.success('删除成功')
|
||||
handleResetQuery()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
ElMessage.error('删除失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
@@ -377,34 +545,56 @@ async function handleDelete(ids) {
|
||||
})
|
||||
}
|
||||
|
||||
// 弹窗状态
|
||||
const dialogVisible = reactive({
|
||||
title: '',
|
||||
visible: false,
|
||||
type: 'create', // 'create' | 'update' | 'detail'
|
||||
})
|
||||
// 批量启用/停用
|
||||
async function handleMoreClick(status: boolean) {
|
||||
if (selectIds.value.length) {
|
||||
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, '警告', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}).then(async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.batchAvailable({ ids: selectIds.value, status })
|
||||
handleResetQuery()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
ElMessageBox.close()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑表单
|
||||
const formData = reactive({
|
||||
id: undefined,
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{{ column.python_field }}: undefined,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
})
|
||||
// 导入弹窗显示状态
|
||||
const importDialogVisible = ref(false)
|
||||
// 导出弹窗显示状态
|
||||
const exportsDialogVisible = ref(false)
|
||||
|
||||
// 表单验证规则(必填项按 is_required 生成)
|
||||
const rules = reactive({
|
||||
{% for column in columns %}
|
||||
{% if column.is_insert == "1" or column.is_edit == "1" %}
|
||||
{% set required = 'true' if column.is_required == '1' else 'false' %}
|
||||
{{ column.python_field }}: [
|
||||
{ required: {{ required }}, message: '请输入{{ column.column_comment or column.python_field }}', trigger: 'blur' },
|
||||
],
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
})
|
||||
// 打开导入弹窗
|
||||
function handleOpenImportDialog() {
|
||||
importDialogVisible.value = true
|
||||
}
|
||||
// 打开导出弹窗
|
||||
function handleOpenExportsModal() {
|
||||
exportsDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 处理上传
|
||||
const handleUpload = async (formData: FormData) => {
|
||||
try {
|
||||
const response = await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.import(formData)
|
||||
if (response.data.code === ResultEnum.SUCCESS) {
|
||||
ElMessage.success(`${response.data.msg},${response.data.data}`)
|
||||
importDialogVisible.value = false
|
||||
await handleQuery()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 列表刷新
|
||||
async function handleRefresh() {
|
||||
Reference in New Issue
Block a user