Compare commits

..
75 Commits
Author SHA1 Message Date
insistence b974e8bd3d !38 RuoYi-Vue3-FastAPI v1.7.1
Merge pull request !38 from insistence/develop
2025-10-14 03:09:29 +00:00
insistence a389ef66bc docs: 更新README文档 2025-10-14 11:05:47 +08:00
insistence dcdeaa0394 chore: 升级版本至1.7.1 2025-10-14 11:05:39 +08:00
insistence f4d7d7a416 chore&fix: 移除passlib直接使用bcrypt修复密码校验异常的问题 报错 #48 #49 2025-10-14 11:05:20 +08:00
insistence f5a91c0ece perf: 代码生成do模板补充表描述 2025-09-05 09:21:40 +08:00
insistence 5ba6587b6c fix: 修复代码生成controller模板编辑接口异常生成字段的问题 2025-09-05 09:21:29 +08:00
insistence f2de18aa43 !36 docs: 更新README文档
Merge pull request !36 from insistence/develop
2025-09-04 02:10:06 +00:00
insistence 24cc7e360a docs: 更新README文档 2025-09-04 09:45:19 +08:00
insistence 3c5f6bf873 chore: 升级版本至1.7.0 2025-09-04 09:45:07 +08:00
insistence 81c9b82abd perf: 优化样式 2025-09-03 17:20:25 +08:00
insistence ac3056b9a7 feat: 显示列信息支持对象格式 2025-09-03 17:18:37 +08:00
insistence 10719a4067 feat: 注册账号设置默认密码最后更新时间 2025-09-03 17:01:25 +08:00
insistence 5597eb651f feat: 账号密码支持自定义更新周期 2025-09-03 16:58:30 +08:00
insistence 385013db9d feat: 初始密码支持自定义修改策略 2025-09-03 16:44:31 +08:00
insistence 2699039c0b perf: 优化布局显示 2025-09-03 16:44:01 +08:00
insistence 831768dafd feat: 新增apscheduler_jobs表对应sqlalchemy模型类 2025-09-03 16:37:41 +08:00
insistence 6ea37823c3 feat&refactor: 菜单搜索支持键盘选择&悬浮主题背景 2025-08-29 16:13:31 +08:00
insistence 67e9f6c98b perf: 优化导航栏显示昵称&设置 2025-08-29 15:52:06 +08:00
insistence f19f192170 feat: 用户导入新增验证提示 2025-08-29 15:51:07 +08:00
insistence 7eb4b75ab1 feat: 用户归属部门新增清除 2025-08-29 15:46:50 +08:00
insistence d0138516e7 perf: 优化checkbox废弃API 2025-08-29 15:44:33 +08:00
insistence ae4f37f1c5 perf: 消除控制台警告信息 2025-08-29 15:42:13 +08:00
insistence 41a9d1edf6 chore: 升级组件依赖 2025-08-29 15:39:07 +08:00
insistence 61fba98d89 feat: 新增底部版权信息及开关 2025-08-29 14:47:44 +08:00
insistence cc6a35d7c7 feat: 新增页签图标显示开关功能 2025-08-29 11:33:05 +08:00
insistence ea0303835f feat: 新增用户默认初始化密码 2025-08-29 08:29:05 +08:00
insistence 7b993655ba perf: 优化代码 2025-08-29 08:21:15 +08:00
insistence d9d7c94513 feat: 代码生成列支持拖动排序 2025-08-27 09:26:01 +08:00
insistence cc6b38acdd feat: 图片上传组件新增disabled属性 2025-08-27 09:25:38 +08:00
insistence 27d847fd25 fix: 修复上传组件被多次引用拖动仅对第一个有效的问题 2025-08-27 08:54:10 +08:00
insistence 9e76400c8e feat: 上传组件新增拖动排序属性 2025-08-27 08:50:07 +08:00
insistence c365459d88 feat: 外链加载时遮罩信息提示 2025-08-27 08:43:17 +08:00
insistence 079b2eefb6 feat: 添加页签openPage支持传递参数 2025-08-27 08:38:43 +08:00
insistence c6d4c6c1a9 refactor: 富文本复制粘贴图片上传至url 2025-08-27 08:33:38 +08:00
insistence 9b6355c68d perf: 优化富文本控制台警告异常 2025-08-27 08:28:55 +08:00
insistence ccacee5f95 perf: 优化代码 2025-08-27 08:25:35 +08:00
insistence 397a51bd96 feat: 显隐列组件支持全选/全不选 2025-08-26 15:07:51 +08:00
insistence f144945215 perf: 优化代码 2025-08-26 11:16:17 +08:00
insistence a1c78172ca feat: 新增默认打包配置项 2025-08-26 11:08:34 +08:00
insistence f03ab1a845 feat: 支持文件&图片组件自定义地址&参数 2025-08-26 11:04:37 +08:00
insistence a64eca3aeb perf: 优化角色禁用不允许分配 2025-08-26 10:51:27 +08:00
insistence bc547f90f9 perf: 登录页和注册页表头使用VITE_APP_BASE_API配置值 2025-08-26 10:42:31 +08:00
insistence 7a66854a22 perf: 优化顶部菜单搜索栏为多层级显示 2025-08-26 10:28:18 +08:00
insistence 795a9204ae perf: 优化前端处理路由函数代码 2025-08-26 10:23:49 +08:00
insistence 11debaf213 refactor: 调整do与sql使其相互适配 2025-08-22 16:17:46 +08:00
insistence 857b7dba80 chore: 更新依赖文件 2025-08-22 16:17:33 +08:00
insistence 5d6bd822d3 perf: 优化启动信息显示 2025-08-21 17:20:55 +08:00
insistence 0b7df2cfc1 feat: 集成alembic以支持数据迁移 2025-08-21 17:20:13 +08:00
insistence 20bc7ebaaa chore: 更新依赖文件 2025-08-04 10:56:14 +08:00
insistence c1a8060139 refactor: 重构IP归属区域查询为异步调用 2025-08-04 10:55:43 +08:00
insistence 5558dd181b perf: 优化接口耗时计算 2025-07-28 15:07:31 +08:00
insistence 0fd4fb283a fix: 修复logout接口未按照app_same_time_login配置项动态判断的问题 #IBZZ1S 2025-07-28 14:55:21 +08:00
insistence 00a7af2d5b !30 RuoYi-Vue3-FastAPI 1.6.2
Merge pull request !30 from insistence/develop
2025-03-19 07:59:40 +00:00
insistence e34a493b3e docs: 更新README文档 2025-03-19 15:55:10 +08:00
insistence a9df9ea56a chore: 升级版本至1.6.2 2025-03-19 15:54:12 +08:00
insistence d89c0df425 perf: 优化代码生成vue模板 #23 2025-03-19 15:46:28 +08:00
insistence d0730e7993 fix: 修复用户导出缺失部门名称的问题 2025-03-18 16:55:16 +08:00
insistence 5fe376f978 perf: pagination更换成flex布局 2025-03-18 08:28:46 +08:00
insistence dd5b1c55c9 feat: 文件上传组件新增类型 2025-03-18 08:12:13 +08:00
insistence 7645c6d9fd feat: 文件上传组件新增disabled属性 2025-03-18 08:11:18 +08:00
insistence 61073970fa fix: 修复代码生成模板时间查询问题 #28 2025-03-17 16:32:46 +08:00
insistence 9ae2ac02eb perf: 优化代码生成新增和编辑字段显示和渲染 2025-03-17 09:51:05 +08:00
insistence 6622c329fc fix: 修复修改字典类型时字典数据更新时间异常的问题 2025-03-17 08:27:39 +08:00
insistence b66d545985 fix: 修复修改字典类型时获取dict_code异常的问题 2025-03-17 08:25:39 +08:00
insistence 1789cb5a9b fix: 修复定时任务状态暂停时执行单次任务会触发cron表达式的问题 #31 2025-03-15 21:59:28 +08:00
insistence ee376c477d fix: 修复日志管理时间查询报错 #27 2025-03-09 18:43:57 +08:00
jiangbaihe 2b6f0905a9 chore: 调整.gitignore策略 2025-03-04 14:58:04 +08:00
insistence c953754bed chore: pg依赖文件新增sqlglot依赖 2025-03-03 17:13:37 +08:00
insistence cea4a10baa !29 RuoYi-Vue3-FastAPI v1.6.1
Merge pull request !29 from insistence/develop
2025-03-03 08:50:47 +00:00
insistence b708d86eff docs: 更新README文档 2025-03-03 16:44:31 +08:00
insistence 28aab8c7d4 chore: 升级版本至1.6.1 2025-03-03 16:43:47 +08:00
insistence 9412e3f344 fix: 修复代码生成主子表vo模板可能缺失NotBlank的问题 2025-02-28 10:46:45 +08:00
insistence b4d5619b1b fix: 引入泛型修复as_query和as_form装饰模型文档丢失的问题 2025-02-28 10:36:29 +08:00
insistence 8ce598ad54 fix: 修复代码生成字段唯一性校验dao层模板判断异常的问题 2025-02-26 09:00:35 +08:00
insistence ca641055e0 fix: 引入sqlglot修复sql语句解析异常的问题 2025-02-21 15:40:47 +08:00
106 changed files with 2032 additions and 780 deletions
+7
View File
@@ -136,3 +136,10 @@ dmypy.json
# Cython debug symbols
cython_debug/
# PyCharm
.idea/
# VSCode
.vscode/
+3 -2
View File
@@ -1,13 +1,14 @@
<p align="center">
<img alt="logo" src="https://oscimg.oschina.net/oscnet/up-d3d0a9303e11d522a06cd263f3079027715.png">
</p>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.6.0</h1>
<h1 align="center" style="margin: 30px 0 30px; font-weight: bold;">RuoYi-Vue3-FastAPI v1.7.1</h1>
<h4 align="center">基于RuoYi-Vue3+FastAPI前后端分离的快速开发框架</h4>
<p align="center">
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/stargazers"><img src="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/badge/star.svg?theme=dark"></a>
<a href="https://github.com/insistence/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/github/stars/insistence/RuoYi-Vue3-FastAPI?style=social"></a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.6.0-brightgreen.svg"></a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI"><img src="https://img.shields.io/badge/RuoYiVue3FastAPI-v1.7.1-brightgreen.svg"></a>
<a href="https://gitee.com/insistence2022/RuoYi-Vue3-FastAPI/blob/master/LICENSE"><img src="https://img.shields.io/github/license/mashape/apistatus.svg"></a>
<img src="https://img.shields.io/badge/node-≥18-blue">
<img src="https://img.shields.io/badge/python-≥3.9-blue">
<img src="https://img.shields.io/badge/MySQL-≥5.7-blue">
</p>
+1 -1
View File
@@ -10,7 +10,7 @@ APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.6.0'
APP_VERSION= '1.7.1'
# 应用是否开启热重载
APP_RELOAD = true
# 应用是否开启IP归属区域查询
+1 -1
View File
@@ -10,7 +10,7 @@ APP_HOST = '0.0.0.0'
# 应用端口
APP_PORT = 9099
# 应用版本
APP_VERSION= '1.6.0'
APP_VERSION= '1.7.1'
# 应用是否开启热重载
APP_RELOAD = false
# 应用是否开启IP归属区域查询
+153
View File
@@ -0,0 +1,153 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the python>=3.9 or backports.zoneinfo library and tzdata library.
# Any required deps can installed by adding `alembic[tz]` to the pip requirements
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
[settings]
# This section is used to set environment variables for the alembic.
# The env option is used to specify the environment for the alembic.
# It can be set to dev or prod.
env = dev
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration with an async dbapi.
+116
View File
@@ -0,0 +1,116 @@
import asyncio
import os
from alembic import context
from logging.config import fileConfig
from sqlalchemy import pool
from sqlalchemy.engine import Connection
from sqlalchemy.ext.asyncio import async_engine_from_config
from config.database import Base, ASYNC_SQLALCHEMY_DATABASE_URL
from utils.import_util import ImportUtil
# 判断vesrions目录是否存在,如果不存在则创建
alembic_veresions_path = 'alembic/versions'
if not os.path.exists(alembic_veresions_path):
os.makedirs(alembic_veresions_path)
# 自动查找所有模型
found_models = ImportUtil.find_models(Base)
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
alembic_config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
if alembic_config.config_file_name is not None:
fileConfig(alembic_config.config_file_name)
# add your model's MetaData object here
# for 'autogenerate' support
target_metadata = Base.metadata
# ASYNC_SQLALCHEMY_DATABASE_URL = 'mysql+asyncmy://root:mysqlroot@127.0.0.1:3306/ruoyi-fastapi'
# other values from the config, defined by the needs of env.py,
alembic_config.set_main_option('sqlalchemy.url', ASYNC_SQLALCHEMY_DATABASE_URL)
def run_migrations_offline() -> None:
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = alembic_config.get_main_option('sqlalchemy.url')
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={'paramstyle': 'named'},
)
with context.begin_transaction():
context.run_migrations()
def do_run_migrations(connection: Connection) -> None:
def process_revision_directives(context, revision, directives):
script = directives[0]
# 检查所有操作集是否为空
all_empty = all(ops.is_empty() for ops in script.upgrade_ops_list)
if all_empty:
# 如果没有实际变更,不生成迁移文件
directives[:] = []
print('❎️ 未检测到模型变更,不生成迁移文件')
else:
print('✅️ 检测到模型变更,生成迁移文件')
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
transaction_per_migration=True,
process_revision_directives=process_revision_directives,
)
with context.begin_transaction():
context.run_migrations()
async def run_async_migrations() -> None:
"""In this scenario we need to create an Engine
and associate a connection with the context.
"""
connectable = async_engine_from_config(
alembic_config.get_section(alembic_config.config_ini_section, {}),
prefix='sqlalchemy.',
poolclass=pool.NullPool,
)
async with connectable.connect() as connection:
await connection.run_sync(do_run_migrations)
await connectable.dispose()
def run_migrations_online() -> None:
"""Run migrations in 'online' mode."""
asyncio.run(run_async_migrations())
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
+2
View File
@@ -252,6 +252,8 @@ class GenConstant:
'double',
'decimal',
]
COLUMNNAME_NOT_ADD_SHOW = ['create_by', 'create_time']
COLUMNNAME_NOT_EDIT_SHOW = ['update_by', 'update_time']
COLUMNNAME_NOT_EDIT = ['id', 'create_by', 'create_time', 'del_flag']
COLUMNNAME_NOT_LIST = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time']
COLUMNNAME_NOT_QUERY = ['id', 'create_by', 'create_time', 'del_flag', 'update_by', 'update_time', 'remark']
+18 -1
View File
@@ -1,8 +1,10 @@
import argparse
import configparser
import os
import sys
from dotenv import load_dotenv
from functools import lru_cache
from pydantic import computed_field
from pydantic_settings import BaseSettings
from typing import Literal
@@ -51,6 +53,13 @@ class DataBaseSettings(BaseSettings):
db_pool_recycle: int = 3600
db_pool_timeout: int = 30
@computed_field
@property
def sqlglot_parse_dialect(self) -> str:
if self.db_type == 'postgresql':
return 'postgres'
return self.db_type
class RedisSettings(BaseSettings):
"""
@@ -198,7 +207,15 @@ class GetConfig:
"""
解析命令行参数
"""
if 'uvicorn' in sys.argv[0]:
# 检查是否在alembic环境中运行,如果是则跳过参数解析
if 'alembic' in sys.argv[0] or any('alembic' in arg for arg in sys.argv):
ini_config = configparser.ConfigParser()
ini_config.read('alembic.ini', encoding='utf-8')
if 'settings' in ini_config:
# 获取env选项
env_value = ini_config['settings'].get('env')
os.environ['APP_ENV'] = env_value if env_value else 'dev'
elif 'uvicorn' in sys.argv[0]:
# 使用uvicorn启动时,命令行参数需要按照uvicorn的文档进行配置,无法自定义参数
pass
else:
+2 -2
View File
@@ -18,7 +18,7 @@ async def init_create_table():
:return:
"""
logger.info('初始化数据库连接...')
logger.info('🔎 初始化数据库连接...')
async with async_engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info('数据库连接成功')
logger.info('✅️ 数据库连接成功')
+7 -7
View File
@@ -19,7 +19,7 @@ class RedisUtil:
:return: Redis连接对象
"""
logger.info('开始连接redis...')
logger.info('🔎 开始连接redis...')
redis = await aioredis.from_url(
url=f'redis://{RedisConfig.redis_host}',
port=RedisConfig.redis_port,
@@ -32,15 +32,15 @@ class RedisUtil:
try:
connection = await redis.ping()
if connection:
logger.info('redis连接成功')
logger.info('✅️ redis连接成功')
else:
logger.error('redis连接失败')
logger.error('❌️ redis连接失败')
except AuthenticationError as e:
logger.error(f'redis用户名或密码错误,详细错误信息:{e}')
logger.error(f'❌️ redis用户名或密码错误,详细错误信息:{e}')
except TimeoutError as e:
logger.error(f'redis连接超时,详细错误信息:{e}')
logger.error(f'❌️ redis连接超时,详细错误信息:{e}')
except RedisError as e:
logger.error(f'redis连接错误,详细错误信息:{e}')
logger.error(f'❌️ redis连接错误,详细错误信息:{e}')
return redis
@classmethod
@@ -52,7 +52,7 @@ class RedisUtil:
:return:
"""
await app.state.redis.close()
logger.info('关闭redis连接成功')
logger.info('✅️ 关闭redis连接成功')
@classmethod
async def init_sys_dict(cls, redis):
@@ -131,7 +131,7 @@ class SchedulerUtil:
:return:
"""
logger.info('开始启动定时任务...')
logger.info('🔎 开始启动定时任务...')
scheduler.start()
async with AsyncSessionLocal() as session:
job_list = await JobDao.get_job_list_for_scheduler(session)
@@ -139,7 +139,7 @@ class SchedulerUtil:
cls.remove_scheduler_job(job_id=str(item.job_id))
cls.add_scheduler_job(item)
scheduler.add_listener(cls.scheduler_event_listener, EVENT_ALL)
logger.info('系统初始定时任务加载成功')
logger.info('✅️ 系统初始定时任务加载成功')
@classmethod
async def close_system_scheduler(cls):
@@ -149,7 +149,7 @@ class SchedulerUtil:
:return:
"""
scheduler.shutdown()
logger.info('关闭定时任务成功')
logger.info('✅️ 关闭定时任务成功')
@classmethod
def get_scheduler_job(cls, job_id: Union[str, int]):
@@ -201,9 +201,12 @@ class SchedulerUtil:
job_executor = job_info.job_executor
if iscoroutinefunction(job_func):
job_executor = 'default'
job_trigger = DateTrigger()
if job_info.status == '0':
job_trigger = OrTrigger(triggers=[DateTrigger(), MyCronTrigger.from_crontab(job_info.cron_expression)])
scheduler.add_job(
func=eval(job_info.invoke_target),
trigger=OrTrigger(triggers=[DateTrigger(), MyCronTrigger.from_crontab(job_info.cron_expression)]),
trigger=job_trigger,
args=job_info.job_args.split(',') if job_info.job_args else None,
kwargs=json.loads(job_info.job_kwargs) if job_info.job_kwargs else None,
id=str(job_info.job_id),
@@ -1,12 +1,13 @@
import httpx
import inspect
import json
import os
import requests
import time
from async_lru import alru_cache
from datetime import datetime
from fastapi import Request
from fastapi.responses import JSONResponse, ORJSONResponse, UJSONResponse
from functools import lru_cache, wraps
from functools import wraps
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Any, Callable, Literal, Optional
from user_agents import parse
@@ -46,7 +47,7 @@ class Log:
def __call__(self, func):
@wraps(func)
async def wrapper(*args, **kwargs):
start_time = time.time()
start_time = time.perf_counter()
# 获取被装饰函数的文件路径
file_path = inspect.getfile(func)
# 获取项目根路径
@@ -74,7 +75,7 @@ class Log:
oper_ip = request.headers.get('X-Forwarded-For')
oper_location = '内网IP'
if AppConfig.app_ip_location_query:
oper_location = get_ip_location(oper_ip)
oper_location = await get_ip_location(oper_ip)
# 根据不同的请求类型使用不同的方法获取请求参数
content_type = request.headers.get('Content-Type')
if content_type and (
@@ -129,7 +130,7 @@ class Log:
logger.exception(e)
result = ResponseUtil.error(msg=str(e))
# 获取请求耗时
cost_time = float(time.time() - start_time) * 100
cost_time = float(time.perf_counter() - start_time) * 1000
# 判断请求是否来自api文档
request_from_swagger = (
request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False
@@ -203,8 +204,8 @@ class Log:
return wrapper
@lru_cache()
def get_ip_location(oper_ip: str):
@alru_cache()
async def get_ip_location(oper_ip: str):
"""
查询ip归属区域
@@ -215,12 +216,13 @@ def get_ip_location(oper_ip: str):
try:
if oper_ip != '127.0.0.1' and oper_ip != 'localhost':
oper_location = '未知'
ip_result = requests.get(f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={oper_ip}')
if ip_result.status_code == 200:
prov = ip_result.json().get('data').get('prov')
city = ip_result.json().get('data').get('city')
if prov or city:
oper_location = f'{prov}-{city}'
async with httpx.AsyncClient() as client:
ip_result = await client.get(f'https://qifu-api.baidubce.com/ip/geo/v1/district?ip={oper_ip}')
if ip_result.status_code == 200:
prov = ip_result.json().get('data', {}).get('prov')
city = ip_result.json().get('data', {}).get('city')
if prov or city:
oper_location = f'{prov}-{city}'
except Exception as e:
oper_location = '未知'
print(e)
@@ -2,10 +2,13 @@ import inspect
from fastapi import Form, Query
from pydantic import BaseModel
from pydantic.fields import FieldInfo
from typing import Type
from typing import Type, TypeVar
def as_query(cls: Type[BaseModel]):
BaseModelVar = TypeVar('BaseModelVar', bound=BaseModel)
def as_query(cls: Type[BaseModelVar]) -> Type[BaseModelVar]:
"""
pydantic模型查询参数装饰器,将pydantic模型用于接收查询参数
"""
@@ -43,7 +46,7 @@ def as_query(cls: Type[BaseModel]):
return cls
def as_form(cls: Type[BaseModel]):
def as_form(cls: Type[BaseModelVar]) -> Type[BaseModelVar]:
"""
pydantic模型表单参数装饰器,将pydantic模型用于接收表单参数
"""
@@ -141,8 +141,11 @@ async def logout(request: Request, token: Optional[str] = Depends(oauth2_scheme)
payload = jwt.decode(
token, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm], options={'verify_exp': False}
)
session_id: str = payload.get('session_id')
await LoginService.logout_services(request, session_id)
if AppConfig.app_same_time_login:
token_id: str = payload.get('session_id')
else:
token_id: str = payload.get('user_id')
await LoginService.logout_services(request, token_id)
logger.info('退出成功')
return ResponseUtil.success(msg='退出成功')
@@ -165,6 +165,7 @@ async def reset_system_user_pwd(
edit_user = EditUserModel(
userId=reset_user.user_id,
password=PwdUtil.get_password_hash(reset_user.password),
pwdUpdateDate=datetime.now(),
updateBy=current_user.user.user_name,
updateTime=datetime.now(),
type='pwd',
@@ -304,6 +305,7 @@ async def reset_system_user_password(
userId=current_user.user.user_id,
oldPassword=reset_password.old_password,
password=reset_password.new_password,
pwdUpdateDate=datetime.now(),
updateBy=current_user.user.user_name,
updateTime=datetime.now(),
)
@@ -104,15 +104,24 @@ class JobDao:
return db_job
@classmethod
async def edit_job_dao(cls, db: AsyncSession, job: dict):
async def edit_job_dao(cls, db: AsyncSession, job: dict, old_job: JobModel):
"""
编辑定时任务数据库操作
:param db: orm对象
:param job: 需要更新的定时任务字典
:param old_job: 原定时任务对象
:return:
"""
await db.execute(update(SysJob), [job])
await db.execute(
update(SysJob)
.where(
SysJob.job_id == old_job.job_id,
SysJob.job_name == old_job.job_name,
SysJob.job_group == old_job.job_group,
)
.values(**job)
)
@classmethod
async def delete_job_dao(cls, db: AsyncSession, job: JobModel):
@@ -5,6 +5,7 @@ from module_admin.entity.do.log_do import SysLogininfor, SysOperLog
from module_admin.entity.vo.log_vo import LogininforModel, LoginLogPageQueryModel, OperLogModel, OperLogPageQueryModel
from utils.common_util import SnakeCaseUtil
from utils.page_util import PageUtil
from utils.time_format_util import TimeFormatUtil
class OperationLogDao:
@@ -38,8 +39,8 @@ class OperationLogDao:
SysOperLog.business_type == query_object.business_type if query_object.business_type else True,
SysOperLog.status == query_object.status if query_object.status else True,
SysOperLog.oper_time.between(
datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)),
datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)),
datetime.combine(TimeFormatUtil.parse_date(query_object.begin_time), time(00, 00, 00)),
datetime.combine(TimeFormatUtil.parse_date(query_object.end_time), time(23, 59, 59)),
)
if query_object.begin_time and query_object.end_time
else True,
@@ -120,8 +121,8 @@ class LoginLogDao:
SysLogininfor.user_name.like(f'%{query_object.user_name}%') if query_object.user_name else True,
SysLogininfor.status == query_object.status if query_object.status else True,
SysLogininfor.login_time.between(
datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)),
datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)),
datetime.combine(TimeFormatUtil.parse_date(query_object.begin_time), time(00, 00, 00)),
datetime.combine(TimeFormatUtil.parse_date(query_object.end_time), time(23, 59, 59)),
)
if query_object.begin_time and query_object.end_time
else True,
@@ -1,6 +1,8 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy import CHAR, Column, DateTime, Integer, String
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysConfig(Base):
@@ -9,14 +11,20 @@ class SysConfig(Base):
"""
__tablename__ = 'sys_config'
__table_args__ = {'comment': '参数配置表'}
config_id = Column(Integer, primary_key=True, autoincrement=True, comment='参数主键')
config_name = Column(String(100), nullable=True, default='', comment='参数名称')
config_key = Column(String(100), nullable=True, default='', comment='参数键名')
config_value = Column(String(500), nullable=True, default='', comment='参数键值')
config_type = Column(String(1), nullable=True, default='N', comment='系统内置(Y是 N否)')
create_by = Column(String(64), nullable=True, default='', comment='创建者')
config_id = Column(Integer, primary_key=True, nullable=False, autoincrement=True, comment='参数主键')
config_name = Column(String(100), nullable=True, server_default="''", comment='参数名称')
config_key = Column(String(100), nullable=True, server_default="''", comment='参数键名')
config_value = Column(String(500), nullable=True, server_default="''", comment='参数键值')
config_type = Column(CHAR(1), nullable=True, server_default='N', comment='系统内置(Y是 N否)')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), nullable=True, default='', comment='更新者')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(String(500), nullable=True, default=None, comment='备注')
remark = Column(
String(500),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
@@ -1,6 +1,8 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy import BigInteger, CHAR, Column, DateTime, Integer, String
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysDept(Base):
@@ -9,18 +11,34 @@ class SysDept(Base):
"""
__tablename__ = 'sys_dept'
__table_args__ = {'comment': '部门表'}
dept_id = Column(Integer, primary_key=True, autoincrement=True, comment='部门id')
parent_id = Column(Integer, default=0, comment='父部门id')
ancestors = Column(String(50), nullable=True, default='', comment='祖级列表')
dept_name = Column(String(30), nullable=True, default='', comment='部门名称')
order_num = Column(Integer, default=0, comment='显示顺序')
leader = Column(String(20), nullable=True, default=None, comment='负责人')
phone = Column(String(11), nullable=True, default=None, comment='联系电话')
email = Column(String(50), nullable=True, default=None, comment='邮箱')
status = Column(String(1), nullable=True, default='0', comment='部门状态(0正常 1停用)')
del_flag = Column(String(1), nullable=True, default='0', comment='删除标志(0代表存在 2代表删除)')
create_by = Column(String(64), nullable=True, default='', comment='创建者')
dept_id = Column(BigInteger, primary_key=True, autoincrement=True, comment='部门id')
parent_id = Column(BigInteger, server_default='0', comment='父部门id')
ancestors = Column(String(50), nullable=True, server_default="''", comment='祖级列表')
dept_name = Column(String(30), nullable=True, server_default="''", comment='部门名称')
order_num = Column(Integer, server_default='0', comment='显示顺序')
leader = Column(
String(20),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='负责人',
)
phone = Column(
String(11),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='联系电话',
)
email = Column(
String(50),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='邮箱',
)
status = Column(CHAR(1), nullable=True, server_default='0', comment='部门状态(0正常 1停用)')
del_flag = Column(CHAR(1), nullable=True, server_default='0', comment='删除标志(0代表存在 2代表删除)')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), nullable=True, default='', comment='更新者')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
@@ -1,6 +1,8 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String, UniqueConstraint
from sqlalchemy import BigInteger, CHAR, Column, DateTime, Integer, String
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysDictType(Base):
@@ -9,18 +11,22 @@ class SysDictType(Base):
"""
__tablename__ = 'sys_dict_type'
__table_args__ = {'comment': '字典类型表'}
dict_id = Column(Integer, primary_key=True, autoincrement=True, comment='字典主键')
dict_name = Column(String(100), nullable=True, default='', comment='字典名称')
dict_type = Column(String(100), nullable=True, default='', comment='字典类型')
status = Column(String(1), nullable=True, default='0', comment='状态(0正常 1停用)')
create_by = Column(String(64), nullable=True, default='', comment='创建者')
dict_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='字典主键')
dict_name = Column(String(100), nullable=True, server_default="''", comment='字典名称')
dict_type = Column(String(100), unique=True, nullable=True, server_default="''", comment='字典类型')
status = Column(CHAR(1), nullable=True, server_default='0', comment='状态(0正常 1停用)')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), nullable=True, default='', comment='更新者')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(String(500), nullable=True, default=None, comment='备注')
__table_args__ = (UniqueConstraint('dict_type', name='uq_sys_dict_type_dict_type'),)
remark = Column(
String(500),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
class SysDictData(Base):
@@ -29,18 +35,34 @@ class SysDictData(Base):
"""
__tablename__ = 'sys_dict_data'
__table_args__ = {'comment': '字典数据表'}
dict_code = Column(Integer, primary_key=True, autoincrement=True, comment='字典编码')
dict_sort = Column(Integer, nullable=True, default=0, comment='字典排序')
dict_label = Column(String(100), nullable=True, default='', comment='字典标签')
dict_value = Column(String(100), nullable=True, default='', comment='字典键值')
dict_type = Column(String(100), nullable=True, default='', comment='字典类型')
css_class = Column(String(100), nullable=True, default=None, comment='样式属性(其他样式扩展)')
list_class = Column(String(100), nullable=True, default=None, comment='表格回显样式')
is_default = Column(String(1), nullable=True, default='N', comment='是否默认(Y是 N否)')
status = Column(String(1), nullable=True, default='0', comment='状态(0正常 1停用)')
create_by = Column(String(64), nullable=True, default='', comment='创建者')
dict_code = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='字典编码')
dict_sort = Column(Integer, nullable=True, server_default='0', comment='字典排序')
dict_label = Column(String(100), nullable=True, server_default="''", comment='字典标签')
dict_value = Column(String(100), nullable=True, server_default="''", comment='字典键值')
dict_type = Column(String(100), nullable=True, server_default="''", comment='字典类型')
css_class = Column(
String(100),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='样式属性(其他样式扩展)',
)
list_class = Column(
String(100),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='表格回显样式',
)
is_default = Column(CHAR(1), nullable=True, server_default='N', comment='是否默认(Y是 N否)')
status = Column(CHAR(1), nullable=True, server_default='0', comment='状态(0正常 1停用)')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), nullable=True, default='', comment='更新者')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(String(500), nullable=True, default=None, comment='备注')
remark = Column(
String(500),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
@@ -1,6 +1,8 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy import BigInteger, CHAR, Column, DateTime, DOUBLE, Index, LargeBinary, String
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysJob(Base):
@@ -9,28 +11,29 @@ class SysJob(Base):
"""
__tablename__ = 'sys_job'
__table_args__ = {'comment': '定时任务调度表'}
job_id = Column(Integer, primary_key=True, autoincrement=True, comment='任务ID')
job_name = Column(String(64), nullable=True, default='', comment='任务名称')
job_group = Column(String(64), nullable=True, default='default', comment='任务组名')
job_executor = Column(String(64), nullable=True, default='default', comment='任务执行器')
job_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='任务ID')
job_name = Column(String(64), primary_key=True, nullable=False, server_default="''", comment='任务名称')
job_group = Column(String(64), primary_key=True, nullable=False, server_default='default', comment='任务组名')
job_executor = Column(String(64), nullable=True, server_default='default', comment='任务执行器')
invoke_target = Column(String(500), nullable=False, comment='调用目标字符串')
job_args = Column(String(255), nullable=True, default='', comment='位置参数')
job_kwargs = Column(String(255), nullable=True, default='', comment='关键字参数')
cron_expression = Column(String(255), nullable=True, default='', comment='cron执行表达式')
job_args = Column(String(255), nullable=True, server_default="''", comment='位置参数')
job_kwargs = Column(String(255), nullable=True, server_default="''", comment='关键字参数')
cron_expression = Column(String(255), nullable=True, server_default="''", comment='cron执行表达式')
misfire_policy = Column(
String(20),
nullable=True,
default='3',
server_default='3',
comment='计划执行错误策略(1立即执行 2执行一次 3放弃执行)',
)
concurrent = Column(String(1), nullable=True, default='1', comment='是否并发执行(0允许 1禁止)')
status = Column(String(1), nullable=True, default='0', comment='状态(0正常 1暂停)')
create_by = Column(String(64), nullable=True, default='', comment='创建者')
concurrent = Column(CHAR(1), nullable=True, server_default='1', comment='是否并发执行(0允许 1禁止)')
status = Column(CHAR(1), nullable=True, server_default='0', comment='状态(0正常 1暂停)')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), nullable=True, default='', comment='更新者')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(String(500), nullable=True, default='', comment='备注信息')
remark = Column(String(500), nullable=True, server_default="''", comment='备注信息')
class SysJobLog(Base):
@@ -39,16 +42,33 @@ class SysJobLog(Base):
"""
__tablename__ = 'sys_job_log'
__table_args__ = {'comment': '定时任务调度日志表'}
job_log_id = Column(Integer, primary_key=True, autoincrement=True, comment='任务日志ID')
job_log_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='任务日志ID')
job_name = Column(String(64), nullable=False, comment='任务名称')
job_group = Column(String(64), nullable=False, comment='任务组名')
job_executor = Column(String(64), nullable=False, comment='任务执行器')
invoke_target = Column(String(500), nullable=False, comment='调用目标字符串')
job_args = Column(String(255), nullable=True, default='', comment='位置参数')
job_kwargs = Column(String(255), nullable=True, default='', comment='关键字参数')
job_trigger = Column(String(255), nullable=True, default='', comment='任务触发器')
job_message = Column(String(500), nullable=True, default='', comment='日志信息')
status = Column(String(1), nullable=True, default='0', comment='执行状态(0正常 1失败)')
exception_info = Column(String(2000), nullable=True, default='', comment='异常信息')
job_args = Column(String(255), nullable=True, server_default="''", comment='位置参数')
job_kwargs = Column(String(255), nullable=True, server_default="''", comment='关键字参数')
job_trigger = Column(String(255), nullable=True, server_default="''", comment='任务触发器')
job_message = Column(String(500), nullable=True, comment='日志信息')
status = Column(CHAR(1), nullable=True, server_default='0', comment='执行状态(0正常 1失败)')
exception_info = Column(String(2000), nullable=True, server_default="''", comment='异常信息')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
class ApschedulerJobs(Base):
"""
定时任务调度任务表
"""
__tablename__ = 'apscheduler_jobs'
id = Column(String(191), primary_key=True, nullable=False)
next_run_time = Column(
DOUBLE, nullable=True, server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type, False)
)
job_state = Column(LargeBinary, nullable=False)
idx_sys_logininfor_s = Index('ix_apscheduler_jobs_next_run_time', next_run_time)
@@ -1,5 +1,5 @@
from datetime import datetime
from sqlalchemy import BigInteger, Column, DateTime, Index, Integer, String
from sqlalchemy import BigInteger, CHAR, Column, DateTime, Index, Integer, String
from config.database import Base
@@ -9,15 +9,16 @@ class SysLogininfor(Base):
"""
__tablename__ = 'sys_logininfor'
__table_args__ = {'comment': '系统访问记录'}
info_id = Column(Integer, primary_key=True, autoincrement=True, comment='访问ID')
user_name = Column(String(50), nullable=True, default='', comment='用户账号')
ipaddr = Column(String(128), nullable=True, default='', comment='登录IP地址')
login_location = Column(String(255), nullable=True, default='', comment='登录地点')
browser = Column(String(50), nullable=True, default='', comment='浏览器类型')
os = Column(String(50), nullable=True, default='', comment='操作系统')
status = Column(String(1), nullable=True, default='0', comment='登录状态(0成功 1失败)')
msg = Column(String(255), nullable=True, default='', comment='提示消息')
info_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='访问ID')
user_name = Column(String(50), nullable=True, server_default="''", comment='用户账号')
ipaddr = Column(String(128), nullable=True, server_default="''", comment='登录IP地址')
login_location = Column(String(255), nullable=True, server_default="''", comment='登录地点')
browser = Column(String(50), nullable=True, server_default="''", comment='浏览器类型')
os = Column(String(50), nullable=True, server_default="''", comment='操作系统')
status = Column(CHAR(1), nullable=True, server_default='0', comment='登录状态(0成功 1失败)')
msg = Column(String(255), nullable=True, server_default="''", comment='提示消息')
login_time = Column(DateTime, nullable=True, default=datetime.now(), comment='访问时间')
idx_sys_logininfor_s = Index('idx_sys_logininfor_s', status)
@@ -30,24 +31,25 @@ class SysOperLog(Base):
"""
__tablename__ = 'sys_oper_log'
__table_args__ = {'comment': '操作日志记录'}
oper_id = Column(BigInteger, primary_key=True, autoincrement=True, comment='日志主键')
title = Column(String(50), nullable=True, default='', comment='模块标题')
business_type = Column(Integer, default=0, comment='业务类型(0其它 1新增 2修改 3删除)')
method = Column(String(100), nullable=True, default='', comment='方法名称')
request_method = Column(String(10), nullable=True, default='', comment='请求方式')
operator_type = Column(Integer, default=0, comment='操作类别(0其它 1后台用户 2手机端用户)')
oper_name = Column(String(50), nullable=True, default='', comment='操作人员')
dept_name = Column(String(50), nullable=True, default='', comment='部门名称')
oper_url = Column(String(255), nullable=True, default='', comment='请求URL')
oper_ip = Column(String(128), nullable=True, default='', comment='主机地址')
oper_location = Column(String(255), nullable=True, default='', comment='操作地点')
oper_param = Column(String(2000), nullable=True, default='', comment='请求参数')
json_result = Column(String(2000), nullable=True, default='', comment='返回参数')
status = Column(Integer, default=0, comment='操作状态(0正常 1异常)')
error_msg = Column(String(2000), nullable=True, default='', comment='错误消息')
oper_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='日志主键')
title = Column(String(50), nullable=True, server_default="''", comment='模块标题')
business_type = Column(Integer, nullable=True, server_default='0', comment='业务类型(0其它 1新增 2修改 3删除)')
method = Column(String(100), nullable=True, server_default="''", comment='方法名称')
request_method = Column(String(10), nullable=True, server_default="''", comment='请求方式')
operator_type = Column(Integer, nullable=True, server_default='0', comment='操作类别(0其它 1后台用户 2手机端用户)')
oper_name = Column(String(50), nullable=True, server_default="''", comment='操作人员')
dept_name = Column(String(50), nullable=True, server_default="''", comment='部门名称')
oper_url = Column(String(255), nullable=True, server_default="''", comment='请求URL')
oper_ip = Column(String(128), nullable=True, server_default="''", comment='主机地址')
oper_location = Column(String(255), nullable=True, server_default="''", comment='操作地点')
oper_param = Column(String(2000), nullable=True, server_default="''", comment='请求参数')
json_result = Column(String(2000), nullable=True, server_default="''", comment='返回参数')
status = Column(Integer, nullable=True, server_default='0', comment='操作状态(0正常 1异常)')
error_msg = Column(String(2000), nullable=True, server_default="''", comment='错误消息')
oper_time = Column(DateTime, nullable=True, default=datetime.now(), comment='操作时间')
cost_time = Column(BigInteger, default=0, comment='消耗时间')
cost_time = Column(BigInteger, nullable=True, server_default='0', comment='消耗时间')
idx_sys_oper_log_bt = Index('idx_sys_oper_log_bt', business_type)
idx_sys_oper_log_s = Index('idx_sys_oper_log_s', status)
@@ -1,6 +1,8 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy import BigInteger, CHAR, Column, DateTime, Integer, String
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysMenu(Base):
@@ -9,24 +11,40 @@ class SysMenu(Base):
"""
__tablename__ = 'sys_menu'
__table_args__ = {'comment': '菜单权限表'}
menu_id = Column(Integer, primary_key=True, autoincrement=True, comment='菜单ID')
menu_name = Column(String(50), nullable=False, default='', comment='菜单名称')
parent_id = Column(Integer, default=0, comment='父菜单ID')
order_num = Column(Integer, default=0, comment='显示顺序')
path = Column(String(200), nullable=True, default='', comment='路由地址')
component = Column(String(255), nullable=True, default=None, comment='组件路径')
query = Column(String(255), nullable=True, default=None, comment='路由参数')
route_name = Column(String(50), nullable=True, default='', comment='路由名称')
is_frame = Column(Integer, default=1, comment='是否为外链(0是 1否)')
is_cache = Column(Integer, default=0, comment='是否缓存(0缓存 1不缓存)')
menu_type = Column(String(1), nullable=True, default='', comment='菜单类型(M目录 C菜单 F按钮)')
visible = Column(String(1), nullable=True, default='0', comment='菜单状态(0显示 1隐藏)')
status = Column(String(1), nullable=True, default='0', comment='菜单状态(0正常 1停用)')
perms = Column(String(100), nullable=True, default=None, comment='权限标识')
icon = Column(String(100), nullable=True, default='#', comment='菜单图标')
create_by = Column(String(64), nullable=True, default='', comment='创建者')
menu_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='菜单ID')
menu_name = Column(String(50), nullable=False, comment='菜单名称')
parent_id = Column(BigInteger, nullable=True, server_default='0', comment='父菜单ID')
order_num = Column(Integer, server_default='0', comment='显示顺序')
path = Column(String(200), nullable=True, server_default="''", comment='路由地址')
component = Column(
String(255),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='组件路径',
)
query = Column(
String(255),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='路由参数',
)
route_name = Column(String(50), nullable=True, server_default="''", comment='路由名称')
is_frame = Column(Integer, nullable=True, server_default='1', comment='是否为外链(0是 1否)')
is_cache = Column(Integer, nullable=True, server_default='0', comment='是否缓存(0缓存 1不缓存)')
menu_type = Column(CHAR(1), nullable=True, server_default="''", comment='菜单类型(M目录 C菜单 F按钮)')
visible = Column(CHAR(1), nullable=True, server_default='0', comment='菜单状态(0显示 1隐藏)')
status = Column(CHAR(1), nullable=True, server_default='0', comment='菜单状态(0正常 1停用)')
perms = Column(
String(100),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='权限标识',
)
icon = Column(String(100), nullable=True, server_default='#', comment='菜单图标')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), nullable=True, default='', comment='更新者')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(String(500), nullable=True, default='', comment='备注')
remark = Column(String(500), nullable=True, server_default="''", comment='备注')
@@ -1,6 +1,9 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, LargeBinary, String
from sqlalchemy import CHAR, Column, DateTime, Integer, LargeBinary, String
from sqlalchemy.dialects import mysql
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysNotice(Base):
@@ -9,14 +12,25 @@ class SysNotice(Base):
"""
__tablename__ = 'sys_notice'
__table_args__ = {'comment': '通知公告表'}
notice_id = Column(Integer, primary_key=True, autoincrement=True, comment='公告ID')
notice_id = Column(Integer, primary_key=True, nullable=False, autoincrement=True, comment='公告ID')
notice_title = Column(String(50), nullable=False, comment='公告标题')
notice_type = Column(String(1), nullable=False, comment='公告类型(1通知 2公告)')
notice_content = Column(LargeBinary, comment='公告内容')
status = Column(String(1), default='0', comment='公告状态(0正常 1关闭)')
create_by = Column(String(64), default='', comment='创建者')
create_time = Column(DateTime, comment='创建时间', default=datetime.now())
update_by = Column(String(64), default='', comment='更新者')
update_time = Column(DateTime, comment='更新时间', default=datetime.now())
remark = Column(String(255), comment='备注')
notice_type = Column(CHAR(1), nullable=False, comment='公告类型(1通知 2公告)')
notice_content = Column(
mysql.LONGBLOB if DataBaseConfig.db_type == 'mysql' else LargeBinary,
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type, False),
comment='公告内容',
)
status = Column(CHAR(1), nullable=True, server_default='0', comment='公告状态(0正常 1关闭)')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, comment='创建时间', default=datetime.now())
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, comment='更新时间', default=datetime.now())
remark = Column(
String(255),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
@@ -1,6 +1,8 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy import BigInteger, CHAR, Column, DateTime, Integer, String
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysPost(Base):
@@ -9,14 +11,20 @@ class SysPost(Base):
"""
__tablename__ = 'sys_post'
__table_args__ = {'comment': '岗位信息表'}
post_id = Column(Integer, primary_key=True, autoincrement=True, comment='岗位ID')
post_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='岗位ID')
post_code = Column(String(64), nullable=False, comment='岗位编码')
post_name = Column(String(50), nullable=False, comment='岗位名称')
post_sort = Column(Integer, nullable=False, comment='显示顺序')
status = Column(String(1), nullable=False, default='0', comment='状态(0正常 1停用)')
create_by = Column(String(64), default='', comment='创建者')
status = Column(CHAR(1), nullable=False, comment='状态(0正常 1停用)')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), default='', comment='更新者')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(String(500), nullable=True, default=None, comment='备注')
remark = Column(
String(500),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
@@ -1,6 +1,9 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy import BigInteger, CHAR, Column, DateTime, Integer, SmallInteger, String
from sqlalchemy.dialects import mysql
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysRole(Base):
@@ -9,25 +12,42 @@ class SysRole(Base):
"""
__tablename__ = 'sys_role'
__table_args__ = {'comment': '角色信息表'}
role_id = Column(Integer, primary_key=True, autoincrement=True, comment='角色ID')
role_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='角色ID')
role_name = Column(String(30), nullable=False, comment='角色名称')
role_key = Column(String(100), nullable=False, comment='角色权限字符串')
role_sort = Column(Integer, nullable=False, comment='显示顺序')
data_scope = Column(
String(1),
default='1',
CHAR(1),
nullable=True,
server_default='1',
comment='数据范围(1:全部数据权限 2:自定数据权限 3:本部门数据权限 4:本部门及以下数据权限)',
)
menu_check_strictly = Column(Integer, default=1, comment='菜单树选择项是否关联显示')
dept_check_strictly = Column(Integer, default=1, comment='部门树选择项是否关联显示')
status = Column(String(1), nullable=False, default='0', comment='角色状态(0正常 1停用)')
del_flag = Column(String(1), default='0', comment='删除标志(0代表存在 2代表删除)')
create_by = Column(String(64), default='', comment='创建者')
create_time = Column(DateTime, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), default='', comment='更新者')
update_time = Column(DateTime, default=datetime.now(), comment='更新时间')
remark = Column(String(500), default=None, comment='备注')
menu_check_strictly = Column(
mysql.TINYINT(display_width=1) if DataBaseConfig.db_type == 'mysql' else SmallInteger,
nullable=True,
server_default='1',
comment='菜单树选择项是否关联显示',
)
dept_check_strictly = Column(
mysql.TINYINT(display_width=1) if DataBaseConfig.db_type == 'mysql' else SmallInteger,
nullable=True,
server_default='1',
comment='部门树选择项是否关联显示',
)
status = Column(CHAR(1), nullable=False, comment='角色状态(0正常 1停用)')
del_flag = Column(CHAR(1), nullable=True, server_default='0', comment='删除标志(0代表存在 2代表删除)')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(
String(500),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
class SysRoleDept(Base):
@@ -36,9 +56,10 @@ class SysRoleDept(Base):
"""
__tablename__ = 'sys_role_dept'
__table_args__ = {'comment': '角色和部门关联表'}
role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID')
dept_id = Column(Integer, primary_key=True, nullable=False, comment='部门ID')
role_id = Column(BigInteger, primary_key=True, nullable=False, comment='角色ID')
dept_id = Column(BigInteger, primary_key=True, nullable=False, comment='部门ID')
class SysRoleMenu(Base):
@@ -47,6 +68,7 @@ class SysRoleMenu(Base):
"""
__tablename__ = 'sys_role_menu'
__table_args__ = {'comment': '角色和菜单关联表'}
role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID')
menu_id = Column(Integer, primary_key=True, nullable=False, comment='菜单ID')
role_id = Column(BigInteger, primary_key=True, nullable=False, comment='角色ID')
menu_id = Column(BigInteger, primary_key=True, nullable=False, comment='菜单ID')
@@ -1,6 +1,8 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, String
from sqlalchemy import BigInteger, CHAR, Column, DateTime, String
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class SysUser(Base):
@@ -9,26 +11,38 @@ class SysUser(Base):
"""
__tablename__ = 'sys_user'
__table_args__ = {'comment': '用户信息表'}
user_id = Column(Integer, primary_key=True, autoincrement=True, comment='用户ID')
dept_id = Column(Integer, default=None, comment='部门ID')
user_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='用户ID')
dept_id = Column(
BigInteger,
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type, False),
comment='部门ID',
)
user_name = Column(String(30), nullable=False, comment='用户账号')
nick_name = Column(String(30), nullable=False, comment='用户昵称')
user_type = Column(String(2), default='00', comment='用户类型(00系统用户)')
email = Column(String(50), default='', comment='用户邮箱')
phonenumber = Column(String(11), default='', comment='手机号码')
sex = Column(String(1), default='0', comment='用户性别(0男 1女 2未知)')
avatar = Column(String(100), default='', comment='头像地址')
password = Column(String(100), default='', comment='密码')
status = Column(String(1), default='0', comment='帐号状态(0正常 1停用)')
del_flag = Column(String(1), default='0', comment='删除标志(0代表存在 2代表删除)')
login_ip = Column(String(128), default='', comment='最后登录IP')
login_date = Column(DateTime, comment='最后登录时间')
create_by = Column(String(64), default='', comment='创建者')
create_time = Column(DateTime, comment='创建时间', default=datetime.now())
update_by = Column(String(64), default='', comment='更新者')
update_time = Column(DateTime, comment='更新时间', default=datetime.now())
remark = Column(String(500), default=None, comment='备注')
user_type = Column(String(2), nullable=True, server_default='00', comment='用户类型(00系统用户)')
email = Column(String(50), nullable=True, server_default="''", comment='用户邮箱')
phonenumber = Column(String(11), nullable=True, server_default="''", comment='手机号码')
sex = Column(CHAR(1), nullable=True, server_default='0', comment='用户性别(0男 1女 2未知)')
avatar = Column(String(100), nullable=True, server_default="''", comment='头像地址')
password = Column(String(100), nullable=True, server_default="''", comment='密码')
status = Column(CHAR(1), nullable=True, server_default='0', comment='帐号状态(0正常 1停用)')
del_flag = Column(CHAR(1), nullable=True, server_default='0', comment='删除标志(0代表存在 2代表删除)')
login_ip = Column(String(128), nullable=True, server_default="''", comment='最后登录IP')
login_date = Column(DateTime, nullable=True, comment='最后登录时间')
pwd_update_date = Column(DateTime, nullable=True, comment='密码最后更新时间')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建')
create_time = Column(DateTime, nullable=True, comment='创建时间', default=datetime.now())
update_by = Column(String(64), nullable=True, server_default="''", comment='更新')
update_time = Column(DateTime, nullable=True, comment='更新时间', default=datetime.now())
remark = Column(
String(500),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
class SysUserRole(Base):
@@ -37,9 +51,10 @@ class SysUserRole(Base):
"""
__tablename__ = 'sys_user_role'
__table_args__ = {'comment': '用户和角色关联表'}
user_id = Column(Integer, primary_key=True, nullable=False, comment='用户ID')
role_id = Column(Integer, primary_key=True, nullable=False, comment='角色ID')
user_id = Column(BigInteger, primary_key=True, nullable=False, comment='用户ID')
role_id = Column(BigInteger, primary_key=True, nullable=False, comment='角色ID')
class SysUserPost(Base):
@@ -48,6 +63,7 @@ class SysUserPost(Base):
"""
__tablename__ = 'sys_user_post'
__table_args__ = {'comment': '用户与岗位关联表'}
user_id = Column(Integer, primary_key=True, nullable=False, comment='用户ID')
post_id = Column(Integer, primary_key=True, nullable=False, comment='岗位ID')
user_id = Column(BigInteger, primary_key=True, nullable=False, comment='用户ID')
post_id = Column(BigInteger, primary_key=True, nullable=False, comment='岗位ID')
@@ -40,6 +40,7 @@ class UserModel(BaseModel):
del_flag: Optional[Literal['0', '2']] = Field(default=None, description='删除标志(0代表存在 2代表删除)')
login_ip: Optional[str] = Field(default=None, description='最后登录IP')
login_date: Optional[datetime] = Field(default=None, description='最后登录时间')
pwd_update_date: Optional[datetime] = Field(default=None, description='密码最后更新时间')
create_by: Optional[str] = Field(default=None, description='创建者')
create_time: Optional[datetime] = Field(default=None, description='创建时间')
update_by: Optional[str] = Field(default=None, description='更新者')
@@ -125,6 +126,8 @@ class CurrentUserModel(BaseModel):
permissions: List = Field(description='权限信息')
roles: List = Field(description='角色信息')
user: Union[UserInfoModel, None] = Field(description='用户信息')
is_default_modify_pwd: bool = Field(default=False, description='是否初始密码修改提醒')
is_password_expired: bool = Field(default=False, description='密码是否过期提醒')
class UserDetailModel(BaseModel):
@@ -103,9 +103,10 @@ class DictTypeService:
if dict_type_info.dict_type != page_object.dict_type:
for dict_data in dict_data_list:
edit_dict_data = DictDataModel(
dictCode=dict_data.dict_code,
dictCode=dict_data.get('dict_code'),
dictType=page_object.dict_type,
updateBy=page_object.update_by,
updateTime=page_object.update_time,
).model_dump(exclude_unset=True)
await DictDataDao.edit_dict_data_dao(query_db, edit_dict_data)
await DictTypeDao.edit_dict_type_dao(query_db, edit_dict_type)
@@ -129,7 +129,7 @@ class JobService:
elif not await cls.check_job_unique_services(query_db, page_object):
raise ServiceException(message=f'修改定时任务{page_object.job_name}失败,定时任务已存在')
try:
await JobDao.edit_job_dao(query_db, edit_job)
await JobDao.edit_job_dao(query_db, edit_job, job_info)
SchedulerUtil.remove_scheduler_job(job_id=edit_job.get('job_id'))
if edit_job.get('status') == '0':
job_info = await cls.job_detail_services(query_db, edit_job.get('job_id'))
@@ -218,7 +218,7 @@ class LoginService:
else:
# 此方法可实现同一账号同一时间只能登录一次
redis_token = await request.app.state.redis.get(
f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{query_user.get('user_basic_info').user_id}"
f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{query_user.get("user_basic_info").user_id}'
)
if token == redis_token:
if AppConfig.app_same_time_login:
@@ -229,7 +229,7 @@ class LoginService:
)
else:
await request.app.state.redis.set(
f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{query_user.get('user_basic_info').user_id}",
f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{query_user.get("user_basic_info").user_id}',
redis_token,
ex=timedelta(minutes=JwtConfig.jwt_redis_expire_minutes),
)
@@ -242,6 +242,12 @@ class LoginService:
post_ids = ','.join([str(row.post_id) for row in query_user.get('user_post_info')])
role_ids = ','.join([str(row.role_id) for row in query_user.get('user_role_info')])
roles = [row.role_key for row in query_user.get('user_role_info')]
is_default_modify_pwd = await cls.__init_password_is_modify(
request, query_user.get('user_basic_info').pwd_update_date
)
is_password_expired = await cls.__password_is_expired(
request, query_user.get('user_basic_info').pwd_update_date
)
current_user = CurrentUserModel(
permissions=permissions,
@@ -253,12 +259,48 @@ class LoginService:
dept=CamelCaseUtil.transform_result(query_user.get('user_dept_info')),
role=CamelCaseUtil.transform_result(query_user.get('user_role_info')),
),
isDefaultModifyPwd=is_default_modify_pwd,
isPasswordExpired=is_password_expired,
)
return current_user
else:
logger.warning('用户token已失效,请重新登录')
raise AuthException(data='', message='用户token已失效,请重新登录')
@classmethod
async def __init_password_is_modify(cls, request: Request, pwd_update_date: datetime):
"""
判断当前用户是否初始密码登录
:param request: Request对象
:param pwd_update_date: 密码最后更新时间
:return: 是否初始密码登录
"""
init_password_is_modify = await request.app.state.redis.get(
f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.initPasswordModify'
)
return init_password_is_modify == '1' and pwd_update_date is None
@classmethod
async def __password_is_expired(cls, request: Request, pwd_update_date: datetime):
"""
判断当前用户密码是否过期
:param request: Request对象
:param pwd_update_date: 密码最后更新时间
:return: 密码是否过期
"""
password_validate_days = await request.app.state.redis.get(
f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.passwordValidateDays'
)
if password_validate_days and int(password_validate_days) > 0:
if pwd_update_date is None:
return True
expire_date = pwd_update_date + timedelta(days=int(password_validate_days))
if datetime.now() > expire_date:
return True
return False
@classmethod
async def get_current_user_routers(cls, user_id: int, query_db: AsyncSession):
"""
@@ -404,6 +446,7 @@ class LoginService:
userName=user_register.username,
nickName=user_register.username,
password=PwdUtil.get_password_hash(user_register.password),
pwdUpdateDate=datetime.now(),
)
result = await UserService.add_user_services(query_db, add_user)
return result
@@ -466,15 +509,15 @@ class LoginService:
return CrudResponseModel(**result)
@classmethod
async def logout_services(cls, request: Request, session_id: str):
async def logout_services(cls, request: Request, token_id: str):
"""
退出登录services
:param request: Request对象
:param session_id: 会话编号
:param token_id: 令牌编号
:return: 退出登录结果
"""
await request.app.state.redis.delete(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}')
await request.app.state.redis.delete(f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{token_id}')
# await request.app.state.redis.delete(f'{current_user.user.user_id}_access_token')
# await request.app.state.redis.delete(f'{current_user.user.user_id}_session_id')
@@ -494,6 +494,7 @@ class UserService:
}
for item in user_list:
item['deptName'] = item.get('dept').get('deptName')
if item.get('status') == '0':
item['status'] = '正常'
else:
@@ -2,6 +2,7 @@ from datetime import datetime, time
from sqlalchemy import delete, func, select, text, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from sqlglot.expressions import Expression
from typing import List
from config.env import DataBaseConfig
from module_generator.entity.do.gen_do import GenTable, GenTableColumn
@@ -75,15 +76,17 @@ class GenTableDao:
return gen_table_all
@classmethod
async def create_table_by_sql_dao(cls, db: AsyncSession, sql: str):
async def create_table_by_sql_dao(cls, db: AsyncSession, sql_statements: List[Expression]):
"""
根据sql语句创建表结构
:param db: orm对象
:param sql: sql语句
:param sql_statements: sql语句的ast列表
:return:
"""
await db.execute(text(sql))
for sql_statement in sql_statements:
sql = sql_statement.sql(dialect=DataBaseConfig.sqlglot_parse_dialect)
await db.execute(text(sql))
@classmethod
async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False):
@@ -1,7 +1,9 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
from sqlalchemy import BigInteger, CHAR, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from config.database import Base
from config.env import DataBaseConfig
from utils.common_util import SqlalchemyUtil
class GenTable(Base):
@@ -10,30 +12,48 @@ class GenTable(Base):
"""
__tablename__ = 'gen_table'
__table_args__ = {'comment': '代码生成业务表'}
table_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号')
table_name = Column(String(200), nullable=True, default='', comment='表名称')
table_comment = Column(String(500), nullable=True, default='', comment='表描述')
sub_table_name = Column(String(64), nullable=True, comment='关联子表的表名')
sub_table_fk_name = Column(String(64), nullable=True, comment='子表关联的外键名')
class_name = Column(String(100), nullable=True, default='', comment='实体类名称')
tpl_category = Column(String(200), nullable=True, default='crud', comment='使用的模板(crud单表操作 tree树表操作)')
table_id = Column(BigInteger, primary_key=True, nullable=False, autoincrement=True, comment='编号')
table_name = Column(String(200), nullable=True, server_default="''", comment='表名称')
table_comment = Column(String(500), nullable=True, server_default="''", comment='表描述')
sub_table_name = Column(
String(64),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='关联子表的表名',
)
sub_table_fk_name = Column(
String(64),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='子表关联的外键名',
)
class_name = Column(String(100), nullable=True, server_default="''", comment='实体类名称')
tpl_category = Column(
String(200), nullable=True, server_default='crud', comment='使用的模板(crud单表操作 tree树表操作)'
)
tpl_web_type = Column(
String(30), nullable=True, default='', comment='前端模板类型(element-ui模版 element-plus模版)'
String(30), nullable=True, server_default="''", comment='前端模板类型(element-ui模版 element-plus模版)'
)
package_name = Column(String(100), nullable=True, comment='生成包路径')
module_name = Column(String(30), nullable=True, comment='生成模块名')
business_name = Column(String(30), nullable=True, comment='生成业务名')
function_name = Column(String(100), nullable=True, comment='生成功能名')
function_author = Column(String(100), nullable=True, comment='生成功能作者')
gen_type = Column(String(1), nullable=True, default='0', comment='生成代码方式(0zip压缩包 1自定义路径)')
gen_path = Column(String(200), nullable=True, default='/', comment='生成路径(不填默认项目路径)')
function_name = Column(String(50), nullable=True, comment='生成功能名')
function_author = Column(String(50), nullable=True, comment='生成功能作者')
gen_type = Column(CHAR(1), nullable=True, server_default='0', comment='生成代码方式(0zip压缩包 1自定义路径)')
gen_path = Column(String(200), nullable=True, server_default='/', comment='生成路径(不填默认项目路径)')
options = Column(String(1000), nullable=True, comment='其它生成选项')
create_by = Column(String(64), default='', comment='创建者')
create_by = Column(String(64), nullable=True, server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), default='', comment='更新者')
update_by = Column(String(64), nullable=True, server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
remark = Column(String(500), nullable=True, default=None, comment='备注')
remark = Column(
String(500),
nullable=True,
server_default=SqlalchemyUtil.get_server_default_null(DataBaseConfig.db_type),
comment='备注',
)
columns = relationship('GenTableColumn', order_by='GenTableColumn.sort', back_populates='tables')
@@ -44,31 +64,34 @@ class GenTableColumn(Base):
"""
__tablename__ = 'gen_table_column'
__table_args__ = {'comment': '代码生成业务表字段'}
column_id = Column(Integer, primary_key=True, autoincrement=True, comment='编号')
table_id = Column(Integer, ForeignKey('gen_table.table_id'), nullable=True, comment='归属表编号')
column_id = Column(BigInteger, primary_key=True, autoincrement=True, nullable=False, comment='编号')
table_id = Column(BigInteger, ForeignKey('gen_table.table_id'), nullable=True, comment='归属表编号')
column_name = Column(String(200), nullable=True, comment='列名称')
column_comment = Column(String(500), nullable=True, comment='列描述')
column_type = Column(String(100), nullable=True, comment='列类型')
python_type = Column(String(500), nullable=True, comment='PYTHON类型')
python_field = Column(String(200), nullable=True, comment='PYTHON字段名')
is_pk = Column(String(1), nullable=True, comment='是否主键(1是)')
is_increment = Column(String(1), nullable=True, comment='是否自增(1是)')
is_required = Column(String(1), nullable=True, comment='是否必填(1是)')
is_unique = Column(String(1), nullable=True, comment='是否唯一(1是)')
is_insert = Column(String(1), nullable=True, comment='是否为插入字段(1是)')
is_edit = Column(String(1), nullable=True, comment='是否编辑字段(1是)')
is_list = Column(String(1), nullable=True, comment='是否列表字段(1是)')
is_query = Column(String(1), nullable=True, comment='是否查询字段(1是)')
query_type = Column(String(200), nullable=True, default='EQ', comment='查询方式(等于、不等于、大于、小于、范围)')
is_pk = Column(CHAR(1), nullable=True, comment='是否主键(1是)')
is_increment = Column(CHAR(1), nullable=True, comment='是否自增(1是)')
is_required = Column(CHAR(1), nullable=True, comment='是否必填(1是)')
is_unique = Column(CHAR(1), nullable=True, comment='是否唯一(1是)')
is_insert = Column(CHAR(1), nullable=True, comment='是否为插入字段(1是)')
is_edit = Column(CHAR(1), nullable=True, comment='是否编辑字段(1是)')
is_list = Column(CHAR(1), nullable=True, comment='是否列表字段(1是)')
is_query = Column(CHAR(1), nullable=True, comment='是否查询字段(1是)')
query_type = Column(
String(200), nullable=True, server_default='EQ', comment='查询方式(等于、不等于、大于、小于、范围)'
)
html_type = Column(
String(200), nullable=True, comment='显示类型(文本框、文本域、下拉框、复选框、单选框、日期控件)'
)
dict_type = Column(String(200), nullable=True, default='', comment='字典类型')
dict_type = Column(String(200), nullable=True, server_default="''", comment='字典类型')
sort = Column(Integer, nullable=True, comment='排序')
create_by = Column(String(64), default='', comment='创建者')
create_by = Column(String(64), server_default="''", comment='创建者')
create_time = Column(DateTime, nullable=True, default=datetime.now(), comment='创建时间')
update_by = Column(String(64), default='', comment='更新者')
update_by = Column(String(64), server_default="''", comment='更新者')
update_time = Column(DateTime, nullable=True, default=datetime.now(), comment='更新时间')
tables = relationship('GenTable', back_populates='columns')
@@ -1,13 +1,14 @@
import io
import json
import os
import re
import zipfile
from datetime import datetime
from sqlalchemy.ext.asyncio import AsyncSession
from sqlglot import parse as sqlglot_parse
from sqlglot.expressions import Add, Alter, Create, Delete, Drop, Expression, Insert, Table, TruncateTable, Update
from typing import List
from config.constant import GenConstant
from config.env import GenConfig
from config.env import DataBaseConfig, GenConfig
from exceptions.exception import ServiceException
from module_admin.entity.vo.common_vo import CrudResponseModel
from module_admin.entity.vo.user_vo import CurrentUserModel
@@ -197,10 +198,11 @@ class GenTableService:
:param current_user: 当前用户信息对象
:return: 创建表结构结果
"""
if cls.__is_valid_create_table(sql):
sql_statements = sqlglot_parse(sql, dialect=DataBaseConfig.sqlglot_parse_dialect)
if cls.__is_valid_create_table(sql_statements):
try:
table_names = re.findall(r'create\s+table\s+(\w+)', sql, re.IGNORECASE)
await GenTableDao.create_table_by_sql_dao(query_db, sql)
table_names = cls.__get_table_names(sql_statements)
await GenTableDao.create_table_by_sql_dao(query_db, sql_statements)
gen_table_list = await cls.get_gen_db_table_list_by_name_services(query_db, table_names)
await cls.import_gen_table_services(query_db, gen_table_list, current_user)
@@ -211,22 +213,39 @@ class GenTableService:
raise ServiceException(message='建表语句不合法')
@classmethod
def __is_valid_create_table(cls, sql: str):
def __is_valid_create_table(cls, sql_statements: List[Expression]):
"""
校验sql语句是否为合法的建表语句
:param sql: sql语句
:param sql_statements: sql语句的ast列表
:return: 校验结果
"""
create_table_pattern = r'^\s*CREATE\s+TABLE\s+'
if not re.search(create_table_pattern, sql, re.IGNORECASE):
validate_create = [isinstance(sql_statement, Create) for sql_statement in sql_statements]
validate_forbidden_keywords = [
isinstance(
sql_statement,
(Add, Alter, Delete, Drop, Insert, TruncateTable, Update),
)
for sql_statement in sql_statements
]
if not any(validate_create) or any(validate_forbidden_keywords):
return False
forbidden_keywords = ['INSERT', 'UPDATE', 'DELETE', 'DROP', 'ALTER', 'TRUNCATE']
for keyword in forbidden_keywords:
if re.search(rf'\b{keyword}\b', sql, re.IGNORECASE):
return False
return True
@classmethod
def __get_table_names(cls, sql_statements: List[Expression]):
"""
获取sql语句中所有的建表表名
:param sql_statements: sql语句的ast列表
:return: 建表表名列表
"""
table_names = []
for sql_statement in sql_statements:
if isinstance(sql_statement, Create):
table_names.append(sql_statement.find(Table).name)
return table_names
@classmethod
async def preview_code_services(cls, query_db: AsyncSession, table_id: int):
"""
@@ -1,10 +1,14 @@
{% set pkField = pkColumn.python_field %}
{% set pk_field = pkColumn.python_field | camel_to_snake %}
{% set need_import_datetime = namespace(has_datetime=False) %}
{% for column in columns %}
{% if column.python_field == "createTime" %}
from datetime import datetime
{% if column.python_field in ["createTime", "updatetime"] %}
{% set need_import_datetime.has_datetime = True %}
{% endif %}
{% endfor %}
{% if need_import_datetime.has_datetime %}
from datetime import datetime
{% endif %}
from fastapi import APIRouter, Depends, Form, Request
from pydantic_validation_decorator import ValidateFields
from sqlalchemy.ext.asyncio import AsyncSession
@@ -82,8 +86,13 @@ async def edit_{{ moduleName }}_{{ businessName }}(
query_db: AsyncSession = Depends(get_db),
current_user: CurrentUserModel = Depends(LoginService.get_current_user),
):
{% for column in columns %}
{% if column.python_field == "updateBy" %}
edit_{{ businessName }}.update_by = current_user.user.user_name
{% elif column.python_field == "updateTime" %}
edit_{{ businessName }}.update_time = datetime.now()
{% endif %}
{% endfor %}
edit_{{ businessName }}_result = await {{ BusinessName }}Service.edit_{{ businessName }}_services(query_db, edit_{{ businessName }})
logger.info(edit_{{ businessName }}_result.message)
@@ -70,7 +70,7 @@ class {{ BusinessName }}Dao:
await db.execute(
select({{ ClassName }}).where(
{% for column in columns %}
{% if column.required %}
{% if column.unique %}
{{ ClassName }}.{{ column.python_field | camel_to_snake }} == {{ businessName }}.{{ column.python_field | camel_to_snake }} if {{ businessName }}.{{ column.python_field | camel_to_snake }} else True,
{% endif %}
{% endfor %}
@@ -118,12 +118,12 @@ class {{ BusinessName }}Dao:
{{ ClassName }}.{{ field }} <= query_object.{{ field }} if query_object.{{ field }} else True,
{% elif column.query_type == "LIKE" %}
{{ ClassName }}.{{ field }}.like(f'%{% raw %}{{% endraw %}query_object.{{ field }}{% raw %}}{% endraw %}%') if query_object.{{ field }} else True,
{% elif column.query_type == "BETWEEN" %}
{% elif column.html_type == "datetime" and column.query_type == "BETWEEN" %}
{{ ClassName }}.{{ field }}.between(
datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)),
datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)),
datetime.combine(datetime.strptime(query_object.begin_{{ column.column_name }}, '%Y-%m-%d'), time(00, 00, 00)),
datetime.combine(datetime.strptime(query_object.end_{{ column.column_name }}, '%Y-%m-%d'), time(23, 59, 59)),
)
if query_object.begin_time and query_object.end_time
if query_object.begin_{{ column.column_name }} and query_object.end_{{ column.column_name }}
else True,
{% endif %}
{% endif %}
@@ -145,7 +145,7 @@ class {{ BusinessName }}Dao:
:param {{ businessName }}: {{ functionName }}对象
:return:
"""
db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump(exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list', {% endif %}{% for column in columns %}{% if not column.insert %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %}))
db_{{ businessName }} = {{ ClassName }}(**{{ businessName }}.model_dump(exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list', {% endif %}{% for column in columns %}{% if not column.insert and column.column_name not in column_not_add_show + column_not_edit_show %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %}))
db.add(db_{{ businessName }})
await db.flush()
@@ -13,6 +13,7 @@ class {{ ClassName }}(Base):
"""
__tablename__ = '{{ tableName }}'
__table_args__ = {'comment': '{{ tableComment }}'}
{% for column in columns %}
{{ column.column_name }} = Column({{ column.column_type | get_sqlalchemy_type }}, {% if column.pk %}primary_key=True, {% endif %}{% if column.increment %}autoincrement=True, {% endif %}{% if column.required or column.pk %}nullable=False{% else %}nullable=True{% endif %}, comment='{{ column.column_comment }}')
@@ -103,7 +103,7 @@ class {{ BusinessName }}Service:
:param page_object: 编辑{{ functionName }}对象
:return: 编辑{{ functionName }}校验结果
"""
edit_{{ businessName }} = page_object.model_dump(exclude_unset=True, exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list', {% endif %}{% for column in columns %}{% if not column.edit and not column.pk %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %})
edit_{{ businessName }} = page_object.model_dump(exclude_unset=True, exclude={% raw %}{{% endraw %}{% if table.sub %}'{{ subclassName }}_list', {% endif %}{% for column in columns %}{% if not column.edit and not column.pk and column.column_name not in column_not_edit_show %}'{{ column.python_field | camel_to_snake }}'{% if not loop.last %}, {% endif %}{% endif %}{% endfor %}{% raw %}}{% endraw %})
{{ businessName }}_info = await cls.{{ businessName }}_detail_services(query_db, page_object.{{ pk_field }})
if {{ businessName }}_info.{{ pk_field }}:
{% for column in columns %}
@@ -3,13 +3,17 @@
{% set pkParentheseIndex = pkColumn.column_comment.find("") %}
{% set pk_field_comment = pkColumn.column_comment[:pkParentheseIndex] if pkParentheseIndex != -1 else pkColumn.column_comment %}
{% set vo_field_required = namespace(has_required=False) %}
{% set vo_field_daterange = namespace(has_daterange=False) %}
{% for column in columns %}
{% if column.required %}
{% set vo_field_required.has_required = True %}
{% endif %}
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %}
{% set vo_field_daterange.has_daterange = True %}
{% endif %}
{% endfor %}
{% if table.sub %}
{% set sub_vo_field_required = namespace(has_required=False) %}
{% if table.sub %}
{% for sub_column in subTable.columns %}
{% if sub_column.required %}
{% set sub_vo_field_required.has_required = True %}
@@ -21,7 +25,7 @@
{% endfor %}
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
{% if vo_field_required.has_required %}
{% if vo_field_required.has_required or sub_vo_field_required.has_required %}
from pydantic_validation_decorator import NotBlank
{% endif %}
{% if table.sub %}
@@ -142,9 +146,16 @@ class {{ BusinessName }}QueryModel({% if table.sub %}{{ BusinessName }}BaseModel
"""
{{ functionName }}不分页查询模型
"""
begin_time: Optional[str] = Field(default=None, description='开始时间')
end_time: Optional[str] = Field(default=None, description='结束时间')
{% if vo_field_daterange.has_daterange %}
{% for column in columns %}
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %}
begin_{{ column.column_name }}: Optional[str] = Field(default=None, description='开始{{ column.column_comment }}')
end_{{ column.column_name }}: Optional[str] = Field(default=None, description='结束{{ column.column_comment }}')
{% endif %}
{% endfor %}
{% else %}
pass
{% endif %}
@as_query
@@ -35,12 +35,13 @@
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-date-picker clearable
<el-date-picker
v-model="queryParams.{{ column.python_field }}"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择{{ comment }}">
</el-date-picker>
placeholder="请选择{{ comment }}"
clearable
/>
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type == "BETWEEN" %}
<el-form-item label="{{ comment }}">
@@ -52,7 +53,7 @@
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
/>
</el-form-item>
{% endif %}
{% endif %}
@@ -161,33 +162,33 @@
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
{% for column in columns %}
{% set field = column.python_field %}
{% if column.insert and not column.pk %}
{% if (column.insert or column.edit) and not column.pk and column.column_name not in column_not_add_show + column_not_edit_show %}
{% if column.usable_column or not column.super_column %}
{% set parentheseIndex = column.column_comment.find("") %}
{% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %}
{% set dictType = column.dict_type %}
{% if treeParentCode and column.python_field == treeParentCode %}
<el-form-item label="{{ comment }}" prop="{{ treeParentCode }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ treeParentCode }}">
<treeselect v-model="form.{{ treeParentCode }}" :options="{{ businessName }}Options" :normalizer="normalizer" placeholder="请选择{{ comment }}" />
</el-form-item>
{% elif column.html_type == "input" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-input v-model="form.{{ field }}" placeholder="请输入{{ comment }}" />
</el-form-item>
{% elif column.html_type == "imageUpload" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<image-upload v-model="form.{{ field }}"/>
</el-form-item>
{% elif column.html_type == "fileUpload" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<file-upload v-model="form.{{ field }}"/>
</el-form-item>
{% elif column.html_type == "editor" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<editor v-model="form.{{ field }}" :min-height="192"/>
</el-form-item>
{% elif column.html_type == "select" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-select v-model="form.{{ field }}" placeholder="请选择{{ comment }}">
<el-option
v-for="dict in dict.type.{{ dictType }}"
@@ -202,13 +203,13 @@
</el-select>
</el-form-item>
{% elif column.html_type == "select" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-select v-model="form.{{ field }}" placeholder="请选择{{ comment }}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
{% elif column.html_type == "checkbox" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-checkbox-group v-model="form.{{ field }}">
<el-checkbox
v-for="dict in dict.type.{{ dictType }}"
@@ -219,13 +220,13 @@
</el-checkbox-group>
</el-form-item>
{% elif column.html_type == "checkbox" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-checkbox-group v-model="form.{{ field }}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
{% elif column.html_type == "radio" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-radio-group v-model="form.{{ field }}">
<el-radio
v-for="dict in dict.type.{{ dictType }}"
@@ -240,13 +241,13 @@
</el-radio-group>
</el-form-item>
{% elif column.html_type == "radio" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-radio-group v-model="form.{{ field }}">
<el-radio label="请选择字典生成" value="" />
</el-radio-group>
</el-form-item>
{% elif column.html_type == "datetime" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-date-picker clearable
v-model="form.{{ field }}"
type="date"
@@ -255,7 +256,7 @@
</el-date-picker>
</el-form-item>
{% elif column.html_type == "textarea" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-input v-model="form.{{ field }}" type="textarea" placeholder="请输入内容" />
</el-form-item>
{% endif %}
@@ -351,8 +352,8 @@ export default {
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %}
{% set AttrName = column.python_field[0] | upper + column.python_field[1:] %}
if (null != this.daterange{{ AttrName }} && '' != this.daterange{{ AttrName }}) {
this.queryParams.params["begin{{ AttrName }}"] = this.daterange{{ AttrName }}[0];
this.queryParams.params["end{{ AttrName }}"] = this.daterange{{ AttrName }}[1];
this.queryParams.begin{{ AttrName }} = this.daterange{{ AttrName }}[0];
this.queryParams.end{{ AttrName }} = this.daterange{{ AttrName }}[1];
}
{% endif %}
{% endfor %}
@@ -485,6 +486,10 @@ export default {
this.getList();
this.$modal.msgSuccess("删除成功");
}).catch(() => {});
},
/** 是否渲染字段 */
renderField(insert, edit) {
return this.form.{{ pkColumn.python_field }} == null ? insert : edit;
}
},
};
@@ -35,12 +35,13 @@
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-date-picker clearable
<el-date-picker
v-model="queryParams.{{ column.python_field }}"
type="date"
value-format="yyyy-MM-dd"
placeholder="请选择{{ comment }}">
</el-date-picker>
placeholder="请选择{{ comment }}"
clearable
/>
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type == "BETWEEN" %}
<el-form-item label="{{ comment }}">
@@ -52,7 +53,7 @@
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
/>
</el-form-item>
{% endif %}
{% endif %}
@@ -173,29 +174,29 @@
<el-form ref="form" :model="form" :rules="rules" label-width="80px">
{% for column in columns %}
{% set field = column.python_field %}
{% if column.insert and not column.pk %}
{% if (column.insert or column.edit) and not column.pk and column.column_name not in column_not_add_show + column_not_edit_show %}
{% if column.usable_column or not column.super_column %}
{% set parentheseIndex = column.column_comment.find("") %}
{% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %}
{% set dictType = column.dict_type %}
{% if column.html_type == "input" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-input v-model="form.{{ field }}" placeholder="请输入{{ comment }}" />
</el-form-item>
{% elif column.html_type == "imageUpload" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<image-upload v-model="form.{{ field }}"/>
</el-form-item>
{% elif column.html_type == "fileUpload" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<file-upload v-model="form.{{ field }}"/>
</el-form-item>
{% elif column.html_type == "editor" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<editor v-model="form.{{ field }}" :min-height="192"/>
</el-form-item>
{% elif column.html_type == "select" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-select v-model="form.{{ field }}" placeholder="请选择{{ comment }}">
<el-option
v-for="dict in dict.type.{{ dictType }}"
@@ -210,13 +211,13 @@
</el-select>
</el-form-item>
{% elif column.html_type == "select" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-select v-model="form.{{ field }}" placeholder="请选择{{ comment }}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
{% elif column.html_type == "checkbox" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-checkbox-group v-model="form.{{ field }}">
<el-checkbox
v-for="dict in dict.type.{{ dictType }}"
@@ -227,13 +228,13 @@
</el-checkbox-group>
</el-form-item>
{% elif column.html_type == "checkbox" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-checkbox-group v-model="form.{{ field }}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
{% elif column.html_type == "radio" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-radio-group v-model="form.{{ field }}">
<el-radio
v-for="dict in dict.type.{{ dictType }}"
@@ -248,13 +249,13 @@
</el-radio-group>
</el-form-item>
{% elif column.html_type == "radio" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-radio-group v-model="form.{{ field }}">
<el-radio label="请选择字典生成" value="" />
</el-radio-group>
</el-form-item>
{% elif column.html_type == "datetime" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-date-picker clearable
v-model="form.{{ field }}"
type="date"
@@ -263,7 +264,7 @@
</el-date-picker>
</el-form-item>
{% elif column.html_type == "textarea" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-input v-model="form.{{ field }}" type="textarea" placeholder="请输入内容" />
</el-form-item>
{% endif %}
@@ -426,8 +427,8 @@ export default {
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %}
{% set AttrName = column.python_field[0] | upper + column.python_field[1:] %}
if (null != this.daterange{{ AttrName }} && '' != this.daterange{{ AttrName }}) {
this.queryParams.params["begin{{ AttrName }}"] = this.daterange{{ AttrName }}[0];
this.queryParams.params["end{{ AttrName }}"] = this.daterange{{ AttrName }}[1];
this.queryParams.begin{{ AttrName }} = this.daterange{{ AttrName }}[0];
this.queryParams.end{{ AttrName }} = this.daterange{{ AttrName }}[1];
}
{% endif %}
{% endfor %}
@@ -580,6 +581,10 @@ export default {
this.download('{{ moduleName }}/{{ businessName }}/export', {
...this.queryParams
}, `{{ businessName }}_${new Date().getTime()}.xlsx`);
},
/** 是否渲染字段 */
renderField(insert, edit) {
return this.form.{{ pkColumn.python_field }} == null ? insert : edit;
}
},
};
@@ -13,12 +13,13 @@
v-model="queryParams.{{ column.python_field }}"
placeholder="请输入{{ comment }}"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and dictType %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-select v-model="queryParams.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable>
<el-select v-model="queryParams.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable style="width: 240px">
<el-option
v-for="dict in {{ dictType }}"
:key="dict.value"
@@ -29,18 +30,20 @@
</el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-select v-model="queryParams.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable>
<el-select v-model="queryParams.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable style="width: 240px">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-date-picker clearable
<el-date-picker
v-model="queryParams.{{ column.python_field }}"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择{{ comment }}">
</el-date-picker>
placeholder="请选择{{ comment }}"
clearable
style="width: 240px"
/>
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type == "BETWEEN" %}
<el-form-item label="{{ comment }}" style="width: 308px">
@@ -51,7 +54,8 @@
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
style="width: 240px"
/>
</el-form-item>
{% endif %}
{% endif %}
@@ -136,13 +140,13 @@
<el-form ref="{{ businessName }}Ref" :model="form" :rules="rules" label-width="80px">
{% for column in columns %}
{% set field = column.python_field %}
{% if column.insert and not column.pk %}
{% if (column.insert or column.edit) and not column.pk and column.column_name not in column_not_add_show + column_not_edit_show %}
{% if column.usable_column or not column.super_column %}
{% set parentheseIndex = column.column_comment.find("") %}
{% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %}
{% set dictType = column.dict_type %}
{% if treeParentCode and column.python_field == treeParentCode %}
<el-form-item label="{{ comment }}" prop="{{ treeParentCode }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ treeParentCode }}">
<el-tree-select
v-model="form.{{ treeParentCode }}"
:data="{{ businessName }}Options"
@@ -153,23 +157,23 @@
/>
</el-form-item>
{% elif column.html_type == "input" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-input v-model="form.{{ field }}" placeholder="请输入{{ comment }}" />
</el-form-item>
{% elif column.html_type == "imageUpload" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<image-upload v-model="form.{{ field }}"/>
</el-form-item>
{% elif column.html_type == "fileUpload" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<file-upload v-model="form.{{ field }}"/>
</el-form-item>
{% elif column.html_type == "editor" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<editor v-model="form.{{ field }}" :min-height="192"/>
</el-form-item>
{% elif column.html_type == "select" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-select v-model="form.{{ field }}" placeholder="请选择{{ comment }}">
<el-option
v-for="dict in {{ dictType }}"
@@ -184,13 +188,13 @@
</el-select>
</el-form-item>
{% elif column.html_type == "select" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-select v-model="form.{{ field }}" placeholder="请选择{{ comment }}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
{% elif column.html_type == "checkbox" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-checkbox-group v-model="form.{{ field }}">
<el-checkbox
v-for="dict in {{ dictType }}"
@@ -201,13 +205,13 @@
</el-checkbox-group>
</el-form-item>
{% elif column.html_type == "checkbox" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-checkbox-group v-model="form.{{ field }}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
{% elif column.html_type == "radio" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-radio-group v-model="form.{{ field }}">
<el-radio
v-for="dict in {{ dictType }}"
@@ -222,13 +226,13 @@
</el-radio-group>
</el-form-item>
{% elif column.html_type == "radio" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-radio-group v-model="form.{{ field }}">
<el-radio label="请选择字典生成" value="" />
</el-radio-group>
</el-form-item>
{% elif column.html_type == "datetime" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-date-picker clearable
v-model="form.{{ field }}"
type="date"
@@ -237,7 +241,7 @@
</el-date-picker>
</el-form-item>
{% elif column.html_type == "textarea" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-input v-model="form.{{ field }}" type="textarea" placeholder="请输入内容" />
</el-form-item>
{% endif %}
@@ -315,8 +319,8 @@ function getList() {
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %}
{% set AttrName = column.python_field[0] | upper + column.python_field[1:] %}
if (null != daterange{{ AttrName }} && '' != daterange{{ AttrName }}) {
queryParams.value.params["begin{{ AttrName }}"] = daterange{{ AttrName }}.value[0];
queryParams.value.params["end{{ AttrName }}"] = daterange{{ AttrName }}.value[1];
queryParams.value.begin{{ AttrName }} = daterange{{ AttrName }}.value[0];
queryParams.value.end{{ AttrName }} = daterange{{ AttrName }}.value[1];
}
{% endif %}
{% endfor %}
@@ -450,5 +454,10 @@ function handleDelete(row) {
}).catch(() => {});
}
/** 是否渲染字段 */
function renderField(insert, edit) {
return form.value.{{ pkColumn.python_field }} == null ? insert : edit;
}
getList();
</script>
@@ -13,12 +13,13 @@
v-model="queryParams.{{ column.python_field }}"
placeholder="请输入{{ comment }}"
clearable
style="width: 240px"
@keyup.enter="handleQuery"
/>
</el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and dictType %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-select v-model="queryParams.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable>
<el-select v-model="queryParams.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable style="width: 240px">
<el-option
v-for="dict in {{ dictType }}"
:key="dict.value"
@@ -29,18 +30,20 @@
</el-form-item>
{% elif (column.html_type == "select" or column.html_type == "radio") and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-select v-model="queryParams.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable>
<el-select v-model="queryParams.{{ column.python_field }}" placeholder="请选择{{ comment }}" clearable style="width: 240px">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type != "BETWEEN" %}
<el-form-item label="{{ comment }}" prop="{{ column.python_field }}">
<el-date-picker clearable
<el-date-picker
v-model="queryParams.{{ column.python_field }}"
type="date"
value-format="YYYY-MM-DD"
placeholder="请选择{{ comment }}">
</el-date-picker>
placeholder="请选择{{ comment }}"
clearable
style="width: 240px"
/>
</el-form-item>
{% elif column.html_type == "datetime" and column.query_type == "BETWEEN" %}
<el-form-item label="{{ comment }}" style="width: 308px">
@@ -51,7 +54,8 @@
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
></el-date-picker>
style="width: 240px"
/>
</el-form-item>
{% endif %}
{% endif %}
@@ -159,29 +163,29 @@
<el-form ref="{{ businessName }}Ref" :model="form" :rules="rules" label-width="80px">
{% for column in columns %}
{% set field = column.python_field %}
{% if column.insert and not column.pk %}
{% if (column.insert or column.edit) and not column.pk and column.column_name not in column_not_add_show + column_not_edit_show %}
{% if column.usable_column or not column.super_column %}
{% set parentheseIndex = column.column_comment.find("") %}
{% set comment = column.column_comment[:parentheseIndex] if parentheseIndex != -1 else column.column_comment %}
{% set dictType = column.dict_type %}
{% if column.html_type == "input" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-input v-model="form.{{ field }}" placeholder="请输入{{ comment }}" />
</el-form-item>
{% elif column.html_type == "imageUpload" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<image-upload v-model="form.{{ field }}"/>
</el-form-item>
{% elif column.html_type == "fileUpload" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<file-upload v-model="form.{{ field }}"/>
</el-form-item>
{% elif column.html_type == "editor" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<editor v-model="form.{{ field }}" :min-height="192"/>
</el-form-item>
{% elif column.html_type == "select" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-select v-model="form.{{ field }}" placeholder="请选择{{ comment }}">
<el-option
v-for="dict in {{ dictType }}"
@@ -196,13 +200,13 @@
</el-select>
</el-form-item>
{% elif column.html_type == "select" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-select v-model="form.{{ field }}" placeholder="请选择{{ comment }}">
<el-option label="请选择字典生成" value="" />
</el-select>
</el-form-item>
{% elif column.html_type == "checkbox" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-checkbox-group v-model="form.{{ field }}">
<el-checkbox
v-for="dict in {{ dictType }}"
@@ -213,13 +217,13 @@
</el-checkbox-group>
</el-form-item>
{% elif column.html_type == "checkbox" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-checkbox-group v-model="form.{{ field }}">
<el-checkbox>请选择字典生成</el-checkbox>
</el-checkbox-group>
</el-form-item>
{% elif column.html_type == "radio" and dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-radio-group v-model="form.{{ field }}">
<el-radio
v-for="dict in {{ dictType }}"
@@ -234,13 +238,13 @@
</el-radio-group>
</el-form-item>
{% elif column.html_type == "radio" and not dictType %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-radio-group v-model="form.{{ field }}">
<el-radio label="请选择字典生成" value="" />
</el-radio-group>
</el-form-item>
{% elif column.html_type == "datetime" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-date-picker clearable
v-model="form.{{ field }}"
type="date"
@@ -249,7 +253,7 @@
</el-date-picker>
</el-form-item>
{% elif column.html_type == "textarea" %}
<el-form-item label="{{ comment }}" prop="{{ field }}">
<el-form-item v-if="renderField({{ column.insert | lower }}, {{ column.edit | lower }})" label="{{ comment }}" prop="{{ field }}">
<el-input v-model="form.{{ field }}" type="textarea" placeholder="请输入内容" />
</el-form-item>
{% endif %}
@@ -397,8 +401,8 @@ function getList() {
{% if column.html_type == "datetime" and column.query_type == "BETWEEN" %}
{% set AttrName = column.python_field[0] | upper + column.python_field[1:] %}
if (null != daterange{{ AttrName }} && '' != daterange{{ AttrName }}) {
queryParams.value.params["begin{{ AttrName }}"] = daterange{{ AttrName }}.value[0];
queryParams.value.params["end{{ AttrName }}"] = daterange{{ AttrName }}.value[1];
queryParams.value.begin{{ AttrName }} = daterange{{ AttrName }}.value[0];
queryParams.value.end{{ AttrName }} = daterange{{ AttrName }}.value[1];
}
{% endif %}
{% endfor %}
@@ -567,5 +571,10 @@ function handleExport() {
}, `{{ businessName }}_${new Date().getTime()}.xlsx`);
}
/** 是否渲染字段 */
function renderField(insert, edit) {
return form.value.{{ pkColumn.python_field }} == null ? insert : edit;
}
getList();
</script>
+9 -7
View File
@@ -1,17 +1,19 @@
alembic==1.16.4
APScheduler==3.11.0
async-lru==2.0.5
asyncpg==0.30.0
bcrypt==5.0.0
DateTime==5.5
fastapi[all]==0.115.8
fastapi[all]==0.116.1
loguru==0.7.3
openpyxl==3.1.5
pandas==2.2.3
passlib[bcrypt]==1.7.4
Pillow==11.1.0
pandas==2.3.2
Pillow==11.3.0
psutil==7.0.0
pydantic-validation-decorator==0.1.4
PyJWT[crypto]==2.10.1
psycopg2==2.9.10
redis==5.2.1
requests==2.32.3
SQLAlchemy[asyncio]==2.0.38
redis==6.4.0
SQLAlchemy[asyncio]==2.0.43
sqlglot[rs]==27.8.0
user-agents==2.2.0
+9 -7
View File
@@ -1,17 +1,19 @@
alembic==1.16.4
APScheduler==3.11.0
async-lru==2.0.5
asyncmy==0.2.10
bcrypt==5.0.0
DateTime==5.5
fastapi[all]==0.115.8
fastapi[all]==0.116.1
loguru==0.7.3
openpyxl==3.1.5
pandas==2.2.3
passlib[bcrypt]==1.7.4
Pillow==11.1.0
pandas==2.3.2
Pillow==11.3.0
psutil==7.0.0
pydantic-validation-decorator==0.1.4
PyJWT[crypto]==2.10.1
PyMySQL==1.1.1
redis==5.2.1
requests==2.32.3
SQLAlchemy[asyncio]==2.0.38
redis==6.4.0
SQLAlchemy[asyncio]==2.0.43
sqlglot[rs]==27.8.0
user-agents==2.2.0
+2 -2
View File
@@ -31,14 +31,14 @@ from utils.log_util import logger
# 生命周期事件
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info(f'{AppConfig.app_name}开始启动')
logger.info(f'⏰️ {AppConfig.app_name}开始启动')
worship()
await init_create_table()
app.state.redis = await RedisUtil.create_redis_pool()
await RedisUtil.init_sys_dict(app.state.redis)
await RedisUtil.init_sys_config(app.state.redis)
await SchedulerUtil.init_system_scheduler()
logger.info(f'{AppConfig.app_name}启动成功')
logger.info(f'🚀 {AppConfig.app_name}启动成功')
yield
await RedisUtil.close_redis_pool(app)
await SchedulerUtil.close_system_scheduler()
+14 -8
View File
@@ -69,6 +69,7 @@ create table sys_user (
del_flag char(1) default '0',
login_ip varchar(128) default '',
login_date timestamp(0),
pwd_update_date timestamp(0),
create_by varchar(64) default '',
create_time timestamp(0),
update_by varchar(64) default '',
@@ -91,6 +92,7 @@ comment on column sys_user.status is '帐号状态(0正常 1停用)';
comment on column sys_user.del_flag is '删除标志(0代表存在 2代表删除)';
comment on column sys_user.login_ip is '最后登录IP';
comment on column sys_user.login_date is '最后登录时间';
comment on column sys_user.pwd_update_date is '密码最后更新时间';
comment on column sys_user.create_by is '创建者';
comment on column sys_user.create_time is '创建时间';
comment on column sys_user.update_by is '更新者';
@@ -98,6 +100,12 @@ comment on column sys_user.update_time is '更新时间';
comment on column sys_user.remark is '备注';
comment on table sys_user is '用户信息表';
-- ----------------------------
-- 初始化-用户信息表数据
-- ----------------------------
insert into sys_user values(1, 103, 'admin', '超级管理员', '00', 'niangao@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', current_timestamp, current_timestamp, 'admin', current_timestamp, '', null, '管理员');
insert into sys_user values(2, 105, 'niangao', '年糕', '00', 'niangao@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', current_timestamp, current_timestamp, 'admin', current_timestamp, '', null, '测试员');
-- ----------------------------
-- 3、岗位信息表
-- ----------------------------
@@ -136,12 +144,6 @@ insert into sys_post values(2, 'se', '项目经理', 2, '0', 'admin', current
insert into sys_post values(3, 'hr', '人力资源', 3, '0', 'admin', current_timestamp, '', null, '');
insert into sys_post values(4, 'user', '普通员工', 4, '0', 'admin', current_timestamp, '', null, '');
-- ----------------------------
-- 初始化-用户信息表数据
-- ----------------------------
insert into sys_user values(1, 103, 'admin', '超级管理员', '00', 'niangao@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', current_timestamp, 'admin', current_timestamp, '', null, '管理员');
insert into sys_user values(2, 105, 'niangao', '年糕', '00', 'niangao@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', current_timestamp, 'admin', current_timestamp, '', null, '测试员');
-- ----------------------------
-- 4、角色信息表
-- ----------------------------
@@ -522,7 +524,7 @@ create table sys_oper_log (
status int4 default 0,
error_msg varchar(2000) default '',
oper_time timestamp(0),
cost_time int8 default 0,
cost_time bigint default 0,
primary key (oper_id)
);
alter sequence sys_oper_log_oper_id_seq restart 100;
@@ -545,6 +547,7 @@ comment on column sys_oper_log.json_result is '返回参数';
comment on column sys_oper_log.status is '操作状态(0正常 1异常)';
comment on column sys_oper_log.error_msg is '错误消息';
comment on column sys_oper_log.oper_time is '操作时间';
comment on column sys_oper_log.cost_time is '消耗时间';
comment on table sys_oper_log is '操作日志记录';
-- ----------------------------
@@ -703,6 +706,8 @@ insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.
insert into sys_config values(4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', current_timestamp, '', null, '是否开启验证码功能(true开启,false关闭)');
insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', current_timestamp, '', null, '是否开启注册用户功能(true开启,false关闭)');
insert into sys_config values(6, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', current_timestamp, '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)');
insert into sys_config values(7, '用户管理-初始密码修改策略', 'sys.account.initPasswordModify', '1', 'Y', 'admin', current_timestamp, '', null, '0:初始密码修改策略关闭,没有任何提示,1:提醒用户,如果未修改初始密码,则在登录时就会提醒修改密码对话框');
insert into sys_config values(8, '用户管理-账号密码更新周期', 'sys.account.passwordValidateDays', '0', 'Y', 'admin', current_timestamp, '', null, '密码更新周期(填写数字,数据初始化值为0不限制,若修改必须为大于0小于365的正整数),如果超过这个周期登录系统时,则在登录时就会提醒修改密码对话框');
-- ----------------------------
-- 14、系统访问记录
@@ -931,7 +936,8 @@ create table gen_table_column (
create_time timestamp(0),
update_by varchar(64) default '',
update_time timestamp(0),
primary key (column_id)
primary key (column_id),
constraint fk_gen_table_column_table_id foreign key (table_id) references gen_table(table_id)
);
comment on column gen_table_column.column_id is '编号';
comment on column gen_table_column.table_id is '归属表编号';
+7 -3
View File
@@ -54,6 +54,7 @@ create table sys_user (
del_flag char(1) default '0' comment '删除标志(0代表存在 2代表删除)',
login_ip varchar(128) default '' comment '最后登录IP',
login_date datetime comment '最后登录时间',
pwd_update_date datetime comment '密码最后更新时间',
create_by varchar(64) default '' comment '创建者',
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
@@ -65,8 +66,8 @@ create table sys_user (
-- ----------------------------
-- 初始化-用户信息表数据
-- ----------------------------
insert into sys_user values(1, 103, 'admin', '超级管理员', '00', 'niangao@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'admin', sysdate(), '', null, '管理员');
insert into sys_user values(2, 105, 'niangao', '年糕', '00', 'niangao@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), 'admin', sysdate(), '', null, '测试员');
insert into sys_user values(1, 103, 'admin', '超级管理员', '00', 'niangao@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), sysdate(), 'admin', sysdate(), '', null, '管理员');
insert into sys_user values(2, 105, 'niangao', '年糕', '00', 'niangao@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', sysdate(), sysdate(), 'admin', sysdate(), '', null, '测试员');
-- ----------------------------
@@ -554,6 +555,8 @@ insert into sys_config values(3, '主框架页-侧边栏主题', 'sys.
insert into sys_config values(4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', sysdate(), '', null, '是否开启验证码功能(true开启,false关闭)');
insert into sys_config values(5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', sysdate(), '', null, '是否开启注册用户功能(true开启,false关闭)');
insert into sys_config values(6, '用户登录-黑名单列表', 'sys.login.blackIPList', '', 'Y', 'admin', sysdate(), '', null, '设置登录IP黑名单限制,多个匹配项以;分隔,支持匹配(*通配、网段)');
insert into sys_config values(7, '用户管理-初始密码修改策略', 'sys.account.initPasswordModify', '1', 'Y', 'admin', sysdate(), '', null, '0:初始密码修改策略关闭,没有任何提示,1:提醒用户,如果未修改初始密码,则在登录时就会提醒修改密码对话框');
insert into sys_config values(8, '用户管理-账号密码更新周期', 'sys.account.passwordValidateDays', '0', 'Y', 'admin', sysdate(), '', null, '密码更新周期(填写数字,数据初始化值为0不限制,若修改必须为大于0小于365的正整数),如果超过这个周期登录系统时,则在登录时就会提醒修改密码对话框');
-- ----------------------------
@@ -709,5 +712,6 @@ create table gen_table_column (
create_time datetime comment '创建时间',
update_by varchar(64) default '' comment '更新者',
update_time datetime comment '更新时间',
primary key (column_id)
primary key (column_id),
foreign key (table_id) references gen_table(table_id)
) engine=innodb auto_increment=1 comment = '代码生成业务表字段';
@@ -8,6 +8,7 @@ from openpyxl.utils import get_column_letter
from openpyxl.worksheet.datavalidation import DataValidation
from sqlalchemy.engine.row import Row
from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.sql.expression import TextClause, null
from typing import Any, Dict, List, Literal, Union
from config.database import Base
from config.env import CachePathConfig
@@ -100,6 +101,19 @@ class SqlalchemyUtil:
return result_dict
return result
@classmethod
def get_server_default_null(cls, dialect_name: str, need_explicit_null: bool = True) -> Union[TextClause, None]:
"""
根据数据库方言动态返回值为null的server_default
:param dialect_name: 数据库方言名称
:param need_explicit_null: 是否需要显式DEFAULT NULL
:return: 不同数据库方言对应的null_server_default
"""
if need_explicit_null and dialect_name == 'postgresql':
return null()
return None
class CamelCaseUtil:
"""
+121
View File
@@ -0,0 +1,121 @@
import importlib
import inspect
import os
from pathlib import Path
import sys
from functools import lru_cache
from sqlalchemy import inspect as sa_inspect
from typing import Any, List
from config.database import Base
class ImportUtil:
@classmethod
def find_project_root(cls) -> Path:
"""
查找项目根目录
:return: 项目根目录路径
"""
current_dir = Path(__file__).resolve().parent
while current_dir != current_dir.parent:
if any(current_dir.joinpath(file).exists() for file in ['setup.py', 'pyproject.toml', 'requirements.txt']):
return current_dir
current_dir = current_dir.parent
return Path(__file__).resolve().parent
@classmethod
def is_valid_model(cls, obj: Any, base_class: Base) -> bool:
"""
验证是否为有效的SQLAlchemy模型类
:param obj: 待验证的对象
:param base_class: SQLAlchemy的Base类
:return: 验证结果
"""
# 必须继承自Base且不是Base本身
if not (inspect.isclass(obj) and issubclass(obj, base_class) and obj is not base_class):
return False
# 必须有表名定义(排除抽象基类)
if not hasattr(obj, '__tablename__') or obj.__tablename__ is None:
return False
# 必须有至少一个列定义
try:
return len(sa_inspect(obj).columns) > 0
except Exception:
return False
@classmethod
@lru_cache(maxsize=256)
def find_models(cls, base_class: Base) -> List[Base]:
"""
查找并过滤有效的模型类避免重复和无效定义
:param base_class: SQLAlchemy的Base类用于验证模型类
:return: 有效模型类列表
"""
models = []
# 按类对象去重
seen_models = set()
# 按表名去重(防止同表名冲突)
seen_tables = set()
project_root = cls.find_project_root()
sys.path.append(str(project_root))
print(f"⏰️ 开始在项目根目录 {project_root} 中查找模型...")
# 排除目录扩展
exclude_dirs = {
'venv',
'.env',
'.git',
'__pycache__',
'migrations',
'alembic',
'tests',
'test',
'docs',
'examples',
'scripts',
}
for root, dirs, files in os.walk(project_root):
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for file in files:
if file.endswith('.py') and not file.startswith('__'):
relative_path = Path(root).relative_to(project_root)
module_parts = list(relative_path.parts) + [file[:-3]]
module_name = '.'.join(module_parts)
try:
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
# 验证模型有效性
if not cls.is_valid_model(obj, base_class):
continue
# 检查类对象重复
if obj in seen_models:
continue
# 检查表名重复
table_name = obj.__tablename__
if table_name in seen_tables:
continue
seen_models.add(obj)
seen_tables.add(table_name)
models.append(obj)
print(f'✅️ 找到有效模型: {obj.__module__}.{obj.__name__} (表: {table_name})')
except ImportError as e:
if 'cannot import name' not in str(e):
print(f'❗️ 警告: 无法导入模块 {module_name}: {e}')
except Exception as e:
print(f'❌️ 处理模块 {module_name} 时出错: {e}')
return models
+6 -6
View File
@@ -1,6 +1,4 @@
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto')
import bcrypt
class PwdUtil:
@@ -9,7 +7,7 @@ class PwdUtil:
"""
@classmethod
def verify_password(cls, plain_password, hashed_password):
def verify_password(cls, plain_password: str, hashed_password: str) -> bool:
"""
工具方法校验当前输入的密码与数据库存储的密码是否一致
@@ -17,7 +15,9 @@ class PwdUtil:
:param hashed_password: 数据库存储的密码
:return: 校验结果
"""
return pwd_context.verify(plain_password, hashed_password)
return (
bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) if hashed_password else None
)
@classmethod
def get_password_hash(cls, input_password):
@@ -27,4 +27,4 @@ class PwdUtil:
:param input_password: 输入的密码
:return: 加密成功的密码
"""
return pwd_context.hash(input_password)
return bcrypt.hashpw(input_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
@@ -73,6 +73,7 @@ class TemplateUtils:
context = {
'tplCategory': tpl_category,
'tableName': gen_table.table_name,
'tableComment': gen_table.table_comment,
'functionName': function_name if StringUtil.is_not_empty(function_name) else '【请填写功能名称】',
'ClassName': class_name,
'className': class_name.lower(),
@@ -91,6 +92,8 @@ class TemplateUtils:
'table': gen_table,
'dicts': cls.get_dicts(gen_table),
'dbType': DataBaseConfig.db_type,
'column_not_add_show': GenConstant.COLUMNNAME_NOT_ADD_SHOW,
'column_not_edit_show': GenConstant.COLUMNNAME_NOT_EDIT_SHOW,
}
# 设置菜单、树形结构、子表的上下文
@@ -1,4 +1,7 @@
import datetime
from copy import deepcopy
from datetime import datetime
from dateutil.parser import parse
from typing import Dict, List, Union
def object_format_datetime(obj):
@@ -8,7 +11,7 @@ def object_format_datetime(obj):
"""
for attr in dir(obj):
value = getattr(obj, attr)
if isinstance(value, datetime.datetime):
if isinstance(value, datetime):
setattr(obj, attr, value.strftime('%Y-%m-%d %H:%M:%S'))
return obj
@@ -21,7 +24,7 @@ def list_format_datetime(lst):
for obj in lst:
for attr in dir(obj):
value = getattr(obj, attr)
if isinstance(value, datetime.datetime):
if isinstance(value, datetime):
setattr(obj, attr, value.strftime('%Y-%m-%d %H:%M:%S'))
return lst
@@ -41,7 +44,7 @@ def format_datetime_dict_list(dicts):
if isinstance(v, dict):
# 递归遍历子字典
new_item[k] = format_datetime_dict_list([v])[0]
elif isinstance(v, datetime.datetime):
elif isinstance(v, datetime):
# 如果值是 datetime 类型,则格式化为字符串
new_item[k] = v.strftime('%Y-%m-%d %H:%M:%S')
else:
@@ -50,3 +53,89 @@ def format_datetime_dict_list(dicts):
result.append(new_item)
return result
class TimeFormatUtil:
"""
时间格式化工具类
"""
@classmethod
def format_time(cls, time_info: Union[str, datetime], format: str = '%Y-%m-%d %H:%M:%S'):
"""
格式化时间字符串或datetime对象为指定格式
:param time_info: 时间字符串或datetime对象
:param format: 格式化格式默认为'%Y-%m-%d %H:%M:%S'
:return: 格式化后的时间字符串
"""
if isinstance(time_info, datetime):
format_date = time_info.strftime(format)
else:
try:
date = parse(time_info)
format_date = date.strftime(format)
except Exception:
format_date = time_info
return format_date
@classmethod
def parse_date(cls, time_str: str):
"""
解析时间字符串提取日期部分
:param time_str: 时间字符串
:return: 日期部分
"""
try:
dt = parse(time_str)
return dt.date()
except Exception:
return time_str
@classmethod
def format_time_dict(cls, time_dict: Dict, format: str = '%Y-%m-%d %H:%M:%S'):
"""
格式化时间字典
:param time_dict: 时间字典
:param format: 格式化格式默认为'%Y-%m-%d %H:%M:%S'
:return: 格式化后的时间字典
"""
copy_time_dict = deepcopy(time_dict)
for k, v in copy_time_dict.items():
if isinstance(v, (str, datetime)):
copy_time_dict[k] = cls.format_time(v, format)
elif isinstance(v, dict):
copy_time_dict[k] = cls.format_time_dict(v, format)
elif isinstance(v, list):
copy_time_dict[k] = cls.format_time_list(v, format)
else:
copy_time_dict[k] = v
return copy_time_dict
@classmethod
def format_time_list(cls, time_list: List, format: str = '%Y-%m-%d %H:%M:%S'):
"""
格式化时间列表
:param time_list: 时间列表
:param format: 格式化格式默认为'%Y-%m-%d %H:%M:%S'
:return: 格式化后的时间列表
"""
format_time_list = []
for item in time_list:
if isinstance(item, (str, datetime)):
format_item = cls.format_time(item, format)
elif isinstance(item, dict):
format_item = cls.format_time_dict(item, format)
elif isinstance(item, list):
format_item = cls.format_time_list(item, format)
else:
format_item = item
format_time_list.append(format_item)
return format_time_list
+16 -14
View File
@@ -1,6 +1,6 @@
{
"name": "vfadmin",
"version": "1.6.0",
"version": "1.7.1",
"description": "vfadmin管理系统",
"author": "insistence",
"license": "MIT",
@@ -20,34 +20,36 @@
"@antv/g2plot": "^2.4.31",
"@element-plus/icons-vue": "2.3.1",
"@vueup/vue-quill": "1.2.0",
"@vueuse/core": "10.11.0",
"@vueuse/core": "13.3.0",
"ant-design-vue": "^4.1.1",
"axios": "0.28.1",
"axios": "1.9.0",
"clipboard": "2.0.11",
"echarts": "5.5.1",
"element-plus": "2.7.6",
"echarts": "5.6.0",
"element-plus": "2.10.7",
"file-saver": "2.0.5",
"fuse.js": "6.6.2",
"js-beautify": "1.15.1",
"js-cookie": "3.0.5",
"jsencrypt": "3.3.2",
"nprogress": "0.2.0",
"pinia": "2.1.7",
"splitpanes": "3.1.5",
"vue": "3.4.15",
"pinia": "3.0.2",
"splitpanes": "4.0.4",
"vue": "3.5.16",
"vue-cropper": "1.1.1",
"vue-router": "4.4.0",
"vue-router": "4.5.1",
"vuedraggable": "4.1.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "5.0.5",
"@vue/compiler-sfc": "3.3.9",
"@vitejs/plugin-vue": "5.2.4",
"less": "^4.2.0",
"sass": "1.77.5",
"unplugin-auto-import": "0.17.6",
"sass-embedded": "1.89.1",
"unplugin-auto-import": "0.18.6",
"unplugin-vue-setup-extend-plus": "1.0.1",
"vite": "5.3.2",
"vite": "6.3.5",
"vite-plugin-compression": "0.5.1",
"vite-plugin-svg-icons": "2.0.1"
},
"overrides": {
"quill": "2.0.2"
}
}
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746590936918" class="icon" viewBox="0 0 1194 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5378" xmlns:xlink="http://www.w3.org/1999/xlink" width="233.203125" height="200"><path d="M1151.9144 325.11999969V89.12a57.04000031 57.04000031 0 0 0-28.8-49.44 58.15999969 58.15999969 0 0 0-57.76000031 0 57.04000031 57.04000031 0 0 0-28.8 49.44v235.99999969c0.24 84.31999969-33.6 152.56000031-94.08 212.00000062-60.07999969 59.83999969-141.84 80.64-227.04 80.4H225.91440031L388.07439969 457.11999969a56.80000031 56.80000031 0 0 0 12.40000031-62.16 57.76000031 57.76000031 0 0 0-94.00000031-18.63999938L48.8744 631.20000031a56.88 56.88 0 0 0 0 80.79999938l264.96 262.56a58.08 58.08 0 0 0 96.55999969-25.59999938 56.80000031 56.80000031 0 0 0-14.95999969-55.2L232.07439969 731.67999969h483.44000062c116.56000031 0 226.15999969-32.08000031 308.64-113.76 82.15999969-80.80000031 128.23999969-178.15999969 127.83999938-292.87999969" p-id="5379"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1746760911144" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12537" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M395.21211 182.914448c0 62.669318 49.541323 113.472378 110.642936 113.472378 61.093427 0 110.652146-50.80306 110.65214601-113.472378 0-62.685691-49.559742-113.487727-110.65214601-113.487727C444.75241 69.426721 395.21211 120.22978 395.21211 182.914448zM395.21211 523.34693101c0 62.668295 49.541323 113.487727 110.642936 113.48772699 61.093427 0 110.652146-50.820456 110.652146-113.487727 0-62.669318-49.559742-113.472378-110.652146-113.472378C444.75241 409.874553 395.21211 460.67761301 395.21211 523.34693101zM395.21211 841.084529c0 62.686714 49.541323 113.488751 110.642936 113.488751 61.093427 0 110.652146-50.80203599 110.65214601-113.488751 0-62.669318-49.559742-113.471354-110.65214601-113.471354C444.75241 727.614198 395.21211 778.416234 395.21211 841.084529z" p-id="12538"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -1,4 +1,4 @@
@import './variables.module.scss';
@use './variables.module.scss' as *;
@mixin colorBtn($color) {
background: $color;
@@ -1,10 +1,9 @@
@import './variables.module.scss';
@import './mixin.scss';
@import './transition.scss';
@import './element-ui.scss';
@import './sidebar.scss';
@import './btn.scss';
@import './ruoyi.scss';
@use './mixin.scss';
@use './transition.scss';
@use './element-ui.scss';
@use './sidebar.scss';
@use './btn.scss';
@use './ruoyi.scss';
body {
height: 100%;
@@ -131,10 +130,6 @@ aside {
position: relative;
}
.pagination-container {
margin-top: 30px;
}
.text-center {
text-align: center
}
@@ -102,21 +102,12 @@
/** 表格布局 **/
.pagination-container {
position: relative;
height: 25px;
margin-bottom: 10px;
margin-top: 15px;
padding: 10px 20px !important;
display: flex;
justify-content: flex-end;
margin-top: 20px;
background-color: transparent !important;
}
/* 分页器定位 */
.pagination-container .el-pagination {
position: absolute;
right: 0;
top: 0;
}
/* 弹窗中的分页器 */
.el-dialog .pagination-container {
position: static !important;
@@ -207,8 +198,6 @@
}
.card-box {
padding-right: 15px;
padding-left: 15px;
margin-bottom: 10px;
}
@@ -1,9 +1,11 @@
@use './variables.module.scss' as vars;
#app {
.main-container {
min-height: 100%;
transition: margin-left .28s;
margin-left: $base-sidebar-width;
margin-left: vars.$base-sidebar-width;
position: relative;
}
@@ -13,7 +15,7 @@
.sidebar-container {
transition: width 0.28s;
width: $base-sidebar-width !important;
width: vars.$base-sidebar-width !important;
height: 100%;
position: fixed;
font-size: 0px;
@@ -23,7 +25,7 @@
z-index: 1001;
overflow: hidden;
-webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
box-shadow: 2px 0 6px rgba(0,21,41,.35);
box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1);
// reset element-ui css
.horizontal-collapse-transition {
@@ -87,12 +89,12 @@
}
& .theme-dark .is-active > .el-sub-menu__title {
color: $base-menu-color-active !important;
color: vars.$base-menu-color-active !important;
}
& .nest-menu .el-sub-menu>.el-sub-menu__title,
& .el-sub-menu .el-menu-item {
min-width: $base-sidebar-width !important;
min-width: vars.$base-sidebar-width !important;
&:hover {
background-color: rgba(0, 0, 0, 0.06) !important;
@@ -101,10 +103,10 @@
& .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
& .theme-dark .el-sub-menu .el-menu-item {
background-color: $base-sub-menu-background;
background-color: vars.$base-sub-menu-background;
&:hover {
background-color: $base-sub-menu-hover !important;
background-color: vars.$base-sub-menu-hover !important;
}
}
}
@@ -167,7 +169,7 @@
}
.el-menu--collapse .el-menu .el-sub-menu {
min-width: $base-sidebar-width !important;
min-width: vars.$base-sidebar-width !important;
}
// mobile responsive
@@ -178,14 +180,14 @@
.sidebar-container {
transition: transform .28s;
width: $base-sidebar-width !important;
width: vars.$base-sidebar-width !important;
}
&.hideSidebar {
.sidebar-container {
pointer-events: none;
transition-duration: 0.3s;
transform: translate3d(-$base-sidebar-width, 0, 0);
transform: translate3d(-(vars.$base-sidebar-width), 0, 0);
}
}
}
@@ -6,7 +6,7 @@
transition: opacity 0.28s;
}
.fade-enter,
.fade-enter-from,
.fade-leave-active {
opacity: 0;
}
@@ -18,7 +18,7 @@
transition: all .5s;
}
.fade-transform-enter {
.fade-transform-enter-from {
opacity: 0;
transform: translateX(-30px);
}
@@ -34,7 +34,7 @@
transition: all .5s;
}
.breadcrumb-enter,
.breadcrumb-enter-from,
.breadcrumb-leave-active {
opacity: 0;
transform: translateX(20px);
@@ -70,42 +70,46 @@
<p class="title">时间表达式</p>
<table>
<thead>
<th v-for="item of tabTitles" :key="item">{{item}}</th>
<th>Cron 表达式</th>
<tr>
<th v-for="item of tabTitles" :key="item">{{item}}</th>
<th>Cron 表达式</th>
</tr>
</thead>
<tbody>
<td>
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
<el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
<el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
<el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
<el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
<el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
<el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
</td>
<td class="result">
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
</td>
<tr>
<td>
<span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
<el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
<el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
<el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
<el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
<el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
<el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
</td>
<td>
<span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
<el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
</td>
<td class="result">
<span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
<el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
</td>
</tr>
</tbody>
</table>
</div>
@@ -61,22 +61,24 @@ const props = defineProps({
}
}
})
const fullYear = ref(0)
const maxFullYear = ref(0)
const fullYear = Number(new Date().getFullYear())
const maxFullYear = fullYear + 10
const radioValue = ref(1)
const cycle01 = ref(0)
const cycle02 = ref(0)
const average01 = ref(0)
const cycle01 = ref(fullYear)
const cycle02 = ref(fullYear + 1)
const average01 = ref(fullYear)
const average02 = ref(1)
const checkboxList = ref([])
const checkCopy = ref([])
const checkCopy = ref([fullYear])
const cycleTotal = computed(() => {
cycle01.value = props.check(cycle01.value, fullYear.value, maxFullYear.value - 1)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear.value)
cycle01.value = props.check(cycle01.value, fullYear, maxFullYear - 1)
cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear)
return cycle01.value + '-' + cycle02.value
})
const averageTotal = computed(() => {
average01.value = props.check(average01.value, fullYear.value, maxFullYear.value - 1)
average01.value = props.check(average01.value, fullYear, maxFullYear - 1)
average02.value = props.check(average02.value, 1, 10)
return average01.value + '/' + average02.value
})
@@ -97,8 +99,8 @@ function changeRadioValue(value) {
radioValue.value = 3
} else if (value.indexOf("/") > -1) {
const indexArr = value.split('/')
average01.value = Number(indexArr[1])
average02.value = Number(indexArr[0])
average01.value = Number(indexArr[0])
average02.value = Number(indexArr[1])
radioValue.value = 4
} else {
checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
@@ -129,14 +131,6 @@ function onRadioChange() {
break
}
}
onMounted(() => {
fullYear.value = Number(new Date().getFullYear())
maxFullYear.value = fullYear.value + 10
cycle01.value = fullYear.value
cycle02.value = cycle01.value + 1
average01.value = fullYear.value
checkCopy.value = [fullYear.value]
})
</script>
<style lang="scss" scoped>
@@ -27,6 +27,7 @@
</template>
<script setup>
import axios from 'axios';
import { QuillEditor } from "@vueup/vue-quill";
import "@vueup/vue-quill/dist/vue-quill.snow.css";
import { getToken } from "@/utils/auth";
@@ -124,6 +125,7 @@ onMounted(() => {
quill.format("image", false);
}
});
quill.root.addEventListener('paste', handlePasteCapture, true);
}
});
@@ -168,6 +170,29 @@ function handleUploadSuccess(res, file) {
function handleUploadError() {
proxy.$modal.msgError("图片插入失败");
}
//
function handlePasteCapture(e) {
const clipboard = e.clipboardData || window.clipboardData;
if (clipboard && clipboard.items) {
for (let i = 0; i < clipboard.items.length; i++) {
const item = clipboard.items[i];
if (item.type.indexOf('image') !== -1) {
e.preventDefault();
const file = item.getAsFile();
insertImage(file);
}
}
}
}
function insertImage(file) {
const formData = new FormData();
formData.append("file", file);
axios.post(uploadUrl.value, formData, { headers: { "Content-Type": "multipart/form-data", Authorization: headers.value.Authorization } }).then(res => {
handleUploadSuccess(res.data);
})
}
</script>
<style>
@@ -5,6 +5,7 @@
:action="uploadFileUrl"
:before-upload="handleBeforeUpload"
:file-list="fileList"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
@@ -13,25 +14,26 @@
:headers="headers"
class="upload-file-uploader"
ref="fileUpload"
v-if="!disabled"
>
<!-- 上传按钮 -->
<el-button type="primary">选取文件</el-button>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
<template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
的文件
</div>
<!-- 文件列表 -->
<transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<transition-group ref="uploadFileList" class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
<li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
<el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
<span class="el-icon-document"> {{ getFileName(file.name) }} </span>
</el-link>
<div class="ele-upload-list__item-content-action">
<el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
<el-link :underline="false" @click="handleDelete(index)" type="danger" v-if="!disabled">删除</el-link>
</div>
</li>
</transition-group>
@@ -40,28 +42,48 @@
<script setup>
import { getToken } from "@/utils/auth";
import Sortable from 'sortablejs';
const props = defineProps({
modelValue: [String, Object, Array],
//
action: {
type: String,
default: "/common/upload"
},
//
data: {
type: Object
},
//
limit: {
type: Number,
default: 5,
default: 5
},
// (MB)
fileSize: {
type: Number,
default: 5,
default: 5
},
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["doc", "xls", "ppt", "txt", "pdf"],
default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
},
//
isShowTip: {
type: Boolean,
default: true
},
//
disabled: {
type: Boolean,
default: false
},
//
drag: {
type: Boolean,
default: true
}
});
@@ -70,7 +92,7 @@ const emit = defineEmits();
const number = ref(0);
const uploadList = ref([]);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); //
const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action); //
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const showTip = computed(
@@ -134,6 +156,7 @@ function handleExceed() {
//
function handleUploadError(err) {
proxy.$modal.msgError("上传文件失败");
proxy.$modal.closeLoading();
}
//
@@ -188,9 +211,30 @@ function listToString(list, separator) {
}
return strs != '' ? strs.substr(0, strs.length - 1) : '';
}
//
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.uploadFileList?.$el || proxy.$refs.uploadFileList
Sortable.create(element, {
ghostClass: 'file-upload-darg',
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss">
.file-upload-darg {
opacity: 0.5;
background: #c8ebfb;
}
.upload-file-uploader {
margin-bottom: 5px;
}
@@ -199,6 +243,7 @@ function listToString(list, separator) {
line-height: 2;
margin-bottom: 10px;
position: relative;
transition: none !important;
}
.upload-file-list .ele-upload-list__item-content {
display: flex;
@@ -1,19 +1,46 @@
<template>
<div :class="{ 'show': show }" class="header-search">
<div class="header-search">
<svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
<el-select
ref="headerSearchSelectRef"
v-model="search"
:remote-method="querySearch"
filterable
default-first-option
remote
placeholder="Search"
class="header-search-select"
@change="change"
<el-dialog
v-model="show"
width="600"
@close="close"
:show-close="false"
append-to-body
>
<el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
</el-select>
<el-input
v-model="search"
ref="headerSearchSelectRef"
size="large"
@input="querySearch"
prefix-icon="Search"
placeholder="菜单搜索,支持标题、URL模糊查询"
clearable
@keyup.enter="selectActiveResult"
@keydown.up.prevent="navigateResult('up')"
@keydown.down.prevent="navigateResult('down')"
>
</el-input>
<div class="result-wrap">
<el-scrollbar>
<div class="search-item" tabindex="1" v-for="(item, index) in options" :key="item.path" :style="activeStyle(index)" @mouseenter="activeIndex = index" @mouseleave="activeIndex = -1">
<div class="left">
<svg-icon class="menu-icon" :icon-class="item.icon" />
</div>
<div class="search-info" @click="change(item)">
<div class="menu-title">
{{ item.title.join(" / ") }}
</div>
<div class="menu-path">
{{ item.path }}
</div>
</div>
<svg-icon icon-class="enter" v-show="index === activeIndex"/>
</div>
</el-scrollbar>
</div>
</el-dialog>
</div>
</template>
@@ -21,38 +48,46 @@
import Fuse from 'fuse.js'
import { getNormalPath } from '@/utils/ruoyi'
import { isHttp } from '@/utils/validate'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
const search = ref('');
const options = ref([]);
const searchPool = ref([]);
const show = ref(false);
const fuse = ref(undefined);
const headerSearchSelectRef = ref(null);
const router = useRouter();
const routes = computed(() => usePermissionStore().routes);
const search = ref('')
const options = ref([])
const searchPool = ref([])
const activeIndex = ref(-1)
const show = ref(false)
const fuse = ref(undefined)
const headerSearchSelectRef = ref(null)
const router = useRouter()
const theme = computed(() => useSettingsStore().theme)
const routes = computed(() => usePermissionStore().defaultRoutes)
function click() {
show.value = !show.value
if (show.value) {
headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
options.value = searchPool.value
}
};
}
function close() {
headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
search.value = ''
options.value = []
show.value = false
activeIndex.value = -1
}
function change(val) {
const path = val.path;
const query = val.query;
const path = val.path
const query = val.query
if (isHttp(path)) {
// http(s)://
const pindex = path.indexOf("http");
window.open(path.substr(pindex, path.length), "_blank");
const pindex = path.indexOf("http")
window.open(path.substr(pindex, path.length), "_blank")
} else {
if (query) {
router.push({ path: path, query: JSON.parse(query) });
router.push({ path: path, query: JSON.parse(query) })
} else {
router.push(path)
}
@@ -64,6 +99,7 @@ function change(val) {
show.value = false
})
}
function initFuse(list) {
fuse.value = new Fuse(list, {
shouldSort: true,
@@ -80,6 +116,7 @@ function initFuse(list) {
}]
})
}
// Filter out the routes that can be displayed in the sidebar
// And generate the internationalized title
function generateRoutes(routes, basePath = '', prefixTitle = []) {
@@ -88,16 +125,17 @@ function generateRoutes(routes, basePath = '', prefixTitle = []) {
for (const r of routes) {
// skip hidden router
if (r.hidden) { continue }
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path
const data = {
path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
title: [...prefixTitle]
title: [...prefixTitle],
icon: ''
}
if (r.meta && r.meta.title) {
data.title = [...data.title, r.meta.title]
if (r.redirect !== 'noRedirect') {
data.icon = r.meta.icon
if (r.redirect !== "noRedirect") {
// only push the routes with title
// special case: need to exclude parent router without redirect
res.push(data)
@@ -117,30 +155,42 @@ function generateRoutes(routes, basePath = '', prefixTitle = []) {
}
return res
}
function querySearch(query) {
activeIndex.value = -1
if (query !== '') {
options.value = fuse.value.search(query)
options.value = fuse.value.search(query).map((item) => item.item) ?? searchPool.value
} else {
options.value = []
options.value = searchPool.value
}
}
function activeStyle(index) {
if (index !== activeIndex.value) return {}
return {
"background-color": theme.value,
"color": "#fff"
}
}
function navigateResult(direction) {
if (direction === "up") {
activeIndex.value = activeIndex.value <= 0 ? options.value.length - 1 : activeIndex.value - 1
} else if (direction === "down") {
activeIndex.value = activeIndex.value >= options.value.length - 1 ? 0 : activeIndex.value + 1
}
}
function selectActiveResult() {
if (options.value.length > 0 && activeIndex.value >= 0) {
change(options.value[activeIndex.value])
}
}
onMounted(() => {
searchPool.value = generateRoutes(routes.value);
})
watchEffect(() => {
searchPool.value = generateRoutes(routes.value)
})
watch(show, (value) => {
if (value) {
document.body.addEventListener('click', close)
} else {
document.body.removeEventListener('click', close)
}
})
watch(searchPool, (list) => {
initFuse(list)
})
@@ -148,40 +198,55 @@ watch(searchPool, (list) => {
<style lang='scss' scoped>
.header-search {
font-size: 0 !important;
.search-icon {
cursor: pointer;
font-size: 18px;
vertical-align: middle;
}
}
.header-search-select {
font-size: 18px;
transition: width 0.2s;
width: 0;
overflow: hidden;
background: transparent;
border-radius: 0;
display: inline-block;
vertical-align: middle;
.result-wrap {
height: 280px;
margin: 6px 0;
:deep(.el-input__inner) {
border-radius: 0;
border: 0;
padding-left: 0;
padding-right: 0;
box-shadow: none !important;
border-bottom: 1px solid #d9d9d9;
vertical-align: middle;
.search-item {
display: flex;
height: 48px;
align-items: center;
padding-right: 10px;
.left {
width: 60px;
text-align: center;
.menu-icon {
width: 18px;
height: 18px;
}
}
.search-info {
padding-left: 5px;
margin-top: 10px;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-start;
flex: 1;
.menu-title,
.menu-path {
height: 20px;
}
.menu-path {
color: #ccc;
font-size: 10px;
}
}
}
&.show {
.header-search-select {
width: 210px;
margin-left: 10px;
}
.search-item:hover {
cursor: pointer;
}
}
</style>
@@ -2,10 +2,12 @@
<div class="component-upload-image">
<el-upload
multiple
:disabled="disabled"
:action="uploadImgUrl"
list-type="picture-card"
:on-success="handleUploadSuccess"
:before-upload="handleBeforeUpload"
:data="data"
:limit="limit"
:on-error="handleUploadError"
:on-exceed="handleExceed"
@@ -20,7 +22,7 @@
<el-icon class="avatar-uploader-icon"><plus /></el-icon>
</el-upload>
<!-- 上传提示 -->
<div class="el-upload__tip" v-if="showTip">
<div class="el-upload__tip" v-if="showTip && !disabled">
请上传
<template v-if="fileSize">
大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
@@ -48,29 +50,49 @@
<script setup>
import { getToken } from "@/utils/auth";
import { isExternal } from "@/utils/validate";
import Sortable from 'sortablejs';
const props = defineProps({
modelValue: [String, Object, Array],
//
action: {
type: String,
default: "/common/upload"
},
//
data: {
type: Object
},
//
limit: {
type: Number,
default: 5,
default: 5
},
// (MB)
fileSize: {
type: Number,
default: 5,
default: 5
},
// , ['png', 'jpg', 'jpeg']
fileType: {
type: Array,
default: () => ["png", "jpg", "jpeg"],
default: () => ["png", "jpg", "jpeg"]
},
//
isShowTip: {
type: Boolean,
default: true
},
//
disabled: {
type: Boolean,
default: false
},
//
drag: {
type: Boolean,
default: true
}
});
const { proxy } = getCurrentInstance();
@@ -80,7 +102,7 @@ const uploadList = ref([]);
const dialogImageUrl = ref("");
const dialogVisible = ref(false);
const baseUrl = import.meta.env.VITE_APP_BASE_API;
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); //
const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + props.action); //
const headers = ref({ Authorization: "Bearer " + getToken() });
const fileList = ref([]);
const showTip = computed(
@@ -206,6 +228,22 @@ function listToString(list, separator) {
}
return strs != "" ? strs.substr(0, strs.length - 1) : "";
}
//
onMounted(() => {
if (props.drag && !props.disabled) {
nextTick(() => {
const element = proxy.$refs.imageUpload?.$el?.querySelector('.el-upload-list')
Sortable.create(element, {
onEnd: (evt) => {
const movedItem = fileList.value.splice(evt.oldIndex, 1)[0]
fileList.value.splice(evt.newIndex, 0, movedItem)
emit('update:modelValue', listToString(fileList.value))
}
})
})
}
})
</script>
<style scoped lang="scss">
@@ -213,4 +251,8 @@ function listToString(list, separator) {
:deep(.hide .el-upload--picture-card) {
display: none;
}
:deep(.el-upload.el-upload--picture-card.is-disabled) {
display: none !important;
}
</style>
@@ -97,7 +97,6 @@ function handleCurrentChange(val) {
<style scoped>
.pagination-container {
background: #fff;
padding: 32px 16px;
}
.pagination-container.hidden {
display: none;
@@ -7,15 +7,20 @@
<el-tooltip class="item" effect="dark" content="刷新" placement="top">
<el-button circle icon="Refresh" @click="refresh()" />
</el-tooltip>
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
<el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="Object.keys(columns).length > 0">
<el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
<el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
<el-button circle icon="Menu" />
<template #dropdown>
<el-dropdown-menu>
<template v-for="item in columns" :key="item.key">
<!-- 全选/反选 按钮 -->
<el-dropdown-item>
<el-checkbox :indeterminate="isIndeterminate" v-model="isChecked" @change="toggleCheckAll"> 列展示 </el-checkbox>
</el-dropdown-item>
<div class="check-line"></div>
<template v-for="(item, key) in columns" :key="item.key">
<el-dropdown-item>
<el-checkbox :checked="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" />
<el-checkbox v-model="item.visible" @change="checkboxChange($event, key)" :label="item.label" />
</el-dropdown-item>
</template>
</el-dropdown-menu>
@@ -27,7 +32,7 @@
<el-transfer
:titles="['显示', '隐藏']"
v-model="value"
:data="columns"
:data="transferData"
@change="dataChange"
></el-transfer>
</el-dialog>
@@ -39,83 +44,119 @@ const props = defineProps({
/* 是否显示检索条件 */
showSearch: {
type: Boolean,
default: true,
default: true
},
/* 显隐列信息 */
/* 显隐列信息(数组格式、对象格式) */
columns: {
type: Array,
type: [Array, Object],
default: () => ({})
},
/* 是否显示检索图标 */
search: {
type: Boolean,
default: true,
default: true
},
/* 显隐列类型(transfer穿梭框、checkbox复选框) */
showColumnsType: {
type: String,
default: "checkbox",
default: "checkbox"
},
/* 右外边距 */
gutter: {
type: Number,
default: 10,
default: 10
},
})
const emits = defineEmits(['update:showSearch', 'queryTable']);
const emits = defineEmits(['update:showSearch', 'queryTable'])
//
const value = ref([]);
const value = ref([])
//
const title = ref("显示/隐藏");
const title = ref("显示/隐藏")
//
const open = ref(false);
const open = ref(false)
const style = computed(() => {
const ret = {};
const ret = {}
if (props.gutter) {
ret.marginRight = `${props.gutter / 2}px`;
ret.marginRight = `${props.gutter / 2}px`
}
return ret;
});
return ret
})
// /
const isChecked = computed({
get: () => Array.isArray(props.columns) ? props.columns.every(col => col.visible) : Object.values(props.columns).every((col) => col.visible),
set: () => {}
})
const isIndeterminate = computed(() => Array.isArray(props.columns) ? props.columns.some((col) => col.visible) && !isChecked.value : Object.values(props.columns).some((col) => col.visible) && !isChecked.value)
const transferData = computed(() => Array.isArray(props.columns) ? props.columns.map((item, index) => ({ key: index, label: item.label })) : Object.keys(props.columns).map((key, index) => ({ key: index, label: props.columns[key].label })))
//
function toggleSearch() {
emits("update:showSearch", !props.showSearch);
emits("update:showSearch", !props.showSearch)
}
//
function refresh() {
emits("queryTable");
emits("queryTable")
}
//
function dataChange(data) {
for (let item in props.columns) {
const key = props.columns[item].key;
props.columns[item].visible = !data.includes(key);
if (Array.isArray(props.columns)) {
for (let item in props.columns) {
const key = props.columns[item].key
props.columns[item].visible = !data.includes(key)
}
} else {
Object.keys(props.columns).forEach((key, index) => {
props.columns[key].visible = !data.includes(index)
})
}
}
// dialog
function showColumn() {
open.value = true;
open.value = true
}
if (props.showColumnsType == 'transfer') {
//
for (let item in props.columns) {
if (props.columns[item].visible === false) {
value.value.push(parseInt(item));
if (props.showColumnsType == "transfer") {
// transfer穿
if (Array.isArray(props.columns)) {
for (let item in props.columns) {
if (props.columns[item].visible === false) {
value.value.push(parseInt(item))
}
}
} else {
Object.keys(props.columns).forEach((key, index) => {
if (props.columns[key].visible === false) {
value.value.push(index)
}
})
}
}
//
function checkboxChange(event, label) {
props.columns.filter(item => item.label == label)[0].visible = event;
//
function checkboxChange(event, key) {
if (Array.isArray(props.columns)) {
props.columns.filter(item => item.key == key)[0].visible = event
} else {
props.columns[key].visible = event
}
}
// /
function toggleCheckAll() {
const newValue = !isChecked.value
if (Array.isArray(props.columns)) {
props.columns.forEach((col) => (col.visible = newValue))
} else {
Object.values(props.columns).forEach((col) => (col.visible = newValue))
}
}
</script>
<style lang='scss' scoped>
@@ -131,4 +172,10 @@ function checkboxChange(event, label) {
line-height: 30px;
padding: 0 17px;
}
.check-line {
width: 90%;
height: 1px;
background-color: #ccc;
margin: 3px auto;
}
</style>
@@ -63,7 +63,7 @@ const topMenus = computed(() => {
routers.value.map((menu) => {
if (menu.hidden !== true) {
//
if (menu.path === "/") {
if (menu.path === '/' && menu.children) {
topMenus.push(menu.children[0]);
} else {
topMenus.push(menu);
@@ -2,7 +2,6 @@
* v-copyText 复制文本内容
* Copyright (c) 2022 ruoyi
*/
export default {
beforeMount(el, { value, arg }) {
if (arg === "callback") {
@@ -1,8 +1,7 @@
/**
* v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi
*/
/**
* v-hasPermi 操作权限处理
* Copyright (c) 2019 ruoyi
*/
import useUserStore from '@/store/modules/user'
export default {
@@ -1,8 +1,7 @@
/**
* v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi
*/
/**
* v-hasRole 角色权限处理
* Copyright (c) 2019 ruoyi
*/
import useUserStore from '@/store/modules/user'
export default {
@@ -8,10 +8,12 @@
</transition>
</router-view>
<iframe-toggle />
<copyright />
</section>
</template>
<script setup>
import copyright from "./Copyright/index"
import iframeToggle from "./IframeToggle/index"
import useTagsViewStore from '@/store/modules/tagsView'
@@ -22,7 +24,7 @@ onMounted(() => {
addIframe()
})
watch((route) => {
watchEffect(() => {
addIframe()
})
@@ -42,6 +44,10 @@ function addIframe() {
overflow: hidden;
}
.app-main:has(.copyright) {
padding-bottom: 36px;
}
.fixed-header + .app-main {
padding-top: 50px;
}
@@ -0,0 +1,31 @@
<template>
<footer v-if="visible" class="copyright">
<span>{{ content }}</span>
</footer>
</template>
<script setup>
import useSettingsStore from '@/store/modules/settings'
const settingsStore = useSettingsStore()
const visible = computed(() => settingsStore.footerVisible)
const content = computed(() => settingsStore.footerContent)
</script>
<style scoped>
.copyright {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 36px;
padding: 10px 20px;
text-align: right;
background-color: #f8f8f8;
color: #666;
font-size: 14px;
border-top: 1px solid #e7e7e7;
z-index: 999;
}
</style>
@@ -1,9 +1,10 @@
<template>
<div :style="'height:' + height">
<div :style="'height:' + height" v-loading="loading" element-loading-text="正在加载页面,请稍候!">
<iframe
:id="iframeId"
style="width: 100%; height: 100%"
:src="src"
ref="iframeRef"
frameborder="no"
></iframe>
</div>
@@ -20,5 +21,15 @@ const props = defineProps({
}
});
const height = ref(document.documentElement.clientHeight - 94.5 + "px");
const loading = ref(true)
const height = ref(document.documentElement.clientHeight - 94.5 + 'px')
const iframeRef = ref(null)
onMounted(() => {
if (iframeRef.value) {
iframeRef.value.onload = () => {
loading.value = false
}
}
})
</script>
@@ -1,8 +1,8 @@
<template>
<div class="navbar">
<hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
<breadcrumb id="breadcrumb-container" class="breadcrumb-container" v-if="!settingsStore.topNav" />
<top-nav id="topmenu-container" class="topmenu-container" v-if="settingsStore.topNav" />
<breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
<top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
<div class="right-menu">
<template v-if="appStore.device !== 'mobile'">
@@ -29,27 +29,26 @@
<size-select id="size-select" class="right-menu-item hover-effect" />
</el-tooltip>
</template>
<div class="avatar-container">
<el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
<div class="avatar-wrapper">
<img :src="userStore.avatar" class="user-avatar" />
<el-icon><caret-bottom /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<el-dropdown @command="handleCommand" class="avatar-container right-menu-item hover-effect" trigger="hover">
<div class="avatar-wrapper">
<img :src="userStore.avatar" class="user-avatar" />
<span class="user-nickname"> {{ userStore.nickName }} </span>
</div>
<template #dropdown>
<el-dropdown-menu>
<router-link to="/user/profile">
<el-dropdown-item>个人中心</el-dropdown-item>
</router-link>
<el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
<span>布局设置</span>
</el-dropdown-item>
<el-dropdown-item divided command="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<el-dropdown-item divided command="logout">
<span>退出登录</span>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
@@ -79,13 +78,13 @@ function toggleSideBar() {
function handleCommand(command) {
switch (command) {
case "setLayout":
setLayout();
break;
setLayout()
break
case "logout":
logout();
break;
logout()
break
default:
break;
break
}
}
@@ -96,14 +95,14 @@ function logout() {
type: 'warning'
}).then(() => {
userStore.logOut().then(() => {
location.href = '/index';
location.href = '/index'
})
}).catch(() => { });
}).catch(() => { })
}
const emits = defineEmits(['setLayout'])
function setLayout() {
emits('setLayout');
emits('setLayout')
}
function toggleTheme() {
@@ -161,7 +160,7 @@ function toggleTheme() {
padding: 0 8px;
height: 100%;
font-size: 18px;
color: var(--navbar-text);
color: #5a5e66;
vertical-align: text-bottom;
&.hover-effect {
@@ -188,17 +187,30 @@ function toggleTheme() {
}
.avatar-container {
margin-right: 40px;
margin-right: 0px;
padding-right: 0px;
.avatar-wrapper {
margin-top: 5px;
margin-top: 10px;
right: 8px;
position: relative;
.user-avatar {
cursor: pointer;
width: 40px;
height: 40px;
border-radius: 10px;
width: 30px;
height: 30px;
margin-right: 8px;
border-radius: 50%;
vertical-align: middle;
border-style: none;
}
.user-nickname{
position: relative;
left: 0px;
// bottom: 10px;
font-size: 14px;
font-weight: bold;
}
i {
@@ -1,5 +1,5 @@
<template>
<el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
<el-drawer v-model="showSettings" :withHeader="false" :lock-scroll="false" direction="rtl" size="300px">
<div class="setting-drawer-title">
<h3 class="drawer-title">主题风格设置</h3>
</div>
@@ -49,6 +49,13 @@
</span>
</div>
<div class="drawer-item">
<span>显示页签图标</span>
<span class="comp-style">
<el-switch v-model="settingsStore.tagsIcon" :disabled="!settingsStore.tagsView" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>固定 Header</span>
<span class="comp-style">
@@ -66,7 +73,14 @@
<div class="drawer-item">
<span>动态标题</span>
<span class="comp-style">
<el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" />
<el-switch v-model="settingsStore.dynamicTitle" @change="dynamicTitleChange" class="drawer-switch" />
</span>
</div>
<div class="drawer-item">
<span>底部版权</span>
<span class="comp-style">
<el-switch v-model="settingsStore.footerVisible" class="drawer-switch" />
</span>
</div>
@@ -79,10 +93,6 @@
</template>
<script setup>
import variables from '@/assets/styles/variables.module.scss'
import axios from 'axios'
import { ElLoading, ElMessage } from 'element-plus'
import { useDynamicTitle } from '@/utils/dynamicTitle'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
import usePermissionStore from '@/store/modules/permission'
@@ -106,6 +116,11 @@ function topNavChange(val) {
}
}
/** 是否需要dynamicTitle */
function dynamicTitleChange() {
useSettingsStore().setTitle(useSettingsStore().title)
}
function themeChange(val) {
settingsStore.theme = val;
handleThemeStyle(val);
@@ -119,9 +134,11 @@ function saveSetting() {
let layoutSetting = {
"topNav": storeSettings.value.topNav,
"tagsView": storeSettings.value.tagsView,
"tagsIcon": storeSettings.value.tagsIcon,
"fixedHeader": storeSettings.value.fixedHeader,
"sidebarLogo": storeSettings.value.sidebarLogo,
"dynamicTitle": storeSettings.value.dynamicTitle,
"footerVisible": storeSettings.value.footerVisible,
"sideTheme": storeSettings.value.sideTheme,
"theme": storeSettings.value.theme
};
@@ -47,8 +47,6 @@ const getLogoTextColor = computed(() => {
</script>
<style lang="scss" scoped>
@import '@/assets/styles/variables.module.scss';
.sidebarLogoFade-enter-active {
transition: opacity 1.5s;
}
@@ -5,13 +5,14 @@
v-for="tag in visitedViews"
:key="tag.path"
:data-path="tag.path"
:class="isActive(tag) ? 'active' : ''"
:class="{ 'active': isActive(tag), 'has-icon': tagsIcon }"
:to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
class="tags-view-item"
:style="activeStyle(tag)"
@click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
@contextmenu.prevent="openMenu(tag, $event)"
>
<svg-icon v-if="tagsIcon && tag.meta && tag.meta.icon && tag.meta.icon !== '#'" :icon-class="tag.meta.icon" />
{{ tag.title }}
<span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
<close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
@@ -62,6 +63,7 @@ const router = useRouter();
const visitedViews = computed(() => useTagsViewStore().visitedViews);
const routes = computed(() => usePermissionStore().routes);
const theme = computed(() => useSettingsStore().theme);
const tagsIcon = computed(() => useSettingsStore().tagsIcon)
watch(route, () => {
addTags()
@@ -285,6 +287,11 @@ function handleScroll() {
}
}
}
.tags-view-item.active.has-icon::before {
content: none !important;
}
.contextmenu {
margin: 0;
background: var(--el-bg-color-overlay, #fff);
+4 -6
View File
@@ -17,8 +17,6 @@
import { useWindowSize } from '@vueuse/core'
import Sidebar from './components/Sidebar/index.vue'
import { AppMain, Navbar, Settings, TagsView } from './components'
import defaultSettings from '@/settings'
import useAppStore from '@/store/modules/app'
import useSettingsStore from '@/store/modules/settings'
@@ -66,11 +64,11 @@ function setLayout() {
</script>
<style lang="scss" scoped>
@import "@/assets/styles/mixin.scss";
@import "@/assets/styles/variables.module.scss";
@use "@/assets/styles/mixin.scss" as mix;
@use "@/assets/styles/variables.module.scss" as vars;
.app-wrapper {
@include clearfix;
@include mix.clearfix;
position: relative;
height: 100%;
width: 100%;
@@ -96,7 +94,7 @@ function setLayout() {
top: 0;
right: 0;
z-index: 9;
width: calc(100% - #{$base-sidebar-width});
width: calc(100% - #{vars.$base-sidebar-width});
transition: width 0.28s;
}
+2
View File
@@ -26,6 +26,7 @@ import elementIcons from '@/components/SvgIcon/svgicon'
import './permission' // permission control
import { useDict } from '@/utils/dict'
import { getConfigKey } from "@/api/system/config"
import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
// 分页组件
@@ -52,6 +53,7 @@ app.config.globalProperties.parseTime = parseTime
app.config.globalProperties.resetForm = resetForm
app.config.globalProperties.handleTree = handleTree
app.config.globalProperties.addDateRange = addDateRange
app.config.globalProperties.getConfigKey = getConfigKey
app.config.globalProperties.selectDictLabel = selectDictLabel
app.config.globalProperties.selectDictLabels = selectDictLabels
+4 -2
View File
@@ -59,8 +59,10 @@ export default {
return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
},
// 打开tab页签
openPage(url) {
return router.push(url);
openPage(title, url, params) {
const obj = { path: url, meta: { title: title } }
useTagsViewStore().addView(obj)
return router.push({ path: url, query: params })
},
// 修改tab页签
updatePage(obj) {
+1 -1
View File
@@ -77,7 +77,7 @@ export const constantRoutes = [
redirect: 'noredirect',
children: [
{
path: 'profile',
path: 'profile/:activeTab?',
component: () => import('@/views/system/user/profile/index'),
name: 'Profile',
meta: { title: '个人中心', icon: 'user' }
+14 -5
View File
@@ -3,10 +3,12 @@ export default {
* 网页标题
*/
title: import.meta.env.VITE_APP_TITLE,
/**
* 侧边栏主题 深色主题theme-dark浅色主题theme-light
*/
sideTheme: 'theme-dark',
/**
* 是否系统布局配置
*/
@@ -22,6 +24,11 @@ export default {
*/
tagsView: true,
/**
* 显示页签图标
*/
tagsIcon: false,
/**
* 是否固定头部
*/
@@ -38,10 +45,12 @@ export default {
dynamicTitle: false,
/**
* @type {string | array} 'production' | ['production', 'development']
* @description Need show err logs component.
* The default is only used in the production env
* If you want to also use it in dev, you can pass ['production', 'development']
* 是否显示底部版权
*/
errorLog: 'production'
footerVisible: false,
/**
* 底部版权文本内容
*/
footerContent: 'Copyright © 2024-2025 insistence.tech All Rights Reserved.'
}
@@ -85,28 +85,13 @@ function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
function filterChildren(childrenMap, lastRouter = false) {
var children = []
childrenMap.forEach((el, index) => {
if (el.children && el.children.length) {
if (el.component === 'ParentView' && !lastRouter) {
el.children.forEach(c => {
c.path = el.path + '/' + c.path
if (c.children && c.children.length) {
children = children.concat(filterChildren(c.children, c))
return
}
children.push(c)
})
return
}
childrenMap.forEach(el => {
el.path = lastRouter ? lastRouter.path + '/' + el.path : el.path
if (el.children && el.children.length && el.component === 'ParentView') {
children = children.concat(filterChildren(el.children, el))
} else {
children.push(el)
}
if (lastRouter) {
el.path = lastRouter.path + '/' + el.path
if (el.children && el.children.length) {
children = children.concat(filterChildren(el.children, el))
return
}
}
children = children.concat(el)
})
return children
}
@@ -136,7 +121,7 @@ export const loadView = (view) => {
res = () => modules[path]();
}
}
return res;
return res
}
export default usePermissionStore
@@ -5,7 +5,7 @@ import { useDynamicTitle } from '@/utils/dynamicTitle'
const isDark = useDark()
const toggleDark = useToggle(isDark)
const { sideTheme, showSettings, topNav, tagsView, fixedHeader, sidebarLogo, dynamicTitle } = defaultSettings
const { sideTheme, showSettings, topNav, tagsView, tagsIcon, fixedHeader, sidebarLogo, dynamicTitle, footerVisible, footerContent } = defaultSettings
const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
@@ -19,9 +19,12 @@ const useSettingsStore = defineStore(
showSettings: showSettings,
topNav: storageSetting.topNav === undefined ? topNav : storageSetting.topNav,
tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
tagsIcon: storageSetting.tagsIcon === undefined ? tagsIcon : storageSetting.tagsIcon,
fixedHeader: storageSetting.fixedHeader === undefined ? fixedHeader : storageSetting.fixedHeader,
sidebarLogo: storageSetting.sidebarLogo === undefined ? sidebarLogo : storageSetting.sidebarLogo,
dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
footerVisible: storageSetting.footerVisible === undefined ? footerVisible : storageSetting.footerVisible,
footerContent: footerContent,
isDark: isDark.value
}),
actions: {
@@ -1,3 +1,5 @@
import router from '@/router'
import { ElMessageBox } from 'element-plus'
import { login, logout, getInfo } from '@/api/login'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { isHttp, isEmpty } from "@/utils/validate"
@@ -10,6 +12,7 @@ const useUserStore = defineStore(
token: getToken(),
id: '',
name: '',
nickName: '',
avatar: '',
roles: [],
permissions: []
@@ -48,7 +51,20 @@ const useUserStore = defineStore(
}
this.id = user.userId
this.name = user.userName
this.nickName = user.nickName
this.avatar = avatar
/* 初始密码提示 */
if(res.isDefaultModifyPwd) {
ElMessageBox.confirm('您的密码还是初始密码,请修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
}
/* 过期密码提示 */
if(!res.isDefaultModifyPwd && res.isPasswordExpired) {
ElMessageBox.confirm('您的密码已过期,请尽快修改密码!', '安全提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }).then(() => {
router.push({ name: 'Profile', params: { activeTab: 'resetPwd' } })
}).catch(() => {})
}
resolve(res)
}).catch(error => {
reject(error)
@@ -1,4 +1,3 @@
import store from '@/store'
import defaultSettings from '@/settings'
import useSettingsStore from '@/store/modules/settings'
@@ -318,7 +318,7 @@ function buildElRadioGroupChild(conf) {
if (conf.options && conf.options.length) {
const tag = conf.optionType === 'button' ? 'el-radio-button' : 'el-radio'
const border = conf.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :value="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
}
return children.join('\n')
}
@@ -328,7 +328,7 @@ function buildElCheckboxGroupChild(conf) {
if (conf.options && conf.options.length) {
const tag = conf.optionType === 'button' ? 'el-checkbox-button' : 'el-checkbox'
const border = conf.border ? 'border' : ''
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :disabled="item.disabled" ${border}>{{item.label}}</${tag}>`)
children.push(`<${tag} v-for="(item, index) in ${conf.vModel}Options" :key="index" :label="item.value" :value="item.label" :disabled="item.disabled" ${border} />`)
}
return children.join('\n')
}
+5 -3
View File
@@ -1,7 +1,7 @@
<template>
<div class="login">
<el-form ref="loginRef" :model="loginForm" :rules="loginRules" class="login-form">
<h3 class="title">vfadmin后台管理系统</h3>
<h3 class="title">{{ title }}</h3>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
@@ -59,7 +59,7 @@
</el-form>
<!-- 底部 -->
<div class="el-login-footer">
<span>Copyright © 2024 insistence.tech All Rights Reserved.</span>
<span>Copyright © 2024-2025 insistence.tech All Rights Reserved.</span>
</div>
</div>
</template>
@@ -70,7 +70,8 @@ import Cookies from "js-cookie";
import { encrypt, decrypt } from "@/utils/jsencrypt";
import useUserStore from '@/store/modules/user'
const userStore = useUserStore()
const title = import.meta.env.VITE_APP_TITLE;
const userStore = useUserStore();
const route = useRoute();
const router = useRouter();
const { proxy } = getCurrentInstance();
@@ -183,6 +184,7 @@ getCookie();
background: #ffffff;
width: 400px;
padding: 25px 25px 5px 25px;
z-index: 1;
.el-input {
height: 40px;
input {
+1 -1
View File
@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-row>
<el-row :gutter="10">
<el-col :span="24" class="card-box">
<el-card>
<template #header><Monitor style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">基本信息</span></template>
@@ -327,8 +327,9 @@
</template>
<script setup name="Job">
import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from "@/api/monitor/job";
import Crontab from '@/components/Crontab'
import { listJob, getJob, delJob, addJob, updateJob, runJob, changeJobStatus } from "@/api/monitor/job"
const router = useRouter();
const { proxy } = getCurrentInstance();
const { sys_job_group, sys_job_status, sys_job_executor } = proxy.useDict("sys_job_group", "sys_job_status", "sys_job_executor");
@@ -201,7 +201,7 @@
import { list, delOperlog, cleanOperlog } from "@/api/monitor/operlog";
const { proxy } = getCurrentInstance();
const { sys_oper_type, sys_common_status } = proxy.useDict("sys_oper_type","sys_common_status");
const { sys_oper_type, sys_common_status } = proxy.useDict("sys_oper_type", "sys_common_status");
const operlogList = ref([]);
const open = ref(false);
@@ -1,6 +1,6 @@
<template>
<div class="app-container">
<el-row>
<el-row :gutter="10">
<el-col :span="12" class="card-box">
<el-card>
<template #header><Cpu style="width: 1em; height: 1em; vertical-align: middle;" /> <span style="vertical-align: middle;">CPU</span></template>
@@ -1,7 +1,7 @@
<template>
<div class="register">
<el-form ref="registerRef" :model="registerForm" :rules="registerRules" class="register-form">
<h3 class="title">vfadmin后台管理系统</h3>
<h3 class="title">{{ title }}</h3>
<el-form-item prop="username">
<el-input
v-model="registerForm.username"
@@ -79,6 +79,7 @@
import { ElMessageBox } from "element-plus";
import { getCodeImg, register } from "@/api/login";
const title = import.meta.env.VITE_APP_TITLE;
const router = useRouter();
const { proxy } = getCurrentInstance();
@@ -23,7 +23,7 @@
<span>{{ (pageNum - 1) * pageSize + scope.$index + 1 }}</span>
</template>
</el-table-column>
<el-table-column type="selection" :reserve-selection="true" width="55"></el-table-column>
<el-table-column type="selection" :reserve-selection="true" :selectable="checkSelectable" width="55"></el-table-column>
<el-table-column label="角色编号" align="center" prop="roleId" />
<el-table-column label="角色名称" align="center" prop="roleName" />
<el-table-column label="权限字符" align="center" prop="roleKey" />
@@ -65,21 +65,32 @@ const form = ref({
/** 单击选中行数据 */
function clickRow(row) {
proxy.$refs["roleRef"].toggleRowSelection(row);
if (checkSelectable(row)) {
proxy.$refs["roleRef"].toggleRowSelection(row);
}
};
/** 多选框选中数据 */
function handleSelectionChange(selection) {
roleIds.value = selection.map(item => item.roleId);
};
/** 保存选中的数据编号 */
function getRowKey(row) {
return row.roleId;
};
//
function checkSelectable(row) {
return row.status === "0" ? true : false;
};
/** 关闭按钮 */
function close() {
const obj = { path: "/system/user" };
proxy.$tab.closeOpenPage(obj);
};
/** 提交按钮 */
function submitForm() {
const userId = form.value.userId;

Some files were not shown because too many files have changed in this diff Show More