mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-14 05:00:10 +00:00
@@ -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('字典标签不能为空')
|
||||||
|
|||||||
@@ -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):
|
||||||
"""更新"""
|
"""更新"""
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
# 使用线程池执行异步操作以避免阻塞调度器和数据库锁定问题
|
# 使用线程池执行异步操作以避免阻塞调度器和数据库锁定问题
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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 -*-
|
||||||
+8
-4
@@ -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 }}列表成功")
|
||||||
|
|
||||||
+19
-6
@@ -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
|
||||||
|
)
|
||||||
+3
-2
@@ -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 }}')
|
||||||
+28
-1
@@ -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 %}
|
||||||
}
|
}
|
||||||
+283
-93
@@ -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,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>
|
||||||
|
|||||||
@@ -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>
|
||||||
<!-- 开始日期和结束日期 -->
|
<!-- 开始日期和结束日期 -->
|
||||||
|
|||||||
@@ -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 />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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,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 ? '登录日志' : '操作日志' }}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user