上线系统配置功能

This commit is contained in:
zhangtao
2025-01-09 10:36:43 +08:00
parent 5aea8479d0
commit 4e15f2ad3d
39 changed files with 529 additions and 502 deletions
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from fastapi import APIRouter, Depends, Query, Path from typing import List
from fastapi import APIRouter, Depends, Request, UploadFile
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from app.common.response import SuccessResponse from app.common.response import SuccessResponse
@@ -10,7 +11,7 @@ from app.core.dependencies import AuthPermission
from app.core.router_class import OperationLogRoute from app.core.router_class import OperationLogRoute
from app.core.logger import logger from app.core.logger import logger
from app.api.v1.schemas.system.auth_schema import AuthSchema from app.api.v1.schemas.system.auth_schema import AuthSchema
from app.api.v1.schemas.system.config_schema import ConfigCreateSchema, ConfigUpdateSchema from app.api.v1.schemas.system.config_schema import ConfigUpdateSchema
from app.api.v1.params.system.config_param import ConfigQueryParams from app.api.v1.params.system.config_param import ConfigQueryParams
from app.api.v1.services.system.config_service import ConfigService from app.api.v1.services.system.config_service import ConfigService
@@ -29,42 +30,29 @@ async def get_obj_list(
logger.info(f"{auth.user.name} 查询配置列表成功") logger.info(f"{auth.user.name} 查询配置列表成功")
return SuccessResponse(data=result_dict, msg="查询配置列表成功") return SuccessResponse(data=result_dict, msg="查询配置列表成功")
@router.put("/batch", summary="批量修改配置", description="批量修改配置")
@router.post("/create", summary="创建配置", description="创建配置") async def batch_objs(
async def create_obj( request: Request,
data: ConfigCreateSchema, data: List[ConfigUpdateSchema],
auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:create"])),
) -> JSONResponse:
result_dict = await ConfigService.create_services(auth=auth, data=data)
logger.info(f"{auth.user.name} 创建配置成功: {result_dict}")
return SuccessResponse(data=result_dict, msg="创建配置成功")
@router.put("/update", summary="修改配置", description="修改配置")
async def update_obj(
data: ConfigUpdateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:update"])), auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:update"])),
) -> JSONResponse: ) -> JSONResponse:
result_dict = await ConfigService.update_services(auth=auth, id=data.id, data=data) result_list_dict = await ConfigService.batch_services(auth=auth, request=request, data=data)
logger.info(f"{auth.user.name} 修改配置成功: {result_dict}") logger.info(f"{auth.user.name} 批量更新配置成功 {result_list_dict}")
return SuccessResponse(data=result_dict, msg="修改配置成功") return SuccessResponse(data=result_list_dict, msg="批量更新配置成功")
@router.post("/upload", summary="上传文件", dependencies=[Depends(AuthPermission(permissions=["system:config:upload"]))])
@router.delete("/delete", summary="删除配置", description="删除配置") async def upload_file(
async def delete_obj( file: UploadFile,
id: int = Query(..., description="配置ID"), request: Request
auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:delete"])),
) -> JSONResponse: ) -> JSONResponse:
await ConfigService.delete_services(auth=auth, id=id) result_str = await ConfigService.upload_services(request=request, file=file)
logger.info(f"{auth.user.name} 删除配置成功: {id}") logger.info(f"上传文件: {result_str}")
return SuccessResponse(msg="删除配置成功") return SuccessResponse(data=result_str, msg='上传文件成功')
@router.get("/init", summary="获取初始化配置", description="获取初始化配置")
@router.get("/detail", summary="获取配置详情", description="获取配置详情") async def get_init_config(
async def get_obj_detail( request: Request,
id: int = Query(..., description="配置ID"),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:query"])),
) -> JSONResponse: ) -> JSONResponse:
result_dict = await ConfigService.detail_services(id=id, auth=auth) result_dict = await ConfigService.get_init_config(request=request)
logger.info(f"{auth.user.name} 获取配置详情成功 {id}") logger.info(f"获取初始化配置成功 {result_dict}")
return SuccessResponse(data=result_dict, msg="获取配置详情成功") return SuccessResponse(data=result_dict, msg="获取初始化配置成功")
@@ -16,23 +16,10 @@ class ConfigCRUD(CRUDBase[ConfigModel, ConfigCreateSchema, ConfigUpdateSchema]):
self.auth = auth self.auth = auth
super().__init__(model=ConfigModel, auth=auth) super().__init__(model=ConfigModel, auth=auth)
async def get_curd(self, id: int) -> Optional[ConfigModel]:
"""获取配置详情"""
return await self.get(id=id)
async def list_curd(self, search: Dict = None, order_by: List[Dict[str, str]] = None) -> Sequence[ConfigModel]: async def list_curd(self, search: Dict = None, order_by: List[Dict[str, str]] = None) -> Sequence[ConfigModel]:
"""获取配置列表""" """获取配置列表"""
return await self.list(search=search, order_by=order_by) return await self.list(search=search, order_by=order_by)
async def create_curd(self, data: ConfigCreateSchema) -> Optional[ConfigModel]:
"""创建配置"""
return await self.create(data=data)
async def update_curd(self, id: int, data: ConfigUpdateSchema) -> Optional[ConfigModel]: async def update_curd(self, id: int, data: ConfigUpdateSchema) -> Optional[ConfigModel]:
"""更新配置""" """更新配置"""
return await self.update(id=id, data=data) return await self.update(id=id, data=data)
async def delete_curd(self, ids: List[int]) -> None:
"""删除配置"""
return await self.delete(ids=ids)
@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator from pydantic import BaseModel, ConfigDict, Field, model_validator
@@ -29,6 +29,4 @@ class ConfigUpdateSchema(ConfigCreateSchema):
class ConfigOutSchema(ConfigCreateSchema): class ConfigOutSchema(ConfigCreateSchema):
"""配置响应模型""" """配置响应模型"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
parent_name: Optional[str] = Field(default=None, max_length=40, description="父配置名称")
id: int = Field(description="主键ID") id: int = Field(description="主键ID")
@@ -19,9 +19,9 @@ class CacheService:
:param request: Request对象 :param request: Request对象
:return: 缓存监控信息 :return: 缓存监控信息
""" """
info = await Cache(request).info() info = await Cache(request.app.state.redis).info()
db_size = await Cache(request).db_size() db_size = await Cache(request.app.state.redis).db_size()
command_stats_dict = await Cache(request).commandstats() command_stats_dict = await Cache(request.app.state.redis).commandstats()
command_stats = [ command_stats = [
dict(name=key.split('_')[1], value=str(value.get('calls'))) for key, value in command_stats_dict.items() dict(name=key.split('_')[1], value=str(value.get('calls'))) for key, value in command_stats_dict.items()
@@ -59,7 +59,7 @@ class CacheService:
:param cache_name: 缓存名称 :param cache_name: 缓存名称
:return: 缓存键名列表信息 :return: 缓存键名列表信息
""" """
cache_keys = await Cache(request).get_keys(f'{cache_name}*') cache_keys = await Cache(request.app.state.redis).get_keys(f'{cache_name}*')
cache_key_list = [key.split(':', 1)[1] for key in cache_keys if key.startswith(f'{cache_name}:')] cache_key_list = [key.split(':', 1)[1] for key in cache_keys if key.startswith(f'{cache_name}:')]
return cache_key_list return cache_key_list
@@ -74,7 +74,7 @@ class CacheService:
:param cache_key: 缓存键名 :param cache_key: 缓存键名
:return: 缓存内容信息 :return: 缓存内容信息
""" """
cache_value = await Cache(request).get(f'{cache_name}:{cache_key}') cache_value = await Cache(request.app.state.redis).get(f'{cache_name}:{cache_key}')
return CacheInfoSchema(cache_key=cache_key, cache_name=cache_name, cache_value=cache_value, remark='').model_dump() return CacheInfoSchema(cache_key=cache_key, cache_name=cache_name, cache_value=cache_value, remark='').model_dump()
@@ -87,9 +87,9 @@ class CacheService:
:param cache_name: 缓存名称 :param cache_name: 缓存名称
:return: 操作缓存响应信息 :return: 操作缓存响应信息
""" """
cache_keys = await Cache(request).get_keys(f'{cache_name}*') cache_keys = await Cache(request.app.state.redis).get_keys(f'{cache_name}*')
if cache_keys: if cache_keys:
await Cache(request).delete(*cache_keys) await Cache(request.app.state.redis).delete(*cache_keys)
return True return True
@@ -102,9 +102,9 @@ class CacheService:
:param cache_key: 缓存键名 :param cache_key: 缓存键名
:return: 操作缓存响应信息 :return: 操作缓存响应信息
""" """
cache_keys = await Cache(request).get_keys(f'*{cache_key}') cache_keys = await Cache(request.app.state.redis).get_keys(f'*{cache_key}')
if cache_keys: if cache_keys:
await Cache(request).delete(*cache_keys) await Cache(request.app.state.redis).delete(*cache_keys)
return True return True
@@ -116,8 +116,8 @@ class CacheService:
:param request: Request对象 :param request: Request对象
:return: 操作缓存响应信息 :return: 操作缓存响应信息
""" """
cache_keys = await Cache(request).get_keys cache_keys = await Cache(request.app.state.redis).get_keys
if cache_keys: if cache_keys:
await Cache(request).delete(*cache_keys) await Cache(request.app.state.redis).delete(*cache_keys)
return True return True
@@ -17,12 +17,12 @@ class OnlineService:
async def get_online_list(cls, request: Request, search: OnlineQueryParams) -> List[Dict]: async def get_online_list(cls, request: Request, search: OnlineQueryParams) -> List[Dict]:
"""获取在线用户列表信息""" """获取在线用户列表信息"""
# 获取所有在线用户信息 # 获取所有在线用户信息
token_keys = await Cache(request).get_keys(f'{RedisInitKeyConfig.ONLINE_USER.key}*') token_keys = await Cache(request.app.state.redis).get_keys(f'{RedisInitKeyConfig.ONLINE_USER.key}*')
if not token_keys: if not token_keys:
return [] return []
# 批量获取在线用户信息 # 批量获取在线用户信息
online_values = await Cache(request).mget(*token_keys) online_values = await Cache(request.app.state.redis).mget(*token_keys)
online_list = [] online_list = []
for online_value in online_values: for online_value in online_values:
@@ -53,8 +53,8 @@ class OnlineService:
# 批量删除token # 批量删除token
token_ids = ids.split(',') token_ids = ids.split(',')
for token_id in token_ids: for token_id in token_ids:
await Cache(request).delete(f"{RedisInitKeyConfig.ONLINE_USER.key}:{token_id}") await Cache(request.app.state.redis).delete(f"{RedisInitKeyConfig.ONLINE_USER.key}:{token_id}")
await Cache(request).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{token_id}") await Cache(request.app.state.redis).delete(f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{token_id}")
return True return True
@staticmethod @staticmethod
@@ -85,7 +85,7 @@ class LoginService:
user_agent = parse(request.headers.get("user-agent")) user_agent = parse(request.headers.get("user-agent"))
# 缓存中构建在线用户信息 # 缓存中构建在线用户信息
await Cache(request).set( await Cache(request.app.state.redis).set(
key=f"{RedisInitKeyConfig.ONLINE_USER.key}:{user.username}", key=f"{RedisInitKeyConfig.ONLINE_USER.key}:{user.username}",
value=OnlineOutSchema( value=OnlineOutSchema(
session_id=token.access_token, session_id=token.access_token,
@@ -130,17 +130,17 @@ class LoginService:
)) ))
# 清除该用户之前的token # 清除该用户之前的token
await Cache(request).delete(key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{username}') await Cache(request.app.state.redis).delete(key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{username}')
await Cache(request).delete(key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{username}') await Cache(request.app.state.redis).delete(key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{username}')
# 设置新的token # 设置新的token
await Cache(request).set( await Cache(request.app.state.redis).set(
key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{username}', key=f'{RedisInitKeyConfig.ACCESS_TOKEN.key}:{username}',
value=access_token, value=access_token,
expire=int(access_expires.total_seconds()) expire=int(access_expires.total_seconds())
) )
await Cache(request).set( await Cache(request.app.state.redis).set(
key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{username}', key=f'{RedisInitKeyConfig.REFRESH_TOKEN.key}:{username}',
value=refresh_token, value=refresh_token,
expire=int(refresh_expires.total_seconds()) expire=int(refresh_expires.total_seconds())
@@ -189,7 +189,7 @@ class LoginService:
username: str = payload.sub username: str = payload.sub
# 删除Redis中的token # 删除Redis中的token
await Cache(request).clear() # await Cache(request.app.state.redis).clear()
logger.info(f"用户退出登录成功,会话编号:{username}") logger.info(f"用户退出登录成功,会话编号:{username}")
return True return True
@@ -221,7 +221,7 @@ class CaptchaService:
# 保存到Redis并设置过期时间 # 保存到Redis并设置过期时间
redis_key = f"{RedisInitKeyConfig.CAPTCHA_CODES.key}:{captcha_key}" redis_key = f"{RedisInitKeyConfig.CAPTCHA_CODES.key}:{captcha_key}"
await Cache(request).set( await Cache(request.app.state.redis).set(
key=redis_key, key=redis_key,
value=captcha_value, value=captcha_value,
expire=settings.CAPTCHA_EXPIRE_SECONDS expire=settings.CAPTCHA_EXPIRE_SECONDS
@@ -257,7 +257,7 @@ class CaptchaService:
# 获取Redis中存储的验证码 # 获取Redis中存储的验证码
redis_key = f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{key}' redis_key = f'{RedisInitKeyConfig.CAPTCHA_CODES.key}:{key}'
captcha_value = await Cache(request).get(redis_key) captcha_value = await Cache(request.app.state.redis).get(redis_key)
if not captcha_value: if not captcha_value:
logger.warning('验证码已过期或不存在') logger.warning('验证码已过期或不存在')
raise CustomException(msg="验证码已过期") raise CustomException(msg="验证码已过期")
@@ -268,6 +268,6 @@ class CaptchaService:
raise CustomException(msg="验证码错误") raise CustomException(msg="验证码错误")
# 验证成功后删除验证码,避免重复使用 # 验证成功后删除验证码,避免重复使用
await Cache(request).delete(key=redis_key) await Cache(request.app.state.redis).delete(key=redis_key)
logger.info(f'验证码校验成功,key:{key}') logger.info(f'验证码校验成功,key:{key}')
return True return True
@@ -1,11 +1,24 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json
from typing import List, Dict from typing import List, Dict
from fastapi import Depends, Request, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession
from aioredis import Redis
from app.api.v1.schemas.system.auth_schema import AuthSchema from app.api.v1.schemas.system.auth_schema import AuthSchema
from app.api.v1.schemas.system.config_schema import ConfigCreateSchema, ConfigUpdateSchema, ConfigOutSchema from app.api.v1.schemas.system.config_schema import ConfigOutSchema, ConfigUpdateSchema
from app.api.v1.params.system.config_param import ConfigQueryParams from app.api.v1.params.system.config_param import ConfigQueryParams
from app.api.v1.cruds.system.config_crud import ConfigCRUD from app.api.v1.cruds.system.config_crud import ConfigCRUD
from app.common.enums import RedisInitKeyConfig
from app.core.cache_crud import Cache
from app.core.dependencies import db_getter
from app.utils.upload_util import UploadUtil
from app.core.base_schema import UploadResponseSchema
from app.core.exceptions import CustomException
from app.config.setting import settings
from app.core.logger import logger
class ConfigService: class ConfigService:
@@ -13,27 +26,61 @@ class ConfigService:
配置管理模块服务层 配置管理模块服务层
""" """
@classmethod
async def detail_services(cls, auth: AuthSchema, id: int) -> Dict:
config_obj = await ConfigCRUD(auth).get_curd(id=id)
return ConfigOutSchema.model_validate(config_obj).model_dump()
@classmethod @classmethod
async def list_services(cls, auth: AuthSchema, search: ConfigQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]: async def list_services(cls, auth: AuthSchema, search: ConfigQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]:
config_obj_list = await ConfigCRUD(auth).list_curd(search=search.__dict__, order_by=order_by) config_obj_list = await ConfigCRUD(auth).list_curd(search=search.__dict__, order_by=order_by)
return [ConfigOutSchema.model_validate(config_obj).model_dump() for config_obj in config_obj_list] return [ConfigOutSchema.model_validate(config_obj).model_dump() for config_obj in config_obj_list]
@classmethod @classmethod
async def create_services(cls, auth: AuthSchema, data: ConfigCreateSchema) -> Dict: async def batch_services(cls, auth: AuthSchema, request: Request, data: List[ConfigUpdateSchema]) -> List[Dict]:
config_obj = await ConfigCRUD(auth).create_curd(data=data) result = []
return ConfigOutSchema.model_validate(config_obj).model_dump() for obj in data:
new_obj = await ConfigCRUD(auth).update_curd(id=obj.id, data=obj)
new_obj_dict = ConfigOutSchema.model_validate(new_obj).model_dump()
result.append(new_obj_dict)
cls.init_config(redis=request.app.state.redis, db=auth.db)
return result
@classmethod @classmethod
async def update_services(cls, auth: AuthSchema, id: int, data: ConfigUpdateSchema) -> Dict: async def upload_services(cls, request: Request, file: UploadFile) -> Dict:
config_obj = await ConfigCRUD(auth).update_curd(id=id, data=data) """上传文件"""
return ConfigOutSchema.model_validate(config_obj).model_dump() if not file:
raise CustomException(msg="请选择要上传的文件")
filename, filepath = await UploadUtil.upload_file(file=file)
return UploadResponseSchema(
file_path=f'{filepath}',
file_name=filename,
origin_name=file.filename,
file_url=f'{request.base_url}{filepath}',
).model_dump()
@classmethod @classmethod
async def delete_services(cls, auth: AuthSchema, id: int) -> None: async def init_config(cls, redis: Redis, db: AsyncSession):
await ConfigCRUD(auth).delete_curd(ids=[id]) auth = AuthSchema(db=db)
config_obj_list = await ConfigCRUD(auth).list_curd()
config_obj_list_dict = [ConfigOutSchema.model_validate(config_obj).model_dump() for config_obj in config_obj_list]
# 保存到Redis并设置过期时间
redis_key = f"{RedisInitKeyConfig.System_Config.key}:{'init_system_config'}"
try:
value = json.dumps(config_obj_list_dict, ensure_ascii=False)
await Cache(redis).set(
key=redis_key,
value=value,
expire=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
)
logger.info(f"初始化系统配置成功: {config_obj_list_dict}")
except Exception as e:
logger.error(f"初始化系统配置失败: {e}")
raise CustomException(msg="初始化系统配置失败")
@classmethod
async def get_init_config(cls, request: Request) -> Dict:
"""获取系统配置"""
redis_key = f"{RedisInitKeyConfig.System_Config.key}:{'init_system_config'}"
config_obj_list_dict = await Cache(request.app.state.redis).get(redis_key)
if not config_obj_list_dict:
raise CustomException(msg="系统配置不存在")
return config_obj_list_dict
+1
View File
@@ -30,6 +30,7 @@ class RedisInitKeyConfig(Enum):
REFRESH_TOKEN = {'key': 'refresh_token', 'remark': '刷新令牌信息'} REFRESH_TOKEN = {'key': 'refresh_token', 'remark': '刷新令牌信息'}
CAPTCHA_CODES = {'key': 'captcha_codes', 'remark': '图片验证码'} CAPTCHA_CODES = {'key': 'captcha_codes', 'remark': '图片验证码'}
ONLINE_USER = {'key': 'online_user', 'remark': '在线用户信息'} ONLINE_USER = {'key': 'online_user', 'remark': '在线用户信息'}
System_Config = {'key': 'system_config', 'remark': '系统配置'}
@property @property
def key(self) -> str: def key(self) -> str:
+3 -5
View File
@@ -3,18 +3,16 @@
import pickle import pickle
from typing import Any, Optional from typing import Any, Optional
from aioredis import Redis from aioredis import Redis
from fastapi import Request
from app.config.setting import settings
from app.core.logger import logger from app.core.logger import logger
class Cache: class Cache:
"""缓存工具类""" """缓存工具类"""
def __init__(self, request: Request): def __init__(self, redis: Redis):
"""初始化""" """初始化"""
self.redis: Redis = request.app.state.redis self.redis = redis
async def mget(self, *keys: list)-> list: async def mget(self, *keys: list)-> list:
"""批量获取缓存 """批量获取缓存
@@ -73,7 +71,7 @@ class Cache:
return False return False
await self.redis.set( await self.redis.set(
name = f"{key}", name = key,
value = data, value = data,
ex=expire ex=expire
) )
-1
View File
@@ -49,7 +49,6 @@ class RequestLogMiddleware(BaseHTTPMiddleware):
response.headers["X-Process-Time"] = str(process_time) response.headers["X-Process-Time"] = str(process_time)
logger.info( logger.info(
f"请求来源: {request.client.host}, "
f"响应状态: {response.status_code}, " f"响应状态: {response.status_code}, "
f"响应内容长度: {response.headers.get('content-length', '0')}, " f"响应内容长度: {response.headers.get('content-length', '0')}, "
f"处理时间: {process_time}s" f"处理时间: {process_time}s"
+6
View File
@@ -31,6 +31,8 @@ from app.core.exceptions import (
ResponseValidationHandle, ResponseValidationHandle,
ResponseValidationError ResponseValidationError
) )
from app.api.v1.services.system.config_service import ConfigService
from app.core.database import async_session
@asynccontextmanager @asynccontextmanager
@@ -40,6 +42,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]:
""" """
logger.info(settings.BANNER + '\n' + f'{settings.TITLE} 服务开始启动...') logger.info(settings.BANNER + '\n' + f'{settings.TITLE} 服务开始启动...')
await import_modules_async(modules=settings.get_events, desc="全局事件", app=app, status=True) await import_modules_async(modules=settings.get_events, desc="全局事件", app=app, status=True)
async with async_session() as session:
await ConfigService().init_config(redis=app.state.redis, db=session)
logger.info('初始化系统配置完成...')
logger.info(f'{settings.TITLE} 服务成功启动...') logger.info(f'{settings.TITLE} 服务成功启动...')
yield yield
+18 -10
View File
@@ -12,7 +12,7 @@
"parent_id": 1, "parent_id": 1,
"name": "网页标题", "name": "网页标题",
"fied_key": "web_title", "fied_key": "web_title",
"fied_value": "fastapi-vue-admin", "fied_value": "FastAPI Vue Admin",
"order": 1 "order": 1
}, },
{ {
@@ -20,7 +20,7 @@
"parent_id": 1, "parent_id": 1,
"name": "网站小图标", "name": "网站小图标",
"fied_key": "web_favicon", "fied_key": "web_favicon",
"fied_value": null, "fied_value": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/logo_20250109090456A114.png",
"order": 2 "order": 2
}, },
{ {
@@ -36,7 +36,7 @@
"parent_id": 4, "parent_id": 4,
"name": "登陆标题", "name": "登陆标题",
"fied_key": "login_title", "fied_key": "login_title",
"fied_value": "fastapi-vue-admin", "fied_value": "FastAPI Vue Admin",
"order": 1 "order": 1
}, },
{ {
@@ -44,7 +44,7 @@
"parent_id": 4, "parent_id": 4,
"name": "登陆描述", "name": "登陆描述",
"fied_key": "login_description", "fied_key": "login_description",
"fied_value": "企业级后台管理系统", "fied_value": "FastAPI Vue Admin 是完全开源的权限管理系统",
"order": 2 "order": 2
}, },
{ {
@@ -52,7 +52,7 @@
"parent_id": 4, "parent_id": 4,
"name": "登录logo", "name": "登录logo",
"fied_key": "login_logo", "fied_key": "login_logo",
"fied_value": null, "fied_value": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/logo_20250109090448A263.png",
"order": 3 "order": 3
}, },
{ {
@@ -60,7 +60,7 @@
"parent_id": 4, "parent_id": 4,
"name": "登录背景", "name": "登录背景",
"fied_key": "login_background", "fied_key": "login_background",
"fied_value": null, "fied_value": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/background_20250109090435A357.png",
"order": 4 "order": 4
}, },
{ {
@@ -68,7 +68,7 @@
"parent_id": 4, "parent_id": 4,
"name": "版权信息", "name": "版权信息",
"fied_key": "copyright", "fied_key": "copyright",
"fied_value": "2021-2025 fastapi-vue-admin.com 版权所有", "fied_value": "Copyright © 2021-2025 fastapi-vue-admin.com 版权所有",
"order": 5 "order": 5
}, },
{ {
@@ -82,7 +82,7 @@
{ {
"id": 11, "id": 11,
"parent_id": 4, "parent_id": 4,
"name": "帮助链接", "name": "帮助",
"fied_key": "help_url", "fied_key": "help_url",
"fied_value": "https://django-vue-admin.com", "fied_value": "https://django-vue-admin.com",
"order": 7 "order": 7
@@ -90,7 +90,7 @@
{ {
"id": 12, "id": 12,
"parent_id": 4, "parent_id": 4,
"name": "隐私链接", "name": "隐私",
"fied_key": "privacy_url", "fied_key": "privacy_url",
"fied_value": "/api/system/clause/privacy.html", "fied_value": "/api/system/clause/privacy.html",
"order": 8 "order": 8
@@ -98,9 +98,17 @@
{ {
"id": 13, "id": 13,
"parent_id": 4, "parent_id": 4,
"name": "条款链接", "name": "条款",
"fied_key": "clause_url", "fied_key": "clause_url",
"fied_value": "/api/system/clause/terms_service.html", "fied_value": "/api/system/clause/terms_service.html",
"order": 9 "order": 9
},
{
"id": 14,
"parent_id": 4,
"name": "源码",
"fied_key": "code_url",
"fied_value": "https://gitee.com/tao__tao/fastapi_vue_admin.git",
"order": 10
} }
] ]
+36
View File
@@ -970,5 +970,41 @@
"redirect": null, "redirect": null,
"description": "初始化数据", "description": "初始化数据",
"creator_id": 1 "creator_id": 1
},
{
"id": 55,
"name": "批量更新配置",
"type": 3,
"icon": null,
"order": 4,
"permission": "system:config:update",
"route_name": null,
"route_path": null,
"component_path": null,
"parent_id": 51,
"available": true,
"cache": true,
"hidden": false,
"redirect": null,
"description": "初始化数据",
"creator_id": 1
},
{
"id": 56,
"name": "文件上传",
"type": 3,
"icon": null,
"order": 5,
"permission": "system:config:upload",
"route_name": null,
"route_path": null,
"component_path": null,
"parent_id": 51,
"available": true,
"cache": true,
"hidden": false,
"redirect": null,
"description": "初始化数据",
"creator_id": 1
} }
] ]
@@ -223,10 +223,6 @@
"role_id": 1, "role_id": 1,
"menu_id": 56 "menu_id": 56
}, },
{
"role_id": 1,
"menu_id": 57
},
{ {
"role_id": 2, "role_id": 2,
"menu_id": 1 "menu_id": 1
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

+51
View File
@@ -20,6 +20,7 @@
"store": "^2.0.12", "store": "^2.0.12",
"vue": "^3.3.11", "vue": "^3.3.11",
"vue-echarts": "^6.6.9", "vue-echarts": "^6.6.9",
"vue-json-viewer": "^3.0.4",
"vue-request": "^2.0.4", "vue-request": "^2.0.4",
"vue-router": "^4.2.5", "vue-router": "^4.2.5",
"vuex": "^4.1.0", "vuex": "^4.1.0",
@@ -1506,6 +1507,17 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/clipboard": {
"version": "2.0.11",
"resolved": "https://registry.npmmirror.com/clipboard/-/clipboard-2.0.11.tgz",
"integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
"license": "MIT",
"dependencies": {
"good-listener": "^1.2.2",
"select": "^1.1.2",
"tiny-emitter": "^2.0.0"
}
},
"node_modules/codepage": { "node_modules/codepage": {
"version": "1.15.0", "version": "1.15.0",
"resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz",
@@ -1604,6 +1616,12 @@
"node": ">=0.4.0" "node": ">=0.4.0"
} }
}, },
"node_modules/delegate": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/delegate/-/delegate-3.2.0.tgz",
"integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==",
"license": "MIT"
},
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "1.0.3", "version": "1.0.3",
"resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz",
@@ -1767,6 +1785,15 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/good-listener": {
"version": "1.2.2",
"resolved": "https://registry.npmmirror.com/good-listener/-/good-listener-1.2.2.tgz",
"integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
"license": "MIT",
"dependencies": {
"delegate": "^3.1.2"
}
},
"node_modules/has-flag": { "node_modules/has-flag": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
@@ -2537,6 +2564,12 @@
"compute-scroll-into-view": "^1.0.20" "compute-scroll-into-view": "^1.0.20"
} }
}, },
"node_modules/select": {
"version": "1.1.2",
"resolved": "https://registry.npmmirror.com/select/-/select-1.1.2.tgz",
"integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==",
"license": "MIT"
},
"node_modules/shallow-equal": { "node_modules/shallow-equal": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz", "resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
@@ -2679,6 +2712,12 @@
"node": ">=12.22" "node": ">=12.22"
} }
}, },
"node_modules/tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"license": "MIT"
},
"node_modules/to-regex-range": { "node_modules/to-regex-range": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz",
@@ -2845,6 +2884,18 @@
} }
} }
}, },
"node_modules/vue-json-viewer": {
"version": "3.0.4",
"resolved": "https://registry.npmmirror.com/vue-json-viewer/-/vue-json-viewer-3.0.4.tgz",
"integrity": "sha512-pnC080rTub6YjccthVSNQod2z9Sl5IUUq46srXtn6rxwhW8QM4rlYn+CTSLFKXWfw+N3xv77Cioxw7B4XUKIbQ==",
"license": "MIT",
"dependencies": {
"clipboard": "^2.0.4"
},
"peerDependencies": {
"vue": "^3.2.2"
}
},
"node_modules/vue-request": { "node_modules/vue-request": {
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmmirror.com/vue-request/-/vue-request-2.0.4.tgz", "resolved": "https://registry.npmmirror.com/vue-request/-/vue-request-2.0.4.tgz",
+13 -6
View File
@@ -24,19 +24,26 @@ export function createConfig(body) {
}); });
} }
export function updateConfig(body) { export function batchConfig(body) {
return request({ return request({
url: "/api/v1/system/config/update", url: "/api/v1/system/config/batch",
method: "put", method: "put",
data: body, data: body,
}); });
} }
export function deleteConfig(params) { export function uploadFile(body) {
return request({ return request({
url: "/api/v1/system/config/delete", url: "/api/v1/system/config/upload",
method: "delete", method: "post",
params: params, data: body,
headers: { "Content-Type": "multipart/form-data" },
}); });
} }
export function getInitConfig() {
return request({
url: "/api/v1/system/config/init",
method: "get",
});
}
+31 -4
View File
@@ -11,8 +11,8 @@
> >
<!-- Logo 区域 --> <!-- Logo 区域 -->
<div class="logo-container"> <div class="logo-container">
<a-image src="/logo.png" :preview="false" :width="28" :height="28" /> <a-image :src="initConfigState.web_favicon" :preview="false" :width="28" :height="28" />
<h1 class="logo-title" v-show="!menuState.collapsed">fastapi-vue-admin</h1> <h1 class="logo-title" v-show="!menuState.collapsed">{{ initConfigState.web_title }}</h1>
</div> </div>
<a-menu <a-menu
@@ -147,6 +147,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, computed, watch, onMounted, inject, type Ref } from "vue"; import { reactive, computed, watch, onMounted, inject, type Ref } from "vue";
import type { MenuProps, ItemType } from "ant-design-vue"; import type { MenuProps, ItemType } from "ant-design-vue";
import { message } from 'ant-design-vue';
import { useRouter, useRoute } from "vue-router"; import { useRouter, useRoute } from "vue-router";
import storage from 'store'; import storage from 'store';
import store from '@/store'; import store from '@/store';
@@ -166,6 +167,7 @@ import {
} from '@ant-design/icons-vue'; } from '@ant-design/icons-vue';
import { logout } from '@/api/system/auth'; import { logout } from '@/api/system/auth';
import { getNoticeList } from '@/api/system/notice' import { getNoticeList } from '@/api/system/notice'
import { getInitConfig } from "@/api/system/config"
// 通知公告获取 // 通知公告获取
const dataSource = reactive({ const dataSource = reactive({
@@ -327,13 +329,38 @@ const noticeState = reactive({
}); });
const initConfigState = reactive({
web_title: '',
web_favicon: '',
});
const initConfig = () => {
getInitConfig()
.then(response => {
const { status_code, data } = response.data;
if (status_code === 200) {
const configData = JSON.parse(data);
configData.forEach(item => {
if (item.fied_key === 'web_title') {
initConfigState.web_title = item.fied_value;
} else if (item.fied_key === 'web_favicon') {
initConfigState.web_favicon = item.fied_value;
}
});
}
})
.catch(error => {
message.error('获取系统配置失败');
console.error(error); // 打印错误信息以便调试
});
};
// 在页面加载时获取通知列表 // 在页面加载时获取通知列表
onMounted(() => { onMounted(() => {
initMenu(); initMenu();
handleNoticeList(); handleNoticeList();
initConfig();
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
+3 -1
View File
@@ -74,6 +74,7 @@
:loading="loading" :loading="loading"
:dataSource="cacheNames" :dataSource="cacheNames"
:pagination="false" :pagination="false"
:scroll="{ y: 600 }"
rowKey="cache_name" rowKey="cache_name"
> >
<a-table-column key="cache_name" title="缓存名称" align="center" :ellipsis="true"> <a-table-column key="cache_name" title="缓存名称" align="center" :ellipsis="true">
@@ -109,6 +110,7 @@
:loading="subLoading" :loading="subLoading"
:dataSource="cacheKeys.map(key => ({ cacheKey: key }))" :dataSource="cacheKeys.map(key => ({ cacheKey: key }))"
:pagination="false" :pagination="false"
:scroll="{ y: 600 }"
rowKey="cacheKey" rowKey="cacheKey"
> >
<a-table-column key="cacheKey" title="缓存键名" align="center" :ellipsis="true"> <a-table-column key="cacheKey" title="缓存键名" align="center" :ellipsis="true">
@@ -147,7 +149,7 @@
<a-form-item label="缓存内容:" name="cache_value"> <a-form-item label="缓存内容:" name="cache_value">
<a-textarea <a-textarea
v-model:value="cacheForm.cache_value" v-model:value="cacheForm.cache_value"
:rows="8" :rows="19"
readonly readonly
/> />
</a-form-item> </a-form-item>
+89 -11
View File
@@ -1,15 +1,15 @@
<template> <template>
<a-layout> <a-layout>
<!-- 页面主体 --> <!-- 页面主体 -->
<div class="container"> <div class="container" :style="{ backgroundImage: `url(${initConfigState.login_background})` }">
<a-layout-content :style="contentStyle"> <a-layout-content :style="contentStyle">
<div class="header"> <div class="header">
<div class="logo"> <div class="logo">
<a-image src="/logo.png" :preview="false" /> <a-image :src="initConfigState.login_logo" :preview="false" />
</div> </div>
<div class="title">FastAPI Vue Admin</div> <div class="title">{{ initConfigState.login_title }}</div>
</div> </div>
<div class="desc">FastAPI Vue Admin 是完全开源的权限管理系统</div> <div class="desc">{{ initConfigState.login_description }}</div>
<div class="login-main" style="width: 330px; margin: 0 auto;"> <div class="login-main" style="width: 330px; margin: 0 auto;">
<a-tabs centered> <a-tabs centered>
@@ -62,13 +62,28 @@
<!-- 页面底部 --> <!-- 页面底部 -->
<a-layout-footer :style="footerStyle"> <a-layout-footer :style="footerStyle">
<div class="footer-copyright"> <div class="footer-content">
<a-button type="link" href="https://gitee.com/tao__tao/fastapi_vue_admin.git"> <div class="footer-links">
<icon-font type="icon-copyright" :style="{ fontSize: '16px' }" /> <a-button type="link" :href="initConfigState.code_url">
Copyright © 2024 insistence.tech All Rights Reserved. 皖ICP备2023021369号-1 <icon-font type="icon-copyright" :style="{ fontSize: '16px' }" />
</a-button> {{ initConfigState.copyright }} |
</a-button>
<a-button type="link" :href="initConfigState.help_url">
{{ initConfigState.help_name }} |
</a-button>
<a-button type="link" :href="initConfigState.privacy_url">
{{ initConfigState.privacy_name }} |
</a-button>
<a-button type="link" :href="initConfigState.clause_url">
{{ initConfigState.clause_name }}
</a-button>
</div>
<div class="footer-record">
{{ initConfigState.keep_record }}
</div>
</div> </div>
</a-layout-footer> </a-layout-footer>
</div> </div>
<!-- 弹窗区域 --> <!-- 弹窗区域 -->
@@ -146,6 +161,7 @@ import { save_token } from "@/utils/util"
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { login, getCaptcha } from "@/api/system/auth" import { login, getCaptcha } from "@/api/system/auth"
import { registerUser, forgetPassword } from "@/api/system/user" import { registerUser, forgetPassword } from "@/api/system/user"
import { getInitConfig } from "@/api/system/config"
import type { LoginForm, CaptchaState, ForgetPasswordForm, RegisterForm } from './types' import type { LoginForm, CaptchaState, ForgetPasswordForm, RegisterForm } from './types'
const router = useRouter(); const router = useRouter();
@@ -271,7 +287,70 @@ const requestCaptcha = () => {
.catch(() => captchaState.enable = false); .catch(() => captchaState.enable = false);
}; };
onMounted(requestCaptcha); const initConfigState = reactive({
login_title: '',
login_description: '/logo.png',
login_logo: '',
login_background: '/background.png',
copyright: '',
copyright_name: '',
keep_record: '',
keep_record_name: '',
help_url: '',
help_name: '',
privacy_url: '',
privacy_name: '',
clause_url: '',
clause_name: '',
code_url: '',
});
const initConfig = () => {
getInitConfig()
.then(response => {
const { status_code, data } = response.data;
if (status_code === 200) {
const configData = JSON.parse(data);
configData.forEach(item => {
if (item.fied_key === 'login_title') {
initConfigState.login_title = item.fied_value;
} else if (item.fied_key === 'login_description') {
initConfigState.login_description = item.fied_value;
} else if (item.fied_key === 'login_logo') {
initConfigState.login_logo = item.fied_value;
} else if (item.fied_key === 'login_background') {
initConfigState.login_background = item.fied_value;
} else if (item.fied_key === 'copyright') {
initConfigState.copyright = item.fied_value;
initConfigState.copyright_name = item.name;
} else if (item.fied_key === 'keep_record') {
initConfigState.keep_record = item.fied_value;
initConfigState.keep_record_name = item.name;
} else if (item.fied_key === 'help_url') {
initConfigState.help_url = item.fied_value;
initConfigState.help_name = item.name;
} else if (item.fied_key === 'privacy_url') {
initConfigState.privacy_url = item.fied_value;
initConfigState.privacy_name = item.name;
} else if (item.fied_key === 'clause_url') {
initConfigState.clause_url = item.fied_value;
initConfigState.clause_name = item.name;
} else if (item.fied_key === 'code_url') {
initConfigState.code_url = item.fied_value;
}
});
}
})
.catch(error => {
message.error('获取系统配置失败');
console.error(error); // 打印错误信息以便调试
});
};
onMounted(() => {
requestCaptcha();
initConfig();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -282,7 +361,6 @@ onMounted(requestCaptcha);
} }
.container { .container {
background-image: url("/background.png");
background-size: 100% 100%; background-size: 100% 100%;
.desc { .desc {
+160 -357
View File
@@ -1,376 +1,179 @@
<template> <template>
<div> <div>
<!-- 页面头部 --> <!-- 页面头部 -->
<page-header /> <page-header />
<!-- 搜索表单 --> <!-- 配置项编辑区域 -->
<div class="tree-search-wrapper"> <div class="config-edit-wrapper">
<a-card :bordered="false"> <a-card title="系统配置" :bordered="false">
<a-form :model="queryState" @finish="onFinish"> <!-- 动态生成配置项 -->
<a-row> <template v-for="group in updateState" :key="group.id">
<a-col flex="0 1 450px"> <a-card :title="group.name" :bordered="false" style="margin-bottom: 24px;">
<a-form-item name="name" label="名称" style="max-width: 300px;"> <a-row :gutter="16">
<a-input v-model:value="queryState.name" placeholder="请输入名称" allowClear /> <template v-for="config in group.children" :key="config.id">
</a-form-item> <a-col :span="12">
</a-col> <!-- 图片上传类型 -->
</a-row> <a-form-item v-if="config.fied_key === 'web_favicon' || config.fied_key === 'login_logo' || config.fied_key === 'login_background'" :label="config.name">
<a-row> <a-upload
<a-col> v-model:file-list="config.fileList"
<a-space> list-type="picture-card"
<a-button type="primary" html-type="submit" :loading="tableLoading">查询</a-button> :before-upload="beforeUpload"
<a-button @click="resetFields">重置</a-button> :custom-request="(options) => handleUpload(options, config)"
</a-space> >
</a-col> <div v-if="!config.fileList || config.fileList.length === 0">
</a-row> <plus-outlined />
</a-form> <div style="margin-top: 8px">上传图片</div>
</a-card> </div>
</a-upload>
</a-form-item>
<!-- 输入框类型 -->
<a-form-item v-else :label="config.name">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
</a-col>
</template>
</a-row>
</a-card>
</template>
<!-- 保存按钮 -->
<div style="text-align: right; margin-top: 24px;">
<a-button type="primary" @click="handleSave">保存</a-button>
</div> </div>
</a-card>
<!-- 表格区域 -->
<div class="table-wrapper">
<a-card title="配置列表" :bordered="false" :headStyle="{ borderBottom: 'none', padding: '20px 24px' }"
:bodyStyle="{ padding: '0 24px', minHeight: 'calc(100vh - 400px)' }">
<template #extra>
<a-space>
<a-button type="primary" :icon="h(PlusOutlined)" @click="modalHandle('create')">新建</a-button>
<a-button type="primary" @click="openSystemConfigModal">系统配置</a-button>
</a-space>
</template>
<!-- 树形表格 -->
<a-table :rowKey="record => record.id"
:columns="columns"
:data-source="dataSource"
:loading="tableLoading"
:scroll="{ x: 500, y: 'calc(100vh - 500px)' }"
:row-selection="rowSelection"
:pagination="false"
:style="{ minHeight: '500px' }">
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'action'">
<a-space size="middle">
<a @click="modalHandle('view', record)">查看</a>
<a @click="modalHandle('update', record)">修改</a>
<a-popconfirm title="确定要删除吗?" ok-text="确定" cancel-text="取消"
@confirm="deleteRow(record)">
<a style="color: red;">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</a-table>
</a-card>
</div>
<!-- 系统配置弹窗 -->
<a-modal v-model:open="systemConfigModalVisible" title="系统配置" :width="800" :destroyOnClose="true"
:confirmLoading="systemConfigModalLoading" @ok="handleSystemConfigSubmit">
<!-- 基础配置部分 -->
<a-card title="基础配置" :bordered="false" style="margin-bottom: 16px;">
<a-form ref="systemConfigForm" :model="systemConfigState" :label-col="{ span: 5 }"
:wrapper-col="{ span: 15 }">
<!-- 基础配置的顶级配置项 -->
<a-form-item v-for="config in systemConfigState.filter(item => item.fied_key === 'base')"
:key="config.id" :name="config.id" :label="config.name"
:rules="[{ required: true, message: `请输入${config.name}` }]">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
<!-- 基础配置的子配置项 -->
<a-form-item v-for="config in systemConfigState.filter(item => item.parent_id === 1)"
:key="config.id" :name="config.id" :label="config.name"
:rules="[{ required: true, message: `请输入${config.name}` }]">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
</a-form>
</a-card>
<!-- 登录页配置部分 -->
<a-card title="登录页配置" :bordered="false">
<a-form ref="systemConfigForm" :model="systemConfigState" :label-col="{ span: 5 }"
:wrapper-col="{ span: 15 }">
<!-- 登录页配置的顶级配置项 -->
<a-form-item v-for="config in systemConfigState.filter(item => item.fied_key === 'login')"
:key="config.id" :name="config.id" :label="config.name"
:rules="[{ required: true, message: `请输入${config.name}` }]">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
<!-- 登录页配置的子配置项 -->
<a-form-item v-for="config in systemConfigState.filter(item => item.parent_id === 4)"
:key="config.id" :name="config.id" :label="config.name"
:rules="[{ required: true, message: `请输入${config.name}` }]">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear />
</a-form-item>
</a-form>
</a-card>
</a-modal>
</div> </div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, reactive, computed, onMounted, h, unref } from 'vue'; import { reactive, onMounted } from 'vue';
import { message, Modal, Table } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import type { MenuProps, TableColumnsType } from 'ant-design-vue'; import { PlusOutlined } from '@ant-design/icons-vue';
import { PlusOutlined, DownOutlined, CheckOutlined, StopOutlined } from '@ant-design/icons-vue'; import { getConfigList, batchConfig, uploadFile } from '@/api/system/config';
import { listToTree, cloneDeep, isEmpty } from '@/utils/util'; import PageHeader from '@/components/PageHeader.vue';
import { getConfigList, createConfig, updateConfig, deleteConfig } from '@/api/system/config'; import type { tableDataType } from './types'
import PageHeader from '@/components/PageHeader.vue'
import type { searchDataType, tableDataType } from './types';
// 响应式数据 interface ConfigGroup extends tableDataType {
const createForm = ref(); children: tableDataType[];
const updateForm = ref(); fileList?: any[];
const tableLoading = ref(false); }
const openModal = ref(false);
const modalTitle = ref('');
const modalSubmitLoading = ref(false);
const detailStateLoading = ref(false);
const dataSource = ref<tableDataType[]>([]);
const selectedRowKeys = ref<tableDataType['id'][]>([]);
const queryState = reactive<searchDataType>({ const updateState = reactive<ConfigGroup[]>([]);
name: null,
});
const createState = reactive<tableDataType>({
name: '',
order: 1,
parent_id: undefined,
fied_key: null,
fied_value: '',
}); // 加载配置数据
const updateState = reactive<tableDataType>({ const loadConfigData = async () => {
id: undefined, try {
name: '', const response = await getConfigList({});
order: 1, const items = response.data.data.items;
parent_id: undefined,
fied_key: null,
fied_value: '',
});
const detailState = ref<tableDataType>({});
const columns = reactive<TableColumnsType>([ // 将配置项按父级分组
{ const groups = items
title: '配置名称', .filter(item => item.parent_id === null) // 获取顶级配置项
dataIndex: 'name', .map(group => ({
ellipsis: true, ...group,
key: 'name', children: items.filter(item => item.parent_id === group.id) // 获取子配置项
// width: 160 }));
},
{
title: '排序',
dataIndex: 'order',
key: 'order',
// align: 'center',
ellipsis: true,
// width: 100
},
{
title: '父级',
dataIndex: 'parent_id',
key: 'parent_id',
ellipsis: true,
// align: 'center',
// width: 100
},
{
title: '键',
dataIndex: 'fied_key',
key: 'fied_key',
ellipsis: true,
// align: 'center'
},
{
title: '值',
dataIndex: 'fied_value',
key: 'fied_value',
ellipsis: true,
// align: 'center'
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
align: 'center',
width: 150
}
]);
const rowSelection = computed(() => { // 初始化图片上传列表
return { groups.forEach(group => {
selectedRowKeys: unref(selectedRowKeys), group.children.forEach(config => {
onChange: (selectingRowKeys: tableDataType['id'][]) => { if (config.fied_key === 'web_favicon' || config.fied_key === 'login_logo' || config.fied_key === 'login_background') {
selectedRowKeys.value = selectingRowKeys; config.fileList = config.fied_value ? [{ url: config.fied_value }] : [];
}, }
hideDefaultSelections: true, });
selections: [ });
Table.SELECTION_ALL,
Table.SELECTION_INVERT, // 避免直接修改 reactive 对象,使用重新赋值的方式
Table.SELECTION_NONE updateState.length = 0; // 清空数组
] updateState.push(...groups); // 添加新数据
} } catch (error) {
}); console.error('加载配置数据失败:', error);
message.error('加载配置数据失败');
}
};
// 图片上传前的校验
const beforeUpload = (file: File) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('只能上传图片文件!');
}
return isImage;
};
// 自定义上传逻辑
const handleUpload = async (options: any, config: any) => {
const { file, onSuccess, onError } = options;
try {
const formData = new FormData();
formData.append('file', file);
const response = await uploadFile(formData);
const fileUrl = response.data.data.file_url; // 使用 file_url 更新配置项的值
// 更新配置项的值
config.fied_value = fileUrl;
config.fileList = [{ url: fileUrl }]; // 更新文件列表显示
onSuccess(response, file);
message.success('上传成功');
} catch (error) {
onError(error);
console.error('上传失败:', error);
message.error('上传失败');
}
};
// 保存配置
const handleSave = async () => {
try {
// 提取所有配置项(包括父级和子级)
const configs = updateState.flatMap(group => [
{
id: group.id,
name: group.name,
order: group.order,
fied_key: group.fied_key,
fied_value: group.fied_value,
parent_id: group.parent_id,
},
...group.children.map(config => ({
id: config.id,
name: config.name,
order: config.order,
fied_key: config.fied_key,
fied_value: config.fied_value,
parent_id: config.parent_id,
})),
]);
// 调用批量保存接口,直接发送数组
await batchConfig(configs); // 修改为直接发送数组
message.success('配置保存成功');
} catch (error) {
console.error('保存配置失败:', error);
message.error('配置保存失败');
}
};
// 生命周期钩子 // 生命周期钩子
onMounted(() => loadingData()); onMounted(() => {
loadConfigData();
// 查询 });
const onFinish = () => {
loadingData();
};
// 加载表格数据
const loadingData = () => {
tableLoading.value = true;
let params = {};
if (queryState.name) {
params['name'] = queryState.name;
}
getConfigList(params).then(response => {
const result = response.data;
dataSource.value = listToTree(result.data.items);
}).catch(error => {
console.log(error);
}).finally(() => {
tableLoading.value = false;
});
};
// 重置查询
const resetFields = () => {
Object.keys(queryState).forEach((key: string) => {
delete queryState[key];
});
loadingData();
};
// 删除
const deleteRow = (row: tableDataType) => {
deleteConfig({ id: row.id }).then(response => {
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
console.log(error);
});
};
// 弹窗关键字处理
const modalHandle = (modalType: string, record?: tableDataType) => {
modalTitle.value = modalType;
openModal.value = true;
if (modalType === 'view' && record !== undefined) {
detailStateLoading.value = true;
detailState.value = record
detailStateLoading.value = false;
} else if (modalType === 'update' && record !== undefined) {
Object.keys(updateState).forEach(key => {
updateState[key] = record[key];
})
}
};
// 弹窗提交(详情/新建/修改)
const handleModalSumbit = () => {
modalSubmitLoading.value = true;
if (modalTitle.value === 'view') {
modalSubmitLoading.value = false;
openModal.value = false;
} else if (modalTitle.value === 'create') {
createForm.value.validate().then(() => {
const createBody = cloneDeep(createState);
Object.keys(createBody).forEach(key => {
if (isEmpty(createBody[key])) {
delete createBody[key];
}
});
createConfig(createBody).then(response => {
const result = response.data;
modalSubmitLoading.value = false;
openModal.value = false;
Object.keys(createState).forEach(key => delete createState[key]);
createState.order = 1;
message.success(result.msg);
loadingData();
}).catch(error => {
console.error(error);
});
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
})
} else if (modalTitle.value === 'update') {
updateForm.value.validate().then(() => {
updateConfig(updateState).then(response => {
modalSubmitLoading.value = false;
openModal.value = false;
const result = response.data;
message.success(result.msg);
loadingData();
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
});
}).catch(error => {
modalSubmitLoading.value = false;
console.error(error)
})
}
};
// 系统配置弹窗状态
const systemConfigModalVisible = ref(false);
const systemConfigModalLoading = ref(false);
const systemConfigState = ref<tableDataType[]>([]);
// 打开系统配置弹窗
const openSystemConfigModal = async () => {
systemConfigModalVisible.value = true;
systemConfigModalLoading.value = true;
try {
const response = await getConfigList({});
systemConfigState.value = listToTree(response.data.items);
} catch (error) {
console.error(error);
} finally {
systemConfigModalLoading.value = false;
}
};
// 提交系统配置
const handleSystemConfigSubmit = async () => {
systemConfigModalLoading.value = true;
try {
const updatePromises = systemConfigState.value.map(config =>
updateConfig(config)
);
await Promise.all(updatePromises);
message.success('系统配置更新成功');
systemConfigModalVisible.value = false;
loadingData(); // 重新加载数据
} catch (error) {
console.error(error);
} finally {
systemConfigModalLoading.value = false;
}
};
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.tree-search-wrapper { .config-edit-wrapper {
margin-block-end: 16px; margin-block-end: 16px;
}
.ant-card {
margin-bottom: 24px;
&:last-child {
margin-bottom: 0;
}
} }
</style> </style>
+1 -6
View File
@@ -1,15 +1,10 @@
export interface searchDataType {
name?: string
}
export interface tableDataType { export interface tableDataType {
id?: number; id?: number;
index?: number;
name?: string; name?: string;
order?: number; order?: number;
fied_key?: string; fied_key?: string;
fied_value?: string; fied_value?: string;
parent_id?: number; parent_id?: number;
parent_name?: string; fileList?: any[];
children?: tableDataType[]; children?: tableDataType[];
} }