1、系统配置修改上线 2、 pinia取代vuex

This commit is contained in:
zhangtao
2025-01-10 10:39:15 +08:00
parent 8d9a64aed3
commit 1ae731eafe
21 changed files with 379 additions and 568 deletions
@@ -19,26 +19,23 @@ from app.api.v1.services.system.config_service import ConfigService
router = APIRouter(route_class=OperationLogRoute) router = APIRouter(route_class=OperationLogRoute)
@router.get("/list", summary="查询配置", description="查询配置") @router.get("/info", summary="获取配置", description="获取配置")
async def get_obj_list( async def get_obj_list(
page: PaginationQueryParams = Depends(),
search: ConfigQueryParams = Depends(),
auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:query"])), auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:query"])),
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await ConfigService.list_services(auth=auth, search=search, order_by=page.order_by) result_dict = await ConfigService.get_services(auth=auth, id=1)
result_dict = await PaginationService.get_page_obj(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size) 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.put("/update", summary="修改配置", description="修改配置")
async def batch_objs( async def update_objs(
request: Request, request: Request,
data: List[ConfigUpdateSchema], data: ConfigUpdateSchema,
auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:update"])), auth: AuthSchema = Depends(AuthPermission(permissions=["system:config:update"])),
) -> JSONResponse: ) -> JSONResponse:
result_list_dict = await ConfigService.batch_services(auth=auth, request=request, data=data) result_dict = await ConfigService.update_services(auth=auth, request=request, data=data)
logger.info(f"{auth.user.name} 批量更新配置成功 {result_list_dict}") logger.info(f"{auth.user.name} 更新配置成功 {result_dict}")
return SuccessResponse(data=result_list_dict, msg="批量更新配置成功") return SuccessResponse(data=result_dict, msg="更新配置成功")
@router.post("/upload", summary="上传文件", dependencies=[Depends(AuthPermission(permissions=["system:config:upload"]))]) @router.post("/upload", summary="上传文件", dependencies=[Depends(AuthPermission(permissions=["system:config:upload"]))])
async def upload_file( async def upload_file(
@@ -16,9 +16,9 @@ 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 list_curd(self, search: Dict = None, order_by: List[Dict[str, str]] = None) -> Sequence[ConfigModel]: async def get_curd(self, id: int) -> Optional[ConfigModel]:
"""获取配置列表""" """获取配置"""
return await self.list(search=search, order_by=order_by) return await self.get(id=id)
async def update_curd(self, id: int, data: ConfigUpdateSchema) -> Optional[ConfigModel]: async def update_curd(self, id: int, data: ConfigUpdateSchema) -> Optional[ConfigModel]:
"""更新配置""" """更新配置"""
@@ -1,7 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from sqlalchemy import Boolean, Column, ForeignKey, String, Integer, Text, DateTime, JSON from sqlalchemy import Column, String, Integer
from sqlalchemy.orm import relationship
from app.core.base_model import ModelBase from app.core.base_model import ModelBase
@@ -15,21 +14,15 @@ class ConfigModel(ModelBase):
# 基础字段 # 基础字段
id = Column(Integer, primary_key=True, autoincrement=True, unique=True, comment='主键ID') id = Column(Integer, primary_key=True, autoincrement=True, unique=True, comment='主键ID')
name = Column(String(40), nullable=False, comment="配置名称", unique=True)
order = Column(Integer, nullable=False, default=1, comment="显示排序")
fied_key = Column(String(100), nullable=False, comment="键", unique=True)
fied_value = Column(Text, nullable=True, comment="值")
# 层级关系 title = Column(String(40), nullable=False, default="FastAPI Vue Admin", comment="网站标题")
parent_id = Column( favicon = Column(String(100), nullable=False, default="http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/logo_20250109090456A114.png", comment="网站favicon")
Integer, logo = Column(String(100), nullable=False, default="http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/logo_20250109090448A263.png", comment="网站logo")
ForeignKey("system_config.id", ondelete="CASCADE", onupdate="CASCADE"), background = Column(String(100), nullable=False, default="http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/background_20250109090435A357.png", comment="网站背景")
nullable=True, description = Column(String(100), nullable=False, default="FastAPI Vue Admin 是完全开源的权限管理系统", comment="网站描述")
index=True, copyright = Column(String(100), nullable=False, default="Copyright © 2021-2025 fastapi-vue-admin.com 版权所有", comment="版权信息")
comment="父级配置ID" keep_record = Column(String(100), nullable=False, default="晋ICP备18005113号-3", comment="备案信息")
) help_url = Column(String(100), nullable=False, default="https://django-vue-admin.com", comment="帮助链接")
parent = relationship( privacy_url = Column(String(100), nullable=False, default="https://gitee.com/tao__tao/fastapi_vue_admin/blob/main/docs/clause/privacy.md", comment="隐私政策链接")
"ConfigModel", clause_url = Column(String(100), nullable=False, default="https://gitee.com/tao__tao/fastapi_vue_admin/blob/main/docs/clause/terms_service.md", comment="服务条款链接")
cascade='all, delete-orphan', code_url = Column(String(100), nullable=False, default="https://gitee.com/tao__tao/fastapi_vue_admin.git", comment="源码地址")
uselist=False
)
@@ -9,11 +9,11 @@ class ConfigQueryParams:
def __init__( def __init__(
self, self,
name: Optional[str] = Query(None, description="配置名称", min_length=2, max_length=50), title: Optional[str] = Query(None, description="网站标题", min_length=2, max_length=50),
) -> None: ) -> None:
super().__init__() super().__init__()
# 模糊查询字段 # 模糊查询字段
self.name = ("like", name) self.title = ("like", title)
@@ -7,18 +7,17 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator
class ConfigCreateSchema(BaseModel): class ConfigCreateSchema(BaseModel):
"""配置创建模型""" """配置创建模型"""
name: str = Field(..., max_length=40, description="配置名称") title: str = Field(..., max_length=40, description="网站标题")
order: int = Field(default=1, ge=0, description="显示顺序") favicon: str = Field(..., max_length=100, description="网站favicon")
fied_key: str = Field(..., description="键") logo: str = Field(..., max_length=100, description="网站logo")
fied_value: Optional[str] = Field(default=None, description="值") background: str = Field(..., max_length=100, description="网站背景")
parent_id: Optional[int] = Field(default=None, ge=0, description="父配置ID") description: str = Field(..., max_length=100, description="网站描述")
copyright: str = Field(..., max_length=100, description="版权信息")
@classmethod keep_record: str = Field(..., max_length=100, description="备案信息")
@model_validator(mode='after') help_url: str = Field(..., max_length=100, description="帮助链接")
def validate_fields(cls, data): privacy_url: str = Field(..., max_length=100, description="隐私政策链接")
if not data.name or len(data.name.strip()) == 0: clause_url: str = Field(..., max_length=100, description="服务条款链接")
raise ValueError("配置名称不能为空") code_url: str = Field(..., max_length=100, description="源码地址")
return data
class ConfigUpdateSchema(ConfigCreateSchema): class ConfigUpdateSchema(ConfigCreateSchema):
@@ -1,19 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import json import json
from typing import List, Dict from typing import Dict
from fastapi import Depends, Request, UploadFile from fastapi import Request, UploadFile
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from aioredis import Redis 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 ConfigOutSchema, ConfigUpdateSchema 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.cruds.system.config_crud import ConfigCRUD from app.api.v1.cruds.system.config_crud import ConfigCRUD
from app.common.enums import RedisInitKeyConfig from app.common.enums import RedisInitKeyConfig
from app.core.cache_crud import Cache from app.core.cache_crud import Cache
from app.core.dependencies import db_getter
from app.utils.upload_util import UploadUtil from app.utils.upload_util import UploadUtil
from app.core.base_schema import UploadResponseSchema from app.core.base_schema import UploadResponseSchema
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
@@ -27,20 +25,17 @@ class ConfigService:
""" """
@classmethod @classmethod
async def list_services(cls, auth: AuthSchema, search: ConfigQueryParams = None, order_by: List[Dict[str, str]] = None) -> List[Dict]: async def get_services(cls, auth: AuthSchema, id: int) -> Dict:
config_obj_list = await ConfigCRUD(auth).list_curd(search=search.__dict__, order_by=order_by) config_obj = await ConfigCRUD(auth).get_curd(id=id)
return [ConfigOutSchema.model_validate(config_obj).model_dump() for config_obj in config_obj_list] return ConfigOutSchema.model_validate(config_obj).model_dump()
@classmethod @classmethod
async def batch_services(cls, auth: AuthSchema, request: Request, data: List[ConfigUpdateSchema]) -> List[Dict]: async def update_services(cls, auth: AuthSchema, request: Request, data: ConfigUpdateSchema) -> Dict:
result = [] new_obj = await ConfigCRUD(auth).update_curd(id=data.id, data=data)
for obj in data: new_obj_dict = ConfigOutSchema.model_validate(new_obj).model_dump()
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) cls.init_config(redis=request.app.state.redis, db=auth.db)
return result return new_obj_dict
@classmethod @classmethod
async def upload_services(cls, request: Request, file: UploadFile) -> Dict: async def upload_services(cls, request: Request, file: UploadFile) -> Dict:
@@ -59,19 +54,19 @@ class ConfigService:
@classmethod @classmethod
async def init_config(cls, redis: Redis, db: AsyncSession): async def init_config(cls, redis: Redis, db: AsyncSession):
auth = AuthSchema(db=db) auth = AuthSchema(db=db)
config_obj_list = await ConfigCRUD(auth).list_curd() config_obj = await ConfigCRUD(auth).get_curd(id=1)
config_obj_list_dict = [ConfigOutSchema.model_validate(config_obj).model_dump() for config_obj in config_obj_list] config_obj_dict = ConfigOutSchema.model_validate(config_obj).model_dump()
# 保存到Redis并设置过期时间 # 保存到Redis并设置过期时间
redis_key = f"{RedisInitKeyConfig.System_Config.key}:{'init_system_config'}" redis_key = f"{RedisInitKeyConfig.System_Config.key}:{'init_system_config'}"
try: try:
value = json.dumps(config_obj_list_dict, ensure_ascii=False) value = json.dumps(config_obj_dict, ensure_ascii=False)
await Cache(redis).set( await Cache(redis).set(
key=redis_key, key=redis_key,
value=value, value=value,
expire=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60 expire=settings.ACCESS_TOKEN_EXPIRE_MINUTES * 60
) )
logger.info(f"初始化系统配置成功: {config_obj_list_dict}") logger.info(f"初始化系统配置成功: {config_obj_dict}")
except Exception as e: except Exception as e:
logger.error(f"初始化系统配置失败: {e}") logger.error(f"初始化系统配置失败: {e}")
raise CustomException(msg="初始化系统配置失败") raise CustomException(msg="初始化系统配置失败")
+11 -109
View File
@@ -1,114 +1,16 @@
[ [
{ {
"id": 1, "id": 1,
"parent_id": null, "title": "FastAPI Vue Admin",
"name": "基础配置", "favicon": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/logo_20250109090456A114.png",
"fied_key": "base", "logo": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/logo_20250109090448A263.png",
"fied_value": null, "background": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/background_20250109090435A357.png",
"order": 1 "description": "FastAPI Vue Admin 是完全开源的权限管理系统",
}, "copyright": "Copyright © 2021-2025 fastapi-vue-admin.com 版权所有",
{ "keep_record": "晋ICP备18005113号-3",
"id": 2, "help_url": "https://django-vue-admin.com",
"parent_id": 1, "privacy_url": "/api/system/clause/privacy.html",
"name": "网页标题", "clause_url": "/api/system/clause/terms_service.html",
"fied_key": "web_title", "code_url": "https://gitee.com/tao__tao/fastapi_vue_admin.git"
"fied_value": "FastAPI Vue Admin",
"order": 1
},
{
"id": 3,
"parent_id": 1,
"name": "网站小图标",
"fied_key": "web_favicon",
"fied_value": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/logo_20250109090456A114.png",
"order": 2
},
{
"id": 4,
"parent_id": null,
"name": "登录页配置",
"fied_key": "login",
"fied_value": null,
"order": 2
},
{
"id": 5,
"parent_id": 4,
"name": "登陆标题",
"fied_key": "login_title",
"fied_value": "FastAPI Vue Admin",
"order": 1
},
{
"id": 6,
"parent_id": 4,
"name": "登陆描述",
"fied_key": "login_description",
"fied_value": "FastAPI Vue Admin 是完全开源的权限管理系统",
"order": 2
},
{
"id": 7,
"parent_id": 4,
"name": "登录logo",
"fied_key": "login_logo",
"fied_value": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/logo_20250109090448A263.png",
"order": 3
},
{
"id": 8,
"parent_id": 4,
"name": "登录背景",
"fied_key": "login_background",
"fied_value": "http://localhost:8000/api/v1/static/upload/image/png/2025/01/09/background_20250109090435A357.png",
"order": 4
},
{
"id": 9,
"parent_id": 4,
"name": "版权信息",
"fied_key": "copyright",
"fied_value": "Copyright © 2021-2025 fastapi-vue-admin.com 版权所有",
"order": 5
},
{
"id": 10,
"parent_id": 4,
"name": "备案信息",
"fied_key": "keep_record",
"fied_value": "晋ICP备18005113号-3",
"order": 6
},
{
"id": 11,
"parent_id": 4,
"name": "帮助",
"fied_key": "help_url",
"fied_value": "https://django-vue-admin.com",
"order": 7
},
{
"id": 12,
"parent_id": 4,
"name": "隐私",
"fied_key": "privacy_url",
"fied_value": "/api/system/clause/privacy.html",
"order": 8
},
{
"id": 13,
"parent_id": 4,
"name": "条款",
"fied_key": "clause_url",
"fied_value": "/api/system/clause/terms_service.html",
"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
} }
] ]
Binary file not shown.
+4 -20
View File
@@ -1,32 +1,16 @@
import request from "@/utils/request"; import request from "@/utils/request";
export function getConfigList(params) { export function getConfigInfo() {
return request({ return request({
url: "/api/v1/system/config/list", url: "/api/v1/system/config/info",
method: "get", method: "get",
params: params,
}); });
} }
export function getConfigDetail(params) {
return request({
url: "/api/v1/system/config/detail",
method: "get",
params: params,
});
}
export function createConfig(body) { export function updateConfig(body) {
return request({ return request({
url: "/api/v1/system/config/create", url: "/api/v1/system/config/update",
method: "post",
data: body,
});
}
export function batchConfig(body) {
return request({
url: "/api/v1/system/config/batch",
method: "put", method: "put",
data: body, data: body,
}); });
+12 -29
View File
@@ -150,7 +150,7 @@ import type { MenuProps, ItemType } from "ant-design-vue";
import { message } 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 { useUserStore } from "@/store/index";
import * as icons from '@ant-design/icons-vue'; import * as icons from '@ant-design/icons-vue';
import { h } from 'vue'; import { h } from 'vue';
import { listToTree } from '@/utils/util'; import { listToTree } from '@/utils/util';
@@ -168,6 +168,9 @@ import {
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" import { getInitConfig } from "@/api/system/config"
import { useConfigStore } from "@/store/index";
const userStore = useUserStore();
// 通知公告获取 // 通知公告获取
const dataSource = reactive({ const dataSource = reactive({
@@ -194,7 +197,7 @@ const router = useRouter();
const route = useRoute(); const route = useRoute();
// 计算属性 // 计算属性
const userInfo = computed(() => store.state.user.basicInfo); const userInfo = computed(() => userStore.basicInfo);
const unreadCount = computed(() => dataSource.total); // 通知条数 const unreadCount = computed(() => dataSource.total); // 通知条数
// 菜单状态 // 菜单状态
@@ -229,7 +232,7 @@ const handleLogout = async () => {
await logout({ token: storage.get('Access-Token') }); await logout({ token: storage.get('Access-Token') });
storage.remove('Access-Token'); storage.remove('Access-Token');
storage.remove('Refresh-Token'); storage.remove('Refresh-Token');
await store.dispatch('clearUserInfo'); await userStore.clearUserInfo;
router.push('/login'); router.push('/login');
} catch (error) { } catch (error) {
console.error('退出登录失败:', error); console.error('退出登录失败:', error);
@@ -244,7 +247,7 @@ const handleMenuClick: MenuProps['onClick'] = (menuInfo) => {
// 监听路由变化 // 监听路由变化
watch(() => route.path, (newPath) => { watch(() => route.path, (newPath) => {
const routePath = store.state.user.routeList.map(item => item.route_path); const routePath = userStore.routeList.map(item => item.route_path);
if (!routePath.includes(newPath)) { if (!routePath.includes(newPath)) {
menuState.openKeys = []; menuState.openKeys = [];
menuState.selectedKeys = []; menuState.selectedKeys = [];
@@ -274,11 +277,11 @@ const generateMenuItem = (item: any): ItemType => {
// 初始化菜单 // 初始化菜单
const initMenu = () => { const initMenu = () => {
// 确保路由列表存在 // 确保路由列表存在
if (!store.state.user.routeList?.length) { if (!userStore.routeList?.length) {
return; return;
} }
const menuTree = listToTree(store.state.user.routeList); const menuTree = listToTree(userStore.routeList);
menuState.menus = menuTree menuState.menus = menuTree
.filter(item => !item.hidden) .filter(item => !item.hidden)
.map(generateMenuItem) .map(generateMenuItem)
@@ -328,38 +331,18 @@ const noticeState = reactive({
loading: false loading: false
}); });
const configStore = useConfigStore();
const initConfigState = reactive({ const initConfigState = reactive({
web_title: '', web_title: configStore.getConfigValue("title"),
web_favicon: '', web_favicon: configStore.getConfigValue("logo"),
}); });
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>
+26 -28
View File
@@ -6,39 +6,37 @@ import "ant-design-vue/dist/reset.css";
import VChart from "vue-echarts"; import VChart from "vue-echarts";
import "echarts"; import "echarts";
import './style.css' import './style.css'
import { createPinia } from 'pinia';
import { getInitConfig } from "@/api/system/config" import { useConfigStore } from "@/store/index";
const app = createApp(App); const app = createApp(App);
const pinia = createPinia();
const initConfig = () => { app.use(pinia);
return getInitConfig()
.then((response) => { const initConfig = async () => {
const { status_code, data } = response.data; const configStore = useConfigStore();
if (status_code === 200) { await configStore.fetchConfig();
const configData = JSON.parse(data);
configData.forEach((item) => { const loginTitle = configStore.getConfigValue("title");
if (item.fied_key === "login_title") { const loginLogo = configStore.getConfigValue("favicon");
document.title = item.fied_value || "fastapi vue admin";
} else if (item.fied_key === "login_logo") { if (loginTitle) {
const favicon = document.querySelector('link[rel="icon"]'); document.title = loginTitle;
if (favicon) { }
favicon.href = item.fied_value || "/logo.png";
} if (loginLogo) {
} const favicon = document.querySelector('link[rel="icon"]');
}); if (favicon) {
} favicon.href = loginLogo;
}) }
.catch((error) => { }
message.error("获取系统配置失败");
console.error(error); // 打印错误信息以便调试
});
}; };
// 初始化配置并挂载应用 // 初始化配置并挂载应用
initConfig().then(() => { initConfig().then(() => {
app.use(router); app.use(router);
app.use(Antd); app.use(Antd);
app.component("VChart", VChart); app.component("VChart", VChart);
app.mount("#app"); app.mount("#app");
}); });
-25
View File
@@ -1,25 +0,0 @@
const modules = import.meta.glob("../views/**/**.vue");
export const generator = (routers) => {
return routers.map((item) => {
const currentRouter = {
path: item.route_path,
name: item.route_name,
component: item.component_path
? modules[`../views/${item.component_path}.vue`]
: null,
redirect: item.redirect,
meta: {
title: item.name,
icon: item.icon || undefined,
keepAlive: item.cache,
hidden: item.hidden,
order: item.order,
},
};
if (item.children && item.children.length > 0) {
currentRouter.children = generator(item.children);
}
return currentRouter;
});
};
+39 -12
View File
@@ -1,10 +1,37 @@
import { createRouter, createWebHistory } from "vue-router"; import { createRouter, createWebHistory } from "vue-router";
import storage from "store";
import BasicLayout from "@/layouts/basicLayout.vue"; import BasicLayout from "@/layouts/basicLayout.vue";
import Login from "@/views/system/auth/login.vue"; import Login from "@/views/system/auth/login.vue";
import storage from "store";
import store from "@/store";
import { generator } from "./generateRouter";
import { listToTree } from "@/utils/util"; import { listToTree } from "@/utils/util";
import { useUserStore } from "@/store/index";
const modules = import.meta.glob("../views/**/**.vue");
export const generator = (routers) => {
return routers.map((item) => {
const currentRouter = {
path: item.route_path,
name: item.route_name,
component: item.component_path
? modules[`../views/${item.component_path}.vue`]
: null,
redirect: item.redirect,
meta: {
title: item.name,
icon: item.icon || undefined,
keepAlive: item.cache,
hidden: item.hidden,
order: item.order,
},
};
if (item.children && item.children.length > 0) {
currentRouter.children = generator(item.children);
}
return currentRouter;
});
};
const routes = [ const routes = [
{ path: "/login", name: "Login", component: Login }, { path: "/login", name: "Login", component: Login },
@@ -39,21 +66,21 @@ const router = createRouter({
routes, routes,
}); });
router.beforeEach((to) => { router.beforeEach(async (to) => {
const token = storage.get("Access-Token"); const token = storage.get("Access-Token");
const userStore = useUserStore();
if (!token && to.name !== "Login") { if (!token && to.name !== "Login") {
return { name: "Login" }; return { name: "Login" };
} else if (token && to.name === "Login") { } else if (token && to.name === "Login") {
return { name: "Index" }; return { name: "Index" };
} else if (token && !store.state.user.hasGetRoute) { } else if (token && !userStore.hasGetRoute) {
return store.dispatch("getUserInfo").then(() => { await userStore.getUserInfo();
const routersTree = listToTree(store.state.user.routeList); const routersTree = listToTree(userStore.routeList);
const routerMap = generator(routersTree); const routerMap = generator(routersTree);
rootRouter.children = [...rootRouter.children, ...routerMap]; rootRouter.children = [...rootRouter.children, ...routerMap];
router.addRoute(rootRouter); router.addRoute(rootRouter);
return to.fullPath; return to.fullPath;
});
} }
}); });
export default router; export default router;
-26
View File
@@ -1,26 +0,0 @@
import { defineStore } from 'pinia';
import { getInitConfig } from "@/api/system/config";
export const useConfigStore = defineStore('config', {
state: () => ({
systemConfig: {},
}),
actions: {
async getConfigInfo() {
try {
const response = await getInitConfig();
const result = response.data;
const configData = JSON.parse(result.data);
this.systemConfig = configData;
} catch (error) {
console.error(error);
}
},
setSystemConfig(config) {
this.systemConfig = config;
},
clearConfigInfo() {
this.systemConfig = {};
},
},
});
+59 -10
View File
@@ -1,14 +1,63 @@
import { createStore } from "vuex"; import { defineStore } from "pinia";
import user from "./modules/user"; import { getCurrentUserInfo } from "@/api/system/user";
import { getInitConfig } from "@/api/system/config";
const store = createStore({
modules: { export const useUserStore = defineStore("user", {
user, state: () => ({
basicInfo: {},
routeList: [],
hasGetRoute: false,
}),
actions: {
async getUserInfo() {
try {
const response = await getCurrentUserInfo();
const result = response.data;
const routers = result.data.menus;
delete result.data.menus;
this.setRoute(routers);
this.basicInfo = { ...this.basicInfo, ...result.data };
} catch (error) {
console.error(error);
}
},
setRoute(routers) {
this.routeList = routers;
this.hasGetRoute = true;
},
setAvatar(avatar) {
this.basicInfo = { ...this.basicInfo, avatar };
},
clearUserInfo() {
this.basicInfo = {};
this.routeList = [];
this.hasGetRoute = false;
},
}, },
state: {},
mutations: {},
actions: {},
getters: {},
}); });
export default store; export const useConfigStore = defineStore("config", {
state: () => ({
configData: {}, // 存储系统配置
isConfigLoaded: false, // 标记配置是否已加载
}),
actions: {
async fetchConfig() {
try {
const response = await getInitConfig();
const { status_code, data } = response.data;
if (status_code === 200) {
this.configData = JSON.parse(data);
console.log(this.configData);
this.isConfigLoaded = true;
}
} catch (error) {
console.error("获取系统配置失败", error);
}
},
getConfigValue(key) {
return this.configData[key] || null;
},
},
});
-44
View File
@@ -1,44 +0,0 @@
import { getCurrentUserInfo } from "@/api/system/user";
const user = {
state: {
basicInfo: {},
routeList: [],
hasGetRoute: false,
},
mutations: {
setRoute(state, routers) {
state.routeList = routers;
state.hasGetRoute = true;
},
setAvatar(state, avatar) {
state.basicInfo.avatar = avatar;
},
},
actions: {
getUserInfo({ commit, state }) {
return new Promise((resolve) => {
getCurrentUserInfo()
.then((response) => {
const result = response.data;
const routers = result.data.menus;
delete result.data.menus;
commit("setRoute", routers);
state.basicInfo = Object.assign(state.basicInfo || {}, result.data);
resolve();
})
.catch((error) => {
console.log(error);
});
});
},
clearUserInfo({ state }) {
state.basicInfo = {};
state.routeList = [];
state.hasGetRoute = false;
},
},
};
export default user;
-36
View File
@@ -1,36 +0,0 @@
import { defineStore } from 'pinia';
import { getCurrentUserInfo } from "@/api/system/user";
export const useUserStore = defineStore('user', {
state: () => ({
basicInfo: {},
routeList: [],
hasGetRoute: false,
}),
actions: {
async getUserInfo() {
try {
const response = await getCurrentUserInfo();
const result = response.data;
const routers = result.data.menus;
delete result.data.menus;
this.setRoute(routers);
this.basicInfo = Object.assign(this.basicInfo || {}, result.data);
} catch (error) {
console.error(error);
}
},
setRoute(routers) {
this.routeList = routers;
this.hasGetRoute = true;
},
setAvatar(avatar) {
this.basicInfo.avatar = avatar;
},
clearUserInfo() {
this.basicInfo = {};
this.routeList = [];
this.hasGetRoute = false;
},
},
});
+5 -3
View File
@@ -122,15 +122,17 @@ import { ref, reactive } from 'vue';
import PageHeader from '@/components/PageHeader.vue' import PageHeader from '@/components/PageHeader.vue'
import { timeFix } from '@/utils/util'; import { timeFix } from '@/utils/util';
import { PlusOutlined, UserOutlined } from '@ant-design/icons-vue'; import { PlusOutlined, UserOutlined } from '@ant-design/icons-vue';
import store from '@/store'; import { useUserStore } from "@/store/index";
const userStore = useUserStore();
const loading = ref(true); const loading = ref(true);
let timefix = timeFix(); let timefix = timeFix();
const userInfo = store.state.user.basicInfo; const userInfo = userStore.basicInfo;
console.log("store.state.user.basicInfo",store.state.user.basicInfo) console.log("store.state.user.basicInfo",userStore.basicInfo)
const welcome = '祝你开心每一天!'; const welcome = '祝你开心每一天!';
const projectItems = [ const projectItems = [
+20 -65
View File
@@ -1,15 +1,15 @@
<template> <template>
<a-layout> <a-layout>
<!-- 页面主体 --> <!-- 页面主体 -->
<div class="container" :style="{ backgroundImage: `url(${initConfigState.login_background})` }"> <div class="container" :style="{ backgroundImage: `url(${initConfigState.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="initConfigState.login_logo" :preview="false" /> <a-image :src="initConfigState.logo" :preview="false" />
</div> </div>
<div class="title">{{ initConfigState.login_title }}</div> <div class="title">{{ initConfigState.title }}</div>
</div> </div>
<div class="desc">{{ initConfigState.login_description }}</div> <div class="desc">{{ initConfigState.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>
@@ -69,13 +69,13 @@
{{ initConfigState.copyright }} | {{ initConfigState.copyright }} |
</a-button> </a-button>
<a-button type="link" :href="initConfigState.help_url"> <a-button type="link" :href="initConfigState.help_url">
{{ initConfigState.help_name }} | 帮助 |
</a-button> </a-button>
<a-button type="link" :href="initConfigState.privacy_url"> <a-button type="link" :href="initConfigState.privacy_url">
{{ initConfigState.privacy_name }} | 隐私 |
</a-button> </a-button>
<a-button type="link" :href="initConfigState.clause_url"> <a-button type="link" :href="initConfigState.clause_url">
{{ initConfigState.clause_name }} 条款
</a-button> </a-button>
</div> </div>
<div class="footer-record"> <div class="footer-record">
@@ -161,8 +161,8 @@ 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'
import { useConfigStore } from "@/store/index";
const router = useRouter(); const router = useRouter();
const loginFlag = ref(false); const loginFlag = ref(false);
@@ -287,69 +287,24 @@ const requestCaptcha = () => {
.catch(() => captchaState.enable = false); .catch(() => captchaState.enable = false);
}; };
const configStore = useConfigStore();
const initConfigState = reactive({ const initConfigState = reactive({
login_title: '', title: configStore.getConfigValue("title"),
login_description: '/logo.png', description: configStore.getConfigValue("description"),
login_logo: '', logo: configStore.getConfigValue("logo"),
login_background: '/background.png', background: configStore.getConfigValue("background"),
copyright: '', copyright: configStore.getConfigValue("copyright"),
copyright_name: '', keep_record: configStore.getConfigValue("keep_record"),
keep_record: '', help_url: configStore.getConfigValue("help_url"),
keep_record_name: '', privacy_url: configStore.getConfigValue("privacy_url"),
help_url: '', clause_url: configStore.getConfigValue("clause_url"),
help_name: '', code_url: configStore.getConfigValue("code_url"),
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(() => { onMounted(() => {
requestCaptcha(); requestCaptcha();
initConfig();
}); });
</script> </script>
+138 -84
View File
@@ -7,35 +7,94 @@
<div class="config-edit-wrapper"> <div class="config-edit-wrapper">
<a-card title="系统配置" :bordered="false"> <a-card title="系统配置" :bordered="false">
<!-- 动态生成配置项 --> <!-- 动态生成配置项 -->
<template v-for="group in updateState" :key="group.id"> <a-row :gutter="16">
<a-card :title="group.name" :bordered="false" style="margin-bottom: 24px;"> <a-col :span="12">
<a-row :gutter="16"> <!-- 网站标题 -->
<template v-for="config in group.children" :key="config.id"> <a-form-item label="网站标题">
<a-col :span="12"> <a-input v-model:value="configData.title" placeholder="请输入网站标题" allowClear />
<!-- 图片上传类型 --> </a-form-item>
<a-form-item v-if="config.fied_key === 'web_favicon' || config.fied_key === 'login_logo' || config.fied_key === 'login_background'" :label="config.name">
<a-upload
v-model:file-list="config.fileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="(options) => handleUpload(options, config)"
>
<div v-if="!config.fileList || config.fileList.length === 0">
<plus-outlined />
<div style="margin-top: 8px">上传图片</div>
</div>
</a-upload>
</a-form-item>
<!-- 输入框类型 --> <!-- 网站图标 -->
<a-form-item v-else :label="config.name"> <a-form-item label="网站图标">
<a-input v-model:value="config.fied_value" :placeholder="`请输入${config.name}`" allowClear /> <a-upload
</a-form-item> v-model:file-list="faviconFileList"
</a-col> list-type="picture-card"
</template> :before-upload="beforeUpload"
</a-row> :custom-request="(options) => handleUpload(options, 'favicon')"
</a-card> >
</template> <div v-if="!faviconFileList || faviconFileList.length === 0">
<plus-outlined />
<div style="margin-top: 8px">上传图标</div>
</div>
</a-upload>
</a-form-item>
<!-- 登录页Logo -->
<a-form-item label="登录页Logo">
<a-upload
v-model:file-list="logoFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="(options) => handleUpload(options, 'logo')"
>
<div v-if="!logoFileList || logoFileList.length === 0">
<plus-outlined />
<div style="margin-top: 8px">上传Logo</div>
</div>
</a-upload>
</a-form-item>
<!-- 登录页背景图 -->
<a-form-item label="登录页背景图">
<a-upload
v-model:file-list="backgroundFileList"
list-type="picture-card"
:before-upload="beforeUpload"
:custom-request="(options) => handleUpload(options, 'background')"
>
<div v-if="!backgroundFileList || backgroundFileList.length === 0">
<plus-outlined />
<div style="margin-top: 8px">上传背景图</div>
</div>
</a-upload>
</a-form-item>
<!-- 版权信息 -->
<a-form-item label="网站描述">
<a-input v-model:value="configData.description" placeholder="请输入版权信息" allowClear />
</a-form-item>
<!-- 版权信息 -->
<a-form-item label="版权信息">
<a-input v-model:value="configData.copyright" placeholder="请输入版权信息" allowClear />
</a-form-item>
<!-- 备案号 -->
<a-form-item label="备案号">
<a-input v-model:value="configData.keep_record" placeholder="请输入备案号" allowClear />
</a-form-item>
<!-- 帮助链接 -->
<a-form-item label="帮助链接">
<a-input v-model:value="configData.help_url" placeholder="请输入帮助链接" allowClear />
</a-form-item>
<!-- 隐私条款链接 -->
<a-form-item label="隐私条款链接">
<a-input v-model:value="configData.privacy_url" placeholder="请输入隐私条款链接" allowClear />
</a-form-item>
<!-- 服务条款链接 -->
<a-form-item label="服务条款链接">
<a-input v-model:value="configData.clause_url" placeholder="请输入服务条款链接" allowClear />
</a-form-item>
<!-- 代码仓库链接 -->
<a-form-item label="代码仓库链接">
<a-input v-model:value="configData.code_url" placeholder="请输入代码仓库链接" allowClear />
</a-form-item>
</a-col>
</a-row>
<!-- 保存按钮 --> <!-- 保存按钮 -->
<div style="text-align: right; margin-top: 24px;"> <div style="text-align: right; margin-top: 24px;">
@@ -47,46 +106,53 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { reactive, onMounted } from 'vue'; import { reactive, ref, onMounted } from 'vue';
import { message } from 'ant-design-vue'; import { message } from 'ant-design-vue';
import { PlusOutlined } from '@ant-design/icons-vue'; import { PlusOutlined } from '@ant-design/icons-vue';
import { getConfigList, batchConfig, uploadFile } from '@/api/system/config'; import { getConfigInfo, updateConfig, uploadFile } from '@/api/system/config';
import PageHeader from '@/components/PageHeader.vue'; import PageHeader from '@/components/PageHeader.vue';
import type { tableDataType } from './types' import type { tableDataType } from './types';
interface ConfigGroup extends tableDataType { // 配置数据
children: tableDataType[]; const configData = reactive<tableDataType>({
fileList?: any[]; id: 1,
} title: 'FastAPI Vue Admin',
favicon: 'http://example.com/favicon.png',
logo: 'http://example.com/logo.png',
background: 'http://example.com/background.png',
description: 'FastAPI Vue Admin 是完全开源的权限管理系统',
copyright: 'Copyright © 2021-2025 fastapi-vue-admin.com',
keep_record: '晋ICP备18005113号-3',
help_url: 'https://django-vue-admin.com',
privacy_url: '/api/system/clause/privacy.html',
clause_url: '/api/system/clause/terms_service.html',
code_url: 'https://gitee.com/tao__tao/fastapi_vue_admin.git',
});
const updateState = reactive<ConfigGroup[]>([]); // 文件上传列表
const faviconFileList = ref<any[]>([]);
const logoFileList = ref<any[]>([]);
const backgroundFileList = ref<any[]>([]);
// 加载配置数据 // 加载配置数据
const loadConfigData = async () => { const loadConfigData = async () => {
try { try {
const response = await getConfigList({}); const response = await getConfigInfo({});
const items = response.data.data.items; const data = response.data.data;
// 将配置项按父级分组 // 填充配置数据
const groups = items Object.assign(configData, data);
.filter(item => item.parent_id === null) // 获取顶级配置项
.map(group => ({
...group,
children: items.filter(item => item.parent_id === group.id) // 获取子配置项
}));
// 初始化图片上传列表 // 初始化文件上传列表
groups.forEach(group => { if (data.favicon) {
group.children.forEach(config => { faviconFileList.value = [{ url: data.favicon }];
if (config.fied_key === 'web_favicon' || config.fied_key === 'login_logo' || config.fied_key === 'login_background') { }
config.fileList = config.fied_value ? [{ url: config.fied_value }] : []; if (data.logo) {
} logoFileList.value = [{ url: data.logo }];
}); }
}); if (data.background) {
backgroundFileList.value = [{ url: data.background }];
// 避免直接修改 reactive 对象,使用重新赋值的方式 }
updateState.length = 0; // 清空数组
updateState.push(...groups); // 添加新数据
} catch (error) { } catch (error) {
console.error('加载配置数据失败:', error); console.error('加载配置数据失败:', error);
message.error('加载配置数据失败'); message.error('加载配置数据失败');
@@ -103,7 +169,7 @@ const beforeUpload = (file: File) => {
}; };
// 自定义上传逻辑 // 自定义上传逻辑
const handleUpload = async (options: any, config: any) => { const handleUpload = async (options: any, type: string) => {
const { file, onSuccess, onError } = options; const { file, onSuccess, onError } = options;
try { try {
@@ -111,11 +177,19 @@ const handleUpload = async (options: any, config: any) => {
formData.append('file', file); formData.append('file', file);
const response = await uploadFile(formData); const response = await uploadFile(formData);
const fileUrl = response.data.data.file_url; // 使用 file_url 更新配置项的值 const fileUrl = response.data.data.file_url;
// 更新配置项的值 // 更新配置数据
config.fied_value = fileUrl; if (type === 'favicon') {
config.fileList = [{ url: fileUrl }]; // 更新文件列表显示 configData.favicon = fileUrl;
faviconFileList.value = [{ url: fileUrl }];
} else if (type === 'logo') {
configData.logo = fileUrl;
logoFileList.value = [{ url: fileUrl }];
} else if (type === 'background') {
configData.background = fileUrl;
backgroundFileList.value = [{ url: fileUrl }];
}
onSuccess(response, file); onSuccess(response, file);
message.success('上传成功'); message.success('上传成功');
@@ -129,28 +203,8 @@ const handleUpload = async (options: any, config: any) => {
// 保存配置 // 保存配置
const handleSave = async () => { const handleSave = async () => {
try { try {
// 提取所有配置项(包括父级和子级) // 调用更新配置接口
const configs = updateState.flatMap(group => [ await updateConfig(configData);
{
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('配置保存成功'); message.success('配置保存成功');
} catch (error) { } catch (error) {
console.error('保存配置失败:', error); console.error('保存配置失败:', error);
+12 -8
View File
@@ -1,10 +1,14 @@
export interface tableDataType { export interface tableDataType {
id?: number; id?: number; // id 是可选的,类型为 number
name?: string; title: string; // 网站标题
order?: number; favicon: string; // 网站图标
fied_key?: string; logo: string; // 登录页Logo
fied_value?: string; background: string; // 登录页背景图
parent_id?: number; description: string; // 网站描述
fileList?: any[]; copyright: string; // 版权信息
children?: tableDataType[]; keep_record: string; // 备案号
help_url: string; // 帮助链接
privacy_url: string; // 隐私条款链接
clause_url: string; // 服务条款链接
code_url: string; // 代码仓库链接
} }