Merge pull request #208 from 1014TaoTao/v2.0.0

V2.0.0
This commit is contained in:
fastapiadmin
2025-10-29 01:43:20 +08:00
committed by GitHub
54 changed files with 1226 additions and 306 deletions
@@ -35,11 +35,14 @@ class JobLogQueryParam:
def __init__( def __init__(
self, self,
job_id: Optional[int] = Query(None, description="定时任务ID"),
job_name: Optional[str] = Query(None, description="任务名称"), job_name: Optional[str] = Query(None, description="任务名称"),
status: Optional[bool] = Query(None, description="状态: 正常,失败"), status: Optional[bool] = Query(None, description="状态: 正常,失败"),
start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2025-01-01 00:00:00"), start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2025-01-01 00:00:00"),
end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2025-12-31 23:59:59"), end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2025-12-31 23:59:59"),
) -> None: ) -> None:
# 定时任务ID查询
self.job_id = job_id
# 模糊查询字段 # 模糊查询字段
self.job_name = ("like", job_name) self.job_name = ("like", job_name)
# 精确查询字段 # 精确查询字段
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from typing import Optional from typing import Optional
import re
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
from app.core.validator import DateTimeStr from app.core.validator import DateTimeStr, datetime_validator
class JobCreateSchema(BaseModel): class JobCreateSchema(BaseModel):
@@ -25,6 +26,52 @@ class JobCreateSchema(BaseModel):
description: Optional[str] = Field(default=None, max_length=255, description='描述') description: Optional[str] = Field(default=None, max_length=255, description='描述')
status: Optional[bool] = Field(default=False, description='任务状态:启动,停止') status: Optional[bool] = Field(default=False, description='任务状态:启动,停止')
@model_validator(mode='before')
@classmethod
def _normalize(cls, data):
"""前置归一化:字符串去空格、布尔/数字兼容转换。"""
if isinstance(data, dict):
for key in ('name', 'func', 'trigger', 'args', 'kwargs', 'jobstore', 'executor', 'trigger_args', 'start_date', 'end_date', 'description'):
val = data.get(key)
if isinstance(val, str):
data[key] = val.strip()
for bkey in ('coalesce', 'status'):
val = data.get(bkey)
if isinstance(val, str):
lowered = val.strip().lower()
if lowered in {'true', '1', 'y', 'yes'}:
data[bkey] = True
elif lowered in {'false', '0', 'n', 'no'}:
data[bkey] = False
elif isinstance(val, int):
data[bkey] = bool(val)
val = data.get('max_instances')
if isinstance(val, str) and val.strip().isdigit():
data['max_instances'] = int(val.strip())
return data
@field_validator('trigger')
@classmethod
def _validate_trigger(cls, v: str) -> str:
allowed = {'cron', 'interval', 'date'}
v = v.strip()
if v not in allowed:
raise ValueError('触发器必须为 cron/interval/date')
return v
@model_validator(mode='after')
def _validate_dates(self):
"""跨字段校验:结束时间不得早于开始时间。"""
if self.start_date and self.end_date:
try:
start = datetime_validator(self.start_date)
end = datetime_validator(self.end_date)
except Exception:
raise ValueError('时间格式必须为 YYYY-MM-DD HH:MM:SS')
if end < start:
raise ValueError('结束时间不能早于开始时间')
return self
class JobUpdateSchema(JobCreateSchema): class JobUpdateSchema(JobCreateSchema):
"""定时任务更新模型""" """定时任务更新模型"""
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional from typing import Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
from urllib.parse import urlparse
class ApplicationCreateSchema(BaseModel): class ApplicationCreateSchema(BaseModel):
@@ -14,6 +15,62 @@ class ApplicationCreateSchema(BaseModel):
status: bool = Field(True, description="是否启用(True:启用 False:禁用)") status: bool = Field(True, description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述") description: Optional[str] = Field(default=None, max_length=255, description="描述")
@model_validator(mode="before")
@classmethod
def _normalize(cls, data):
"""模型级前置处理:去除首尾空格,空字符串转为 None(可选字段),并规范布尔。"""
if isinstance(data, dict):
for key in ("name", "access_url", "icon_url", "description"):
val = data.get(key)
if isinstance(val, str):
val = val.strip()
# 将可选字段的空字符串转换为 None
if key in ("icon_url", "description") and val == "":
val = None
data[key] = val
# 规范布尔字符串/数字为布尔值
status_val = data.get("status")
if isinstance(status_val, str):
lowered = status_val.strip().lower()
if lowered in {"true", "1", "y", "yes"}:
data["status"] = True
elif lowered in {"false", "0", "n", "no"}:
data["status"] = False
elif isinstance(status_val, int):
data["status"] = bool(status_val)
return data
@field_validator('name')
@classmethod
def _validate_name_length(cls, v: str) -> str:
if len(v) > 64:
raise ValueError('应用名称长度不能超过64字符')
return v
@field_validator('access_url')
@classmethod
def _validate_access_url(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError('访问地址不能为空')
parsed = urlparse(v)
if parsed.scheme not in ('http', 'https'):
raise ValueError('访问地址必须为 http/https URL')
return v
@field_validator('icon_url')
@classmethod
def _validate_icon_url(cls, v: Optional[str]) -> Optional[str]:
if v is None:
return v
v = v.strip()
if v == "":
return None
parsed = urlparse(v)
if parsed.scheme not in ('http', 'https'):
raise ValueError('应用图标URL必须为 http/https URL')
return v
class ApplicationUpdateSchema(ApplicationCreateSchema): class ApplicationUpdateSchema(ApplicationCreateSchema):
"""应用更新模型""" """应用更新模型"""
@@ -1,4 +1,4 @@
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
from typing import Optional from typing import Optional
@@ -11,6 +11,35 @@ class ImportFieldModel(BaseModel):
is_required: Optional[str] = Field(description='是否必传') is_required: Optional[str] = Field(description='是否必传')
selected: Optional[bool] = Field(description='是否勾选') selected: Optional[bool] = Field(description='是否勾选')
@model_validator(mode='before')
@classmethod
def _normalize(cls, data):
if isinstance(data, dict):
for key in ('base_column', 'excel_column', 'default_value'):
val = data.get(key)
if isinstance(val, str):
val = val.strip()
if val == '':
val = None
data[key] = val
# is_required 兼容转换
val = data.get('is_required')
if isinstance(val, str):
lowered = val.strip().lower()
if lowered in {'true', '1', 'y', 'yes'}:
data['is_required'] = True
elif lowered in {'false', '0', 'n', 'no'}:
data['is_required'] = False
return data
@model_validator(mode='after')
def _validate(self):
if self.selected and not (self.base_column and self.base_column.strip()):
raise ValueError('选中字段必须提供数据库字段名')
if self.is_required and not (self.excel_column and self.excel_column.strip()):
raise ValueError('必传字段必须提供excel字段名')
return self
class ImportModel(BaseModel): class ImportModel(BaseModel):
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True) model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
@@ -19,3 +48,29 @@ class ImportModel(BaseModel):
filed_info: Optional[list[ImportFieldModel]] = Field(description='字段关联表') filed_info: Optional[list[ImportFieldModel]] = Field(description='字段关联表')
file_name: Optional[str] = Field(description='文件名') file_name: Optional[str] = Field(description='文件名')
@model_validator(mode='before')
@classmethod
def _normalize(cls, data):
if isinstance(data, dict):
for key in ('table_name', 'sheet_name', 'file_name'):
val = data.get(key)
if isinstance(val, str):
val = val.strip()
if val == '':
val = None
data[key] = val
return data
@model_validator(mode='after')
def _validate(self):
# excel_column 不重复(忽略 None)
if self.filed_info:
seen = set()
for f in self.filed_info:
if f.excel_column:
key = f.excel_column.strip()
if key in seen:
raise ValueError('excel字段名存在重复')
seen.add(key)
return self
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional from typing import Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
@@ -12,6 +12,65 @@ class DemoCreateSchema(BaseModel):
status: bool = Field(True, description="是否启用(True:启用 False:禁用)") status: bool = Field(True, description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述") description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator('name')
@classmethod
def _validate_name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError('名称不能为空')
return v
@model_validator(mode='before')
@classmethod
def _normalize(cls, data):
if isinstance(data, dict):
for key in ('name', 'description'):
val = data.get(key)
if isinstance(val, str):
val = val.strip()
if key == 'description' and val == '':
val = None
data[key] = val
# status兼容
val = data.get('status')
if isinstance(val, str):
lowered = val.strip().lower()
if lowered in {'true', '1', 'y', 'yes'}:
data['status'] = True
elif lowered in {'false', '0', 'n', 'no'}:
data['status'] = False
elif isinstance(val, int):
data['status'] = bool(val)
return data
@model_validator(mode='wrap')
@classmethod
def _wrap(cls, data, handler):
# 进一步处理:压缩名称/描述中的多余空白,并支持更多 status 同义词
if isinstance(data, dict):
name = data.get('name')
if isinstance(name, str):
data['name'] = ' '.join(name.split())
status_val = data.get('status')
if isinstance(status_val, str):
lowered = status_val.strip().lower()
if lowered in {'enabled', 'enable', 'on'}:
data['status'] = True
elif lowered in {'disabled', 'disable', 'off'}:
data['status'] = False
desc = data.get('description')
if isinstance(desc, str):
data['description'] = ' '.join(desc.split())
result = handler(data)
return result
@model_validator(mode='after')
def _check_disabled_requires_description(self):
# 业务示例:禁用时必须填写描述
if self.status is False and (self.description is None or (isinstance(self.description, str) and self.description.strip() == '')):
raise ValueError('禁用时必须填写描述')
return self
class DemoUpdateSchema(DemoCreateSchema): class DemoUpdateSchema(DemoCreateSchema):
"""更新模型""" """更新模型"""
@@ -222,10 +222,6 @@ async def gen_code_local_controller(
返回: 返回:
- JSONResponse: 包含生成结果的JSON响应 - 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) result = await GenTableService.generate_code_service(auth, table_name)
logger.info('生成代码到指定路径成功') logger.info('生成代码到指定路径成功')
return SuccessResponse(msg="生成代码到指定路径成功", data=result) return SuccessResponse(msg="生成代码到指定路径成功", data=result)
@@ -408,18 +408,35 @@ class GenTableService:
""" """
env = Jinja2TemplateInitializerUtil.init_jinja2() env = Jinja2TemplateInitializerUtil.init_jinja2()
render_info = await cls.__get_gen_render_info(auth, table_name) 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]: for template in render_info[0]:
try: try:
render_content = await env.get_template(template).render_async(**render_info[2]) 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: 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) 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: with open(gen_path, 'w', encoding='utf-8') as f:
f.write(render_content) f.write(render_content)
except Exception as e: 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 @classmethod
async def batch_gen_code_service(cls, auth: AuthSchema, table_names: List[str]) -> bytes: async def batch_gen_code_service(cls, auth: AuthSchema, table_names: List[str]) -> bytes:
@@ -682,13 +699,14 @@ class GenTableService:
- Optional[str]: 生成的文件路径,若失败则返回None。 - Optional[str]: 生成的文件路径,若失败则返回None。
""" """
try: 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) file_name = Jinja2TemplateUtil.get_file_name(template, gen_table)
# 修复:检查文件名是否为空
if not file_name: if not file_name:
return None return None
if gen_path == '/': # 默认写入到项目根目录(backend的上一级)
return os.path.join(os.getcwd(), GEN_PATH, file_name) project_root = str(settings.BASE_DIR.parent)
if gen_path in ['', '/']:
return os.path.join(project_root, file_name)
else: else:
return os.path.join(gen_path, file_name) return os.path.join(gen_path, file_name)
except Exception: except Exception:
@@ -2,8 +2,9 @@
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from datetime import datetime from datetime import datetime
from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse
class ResourceItemSchema(BaseModel): class ResourceItemSchema(BaseModel):
@@ -20,6 +21,33 @@ class ResourceItemSchema(BaseModel):
modified_time: Optional[datetime] = Field(None, description="修改时间") modified_time: Optional[datetime] = Field(None, description="修改时间")
is_hidden: bool = Field(False, description="是否为隐藏文件") is_hidden: bool = Field(False, description="是否为隐藏文件")
@field_validator('file_url')
@classmethod
def _validate_file_url(cls, v: str) -> str:
v = v.strip()
parsed = urlparse(v)
if parsed.scheme not in ('http', 'https'):
raise ValueError('文件URL必须为 http/https')
return v
@field_validator('relative_path')
@classmethod
def _validate_relative_path(cls, v: str) -> str:
v = v.strip()
if '..' in v or v.startswith('\\'):
raise ValueError('相对路径包含不安全字符')
return v
@model_validator(mode='after')
def _validate_flags(self):
if self.is_file and self.is_dir:
raise ValueError('不能同时为文件和目录')
if not self.is_file and not self.is_dir:
raise ValueError('必须是文件或目录之一')
# 根据名称自动修正隐藏标记
self.is_hidden = self.name.startswith('.')
return self
class ResourceDirectorySchema(BaseModel): class ResourceDirectorySchema(BaseModel):
"""资源目录模型""" """资源目录模型"""
@@ -78,6 +106,14 @@ class ResourceRenameSchema(BaseModel):
raise ValueError("参数不能为空") raise ValueError("参数不能为空")
return value.strip() return value.strip()
@field_validator('new_name')
@classmethod
def _validate_new_name(cls, v: str) -> str:
v = v.strip()
if '..' in v or '/' in v or '\\' in v:
raise ValueError('新名称包含不安全字符')
return v
class ResourceCreateDirSchema(BaseModel): class ResourceCreateDirSchema(BaseModel):
"""创建目录模型""" """创建目录模型"""
@@ -5,6 +5,7 @@ import shutil
from datetime import datetime from datetime import datetime
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from pathlib import Path from pathlib import Path
from urllib.parse import urlparse
from fastapi import UploadFile from fastapi import UploadFile
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
@@ -61,6 +62,26 @@ class ResourceService:
if not path: if not path:
return resource_root return resource_root
# 支持前端传递的完整URL或以STATIC_URL/ROOT_PATH+STATIC_URL开头的URL路径,转换为相对资源路径
if isinstance(path, str):
static_prefix = settings.STATIC_URL.rstrip('/')
root_prefix = settings.ROOT_PATH.rstrip('/') if getattr(settings, 'ROOT_PATH', '') else ''
root_static_prefix = f"{root_prefix}{static_prefix}" if root_prefix else static_prefix
def strip_prefix(p: str) -> str:
if p.startswith(root_static_prefix):
return p[len(root_static_prefix):].lstrip('/')
if p.startswith(static_prefix):
return p[len(static_prefix):].lstrip('/')
return p
if path.startswith('http://') or path.startswith('https://'):
parsed = urlparse(path)
url_path = parsed.path or ''
path = strip_prefix(url_path)
else:
path = strip_prefix(path)
# 清理路径,移除危险字符 # 清理路径,移除危险字符
path = path.strip().replace('..', '').replace('//', '/') path = path.strip().replace('..', '').replace('//', '/')
@@ -78,7 +99,7 @@ class ResourceService:
raise CustomException(msg=f'访问路径不在允许范围内: {path}') raise CustomException(msg=f'访问路径不在允许范围内: {path}')
# 防止路径遍历攻击 # 防止路径遍历攻击
if '..' in safe_path or safe_path.count('/') > cls.MAX_PATH_DEPTH: # 限制最大目录深度 if '..' in safe_path or safe_path.count('/') > cls.MAX_PATH_DEPTH:
raise CustomException(msg=f'不安全的路径格式: {path}') raise CustomException(msg=f'不安全的路径格式: {path}')
return safe_path return safe_path
@@ -522,14 +543,14 @@ class ResourceService:
@classmethod @classmethod
async def download_file_service(cls, file_path: str, base_url: Optional[str] = None) -> str: async def download_file_service(cls, file_path: str, base_url: Optional[str] = None) -> str:
""" """
下载文件(返回文件路径) 下载文件(返回本地文件系统路径)
参数: 参数:
- file_path (str): 文件路径。 - file_path (str): 文件路径(可为相对路径、绝对路径或完整URL)。
- base_url (Optional[str]): 基础URL,用于生成完整URL。 - base_url (Optional[str]): 基础URL,用于生成完整URL(不再直接返回URL)。
返回: 返回:
- str: 文件访问URL。 - str: 本地文件系统路径。
""" """
try: try:
safe_path = cls._get_safe_path(file_path) safe_path = cls._get_safe_path(file_path)
@@ -540,10 +561,9 @@ class ResourceService:
if not os.path.isfile(safe_path): if not os.path.isfile(safe_path):
raise CustomException(msg='路径不是文件') raise CustomException(msg='路径不是文件')
# 生成HTTP URL路径而不是返回文件系统路径 # 返回本地文件路径给 FileResponse 使用
http_url = cls._generate_http_url(safe_path, base_url) logger.info(f"定位文件路径: {safe_path}")
logger.info(f"生成文件访问URL: {http_url}") return safe_path
return http_url
except CustomException: except CustomException:
raise raise
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel, ConfigDict, Field, field_validator from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
@@ -23,6 +23,45 @@ class DeptCreateSchema(BaseModel):
value = value.replace(" ", "") value = value.replace(" ", "")
return value return value
@field_validator('code')
@classmethod
def validate_code(cls, value: Optional[str]):
if value is None:
return value
v = value.strip()
if v == "":
return None
import re
if not re.match(r'^[A-Za-z][A-Za-z0-9_]*$', v):
raise ValueError("部门编码必须以字母开头,且仅包含字母/数字/下划线")
return v
@model_validator(mode='before')
@classmethod
def _normalize(cls, data):
if isinstance(data, dict):
for key in ('code', 'description'):
val = data.get(key)
if isinstance(val, str):
val = val.strip()
if key == 'description' and val == '':
val = None
data[key] = val
pid = data.get('parent_id')
if isinstance(pid, str) and pid.strip().isdigit():
data['parent_id'] = int(pid.strip())
# status兼容
status_val = data.get('status')
if isinstance(status_val, str):
lowered = status_val.strip().lower()
if lowered in {'true', '1', 'y', 'yes'}:
data['status'] = True
elif lowered in {'false', '0', 'n', 'no'}:
data['status'] = False
elif isinstance(status_val, int):
data['status'] = bool(status_val)
return data
class DeptUpdateSchema(DeptCreateSchema): class DeptUpdateSchema(DeptCreateSchema):
"""部门更新模型""" """部门更新模型"""
@@ -56,6 +56,7 @@ class DictDataCreateSchema(BaseModel):
description: Optional[str] = Field(default=None, max_length=255, description="描述") description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator('dict_label') @field_validator('dict_label')
@classmethod
def validate_dict_label(cls, value: str): def validate_dict_label(cls, value: str):
if not value or value.strip() == '': if not value or value.strip() == '':
raise ValueError('字典标签不能为空') raise ValueError('字典标签不能为空')
+51 -1
View File
@@ -1,9 +1,10 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional from typing import Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
import re
class OperationLogCreateSchema(BaseModel): class OperationLogCreateSchema(BaseModel):
@@ -22,6 +23,55 @@ class OperationLogCreateSchema(BaseModel):
description: Optional[str] = Field(default=None, max_length=255, description="描述") description: Optional[str] = Field(default=None, max_length=255, description="描述")
creator_id: Optional[int] = Field(default=None, description="创建人ID") creator_id: Optional[int] = Field(default=None, description="创建人ID")
@model_validator(mode='before')
@classmethod
def _normalize(cls, values):
if isinstance(values, dict):
# 字符串去空格
for k in ["request_path", "request_method", "request_payload", "request_ip", "login_location", "request_os", "request_browser", "response_json", "process_time", "description"]:
if k in values and isinstance(values[k], str):
values[k] = values[k].strip() or None if values[k].strip() == "" and k in {"request_payload", "response_json", "description"} else values[k].strip()
# 方法大写
if "request_method" in values and isinstance(values["request_method"], str):
values["request_method"] = values["request_method"].strip().upper()
# 响应码转整数
if "response_code" in values and isinstance(values["response_code"], str):
try:
values["response_code"] = int(values["response_code"].strip())
except Exception:
pass
return values
@field_validator("type")
@classmethod
def _validate_type(cls, value: Optional[int]):
if value is None:
return value
if value not in {1, 2}:
raise ValueError("日志类型仅支持 1(登录) 或 2(操作)")
return value
@field_validator("request_method")
@classmethod
def _validate_method(cls, value: Optional[str]):
if value is None:
return value
allowed = {"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}
if value.upper() not in allowed:
raise ValueError(f"请求方法必须为 {', '.join(sorted(allowed))}")
return value.upper()
@field_validator("request_ip")
@classmethod
def _validate_ip(cls, value: Optional[str]):
if value is None or value == "":
return value
ipv4 = r"^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$"
ipv6 = r"^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$"
if not re.match(ipv4, value) and not re.match(ipv6, value):
raise ValueError("请求IP必须为有效的IPv4或IPv6地址")
return value
class OperationLogOutSchema(OperationLogCreateSchema, BaseSchema): class OperationLogOutSchema(OperationLogCreateSchema, BaseSchema):
"""日志响应模型""" """日志响应模型"""
@@ -28,6 +28,36 @@ class MenuCreateSchema(BaseModel):
parent_id: Optional[int] = Field(default=None, ge=1, description="父菜单ID") parent_id: Optional[int] = Field(default=None, ge=1, description="父菜单ID")
description: Optional[str] = Field(default=None, max_length=255, description="描述") description: Optional[str] = Field(default=None, max_length=255, description="描述")
@model_validator(mode='before')
@classmethod
def _normalize(cls, values):
if isinstance(values, dict):
# 字符串去空格
for k in ["name", "icon", "permission", "route_name", "route_path", "component_path", "redirect", "title", "description"]:
if k in values and isinstance(values[k], str):
values[k] = values[k].strip() or None if values[k].strip() == "" else values[k].strip()
# 布尔兼容
for k in ["status", "keep_alive", "hidden", "always_show", "affix"]:
if k in values and isinstance(values[k], str):
values[k] = values[k].strip().lower() in {"true", "1", "yes", "y"}
# 父ID转整型
if "parent_id" in values and isinstance(values["parent_id"], str):
try:
values["parent_id"] = int(values["parent_id"].strip())
except Exception:
pass
# 路由名/路径规范
import re
if "route_name" in values and isinstance(values["route_name"], str):
rn = values["route_name"]
if rn and not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{1,99}$", rn):
raise ValueError("路由名称需字母开头,仅含字母/数字/_ . -")
if "route_path" in values and isinstance(values["route_path"], str):
rp = values["route_path"]
if rp and not rp.startswith("/"):
raise ValueError("路由路径需以 / 开头")
return values
@model_validator(mode='after') @model_validator(mode='after')
def validate_fields(self): def validate_fields(self):
return menu_request_validator(self) return menu_request_validator(self)
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional from typing import Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
@@ -14,6 +14,42 @@ class NoticeCreateSchema(BaseModel):
status: bool = Field(default=True, description="是否启用(True:启用 False:禁用)") status: bool = Field(default=True, description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述") description: Optional[str] = Field(default=None, max_length=255, description="描述")
@model_validator(mode='before')
@classmethod
def _normalize(cls, values):
if isinstance(values, dict):
# 字符串去空格
for k in ["notice_title", "notice_type", "notice_content", "description"]:
if k in values and isinstance(values[k], str):
values[k] = values[k].strip() or None if values[k].strip() == "" and k == "description" else values[k].strip()
# 布尔兼容
if "status" in values and isinstance(values["status"], str):
values["status"] = values["status"].strip().lower() in {"true", "1", "yes", "y"}
# 类型映射
mapping = {"1": "1", "2": "2", "通知": "1", "公告": "2", "notice": "1", "announcement": "2"}
if "notice_type" in values and isinstance(values["notice_type"], str):
v = values["notice_type"].strip().lower()
if v in mapping:
values["notice_type"] = mapping[v]
return values
@field_validator("notice_type")
@classmethod
def _validate_notice_type(cls, value: str):
if value not in {"1", "2"}:
raise ValueError("公告类型仅支持 '1'(通知) 或 '2'(公告)")
return value
@model_validator(mode='after')
def _validate_after(self):
if not self.notice_title.strip():
raise ValueError("公告标题不能为空")
if not self.notice_content.strip():
raise ValueError("公告内容不能为空")
if self.status is False and (not self.description or not str(self.description).strip()):
raise ValueError("禁用状态下必须填写描述")
return self
class NoticeUpdateSchema(NoticeCreateSchema): class NoticeUpdateSchema(NoticeCreateSchema):
"""公告通知更新模型""" """公告通知更新模型"""
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional from typing import Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
@@ -15,6 +15,43 @@ class ParamsCreateSchema(BaseModel):
status: bool = Field(default=True, description="状态(True:正常 False:停用)") status: bool = Field(default=True, description="状态(True:正常 False:停用)")
description: Optional[str] = Field(default=None, max_length=500, description="描述") description: Optional[str] = Field(default=None, max_length=500, description="描述")
@model_validator(mode='before')
@classmethod
def _normalize(cls, data):
"""前置归一化:字符串去空格、空串转 None、布尔兼容转换,并规范键为小写。"""
if isinstance(data, dict):
for key in ('config_name', 'config_key', 'config_value', 'description'):
val = data.get(key)
if isinstance(val, str):
val = val.strip()
if key in ('config_value', 'description') and val == '':
val = None
data[key] = val
# 规范键为小写
if isinstance(data.get('config_key'), str):
data['config_key'] = data['config_key'].lower()
# 规范布尔
for bkey in ('config_type', 'status'):
val = data.get(bkey)
if isinstance(val, str):
lowered = val.strip().lower()
if lowered in {'true', '1', 'y', 'yes'}:
data[bkey] = True
elif lowered in {'false', '0', 'n', 'no'}:
data[bkey] = False
elif isinstance(val, int):
data[bkey] = bool(val)
return data
@field_validator('config_key')
@classmethod
def _validate_config_key(cls, v: str) -> str:
v = v.strip().lower()
import re
if not re.match(r'^[a-z][a-z0-9_.-]*$', v):
raise ValueError('参数键名必须以小写字母开头,仅包含小写字母/数字/_.-')
return v
class ParamsUpdateSchema(ParamsCreateSchema): class ParamsUpdateSchema(ParamsCreateSchema):
"""配置更新模型""" """配置更新模型"""
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional from typing import Optional
from pydantic import BaseModel, ConfigDict, Field from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
from app.core.validator import DateTimeStr from app.core.validator import DateTimeStr
@@ -13,6 +13,41 @@ class PositionCreateSchema(BaseModel):
status: bool = Field(default=True, description="是否启用(True:启用 False:禁用)") status: bool = Field(default=True, description="是否启用(True:启用 False:禁用)")
description: Optional[str] = Field(default=None, max_length=255, description="描述") description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator('name')
@classmethod
def _validate_name(cls, v: str) -> str:
v = v.strip()
if not v:
raise ValueError('岗位名称不能为空')
return v
@model_validator(mode='before')
@classmethod
def _normalize(cls, data):
if isinstance(data, dict):
for key in ('name', 'description'):
val = data.get(key)
if isinstance(val, str):
val = val.strip()
if key == 'description' and val == '':
val = None
data[key] = val
# order字符串转为整数
order_val = data.get('order')
if isinstance(order_val, str) and order_val.strip().isdigit():
data['order'] = int(order_val.strip())
# status兼容
status_val = data.get('status')
if isinstance(status_val, str):
lowered = status_val.strip().lower()
if lowered in {'true', '1', 'y', 'yes'}:
data['status'] = True
elif lowered in {'false', '0', 'n', 'no'}:
data['status'] = False
elif isinstance(status_val, int):
data['status'] = bool(status_val)
return data
class PositionUpdateSchema(PositionCreateSchema): class PositionUpdateSchema(PositionCreateSchema):
"""岗位更新模型""" """岗位更新模型"""
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import List, Optional from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator
from app.core.base_schema import BaseSchema from app.core.base_schema import BaseSchema
from app.core.validator import role_permission_request_validator from app.core.validator import role_permission_request_validator
@@ -19,6 +19,49 @@ class RoleCreateSchema(BaseModel):
status: bool = Field(default=True, description="是否启用") status: bool = Field(default=True, description="是否启用")
description: Optional[str] = Field(default=None, max_length=255, description="描述") description: Optional[str] = Field(default=None, max_length=255, description="描述")
@field_validator("code")
@classmethod
def validate_code(cls, value: Optional[str]):
if value is None:
return value
import re
v = value.strip()
if not re.match(r"^[A-Za-z][A-Za-z0-9_]{1,39}$", v):
raise ValueError("角色编码需字母开头,允许字母/数字/下划线,长度2-40")
return v
@field_validator("name")
@classmethod
def validate_name(cls, value: str):
v = value.strip()
if not v:
raise ValueError("角色名称不能为空")
return v
@model_validator(mode='before')
@classmethod
def _normalize(cls, values):
if isinstance(values, dict):
for k in ["name", "code", "description"]:
if k in values and isinstance(values[k], str):
values[k] = values[k].strip() or None if values[k].strip() == "" else values[k].strip()
# bool 兼容
if "status" in values and isinstance(values["status"], str):
values["status"] = values["status"].strip().lower() in {"true", "1", "yes", "y"}
# 数字兼容
if "order" in values and isinstance(values["order"], str):
try:
values["order"] = int(values["order"].strip())
except Exception:
pass
return values
@model_validator(mode='after')
def _validate_after(self):
if self.status is False and (not self.description or not str(self.description).strip()):
raise ValueError("禁用状态下必须填写描述")
return self
class RolePermissionSettingSchema(BaseModel): class RolePermissionSettingSchema(BaseModel):
"""角色权限配置模型""" """角色权限配置模型"""
@@ -27,6 +70,18 @@ class RolePermissionSettingSchema(BaseModel):
menu_ids: List[int] = Field(default_factory=list, description='菜单ID列表') menu_ids: List[int] = Field(default_factory=list, description='菜单ID列表')
dept_ids: List[int] = Field(default_factory=list, description='部门ID列表') dept_ids: List[int] = Field(default_factory=list, description='部门ID列表')
@model_validator(mode='before')
@classmethod
def _normalize(cls, values):
if isinstance(values, dict):
for k in ["role_ids", "menu_ids", "dept_ids"]:
if k in values and values[k] is not None:
try:
values[k] = list({int(x) for x in values[k]})
except Exception:
pass
return values
@model_validator(mode='after') @model_validator(mode='after')
def validate_fields(self): def validate_fields(self):
"""验证权限配置字段""" """验证权限配置字段"""
@@ -1,11 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel, ConfigDict, Field, EmailStr, field_validator from pydantic import BaseModel, ConfigDict, Field, EmailStr, field_validator, model_validator
from app.core.validator import DateTimeStr, mobile_validator from app.core.validator import DateTimeStr, mobile_validator
from app.core.base_schema import BaseSchema, CommonSchema from app.core.base_schema import BaseSchema, CommonSchema
from app.api.v1.module_system.role.schema import RoleOutSchema from app.api.v1.module_system.role.schema import RoleOutSchema
from urllib.parse import urlparse
class CurrentUserUpdateSchema(BaseModel): class CurrentUserUpdateSchema(BaseModel):
"""基础用户信息""" """基础用户信息"""
@@ -20,6 +21,16 @@ class CurrentUserUpdateSchema(BaseModel):
def validate_mobile(cls, value: Optional[str]): def validate_mobile(cls, value: Optional[str]):
return mobile_validator(value) return mobile_validator(value)
@field_validator("avatar")
@classmethod
def validate_avatar(cls, value: Optional[str]):
if not value:
return value
parsed = urlparse(value)
if parsed.scheme in ("http", "https") and parsed.netloc:
return value
raise ValueError("头像地址需为有效的HTTP/HTTPS URL")
class UserRegisterSchema(BaseModel): class UserRegisterSchema(BaseModel):
"""注册""" """注册"""
@@ -36,6 +47,36 @@ class UserRegisterSchema(BaseModel):
def validate_mobile(cls, value: Optional[str]): def validate_mobile(cls, value: Optional[str]):
return mobile_validator(value) return mobile_validator(value)
@field_validator("username")
@classmethod
def validate_username(cls, value: str):
v = value.strip()
if not v:
raise ValueError("账号不能为空")
# 字母开头,允许字母数字_.-
import re
if not re.match(r"^[A-Za-z][A-Za-z0-9_.-]{2,31}$", v):
raise ValueError("账号需字母开头,3-32位,仅含字母/数字/_ . -")
return v
@model_validator(mode='before')
@classmethod
def _normalize(cls, values):
if isinstance(values, dict):
for k in ["name", "username", "password", "description"]:
if k in values and isinstance(values[k], str):
values[k] = values[k].strip() or values[k]
# role_ids 去重并转为 int
if "role_ids" in values and values["role_ids"] is not None:
try:
values["role_ids"] = list[int]({int(x) for x in values["role_ids"]})
except Exception:
pass
# mobile 空串转 None
if "mobile" in values and isinstance(values["mobile"], str) and values["mobile"].strip() == "":
values["mobile"] = None
return values
class UserForgetPasswordSchema(BaseModel): class UserForgetPasswordSchema(BaseModel):
"""忘记密码""" """忘记密码"""
@@ -75,6 +116,35 @@ class UserCreateSchema(CurrentUserUpdateSchema):
role_ids: Optional[List[int]] = Field(default=[], description='角色ID') role_ids: Optional[List[int]] = Field(default=[], description='角色ID')
position_ids: Optional[List[int]] = Field(default=[], description='岗位ID') position_ids: Optional[List[int]] = Field(default=[], description='岗位ID')
@model_validator(mode='before')
@classmethod
def _normalize(cls, values):
if isinstance(values, dict):
# 字符串去空格和空串转 None
for k in ["username", "password", "description", "name"]:
if k in values and isinstance(values[k], str):
values[k] = values[k].strip() or None if values[k].strip() == "" else values[k].strip()
# bool 兼容
for k in ["status", "is_superuser"]:
if k in values:
v = values[k]
if isinstance(v, str):
values[k] = v.strip().lower() in {"true", "1", "yes", "y"}
# 列表转 int 去重
for k in ["role_ids", "position_ids"]:
if k in values and values[k] is not None:
try:
values[k] = list({int(x) for x in values[k]})
except Exception:
pass
return values
@model_validator(mode='after')
def _validate_after(self):
if self.status is False and (not self.description or not str(self.description).strip()):
raise ValueError("禁用状态下必须填写备注描述")
return self
class UserUpdateSchema(UserCreateSchema): class UserUpdateSchema(UserCreateSchema):
"""更新""" """更新"""
+1 -2
View File
@@ -15,8 +15,6 @@ from apscheduler.jobstores.redis import RedisJobStore
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.date import DateTrigger
from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.interval import IntervalTrigger
import asyncio
import threading
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from app.config.setting import settings from app.config.setting import settings
@@ -117,6 +115,7 @@ class SchedulerUtil:
status=status, status=status,
exception_info=exception_info, exception_info=exception_info,
create_time=datetime.now(), create_time=datetime.now(),
job_id=job_id,
) )
# 使用线程池执行异步操作以避免阻塞调度器和数据库锁定问题 # 使用线程池执行异步操作以避免阻塞调度器和数据库锁定问题
+8 -1
View File
@@ -109,7 +109,14 @@ def handle_exception(app: FastAPI):
"value could not be parsed to a boolean": "类型错误,提交参数应该为布尔值!", "value could not be parsed to a boolean": "类型错误,提交参数应该为布尔值!",
"Input should be a valid list": "类型错误,输入应该是一个有效的列表!" "Input should be a valid list": "类型错误,输入应该是一个有效的列表!"
} }
msg = error_mapping.get(exc.errors()[0].get('msg'), exc.errors()[0].get('msg')) raw_msg = exc.errors()[0].get('msg')
msg = error_mapping.get(raw_msg, raw_msg)
# 去掉Pydantic默认的前缀“Value error”, 仅保留具体提示内容
if isinstance(msg, str) and msg.startswith("Value error"):
if "," in msg:
msg = msg.split(",", 1)[1].strip()
else:
msg = msg.replace("Value error", "").strip()
logger.error(f"请求地址: {request.url}, 错误信息: {msg}, 错误详情: {exc}") logger.error(f"请求地址: {request.url}, 错误信息: {msg}, 错误详情: {exc}")
return ErrorResponse(msg=str(msg), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, data=exc.body) return ErrorResponse(msg=str(msg), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, data=exc.body)
+46 -1
View File
@@ -29,6 +29,50 @@ class AIClient:
http_client=self.http_client http_client=self.http_client
) )
def _friendly_error_message(self, e: Exception) -> str:
"""将 OpenAI 或网络异常转换为友好的中文提示。"""
# 尝试获取状态码与错误体
status_code = getattr(e, "status_code", None)
body = getattr(e, "body", None)
message = None
error_type = None
error_code = None
try:
if isinstance(body, dict) and "error" in body:
err = body.get("error") or {}
error_type = err.get("type")
error_code = err.get("code")
message = err.get("message")
except Exception:
# 忽略解析失败
pass
text = str(e)
msg = message or text
# 特定错误映射
# 欠费/账户状态异常
if (error_code == "Arrearage") or (error_type == "Arrearage") or ("in good standing" in (msg or "")):
return "账户欠费或结算异常,访问被拒绝。请检查账号状态或更换有效的 API Key。"
# 鉴权失败
if status_code == 401 or "invalid api key" in msg.lower():
return "鉴权失败,API Key 无效或已过期。请检查系统配置中的 API Key。"
# 权限不足或被拒绝
if status_code == 403 or error_type in {"PermissionDenied", "permission_denied"}:
return "访问被拒绝,权限不足或账号受限。请检查账户权限设置。"
# 配额不足或限流
if status_code == 429 or error_type in {"insufficient_quota", "rate_limit_exceeded"}:
return "请求过于频繁或配额已用尽。请稍后重试或提升账户配额。"
# 客户端错误
if status_code == 400:
return f"请求参数错误或服务拒绝:{message or '请检查输入内容。'}"
# 服务端错误
if status_code in {500, 502, 503, 504}:
return "服务暂时不可用,请稍后重试。"
# 默认兜底
return f"处理您的请求时出现错误:{msg}"
async def process(self, query: str) -> AsyncGenerator[str, None]: async def process(self, query: str) -> AsyncGenerator[str, None]:
""" """
处理查询并返回流式响应 处理查询并返回流式响应
@@ -58,8 +102,9 @@ class AIClient:
yield chunk.choices[0].delta.content yield chunk.choices[0].delta.content
except Exception as e: except Exception as e:
# 记录详细错误,返回友好提示
logger.error(f"AI处理查询失败: {str(e)}") logger.error(f"AI处理查询失败: {str(e)}")
yield f"抱歉,处理您的请求时出现了错误: {str(e)}" yield self._friendly_error_message(e)
async def close(self) -> None: async def close(self) -> None:
""" """
+21 -17
View File
@@ -248,33 +248,35 @@ class Jinja2TemplateUtil:
# 处理空值情况 # 处理空值情况
category = tpl_category or GenConstant.TPL_CRUD category = tpl_category or GenConstant.TPL_CRUD
templates = [ templates = [
# Python相关模板 # Python相关模板(调整为实际目录)
'python/controller.py.j2', 'backend/app/v1/module_demo/python/controller.py.j2',
'python/service.py.j2', 'backend/app/v1/module_demo/python/service.py.j2',
'python/crud.py.j2', 'backend/app/v1/module_demo/python/crud.py.j2',
'python/schema.py.j2', 'backend/app/v1/module_demo/python/schema.py.j2',
'python/param.py.j2', 'backend/app/v1/module_demo/python/param.py.j2',
'python/model.py.j2', 'backend/app/v1/module_demo/python/model.py.j2',
# Vue相关模板 'backend/app/v1/module_demo/python/__init__.py.j2',
f'{use_web_type}/api.ts.j2',
# SQL脚本模板 # Vue相关模板(API)
'sql/sql.sql.j2', 'frontend/src/api/api.ts.j2',
# SQL脚本模板(调整为实际目录)
'backend/sql/sql.sql.j2',
] ]
if category == GenConstant.TPL_CRUD: 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: 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: 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 return templates
@classmethod @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): 生成表的配置信息。 - gen_table (GenTableOutSchema): 生成表的配置信息。
返回: 返回:
@@ -299,7 +301,9 @@ class Jinja2TemplateUtil:
return f'{python_path}/app/api/v1/{module_name}/{business_name}/param.py' return f'{python_path}/app/api/v1/{module_name}/{business_name}/param.py'
elif 'schema.py.j2' in template: elif 'schema.py.j2' in template:
return f'{python_path}/app/api/v1/{module_name}/{business_name}/schema.py' 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' return f'{cls.BACKEND_PROJECT_PATH}/sql/{module_name}/{business_name}_menu.sql'
elif 'api.ts.j2' in template: elif 'api.ts.j2' in template:
return f'{vue_path}/src/api/{module_name}/{business_name}.ts' return f'{vue_path}/src/api/{module_name}/{business_name}.ts'
@@ -0,0 +1 @@
# -*- coding:utf-8 -*-
@@ -6,7 +6,6 @@ from app.common.response import SuccessResponse, StreamResponse
from app.core.dependencies import AuthPermission from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute from app.core.router_class import OperationLogRoute
from app.api.v1.module_system.auth.schema import AuthSchema 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.core.base_params import PaginationQueryParam
from app.utils.common_util import bytes2file_response from app.utils.common_util import bytes2file_response
from app.core.logger import logger 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(), search: {{ table_name|snake_to_pascal_case }}QueryParam = Depends(),
auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"])) auth: AuthSchema = Depends(AuthPermission(["{{ permission_prefix }}:query"]))
) -> JSONResponse: ) -> JSONResponse:
"""查询{{ function_name }}列表接口""" """查询{{ 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 {{ table_name|snake_to_pascal_case }}Service.page_service(
result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size) 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 }}列表成功") logger.info("查询{{ function_name }}列表成功")
return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功") return SuccessResponse(data=result_dict, msg="查询{{ function_name }}列表成功")
@@ -1,11 +1,11 @@
# -*- coding:utf-8 -*- # -*- 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.core.base_crud import CRUDBase
from app.api.v1.module_system.auth.schema import AuthSchema from app.api.v1.module_system.auth.schema import AuthSchema
from .model import {{ table_name|snake_to_pascal_case }}Model 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]): 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""" """初始化CRUD"""
super().__init__(model={{ table_name|snake_to_pascal_case }}Model, auth=auth) 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]: async def create_crud(self, data: {{ table_name|snake_to_pascal_case }}CreateSchema) -> Optional[{{ table_name|snake_to_pascal_case }}Model]:
"""创建""" """创建"""
@@ -38,3 +38,16 @@ 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: 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
)
@@ -2,8 +2,8 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from sqlalchemy import String, Integer, Text, DateTime from sqlalchemy import String, Integer, Text, DateTime, Boolean, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.base_model import CreatorMixin from app.core.base_model import CreatorMixin
@@ -15,6 +15,7 @@ class {{ table_name|snake_to_pascal_case }}Model(CreatorMixin):
__tablename__ = '{{ table_name }}' __tablename__ = '{{ table_name }}'
__table_args__ = {'comment': '{{ function_name }}'} __table_args__ = {'comment': '{{ function_name }}'}
__loader_options__ = ["creator"]
{% for column in columns %} {% 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 }}') {{ column.column_name }}: Mapped[Optional[{{ column.python_type }}]] = mapped_column({{ column.column_type|get_sqlalchemy_type }}, nullable=True, comment='{{ column.column_comment }}')
@@ -35,6 +35,20 @@ class {{ table_name|snake_to_pascal_case }}Service:
obj_list = await {{ table_name|snake_to_pascal_case }}CRUD(auth).list_crud(search=search_dict, order_by=order_by) 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] 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 @classmethod
async def create_service(cls, auth: AuthSchema, data: {{ table_name|snake_to_pascal_case }}CreateSchema) -> Dict: 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() 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) return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
@classmethod @classmethod
@@ -117,7 +142,9 @@ class {{ table_name|snake_to_pascal_case }}Service:
"{{ column.column_name }}": row['{{ column.column_name }}'], "{{ column.column_name }}": row['{{ column.column_name }}'],
{% endfor %} {% 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 success_count += 1
except Exception as e: except Exception as e:
error_msgs.append(f"第{count}行: {str(e)}") error_msgs.append(f"第{count}行: {str(e)}")
@@ -1,6 +1,6 @@
import request from "@/utils/request"; 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) // 参考 demo.ts 的风格,提供标准的 CRUD 与导入/导出 API(TypeScript)
const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = { const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = {
@@ -60,7 +60,7 @@ const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = {
// 下载导入模板 // 下载导入模板
downloadTemplate() { downloadTemplate() {
return request<ApiResponse>({ return request<Blob>({
url: `${API_PATH}/download/template`, url: `${API_PATH}/download/template`,
method: "post", method: "post",
responseType: "blob", responseType: "blob",
@@ -68,7 +68,7 @@ const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = {
}, },
// 导入 // 导入
import(data: any) { import(data: FormData) {
return request<ApiResponse>({ return request<ApiResponse>({
url: `${API_PATH}/import`, url: `${API_PATH}/import`,
method: "post", method: "post",
@@ -76,6 +76,14 @@ const {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API = {
headers: { "Content-Type": "multipart/form-data" }, 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; 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 { export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}PageQuery extends PageQuery {
{% for column in columns %} {% for column in columns %}
{% if column.is_query == "1" and column.query_type != "BETWEEN" %} {% if column.is_query == "1" and column.query_type != "BETWEEN" %}
{{ column.python_field }}?: {{ {{ column.python_field }}?: {{
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio') 'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
else 'number' if column.is_pk == '1' else 'number' if column.is_pk == '1'
else 'string' else 'string'
}}; }};
{% endif %} {% endif %}
{% endfor %} {% endfor %}
// 时间范围查询(按示例统一字段名,如需按字段拆分可在页面层处理) // 时间范围查询(按示例统一字段名,如需按字段拆分可在页面层处理)
start_time?: string; start_time?: string;
@@ -101,11 +109,11 @@ export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Pag
export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Table { export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Table {
{% for column in columns %} {% for column in columns %}
{{ column.python_field }}?: {{ {{ column.python_field }}?: {{
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio') 'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
else 'number' if column.is_pk == '1' else 'number' if column.is_pk == '1'
else 'string' else 'string'
}}; }};
{% endfor %} {% endfor %}
creator?: creatorType; creator?: creatorType;
} }
@@ -113,12 +121,12 @@ export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Tab
export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Form { export interface {{ business_name|replace('_', ' ')|title|replace(' ', '') }}Form {
id?: number; id?: number;
{% for column in columns %} {% for column in columns %}
{% if column.is_insert == "1" or column.is_edit == "1" %} {% if column.is_insert == "1" or column.is_edit == "1" %}
{{ column.python_field }}?: {{ {{ column.python_field }}?: {{
'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio') 'boolean' if ('status' in (column.python_field|lower)) or (column.html_type == 'radio')
else 'number' if column.is_pk == '1' else 'number' if column.is_pk == '1'
else 'string' else 'string'
}}; }};
{% endif %} {% endif %}
{% endfor %} {% endfor %}
} }
@@ -1,8 +1,8 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div v-show="visible" class="search-container">
<el-form :model="queryFormData" ref="queryFormRef" :inline="true" label-suffix=":"> <el-form ref="queryFormRef" :model="queryFormData" label-suffix=":" :inline="true" @submit.prevent="handleQuery">
{% for column in columns %} {% for column in columns %}
{% if column.is_query == "1" %} {% if column.is_query == "1" %}
{% set dict_type = column.dict_type %} {% set dict_type = column.dict_type %}
@@ -12,7 +12,7 @@
{% if column.html_type == "input" %} {% if column.html_type == "input" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}"> <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> </el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %} {% elif (column.html_type == "select" or column.html_type == "radio") and dict_type != "" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}"> <el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
@@ -30,16 +30,32 @@
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}"> <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-date-picker v-model="queryFormData.{{ column.python_field }}" type="date" value-format="YYYY-MM-DD" clearable placeholder="请选择{{ comment }}" />
</el-form-item> </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 %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<el-form-item class="search-buttons"> <!-- 可选:创建人选择与统一日期范围(展开后显示) -->
<el-button type="primary" icon="search" @click="handleQuery">查询</el-button> <el-form-item v-if="isExpand" prop="creator" label="创建人">
<el-button icon="refresh" @click="handleResetQuery">重置</el-button> <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-item>
</el-form> </el-form>
</div> </div>
@@ -59,23 +75,61 @@
<!-- 功能区域 --> <!-- 功能区域 -->
<div class="data-table__toolbar"> <div class="data-table__toolbar">
<div class="data-table__toolbar--actions"> <div class="data-table__toolbar--left">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="1.5"> <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>
<el-col :span="1.5"> <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-col>
</el-row> </el-row>
</div> </div>
<div class="data-table__toolbar--tools"> <div class="data-table__toolbar--right">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="1.5"> <el-col :span="1.5">
<el-tooltip content="刷新"> <el-tooltip content="导入">
<el-button type="primary" icon="refresh" circle @click="handleRefresh" /> <el-button v-hasPerm="['{{ module_name }}:{{ business_name }}:import']" type="success" icon="upload" circle @click="handleOpenImportDialog" />
</el-tooltip> </el-tooltip>
</el-col> </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> </el-row>
</div> </div>
</div> </div>
@@ -95,10 +149,10 @@
<template #empty> <template #empty>
<el-empty :image-size="80" description="暂无数据" /> <el-empty :image-size="80" description="暂无数据" />
</template> </template>
<el-table-column type="selection" min-width="55" align="center" /> <el-table-column v-if="tableColumns.find((col) => col.prop === 'selection')?.show" 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 === 'index')?.show" fixed label="序号" min-width="60">
<template #default="scope"> <template #default="scope">
{{ '{' }}{{ '{' }} (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 {{ '}' }}{{ '}' }} {{ '{{' }} (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 {{ '}}' }}
</template> </template>
</el-table-column> </el-table-column>
{% for column in columns %} {% for column in columns %}
@@ -107,20 +161,24 @@
{% set parentheseIndex = column_comment.find("(") %} {% set parentheseIndex = column_comment.find("(") %}
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %} {% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
{% if column.is_list == "1" %} {% if column.is_list == "1" %}
<el-table-column label="{{ comment }}" prop="{{ python_field }}" min-width="140"> <el-table-column v-if="tableColumns.find((col) => col.prop === '{{ python_field }}')?.show" label="{{ comment }}" prop="{{ python_field }}" min-width="140">
{% if column.html_type == "datetime" %} {% if python_field == "status" %}
<template #default="scope"> <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> </template>
{% endif %} {% endif %}
</el-table-column> </el-table-column>
{% endif %} {% endif %}
{% endfor %} {% 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"> <template #default="scope">
<el-button type="info" size="small" link icon="document" @click="handleOpenDialog('detail', 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 type="primary" size="small" link icon="edit" @click="handleOpenDialog('update', 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 type="danger" size="small" link icon="delete" @click="handleDelete([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> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>
@@ -158,7 +216,14 @@
{% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %} {% set comment = column_comment[:parentheseIndex] if parentheseIndex != -1 else column_comment %}
{% set required = 'true' if column.is_required == '1' else 'false' %} {% 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-form-item label="{{ comment }}" prop="{{ column.python_field }}" :required="{{ required }}">
<el-input v-model="formData.{{ column.python_field }}" placeholder="请输入{{ comment }}" /> <el-input v-model="formData.{{ column.python_field }}" placeholder="请输入{{ comment }}" />
</el-form-item> </el-form-item>
@@ -203,72 +268,177 @@
<template #footer> <template #footer>
<div class="dialog-footer"> <div class="dialog-footer">
<el-button @click="handleCloseDialog">取消</el-button> <el-button @click="handleCloseDialog">取消</el-button>
<el-button v-if="dialogVisible.type !== 'detail'" type="primary" @click="handleSubmit">确定</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 type="primary" @click="handleCloseDialog">确定</el-button> <el-button v-else v-hasPerm="['{{ module_name }}:{{ business_name|lower }}:detail']" type="primary" @click="handleCloseDialog">确定</el-button>
</div> </div>
</template> </template>
</el-dialog> </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> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, reactive, onMounted } from 'vue' import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus' 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 {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API from '@/api/{{ module_name }}/{{ business_name }}'
import { useDictStore } from '@/store/index' import { useDictStore } from '@/store/index'
import SingleImageUpload from '@/components/Upload/SingleImageUpload.vue' 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 queryFormRef = ref()
const dataFormRef = ref() const dataFormRef = ref()
const total = ref(0) const total = ref(0)
const selectIds = ref([]) const selectIds = ref<number[]>([])
const selectionRows = ref<any[]>([])
const loading = ref(false) const loading = ref(false)
// 字典仓库与需要加载的字典类型 // 字典仓库与需要加载的字典类型
const dictStore = useDictStore() const dictStore = useDictStore()
const dictTypes = [ const dictTypes = [
{% for column in columns %} {% for column in columns %}
{% if column.dict_type %} {% if column.dict_type %}
'{{ column.dict_type }}', '{{ column.dict_type }}',
{% endif %} {% endif %}
{% endfor %} {% 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({ const queryFormData = reactive({
page_no: 1, page_no: 1,
page_size: 10, page_size: 10,
{% for column in columns %} {% for column in columns %}
{% if column.is_query == "1" and column.query_type != "BETWEEN" %} {% if column.is_query == "1" and column.query_type != "BETWEEN" %}
{{ column.python_field }}: undefined, {{ column.python_field }}: undefined,
{% endif %} {% endif %}
{% endfor %} {% endfor %}
start_time: undefined,
end_time: undefined,
creator: undefined,
}) })
// 加载表格数据 // 加载表格数据
async function loadingData() { async function loadingData() {
loading.value = true loading.value = true
try { try {
const params = { const response = await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.list(queryFormData)
...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 返回结构
pageTableData.value = response.data.data.items pageTableData.value = response.data.data.items
total.value = response.data.data.total total.value = response.data.data.total
} catch (error) { } catch (error) {
@@ -284,22 +454,25 @@ async function handleQuery() {
loadingData() loadingData()
} }
// 选择创建人后触发查询
function handleConfirm() {
handleQuery()
}
// 重置查询 // 重置查询
async function handleResetQuery() { async function handleResetQuery() {
queryFormRef.value.resetFields() queryFormRef.value.resetFields()
queryFormData.page_no = 1 queryFormData.page_no = 1
// 重置所有日期范围选择器 dateRange.value = []
{% for column in columns %} queryFormData.start_time = undefined
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %} queryFormData.end_time = undefined
dateRangeMap.{{ column.python_field }} = []
{% endif %}
{% endfor %}
loadingData() loadingData()
} }
// 行复选框选中项变化 // 行复选框选中项变化
function handleSelectionChange(selection) { function handleSelectionChange(selection: any[]) {
selectIds.value = selection.map((item) => item.id) 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 dialogVisible.type = type
if (id) { if (id) {
const response = await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.detail(id) const response = await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.detail(id)
@@ -328,23 +501,20 @@ async function handleOpenDialog(type, id) {
// 提交表单 // 提交表单
async function handleSubmit() { async function handleSubmit() {
dataFormRef.value.validate(async (valid) => { dataFormRef.value.validate(async (valid: any) => {
if (valid) { if (valid) {
loading.value = true loading.value = true
try { try {
const id = formData.id const id = formData.id
if (id) { if (id) {
await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.update(id, { id, ...formData }) await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.update(id, { id, ...formData })
ElMessage.success('修改成功')
} else { } else {
await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.create(formData) await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.create(formData)
ElMessage.success('新增成功')
} }
dialogVisible.visible = false dialogVisible.visible = false
handleResetQuery() handleResetQuery()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('操作失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -353,7 +523,7 @@ async function handleSubmit() {
} }
// 删除、批量删除 // 删除、批量删除
async function handleDelete(ids) { async function handleDelete(ids: number[]) {
ElMessageBox.confirm('确认删除该项数据?', '警告', { ElMessageBox.confirm('确认删除该项数据?', '警告', {
confirmButtonText: '确定', confirmButtonText: '确定',
cancelButtonText: '取消', cancelButtonText: '取消',
@@ -363,11 +533,9 @@ async function handleDelete(ids) {
try { try {
loading.value = true loading.value = true
await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.delete(ids) await {{ business_name|replace('_', ' ')|title|replace(' ', '') }}API.delete(ids)
ElMessage.success('删除成功')
handleResetQuery() handleResetQuery()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
ElMessage.error('删除失败')
} finally { } finally {
loading.value = false loading.value = false
} }
@@ -377,34 +545,56 @@ async function handleDelete(ids) {
}) })
} }
// 弹窗状态 // 批量启用/停用
const dialogVisible = reactive({ async function handleMoreClick(status: boolean) {
title: '', if (selectIds.value.length) {
visible: false, ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, '警告', {
type: 'create', // 'create' | 'update' | 'detail' 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({ const importDialogVisible = ref(false)
id: undefined, // 导出弹窗显示状态
{% for column in columns %} const exportsDialogVisible = ref(false)
{% if column.is_insert == "1" or column.is_edit == "1" %}
{{ column.python_field }}: undefined,
{% endif %}
{% endfor %}
})
// 表单验证规则(必填项按 is_required 生成) // 打开导入弹窗
const rules = reactive({ function handleOpenImportDialog() {
{% for column in columns %} importDialogVisible.value = true
{% if column.is_insert == "1" or column.is_edit == "1" %} }
{% set required = 'true' if column.is_required == '1' else 'false' %} // 打开导出弹窗
{{ column.python_field }}: [ function handleOpenExportsModal() {
{ required: {{ required }}, message: '请输入{{ column.column_comment or column.python_field }}', trigger: 'blur' }, exportsDialogVisible.value = true
], }
{% endif %}
{% endfor %} // 处理上传
}) 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() { async function handleRefresh() {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 237 KiB

+1 -1
View File
@@ -1,7 +1,7 @@
<template> <template>
<div v-show="visible"> <div v-show="visible">
<el-card v-bind="cardAttrs"> <el-card v-bind="cardAttrs">
<el-form ref="queryFormRef" :model="queryParams" v-bind="formAttrs" :class="isGrid"> <el-form ref="queryFormRef" :model="queryParams" v-bind="formAttrs" :class="isGrid" @submit.prevent="handleQuery">
<template v-for="(item, index) in formItems" :key="item.prop"> <template v-for="(item, index) in formItems" :key="item.prop">
<el-form-item <el-form-item
v-show="isExpand ? true : index < showNumber" v-show="isExpand ? true : index < showNumber"
@@ -82,7 +82,7 @@ const getOperationMaxWidth = () => {
const buttons = item.querySelectorAll(".el-button"); const buttons = item.querySelectorAll(".el-button");
// 获取每行按钮的总宽度 // 获取每行按钮的总宽度
totalWidth = Array.from(buttons).reduce((acc, button: any) => { totalWidth = Array.from(buttons).reduce((acc, button: any) => {
return acc + button.scrollWidth + 22; // 每个按钮的宽度加上预留宽度 return acc + button.scrollWidth + 14; // 每个按钮的宽度加上预留宽度
}, 0); }, 0);
// 获取最大的宽度 // 获取最大的宽度
@@ -3,7 +3,7 @@
<el-drawer v-model="drawerVisible" :title="'【' + props.jobName + '】任务日志'" :size="drawerSize"> <el-drawer v-model="drawerVisible" :title="'【' + props.jobName + '】任务日志'" :size="drawerSize">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":"> <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
<el-form-item prop="status" label="执行状态"> <el-form-item prop="status" label="执行状态">
<el-select v-model="queryFormData.status" placeholder="请选择执行状态" style="width: 167.5px" clearable> <el-select v-model="queryFormData.status" placeholder="请选择执行状态" style="width: 167.5px" clearable>
<el-option :value="true" label="成功" /> <el-option :value="true" label="成功" />
@@ -127,7 +127,7 @@
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog"> <el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
<!-- 详情 --> <!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<el-descriptions :column="2" border> <el-descriptions :column="2" border label-width="120px">
<el-descriptions-item label="日志ID" :span="2">{{ detailFormData.id }}</el-descriptions-item> <el-descriptions-item label="日志ID" :span="2">{{ detailFormData.id }}</el-descriptions-item>
<el-descriptions-item label="任务名称" :span="2">{{ detailFormData.job_name }}</el-descriptions-item> <el-descriptions-item label="任务名称" :span="2">{{ detailFormData.job_name }}</el-descriptions-item>
<el-descriptions-item label="任务组名" :span="2">{{ detailFormData.job_group }}</el-descriptions-item> <el-descriptions-item label="任务组名" :span="2">{{ detailFormData.job_group }}</el-descriptions-item>
+4 -3
View File
@@ -8,6 +8,7 @@
:model="queryFormData" :model="queryFormData"
:inline="true" :inline="true"
label-suffix=":" label-suffix=":"
@submit.prevent="handleQuery"
> >
<el-form-item prop="name" label="任务名称"> <el-form-item prop="name" label="任务名称">
<el-input <el-input
@@ -24,7 +25,7 @@
style="width: 167.5px" style="width: 167.5px"
> >
<el-option value="true" label="启用" /> <el-option value="true" label="启用" />
<el-option value="false" label="暂停" /> <el-option value="false" label="停用" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<!-- 时间范围,收起状态下隐藏 --> <!-- 时间范围,收起状态下隐藏 -->
@@ -43,7 +44,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['app:job:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['app:job:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['app:job:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['app:job:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
@@ -521,7 +522,7 @@
max-height="500px" max-height="500px"
@change="handlechangeCron" @change="handlechangeCron"
@close="openCron = false" @close="openCron = false"
></vue3CronPlus> />
</el-popover> </el-popover>
</el-form-item> </el-form-item>
<!-- 开始日期和结束日期 --> <!-- 开始日期和结束日期 -->
+47 -79
View File
@@ -2,29 +2,47 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<!-- 顶部搜索和操作区域 --> <!-- 顶部搜索和操作区域 -->
<div class="search-container mb-4"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":"> <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
<el-form-item prop="name" label="应用名称"> <el-form-item prop="name" label="应用名称">
<el-input v-model="queryFormData.name" placeholder="请输入应用名称" clearable style="width: 200px" /> <el-input v-model="queryFormData.name" placeholder="请输入应用名称" clearable/>
</el-form-item> </el-form-item>
<el-form-item prop="status" label="状态"> <el-form-item prop="status" label="状态">
<el-select v-model="queryFormData.status" placeholder="请选择状态" clearable style="width: 120px"> <el-select v-model="queryFormData.status" placeholder="请选择状态" clearable style="width: 170px;">
<el-option label="启用" :value="true" /> <el-option label="启用" :value="true" />
<el-option label="停用" :value="false" /> <el-option label="停用" :value="false" />
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item prop="creator" label="创建人"> <el-form-item v-if="isExpand" prop="creator" label="创建人">
<el-input v-model="queryFormData.creator" placeholder="请输入创建人" clearable style="width: 150px" /> <UserTableSelect
v-model="queryFormData.creator"
@confirm-click="handleConfirm"
@clear-click="handleQuery"
/>
</el-form-item> </el-form-item>
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['application:myapp:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['application:myapp:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['application:myapp:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['application:myapp: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-item>
</el-form> </el-form>
</div> </div>
<!-- 应用卡片展示区域 --> <!-- 应用卡片展示区域 -->
<el-card shadow="hover" class="app-grid-card"> <el-card shadow="hover" class="app-grid-card" >
<template #header> <template #header>
<div class="card-header"> <div class="card-header">
<span>应用市场</span> <span>应用市场</span>
@@ -35,7 +53,7 @@
</template> </template>
<!-- 应用网格 --> <!-- 应用网格 -->
<div v-loading="loading" class="app-grid"> <div v-loading="loading" class="app-grid" >
<el-card <el-card
v-for="app in applicationList" v-for="app in applicationList"
:key="app.id" :key="app.id"
@@ -136,17 +154,14 @@
<!-- 分页区域 --> <!-- 分页区域 -->
<template #footer> <template #footer>
<div class="pagination-container"> <!-- 使用卡片 footer 样式右对齐,无需额外容器 -->
<el-pagination <pagination
v-model:current-page="queryFormData.page_no" v-model:total="total"
v-model:page-size="queryFormData.page_size" v-model:page="queryFormData.page_no"
:total="total" v-model:limit="queryFormData.page_size"
:page-sizes="[12, 24, 48]" :page-sizes="[12, 24, 48]"
layout="total, sizes, prev, pager, next, jumper" @pagination="loadApplicationList"
@size-change="handleSizeChange" />
@current-change="handleCurrentChange"
/>
</div>
</template> </template>
</el-card> </el-card>
@@ -232,6 +247,8 @@ const total = ref(0);
const dialogVisible = ref(false); const dialogVisible = ref(false);
const dialogType = ref<'create' | 'edit'>('create'); const dialogType = ref<'create' | 'edit'>('create');
const currentApp = ref<ApplicationInfo | null>(null); const currentApp = ref<ApplicationInfo | null>(null);
const isExpand = ref(false);
const isExpandable = ref(true);
// 分页查询参数 // 分页查询参数
const queryFormData = reactive<ApplicationPageQuery>({ const queryFormData = reactive<ApplicationPageQuery>({
@@ -256,7 +273,9 @@ const formData = reactive<ApplicationForm>({
// 表单验证规则 // 表单验证规则
const formRules = reactive({ const formRules = reactive({
name: [{ required: true, message: "请输入应用名称", trigger: "blur" }], name: [
{ required: true, message: "请输入应用名称", trigger: "blur" },
{ min: 2, max: 30, message: "长度在 2 到 30 个字符", trigger: "blur" }],
access_url: [ access_url: [
{ required: true, message: "请输入访问地址", trigger: "blur" }, { required: true, message: "请输入访问地址", trigger: "blur" },
{ type: 'url' as const, message: "请输入正确的URL格式", trigger: "blur" } { type: 'url' as const, message: "请输入正确的URL格式", trigger: "blur" }
@@ -298,6 +317,11 @@ async function handleQuery() {
await loadApplicationList(); await loadApplicationList();
} }
// 选择创建人后触发查询
function handleConfirm() {
handleQuery();
}
// 重置查询 // 重置查询
async function handleResetQuery() { async function handleResetQuery() {
queryFormRef.value?.resetFields(); queryFormRef.value?.resetFields();
@@ -305,19 +329,6 @@ async function handleResetQuery() {
await loadApplicationList(); await loadApplicationList();
} }
// 分页大小改变
async function handleSizeChange(size: number) {
queryFormData.page_size = size;
queryFormData.page_no = 1;
await loadApplicationList();
}
// 当前页改变
async function handleCurrentChange(page: number) {
queryFormData.page_no = page;
await loadApplicationList();
}
// 创建应用 // 创建应用
function handleCreateApp() { function handleCreateApp() {
console.log('handleCreateApp'); console.log('handleCreateApp');
@@ -465,19 +476,6 @@ onMounted(() => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.app-container {
padding: 20px;
background-color: var(--el-bg-color-page);
min-height: calc(100vh - 84px);
}
.search-container {
background: var(--el-bg-color);
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.app-grid-card { .app-grid-card {
height: calc(100vh - 200px); height: calc(100vh - 200px);
position: relative; position: relative;
@@ -486,6 +484,8 @@ onMounted(() => {
:deep(.el-card__footer) { :deep(.el-card__footer) {
margin-top: auto; margin-top: auto;
display: flex;
justify-content: flex-end;
} }
} }
@@ -493,14 +493,11 @@ onMounted(() => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
font-weight: 600;
} }
.app-grid { .app-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 16px;
padding: 20px 0;
flex: 1; flex: 1;
} }
@@ -508,13 +505,6 @@ onMounted(() => {
border: 1px solid var(--el-border-color-lighter); border: 1px solid var(--el-border-color-lighter);
border-radius: 6px; border-radius: 6px;
transition: all 0.3s ease; transition: all 0.3s ease;
min-height: 180px;
width: 100%;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
} }
.app-card-header-el { .app-card-header-el {
@@ -531,14 +521,6 @@ onMounted(() => {
min-width: 0; min-width: 0;
} }
.app-avatar-el {
flex-shrink: 0;
}
.app-title-section {
flex: 1;
min-width: 0;
}
.app-name-el { .app-name-el {
font-size: 16px; font-size: 16px;
@@ -551,18 +533,7 @@ onMounted(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.app-status-tag {
margin-top: 2px;
}
.app-menu-btn {
padding: 4px;
color: var(--el-text-color-secondary);
&:hover {
color: var(--el-color-primary);
}
}
.app-card-content { .app-card-content {
margin: 12px 0; margin: 12px 0;
@@ -571,8 +542,6 @@ onMounted(() => {
.app-description { .app-description {
font-size: 14px; font-size: 14px;
color: var(--el-text-color-regular); color: var(--el-text-color-regular);
line-height: 1.5;
display: -webkit-box; display: -webkit-box;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
line-clamp: 2; line-clamp: 2;
@@ -625,7 +594,6 @@ onMounted(() => {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
max-width: 100%;
} }
.app-card-actions { .app-card-actions {
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":"> <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
<el-form-item prop="name" label="名称"> <el-form-item prop="name" label="名称">
<el-input v-model="queryFormData.name" placeholder="请输入名称" clearable /> <el-input v-model="queryFormData.name" placeholder="请输入名称" clearable />
</el-form-item> </el-form-item>
@@ -22,7 +22,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery"> <el-button type="primary" icon="search" native-type="submit">
查询 查询
</el-button> </el-button>
<el-button icon="refresh" @click="handleResetQuery"> <el-button icon="refresh" @click="handleResetQuery">
@@ -2,7 +2,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryRef" :model="queryFormData" :inline="true" label-suffix=":"> <el-form ref="queryRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
<el-form-item label="表名称" prop="table_name"> <el-form-item label="表名称" prop="table_name">
<el-input v-model="queryFormData.table_name" placeholder="请输入表名称" clearable style="width: 200px" @keyup.enter="handleQuery"/> <el-input v-model="queryFormData.table_name" placeholder="请输入表名称" clearable style="width: 200px" @keyup.enter="handleQuery"/>
</el-form-item> </el-form-item>
@@ -13,7 +13,7 @@
<el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"/> <el-date-picker v-model="dateRange" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"/>
</el-form-item> </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" @click="handleQuery">查询</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>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -1557,7 +1557,7 @@ async function loadTableDetail(id: number | string) {
} }
} }
// ===== 生命周期函数 ===== // ===== 生命周期函数
/** 组件挂载时 */ /** 组件挂载时 */
onMounted(() => { onMounted(() => {
@@ -8,6 +8,7 @@
:model="queryFormData" :model="queryFormData"
label-suffix=":" label-suffix=":"
:inline="true" :inline="true"
@submit.prevent="handleQuery"
> >
<el-form-item prop="name" label="名称"> <el-form-item prop="name" label="名称">
<el-input v-model="queryFormData.name" placeholder="请输入名称" clearable /> <el-input v-model="queryFormData.name" placeholder="请输入名称" clearable />
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="ipaddr" label="IP地址"> <el-form-item prop="ipaddr" label="IP地址">
<el-input v-model="queryFormData.ipaddr" placeholder="请输入IP地址" clearable /> <el-input v-model="queryFormData.ipaddr" placeholder="请输入IP地址" clearable />
</el-form-item> </el-form-item>
@@ -15,7 +15,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['monitor:online:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['monitor:online:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['monitor:online:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['monitor:online:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
+14 -7
View File
@@ -2,12 +2,18 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":"> <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
<el-form-item prop="name" label="关键词"> <el-form-item prop="name" label="关键词">
<el-input v-model="queryFormData.name" placeholder="请输入文件名或目录名" clearable /> <el-input
v-model="queryFormData.name"
placeholder="请输入文件名或目录名"
clearable
style="width: 200px"
@keyup.enter="handleQuery"
/>
</el-form-item> </el-form-item>
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['monitor:resource:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['monitor:resource:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['monitor:resource:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['monitor:resource:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
</el-form-item> </el-form-item>
</el-form> </el-form>
@@ -106,8 +112,8 @@
<template #empty> <template #empty>
<el-empty :image-size="80" description="暂无数据" /> <el-empty :image-size="80" description="暂无数据" />
</template> </template>
<el-table-column type="selection" min-width="55" align="center" /> <el-table-column type="selection" min-width="40" align="center" />
<el-table-column type="index" fixed label="序号" min-width="60" /> <el-table-column type="index" fixed label="序号" min-width="40" />
<el-table-column label="名称" prop="name" min-width="200"> <el-table-column label="名称" prop="name" min-width="200">
<template #default="{ row }"> <template #default="{ row }">
<div class="file-name"> <div class="file-name">
@@ -208,8 +214,8 @@
将文件拖到此处,或<em>点击上传</em> 将文件拖到此处,或<em>点击上传</em>
</div> </div>
<template #tip> <template #tip>
<div class="el-upload__tip"> <div class="el-upload__tip" style="color: red;">
支持多文件上传,单个文件不超过100MB 不支持多文件上传,单个文件不超过100MB,多文件上传,取最后一个文件上传
</div> </div>
</template> </template>
</el-upload> </el-upload>
@@ -719,6 +725,7 @@ onMounted(() => {
.grid-view { .grid-view {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
height: calc(100vh - 200px);
gap: 20px; gap: 20px;
.grid-item { .grid-item {
+1 -1
View File
@@ -1,5 +1,5 @@
<template> <template>
<div class="app-container"> <div class="app-container" >
<el-row :gutter="16"> <el-row :gutter="16">
<el-col :span="12" class="mb-4"> <el-col :span="12" class="mb-4">
<el-card :loading="loading" shadow="hover"> <el-card :loading="loading" shadow="hover">
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="name" label="部门名称"> <el-form-item prop="name" label="部门名称">
<el-input v-model="queryFormData.name" placeholder="请输入部门名称" clearable /> <el-input v-model="queryFormData.name" placeholder="请输入部门名称" clearable />
</el-form-item> </el-form-item>
@@ -22,7 +22,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:dept:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['system:dept:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['system:dept:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['system:dept:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
@@ -3,7 +3,7 @@
<el-drawer v-model="drawerVisible" :title="'【' + props.dictLabel + '】字典数据'" :size="drawerSize"> <el-drawer v-model="drawerVisible" :title="'【' + props.dictLabel + '】字典数据'" :size="drawerSize">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
<el-form-item prop="dict_label" label="字典数据标签"> <el-form-item prop="dict_label" label="字典数据标签">
<el-input v-model="queryFormData.dict_label" placeholder="请输入字典标签" clearable /> <el-input v-model="queryFormData.dict_label" placeholder="请输入字典标签" clearable />
@@ -30,7 +30,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button type="primary" icon="search" native-type="submit">查询</el-button>
<el-button icon="refresh" @click="handleResetQuery">重置</el-button> <el-button icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="dict_name" label="字典名称"> <el-form-item prop="dict_name" label="字典名称">
<el-input v-model="queryFormData.dict_name" placeholder="请输入字典名称" clearable /> <el-input v-model="queryFormData.dict_name" placeholder="请输入字典名称" clearable />
</el-form-item> </el-form-item>
@@ -32,7 +32,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:dict_type:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['system:dict_type:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['system:dict_type:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['system:dict_type:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
+3 -3
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="request_path" label="请求路径"> <el-form-item prop="request_path" label="请求路径">
<el-input v-model="queryFormData.request_path" placeholder="请输入请求路径" clearable /> <el-input v-model="queryFormData.request_path" placeholder="请输入请求路径" clearable />
</el-form-item> </el-form-item>
@@ -29,7 +29,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:log:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['system:log:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['system:log:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['system:log:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
@@ -154,7 +154,7 @@
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog"> <el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
<!-- 详情 --> <!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'"> <template v-if="dialogVisible.type === 'detail'">
<el-descriptions :column="8" border label-width="140px"> <el-descriptions :column="8" border label-width="200px">
<el-descriptions-item label="日志类型" :span="2"> <el-descriptions-item label="日志类型" :span="2">
<el-tag :type="formData.type === 1 ? 'success' : 'primary'"> <el-tag :type="formData.type === 1 ? 'success' : 'primary'">
{{ formData.type === 1 ? '登录日志' : '操作日志' }} {{ formData.type === 1 ? '登录日志' : '操作日志' }}
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="name" label="菜单名称"> <el-form-item prop="name" label="菜单名称">
<el-input v-model="queryFormData.name" placeholder="请输入菜单名称" clearable /> <el-input v-model="queryFormData.name" placeholder="请输入菜单名称" clearable />
</el-form-item> </el-form-item>
@@ -22,7 +22,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:menu:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['system:menu:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['system:menu:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['system:menu:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":"> <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery">
<el-form-item prop="notice_title" label="标题"> <el-form-item prop="notice_title" label="标题">
<el-input v-model="queryFormData.notice_title" placeholder="请输入标题" clearable /> <el-input v-model="queryFormData.notice_title" placeholder="请输入标题" clearable />
</el-form-item> </el-form-item>
@@ -39,7 +39,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:notice:query']" type="primary" icon="search" @click="handleQuery"> <el-button v-hasPerm="['system:notice:query']" type="primary" icon="search" native-type="submit">
查询 查询
</el-button> </el-button>
<el-button v-hasPerm="['system:notice:query']" icon="refresh" @click="handleResetQuery"> <el-button v-hasPerm="['system:notice:query']" icon="refresh" @click="handleResetQuery">
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="config_name" label="配置名称"> <el-form-item prop="config_name" label="配置名称">
<el-input v-model="queryFormData.config_name" placeholder="请输入配置名称" clearable /> <el-input v-model="queryFormData.config_name" placeholder="请输入配置名称" clearable />
</el-form-item> </el-form-item>
@@ -31,7 +31,7 @@
/> />
</el-form-item> </el-form-item>
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:config:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['system:config:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['system:config:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['system:config:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="name" label="岗位名称"> <el-form-item prop="name" label="岗位名称">
<el-input v-model="queryFormData.name" placeholder="请输入岗位名称" clearable /> <el-input v-model="queryFormData.name" placeholder="请输入岗位名称" clearable />
</el-form-item> </el-form-item>
@@ -29,7 +29,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:position:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['system:position:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['system:position:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['system:position:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
+2 -2
View File
@@ -3,7 +3,7 @@
<div class="app-container"> <div class="app-container">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="name" label="角色名称"> <el-form-item prop="name" label="角色名称">
<el-input v-model="queryFormData.name" placeholder="请输入角色名称" clearable /> <el-input v-model="queryFormData.name" placeholder="请输入角色名称" clearable />
</el-form-item> </el-form-item>
@@ -29,7 +29,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:role:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['system:role:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['system:role:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['system:role:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">
+2 -2
View File
@@ -11,7 +11,7 @@
<el-col :lg="20" :xs="24"> <el-col :lg="20" :xs="24">
<!-- 搜索区域 --> <!-- 搜索区域 -->
<div class="search-container"> <div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" > <el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":" @submit.prevent="handleQuery" >
<el-form-item prop="username" label="账号"> <el-form-item prop="username" label="账号">
<el-input v-model="queryFormData.username" placeholder="请输入账号" clearable /> <el-input v-model="queryFormData.username" placeholder="请输入账号" clearable />
</el-form-item> </el-form-item>
@@ -40,7 +40,7 @@
</el-form-item> </el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button v-hasPerm="['system:user:query']" type="primary" icon="search" @click="handleQuery">查询</el-button> <el-button v-hasPerm="['system:user:query']" type="primary" icon="search" native-type="submit">查询</el-button>
<el-button v-hasPerm="['system:user:query']" icon="refresh" @click="handleResetQuery">重置</el-button> <el-button v-hasPerm="['system:user:query']" icon="refresh" @click="handleResetQuery">重置</el-button>
<!-- 展开/收起 --> <!-- 展开/收起 -->
<template v-if="isExpandable"> <template v-if="isExpandable">